@task-mcp/shared 1.0.4 → 1.0.7

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 (98) hide show
  1. package/dist/algorithms/critical-path.d.ts.map +1 -1
  2. package/dist/algorithms/critical-path.js +50 -26
  3. package/dist/algorithms/critical-path.js.map +1 -1
  4. package/dist/algorithms/dependency-integrity.d.ts +73 -0
  5. package/dist/algorithms/dependency-integrity.d.ts.map +1 -0
  6. package/dist/algorithms/dependency-integrity.js +189 -0
  7. package/dist/algorithms/dependency-integrity.js.map +1 -0
  8. package/dist/algorithms/index.d.ts +2 -0
  9. package/dist/algorithms/index.d.ts.map +1 -1
  10. package/dist/algorithms/index.js +2 -0
  11. package/dist/algorithms/index.js.map +1 -1
  12. package/dist/algorithms/tech-analysis.d.ts +106 -0
  13. package/dist/algorithms/tech-analysis.d.ts.map +1 -0
  14. package/dist/algorithms/tech-analysis.js +296 -0
  15. package/dist/algorithms/tech-analysis.js.map +1 -0
  16. package/dist/algorithms/tech-analysis.test.d.ts +2 -0
  17. package/dist/algorithms/tech-analysis.test.d.ts.map +1 -0
  18. package/dist/algorithms/tech-analysis.test.js +338 -0
  19. package/dist/algorithms/tech-analysis.test.js.map +1 -0
  20. package/dist/algorithms/topological-sort.d.ts.map +1 -1
  21. package/dist/algorithms/topological-sort.js +60 -8
  22. package/dist/algorithms/topological-sort.js.map +1 -1
  23. package/dist/schemas/inbox.d.ts +55 -0
  24. package/dist/schemas/inbox.d.ts.map +1 -0
  25. package/dist/schemas/inbox.js +25 -0
  26. package/dist/schemas/inbox.js.map +1 -0
  27. package/dist/schemas/index.d.ts +3 -1
  28. package/dist/schemas/index.d.ts.map +1 -1
  29. package/dist/schemas/index.js +9 -1
  30. package/dist/schemas/index.js.map +1 -1
  31. package/dist/schemas/project.d.ts +154 -41
  32. package/dist/schemas/project.d.ts.map +1 -1
  33. package/dist/schemas/project.js +38 -33
  34. package/dist/schemas/project.js.map +1 -1
  35. package/dist/schemas/response-format.d.ts +80 -0
  36. package/dist/schemas/response-format.d.ts.map +1 -0
  37. package/dist/schemas/response-format.js +17 -0
  38. package/dist/schemas/response-format.js.map +1 -0
  39. package/dist/schemas/task.d.ts +592 -94
  40. package/dist/schemas/task.d.ts.map +1 -1
  41. package/dist/schemas/task.js +124 -64
  42. package/dist/schemas/task.js.map +1 -1
  43. package/dist/schemas/view.d.ts +128 -37
  44. package/dist/schemas/view.d.ts.map +1 -1
  45. package/dist/schemas/view.js +38 -24
  46. package/dist/schemas/view.js.map +1 -1
  47. package/dist/utils/date.d.ts.map +1 -1
  48. package/dist/utils/date.js +17 -2
  49. package/dist/utils/date.js.map +1 -1
  50. package/dist/utils/hierarchy.d.ts +75 -0
  51. package/dist/utils/hierarchy.d.ts.map +1 -0
  52. package/dist/utils/hierarchy.js +179 -0
  53. package/dist/utils/hierarchy.js.map +1 -0
  54. package/dist/utils/id.d.ts +51 -1
  55. package/dist/utils/id.d.ts.map +1 -1
  56. package/dist/utils/id.js +124 -4
  57. package/dist/utils/id.js.map +1 -1
  58. package/dist/utils/id.test.d.ts +2 -0
  59. package/dist/utils/id.test.d.ts.map +1 -0
  60. package/dist/utils/id.test.js +228 -0
  61. package/dist/utils/id.test.js.map +1 -0
  62. package/dist/utils/index.d.ts +4 -2
  63. package/dist/utils/index.d.ts.map +1 -1
  64. package/dist/utils/index.js +7 -2
  65. package/dist/utils/index.js.map +1 -1
  66. package/dist/utils/natural-language.d.ts +45 -0
  67. package/dist/utils/natural-language.d.ts.map +1 -1
  68. package/dist/utils/natural-language.js +86 -0
  69. package/dist/utils/natural-language.js.map +1 -1
  70. package/dist/utils/projection.d.ts +65 -0
  71. package/dist/utils/projection.d.ts.map +1 -0
  72. package/dist/utils/projection.js +181 -0
  73. package/dist/utils/projection.js.map +1 -0
  74. package/dist/utils/projection.test.d.ts +2 -0
  75. package/dist/utils/projection.test.d.ts.map +1 -0
  76. package/dist/utils/projection.test.js +400 -0
  77. package/dist/utils/projection.test.js.map +1 -0
  78. package/package.json +2 -2
  79. package/src/algorithms/critical-path.ts +56 -24
  80. package/src/algorithms/dependency-integrity.ts +270 -0
  81. package/src/algorithms/index.ts +28 -0
  82. package/src/algorithms/tech-analysis.test.ts +413 -0
  83. package/src/algorithms/tech-analysis.ts +412 -0
  84. package/src/algorithms/topological-sort.ts +66 -9
  85. package/src/schemas/inbox.ts +32 -0
  86. package/src/schemas/index.ts +31 -0
  87. package/src/schemas/project.ts +43 -40
  88. package/src/schemas/response-format.ts +108 -0
  89. package/src/schemas/task.ts +145 -77
  90. package/src/schemas/view.ts +43 -33
  91. package/src/utils/date.ts +18 -2
  92. package/src/utils/hierarchy.ts +224 -0
  93. package/src/utils/id.test.ts +281 -0
  94. package/src/utils/id.ts +139 -4
  95. package/src/utils/index.ts +46 -2
  96. package/src/utils/natural-language.ts +113 -0
  97. package/src/utils/projection.test.ts +505 -0
  98. package/src/utils/projection.ts +251 -0
