@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.
Files changed (49) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/coverage/coverage-final.json +9 -9
  3. package/coverage/index.html +14 -14
  4. package/coverage/lcov-report/index.html +14 -14
  5. package/coverage/lcov-report/src/filters/index.html +1 -1
  6. package/coverage/lcov-report/src/filters/index.ts.html +1 -1
  7. package/coverage/lcov-report/src/index.html +1 -1
  8. package/coverage/lcov-report/src/index.ts.html +1 -1
  9. package/coverage/lcov-report/src/parser/index.html +9 -9
  10. package/coverage/lcov-report/src/parser/index.ts.html +334 -124
  11. package/coverage/lcov-report/src/parser/patterns.ts.html +1 -1
  12. package/coverage/lcov-report/src/scanner/index.html +7 -7
  13. package/coverage/lcov-report/src/scanner/index.ts.html +208 -70
  14. package/coverage/lcov-report/src/sorting/index.html +1 -1
  15. package/coverage/lcov-report/src/sorting/index.ts.html +1 -1
  16. package/coverage/lcov-report/src/utils/dates.ts.html +21 -21
  17. package/coverage/lcov-report/src/utils/id.ts.html +8 -8
  18. package/coverage/lcov-report/src/utils/index.html +1 -1
  19. package/coverage/lcov-report/src/writer/index.html +1 -1
  20. package/coverage/lcov-report/src/writer/index.ts.html +1 -1
  21. package/coverage/lcov.info +479 -342
  22. package/coverage/src/filters/index.html +1 -1
  23. package/coverage/src/filters/index.ts.html +1 -1
  24. package/coverage/src/index.html +1 -1
  25. package/coverage/src/index.ts.html +1 -1
  26. package/coverage/src/parser/index.html +9 -9
  27. package/coverage/src/parser/index.ts.html +334 -124
  28. package/coverage/src/parser/patterns.ts.html +1 -1
  29. package/coverage/src/scanner/index.html +7 -7
  30. package/coverage/src/scanner/index.ts.html +208 -70
  31. package/coverage/src/sorting/index.html +1 -1
  32. package/coverage/src/sorting/index.ts.html +1 -1
  33. package/coverage/src/utils/dates.ts.html +21 -21
  34. package/coverage/src/utils/id.ts.html +8 -8
  35. package/coverage/src/utils/index.html +1 -1
  36. package/coverage/src/writer/index.html +1 -1
  37. package/coverage/src/writer/index.ts.html +1 -1
  38. package/dist/index.d.mts +64 -3
  39. package/dist/index.d.ts +64 -3
  40. package/dist/index.js +154 -0
  41. package/dist/index.mjs +152 -0
  42. package/package.json +1 -1
  43. package/src/index.ts +3 -0
  44. package/src/parser/index.ts +103 -0
  45. package/src/scanner/index.ts +54 -0
  46. package/src/types/index.ts +31 -2
  47. package/src/warnings/filter.ts +93 -0
  48. package/tests/parser/index.test.ts +107 -1
  49. package/tests/scanner/index.test.ts +94 -4
package/dist/index.d.mts CHANGED
@@ -47,11 +47,20 @@ interface ScanResult {
47
47
  parseErrors: number;
48
48
  };
49
49
  }
50
+ type WarningSeverity = 'info' | 'warning' | 'error';
51
+ type WarningCode = 'unsupported-bullet' | 'malformed-checkbox' | 'missing-space-after' | 'missing-space-before' | 'relative-date-no-context' | 'missing-due-date' | 'missing-completed-date' | 'duplicate-todoist-id' | 'file-read-error';
50
52
  interface Warning {
51
53
  file: string;
52
54
  line: number;
53
- text: string;
54
- reason: string;
55
+ column?: number;
56
+ severity: WarningSeverity;
57
+ source: 'md2do';
58
+ ruleId: WarningCode;
59
+ message: string;
60
+ text?: string;
61
+ url?: string;
62
+ /** @deprecated Use message instead */
63
+ reason?: string;
55
64
  }
56
65
 
