roadmapsmith 0.4.0 → 0.5.1

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/src/model.js CHANGED
@@ -1,33 +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
- 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,
32
- phaseWeight
33
- };
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
32
+ };
@@ -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
- MANAGED_END,
106
- MANAGED_START,
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
  };
@@ -73,6 +73,13 @@ function renderSection3CurrentState(model, lines) {
73
73
  }
74
74
  lines.push('');
75
75
 
76
+ if (model.currentState.workspaces && model.currentState.workspaces.length > 0) {
77
+ lines.push('### Workspace Packages');
78
+ lines.push('');
79
+ lines.push(`- Workspace packages detected: ${model.currentState.workspaces.join(', ')}`);
80
+ lines.push('');
81
+ }
82
+
76
83
  lines.push('### Known Limitations');
77
84
  lines.push('');
78
85
  if (model.currentState.knownLimitations && model.currentState.knownLimitations.length > 0) {
@@ -169,8 +176,10 @@ function renderSection5Milestones(model, lines) {
169
176
  lines.push('**What Must Be Stable:**');
170
177
  lines.push('');
171
178
  for (const item of milestone.mustBeStable) {
172
- const id = `prof-ms-${msSlug}-stable-${slugify(item)}`;
173
- lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] \`[P1]\` ${item} <!-- rs:task=${id} -->`);
179
+ const text = typeof item === 'string' ? item : item.text;
180
+ const note = typeof item === 'object' && item.note ? ` — _${item.note}_` : '';
181
+ const id = `prof-ms-${msSlug}-stable-${slugify(text)}`;
182
+ lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] \`[P1]\` ${text}${note} <!-- rs:task=${id} -->`);
174
183
  }
175
184
  lines.push('');
176
185
  }
