roadmapsmith 0.9.4 → 0.9.6

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/README.md CHANGED
@@ -22,6 +22,29 @@ npx skills add PapiScholz/roadmapsmith --skill roadmap-sync
22
22
 
23
23
  This adds the `roadmap-sync` agent skill. It does not install the CLI package.
24
24
 
25
+ ## Updating
26
+
27
+ Update the CLI based on how it was installed:
28
+
29
+ ```bash
30
+ # Global npm install
31
+ npm install -g roadmapsmith@latest
32
+
33
+ # Project dependency
34
+ npm install roadmapsmith@latest
35
+
36
+ # One-off execution without installing
37
+ npx roadmapsmith@latest sync --audit
38
+ ```
39
+
40
+ The `roadmap-sync` agent skill is separate from the CLI. Re-running the skills install updates the agent instructions, but it does not update the `roadmapsmith` npm binary:
41
+
42
+ ```bash
43
+ npx skills add PapiScholz/roadmapsmith --skill roadmap-sync
44
+ ```
45
+
46
+ Fixes are available through `@latest` only after a new npm package version has been published. Before publication, install from a local checkout or a packed tarball for testing.
47
+
25
48
  ## Operating Modes
26
49
 
27
50
  ### Zero Mode
package/bin/cli.js CHANGED
@@ -44,6 +44,14 @@ function maybeFilterTasks(tasks, filterValue) {
44
44
  });
45
45
  }
46
46
 
