roadmapsmith 0.9.8 → 0.9.9
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 +56 -11
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
|
@@ -249,23 +249,47 @@ function collectPathishTokens(text) {
|
|
|
249
249
|
return tokens.filter(Boolean);
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
// LINE_REF_RE matches "path/file.ext:NN" or "path/file.ext:NN-MM" — indicates WHERE
|
|
253
|
+
// to implement, not that implementation exists. Paths matching this pattern are added
|
|
254
|
+
// to lineReferenceHints and excluded from hasDirectReferencePass scoring.
|
|
255
|
+
const LINE_REF_RE = /^(.+?):(\d+)(?:-\d+)?$/;
|
|
256
|
+
|
|
252
257
|
function extractExplicitPaths(text) {
|
|
253
258
|
const results = new Set();
|
|
259
|
+
const lineReferenceHints = new Set();
|
|
260
|
+
|
|
254
261
|
const quoted = String(text).match(/`([^`]+)`/g) || [];
|
|
255
262
|
for (const token of quoted) {
|
|
256
263
|
const clean = token.slice(1, -1);
|
|
264
|
+
if (clean.includes('*') || clean.includes('?')) continue; // glob
|
|
257
265
|
if (clean.includes('/') || clean.includes('\\') || clean.includes('.')) {
|
|
258
|
-
|
|
266
|
+
const lineMatch = LINE_REF_RE.exec(clean);
|
|
267
|
+
if (lineMatch && hasKnownFileExtension(lineMatch[1])) {
|
|
268
|
+
lineReferenceHints.add(lineMatch[1]);
|
|
269
|
+
results.add(lineMatch[1]);
|
|
270
|
+
} else {
|
|
271
|
+
results.add(clean);
|
|
272
|
+
}
|
|
259
273
|
}
|
|
260
274
|
}
|
|
261
275
|
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
const token = stripTrailingPathPunctuation(
|
|
265
|
-
if (
|
|
276
|
+
for (const word of String(text).split(/\s+/)) {
|
|
277
|
+
if (!word.includes('/')) continue;
|
|
278
|
+
const token = stripTrailingPathPunctuation(word);
|
|
279
|
+
if (token.includes('*') || token.includes('?')) continue; // glob
|
|
280
|
+
const lineMatch = LINE_REF_RE.exec(token);
|
|
281
|
+
if (lineMatch && hasKnownFileExtension(lineMatch[1])) {
|
|
282
|
+
if (isLikelyPath(lineMatch[1])) {
|
|
283
|
+
lineReferenceHints.add(lineMatch[1]);
|
|
284
|
+
results.add(lineMatch[1]);
|
|
285
|
+
}
|
|
286
|
+
} else if (isLikelyPath(token)) {
|
|
287
|
+
results.add(token);
|
|
288
|
+
}
|
|
266
289
|
}
|
|
267
290
|
|
|
268
|
-
|
|
291
|
+
const paths = Array.from(results).sort((left, right) => left.localeCompare(right));
|
|
292
|
+
return { paths, lineReferenceHints };
|
|
269
293
|
}
|
|
270
294
|
|
|
271
295
|
// Standalone filenames (no slash) mentioned in task prose — e.g. "roadmap-skill.config.json",
|
|
@@ -644,7 +668,7 @@ function extractEvidencePaths(evidenceText) {
|
|
|
644
668
|
|
|
645
669
|
function evidenceLineHasPassingSummary(evidenceText) {
|
|
646
670
|
const text = String(evidenceText || '');
|
|
647
|
-
if (/\b\d
|
|
671
|
+
if (/\b\d+(?:\s*\/\s*\d+)?\s+tests?\s+passing\b/i.test(text)) {
|
|
648
672
|
return true;
|
|
649
673
|
}
|
|
650
674
|
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 +1033,16 @@ function buildValidationContext(projectRoot, config, plugins) {
|
|
|
1009
1033
|
}
|
|
1010
1034
|
|
|
1011
1035
|
function validateTask(task, context, config, plugins) {
|
|
1012
|
-
const pathHints = extractExplicitPaths(task.text);
|
|
1036
|
+
const { paths: pathHints, lineReferenceHints } = extractExplicitPaths(task.text);
|
|
1037
|
+
// Paths that are line-reference hints (file.ts:NN) indicate WHERE to implement,
|
|
1038
|
+
// not that implementation exists. They are excluded from hasDirectReferencePass.
|
|
1039
|
+
const purePathHints = pathHints.filter((p) => !lineReferenceHints.has(p));
|
|
1013
1040
|
const standaloneFilenames = extractStandaloneFilenames(task.text);
|
|
1014
1041
|
const symbolHints = extractSymbolHints(task.text);
|
|
1015
1042
|
const authoritativeEvidence = evaluateAuthoritativeEvidence(task, context.fileIndex);
|
|
1016
1043
|
|
|
1017
1044
|
const filesFromPaths = findFilesByPathHints(pathHints, context.fileIndex);
|
|
1045
|
+
const filesFromPurePathHints = findFilesByPathHints(purePathHints, context.fileIndex);
|
|
1018
1046
|
const filesFromSymbols = findFilesBySymbols(symbolHints, context.fileIndex);
|
|
1019
1047
|
// Combine path hints AND standalone filenames for token exclusion so that tokens
|
|
1020
1048
|
// derived from any referenced filename (e.g. "roadmap-skill" from
|
|
@@ -1107,7 +1135,7 @@ function validateTask(task, context, config, plugins) {
|
|
|
1107
1135
|
}
|
|
1108
1136
|
}
|
|
1109
1137
|
|
|
1110
|
-
if (requiresTest && !evidence.test && !authoritativeEvidence.passed) {
|
|
1138
|
+
if (requiresTest && !evidence.test && !authoritativeEvidence.passed && filesFromPurePathHints.length === 0) {
|
|
1111
1139
|
reasons.push('missing test evidence');
|
|
1112
1140
|
}
|
|
1113
1141
|
|
|
@@ -1120,7 +1148,8 @@ function validateTask(task, context, config, plugins) {
|
|
|
1120
1148
|
const attempted = hasStrongEvidence || hasWeakEvidence || pathHints.length > 0 || symbolHints.length > 0 || authoritativeEvidence.active;
|
|
1121
1149
|
const { categories: strongEvidenceCategories } = countStrongEvidenceCategories(task.text, evidence);
|
|
1122
1150
|
const strongEvidenceCount = strongEvidenceCategories.length;
|
|
1123
|
-
|
|
1151
|
+
// Only pure path hints (not line-reference hints like file.ts:169) count as direct evidence.
|
|
1152
|
+
const hasDirectReferencePass = filesFromPurePathHints.length > 0 || filesFromSymbols.length > 0;
|
|
1124
1153
|
const hasArtifactTaskPass = evidence.artifact && (
|
|
1125
1154
|
isDocTask(task.text) ||
|
|
1126
1155
|
evidence.heuristicArtifacts.length > 0 ||
|
|
@@ -1167,7 +1196,7 @@ function validateTask(task, context, config, plugins) {
|
|
|
1167
1196
|
task.checked &&
|
|
1168
1197
|
!passed &&
|
|
1169
1198
|
!authoritativeEvidence.active &&
|
|
1170
|
-
|
|
1199
|
+
purePathHints.length === 0 &&
|
|
1171
1200
|
symbolHints.length === 0 &&
|
|
1172
1201
|
!hasDirectReferencePass &&
|
|
1173
1202
|
evidence.structuralEvidence !== false &&
|
|
@@ -1198,11 +1227,27 @@ function validateTask(task, context, config, plugins) {
|
|
|
1198
1227
|
};
|
|
1199
1228
|
}
|
|
1200
1229
|
|
|
1230
|
+
const BLOCKED_BY_RE = /\bBlocked\s+by:\s*([^\n.]+)/i;
|
|
1231
|
+
|
|
1201
1232
|
function validateTasks(tasks, context, config, plugins) {
|
|
1202
1233
|
const result = {};
|
|
1203
1234
|
for (const task of tasks) {
|
|
1204
1235
|
result[task.id] = validateTask(task, context, config, plugins);
|
|
1205
1236
|
}
|
|
1237
|
+
|
|
1238
|
+
// Post-pass: milestone tasks with "Blocked by: id-a, id-b" cannot pass
|
|
1239
|
+
// while any listed dependency is still failing.
|
|
1240
|
+
for (const task of tasks) {
|
|
1241
|
+
const m = BLOCKED_BY_RE.exec(task.text);
|
|
1242
|
+
if (!m) continue;
|
|
1243
|
+
const blockedIds = m[1].split(/[\s,]+/).map((s) => s.trim()).filter(Boolean);
|
|
1244
|
+
const failingDeps = blockedIds.filter((id) => result[id] && !result[id].passed);
|
|
1245
|
+
if (failingDeps.length > 0 && result[task.id] && result[task.id].passed) {
|
|
1246
|
+
result[task.id].passed = false;
|
|
1247
|
+
result[task.id].reasons = [`blocked by incomplete tasks: ${failingDeps.join(', ')}`];
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1206
1251
|
return result;
|
|
1207
1252
|
}
|
|
1208
1253
|
|