roadmapsmith 0.9.22 → 0.9.24

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.22",
3
+ "version": "0.9.24",
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.",
5
5
  "author": {
6
6
  "name": "PapiScholz"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roadmapsmith",
3
- "version": "0.9.22",
3
+ "version": "0.9.24",
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.",
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.22",
3
+ "version": "0.9.24",
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.",
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.22"
32
+ "version": "0.9.24"
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.22"
38
+ "version": "0.9.24"
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.22"
44
+ "version": "0.9.24"
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 doctor JSON.",
50
- "version": "0.9.22"
50
+ "version": "0.9.24"
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.22"
56
+ "version": "0.9.24"
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.22"
62
+ "version": "0.9.24"
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.22"
68
+ "version": "0.9.24"
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.22"
74
+ "version": "0.9.24"
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.22"
80
+ "version": "0.9.24"
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.22"
86
+ "version": "0.9.24"
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.22"
92
+ "version": "0.9.24"
93
93
  }
94
94
  ]
95
95
  }
@@ -21,6 +21,7 @@ const ELECTRON_CONFIGS = [
21
21
  'forge.config.ts'
22
22
  ];
23
23
  const LANDING_ROUTE_RE = /(?:^|\/)(?:contact|services|about|pricing|hero|cta|landing)(?:\/|\.)/i;
24
+ const FIXTURE_PATH_RE = /(^|\/)(?:test|tests)\/fixtures\//i;
24
25
 
