roadmapsmith 0.9.16 → 0.9.22

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/io.js CHANGED
@@ -126,14 +126,55 @@ function detectLanguages(files) {
126
126
  '.sh': 'Shell'
127
127
  };
128
128
 
129
- const languages = new Set();
129
+ const languageScores = new Map();
130
+
131
+ function scoreLanguageFile(file) {
132
+ if (/^(scripts|tools|fixtures|examples)\//.test(file)) {
133
+ return 0.4;
134
+ }
135
+ if (/(^|\/)(__tests__|tests)\//.test(file) || /\.test\.|\.spec\.|_test\./.test(file)) {
136
+ return 0.8;
137
+ }
138
+ if (/^(src|app|electron|pages|components|lib|cmd|internal)\//.test(file)) {
139
+ return 4;
140
+ }
141
+ if (/^(packages|apps)\/[^/]+\/(src|app|electron|lib)\//.test(file)) {
142
+ return 4;
143
+ }
144
+ if (/^(packages|apps)\/[^/]+\//.test(file)) {
145
+ return 2.5;
146
+ }
147
+ return 1.5;
148
+ }
149
+
130
150
  for (const file of files) {
131
151
  const ext = path.extname(file).toLowerCase();
132
- if (languageByExtension[ext]) {
133
- languages.add(languageByExtension[ext]);
152
+ const language = languageByExtension[ext];
153
+ if (!language) {
154
+ continue;
134
155
  }
156
+
157
+ const nextScore = (languageScores.get(language) || 0) + scoreLanguageFile(file);
158
+ languageScores.set(language, nextScore);
135
159
  }
136
- return Array.from(languages).sort((left, right) => left.localeCompare(right));
160
+
161
+ const ranked = Array.from(languageScores.entries()).sort((left, right) => {
162
+ if (right[1] !== left[1]) {
163
+ return right[1] - left[1];
164
+ }
165
+ return left[0].localeCompare(right[0]);
166
+ });
167
+
168
+ if (ranked.length === 0) {
169
+ return [];
170
+ }
171
+
172
+ const maxScore = ranked[0][1];
173
+ const inclusionThreshold = Math.max(1, maxScore * 0.35);
174
+
175
+ return ranked
176
+ .filter(([, score]) => score >= inclusionThreshold)
177
+ .map(([language]) => language);
137
178
  }
138
179
 
