@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.
- package/docs/commands/burndown.txt +7 -1
- package/docs/commands/gantt.txt +36 -0
- package/docs/commands/help.txt +1 -0
- package/docs/index-structure.md +20 -1
- package/docs/quick-start.md +1 -1
- package/docs/task-structure.md +47 -0
- package/example/.kanbn/index.md +63 -0
- package/example/.kanbn/tasks/add-basic-activity-feed.md +53 -0
- package/example/.kanbn/tasks/add-passwordless-login-option.md +34 -0
- package/example/.kanbn/tasks/add-usage-alert-email-thresholds.md +35 -0
- package/example/.kanbn/tasks/build-email-template-system.md +30 -0
- package/example/.kanbn/tasks/build-invoice-download-endpoint.md +43 -0
- package/example/.kanbn/tasks/build-tenant-settings-page.md +48 -0
- package/example/.kanbn/tasks/create-organization-switcher.md +53 -0
- package/example/.kanbn/tasks/create-sandbox-environment-provisioner.md +36 -0
- package/example/.kanbn/tasks/create-self-serve-cancellation-flow.md +38 -0
- package/example/.kanbn/tasks/define-product-pricing-strategy.md +37 -0
- package/example/.kanbn/tasks/design-onboarding-checklist.md +30 -0
- package/example/.kanbn/tasks/design-team-invite-expiry-flow.md +33 -0
- package/example/.kanbn/tasks/implement-data-retention-policy-jobs.md +30 -0
- package/example/.kanbn/tasks/implement-feature-flags-foundation.md +36 -0
- package/example/.kanbn/tasks/implement-project-creation-wizard.md +44 -0
- package/example/.kanbn/tasks/implement-stripe-webhook-signature-check.md +54 -0
- package/example/.kanbn/tasks/implement-team-permissions-ui.md +57 -0
- package/example/.kanbn/tasks/implement-user-signup-and-login.md +62 -0
- package/example/.kanbn/tasks/integrate-crm-lead-sync.md +35 -0
- package/example/.kanbn/tasks/legal-review-terms-and-privacy.md +30 -0
- package/example/.kanbn/tasks/migrate-legacy-events-to-new-schema.md +48 -0
- package/example/.kanbn/tasks/optimize-dashboard-first-load.md +52 -0
- package/example/.kanbn/tasks/prototype-report-export-scheduler.md +34 -0
- package/example/.kanbn/tasks/publish-internal-qa-checklist.md +53 -0
- package/example/.kanbn/tasks/setup-ci-pipeline.md +53 -0
- package/package.json +3 -2
- package/routes/gantt.json +27 -0
- package/skills/kanbn-plan/SKILL.md +0 -0
- package/src/board.js +21 -2
- package/src/controller/add.js +33 -1
- package/src/controller/burndown.js +72 -5
- package/src/controller/edit.js +68 -1
- package/src/controller/find.js +5 -0
- package/src/controller/gantt.js +375 -0
- package/src/controller/init.js +2 -1
- package/src/controller/sort.js +8 -0
- package/src/main.d.ts +295 -340
- package/src/main.js +2591 -2050
- 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
|
-
|
|
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
|
}
|