57
66
  /**
@@ -812,6 +821,58 @@ declare namespace index {
812
821
  export { type index_TaskComparator as TaskComparator, index_byAssignee as byAssignee, index_byCompletionStatus as byCompletionStatus, index_byCreatedDate as byCreatedDate, index_byDueDate as byDueDate, index_byFile as byFile, index_byPerson as byPerson, index_byPriority as byPriority, index_byProject as byProject, index_combineComparators as combineComparators, index_reverse as reverse };
813
822
  }
814
823
 
824
+ interface WarningFilterConfig {
825
+ enabled?: boolean | undefined;
826
+ rules?: Record<string, 'error' | 'warn' | 'info' | 'off'> | undefined;
827
+ }
828
+ /**
829
+ * Filter warnings based on configuration rules
830
+ *
831
+ * This function applies warning configuration rules to filter out disabled warnings
832
+ * and optionally all warnings if globally disabled.
833
+ *
834
+ * @param warnings - Array of warnings to filter
835
+ * @param config - Warning configuration with enabled flag and rule overrides
836
+ * @returns Filtered array of warnings
837
+ *
838
+ * @example
839
+ * ```typescript
840
+ * const config = {
841
+ * enabled: true,
842
+ * rules: {
843
+ * 'missing-due-date': 'off',
844
+ * 'duplicate-todoist-id': 'error',
845
+ * },
846
+ * };
847
+ *
848
+ * const filtered = filterWarnings(allWarnings, config);
849
+ * // Returns warnings except those with ruleId 'missing-due-date'
850
+ * ```
851
+ */
852
+ declare function filterWarnings(warnings: Warning[], config?: WarningFilterConfig): Warning[];
853
+ /**
854
+ * Group warnings by severity
855
+ *
856
+ * Useful for displaying warnings in order of importance or
857
+ * treating errors differently from warnings.
858
+ *
859
+ * @param warnings - Array of warnings to group
860
+ * @returns Object with warnings grouped by severity level
861
+ *
862
+ * @example
863
+ * ```typescript
864
+ * const grouped = groupWarningsBySeverity(warnings);
865
+ * console.log(`${grouped.error.length} errors`);
866
+ * console.log(`${grouped.warning.length} warnings`);
867
+ * console.log(`${grouped.info.length} info messages`);
868
+ * ```
869
+ */
870
+ declare function groupWarningsBySeverity(warnings: Warning[]): {
871
+ error: Warning[];
872
+ warning: Warning[];
873
+ info: Warning[];
874
+ };
875
+
815
876
  /**
816
877
  * Parse an absolute date string in various formats
817
878
  *
@@ -876,4 +937,4 @@ declare function extractDateFromHeading(line: string): Date | null;
876
937
  */
877
938
  declare function generateTaskId(file: string, line: number, text: string): string;
878
939
 
879
- export { ASSIGNEE, COMPLETED_DATE, DUE_DATE_ABSOLUTE, DUE_DATE_RELATIVE, DUE_DATE_SHORT, HEADING_DATE_ISO, HEADING_DATE_NATURAL, HEADING_DATE_SLASH, MarkdownScanner, PATTERNS, PRIORITY_HIGH, PRIORITY_NORMAL, PRIORITY_URGENT, type ParsingContext, type Priority, type ScanResult, TAG, TASK_CHECKBOX, TODOIST_ID, type Task, type TaskFilterCriteria, type UpdateTaskOptions, type Warning, type WriteTaskResult, addTask, cleanTaskText, extractAssignee, extractCompletedDate, extractDateFromHeading, extractDueDate, extractPersonFromFilename, extractPriority, extractProjectFromPath, extractTags, extractTodoistId, index$1 as filters, generateTaskId, parseAbsoluteDate, parseTask, resolveRelativeDate, index as sorting, updateTask, updateTasks };
940
+ export { ASSIGNEE, COMPLETED_DATE, DUE_DATE_ABSOLUTE, DUE_DATE_RELATIVE, DUE_DATE_SHORT, HEADING_DATE_ISO, HEADING_DATE_NATURAL, HEADING_DATE_SLASH, MarkdownScanner, PATTERNS, PRIORITY_HIGH, PRIORITY_NORMAL, PRIORITY_URGENT, type ParsingContext, type Priority, type ScanResult, TAG, TASK_CHECKBOX, TODOIST_ID, type Task, type TaskFilterCriteria, type UpdateTaskOptions, type Warning, type WarningCode, type WarningFilterConfig, type WarningSeverity, type WriteTaskResult, addTask, cleanTaskText, extractAssignee, extractCompletedDate, extractDateFromHeading, extractDueDate, extractPersonFromFilename, extractPriority, extractProjectFromPath, extractTags, extractTodoistId, filterWarnings, index$1 as filters, generateTaskId, groupWarningsBySeverity, parseAbsoluteDate, parseTask, resolveRelativeDate, index as sorting, updateTask, updateTasks };
package/dist/index.d.ts CHANGED
@@ -47,11 +47,20 @@ interface ScanResult {
47
47
  parseErrors: number;
48
48
  };