139
180
  function detectTestFrameworks(projectRoot, files) {
package/src/match.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { similarityScore, slugify, tokenize, uniqueBy } = require('./utils');
3
+ const { normalizeText, similarityScore, slugify, tokenize, uniqueBy } = require('./utils');
4
4
  const { PHASE_ORDER } = require('./model');
5
5
 
6
6
  function canonicalSignature(text) {
@@ -17,12 +17,28 @@ function inferPriorityWeight(priority) {
17
17
  return 4;
18
18
  }
19
19
 
20
- function findBestTaskMatch(candidate, existingTasks, minScore = 0.55) {
20
+ function normalizeTaskText(text) {
21
+ return normalizeText(String(text || '').replace(/`?\[P[0-3]\]`?\s*/gi, ' '));
22
+ }
23
+
24
+ function findBestTaskMatch(candidate, existingTasks, options = {}) {
25
+ const minScore = typeof options.minScore === 'number' ? options.minScore : 0.55;
26
+ const allowFuzzy = options.allowFuzzy !== false;
21
27
  const direct = existingTasks.find((task) => task.id === candidate.id);
22
28
  if (direct) {
23
29
  return { task: direct, score: 1 };
24
30
  }
25
31
 
32
+ const normalizedCandidateText = normalizeTaskText(candidate.text);
33
+ const exactText = existingTasks.find((task) => normalizeTaskText(task.text) === normalizedCandidateText);
34
+ if (exactText) {
35
+ return { task: exactText, score: 0.95 };
36
+ }
37
+
38
+ if (!allowFuzzy) {
39
+ return null;
40
+ }
41
+
26
42
  let best = null;
27
43
  for (const task of existingTasks) {
28
44
  const score = similarityScore(candidate.text, task.text);
@@ -27,6 +27,9 @@ function parseHeadingLine(line) {
27
27
  if (content.startsWith('### ')) {
28
28
  return content.slice(4).trim();
29
29
  }
30
+ if (content.startsWith('#### ')) {
31
+ return content.slice(5).trim();
32
+ }
30
33
  return null;
31
34
  }
32
35
 
@@ -206,6 +209,15 @@ function findManagedRange(lines) {
206
209
  return null;
207
210
  }
208
211
 
212
+ function tasksInManagedBlock(parsedRoadmap) {
213
+ if (!parsedRoadmap || !parsedRoadmap.managedRange) {
214
+ return parsedRoadmap && Array.isArray(parsedRoadmap.tasks) ? parsedRoadmap.tasks : [];
215
+ }
216
+
217
+ const { start, end } = parsedRoadmap.managedRange;
218
+ return parsedRoadmap.tasks.filter((task) => task.lineIndex > start && task.lineIndex < end);
219
+ }
220
+
209
221
  function upsertManagedBlock(existingContent, managedBody) {
210
222
  const existing = String(existingContent || '');
211
223
  const lines = existing.split(/\r?\n/);
@@ -227,5 +239,6 @@ function upsertManagedBlock(existingContent, managedBody) {
227
239
  module.exports = {
228
240
  findManagedRange,
229
241
  parseRoadmap,
242
+ tasksInManagedBlock,
230
243
  upsertManagedBlock
231
244
  };
package/src/slash.js CHANGED
@@ -5,83 +5,102 @@ const SLASH_ACTIONS = [
5
5
  id: 'zero',
6
6
  description: 'Interview the developer in terminal and generate the first roadmap for an empty or low-context repo.',
7
7
  classicCliExample: 'roadmapsmith zero',
8
- slashExamples: ['/zero', '/road zero', '/roadmap-sync zero'],
9
8
  taskLabel: 'RoadmapSmith: Zero Mode'
10
9
  },
11
10
  {
12
11
  id: 'maintain',
13
- description: 'Regenerate, sync, and audit the roadmap for an existing repository.',
12
+ description: 'Preserve-first existing-repo flow: update, sync, and audit the roadmap without rebuilding substantive domain content.',
14
13
  classicCliExample: 'roadmapsmith maintain',
15
- slashExamples: ['/maintain', '/road maintain', '/roadmap-sync maintain'],
16
14
  taskLabel: 'RoadmapSmith: Maintain'
17
15
  },
18
16
  {
19
17
  id: 'status',
20
- description: 'Inspect CLI, roadmap, VS Code task, and Claude hook readiness.',
18
+ description: 'Inspect CLI, roadmap, VS Code task, Codex, and Claude readiness.',
21
19
  classicCliExample: 'roadmapsmith doctor --json',
22
- slashExamples: ['/status', '/road status', '/roadmap-sync status'],
23
20
  taskLabel: 'RoadmapSmith: Status'
24
21
  },
25
22
  {
26
23
  id: 'init',
27
24
  description: 'Create ROADMAP.md and AGENTS.md when they are missing.',
28
25
  classicCliExample: 'roadmapsmith init',
29
- slashExamples: ['/init', '/road init', '/roadmap-sync init'],
30
26
  taskLabel: 'RoadmapSmith: Init'
31
27
  },
32
28
  {
33
29
  id: 'generate',
34
- description: 'Rebuild the managed roadmap block from repository context.',
30
+ description: 'Generate or update ROADMAP.md, refusing destructive replacement unless rerun with --full-regen.',
35
31
  classicCliExample: 'roadmapsmith generate --project-root .',
36
- slashExamples: ['/generate', '/road generate', '/roadmap-sync generate'],
37
32
  taskLabel: 'RoadmapSmith: Generate'
38
33
  },
39
34
  {
40
35
  id: 'validate',
41
36
  description: 'Inspect per-task evidence status as JSON.',
42
37
  classicCliExample: 'roadmapsmith validate --json --project-root .',
43
- slashExamples: ['/validate', '/road validate', '/roadmap-sync validate'],
44
38
  taskLabel: 'RoadmapSmith: Validate'
45
39
  },
46
40
  {
47
41
  id: 'sync',
48
42
  description: 'Apply evidence-backed checklist sync to ROADMAP.md.',
49
43
  classicCliExample: 'roadmapsmith sync --project-root .',
50
- slashExamples: ['/sync', '/road sync', '/roadmap-sync sync'],
51
44
  taskLabel: 'RoadmapSmith: Sync'
52
45
  },
53
46
  {
54
47
  id: 'audit',
55
48
  description: 'Run sync and print the post-sync mismatch summary.',
56
49
  classicCliExample: 'roadmapsmith sync --audit --project-root .',
57
- slashExamples: ['/audit', '/road audit', '/roadmap-sync audit'],
58
50
  taskLabel: 'RoadmapSmith: Sync Audit'
59
51
  },
60
52
  {
61
53
  id: 'setup',
62
54
  description: 'Generate visible VS Code tasks and optional Claude hook wiring.',
63
55
  classicCliExample: 'roadmapsmith setup',
64
- slashExamples: ['/setup', '/road setup', '/roadmap-sync setup'],
65
56
  taskLabel: 'RoadmapSmith: Refresh Setup'
66
57
  }
67
58
  ];
68
59
 
69
- const SLASH_ROOT_ALIASES = new Set(['/road', '/roadmap-sync']);
60
+ const SLASH_ROOT_ALIASES = new Set(['/roadmap', '/road']);
61
+ const LEGACY_ROUTER_ALIAS = '/roadmap-sync';
70
62
 
71
- const DIRECT_SLASH_ALIAS_TO_ACTION = Object.freeze({
72
- '/zero': 'zero',
73
- '/maintain': 'maintain',
74
- '/status': 'status',
75
- '/init': 'init',
76
- '/generate': 'generate',
77
- '/validate': 'validate',
78
- '/sync': 'sync',
79
- '/audit': 'audit',
80
- '/setup': 'setup'
81
- });
63
+ function getNamespacedDirectSlash(actionId) {
64
+ return actionId === 'sync' ? '/roadmap-update' : `/roadmap-${actionId}`;
65
+ }
66
+
67
+ const DIRECT_HOST_NATIVE_ALIAS_TO_ACTION = Object.freeze(
68
+ Object.fromEntries(SLASH_ACTIONS.map((action) => [getNamespacedDirectSlash(action.id), action.id]))
69
+ );
70
+
71
+ const DIRECT_DEPRECATED_CLI_ALIAS_TO_ACTION = Object.freeze(
72
+ Object.fromEntries(SLASH_ACTIONS.map((action) => [`/${action.id}`, action.id]))
73
+ );
74
+
75
+ function getHostNativeSkillNames() {
76
+ return [
77
+ 'roadmap',
78
+ 'roadmap-zero',
79
+ 'roadmap-maintain',
80
+ 'roadmap-status',
81
+ 'roadmap-init',
82
+ 'roadmap-generate',
83
+ 'roadmap-validate',
84
+ 'roadmap-update',
85
+ 'roadmap-sync',
86
+ 'roadmap-audit',
87
+ 'roadmap-setup'
88
+ ];
89
+ }
90
+
91
+ function getHostNativeSlashCommands() {
92
+ return getHostNativeSkillNames().map((name) => `/${name}`);
93
+ }
82
94
 
83
95
  function normalizeActionId(value) {
84
- return String(value || '').trim().toLowerCase().replace(/^\/+/, '');
96
+ let normalized = String(value || '').trim().toLowerCase().replace(/^\/+/, '');
97
+ if (normalized.startsWith('roadmap-')) {
98
+ normalized = normalized.slice('roadmap-'.length);
99
+ }
100
+ if (normalized === 'update') {
101
+ normalized = 'sync';
102
+ }
103
+ return normalized;
85
104
  }
86
105
 
87
106
  function isSlashToken(value) {
@@ -93,8 +112,17 @@ function getSlashAction(actionId) {
93
112
  return SLASH_ACTIONS.find((action) => action.id === normalized) || null;
94
113
  }
95
114
 
115
+ function getLegacyRouterSlash(action) {
116
+ return `/roadmap-sync ${action.id === 'sync' ? 'update' : action.id}`;
117
+ }
118
+
96
119
  function getSlashActionSpecs() {
97
- return SLASH_ACTIONS.map((action) => ({ ...action }));
120
+ return SLASH_ACTIONS.map((action) => ({
121
+ ...action,
122
+ directSlash: getNamespacedDirectSlash(action.id),
123
+ routerSlash: `/roadmap ${action.id}`,
124
+ legacyRouterSlash: getLegacyRouterSlash(action)
125
+ }));
98
126
  }
99
127
 
100
128
  function getSlashSuggestions(query) {
@@ -108,7 +136,35 @@ function getSlashSuggestions(query) {
108
136
  return !action.id.startsWith(normalized) && action.id.includes(normalized);
109
137
  });
110
138
 
111
- return [...startsWithMatches, ...containsMatches].map((action) => ({ ...action }));
139
+ return [...startsWithMatches, ...containsMatches].map((action) => ({
140
+ ...action,
141
+ directSlash: getNamespacedDirectSlash(action.id),
142
+ routerSlash: `/roadmap ${action.id}`,
143
+ legacyRouterSlash: getLegacyRouterSlash(action)
144
+ }));
145
+ }
146
+
147
+ function paletteResponse(source, query, deprecated = false, deprecationMessage = '') {
148
+ return {
149
+ kind: 'palette',
150
+ query,
151
+ source,
152
+ suggestions: getSlashSuggestions(query),
153
+ deprecated,
154
+ deprecationMessage
155
+ };
156
+ }
157
+
158
+ function executeResponse(source, actionId, query, deprecated = false, deprecationMessage = '') {
159
+ return {
160
+ kind: 'execute',
161
+ actionId,
162
+ query,
163
+ source,
164
+ suggestions: getSlashSuggestions(query),
165
+ deprecated,
166
+ deprecationMessage
167
+ };
112
168
  }
113
169
 
114
170
  function resolveSlashInvocation(command, args = []) {
@@ -118,56 +174,68 @@ function resolveSlashInvocation(command, args = []) {
118
174
 
119
175
  const normalizedCommand = String(command).trim().toLowerCase();
120
176
 
121
- if (Object.prototype.hasOwnProperty.call(DIRECT_SLASH_ALIAS_TO_ACTION, normalizedCommand)) {
122
- return {
123
- kind: 'execute',
124
- actionId: DIRECT_SLASH_ALIAS_TO_ACTION[normalizedCommand],
125
- query: normalizeActionId(normalizedCommand),
126
- source: normalizedCommand,
127
- suggestions: getSlashSuggestions(normalizedCommand)
128
- };
177
+ if (normalizedCommand === LEGACY_ROUTER_ALIAS) {
178
+ if (args.length === 0) {
179
+ return paletteResponse(normalizedCommand, '');
180
+ }
181
+
182
+ const queryToken = normalizeActionId(args[0]);
183
+ const deprecationMessage = 'Legacy CLI compatibility root /roadmap-sync <action> is deprecated. Use /roadmap <action> or the direct /roadmap-* commands.';
184
+ const exactAction = getSlashAction(queryToken);
185
+ if (exactAction) {
186
+ return executeResponse(normalizedCommand, exactAction.id, queryToken, true, deprecationMessage);
187
+ }
188
+
189
+ return paletteResponse(normalizedCommand, queryToken, true, deprecationMessage);
190
+ }
191
+
192
+ if (Object.prototype.hasOwnProperty.call(DIRECT_HOST_NATIVE_ALIAS_TO_ACTION, normalizedCommand)) {
193
+ return executeResponse(
194
+ normalizedCommand,
195
+ DIRECT_HOST_NATIVE_ALIAS_TO_ACTION[normalizedCommand],
196
+ normalizeActionId(normalizedCommand)
197
+ );
198
+ }
199
+
200
+ if (Object.prototype.hasOwnProperty.call(DIRECT_DEPRECATED_CLI_ALIAS_TO_ACTION, normalizedCommand)) {
201
+ const actionId = DIRECT_DEPRECATED_CLI_ALIAS_TO_ACTION[normalizedCommand];
202
+ return executeResponse(
203
+ normalizedCommand,
204
+ actionId,
205
+ normalizeActionId(normalizedCommand),
206
+ true,
207
+ `CLI compatibility alias ${normalizedCommand} is deprecated. Use ${getNamespacedDirectSlash(actionId)} or /roadmap ${actionId}.`
208
+ );
129
209
  }
130
210
 
131
211
  if (SLASH_ROOT_ALIASES.has(normalizedCommand)) {
132
212
  const queryToken = args.length > 0 ? normalizeActionId(args[0]) : '';
213
+ const deprecated = normalizedCommand === '/road';
214
+ const deprecationMessage = deprecated
215
+ ? 'CLI compatibility alias /road is deprecated. Use /roadmap.'
216
+ : '';
217
+
133
218
  if (!queryToken) {
134
- return {
135
- kind: 'palette',
136
- query: '',
137
- source: normalizedCommand,
138
- suggestions: getSlashSuggestions('')
139
- };
219
+ return paletteResponse(normalizedCommand, '', deprecated, deprecationMessage);
140
220
  }
141
221
 
142
222
  const exactAction = getSlashAction(queryToken);
143
223
  if (exactAction) {
144
- return {
145
- kind: 'execute',
146
- actionId: exactAction.id,
147
- query: queryToken,
148
- source: normalizedCommand,
149
- suggestions: getSlashSuggestions(queryToken)
150
- };
224
+ return executeResponse(normalizedCommand, exactAction.id, queryToken, deprecated, deprecationMessage);
151
225
  }
152
226
 
153
- return {
154
- kind: 'palette',
155
- query: queryToken,
156
- source: normalizedCommand,
157
- suggestions: getSlashSuggestions(queryToken)
158
- };
227
+ return paletteResponse(normalizedCommand, queryToken, deprecated, deprecationMessage);
159
228
  }
160
229
 
161
- return {
162
- kind: 'palette',
163
- query: normalizeActionId(normalizedCommand),
164
- source: normalizedCommand,
165
- suggestions: getSlashSuggestions(normalizedCommand)
166
- };
230
+ if (normalizedCommand.startsWith('/roadmap-')) {
231
+ return paletteResponse(normalizedCommand, normalizeActionId(normalizedCommand));
232
+ }
233
+
234
+ return paletteResponse(normalizedCommand, normalizeActionId(normalizedCommand));
167
235
  }
168
236
 
169
237
  function renderSlashPalette(options = {}) {
170
- const source = options.source || '/road';
238
+ const source = options.source || '/roadmap';
171
239
  const query = normalizeActionId(options.query);
172
240
  const suggestions = Array.isArray(options.suggestions) ? options.suggestions : getSlashSuggestions(query);
173
241
  const lines = [];
@@ -175,6 +243,11 @@ function renderSlashPalette(options = {}) {
175
243
  lines.push('RoadmapSmith slash palette');
176
244
  lines.push('');
177
245
 
246
+ if (options.deprecated && options.deprecationMessage) {
247
+ lines.push(`Deprecated alias: ${options.deprecationMessage}`);
248
+ lines.push('');
249
+ }
250
+
178
251
  if (query) {
179
252
  lines.push(`Input: ${source} ${query}`);
180
253
  if (suggestions.length > 0) {
@@ -193,20 +266,21 @@ function renderSlashPalette(options = {}) {
193
266
  lines.push('No related slash actions found.');
194
267
  } else {
195
268
  suggestions.forEach((action) => {
196
- lines.push(`- /${action.id}: ${action.description}`);
269
+ lines.push(`- ${action.directSlash}: ${action.description}`);
270
+ lines.push(` Router form: ${action.routerSlash}`);
271
+ lines.push(` Legacy router: ${action.legacyRouterSlash}`);
197
272
  lines.push(` Classic CLI: ${action.classicCliExample}`);
198
- lines.push(` Skill form: /roadmap-sync ${action.id}`);
199
273
  lines.push(` VS Code task: ${action.taskLabel}`);
200
274
  });
201
275
  }
202
276
 
203
277
  lines.push('');
204
278
  lines.push('Examples:');
205
- lines.push('- roadmapsmith zero');
206
- lines.push('- roadmapsmith maintain');
207
- lines.push('- roadmapsmith /road');
208
- lines.push('- roadmapsmith /maintain');
209
- lines.push('- roadmapsmith /roadmap-sync maintain');
279
+ lines.push('- roadmapsmith /roadmap');
280
+ lines.push('- roadmapsmith /roadmap maintain');
281
+ lines.push('- roadmapsmith /roadmap-maintain');
282
+ lines.push('- roadmapsmith /roadmap-update');
283
+ lines.push('- roadmapsmith /roadmap-sync validate');
210
284
  lines.push('');
211
285
  lines.push('Installing the skill alone does not expose CLI behavior in VS Code. Use roadmapsmith setup for the visible task/launcher layer.');
212
286
 
@@ -214,8 +288,13 @@ function renderSlashPalette(options = {}) {
214
288
  }
215
289
 
216
290
  module.exports = {
217
- DIRECT_SLASH_ALIAS_TO_ACTION,
291
+ DIRECT_DEPRECATED_CLI_ALIAS_TO_ACTION,
292
+ DIRECT_HOST_NATIVE_ALIAS_TO_ACTION,
293
+ LEGACY_ROUTER_ALIAS,
218
294
  SLASH_ROOT_ALIASES,
295
+ getHostNativeSkillNames,
296
+ getHostNativeSlashCommands,
297
+ getNamespacedDirectSlash,
219
298
  getSlashAction,
220
299
  getSlashActionSpecs,
221
300
  getSlashSuggestions,
package/src/utils.js CHANGED
@@ -133,6 +133,7 @@ function parseArgv(argv) {
133
133
  module.exports = {
134
134
  escapeRegExp,
135
135
  ensureTrailingNewline,
136
+ normalizeText,
136
137
  parseArgv,
137
138
  similarityScore,
138
139
  slugify,