@backlog-md/core 0.2.1 → 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/dist/core/Core.js CHANGED
@@ -5,8 +5,8 @@
5
5
  * by accepting adapter implementations for I/O operations.
6
6
  */
7
7
  import { parseBacklogConfig, serializeBacklogConfig } from "./config-parser";
8
- import { parseTaskMarkdown, extractTaskIndexFromPath } from "../markdown";
9
- import { sortTasks, sortTasksBy, groupTasksByStatus } from "../utils";
8
+ import { parseTaskMarkdown, serializeTaskMarkdown, extractTaskIndexFromPath, parseMilestoneMarkdown, serializeMilestoneMarkdown, getMilestoneFilename, extractMilestoneIdFromFilename, } from "../markdown";
9
+ import { sortTasks, sortTasksBy, groupTasksByStatus, groupTasksByMilestone, milestoneKey, } from "../utils";
10
10
  /**
11
11
  * Core class for Backlog.md operations
12
12
  *
@@ -239,23 +239,15 @@ export class Core {
239
239
  let entries = Array.from(this.taskIndex.values()).filter((e) => e.source === source);
240
240
  // Per-source limit
241
241
  const limit = source === "tasks" ? tasksLimit : completedLimit;
242
- // Sort based on source type
243
- if (source === "completed" && completedSortByIdDesc) {
244
- // Sort completed by ID descending (higher ID = more recent)
245
- entries = entries.sort((a, b) => {
246
- // Extract numeric part for comparison
247
- const aNum = parseInt(a.id.replace(/\D/g, ""), 10) || 0;
248
- const bNum = parseInt(b.id.replace(/\D/g, ""), 10) || 0;
249
- return bNum - aNum; // Descending
250
- });
251
- }
252
- else {
253
- // Sort by title
254
- entries = entries.sort((a, b) => {
255
- const cmp = a.title.localeCompare(b.title);
256
- return tasksSortDirection === "asc" ? cmp : -cmp;
257
- });
258
- }
242
+ // Sort by ID (numeric)
243
+ entries = entries.sort((a, b) => {
244
+ const aNum = parseInt(a.id.replace(/\D/g, ""), 10) || 0;
245
+ const bNum = parseInt(b.id.replace(/\D/g, ""), 10) || 0;
246
+ // Completed: descending (most recent first), Active: ascending
247
+ return source === "completed" && completedSortByIdDesc
248
+ ? bNum - aNum
249
+ : aNum - bNum;
250
+ });
259
251
  const total = entries.length;
260
252
  const pageEntries = entries.slice(offset, offset + limit);
261
253
  // Load only the tasks for this page
@@ -290,22 +282,15 @@ export class Core {
290
282
  const completedSortByIdDesc = options?.completedSortByIdDesc ?? true;
291
283
  // Get index entries for this source
292
284
  let entries = Array.from(this.taskIndex.values()).filter((e) => e.source === source);
293
- // Sort based on source type
294
- if (source === "completed" && completedSortByIdDesc) {
295
- // Sort completed by ID descending (higher ID = more recent)
296
- entries = entries.sort((a, b) => {
297
- const aNum = parseInt(a.id.replace(/\D/g, ""), 10) || 0;
298
- const bNum = parseInt(b.id.replace(/\D/g, ""), 10) || 0;
299
- return bNum - aNum;
300
- });
301
- }
302
- else {
303
- // Sort by title
304
- entries = entries.sort((a, b) => {
305
- const cmp = a.title.localeCompare(b.title);
306
- return sortDirection === "asc" ? cmp : -cmp;
307
- });
308
- }
285
+ // Sort by ID (numeric)
286
+ entries = entries.sort((a, b) => {
287
+ const aNum = parseInt(a.id.replace(/\D/g, ""), 10) || 0;
288
+ const bNum = parseInt(b.id.replace(/\D/g, ""), 10) || 0;
289
+ // Completed: descending (most recent first), Active: ascending
290
+ return source === "completed" && completedSortByIdDesc
291
+ ? bNum - aNum
292
+ : aNum - bNum;
293
+ });
309
294
  const total = entries.length;
310
295
  const pageEntries = entries.slice(currentOffset, currentOffset + limit);
311
296
  // Load only the tasks for this page
@@ -345,6 +330,10 @@ export class Core {
345
330
  if (filter?.priority) {
346
331
  tasks = tasks.filter((t) => t.priority === filter.priority);
347
332
  }
333
+ if (filter?.milestone) {
334
+ const filterKey = milestoneKey(filter.milestone);
335
+ tasks = tasks.filter((t) => milestoneKey(t.milestone) === filterKey);
336
+ }
348
337
  if (filter?.labels && filter.labels.length > 0) {
349
338
  tasks = tasks.filter((t) => filter.labels.some((label) => t.labels.includes(label)));
350
339
  }
@@ -365,6 +354,231 @@ export class Core {
365
354
  const tasks = Array.from(this.tasks.values());
366
355
  return groupTasksByStatus(tasks, this.config.statuses);
367
356
  }
357
+ /**
358
+ * Get tasks grouped by milestone
359
+ *
360
+ * Returns a MilestoneSummary with:
361
+ * - milestones: List of milestone IDs in display order
362
+ * - buckets: Array of MilestoneBucket with tasks, progress, status counts
363
+ *
364
+ * The first bucket is always "Tasks without milestone".
365
+ * Each bucket includes progress percentage based on done status.
366
+ *
367
+ * @example
368
+ * ```typescript
369
+ * const summary = core.getTasksByMilestone();
370
+ * for (const bucket of summary.buckets) {
371
+ * console.log(`${bucket.label}: ${bucket.progress}% complete`);
372
+ * console.log(` ${bucket.doneCount}/${bucket.total} tasks done`);
373
+ * }
374
+ * ```
375
+ */
376
+ getTasksByMilestone() {
377
+ this.ensureInitialized();
378
+ const tasks = Array.from(this.tasks.values());
379
+ return groupTasksByMilestone(tasks, this.config.milestones, this.config.statuses);
380
+ }
381
+ // =========================================================================
382
+ // Milestone CRUD Operations
383
+ // =========================================================================
384
+ /**
385
+ * Get the milestones directory path
386
+ */
387
+ getMilestonesDir() {
388
+ return this.fs.join(this.projectRoot, "backlog", "milestones");
389
+ }
390
+ /**
391
+ * List all milestones from the milestones directory
392
+ *
393
+ * @returns Array of Milestone objects sorted by ID
394
+ */
395
+ async listMilestones() {
396
+ const milestonesDir = this.getMilestonesDir();
397
+ // Check if directory exists
398
+ if (!(await this.fs.exists(milestonesDir))) {
399
+ return [];
400
+ }
401
+ const entries = await this.fs.readDir(milestonesDir);
402
+ const milestones = [];
403
+ for (const entry of entries) {
404
+ // Skip non-milestone files
405
+ if (!entry.endsWith(".md"))
406
+ continue;
407
+ if (entry.toLowerCase() === "readme.md")
408
+ continue;
409
+ const milestoneId = extractMilestoneIdFromFilename(entry);
410
+ if (!milestoneId)
411
+ continue;
412
+ const filepath = this.fs.join(milestonesDir, entry);
413
+ // Skip directories
414
+ if (await this.fs.isDirectory(filepath))
415
+ continue;
416
+ try {
417
+ const content = await this.fs.readFile(filepath);
418
+ milestones.push(parseMilestoneMarkdown(content));
419
+ }
420
+ catch (error) {
421
+ console.warn(`Failed to parse milestone file ${filepath}:`, error);
422
+ }
423
+ }
424
+ // Sort by ID for consistent ordering
425
+ return milestones.sort((a, b) => a.id.localeCompare(b.id, undefined, { numeric: true }));
426
+ }
427
+ /**
428
+ * Load a single milestone by ID
429
+ *
430
+ * @param id - Milestone ID (e.g., "m-0")
431
+ * @returns Milestone or null if not found
432
+ */
433
+ async loadMilestone(id) {
434
+ const milestonesDir = this.getMilestonesDir();
435
+ if (!(await this.fs.exists(milestonesDir))) {
436
+ return null;
437
+ }
438
+ const entries = await this.fs.readDir(milestonesDir);
439
+ // Find file matching the ID
440
+ const milestoneFile = entries.find((entry) => {
441
+ const fileId = extractMilestoneIdFromFilename(entry);
442
+ return fileId === id;
443
+ });
444
+ if (!milestoneFile) {
445
+ return null;
446
+ }
447
+ const filepath = this.fs.join(milestonesDir, milestoneFile);
448
+ try {
449
+ const content = await this.fs.readFile(filepath);
450
+ return parseMilestoneMarkdown(content);
451
+ }
452
+ catch {
453
+ return null;
454
+ }
455
+ }
456
+ /**
457
+ * Create a new milestone
458
+ *
459
+ * @param input - Milestone creation input
460
+ * @returns Created milestone
461
+ */
462
+ async createMilestone(input) {
463
+ const milestonesDir = this.getMilestonesDir();
464
+ // Ensure milestones directory exists
465
+ await this.fs.createDir(milestonesDir, { recursive: true });
466
+ // Find next available milestone ID
467
+ const entries = await this.fs.readDir(milestonesDir).catch(() => []);
468
+ const existingIds = entries
469
+ .map((f) => {
470
+ const match = f.match(/^m-(\d+)/);
471
+ return match?.[1] ? parseInt(match[1], 10) : -1;
472
+ })
473
+ .filter((id) => id >= 0);
474
+ const nextId = existingIds.length > 0 ? Math.max(...existingIds) + 1 : 0;
475
+ const id = `m-${nextId}`;
476
+ const description = input.description || `Milestone: ${input.title}`;
477
+ // Create a temporary milestone to generate content
478
+ const tempMilestone = {
479
+ id,
480
+ title: input.title,
481
+ description,
482
+ rawContent: "",
483
+ tasks: [],
484
+ };
485
+ // Generate content
486
+ const content = serializeMilestoneMarkdown(tempMilestone);
487
+ // Create the final milestone with correct rawContent
488
+ const milestone = {
489
+ id,
490
+ title: input.title,
491
+ description,
492
+ rawContent: content,
493
+ tasks: [],
494
+ };
495
+ // Write file
496
+ const filename = getMilestoneFilename(id, input.title);
497
+ const filepath = this.fs.join(milestonesDir, filename);
498
+ await this.fs.writeFile(filepath, content);
499
+ return milestone;
500
+ }
501
+ /**
502
+ * Update an existing milestone
503
+ *
504
+ * @param id - Milestone ID to update
505
+ * @param input - Fields to update
506
+ * @returns Updated milestone or null if not found
507
+ */
508
+ async updateMilestone(id, input) {
509
+ const existing = await this.loadMilestone(id);
510
+ if (!existing) {
511
+ return null;
512
+ }
513
+ const milestonesDir = this.getMilestonesDir();
514
+ const entries = await this.fs.readDir(milestonesDir);
515
+ // Find the current file
516
+ const currentFile = entries.find((entry) => {
517
+ const fileId = extractMilestoneIdFromFilename(entry);
518
+ return fileId === id;
519
+ });
520
+ if (!currentFile) {
521
+ return null;
522
+ }
523
+ // Build updated values
524
+ const newTitle = input.title ?? existing.title;
525
+ const newDescription = input.description ?? existing.description;
526
+ // Create a temporary milestone to generate content
527
+ const tempMilestone = {
528
+ id: existing.id,
529
+ title: newTitle,
530
+ description: newDescription,
531
+ rawContent: "",
532
+ tasks: existing.tasks,
533
+ };
534
+ // Generate new content
535
+ const content = serializeMilestoneMarkdown(tempMilestone);
536
+ // Create the final updated milestone
537
+ const updated = {
538
+ id: existing.id,
539
+ title: newTitle,
540
+ description: newDescription,
541
+ rawContent: content,
542
+ tasks: existing.tasks,
543
+ };
544
+ // Delete old file
545
+ const oldPath = this.fs.join(milestonesDir, currentFile);
546
+ await this.fs.deleteFile(oldPath);
547
+ // Write new file (with potentially new filename if title changed)
548
+ const newFilename = getMilestoneFilename(id, updated.title);
549
+ const newPath = this.fs.join(milestonesDir, newFilename);
550
+ await this.fs.writeFile(newPath, content);
551
+ return updated;
552
+ }
553
+ /**
554
+ * Delete a milestone
555
+ *
556
+ * @param id - Milestone ID to delete
557
+ * @returns true if deleted, false if not found
558
+ */
559
+ async deleteMilestone(id) {
560
+ const milestonesDir = this.getMilestonesDir();
561
+ if (!(await this.fs.exists(milestonesDir))) {
562
+ return false;
563
+ }
564
+ const entries = await this.fs.readDir(milestonesDir);
565
+ // Find file matching the ID
566
+ const milestoneFile = entries.find((entry) => {
567
+ const fileId = extractMilestoneIdFromFilename(entry);
568
+ return fileId === id;
569
+ });
570
+ if (!milestoneFile) {
571
+ return false;
572
+ }
573
+ const filepath = this.fs.join(milestonesDir, milestoneFile);
574
+ try {
575
+ await this.fs.deleteFile(filepath);
576
+ return true;
577
+ }
578
+ catch {
579
+ return false;
580
+ }
581
+ }
368
582
  /**
369
583
  * Get a single task by ID
370
584
  *
@@ -503,6 +717,10 @@ export class Core {
503
717
  if (filter.priority) {
504
718
  result = result.filter((t) => t.priority === filter.priority);
505
719
  }
720
+ if (filter.milestone) {
721
+ const filterKey = milestoneKey(filter.milestone);
722
+ result = result.filter((t) => milestoneKey(t.milestone) === filterKey);
723
+ }
506
724
  if (filter.labels && filter.labels.length > 0) {
507
725
  result = result.filter((t) => filter.labels.some((label) => t.labels.includes(label)));
508
726
  }
@@ -536,5 +754,293 @@ export class Core {
536
754
  }
537
755
  }
538
756
  }
757
+ // =========================================================================
758
+ // Milestone-Task Sync Helpers
759
+ // =========================================================================
760
+ /**
761
+ * Add a task ID to a milestone's tasks array
762
+ *
763
+ * @param taskId - Task ID to add
764
+ * @param milestoneId - Milestone ID to update
765
+ */
766
+ async addTaskToMilestone(taskId, milestoneId) {
767
+ const milestone = await this.loadMilestone(milestoneId);
768
+ if (!milestone) {
769
+ console.warn(`Milestone ${milestoneId} not found when adding task ${taskId}`);
770
+ return;
771
+ }
772
+ // Check if task already in milestone
773
+ if (milestone.tasks.includes(taskId)) {
774
+ return;
775
+ }
776
+ // Update milestone with new task
777
+ const updatedMilestone = {
778
+ ...milestone,
779
+ tasks: [...milestone.tasks, taskId],
780
+ };
781
+ await this.writeMilestoneFile(updatedMilestone);
782
+ }
783
+ /**
784
+ * Remove a task ID from a milestone's tasks array
785
+ *
786
+ * @param taskId - Task ID to remove
787
+ * @param milestoneId - Milestone ID to update
788
+ */
789
+ async removeTaskFromMilestone(taskId, milestoneId) {
790
+ const milestone = await this.loadMilestone(milestoneId);
791
+ if (!milestone) {
792
+ return;
793
+ }
794
+ // Check if task is in milestone
795
+ if (!milestone.tasks.includes(taskId)) {
796
+ return;
797
+ }
798
+ // Update milestone without the task
799
+ const updatedMilestone = {
800
+ ...milestone,
801
+ tasks: milestone.tasks.filter((id) => id !== taskId),
802
+ };
803
+ await this.writeMilestoneFile(updatedMilestone);
804
+ }
805
+ /**
806
+ * Write a milestone to disk
807
+ */
808
+ async writeMilestoneFile(milestone) {
809
+ const milestonesDir = this.getMilestonesDir();
810
+ const entries = await this.fs.readDir(milestonesDir).catch(() => []);
811
+ // Find and delete the current file
812
+ const currentFile = entries.find((entry) => {
813
+ const fileId = extractMilestoneIdFromFilename(entry);
814
+ return fileId === milestone.id;
815
+ });
816
+ if (currentFile) {
817
+ const oldPath = this.fs.join(milestonesDir, currentFile);
818
+ await this.fs.deleteFile(oldPath).catch(() => { });
819
+ }
820
+ // Write new file
821
+ const content = serializeMilestoneMarkdown(milestone);
822
+ const filename = getMilestoneFilename(milestone.id, milestone.title);
823
+ const filepath = this.fs.join(milestonesDir, filename);
824
+ await this.fs.writeFile(filepath, content);
825
+ }
826
+ /**
827
+ * Get the tasks directory path
828
+ */
829
+ getTasksDir() {
830
+ return this.fs.join(this.projectRoot, "backlog", "tasks");
831
+ }
832
+ // =========================================================================
833
+ // Task CRUD Operations
834
+ // =========================================================================
835
+ /**
836
+ * Create a new task
837
+ *
838
+ * @param input - Task creation input
839
+ * @returns Created task
840
+ */
841
+ async createTask(input) {
842
+ this.ensureInitialized();
843
+ const tasksDir = this.getTasksDir();
844
+ // Ensure tasks directory exists
845
+ await this.fs.createDir(tasksDir, { recursive: true });
846
+ // Generate next task ID
847
+ const existingIds = Array.from(this.tasks.keys())
848
+ .map((id) => parseInt(id.replace(/\D/g, ""), 10))
849
+ .filter((n) => !isNaN(n));
850
+ const nextId = existingIds.length > 0 ? Math.max(...existingIds) + 1 : 1;
851
+ const taskId = String(nextId);
852
+ // Build task object
853
+ const now = new Date().toISOString().split("T")[0];
854
+ const task = {
855
+ id: taskId,
856
+ title: input.title,
857
+ status: input.status || this.config.defaultStatus || "To Do",
858
+ priority: input.priority,
859
+ assignee: input.assignee || [],
860
+ createdDate: now,
861
+ labels: input.labels || [],
862
+ milestone: input.milestone,
863
+ dependencies: input.dependencies || [],
864
+ parentTaskId: input.parentTaskId,
865
+ description: input.description,
866
+ implementationPlan: input.implementationPlan,
867
+ implementationNotes: input.implementationNotes,
868
+ acceptanceCriteriaItems: input.acceptanceCriteria?.map((ac, i) => ({
869
+ index: i + 1,
870
+ text: ac.text,
871
+ checked: ac.checked || false,
872
+ })),
873
+ rawContent: input.rawContent,
874
+ source: "local",
875
+ };
876
+ // Serialize and write file
877
+ const content = serializeTaskMarkdown(task);
878
+ const safeTitle = input.title
879
+ .replace(/[<>:"/\\|?*]/g, "")
880
+ .replace(/\s+/g, " ")
881
+ .slice(0, 50);
882
+ const filename = `${taskId} - ${safeTitle}.md`;
883
+ const filepath = this.fs.join(tasksDir, filename);
884
+ await this.fs.writeFile(filepath, content);
885
+ // Update in-memory cache
886
+ task.filePath = filepath;
887
+ this.tasks.set(taskId, task);
888
+ // Sync milestone if specified
889
+ if (input.milestone) {
890
+ await this.addTaskToMilestone(taskId, input.milestone);
891
+ }
892
+ return task;
893
+ }
894
+ /**
895
+ * Update an existing task
896
+ *
897
+ * @param id - Task ID to update
898
+ * @param input - Fields to update
899
+ * @returns Updated task or null if not found
900
+ */
901
+ async updateTask(id, input) {
902
+ this.ensureInitialized();
903
+ const existing = this.tasks.get(id);
904
+ if (!existing) {
905
+ return null;
906
+ }
907
+ const oldMilestone = existing.milestone;
908
+ const newMilestone = input.milestone === null
909
+ ? undefined
910
+ : input.milestone !== undefined
911
+ ? input.milestone
912
+ : oldMilestone;
913
+ // Build updated task
914
+ const now = new Date().toISOString().split("T")[0];
915
+ const updated = {
916
+ ...existing,
917
+ title: input.title ?? existing.title,
918
+ status: input.status ?? existing.status,
919
+ priority: input.priority ?? existing.priority,
920
+ milestone: newMilestone,
921
+ updatedDate: now,
922
+ description: input.description ?? existing.description,
923
+ implementationPlan: input.clearImplementationPlan
924
+ ? undefined
925
+ : input.implementationPlan ?? existing.implementationPlan,
926
+ implementationNotes: input.clearImplementationNotes
927
+ ? undefined
928
+ : input.implementationNotes ?? existing.implementationNotes,
929
+ ordinal: input.ordinal ?? existing.ordinal,
930
+ dependencies: input.dependencies ?? existing.dependencies,
931
+ };
932
+ // Handle label operations
933
+ if (input.labels) {
934
+ updated.labels = input.labels;
935
+ }
936
+ else {
937
+ if (input.addLabels) {
938
+ updated.labels = [...new Set([...updated.labels, ...input.addLabels])];
939
+ }
940
+ if (input.removeLabels) {
941
+ updated.labels = updated.labels.filter((l) => !input.removeLabels.includes(l));
942
+ }
943
+ }
944
+ // Handle assignee
945
+ if (input.assignee) {
946
+ updated.assignee = input.assignee;
947
+ }
948
+ // Handle dependency operations
949
+ if (input.addDependencies) {
950
+ updated.dependencies = [
951
+ ...new Set([...updated.dependencies, ...input.addDependencies]),
952
+ ];
953
+ }
954
+ if (input.removeDependencies) {
955
+ updated.dependencies = updated.dependencies.filter((d) => !input.removeDependencies.includes(d));
956
+ }
957
+ // Handle acceptance criteria
958
+ if (input.acceptanceCriteria) {
959
+ updated.acceptanceCriteriaItems = input.acceptanceCriteria.map((ac, i) => ({
960
+ index: i + 1,
961
+ text: ac.text,
962
+ checked: ac.checked || false,
963
+ }));
964
+ }
965
+ // Serialize and write file
966
+ const content = serializeTaskMarkdown(updated);
967
+ // Delete old file if exists
968
+ if (existing.filePath) {
969
+ await this.fs.deleteFile(existing.filePath).catch(() => { });
970
+ }
971
+ // Write new file
972
+ const tasksDir = this.getTasksDir();
973
+ const safeTitle = updated.title
974
+ .replace(/[<>:"/\\|?*]/g, "")
975
+ .replace(/\s+/g, " ")
976
+ .slice(0, 50);
977
+ const filename = `${id} - ${safeTitle}.md`;
978
+ const filepath = this.fs.join(tasksDir, filename);
979
+ await this.fs.writeFile(filepath, content);
980
+ // Update in-memory cache
981
+ updated.filePath = filepath;
982
+ this.tasks.set(id, updated);
983
+ // Handle milestone sync
984
+ const milestoneChanged = milestoneKey(oldMilestone) !== milestoneKey(newMilestone);
985
+ if (milestoneChanged) {
986
+ // Remove from old milestone
987
+ if (oldMilestone) {
988
+ await this.removeTaskFromMilestone(id, oldMilestone);
989
+ }
990
+ // Add to new milestone
991
+ if (newMilestone) {
992
+ await this.addTaskToMilestone(id, newMilestone);
993
+ }
994
+ }
995
+ return updated;
996
+ }
997
+ /**
998
+ * Delete a task
999
+ *
1000
+ * @param id - Task ID to delete
1001
+ * @returns true if deleted, false if not found
1002
+ */
1003
+ async deleteTask(id) {
1004
+ this.ensureInitialized();
1005
+ const task = this.tasks.get(id);
1006
+ if (!task) {
1007
+ return false;
1008
+ }
1009
+ // Remove from milestone if assigned
1010
+ if (task.milestone) {
1011
+ await this.removeTaskFromMilestone(id, task.milestone);
1012
+ }
1013
+ // Delete file
1014
+ if (task.filePath) {
1015
+ try {
1016
+ await this.fs.deleteFile(task.filePath);
1017
+ }
1018
+ catch {
1019
+ // File may already be deleted
1020
+ }
1021
+ }
1022
+ // Remove from in-memory cache
1023
+ this.tasks.delete(id);
1024
+ return true;
1025
+ }
1026
+ /**
1027
+ * Load specific tasks by their IDs (for lazy loading milestone tasks)
1028
+ *
1029
+ * @param ids - Array of task IDs to load
1030
+ * @returns Array of loaded tasks (missing tasks excluded)
1031
+ */
1032
+ async loadTasksByIds(ids) {
1033
+ // If fully initialized, return from cache
1034
+ if (this.initialized) {
1035
+ return ids
1036
+ .map((id) => this.tasks.get(id))
1037
+ .filter((t) => t !== undefined);
1038
+ }
1039
+ // If lazy initialized, load on demand
1040
+ if (this.lazyInitialized) {
1041
+ return this.loadTasks(ids);
1042
+ }
1043
+ throw new Error("Core not initialized. Call initialize() or initializeLazy() first.");
1044
+ }
539
1045
  }
540
1046
  //# sourceMappingURL=Core.js.map