49
49
  }
50
+ type WarningSeverity = 'info' | 'warning' | 'error';
51
+ type WarningCode = 'unsupported-bullet' | 'malformed-checkbox' | 'missing-space-after' | 'missing-space-before' | 'relative-date-no-context' | 'missing-due-date' | 'missing-completed-date' | 'duplicate-todoist-id' | 'file-read-error';
50
52
  interface Warning {
51
53
  file: string;
52
54
  line: number;
53
- text: string;
54
- reason: string;
55
+ column?: number;
56
+ severity: WarningSeverity;
57
+ source: 'md2do';
58
+ ruleId: WarningCode;
59
+ message: string;
60
+ text?: string;
61
+ url?: string;
62
+ /** @deprecated Use message instead */
63
+ reason?: string;
55
64
  }
56
65
 
57
66
  /**
@@ -812,6 +821,58 @@ declare namespace index {
812
821
  export { type index_TaskComparator as TaskComparator, index_byAssignee as byAssignee, index_byCompletionStatus as byCompletionStatus, index_byCreatedDate as byCreatedDate, index_byDueDate as byDueDate, index_byFile as byFile, index_byPerson as byPerson, index_byPriority as byPriority, index_byProject as byProject, index_combineComparators as combineComparators, index_reverse as reverse };
813
822
  }
814
823
 
824
+ interface WarningFilterConfig {
825
+ enabled?: boolean | undefined;
826
+ rules?: Record<string, 'error' | 'warn' | 'info' | 'off'> | undefined;
827
+ }
828
+ /**
829
+ * Filter warnings based on configuration rules
830
+ *
831
+ * This function applies warning configuration rules to filter out disabled warnings
832
+ * and optionally all warnings if globally disabled.
833
+ *
834
+ * @param warnings - Array of warnings to filter
835
+ * @param config - Warning configuration with enabled flag and rule overrides
836
+ * @returns Filtered array of warnings
837
+ *
838
+ * @example
839
+ * ```typescript
840
+ * const config = {
841
+ * enabled: true,
842
+ * rules: {
843
+ * 'missing-due-date': 'off',
844
+ * 'duplicate-todoist-id': 'error',
845
+ * },
846
+ * };
847
+ *
848
+ * const filtered = filterWarnings(allWarnings, config);
849
+ * // Returns warnings except those with ruleId 'missing-due-date'
850
+ * ```
851
+ */
852
+ declare function filterWarnings(warnings: Warning[], config?: WarningFilterConfig): Warning[];
853
+ /**
854
+ * Group warnings by severity
855
+ *
856
+ * Useful for displaying warnings in order of importance or
857
+ * treating errors differently from warnings.
858
+ *
859
+ * @param warnings - Array of warnings to group
860
+ * @returns Object with warnings grouped by severity level
861
+ *
862
+ * @example
863
+ * ```typescript
864
+ * const grouped = groupWarningsBySeverity(warnings);
865
+ * console.log(`${grouped.error.length} errors`);
866
+ * console.log(`${grouped.warning.length} warnings`);
867
+ * console.log(`${grouped.info.length} info messages`);
868
+ * ```
869
+ */
870
+ declare function groupWarningsBySeverity(warnings: Warning[]): {
871
+ error: Warning[];
872
+ warning: Warning[];
873
+ info: Warning[];
874
+ };
875
+
815
876
  /**
816
877
  * Parse an absolute date string in various formats
817
878
  *
@@ -876,4 +937,4 @@ declare function extractDateFromHeading(line: string): Date | null;
876
937
  */
877
938
  declare function generateTaskId(file: string, line: number, text: string): string;
878
939
 