@@ -0,0 +1,108 @@
1
+ import { z } from "zod";
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 = z.enum(["concise", "standard", "detailed"]);
16
+ export type ResponseFormat = z.infer<typeof ResponseFormat>;
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
+ }
@@ -1,116 +1,184 @@
1
- import { type } from "arktype";
1
+ import { z } from "zod";
2
2
 
3
3
  // Priority levels
4
- export const Priority = type("'critical' | 'high' | 'medium' | 'low'");
5
- export type Priority = typeof Priority.infer;
4
+ export const Priority = z.enum(["critical", "high", "medium", "low"]);
5
+ export type Priority = z.infer<typeof Priority>;
6
6
 
7
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;
8
+ export const TaskStatus = z.enum([
9
+ "pending",
10
+ "in_progress",
11
+ "blocked",
12
+ "completed",
13
+ "cancelled",
14
+ ]);
15
+ export type TaskStatus = z.infer<typeof TaskStatus>;
12
16
 
13
17
  // Dependency relationship types
14
- export const DependencyType = type(
15
- "'blocks' | 'blocked_by' | 'related'"
16
- );
17
- export type DependencyType = typeof DependencyType.infer;
18
+ export const DependencyType = z.enum(["blocks", "blocked_by", "related"]);
19
+ export type DependencyType = z.infer<typeof DependencyType>;
18
20
 
19
21
  // A single dependency link
