@task-mcp/shared 0.1.0
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 +46 -0
- package/dist/algorithms/critical-path.d.ts.map +1 -0
- package/dist/algorithms/critical-path.js +308 -0
- package/dist/algorithms/critical-path.js.map +1 -0
- package/dist/algorithms/critical-path.test.d.ts +2 -0
- package/dist/algorithms/critical-path.test.d.ts.map +1 -0
- package/dist/algorithms/critical-path.test.js +194 -0
- package/dist/algorithms/critical-path.test.js.map +1 -0
- package/dist/algorithms/index.d.ts +3 -0
- package/dist/algorithms/index.d.ts.map +1 -0
- package/dist/algorithms/index.js +3 -0
- package/dist/algorithms/index.js.map +1 -0
- package/dist/algorithms/topological-sort.d.ts +41 -0
- package/dist/algorithms/topological-sort.d.ts.map +1 -0
- package/dist/algorithms/topological-sort.js +168 -0
- package/dist/algorithms/topological-sort.js.map +1 -0
- package/dist/algorithms/topological-sort.test.d.ts +2 -0
- package/dist/algorithms/topological-sort.test.d.ts.map +1 -0
- package/dist/algorithms/topological-sort.test.js +162 -0
- package/dist/algorithms/topological-sort.test.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas/index.d.ts +4 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +7 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/project.d.ts +55 -0
- package/dist/schemas/project.d.ts.map +1 -0
- package/dist/schemas/project.js +48 -0
- package/dist/schemas/project.js.map +1 -0
- package/dist/schemas/task.d.ts +124 -0
- package/dist/schemas/task.d.ts.map +1 -0
- package/dist/schemas/task.js +89 -0
- package/dist/schemas/task.js.map +1 -0
- package/dist/schemas/view.d.ts +44 -0
- package/dist/schemas/view.d.ts.map +1 -0
- package/dist/schemas/view.js +33 -0
- package/dist/schemas/view.js.map +1 -0
- package/dist/utils/date.d.ts +25 -0
- package/dist/utils/date.d.ts.map +1 -0
- package/dist/utils/date.js +103 -0
- package/dist/utils/date.js.map +1 -0
- package/dist/utils/date.test.d.ts +2 -0
- package/dist/utils/date.test.d.ts.map +1 -0
- package/dist/utils/date.test.js +138 -0
- package/dist/utils/date.test.js.map +1 -0
- package/dist/utils/id.d.ts +27 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +41 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/natural-language.d.ts +12 -0
- package/dist/utils/natural-language.d.ts.map +1 -0
- package/dist/utils/natural-language.js +112 -0
- package/dist/utils/natural-language.js.map +1 -0
- package/dist/utils/natural-language.test.d.ts +2 -0
- package/dist/utils/natural-language.test.d.ts.map +1 -0
- package/dist/utils/natural-language.test.js +132 -0
- package/dist/utils/natural-language.test.js.map +1 -0
- package/package.json +46 -0
- package/src/algorithms/critical-path.test.ts +241 -0
- package/src/algorithms/critical-path.ts +413 -0
- package/src/algorithms/index.ts +17 -0
- package/src/algorithms/topological-sort.test.ts +190 -0
- package/src/algorithms/topological-sort.ts +204 -0
- package/src/index.ts +8 -0
- package/src/schemas/index.ts +30 -0
- package/src/schemas/project.ts +62 -0
- package/src/schemas/task.ts +116 -0
- package/src/schemas/view.ts +46 -0
- package/src/utils/date.test.ts +160 -0
- package/src/utils/date.ts +119 -0
- package/src/utils/id.ts +45 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/natural-language.test.ts +154 -0
- package/src/utils/natural-language.ts +125 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import type { Task } from "../schemas/task.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Node representation for graph algorithms
|
|
5
|
+
*/
|
|
6
|
+
export interface TaskNode {
|
|
7
|
+
id: string;
|
|
8
|
+
dependencies: string[]; // IDs of tasks this depends on (blocked_by)
|
|
9
|
+
priority: number; // Numeric priority for tie-breaking (higher = more important)
|
|
10
|
+
estimate: number; // Duration in minutes
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Convert priority string to numeric value
|
|
15
|
+
*/
|
|
16
|
+
export function priorityToNumber(priority: string): number {
|
|
17
|
+
const map: Record<string, number> = {
|
|
18
|
+
critical: 4,
|
|
19
|
+
high: 3,
|
|
20
|
+
medium: 2,
|
|
21
|
+
low: 1,
|
|
22
|
+
};
|
|
23
|
+
return map[priority] ?? 2;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Convert Task to TaskNode
|
|
28
|
+
*/
|
|
29
|
+
export function taskToNode(task: Task): TaskNode {
|
|
30
|
+
return {
|
|
31
|
+
id: task.id,
|
|
32
|
+
dependencies: (task.dependencies ?? [])
|
|
33
|
+
.filter((d) => d.type === "blocked_by")
|
|
34
|
+
.map((d) => d.taskId),
|
|
35
|
+
priority: priorityToNumber(task.priority),
|
|
36
|
+
estimate: task.estimate?.expected ?? 0,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Topological sort using Kahn's algorithm with priority tie-breaking
|
|
42
|
+
*
|
|
43
|
+
* Returns tasks in optimal execution order:
|
|
44
|
+
* 1. Respects dependencies (blocked_by relationships)
|
|
45
|
+
* 2. Higher priority tasks come first when dependencies allow
|
|
46
|
+
*
|
|
47
|
+
* @throws Error if circular dependency detected
|
|
48
|
+
*/
|
|
49
|
+
export function topologicalSort(tasks: Task[]): Task[] {
|
|
50
|
+
const nodes = tasks.map(taskToNode);
|
|
51
|
+
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
|
|
52
|
+
const taskMap = new Map(tasks.map((t) => [t.id, t]));
|
|
53
|
+
|
|
54
|
+
// Calculate in-degree for each node
|
|
55
|
+
const inDegree = new Map<string, number>();
|
|
56
|
+
const adjacency = new Map<string, string[]>();
|
|
57
|
+
|
|
58
|
+
for (const node of nodes) {
|
|
59
|
+
inDegree.set(node.id, 0);
|
|
60
|
+
adjacency.set(node.id, []);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Build graph: if A is blocked_by B, then B -> A (B must come before A)
|
|
64
|
+
for (const node of nodes) {
|
|
65
|
+
for (const depId of node.dependencies) {
|
|
66
|
+
if (nodeMap.has(depId)) {
|
|
67
|
+
const adj = adjacency.get(depId);
|
|
68
|
+
if (adj) adj.push(node.id);
|
|
69
|
+
inDegree.set(node.id, (inDegree.get(node.id) ?? 0) + 1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Initialize queue with nodes that have no dependencies
|
|
75
|
+
const queue: TaskNode[] = [];
|
|
76
|
+
for (const node of nodes) {
|
|
77
|
+
if (inDegree.get(node.id) === 0) {
|
|
78
|
+
queue.push(node);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Sort by priority (higher first)
|
|
83
|
+
queue.sort((a, b) => b.priority - a.priority);
|
|
84
|
+
|
|
85
|
+
const result: Task[] = [];
|
|
86
|
+
|
|
87
|
+
while (queue.length > 0) {
|
|
88
|
+
const current = queue.shift()!;
|
|
89
|
+
const task = taskMap.get(current.id);
|
|
90
|
+
if (task) {
|
|
91
|
+
result.push(task);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Update neighbors
|
|
95
|
+
for (const neighborId of adjacency.get(current.id)!) {
|
|
96
|
+
const newDegree = inDegree.get(neighborId)! - 1;
|
|
97
|
+
inDegree.set(neighborId, newDegree);
|
|
98
|
+
|
|
99
|
+
if (newDegree === 0) {
|
|
100
|
+
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);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check for cycles
|
|
109
|
+
if (result.length !== tasks.length) {
|
|
110
|
+
const remaining = tasks.filter((t) => !result.some((r) => r.id === t.id));
|
|
111
|
+
const cycleIds = remaining.map((t) => t.id).join(", ");
|
|
112
|
+
throw new Error(`Circular dependency detected among tasks: ${cycleIds}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Detect if adding a dependency would create a cycle
|
|
120
|
+
*/
|
|
121
|
+
export function wouldCreateCycle(
|
|
122
|
+
tasks: Task[],
|
|
123
|
+
fromId: string,
|
|
124
|
+
toId: string
|
|
125
|
+
): boolean {
|
|
126
|
+
// Create a temporary task list with the new dependency
|
|
127
|
+
const tempTasks = tasks.map((t) => {
|
|
128
|
+
if (t.id === fromId) {
|
|
129
|
+
return {
|
|
130
|
+
...t,
|
|
131
|
+
dependencies: [
|
|
132
|
+
...(t.dependencies ?? []),
|
|
133
|
+
{ taskId: toId, type: "blocked_by" as const },
|
|
134
|
+
],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return t;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
topologicalSort(tempTasks);
|
|
142
|
+
return false;
|
|
143
|
+
} catch {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Find all tasks that depend on a given task (directly or transitively)
|
|
150
|
+
*/
|
|
151
|
+
export function findDependents(tasks: Task[], taskId: string): Task[] {
|
|
152
|
+
const taskMap = new Map(tasks.map((t) => [t.id, t]));
|
|
153
|
+
const visited = new Set<string>();
|
|
154
|
+
const result: Task[] = [];
|
|
155
|
+
|
|
156
|
+
function dfs(id: string) {
|
|
157
|
+
for (const task of tasks) {
|
|
158
|
+
const deps = (task.dependencies ?? [])
|
|
159
|
+
.filter((d) => d.type === "blocked_by")
|
|
160
|
+
.map((d) => d.taskId);
|
|
161
|
+
|
|
162
|
+
if (deps.includes(id) && !visited.has(task.id)) {
|
|
163
|
+
visited.add(task.id);
|
|
164
|
+
result.push(task);
|
|
165
|
+
dfs(task.id);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
dfs(taskId);
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Find all tasks that a given task depends on (directly or transitively)
|
|
176
|
+
*/
|
|
177
|
+
export function findDependencies(tasks: Task[], taskId: string): Task[] {
|
|
178
|
+
const taskMap = new Map(tasks.map((t) => [t.id, t]));
|
|
179
|
+
const visited = new Set<string>();
|
|
180
|
+
const result: Task[] = [];
|
|
181
|
+
|
|
182
|
+
function dfs(id: string) {
|
|
183
|
+
const task = taskMap.get(id);
|
|
184
|
+
if (!task) return;
|
|
185
|
+
|
|
186
|
+
const deps = (task.dependencies ?? [])
|
|
187
|
+
.filter((d) => d.type === "blocked_by")
|
|
188
|
+
.map((d) => d.taskId);
|
|
189
|
+
|
|
190
|
+
for (const depId of deps) {
|
|
191
|
+
if (!visited.has(depId)) {
|
|
192
|
+
visited.add(depId);
|
|
193
|
+
const depTask = taskMap.get(depId);
|
|
194
|
+
if (depTask) {
|
|
195
|
+
result.push(depTask);
|
|
196
|
+
dfs(depId);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
dfs(taskId);
|
|
203
|
+
return result;
|
|
204
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Task schemas
|
|
2
|
+
export {
|
|
3
|
+
Priority,
|
|
4
|
+
TaskStatus,
|
|
5
|
+
DependencyType,
|
|
6
|
+
Dependency,
|
|
7
|
+
TimeEstimate,
|
|
8
|
+
Recurrence,
|
|
9
|
+
Task,
|
|
10
|
+
TaskCreateInput,
|
|
11
|
+
TaskUpdateInput,
|
|
12
|
+
} from "./task.js";
|
|
13
|
+
|
|
14
|
+
// Project schemas
|
|
15
|
+
export {
|
|
16
|
+
ProjectStatus,
|
|
17
|
+
Context,
|
|
18
|
+
Project,
|
|
19
|
+
ProjectCreateInput,
|
|
20
|
+
ProjectUpdateInput,
|
|
21
|
+
} from "./project.js";
|
|
22
|
+
|
|
23
|
+
// View schemas
|
|
24
|
+
export {
|
|
25
|
+
SmartViewFilter,
|
|
26
|
+
SortField,
|
|
27
|
+
SortOrder,
|
|
28
|
+
SmartView,
|
|
29
|
+
BuiltInView,
|
|
30
|
+
} from "./view.js";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
import { Priority } from "./task.js";
|
|
3
|
+
|
|
4
|
+
// Project status
|
|
5
|
+
export const ProjectStatus = type(
|
|
6
|
+
"'active' | 'on_hold' | 'completed' | 'archived'"
|
|
7
|
+
);
|
|
8
|
+
export type ProjectStatus = typeof ProjectStatus.infer;
|
|
9
|
+
|
|
10
|
+
// Context definition
|
|
11
|
+
export const Context = type({
|
|
12
|
+
name: "string",
|
|
13
|
+
"color?": "string", // hex color
|
|
14
|
+
"description?": "string",
|
|
15
|
+
});
|
|
16
|
+
export type Context = typeof Context.infer;
|
|
17
|
+
|
|
18
|
+
// Project schema
|
|
19
|
+
export const Project = type({
|
|
20
|
+
id: "string",
|
|
21
|
+
name: "string",
|
|
22
|
+
"description?": "string",
|
|
23
|
+
status: ProjectStatus,
|
|
24
|
+
|
|
25
|
+
// Project-level settings
|
|
26
|
+
"defaultPriority?": Priority,
|
|
27
|
+
"contexts?": Context.array(),
|
|
28
|
+
|
|
29
|
+
// Metadata
|
|
30
|
+
createdAt: "string",
|
|
31
|
+
updatedAt: "string",
|
|
32
|
+
"targetDate?": "string",
|
|
33
|
+
|
|
34
|
+
// Computed stats
|
|
35
|
+
"completionPercentage?": "number",
|
|
36
|
+
"criticalPathLength?": "number", // Total minutes on critical path
|
|
37
|
+
"blockedTaskCount?": "number",
|
|
38
|
+
"totalTasks?": "number",
|
|
39
|
+
"completedTasks?": "number",
|
|
40
|
+
});
|
|
41
|
+
export type Project = typeof Project.infer;
|
|
42
|
+
|
|
43
|
+
// Project creation input
|
|
44
|
+
export const ProjectCreateInput = type({
|
|
45
|
+
name: "string",
|
|
46
|
+
"description?": "string",
|
|
47
|
+
"defaultPriority?": Priority,
|
|
48
|
+
"contexts?": Context.array(),
|
|
49
|
+
"targetDate?": "string",
|
|
50
|
+
});
|
|
51
|
+
export type ProjectCreateInput = typeof ProjectCreateInput.infer;
|
|
52
|
+
|
|
53
|
+
// Project update input
|
|
54
|
+
export const ProjectUpdateInput = type({
|
|
55
|
+
"name?": "string",
|
|
56
|
+
"description?": "string",
|
|
57
|
+
"status?": ProjectStatus,
|
|
58
|
+
"defaultPriority?": Priority,
|
|
59
|
+
"contexts?": Context.array(),
|
|
60
|
+
"targetDate?": "string",
|
|
61
|
+
});
|
|
62
|
+
export type ProjectUpdateInput = typeof ProjectUpdateInput.infer;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
|
|
3
|
+
// Priority levels
|
|
4
|
+
export const Priority = type("'critical' | 'high' | 'medium' | 'low'");
|
|
5
|
+
export type Priority = typeof Priority.infer;
|
|
6
|
+
|
|
7
|
+
// Task status with clear state machine
|
|
8
|
+
export const TaskStatus = type(
|
|
9
|
+
"'pending' | 'in_progress' | 'blocked' | 'completed' | 'cancelled'"
|
|
10
|
+
);
|
|
11
|
+
export type TaskStatus = typeof TaskStatus.infer;
|
|
12
|
+
|
|
13
|
+
// Dependency relationship types
|
|
14
|
+
export const DependencyType = type(
|
|
15
|
+
"'blocks' | 'blocked_by' | 'related'"
|
|
16
|
+
);
|
|
17
|
+
export type DependencyType = typeof DependencyType.infer;
|
|
18
|
+
|
|
19
|
+
// A single dependency link
|
|
20
|
+
export const Dependency = type({
|
|
21
|
+
taskId: "string",
|
|
22
|
+
type: DependencyType,
|
|
23
|
+
"reason?": "string",
|
|
24
|
+
});
|
|
25
|
+
export type Dependency = typeof Dependency.infer;
|
|
26
|
+
|
|
27
|
+
// Time estimation
|
|
28
|
+
export const TimeEstimate = type({
|
|
29
|
+
"optimistic?": "number", // minutes
|
|
30
|
+
"expected?": "number", // minutes
|
|
31
|
+
"pessimistic?": "number", // minutes
|
|
32
|
+
"confidence?": "'low' | 'medium' | 'high'",
|
|
33
|
+
});
|
|
34
|
+
export type TimeEstimate = typeof TimeEstimate.infer;
|
|
35
|
+
|
|
36
|
+
// Recurrence pattern
|
|
37
|
+
export const Recurrence = type({
|
|
38
|
+
pattern: "'daily' | 'weekly' | 'monthly' | 'after_completion'",
|
|
39
|
+
"interval?": "number", // every N days/weeks/months
|
|
40
|
+
"daysOfWeek?": "number[]", // 0-6 for weekly
|
|
41
|
+
"endDate?": "string",
|
|
42
|
+
});
|
|
43
|
+
export type Recurrence = typeof Recurrence.infer;
|
|
44
|
+
|
|
45
|
+
// Core Task schema
|
|
46
|
+
export const Task = type({
|
|
47
|
+
id: "string",
|
|
48
|
+
title: "string",
|
|
49
|
+
"description?": "string",
|
|
50
|
+
status: TaskStatus,
|
|
51
|
+
priority: Priority,
|
|
52
|
+
projectId: "string",
|
|
53
|
+
|
|
54
|
+
// Dependencies
|
|
55
|
+
"dependencies?": Dependency.array(),
|
|
56
|
+
|
|
57
|
+
// Time tracking
|
|
58
|
+
"estimate?": TimeEstimate,
|
|
59
|
+
"actualMinutes?": "number",
|
|
60
|
+
"dueDate?": "string", // ISO date string
|
|
61
|
+
"startDate?": "string", // When task can start
|
|
62
|
+
"startedAt?": "string",
|
|
63
|
+
"completedAt?": "string",
|
|
64
|
+
|
|
65
|
+
// Organization
|
|
66
|
+
"contexts?": "string[]", // e.g., ["focus", "review"]
|
|
67
|
+
"tags?": "string[]",
|
|
68
|
+
|
|
69
|
+
// Recurrence
|
|
70
|
+
"recurrence?": Recurrence,
|
|
71
|
+
|
|
72
|
+
// Metadata
|
|
73
|
+
createdAt: "string",
|
|
74
|
+
updatedAt: "string",
|
|
75
|
+
|
|
76
|
+
// Computed fields (populated at runtime)
|
|
77
|
+
"criticalPath?": "boolean",
|
|
78
|
+
"slack?": "number", // Minutes of slack time
|
|
79
|
+
"earliestStart?": "number", // Minutes from project start
|
|
80
|
+
"latestStart?": "number",
|
|
81
|
+
});
|
|
82
|
+
export type Task = typeof Task.infer;
|
|
83
|
+
|
|
84
|
+
// Task creation input (minimal required fields)
|
|
85
|
+
export const TaskCreateInput = type({
|
|
86
|
+
title: "string",
|
|
87
|
+
"description?": "string",
|
|
88
|
+
"projectId?": "string",
|
|
89
|
+
"priority?": Priority,
|
|
90
|
+
"dependencies?": Dependency.array(),
|
|
91
|
+
"estimate?": TimeEstimate,
|
|
92
|
+
"dueDate?": "string",
|
|
93
|
+
"startDate?": "string",
|
|
94
|
+
"contexts?": "string[]",
|
|
95
|
+
"tags?": "string[]",
|
|
96
|
+
"recurrence?": Recurrence,
|
|
97
|
+
});
|
|
98
|
+
export type TaskCreateInput = typeof TaskCreateInput.infer;
|
|
99
|
+
|
|
100
|
+
// Task update input
|
|
101
|
+
export const TaskUpdateInput = type({
|
|
102
|
+
"title?": "string",
|
|
103
|
+
"description?": "string",
|
|
104
|
+
"status?": TaskStatus,
|
|
105
|
+
"priority?": Priority,
|
|
106
|
+
"projectId?": "string",
|
|
107
|
+
"dependencies?": Dependency.array(),
|
|
108
|
+
"estimate?": TimeEstimate,
|
|
109
|
+
"actualMinutes?": "number",
|
|
110
|
+
"dueDate?": "string",
|
|
111
|
+
"startDate?": "string",
|
|
112
|
+
"contexts?": "string[]",
|
|
113
|
+
"tags?": "string[]",
|
|
114
|
+
"recurrence?": Recurrence,
|
|
115
|
+
});
|
|
116
|
+
export type TaskUpdateInput = typeof TaskUpdateInput.infer;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
import { Priority, TaskStatus } from "./task.js";
|
|
3
|
+
|
|
4
|
+
// Smart View filter
|
|
5
|
+
export const SmartViewFilter = type({
|
|
6
|
+
"statuses?": TaskStatus.array(),
|
|
7
|
+
"priorities?": Priority.array(),
|
|
8
|
+
"contexts?": "string[]",
|
|
9
|
+
"tags?": "string[]",
|
|
10
|
+
"projectIds?": "string[]",
|
|
11
|
+
"dueBefore?": "string",
|
|
12
|
+
"dueAfter?": "string",
|
|
13
|
+
"isBlocked?": "boolean",
|
|
14
|
+
"isCriticalPath?": "boolean",
|
|
15
|
+
"hasNoDependencies?": "boolean",
|
|
16
|
+
"search?": "string", // Search in title/description
|
|
17
|
+
});
|
|
18
|
+
export type SmartViewFilter = typeof SmartViewFilter.infer;
|
|
19
|
+
|
|
20
|
+
// Sort options
|
|
21
|
+
export const SortField = type(
|
|
22
|
+
"'priority' | 'dueDate' | 'createdAt' | 'criticalPath' | 'slack' | 'title'"
|
|
23
|
+
);
|
|
24
|
+
export type SortField = typeof SortField.infer;
|
|
25
|
+
|
|
26
|
+
export const SortOrder = type("'asc' | 'desc'");
|
|
27
|
+
export type SortOrder = typeof SortOrder.infer;
|
|
28
|
+
|
|
29
|
+
// Smart View definition
|
|
30
|
+
export const SmartView = type({
|
|
31
|
+
id: "string",
|
|
32
|
+
name: "string",
|
|
33
|
+
"description?": "string",
|
|
34
|
+
filter: SmartViewFilter,
|
|
35
|
+
"sortBy?": SortField,
|
|
36
|
+
"sortOrder?": SortOrder,
|
|
37
|
+
createdAt: "string",
|
|
38
|
+
updatedAt: "string",
|
|
39
|
+
});
|
|
40
|
+
export type SmartView = typeof SmartView.infer;
|
|
41
|
+
|
|
42
|
+
// Built-in view names
|
|
43
|
+
export const BuiltInView = type(
|
|
44
|
+
"'today' | 'this_week' | 'blocked' | 'critical_path' | 'quick_wins' | 'all'"
|
|
45
|
+
);
|
|
46
|
+
export type BuiltInView = typeof BuiltInView.infer;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { now, parseRelativeDate, formatDate, isToday, isPastDue, isWithinDays } from "./date.js";
|
|
3
|
+
|
|
4
|
+
describe("now", () => {
|
|
5
|
+
test("returns ISO timestamp string", () => {
|
|
6
|
+
const result = now();
|
|
7
|
+
expect(typeof result).toBe("string");
|
|
8
|
+
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("returns current time within tolerance", () => {
|
|
12
|
+
const before = Date.now();
|
|
13
|
+
const result = now();
|
|
14
|
+
const after = Date.now();
|
|
15
|
+
|
|
16
|
+
const resultTime = new Date(result).getTime();
|
|
17
|
+
expect(resultTime).toBeGreaterThanOrEqual(before);
|
|
18
|
+
expect(resultTime).toBeLessThanOrEqual(after);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("parseRelativeDate", () => {
|
|
23
|
+
test("parses 'today'", () => {
|
|
24
|
+
const result = parseRelativeDate("today");
|
|
25
|
+
expect(result).not.toBeNull();
|
|
26
|
+
const today = new Date();
|
|
27
|
+
expect(result!.getDate()).toBe(today.getDate());
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("parses '오늘' (Korean today)", () => {
|
|
31
|
+
const result = parseRelativeDate("오늘");
|
|
32
|
+
expect(result).not.toBeNull();
|
|
33
|
+
const today = new Date();
|
|
34
|
+
expect(result!.getDate()).toBe(today.getDate());
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("parses 'tomorrow'", () => {
|
|
38
|
+
const result = parseRelativeDate("tomorrow");
|
|
39
|
+
expect(result).not.toBeNull();
|
|
40
|
+
const tomorrow = new Date();
|
|
41
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
42
|
+
expect(result!.getDate()).toBe(tomorrow.getDate());
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("parses '내일' (Korean tomorrow)", () => {
|
|
46
|
+
const result = parseRelativeDate("내일");
|
|
47
|
+
expect(result).not.toBeNull();
|
|
48
|
+
const tomorrow = new Date();
|
|
49
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
50
|
+
expect(result!.getDate()).toBe(tomorrow.getDate());
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("parses 'yesterday'", () => {
|
|
54
|
+
const result = parseRelativeDate("yesterday");
|
|
55
|
+
expect(result).not.toBeNull();
|
|
56
|
+
const yesterday = new Date();
|
|
57
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
58
|
+
expect(result!.getDate()).toBe(yesterday.getDate());
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("parses 'next week'", () => {
|
|
62
|
+
const result = parseRelativeDate("next week");
|
|
63
|
+
expect(result).not.toBeNull();
|
|
64
|
+
const nextWeek = new Date();
|
|
65
|
+
nextWeek.setDate(nextWeek.getDate() + 7);
|
|
66
|
+
expect(result!.getDate()).toBe(nextWeek.getDate());
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("parses 'in 3 days'", () => {
|
|
70
|
+
const result = parseRelativeDate("in 3 days");
|
|
71
|
+
expect(result).not.toBeNull();
|
|
72
|
+
const future = new Date();
|
|
73
|
+
future.setDate(future.getDate() + 3);
|
|
74
|
+
expect(result!.getDate()).toBe(future.getDate());
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("parses '5일 후' (Korean days later)", () => {
|
|
78
|
+
const result = parseRelativeDate("5일 후");
|
|
79
|
+
expect(result).not.toBeNull();
|
|
80
|
+
const future = new Date();
|
|
81
|
+
future.setDate(future.getDate() + 5);
|
|
82
|
+
expect(result!.getDate()).toBe(future.getDate());
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("parses weekday names", () => {
|
|
86
|
+
const result = parseRelativeDate("monday");
|
|
87
|
+
expect(result).not.toBeNull();
|
|
88
|
+
expect(result!.getDay()).toBe(1); // Monday = 1
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("parses ISO date string", () => {
|
|
92
|
+
const result = parseRelativeDate("2025-12-31");
|
|
93
|
+
expect(result).not.toBeNull();
|
|
94
|
+
expect(result!.getFullYear()).toBe(2025);
|
|
95
|
+
expect(result!.getMonth()).toBe(11); // December = 11
|
|
96
|
+
expect(result!.getDate()).toBe(31);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("returns null for invalid input", () => {
|
|
100
|
+
const result = parseRelativeDate("invalid date string");
|
|
101
|
+
expect(result).toBeNull();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("formatDate", () => {
|
|
106
|
+
test("formats date as YYYY-MM-DD", () => {
|
|
107
|
+
const date = new Date("2025-06-15T10:30:00");
|
|
108
|
+
expect(formatDate(date)).toBe("2025-06-15");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("isToday", () => {
|
|
113
|
+
test("returns true for today", () => {
|
|
114
|
+
expect(isToday(new Date())).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("returns false for tomorrow", () => {
|
|
118
|
+
const tomorrow = new Date();
|
|
119
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
120
|
+
expect(isToday(tomorrow)).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("accepts string input", () => {
|
|
124
|
+
expect(isToday(new Date().toISOString())).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("isPastDue", () => {
|
|
129
|
+
test("returns true for yesterday", () => {
|
|
130
|
+
const yesterday = new Date();
|
|
131
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
132
|
+
expect(isPastDue(yesterday)).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("returns false for tomorrow", () => {
|
|
136
|
+
const tomorrow = new Date();
|
|
137
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
138
|
+
expect(isPastDue(tomorrow)).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("isWithinDays", () => {
|
|
143
|
+
test("returns true for date within range", () => {
|
|
144
|
+
const inThreeDays = new Date();
|
|
145
|
+
inThreeDays.setDate(inThreeDays.getDate() + 3);
|
|
146
|
+
expect(isWithinDays(inThreeDays, 7)).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("returns false for date outside range", () => {
|
|
150
|
+
const inTenDays = new Date();
|
|
151
|
+
inTenDays.setDate(inTenDays.getDate() + 10);
|
|
152
|
+
expect(isWithinDays(inTenDays, 7)).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("returns false for past date", () => {
|
|
156
|
+
const yesterday = new Date();
|
|
157
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
158
|
+
expect(isWithinDays(yesterday, 7)).toBe(false);
|
|
159
|
+
});
|
|
160
|
+
});
|