@task-mcp/shared 1.0.22 → 1.0.24

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.
Files changed (184) hide show
  1. package/dist/algorithms/critical-path.d.ts +47 -0
  2. package/dist/algorithms/critical-path.d.ts.map +1 -0
  3. package/dist/algorithms/critical-path.js +340 -0
  4. package/dist/algorithms/critical-path.js.map +1 -0
  5. package/dist/algorithms/critical-path.test.d.ts +2 -0
  6. package/dist/algorithms/critical-path.test.d.ts.map +1 -0
  7. package/dist/algorithms/critical-path.test.js +184 -0
  8. package/dist/algorithms/critical-path.test.js.map +1 -0
  9. package/dist/algorithms/dependency-integrity.d.ts +81 -0
  10. package/dist/algorithms/dependency-integrity.d.ts.map +1 -0
  11. package/dist/algorithms/dependency-integrity.js +209 -0
  12. package/dist/algorithms/dependency-integrity.js.map +1 -0
  13. package/dist/algorithms/dependency-integrity.test.d.ts +2 -0
  14. package/dist/algorithms/dependency-integrity.test.d.ts.map +1 -0
  15. package/dist/algorithms/dependency-integrity.test.js +296 -0
  16. package/dist/algorithms/dependency-integrity.test.js.map +1 -0
  17. package/dist/algorithms/index.d.ts +5 -0
  18. package/dist/algorithms/index.d.ts.map +1 -0
  19. package/dist/algorithms/index.js +5 -0
  20. package/dist/algorithms/index.js.map +1 -0
  21. package/dist/algorithms/tech-analysis.d.ts +106 -0
  22. package/dist/algorithms/tech-analysis.d.ts.map +1 -0
  23. package/dist/algorithms/tech-analysis.js +351 -0
  24. package/dist/algorithms/tech-analysis.js.map +1 -0
  25. package/dist/algorithms/tech-analysis.test.d.ts +2 -0
  26. package/dist/algorithms/tech-analysis.test.d.ts.map +1 -0
  27. package/dist/algorithms/tech-analysis.test.js +330 -0
  28. package/dist/algorithms/tech-analysis.test.js.map +1 -0
  29. package/dist/algorithms/topological-sort.d.ts +58 -0
  30. package/dist/algorithms/topological-sort.d.ts.map +1 -0
  31. package/dist/algorithms/topological-sort.js +201 -0
  32. package/dist/algorithms/topological-sort.js.map +1 -0
  33. package/dist/algorithms/topological-sort.test.d.ts +2 -0
  34. package/dist/algorithms/topological-sort.test.d.ts.map +1 -0
  35. package/dist/algorithms/topological-sort.test.js +154 -0
  36. package/dist/algorithms/topological-sort.test.js.map +1 -0
  37. package/dist/index.d.ts +4 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +7 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/schemas/inbox.d.ts +55 -0
  42. package/dist/schemas/inbox.d.ts.map +1 -0
  43. package/dist/schemas/inbox.js +25 -0
  44. package/dist/schemas/inbox.js.map +1 -0
  45. package/dist/schemas/index.d.ts +7 -0
  46. package/dist/schemas/index.d.ts.map +1 -0
  47. package/dist/schemas/index.js +17 -0
  48. package/dist/schemas/index.js.map +1 -0
  49. package/dist/schemas/llm-guide.d.ts +147 -0
  50. package/dist/schemas/llm-guide.d.ts.map +1 -0
  51. package/dist/schemas/llm-guide.js +72 -0
  52. package/dist/schemas/llm-guide.js.map +1 -0
  53. package/dist/schemas/project.d.ts +177 -0
  54. package/dist/schemas/project.d.ts.map +1 -0
  55. package/dist/schemas/project.js +56 -0
  56. package/dist/schemas/project.js.map +1 -0
  57. package/dist/schemas/response-format.d.ts +148 -0
  58. package/dist/schemas/response-format.d.ts.map +1 -0
  59. package/dist/schemas/response-format.js +18 -0
  60. package/dist/schemas/response-format.js.map +1 -0
  61. package/dist/schemas/response-schema.d.ts +307 -0
  62. package/dist/schemas/response-schema.d.ts.map +1 -0
  63. package/dist/schemas/response-schema.js +78 -0
  64. package/dist/schemas/response-schema.js.map +1 -0
  65. package/dist/schemas/response-schema.test.d.ts +2 -0
  66. package/dist/schemas/response-schema.test.d.ts.map +1 -0
  67. package/dist/schemas/response-schema.test.js +256 -0
  68. package/dist/schemas/response-schema.test.js.map +1 -0
  69. package/dist/schemas/state.d.ts +17 -0
  70. package/dist/schemas/state.d.ts.map +1 -0
  71. package/dist/schemas/state.js +17 -0
  72. package/dist/schemas/state.js.map +1 -0
  73. package/dist/schemas/task.d.ts +881 -0
  74. package/dist/schemas/task.d.ts.map +1 -0
  75. package/dist/schemas/task.js +177 -0
  76. package/dist/schemas/task.js.map +1 -0
  77. package/dist/schemas/view.d.ts +143 -0
  78. package/dist/schemas/view.d.ts.map +1 -0
  79. package/dist/schemas/view.js +48 -0
  80. package/dist/schemas/view.js.map +1 -0
  81. package/dist/utils/dashboard-renderer.d.ts +93 -0
  82. package/dist/utils/dashboard-renderer.d.ts.map +1 -0
  83. package/dist/utils/dashboard-renderer.js +416 -0
  84. package/dist/utils/dashboard-renderer.js.map +1 -0
  85. package/dist/utils/dashboard-renderer.test.d.ts +2 -0
  86. package/dist/utils/dashboard-renderer.test.d.ts.map +1 -0
  87. package/dist/utils/dashboard-renderer.test.js +772 -0
  88. package/dist/utils/dashboard-renderer.test.js.map +1 -0
  89. package/dist/utils/date.d.ts +94 -0
  90. package/dist/utils/date.d.ts.map +1 -0
  91. package/dist/utils/date.js +323 -0
  92. package/dist/utils/date.js.map +1 -0
  93. package/dist/utils/date.test.d.ts +2 -0
  94. package/dist/utils/date.test.d.ts.map +1 -0
  95. package/dist/utils/date.test.js +276 -0
  96. package/dist/utils/date.test.js.map +1 -0
  97. package/dist/utils/hierarchy.d.ts +102 -0
  98. package/dist/utils/hierarchy.d.ts.map +1 -0
  99. package/dist/utils/hierarchy.js +236 -0
  100. package/dist/utils/hierarchy.js.map +1 -0
  101. package/dist/utils/hierarchy.test.d.ts +2 -0
  102. package/dist/utils/hierarchy.test.d.ts.map +1 -0
  103. package/dist/utils/hierarchy.test.js +423 -0
  104. package/dist/utils/hierarchy.test.js.map +1 -0
  105. package/dist/utils/id.d.ts +60 -0
  106. package/dist/utils/id.d.ts.map +1 -0
  107. package/dist/utils/id.js +118 -0
  108. package/dist/utils/id.js.map +1 -0
  109. package/dist/utils/id.test.d.ts +2 -0
  110. package/dist/utils/id.test.d.ts.map +1 -0
  111. package/dist/utils/id.test.js +193 -0
  112. package/dist/utils/id.test.js.map +1 -0
  113. package/dist/utils/index.d.ts +12 -0
  114. package/dist/utils/index.d.ts.map +1 -0
  115. package/dist/utils/index.js +34 -0
  116. package/dist/utils/index.js.map +1 -0
  117. package/dist/utils/natural-language.d.ts +111 -0
  118. package/dist/utils/natural-language.d.ts.map +1 -0
  119. package/dist/utils/natural-language.js +297 -0
  120. package/dist/utils/natural-language.js.map +1 -0
  121. package/dist/utils/natural-language.test.d.ts +2 -0
  122. package/dist/utils/natural-language.test.d.ts.map +1 -0
  123. package/dist/utils/natural-language.test.js +197 -0
  124. package/dist/utils/natural-language.test.js.map +1 -0
  125. package/dist/utils/priority-queue.d.ts +17 -0
  126. package/dist/utils/priority-queue.d.ts.map +1 -0
  127. package/dist/utils/priority-queue.js +62 -0
  128. package/dist/utils/priority-queue.js.map +1 -0
  129. package/dist/utils/priority-queue.test.d.ts +2 -0
  130. package/dist/utils/priority-queue.test.d.ts.map +1 -0
  131. package/dist/utils/priority-queue.test.js +82 -0
  132. package/dist/utils/priority-queue.test.js.map +1 -0
  133. package/dist/utils/projection.d.ts +65 -0
  134. package/dist/utils/projection.d.ts.map +1 -0
  135. package/dist/utils/projection.js +180 -0
  136. package/dist/utils/projection.js.map +1 -0
  137. package/dist/utils/projection.test.d.ts +2 -0
  138. package/dist/utils/projection.test.d.ts.map +1 -0
  139. package/dist/utils/projection.test.js +341 -0
  140. package/dist/utils/projection.test.js.map +1 -0
  141. package/dist/utils/terminal-ui.d.ts +208 -0
  142. package/dist/utils/terminal-ui.d.ts.map +1 -0
  143. package/dist/utils/terminal-ui.js +614 -0
  144. package/dist/utils/terminal-ui.js.map +1 -0
  145. package/dist/utils/terminal-ui.test.d.ts +2 -0
  146. package/dist/utils/terminal-ui.test.d.ts.map +1 -0
  147. package/dist/utils/terminal-ui.test.js +683 -0
  148. package/dist/utils/terminal-ui.test.js.map +1 -0
  149. package/dist/utils/workspace.d.ts +102 -0
  150. package/dist/utils/workspace.d.ts.map +1 -0
  151. package/dist/utils/workspace.js +183 -0
  152. package/dist/utils/workspace.js.map +1 -0
  153. package/dist/utils/workspace.test.d.ts +2 -0
  154. package/dist/utils/workspace.test.d.ts.map +1 -0
  155. package/dist/utils/workspace.test.js +97 -0
  156. package/dist/utils/workspace.test.js.map +1 -0
  157. package/package.json +5 -1
  158. package/src/algorithms/critical-path.test.ts +227 -0
  159. package/src/algorithms/critical-path.ts +14 -34
  160. package/src/algorithms/dependency-integrity.test.ts +335 -0
  161. package/src/algorithms/dependency-integrity.ts +4 -13
  162. package/src/algorithms/tech-analysis.test.ts +405 -0
  163. package/src/algorithms/tech-analysis.ts +27 -27
  164. package/src/algorithms/topological-sort.test.ts +182 -0
  165. package/src/algorithms/topological-sort.ts +6 -10
  166. package/src/schemas/index.ts +2 -13
  167. package/src/schemas/response-format.ts +6 -6
  168. package/src/schemas/response-schema.test.ts +314 -0
  169. package/src/schemas/response-schema.ts +25 -20
  170. package/src/schemas/task.ts +4 -22
  171. package/src/utils/dashboard-renderer.test.ts +976 -0
  172. package/src/utils/dashboard-renderer.ts +27 -59
  173. package/src/utils/date.test.ts +329 -0
  174. package/src/utils/date.ts +2 -10
  175. package/src/utils/hierarchy.test.ts +488 -0
  176. package/src/utils/hierarchy.ts +4 -5
  177. package/src/utils/id.test.ts +235 -0
  178. package/src/utils/index.ts +7 -1
  179. package/src/utils/natural-language.test.ts +234 -0
  180. package/src/utils/priority-queue.test.ts +103 -0
  181. package/src/utils/projection.test.ts +430 -0
  182. package/src/utils/terminal-ui.test.ts +831 -0
  183. package/src/utils/terminal-ui.ts +53 -54
  184. package/src/utils/workspace.test.ts +125 -0
