roadmapsmith 0.9.22 → 0.9.24
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 +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/skills.json +11 -11
- package/src/classifier/index.js +18 -14
- package/src/generator/index.js +15 -3
- package/src/sync/index.js +93 -4
- package/src/validator/index.js +23 -13
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roadmapsmith",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.24",
|
|
4
4
|
"description": "One-command evidence-backed ROADMAP.md generator and sync tool for AI coding agents, with shared RoadmapSmith plugin skills for Codex and Claude.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "PapiScholz"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roadmapsmith",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.24",
|
|
4
4
|
"description": "One-command evidence-backed ROADMAP.md generator and sync tool for AI coding agents, with shared RoadmapSmith plugin skills for Codex and Claude.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "PapiScholz"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roadmapsmith",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.24",
|
|
4
4
|
"description": "One-command evidence-backed ROADMAP.md generator and sync tool for AI coding agents, with shared RoadmapSmith plugin skills for Codex and Claude.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
package/skills.json
CHANGED
|
@@ -29,67 +29,67 @@
|
|
|
29
29
|
"name": "roadmap",
|
|
30
30
|
"path": "skills/roadmap",
|
|
31
31
|
"description": "Native slash palette for RoadmapSmith commands and recommended entrypoints across supported hosts.",
|
|
32
|
-
"version": "0.9.
|
|
32
|
+
"version": "0.9.24"
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"name": "roadmap-zero",
|
|
36
36
|
"path": "skills/roadmap-zero",
|
|
37
37
|
"description": "Native slash entrypoint for the one-command Zero Mode CLI workflow.",
|
|
38
|
-
"version": "0.9.
|
|
38
|
+
"version": "0.9.24"
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
41
|
"name": "roadmap-maintain",
|
|
42
42
|
"path": "skills/roadmap-maintain",
|
|
43
43
|
"description": "Native slash entrypoint for the preserve-first generate + sync + audit flow.",
|
|
44
|
-
"version": "0.9.
|
|
44
|
+
"version": "0.9.24"
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
47
|
"name": "roadmap-status",
|
|
48
48
|
"path": "skills/roadmap-status",
|
|
49
49
|
"description": "Native slash readiness check grounded in roadmapsmith doctor JSON.",
|
|
50
|
-
"version": "0.9.
|
|
50
|
+
"version": "0.9.24"
|
|
51
51
|
},
|
|
52
52
|
{
|
|
53
53
|
"name": "roadmap-init",
|
|
54
54
|
"path": "skills/roadmap-init",
|
|
55
55
|
"description": "Native slash entrypoint for creating ROADMAP.md and AGENTS.md.",
|
|
56
|
-
"version": "0.9.
|
|
56
|
+
"version": "0.9.24"
|
|
57
57
|
},
|
|
58
58
|
{
|
|
59
59
|
"name": "roadmap-generate",
|
|
60
60
|
"path": "skills/roadmap-generate",
|
|
61
61
|
"description": "Native slash entrypoint for managed roadmap updates that require --full-regen before destructive replacement.",
|
|
62
|
-
"version": "0.9.
|
|
62
|
+
"version": "0.9.24"
|
|
63
63
|
},
|
|
64
64
|
{
|
|
65
65
|
"name": "roadmap-validate",
|
|
66
66
|
"path": "skills/roadmap-validate",
|
|
67
67
|
"description": "Native slash entrypoint for evidence-backed roadmap validation.",
|
|
68
|
-
"version": "0.9.
|
|
68
|
+
"version": "0.9.24"
|
|
69
69
|
},
|
|
70
70
|
{
|
|
71
71
|
"name": "roadmap-update",
|
|
72
72
|
"path": "skills/roadmap-update",
|
|
73
73
|
"description": "Native slash entrypoint for applying evidence-backed checklist sync.",
|
|
74
|
-
"version": "0.9.
|
|
74
|
+
"version": "0.9.24"
|
|
75
75
|
},
|
|
76
76
|
{
|
|
77
77
|
"name": "roadmap-sync",
|
|
78
78
|
"path": "skills/roadmap-sync",
|
|
79
79
|
"description": "Legacy namespaced root plus policy guidance for RoadmapSmith slash workflows.",
|
|
80
|
-
"version": "0.9.
|
|
80
|
+
"version": "0.9.24"
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
83
|
"name": "roadmap-audit",
|
|
84
84
|
"path": "skills/roadmap-audit",
|
|
85
85
|
"description": "Native slash entrypoint for the current sync-plus-audit workflow.",
|
|
86
|
-
"version": "0.9.
|
|
86
|
+
"version": "0.9.24"
|
|
87
87
|
},
|
|
88
88
|
{
|
|
89
89
|
"name": "roadmap-setup",
|
|
90
90
|
"path": "skills/roadmap-setup",
|
|
91
91
|
"description": "Native slash entrypoint for generating RoadmapSmith host integration files.",
|
|
92
|
-
"version": "0.9.
|
|
92
|
+
"version": "0.9.24"
|
|
93
93
|
}
|
|
94
94
|
]
|
|
95
95
|
}
|
package/src/classifier/index.js
CHANGED
|
@@ -21,6 +21,7 @@ const ELECTRON_CONFIGS = [
|
|
|
21
21
|
'forge.config.ts'
|
|
22
22
|
];
|
|
23
23
|
const LANDING_ROUTE_RE = /(?:^|\/)(?:contact|services|about|pricing|hero|cta|landing)(?:\/|\.)/i;
|
|
24
|
+
const FIXTURE_PATH_RE = /(^|\/)(?:test|tests)\/fixtures\//i;
|
|
24
25
|
|
|
25
26
|
function readPackageDeps(projectRoot) {
|
|
26
27
|
if (!projectRoot) return [];
|
|
@@ -52,6 +53,9 @@ function hasWorkspaces(projectRoot) {
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
function classifyProject({ projectRoot, files }) {
|
|
56
|
+
const candidateFiles = Array.isArray(files)
|
|
57
|
+
? files.filter((file) => !FIXTURE_PATH_RE.test(String(file || '')))
|
|
58
|
+
: [];
|
|
55
59
|
const signals = [];
|
|
56
60
|
|
|
57
61
|
if (hasWorkspaces(projectRoot)) {
|
|
@@ -59,8 +63,8 @@ function classifyProject({ projectRoot, files }) {
|
|
|
59
63
|
return { type: 'monorepo', confidence: 'high', signals };
|
|
60
64
|
}
|
|
61
65
|
|
|
62
|
-
const hasPy = hasFilename(
|
|
63
|
-
if (hasPy && !
|
|
66
|
+
const hasPy = hasFilename(candidateFiles, 'pyproject.toml') || hasFilename(candidateFiles, 'setup.py');
|
|
67
|
+
if (hasPy && !candidateFiles.some((f) => /\.[jt]sx?$/.test(f))) {
|
|
64
68
|
signals.push('pyproject.toml / setup.py, no JS files');
|
|
65
69
|
return { type: 'python-package', confidence: 'high', signals };
|
|
66
70
|
}
|
|
@@ -82,72 +86,72 @@ function classifyProject({ projectRoot, files }) {
|
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
for (const dir of WEB_DIRS) {
|
|
85
|
-
if (hasDir(
|
|
89
|
+
if (hasDir(candidateFiles, dir)) {
|
|
86
90
|
webScore += 2;
|
|
87
91
|
signals.push(`directory: ${dir.replace(/\/$/, '')}`);
|
|
88
92
|
}
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
for (const dir of ASSET_DIRS) {
|
|
92
|
-
if (hasDir(
|
|
96
|
+
if (hasDir(candidateFiles, dir)) {
|
|
93
97
|
webScore += 1;
|
|
94
98
|
signals.push(`directory: ${dir.replace(/\/$/, '')}`);
|
|
95
99
|
}
|
|
96
100
|
}
|
|
97
101
|
|
|
98
|
-
if (hasDir(
|
|
102
|
+
if (hasDir(candidateFiles, 'electron/')) {
|
|
99
103
|
electronScore += 3;
|
|
100
104
|
signals.push('directory: electron');
|
|
101
105
|
}
|
|
102
106
|
|
|
103
|
-
if (
|
|
107
|
+
if (candidateFiles.some((file) => /^electron\/.+\.(js|ts|cjs|mjs)$/.test(file))) {
|
|
104
108
|
electronScore += 2;
|
|
105
109
|
signals.push('electron main/preload sources');
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
for (const cfg of ELECTRON_CONFIGS) {
|
|
109
|
-
if (hasFilename(
|
|
113
|
+
if (hasFilename(candidateFiles, cfg)) {
|
|
110
114
|
electronScore += 2;
|
|
111
115
|
signals.push(`config: ${cfg}`);
|
|
112
116
|
}
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
for (const cfg of WEB_CONFIGS) {
|
|
116
|
-
if (hasFilename(
|
|
120
|
+
if (hasFilename(candidateFiles, cfg)) {
|
|
117
121
|
webScore += 3;
|
|
118
122
|
signals.push(`config: ${cfg}`);
|
|
119
123
|
}
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
for (const cfg of STYLE_CONFIGS) {
|
|
123
|
-
if (hasFilename(
|
|
127
|
+
if (hasFilename(candidateFiles, cfg)) {
|
|
124
128
|
webScore += 1;
|
|
125
129
|
signals.push(`config: ${cfg}`);
|
|
126
130
|
}
|
|
127
131
|
}
|
|
128
132
|
|
|
129
|
-
if (
|
|
133
|
+
if (candidateFiles.some((f) => /\.css$/.test(f))) {
|
|
130
134
|
webScore += 1;
|
|
131
135
|
signals.push('CSS files present');
|
|
132
136
|
}
|
|
133
137
|
|
|
134
|
-
const landingRoutes =
|
|
138
|
+
const landingRoutes = candidateFiles.filter((f) => LANDING_ROUTE_RE.test(f));
|
|
135
139
|
if (landingRoutes.length > 0) {
|
|
136
140
|
landingScore += landingRoutes.length * 2;
|
|
137
141
|
signals.push(`landing/service routes: ${landingRoutes.length}`);
|
|
138
142
|
}
|
|
139
143
|
|
|
140
|
-
if (hasFilename(
|
|
144
|
+
if (hasFilename(candidateFiles, 'favicon.ico') || hasFilename(candidateFiles, 'logo.png') || hasFilename(candidateFiles, 'logo.svg')) {
|
|
141
145
|
landingScore += 1;
|
|
142
146
|
signals.push('branding asset in public/');
|
|
143
147
|
}
|
|
144
148
|
|
|
145
|
-
if (webScore === 0 && (
|
|
149
|
+
if (webScore === 0 && (candidateFiles.some((f) => f.startsWith('bin/')) || hasFilename(candidateFiles, 'cli.js'))) {
|
|
146
150
|
signals.push('bin/ directory or cli.js');
|
|
147
151
|
return { type: 'cli-tool', confidence: 'medium', signals };
|
|
148
152
|
}
|
|
149
153
|
|
|
150
|
-
if (webScore === 0 && hasFilename(
|
|
154
|
+
if (webScore === 0 && hasFilename(candidateFiles, 'package.json')) {
|
|
151
155
|
signals.push('package.json, no web signals');
|
|
152
156
|
return { type: 'npm-package', confidence: 'low', signals };
|
|
153
157
|
}
|
package/src/generator/index.js
CHANGED
|
@@ -207,6 +207,10 @@ function renderAdditionTask(task) {
|
|
|
207
207
|
return `- [ ] ${task.text} <!-- rs:task=${task.id} -->`;
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
function isGenericPreserveModeCandidate(candidate) {
|
|
211
|
+
return candidate && ['default', 'classifier', 'todo-hint'].includes(candidate.source);
|
|
212
|
+
}
|
|
213
|
+
|
|
210
214
|
function buildManagedAdditionsLines(tasks, options = {}) {
|
|
211
215
|
const groups = groupByPhase(tasks);
|
|
212
216
|
const lines = [];
|
|
@@ -465,6 +469,10 @@ function insertPreserveModeTasks(existingContent, parsedRoadmap, tasks) {
|
|
|
465
469
|
return nextLines.join('\n');
|
|
466
470
|
}
|
|
467
471
|
|
|
472
|
+
function filterPreserveModeCandidates(candidates) {
|
|
473
|
+
return candidates.filter((candidate) => !isGenericPreserveModeCandidate(candidate));
|
|
474
|
+
}
|
|
475
|
+
|
|
468
476
|
function mergeWithExisting(candidates, existingTasks, options = {}) {
|
|
469
477
|
const matchedExistingIds = new Set();
|
|
470
478
|
const merged = [];
|
|
@@ -810,14 +818,18 @@ function generateRoadmapDocument(options) {
|
|
|
810
818
|
|
|
811
819
|
if (hasSubstantiveManagedBlock(existing) && preserveManagedBlock && !forceFullRegenerate) {
|
|
812
820
|
const unmatchedCandidates = allCandidates.filter((candidate) => {
|
|
813
|
-
return !findBestTaskMatch(candidate, existingManagedTasks, {
|
|
821
|
+
return !findBestTaskMatch(candidate, existingManagedTasks, {
|
|
822
|
+
allowFuzzy: true,
|
|
823
|
+
minScore: 0.72
|
|
824
|
+
});
|
|
814
825
|
});
|
|
826
|
+
const preserveModeCandidates = filterPreserveModeCandidates(unmatchedCandidates);
|
|
815
827
|
|
|
816
|
-
if (
|
|
828
|
+
if (preserveModeCandidates.length === 0) {
|
|
817
829
|
return existingContent;
|
|
818
830
|
}
|
|
819
831
|
|
|
820
|
-
return insertPreserveModeTasks(existingContent, existing,
|
|
832
|
+
return insertPreserveModeTasks(existingContent, existing, preserveModeCandidates);
|
|
821
833
|
}
|
|
822
834
|
|
|
823
835
|
if (hasSubstantiveManagedBlock(existing) && !forceFullRegenerate) {
|
package/src/sync/index.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
const { parseRoadmap } = require('../parser');
|
|
4
4
|
const { ensureTrailingNewline } = require('../utils');
|
|
5
5
|
|
|
6
|
+
const WARNING_REASON_PREFIX = 'attempted but validation failed:';
|
|
7
|
+
|
|
6
8
|
function setChecklistState(line, checked) {
|
|
7
9
|
return line.replace(/- \[( |x|X)\]/, `- [${checked ? 'x' : ' '}]`);
|
|
8
10
|
}
|
|
@@ -11,6 +13,95 @@ function formatWarning(indent, reason) {
|
|
|
11
13
|
return `${indent} - ⚠️ attempted but validation failed: ${reason}`;
|
|
12
14
|
}
|
|
13
15
|
|
|
16
|
+
function isWhitespaceCharacter(char) {
|
|
17
|
+
return char === ' ' || char === '\t' || char === '\n' || char === '\r' || char === '\f' || char === '\v';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function stripLeadingWarningMarker(value) {
|
|
21
|
+
let index = 0;
|
|
22
|
+
const source = String(value || '');
|
|
23
|
+
while (index < source.length && isWhitespaceCharacter(source[index])) {
|
|
24
|
+
index += 1;
|
|
25
|
+
}
|
|
26
|
+
if (source.slice(index, index + 2) === '⚠️') {
|
|
27
|
+
index += 2;
|
|
28
|
+
}
|
|
29
|
+
while (index < source.length && isWhitespaceCharacter(source[index])) {
|
|
30
|
+
index += 1;
|
|
31
|
+
}
|
|
32
|
+
return source.slice(index);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function splitWarningReasonSegments(value) {
|
|
36
|
+
const source = String(value || '');
|
|
37
|
+
const segments = [];
|
|
38
|
+
let current = '';
|
|
39
|
+
let index = 0;
|
|
40
|
+
|
|
41
|
+
while (index < source.length) {
|
|
42
|
+
const char = source[index];
|
|
43
|
+
if (char === ';') {
|
|
44
|
+
const trimmed = current.trim();
|
|
45
|
+
if (trimmed) {
|
|
46
|
+
segments.push(trimmed);
|
|
47
|
+
}
|
|
48
|
+
current = '';
|
|
49
|
+
index += 1;
|
|
50
|
+
while (index < source.length && isWhitespaceCharacter(source[index])) {
|
|
51
|
+
index += 1;
|
|
52
|
+
}
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
current += char;
|
|
57
|
+
index += 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const trimmed = current.trim();
|
|
61
|
+
if (trimmed) {
|
|
62
|
+
segments.push(trimmed);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return segments;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeWarningReason(reason) {
|
|
69
|
+
let normalized = String(reason || '').trim();
|
|
70
|
+
if (!normalized) {
|
|
71
|
+
return '';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
normalized = stripLeadingWarningMarker(normalized).trim();
|
|
75
|
+
const prefixIndex = normalized.indexOf(WARNING_REASON_PREFIX);
|
|
76
|
+
if (prefixIndex >= 0) {
|
|
77
|
+
normalized = normalized.slice(prefixIndex + WARNING_REASON_PREFIX.length).trim();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return normalized;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeWarningReasons(reasons) {
|
|
84
|
+
const normalized = [];
|
|
85
|
+
const seen = new Set();
|
|
86
|
+
for (const reason of Array.isArray(reasons) ? reasons : [reasons]) {
|
|
87
|
+
for (const chunk of splitWarningReasonSegments(reason)) {
|
|
88
|
+
const clean = normalizeWarningReason(chunk);
|
|
89
|
+
if (!clean || seen.has(clean)) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
seen.add(clean);
|
|
93
|
+
normalized.push(clean);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return normalized;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function shouldPreserveExistingWarning(existingReason, newReason) {
|
|
100
|
+
const cleanExisting = normalizeWarningReason(existingReason);
|
|
101
|
+
const cleanNew = normalizeWarningReason(newReason) || 'validation failed';
|
|
102
|
+
return cleanNew === 'validation failed' && cleanExisting && cleanExisting !== cleanNew;
|
|
103
|
+
}
|
|
104
|
+
|
|
14
105
|
function applySync(content, parsedTasks, results) {
|
|
15
106
|
const parsed = parseRoadmap(content);
|
|
16
107
|
const lines = [...parsed.lines];
|
|
@@ -30,7 +121,7 @@ function applySync(content, parsedTasks, results) {
|
|
|
30
121
|
|
|
31
122
|
lines[lineIndex] = setChecklistState(lines[lineIndex], result.passed);
|
|
32
123
|
|
|
33
|
-
const reason = result.reasons.join('; ');
|
|
124
|
+
const reason = normalizeWarningReasons(result.reasons).join('; ');
|
|
34
125
|
const warningText = formatWarning(task.indent || '', reason || 'validation failed');
|
|
35
126
|
const hasWarning = task.warningLineIndex != null;
|
|
36
127
|
const warningIndex = hasWarning ? task.warningLineIndex + offset : null;
|
|
@@ -47,9 +138,7 @@ function applySync(content, parsedTasks, results) {
|
|
|
47
138
|
if (warningIndex != null && warningIndex >= 0 && warningIndex < lines.length) {
|
|
48
139
|
const existingReason = lines[warningIndex].split('validation failed:')[1];
|
|
49
140
|
const newReason = reason || 'validation failed';
|
|
50
|
-
|
|
51
|
-
const existingIsMoreSpecific = existingReason && existingReason.trim().length > newReason.length;
|
|
52
|
-
if (!existingIsMoreSpecific) {
|
|
141
|
+
if (!shouldPreserveExistingWarning(existingReason, newReason)) {
|
|
53
142
|
lines[warningIndex] = warningText;
|
|
54
143
|
}
|
|
55
144
|
} else {
|
package/src/validator/index.js
CHANGED
|
@@ -373,6 +373,10 @@ function isDocTask(taskText) {
|
|
|
373
373
|
return /\b(add|create|write|update|init|initialize|introduce|setup|document)\b/.test(normalized);
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
+
function isImplementationTask(taskText) {
|
|
377
|
+
return !isDocTask(taskText) && (isCodeTask(taskText) || taskDescribesChange(taskText));
|
|
378
|
+
}
|
|
379
|
+
|
|
376
380
|
function findFilesByPathHints(pathHints, fileIndex) {
|
|
377
381
|
const matches = [];
|
|
378
382
|
for (const hint of pathHints) {
|
|
@@ -1177,7 +1181,13 @@ function validateTask(task, context, config, plugins) {
|
|
|
1177
1181
|
uniqueReasons = Array.isArray(overrideResult.reasons) ? Array.from(new Set(overrideResult.reasons)) : [];
|
|
1178
1182
|
}
|
|
1179
1183
|
|
|
1180
|
-
const
|
|
1184
|
+
const hasConcreteReferenceEvidence = filesFromPurePathHints.length > 0 || filesFromSymbols.length > 0;
|
|
1185
|
+
const attempted = authoritativeEvidence.active
|
|
1186
|
+
|| hasRuleGrantedEvidence
|
|
1187
|
+
|| evidence.code
|
|
1188
|
+
|| evidence.test
|
|
1189
|
+
|| evidence.artifact
|
|
1190
|
+
|| hasConcreteReferenceEvidence;
|
|
1181
1191
|
const { categories: strongEvidenceCategories } = countStrongEvidenceCategories(task.text, evidence);
|
|
1182
1192
|
const strongEvidenceCount = strongEvidenceCategories.length;
|
|
1183
1193
|
// Only pure path hints (not line-reference hints like file.ts:169) count as direct evidence.
|
|
@@ -1220,6 +1230,7 @@ function validateTask(task, context, config, plugins) {
|
|
|
1220
1230
|
// WHERE to implement, not that implementation is done. Unchecked tasks need authoritative
|
|
1221
1231
|
// evidence, artifact evidence, or strong code+test threshold to pass.
|
|
1222
1232
|
// Already-checked tasks with found path hints are preserved via shouldPreserveCheckedTask.
|
|
1233
|
+
const hasHighConfidenceImplementationEvidence = meetsStrongThreshold && evidence.code && evidence.test;
|
|
1223
1234
|
let passed = authoritativeEvidence.passed || hasArtifactTaskPass || hasTrustedRuleEvidencePass || meetsStrongThreshold;
|
|
1224
1235
|
|
|
1225
1236
|
if (!passed && !task.checked && hasDirectReferencePass) {
|
|
@@ -1234,30 +1245,29 @@ function validateTask(task, context, config, plugins) {
|
|
|
1234
1245
|
// human/agent judgment that the feature is incomplete.
|
|
1235
1246
|
if (task.warningText && !task.checked && passed && !authoritativeEvidence.passed) {
|
|
1236
1247
|
passed = false;
|
|
1237
|
-
uniqueReasons.
|
|
1238
|
-
|
|
1248
|
+
if (uniqueReasons.length === 0) {
|
|
1249
|
+
uniqueReasons.push('validation failed');
|
|
1250
|
+
}
|
|
1239
1251
|
}
|
|
1240
1252
|
if (negativeSignalMatches.length > 0) {
|
|
1241
1253
|
passed = false;
|
|
1242
1254
|
}
|
|
1243
1255
|
|
|
1244
|
-
//
|
|
1245
|
-
//
|
|
1246
|
-
// Requires either: an Evidence line (authoritativeEvidence.passed), high-confidence evidence
|
|
1247
|
-
// (code + test), grant-evidence from config (hasTrustedRuleEvidencePass), or canonical artifact
|
|
1248
|
-
// evidence (hasArtifactTaskPass — e.g. "Add SECURITY.md").
|
|
1256
|
+
// Unchecked implementation tasks need explicit evidence or high-confidence implementation
|
|
1257
|
+
// evidence. Weak token overlap, direct file references, or code-only matches are not enough.
|
|
1249
1258
|
if (
|
|
1250
1259
|
!task.checked &&
|
|
1251
1260
|
passed &&
|
|
1252
|
-
|
|
1261
|
+
isImplementationTask(task.text) &&
|
|
1253
1262
|
!authoritativeEvidence.passed &&
|
|
1254
1263
|
!hasTrustedRuleEvidencePass &&
|
|
1255
|
-
!hasArtifactTaskPass
|
|
1264
|
+
!hasArtifactTaskPass &&
|
|
1265
|
+
!hasHighConfidenceImplementationEvidence
|
|
1256
1266
|
) {
|
|
1257
1267
|
passed = false;
|
|
1258
|
-
const
|
|
1259
|
-
if (!uniqueReasons.includes(
|
|
1260
|
-
uniqueReasons.push(
|
|
1268
|
+
const implementationReason = 'implementation task requires Evidence line or high-confidence evidence (code + test) to be marked complete';
|
|
1269
|
+
if (!uniqueReasons.includes(implementationReason)) {
|
|
1270
|
+
uniqueReasons.push(implementationReason);
|
|
1261
1271
|
}
|
|
1262
1272
|
}
|
|
1263
1273
|
|