20
- export const Dependency = type({
21
- taskId: "string",
22
+ export const Dependency = z.object({
23
+ taskId: z.string(),
22
24
  type: DependencyType,
23
- "reason?": "string",
25
+ reason: z.string().optional(),
24
26
  });
25
- export type Dependency = typeof Dependency.infer;
27
+ export type Dependency = z.infer<typeof Dependency>;
26
28
 
27
29
  // Time estimation
28
- export const TimeEstimate = type({
29
- "optimistic?": "number", // minutes
30
- "expected?": "number", // minutes
31
- "pessimistic?": "number", // minutes
32
- "confidence?": "'low' | 'medium' | 'high'",
30
+ export const TimeEstimate = z.object({
31
+ optimistic: z.number().optional(), // minutes
32
+ expected: z.number().optional(), // minutes
33
+ pessimistic: z.number().optional(), // minutes
34
+ confidence: z.enum(["low", "medium", "high"]).optional(),
33
35
  });
34
- export type TimeEstimate = typeof TimeEstimate.infer;
36
+ export type TimeEstimate = z.infer<typeof TimeEstimate>;
35
37
 
36
38
  // 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",
39
+ export const Recurrence = z.object({
40
+ pattern: z.enum(["daily", "weekly", "monthly", "after_completion"]),
41
+ interval: z.number().optional(), // every N days/weeks/months
42
+ daysOfWeek: z.array(z.number()).optional(), // 0-6 for weekly
43
+ endDate: z.string().optional(),
42
44
  });
43
- export type Recurrence = typeof Recurrence.infer;
45
+ export type Recurrence = z.infer<typeof Recurrence>;
46
+
47
+ // Complexity factors that contribute to task difficulty
48
+ export const ComplexityFactor = z.enum([
49
+ "cross_cutting",
50
+ "state_management",
51
+ "error_handling",
52
+ "performance",
53
+ "security",
54
+ "external_dependency",
55
+ "data_migration",
56
+ "breaking_change",
57
+ "unclear_requirements",
58
+ "coordination",
59
+ ]);
60
+ export type ComplexityFactor = z.infer<typeof ComplexityFactor>;
61
+
62
+ // Complexity analysis result (populated by Claude)
63
+ export const ComplexityAnalysis = z.object({
64
+ score: z.number().optional(), // 1-10 complexity score
65
+ factors: z.array(ComplexityFactor).optional(),
66
+ suggestedSubtasks: z.number().optional(), // 0-10 recommended subtask count
67
+ rationale: z.string().optional(),
68
+ analyzedAt: z.string().optional(),
69
+ });
70
+ export type ComplexityAnalysis = z.infer<typeof ComplexityAnalysis>;
71
+
72
+ // Tech area categories for ordering
73
+ export const TechArea = z.enum([
74
+ "schema",
75
+ "backend",
76
+ "frontend",
77
+ "infra",
78
+ "devops",
79
+ "test",
80
+ "docs",
81
+ "refactor",
82
+ ]);
83
+ export type TechArea = z.infer<typeof TechArea>;
84
+
85
+ // Risk level for changes
86
+ export const RiskLevel = z.enum(["low", "medium", "high", "critical"]);
87
+ export type RiskLevel = z.infer<typeof RiskLevel>;
88
+
89
+ // Tech stack analysis result (populated by Claude)
90
+ export const TechStackAnalysis = z.object({
91
+ areas: z.array(TechArea).optional(),
92
+ hasBreakingChange: z.boolean().optional(),
93
+ riskLevel: RiskLevel.optional(),
94
+ affectedComponents: z.array(z.string()).optional(),
95
+ rationale: z.string().optional(),
96
+ analyzedAt: z.string().optional(),
97
+ });
98
+ export type TechStackAnalysis = z.infer<typeof TechStackAnalysis>;
44
99
 
45
100
  // Core Task schema
46
- export const Task = type({
47
- id: "string",
48
- title: "string",
49
- "description?": "string",
101
+ export const Task = z.object({
102
+ id: z.string(),
103
+ title: z.string(),
104
+ description: z.string().optional(),
50
105
  status: TaskStatus,
51
106
  priority: Priority,
52
- projectId: "string",
107
+ projectId: z.string(),
108
+
109
+ // Hierarchy (subtask support)
110
+ parentId: z.string().nullable().optional(), // Parent task ID for subtasks (null to unlink)
111
+ level: z.number().optional(), // Hierarchy depth (0=root, 1=subtask, 2=sub-subtask, max 3)
53
112
 
54
113
  // Dependencies
55
- "dependencies?": Dependency.array(),
114
+ dependencies: z.array(Dependency).optional(),
56
115
 
57
116
  // 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",
117
+ estimate: TimeEstimate.optional(),
118
+ actualMinutes: z.number().optional(),
119
+ dueDate: z.string().optional(), // ISO date string
120
+ startDate: z.string().optional(), // When task can start
121
+ startedAt: z.string().optional(),
122
+ completedAt: z.string().optional(),
64
123
 
65
124
  // Organization
66
- "contexts?": "string[]", // e.g., ["focus", "review"]
67
- "tags?": "string[]",
125
+ contexts: z.array(z.string()).optional(), // e.g., ["focus", "review"]
126
+ tags: z.array(z.string()).optional(),
68
127
 
69
128
  // Recurrence
70
- "recurrence?": Recurrence,
129
+ recurrence: Recurrence.optional(),
71
130
 
72
131
  // Metadata
73
- createdAt: "string",
74
- updatedAt: "string",
132
+ createdAt: z.string(),
133
+ updatedAt: z.string(),
75
134
 
76
135
  // 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",
136
+ criticalPath: z.boolean().optional(),
137
+ slack: z.number().optional(), // Minutes of slack time
138
+ earliestStart: z.number().optional(), // Minutes from project start
139
+ latestStart: z.number().optional(),
140
+
141
+ // Analysis fields (populated by Claude)
142
+ complexity: ComplexityAnalysis.optional(),
143
+ techStack: TechStackAnalysis.optional(),
81
144
  });
82
- export type Task = typeof Task.infer;
145
+ export type Task = z.infer<typeof Task>;
83
146
 
84
147
  // 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,
148
+ export const TaskCreateInput = z.object({
149
+ title: z.string(),
150
+ description: z.string().optional(),
151
+ projectId: z.string().optional(),
152
+ priority: Priority.optional(),
153
+ parentId: z.string().nullable().optional(), // Parent task ID for creating subtasks (null to unlink)
154
+ dependencies: z.array(Dependency).optional(),
155
+ estimate: TimeEstimate.optional(),
156
+ dueDate: z.string().optional(),
157
+ startDate: z.string().optional(),
158
+ contexts: z.array(z.string()).optional(),
159
+ tags: z.array(z.string()).optional(),
160
+ recurrence: Recurrence.optional(),
97
161
  });
98
- export type TaskCreateInput = typeof TaskCreateInput.infer;
162
+ export type TaskCreateInput = z.infer<typeof TaskCreateInput>;
99
163
 
100
164
  // 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,
165
+ export const TaskUpdateInput = z.object({
166
+ title: z.string().optional(),
167
+ description: z.string().optional(),
168
+ status: TaskStatus.optional(),
169
+ priority: Priority.optional(),
170
+ projectId: z.string().optional(),
171
+ parentId: z.string().nullable().optional(), // Parent task ID for moving task in hierarchy (null to unlink)
172
+ dependencies: z.array(Dependency).optional(),
173
+ estimate: TimeEstimate.optional(),
174
+ actualMinutes: z.number().optional(),
175
+ dueDate: z.string().optional(),
176
+ startDate: z.string().optional(),
177
+ contexts: z.array(z.string()).optional(),
178
+ tags: z.array(z.string()).optional(),
179
+ recurrence: Recurrence.optional(),
180
+ // Analysis fields
181
+ complexity: ComplexityAnalysis.optional(),
182
+ techStack: TechStackAnalysis.optional(),
115
183
  });
116
- export type TaskUpdateInput = typeof TaskUpdateInput.infer;
184
+ export type TaskUpdateInput = z.infer<typeof TaskUpdateInput>;
@@ -1,46 +1,56 @@
1
- import { type } from "arktype";
1
+ import { z } from "zod";
2
2
  import { Priority, TaskStatus } from "./task.js";
3
3
 
4
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
5
+ export const SmartViewFilter = z.object({
6
+ statuses: z.array(TaskStatus).optional(),
7
+ priorities: z.array(Priority).optional(),
8
+ contexts: z.array(z.string()).optional(),
9
+ tags: z.array(z.string()).optional(),
10
+ projectIds: z.array(z.string()).optional(),
11
+ dueBefore: z.string().optional(),
12
+ dueAfter: z.string().optional(),
13
+ isBlocked: z.boolean().optional(),
14
+ isCriticalPath: z.boolean().optional(),
15
+ hasNoDependencies: z.boolean().optional(),
16
+ search: z.string().optional(), // Search in title/description
17
17
  });
18
- export type SmartViewFilter = typeof SmartViewFilter.infer;
18
+ export type SmartViewFilter = z.infer<typeof SmartViewFilter>;
19
19
 
20
20
  // Sort options
21
- export const SortField = type(
22
- "'priority' | 'dueDate' | 'createdAt' | 'criticalPath' | 'slack' | 'title'"
23
- );
24
- export type SortField = typeof SortField.infer;
21
+ export const SortField = z.enum([
22
+ "priority",
23
+ "dueDate",
24
+ "createdAt",
25
+ "criticalPath",
26
+ "slack",
27
+ "title",
28
+ ]);
29
+ export type SortField = z.infer<typeof SortField>;
25
30
 
26
- export const SortOrder = type("'asc' | 'desc'");
27
- export type SortOrder = typeof SortOrder.infer;
31
+ export const SortOrder = z.enum(["asc", "desc"]);
32
+ export type SortOrder = z.infer<typeof SortOrder>;
28
33
 
29
34
  // Smart View definition
30
- export const SmartView = type({
31
- id: "string",
32
- name: "string",
33
- "description?": "string",
35
+ export const SmartView = z.object({
36
+ id: z.string(),
37
+ name: z.string(),
38
+ description: z.string().optional(),
34
39
  filter: SmartViewFilter,
35
- "sortBy?": SortField,
36
- "sortOrder?": SortOrder,
37
- createdAt: "string",
38
- updatedAt: "string",
40
+ sortBy: SortField.optional(),
41
+ sortOrder: SortOrder.optional(),
42
+ createdAt: z.string(),
43
+ updatedAt: z.string(),
39
44
  });
40
- export type SmartView = typeof SmartView.infer;
45
+ export type SmartView = z.infer<typeof SmartView>;
41
46
 
42
47
  // 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;
48
+ export const BuiltInView = z.enum([
49
+ "today",
50
+ "this_week",
51
+ "blocked",
52
+ "critical_path",
53
+ "quick_wins",
54
+ "all",
55
+ ]);
56
+ export type BuiltInView = z.infer<typeof BuiltInView>;
package/src/utils/date.ts CHANGED
@@ -67,10 +67,26 @@ export function parseRelativeDate(input: string): Date | null {
67
67
  return d;
68
68
  }
69
69
 
70
- // Try parsing as ISO date
70
+ // Try parsing as YYYY-MM-DD format (local timezone, no UTC shift)
71
+ const isoDateMatch = input.match(/^(\d{4})-(\d{2})-(\d{2})$/);
72
+ if (isoDateMatch) {
73
+ const [, yearStr, monthStr, dayStr] = isoDateMatch;
74
+ const year = parseInt(yearStr!, 10);
75
+ const month = parseInt(monthStr!, 10) - 1; // 0-indexed
76
+ const day = parseInt(dayStr!, 10);
77
+ const d = new Date(year, month, day);
78
+ // Validate the date is valid (e.g., not Feb 30)
79
+ if (d.getFullYear() === year && d.getMonth() === month && d.getDate() === day) {
80
+ return d;
81
+ }
82
+ }
83
+
84
+ // Try parsing other date formats (fallback)
71
85
  const parsed = new Date(input);
72
86
  if (!isNaN(parsed.getTime())) {
73
- return parsed;
87
+ // For non-YYYY-MM-DD formats, normalize to local midnight
88
+ const d = new Date(parsed.getFullYear(), parsed.getMonth(), parsed.getDate());
89
+ return d;
74
90
  }
75
91
 
76
92
  return null;