roadmapsmith 0.9.0 → 0.9.3
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 +5 -1
- package/package.json +1 -1
- package/src/parser/index.js +10 -7
- package/src/validator/index.js +247 -96
package/README.md
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/PapiScholz/roadmapsmith/main/assets/roadmapsmith-logo.png" alt="RoadmapSmith logo" width="180">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">RoadmapSmith</h1>
|
|
2
6
|
|
|
3
7
|
Production-grade roadmap generator and sync tool for agent-driven projects.
|
|
4
8
|
|
package/package.json
CHANGED
package/src/parser/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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-]+)
|
|
5
|
+
const TASK_LINE_RE = /^(\s*)- \[( |x|X)\] (.*?)(?:\s*<!--\s*rs:task=([a-z0-9-]+)([^>]*)-->)?\s*$/;
|
|
6
6
|
const WARNING_RE = /^\s*-\s+⚠️ attempted but validation failed:\s*(.+?)\s*$/;
|
|
7
7
|
const HEADING_RE = /^#{2,3}\s+(.*)$/;
|
|
8
8
|
|
|
@@ -29,7 +29,9 @@ function parseRoadmap(content) {
|
|
|
29
29
|
const indent = taskMatch[1] || '';
|
|
30
30
|
const checked = taskMatch[2].toLowerCase() === 'x';
|
|
31
31
|
const text = taskMatch[3].trim();
|
|
32
|
-
const markerId = taskMatch[4] || null;
|
|
32
|
+
const markerId = taskMatch[4] || null;
|
|
33
|
+
const markerFlags = taskMatch[5] || '';
|
|
34
|
+
const noTest = /\brs:no-test\b/i.test(markerFlags);
|
|
33
35
|
|
|
34
36
|
let warningLineIndex = null;
|
|
35
37
|
let warningText = null;
|
|
@@ -49,11 +51,12 @@ function parseRoadmap(content) {
|
|
|
49
51
|
checked,
|
|
50
52
|
lineIndex: index,
|
|
51
53
|
warningLineIndex,
|
|
52
|
-
warningText,
|
|
53
|
-
markerId,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
warningText,
|
|
55
|
+
markerId,
|
|
56
|
+
noTest,
|
|
57
|
+
indent,
|
|
58
|
+
section
|
|
59
|
+
});
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
return {
|
package/src/validator/index.js
CHANGED
|
@@ -8,9 +8,11 @@ const { escapeRegExp, tokenize } = require('../utils');
|
|
|
8
8
|
|
|
9
9
|
const CONFIDENCE_RANK = { low: 0, medium: 1, high: 2 };
|
|
10
10
|
|
|
11
|
-
const CODE_EXTENSIONS = new Set([
|
|
12
|
-
'.js', '.cjs', '.mjs', '.ts', '.tsx', '.jsx', '.py', '.go', '.rs', '.java', '.kt', '.swift', '.rb', '.php', '.cs'
|
|
13
|
-
]);
|
|
11
|
+
const CODE_EXTENSIONS = new Set([
|
|
12
|
+
'.js', '.cjs', '.mjs', '.ts', '.tsx', '.jsx', '.py', '.go', '.rs', '.java', '.kt', '.swift', '.rb', '.php', '.cs'
|
|
13
|
+
]);
|
|
14
|
+
const TRANSLATION_DIR_SEGMENTS = ['locale', 'locales', 'i18n', 'translations'];
|
|
15
|
+
const DEFAULT_EXCLUDED_PATH_PREFIXES = ['.claude/', '.agent/', 'roadmap-skill/'];
|
|
14
16
|
|
|
15
17
|
// "docs" omitted from DOC_HINTS — it is a path prefix in scan tasks, not a doc-authoring keyword.
|
|
16
18
|
const DOC_HINTS = ['readme', 'changelog', 'documentation', 'spec', 'diagram', 'runbook'];
|
|
@@ -76,18 +78,69 @@ const NAMESPACE_STRUCTURAL_PATTERNS = {
|
|
|
76
78
|
// Test fixture directories contain synthetic code created to drive test scenarios,
|
|
77
79
|
// not real implementations. Including them pollutes the evidence pool with vocabulary
|
|
78
80
|
// that was deliberately seeded for testing purposes (e.g. namespace-vocab fixtures).
|
|
79
|
-
function isFixturePath(relativePath) {
|
|
80
|
-
return /(?:^|[/\\])fixtures[/\\]/.test(relativePath);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
81
|
+
function isFixturePath(relativePath) {
|
|
82
|
+
return /(?:^|[/\\])fixtures[/\\]/.test(relativePath);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function normalizePathForMatch(rawPath) {
|
|
86
|
+
return String(rawPath || '').replace(/\\/g, '/').toLowerCase();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function shouldExcludeByDefaultPath(relativePath, config) {
|
|
90
|
+
const normalized = normalizePathForMatch(relativePath);
|
|
91
|
+
if (DEFAULT_EXCLUDED_PATH_PREFIXES.some((prefix) => normalized.startsWith(prefix))) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const configuredSkillsDir = config && typeof config.skillsDir === 'string'
|
|
96
|
+
? normalizePathForMatch(config.skillsDir).replace(/^\.?\//, '')
|
|
97
|
+
: '';
|
|
98
|
+
if (configuredSkillsDir && (normalized === configuredSkillsDir || normalized.startsWith(configuredSkillsDir + '/'))) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isTranslationPath(relativePath) {
|
|
105
|
+
const normalized = normalizePathForMatch(relativePath);
|
|
106
|
+
const segments = normalized.split('/').filter(Boolean);
|
|
107
|
+
return segments.some((segment) => TRANSLATION_DIR_SEGMENTS.includes(segment));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function looksLikeTranslationJson(content) {
|
|
111
|
+
let parsed;
|
|
112
|
+
try {
|
|
113
|
+
parsed = JSON.parse(content);
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const values = Object.values(parsed);
|
|
123
|
+
if (values.length === 0) return false;
|
|
124
|
+
const stringValues = values.filter((value) => typeof value === 'string').length;
|
|
125
|
+
return stringValues / values.length >= 0.8;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isMostlyUiStrings(content) {
|
|
129
|
+
const lines = String(content).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
130
|
+
if (lines.length < 5) return false;
|
|
131
|
+
const stringLikeLines = lines.filter((line) => /^['"`][^'"`]{1,200}['"`],?$/.test(line) || /^[A-Za-z0-9_.-]+\s*:\s*['"`][^'"`]{1,200}['"`],?$/.test(line)).length;
|
|
132
|
+
return stringLikeLines / lines.length > 0.8;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function readFileIndex(projectRoot, files, config) {
|
|
136
|
+
const index = [];
|
|
137
|
+
for (const relativePath of files) {
|
|
138
|
+
if (SELF_REFERENTIAL_FILES.has(relativePath)) continue;
|
|
139
|
+
if (isFixturePath(relativePath)) continue;
|
|
140
|
+
if (shouldExcludeByDefaultPath(relativePath, config)) continue;
|
|
141
|
+
|
|
142
|
+
const absolutePath = path.resolve(projectRoot, relativePath);
|
|
143
|
+
const ext = path.extname(relativePath).toLowerCase();
|
|
91
144
|
let content = '';
|
|
92
145
|
try {
|
|
93
146
|
const buffer = fs.readFileSync(absolutePath);
|
|
@@ -95,12 +148,16 @@ function readFileIndex(projectRoot, files) {
|
|
|
95
148
|
continue;
|
|
96
149
|
}
|
|
97
150
|
content = buffer.toString('utf8');
|
|
98
|
-
} catch {
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
151
|
+
} catch {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (isTranslationPath(relativePath)) continue;
|
|
156
|
+
if (ext === '.json' && looksLikeTranslationJson(content)) continue;
|
|
157
|
+
if (isMostlyUiStrings(content)) continue;
|
|
158
|
+
|
|
159
|
+
index.push({
|
|
160
|
+
relativePath,
|
|
104
161
|
absolutePath,
|
|
105
162
|
ext,
|
|
106
163
|
content,
|
|
@@ -240,7 +297,7 @@ function findFilesByPathHints(pathHints, fileIndex) {
|
|
|
240
297
|
return Array.from(new Set(matches)).sort((left, right) => left.localeCompare(right));
|
|
241
298
|
}
|
|
242
299
|
|
|
243
|
-
function findFilesBySymbols(symbolHints, fileIndex) {
|
|
300
|
+
function findFilesBySymbols(symbolHints, fileIndex) {
|
|
244
301
|
const matches = new Set();
|
|
245
302
|
for (const symbol of symbolHints) {
|
|
246
303
|
const regex = new RegExp(`\\b${escapeRegExp(symbol)}\\b`, 'i');
|
|
@@ -254,7 +311,47 @@ function findFilesBySymbols(symbolHints, fileIndex) {
|
|
|
254
311
|
}
|
|
255
312
|
}
|
|
256
313
|
return Array.from(matches).sort((left, right) => left.localeCompare(right));
|
|
257
|
-
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function findFilesByTaskPathTokens(taskText, fileIndex, pathDerivedTokens = new Set()) {
|
|
317
|
+
const tokens = tokenize(taskText)
|
|
318
|
+
.filter((token) => token.length >= 6 && !GENERIC_TASK_TOKENS.has(token) && !pathDerivedTokens.has(token))
|
|
319
|
+
.slice(0, 10);
|
|
320
|
+
if (tokens.length === 0) return [];
|
|
321
|
+
|
|
322
|
+
const matches = new Set();
|
|
323
|
+
for (const file of fileIndex) {
|
|
324
|
+
const pathSegments = normalizePathForMatch(file.relativePath).split('/').filter(Boolean);
|
|
325
|
+
for (const token of tokens) {
|
|
326
|
+
if (pathSegments.some((segment) => segment === token || segment.includes(token))) {
|
|
327
|
+
matches.add(file.relativePath);
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (matches.size >= 20) break;
|
|
332
|
+
}
|
|
333
|
+
return Array.from(matches).sort((left, right) => left.localeCompare(right));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function mergeRuleEvidence(baseEvidence, ruleEvidence) {
|
|
337
|
+
if (!ruleEvidence || typeof ruleEvidence !== 'object') return baseEvidence;
|
|
338
|
+
const merged = { ...baseEvidence };
|
|
339
|
+
|
|
340
|
+
for (const [key, value] of Object.entries(ruleEvidence)) {
|
|
341
|
+
if (Array.isArray(value)) {
|
|
342
|
+
const existing = Array.isArray(merged[key]) ? merged[key] : [];
|
|
343
|
+
merged[key] = Array.from(new Set([...existing, ...value]));
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (typeof value === 'boolean') {
|
|
347
|
+
merged[key] = Boolean(merged[key]) || value;
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
merged[key] = value;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return merged;
|
|
354
|
+
}
|
|
258
355
|
|
|
259
356
|
// Tokens extracted from a referenced file path (e.g. "roadmap-skill" from
|
|
260
357
|
// "roadmap-skill.config.json") must not be reused as code evidence signals.
|
|
@@ -484,29 +581,30 @@ function checkNamespaceStructuralEvidence(taskId, taskText, fileIndex) {
|
|
|
484
581
|
};
|
|
485
582
|
}
|
|
486
583
|
|
|
487
|
-
function evaluateRule(rule, task, context) {
|
|
488
|
-
if (!rule) {
|
|
489
|
-
return { passed: true, reasons: [], evidence: {} };
|
|
490
|
-
}
|
|
584
|
+
function evaluateRule(rule, task, context) {
|
|
585
|
+
if (!rule) {
|
|
586
|
+
return { passed: true, reasons: [], evidence: {}, overrideResult: false };
|
|
587
|
+
}
|
|
491
588
|
|
|
492
589
|
if (rule.when) {
|
|
493
590
|
const regexp = new RegExp(rule.when, 'i');
|
|
494
|
-
if (!regexp.test(task.text)) {
|
|
495
|
-
return { passed: true, reasons: [], evidence: {} };
|
|
496
|
-
}
|
|
497
|
-
}
|
|
591
|
+
if (!regexp.test(task.text)) {
|
|
592
|
+
return { passed: true, reasons: [], evidence: {}, overrideResult: false };
|
|
593
|
+
}
|
|
594
|
+
}
|
|
498
595
|
|
|
499
596
|
if (typeof rule.check === 'function') {
|
|
500
597
|
const custom = rule.check(task, context);
|
|
501
|
-
if (!custom) {
|
|
502
|
-
return { passed: true, reasons: [], evidence: {} };
|
|
503
|
-
}
|
|
504
|
-
return {
|
|
505
|
-
passed: custom.passed !== false,
|
|
506
|
-
reasons: Array.isArray(custom.reasons) ? custom.reasons : [],
|
|
507
|
-
evidence: custom.evidence || {}
|
|
508
|
-
|
|
509
|
-
|
|
598
|
+
if (!custom) {
|
|
599
|
+
return { passed: true, reasons: [], evidence: {}, overrideResult: false };
|
|
600
|
+
}
|
|
601
|
+
return {
|
|
602
|
+
passed: custom.passed !== false,
|
|
603
|
+
reasons: Array.isArray(custom.reasons) ? custom.reasons : [],
|
|
604
|
+
evidence: custom.evidence || {},
|
|
605
|
+
overrideResult: rule.overrideResult === true || custom.overrideResult === true
|
|
606
|
+
};
|
|
607
|
+
}
|
|
510
608
|
|
|
511
609
|
const reasons = [];
|
|
512
610
|
const evidence = {};
|
|
@@ -539,20 +637,46 @@ function evaluateRule(rule, task, context) {
|
|
|
539
637
|
}
|
|
540
638
|
}
|
|
541
639
|
|
|
542
|
-
if (rule.type === 'test' && context.testFrameworks.length === 0) {
|
|
543
|
-
reasons.push(rule.message || 'test framework not detected');
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
640
|
+
if (rule.type === 'test' && context.testFrameworks.length === 0) {
|
|
641
|
+
reasons.push(rule.message || 'test framework not detected');
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (rule.type === 'grant-evidence') {
|
|
645
|
+
const evidenceTargets = Array.isArray(rule.evidence) ? rule.evidence : [rule.evidence].filter(Boolean);
|
|
646
|
+
for (const key of evidenceTargets) {
|
|
647
|
+
evidence[key] = true;
|
|
648
|
+
}
|
|
649
|
+
if (Array.isArray(rule.files) && rule.files.length > 0) {
|
|
650
|
+
evidence.files = rule.files;
|
|
651
|
+
}
|
|
652
|
+
if (Array.isArray(rule.symbols) && rule.symbols.length > 0) {
|
|
653
|
+
evidence.symbols = rule.symbols;
|
|
654
|
+
}
|
|
655
|
+
if (Array.isArray(rule.codeFiles) && rule.codeFiles.length > 0) {
|
|
656
|
+
evidence.codeFiles = rule.codeFiles;
|
|
657
|
+
evidence.code = true;
|
|
658
|
+
}
|
|
659
|
+
if (Array.isArray(rule.testFiles) && rule.testFiles.length > 0) {
|
|
660
|
+
evidence.testFiles = rule.testFiles;
|
|
661
|
+
evidence.test = true;
|
|
662
|
+
}
|
|
663
|
+
if (Array.isArray(rule.artifactFiles) && rule.artifactFiles.length > 0) {
|
|
664
|
+
evidence.artifactFiles = rule.artifactFiles;
|
|
665
|
+
evidence.artifact = true;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return {
|
|
670
|
+
passed: reasons.length === 0,
|
|
671
|
+
reasons,
|
|
672
|
+
evidence,
|
|
673
|
+
overrideResult: rule.overrideResult === true
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function buildValidationContext(projectRoot, config, plugins) {
|
|
678
|
+
const files = walkFiles(projectRoot);
|
|
679
|
+
const fileIndex = readFileIndex(projectRoot, files, config);
|
|
556
680
|
const testFrameworks = detectTestFrameworks(projectRoot, files);
|
|
557
681
|
|
|
558
682
|
return {
|
|
@@ -575,10 +699,11 @@ function validateTask(task, context, config, plugins) {
|
|
|
575
699
|
// Combine path hints AND standalone filenames for token exclusion so that tokens
|
|
576
700
|
// derived from any referenced filename (e.g. "roadmap-skill" from
|
|
577
701
|
// "roadmap-skill.config.json") are excluded from code evidence scoring.
|
|
578
|
-
const pathDerivedTokens = extractPathDerivedTokens([...pathHints, ...standaloneFilenames]);
|
|
579
|
-
const filesFromCode = findCodeEvidence(task.text, context.fileIndex, pathDerivedTokens);
|
|
580
|
-
const
|
|
581
|
-
const
|
|
702
|
+
const pathDerivedTokens = extractPathDerivedTokens([...pathHints, ...standaloneFilenames]);
|
|
703
|
+
const filesFromCode = findCodeEvidence(task.text, context.fileIndex, pathDerivedTokens);
|
|
704
|
+
const filesFromWeakPathTokens = findFilesByTaskPathTokens(task.text, context.fileIndex, pathDerivedTokens);
|
|
705
|
+
const filesFromTests = findTestEvidence(task.text, context.fileIndex);
|
|
706
|
+
const { files: filesFromArtifacts, heuristicArtifacts } = findArtifactEvidence(task.text, context.fileIndex);
|
|
582
707
|
|
|
583
708
|
const structuralCheck = checkNamespaceStructuralEvidence(task.id, task.text, context.fileIndex);
|
|
584
709
|
|
|
@@ -587,9 +712,10 @@ function validateTask(task, context, config, plugins) {
|
|
|
587
712
|
test: filesFromTests.length > 0,
|
|
588
713
|
artifact: filesFromArtifacts.length > 0,
|
|
589
714
|
files: filesFromPaths,
|
|
590
|
-
symbols: filesFromSymbols,
|
|
591
|
-
codeFiles: filesFromCode,
|
|
592
|
-
|
|
715
|
+
symbols: filesFromSymbols,
|
|
716
|
+
codeFiles: filesFromCode,
|
|
717
|
+
weakPathFiles: filesFromWeakPathTokens,
|
|
718
|
+
testFiles: filesFromTests,
|
|
593
719
|
artifactFiles: filesFromArtifacts,
|
|
594
720
|
heuristicArtifacts,
|
|
595
721
|
structuralEvidence: structuralCheck.applicable ? structuralCheck.passed : null,
|
|
@@ -610,49 +736,74 @@ function validateTask(task, context, config, plugins) {
|
|
|
610
736
|
reasons.push(structuralCheck.reason || `no structural evidence for namespace "${extractTaskNamespace(task.id)}"`);
|
|
611
737
|
}
|
|
612
738
|
|
|
613
|
-
const hasEvidence = evidence.code || evidence.test || evidence.artifact || evidence.files.length > 0;
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
739
|
+
const hasEvidence = evidence.code || evidence.test || evidence.artifact || evidence.files.length > 0;
|
|
740
|
+
const hasWeakEvidence = filesFromWeakPathTokens.length > 0;
|
|
741
|
+
if (!hasEvidence && !hasWeakEvidence && !structuralCheck.applicable) {
|
|
742
|
+
reasons.push('no code, test, or artifact evidence found');
|
|
743
|
+
} else if (!hasEvidence && !hasWeakEvidence && structuralCheck.applicable && structuralCheck.passed) {
|
|
744
|
+
reasons.push('no code, test, or artifact evidence found');
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const requiresTest = !task.noTest && context.testFrameworks.length > 0 && isCodeTask(task.text) && !isDocTask(task.text);
|
|
748
|
+
if (requiresTest && !evidence.test) {
|
|
749
|
+
reasons.push('missing test evidence');
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const configuredRules = Array.isArray(config.validators) ? config.validators : [];
|
|
753
|
+
const pluginRules = collectPluginContributions(plugins || [], 'registerValidators', context);
|
|
754
|
+
let overrideResult = null;
|
|
755
|
+
for (const rule of [...configuredRules, ...pluginRules]) {
|
|
756
|
+
const ruleResult = evaluateRule(rule, task, context);
|
|
757
|
+
if (ruleResult.evidence && Object.keys(ruleResult.evidence).length > 0) {
|
|
758
|
+
Object.assign(evidence, mergeRuleEvidence(evidence, ruleResult.evidence));
|
|
759
|
+
}
|
|
760
|
+
if (!ruleResult.passed) {
|
|
761
|
+
reasons.push(...ruleResult.reasons);
|
|
762
|
+
}
|
|
763
|
+
if (ruleResult.overrideResult) {
|
|
764
|
+
overrideResult = ruleResult;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const hasStrongEvidence = evidence.code || evidence.test || evidence.artifact || evidence.files.length > 0;
|
|
769
|
+
if (hasStrongEvidence) {
|
|
770
|
+
const noEvidenceReason = 'no code, test, or artifact evidence found';
|
|
771
|
+
const idx = reasons.indexOf(noEvidenceReason);
|
|
772
|
+
if (idx >= 0) {
|
|
773
|
+
reasons.splice(idx, 1);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
let uniqueReasons = Array.from(new Set(reasons));
|
|
778
|
+
|
|
779
|
+
if (overrideResult) {
|
|
780
|
+
uniqueReasons = Array.isArray(overrideResult.reasons) ? Array.from(new Set(overrideResult.reasons)) : [];
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const attempted = hasStrongEvidence || hasWeakEvidence || pathHints.length > 0 || symbolHints.length > 0;
|
|
784
|
+
|
|
785
|
+
const evidenceCount = [evidence.code, evidence.test, evidence.artifact].filter(Boolean).length;
|
|
786
|
+
let confidence = evidenceCount >= 2 ? 'high' : evidenceCount === 1 ? 'medium' : 'low';
|
|
787
|
+
if (confidence === 'low' && hasWeakEvidence) {
|
|
788
|
+
confidence = 'low';
|
|
789
|
+
}
|
|
639
790
|
|
|
640
791
|
// True when the only passing evidence is artifact/doc files and the task is not a doc task.
|
|
641
792
|
// Used by auditValidation to flag implementation tasks that pass solely via documentation.
|
|
642
793
|
const evidenceIsDocOnly = !evidence.code && !evidence.test && evidence.artifact && !isDocTask(task.text);
|
|
643
794
|
|
|
644
|
-
return {
|
|
645
|
-
taskId: task.id,
|
|
646
|
-
passed: uniqueReasons.length === 0,
|
|
647
|
-
confidence,
|
|
648
|
-
reasons: uniqueReasons,
|
|
795
|
+
return {
|
|
796
|
+
taskId: task.id,
|
|
797
|
+
passed: overrideResult ? overrideResult.passed !== false : uniqueReasons.length === 0,
|
|
798
|
+
confidence,
|
|
799
|
+
reasons: uniqueReasons,
|
|
649
800
|
evidence,
|
|
650
801
|
evidenceIsDocOnly,
|
|
651
802
|
requiresTest,
|
|
652
|
-
hasEvidence,
|
|
653
|
-
attempted
|
|
654
|
-
};
|
|
655
|
-
}
|
|
803
|
+
hasEvidence: hasStrongEvidence || hasWeakEvidence,
|
|
804
|
+
attempted
|
|
805
|
+
};
|
|
806
|
+
}
|
|
656
807
|
|
|
657
808
|
function validateTasks(tasks, context, config, plugins) {
|
|
658
809
|
const result = {};
|