roadmapsmith 0.9.10 → 0.9.12
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/parser/index.js +13 -0
- package/src/validator/index.js +62 -8
package/package.json
CHANGED
package/src/parser/index.js
CHANGED
|
@@ -86,6 +86,11 @@ function parseWarningLine(content) {
|
|
|
86
86
|
return content.slice(WARNING_PREFIX.length).trim();
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
function parseBlockedByLine(content) {
|
|
90
|
+
if (!/^blocked\s+by:/i.test(content)) return null;
|
|
91
|
+
return content.replace(/^blocked\s+by:\s*/i, '').trim();
|
|
92
|
+
}
|
|
93
|
+
|
|
89
94
|
function parseRoadmap(content) {
|
|
90
95
|
const lines = String(content || '').split(/\r?\n/);
|
|
91
96
|
const managedRange = findManagedRange(lines);
|
|
@@ -111,6 +116,7 @@ function parseRoadmap(content) {
|
|
|
111
116
|
let warningLineIndex = null;
|
|
112
117
|
let warningText = null;
|
|
113
118
|
const evidenceLines = [];
|
|
119
|
+
const blockedByIds = [];
|
|
114
120
|
let lastChildLineIndex = index;
|
|
115
121
|
for (let childIndex = index + 1; childIndex < lines.length; childIndex += 1) {
|
|
116
122
|
const childLine = lines[childIndex];
|
|
@@ -145,6 +151,12 @@ function parseRoadmap(content) {
|
|
|
145
151
|
warningLineIndex = childIndex;
|
|
146
152
|
warningText = warningTextValue;
|
|
147
153
|
}
|
|
154
|
+
|
|
155
|
+
const blockedByText = parseBlockedByLine(childBullet.content);
|
|
156
|
+
if (blockedByText !== null) {
|
|
157
|
+
const ids = blockedByText.split(/[\s,]+/).map((s) => s.trim()).filter(Boolean);
|
|
158
|
+
blockedByIds.push(...ids);
|
|
159
|
+
}
|
|
148
160
|
}
|
|
149
161
|
|
|
150
162
|
const id = markerId || slugify(text);
|
|
@@ -157,6 +169,7 @@ function parseRoadmap(content) {
|
|
|
157
169
|
warningLineIndex,
|
|
158
170
|
warningText,
|
|
159
171
|
evidenceLines,
|
|
172
|
+
blockedByIds,
|
|
160
173
|
markerId,
|
|
161
174
|
noTest,
|
|
162
175
|
indent,
|
package/src/validator/index.js
CHANGED
|
@@ -50,6 +50,25 @@ const GENERIC_TASK_TOKENS = new Set([
|
|
|
50
50
|
'phrases', 'conceptual',
|
|
51
51
|
]);
|
|
52
52
|
|
|
53
|
+
// Patterns that indicate the task describes work still to be done, not completed work.
|
|
54
|
+
// Regex form catches verb and noun forms ("Manejo") and two-word constructions ("Recovery path")
|
|
55
|
+
// that an exact-match Set would miss. When a task matches, code token overlap alone cannot pass
|
|
56
|
+
// it — either an Evidence line or high-confidence evidence (code + test) is required.
|
|
57
|
+
const CHANGE_VERB_PATTERNS = [
|
|
58
|
+
// Spanish — verb and noun forms of pending-work descriptions
|
|
59
|
+
/^(agregar|añadir|implementar|configurar|reemplazar|cambiar|corregir|manejar|manejo|proteger|sanitizar|validar|deshabilitar|mostrar|generar|expandir|reducir|completar|crear|eliminar|migrar|refactorizar|recovery\s+path)\b/i,
|
|
60
|
+
// English
|
|
61
|
+
/^(add|implement|configure|replace|change|fix|handle|protect|sanitize|validate|disable|show|generate|expand|reduce|complete|create|remove|migrate|refactor|recovery\s+path)\b/i,
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
function taskDescribesChange(taskText) {
|
|
65
|
+
const normalized = String(taskText)
|
|
66
|
+
.replace(/^\*\*\[.*?\]\*\*\s*/, '')
|
|
67
|
+
.replace(/^\[.*?\]\s*/, '')
|
|
68
|
+
.trim();
|
|
69
|
+
return CHANGE_VERB_PATTERNS.some((p) => p.test(normalized));
|
|
70
|
+
}
|
|
71
|
+
|
|
53
72
|
const CANONICAL_FILES = {
|
|
54
73
|
security: 'SECURITY.md',
|
|
55
74
|
readme: 'README.md',
|
|
@@ -179,6 +198,7 @@ function hasFileExtension(token) {
|
|
|
179
198
|
|
|
180
199
|
function isLikelyPath(token) {
|
|
181
200
|
if (token.includes('*') || token.includes('?')) return false; // glob/wildcard
|
|
201
|
+
if (/^\/api\//i.test(token)) return false; // HTTP API route paths are not file paths
|
|
182
202
|
if (/^\.{1,2}\/|^\//.test(token)) {
|
|
183
203
|
// Bare "/" or "./" with nothing after is not a real path (e.g. "API / ESC-POS" → "/")
|
|
184
204
|
return /[A-Za-z0-9_]/.test(token);
|
|
@@ -1173,9 +1193,11 @@ function validateTask(task, context, config, plugins) {
|
|
|
1173
1193
|
let confidence = 'low';
|
|
1174
1194
|
if (authoritativeEvidence.passed) {
|
|
1175
1195
|
confidence = authoritativeEvidence.confidence || 'medium';
|
|
1176
|
-
} else if (meetsStrongThreshold) {
|
|
1196
|
+
} else if (meetsStrongThreshold && evidence.test) {
|
|
1197
|
+
// 'high' requires code + test — code + feature-surface alone is 'medium'
|
|
1198
|
+
// so that action-verb tasks (path hint exists but no test) stay gated.
|
|
1177
1199
|
confidence = 'high';
|
|
1178
|
-
} else if (strongEvidenceCount === 1 || hasDirectReferencePass || hasArtifactTaskPass || hasTrustedRuleEvidencePass) {
|
|
1200
|
+
} else if (meetsStrongThreshold || strongEvidenceCount === 1 || hasDirectReferencePass || hasArtifactTaskPass || hasTrustedRuleEvidencePass) {
|
|
1179
1201
|
confidence = 'medium';
|
|
1180
1202
|
}
|
|
1181
1203
|
|
|
@@ -1207,7 +1229,10 @@ function validateTask(task, context, config, plugins) {
|
|
|
1207
1229
|
}
|
|
1208
1230
|
}
|
|
1209
1231
|
|
|
1210
|
-
|
|
1232
|
+
// Unchecked tasks with an existing ⚠️ warning are preserved as failing unless an Evidence line
|
|
1233
|
+
// explicitly confirms implementation. meetsStrongThreshold (token match) cannot override a
|
|
1234
|
+
// human/agent judgment that the feature is incomplete.
|
|
1235
|
+
if (task.warningText && !task.checked && passed && !authoritativeEvidence.passed) {
|
|
1211
1236
|
passed = false;
|
|
1212
1237
|
uniqueReasons.push(task.warningText);
|
|
1213
1238
|
uniqueReasons = Array.from(new Set(uniqueReasons));
|
|
@@ -1216,6 +1241,27 @@ function validateTask(task, context, config, plugins) {
|
|
|
1216
1241
|
passed = false;
|
|
1217
1242
|
}
|
|
1218
1243
|
|
|
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").
|
|
1249
|
+
if (
|
|
1250
|
+
!task.checked &&
|
|
1251
|
+
passed &&
|
|
1252
|
+
taskDescribesChange(task.text) &&
|
|
1253
|
+
!authoritativeEvidence.passed &&
|
|
1254
|
+
!hasTrustedRuleEvidencePass &&
|
|
1255
|
+
!hasArtifactTaskPass &&
|
|
1256
|
+
confidence !== 'high'
|
|
1257
|
+
) {
|
|
1258
|
+
passed = false;
|
|
1259
|
+
const actionVerbReason = 'action task requires Evidence line or high-confidence evidence (code + test) to be marked complete';
|
|
1260
|
+
if (!uniqueReasons.includes(actionVerbReason)) {
|
|
1261
|
+
uniqueReasons.push(actionVerbReason);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1219
1265
|
// Preserve already-checked tasks when the validator can't confirm implementation but also
|
|
1220
1266
|
// can't find strong evidence against it. Two cases:
|
|
1221
1267
|
// 1. No path/symbol hints at all — no machine-readable claims to evaluate.
|
|
@@ -1264,13 +1310,21 @@ function validateTasks(tasks, context, config, plugins) {
|
|
|
1264
1310
|
result[task.id] = validateTask(task, context, config, plugins);
|
|
1265
1311
|
}
|
|
1266
1312
|
|
|
1267
|
-
// Post-pass:
|
|
1268
|
-
//
|
|
1313
|
+
// Post-pass: tasks with "Blocked by: id-a, id-b" cannot pass while any listed dependency
|
|
1314
|
+
// is still failing. The IDs may appear inline in task.text OR as child bullet lines
|
|
1315
|
+
// (parsed by parser into task.blockedByIds).
|
|
1269
1316
|
for (const task of tasks) {
|
|
1317
|
+
const blockedIds = [];
|
|
1270
1318
|
const m = BLOCKED_BY_RE.exec(task.text);
|
|
1271
|
-
if (
|
|
1272
|
-
|
|
1273
|
-
|
|
1319
|
+
if (m) {
|
|
1320
|
+
blockedIds.push(...m[1].split(/[\s,]+/).map((s) => s.trim()).filter(Boolean));
|
|
1321
|
+
}
|
|
1322
|
+
if (Array.isArray(task.blockedByIds) && task.blockedByIds.length > 0) {
|
|
1323
|
+
blockedIds.push(...task.blockedByIds);
|
|
1324
|
+
}
|
|
1325
|
+
if (blockedIds.length === 0) continue;
|
|
1326
|
+
const uniqueBlockedIds = Array.from(new Set(blockedIds));
|
|
1327
|
+
const failingDeps = uniqueBlockedIds.filter((id) => result[id] && !result[id].passed);
|
|
1274
1328
|
if (failingDeps.length > 0 && result[task.id] && result[task.id].passed) {
|
|
1275
1329
|
result[task.id].passed = false;
|
|
1276
1330
|
result[task.id].reasons = [`blocked by incomplete tasks: ${failingDeps.join(', ')}`];
|