25
26
  function readPackageDeps(projectRoot) {
26
27
  if (!projectRoot) return [];
@@ -52,6 +53,9 @@ function hasWorkspaces(projectRoot) {
52
53
  }
53
54
 
54
55
  function classifyProject({ projectRoot, files }) {
56
+ const candidateFiles = Array.isArray(files)
57
+ ? files.filter((file) => !FIXTURE_PATH_RE.test(String(file || '')))
58
+ : [];
55
59
  const signals = [];
56
60
 
57
61
  if (hasWorkspaces(projectRoot)) {
@@ -59,8 +63,8 @@ function classifyProject({ projectRoot, files }) {
59
63
  return { type: 'monorepo', confidence: 'high', signals };
60
64
  }
61
65
 
62
- const hasPy = hasFilename(files, 'pyproject.toml') || hasFilename(files, 'setup.py');
63
- if (hasPy && !files.some((f) => /\.[jt]sx?$/.test(f))) {
66
+ const hasPy = hasFilename(candidateFiles, 'pyproject.toml') || hasFilename(candidateFiles, 'setup.py');
67
+ if (hasPy && !candidateFiles.some((f) => /\.[jt]sx?$/.test(f))) {
64
68
  signals.push('pyproject.toml / setup.py, no JS files');
65
69
  return { type: 'python-package', confidence: 'high', signals };
66
70
  }
@@ -82,72 +86,72 @@ function classifyProject({ projectRoot, files }) {
82
86
  }
83
87
 
84
88
  for (const dir of WEB_DIRS) {
85
- if (hasDir(files, dir)) {
89
+ if (hasDir(candidateFiles, dir)) {
86
90
  webScore += 2;
87
91
  signals.push(`directory: ${dir.replace(/\/$/, '')}`);
88
92
  }
89
93
  }
90
94
 
91
95
  for (const dir of ASSET_DIRS) {
92
- if (hasDir(files, dir)) {
96
+ if (hasDir(candidateFiles, dir)) {
93
97
  webScore += 1;
94
98
  signals.push(`directory: ${dir.replace(/\/$/, '')}`);
95
99
  }
96
100
  }
97
101
 
98
- if (hasDir(files, 'electron/')) {
102
+ if (hasDir(candidateFiles, 'electron/')) {
99
103
  electronScore += 3;
100
104
  signals.push('directory: electron');
101
105
  }
102
106
 
103
- if (files.some((file) => /^electron\/.+\.(js|ts|cjs|mjs)$/.test(file))) {
107
+ if (candidateFiles.some((file) => /^electron\/.+\.(js|ts|cjs|mjs)$/.test(file))) {
104
108
  electronScore += 2;
105
109
  signals.push('electron main/preload sources');
106
110
  }
107
111
 
108
112
  for (const cfg of ELECTRON_CONFIGS) {
109
- if (hasFilename(files, cfg)) {
113
+ if (hasFilename(candidateFiles, cfg)) {
110
114
  electronScore += 2;
111
115
  signals.push(`config: ${cfg}`);
112
116
  }
113
117
  }
114
118
 
115
119
  for (const cfg of WEB_CONFIGS) {
116
- if (hasFilename(files, cfg)) {
120
+ if (hasFilename(candidateFiles, cfg)) {
117
121
  webScore += 3;
118
122
  signals.push(`config: ${cfg}`);
119
123
  }
120
124
  }
121
125
 
122
126
  for (const cfg of STYLE_CONFIGS) {
123
- if (hasFilename(files, cfg)) {
127
+ if (hasFilename(candidateFiles, cfg)) {
124
128
  webScore += 1;
125
129
  signals.push(`config: ${cfg}`);
126
130
  }
127
131
  }
128
132
 
129
- if (files.some((f) => /\.css$/.test(f))) {
133
+ if (candidateFiles.some((f) => /\.css$/.test(f))) {
130
134
  webScore += 1;
131
135
  signals.push('CSS files present');
132
136
  }
133
137
 
134
- const landingRoutes = files.filter((f) => LANDING_ROUTE_RE.test(f));
138
+ const landingRoutes = candidateFiles.filter((f) => LANDING_ROUTE_RE.test(f));
135
139
  if (landingRoutes.length > 0) {
136
140
  landingScore += landingRoutes.length * 2;
137
141
  signals.push(`landing/service routes: ${landingRoutes.length}`);
138
142
  }
139
143
 
140
- if (hasFilename(files, 'favicon.ico') || hasFilename(files, 'logo.png') || hasFilename(files, 'logo.svg')) {
144
+ if (hasFilename(candidateFiles, 'favicon.ico') || hasFilename(candidateFiles, 'logo.png') || hasFilename(candidateFiles, 'logo.svg')) {
141
145
  landingScore += 1;
142
146
  signals.push('branding asset in public/');
143
147
  }
144
148
 
145
- if (webScore === 0 && (files.some((f) => f.startsWith('bin/')) || hasFilename(files, 'cli.js'))) {
149
+ if (webScore === 0 && (candidateFiles.some((f) => f.startsWith('bin/')) || hasFilename(candidateFiles, 'cli.js'))) {
146
150
  signals.push('bin/ directory or cli.js');
147
151
  return { type: 'cli-tool', confidence: 'medium', signals };
148
152
  }
149
153
 
150
- if (webScore === 0 && hasFilename(files, 'package.json')) {
154
+ if (webScore === 0 && hasFilename(candidateFiles, 'package.json')) {
151
155
  signals.push('package.json, no web signals');
152
156
  return { type: 'npm-package', confidence: 'low', signals };
153
157
  }
@@ -207,6 +207,10 @@ function renderAdditionTask(task) {
207
207
  return `- [ ] ${task.text} <!-- rs:task=${task.id} -->`;
208
208
  }
209
209
 
210
+ function isGenericPreserveModeCandidate(candidate) {
211
+ return candidate && ['default', 'classifier', 'todo-hint'].includes(candidate.source);
212
+ }
213
+
210
214
  function buildManagedAdditionsLines(tasks, options = {}) {
211
215
  const groups = groupByPhase(tasks);
212
216
  const lines = [];
@@ -465,6 +469,10 @@ function insertPreserveModeTasks(existingContent, parsedRoadmap, tasks) {
465
469
  return nextLines.join('\n');
466
470
  }
467
471
 
472
+ function filterPreserveModeCandidates(candidates) {
473
+ return candidates.filter((candidate) => !isGenericPreserveModeCandidate(candidate));
474
+ }
475
+
468
476
  function mergeWithExisting(candidates, existingTasks, options = {}) {
469
477
  const matchedExistingIds = new Set();
470
478
  const merged = [];
@@ -810,14 +818,18 @@ function generateRoadmapDocument(options) {
810
818
 
811
819
  if (hasSubstantiveManagedBlock(existing) && preserveManagedBlock && !forceFullRegenerate) {
812
820
  const unmatchedCandidates = allCandidates.filter((candidate) => {
813
- return !findBestTaskMatch(candidate, existingManagedTasks, { allowFuzzy: false });
821
+ return !findBestTaskMatch(candidate, existingManagedTasks, {
822
+ allowFuzzy: true,
823
+ minScore: 0.72
824
+ });
814
825
  });
826
+ const preserveModeCandidates = filterPreserveModeCandidates(unmatchedCandidates);
815
827
 
816
- if (unmatchedCandidates.length === 0) {
828
+ if (preserveModeCandidates.length === 0) {
817
829
  return existingContent;
818
830
  }
819
831
 
820
- return insertPreserveModeTasks(existingContent, existing, unmatchedCandidates);
832
+ return insertPreserveModeTasks(existingContent, existing, preserveModeCandidates);
821
833
  }
822
834
 
823
835
  if (hasSubstantiveManagedBlock(existing) && !forceFullRegenerate) {
package/src/sync/index.js CHANGED
@@ -3,6 +3,8 @@
3
3
  const { parseRoadmap } = require('../parser');
4
4
  const { ensureTrailingNewline } = require('../utils');
5
5
 
6
+ const WARNING_REASON_PREFIX = 'attempted but validation failed:';
7
+
6
8
  function setChecklistState(line, checked) {
7
9
  return line.replace(/- \[( |x|X)\]/, `- [${checked ? 'x' : ' '}]`);
8
10
  }
@@ -11,6 +13,95 @@ function formatWarning(indent, reason) {
11
13
  return `${indent} - ⚠️ attempted but validation failed: ${reason}`;
12
14
  }
13
15
 
16
+ function isWhitespaceCharacter(char) {
17
+ return char === ' ' || char === '\t' || char === '\n' || char === '\r' || char === '\f' || char === '\v';
18
+ }
19
+
20
+ function stripLeadingWarningMarker(value) {
21
+ let index = 0;
22
+ const source = String(value || '');
23
+ while (index < source.length && isWhitespaceCharacter(source[index])) {
24
+ index += 1;
25
+ }
26
+ if (source.slice(index, index + 2) === '⚠️') {
27
+ index += 2;
28
+ }
29
+ while (index < source.length && isWhitespaceCharacter(source[index])) {
30
+ index += 1;
31
+ }
32
+ return source.slice(index);
33
+ }
34
+
35
+ function splitWarningReasonSegments(value) {
36
+ const source = String(value || '');
37
+ const segments = [];
38
+ let current = '';
39
+ let index = 0;
40
+
41
+ while (index < source.length) {
42
+ const char = source[index];
43
+ if (char === ';') {
44
+ const trimmed = current.trim();
45
+ if (trimmed) {
46
+ segments.push(trimmed);
47
+ }
48
+ current = '';
49
+ index += 1;
50
+ while (index < source.length && isWhitespaceCharacter(source[index])) {
51
+ index += 1;
52
+ }
53
+ continue;
54
+ }
55
+
56
+ current += char;
57
+ index += 1;
58
+ }
59
+
60
+ const trimmed = current.trim();
61
+ if (trimmed) {
62
+ segments.push(trimmed);
63
+ }
64
+
65
+ return segments;
66
+ }
67
+
68
+ function normalizeWarningReason(reason) {
69
+ let normalized = String(reason || '').trim();
70
+ if (!normalized) {
71
+ return '';
72
+ }
73
+
74
+ normalized = stripLeadingWarningMarker(normalized).trim();
75
+ const prefixIndex = normalized.indexOf(WARNING_REASON_PREFIX);
76
+ if (prefixIndex >= 0) {
77
+ normalized = normalized.slice(prefixIndex + WARNING_REASON_PREFIX.length).trim();
78
+ }
79
+
80
+ return normalized;
81
+ }
82
+
83
+ function normalizeWarningReasons(reasons) {
84
+ const normalized = [];
85
+ const seen = new Set();
86
+ for (const reason of Array.isArray(reasons) ? reasons : [reasons]) {
87
+ for (const chunk of splitWarningReasonSegments(reason)) {
88
+ const clean = normalizeWarningReason(chunk);
89
+ if (!clean || seen.has(clean)) {
90
+ continue;
91
+ }
92
+ seen.add(clean);
93
+ normalized.push(clean);
94
+ }
95
+ }
96
+ return normalized;
97
+ }
98
+
99
+ function shouldPreserveExistingWarning(existingReason, newReason) {
100
+ const cleanExisting = normalizeWarningReason(existingReason);
101
+ const cleanNew = normalizeWarningReason(newReason) || 'validation failed';
102
+ return cleanNew === 'validation failed' && cleanExisting && cleanExisting !== cleanNew;
103
+ }
104
+
14
105
  function applySync(content, parsedTasks, results) {
15
106
  const parsed = parseRoadmap(content);
16
107
  const lines = [...parsed.lines];
@@ -30,7 +121,7 @@ function applySync(content, parsedTasks, results) {
30
121
 
31
122
  lines[lineIndex] = setChecklistState(lines[lineIndex], result.passed);
32
123
 
33
- const reason = result.reasons.join('; ');
124
+ const reason = normalizeWarningReasons(result.reasons).join('; ');
34
125
  const warningText = formatWarning(task.indent || '', reason || 'validation failed');
35
126
  const hasWarning = task.warningLineIndex != null;
36
127
  const warningIndex = hasWarning ? task.warningLineIndex + offset : null;
@@ -47,9 +138,7 @@ function applySync(content, parsedTasks, results) {
47
138
  if (warningIndex != null && warningIndex >= 0 && warningIndex < lines.length) {
48
139
  const existingReason = lines[warningIndex].split('validation failed:')[1];
49
140
  const newReason = reason || 'validation failed';
50
- // Preserve existing warning when it's more descriptive than the new generic message.
51
- const existingIsMoreSpecific = existingReason && existingReason.trim().length > newReason.length;
52
- if (!existingIsMoreSpecific) {
141
+ if (!shouldPreserveExistingWarning(existingReason, newReason)) {
53
142
  lines[warningIndex] = warningText;
54
143
  }
55
144
  } else {
@@ -373,6 +373,10 @@ function isDocTask(taskText) {
373
373
  return /\b(add|create|write|update|init|initialize|introduce|setup|document)\b/.test(normalized);
374
374
  }
375
375
 
376
+ function isImplementationTask(taskText) {
377
+ return !isDocTask(taskText) && (isCodeTask(taskText) || taskDescribesChange(taskText));
378
+ }
379
+
376
380
  function findFilesByPathHints(pathHints, fileIndex) {
377
381
  const matches = [];
378
382
  for (const hint of pathHints) {
@@ -1177,7 +1181,13 @@ function validateTask(task, context, config, plugins) {
1177
1181
  uniqueReasons = Array.isArray(overrideResult.reasons) ? Array.from(new Set(overrideResult.reasons)) : [];
1178
1182
  }
1179
1183
 
1180
- const attempted = hasStrongEvidence || hasWeakEvidence || pathHints.length > 0 || symbolHints.length > 0 || authoritativeEvidence.active;
1184
+ const hasConcreteReferenceEvidence = filesFromPurePathHints.length > 0 || filesFromSymbols.length > 0;
1185
+ const attempted = authoritativeEvidence.active
1186
+ || hasRuleGrantedEvidence
1187
+ || evidence.code
1188
+ || evidence.test
1189
+ || evidence.artifact
1190
+ || hasConcreteReferenceEvidence;
1181
1191
  const { categories: strongEvidenceCategories } = countStrongEvidenceCategories(task.text, evidence);
1182
1192
  const strongEvidenceCount = strongEvidenceCategories.length;
1183
1193
  // Only pure path hints (not line-reference hints like file.ts:169) count as direct evidence.
@@ -1220,6 +1230,7 @@ function validateTask(task, context, config, plugins) {
1220
1230
  // WHERE to implement, not that implementation is done. Unchecked tasks need authoritative
1221
1231
  // evidence, artifact evidence, or strong code+test threshold to pass.
1222
1232
  // Already-checked tasks with found path hints are preserved via shouldPreserveCheckedTask.
1233
+ const hasHighConfidenceImplementationEvidence = meetsStrongThreshold && evidence.code && evidence.test;
1223
1234
  let passed = authoritativeEvidence.passed || hasArtifactTaskPass || hasTrustedRuleEvidencePass || meetsStrongThreshold;
1224
1235
 
1225
1236
  if (!passed && !task.checked && hasDirectReferencePass) {
@@ -1234,30 +1245,29 @@ function validateTask(task, context, config, plugins) {
1234
1245
  // human/agent judgment that the feature is incomplete.
1235
1246
  if (task.warningText && !task.checked && passed && !authoritativeEvidence.passed) {
1236
1247
  passed = false;
1237
- uniqueReasons.push(task.warningText);
1238
- uniqueReasons = Array.from(new Set(uniqueReasons));
1248
+ if (uniqueReasons.length === 0) {
1249
+ uniqueReasons.push('validation failed');
1250
+ }
1239
1251
  }
1240
1252
  if (negativeSignalMatches.length > 0) {
1241
1253
  passed = false;
1242
1254
  }
1243
1255
 
1244
- // Action-verb gate (Causa 3): unchecked tasks that describe a change to be made
1245
- // (Agregar, Configurar, Add, Fix, Manejo, Recovery path, …) cannot pass on code token overlap alone.
1246
- // Requires either: an Evidence line (authoritativeEvidence.passed), high-confidence evidence
1247
- // (code + test), grant-evidence from config (hasTrustedRuleEvidencePass), or canonical artifact
1248
- // evidence (hasArtifactTaskPass — e.g. "Add SECURITY.md").
1256
+ // Unchecked implementation tasks need explicit evidence or high-confidence implementation
1257
+ // evidence. Weak token overlap, direct file references, or code-only matches are not enough.
1249
1258
  if (
1250
1259
  !task.checked &&
1251
1260
  passed &&
1252
- taskDescribesChange(task.text) &&
1261
+ isImplementationTask(task.text) &&
1253
1262
  !authoritativeEvidence.passed &&
1254
1263
  !hasTrustedRuleEvidencePass &&
1255
- !hasArtifactTaskPass
1264
+ !hasArtifactTaskPass &&
1265
+ !hasHighConfidenceImplementationEvidence
1256
1266
  ) {
1257
1267
  passed = false;
1258
- const actionVerbReason = 'action task requires Evidence line or high-confidence evidence (code + test) to be marked complete';
1259
- if (!uniqueReasons.includes(actionVerbReason)) {
1260
- uniqueReasons.push(actionVerbReason);
1268
+ const implementationReason = 'implementation task requires Evidence line or high-confidence evidence (code + test) to be marked complete';
1269
+ if (!uniqueReasons.includes(implementationReason)) {
1270
+ uniqueReasons.push(implementationReason);
1261
1271
  }
1262
1272
  }
1263
1273