roadmapsmith 0.9.25 → 0.9.26

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roadmapsmith",
3
- "version": "0.9.25",
3
+ "version": "0.9.26",
4
4
  "description": "One-command evidence-backed ROADMAP.md generator and sync tool for AI coding agents, with shared RoadmapSmith plugin skills for Codex and Claude plus the roadmapsmith status readiness surface.",
5
5
  "author": {
6
6
  "name": "PapiScholz"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roadmapsmith",
3
- "version": "0.9.25",
3
+ "version": "0.9.26",
4
4
  "description": "One-command evidence-backed ROADMAP.md generator and sync tool for AI coding agents, with shared RoadmapSmith plugin skills for Codex and Claude plus the roadmapsmith status readiness surface.",
5
5
  "author": {
6
6
  "name": "PapiScholz"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roadmapsmith",
3
- "version": "0.9.25",
3
+ "version": "0.9.26",
4
4
  "description": "One-command evidence-backed ROADMAP.md generator and sync tool for AI coding agents, with shared RoadmapSmith plugin skills for Codex and Claude plus the roadmapsmith status readiness surface.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/skills.json CHANGED
@@ -29,67 +29,67 @@
29
29
  "name": "roadmap",
30
30
  "path": "skills/roadmap",
31
31
  "description": "Native slash palette for RoadmapSmith commands and recommended entrypoints across supported hosts.",
32
- "version": "0.9.25"
32
+ "version": "0.9.26"
33
33
  },
34
34
  {
35
35
  "name": "roadmap-zero",
36
36
  "path": "skills/roadmap-zero",
37
37
  "description": "Native slash entrypoint for the one-command Zero Mode CLI workflow.",
38
- "version": "0.9.25"
38
+ "version": "0.9.26"
39
39
  },
40
40
  {
41
41
  "name": "roadmap-maintain",
42
42
  "path": "skills/roadmap-maintain",
43
43
  "description": "Native slash entrypoint for the preserve-first generate + sync + audit flow.",
44
- "version": "0.9.25"
44
+ "version": "0.9.26"
45
45
  },
46
46
  {
47
47
  "name": "roadmap-status",
48
48
  "path": "skills/roadmap-status",
49
49
  "description": "Native slash readiness check grounded in roadmapsmith status JSON.",
50
- "version": "0.9.25"
50
+ "version": "0.9.26"
51
51
  },
52
52
  {
53
53
  "name": "roadmap-init",
54
54
  "path": "skills/roadmap-init",
55
55
  "description": "Native slash entrypoint for creating ROADMAP.md and AGENTS.md.",
56
- "version": "0.9.25"
56
+ "version": "0.9.26"
57
57
  },
58
58
  {
59
59
  "name": "roadmap-generate",
60
60
  "path": "skills/roadmap-generate",
61
61
  "description": "Native slash entrypoint for managed roadmap updates that require --full-regen before destructive replacement.",
62
- "version": "0.9.25"
62
+ "version": "0.9.26"
63
63
  },
64
64
  {
65
65
  "name": "roadmap-validate",
66
66
  "path": "skills/roadmap-validate",
67
67
  "description": "Native slash entrypoint for evidence-backed roadmap validation.",
68
- "version": "0.9.25"
68
+ "version": "0.9.26"
69
69
  },
70
70
  {
71
71
  "name": "roadmap-update",
72
72
  "path": "skills/roadmap-update",
73
73
  "description": "Native slash entrypoint for applying evidence-backed checklist sync.",
74
- "version": "0.9.25"
74
+ "version": "0.9.26"
75
75
  },
76
76
  {
77
77
  "name": "roadmap-sync",
78
78
  "path": "skills/roadmap-sync",
79
79
  "description": "Legacy namespaced root plus policy guidance for RoadmapSmith slash workflows.",
80
- "version": "0.9.25"
80
+ "version": "0.9.26"
81
81
  },
82
82
  {
83
83
  "name": "roadmap-audit",
84
84
  "path": "skills/roadmap-audit",
85
85
  "description": "Native slash entrypoint for the current sync-plus-audit workflow.",
86
- "version": "0.9.25"
86
+ "version": "0.9.26"
87
87
  },
88
88
  {
89
89
  "name": "roadmap-setup",
90
90
  "path": "skills/roadmap-setup",
91
91
  "description": "Native slash entrypoint for generating RoadmapSmith host integration files.",
92
- "version": "0.9.25"
92
+ "version": "0.9.26"
93
93
  }
94
94
  ]
