@md2do/core 0.2.3 → 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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # @md2do/core
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Add comprehensive warning system for markdown validation and VSCode extension
8
+ - Add warning configuration system with customizable rules
9
+ - Add VSCode extension with task explorer, diagnostics, hover tooltips, and smart completion
10
+ - Support for validating malformed checkboxes, missing metadata, duplicate IDs, and more
11
+ - Add .vsix distribution for beta testing
12
+
3
13
  ## 0.2.3
4
14
 
5
15
  ### Patch Changes
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
  };
@@ -237,36 +243,52 @@ function parseTask(line, lineNumber, file, context) {
237
243
  const warnings = [];
238
244
  if (/^\s*[*+]\s+\[[ xX]\]/.test(line)) {
239
245
  warnings.push({
246
+ severity: "warning",
247
+ source: "md2do",
248
+ ruleId: "unsupported-bullet",
240
249
  file,
241
250
  line: lineNumber,
242
251
  text: line.trim(),
252
+ message: "Unsupported bullet marker (* or +). Use dash (-) for task lists.",
243
253
  reason: "Unsupported bullet marker (* or +). Use dash (-) for task lists."
244
254
  });
245
255
  return { task: null, warnings };
246
256
  }
247
257
  if (/^\s*-\s+\[[xX]\s+\]/.test(line) || /^\s*-\s+\[\s+[xX]\]/.test(line)) {
248
258
  warnings.push({
259
+ severity: "warning",
260
+ source: "md2do",
261
+ ruleId: "malformed-checkbox",
249
262
  file,
250
263
  line: lineNumber,
251
264
  text: line.trim(),
265
+ message: "Malformed checkbox with extra spaces. Use [x] or [ ] without extra spaces.",
252
266
  reason: "Malformed checkbox with extra spaces. Use [x] or [ ] without extra spaces."
253
267
  });
254
268
  return { task: null, warnings };
255
269
  }
256
270
  if (/^\s*-\s+\[[ xX]\][^\s]/.test(line)) {
257
271
  warnings.push({
272
+ severity: "warning",
273
+ source: "md2do",
274
+ ruleId: "missing-space-after",
258
275
  file,
259
276
  line: lineNumber,
260
277
  text: line.trim(),
278
+ message: 'Missing space after checkbox. Use "- [x] Task" format.',
261
279
  reason: 'Missing space after checkbox. Use "- [x] Task" format.'
262
280
  });
263
281
  return { task: null, warnings };
264
282
  }
265
283
  if (/^\s*-\[[ xX]\]/.test(line)) {
266
284
  warnings.push({
285
+ severity: "warning",
286
+ source: "md2do",
287
+ ruleId: "missing-space-before",
267
288
  file,
268
289
  line: lineNumber,
269
290
  text: line.trim(),
291
+ message: 'Missing space before checkbox. Use "- [x] Task" format.',
270
292
  reason: 'Missing space before checkbox. Use "- [x] Task" format.'
271
293
  });
272
294
  return { task: null, warnings };
@@ -292,17 +314,25 @@ function parseTask(line, lineNumber, file, context) {
292
314
  }
293
315
  if (!completed && !dueDateResult.date && !context.currentDate) {
294
316
  warnings.push({
317
+ severity: "info",
318
+ source: "md2do",
319
+ ruleId: "missing-due-date",
295
320
  file,
296
321
  line: lineNumber,
297
322
  text: fullText.trim(),
323
+ message: "Task has no due date. Add [due: YYYY-MM-DD] or place under a heading with a date.",
298
324
  reason: "Task has no due date. Add [due: YYYY-MM-DD] or place under a heading with a date."
299
325
  });
300
326
  }
301
327
  if (completed && !completedDate) {
302
328
  warnings.push({
329
+ severity: "info",
330
+ source: "md2do",
331
+ ruleId: "missing-completed-date",
303
332
  file,
304
333
  line: lineNumber,
305
334
  text: fullText.trim(),
335
+ message: "Completed task missing completion date. Add [completed: YYYY-MM-DD].",
306
336
  reason: "Completed task missing completion date. Add [completed: YYYY-MM-DD]."
307
337
  });
308
338
  }
@@ -375,9 +405,13 @@ var MarkdownScanner = class {
375
405
  const existing = todoistIds.get(result.task.todoistId);
376
406
  if (existing) {
377
407
  warnings.push({
408
+ severity: "error",
409
+ source: "md2do",
410
+ ruleId: "duplicate-todoist-id",
378
411
  file: filePath,
379
412
  line: lineNumber,
380
413
  text: result.task.text,
414
+ message: `Duplicate Todoist ID [todoist:${result.task.todoistId}]. Also found at ${existing.file}:${existing.line}.`,
381
415
  reason: `Duplicate Todoist ID [todoist:${result.task.todoistId}]. Also found at ${existing.file}:${existing.line}.`
382
416
  });
383
417
  } else {
@@ -416,9 +450,13 @@ var MarkdownScanner = class {
416
450
  const existing = todoistIds.get(task.todoistId);
417
451
  if (existing && existing.file !== task.file) {
418
452
  allWarnings.push({
453
+ severity: "error",
454
+ source: "md2do",
455
+ ruleId: "duplicate-todoist-id",
419
456
  file: task.file,
420
457
  line: task.line,
421
458
  text: task.text,
459
+ message: `Duplicate Todoist ID [todoist:${task.todoistId}] across files. Also found at ${existing.file}:${existing.line}.`,
422
460
  reason: `Duplicate Todoist ID [todoist:${task.todoistId}] across files. Also found at ${existing.file}:${existing.line}.`
423
461
  });
424
462
  } else if (!existing) {
@@ -867,6 +905,32 @@ function combineComparators(comparators) {
867
905
  function reverse(comparator) {
868
906
  return (a, b) => -comparator(a, b);
869
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
+ }
870
934
  // Annotate the CommonJS export names for ESM import in node:
871
935
  0 && (module.exports = {
872
936
  ASSIGNEE,
@@ -896,8 +960,10 @@ function reverse(comparator) {
896
960
  extractProjectFromPath,
897
961
  extractTags,
898
962
  extractTodoistId,
963
+ filterWarnings,
899
964
  filters,
900
965
  generateTaskId,
966
+ groupWarningsBySeverity,
901
967
  parseAbsoluteDate,
902
968
  parseTask,
903
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
  };
@@ -180,36 +184,52 @@ function parseTask(line, lineNumber, file, context) {
180
184
  const warnings = [];
181
185
  if (/^\s*[*+]\s+\[[ xX]\]/.test(line)) {
182
186
  warnings.push({
187
+ severity: "warning",
188
+ source: "md2do",
189
+ ruleId: "unsupported-bullet",
183
190
  file,
184
191
  line: lineNumber,
185
192
  text: line.trim(),
193
+ message: "Unsupported bullet marker (* or +). Use dash (-) for task lists.",
186
194
  reason: "Unsupported bullet marker (* or +). Use dash (-) for task lists."
187
195
  });
188
196
  return { task: null, warnings };
189
197
  }
190
198
  if (/^\s*-\s+\[[xX]\s+\]/.test(line) || /^\s*-\s+\[\s+[xX]\]/.test(line)) {
191
199
  warnings.push({
200
+ severity: "warning",
201
+ source: "md2do",
202
+ ruleId: "malformed-checkbox",
192
203
  file,
193
204
  line: lineNumber,
194
205
  text: line.trim(),
206
+ message: "Malformed checkbox with extra spaces. Use [x] or [ ] without extra spaces.",
195
207
  reason: "Malformed checkbox with extra spaces. Use [x] or [ ] without extra spaces."
196
208
  });
197
209
  return { task: null, warnings };
198
210
  }
199
211
  if (/^\s*-\s+\[[ xX]\][^\s]/.test(line)) {
200
212
  warnings.push({
213
+ severity: "warning",
214
+ source: "md2do",
215
+ ruleId: "missing-space-after",
201
216
  file,
202
217
  line: lineNumber,
203
218
  text: line.trim(),
219
+ message: 'Missing space after checkbox. Use "- [x] Task" format.',
204
220
  reason: 'Missing space after checkbox. Use "- [x] Task" format.'
205
221
  });
206
222
  return { task: null, warnings };
207
223
  }
208
224
  if (/^\s*-\[[ xX]\]/.test(line)) {
209
225
  warnings.push({
226
+ severity: "warning",
227
+ source: "md2do",
228
+ ruleId: "missing-space-before",
210
229
  file,
211
230
  line: lineNumber,
212
231
  text: line.trim(),
232
+ message: 'Missing space before checkbox. Use "- [x] Task" format.',
213
233
  reason: 'Missing space before checkbox. Use "- [x] Task" format.'
214
234
  });
215
235
  return { task: null, warnings };
@@ -235,17 +255,25 @@ function parseTask(line, lineNumber, file, context) {
235
255
  }
236
256
  if (!completed && !dueDateResult.date && !context.currentDate) {
237
257
  warnings.push({
258
+ severity: "info",
259
+ source: "md2do",
260
+ ruleId: "missing-due-date",
238
261
  file,
239
262
  line: lineNumber,
240
263
  text: fullText.trim(),
264
+ message: "Task has no due date. Add [due: YYYY-MM-DD] or place under a heading with a date.",
241
265
  reason: "Task has no due date. Add [due: YYYY-MM-DD] or place under a heading with a date."
242
266
  });
243
267
  }
244
268
  if (completed && !completedDate) {
245
269
  warnings.push({
270
+ severity: "info",
271
+ source: "md2do",
272
+ ruleId: "missing-completed-date",
246
273
  file,
247
274
  line: lineNumber,
248
275
  text: fullText.trim(),
276
+ message: "Completed task missing completion date. Add [completed: YYYY-MM-DD].",
249
277
  reason: "Completed task missing completion date. Add [completed: YYYY-MM-DD]."
250
278
  });
251
279
  }
@@ -318,9 +346,13 @@ var MarkdownScanner = class {
318
346
  const existing = todoistIds.get(result.task.todoistId);
319
347
  if (existing) {
320
348
  warnings.push({
349
+ severity: "error",
350
+ source: "md2do",
351
+ ruleId: "duplicate-todoist-id",
321
352
  file: filePath,
322
353
  line: lineNumber,
323
354
  text: result.task.text,
355
+ message: `Duplicate Todoist ID [todoist:${result.task.todoistId}]. Also found at ${existing.file}:${existing.line}.`,
324
356
  reason: `Duplicate Todoist ID [todoist:${result.task.todoistId}]. Also found at ${existing.file}:${existing.line}.`
325
357
  });
326
358
  } else {
@@ -359,9 +391,13 @@ var MarkdownScanner = class {
359
391
  const existing = todoistIds.get(task.todoistId);
360
392
  if (existing && existing.file !== task.file) {
361
393
  allWarnings.push({
394
+ severity: "error",
395
+ source: "md2do",
396
+ ruleId: "duplicate-todoist-id",
362
397
  file: task.file,
363
398
  line: task.line,
364
399
  text: task.text,
400
+ message: `Duplicate Todoist ID [todoist:${task.todoistId}] across files. Also found at ${existing.file}:${existing.line}.`,
365
401
  reason: `Duplicate Todoist ID [todoist:${task.todoistId}] across files. Also found at ${existing.file}:${existing.line}.`
366
402
  });
367
403
  } else if (!existing) {
@@ -810,6 +846,32 @@ function combineComparators(comparators) {
810
846
  function reverse(comparator) {
811
847
  return (a, b) => -comparator(a, b);
812
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
+ }
813
875
  export {
814
876
  ASSIGNEE,
815
877
  COMPLETED_DATE,
@@ -838,8 +900,10 @@ export {
838
900
  extractProjectFromPath,
839
901
  extractTags,
840
902
  extractTodoistId,
903
+ filterWarnings,
841
904
  filters_exports as filters,
842
905
  generateTaskId,
906
+ groupWarningsBySeverity,
843
907
  parseAbsoluteDate,
844
908
  parseTask,
845
909
  resolveRelativeDate,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@md2do/core",
3
- "version": "0.2.3",
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';
@@ -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
  },
@@ -212,9 +217,14 @@ export function parseTask(
212
217
  // Check for asterisk/plus bullet markers (not supported)
213
218
  if (/^\s*[*+]\s+\[[ xX]\]/.test(line)) {
214
219
  warnings.push({
220
+ severity: 'warning',
221
+ source: 'md2do',
222
+ ruleId: 'unsupported-bullet',
215
223
  file,
216
224
  line: lineNumber,
217
225
  text: line.trim(),
226
+ message:
227
+ 'Unsupported bullet marker (* or +). Use dash (-) for task lists.',
218
228
  reason:
219
229
  'Unsupported bullet marker (* or +). Use dash (-) for task lists.',
220
230
  });
@@ -224,9 +234,14 @@ export function parseTask(
224
234
  // Check for extra spaces inside checkbox: [x ] or [ x]
225
235
  if (/^\s*-\s+\[[xX]\s+\]/.test(line) || /^\s*-\s+\[\s+[xX]\]/.test(line)) {
226
236
  warnings.push({
237
+ severity: 'warning',
238
+ source: 'md2do',
239
+ ruleId: 'malformed-checkbox',
227
240
  file,
228
241
  line: lineNumber,
229
242
  text: line.trim(),
243
+ message:
244
+ 'Malformed checkbox with extra spaces. Use [x] or [ ] without extra spaces.',
230
245
  reason:
231
246
  'Malformed checkbox with extra spaces. Use [x] or [ ] without extra spaces.',
232
247
  });
@@ -236,9 +251,13 @@ export function parseTask(
236
251
  // Check for missing space after checkbox: [x]Task
237
252
  if (/^\s*-\s+\[[ xX]\][^\s]/.test(line)) {
238
253
  warnings.push({
254
+ severity: 'warning',
255
+ source: 'md2do',
256
+ ruleId: 'missing-space-after',
239
257
  file,
240
258
  line: lineNumber,
241
259
  text: line.trim(),
260
+ message: 'Missing space after checkbox. Use "- [x] Task" format.',
242
261
  reason: 'Missing space after checkbox. Use "- [x] Task" format.',
243
262
  });
244
263
  return { task: null, warnings };
@@ -247,9 +266,13 @@ export function parseTask(
247
266
  // Check for missing space before checkbox: -[x]
248
267
  if (/^\s*-\[[ xX]\]/.test(line)) {
249
268
  warnings.push({
269
+ severity: 'warning',
270
+ source: 'md2do',
271
+ ruleId: 'missing-space-before',
250
272
  file,
251
273
  line: lineNumber,
252
274
  text: line.trim(),
275
+ message: 'Missing space before checkbox. Use "- [x] Task" format.',
253
276
  reason: 'Missing space before checkbox. Use "- [x] Task" format.',
254
277
  });
255
278
  return { task: null, warnings };
@@ -288,9 +311,14 @@ export function parseTask(
288
311
  // Incomplete tasks without any date (no explicit due date and no context date)
289
312
  if (!completed && !dueDateResult.date && !context.currentDate) {
290
313
  warnings.push({
314
+ severity: 'info',
315
+ source: 'md2do',
316
+ ruleId: 'missing-due-date',
291
317
  file,
292
318
  line: lineNumber,
293
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.',
294
322
  reason:
295
323
  'Task has no due date. Add [due: YYYY-MM-DD] or place under a heading with a date.',
296
324
  });
@@ -299,9 +327,14 @@ export function parseTask(
299
327
  // Completed tasks should have completion dates
300
328
  if (completed && !completedDate) {
301
329
  warnings.push({
330
+ severity: 'info',
331
+ source: 'md2do',
332
+ ruleId: 'missing-completed-date',
302
333
  file,
303
334
  line: lineNumber,
304
335
  text: fullText.trim(),
336
+ message:
337
+ 'Completed task missing completion date. Add [completed: YYYY-MM-DD].',
305
338
  reason:
306
339
  'Completed task missing completion date. Add [completed: YYYY-MM-DD].',
307
340
  });
@@ -114,9 +114,13 @@ export class MarkdownScanner {
114
114
  const existing = todoistIds.get(result.task.todoistId);
115
115
  if (existing) {
116
116
  warnings.push({
117
+ severity: 'error',
118
+ source: 'md2do',
119
+ ruleId: 'duplicate-todoist-id',
117
120
  file: filePath,
118
121
  line: lineNumber,
119
122
  text: result.task.text,
123
+ message: `Duplicate Todoist ID [todoist:${result.task.todoistId}]. Also found at ${existing.file}:${existing.line}.`,
120
124
  reason: `Duplicate Todoist ID [todoist:${result.task.todoistId}]. Also found at ${existing.file}:${existing.line}.`,
121
125
  });
122
126
  } else {
@@ -168,9 +172,13 @@ export class MarkdownScanner {
168
172
  // Only add warning if duplicate is in a different file
169
173
  // (same-file duplicates are already caught by scanFile)
170
174
  allWarnings.push({
175
+ severity: 'error',
176
+ source: 'md2do',
177
+ ruleId: 'duplicate-todoist-id',
171
178
  file: task.file,
172
179
  line: task.line,
173
180
  text: task.text,
181
+ message: `Duplicate Todoist ID [todoist:${task.todoistId}] across files. Also found at ${existing.file}:${existing.line}.`,
174
182
  reason: `Duplicate Todoist ID [todoist:${task.todoistId}] across files. Also found at ${existing.file}:${existing.line}.`,
175
183
  });
176
184
  } else if (!existing) {
@@ -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
- text: string;
72
- reason: string;
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
+ }