roadmapsmith 0.9.29 → 0.9.31

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/slash.js CHANGED
@@ -3,65 +3,78 @@
3
3
  const SLASH_ACTIONS = [
4
4
  {
5
5
  id: 'zero',
6
+ tier: 'canonical',
6
7
  description: 'Interview the developer in terminal and generate the first roadmap for an empty or low-context repo.',
7
8
  classicCliExample: 'roadmapsmith zero',
8
9
  taskLabel: 'RoadmapSmith: Zero Mode'
9
10
  },
10
11
  {
11
12
  id: 'maintain',
13
+ tier: 'canonical',
12
14
  description: 'Preserve-first existing-repo flow: update, sync, and audit the roadmap without rebuilding substantive domain content.',
13
15
  classicCliExample: 'roadmapsmith maintain',
14
16
  taskLabel: 'RoadmapSmith: Maintain'
15
17
  },
16
18
  {
17
19
  id: 'status',
20
+ tier: 'canonical',
18
21
  description: 'Inspect CLI, roadmap, VS Code task, Codex, and Claude readiness.',
19
22
  classicCliExample: 'roadmapsmith status --json',
20
23
  taskLabel: 'RoadmapSmith: Status'
21
24
  },
25
+ {
26
+ id: 'validate',
27
+ tier: 'canonical',
28
+ description: 'Inspect per-task evidence status as JSON.',
29
+ classicCliExample: 'roadmapsmith validate --json --project-root .',
30
+ taskLabel: 'RoadmapSmith: Validate'
31
+ },
32
+ {
33
+ id: 'update',
34
+ tier: 'canonical',
35
+ aliases: ['sync'],
36
+ description: 'Apply evidence-backed checklist refresh to ROADMAP.md or complete one task with verified evidence.',
37
+ classicCliExample: 'roadmapsmith update --project-root .',
38
+ taskLabel: 'RoadmapSmith: Update'
39
+ },
40
+ {
41
+ id: 'setup',
42
+ tier: 'canonical',
43
+ description: 'Generate visible VS Code tasks and optional Claude hook wiring.',
44
+ classicCliExample: 'roadmapsmith setup',
45
+ taskLabel: 'RoadmapSmith: Refresh Setup'
46
+ },
22
47
  {
23
48
  id: 'init',
49
+ tier: 'advanced',
24
50
  description: 'Create ROADMAP.md and AGENTS.md when they are missing.',
25
51
  classicCliExample: 'roadmapsmith init',
26
52
  taskLabel: 'RoadmapSmith: Init'
27
53
  },
28
54
  {
29
55
  id: 'generate',
56
+ tier: 'advanced',
30
57
  description: 'Generate or update ROADMAP.md, refusing destructive replacement unless rerun with --full-regen.',
31
58
  classicCliExample: 'roadmapsmith generate --project-root .',
32
59
  taskLabel: 'RoadmapSmith: Generate'
33
60
  },
34
- {
35
- id: 'validate',
36
- description: 'Inspect per-task evidence status as JSON.',
37
- classicCliExample: 'roadmapsmith validate --json --project-root .',
38
- taskLabel: 'RoadmapSmith: Validate'
39
- },
40
- {
41
- id: 'sync',
42
- description: 'Apply evidence-backed checklist sync to ROADMAP.md.',
43
- classicCliExample: 'roadmapsmith sync --project-root .',
44
- taskLabel: 'RoadmapSmith: Sync'
45
- },
46
61
  {
47
62
  id: 'audit',
63
+ tier: 'advanced',
48
64
  description: 'Run sync and print the post-sync mismatch summary.',
49
65
  classicCliExample: 'roadmapsmith sync --audit --project-root .',
50
66
  taskLabel: 'RoadmapSmith: Sync Audit'
51
- },
52
- {
53
- id: 'setup',
54
- description: 'Generate visible VS Code tasks and optional Claude hook wiring.',
55
- classicCliExample: 'roadmapsmith setup',
56
- taskLabel: 'RoadmapSmith: Refresh Setup'
57
67
  }
58
68
  ];
59
69
 
60
70
  const SLASH_ROOT_ALIASES = new Set(['/roadmap', '/road']);
61
71
  const LEGACY_ROUTER_ALIAS = '/roadmap-sync';
