roadmapsmith 0.9.7 → 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 +77 -14
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)) {
|
|
@@ -800,9 +824,6 @@ function findNegativeImplementationSignals(candidatePaths, fileIndex) {
|
|
|
800
824
|
|
|
801
825
|
const indexedFiles = new Map(fileIndex.map((file) => [file.relativePath, file]));
|
|
802
826
|
const negativeSignals = [
|
|
803
|
-
/\bTODO\b/i,
|
|
804
|
-
/\bFIXME\b/i,
|
|
805
|
-
/\bdisabled\b/i,
|
|
806
827
|
/\bnot implemented\b/i,
|
|
807
828
|
/throw\s+new\s+Error\s*\(\s*['"`][^'"`]*not implemented/i
|
|
808
829
|
];
|
|
@@ -1012,12 +1033,16 @@ function buildValidationContext(projectRoot, config, plugins) {
|
|
|
1012
1033
|
}
|
|
1013
1034
|
|
|
1014
1035
|
function validateTask(task, context, config, plugins) {
|
|
1015
|
-
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));
|
|
1016
1040
|
const standaloneFilenames = extractStandaloneFilenames(task.text);
|
|
1017
1041
|
const symbolHints = extractSymbolHints(task.text);
|
|
1018
1042
|
const authoritativeEvidence = evaluateAuthoritativeEvidence(task, context.fileIndex);
|
|
1019
1043
|
|
|
1020
1044
|
const filesFromPaths = findFilesByPathHints(pathHints, context.fileIndex);
|
|
1045
|
+
const filesFromPurePathHints = findFilesByPathHints(purePathHints, context.fileIndex);
|
|
1021
1046
|
const filesFromSymbols = findFilesBySymbols(symbolHints, context.fileIndex);
|
|
1022
1047
|
// Combine path hints AND standalone filenames for token exclusion so that tokens
|
|
1023
1048
|
// derived from any referenced filename (e.g. "roadmap-skill" from
|
|
@@ -1110,7 +1135,7 @@ function validateTask(task, context, config, plugins) {
|
|
|
1110
1135
|
}
|
|
1111
1136
|
}
|
|
1112
1137
|
|
|
1113
|
-
if (requiresTest && !evidence.test && !authoritativeEvidence.passed) {
|
|
1138
|
+
if (requiresTest && !evidence.test && !authoritativeEvidence.passed && filesFromPurePathHints.length === 0) {
|
|
1114
1139
|
reasons.push('missing test evidence');
|
|
1115
1140
|
}
|
|
1116
1141
|
|
|
@@ -1123,7 +1148,8 @@ function validateTask(task, context, config, plugins) {
|
|
|
1123
1148
|
const attempted = hasStrongEvidence || hasWeakEvidence || pathHints.length > 0 || symbolHints.length > 0 || authoritativeEvidence.active;
|
|
1124
1149
|
const { categories: strongEvidenceCategories } = countStrongEvidenceCategories(task.text, evidence);
|
|
1125
1150
|
const strongEvidenceCount = strongEvidenceCategories.length;
|
|
1126
|
-
|
|
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;
|
|
1127
1153
|
const hasArtifactTaskPass = evidence.artifact && (
|
|
1128
1154
|
isDocTask(task.text) ||
|
|
1129
1155
|
evidence.heuristicArtifacts.length > 0 ||
|
|
@@ -1166,6 +1192,23 @@ function validateTask(task, context, config, plugins) {
|
|
|
1166
1192
|
passed = false;
|
|
1167
1193
|
}
|
|
1168
1194
|
|
|
1195
|
+
const shouldPreserveCheckedTask =
|
|
1196
|
+
task.checked &&
|
|
1197
|
+
!passed &&
|
|
1198
|
+
!authoritativeEvidence.active &&
|
|
1199
|
+
purePathHints.length === 0 &&
|
|
1200
|
+
symbolHints.length === 0 &&
|
|
1201
|
+
!hasDirectReferencePass &&
|
|
1202
|
+
evidence.structuralEvidence !== false &&
|
|
1203
|
+
negativeSignalMatches.length === 0;
|
|
1204
|
+
let preservedCheckedState = false;
|
|
1205
|
+
if (shouldPreserveCheckedTask) {
|
|
1206
|
+
passed = true;
|
|
1207
|
+
confidence = 'low';
|
|
1208
|
+
uniqueReasons = [];
|
|
1209
|
+
preservedCheckedState = true;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1169
1212
|
// True when the only passing evidence is artifact/doc files and the task is not a doc task.
|
|
1170
1213
|
// Used by auditValidation to flag implementation tasks that pass solely via documentation.
|
|
1171
1214
|
const evidenceIsDocOnly = !evidence.code && !evidence.test && evidence.artifact && !isDocTask(task.text);
|
|
@@ -1179,15 +1222,32 @@ function validateTask(task, context, config, plugins) {
|
|
|
1179
1222
|
evidenceIsDocOnly,
|
|
1180
1223
|
requiresTest,
|
|
1181
1224
|
hasEvidence: hasStrongEvidence || hasWeakEvidence,
|
|
1182
|
-
attempted
|
|
1225
|
+
attempted,
|
|
1226
|
+
preservedCheckedState
|
|
1183
1227
|
};
|
|
1184
1228
|
}
|
|
1185
1229
|
|
|
1230
|
+
const BLOCKED_BY_RE = /\bBlocked\s+by:\s*([^\n.]+)/i;
|
|
1231
|
+
|
|
1186
1232
|
function validateTasks(tasks, context, config, plugins) {
|
|
1187
1233
|
const result = {};
|
|
1188
1234
|
for (const task of tasks) {
|
|
1189
1235
|
result[task.id] = validateTask(task, context, config, plugins);
|
|
1190
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
|
+
|
|
1191
1251
|
return result;
|
|
1192
1252
|
}
|
|
1193
1253
|
|
|
@@ -1237,6 +1297,9 @@ function applyMinimumConfidence(results, minimumConfidence) {
|
|
|
1237
1297
|
const minRank = CONFIDENCE_RANK[minimumConfidence] ?? 0;
|
|
1238
1298
|
if (minRank === 0) return;
|
|
1239
1299
|
for (const result of Object.values(results)) {
|
|
1300
|
+
if (result.preservedCheckedState) {
|
|
1301
|
+
continue;
|
|
1302
|
+
}
|
|
1240
1303
|
if ((CONFIDENCE_RANK[result.confidence] ?? 0) < minRank) {
|
|
1241
1304
|
result.passed = false;
|
|
1242
1305
|
result.reasons = [
|