@basementuniverse/kanbn 0.9.0 → 1.0.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 (46) hide show
  1. package/docs/commands/burndown.txt +7 -1
  2. package/docs/commands/gantt.txt +36 -0
  3. package/docs/commands/help.txt +1 -0
  4. package/docs/index-structure.md +20 -1
  5. package/docs/quick-start.md +1 -1
  6. package/docs/task-structure.md +47 -0
  7. package/example/.kanbn/index.md +63 -0
  8. package/example/.kanbn/tasks/add-basic-activity-feed.md +53 -0
  9. package/example/.kanbn/tasks/add-passwordless-login-option.md +34 -0
  10. package/example/.kanbn/tasks/add-usage-alert-email-thresholds.md +35 -0
  11. package/example/.kanbn/tasks/build-email-template-system.md +30 -0
  12. package/example/.kanbn/tasks/build-invoice-download-endpoint.md +43 -0
  13. package/example/.kanbn/tasks/build-tenant-settings-page.md +48 -0
  14. package/example/.kanbn/tasks/create-organization-switcher.md +53 -0
  15. package/example/.kanbn/tasks/create-sandbox-environment-provisioner.md +36 -0
  16. package/example/.kanbn/tasks/create-self-serve-cancellation-flow.md +38 -0
  17. package/example/.kanbn/tasks/define-product-pricing-strategy.md +37 -0
  18. package/example/.kanbn/tasks/design-onboarding-checklist.md +30 -0
  19. package/example/.kanbn/tasks/design-team-invite-expiry-flow.md +33 -0
  20. package/example/.kanbn/tasks/implement-data-retention-policy-jobs.md +30 -0
  21. package/example/.kanbn/tasks/implement-feature-flags-foundation.md +36 -0
  22. package/example/.kanbn/tasks/implement-project-creation-wizard.md +44 -0
  23. package/example/.kanbn/tasks/implement-stripe-webhook-signature-check.md +54 -0
  24. package/example/.kanbn/tasks/implement-team-permissions-ui.md +57 -0
  25. package/example/.kanbn/tasks/implement-user-signup-and-login.md +62 -0
  26. package/example/.kanbn/tasks/integrate-crm-lead-sync.md +35 -0
  27. package/example/.kanbn/tasks/legal-review-terms-and-privacy.md +30 -0
  28. package/example/.kanbn/tasks/migrate-legacy-events-to-new-schema.md +48 -0
  29. package/example/.kanbn/tasks/optimize-dashboard-first-load.md +52 -0
  30. package/example/.kanbn/tasks/prototype-report-export-scheduler.md +34 -0
  31. package/example/.kanbn/tasks/publish-internal-qa-checklist.md +53 -0
  32. package/example/.kanbn/tasks/setup-ci-pipeline.md +53 -0
  33. package/package.json +3 -2
  34. package/routes/gantt.json +27 -0
  35. package/skills/kanbn-plan/SKILL.md +0 -0
  36. package/src/board.js +21 -2
  37. package/src/controller/add.js +33 -1
  38. package/src/controller/burndown.js +72 -5
  39. package/src/controller/edit.js +68 -1
  40. package/src/controller/find.js +5 -0
  41. package/src/controller/gantt.js +375 -0
  42. package/src/controller/init.js +2 -1
  43. package/src/controller/sort.js +8 -0
  44. package/src/main.d.ts +295 -340
  45. package/src/main.js +2591 -2050
  46. package/src/parse-task.js +211 -3
package/src/parse-task.js CHANGED
@@ -16,7 +16,7 @@ function compileDescription(data) {
16
16
  description.push(data.raw.content.replace(/[\r\n]{3}/g, '\n\n').trim());
17
17
  }
