roadmapsmith 0.9.10 → 0.9.11
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 -6
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,28 @@ const GENERIC_TASK_TOKENS = new Set([
|
|
|
50
50
|
'phrases', 'conceptual',
|
|
51
51
|
]);
|
|
52
52
|
|
|
53
|
+
// Verbs that indicate the task describes work still to be done, not completed work.
|
|
54
|
+
// When a task starts with one of these verbs, code token overlap alone cannot pass it —
|
|
55
|
+
// either an Evidence line (authoritativeEvidence.passed) or test evidence is required.
|
|
56
|
+
const ACTION_VERBS = new Set([
|
|
57
|
+
'agregar', 'mostrar', 'implementar', 'configurar', 'reemplazar', 'cambiar',
|
|
58
|
+
'corregir', 'manejar', 'proteger', 'sanitizar', 'validar', 'deshabilitar',
|
|
59
|
+
'generar', 'expandir', 'reducir', 'completar', 'crear', 'eliminar',
|
|
60
|
+
'add', 'show', 'implement', 'configure', 'replace', 'change', 'fix',
|
|
61
|
+
'handle', 'protect', 'sanitize', 'validate', 'disable', 'generate',
|
|
62
|
+
'expand', 'reduce', 'complete', 'create', 'remove'
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
function taskStartsWithActionVerb(taskText) {
|
|
66
|
+
const normalized = String(taskText)
|
|
67
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
68
|
+
.replace(/\[([^\]]+)\]/g, '$1')
|
|
69
|
+
.trim()
|
|
70
|
+
.toLowerCase();
|
|
71
|
+
const firstWord = normalized.split(/[\s,;:()[\]]+/)[0] || '';
|
|
72
|
+
return ACTION_VERBS.has(firstWord);
|
|
73
|
+
}
|
|
74
|
+
|
|
53
75
|
const CANONICAL_FILES = {
|
|
54
76
|
security: 'SECURITY.md',
|
|
55
77
|
readme: 'README.md',
|
|
@@ -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,29 @@ function validateTask(task, context, config, plugins) {
|
|
|
1216
1241
|
passed = false;
|
|
1217
1242
|
}
|
|
1218
1243
|
|
|
1244
|
+
// Action-verb gate (Causa 3): unchecked tasks whose text starts with a pending-work verb
|
|
1245
|
+
// (Agregar, Configurar, Add, Fix, …) cannot pass on code token overlap alone.
|
|
1246
|
+
// The verb signals "this is work to do", so the validator requires either:
|
|
1247
|
+
// a) an Evidence line confirming the work is done (authoritativeEvidence.passed), or
|
|
1248
|
+
// b) test evidence proving the feature is exercised, or
|
|
1249
|
+
// c) grant-evidence from a config rule (hasTrustedRuleEvidencePass), or
|
|
1250
|
+
// d) canonical artifact evidence (hasArtifactTaskPass — e.g. "Add SECURITY.md").
|
|
1251
|
+
if (
|
|
1252
|
+
!task.checked &&
|
|
1253
|
+
passed &&
|
|
1254
|
+
taskStartsWithActionVerb(task.text) &&
|
|
1255
|
+
!authoritativeEvidence.passed &&
|
|
1256
|
+
!hasTrustedRuleEvidencePass &&
|
|
1257
|
+
!hasArtifactTaskPass &&
|
|
1258
|
+
!evidence.test
|
|
1259
|
+
) {
|
|
1260
|
+
passed = false;
|
|
1261
|
+
const actionVerbReason = 'action-verb task requires high-confidence evidence or Evidence line';
|
|
1262
|
+
if (!uniqueReasons.includes(actionVerbReason)) {
|
|
1263
|
+
uniqueReasons.push(actionVerbReason);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1219
1267
|
// Preserve already-checked tasks when the validator can't confirm implementation but also
|
|
1220
1268
|
// can't find strong evidence against it. Two cases:
|
|
1221
1269
|
// 1. No path/symbol hints at all — no machine-readable claims to evaluate.
|
|
@@ -1264,13 +1312,21 @@ function validateTasks(tasks, context, config, plugins) {
|
|
|
1264
1312
|
result[task.id] = validateTask(task, context, config, plugins);
|
|
1265
1313
|
}
|
|
1266
1314
|
|
|
1267
|
-
// Post-pass:
|
|
1268
|
-
//
|
|
1315
|
+
// Post-pass: tasks with "Blocked by: id-a, id-b" cannot pass while any listed dependency
|
|
1316
|
+
// is still failing. The IDs may appear inline in task.text OR as child bullet lines
|
|
1317
|
+
// (parsed by parser into task.blockedByIds).
|
|
1269
1318
|
for (const task of tasks) {
|
|
1319
|
+
const blockedIds = [];
|
|
1270
1320
|
const m = BLOCKED_BY_RE.exec(task.text);
|
|
1271
|
-
if (
|
|
1272
|
-
|
|
1273
|
-
|
|
1321
|
+
if (m) {
|
|
1322
|
+
blockedIds.push(...m[1].split(/[\s,]+/).map((s) => s.trim()).filter(Boolean));
|
|
1323
|
+
}
|
|
1324
|
+
if (Array.isArray(task.blockedByIds) && task.blockedByIds.length > 0) {
|
|
1325
|
+
blockedIds.push(...task.blockedByIds);
|
|
1326
|
+
}
|
|
1327
|
+
if (blockedIds.length === 0) continue;
|
|
1328
|
+
const uniqueBlockedIds = Array.from(new Set(blockedIds));
|
|
1329
|
+
const failingDeps = uniqueBlockedIds.filter((id) => result[id] && !result[id].passed);
|
|
1274
1330
|
if (failingDeps.length > 0 && result[task.id] && result[task.id].passed) {
|
|
1275
1331
|
result[task.id].passed = false;
|
|
1276
1332
|
result[task.id].reasons = [`blocked by incomplete tasks: ${failingDeps.join(', ')}`];
|