879
- export { ASSIGNEE, COMPLETED_DATE, DUE_DATE_ABSOLUTE, DUE_DATE_RELATIVE, DUE_DATE_SHORT, HEADING_DATE_ISO, HEADING_DATE_NATURAL, HEADING_DATE_SLASH, MarkdownScanner, PATTERNS, PRIORITY_HIGH, PRIORITY_NORMAL, PRIORITY_URGENT, type ParsingContext, type Priority, type ScanResult, TAG, TASK_CHECKBOX, TODOIST_ID, type Task, type TaskFilterCriteria, type UpdateTaskOptions, type Warning, type WriteTaskResult, addTask, cleanTaskText, extractAssignee, extractCompletedDate, extractDateFromHeading, extractDueDate, extractPersonFromFilename, extractPriority, extractProjectFromPath, extractTags, extractTodoistId, index$1 as filters, generateTaskId, parseAbsoluteDate, parseTask, resolveRelativeDate, index as sorting, updateTask, updateTasks };
940
+ export { ASSIGNEE, COMPLETED_DATE, DUE_DATE_ABSOLUTE, DUE_DATE_RELATIVE, DUE_DATE_SHORT, HEADING_DATE_ISO, HEADING_DATE_NATURAL, HEADING_DATE_SLASH, MarkdownScanner, PATTERNS, PRIORITY_HIGH, PRIORITY_NORMAL, PRIORITY_URGENT, type ParsingContext, type Priority, type ScanResult, TAG, TASK_CHECKBOX, TODOIST_ID, type Task, type TaskFilterCriteria, type UpdateTaskOptions, type Warning, type WarningCode, type WarningFilterConfig, type WarningSeverity, type WriteTaskResult, addTask, cleanTaskText, extractAssignee, extractCompletedDate, extractDateFromHeading, extractDueDate, extractPersonFromFilename, extractPriority, extractProjectFromPath, extractTags, extractTodoistId, filterWarnings, index$1 as filters, generateTaskId, groupWarningsBySeverity, parseAbsoluteDate, parseTask, resolveRelativeDate, index as sorting, updateTask, updateTasks };
package/dist/index.js CHANGED
@@ -57,8 +57,10 @@ __export(index_exports, {
57
57
  extractProjectFromPath: () => extractProjectFromPath,
58
58
  extractTags: () => extractTags,
59
59
  extractTodoistId: () => extractTodoistId,
60
+ filterWarnings: () => filterWarnings,
60
61
  filters: () => filters_exports,
61
62
  generateTaskId: () => generateTaskId,
63
+ groupWarningsBySeverity: () => groupWarningsBySeverity,
62
64
  parseAbsoluteDate: () => parseAbsoluteDate,
63
65
  parseTask: () => parseTask,
64
66
  resolveRelativeDate: () => resolveRelativeDate,
@@ -216,11 +218,15 @@ function extractDueDate(text, context) {
216
218
  return {
217
219
  date: void 0,
218
220
  warning: {
221
+ severity: "warning",
222
+ source: "md2do",
223
+ ruleId: "relative-date-no-context",
219
224
  file: "",
220
225
  // Will be filled in by caller
221
226
  line: 0,
222
227
  // Will be filled in by caller
223
228
  text: text.trim(),
229
+ message: "Relative due date without context date from heading. Add a heading with a date above this task.",
224
230
  reason: "Relative due date without context date from heading. Add a heading with a date above this task."
225
231
  }
226
232
  };
@@ -235,6 +241,58 @@ function cleanTaskText(text) {
235
241
  }
236
242
  function parseTask(line, lineNumber, file, context) {
237
243
  const warnings = [];
244
+ if (/^\s*[*+]\s+\[[ xX]\]/.test(line)) {
245
+ warnings.push({
246
+ severity: "warning",
247
+ source: "md2do",
248
+ ruleId: "unsupported-bullet",
249
+ file,
250
+ line: lineNumber,
251
+ text: line.trim(),
252
+ message: "Unsupported bullet marker (* or +). Use dash (-) for task lists.",
253
+ reason: "Unsupported bullet marker (* or +). Use dash (-) for task lists."
254
+ });
255
+ return { task: null, warnings };
256
+ }
257
+ if (/^\s*-\s+\[[xX]\s+\]/.test(line) || /^\s*-\s+\[\s+[xX]\]/.test(line)) {
258
+ warnings.push({
259
+ severity: "warning",
260
+ source: "md2do",
261
+ ruleId: "malformed-checkbox",
262
+ file,
263
+ line: lineNumber,
264
+ text: line.trim(),
265
+ message: "Malformed checkbox with extra spaces. Use [x] or [ ] without extra spaces.",
266
+ reason: "Malformed checkbox with extra spaces. Use [x] or [ ] without extra spaces."
267
+ });
268
+ return { task: null, warnings };
269
+ }
270
+ if (/^\s*-\s+\[[ xX]\][^\s]/.test(line)) {
271
+ warnings.push({
272
+ severity: "warning",
273
+ source: "md2do",
274
+ ruleId: "missing-space-after",
275
+ file,
276
+ line: lineNumber,
277
+ text: line.trim(),
278
+ message: 'Missing space after checkbox. Use "- [x] Task" format.',
279
+ reason: 'Missing space after checkbox. Use "- [x] Task" format.'
280
+ });
281
+ return { task: null, warnings };
282
+ }
283
+ if (/^\s*-\[[ xX]\]/.test(line)) {
284
+ warnings.push({
285
+ severity: "warning",
286
+ source: "md2do",
287
+ ruleId: "missing-space-before",
288
+ file,
289
+ line: lineNumber,
290
+ text: line.trim(),
291
+ message: 'Missing space before checkbox. Use "- [x] Task" format.',
292
+ reason: 'Missing space before checkbox. Use "- [x] Task" format.'
293
+ });
294
+ return { task: null, warnings };
295
+ }
238
296
  const taskMatch = line.match(PATTERNS.TASK_CHECKBOX);
239
297
  if (!taskMatch?.[0] || !taskMatch[2]) {
240
298
  return { task: null, warnings };
@@ -254,6 +312,30 @@ function parseTask(line, lineNumber, file, context) {
254
312
  line: lineNumber
255
313
  });
256
314
  }
315
+ if (!completed && !dueDateResult.date && !context.currentDate) {
316
+ warnings.push({
317
+ severity: "info",
318
+ source: "md2do",
319
+ ruleId: "missing-due-date",
320
+ file,
321
+ line: lineNumber,
322
+ text: fullText.trim(),
323
+ message: "Task has no due date. Add [due: YYYY-MM-DD] or place under a heading with a date.",
324
+ reason: "Task has no due date. Add [due: YYYY-MM-DD] or place under a heading with a date."
325
+ });
326
+ }
327
+ if (completed && !completedDate) {
328
+ warnings.push({
329
+ severity: "info",
330
+ source: "md2do",
331
+ ruleId: "missing-completed-date",
332
+ file,
333
+ line: lineNumber,
334
+ text: fullText.trim(),
335
+ message: "Completed task missing completion date. Add [completed: YYYY-MM-DD].",
336
+ reason: "Completed task missing completion date. Add [completed: YYYY-MM-DD]."
337
+ });
338
+ }
257
339
  const cleanText = cleanTaskText(fullText);
258
340
  const id = generateTaskId(file, lineNumber, cleanText);
259
341
  const task = {
@@ -299,6 +381,7 @@ var MarkdownScanner = class {
299
381
  scanFile(filePath, content) {
300
382
  const tasks = [];
301
383
  const warnings = [];
384
+ const todoistIds = /* @__PURE__ */ new Map();
302
385
  const context = {};
303
386
  const project = extractProjectFromPath(filePath);
304
387
  if (project !== void 0) context.project = project;
@@ -318,6 +401,26 @@ var MarkdownScanner = class {
318
401
  const result = parseTask(line, lineNumber, filePath, context);
319
402
  if (result.task) {
320
403
  tasks.push(result.task);
404
+ if (result.task.todoistId) {
405
+ const existing = todoistIds.get(result.task.todoistId);
406
+ if (existing) {
407
+ warnings.push({
408
+ severity: "error",
409
+ source: "md2do",
410
+ ruleId: "duplicate-todoist-id",
411
+ file: filePath,
412
+ line: lineNumber,
413
+ text: result.task.text,
414
+ message: `Duplicate Todoist ID [todoist:${result.task.todoistId}]. Also found at ${existing.file}:${existing.line}.`,
415
+ reason: `Duplicate Todoist ID [todoist:${result.task.todoistId}]. Also found at ${existing.file}:${existing.line}.`
416
+ });
417
+ } else {
418
+ todoistIds.set(result.task.todoistId, {
419
+ file: filePath,
420
+ line: lineNumber
421
+ });
422
+ }
423
+ }
321
424
  }
322
425
  if (result.warnings.length > 0) {
323
426
  warnings.push(...result.warnings);
@@ -337,10 +440,33 @@ var MarkdownScanner = class {
337
440
  scanFiles(files) {
338
441
  const allTasks = [];
339
442
  const allWarnings = [];
443
+ const todoistIds = /* @__PURE__ */ new Map();
340
444
  for (const file of files) {
341
445
  const result = this.scanFile(file.path, file.content);
342
446
  allTasks.push(...result.tasks);
343
447
  allWarnings.push(...result.warnings);
448
+ for (const task of result.tasks) {
449
+ if (task.todoistId) {
450
+ const existing = todoistIds.get(task.todoistId);
451
+ if (existing && existing.file !== task.file) {
452
+ allWarnings.push({
453
+ severity: "error",
454
+ source: "md2do",
455
+ ruleId: "duplicate-todoist-id",
456
+ file: task.file,
457
+ line: task.line,
458
+ text: task.text,
459
+ message: `Duplicate Todoist ID [todoist:${task.todoistId}] across files. Also found at ${existing.file}:${existing.line}.`,
460
+ reason: `Duplicate Todoist ID [todoist:${task.todoistId}] across files. Also found at ${existing.file}:${existing.line}.`
461
+ });
462
+ } else if (!existing) {
463
+ todoistIds.set(task.todoistId, {
464
+ file: task.file,
465
+ line: task.line
466
+ });
467
+ }
468
+ }
469
+ }
344
470
  }
345
471
  return {
346
472
  tasks: allTasks,
@@ -779,6 +905,32 @@ function combineComparators(comparators) {
779
905
  function reverse(comparator) {
780
906
  return (a, b) => -comparator(a, b);
781
907
  }
908
+
909
+ // src/warnings/filter.ts
910
+ function filterWarnings(warnings, config = {}) {
911
+ if (config.enabled === false) {
912
+ return [];
913
+ }
914
+ if (!config.rules) {
915
+ return warnings;
916
+ }
917
+ return warnings.filter((warning) => {
918
+ const level = config.rules?.[warning.ruleId];
919
+ if (level === void 0) {
920
+ return true;
921
+ }
922
+ return level !== "off";
923
+ });
924
+ }
925
+ function groupWarningsBySeverity(warnings) {
926
+ return warnings.reduce(
927
+ (acc, warning) => {
928
+ acc[warning.severity].push(warning);
929
+ return acc;
930
+ },
931
+ { error: [], warning: [], info: [] }
932
+ );
933
+ }
782
934
  // Annotate the CommonJS export names for ESM import in node:
783
935
  0 && (module.exports = {
784
936
  ASSIGNEE,
@@ -808,8 +960,10 @@ function reverse(comparator) {
808
960
  extractProjectFromPath,
809
961
  extractTags,
810
962
  extractTodoistId,
963
+ filterWarnings,
811
964
  filters,
812
965
  generateTaskId,
966
+ groupWarningsBySeverity,
813
967
  parseAbsoluteDate,
814
968
  parseTask,
815
969
  resolveRelativeDate,
package/dist/index.mjs CHANGED
@@ -159,11 +159,15 @@ function extractDueDate(text, context) {
159
159
  return {
160
160
  date: void 0,
161
161
  warning: {
162
+ severity: "warning",
163
+ source: "md2do",
164
+ ruleId: "relative-date-no-context",
162
165
  file: "",
163
166
  // Will be filled in by caller
164
167
  line: 0,
165
168
  // Will be filled in by caller
166
169
  text: text.trim(),
170
+ message: "Relative due date without context date from heading. Add a heading with a date above this task.",
167
171
  reason: "Relative due date without context date from heading. Add a heading with a date above this task."
168
172
  }
169
173
  };
@@ -178,6 +182,58 @@ function cleanTaskText(text) {
178
182
  }
179
183
  function parseTask(line, lineNumber, file, context) {
180
184
  const warnings = [];
185
+ if (/^\s*[*+]\s+\[[ xX]\]/.test(line)) {
186
+ warnings.push({
187
+ severity: "warning",
188
+ source: "md2do",
189
+ ruleId: "unsupported-bullet",
190
+ file,
191
+ line: lineNumber,
192
+ text: line.trim(),
193
+ message: "Unsupported bullet marker (* or +). Use dash (-) for task lists.",
194
+ reason: "Unsupported bullet marker (* or +). Use dash (-) for task lists."
195
+ });
196
+ return { task: null, warnings };
197
+ }
198
+ if (/^\s*-\s+\[[xX]\s+\]/.test(line) || /^\s*-\s+\[\s+[xX]\]/.test(line)) {
199
+ warnings.push({
200
+ severity: "warning",
201
+ source: "md2do",
202
+ ruleId: "malformed-checkbox",
203
+ file,
204
+ line: lineNumber,
205
+ text: line.trim(),
206
+ message: "Malformed checkbox with extra spaces. Use [x] or [ ] without extra spaces.",
207
+ reason: "Malformed checkbox with extra spaces. Use [x] or [ ] without extra spaces."
208
+ });
209
+ return { task: null, warnings };
210
+ }
211
+ if (/^\s*-\s+\[[ xX]\][^\s]/.test(line)) {
212
+ warnings.push({
213
+ severity: "warning",
214
+ source: "md2do",
215
+ ruleId: "missing-space-after",
216
+ file,
217
+ line: lineNumber,
218
+ text: line.trim(),
219
+ message: 'Missing space after checkbox. Use "- [x] Task" format.',
220
+ reason: 'Missing space after checkbox. Use "- [x] Task" format.'
221
+ });
222
+ return { task: null, warnings };
223
+ }
224
+ if (/^\s*-\[[ xX]\]/.test(line)) {
225
+ warnings.push({
226
+ severity: "warning",
227
+ source: "md2do",
228
+ ruleId: "missing-space-before",
229
+ file,
230
+ line: lineNumber,
231
+ text: line.trim(),
232
+ message: 'Missing space before checkbox. Use "- [x] Task" format.',
233
+ reason: 'Missing space before checkbox. Use "- [x] Task" format.'
234
+ });
235
+ return { task: null, warnings };
236
+ }
181
237
  const taskMatch = line.match(PATTERNS.TASK_CHECKBOX);
182
238
  if (!taskMatch?.[0] || !taskMatch[2]) {
183
239
  return { task: null, warnings };
@@ -197,6 +253,30 @@ function parseTask(line, lineNumber, file, context) {
197
253
  line: lineNumber
198
254
  });
199
255
  }
256
+ if (!completed && !dueDateResult.date && !context.currentDate) {
257
+ warnings.push({
258
+ severity: "info",
259
+ source: "md2do",
260
+ ruleId: "missing-due-date",
261
+ file,
262
+ line: lineNumber,
263
+ text: fullText.trim(),
264
+ message: "Task has no due date. Add [due: YYYY-MM-DD] or place under a heading with a date.",
265
+ reason: "Task has no due date. Add [due: YYYY-MM-DD] or place under a heading with a date."
266
+ });
267
+ }
268
+ if (completed && !completedDate) {
269
+ warnings.push({
270
+ severity: "info",
271
+ source: "md2do",
272
+ ruleId: "missing-completed-date",
273
+ file,
274
+ line: lineNumber,
275
+ text: fullText.trim(),
276
+ message: "Completed task missing completion date. Add [completed: YYYY-MM-DD].",
277
+ reason: "Completed task missing completion date. Add [completed: YYYY-MM-DD]."
278
+ });
279
+ }
200
280
  const cleanText = cleanTaskText(fullText);
201
281
  const id = generateTaskId(file, lineNumber, cleanText);
202
282
  const task = {
@@ -242,6 +322,7 @@ var MarkdownScanner = class {
242
322
  scanFile(filePath, content) {
243
323
  const tasks = [];
244
324
  const warnings = [];
325
+ const todoistIds = /* @__PURE__ */ new Map();
245
326
  const context = {};
246
327
  const project = extractProjectFromPath(filePath);
247
328
  if (project !== void 0) context.project = project;
@@ -261,6 +342,26 @@ var MarkdownScanner = class {
261
342
  const result = parseTask(line, lineNumber, filePath, context);
262
343
  if (result.task) {
263
344
  tasks.push(result.task);
345
+ if (result.task.todoistId) {
346
+ const existing = todoistIds.get(result.task.todoistId);
347
+ if (existing) {
348
+ warnings.push({
349
+ severity: "error",
350
+ source: "md2do",
351
+ ruleId: "duplicate-todoist-id",
352
+ file: filePath,
353
+ line: lineNumber,
354
+ text: result.task.text,
355
+ message: `Duplicate Todoist ID [todoist:${result.task.todoistId}]. Also found at ${existing.file}:${existing.line}.`,
356
+ reason: `Duplicate Todoist ID [todoist:${result.task.todoistId}]. Also found at ${existing.file}:${existing.line}.`
357
+ });
358
+ } else {
359
+ todoistIds.set(result.task.todoistId, {
360
+ file: filePath,
361
+ line: lineNumber
362
+ });
363
+ }
364
+ }
264
365
  }
265
366
  if (result.warnings.length > 0) {
266
367
  warnings.push(...result.warnings);
@@ -280,10 +381,33 @@ var MarkdownScanner = class {
280
381
  scanFiles(files) {
281
382
  const allTasks = [];
282
383
  const allWarnings = [];
384
+ const todoistIds = /* @__PURE__ */ new Map();
283
385
  for (const file of files) {
284
386
  const result = this.scanFile(file.path, file.content);
285
387
  allTasks.push(...result.tasks);
286
388
  allWarnings.push(...result.warnings);
389
+ for (const task of result.tasks) {
390
+ if (task.todoistId) {
391
+ const existing = todoistIds.get(task.todoistId);
392
+ if (existing && existing.file !== task.file) {
393
+ allWarnings.push({
394
+ severity: "error",
395
+ source: "md2do",
396
+ ruleId: "duplicate-todoist-id",
397
+ file: task.file,
398
+ line: task.line,
399
+ text: task.text,
400
+ message: `Duplicate Todoist ID [todoist:${task.todoistId}] across files. Also found at ${existing.file}:${existing.line}.`,
401
+ reason: `Duplicate Todoist ID [todoist:${task.todoistId}] across files. Also found at ${existing.file}:${existing.line}.`
402
+ });
403
+ } else if (!existing) {
404
+ todoistIds.set(task.todoistId, {
405
+ file: task.file,
406
+ line: task.line
407
+ });
408
+ }
409
+ }
410
+ }
287
411
  }
288
412
  return {
289
413
  tasks: allTasks,
@@ -722,6 +846,32 @@ function combineComparators(comparators) {
722
846
  function reverse(comparator) {
723
847
  return (a, b) => -comparator(a, b);
724
848
  }
849
+
850
+ // src/warnings/filter.ts
851
+ function filterWarnings(warnings, config = {}) {
852
+ if (config.enabled === false) {
853
+ return [];
854
+ }
855
+ if (!config.rules) {
856
+ return warnings;
857
+ }
858
+ return warnings.filter((warning) => {
859
+ const level = config.rules?.[warning.ruleId];
860
+ if (level === void 0) {
861
+ return true;
862
+ }
863
+ return level !== "off";
864
+ });
865
+ }
866
+ function groupWarningsBySeverity(warnings) {
867
+ return warnings.reduce(
868
+ (acc, warning) => {
869
+ acc[warning.severity].push(warning);
870
+ return acc;
871
+ },
872
+ { error: [], warning: [], info: [] }
873
+ );
874
+ }
725
875
  export {
726
876
  ASSIGNEE,
727
877
  COMPLETED_DATE,
@@ -750,8 +900,10 @@ export {
750
900
  extractProjectFromPath,
751
901
  extractTags,
752
902
  extractTodoistId,
903
+ filterWarnings,
753
904
  filters_exports as filters,
754
905
  generateTaskId,
906
+ groupWarningsBySeverity,
755
907
  parseAbsoluteDate,
756
908
  parseTask,
757
909
  resolveRelativeDate,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@md2do/core",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Core parsing, filtering, scanning, and file writing for md2do",
5
5
  "keywords": [
6
6
  "markdown",
package/src/index.ts CHANGED
@@ -19,6 +19,9 @@ export * as filters from './filters/index.js';
19
19
  // Sorting (export as namespace to avoid conflicts)
20
20
  export * as sorting from './sorting/index.js';
21
21
 
22
+ // Warnings
23
+ export * from './warnings/filter.js';
24
+
22
25
  // Utilities
23
26
  export * from './utils/dates.js';
24
27
  export * from './utils/id.js';