roadmapsmith 0.9.8 → 0.9.10
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/package.json +1 -1
- package/src/io.js +1 -1
- package/src/parser/index.js +1 -1
- package/src/sync/index.js +7 -1
- package/src/validator/index.js +91 -17
package/package.json
CHANGED
package/src/io.js
CHANGED
|
@@ -159,7 +159,7 @@ function detectTestFrameworks(projectRoot, files) {
|
|
|
159
159
|
if (files.some((file) => file.endsWith('pyproject.toml')) || files.some((file) => file.endsWith('pytest.ini'))) {
|
|
160
160
|
frameworks.add('pytest');
|
|
161
161
|
}
|
|
162
|
-
if (files.some((file) => /(^|\/)test_
|
|
162
|
+
if (files.some((file) => /(^|\/)test_[^/]*\.py$/.test(file)) || files.some((file) => /(^|\/)tests\//.test(file))) {
|
|
163
163
|
frameworks.add('python-tests');
|
|
164
164
|
}
|
|
165
165
|
if (files.some((file) => file.endsWith('go.mod'))) {
|
package/src/parser/index.js
CHANGED
|
@@ -203,7 +203,7 @@ function upsertManagedBlock(existingContent, managedBody) {
|
|
|
203
203
|
if (existing.trim().length === 0) {
|
|
204
204
|
return [MANAGED_START, ...bodyLines, MANAGED_END].join('\n');
|
|
205
205
|
}
|
|
206
|
-
return `${existing.
|
|
206
|
+
return `${existing.trimEnd()}\n\n${MANAGED_START}\n${managedBody}\n${MANAGED_END}`;
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
const prefix = lines.slice(0, range.start + 1);
|
package/src/sync/index.js
CHANGED
|
@@ -45,7 +45,13 @@ function applySync(content, parsedTasks, results) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
if (warningIndex != null && warningIndex >= 0 && warningIndex < lines.length) {
|
|
48
|
-
lines[warningIndex]
|
|
48
|
+
const existingReason = lines[warningIndex].split('validation failed:')[1];
|
|
49
|
+
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) {
|
|
53
|
+
lines[warningIndex] = warningText;
|
|
54
|
+
}
|
|
49
55
|
} else {
|
|
50
56
|
lines.splice(lastChildLineIndex + 1, 0, warningText);
|
|
51
57
|
offset += 1;
|
package/src/validator/index.js
CHANGED
|
@@ -178,7 +178,11 @@ function hasFileExtension(token) {
|
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
function isLikelyPath(token) {
|
|
181
|
-
if (
|
|
181
|
+
if (token.includes('*') || token.includes('?')) return false; // glob/wildcard
|
|
182
|
+
if (/^\.{1,2}\/|^\//.test(token)) {
|
|
183
|
+
// Bare "/" or "./" with nothing after is not a real path (e.g. "API / ESC-POS" → "/")
|
|
184
|
+
return /[A-Za-z0-9_]/.test(token);
|
|
185
|
+
}
|
|
182
186
|
if (hasFileExtension(token)) return true;
|
|
183
187
|
if (KNOWN_PATH_ROOTS.some((root) => token.startsWith(root))) return true;
|
|
184
188
|
// The ">= 2 slashes" rule was intentionally removed: it caused conceptual slash phrases
|
|
@@ -249,23 +253,51 @@ function collectPathishTokens(text) {
|
|
|
249
253
|
return tokens.filter(Boolean);
|
|
250
254
|
}
|
|
251
255
|
|
|
256
|
+
// LINE_REF_RE matches "path/file.ext:NN" or "path/file.ext:NN-MM" — indicates WHERE
|
|
257
|
+
// to implement, not that implementation exists. Paths matching this pattern are added
|
|
258
|
+
// to lineReferenceHints and excluded from hasDirectReferencePass scoring.
|
|
259
|
+
const LINE_REF_RE = /^(.+?):(\d+)(?:-\d+)?$/;
|
|
260
|
+
|
|
252
261
|
function extractExplicitPaths(text) {
|
|
253
262
|
const results = new Set();
|
|
263
|
+
const lineReferenceHints = new Set();
|
|
264
|
+
|
|
254
265
|
const quoted = String(text).match(/`([^`]+)`/g) || [];
|
|
255
266
|
for (const token of quoted) {
|
|
256
267
|
const clean = token.slice(1, -1);
|
|
257
|
-
if (clean.includes('
|
|
268
|
+
if (clean.includes('*') || clean.includes('?')) continue; // glob
|
|
269
|
+
const hasSlash = clean.includes('/') || clean.includes('\\');
|
|
270
|
+
// Require a slash or a known file extension — rejects property access like err.message,
|
|
271
|
+
// fs.readFileSync, error.stack (whose extensions are not in KNOWN_FILE_EXTENSIONS).
|
|
272
|
+
if (!hasSlash && !hasKnownFileExtension(clean)) continue;
|
|
273
|
+
const lineMatch = LINE_REF_RE.exec(clean);
|
|
274
|
+
if (lineMatch && hasKnownFileExtension(lineMatch[1])) {
|
|
275
|
+
lineReferenceHints.add(lineMatch[1]);
|
|
276
|
+
results.add(lineMatch[1]);
|
|
277
|
+
} else {
|
|
258
278
|
results.add(clean);
|
|
259
279
|
}
|
|
260
280
|
}
|
|
261
281
|
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
const token = stripTrailingPathPunctuation(
|
|
265
|
-
if (
|
|
282
|
+
for (const word of String(text).split(/\s+/)) {
|
|
283
|
+
if (!word.includes('/')) continue;
|
|
284
|
+
const token = stripTrailingPathPunctuation(word);
|
|
285
|
+
if (token.includes('*') || token.includes('?')) continue; // glob
|
|
286
|
+
const lineMatch = LINE_REF_RE.exec(token);
|
|
287
|
+
if (lineMatch && hasKnownFileExtension(lineMatch[1])) {
|
|
288
|
+
if (isLikelyPath(lineMatch[1])) {
|
|
289
|
+
lineReferenceHints.add(lineMatch[1]);
|
|
290
|
+
results.add(lineMatch[1]);
|
|
291
|
+
}
|
|
292
|
+
} else if (isLikelyPath(token)) {
|
|
293
|
+
results.add(token);
|
|
294
|
+
}
|
|
266
295
|
}
|
|
267
296
|
|
|
268
|
-
|
|
297
|
+
const paths = Array.from(results)
|
|
298
|
+
.filter((p) => !p.includes('*') && !p.includes('?'))
|
|
299
|
+
.sort((left, right) => left.localeCompare(right));
|
|
300
|
+
return { paths, lineReferenceHints };
|
|
269
301
|
}
|
|
270
302
|
|
|
271
303
|
// Standalone filenames (no slash) mentioned in task prose — e.g. "roadmap-skill.config.json",
|
|
@@ -644,7 +676,7 @@ function extractEvidencePaths(evidenceText) {
|
|
|
644
676
|
|
|
645
677
|
function evidenceLineHasPassingSummary(evidenceText) {
|
|
646
678
|
const text = String(evidenceText || '');
|
|
647
|
-
if (/\b\d
|
|
679
|
+
if (/\b\d+(?:\s*\/\s*\d+)?\s+tests?\s+passing\b/i.test(text)) {
|
|
648
680
|
return true;
|
|
649
681
|
}
|
|
650
682
|
if (/\b(?:vitest|jest|npm test|pnpm test|yarn test|bun test)\b.*\b(?:pass(?:ing|ed)?|green|success(?:ful|fully)?)\b/i.test(text)) {
|
|
@@ -1009,12 +1041,16 @@ function buildValidationContext(projectRoot, config, plugins) {
|
|
|
1009
1041
|
}
|
|
1010
1042
|
|
|
1011
1043
|
function validateTask(task, context, config, plugins) {
|
|
1012
|
-
const pathHints = extractExplicitPaths(task.text);
|
|
1044
|
+
const { paths: pathHints, lineReferenceHints } = extractExplicitPaths(task.text);
|
|
1045
|
+
// Paths that are line-reference hints (file.ts:NN) indicate WHERE to implement,
|
|
1046
|
+
// not that implementation exists. They are excluded from hasDirectReferencePass.
|
|
1047
|
+
const purePathHints = pathHints.filter((p) => !lineReferenceHints.has(p));
|
|
1013
1048
|
const standaloneFilenames = extractStandaloneFilenames(task.text);
|
|
1014
1049
|
const symbolHints = extractSymbolHints(task.text);
|
|
1015
1050
|
const authoritativeEvidence = evaluateAuthoritativeEvidence(task, context.fileIndex);
|
|
1016
1051
|
|
|
1017
1052
|
const filesFromPaths = findFilesByPathHints(pathHints, context.fileIndex);
|
|
1053
|
+
const filesFromPurePathHints = findFilesByPathHints(purePathHints, context.fileIndex);
|
|
1018
1054
|
const filesFromSymbols = findFilesBySymbols(symbolHints, context.fileIndex);
|
|
1019
1055
|
// Combine path hints AND standalone filenames for token exclusion so that tokens
|
|
1020
1056
|
// derived from any referenced filename (e.g. "roadmap-skill" from
|
|
@@ -1032,7 +1068,9 @@ function validateTask(task, context, config, plugins) {
|
|
|
1032
1068
|
code: filesFromCode.length > 0 || filesFromSymbols.length > 0,
|
|
1033
1069
|
test: filesFromTests.length > 0,
|
|
1034
1070
|
artifact: filesFromArtifacts.length > 0,
|
|
1035
|
-
|
|
1071
|
+
// Use only pure path hints (not line-reference hints) so that "file.ts:169" style hints
|
|
1072
|
+
// — which indicate WHERE to implement — do not contribute to feature-surface scoring.
|
|
1073
|
+
files: filesFromPurePathHints,
|
|
1036
1074
|
symbols: filesFromSymbols,
|
|
1037
1075
|
codeFiles: filesFromCode,
|
|
1038
1076
|
weakPathFiles: filesFromWeakPathTokens,
|
|
@@ -1049,7 +1087,9 @@ function validateTask(task, context, config, plugins) {
|
|
|
1049
1087
|
applyAuthoritativeEvidence(evidence, authoritativeEvidence, context.fileIndex);
|
|
1050
1088
|
|
|
1051
1089
|
const reasons = [];
|
|
1052
|
-
|
|
1090
|
+
// Suppress path hint failures when authoritative evidence already confirms the task —
|
|
1091
|
+
// a bad path hint (typo, moved file) should not override a passing Evidence line.
|
|
1092
|
+
if (pathHints.length > 0 && filesFromPaths.length === 0 && !authoritativeEvidence.passed) {
|
|
1053
1093
|
reasons.push(`missing referenced file(s): ${pathHints.join(', ')}`);
|
|
1054
1094
|
}
|
|
1055
1095
|
if (Array.isArray(authoritativeEvidence.reasons) && authoritativeEvidence.reasons.length > 0) {
|
|
@@ -1107,7 +1147,7 @@ function validateTask(task, context, config, plugins) {
|
|
|
1107
1147
|
}
|
|
1108
1148
|
}
|
|
1109
1149
|
|
|
1110
|
-
if (requiresTest && !evidence.test && !authoritativeEvidence.passed) {
|
|
1150
|
+
if (requiresTest && !evidence.test && !authoritativeEvidence.passed && filesFromPurePathHints.length === 0) {
|
|
1111
1151
|
reasons.push('missing test evidence');
|
|
1112
1152
|
}
|
|
1113
1153
|
|
|
@@ -1120,7 +1160,8 @@ function validateTask(task, context, config, plugins) {
|
|
|
1120
1160
|
const attempted = hasStrongEvidence || hasWeakEvidence || pathHints.length > 0 || symbolHints.length > 0 || authoritativeEvidence.active;
|
|
1121
1161
|
const { categories: strongEvidenceCategories } = countStrongEvidenceCategories(task.text, evidence);
|
|
1122
1162
|
const strongEvidenceCount = strongEvidenceCategories.length;
|
|
1123
|
-
|
|
1163
|
+
// Only pure path hints (not line-reference hints like file.ts:169) count as direct evidence.
|
|
1164
|
+
const hasDirectReferencePass = filesFromPurePathHints.length > 0 || filesFromSymbols.length > 0;
|
|
1124
1165
|
const hasArtifactTaskPass = evidence.artifact && (
|
|
1125
1166
|
isDocTask(task.text) ||
|
|
1126
1167
|
evidence.heuristicArtifacts.length > 0 ||
|
|
@@ -1153,7 +1194,19 @@ function validateTask(task, context, config, plugins) {
|
|
|
1153
1194
|
uniqueReasons = Array.from(new Set(uniqueReasons));
|
|
1154
1195
|
}
|
|
1155
1196
|
|
|
1156
|
-
|
|
1197
|
+
// hasDirectReferencePass is intentionally excluded: a path hint in task text indicates
|
|
1198
|
+
// WHERE to implement, not that implementation is done. Unchecked tasks need authoritative
|
|
1199
|
+
// evidence, artifact evidence, or strong code+test threshold to pass.
|
|
1200
|
+
// Already-checked tasks with found path hints are preserved via shouldPreserveCheckedTask.
|
|
1201
|
+
let passed = authoritativeEvidence.passed || hasArtifactTaskPass || hasTrustedRuleEvidencePass || meetsStrongThreshold;
|
|
1202
|
+
|
|
1203
|
+
if (!passed && !task.checked && hasDirectReferencePass) {
|
|
1204
|
+
const locationReason = 'file reference shows implementation location, not confirmed completion';
|
|
1205
|
+
if (!uniqueReasons.includes(locationReason)) {
|
|
1206
|
+
uniqueReasons.push(locationReason);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1157
1210
|
if (task.warningText && passed && !authoritativeEvidence.passed && !meetsStrongThreshold) {
|
|
1158
1211
|
passed = false;
|
|
1159
1212
|
uniqueReasons.push(task.warningText);
|
|
@@ -1163,15 +1216,20 @@ function validateTask(task, context, config, plugins) {
|
|
|
1163
1216
|
passed = false;
|
|
1164
1217
|
}
|
|
1165
1218
|
|
|
1219
|
+
// Preserve already-checked tasks when the validator can't confirm implementation but also
|
|
1220
|
+
// can't find strong evidence against it. Two cases:
|
|
1221
|
+
// 1. No path/symbol hints at all — no machine-readable claims to evaluate.
|
|
1222
|
+
// 2. Path hints resolve to existing files but code/test evidence is absent —
|
|
1223
|
+
// file presence is not implementation; don't uncheck on that alone.
|
|
1224
|
+
// Symbol hints are NOT preserved: a missing symbol is a concrete falsifiable claim.
|
|
1166
1225
|
const shouldPreserveCheckedTask =
|
|
1167
1226
|
task.checked &&
|
|
1168
1227
|
!passed &&
|
|
1169
1228
|
!authoritativeEvidence.active &&
|
|
1170
|
-
pathHints.length === 0 &&
|
|
1171
1229
|
symbolHints.length === 0 &&
|
|
1172
|
-
|
|
1230
|
+
negativeSignalMatches.length === 0 &&
|
|
1173
1231
|
evidence.structuralEvidence !== false &&
|
|
1174
|
-
|
|
1232
|
+
(hasDirectReferencePass || purePathHints.length === 0);
|
|
1175
1233
|
let preservedCheckedState = false;
|
|
1176
1234
|
if (shouldPreserveCheckedTask) {
|
|
1177
1235
|
passed = true;
|
|
@@ -1198,11 +1256,27 @@ function validateTask(task, context, config, plugins) {
|
|
|
1198
1256
|
};
|
|
1199
1257
|
}
|
|
1200
1258
|
|
|
1259
|
+
const BLOCKED_BY_RE = /\bBlocked\s+by:\s*([^\n.]+)/i;
|
|
1260
|
+
|
|
1201
1261
|
function validateTasks(tasks, context, config, plugins) {
|
|
1202
1262
|
const result = {};
|
|
1203
1263
|
for (const task of tasks) {
|
|
1204
1264
|
result[task.id] = validateTask(task, context, config, plugins);
|
|
1205
1265
|
}
|
|
1266
|
+
|
|
1267
|
+
// Post-pass: milestone tasks with "Blocked by: id-a, id-b" cannot pass
|
|
1268
|
+
// while any listed dependency is still failing.
|
|
1269
|
+
for (const task of tasks) {
|
|
1270
|
+
const m = BLOCKED_BY_RE.exec(task.text);
|
|
1271
|
+
if (!m) continue;
|
|
1272
|
+
const blockedIds = m[1].split(/[\s,]+/).map((s) => s.trim()).filter(Boolean);
|
|
1273
|
+
const failingDeps = blockedIds.filter((id) => result[id] && !result[id].passed);
|
|
1274
|
+
if (failingDeps.length > 0 && result[task.id] && result[task.id].passed) {
|
|
1275
|
+
result[task.id].passed = false;
|
|
1276
|
+
result[task.id].reasons = [`blocked by incomplete tasks: ${failingDeps.join(', ')}`];
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1206
1280
|
return result;
|
|
1207
1281
|
}
|
|
1208
1282
|
|