roadmapsmith 0.2.0 → 0.5.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/package.json +40 -41
- package/src/config.js +211 -187
- package/src/generator/index.js +233 -107
- package/src/index.js +10 -7
- package/src/io.js +263 -227
- package/src/match.js +85 -85
- package/src/model.js +31 -27
- package/src/parser/index.js +106 -108
- package/src/renderer/compact.js +89 -0
- package/src/renderer/helpers.js +19 -0
- package/src/renderer/index.js +18 -0
- package/src/renderer/professional.js +474 -0
- package/src/utils.js +141 -142
- package/src/validator/index.js +22 -3
package/src/model.js
CHANGED
|
@@ -1,28 +1,32 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const PHASE_ORDER = ['P0', 'P1', 'P2'];
|
|
4
|
-
|
|
5
|
-
function phaseWeight(phase) {
|
|
6
|
-
const idx = PHASE_ORDER.indexOf(phase);
|
|
7
|
-
return idx >= 0 ? idx : PHASE_ORDER.length;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function createRoadmapModel(input) {
|
|
11
|
-
return {
|
|
12
|
-
northStar: input.northStar,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const PHASE_ORDER = ['P0', 'P1', 'P2'];
|
|
4
|
+
|
|
5
|
+
function phaseWeight(phase) {
|
|
6
|
+
const idx = PHASE_ORDER.indexOf(phase);
|
|
7
|
+
return idx >= 0 ? idx : PHASE_ORDER.length;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function createRoadmapModel(input) {
|
|
11
|
+
return {
|
|
12
|
+
northStar: input.northStar,
|
|
13
|
+
product: input.product || {},
|
|
14
|
+
currentState: input.currentState,
|
|
15
|
+
phases: input.phases,
|
|
16
|
+
steps: input.steps || [],
|
|
17
|
+
phasesDetailed: input.phasesDetailed || [],
|
|
18
|
+
milestones: input.milestones,
|
|
19
|
+
commandBreakdown: input.commandBreakdown,
|
|
20
|
+
exitCriteria: input.exitCriteria,
|
|
21
|
+
risks: input.risks,
|
|
22
|
+
antiGoals: input.antiGoals,
|
|
23
|
+
successCriteria: input.successCriteria || [],
|
|
24
|
+
customSections: input.customSections || [],
|
|
25
|
+
checkedById: input.checkedById || {}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = {
|
|
30
|
+
PHASE_ORDER,
|
|
31
|
+
createRoadmapModel
|
|
28
32
|
};
|
package/src/parser/index.js
CHANGED
|
@@ -1,109 +1,107 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { slugify } = require('../utils');
|
|
4
|
-
|
|
5
|
-
const TASK_LINE_RE = /^(\s*)- \[( |x|X)\] (.*?)(?:\s*<!--\s*rs:task=([a-z0-9-]+)\s*-->)?\s*$/;
|
|
6
|
-
const WARNING_RE = /^\s*-\s+⚠️ attempted but validation failed:\s*(.+?)\s*$/;
|
|
7
|
-
const HEADING_RE = /^#{2,3}\s+(.*)$/;
|
|
8
|
-
|
|
9
|
-
const MANAGED_START = '<!-- rs:managed:start -->';
|
|
10
|
-
const MANAGED_END = '<!-- rs:managed:end -->';
|
|
11
|
-
|
|
12
|
-
function parseRoadmap(content) {
|
|
13
|
-
const lines = String(content || '').split(/\r?\n/);
|
|
14
|
-
const tasks = [];
|
|
15
|
-
let section = '';
|
|
16
|
-
|
|
17
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
18
|
-
const line = lines[index];
|
|
19
|
-
const headingMatch = line.match(HEADING_RE);
|
|
20
|
-
if (headingMatch) {
|
|
21
|
-
section = headingMatch[1].trim();
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const taskMatch = line.match(TASK_LINE_RE);
|
|
25
|
-
if (!taskMatch) {
|
|
26
|
-
continue;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const indent = taskMatch[1] || '';
|
|
30
|
-
const checked = taskMatch[2].toLowerCase() === 'x';
|
|
31
|
-
const text = taskMatch[3].trim();
|
|
32
|
-
const markerId = taskMatch[4] || null;
|
|
33
|
-
|
|
34
|
-
let warningLineIndex = null;
|
|
35
|
-
let warningText = null;
|
|
36
|
-
if (index + 1 < lines.length) {
|
|
37
|
-
const nextLine = lines[index + 1];
|
|
38
|
-
const warningMatch = nextLine.match(WARNING_RE);
|
|
39
|
-
if (warningMatch) {
|
|
40
|
-
warningLineIndex = index + 1;
|
|
41
|
-
warningText = warningMatch[1].trim();
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const id = markerId || slugify(text);
|
|
46
|
-
tasks.push({
|
|
47
|
-
id,
|
|
48
|
-
text,
|
|
49
|
-
checked,
|
|
50
|
-
lineIndex: index,
|
|
51
|
-
warningLineIndex,
|
|
52
|
-
warningText,
|
|
53
|
-
markerId,
|
|
54
|
-
indent,
|
|
55
|
-
section
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return {
|
|
60
|
-
lines,
|
|
61
|
-
tasks
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function findManagedRange(lines) {
|
|
66
|
-
let start = -1;
|
|
67
|
-
let end = -1;
|
|
68
|
-
|
|
69
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
70
|
-
if (lines[i].trim() === MANAGED_START) {
|
|
71
|
-
start = i;
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
if (lines[i].trim() === MANAGED_END) {
|
|
75
|
-
end = i;
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (start >= 0 && end >= 0 && start < end) {
|
|
81
|
-
return { start, end };
|
|
82
|
-
}
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function upsertManagedBlock(existingContent, managedBody) {
|
|
87
|
-
const existing = String(existingContent || '');
|
|
88
|
-
const lines = existing.split(/\r?\n/);
|
|
89
|
-
const range = findManagedRange(lines);
|
|
90
|
-
const bodyLines = managedBody.split(/\r?\n/);
|
|
91
|
-
|
|
92
|
-
if (!range) {
|
|
93
|
-
if (existing.trim().length === 0) {
|
|
94
|
-
return [MANAGED_START, ...bodyLines, MANAGED_END].join('\n');
|
|
95
|
-
}
|
|
96
|
-
return `${existing.replace(/\s+$/, '')}\n\n${MANAGED_START}\n${managedBody}\n${MANAGED_END}`;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const prefix = lines.slice(0, range.start + 1);
|
|
100
|
-
const suffix = lines.slice(range.end);
|
|
101
|
-
return [...prefix, ...bodyLines, ...suffix].join('\n');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
module.exports = {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
parseRoadmap,
|
|
108
|
-
upsertManagedBlock
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { slugify } = require('../utils');
|
|
4
|
+
|
|
5
|
+
const TASK_LINE_RE = /^(\s*)- \[( |x|X)\] (.*?)(?:\s*<!--\s*rs:task=([a-z0-9-]+)\s*-->)?\s*$/;
|
|
6
|
+
const WARNING_RE = /^\s*-\s+⚠️ attempted but validation failed:\s*(.+?)\s*$/;
|
|
7
|
+
const HEADING_RE = /^#{2,3}\s+(.*)$/;
|
|
8
|
+
|
|
9
|
+
const MANAGED_START = '<!-- rs:managed:start -->';
|
|
10
|
+
const MANAGED_END = '<!-- rs:managed:end -->';
|
|
11
|
+
|
|
12
|
+
function parseRoadmap(content) {
|
|
13
|
+
const lines = String(content || '').split(/\r?\n/);
|
|
14
|
+
const tasks = [];
|
|
15
|
+
let section = '';
|
|
16
|
+
|
|
17
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
18
|
+
const line = lines[index];
|
|
19
|
+
const headingMatch = line.match(HEADING_RE);
|
|
20
|
+
if (headingMatch) {
|
|
21
|
+
section = headingMatch[1].trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const taskMatch = line.match(TASK_LINE_RE);
|
|
25
|
+
if (!taskMatch) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const indent = taskMatch[1] || '';
|
|
30
|
+
const checked = taskMatch[2].toLowerCase() === 'x';
|
|
31
|
+
const text = taskMatch[3].trim();
|
|
32
|
+
const markerId = taskMatch[4] || null;
|
|
33
|
+
|
|
34
|
+
let warningLineIndex = null;
|
|
35
|
+
let warningText = null;
|
|
36
|
+
if (index + 1 < lines.length) {
|
|
37
|
+
const nextLine = lines[index + 1];
|
|
38
|
+
const warningMatch = nextLine.match(WARNING_RE);
|
|
39
|
+
if (warningMatch) {
|
|
40
|
+
warningLineIndex = index + 1;
|
|
41
|
+
warningText = warningMatch[1].trim();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const id = markerId || slugify(text);
|
|
46
|
+
tasks.push({
|
|
47
|
+
id,
|
|
48
|
+
text,
|
|
49
|
+
checked,
|
|
50
|
+
lineIndex: index,
|
|
51
|
+
warningLineIndex,
|
|
52
|
+
warningText,
|
|
53
|
+
markerId,
|
|
54
|
+
indent,
|
|
55
|
+
section
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
lines,
|
|
61
|
+
tasks
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function findManagedRange(lines) {
|
|
66
|
+
let start = -1;
|
|
67
|
+
let end = -1;
|
|
68
|
+
|
|
69
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
70
|
+
if (lines[i].trim() === MANAGED_START) {
|
|
71
|
+
start = i;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (lines[i].trim() === MANAGED_END) {
|
|
75
|
+
end = i;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (start >= 0 && end >= 0 && start < end) {
|
|
81
|
+
return { start, end };
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function upsertManagedBlock(existingContent, managedBody) {
|
|
87
|
+
const existing = String(existingContent || '');
|
|
88
|
+
const lines = existing.split(/\r?\n/);
|
|
89
|
+
const range = findManagedRange(lines);
|
|
90
|
+
const bodyLines = managedBody.split(/\r?\n/);
|
|
91
|
+
|
|
92
|
+
if (!range) {
|
|
93
|
+
if (existing.trim().length === 0) {
|
|
94
|
+
return [MANAGED_START, ...bodyLines, MANAGED_END].join('\n');
|
|
95
|
+
}
|
|
96
|
+
return `${existing.replace(/\s+$/, '')}\n\n${MANAGED_START}\n${managedBody}\n${MANAGED_END}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const prefix = lines.slice(0, range.start + 1);
|
|
100
|
+
const suffix = lines.slice(range.end);
|
|
101
|
+
return [...prefix, ...bodyLines, ...suffix].join('\n');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
parseRoadmap,
|
|
106
|
+
upsertManagedBlock
|
|
109
107
|
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { slugify, ensureTrailingNewline } = require('../utils');
|
|
4
|
+
const { taskLine, checkedState } = require('./helpers');
|
|
5
|
+
|
|
6
|
+
function renderCompact(model) {
|
|
7
|
+
const lines = [];
|
|
8
|
+
|
|
9
|
+
lines.push('# Project Roadmap');
|
|
10
|
+
lines.push('');
|
|
11
|
+
lines.push('## Product North Star');
|
|
12
|
+
lines.push(model.northStar);
|
|
13
|
+
lines.push('');
|
|
14
|
+
|
|
15
|
+
lines.push('## Current State');
|
|
16
|
+
lines.push(`- Implemented surface: ${model.currentState.implementedSummary}`);
|
|
17
|
+
lines.push(`- TODO surface: ${model.currentState.todoSummary}`);
|
|
18
|
+
lines.push(`- Detected stacks: ${model.currentState.stackSummary}`);
|
|
19
|
+
lines.push('');
|
|
20
|
+
|
|
21
|
+
lines.push('## Phased Roadmap');
|
|
22
|
+
lines.push('');
|
|
23
|
+
lines.push('### Phase P0 (Critical)');
|
|
24
|
+
for (const task of model.phases.P0) {
|
|
25
|
+
lines.push(taskLine(task));
|
|
26
|
+
}
|
|
27
|
+
lines.push('');
|
|
28
|
+
lines.push('### Phase P1 (Important)');
|
|
29
|
+
for (const task of model.phases.P1) {
|
|
30
|
+
lines.push(taskLine(task));
|
|
31
|
+
}
|
|
32
|
+
lines.push('');
|
|
33
|
+
lines.push('### Phase P2 (Optimization)');
|
|
34
|
+
for (const task of model.phases.P2) {
|
|
35
|
+
lines.push(taskLine(task));
|
|
36
|
+
}
|
|
37
|
+
lines.push('');
|
|
38
|
+
|
|
39
|
+
lines.push('## Release Milestones');
|
|
40
|
+
for (const milestone of model.milestones) {
|
|
41
|
+
const id = `milestone-${slugify(milestone.version)}`;
|
|
42
|
+
lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] ${milestone.version}: ${milestone.goal} <!-- rs:task=${id} -->`);
|
|
43
|
+
}
|
|
44
|
+
lines.push('');
|
|
45
|
+
|
|
46
|
+
lines.push('## Command/Module Breakdown');
|
|
47
|
+
if (model.commandBreakdown.length === 0) {
|
|
48
|
+
const id = 'identify-command-module-boundaries';
|
|
49
|
+
lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] Identify command/module boundaries for the next increment <!-- rs:task=${id} -->`);
|
|
50
|
+
} else {
|
|
51
|
+
for (const item of model.commandBreakdown) {
|
|
52
|
+
const id = `module-${slugify(item)}`;
|
|
53
|
+
lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] ${item} <!-- rs:task=${id} -->`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
lines.push('');
|
|
57
|
+
|
|
58
|
+
lines.push('## Exit Criteria Per Phase');
|
|
59
|
+
for (const item of model.exitCriteria) {
|
|
60
|
+
const id = `exit-${slugify(item)}`;
|
|
61
|
+
lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] ${item} <!-- rs:task=${id} -->`);
|
|
62
|
+
}
|
|
63
|
+
lines.push('');
|
|
64
|
+
|
|
65
|
+
for (const section of model.customSections) {
|
|
66
|
+
lines.push(`## ${section.title}`);
|
|
67
|
+
for (const line of section.items) {
|
|
68
|
+
lines.push(line);
|
|
69
|
+
}
|
|
70
|
+
lines.push('');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
lines.push('## Risks and Anti-goals');
|
|
74
|
+
lines.push('### Risks');
|
|
75
|
+
for (const risk of model.risks) {
|
|
76
|
+
const id = `risk-${slugify(risk)}`;
|
|
77
|
+
lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] ${risk} <!-- rs:task=${id} -->`);
|
|
78
|
+
}
|
|
79
|
+
lines.push('');
|
|
80
|
+
lines.push('### Anti-goals');
|
|
81
|
+
for (const antiGoal of model.antiGoals) {
|
|
82
|
+
const id = `anti-goal-${slugify(antiGoal)}`;
|
|
83
|
+
lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] ${antiGoal} <!-- rs:task=${id} -->`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return ensureTrailingNewline(lines.join('\n')).trimEnd();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = { renderCompact };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function taskLine(task) {
|
|
4
|
+
return `- [${task.checked ? 'x' : ' '}] ${task.text} <!-- rs:task=${task.id} -->`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function sectionHeader(n, title) {
|
|
8
|
+
return `## ${n}. ${title}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function checkedState(model, id) {
|
|
12
|
+
return Boolean(model.checkedById && model.checkedById[id]);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function priorityLabel(priority) {
|
|
16
|
+
return priority ? `\`[${priority}]\`` : '';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { taskLine, sectionHeader, checkedState, priorityLabel };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { renderCompact } = require('./compact');
|
|
4
|
+
const { renderProfessional } = require('./professional');
|
|
5
|
+
|
|
6
|
+
function renderBody(model, profile) {
|
|
7
|
+
if (profile === 'professional') return renderProfessional(model);
|
|
8
|
+
if (profile === 'enterprise') {
|
|
9
|
+
throw new Error(
|
|
10
|
+
'roadmapProfile "enterprise" is not yet implemented. ' +
|
|
11
|
+
'Use "professional" instead, or contribute via the plugin registry. ' +
|
|
12
|
+
'See docs/use-cases/ for the extension guide.'
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
return renderCompact(model);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { renderBody };
|