72
+ const CANONICAL_ACTION_IDS = Object.freeze(SLASH_ACTIONS.filter((action) => action.tier === 'canonical').map((action) => action.id));
73
+ const ADVANCED_ACTION_IDS = Object.freeze(SLASH_ACTIONS.filter((action) => action.tier === 'advanced').map((action) => action.id));
74
+ const COMPATIBILITY_ACTION_IDS = Object.freeze([]);
62
75
 
63
76
  function getNamespacedDirectSlash(actionId) {
64
- return actionId === 'sync' ? '/roadmap-update' : `/roadmap-${actionId}`;
77
+ return `/roadmap-${actionId}`;
65
78
  }
66
79
 
67
80
  const DIRECT_HOST_NATIVE_ALIAS_TO_ACTION = Object.freeze(
@@ -69,25 +82,64 @@ const DIRECT_HOST_NATIVE_ALIAS_TO_ACTION = Object.freeze(
69
82
  );
70
83
 
71
84
  const DIRECT_DEPRECATED_CLI_ALIAS_TO_ACTION = Object.freeze(
72
- Object.fromEntries(SLASH_ACTIONS.map((action) => [`/${action.id}`, action.id]))
85
+ Object.fromEntries(
86
+ SLASH_ACTIONS.flatMap((action) => {
87
+ const aliases = Array.isArray(action.aliases) ? action.aliases : [];
88
+ return [action.id, ...aliases].map((alias) => [`/${alias}`, action.id]);
89
+ })
90
+ )
73
91
  );
74
92
 
75
- function getHostNativeSkillNames() {
93
+ const ACTION_ALIAS_TO_ID = Object.freeze(
94
+ Object.fromEntries(
95
+ SLASH_ACTIONS.flatMap((action) => {
96
+ const aliases = Array.isArray(action.aliases) ? action.aliases : [];
97
+ return [action.id, ...aliases].map((alias) => [alias, action.id]);
98
+ })
99
+ )
100
+ );
101
+
102
+ function mapActionIdsToSkillNames(actionIds) {
76
103
  return [
77
104
  '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'
105
+ ...actionIds.map((actionId) => getNamespacedDirectSlash(actionId).slice(1))
88
106
  ];
89
107
  }
90
108
 
109
+ function getCanonicalHostNativeSkillNames() {
110
+ return mapActionIdsToSkillNames(CANONICAL_ACTION_IDS);
111
+ }
112
+
113
+ function getAdvancedHostNativeSkillNames() {
114
+ return ADVANCED_ACTION_IDS.map((actionId) => getNamespacedDirectSlash(actionId).slice(1));
115
+ }
116
+
117
+ function getCompatibilityHostNativeSkillNames() {
118
+ return [
119
+ 'roadmap-sync'
120
+ ];
121
+ }
122
+
123
+ function getHostNativeSkillNames() {
124
+ return [
125
+ ...getCanonicalHostNativeSkillNames(),
126
+ ...getAdvancedHostNativeSkillNames(),
127
+ ...getCompatibilityHostNativeSkillNames()
128
+ ];
129
+ }
130
+
131
+ function getCanonicalHostNativeSlashCommands() {
132
+ return getCanonicalHostNativeSkillNames().map((name) => `/${name}`);
133
+ }
134
+
135
+ function getAdvancedHostNativeSlashCommands() {
136
+ return getAdvancedHostNativeSkillNames().map((name) => `/${name}`);
137
+ }
138
+
139
+ function getCompatibilityHostNativeSlashCommands() {
140
+ return getCompatibilityHostNativeSkillNames().map((name) => `/${name}`);
141
+ }
142
+
91
143
  function getHostNativeSlashCommands() {
92
144
  return getHostNativeSkillNames().map((name) => `/${name}`);
93
145
  }
@@ -97,23 +149,29 @@ function normalizeActionId(value) {
97
149
  if (normalized.startsWith('roadmap-')) {
98
150
  normalized = normalized.slice('roadmap-'.length);
99
151
  }
100
- if (normalized === 'update') {
101
- normalized = 'sync';
102
- }
103
152
  return normalized;
104
153
  }
105
154
 
155
+ function canonicalizeActionId(value) {
156
+ const normalized = normalizeActionId(value);
157
+ return ACTION_ALIAS_TO_ID[normalized] || normalized;
158
+ }
159
+
106
160
  function isSlashToken(value) {
107
161
  return typeof value === 'string' && value.trim().startsWith('/');
108
162
  }
109
163
 
110
164
  function getSlashAction(actionId) {
111
- const normalized = normalizeActionId(actionId);
165
+ const normalized = canonicalizeActionId(actionId);
112
166
  return SLASH_ACTIONS.find((action) => action.id === normalized) || null;
113
167
  }
114
168
 
115
169
  function getLegacyRouterSlash(action) {
116
- return `/roadmap-sync ${action.id === 'sync' ? 'update' : action.id}`;
170
+ return `/roadmap-sync ${action.id}`;
171
+ }
172
+
173
+ function actionSearchTerms(action) {
174
+ return [action.id, ...(Array.isArray(action.aliases) ? action.aliases : [])];
117
175
  }
118
176
 
119
177
  function getSlashActionSpecs() {
@@ -127,13 +185,22 @@ function getSlashActionSpecs() {
127
185
 
128
186
  function getSlashSuggestions(query) {
129
187
  const normalized = normalizeActionId(query);
188
+ const visibleActions = SLASH_ACTIONS.filter((action) => action.tier !== 'compatibility');
130
189
  if (!normalized) {
131
- return getSlashActionSpecs();
190
+ return visibleActions.map((action) => ({
191
+ ...action,
192
+ directSlash: getNamespacedDirectSlash(action.id),
193
+ routerSlash: `/roadmap ${action.id}`,
194
+ legacyRouterSlash: getLegacyRouterSlash(action)
195
+ }));
132
196
  }
133
197
 
134
- const startsWithMatches = SLASH_ACTIONS.filter((action) => action.id.startsWith(normalized));
135
- const containsMatches = SLASH_ACTIONS.filter((action) => {
136
- return !action.id.startsWith(normalized) && action.id.includes(normalized);
198
+ const startsWithMatches = visibleActions.filter((action) => {
199
+ return actionSearchTerms(action).some((term) => term.startsWith(normalized));
200
+ });
201
+ const containsMatches = visibleActions.filter((action) => {
202
+ return !actionSearchTerms(action).some((term) => term.startsWith(normalized))
203
+ && actionSearchTerms(action).some((term) => term.includes(normalized));
137
204
  });
138
205
 
139
206
  return [...startsWithMatches, ...containsMatches].map((action) => ({
@@ -175,12 +242,12 @@ function resolveSlashInvocation(command, args = []) {
175
242
  const normalizedCommand = String(command).trim().toLowerCase();
176
243
 
177
244
  if (normalizedCommand === LEGACY_ROUTER_ALIAS) {
245
+ const deprecationMessage = 'Legacy CLI compatibility root /roadmap-sync <action> is deprecated. Use /roadmap <action> or the direct /roadmap-* commands.';
178
246
  if (args.length === 0) {
179
- return paletteResponse(normalizedCommand, '');
247
+ return paletteResponse(normalizedCommand, '', true, deprecationMessage);
180
248
  }
181
249
 
182
250
  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
251
  const exactAction = getSlashAction(queryToken);
185
252
  if (exactAction) {
186
253
  return executeResponse(normalizedCommand, exactAction.id, queryToken, true, deprecationMessage);
@@ -268,7 +335,9 @@ function renderSlashPalette(options = {}) {
268
335
  suggestions.forEach((action) => {
269
336
  lines.push(`- ${action.directSlash}: ${action.description}`);
270
337
  lines.push(` Router form: ${action.routerSlash}`);
271
- lines.push(` Legacy router: ${action.legacyRouterSlash}`);
338
+ if (options.deprecated || source === LEGACY_ROUTER_ALIAS) {
339
+ lines.push(` Legacy router: ${action.legacyRouterSlash}`);
340
+ }
272
341
  lines.push(` Classic CLI: ${action.classicCliExample}`);
273
342
  lines.push(` VS Code task: ${action.taskLabel}`);
274
343
  });
@@ -280,7 +349,9 @@ function renderSlashPalette(options = {}) {
280
349
  lines.push('- roadmapsmith /roadmap maintain');
281
350
  lines.push('- roadmapsmith /roadmap-maintain');
282
351
  lines.push('- roadmapsmith /roadmap-update');
283
- lines.push('- roadmapsmith /roadmap-sync validate');
352
+ if (options.deprecated || source === LEGACY_ROUTER_ALIAS) {
353
+ lines.push('- roadmapsmith /roadmap-sync validate');
354
+ }
284
355
  lines.push('');
285
356
  lines.push('Installing the skill alone does not expose CLI behavior in VS Code. Use roadmapsmith setup for the visible task/launcher layer.');
286
357
 
@@ -288,10 +359,20 @@ function renderSlashPalette(options = {}) {
288
359
  }
289
360
 
290
361
  module.exports = {
362
+ ACTION_ALIAS_TO_ID,
291
363
  DIRECT_DEPRECATED_CLI_ALIAS_TO_ACTION,
292
364
  DIRECT_HOST_NATIVE_ALIAS_TO_ACTION,
365
+ ADVANCED_ACTION_IDS,
366
+ CANONICAL_ACTION_IDS,
367
+ COMPATIBILITY_ACTION_IDS,
293
368
  LEGACY_ROUTER_ALIAS,
294
369
  SLASH_ROOT_ALIASES,
370
+ getAdvancedHostNativeSkillNames,
371
+ getAdvancedHostNativeSlashCommands,
372
+ getCanonicalHostNativeSkillNames,
373
+ getCanonicalHostNativeSlashCommands,
374
+ getCompatibilityHostNativeSkillNames,
375
+ getCompatibilityHostNativeSlashCommands,
295
376
  getHostNativeSkillNames,
296
377
  getHostNativeSlashCommands,
297
378
  getNamespacedDirectSlash,
@@ -299,6 +380,7 @@ module.exports = {
299
380
  getSlashActionSpecs,
300
381
  getSlashSuggestions,
301
382
  isSlashToken,
383
+ canonicalizeActionId,
302
384
  normalizeActionId,
303
385
  renderSlashPalette,
304
386
  resolveSlashInvocation
package/src/sync/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { parseRoadmap } = require('../parser');
4
4
  const { ensureTrailingNewline } = require('../utils');
5
+ const { isLowSpecificityReason } = require('../reasons');
5
6
 
6
7
  const ATTEMPTED_WARNING_REASON_PREFIX = 'attempted but validation failed:';
7
8
  const NO_EVIDENCE_WARNING_REASON_PREFIX = 'no implementation evidence found yet:';
@@ -106,10 +107,14 @@ function normalizeWarningReasons(reasons) {
106
107
  return normalized;
107
108
  }
108
109
 
109
- function shouldPreserveExistingWarning(existingReason, newReason) {
110
+ function shouldPreserveExistingWarning(existingReason, newReason, options) {
111
+ if (options && options.forceRefresh) return false;
110
112
  const cleanExisting = normalizeWarningReason(existingReason);
111
113
  const cleanNew = normalizeWarningReason(newReason) || 'validation failed';
112
- return cleanNew === 'validation failed' && cleanExisting && cleanExisting !== cleanNew;
114
+ // Preserve when the freshly computed reason is a low-specificity policy message (no
115
+ // file- or symbol-specific information) and the existing annotation carries more
116
+ // diagnostic value (is not itself a low-specificity message).
117
+ return isLowSpecificityReason(cleanNew) && Boolean(cleanExisting) && !isLowSpecificityReason(cleanExisting);
113
118
  }
114
119
 
115
120
  function formatVerificationRecipe(indent, recipe) {
@@ -129,7 +134,7 @@ function findVerificationRecipeIndex(lines, taskLineIndex) {
129
134
  return null;
130
135
  }
131
136
 
132
- function applySync(content, parsedTasks, results) {
137
+ function applySync(content, parsedTasks, results, options) {
133
138
  const parsed = parseRoadmap(content);
134
139
  const lines = [...parsed.lines];
135
140
  const tasks = parsedTasks || parsed.tasks;
@@ -187,7 +192,7 @@ function applySync(content, parsedTasks, results) {
187
192
  } else if (warningIndex != null && warningIndex >= 0 && warningIndex < lines.length) {
188
193
  const existingReason = normalizeWarningReason(lines[warningIndex]);
189
194
  const newReason = reason || 'validation failed';
190
- if (!shouldPreserveExistingWarning(existingReason, newReason)) {
195
+ if (!shouldPreserveExistingWarning(existingReason, newReason, options)) {
191
196
  lines[warningIndex] = warningText;
192
197
  }
193
198
  } else {