roadmapsmith 0.9.6 → 0.9.8
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 +124 -20
- package/src/sync/index.js +2 -1
- package/src/validator/index.js +321 -11
package/package.json
CHANGED
package/src/parser/index.js
CHANGED
|
@@ -2,12 +2,89 @@
|
|
|
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/);
|
|
@@ -17,31 +94,56 @@ function parseRoadmap(content) {
|
|
|
17
94
|
|
|
18
95
|
for (let index = 0; index < lines.length; index += 1) {
|
|
19
96
|
const line = lines[index];
|
|
20
|
-
const
|
|
21
|
-
if (
|
|
22
|
-
section =
|
|
97
|
+
const headingText = parseHeadingLine(line);
|
|
98
|
+
if (headingText) {
|
|
99
|
+
section = headingText;
|
|
23
100
|
}
|
|
24
101
|
|
|
25
|
-
const
|
|
26
|
-
if (!
|
|
102
|
+
const taskLine = parseTaskLine(line);
|
|
103
|
+
if (!taskLine) {
|
|
27
104
|
continue;
|
|
28
105
|
}
|
|
29
106
|
|
|
30
|
-
const indent
|
|
31
|
-
const checked = taskMatch[2].toLowerCase() === 'x';
|
|
32
|
-
const text = taskMatch[3].trim();
|
|
33
|
-
const markerId = taskMatch[4] || null;
|
|
34
|
-
const markerFlags = taskMatch[5] || '';
|
|
107
|
+
const { indent, checked, text, markerId, markerFlags } = taskLine;
|
|
35
108
|
const noTest = /\brs:no-test\b/i.test(markerFlags);
|
|
109
|
+
const taskIndentWidth = getIndentWidth(indent);
|
|
36
110
|
|
|
37
111
|
let warningLineIndex = null;
|
|
38
112
|
let warningText = null;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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;
|
|
45
147
|
}
|
|
46
148
|
}
|
|
47
149
|
|
|
@@ -51,8 +153,10 @@ function parseRoadmap(content) {
|
|
|
51
153
|
text,
|
|
52
154
|
checked,
|
|
53
155
|
lineIndex: index,
|
|
156
|
+
lastChildLineIndex,
|
|
54
157
|
warningLineIndex,
|
|
55
158
|
warningText,
|
|
159
|
+
evidenceLines,
|
|
56
160
|
markerId,
|
|
57
161
|
noTest,
|
|
58
162
|
indent,
|
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,204 @@ 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
|
+
/\bnot implemented\b/i,
|
|
804
|
+
/throw\s+new\s+Error\s*\(\s*['"`][^'"`]*not implemented/i
|
|
805
|
+
];
|
|
806
|
+
|
|
807
|
+
const matches = [];
|
|
808
|
+
for (const relativePath of unionArrays(candidatePaths)) {
|
|
809
|
+
const file = indexedFiles.get(relativePath);
|
|
810
|
+
if (!file || file.isTestFile || !CODE_EXTENSIONS.has(file.ext)) {
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
if (negativeSignals.some((pattern) => pattern.test(file.content))) {
|
|
814
|
+
matches.push(relativePath);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return matches.sort((left, right) => left.localeCompare(right));
|
|
818
|
+
}
|
|
819
|
+
|
|
579
820
|
function extractTaskNamespace(taskId) {
|
|
580
821
|
if (!taskId) return null;
|
|
581
822
|
const match = String(taskId).match(/^([a-z][a-z0-9]*)-/);
|
|
@@ -771,6 +1012,7 @@ function validateTask(task, context, config, plugins) {
|
|
|
771
1012
|
const pathHints = extractExplicitPaths(task.text);
|
|
772
1013
|
const standaloneFilenames = extractStandaloneFilenames(task.text);
|
|
773
1014
|
const symbolHints = extractSymbolHints(task.text);
|
|
1015
|
+
const authoritativeEvidence = evaluateAuthoritativeEvidence(task, context.fileIndex);
|
|
774
1016
|
|
|
775
1017
|
const filesFromPaths = findFilesByPathHints(pathHints, context.fileIndex);
|
|
776
1018
|
const filesFromSymbols = findFilesBySymbols(symbolHints, context.fileIndex);
|
|
@@ -800,12 +1042,19 @@ function validateTask(task, context, config, plugins) {
|
|
|
800
1042
|
heuristicArtifacts,
|
|
801
1043
|
structuralEvidence: structuralCheck.applicable ? structuralCheck.passed : null,
|
|
802
1044
|
structuralFiles: structuralCheck.structuralFiles,
|
|
1045
|
+
authoritative: false,
|
|
1046
|
+
authoritativeFiles: [],
|
|
1047
|
+
authoritativeSummaries: []
|
|
803
1048
|
};
|
|
1049
|
+
applyAuthoritativeEvidence(evidence, authoritativeEvidence, context.fileIndex);
|
|
804
1050
|
|
|
805
1051
|
const reasons = [];
|
|
806
1052
|
if (pathHints.length > 0 && filesFromPaths.length === 0) {
|
|
807
1053
|
reasons.push(`missing referenced file(s): ${pathHints.join(', ')}`);
|
|
808
1054
|
}
|
|
1055
|
+
if (Array.isArray(authoritativeEvidence.reasons) && authoritativeEvidence.reasons.length > 0) {
|
|
1056
|
+
reasons.push(...authoritativeEvidence.reasons);
|
|
1057
|
+
}
|
|
809
1058
|
if (symbolHints.length > 0 && filesFromSymbols.length === 0) {
|
|
810
1059
|
reasons.push(`missing symbol(s): ${symbolHints.join(', ')}`);
|
|
811
1060
|
}
|
|
@@ -818,9 +1067,9 @@ function validateTask(task, context, config, plugins) {
|
|
|
818
1067
|
|
|
819
1068
|
const hasEvidence = evidence.code || evidence.test || evidence.artifact || evidence.files.length > 0;
|
|
820
1069
|
const hasWeakEvidence = filesFromWeakPathTokens.length > 0;
|
|
821
|
-
if (!hasEvidence && !hasWeakEvidence && !structuralCheck.applicable) {
|
|
1070
|
+
if (!hasEvidence && !hasWeakEvidence && !structuralCheck.applicable && !authoritativeEvidence.hasExtractedPaths && !authoritativeEvidence.passed) {
|
|
822
1071
|
reasons.push('no code, test, or artifact evidence found');
|
|
823
|
-
} else if (!hasEvidence && !hasWeakEvidence && structuralCheck.applicable && structuralCheck.passed) {
|
|
1072
|
+
} else if (!hasEvidence && !hasWeakEvidence && structuralCheck.applicable && structuralCheck.passed && !authoritativeEvidence.hasExtractedPaths && !authoritativeEvidence.passed) {
|
|
824
1073
|
reasons.push('no code, test, or artifact evidence found');
|
|
825
1074
|
} else if (!hasEvidence && hasWeakEvidence) {
|
|
826
1075
|
if (weakPathContentTokens.length === 0) {
|
|
@@ -834,9 +1083,11 @@ function validateTask(task, context, config, plugins) {
|
|
|
834
1083
|
const configuredRules = Array.isArray(config.validators) ? config.validators : [];
|
|
835
1084
|
const pluginRules = collectPluginContributions(plugins || [], 'registerValidators', context);
|
|
836
1085
|
let overrideResult = null;
|
|
1086
|
+
let hasRuleGrantedEvidence = false;
|
|
837
1087
|
for (const rule of [...configuredRules, ...pluginRules]) {
|
|
838
1088
|
const ruleResult = evaluateRule(rule, task, context);
|
|
839
1089
|
if (ruleResult.evidence && Object.keys(ruleResult.evidence).length > 0) {
|
|
1090
|
+
hasRuleGrantedEvidence = true;
|
|
840
1091
|
Object.assign(evidence, mergeRuleEvidence(evidence, ruleResult.evidence));
|
|
841
1092
|
}
|
|
842
1093
|
if (!ruleResult.passed) {
|
|
@@ -856,7 +1107,7 @@ function validateTask(task, context, config, plugins) {
|
|
|
856
1107
|
}
|
|
857
1108
|
}
|
|
858
1109
|
|
|
859
|
-
if (requiresTest && !evidence.test) {
|
|
1110
|
+
if (requiresTest && !evidence.test && !authoritativeEvidence.passed) {
|
|
860
1111
|
reasons.push('missing test evidence');
|
|
861
1112
|
}
|
|
862
1113
|
|
|
@@ -866,12 +1117,67 @@ function validateTask(task, context, config, plugins) {
|
|
|
866
1117
|
uniqueReasons = Array.isArray(overrideResult.reasons) ? Array.from(new Set(overrideResult.reasons)) : [];
|
|
867
1118
|
}
|
|
868
1119
|
|
|
869
|
-
const attempted = hasStrongEvidence || hasWeakEvidence || pathHints.length > 0 || symbolHints.length > 0;
|
|
1120
|
+
const attempted = hasStrongEvidence || hasWeakEvidence || pathHints.length > 0 || symbolHints.length > 0 || authoritativeEvidence.active;
|
|
1121
|
+
const { categories: strongEvidenceCategories } = countStrongEvidenceCategories(task.text, evidence);
|
|
1122
|
+
const strongEvidenceCount = strongEvidenceCategories.length;
|
|
1123
|
+
const hasDirectReferencePass = filesFromPaths.length > 0 || filesFromSymbols.length > 0;
|
|
1124
|
+
const hasArtifactTaskPass = evidence.artifact && (
|
|
1125
|
+
isDocTask(task.text) ||
|
|
1126
|
+
evidence.heuristicArtifacts.length > 0 ||
|
|
1127
|
+
filesFromPaths.some((relativePath) => !CODE_EXTENSIONS.has(path.extname(relativePath).toLowerCase()))
|
|
1128
|
+
);
|
|
1129
|
+
const hasTrustedRuleEvidencePass = hasRuleGrantedEvidence && uniqueReasons.length === 0;
|
|
1130
|
+
const meetsStrongThreshold = !isDocTask(task.text) && strongEvidenceCount >= 2;
|
|
1131
|
+
|
|
1132
|
+
let confidence = 'low';
|
|
1133
|
+
if (authoritativeEvidence.passed) {
|
|
1134
|
+
confidence = authoritativeEvidence.confidence || 'medium';
|
|
1135
|
+
} else if (meetsStrongThreshold) {
|
|
1136
|
+
confidence = 'high';
|
|
1137
|
+
} else if (strongEvidenceCount === 1 || hasDirectReferencePass || hasArtifactTaskPass || hasTrustedRuleEvidencePass) {
|
|
1138
|
+
confidence = 'medium';
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
const negativeSignalMatches = findNegativeImplementationSignals(
|
|
1142
|
+
unionArrays(
|
|
1143
|
+
evidence.codeFiles,
|
|
1144
|
+
evidence.files,
|
|
1145
|
+
evidence.symbols,
|
|
1146
|
+
evidence.weakPathFiles,
|
|
1147
|
+
evidence.structuralFiles
|
|
1148
|
+
),
|
|
1149
|
+
context.fileIndex
|
|
1150
|
+
);
|
|
1151
|
+
if (negativeSignalMatches.length > 0) {
|
|
1152
|
+
uniqueReasons.push(`negative implementation signal found in matched evidence: ${negativeSignalMatches.join(', ')}`);
|
|
1153
|
+
uniqueReasons = Array.from(new Set(uniqueReasons));
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
let passed = authoritativeEvidence.passed || hasDirectReferencePass || hasArtifactTaskPass || hasTrustedRuleEvidencePass || meetsStrongThreshold;
|
|
1157
|
+
if (task.warningText && passed && !authoritativeEvidence.passed && !meetsStrongThreshold) {
|
|
1158
|
+
passed = false;
|
|
1159
|
+
uniqueReasons.push(task.warningText);
|
|
1160
|
+
uniqueReasons = Array.from(new Set(uniqueReasons));
|
|
1161
|
+
}
|
|
1162
|
+
if (negativeSignalMatches.length > 0) {
|
|
1163
|
+
passed = false;
|
|
1164
|
+
}
|
|
870
1165
|
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
|
|
1166
|
+
const shouldPreserveCheckedTask =
|
|
1167
|
+
task.checked &&
|
|
1168
|
+
!passed &&
|
|
1169
|
+
!authoritativeEvidence.active &&
|
|
1170
|
+
pathHints.length === 0 &&
|
|
1171
|
+
symbolHints.length === 0 &&
|
|
1172
|
+
!hasDirectReferencePass &&
|
|
1173
|
+
evidence.structuralEvidence !== false &&
|
|
1174
|
+
negativeSignalMatches.length === 0;
|
|
1175
|
+
let preservedCheckedState = false;
|
|
1176
|
+
if (shouldPreserveCheckedTask) {
|
|
1177
|
+
passed = true;
|
|
874
1178
|
confidence = 'low';
|
|
1179
|
+
uniqueReasons = [];
|
|
1180
|
+
preservedCheckedState = true;
|
|
875
1181
|
}
|
|
876
1182
|
|
|
877
1183
|
// True when the only passing evidence is artifact/doc files and the task is not a doc task.
|
|
@@ -880,14 +1186,15 @@ function validateTask(task, context, config, plugins) {
|
|
|
880
1186
|
|
|
881
1187
|
return {
|
|
882
1188
|
taskId: task.id,
|
|
883
|
-
passed: overrideResult ? overrideResult.passed !== false : uniqueReasons.length === 0,
|
|
1189
|
+
passed: overrideResult ? overrideResult.passed !== false : (passed && uniqueReasons.length === 0),
|
|
884
1190
|
confidence,
|
|
885
1191
|
reasons: uniqueReasons,
|
|
886
1192
|
evidence,
|
|
887
1193
|
evidenceIsDocOnly,
|
|
888
1194
|
requiresTest,
|
|
889
1195
|
hasEvidence: hasStrongEvidence || hasWeakEvidence,
|
|
890
|
-
attempted
|
|
1196
|
+
attempted,
|
|
1197
|
+
preservedCheckedState
|
|
891
1198
|
};
|
|
892
1199
|
}
|
|
893
1200
|
|
|
@@ -945,6 +1252,9 @@ function applyMinimumConfidence(results, minimumConfidence) {
|
|
|
945
1252
|
const minRank = CONFIDENCE_RANK[minimumConfidence] ?? 0;
|
|
946
1253
|
if (minRank === 0) return;
|
|
947
1254
|
for (const result of Object.values(results)) {
|
|
1255
|
+
if (result.preservedCheckedState) {
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
948
1258
|
if ((CONFIDENCE_RANK[result.confidence] ?? 0) < minRank) {
|
|
949
1259
|
result.passed = false;
|
|
950
1260
|
result.reasons = [
|