roadmapsmith 0.9.5 → 0.9.7
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/README.md +23 -0
- package/bin/cli.js +12 -3
- package/package.json +1 -1
- package/src/parser/index.js +128 -20
- package/src/sync/index.js +2 -1
- package/src/validator/index.js +303 -11
package/README.md
CHANGED
|
@@ -22,6 +22,29 @@ npx skills add PapiScholz/roadmapsmith --skill roadmap-sync
|
|
|
22
22
|
|
|
23
23
|
This adds the `roadmap-sync` agent skill. It does not install the CLI package.
|
|
24
24
|
|
|
25
|
+
## Updating
|
|
26
|
+
|
|
27
|
+
Update the CLI based on how it was installed:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Global npm install
|
|
31
|
+
npm install -g roadmapsmith@latest
|
|
32
|
+
|
|
33
|
+
# Project dependency
|
|
34
|
+
npm install roadmapsmith@latest
|
|
35
|
+
|
|
36
|
+
# One-off execution without installing
|
|
37
|
+
npx roadmapsmith@latest sync --audit
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The `roadmap-sync` agent skill is separate from the CLI. Re-running the skills install updates the agent instructions, but it does not update the `roadmapsmith` npm binary:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx skills add PapiScholz/roadmapsmith --skill roadmap-sync
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Fixes are available through `@latest` only after a new npm package version has been published. Before publication, install from a local checkout or a packed tarball for testing.
|
|
47
|
+
|
|
25
48
|
## Operating Modes
|
|
26
49
|
|
|
27
50
|
### Zero Mode
|
package/bin/cli.js
CHANGED
|
@@ -44,6 +44,14 @@ function maybeFilterTasks(tasks, filterValue) {
|
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
function tasksInManagedBlock(parsedRoadmap) {
|
|
48
|
+
if (!parsedRoadmap.managedRange) {
|
|
49
|
+
return parsedRoadmap.tasks;
|
|
50
|
+
}
|
|
51
|
+
const { start, end } = parsedRoadmap.managedRange;
|
|
52
|
+
return parsedRoadmap.tasks.filter((task) => task.lineIndex > start && task.lineIndex < end);
|
|
53
|
+
}
|
|
54
|
+
|
|
47
55
|
function printAudit(audit) {
|
|
48
56
|
console.log(`Audit summary: ${audit.checkedWithoutEvidence.length} checked-without-evidence, ${audit.readyButUnchecked.length} ready-but-unchecked.`);
|
|
49
57
|
if (audit.checkedWithoutEvidence.length > 0) {
|
|
@@ -157,10 +165,11 @@ async function run() {
|
|
|
157
165
|
}
|
|
158
166
|
|
|
159
167
|
const parsedRoadmap = parseRoadmap(content);
|
|
168
|
+
const syncTasks = tasksInManagedBlock(parsedRoadmap);
|
|
160
169
|
const validationContext = buildValidationContext(projectRoot, config, loadPlugins(projectRoot, config.plugins));
|
|
161
|
-
const results = validateTasks(
|
|
170
|
+
const results = validateTasks(syncTasks, validationContext, config, validationContext.plugins);
|
|
162
171
|
applyMinimumConfidence(results, config.validation?.minimumConfidence);
|
|
163
|
-
const next = applySync(content,
|
|
172
|
+
const next = applySync(content, syncTasks, results);
|
|
164
173
|
const dryRun = isEnabled(flags['dry-run']);
|
|
165
174
|
const writeResult = writeText(roadmapFile, next, { dryRun });
|
|
166
175
|
|
|
@@ -175,7 +184,7 @@ async function run() {
|
|
|
175
184
|
}
|
|
176
185
|
|
|
177
186
|
if (isEnabled(flags.audit)) {
|
|
178
|
-
const audit = auditValidation(
|
|
187
|
+
const audit = auditValidation(syncTasks, results);
|
|
179
188
|
printAudit(audit);
|
|
180
189
|
}
|
|
181
190
|
return;
|
package/package.json
CHANGED
package/src/parser/index.js
CHANGED
|
@@ -2,45 +2,148 @@
|
|
|
2
2
|
|
|
3
3
|
const { slugify } = require('../utils');
|
|
4
4
|
|
|
5
|
-
const TASK_LINE_RE = /^(\s*)- \[( |x|X)\] (.*?)(?:\s*<!--\s*rs:task=([a-z0-9-]+)([^>]*)-->)?\s*$/;
|
|
6
|
-
const WARNING_RE = /^\s*-\s+⚠️ attempted but validation failed:\s*(.+?)\s*$/;
|
|
7
|
-
const HEADING_RE = /^#{2,3}\s+(.*)$/;
|
|
8
|
-
|
|
9
5
|
const MANAGED_START = '<!-- rs:managed:start -->';
|
|
10
6
|
const MANAGED_END = '<!-- rs:managed:end -->';
|
|
7
|
+
const WARNING_PREFIX = '⚠️ attempted but validation failed:';
|
|
8
|
+
|
|
9
|
+
function getIndentWidth(text) {
|
|
10
|
+
return String(text || '').replace(/\t/g, ' ').length;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function splitIndent(line) {
|
|
14
|
+
const value = String(line || '');
|
|
15
|
+
const trimmed = value.trimStart();
|
|
16
|
+
return {
|
|
17
|
+
indent: value.slice(0, value.length - trimmed.length),
|
|
18
|
+
content: trimmed
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseHeadingLine(line) {
|
|
23
|
+
const { content } = splitIndent(line);
|
|
24
|
+
if (content.startsWith('## ')) {
|
|
25
|
+
return content.slice(3).trim();
|
|
26
|
+
}
|
|
27
|
+
if (content.startsWith('### ')) {
|
|
28
|
+
return content.slice(4).trim();
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseTaskLine(line) {
|
|
34
|
+
const { indent, content } = splitIndent(line);
|
|
35
|
+
if (content.length < 6) return null;
|
|
36
|
+
if (content[0] !== '-' || content[1] !== ' ' || content[2] !== '[') return null;
|
|
37
|
+
|
|
38
|
+
const checkedToken = content[3];
|
|
39
|
+
if (checkedToken !== ' ' && checkedToken !== 'x' && checkedToken !== 'X') return null;
|
|
40
|
+
if (content[4] !== ']' || content[5] !== ' ') return null;
|
|
41
|
+
|
|
42
|
+
let text = content.slice(6).trimEnd();
|
|
43
|
+
let markerId = null;
|
|
44
|
+
let markerFlags = '';
|
|
45
|
+
|
|
46
|
+
const markerStart = text.lastIndexOf('<!--');
|
|
47
|
+
if (markerStart >= 0 && text.endsWith('-->')) {
|
|
48
|
+
const markerBody = text.slice(markerStart + 4, -3).trim();
|
|
49
|
+
if (markerBody.startsWith('rs:task=')) {
|
|
50
|
+
const markerPayload = markerBody.slice('rs:task='.length).trim();
|
|
51
|
+
const markerParts = markerPayload.split(/\s+/).filter(Boolean);
|
|
52
|
+
markerId = markerParts[0] || null;
|
|
53
|
+
markerFlags = markerParts.slice(1).join(' ');
|
|
54
|
+
text = text.slice(0, markerStart).trimEnd();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
indent,
|
|
60
|
+
checked: checkedToken.toLowerCase() === 'x',
|
|
61
|
+
text: text.trim(),
|
|
62
|
+
markerId,
|
|
63
|
+
markerFlags
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseChildBulletLine(line) {
|
|
68
|
+
const { indent, content } = splitIndent(line);
|
|
69
|
+
if (!content.startsWith('- ')) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
indent,
|
|
74
|
+
content: content.slice(2).trim()
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function parseEvidenceLine(content) {
|
|
79
|
+
if (content.length < 9) return null;
|
|
80
|
+
if (content.slice(0, 9).toLowerCase() !== 'evidence:') return null;
|
|
81
|
+
return content.slice(9).trim();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseWarningLine(content) {
|
|
85
|
+
if (!content.startsWith(WARNING_PREFIX)) return null;
|
|
86
|
+
return content.slice(WARNING_PREFIX.length).trim();
|
|
87
|
+
}
|
|
11
88
|
|
|
12
89
|
function parseRoadmap(content) {
|
|
13
90
|
const lines = String(content || '').split(/\r?\n/);
|
|
91
|
+
const managedRange = findManagedRange(lines);
|
|
14
92
|
const tasks = [];
|
|
15
93
|
let section = '';
|
|
16
94
|
|
|
17
95
|
for (let index = 0; index < lines.length; index += 1) {
|
|
18
96
|
const line = lines[index];
|
|
19
|
-
const
|
|
20
|
-
if (
|
|
21
|
-
section =
|
|
97
|
+
const headingText = parseHeadingLine(line);
|
|
98
|
+
if (headingText) {
|
|
99
|
+
section = headingText;
|
|
22
100
|
}
|
|
23
101
|
|
|
24
|
-
const
|
|
25
|
-
if (!
|
|
102
|
+
const taskLine = parseTaskLine(line);
|
|
103
|
+
if (!taskLine) {
|
|
26
104
|
continue;
|
|
27
105
|
}
|
|
28
106
|
|
|
29
|
-
const indent
|
|
30
|
-
const checked = taskMatch[2].toLowerCase() === 'x';
|
|
31
|
-
const text = taskMatch[3].trim();
|
|
32
|
-
const markerId = taskMatch[4] || null;
|
|
33
|
-
const markerFlags = taskMatch[5] || '';
|
|
107
|
+
const { indent, checked, text, markerId, markerFlags } = taskLine;
|
|
34
108
|
const noTest = /\brs:no-test\b/i.test(markerFlags);
|
|
109
|
+
const taskIndentWidth = getIndentWidth(indent);
|
|
35
110
|
|
|
36
111
|
let warningLineIndex = null;
|
|
37
112
|
let warningText = null;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
113
|
+
const evidenceLines = [];
|
|
114
|
+
let lastChildLineIndex = index;
|
|
115
|
+
for (let childIndex = index + 1; childIndex < lines.length; childIndex += 1) {
|
|
116
|
+
const childLine = lines[childIndex];
|
|
117
|
+
if (!childLine.trim()) {
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
if (parseHeadingLine(childLine) || parseTaskLine(childLine)) {
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const childBullet = parseChildBulletLine(childLine);
|
|
125
|
+
if (!childBullet) {
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
if (getIndentWidth(childBullet.indent || '') <= taskIndentWidth) {
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
lastChildLineIndex = childIndex;
|
|
133
|
+
|
|
134
|
+
const evidenceText = parseEvidenceLine(childBullet.content);
|
|
135
|
+
if (evidenceText != null) {
|
|
136
|
+
evidenceLines.push({
|
|
137
|
+
lineIndex: childIndex,
|
|
138
|
+
text: evidenceText,
|
|
139
|
+
raw: childLine
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const warningTextValue = parseWarningLine(childBullet.content);
|
|
144
|
+
if (warningTextValue != null) {
|
|
145
|
+
warningLineIndex = childIndex;
|
|
146
|
+
warningText = warningTextValue;
|
|
44
147
|
}
|
|
45
148
|
}
|
|
46
149
|
|
|
@@ -50,8 +153,10 @@ function parseRoadmap(content) {
|
|
|
50
153
|
text,
|
|
51
154
|
checked,
|
|
52
155
|
lineIndex: index,
|
|
156
|
+
lastChildLineIndex,
|
|
53
157
|
warningLineIndex,
|
|
54
158
|
warningText,
|
|
159
|
+
evidenceLines,
|
|
55
160
|
markerId,
|
|
56
161
|
noTest,
|
|
57
162
|
indent,
|
|
@@ -61,6 +166,8 @@ function parseRoadmap(content) {
|
|
|
61
166
|
|
|
62
167
|
return {
|
|
63
168
|
lines,
|
|
169
|
+
managedRange,
|
|
170
|
+
hasManagedBlock: Boolean(managedRange),
|
|
64
171
|
tasks
|
|
65
172
|
};
|
|
66
173
|
}
|
|
@@ -105,6 +212,7 @@ function upsertManagedBlock(existingContent, managedBody) {
|
|
|
105
212
|
}
|
|
106
213
|
|
|
107
214
|
module.exports = {
|
|
215
|
+
findManagedRange,
|
|
108
216
|
parseRoadmap,
|
|
109
217
|
upsertManagedBlock
|
|
110
218
|
};
|
package/src/sync/index.js
CHANGED
|
@@ -34,6 +34,7 @@ function applySync(content, parsedTasks, results) {
|
|
|
34
34
|
const warningText = formatWarning(task.indent || '', reason || 'validation failed');
|
|
35
35
|
const hasWarning = task.warningLineIndex != null;
|
|
36
36
|
const warningIndex = hasWarning ? task.warningLineIndex + offset : null;
|
|
37
|
+
const lastChildLineIndex = (task.lastChildLineIndex != null ? task.lastChildLineIndex : task.lineIndex) + offset;
|
|
37
38
|
|
|
38
39
|
if (result.passed || !result.attempted) {
|
|
39
40
|
if (warningIndex != null && warningIndex >= 0 && warningIndex < lines.length) {
|
|
@@ -46,7 +47,7 @@ function applySync(content, parsedTasks, results) {
|
|
|
46
47
|
if (warningIndex != null && warningIndex >= 0 && warningIndex < lines.length) {
|
|
47
48
|
lines[warningIndex] = warningText;
|
|
48
49
|
} else {
|
|
49
|
-
lines.splice(
|
|
50
|
+
lines.splice(lastChildLineIndex + 1, 0, warningText);
|
|
50
51
|
offset += 1;
|
|
51
52
|
}
|
|
52
53
|
}
|
package/src/validator/index.js
CHANGED
|
@@ -206,6 +206,49 @@ function hasKnownFileExtension(token) {
|
|
|
206
206
|
return KNOWN_FILE_EXTENSIONS.has(token.slice(lastDot).toLowerCase());
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
function isAsciiAlphaNumeric(char) {
|
|
210
|
+
if (!char || char.length === 0) return false;
|
|
211
|
+
const code = char.charCodeAt(0);
|
|
212
|
+
return (code >= 48 && code <= 57) || (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function isPathTokenCharacter(char) {
|
|
216
|
+
return isAsciiAlphaNumeric(char) || char === '.' || char === '_' || char === '-' || char === '/' || char === '\\';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function stripTrailingPathPunctuation(token) {
|
|
220
|
+
let result = String(token || '');
|
|
221
|
+
while (result.length > 0) {
|
|
222
|
+
const lastChar = result[result.length - 1];
|
|
223
|
+
if (lastChar !== '.' && lastChar !== ',' && lastChar !== ';' && lastChar !== ':' && lastChar !== '!' && lastChar !== '?' && lastChar !== ')') {
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
result = result.slice(0, -1);
|
|
227
|
+
}
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function collectPathishTokens(text) {
|
|
232
|
+
const tokens = [];
|
|
233
|
+
let current = '';
|
|
234
|
+
const source = String(text || '');
|
|
235
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
236
|
+
const char = source[index];
|
|
237
|
+
if (isPathTokenCharacter(char)) {
|
|
238
|
+
current += char;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (current) {
|
|
242
|
+
tokens.push(stripTrailingPathPunctuation(current));
|
|
243
|
+
current = '';
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (current) {
|
|
247
|
+
tokens.push(stripTrailingPathPunctuation(current));
|
|
248
|
+
}
|
|
249
|
+
return tokens.filter(Boolean);
|
|
250
|
+
}
|
|
251
|
+
|
|
209
252
|
function extractExplicitPaths(text) {
|
|
210
253
|
const results = new Set();
|
|
211
254
|
const quoted = String(text).match(/`([^`]+)`/g) || [];
|
|
@@ -218,7 +261,7 @@ function extractExplicitPaths(text) {
|
|
|
218
261
|
|
|
219
262
|
const pathTokens = String(text).match(/([A-Za-z0-9_.-]+\/[A-Za-z0-9_./-]+)/g) || [];
|
|
220
263
|
for (const raw of pathTokens) {
|
|
221
|
-
const token = raw
|
|
264
|
+
const token = stripTrailingPathPunctuation(raw);
|
|
222
265
|
if (isLikelyPath(token)) results.add(token);
|
|
223
266
|
}
|
|
224
267
|
|
|
@@ -235,7 +278,7 @@ function extractStandaloneFilenames(text) {
|
|
|
235
278
|
STANDALONE_FILE_RE.lastIndex = 0;
|
|
236
279
|
let m = STANDALONE_FILE_RE.exec(String(text));
|
|
237
280
|
while (m) {
|
|
238
|
-
const token = m[1]
|
|
281
|
+
const token = stripTrailingPathPunctuation(m[1]);
|
|
239
282
|
if (hasKnownFileExtension(token) && !token.startsWith('.')) {
|
|
240
283
|
results.add(token);
|
|
241
284
|
}
|
|
@@ -576,6 +619,207 @@ function findArtifactEvidence(taskText, fileIndex) {
|
|
|
576
619
|
return { files: files.slice(0, 20), heuristicArtifacts };
|
|
577
620
|
}
|
|
578
621
|
|
|
622
|
+
function unionArrays(...values) {
|
|
623
|
+
return Array.from(new Set(values.flat().filter(Boolean)));
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function isTestPath(relativePath) {
|
|
627
|
+
return /(^|\/)(__tests__|tests)\//.test(relativePath) || /\.test\.|\.spec\.|_test\.go$/.test(relativePath);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function extractEvidencePaths(evidenceText) {
|
|
631
|
+
const paths = new Set();
|
|
632
|
+
for (const rawCandidate of collectPathishTokens(evidenceText)) {
|
|
633
|
+
const candidate = rawCandidate.split('\\').join('/');
|
|
634
|
+
if (!candidate.includes('/') || candidate.includes('*') || candidate.includes('?')) {
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
if (!hasKnownFileExtension(candidate)) {
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
paths.add(candidate.replace(/^\.\//, ''));
|
|
641
|
+
}
|
|
642
|
+
return Array.from(paths).sort((left, right) => left.localeCompare(right));
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function evidenceLineHasPassingSummary(evidenceText) {
|
|
646
|
+
const text = String(evidenceText || '');
|
|
647
|
+
if (/\b\d+\s*\/\s*\d+\s+tests?\s+passing\b/i.test(text)) {
|
|
648
|
+
return true;
|
|
649
|
+
}
|
|
650
|
+
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)) {
|
|
651
|
+
return true;
|
|
652
|
+
}
|
|
653
|
+
return false;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function evidenceSummaryImpliesTests(evidenceText) {
|
|
657
|
+
return /\btests?\b/i.test(String(evidenceText || '')) ||
|
|
658
|
+
/\b(?:vitest|jest|npm test|pnpm test|yarn test|bun test)\b/i.test(String(evidenceText || ''));
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function evaluateAuthoritativeEvidence(task, fileIndex) {
|
|
662
|
+
const evidenceLines = Array.isArray(task.evidenceLines) ? task.evidenceLines : [];
|
|
663
|
+
if (evidenceLines.length === 0) {
|
|
664
|
+
return {
|
|
665
|
+
active: false,
|
|
666
|
+
passed: false,
|
|
667
|
+
confidence: null,
|
|
668
|
+
referencedPaths: [],
|
|
669
|
+
matchedPaths: [],
|
|
670
|
+
summaryMatches: [],
|
|
671
|
+
summaryImpliesTests: false,
|
|
672
|
+
hasExtractedPaths: false
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const referencedPaths = unionArrays(...evidenceLines.map((line) => extractEvidencePaths(line.text)));
|
|
677
|
+
const matchedPaths = referencedPaths.length > 0 ? findFilesByPathHints(referencedPaths, fileIndex) : [];
|
|
678
|
+
const summaryMatches = evidenceLines
|
|
679
|
+
.filter((line) => evidenceLineHasPassingSummary(line.text))
|
|
680
|
+
.map((line) => line.text);
|
|
681
|
+
const summaryImpliesTests = summaryMatches.some((line) => evidenceSummaryImpliesTests(line));
|
|
682
|
+
const matchedTestPaths = matchedPaths.filter((relativePath) => isTestPath(relativePath));
|
|
683
|
+
const matchedNonTestPaths = matchedPaths.filter((relativePath) => !isTestPath(relativePath));
|
|
684
|
+
|
|
685
|
+
if (referencedPaths.length > 0) {
|
|
686
|
+
if (matchedPaths.length === 0) {
|
|
687
|
+
return {
|
|
688
|
+
active: true,
|
|
689
|
+
passed: false,
|
|
690
|
+
confidence: 'low',
|
|
691
|
+
referencedPaths,
|
|
692
|
+
matchedPaths: [],
|
|
693
|
+
summaryMatches: [],
|
|
694
|
+
summaryImpliesTests: false,
|
|
695
|
+
hasExtractedPaths: true,
|
|
696
|
+
reasons: [`evidence file(s) not found: ${referencedPaths.join(', ')}`]
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return {
|
|
701
|
+
active: true,
|
|
702
|
+
passed: true,
|
|
703
|
+
confidence: matchedTestPaths.length > 0 && matchedNonTestPaths.length > 0 ? 'high' : 'medium',
|
|
704
|
+
referencedPaths,
|
|
705
|
+
matchedPaths,
|
|
706
|
+
summaryMatches: [],
|
|
707
|
+
summaryImpliesTests: matchedTestPaths.length > 0,
|
|
708
|
+
hasExtractedPaths: true,
|
|
709
|
+
reasons: []
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (summaryMatches.length > 0) {
|
|
714
|
+
return {
|
|
715
|
+
active: true,
|
|
716
|
+
passed: true,
|
|
717
|
+
confidence: 'medium',
|
|
718
|
+
referencedPaths: [],
|
|
719
|
+
matchedPaths: [],
|
|
720
|
+
summaryMatches,
|
|
721
|
+
summaryImpliesTests,
|
|
722
|
+
hasExtractedPaths: false,
|
|
723
|
+
reasons: []
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
return {
|
|
728
|
+
active: true,
|
|
729
|
+
passed: false,
|
|
730
|
+
confidence: null,
|
|
731
|
+
referencedPaths: [],
|
|
732
|
+
matchedPaths: [],
|
|
733
|
+
summaryMatches: [],
|
|
734
|
+
summaryImpliesTests: false,
|
|
735
|
+
hasExtractedPaths: false,
|
|
736
|
+
reasons: []
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function applyAuthoritativeEvidence(evidence, authoritativeEvidence, fileIndex) {
|
|
741
|
+
evidence.authoritative = authoritativeEvidence.passed;
|
|
742
|
+
evidence.authoritativeFiles = authoritativeEvidence.matchedPaths;
|
|
743
|
+
evidence.authoritativeSummaries = authoritativeEvidence.summaryMatches;
|
|
744
|
+
evidence.files = unionArrays(evidence.files, authoritativeEvidence.matchedPaths);
|
|
745
|
+
|
|
746
|
+
const indexedFiles = new Map(fileIndex.map((file) => [file.relativePath, file]));
|
|
747
|
+
for (const relativePath of authoritativeEvidence.matchedPaths) {
|
|
748
|
+
const file = indexedFiles.get(relativePath);
|
|
749
|
+
if (!file) {
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
if (file.isTestFile || isTestPath(relativePath)) {
|
|
753
|
+
evidence.test = true;
|
|
754
|
+
evidence.testFiles = unionArrays(evidence.testFiles, [relativePath]);
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
if (CODE_EXTENSIONS.has(file.ext)) {
|
|
758
|
+
evidence.code = true;
|
|
759
|
+
evidence.codeFiles = unionArrays(evidence.codeFiles, [relativePath]);
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
evidence.artifact = true;
|
|
763
|
+
evidence.artifactFiles = unionArrays(evidence.artifactFiles, [relativePath]);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (authoritativeEvidence.summaryImpliesTests) {
|
|
767
|
+
evidence.test = true;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function countStrongEvidenceCategories(taskText, evidence) {
|
|
772
|
+
if (isDocTask(taskText)) {
|
|
773
|
+
return {
|
|
774
|
+
categories: [
|
|
775
|
+
...(evidence.artifact ? ['artifact'] : []),
|
|
776
|
+
...(evidence.files.length > 0 ? ['referenced-file'] : []),
|
|
777
|
+
...(evidence.code ? ['code'] : []),
|
|
778
|
+
...(evidence.test ? ['test'] : [])
|
|
779
|
+
]
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const categories = [];
|
|
784
|
+
if (evidence.code) {
|
|
785
|
+
categories.push('code');
|
|
786
|
+
}
|
|
787
|
+
if (evidence.test) {
|
|
788
|
+
categories.push('test');
|
|
789
|
+
}
|
|
790
|
+
if (evidence.files.length > 0 || evidence.symbols.length > 0 || evidence.structuralEvidence === true) {
|
|
791
|
+
categories.push('feature-surface');
|
|
792
|
+
}
|
|
793
|
+
return { categories };
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function findNegativeImplementationSignals(candidatePaths, fileIndex) {
|
|
797
|
+
if (!Array.isArray(candidatePaths) || candidatePaths.length === 0) {
|
|
798
|
+
return [];
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const indexedFiles = new Map(fileIndex.map((file) => [file.relativePath, file]));
|
|
802
|
+
const negativeSignals = [
|
|
803
|
+
/\bTODO\b/i,
|
|
804
|
+
/\bFIXME\b/i,
|
|
805
|
+
/\bdisabled\b/i,
|
|
806
|
+
/\bnot implemented\b/i,
|
|
807
|
+
/throw\s+new\s+Error\s*\(\s*['"`][^'"`]*not implemented/i
|
|
808
|
+
];
|
|
809
|
+
|
|
810
|
+
const matches = [];
|
|
811
|
+
for (const relativePath of unionArrays(candidatePaths)) {
|
|
812
|
+
const file = indexedFiles.get(relativePath);
|
|
813
|
+
if (!file || file.isTestFile || !CODE_EXTENSIONS.has(file.ext)) {
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
if (negativeSignals.some((pattern) => pattern.test(file.content))) {
|
|
817
|
+
matches.push(relativePath);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return matches.sort((left, right) => left.localeCompare(right));
|
|
821
|
+
}
|
|
822
|
+
|
|
579
823
|
function extractTaskNamespace(taskId) {
|
|
580
824
|
if (!taskId) return null;
|
|
581
825
|
const match = String(taskId).match(/^([a-z][a-z0-9]*)-/);
|
|
@@ -771,6 +1015,7 @@ function validateTask(task, context, config, plugins) {
|
|
|
771
1015
|
const pathHints = extractExplicitPaths(task.text);
|
|
772
1016
|
const standaloneFilenames = extractStandaloneFilenames(task.text);
|
|
773
1017
|
const symbolHints = extractSymbolHints(task.text);
|
|
1018
|
+
const authoritativeEvidence = evaluateAuthoritativeEvidence(task, context.fileIndex);
|
|
774
1019
|
|
|
775
1020
|
const filesFromPaths = findFilesByPathHints(pathHints, context.fileIndex);
|
|
776
1021
|
const filesFromSymbols = findFilesBySymbols(symbolHints, context.fileIndex);
|
|
@@ -800,12 +1045,19 @@ function validateTask(task, context, config, plugins) {
|
|
|
800
1045
|
heuristicArtifacts,
|
|
801
1046
|
structuralEvidence: structuralCheck.applicable ? structuralCheck.passed : null,
|
|
802
1047
|
structuralFiles: structuralCheck.structuralFiles,
|
|
1048
|
+
authoritative: false,
|
|
1049
|
+
authoritativeFiles: [],
|
|
1050
|
+
authoritativeSummaries: []
|
|
803
1051
|
};
|
|
1052
|
+
applyAuthoritativeEvidence(evidence, authoritativeEvidence, context.fileIndex);
|
|
804
1053
|
|
|
805
1054
|
const reasons = [];
|
|
806
1055
|
if (pathHints.length > 0 && filesFromPaths.length === 0) {
|
|
807
1056
|
reasons.push(`missing referenced file(s): ${pathHints.join(', ')}`);
|
|
808
1057
|
}
|
|
1058
|
+
if (Array.isArray(authoritativeEvidence.reasons) && authoritativeEvidence.reasons.length > 0) {
|
|
1059
|
+
reasons.push(...authoritativeEvidence.reasons);
|
|
1060
|
+
}
|
|
809
1061
|
if (symbolHints.length > 0 && filesFromSymbols.length === 0) {
|
|
810
1062
|
reasons.push(`missing symbol(s): ${symbolHints.join(', ')}`);
|
|
811
1063
|
}
|
|
@@ -818,9 +1070,9 @@ function validateTask(task, context, config, plugins) {
|
|
|
818
1070
|
|
|
819
1071
|
const hasEvidence = evidence.code || evidence.test || evidence.artifact || evidence.files.length > 0;
|
|
820
1072
|
const hasWeakEvidence = filesFromWeakPathTokens.length > 0;
|
|
821
|
-
if (!hasEvidence && !hasWeakEvidence && !structuralCheck.applicable) {
|
|
1073
|
+
if (!hasEvidence && !hasWeakEvidence && !structuralCheck.applicable && !authoritativeEvidence.hasExtractedPaths && !authoritativeEvidence.passed) {
|
|
822
1074
|
reasons.push('no code, test, or artifact evidence found');
|
|
823
|
-
} else if (!hasEvidence && !hasWeakEvidence && structuralCheck.applicable && structuralCheck.passed) {
|
|
1075
|
+
} else if (!hasEvidence && !hasWeakEvidence && structuralCheck.applicable && structuralCheck.passed && !authoritativeEvidence.hasExtractedPaths && !authoritativeEvidence.passed) {
|
|
824
1076
|
reasons.push('no code, test, or artifact evidence found');
|
|
825
1077
|
} else if (!hasEvidence && hasWeakEvidence) {
|
|
826
1078
|
if (weakPathContentTokens.length === 0) {
|
|
@@ -834,9 +1086,11 @@ function validateTask(task, context, config, plugins) {
|
|
|
834
1086
|
const configuredRules = Array.isArray(config.validators) ? config.validators : [];
|
|
835
1087
|
const pluginRules = collectPluginContributions(plugins || [], 'registerValidators', context);
|
|
836
1088
|
let overrideResult = null;
|
|
1089
|
+
let hasRuleGrantedEvidence = false;
|
|
837
1090
|
for (const rule of [...configuredRules, ...pluginRules]) {
|
|
838
1091
|
const ruleResult = evaluateRule(rule, task, context);
|
|
839
1092
|
if (ruleResult.evidence && Object.keys(ruleResult.evidence).length > 0) {
|
|
1093
|
+
hasRuleGrantedEvidence = true;
|
|
840
1094
|
Object.assign(evidence, mergeRuleEvidence(evidence, ruleResult.evidence));
|
|
841
1095
|
}
|
|
842
1096
|
if (!ruleResult.passed) {
|
|
@@ -856,7 +1110,7 @@ function validateTask(task, context, config, plugins) {
|
|
|
856
1110
|
}
|
|
857
1111
|
}
|
|
858
1112
|
|
|
859
|
-
if (requiresTest && !evidence.test) {
|
|
1113
|
+
if (requiresTest && !evidence.test && !authoritativeEvidence.passed) {
|
|
860
1114
|
reasons.push('missing test evidence');
|
|
861
1115
|
}
|
|
862
1116
|
|
|
@@ -866,12 +1120,50 @@ function validateTask(task, context, config, plugins) {
|
|
|
866
1120
|
uniqueReasons = Array.isArray(overrideResult.reasons) ? Array.from(new Set(overrideResult.reasons)) : [];
|
|
867
1121
|
}
|
|
868
1122
|
|
|
869
|
-
const attempted = hasStrongEvidence || hasWeakEvidence || pathHints.length > 0 || symbolHints.length > 0;
|
|
1123
|
+
const attempted = hasStrongEvidence || hasWeakEvidence || pathHints.length > 0 || symbolHints.length > 0 || authoritativeEvidence.active;
|
|
1124
|
+
const { categories: strongEvidenceCategories } = countStrongEvidenceCategories(task.text, evidence);
|
|
1125
|
+
const strongEvidenceCount = strongEvidenceCategories.length;
|
|
1126
|
+
const hasDirectReferencePass = filesFromPaths.length > 0 || filesFromSymbols.length > 0;
|
|
1127
|
+
const hasArtifactTaskPass = evidence.artifact && (
|
|
1128
|
+
isDocTask(task.text) ||
|
|
1129
|
+
evidence.heuristicArtifacts.length > 0 ||
|
|
1130
|
+
filesFromPaths.some((relativePath) => !CODE_EXTENSIONS.has(path.extname(relativePath).toLowerCase()))
|
|
1131
|
+
);
|
|
1132
|
+
const hasTrustedRuleEvidencePass = hasRuleGrantedEvidence && uniqueReasons.length === 0;
|
|
1133
|
+
const meetsStrongThreshold = !isDocTask(task.text) && strongEvidenceCount >= 2;
|
|
1134
|
+
|
|
1135
|
+
let confidence = 'low';
|
|
1136
|
+
if (authoritativeEvidence.passed) {
|
|
1137
|
+
confidence = authoritativeEvidence.confidence || 'medium';
|
|
1138
|
+
} else if (meetsStrongThreshold) {
|
|
1139
|
+
confidence = 'high';
|
|
1140
|
+
} else if (strongEvidenceCount === 1 || hasDirectReferencePass || hasArtifactTaskPass || hasTrustedRuleEvidencePass) {
|
|
1141
|
+
confidence = 'medium';
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
const negativeSignalMatches = findNegativeImplementationSignals(
|
|
1145
|
+
unionArrays(
|
|
1146
|
+
evidence.codeFiles,
|
|
1147
|
+
evidence.files,
|
|
1148
|
+
evidence.symbols,
|
|
1149
|
+
evidence.weakPathFiles,
|
|
1150
|
+
evidence.structuralFiles
|
|
1151
|
+
),
|
|
1152
|
+
context.fileIndex
|
|
1153
|
+
);
|
|
1154
|
+
if (negativeSignalMatches.length > 0) {
|
|
1155
|
+
uniqueReasons.push(`negative implementation signal found in matched evidence: ${negativeSignalMatches.join(', ')}`);
|
|
1156
|
+
uniqueReasons = Array.from(new Set(uniqueReasons));
|
|
1157
|
+
}
|
|
870
1158
|
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
1159
|
+
let passed = authoritativeEvidence.passed || hasDirectReferencePass || hasArtifactTaskPass || hasTrustedRuleEvidencePass || meetsStrongThreshold;
|
|
1160
|
+
if (task.warningText && passed && !authoritativeEvidence.passed && !meetsStrongThreshold) {
|
|
1161
|
+
passed = false;
|
|
1162
|
+
uniqueReasons.push(task.warningText);
|
|
1163
|
+
uniqueReasons = Array.from(new Set(uniqueReasons));
|
|
1164
|
+
}
|
|
1165
|
+
if (negativeSignalMatches.length > 0) {
|
|
1166
|
+
passed = false;
|
|
875
1167
|
}
|
|
876
1168
|
|
|
877
1169
|
// True when the only passing evidence is artifact/doc files and the task is not a doc task.
|
|
@@ -880,7 +1172,7 @@ function validateTask(task, context, config, plugins) {
|
|
|
880
1172
|
|
|
881
1173
|
return {
|
|
882
1174
|
taskId: task.id,
|
|
883
|
-
passed: overrideResult ? overrideResult.passed !== false : uniqueReasons.length === 0,
|
|
1175
|
+
passed: overrideResult ? overrideResult.passed !== false : (passed && uniqueReasons.length === 0),
|
|
884
1176
|
confidence,
|
|
885
1177
|
reasons: uniqueReasons,
|
|
886
1178
|
evidence,
|