roadmapsmith 0.9.29 → 0.9.31
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/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +3 -3
- package/README.md +100 -326
- package/bin/cli.js +50 -17
- package/package.json +2 -2
- package/skills/roadmap-maintain/SKILL.md +1 -0
- package/skills/roadmap-status/SKILL.md +7 -6
- package/skills/roadmap-update/SKILL.md +4 -4
- package/skills.json +13 -13
- package/src/host.js +96 -27
- package/src/reasons.js +28 -0
- package/src/slash.js +126 -44
- package/src/sync/index.js +9 -4
- package/src/validator/index.js +268 -37
package/src/validator/index.js
CHANGED
|
@@ -14,6 +14,7 @@ const CODE_EXTENSIONS = new Set([
|
|
|
14
14
|
const TRANSLATION_DIR_SEGMENTS = ['locale', 'locales', 'i18n', 'translations'];
|
|
15
15
|
const DEFAULT_EXCLUDED_PATH_PREFIXES = ['.claude/', '.agent/', 'roadmap-skill/'];
|
|
16
16
|
const GENERATED_OUTPUT_PREFIXES = ['dist-electron/', 'dist/', 'build/', 'out/', '.next/', 'coverage/'];
|
|
17
|
+
const AUXILIARY_HEURISTIC_PATH_SEGMENTS = new Set(['scripts', 'tools', 'tooling', 'demo', 'demos']);
|
|
17
18
|
|
|
18
19
|
// "docs" omitted from DOC_HINTS — it is a path prefix in scan tasks, not a doc-authoring keyword.
|
|
19
20
|
const DOC_HINTS = ['readme', 'changelog', 'documentation', 'spec', 'diagram', 'runbook'];
|
|
@@ -157,6 +158,88 @@ function isGeneratedOutputPath(relativePath) {
|
|
|
157
158
|
return GENERATED_OUTPUT_PREFIXES.some((prefix) => normalized.startsWith(prefix));
|
|
158
159
|
}
|
|
159
160
|
|
|
161
|
+
function splitNormalizedPathSegments(relativePath) {
|
|
162
|
+
return normalizePathForMatch(relativePath).split('/').filter(Boolean);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isAuxiliaryHeuristicPath(relativePath) {
|
|
166
|
+
return splitNormalizedPathSegments(relativePath).some((segment) => AUXILIARY_HEURISTIC_PATH_SEGMENTS.has(segment));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function relativePathExplicitlyReferenced(relativePath, options = {}) {
|
|
170
|
+
const normalized = normalizeReferencedPath(relativePath);
|
|
171
|
+
const explicitPaths = Array.isArray(options.explicitPaths) ? options.explicitPaths : [];
|
|
172
|
+
const explicitFilenames = Array.isArray(options.explicitFilenames) ? options.explicitFilenames : [];
|
|
173
|
+
if (explicitPaths.some((entry) => referencedPathMatches(normalized, entry))) {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const baseName = path.basename(normalized);
|
|
178
|
+
return explicitFilenames.some((entry) => normalizeReferencedPath(entry) === baseName);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function shouldSkipHeuristicFile(relativePath, options = {}) {
|
|
182
|
+
return isAuxiliaryHeuristicPath(relativePath) && !relativePathExplicitlyReferenced(relativePath, options);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function authoredSiblingGroupKey(relativePath) {
|
|
186
|
+
const normalized = normalizePathForMatch(relativePath);
|
|
187
|
+
const ext = path.extname(normalized);
|
|
188
|
+
return ext ? normalized.slice(0, -ext.length) : normalized;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function authoredSiblingExtensionRank(relativePath) {
|
|
192
|
+
const ext = path.extname(String(relativePath || '')).toLowerCase();
|
|
193
|
+
switch (ext) {
|
|
194
|
+
case '.ts':
|
|
195
|
+
return 0;
|
|
196
|
+
case '.tsx':
|
|
197
|
+
return 1;
|
|
198
|
+
case '.jsx':
|
|
199
|
+
return 2;
|
|
200
|
+
case '.js':
|
|
201
|
+
return 3;
|
|
202
|
+
case '.mjs':
|
|
203
|
+
return 4;
|
|
204
|
+
case '.cjs':
|
|
205
|
+
return 5;
|
|
206
|
+
case '.py':
|
|
207
|
+
return 6;
|
|
208
|
+
default:
|
|
209
|
+
return 10;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function dedupeAuthoredCompiledSiblings(relativePaths, fileIndex) {
|
|
214
|
+
const indexedPaths = new Set(fileIndex.map((file) => file.relativePath));
|
|
215
|
+
const groups = new Map();
|
|
216
|
+
for (const relativePath of Array.from(new Set(relativePaths)).filter(Boolean)) {
|
|
217
|
+
if (!indexedPaths.has(relativePath)) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const key = authoredSiblingGroupKey(relativePath);
|
|
221
|
+
const group = groups.get(key) || [];
|
|
222
|
+
group.push(relativePath);
|
|
223
|
+
groups.set(key, group);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const selected = [];
|
|
227
|
+
for (const group of groups.values()) {
|
|
228
|
+
group.sort((left, right) => {
|
|
229
|
+
const rankDelta = authoredSiblingExtensionRank(left) - authoredSiblingExtensionRank(right);
|
|
230
|
+
if (rankDelta !== 0) return rankDelta;
|
|
231
|
+
return left.localeCompare(right);
|
|
232
|
+
});
|
|
233
|
+
selected.push(group[0]);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return selected.sort((left, right) => left.localeCompare(right));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function finalizeHeuristicMatches(relativePaths, fileIndex, limit = 20) {
|
|
240
|
+
return dedupeAuthoredCompiledSiblings(relativePaths, fileIndex).slice(0, limit);
|
|
241
|
+
}
|
|
242
|
+
|
|
160
243
|
function readFileIndex(projectRoot, files, config) {
|
|
161
244
|
const index = [];
|
|
162
245
|
for (const relativePath of files) {
|
|
@@ -633,7 +716,7 @@ function findFilesByPathHints(pathHints, pathHintResolver) {
|
|
|
633
716
|
return Array.from(new Set(matches)).sort((left, right) => left.localeCompare(right));
|
|
634
717
|
}
|
|
635
718
|
|
|
636
|
-
function findFilesBySymbols(symbolHints, fileIndex) {
|
|
719
|
+
function findFilesBySymbols(symbolHints, fileIndex, heuristicOptions = {}) {
|
|
637
720
|
const matches = new Set();
|
|
638
721
|
for (const symbol of symbolHints) {
|
|
639
722
|
const regex = new RegExp(`\\b${escapeRegExp(symbol)}\\b`, 'i');
|
|
@@ -642,15 +725,18 @@ function findFilesBySymbols(symbolHints, fileIndex) {
|
|
|
642
725
|
if (!CODE_EXTENSIONS.has(file.ext)) {
|
|
643
726
|
continue;
|
|
644
727
|
}
|
|
728
|
+
if (shouldSkipHeuristicFile(file.relativePath, heuristicOptions)) {
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
645
731
|
if (regex.test(file.content)) {
|
|
646
732
|
matches.add(file.relativePath);
|
|
647
733
|
}
|
|
648
734
|
}
|
|
649
735
|
}
|
|
650
|
-
return Array.from(matches)
|
|
736
|
+
return finalizeHeuristicMatches(Array.from(matches), fileIndex);
|
|
651
737
|
}
|
|
652
738
|
|
|
653
|
-
function findFilesByTaskPathTokens(taskText, fileIndex, pathDerivedTokens = new Set()) {
|
|
739
|
+
function findFilesByTaskPathTokens(taskText, fileIndex, pathDerivedTokens = new Set(), heuristicOptions = {}) {
|
|
654
740
|
const tokens = tokenize(taskText)
|
|
655
741
|
.filter((token) => token.length >= 6 && !GENERIC_TASK_TOKENS.has(token) && !pathDerivedTokens.has(token))
|
|
656
742
|
.slice(0, 10);
|
|
@@ -659,6 +745,9 @@ function findFilesByTaskPathTokens(taskText, fileIndex, pathDerivedTokens = new
|
|
|
659
745
|
const matches = new Set();
|
|
660
746
|
for (const file of fileIndex) {
|
|
661
747
|
if (file.generatedOutput) continue;
|
|
748
|
+
if (shouldSkipHeuristicFile(file.relativePath, heuristicOptions)) {
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
662
751
|
const pathSegments = normalizePathForMatch(file.relativePath).split('/').filter(Boolean);
|
|
663
752
|
for (const token of tokens) {
|
|
664
753
|
if (pathSegments.some((segment) => segment === token || segment.includes(token))) {
|
|
@@ -668,16 +757,16 @@ function findFilesByTaskPathTokens(taskText, fileIndex, pathDerivedTokens = new
|
|
|
668
757
|
}
|
|
669
758
|
if (matches.size >= 20) break;
|
|
670
759
|
}
|
|
671
|
-
return Array.from(matches)
|
|
760
|
+
return finalizeHeuristicMatches(Array.from(matches), fileIndex);
|
|
672
761
|
}
|
|
673
762
|
|
|
674
|
-
function extractTaskEvidenceTokens(taskText, pathDerivedTokens = new Set()) {
|
|
763
|
+
function extractTaskEvidenceTokens(taskText, pathDerivedTokens = new Set(), minimumLength = 3) {
|
|
675
764
|
return tokenize(taskText)
|
|
676
|
-
.filter((token) => token.length >=
|
|
765
|
+
.filter((token) => token.length >= minimumLength && !GENERIC_TASK_TOKENS.has(token) && !token.endsWith('/') && !pathDerivedTokens.has(token))
|
|
677
766
|
.slice(0, 8);
|
|
678
767
|
}
|
|
679
768
|
|
|
680
|
-
function findWeakPathContentSpecificTokens(taskText, fileIndex, weakPathFiles, pathDerivedTokens = new Set()) {
|
|
769
|
+
function findWeakPathContentSpecificTokens(taskText, fileIndex, weakPathFiles, pathDerivedTokens = new Set(), heuristicOptions = {}) {
|
|
681
770
|
const tokens = extractTaskEvidenceTokens(taskText, pathDerivedTokens);
|
|
682
771
|
if (tokens.length === 0 || weakPathFiles.length === 0) return [];
|
|
683
772
|
|
|
@@ -687,6 +776,9 @@ function findWeakPathContentSpecificTokens(taskText, fileIndex, weakPathFiles, p
|
|
|
687
776
|
if (!weakFiles.has(file.relativePath) || !CODE_EXTENSIONS.has(file.ext) || file.isTestFile) {
|
|
688
777
|
continue;
|
|
689
778
|
}
|
|
779
|
+
if (shouldSkipHeuristicFile(file.relativePath, heuristicOptions)) {
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
690
782
|
|
|
691
783
|
const normalizedPath = normalizePathForMatch(file.relativePath);
|
|
692
784
|
const lowered = file.content.toLowerCase();
|
|
@@ -745,7 +837,7 @@ function extractPathDerivedTokens(pathHints) {
|
|
|
745
837
|
return tokens;
|
|
746
838
|
}
|
|
747
839
|
|
|
748
|
-
function findCodeEvidence(taskText, fileIndex, pathDerivedTokens = new Set()) {
|
|
840
|
+
function findCodeEvidence(taskText, fileIndex, pathDerivedTokens = new Set(), heuristicOptions = {}) {
|
|
749
841
|
const tokens = extractTaskEvidenceTokens(taskText, pathDerivedTokens);
|
|
750
842
|
if (tokens.length === 0) {
|
|
751
843
|
return [];
|
|
@@ -756,6 +848,9 @@ function findCodeEvidence(taskText, fileIndex, pathDerivedTokens = new Set()) {
|
|
|
756
848
|
if (file.generatedOutput || !CODE_EXTENSIONS.has(file.ext) || file.isTestFile) {
|
|
757
849
|
continue;
|
|
758
850
|
}
|
|
851
|
+
if (shouldSkipHeuristicFile(file.relativePath, heuristicOptions)) {
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
759
854
|
|
|
760
855
|
let score = 0;
|
|
761
856
|
const lowered = file.content.toLowerCase();
|
|
@@ -776,7 +871,7 @@ function findCodeEvidence(taskText, fileIndex, pathDerivedTokens = new Set()) {
|
|
|
776
871
|
}
|
|
777
872
|
}
|
|
778
873
|
|
|
779
|
-
return matches
|
|
874
|
+
return finalizeHeuristicMatches(matches, fileIndex);
|
|
780
875
|
}
|
|
781
876
|
|
|
782
877
|
function normalizeReferencedPath(rawPath) {
|
|
@@ -811,7 +906,7 @@ function extractTestReadReferences(content) {
|
|
|
811
906
|
return refs;
|
|
812
907
|
}
|
|
813
908
|
|
|
814
|
-
function findTestEvidence(taskText, fileIndex, referencedPaths = []) {
|
|
909
|
+
function findTestEvidence(taskText, fileIndex, referencedPaths = [], heuristicOptions = {}) {
|
|
815
910
|
const tokens = tokenize(taskText)
|
|
816
911
|
.filter((token) => token.length >= 3 && !GENERIC_TASK_TOKENS.has(token) && !token.endsWith('/'))
|
|
817
912
|
.slice(0, 8);
|
|
@@ -829,6 +924,9 @@ function findTestEvidence(taskText, fileIndex, referencedPaths = []) {
|
|
|
829
924
|
|
|
830
925
|
for (const file of fileIndex) {
|
|
831
926
|
if (file.generatedOutput || !file.isTestFile) continue;
|
|
927
|
+
if (shouldSkipHeuristicFile(file.relativePath, heuristicOptions)) {
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
832
930
|
|
|
833
931
|
// A test file counts as evidence only when it imports a module whose path contains
|
|
834
932
|
// one of the task's meaningful tokens. Content-keyword matching is intentionally absent:
|
|
@@ -865,7 +963,7 @@ function findTestEvidence(taskText, fileIndex, referencedPaths = []) {
|
|
|
865
963
|
}
|
|
866
964
|
}
|
|
867
965
|
|
|
868
|
-
return matches
|
|
966
|
+
return finalizeHeuristicMatches(matches, fileIndex);
|
|
869
967
|
}
|
|
870
968
|
|
|
871
969
|
function findArtifactEvidence(taskText, fileIndex) {
|
|
@@ -1460,24 +1558,104 @@ function testCoversEndpoint(testFile, route, context) {
|
|
|
1460
1558
|
return segments.length > 0 && segments.every((segment) => normalizedContent.includes(segment));
|
|
1461
1559
|
}
|
|
1462
1560
|
|
|
1561
|
+
function extractPathDomainTokens(relativePath) {
|
|
1562
|
+
return splitNormalizedPathSegments(relativePath)
|
|
1563
|
+
.flatMap((segment) => tokenize(segment))
|
|
1564
|
+
.filter((token) => token.length >= 2 && !GENERIC_TASK_TOKENS.has(token));
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
function collectRecipeLineContext(content, index) {
|
|
1568
|
+
const source = String(content || '');
|
|
1569
|
+
const lineStart = source.lastIndexOf('\n', index);
|
|
1570
|
+
const lineEnd = source.indexOf('\n', index);
|
|
1571
|
+
const previousLineStart = lineStart > 0 ? source.lastIndexOf('\n', lineStart - 1) : -1;
|
|
1572
|
+
const start = previousLineStart >= 0 ? previousLineStart + 1 : 0;
|
|
1573
|
+
const end = lineEnd >= 0 ? lineEnd : source.length;
|
|
1574
|
+
return source.slice(start, end);
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
function appendUniqueDiagnostic(diagnostics, diagnostic) {
|
|
1578
|
+
if (!diagnostic || !diagnostic.code) {
|
|
1579
|
+
return diagnostics;
|
|
1580
|
+
}
|
|
1581
|
+
if (diagnostics.some((item) => item.code === diagnostic.code)) {
|
|
1582
|
+
return diagnostics;
|
|
1583
|
+
}
|
|
1584
|
+
diagnostics.push(diagnostic);
|
|
1585
|
+
return diagnostics;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
function buildTaskRecipeCandidatePool(task, context) {
|
|
1589
|
+
const { paths: pathHints, externalPaths } = extractExplicitPaths(task.text);
|
|
1590
|
+
const standaloneFilenames = extractStandaloneFilenames(task.text);
|
|
1591
|
+
const symbolHints = extractSymbolHints(task.text);
|
|
1592
|
+
const pathDerivedTokens = extractPathDerivedTokens([...pathHints, ...externalPaths, ...standaloneFilenames]);
|
|
1593
|
+
const heuristicOptions = {
|
|
1594
|
+
explicitPaths: [...pathHints, ...externalPaths],
|
|
1595
|
+
explicitFilenames: standaloneFilenames
|
|
1596
|
+
};
|
|
1597
|
+
const candidatePaths = unionArrays(
|
|
1598
|
+
findFilesByPathHints(pathHints, context.pathHintResolver),
|
|
1599
|
+
findFilesBySymbols(symbolHints, context.fileIndex, heuristicOptions),
|
|
1600
|
+
findCodeEvidence(task.text, context.fileIndex, pathDerivedTokens, heuristicOptions),
|
|
1601
|
+
findFilesByTaskPathTokens(task.text, context.fileIndex, pathDerivedTokens, heuristicOptions)
|
|
1602
|
+
);
|
|
1603
|
+
const indexedFiles = new Map(context.fileIndex.map((file) => [file.relativePath, file]));
|
|
1604
|
+
return candidatePaths.map((relativePath) => indexedFiles.get(relativePath)).filter(Boolean);
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1463
1607
|
function findVerificationRecipe(task, context) {
|
|
1608
|
+
if ((Array.isArray(task.verifyLines) && task.verifyLines.length > 0) || (Array.isArray(task.evidenceLines) && task.evidenceLines.length > 0)) {
|
|
1609
|
+
return { recipe: null, specificityFailure: false, foundStaticSignal: false };
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1464
1612
|
const patterns = [
|
|
1465
1613
|
/disabled\s*=\s*\{[^}]+\}/i,
|
|
1466
1614
|
/<(?:dialog|alertdialog)\b[^>]*\bopen\s*=/i,
|
|
1467
1615
|
/abortcontroller|abortsignal\.timeout/i,
|
|
1468
1616
|
/router\.push\s*\(/i
|
|
1469
1617
|
];
|
|
1470
|
-
|
|
1618
|
+
const taskTokens = extractTaskEvidenceTokens(task.text, new Set(), 2);
|
|
1619
|
+
const candidateFiles = buildTaskRecipeCandidatePool(task, context);
|
|
1620
|
+
const candidates = [];
|
|
1621
|
+
let foundStaticSignal = false;
|
|
1622
|
+
|
|
1623
|
+
for (const file of candidateFiles) {
|
|
1471
1624
|
if (file.generatedOutput || file.isTestFile || !CODE_EXTENSIONS.has(file.ext)) continue;
|
|
1472
|
-
const
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1625
|
+
for (const pattern of patterns) {
|
|
1626
|
+
const match = pattern.exec(file.content);
|
|
1627
|
+
if (!match) {
|
|
1628
|
+
continue;
|
|
1629
|
+
}
|
|
1630
|
+
foundStaticSignal = true;
|
|
1631
|
+
const pathTokens = extractPathDomainTokens(file.relativePath);
|
|
1632
|
+
if (!pathTokens.some((token) => taskTokens.includes(token))) {
|
|
1633
|
+
continue;
|
|
1634
|
+
}
|
|
1635
|
+
const lineContext = collectRecipeLineContext(file.content, match.index);
|
|
1636
|
+
const contextTokens = extractTaskEvidenceTokens(`${match[0]} ${lineContext}`, new Set(), 2);
|
|
1637
|
+
if (!contextTokens.some((token) => taskTokens.includes(token))) {
|
|
1638
|
+
continue;
|
|
1639
|
+
}
|
|
1640
|
+
const line = file.content.slice(0, match.index).split(/\r?\n/).length;
|
|
1641
|
+
const command = context.config.validation && context.config.validation.recipeCommand
|
|
1642
|
+
? `; run ${String(context.config.validation.recipeCommand).replace('{testFile}', '<test-file>')}`
|
|
1643
|
+
: '';
|
|
1644
|
+
candidates.push(`${file.relativePath}:${line} inspect ${match[0].trim()}${command}`);
|
|
1645
|
+
break;
|
|
1646
|
+
}
|
|
1479
1647
|
}
|
|
1480
|
-
|
|
1648
|
+
|
|
1649
|
+
const uniqueCandidates = Array.from(new Set(candidates));
|
|
1650
|
+
if (uniqueCandidates.length === 1) {
|
|
1651
|
+
return { recipe: uniqueCandidates[0], specificityFailure: false, foundStaticSignal };
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
return {
|
|
1655
|
+
recipe: null,
|
|
1656
|
+
specificityFailure: foundStaticSignal || candidates.length > 0,
|
|
1657
|
+
foundStaticSignal
|
|
1658
|
+
};
|
|
1481
1659
|
}
|
|
1482
1660
|
|
|
1483
1661
|
function isBehavioralTask(taskText) {
|
|
@@ -1490,17 +1668,32 @@ function evaluateDeterministicVerification(task, context) {
|
|
|
1490
1668
|
if (!isBehavioralTask(task.text)) {
|
|
1491
1669
|
return { applicable: false, passed: false, reasons: [], diagnostics: [], recipe: null };
|
|
1492
1670
|
}
|
|
1493
|
-
const
|
|
1671
|
+
const recipeResult = findVerificationRecipe(task, context);
|
|
1672
|
+
const diagnostics = [];
|
|
1673
|
+
if (recipeResult.specificityFailure) {
|
|
1674
|
+
diagnostics.push({
|
|
1675
|
+
code: 'REQUIRES_HUMAN_EVIDENCE',
|
|
1676
|
+
severity: 'warning',
|
|
1677
|
+
message: 'behavioral task requires explicit human or test evidence'
|
|
1678
|
+
});
|
|
1679
|
+
diagnostics.push({
|
|
1680
|
+
code: 'NO_SPECIFIC_RECIPE',
|
|
1681
|
+
severity: 'warning',
|
|
1682
|
+
message: 'no task-specific verification recipe could be generated'
|
|
1683
|
+
});
|
|
1684
|
+
} else {
|
|
1685
|
+
diagnostics.push({
|
|
1686
|
+
code: recipeResult.recipe ? 'REQUIRES_HUMAN_EVIDENCE' : 'NO_STATIC_SIGNAL',
|
|
1687
|
+
severity: 'warning',
|
|
1688
|
+
message: recipeResult.recipe ? 'behavioral task requires explicit human or test evidence' : 'no static implementation signal was found for behavioral task'
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1494
1691
|
return {
|
|
1495
1692
|
applicable: false,
|
|
1496
1693
|
passed: false,
|
|
1497
1694
|
reasons: [],
|
|
1498
|
-
diagnostics
|
|
1499
|
-
|
|
1500
|
-
severity: 'warning',
|
|
1501
|
-
message: recipe ? 'behavioral task requires explicit human or test evidence' : 'no static implementation signal was found for behavioral task'
|
|
1502
|
-
}],
|
|
1503
|
-
recipe
|
|
1695
|
+
diagnostics,
|
|
1696
|
+
recipe: recipeResult.recipe
|
|
1504
1697
|
};
|
|
1505
1698
|
}
|
|
1506
1699
|
const reasons = [];
|
|
@@ -1576,11 +1769,11 @@ function evaluateDeterministicVerification(task, context) {
|
|
|
1576
1769
|
reasons,
|
|
1577
1770
|
diagnostics,
|
|
1578
1771
|
generatedTestEvidence,
|
|
1579
|
-
recipe:
|
|
1772
|
+
recipe: null
|
|
1580
1773
|
};
|
|
1581
1774
|
}
|
|
1582
1775
|
|
|
1583
|
-
function buildValidationContext(projectRoot, config, plugins) {
|
|
1776
|
+
function buildValidationContext(projectRoot, config, plugins, options = {}) {
|
|
1584
1777
|
const files = walkFiles(projectRoot);
|
|
1585
1778
|
const fileIndex = readFileIndex(projectRoot, files, config);
|
|
1586
1779
|
const testFrameworks = detectTestFrameworks(projectRoot, files);
|
|
@@ -1594,7 +1787,8 @@ function buildValidationContext(projectRoot, config, plugins) {
|
|
|
1594
1787
|
fileIndex,
|
|
1595
1788
|
pathHintResolver,
|
|
1596
1789
|
testFrameworks,
|
|
1597
|
-
testReportRecords: readTestReportRecords(projectRoot, config.validation)
|
|
1790
|
+
testReportRecords: readTestReportRecords(projectRoot, config.validation),
|
|
1791
|
+
strictValidation: options.strictValidation === true
|
|
1598
1792
|
};
|
|
1599
1793
|
}
|
|
1600
1794
|
|
|
@@ -1668,21 +1862,26 @@ function validateTask(task, context, config, plugins) {
|
|
|
1668
1862
|
const purePathHints = pathHints.filter((p) => !lineReferenceHints.has(p));
|
|
1669
1863
|
const standaloneFilenames = extractStandaloneFilenames(task.text);
|
|
1670
1864
|
const symbolHints = extractSymbolHints(task.text);
|
|
1865
|
+
const heuristicOptions = {
|
|
1866
|
+
explicitPaths: [...pathHints, ...externalPaths],
|
|
1867
|
+
explicitFilenames: standaloneFilenames
|
|
1868
|
+
};
|
|
1869
|
+
const strictValidation = context.strictValidation === true;
|
|
1671
1870
|
const authoritativeEvidence = evaluateAuthoritativeEvidence(task, context.pathHintResolver);
|
|
1672
1871
|
const deterministicVerification = evaluateDeterministicVerification(task, context);
|
|
1673
1872
|
const hasExplicitPendingItems = Array.isArray(task.explicitPendingItems) && task.explicitPendingItems.length > 0;
|
|
1674
1873
|
|
|
1675
1874
|
const filesFromPaths = findFilesByPathHints(pathHints, context.pathHintResolver);
|
|
1676
1875
|
const filesFromPurePathHints = findFilesByPathHints(purePathHints, context.pathHintResolver);
|
|
1677
|
-
const filesFromSymbols = findFilesBySymbols(symbolHints, context.fileIndex);
|
|
1876
|
+
const filesFromSymbols = findFilesBySymbols(symbolHints, context.fileIndex, heuristicOptions);
|
|
1678
1877
|
// Combine path hints AND standalone filenames for token exclusion so that tokens
|
|
1679
1878
|
// derived from any referenced filename (e.g. "roadmap-skill" from
|
|
1680
1879
|
// "roadmap-skill.config.json") are excluded from code evidence scoring.
|
|
1681
1880
|
const pathDerivedTokens = extractPathDerivedTokens([...pathHints, ...externalPaths, ...standaloneFilenames]);
|
|
1682
|
-
const filesFromCode = findCodeEvidence(task.text, context.fileIndex, pathDerivedTokens);
|
|
1683
|
-
const filesFromWeakPathTokens = findFilesByTaskPathTokens(task.text, context.fileIndex, pathDerivedTokens);
|
|
1684
|
-
const weakPathContentTokens = findWeakPathContentSpecificTokens(task.text, context.fileIndex, filesFromWeakPathTokens, pathDerivedTokens);
|
|
1685
|
-
const filesFromTests = findTestEvidence(task.text, context.fileIndex, [...pathHints, ...standaloneFilenames]);
|
|
1881
|
+
const filesFromCode = findCodeEvidence(task.text, context.fileIndex, pathDerivedTokens, heuristicOptions);
|
|
1882
|
+
const filesFromWeakPathTokens = findFilesByTaskPathTokens(task.text, context.fileIndex, pathDerivedTokens, heuristicOptions);
|
|
1883
|
+
const weakPathContentTokens = findWeakPathContentSpecificTokens(task.text, context.fileIndex, filesFromWeakPathTokens, pathDerivedTokens, heuristicOptions);
|
|
1884
|
+
const filesFromTests = findTestEvidence(task.text, context.fileIndex, [...pathHints, ...standaloneFilenames], heuristicOptions);
|
|
1686
1885
|
const { files: filesFromArtifacts, heuristicArtifacts } = findArtifactEvidence(task.text, context.fileIndex);
|
|
1687
1886
|
|
|
1688
1887
|
const structuralCheck = checkNamespaceStructuralEvidence(task.id, task.text, context.fileIndex);
|
|
@@ -1855,7 +2054,9 @@ function validateTask(task, context, config, plugins) {
|
|
|
1855
2054
|
// Heuristic proximity remains useful to locate candidates, but is never sufficient to
|
|
1856
2055
|
// complete an unchecked task. Completion requires human Evidence, a trusted rule, or a
|
|
1857
2056
|
// typed deterministic verifier.
|
|
1858
|
-
let passed =
|
|
2057
|
+
let passed = strictValidation
|
|
2058
|
+
? (authoritativeEvidence.passed || deterministicVerification.passed)
|
|
2059
|
+
: (authoritativeEvidence.passed || hasTrustedRuleEvidencePass || deterministicVerification.passed);
|
|
1859
2060
|
|
|
1860
2061
|
if (!task.checked && !authoritativeEvidence.passed && !hasTrustedRuleEvidencePass && !deterministicVerification.passed && hasHighConfidenceImplementationEvidence) {
|
|
1861
2062
|
uniqueReasons.push('implementation task requires deterministic Verify metadata or explicit Evidence to be marked complete');
|
|
@@ -1926,6 +2127,7 @@ function validateTask(task, context, config, plugins) {
|
|
|
1926
2127
|
const shouldPreserveCheckedTask =
|
|
1927
2128
|
task.checked &&
|
|
1928
2129
|
!passed &&
|
|
2130
|
+
!strictValidation &&
|
|
1929
2131
|
!authoritativeEvidence.active &&
|
|
1930
2132
|
symbolHints.length === 0 &&
|
|
1931
2133
|
negativeSignalMatches.length === 0 &&
|
|
@@ -1943,7 +2145,9 @@ function validateTask(task, context, config, plugins) {
|
|
|
1943
2145
|
// Used by auditValidation to flag implementation tasks that pass solely via documentation.
|
|
1944
2146
|
const evidenceIsDocOnly = !evidence.code && !evidence.test && evidence.artifact && !isDocTask(task.text);
|
|
1945
2147
|
|
|
1946
|
-
const finalPassed =
|
|
2148
|
+
const finalPassed = (!strictValidation && overrideResult)
|
|
2149
|
+
? (overrideResult.passed !== false && !hasExplicitPendingItems)
|
|
2150
|
+
: (passed && uniqueReasons.length === 0 && !hasExplicitPendingItems);
|
|
1947
2151
|
return {
|
|
1948
2152
|
taskId: task.id,
|
|
1949
2153
|
passed: finalPassed,
|
|
@@ -1992,6 +2196,33 @@ function validateTasks(tasks, context, config, plugins) {
|
|
|
1992
2196
|
}
|
|
1993
2197
|
}
|
|
1994
2198
|
|
|
2199
|
+
const recipeOwners = new Map();
|
|
2200
|
+
for (const [taskId, taskResult] of Object.entries(result)) {
|
|
2201
|
+
if (!taskResult.verificationRecipe) {
|
|
2202
|
+
continue;
|
|
2203
|
+
}
|
|
2204
|
+
const owners = recipeOwners.get(taskResult.verificationRecipe) || [];
|
|
2205
|
+
owners.push(taskId);
|
|
2206
|
+
recipeOwners.set(taskResult.verificationRecipe, owners);
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
for (const [recipe, owners] of recipeOwners.entries()) {
|
|
2210
|
+
if (owners.length < 2) {
|
|
2211
|
+
continue;
|
|
2212
|
+
}
|
|
2213
|
+
for (const taskId of owners) {
|
|
2214
|
+
if (!result[taskId] || result[taskId].verificationRecipe !== recipe) {
|
|
2215
|
+
continue;
|
|
2216
|
+
}
|
|
2217
|
+
result[taskId].verificationRecipe = null;
|
|
2218
|
+
appendUniqueDiagnostic(result[taskId].diagnostics, {
|
|
2219
|
+
code: 'NO_SPECIFIC_RECIPE',
|
|
2220
|
+
severity: 'warning',
|
|
2221
|
+
message: 'generated verification recipe was not unique to a single task'
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
|
|
1995
2226
|
return result;
|
|
1996
2227
|
}
|
|
1997
2228
|
|