95
95
  }
@@ -275,15 +275,29 @@ function isAsciiAlphaNumeric(char) {
275
275
  return (code >= 48 && code <= 57) || (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
276
276
  }
277
277
 
278
- function isPathTokenCharacter(char) {
279
- return isAsciiAlphaNumeric(char) || char === '.' || char === '_' || char === '-' || char === '/' || char === '\\';
278
+ function isPathTokenCharacter(char, current) {
279
+ if (char === '~') {
280
+ return !current;
281
+ }
282
+ return isAsciiAlphaNumeric(char) || char === '.' || char === '_' || char === '-' || char === '/' || char === '\\' || char === ':';
280
283
  }
281
284
 
282
285
  function stripTrailingPathPunctuation(token) {
283
286
  let result = String(token || '');
284
287
  while (result.length > 0) {
285
288
  const lastChar = result[result.length - 1];
286
- if (lastChar !== '.' && lastChar !== ',' && lastChar !== ';' && lastChar !== ':' && lastChar !== '!' && lastChar !== '?' && lastChar !== ')') {
289
+ if (
290
+ lastChar !== '.' &&
291
+ lastChar !== ',' &&
292
+ lastChar !== ';' &&
293
+ lastChar !== ':' &&
294
+ lastChar !== '!' &&
295
+ lastChar !== '?' &&
296
+ lastChar !== ')' &&
297
+ lastChar !== ']' &&
298
+ lastChar !== '>' &&
299
+ lastChar !== '`'
300
+ ) {
287
301
  break;
288
302
  }
289
303
  result = result.slice(0, -1);
@@ -294,22 +308,41 @@ function stripTrailingPathPunctuation(token) {
294
308
  function collectPathishTokens(text) {
295
309
  const tokens = [];
296
310
  let current = '';
311
+ let tokenStart = -1;
297
312
  const source = String(text || '');
298
313
  for (let index = 0; index < source.length; index += 1) {
299
314
  const char = source[index];
300
- if (isPathTokenCharacter(char)) {
315
+ if (isPathTokenCharacter(char, current)) {
316
+ if (!current) {
317
+ tokenStart = index;
318
+ }
301
319
  current += char;
302
320
  continue;
303
321
  }
304
322
  if (current) {
305
- tokens.push(stripTrailingPathPunctuation(current));
323
+ const value = stripTrailingPathPunctuation(current);
324
+ if (value) {
325
+ tokens.push({
326
+ value,
327
+ start: tokenStart,
328
+ end: tokenStart + value.length
329
+ });
330
+ }
306
331
  current = '';
332
+ tokenStart = -1;
307
333
  }
308
334
  }
309
335
  if (current) {
310
- tokens.push(stripTrailingPathPunctuation(current));
336
+ const value = stripTrailingPathPunctuation(current);
337
+ if (value) {
338
+ tokens.push({
339
+ value,
340
+ start: tokenStart,
341
+ end: tokenStart + value.length
342
+ });
343
+ }
311
344
  }
312
- return tokens.filter(Boolean);
345
+ return tokens;
313
346
  }
314
347
 
315
348
  // LINE_REF_RE matches "path/file.ext:NN" or "path/file.ext:NN-MM" — indicates WHERE
@@ -317,56 +350,142 @@ function collectPathishTokens(text) {
317
350
  // to lineReferenceHints and excluded from hasDirectReferencePass scoring.
318
351
  const LINE_REF_RE = /^(.+?):(\d+)(?:-\d+)?$/;
319
352
 
320
- function normalizeExplicitPathCandidate(rawToken) {
321
- const clean = stripTrailingPathPunctuation(String(rawToken || '').trim());
353
+ function normalizePathCandidateToken(rawToken) {
354
+ const stripped = stripTrailingPathPunctuation(String(rawToken || '').trim());
355
+ if (!stripped) {
356
+ return '';
357
+ }
358
+ const normalized = stripped.replace(/\\/g, '/');
359
+ if (/^~\//.test(normalized)) {
360
+ return normalized;
361
+ }
362
+ return normalized.replace(/^~(?=\/)/, '~');
363
+ }
364
+
365
+ function isExternalPathToken(token) {
366
+ return /^~\//.test(String(token || '').trim().replace(/\\/g, '/'));
367
+ }
368
+
369
+ function classifyExplicitPathCandidate(rawToken) {
370
+ const clean = normalizePathCandidateToken(rawToken);
322
371
  if (!clean || clean.includes('*') || clean.includes('?')) {
323
372
  return null;
324
373
  }
325
374
 
326
375
  const lineMatch = LINE_REF_RE.exec(clean);
327
376
  if (lineMatch) {
328
- const linePath = stripTrailingPathPunctuation(lineMatch[1]);
377
+ const linePath = normalizePathCandidateToken(lineMatch[1]);
378
+ if (isExternalPathToken(linePath)) {
379
+ return { path: linePath, kind: 'external', isLineReference: true };
380
+ }
329
381
  if (isRealFilePath(linePath)) {
330
- return { path: linePath, isLineReference: true };
382
+ return { path: linePath, kind: 'repo', isLineReference: true };
331
383
  }
332
384
  return null;
333
385
  }
334
386
 
387
+ if (isExternalPathToken(clean)) {
388
+ return { path: clean, kind: 'external', isLineReference: false };
389
+ }
390
+
335
391
  if (!isRealFilePath(clean)) {
336
392
  return null;
337
393
  }
338
394
 
339
- return { path: clean, isLineReference: false };
395
+ return { path: clean, kind: 'repo', isLineReference: false };
396
+ }
397
+
398
+ function addClassifiedPath(classified, results, externalPaths, lineReferenceHints) {
399
+ if (!classified) return;
400
+ if (classified.kind === 'external') {
401
+ externalPaths.add(classified.path);
402
+ } else {
403
+ results.add(classified.path);
404
+ if (classified.isLineReference) {
405
+ lineReferenceHints.add(classified.path);
406
+ }
407
+ }
408
+ }
409
+
410
+ function findHttpRequestRouteRanges(text) {
411
+ const ranges = [];
412
+ const pattern = /\b(?:GET|POST|PUT|PATCH|DELETE)\s+(\/[^\s`]+)/gi;
413
+ let match = pattern.exec(String(text || ''));
414
+ while (match) {
415
+ const routeToken = match[1];
416
+ const routeStart = match.index + match[0].length - routeToken.length;
417
+ ranges.push({ start: routeStart, end: routeStart + routeToken.length });
418
+ match = pattern.exec(String(text || ''));
419
+ }
420
+ return ranges;
421
+ }
422
+
423
+ function isTokenInsideRanges(token, ranges) {
424
+ return ranges.some((range) => token.start >= range.start && token.end <= range.end);
425
+ }
426
+
427
+ function addPathTokensFromPlainText(text, results, externalPaths, lineReferenceHints) {
428
+ const ignoredRanges = findHttpRequestRouteRanges(text);
429
+ for (const token of collectPathishTokens(text)) {
430
+ if (!token.value.includes('/') && !isExternalPathToken(token.value)) {
431
+ continue;
432
+ }
433
+ if (isTokenInsideRanges(token, ignoredRanges)) {
434
+ continue;
435
+ }
436
+ addClassifiedPath(classifyExplicitPathCandidate(token.value), results, externalPaths, lineReferenceHints);
437
+ }
438
+ }
439
+
440
+ function addPathTokensFromBacktickSpan(text, results, externalPaths, lineReferenceHints) {
441
+ const wholeSpan = classifyExplicitPathCandidate(text);
442
+ if (wholeSpan) {
443
+ addClassifiedPath(wholeSpan, results, externalPaths, lineReferenceHints);
444
+ return;
445
+ }
446
+
447
+ if (!/[;,]/.test(text)) {
448
+ return;
449
+ }
450
+
451
+ for (const part of text.split(/[;,]/)) {
452
+ addClassifiedPath(classifyExplicitPathCandidate(part), results, externalPaths, lineReferenceHints);
453
+ }
340
454
  }
341
455
 
342
456
  function extractExplicitPaths(text) {
343
457
  const results = new Set();
458
+ const externalPaths = new Set();
344
459
  const lineReferenceHints = new Set();
460
+ const source = String(text || '');
461
+ let cursor = 0;
345
462
 
346
- const quoted = String(text).match(/`([^`]+)`/g) || [];
347
- for (const token of quoted) {
348
- const normalized = normalizeExplicitPathCandidate(token.slice(1, -1));
349
- if (!normalized) continue;
350
- results.add(normalized.path);
351
- if (normalized.isLineReference) {
352
- lineReferenceHints.add(normalized.path);
463
+ while (cursor < source.length) {
464
+ const openTick = source.indexOf('`', cursor);
465
+ if (openTick < 0) {
466
+ addPathTokensFromPlainText(source.slice(cursor), results, externalPaths, lineReferenceHints);
467
+ break;
353
468
  }
354
- }
355
469
 
356
- for (const word of String(text).split(/\s+/)) {
357
- if (!word.includes('/')) continue;
358
- const normalized = normalizeExplicitPathCandidate(word);
359
- if (!normalized) continue;
360
- results.add(normalized.path);
361
- if (normalized.isLineReference) {
362
- lineReferenceHints.add(normalized.path);
470
+ addPathTokensFromPlainText(source.slice(cursor, openTick), results, externalPaths, lineReferenceHints);
471
+ const closeTick = source.indexOf('`', openTick + 1);
472
+ if (closeTick < 0) {
473
+ addPathTokensFromPlainText(source.slice(openTick), results, externalPaths, lineReferenceHints);
474
+ break;
363
475
  }
476
+
477
+ addPathTokensFromBacktickSpan(source.slice(openTick + 1, closeTick), results, externalPaths, lineReferenceHints);
478
+ cursor = closeTick + 1;
364
479
  }
365
480
 
366
481
  const paths = Array.from(results)
367
482
  .filter((p) => !p.includes('*') && !p.includes('?'))
368
483
  .sort((left, right) => left.localeCompare(right));
369
- return { paths, lineReferenceHints };
484
+ return {
485
+ paths,
486
+ externalPaths: Array.from(externalPaths).sort((left, right) => left.localeCompare(right)),
487
+ lineReferenceHints
488
+ };
370
489
  }
371
490
 
372
491
  // Standalone filenames (no slash) mentioned in task prose — e.g. "roadmap-skill.config.json",
@@ -434,10 +553,59 @@ function isImplementationTask(taskText) {
434
553
  return !isDocTask(taskText) && (isCodeTask(taskText) || taskDescribesChange(taskText));
435
554
  }
436
555
 
437
- function findFilesByPathHints(pathHints, fileIndex) {
556
+ function deriveNextAppRouteAlias(relativePath) {
557
+ const normalized = normalizePathForMatch(relativePath);
558
+ const match = normalized.match(/^(?:src\/)?app(?:\/(.*))?\/(page|route)\.(?:js|jsx|ts|tsx)$/);
559
+ if (!match) {
560
+ return null;
561
+ }
562
+
563
+ const routePath = match[1] || '';
564
+ const segments = routePath ? routePath.split('/').filter(Boolean) : [];
565
+ const visibleSegments = [];
566
+ for (const segment of segments) {
567
+ if (/^\([^)]*\)$/.test(segment)) {
568
+ continue;
569
+ }
570
+ if (segment.includes('(') || segment.includes(')') || segment.includes('[') || segment.includes(']') || segment.startsWith('@')) {
571
+ return null;
572
+ }
573
+ visibleSegments.push(segment);
574
+ }
575
+
576
+ return visibleSegments.length > 0 ? `/${visibleSegments.join('/')}` : '/';
577
+ }
578
+
579
+ function buildPathHintResolver(fileIndex) {
580
+ const routeAliasIndex = new Map();
581
+ for (const file of fileIndex) {
582
+ const alias = deriveNextAppRouteAlias(file.relativePath);
583
+ if (!alias) {
584
+ continue;
585
+ }
586
+ const existing = routeAliasIndex.get(alias) || [];
587
+ existing.push(file.relativePath);
588
+ routeAliasIndex.set(alias, existing);
589
+ }
590
+
591
+ for (const [alias, matches] of routeAliasIndex.entries()) {
592
+ routeAliasIndex.set(alias, Array.from(new Set(matches)).sort((left, right) => left.localeCompare(right)));
593
+ }
594
+
595
+ return {
596
+ fileIndex,
597
+ routeAliasIndex
598
+ };
599
+ }
600
+
601
+ function findFilesByPathHints(pathHints, pathHintResolver) {
602
+ const resolver = Array.isArray(pathHintResolver)
603
+ ? buildPathHintResolver(pathHintResolver)
604
+ : pathHintResolver;
605
+ const fileIndex = resolver.fileIndex;
438
606
  const matches = [];
439
607
  for (const hint of pathHints) {
440
- const normalizedHint = hint.replace(/\\/g, '/');
608
+ const normalizedHint = normalizePathCandidateToken(hint);
441
609
  const direct = fileIndex.find((file) => file.relativePath === normalizedHint);
442
610
  if (direct) {
443
611
  matches.push(direct.relativePath);
@@ -449,6 +617,11 @@ function findFilesByPathHints(pathHints, fileIndex) {
449
617
  matches.push(file.relativePath);
450
618
  }
451
619
  }
620
+
621
+ const routeMatches = resolver.routeAliasIndex.get(normalizedHint);
622
+ if (routeMatches && routeMatches.length > 0) {
623
+ matches.push(...routeMatches);
624
+ }
452
625
  }
453
626
  return Array.from(new Set(matches)).sort((left, right) => left.localeCompare(right));
454
627
  }
@@ -740,19 +913,12 @@ function isTestPath(relativePath) {
740
913
  return /(^|\/)(__tests__|tests)\//.test(relativePath) || /\.test\.|\.spec\.|_test\.go$/.test(relativePath);
741
914
  }
742
915
 
743
- function extractEvidencePaths(evidenceText) {
744
- const paths = new Set();
745
- for (const rawCandidate of collectPathishTokens(evidenceText)) {
746
- const candidate = rawCandidate.split('\\').join('/');
747
- if (!candidate.includes('/') || candidate.includes('*') || candidate.includes('?')) {
748
- continue;
749
- }
750
- if (!hasKnownFileExtension(candidate)) {
751
- continue;
752
- }
753
- paths.add(candidate.replace(/^\.\//, ''));
754
- }
755
- return Array.from(paths).sort((left, right) => left.localeCompare(right));
916
+ function extractReferencedPaths(text) {
917
+ const extracted = extractExplicitPaths(text);
918
+ return {
919
+ repoPaths: extracted.paths,
920
+ externalPaths: extracted.externalPaths
921
+ };
756
922
  }
757
923
 
758
924
  function evidenceLineHasPassingSummary(evidenceText) {
@@ -771,7 +937,7 @@ function evidenceSummaryImpliesTests(evidenceText) {
771
937
  /\b(?:vitest|jest|npm test|pnpm test|yarn test|bun test)\b/i.test(String(evidenceText || ''));
772
938
  }
773
939
 
774
- function evaluateAuthoritativeEvidence(task, fileIndex) {
940
+ function evaluateAuthoritativeEvidence(task, pathHintResolver) {
775
941
  const evidenceLines = Array.isArray(task.evidenceLines) ? task.evidenceLines : [];
776
942
  if (evidenceLines.length === 0) {
777
943
  return {
@@ -786,8 +952,9 @@ function evaluateAuthoritativeEvidence(task, fileIndex) {
786
952
  };
787
953
  }
788
954
 
789
- const referencedPaths = unionArrays(...evidenceLines.map((line) => extractEvidencePaths(line.text)));
790
- const matchedPaths = referencedPaths.length > 0 ? findFilesByPathHints(referencedPaths, fileIndex) : [];
955
+ const extractedReferences = evidenceLines.map((line) => extractReferencedPaths(line.text));
956
+ const referencedPaths = unionArrays(...extractedReferences.map((entry) => entry.repoPaths));
957
+ const matchedPaths = referencedPaths.length > 0 ? findFilesByPathHints(referencedPaths, pathHintResolver) : [];
791
958
  const summaryMatches = evidenceLines
792
959
  .filter((line) => evidenceLineHasPassingSummary(line.text))
793
960
  .map((line) => line.text);
@@ -1110,6 +1277,7 @@ function buildValidationContext(projectRoot, config, plugins) {
1110
1277
  const files = walkFiles(projectRoot);
1111
1278
  const fileIndex = readFileIndex(projectRoot, files, config);
1112
1279
  const testFrameworks = detectTestFrameworks(projectRoot, files);
1280
+ const pathHintResolver = buildPathHintResolver(fileIndex);
1113
1281
 
1114
1282
  return {
1115
1283
  projectRoot,
@@ -1117,26 +1285,31 @@ function buildValidationContext(projectRoot, config, plugins) {
1117
1285
  plugins,
1118
1286
  files,
1119
1287
  fileIndex,
1288
+ pathHintResolver,
1120
1289
  testFrameworks
1121
1290
  };
1122
1291
  }
1123
1292
 
1124
1293
  function validateTask(task, context, config, plugins) {
1125
- const { paths: pathHints, lineReferenceHints } = extractExplicitPaths(task.text);
1294
+ const {
1295
+ paths: pathHints,
1296
+ externalPaths,
1297
+ lineReferenceHints
1298
+ } = extractExplicitPaths(task.text);
1126
1299
  // Paths that are line-reference hints (file.ts:NN) indicate WHERE to implement,
1127
1300
  // not that implementation exists. They are excluded from hasDirectReferencePass.
1128
1301
  const purePathHints = pathHints.filter((p) => !lineReferenceHints.has(p));
1129
1302
  const standaloneFilenames = extractStandaloneFilenames(task.text);
1130
1303
  const symbolHints = extractSymbolHints(task.text);
1131
- const authoritativeEvidence = evaluateAuthoritativeEvidence(task, context.fileIndex);
1304
+ const authoritativeEvidence = evaluateAuthoritativeEvidence(task, context.pathHintResolver);
1132
1305
 
1133
- const filesFromPaths = findFilesByPathHints(pathHints, context.fileIndex);
1134
- const filesFromPurePathHints = findFilesByPathHints(purePathHints, context.fileIndex);
1306
+ const filesFromPaths = findFilesByPathHints(pathHints, context.pathHintResolver);
1307
+ const filesFromPurePathHints = findFilesByPathHints(purePathHints, context.pathHintResolver);
1135
1308
  const filesFromSymbols = findFilesBySymbols(symbolHints, context.fileIndex);
1136
1309
  // Combine path hints AND standalone filenames for token exclusion so that tokens
1137
1310
  // derived from any referenced filename (e.g. "roadmap-skill" from
1138
1311
  // "roadmap-skill.config.json") are excluded from code evidence scoring.
1139
- const pathDerivedTokens = extractPathDerivedTokens([...pathHints, ...standaloneFilenames]);
1312
+ const pathDerivedTokens = extractPathDerivedTokens([...pathHints, ...externalPaths, ...standaloneFilenames]);
1140
1313
  const filesFromCode = findCodeEvidence(task.text, context.fileIndex, pathDerivedTokens);
1141
1314
  const filesFromWeakPathTokens = findFilesByTaskPathTokens(task.text, context.fileIndex, pathDerivedTokens);
1142
1315
  const weakPathContentTokens = findWeakPathContentSpecificTokens(task.text, context.fileIndex, filesFromWeakPathTokens, pathDerivedTokens);