@md2do/core 0.2.2 → 0.3.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/CHANGELOG.md +31 -0
- package/coverage/coverage-final.json +9 -9
- package/coverage/index.html +14 -14
- package/coverage/lcov-report/index.html +14 -14
- package/coverage/lcov-report/src/filters/index.html +1 -1
- package/coverage/lcov-report/src/filters/index.ts.html +1 -1
- package/coverage/lcov-report/src/index.html +1 -1
- package/coverage/lcov-report/src/index.ts.html +1 -1
- package/coverage/lcov-report/src/parser/index.html +9 -9
- package/coverage/lcov-report/src/parser/index.ts.html +334 -124
- package/coverage/lcov-report/src/parser/patterns.ts.html +1 -1
- package/coverage/lcov-report/src/scanner/index.html +7 -7
- package/coverage/lcov-report/src/scanner/index.ts.html +208 -70
- package/coverage/lcov-report/src/sorting/index.html +1 -1
- package/coverage/lcov-report/src/sorting/index.ts.html +1 -1
- package/coverage/lcov-report/src/utils/dates.ts.html +21 -21
- package/coverage/lcov-report/src/utils/id.ts.html +8 -8
- package/coverage/lcov-report/src/utils/index.html +1 -1
- package/coverage/lcov-report/src/writer/index.html +1 -1
- package/coverage/lcov-report/src/writer/index.ts.html +1 -1
- package/coverage/lcov.info +479 -342
- package/coverage/src/filters/index.html +1 -1
- package/coverage/src/filters/index.ts.html +1 -1
- package/coverage/src/index.html +1 -1
- package/coverage/src/index.ts.html +1 -1
- package/coverage/src/parser/index.html +9 -9
- package/coverage/src/parser/index.ts.html +334 -124
- package/coverage/src/parser/patterns.ts.html +1 -1
- package/coverage/src/scanner/index.html +7 -7
- package/coverage/src/scanner/index.ts.html +208 -70
- package/coverage/src/sorting/index.html +1 -1
- package/coverage/src/sorting/index.ts.html +1 -1
- package/coverage/src/utils/dates.ts.html +21 -21
- package/coverage/src/utils/id.ts.html +8 -8
- package/coverage/src/utils/index.html +1 -1
- package/coverage/src/writer/index.html +1 -1
- package/coverage/src/writer/index.ts.html +1 -1
- package/dist/index.d.mts +64 -3
- package/dist/index.d.ts +64 -3
- package/dist/index.js +154 -0
- package/dist/index.mjs +152 -0
- package/package.json +1 -1
- package/src/index.ts +3 -0
- package/src/parser/index.ts +103 -0
- package/src/scanner/index.ts +54 -0
- package/src/types/index.ts +31 -2
- package/src/warnings/filter.ts +93 -0
- package/tests/parser/index.test.ts +107 -1
- package/tests/scanner/index.test.ts +94 -4
package/src/parser/index.ts
CHANGED
|
@@ -126,9 +126,14 @@ export function extractDueDate(
|
|
|
126
126
|
return {
|
|
127
127
|
date: undefined,
|
|
128
128
|
warning: {
|
|
129
|
+
severity: 'warning',
|
|
130
|
+
source: 'md2do',
|
|
131
|
+
ruleId: 'relative-date-no-context',
|
|
129
132
|
file: '', // Will be filled in by caller
|
|
130
133
|
line: 0, // Will be filled in by caller
|
|
131
134
|
text: text.trim(),
|
|
135
|
+
message:
|
|
136
|
+
'Relative due date without context date from heading. Add a heading with a date above this task.',
|
|
132
137
|
reason:
|
|
133
138
|
'Relative due date without context date from heading. Add a heading with a date above this task.',
|
|
134
139
|
},
|
|
@@ -208,6 +213,71 @@ export function parseTask(
|
|
|
208
213
|
): { task: Task | null; warnings: Warning[] } {
|
|
209
214
|
const warnings: Warning[] = [];
|
|
210
215
|
|
|
216
|
+
// Detect malformed checkboxes and provide helpful warnings
|
|
217
|
+
// Check for asterisk/plus bullet markers (not supported)
|
|
218
|
+
if (/^\s*[*+]\s+\[[ xX]\]/.test(line)) {
|
|
219
|
+
warnings.push({
|
|
220
|
+
severity: 'warning',
|
|
221
|
+
source: 'md2do',
|
|
222
|
+
ruleId: 'unsupported-bullet',
|
|
223
|
+
file,
|
|
224
|
+
line: lineNumber,
|
|
225
|
+
text: line.trim(),
|
|
226
|
+
message:
|
|
227
|
+
'Unsupported bullet marker (* or +). Use dash (-) for task lists.',
|
|
228
|
+
reason:
|
|
229
|
+
'Unsupported bullet marker (* or +). Use dash (-) for task lists.',
|
|
230
|
+
});
|
|
231
|
+
return { task: null, warnings };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Check for extra spaces inside checkbox: [x ] or [ x]
|
|
235
|
+
if (/^\s*-\s+\[[xX]\s+\]/.test(line) || /^\s*-\s+\[\s+[xX]\]/.test(line)) {
|
|
236
|
+
warnings.push({
|
|
237
|
+
severity: 'warning',
|
|
238
|
+
source: 'md2do',
|
|
239
|
+
ruleId: 'malformed-checkbox',
|
|
240
|
+
file,
|
|
241
|
+
line: lineNumber,
|
|
242
|
+
text: line.trim(),
|
|
243
|
+
message:
|
|
244
|
+
'Malformed checkbox with extra spaces. Use [x] or [ ] without extra spaces.',
|
|
245
|
+
reason:
|
|
246
|
+
'Malformed checkbox with extra spaces. Use [x] or [ ] without extra spaces.',
|
|
247
|
+
});
|
|
248
|
+
return { task: null, warnings };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Check for missing space after checkbox: [x]Task
|
|
252
|
+
if (/^\s*-\s+\[[ xX]\][^\s]/.test(line)) {
|
|
253
|
+
warnings.push({
|
|
254
|
+
severity: 'warning',
|
|
255
|
+
source: 'md2do',
|
|
256
|
+
ruleId: 'missing-space-after',
|
|
257
|
+
file,
|
|
258
|
+
line: lineNumber,
|
|
259
|
+
text: line.trim(),
|
|
260
|
+
message: 'Missing space after checkbox. Use "- [x] Task" format.',
|
|
261
|
+
reason: 'Missing space after checkbox. Use "- [x] Task" format.',
|
|
262
|
+
});
|
|
263
|
+
return { task: null, warnings };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Check for missing space before checkbox: -[x]
|
|
267
|
+
if (/^\s*-\[[ xX]\]/.test(line)) {
|
|
268
|
+
warnings.push({
|
|
269
|
+
severity: 'warning',
|
|
270
|
+
source: 'md2do',
|
|
271
|
+
ruleId: 'missing-space-before',
|
|
272
|
+
file,
|
|
273
|
+
line: lineNumber,
|
|
274
|
+
text: line.trim(),
|
|
275
|
+
message: 'Missing space before checkbox. Use "- [x] Task" format.',
|
|
276
|
+
reason: 'Missing space before checkbox. Use "- [x] Task" format.',
|
|
277
|
+
});
|
|
278
|
+
return { task: null, warnings };
|
|
279
|
+
}
|
|
280
|
+
|
|
211
281
|
// Check if line is a task
|
|
212
282
|
const taskMatch = line.match(PATTERNS.TASK_CHECKBOX);
|
|
213
283
|
if (!taskMatch?.[0] || !taskMatch[2]) {
|
|
@@ -237,6 +307,39 @@ export function parseTask(
|
|
|
237
307
|
});
|
|
238
308
|
}
|
|
239
309
|
|
|
310
|
+
// Warn about missing dates
|
|
311
|
+
// Incomplete tasks without any date (no explicit due date and no context date)
|
|
312
|
+
if (!completed && !dueDateResult.date && !context.currentDate) {
|
|
313
|
+
warnings.push({
|
|
314
|
+
severity: 'info',
|
|
315
|
+
source: 'md2do',
|
|
316
|
+
ruleId: 'missing-due-date',
|
|
317
|
+
file,
|
|
318
|
+
line: lineNumber,
|
|
319
|
+
text: fullText.trim(),
|
|
320
|
+
message:
|
|
321
|
+
'Task has no due date. Add [due: YYYY-MM-DD] or place under a heading with a date.',
|
|
322
|
+
reason:
|
|
323
|
+
'Task has no due date. Add [due: YYYY-MM-DD] or place under a heading with a date.',
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Completed tasks should have completion dates
|
|
328
|
+
if (completed && !completedDate) {
|
|
329
|
+
warnings.push({
|
|
330
|
+
severity: 'info',
|
|
331
|
+
source: 'md2do',
|
|
332
|
+
ruleId: 'missing-completed-date',
|
|
333
|
+
file,
|
|
334
|
+
line: lineNumber,
|
|
335
|
+
text: fullText.trim(),
|
|
336
|
+
message:
|
|
337
|
+
'Completed task missing completion date. Add [completed: YYYY-MM-DD].',
|
|
338
|
+
reason:
|
|
339
|
+
'Completed task missing completion date. Add [completed: YYYY-MM-DD].',
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
240
343
|
// Clean text (remove metadata markers)
|
|
241
344
|
const cleanText = cleanTaskText(fullText);
|
|
242
345
|
|
package/src/scanner/index.ts
CHANGED
|
@@ -74,6 +74,9 @@ export class MarkdownScanner {
|
|
|
74
74
|
const tasks: Task[] = [];
|
|
75
75
|
const warnings: Warning[] = [];
|
|
76
76
|
|
|
77
|
+
// Track Todoist IDs to detect duplicates
|
|
78
|
+
const todoistIds = new Map<string, { file: string; line: number }>();
|
|
79
|
+
|
|
77
80
|
// Initialize context from file path
|
|
78
81
|
const context: ParsingContext = {};
|
|
79
82
|
|
|
@@ -105,6 +108,28 @@ export class MarkdownScanner {
|
|
|
105
108
|
|
|
106
109
|
if (result.task) {
|
|
107
110
|
tasks.push(result.task);
|
|
111
|
+
|
|
112
|
+
// Check for duplicate Todoist IDs
|
|
113
|
+
if (result.task.todoistId) {
|
|
114
|
+
const existing = todoistIds.get(result.task.todoistId);
|
|
115
|
+
if (existing) {
|
|
116
|
+
warnings.push({
|
|
117
|
+
severity: 'error',
|
|
118
|
+
source: 'md2do',
|
|
119
|
+
ruleId: 'duplicate-todoist-id',
|
|
120
|
+
file: filePath,
|
|
121
|
+
line: lineNumber,
|
|
122
|
+
text: result.task.text,
|
|
123
|
+
message: `Duplicate Todoist ID [todoist:${result.task.todoistId}]. Also found at ${existing.file}:${existing.line}.`,
|
|
124
|
+
reason: `Duplicate Todoist ID [todoist:${result.task.todoistId}]. Also found at ${existing.file}:${existing.line}.`,
|
|
125
|
+
});
|
|
126
|
+
} else {
|
|
127
|
+
todoistIds.set(result.task.todoistId, {
|
|
128
|
+
file: filePath,
|
|
129
|
+
line: lineNumber,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
108
133
|
}
|
|
109
134
|
|
|
110
135
|
if (result.warnings.length > 0) {
|
|
@@ -131,10 +156,39 @@ export class MarkdownScanner {
|
|
|
131
156
|
const allTasks: Task[] = [];
|
|
132
157
|
const allWarnings: Warning[] = [];
|
|
133
158
|
|
|
159
|
+
// Track Todoist IDs across all files
|
|
160
|
+
const todoistIds = new Map<string, { file: string; line: number }>();
|
|
161
|
+
|
|
134
162
|
for (const file of files) {
|
|
135
163
|
const result = this.scanFile(file.path, file.content);
|
|
136
164
|
allTasks.push(...result.tasks);
|
|
137
165
|
allWarnings.push(...result.warnings);
|
|
166
|
+
|
|
167
|
+
// Check for duplicate Todoist IDs across files
|
|
168
|
+
for (const task of result.tasks) {
|
|
169
|
+
if (task.todoistId) {
|
|
170
|
+
const existing = todoistIds.get(task.todoistId);
|
|
171
|
+
if (existing && existing.file !== task.file) {
|
|
172
|
+
// Only add warning if duplicate is in a different file
|
|
173
|
+
// (same-file duplicates are already caught by scanFile)
|
|
174
|
+
allWarnings.push({
|
|
175
|
+
severity: 'error',
|
|
176
|
+
source: 'md2do',
|
|
177
|
+
ruleId: 'duplicate-todoist-id',
|
|
178
|
+
file: task.file,
|
|
179
|
+
line: task.line,
|
|
180
|
+
text: task.text,
|
|
181
|
+
message: `Duplicate Todoist ID [todoist:${task.todoistId}] across files. Also found at ${existing.file}:${existing.line}.`,
|
|
182
|
+
reason: `Duplicate Todoist ID [todoist:${task.todoistId}] across files. Also found at ${existing.file}:${existing.line}.`,
|
|
183
|
+
});
|
|
184
|
+
} else if (!existing) {
|
|
185
|
+
todoistIds.set(task.todoistId, {
|
|
186
|
+
file: task.file,
|
|
187
|
+
line: task.line,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
138
192
|
}
|
|
139
193
|
|
|
140
194
|
return {
|
package/src/types/index.ts
CHANGED
|
@@ -65,9 +65,38 @@ export interface ScanResult {
|
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
export type WarningSeverity = 'info' | 'warning' | 'error';
|
|
69
|
+
|
|
70
|
+
export type WarningCode =
|
|
71
|
+
| 'unsupported-bullet' // * or + instead of -
|
|
72
|
+
| 'malformed-checkbox' // [x ] or [ x]
|
|
73
|
+
| 'missing-space-after' // -[x]Task
|
|
74
|
+
| 'missing-space-before' // -[x] Task
|
|
75
|
+
| 'relative-date-no-context' // [due: tomorrow] without heading date
|
|
76
|
+
| 'missing-due-date' // Incomplete task with no due date
|
|
77
|
+
| 'missing-completed-date' // [x] without [completed: date]
|
|
78
|
+
| 'duplicate-todoist-id' // Same Todoist ID in multiple tasks
|
|
79
|
+
| 'file-read-error'; // Failed to read file
|
|
80
|
+
|
|
68
81
|
export interface Warning {
|
|
82
|
+
// Position
|
|
69
83
|
file: string;
|
|
70
84
|
line: number;
|
|
71
|
-
|
|
72
|
-
|
|
85
|
+
column?: number;
|
|
86
|
+
|
|
87
|
+
// Classification
|
|
88
|
+
severity: WarningSeverity;
|
|
89
|
+
source: 'md2do';
|
|
90
|
+
ruleId: WarningCode;
|
|
91
|
+
|
|
92
|
+
// Content
|
|
93
|
+
message: string; // User-facing message
|
|
94
|
+
text?: string; // The actual text that triggered it
|
|
95
|
+
|
|
96
|
+
// Documentation (optional - for future use)
|
|
97
|
+
url?: string;
|
|
98
|
+
|
|
99
|
+
// Legacy field for backward compatibility (deprecated)
|
|
100
|
+
/** @deprecated Use message instead */
|
|
101
|
+
reason?: string;
|
|
73
102
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { Warning } from '../types/index.js';
|
|
2
|
+
|
|
3
|
+
export interface WarningFilterConfig {
|
|
4
|
+
enabled?: boolean | undefined;
|
|
5
|
+
rules?: Record<string, 'error' | 'warn' | 'info' | 'off'> | undefined;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Filter warnings based on configuration rules
|
|
10
|
+
*
|
|
11
|
+
* This function applies warning configuration rules to filter out disabled warnings
|
|
12
|
+
* and optionally all warnings if globally disabled.
|
|
13
|
+
*
|
|
14
|
+
* @param warnings - Array of warnings to filter
|
|
15
|
+
* @param config - Warning configuration with enabled flag and rule overrides
|
|
16
|
+
* @returns Filtered array of warnings
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const config = {
|
|
21
|
+
* enabled: true,
|
|
22
|
+
* rules: {
|
|
23
|
+
* 'missing-due-date': 'off',
|
|
24
|
+
* 'duplicate-todoist-id': 'error',
|
|
25
|
+
* },
|
|
26
|
+
* };
|
|
27
|
+
*
|
|
28
|
+
* const filtered = filterWarnings(allWarnings, config);
|
|
29
|
+
* // Returns warnings except those with ruleId 'missing-due-date'
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function filterWarnings(
|
|
33
|
+
warnings: Warning[],
|
|
34
|
+
config: WarningFilterConfig = {},
|
|
35
|
+
): Warning[] {
|
|
36
|
+
// If warnings are globally disabled, return empty array
|
|
37
|
+
if (config.enabled === false) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// If no rules specified, return all warnings
|
|
42
|
+
if (!config.rules) {
|
|
43
|
+
return warnings;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Filter based on rule configuration
|
|
47
|
+
return warnings.filter((warning) => {
|
|
48
|
+
const level = config.rules?.[warning.ruleId];
|
|
49
|
+
|
|
50
|
+
// If rule is not configured, include the warning (default: show)
|
|
51
|
+
if (level === undefined) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Exclude if rule is set to 'off'
|
|
56
|
+
return level !== 'off';
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Group warnings by severity
|
|
62
|
+
*
|
|
63
|
+
* Useful for displaying warnings in order of importance or
|
|
64
|
+
* treating errors differently from warnings.
|
|
65
|
+
*
|
|
66
|
+
* @param warnings - Array of warnings to group
|
|
67
|
+
* @returns Object with warnings grouped by severity level
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```typescript
|
|
71
|
+
* const grouped = groupWarningsBySeverity(warnings);
|
|
72
|
+
* console.log(`${grouped.error.length} errors`);
|
|
73
|
+
* console.log(`${grouped.warning.length} warnings`);
|
|
74
|
+
* console.log(`${grouped.info.length} info messages`);
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export function groupWarningsBySeverity(warnings: Warning[]): {
|
|
78
|
+
error: Warning[];
|
|
79
|
+
warning: Warning[];
|
|
80
|
+
info: Warning[];
|
|
81
|
+
} {
|
|
82
|
+
return warnings.reduce(
|
|
83
|
+
(acc, warning) => {
|
|
84
|
+
acc[warning.severity].push(warning);
|
|
85
|
+
return acc;
|
|
86
|
+
},
|
|
87
|
+
{ error: [], warning: [], info: [] } as {
|
|
88
|
+
error: Warning[];
|
|
89
|
+
warning: Warning[];
|
|
90
|
+
info: Warning[];
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -419,7 +419,7 @@ describe('parseTask', () => {
|
|
|
419
419
|
const result = parseTask('- [ ] Task [due: tomorrow]', 1, file, {});
|
|
420
420
|
|
|
421
421
|
expect(result.task?.dueDate).toBeUndefined();
|
|
422
|
-
expect(result.warnings).toHaveLength(
|
|
422
|
+
expect(result.warnings).toHaveLength(2); // Relative date + missing date warnings
|
|
423
423
|
expect(result.warnings[0]?.reason).toContain(
|
|
424
424
|
'Relative due date without context',
|
|
425
425
|
);
|
|
@@ -535,4 +535,110 @@ describe('parseTask', () => {
|
|
|
535
535
|
expect(result.task?.completedDate).toBeInstanceOf(Date);
|
|
536
536
|
});
|
|
537
537
|
});
|
|
538
|
+
|
|
539
|
+
describe('Malformed checkbox warnings', () => {
|
|
540
|
+
it('should warn on asterisk bullet marker', () => {
|
|
541
|
+
const result = parseTask('* [x] Task with asterisk', 1, file, {});
|
|
542
|
+
expect(result.task).toBeNull();
|
|
543
|
+
expect(result.warnings).toHaveLength(1);
|
|
544
|
+
expect(result.warnings[0]?.reason).toContain(
|
|
545
|
+
'Unsupported bullet marker (* or +)',
|
|
546
|
+
);
|
|
547
|
+
expect(result.warnings[0]?.file).toBe(file);
|
|
548
|
+
expect(result.warnings[0]?.line).toBe(1);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it('should warn on plus bullet marker', () => {
|
|
552
|
+
const result = parseTask('+ [ ] Task with plus', 1, file, {});
|
|
553
|
+
expect(result.task).toBeNull();
|
|
554
|
+
expect(result.warnings).toHaveLength(1);
|
|
555
|
+
expect(result.warnings[0]?.reason).toContain(
|
|
556
|
+
'Unsupported bullet marker (* or +)',
|
|
557
|
+
);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it('should warn on extra space after x', () => {
|
|
561
|
+
const result = parseTask('- [x ] Task with extra space', 1, file, {});
|
|
562
|
+
expect(result.task).toBeNull();
|
|
563
|
+
expect(result.warnings).toHaveLength(1);
|
|
564
|
+
expect(result.warnings[0]?.reason).toContain('Malformed checkbox');
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it('should warn on extra space before x', () => {
|
|
568
|
+
const result = parseTask('- [ x] Task with extra space', 1, file, {});
|
|
569
|
+
expect(result.task).toBeNull();
|
|
570
|
+
expect(result.warnings).toHaveLength(1);
|
|
571
|
+
expect(result.warnings[0]?.reason).toContain('Malformed checkbox');
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it('should warn on missing space before checkbox', () => {
|
|
575
|
+
const result = parseTask('-[x] Task without space', 1, file, {});
|
|
576
|
+
expect(result.task).toBeNull();
|
|
577
|
+
expect(result.warnings).toHaveLength(1);
|
|
578
|
+
expect(result.warnings[0]?.reason).toContain(
|
|
579
|
+
'Missing space before checkbox',
|
|
580
|
+
);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it('should warn on missing space after checkbox', () => {
|
|
584
|
+
const result = parseTask('- [x]Task without space', 1, file, {});
|
|
585
|
+
expect(result.task).toBeNull();
|
|
586
|
+
expect(result.warnings).toHaveLength(1);
|
|
587
|
+
expect(result.warnings[0]?.reason).toContain(
|
|
588
|
+
'Missing space after checkbox',
|
|
589
|
+
);
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
describe('Missing date warnings', () => {
|
|
594
|
+
it('should warn on incomplete task without date or context', () => {
|
|
595
|
+
const result = parseTask('- [ ] Task without date', 1, file, {});
|
|
596
|
+
expect(result.task).not.toBeNull();
|
|
597
|
+
expect(result.warnings).toHaveLength(1);
|
|
598
|
+
expect(result.warnings[0]?.reason).toContain('Task has no due date');
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it('should not warn on task with explicit due date', () => {
|
|
602
|
+
const result = parseTask('- [ ] Task [due: 2026-01-30]', 1, file, {});
|
|
603
|
+
expect(result.task).not.toBeNull();
|
|
604
|
+
expect(result.warnings).toHaveLength(0);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it('should not warn on task with context date', () => {
|
|
608
|
+
const context: ParsingContext = {
|
|
609
|
+
currentDate: new Date('2026-01-30'),
|
|
610
|
+
};
|
|
611
|
+
const result = parseTask('- [ ] Task with context', 1, file, context);
|
|
612
|
+
expect(result.task).not.toBeNull();
|
|
613
|
+
expect(result.warnings).toHaveLength(0);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
it('should not warn on completed tasks without date', () => {
|
|
617
|
+
const result = parseTask('- [x] Completed task', 1, file, {});
|
|
618
|
+
expect(result.task).not.toBeNull();
|
|
619
|
+
// Should only warn about missing completion date, not due date
|
|
620
|
+
expect(result.warnings).toHaveLength(1);
|
|
621
|
+
expect(result.warnings[0]?.reason).toContain('completion date');
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it('should warn on completed task without completion date', () => {
|
|
625
|
+
const result = parseTask('- [x] Completed task', 1, file, {});
|
|
626
|
+
expect(result.task).not.toBeNull();
|
|
627
|
+
expect(result.warnings).toHaveLength(1);
|
|
628
|
+
expect(result.warnings[0]?.reason).toContain(
|
|
629
|
+
'Completed task missing completion date',
|
|
630
|
+
);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it('should not warn on completed task with completion date', () => {
|
|
634
|
+
const result = parseTask(
|
|
635
|
+
'- [x] Task [completed: 2026-01-25]',
|
|
636
|
+
1,
|
|
637
|
+
file,
|
|
638
|
+
{},
|
|
639
|
+
);
|
|
640
|
+
expect(result.task).not.toBeNull();
|
|
641
|
+
expect(result.warnings).toHaveLength(0);
|
|
642
|
+
});
|
|
643
|
+
});
|
|
538
644
|
});
|
|
@@ -65,7 +65,7 @@ describe('MarkdownScanner', () => {
|
|
|
65
65
|
|
|
66
66
|
describe('Basic task scanning', () => {
|
|
67
67
|
it('should scan simple unchecked task', () => {
|
|
68
|
-
const content = '- [ ] Simple task';
|
|
68
|
+
const content = '- [ ] Simple task [due: 2026-01-30]';
|
|
69
69
|
const result = scanner.scanFile('test.md', content);
|
|
70
70
|
|
|
71
71
|
expect(result.tasks).toHaveLength(1);
|
|
@@ -281,7 +281,7 @@ No tasks at all.
|
|
|
281
281
|
const result = scanner.scanFile('test.md', content);
|
|
282
282
|
|
|
283
283
|
expect(result.tasks[0]?.dueDate).toBeUndefined();
|
|
284
|
-
expect(result.warnings).toHaveLength(
|
|
284
|
+
expect(result.warnings).toHaveLength(2); // Relative date + missing date warnings
|
|
285
285
|
expect(result.warnings[0]?.reason).toContain(
|
|
286
286
|
'Relative due date without context',
|
|
287
287
|
);
|
|
@@ -461,9 +461,9 @@ Line 4
|
|
|
461
461
|
|
|
462
462
|
const result = scanner.scanFiles(files);
|
|
463
463
|
|
|
464
|
-
expect(result.warnings).toHaveLength(
|
|
464
|
+
expect(result.warnings).toHaveLength(4); // 2 relative date + 2 missing date warnings
|
|
465
465
|
expect(result.warnings[0]?.file).toBe('file1.md');
|
|
466
|
-
expect(result.warnings[
|
|
466
|
+
expect(result.warnings[2]?.file).toBe('file2.md');
|
|
467
467
|
});
|
|
468
468
|
|
|
469
469
|
it('should handle empty file array', () => {
|
|
@@ -558,4 +558,94 @@ Line 4
|
|
|
558
558
|
expect(result.tasks).toHaveLength(0);
|
|
559
559
|
});
|
|
560
560
|
});
|
|
561
|
+
|
|
562
|
+
describe('Duplicate Todoist ID warnings', () => {
|
|
563
|
+
it('should warn on duplicate Todoist ID within same file', () => {
|
|
564
|
+
const content = `
|
|
565
|
+
- [ ] First task [todoist:123456] [due: 2026-01-30]
|
|
566
|
+
- [ ] Second task [todoist:123456] [due: 2026-01-31]
|
|
567
|
+
`.trim();
|
|
568
|
+
const result = scanner.scanFile('test.md', content);
|
|
569
|
+
|
|
570
|
+
expect(result.tasks).toHaveLength(2);
|
|
571
|
+
expect(result.warnings).toHaveLength(1);
|
|
572
|
+
expect(result.warnings[0]?.reason).toContain('Duplicate Todoist ID');
|
|
573
|
+
expect(result.warnings[0]?.reason).toContain('123456');
|
|
574
|
+
expect(result.warnings[0]?.line).toBe(2);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it('should not warn on unique Todoist IDs', () => {
|
|
578
|
+
const content = `
|
|
579
|
+
- [ ] First task [todoist:111111] [due: 2026-01-30]
|
|
580
|
+
- [ ] Second task [todoist:222222] [due: 2026-01-31]
|
|
581
|
+
- [ ] Third task [todoist:333333] [due: 2026-02-01]
|
|
582
|
+
`.trim();
|
|
583
|
+
const result = scanner.scanFile('test.md', content);
|
|
584
|
+
|
|
585
|
+
expect(result.tasks).toHaveLength(3);
|
|
586
|
+
expect(result.warnings).toHaveLength(0);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it('should warn on multiple duplicates', () => {
|
|
590
|
+
const content = `
|
|
591
|
+
- [ ] Task A [todoist:111] [due: 2026-01-30]
|
|
592
|
+
- [ ] Task B [todoist:222] [due: 2026-01-31]
|
|
593
|
+
- [ ] Task C [todoist:111] [due: 2026-02-01]
|
|
594
|
+
- [ ] Task D [todoist:222] [due: 2026-02-02]
|
|
595
|
+
- [ ] Task E [todoist:111] [due: 2026-02-03]
|
|
596
|
+
`.trim();
|
|
597
|
+
const result = scanner.scanFile('test.md', content);
|
|
598
|
+
|
|
599
|
+
expect(result.tasks).toHaveLength(5);
|
|
600
|
+
// Should have 3 warnings: C duplicates A, D duplicates B, E duplicates A
|
|
601
|
+
expect(result.warnings).toHaveLength(3);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it('should track duplicates across multiple files', () => {
|
|
605
|
+
const files = [
|
|
606
|
+
{
|
|
607
|
+
path: 'file1.md',
|
|
608
|
+
content: '- [ ] Task in file 1 [todoist:123456] [due: 2026-01-30]',
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
path: 'file2.md',
|
|
612
|
+
content: '- [ ] Task in file 2 [todoist:123456] [due: 2026-01-31]',
|
|
613
|
+
},
|
|
614
|
+
];
|
|
615
|
+
|
|
616
|
+
const result = scanner.scanFiles(files);
|
|
617
|
+
|
|
618
|
+
expect(result.tasks).toHaveLength(2);
|
|
619
|
+
expect(result.warnings).toHaveLength(1);
|
|
620
|
+
expect(result.warnings[0]?.reason).toContain('Duplicate Todoist ID');
|
|
621
|
+
expect(result.warnings[0]?.reason).toContain('across files');
|
|
622
|
+
expect(result.warnings[0]?.file).toBe('file2.md');
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it('should not warn when same file scanned separately', () => {
|
|
626
|
+
const content = '- [ ] Task [todoist:123456] [due: 2026-01-30]';
|
|
627
|
+
|
|
628
|
+
// Scan same file multiple times as if they were different files
|
|
629
|
+
const result1 = scanner.scanFile('test1.md', content);
|
|
630
|
+
const result2 = scanner.scanFile('test2.md', content);
|
|
631
|
+
|
|
632
|
+
// Each scan should have no warnings (they're separate scans)
|
|
633
|
+
expect(result1.warnings).toHaveLength(0);
|
|
634
|
+
expect(result2.warnings).toHaveLength(0);
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it('should include location information in warning', () => {
|
|
638
|
+
const content = `
|
|
639
|
+
- [ ] First task [todoist:999] [due: 2026-01-30]
|
|
640
|
+
- [ ] Second task [due: 2026-01-31]
|
|
641
|
+
- [ ] Third task [todoist:999] [due: 2026-02-01]
|
|
642
|
+
`.trim();
|
|
643
|
+
const result = scanner.scanFile('backlog.md', content);
|
|
644
|
+
|
|
645
|
+
expect(result.warnings).toHaveLength(1);
|
|
646
|
+
expect(result.warnings[0]?.file).toBe('backlog.md');
|
|
647
|
+
expect(result.warnings[0]?.line).toBe(3);
|
|
648
|
+
expect(result.warnings[0]?.reason).toContain('backlog.md:1');
|
|
649
|
+
});
|
|
650
|
+
});
|
|
561
651
|
});
|