@@ -41,11 +41,13 @@ export interface DashboardStats {
41
41
  export interface DependencyMetrics {
42
42
  readyToWork: number;
43
43
  blockedByDependencies: number;
44
- mostDependedOn?: {
45
- id: string;
46
- title: string;
47
- dependentCount: number;
48
- } | undefined;
44
+ mostDependedOn?:
45
+ | {
46
+ id: string;
47
+ title: string;
48
+ dependentCount: number;
49
+ }
50
+ | undefined;
49
51
  }
50
52
 
51
53
  export interface WorkspaceInfo {
@@ -107,9 +109,7 @@ export function calculateStats(tasks: Task[]): DashboardStats {
107
109
  }
108
110
 
109
111
  export function calculateDependencyMetrics(tasks: Task[]): DependencyMetrics {
110
- const completedIds = new Set(
111
- tasks.filter((t) => t.status === "completed").map((t) => t.id)
112
- );
112
+ const completedIds = new Set(tasks.filter((t) => t.status === "completed").map((t) => t.id));
113
113
 
114
114
  let readyToWork = 0;
115
115
  let blockedByDependencies = 0;
@@ -134,10 +134,7 @@ export function calculateDependencyMetrics(tasks: Task[]): DependencyMetrics {
134
134
 
135
135
  // Track dependent counts
136
136
  for (const dep of deps) {
137
- dependentCounts.set(
138
- dep.taskId,
139
- (dependentCounts.get(dep.taskId) ?? 0) + 1
140
- );
137
+ dependentCounts.set(dep.taskId, (dependentCounts.get(dep.taskId) ?? 0) + 1);
141
138
  }
142
139
  }
143
140
 
@@ -192,8 +189,7 @@ export function renderStatusWidget(tasks: Task[]): string {
192
189
  const today = getTodayTasks(tasks);
193
190
  const overdue = getOverdueTasks(tasks);
194
191
  const activeTasks = stats.total - stats.cancelled;
195
- const percent =
196
- activeTasks > 0 ? Math.round((stats.completed / activeTasks) * 100) : 0;
192
+ const percent = activeTasks > 0 ? Math.round((stats.completed / activeTasks) * 100) : 0;
197
193
 
198
194
  const lines: string[] = [];
199
195
 
@@ -252,16 +248,10 @@ export function renderActionsWidget(tasks: Task[]): string {
252
248
 
253
249
  // Get top 4 ready tasks sorted by priority
254
250
  const readyTasks = tasks
255
- .filter(
256
- (t) =>
257
- t.status === "pending" &&
258
- (!t.dependencies || t.dependencies.length === 0)
259
- )
251
+ .filter((t) => t.status === "pending" && (!t.dependencies || t.dependencies.length === 0))
260
252
  .sort((a, b) => {
261
253
  const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
262
- return (
263
- (priorityOrder[a.priority] ?? 2) - (priorityOrder[b.priority] ?? 2)
264
- );
254
+ return (priorityOrder[a.priority] ?? 2) - (priorityOrder[b.priority] ?? 2);
265
255
  })
266
256
  .slice(0, 4);
267
257
 
@@ -341,17 +331,13 @@ export function renderWorkspacesTable(
341
331
  const stats = calculateStats(tasks);
342
332
  const depMetrics = calculateDependencyMetrics(tasks);
343
333
  const activeTasks = stats.total - stats.cancelled;
344
- const percent =
345
- activeTasks > 0
346
- ? Math.round((stats.completed / activeTasks) * 100)
347
- : 0;
334
+ const percent = activeTasks > 0 ? Math.round((stats.completed / activeTasks) * 100) : 0;
348
335
 
349
336
  // Create mini progress bar
350
337
  const barWidth = 8;
351
338
  const filled = Math.round((percent / 100) * barWidth);
352
339
  const empty = barWidth - filled;
353
- const miniBar =
354
- c.green("█".repeat(filled)) + c.gray("░".repeat(empty));
340
+ const miniBar = c.green("█".repeat(filled)) + c.gray("░".repeat(empty));
355
341
 
356
342
  rows.push({
357
343
  name: truncateStr(ws.name, 20),
@@ -392,9 +378,7 @@ export const renderProjectsTable = renderWorkspacesTable;
392
378
  * Render Tasks table for single workspace view
393
379
  */
394
380
  export function renderTasksTable(tasks: Task[], limit: number = 10): string {
395
- const activeTasks = tasks.filter(
396
- (t) => t.status !== "completed" && t.status !== "cancelled"
397
- );
381
+ const activeTasks = tasks.filter((t) => t.status !== "completed" && t.status !== "cancelled");
398
382
 
399
383
  if (activeTasks.length === 0) {
400
384
  return c.gray("No active tasks.");
@@ -421,10 +405,7 @@ export function renderTasksTable(tasks: Task[], limit: number = 10): string {
421
405
  },
422
406
  ];
423
407
 
424
- let result = table(
425
- displayTasks as unknown as Record<string, unknown>[],
426
- columns
427
- );
408
+ let result = table(displayTasks as unknown as Record<string, unknown>[], columns);
428
409
 
429
410
  if (activeTasks.length > limit) {
430
411
  result += `\n${c.gray(`(+${activeTasks.length - limit} more tasks)`)}`;
@@ -440,21 +421,14 @@ export function renderTasksTable(tasks: Task[], limit: number = 10): string {
440
421
  function getTodayTasks(tasks: Task[]): Task[] {
441
422
  const today = new Date().toISOString().split("T")[0];
442
423
  return tasks.filter(
443
- (t) =>
444
- t.dueDate === today &&
445
- t.status !== "completed" &&
446
- t.status !== "cancelled"
424
+ (t) => t.dueDate === today && t.status !== "completed" && t.status !== "cancelled"
447
425
  );
448
426
  }
449
427
 
450
428
  function getOverdueTasks(tasks: Task[]): Task[] {
451
429
  const today = new Date().toISOString().split("T")[0] ?? "";
452
430
  return tasks.filter(
453
- (t) =>
454
- t.dueDate &&
455
- t.dueDate < today &&
456
- t.status !== "completed" &&
457
- t.status !== "cancelled"
431
+ (t) => t.dueDate && t.dueDate < today && t.status !== "completed" && t.status !== "cancelled"
458
432
  );
459
433
  }
460
434
 
@@ -538,9 +512,7 @@ export function renderDashboard(
538
512
 
539
513
  // Tasks table (for single workspace view)
540
514
  if (showTasks && (currentWorkspace || workspaces.length === 1)) {
541
- const activeTasks = tasks.filter(
542
- (t) => t.status !== "completed" && t.status !== "cancelled"
543
- );
515
+ const activeTasks = tasks.filter((t) => t.status !== "completed" && t.status !== "cancelled");
544
516
  if (activeTasks.length > 0) {
545
517
  lines.push(c.bold(`Tasks (${activeTasks.length})`));
546
518
  lines.push("");
@@ -565,7 +537,7 @@ export function renderWorkspaceDashboard(
565
537
  const wsInfo: WorkspaceInfo = {
566
538
  name: workspace,
567
539
  taskCount: tasks.length,
568
- completedCount: tasks.filter(t => t.status === "completed").length,
540
+ completedCount: tasks.filter((t) => t.status === "completed").length,
569
541
  };
570
542
 
571
543
  const data: DashboardData = {
@@ -576,17 +548,13 @@ export function renderWorkspaceDashboard(
576
548
  activeTag: options.activeTag,
577
549
  };
578
550
 
579
- return renderDashboard(
580
- data,
581
- () => tasks,
582
- {
583
- showBanner: true,
584
- showInbox: false,
585
- showWorkspaces: false,
586
- showTasks: true,
587
- stripAnsiCodes: options.stripAnsiCodes,
588
- }
589
- );
551
+ return renderDashboard(data, () => tasks, {
552
+ showBanner: true,
553
+ showInbox: false,
554
+ showWorkspaces: false,
555
+ showTasks: true,
556
+ stripAnsiCodes: options.stripAnsiCodes,
557
+ });
590
558
  }
591
559
 
592
560
  // Legacy export for backwards compatibility
@@ -0,0 +1,329 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import {
3
+ now,
4
+ parseRelativeDate,
5
+ parseRelativeDateSafe,
6
+ parseDateString,
7
+ formatDate,
8
+ formatDisplayDate,
9
+ isToday,
10
+ isPastDue,
11
+ isWithinDays,
12
+ isValidDate,
13
+ DateParseError,
14
+ } from "./date.js";
15
+
16
+ describe("now", () => {
17
+ test("returns ISO timestamp string", () => {
18
+ const result = now();
19
+ expect(typeof result).toBe("string");
20
+ expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
21
+ });
22
+
23
+ test("returns current time within tolerance", () => {
24
+ const before = Date.now();
25
+ const result = now();
26
+ const after = Date.now();
27
+
28
+ const resultTime = new Date(result).getTime();
29
+ expect(resultTime).toBeGreaterThanOrEqual(before);
30
+ expect(resultTime).toBeLessThanOrEqual(after);
31
+ });
32
+ });
33
+
34
+ describe("parseRelativeDate", () => {
35
+ test("parses 'today'", () => {
36
+ const result = parseRelativeDate("today");
37
+ expect(result).not.toBeNull();
38
+ const today = new Date();
39
+ expect(result!.getDate()).toBe(today.getDate());
40
+ });
41
+
42
+ test("parses '오늘' (Korean today)", () => {
43
+ const result = parseRelativeDate("오늘");
44
+ expect(result).not.toBeNull();
45
+ const today = new Date();
46
+ expect(result!.getDate()).toBe(today.getDate());
47
+ });
48
+
49
+ test("parses 'tomorrow'", () => {
50
+ const result = parseRelativeDate("tomorrow");
51
+ expect(result).not.toBeNull();
52
+ const tomorrow = new Date();
53
+ tomorrow.setDate(tomorrow.getDate() + 1);
54
+ expect(result!.getDate()).toBe(tomorrow.getDate());
55
+ });
56
+
57
+ test("parses '내일' (Korean tomorrow)", () => {
58
+ const result = parseRelativeDate("내일");
59
+ expect(result).not.toBeNull();
60
+ const tomorrow = new Date();
61
+ tomorrow.setDate(tomorrow.getDate() + 1);
62
+ expect(result!.getDate()).toBe(tomorrow.getDate());
63
+ });
64
+
65
+ test("parses 'yesterday'", () => {
66
+ const result = parseRelativeDate("yesterday");
67
+ expect(result).not.toBeNull();
68
+ const yesterday = new Date();
69
+ yesterday.setDate(yesterday.getDate() - 1);
70
+ expect(result!.getDate()).toBe(yesterday.getDate());
71
+ });
72
+
73
+ test("parses 'next week'", () => {
74
+ const result = parseRelativeDate("next week");
75
+ expect(result).not.toBeNull();
76
+ const nextWeek = new Date();
77
+ nextWeek.setDate(nextWeek.getDate() + 7);
78
+ expect(result!.getDate()).toBe(nextWeek.getDate());
79
+ });
80
+
81
+ test("parses 'in 3 days'", () => {
82
+ const result = parseRelativeDate("in 3 days");
83
+ expect(result).not.toBeNull();
84
+ const future = new Date();
85
+ future.setDate(future.getDate() + 3);
86
+ expect(result!.getDate()).toBe(future.getDate());
87
+ });
88
+
89
+ test("parses '5일 후' (Korean days later)", () => {
90
+ const result = parseRelativeDate("5일 후");
91
+ expect(result).not.toBeNull();
92
+ const future = new Date();
93
+ future.setDate(future.getDate() + 5);
94
+ expect(result!.getDate()).toBe(future.getDate());
95
+ });
96
+
97
+ test("parses weekday names", () => {
98
+ const result = parseRelativeDate("monday");
99
+ expect(result).not.toBeNull();
100
+ expect(result!.getDay()).toBe(1); // Monday = 1
101
+ });
102
+
103
+ test("parses ISO date string", () => {
104
+ const result = parseRelativeDate("2025-12-31");
105
+ expect(result).not.toBeNull();
106
+ expect(result!.getFullYear()).toBe(2025);
107
+ expect(result!.getMonth()).toBe(11); // December = 11
108
+ expect(result!.getDate()).toBe(31);
109
+ });
110
+
111
+ test("returns null for invalid input", () => {
112
+ const result = parseRelativeDate("invalid date string");
113
+ expect(result).toBeNull();
114
+ });
115
+ });
116
+
117
+ describe("parseRelativeDateSafe", () => {
118
+ test("returns success result for valid date", () => {
119
+ const result = parseRelativeDateSafe("tomorrow");
120
+ expect(result.success).toBe(true);
121
+ if (result.success === true) {
122
+ const tomorrow = new Date();
123
+ tomorrow.setDate(tomorrow.getDate() + 1);
124
+ expect(result.date.getDate()).toBe(tomorrow.getDate());
125
+ }
126
+ });
127
+
128
+ test("returns error for empty input", () => {
129
+ const result = parseRelativeDateSafe("");
130
+ expect(result.success).toBe(false);
131
+ if (result.success === false) {
132
+ expect(result.reason).toBe("empty_input");
133
+ expect(result.error).toContain("empty");
134
+ }
135
+ });
136
+
137
+ test("returns error for whitespace-only input", () => {
138
+ const result = parseRelativeDateSafe(" ");
139
+ expect(result.success).toBe(false);
140
+ if (result.success === false) {
141
+ expect(result.reason).toBe("empty_input");
142
+ }
143
+ });
144
+
145
+ test("returns error for invalid date format", () => {
146
+ const result = parseRelativeDateSafe("not a date");
147
+ expect(result.success).toBe(false);
148
+ if (result.success === false) {
149
+ expect(result.reason).toBe("invalid_format");
150
+ expect(result.error).toContain("Supported formats");
151
+ }
152
+ });
153
+
154
+ test("returns error for malformed ISO date", () => {
155
+ const result = parseRelativeDateSafe("2025-1-5");
156
+ expect(result.success).toBe(false);
157
+ if (result.success === false) {
158
+ expect(result.reason).toBe("invalid_format");
159
+ expect(result.error).toContain("zero-padded");
160
+ }
161
+ });
162
+
163
+ test("returns error for invalid date like Feb 30", () => {
164
+ const result = parseRelativeDateSafe("2025-02-30");
165
+ expect(result.success).toBe(false);
166
+ if (result.success === false) {
167
+ expect(result.reason).toBe("invalid_date");
168
+ expect(result.error).toContain("February 30");
169
+ }
170
+ });
171
+
172
+ test("returns error for year out of range", () => {
173
+ const result = parseRelativeDateSafe("1800-01-01");
174
+ expect(result.success).toBe(false);
175
+ if (result.success === false) {
176
+ expect(result.reason).toBe("out_of_range");
177
+ expect(result.error).toContain("1900-2100");
178
+ }
179
+ });
180
+
181
+ test("returns error for excessive day offset", () => {
182
+ const result = parseRelativeDateSafe("in 5000 days");
183
+ expect(result.success).toBe(false);
184
+ if (result.success === false) {
185
+ expect(result.reason).toBe("out_of_range");
186
+ expect(result.error).toContain("3650");
187
+ }
188
+ });
189
+ });
190
+
191
+ describe("isValidDate", () => {
192
+ test("returns true for valid Date", () => {
193
+ expect(isValidDate(new Date())).toBe(true);
194
+ expect(isValidDate(new Date("2025-01-01"))).toBe(true);
195
+ });
196
+
197
+ test("returns false for Invalid Date", () => {
198
+ expect(isValidDate(new Date("invalid"))).toBe(false);
199
+ expect(isValidDate(new Date(NaN))).toBe(false);
200
+ });
201
+ });
202
+
203
+ describe("parseDateString", () => {
204
+ test("parses valid date string", () => {
205
+ const result = parseDateString("2025-01-15");
206
+ expect(result).toBeInstanceOf(Date);
207
+ expect(result.getFullYear()).toBe(2025);
208
+ });
209
+
210
+ test("throws DateParseError for empty string", () => {
211
+ expect(() => parseDateString("")).toThrow(DateParseError);
212
+ try {
213
+ parseDateString("");
214
+ } catch (e) {
215
+ expect(e).toBeInstanceOf(DateParseError);
216
+ expect((e as DateParseError).reason).toBe("empty_input");
217
+ }
218
+ });
219
+
220
+ test("throws DateParseError for invalid string", () => {
221
+ expect(() => parseDateString("not a date")).toThrow(DateParseError);
222
+ try {
223
+ parseDateString("not a date");
224
+ } catch (e) {
225
+ expect(e).toBeInstanceOf(DateParseError);
226
+ expect((e as DateParseError).reason).toBe("invalid_format");
227
+ expect((e as DateParseError).input).toBe("not a date");
228
+ }
229
+ });
230
+ });
231
+
232
+ describe("formatDisplayDate error handling", () => {
233
+ test("throws DateParseError for invalid string", () => {
234
+ expect(() => formatDisplayDate("invalid")).toThrow(DateParseError);
235
+ });
236
+
237
+ test("throws DateParseError for Invalid Date object", () => {
238
+ expect(() => formatDisplayDate(new Date("invalid"))).toThrow(DateParseError);
239
+ });
240
+
241
+ test("formats valid string date", () => {
242
+ const result = formatDisplayDate("2025-06-15");
243
+ expect(typeof result).toBe("string");
244
+ expect(result).toMatch(/\d+\/\d+\/\d+/);
245
+ });
246
+ });
247
+
248
+ describe("formatDate", () => {
249
+ test("formats date as YYYY-MM-DD", () => {
250
+ const date = new Date("2025-06-15T10:30:00");
251
+ expect(formatDate(date)).toBe("2025-06-15");
252
+ });
253
+ });
254
+
255
+ describe("isToday", () => {
256
+ test("returns true for today", () => {
257
+ expect(isToday(new Date())).toBe(true);
258
+ });
259
+
260
+ test("returns false for tomorrow", () => {
261
+ const tomorrow = new Date();
262
+ tomorrow.setDate(tomorrow.getDate() + 1);
263
+ expect(isToday(tomorrow)).toBe(false);
264
+ });
265
+
266
+ test("accepts string input", () => {
267
+ expect(isToday(new Date().toISOString())).toBe(true);
268
+ });
269
+
270
+ test("returns false for invalid date string", () => {
271
+ expect(isToday("invalid")).toBe(false);
272
+ });
273
+
274
+ test("returns false for Invalid Date object", () => {
275
+ expect(isToday(new Date("invalid"))).toBe(false);
276
+ });
277
+ });
278
+
279
+ describe("isPastDue", () => {
280
+ test("returns true for yesterday", () => {
281
+ const yesterday = new Date();
282
+ yesterday.setDate(yesterday.getDate() - 1);
283
+ expect(isPastDue(yesterday)).toBe(true);
284
+ });
285
+
286
+ test("returns false for tomorrow", () => {
287
+ const tomorrow = new Date();
288
+ tomorrow.setDate(tomorrow.getDate() + 1);
289
+ expect(isPastDue(tomorrow)).toBe(false);
290
+ });
291
+
292
+ test("returns false for invalid date", () => {
293
+ expect(isPastDue("invalid")).toBe(false);
294
+ expect(isPastDue(new Date("invalid"))).toBe(false);
295
+ });
296
+ });
297
+
298
+ describe("isWithinDays", () => {
299
+ test("returns true for date within range", () => {
300
+ const inThreeDays = new Date();
301
+ inThreeDays.setDate(inThreeDays.getDate() + 3);
302
+ expect(isWithinDays(inThreeDays, 7)).toBe(true);
303
+ });
304
+
305
+ test("returns false for date outside range", () => {
306
+ const inTenDays = new Date();
307
+ inTenDays.setDate(inTenDays.getDate() + 10);
308
+ expect(isWithinDays(inTenDays, 7)).toBe(false);
309
+ });
310
+
311
+ test("returns false for past date", () => {
312
+ const yesterday = new Date();
313
+ yesterday.setDate(yesterday.getDate() - 1);
314
+ expect(isWithinDays(yesterday, 7)).toBe(false);
315
+ });
316
+
317
+ test("returns false for invalid date", () => {
318
+ expect(isWithinDays("invalid", 7)).toBe(false);
319
+ expect(isWithinDays(new Date("invalid"), 7)).toBe(false);
320
+ });
321
+
322
+ test("returns false for invalid days parameter", () => {
323
+ const tomorrow = new Date();
324
+ tomorrow.setDate(tomorrow.getDate() + 1);
325
+ expect(isWithinDays(tomorrow, -1)).toBe(false);
326
+ expect(isWithinDays(tomorrow, NaN)).toBe(false);
327
+ expect(isWithinDays(tomorrow, Infinity)).toBe(false);
328
+ });
329
+ });
package/src/utils/date.ts CHANGED
@@ -247,11 +247,7 @@ export function parseDateString(input: string): Date {
247
247
 
248
248
  const d = new Date(input);
249
249
  if (isNaN(d.getTime())) {
250
- throw new DateParseError(
251
- `Unable to parse "${input}" as a date`,
252
- input,
253
- "invalid_format"
254
- );
250
+ throw new DateParseError(`Unable to parse "${input}" as a date`, input, "invalid_format");
255
251
  }
256
252
 
257
253
  return d;
@@ -280,11 +276,7 @@ export function formatDisplayDate(date: Date | string): string {
280
276
  }
281
277
 
282
278
  if (!isValidDate(d)) {
283
- throw new DateParseError(
284
- "Invalid Date object provided",
285
- String(date),
286
- "invalid_date"
287
- );
279
+ throw new DateParseError("Invalid Date object provided", String(date), "invalid_date");
288
280
  }
289
281
 
290
282
  const parts = new Intl.DateTimeFormat(undefined, {