47
+ function tasksInManagedBlock(parsedRoadmap) {
48
+ if (!parsedRoadmap.managedRange) {
49
+ return parsedRoadmap.tasks;
50
+ }
51
+ const { start, end } = parsedRoadmap.managedRange;
52
+ return parsedRoadmap.tasks.filter((task) => task.lineIndex > start && task.lineIndex < end);
53
+ }
54
+
47
55
  function printAudit(audit) {
48
56
  console.log(`Audit summary: ${audit.checkedWithoutEvidence.length} checked-without-evidence, ${audit.readyButUnchecked.length} ready-but-unchecked.`);
49
57
  if (audit.checkedWithoutEvidence.length > 0) {
@@ -157,10 +165,11 @@ async function run() {
157
165
  }
158
166
 
159
167
  const parsedRoadmap = parseRoadmap(content);
168
+ const syncTasks = tasksInManagedBlock(parsedRoadmap);
160
169
  const validationContext = buildValidationContext(projectRoot, config, loadPlugins(projectRoot, config.plugins));
161
- const results = validateTasks(parsedRoadmap.tasks, validationContext, config, validationContext.plugins);
170
+ const results = validateTasks(syncTasks, validationContext, config, validationContext.plugins);
162
171
  applyMinimumConfidence(results, config.validation?.minimumConfidence);
163
- const next = applySync(content, parsedRoadmap.tasks, results);
172
+ const next = applySync(content, syncTasks, results);
164
173
  const dryRun = isEnabled(flags['dry-run']);
165
174
  const writeResult = writeText(roadmapFile, next, { dryRun });
166
175
 
@@ -175,7 +184,7 @@ async function run() {
175
184
  }
176
185
 
177
186
  if (isEnabled(flags.audit)) {
178
- const audit = auditValidation(parsedRoadmap.tasks, results);
187
+ const audit = auditValidation(syncTasks, results);
179
188
  printAudit(audit);
180
189
  }
181
190
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roadmapsmith",
3
- "version": "0.9.4",
3
+ "version": "0.9.6",
4
4
  "description": "Evidence-backed ROADMAP.md generator and sync tool for AI coding agents.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -11,6 +11,7 @@ const MANAGED_END = '<!-- rs:managed:end -->';
11
11
 
12
12
  function parseRoadmap(content) {
13
13
  const lines = String(content || '').split(/\r?\n/);
14
+ const managedRange = findManagedRange(lines);
14
15
  const tasks = [];
15
16
  let section = '';
16
17
 
@@ -61,6 +62,8 @@ function parseRoadmap(content) {
61
62
 
62
63
  return {
63
64
  lines,
65
+ managedRange,
66
+ hasManagedBlock: Boolean(managedRange),
64
67
  tasks
65
68
  };
66
69
  }
@@ -105,6 +108,7 @@ function upsertManagedBlock(existingContent, managedBody) {
105
108
  }
106
109
 
107
110
  module.exports = {
111
+ findManagedRange,
108
112
  parseRoadmap,
109
113
  upsertManagedBlock
110
114
  };
@@ -333,6 +333,38 @@ function findFilesByTaskPathTokens(taskText, fileIndex, pathDerivedTokens = new
333
333
  return Array.from(matches).sort((left, right) => left.localeCompare(right));
334
334
  }
335
335
 
336
+ function extractTaskEvidenceTokens(taskText, pathDerivedTokens = new Set()) {
337
+ return tokenize(taskText)
338
+ .filter((token) => token.length >= 3 && !GENERIC_TASK_TOKENS.has(token) && !token.endsWith('/') && !pathDerivedTokens.has(token))
339
+ .slice(0, 8);
340
+ }
341
+
342
+ function findWeakPathContentSpecificTokens(taskText, fileIndex, weakPathFiles, pathDerivedTokens = new Set()) {
343
+ const tokens = extractTaskEvidenceTokens(taskText, pathDerivedTokens);
344
+ if (tokens.length === 0 || weakPathFiles.length === 0) return [];
345
+
346
+ const weakFiles = new Set(weakPathFiles);
347
+ const matches = new Set();
348
+ for (const file of fileIndex) {
349
+ if (!weakFiles.has(file.relativePath) || !CODE_EXTENSIONS.has(file.ext) || file.isTestFile) {
350
+ continue;
351
+ }
352
+
353
+ const normalizedPath = normalizePathForMatch(file.relativePath);
354
+ const lowered = file.content.toLowerCase();
355
+ for (const token of tokens) {
356
+ if (normalizedPath.includes(token)) {
357
+ continue;
358
+ }
359
+ if (lowered.includes(token)) {
360
+ matches.add(token);
361
+ }
362
+ }
363
+ }
364
+
365
+ return Array.from(matches).sort((left, right) => left.localeCompare(right));
366
+ }
367
+
336
368
  function mergeRuleEvidence(baseEvidence, ruleEvidence) {
337
369
  if (!ruleEvidence || typeof ruleEvidence !== 'object') return baseEvidence;
338
370
  const merged = { ...baseEvidence };
@@ -376,9 +408,7 @@ function extractPathDerivedTokens(pathHints) {
376
408
  }
377
409
 
378
410
  function findCodeEvidence(taskText, fileIndex, pathDerivedTokens = new Set()) {
379
- const tokens = tokenize(taskText)
380
- .filter((token) => token.length >= 3 && !GENERIC_TASK_TOKENS.has(token) && !token.endsWith('/') && !pathDerivedTokens.has(token))
381
- .slice(0, 8);
411
+ const tokens = extractTaskEvidenceTokens(taskText, pathDerivedTokens);
382
412
  if (tokens.length === 0) {
383
413
  return [];
384
414
  }
@@ -750,6 +780,7 @@ function validateTask(task, context, config, plugins) {
750
780
  const pathDerivedTokens = extractPathDerivedTokens([...pathHints, ...standaloneFilenames]);
751
781
  const filesFromCode = findCodeEvidence(task.text, context.fileIndex, pathDerivedTokens);
752
782
  const filesFromWeakPathTokens = findFilesByTaskPathTokens(task.text, context.fileIndex, pathDerivedTokens);
783
+ const weakPathContentTokens = findWeakPathContentSpecificTokens(task.text, context.fileIndex, filesFromWeakPathTokens, pathDerivedTokens);
753
784
  const filesFromTests = findTestEvidence(task.text, context.fileIndex, [...pathHints, ...standaloneFilenames]);
754
785
  const { files: filesFromArtifacts, heuristicArtifacts } = findArtifactEvidence(task.text, context.fileIndex);
755
786
 
@@ -763,6 +794,7 @@ function validateTask(task, context, config, plugins) {
763
794
  symbols: filesFromSymbols,
764
795
  codeFiles: filesFromCode,
765
796
  weakPathFiles: filesFromWeakPathTokens,
797
+ weakPathContentTokens,
766
798
  testFiles: filesFromTests,
767
799
  artifactFiles: filesFromArtifacts,
768
800
  heuristicArtifacts,
@@ -790,6 +822,12 @@ function validateTask(task, context, config, plugins) {
790
822
  reasons.push('no code, test, or artifact evidence found');
791
823
  } else if (!hasEvidence && !hasWeakEvidence && structuralCheck.applicable && structuralCheck.passed) {
792
824
  reasons.push('no code, test, or artifact evidence found');
825
+ } else if (!hasEvidence && hasWeakEvidence) {
826
+ if (weakPathContentTokens.length === 0) {
827
+ reasons.push('weak path-only evidence lacks content-specific token match');
828
+ } else {
829
+ reasons.push('weak path-token evidence lacks strong code, test, or artifact evidence');
830
+ }
793
831
  }
794
832
 
795
833
  const requiresTest = !task.noTest && context.testFrameworks.length > 0 && isCodeTask(task.text) && !isDocTask(task.text);