package/src/utils.js CHANGED
@@ -1,143 +1,142 @@
1
- 'use strict';
2
-
3
- const path = require('path');
4
-
5
- const STOP_WORDS = new Set([
6
- 'a', 'an', 'and', 'are', 'as', 'at', 'be', 'by', 'for', 'from', 'in', 'into', 'is', 'it', 'of', 'on', 'or', 'that',
7
- 'the', 'to', 'with', 'this', 'these', 'those', 'via', 'per', 'task', 'tasks', 'phase', 'priority'
8
- ]);
9
-
10
- function toPosix(input) {
11
- return input.split(path.sep).join('/');
12
- }
13
-
14
- function slugify(text) {
15
- return String(text || '')
16
- .toLowerCase()
17
- .replace(/[^a-z0-9]+/g, '-')
18
- .replace(/^-+|-+$/g, '')
19
- .replace(/-{2,}/g, '-') || 'task';
20
- }
21
-
22
- function normalizeText(text) {
23
- return String(text || '')
24
- .toLowerCase()
25
- .replace(/[`*_~#>\[\](){}.!?,:;"']/g, ' ')
26
- .replace(/\s+/g, ' ')
27
- .trim();
28
- }
29
-
30
- function tokenize(text) {
31
- return normalizeText(text)
32
- .split(' ')
33
- .filter(Boolean)
34
- .filter((token) => !STOP_WORDS.has(token));
35
- }
36
-
37
- function uniqueBy(items, keyFn) {
38
- const seen = new Set();
39
- const result = [];
40
- for (const item of items) {
41
- const key = keyFn(item);
42
- if (seen.has(key)) {
43
- continue;
44
- }
45
- seen.add(key);
46
- result.push(item);
47
- }
48
- return result;
49
- }
50
-
51
- function similarityScore(left, right) {
52
- const leftTokens = new Set(tokenize(left));
53
- const rightTokens = new Set(tokenize(right));
54
- if (leftTokens.size === 0 || rightTokens.size === 0) {
55
- return 0;
56
- }
57
-
58
- let shared = 0;
59
- for (const token of leftTokens) {
60
- if (rightTokens.has(token)) {
61
- shared += 1;
62
- }
63
- }
64
-
65
- const union = new Set([...leftTokens, ...rightTokens]);
66
- return shared / union.size;
67
- }
68
-
69
- function ensureTrailingNewline(text) {
70
- return text.endsWith('\n') ? text : `${text}\n`;
71
- }
72
-
73
- function escapeRegExp(value) {
74
- return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
75
- }
76
-
77
- function parseArgv(argv) {
78
- const flags = {};
79
- const positionals = [];
80
-
81
- for (let i = 0; i < argv.length; i += 1) {
82
- const current = argv[i];
83
- if (!current.startsWith('-')) {
84
- positionals.push(current);
85
- continue;
86
- }
87
-
88
- if (current.startsWith('--')) {
89
- const withoutPrefix = current.slice(2);
90
- const eqIndex = withoutPrefix.indexOf('=');
91
- let key;
92
- let value;
93
-
94
- if (eqIndex >= 0) {
95
- key = withoutPrefix.slice(0, eqIndex);
96
- value = withoutPrefix.slice(eqIndex + 1);
97
- } else {
98
- key = withoutPrefix;
99
- const next = argv[i + 1];
100
- if (next && !next.startsWith('-')) {
101
- value = next;
102
- i += 1;
103
- } else {
104
- value = true;
105
- }
106
- }
107
-
108
- if (Object.prototype.hasOwnProperty.call(flags, key)) {
109
- if (Array.isArray(flags[key])) {
110
- flags[key].push(value);
111
- } else {
112
- flags[key] = [flags[key], value];
113
- }
114
- } else {
115
- flags[key] = value;
116
- }
117
- continue;
118
- }
119
-
120
- const short = current.slice(1);
121
- flags[short] = true;
122
- }
123
-
124
- const command = positionals.length > 0 ? positionals[0] : null;
125
- return {
126
- command,
127
- args: positionals.slice(1),
128
- flags,
129
- positionals
130
- };
131
- }
132
-
133
- module.exports = {
134
- escapeRegExp,
135
- ensureTrailingNewline,
136
- normalizeText,
137
- parseArgv,
138
- similarityScore,
139
- slugify,
140
- toPosix,
141
- tokenize,
142
- uniqueBy
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+
5
+ const STOP_WORDS = new Set([
6
+ 'a', 'an', 'and', 'are', 'as', 'at', 'be', 'by', 'for', 'from', 'in', 'into', 'is', 'it', 'of', 'on', 'or', 'that',
7
+ 'the', 'to', 'with', 'this', 'these', 'those', 'via', 'per', 'task', 'tasks', 'phase', 'priority'
8
+ ]);
9
+
10
+ function toPosix(input) {
11
+ return input.split(path.sep).join('/');
12
+ }
13
+
14
+ function slugify(text) {
15
+ return String(text || '')
16
+ .toLowerCase()
17
+ .replace(/[^a-z0-9]+/g, '-')
18
+ .replace(/^-+|-+$/g, '')
19
+ .replace(/-{2,}/g, '-') || 'task';
20
+ }
21
+
22
+ function normalizeText(text) {
23
+ return String(text || '')
24
+ .toLowerCase()
25
+ .replace(/[`*_~#>\[\](){}.!?,:;"']/g, ' ')
26
+ .replace(/\s+/g, ' ')
27
+ .trim();
28
+ }
29
+
30
+ function tokenize(text) {
31
+ return normalizeText(text)
32
+ .split(' ')
33
+ .filter(Boolean)
34
+ .filter((token) => !STOP_WORDS.has(token));
35
+ }
36
+
37
+ function uniqueBy(items, keyFn) {
38
+ const seen = new Set();
39
+ const result = [];
40
+ for (const item of items) {
41
+ const key = keyFn(item);
42
+ if (seen.has(key)) {
43
+ continue;
44
+ }
45
+ seen.add(key);
46
+ result.push(item);
47
+ }
48
+ return result;
49
+ }
50
+
51
+ function similarityScore(left, right) {
52
+ const leftTokens = new Set(tokenize(left));
53
+ const rightTokens = new Set(tokenize(right));
54
+ if (leftTokens.size === 0 || rightTokens.size === 0) {
55
+ return 0;
56
+ }
57
+
58
+ let shared = 0;
59
+ for (const token of leftTokens) {
60
+ if (rightTokens.has(token)) {
61
+ shared += 1;
62
+ }
63
+ }
64
+
65
+ const union = new Set([...leftTokens, ...rightTokens]);
66
+ return shared / union.size;
67
+ }
68
+
69
+ function ensureTrailingNewline(text) {
70
+ return text.endsWith('\n') ? text : `${text}\n`;
71
+ }
72
+
73
+ function escapeRegExp(value) {
74
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
75
+ }
76
+
77
+ function parseArgv(argv) {
78
+ const flags = {};
79
+ const positionals = [];
80
+
81
+ for (let i = 0; i < argv.length; i += 1) {
82
+ const current = argv[i];
83
+ if (!current.startsWith('-')) {
84
+ positionals.push(current);
85
+ continue;
86
+ }
87
+
88
+ if (current.startsWith('--')) {
89
+ const withoutPrefix = current.slice(2);
90
+ const eqIndex = withoutPrefix.indexOf('=');
91
+ let key;
92
+ let value;
93
+
94
+ if (eqIndex >= 0) {
95
+ key = withoutPrefix.slice(0, eqIndex);
96
+ value = withoutPrefix.slice(eqIndex + 1);
97
+ } else {
98
+ key = withoutPrefix;
99
+ const next = argv[i + 1];
100
+ if (next && !next.startsWith('-')) {
101
+ value = next;
102
+ i += 1;
103
+ } else {
104
+ value = true;
105
+ }
106
+ }
107
+
108
+ if (Object.prototype.hasOwnProperty.call(flags, key)) {
109
+ if (Array.isArray(flags[key])) {
110
+ flags[key].push(value);
111
+ } else {
112
+ flags[key] = [flags[key], value];
113
+ }
114
+ } else {
115
+ flags[key] = value;
116
+ }
117
+ continue;
118
+ }
119
+
120
+ const short = current.slice(1);
121
+ flags[short] = true;
122
+ }
123
+
124
+ const command = positionals.length > 0 ? positionals[0] : null;
125
+ return {
126
+ command,
127
+ args: positionals.slice(1),
128
+ flags,
129
+ positionals
130
+ };
131
+ }
132
+
133
+ module.exports = {
134
+ escapeRegExp,
135
+ ensureTrailingNewline,
136
+ parseArgv,
137
+ similarityScore,
138
+ slugify,
139
+ toPosix,
140
+ tokenize,
141
+ uniqueBy
143
142
  };
@@ -56,6 +56,24 @@ function readFileIndex(projectRoot, files) {
56
56
  return index;
57
57
  }
58
58
 
59
+ const KNOWN_PATH_ROOTS = [
60
+ 'src/', 'lib/', 'bin/', 'test/', 'tests/', 'docs/', 'scripts/',
61
+ 'packages/', 'apps/', 'tools/', '.github/', 'roadmap-skill/'
62
+ ];
63
+
64
+ function hasFileExtension(token) {
65
+ const lastSegment = token.replace(/\\/g, '/').split('/').pop() || '';
66
+ return /\.[A-Za-z0-9]{1,10}$/.test(lastSegment);
67
+ }
68
+
69
+ function isLikelyPath(token) {
70
+ if (/^\.{1,2}\/|^\//.test(token)) return true;
71
+ if (hasFileExtension(token)) return true;
72
+ if (KNOWN_PATH_ROOTS.some((root) => token.startsWith(root))) return true;
73
+ if ((token.match(/\//g) || []).length >= 2) return true;
74
+ return false;
75
+ }
76
+
59
77
  function extractExplicitPaths(text) {
60
78
  const results = new Set();
61
79
  const quoted = String(text).match(/`([^`]+)`/g) || [];
@@ -67,8 +85,9 @@ function extractExplicitPaths(text) {
67
85
  }
68
86
 
69
87
  const pathTokens = String(text).match(/([A-Za-z0-9_.-]+\/[A-Za-z0-9_./-]+)/g) || [];
70
- for (const token of pathTokens) {
71
- results.add(token);
88
+ for (const raw of pathTokens) {
89
+ const token = raw.replace(/[.,;:!?)]+$/, '');
90
+ if (isLikelyPath(token)) results.add(token);
72
91
  }
73
92
 
74
93
  return Array.from(results).sort((left, right) => left.localeCompare(right));
@@ -398,4 +417,4 @@ module.exports = {
398
417
  buildValidationContext,
399
418
  validateTask,
400
419
  validateTasks
401
- };
420
+ };