18
18
  for (let heading in data) {
19
- if (['raw', 'Metadata', 'Sub-tasks', 'Relations', 'Comments'].indexOf(heading) !== -1) {
19
+ if (['raw', 'Metadata', 'Sub-tasks', 'Relations', 'History', 'Comments'].indexOf(heading) !== -1) {
20
20
  continue;
21
21
  }
22
22
  description.push(
@@ -65,6 +65,12 @@ function validateMetadataFromMarkdown(metadata) {
65
65
  { type: 'date'}
66
66
  ]
67
67
  },
68
+ 'postponed': {
69
+ oneOf: [
70
+ { type: 'string' },
71
+ { type: 'date'}
72
+ ]
73
+ },
68
74
  'progress': {
69
75
  type: 'number'
70
76
  },
@@ -92,6 +98,7 @@ function validateMetadataFromJSON(metadata) {
92
98
  'started': { type: 'date'},
93
99
  'completed': { type: 'date'},
94
100
  'due': { type: 'date'},
101
+ 'postponed': { type: 'date'},
95
102
  'progress': { type: 'number' },
96
103
  'tags': {
97
104
  type: 'array',
@@ -166,6 +173,105 @@ function validateComments(comments) {
166
173
  }
167
174
  }
168
175
 
176
+ /**
177
+ * Validate the history object converted from Markdown
178
+ * @param {object[]} history
179
+ */
180
+ function validateHistoryFromMarkdown(history) {
181
+ const result = validate(history, {
182
+ type: 'array',
183
+ items: {
184
+ type: 'object',
185
+ properties: {
186
+ 'type': { type: 'string' },
187
+ 'date': {
188
+ oneOf: [
189
+ { type: 'string' },
190
+ { type: 'date' }
191
+ ]
192
+ },
193
+ 'column': { type: 'string' },
194
+ 'fromColumn': { type: 'string' },
195
+ 'toColumn': { type: 'string' },
196
+ 'author': { type: 'string' },
197
+ 'fromProgress': { oneOf: [{ type: 'number' }, { type: 'string' }] },
198
+ 'toProgress': { oneOf: [{ type: 'number' }, { type: 'string' }] }
199
+ }
200
+ }
201
+ });
202
+ if (result.errors.length) {
203
+ throw new Error(result.errors.map(error => `\n${error.property} ${error.message}`).join(''));
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Validate the history object converted from JSON
209
+ * @param {object[]} history
210
+ */
211
+ function validateHistoryFromJSON(history) {
212
+ const result = validate(history, {
213
+ type: 'array',
214
+ items: {
215
+ type: 'object',
216
+ properties: {
217
+ 'type': { type: 'string' },
218
+ 'date': { type: 'date' },
219
+ 'column': { type: 'string' },
220
+ 'fromColumn': { type: 'string' },
221
+ 'toColumn': { type: 'string' },
222
+ 'author': { type: 'string' },
223
+ 'fromProgress': { type: 'number' },
224
+ 'toProgress': { type: 'number' }
225
+ }
226
+ }
227
+ });
228
+ if (result.errors.length) {
229
+ throw new Error(result.errors.map(error => `\n${error.property} ${error.message}`).join(''));
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Validate required fields in a history event
235
+ * @param {object} historyEvent
236
+ */
237
+ function validateHistoryEvent(historyEvent) {
238
+ if (!historyEvent.type) {
239
+ throw new Error('history event is missing type');
240
+ }
241
+ if (!historyEvent.date) {
242
+ throw new Error(`history event "${historyEvent.type}" is missing date`);
243
+ }
244
+ switch (historyEvent.type) {
245
+ case 'created':
246
+ if (!historyEvent.column) {
247
+ throw new Error('created history event is missing column');
248
+ }
249
+ break;
250
+ case 'moved':
251
+ if (!historyEvent.fromColumn || !historyEvent.toColumn) {
252
+ throw new Error('moved history event is missing fromColumn or toColumn');
253
+ }
254
+ break;
255
+ case 'progress':
256
+ if (historyEvent.fromProgress === undefined || historyEvent.toProgress === undefined) {
257
+ throw new Error('progress history event is missing fromProgress or toProgress');
258
+ }
259
+ break;
260
+ case 'archived':
261
+ if (!historyEvent.fromColumn) {
262
+ throw new Error('archived history event is missing fromColumn');
263
+ }
264
+ break;
265
+ case 'restored':
266
+ if (!historyEvent.toColumn) {
267
+ throw new Error('restored history event is missing toColumn');
268
+ }
269
+ break;
270
+ default:
271
+ throw new Error(`unsupported history event type "${historyEvent.type}"`);
272
+ }
273
+ }
274
+
169
275
  module.exports = {
170
276
 
171
277
  /**
@@ -174,7 +280,7 @@ module.exports = {
174
280
  * @return {object}
175
281
  */
176
282
  md2json(data) {
177
- let id = '', name = '', description = '', metadata = {}, subTasks = [], relations = [], comments = [];
283
+ let id = '', name = '', description = '', metadata = {}, subTasks = [], relations = [], history = [], comments = [];
178
284
  try {
179
285
 
180
286
  // Check data type
@@ -267,6 +373,13 @@ module.exports = {
267
373
  }
268
374
  metadata.due = dateValue;
269
375
  }
376
+ if ('postponed' in metadata && !(metadata.postponed instanceof Date)) {
377
+ const dateValue = chrono.parseDate(metadata.postponed);
378
+ if (dateValue === null) {
379
+ throw new Error('unable to parse postponed date');
380
+ }
381
+ metadata.postponed = dateValue;
382
+ }
270
383
 
271
384
  // Check progress value
272
385
  if ('progress' in metadata) {
@@ -349,6 +462,62 @@ module.exports = {
349
462
  delete task['Comments'];
350
463
  }
351
464
 
465
+ // Parse history
466
+ if ('History' in task) {
467
+ try {
468
+ const parsedHistory = marked.lexer(task['History'].content)[0].items;
469
+ for (let parsedHistoryEvent of parsedHistory) {
470
+ const historyEvent = {};
471
+ const parts = parsedHistoryEvent.text.split('\n');
472
+ for (let part of parts) {
473
+ const parsedPart = part.match(/^([A-Za-z][A-Za-z0-9]*):\s*(.*)$/);
474
+ if (!parsedPart) {
475
+ continue;
476
+ }
477
+ const key = parsedPart[1];
478
+ historyEvent[key] = parsedPart[2].trim();
479
+ }
480
+ history.push(historyEvent);
481
+ }
482
+ } catch (error) {
483
+ throw new Error('history must contain a list');
484
+ }
485
+ delete task['History'];
486
+
487
+ // Validate basic history structure before value coercion
488
+ validateHistoryFromMarkdown(history);
489
+
490
+ // Parse and validate event dates and numeric fields
491
+ history = history.map((historyEvent) => {
492
+ if (!(historyEvent.date instanceof Date)) {
493
+ const dateValue = chrono.parseDate(historyEvent.date);
494
+ if (dateValue === null) {
495
+ throw new Error(`unable to parse history event date for "${historyEvent.type || 'unknown'}"`);
496
+ }
497
+ historyEvent.date = dateValue;
498
+ }
499
+
500
+ if ('fromProgress' in historyEvent) {
501
+ const fromProgressValue = parseFloat(historyEvent.fromProgress);
502
+ if (isNaN(fromProgressValue)) {
503
+ throw new Error('history event fromProgress value is not numeric');
504
+ }
505
+ historyEvent.fromProgress = fromProgressValue;
506
+ }
507
+
508
+ if ('toProgress' in historyEvent) {
509
+ const toProgressValue = parseFloat(historyEvent.toProgress);
510
+ if (isNaN(toProgressValue)) {
511
+ throw new Error('history event toProgress value is not numeric');
512
+ }
513
+ historyEvent.toProgress = toProgressValue;
514
+ }
515
+
516
+ validateHistoryEvent(historyEvent);
517
+ return historyEvent;
518
+ });
519
+ }
520
+
352
521
  // Assemble description
353
522
  // const descriptionParts = [];
354
523
  description = compileDescription(task);
@@ -358,7 +527,11 @@ module.exports = {
358
527
  }
359
528
 
360
529
  // Assemble task object
361
- return { id, name, description, metadata, subTasks, relations, comments };
530
+ const result = { id, name, description, metadata, subTasks, relations, comments };
531
+ if (history.length > 0) {
532
+ result.history = history;
533
+ }
534
+ return result;
362
535
  },
363
536
 
364
537
  /**
@@ -443,6 +616,41 @@ module.exports = {
443
616
  );
444
617
  }
445
618
  }
619
+
620
+ // Add history if present
621
+ if ('history' in data && data.history !== null) {
622
+ validateHistoryFromJSON(data.history);
623
+ data.history.forEach(validateHistoryEvent);
624
+ if (data.history.length > 0) {
625
+ result.push(
626
+ '## History',
627
+ data.history.map((historyEvent) => {
628
+ const historyEventOutput = [];
629
+ historyEventOutput.push(`type: ${historyEvent.type}`);
630
+ historyEventOutput.push(`date: ${historyEvent.date.toISOString()}`);
631
+ if ('column' in historyEvent) {
632
+ historyEventOutput.push(`column: ${historyEvent.column}`);
633
+ }
634
+ if ('fromColumn' in historyEvent) {
635
+ historyEventOutput.push(`fromColumn: ${historyEvent.fromColumn}`);
636
+ }
637
+ if ('toColumn' in historyEvent) {
638
+ historyEventOutput.push(`toColumn: ${historyEvent.toColumn}`);
639
+ }
640
+ if ('fromProgress' in historyEvent) {
641
+ historyEventOutput.push(`fromProgress: ${historyEvent.fromProgress}`);
642
+ }
643
+ if ('toProgress' in historyEvent) {
644
+ historyEventOutput.push(`toProgress: ${historyEvent.toProgress}`);
645
+ }
646
+ if ('author' in historyEvent && historyEvent.author) {
647
+ historyEventOutput.push(`author: ${historyEvent.author}`);
648
+ }
649
+ return `- ${historyEventOutput.map((v, i) => i > 0 ? ` ${v}` : v).join('\n')}`;
650
+ }).join('\n')
651
+ );
652
+ }
653
+ }
446
654
  } catch (error) {
447
655
  throw new Error(`Unable to build task: ${error.message}`);
448
656
  }