roadmapsmith 0.9.28 → 0.9.29
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/README.md +16 -4
- package/bin/cli.js +3 -1
- package/package.json +1 -1
- package/skills/roadmap-sync/SKILL.md +1 -1
- package/skills.json +11 -11
- package/src/config.js +7 -2
- package/src/io.js +2 -0
- package/src/parser/index.js +50 -0
- package/src/sync/index.js +48 -4
- package/src/validator/index.js +366 -9
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roadmapsmith",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.29",
|
|
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 plus the roadmapsmith status readiness surface.",
|
|
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.29",
|
|
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 plus the roadmapsmith status readiness surface.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "PapiScholz"
|
package/README.md
CHANGED
|
@@ -92,6 +92,19 @@ roadmapsmith maintain # existing repo
|
|
|
92
92
|
|
|
93
93
|
Use the lower-level commands only when you want manual control over generation, validation, or sync.
|
|
94
94
|
|
|
95
|
+
## Deterministic completion
|
|
96
|
+
|
|
97
|
+
For unchecked implementation tasks, related files and matching test names are candidate signals only. `maintain` completes a task only from explicit `Evidence:` or typed child verification metadata:
|
|
98
|
+
|
|
99
|
+
```markdown
|
|
100
|
+
- [ ] Enable ESLint builds
|
|
101
|
+
- Verify: kind=property; file=next.config.js; key=ignoreDuringBuilds; equals=false
|
|
102
|
+
- [ ] Prevent double submit
|
|
103
|
+
- Verify: kind=behavior; source=src/login.tsx; test=src/__tests__/login.test.tsx; case=disables submit; trigger=fireEvent.click; assertion=toBeDisabled
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`contains`, `property`, and `endpoints` are static checks. A behavioral check also requires a fresh `validation.testReports` entry using `format: "vitest-json"`; RoadmapSmith reads reports but never executes tests. Missing proof produces a `Verification recipe:` and a warning rather than a checked box. A fresh passing report is persisted as `Test evidence: file=<test>; case=<case>; status=PASS; verifiedAt=<ISO timestamp>` and becomes stale when it predates the referenced source or test file.
|
|
107
|
+
|
|
95
108
|
## Host Support Today
|
|
96
109
|
|
|
97
110
|
| Host | Current support |
|
|
@@ -181,14 +194,13 @@ The repo does not remove user-global skills automatically. Use the `doctor` outp
|
|
|
181
194
|
- Uses stable task IDs: `<!-- rs:task=<slug> -->`.
|
|
182
195
|
- Sync marks `[x]` only when validation passes.
|
|
183
196
|
- `sync --audit` currently runs sync and then prints a mismatch summary; it is not yet a dedicated read-only audit mode.
|
|
184
|
-
-
|
|
185
|
-
|
|
186
|
-
- test evidence required for code tasks when test frameworks are detected.
|
|
197
|
+
- For unchecked implementation tasks, heuristic code/test/path matches are candidates only. Completion requires explicit `Evidence:`, a trusted validator rule, or typed deterministic `Verify:` metadata.
|
|
198
|
+
- `Verify:` supports `contains`, `property`, `endpoints`, and `behavior`; behavior additionally requires a fresh configured Vitest JSON report or a fresh `Test evidence:` annotation.
|
|
187
199
|
- Validation failures in sync write warning lines:
|
|
188
200
|
- `- ⚠️ attempted but validation failed: <reason>` when there is concrete attempt evidence
|
|
189
201
|
- `- ⚠️ no implementation evidence found yet: <reason>` when there is not
|
|
190
202
|
- Preserves unmanaged markdown content by updating only the managed roadmap block.
|
|
191
|
-
- `validate` emits structured diagnostics in human and JSON output
|
|
203
|
+
- `validate` emits structured diagnostics in human and JSON output, including `FAIL:NOT_IMPLEMENTED`, `FAIL:NO_TEST`, `FAIL:MISSING_REFERENCE`, `FAIL:WRONG_VALUE`, `FAIL:PARTIAL`, `WARN:STALE_EVIDENCE`, `WARN:REQUIRES_HUMAN_EVIDENCE`, `WARN:NO_STATIC_SIGNAL`, `WARN:HAS_EXPLICIT_PENDING`, and `WARN:STALE_TEST_REPORT`.
|
|
192
204
|
- `update --task <id> --evidence <text>` writes only when the evidence validates at high confidence; otherwise the roadmap is unchanged.
|
|
193
205
|
|
|
194
206
|
## Defaults
|
package/bin/cli.js
CHANGED
|
@@ -48,7 +48,9 @@ function formatResultLine(task, result) {
|
|
|
48
48
|
const diagnostics = Array.isArray(result.diagnostics) ? result.diagnostics : [];
|
|
49
49
|
const primaryError = diagnostics.find((item) => item.severity === 'error');
|
|
50
50
|
const warnings = diagnostics.filter((item) => item.severity === 'warning');
|
|
51
|
-
const status = primaryError
|
|
51
|
+
const status = primaryError
|
|
52
|
+
? `FAIL:${primaryError.code}`
|
|
53
|
+
: (result.passed ? 'PASS' : (warnings[0] ? `WARN:${warnings[0].code}` : 'FAIL'));
|
|
52
54
|
const parts = [...result.reasons];
|
|
53
55
|
warnings.forEach((item) => parts.push(`WARN:${item.code} ${item.message}`));
|
|
54
56
|
const reason = parts.length > 0 ? ` :: ${parts.join('; ')}` : '';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roadmapsmith",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.29",
|
|
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 plus the roadmapsmith status readiness surface.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -17,4 +17,4 @@ Use this skill only when the host exposes or the user explicitly invokes `/roadm
|
|
|
17
17
|
- `/roadmap-status`
|
|
18
18
|
- `/roadmap-init`, `/roadmap-generate`, `/roadmap-validate`, `/roadmap-update`, `/roadmap-audit`, and `/roadmap-setup`
|
|
19
19
|
3. When the user explicitly invokes `/roadmap-sync <action>`, route to the matching CLI-backed action without changing semantics and mention the migration path to `/roadmap <action>` or the direct `/roadmap-*` command.
|
|
20
|
-
4. Preserve the operating rules for evidence-backed roadmap maintenance and checklist synchronization.
|
|
20
|
+
4. Preserve the operating rules for evidence-backed roadmap maintenance and checklist synchronization: heuristic file/token matches may diagnose candidates, but only explicit `Evidence:` or typed `Verify:` checks may complete an unchecked implementation task. Never claim a behavioral task is complete without fresh test evidence or human verification.
|
package/skills.json
CHANGED
|
@@ -28,67 +28,67 @@
|
|
|
28
28
|
"name": "roadmap",
|
|
29
29
|
"path": "skills/roadmap",
|
|
30
30
|
"description": "Native slash palette for RoadmapSmith commands and recommended entrypoints across supported hosts.",
|
|
31
|
-
"version": "0.9.
|
|
31
|
+
"version": "0.9.29"
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
"name": "roadmap-zero",
|
|
35
35
|
"path": "skills/roadmap-zero",
|
|
36
36
|
"description": "Native slash entrypoint for the one-command Zero Mode CLI workflow.",
|
|
37
|
-
"version": "0.9.
|
|
37
|
+
"version": "0.9.29"
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
"name": "roadmap-maintain",
|
|
41
41
|
"path": "skills/roadmap-maintain",
|
|
42
42
|
"description": "Native slash entrypoint for the preserve-first generate + sync + audit flow.",
|
|
43
|
-
"version": "0.9.
|
|
43
|
+
"version": "0.9.29"
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
46
|
"name": "roadmap-status",
|
|
47
47
|
"path": "skills/roadmap-status",
|
|
48
48
|
"description": "Native slash readiness check grounded in roadmapsmith status JSON.",
|
|
49
|
-
"version": "0.9.
|
|
49
|
+
"version": "0.9.29"
|
|
50
50
|
},
|
|
51
51
|
{
|
|
52
52
|
"name": "roadmap-init",
|
|
53
53
|
"path": "skills/roadmap-init",
|
|
54
54
|
"description": "Native slash entrypoint for creating ROADMAP.md and AGENTS.md.",
|
|
55
|
-
"version": "0.9.
|
|
55
|
+
"version": "0.9.29"
|
|
56
56
|
},
|
|
57
57
|
{
|
|
58
58
|
"name": "roadmap-generate",
|
|
59
59
|
"path": "skills/roadmap-generate",
|
|
60
60
|
"description": "Native slash entrypoint for managed roadmap updates that require --full-regen before destructive replacement.",
|
|
61
|
-
"version": "0.9.
|
|
61
|
+
"version": "0.9.29"
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
64
|
"name": "roadmap-validate",
|
|
65
65
|
"path": "skills/roadmap-validate",
|
|
66
66
|
"description": "Native slash entrypoint for evidence-backed roadmap validation.",
|
|
67
|
-
"version": "0.9.
|
|
67
|
+
"version": "0.9.29"
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
"name": "roadmap-update",
|
|
71
71
|
"path": "skills/roadmap-update",
|
|
72
72
|
"description": "Native slash entrypoint for evidence-backed sync and verified single-task completion.",
|
|
73
|
-
"version": "0.9.
|
|
73
|
+
"version": "0.9.29"
|
|
74
74
|
},
|
|
75
75
|
{
|
|
76
76
|
"name": "roadmap-sync",
|
|
77
77
|
"path": "skills/roadmap-sync",
|
|
78
78
|
"description": "DEPRECATED legacy compatibility root; use roadmap-maintain or roadmap-update.",
|
|
79
|
-
"version": "0.9.
|
|
79
|
+
"version": "0.9.29"
|
|
80
80
|
},
|
|
81
81
|
{
|
|
82
82
|
"name": "roadmap-audit",
|
|
83
83
|
"path": "skills/roadmap-audit",
|
|
84
84
|
"description": "Native slash entrypoint for the current sync-plus-audit workflow.",
|
|
85
|
-
"version": "0.9.
|
|
85
|
+
"version": "0.9.29"
|
|
86
86
|
},
|
|
87
87
|
{
|
|
88
88
|
"name": "roadmap-setup",
|
|
89
89
|
"path": "skills/roadmap-setup",
|
|
90
90
|
"description": "Native slash entrypoint for generating RoadmapSmith host integration files.",
|
|
91
|
-
"version": "0.9.
|
|
91
|
+
"version": "0.9.29"
|
|
92
92
|
}
|
|
93
93
|
]
|
|
94
94
|
}
|
package/src/config.js
CHANGED
|
@@ -31,7 +31,9 @@ const DEFAULT_CONFIG = {
|
|
|
31
31
|
doneCriteria: []
|
|
32
32
|
},
|
|
33
33
|
validation: {
|
|
34
|
-
minimumConfidence: 'low'
|
|
34
|
+
minimumConfidence: 'low',
|
|
35
|
+
testReports: [],
|
|
36
|
+
recipeCommand: ''
|
|
35
37
|
},
|
|
36
38
|
milestones: [
|
|
37
39
|
{ version: 'v0.1', goal: 'Foundation baseline complete' },
|
|
@@ -95,7 +97,10 @@ function mergeConfig(userConfig) {
|
|
|
95
97
|
},
|
|
96
98
|
validation: {
|
|
97
99
|
...DEFAULT_CONFIG.validation,
|
|
98
|
-
...((userConfig && userConfig.validation) || {})
|
|
100
|
+
...((userConfig && userConfig.validation) || {}),
|
|
101
|
+
testReports: userConfig && userConfig.validation && Array.isArray(userConfig.validation.testReports)
|
|
102
|
+
? userConfig.validation.testReports
|
|
103
|
+
: DEFAULT_CONFIG.validation.testReports
|
|
99
104
|
}
|
|
100
105
|
};
|
|
101
106
|
}
|
package/src/io.js
CHANGED
package/src/parser/index.js
CHANGED
|
@@ -89,6 +89,26 @@ function parseEvidenceLine(content) {
|
|
|
89
89
|
return content.slice(9).trim();
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
function parsePrefixedChildLine(content, prefix) {
|
|
93
|
+
const normalizedPrefix = `${prefix}:`;
|
|
94
|
+
if (content.slice(0, normalizedPrefix.length).toLowerCase() !== normalizedPrefix.toLowerCase()) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return content.slice(normalizedPrefix.length).trim();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseVerifyLine(content) {
|
|
101
|
+
return parsePrefixedChildLine(content, 'Verify');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function parseTestEvidenceLine(content) {
|
|
105
|
+
return parsePrefixedChildLine(content, 'Test evidence');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parseVerificationRecipeLine(content) {
|
|
109
|
+
return parsePrefixedChildLine(content, 'Verification recipe');
|
|
110
|
+
}
|
|
111
|
+
|
|
92
112
|
function parseWarningLine(content) {
|
|
93
113
|
if (!content.startsWith(WARNING_PREFIX)) return null;
|
|
94
114
|
let normalized = content.slice(WARNING_PREFIX.length).trim();
|
|
@@ -132,6 +152,10 @@ function parseRoadmap(content) {
|
|
|
132
152
|
let warningLineIndex = null;
|
|
133
153
|
let warningText = null;
|
|
134
154
|
const evidenceLines = [];
|
|
155
|
+
const verifyLines = [];
|
|
156
|
+
const testEvidenceLines = [];
|
|
157
|
+
const explicitPendingItems = [];
|
|
158
|
+
let verificationRecipeLineIndex = null;
|
|
135
159
|
const blockedByIds = [];
|
|
136
160
|
let lastChildLineIndex = index;
|
|
137
161
|
for (let childIndex = index + 1; childIndex < lines.length; childIndex += 1) {
|
|
@@ -162,6 +186,28 @@ function parseRoadmap(content) {
|
|
|
162
186
|
});
|
|
163
187
|
}
|
|
164
188
|
|
|
189
|
+
const verifyText = parseVerifyLine(childBullet.content);
|
|
190
|
+
if (verifyText != null) {
|
|
191
|
+
verifyLines.push({ lineIndex: childIndex, text: verifyText, raw: childLine });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const testEvidenceText = parseTestEvidenceLine(childBullet.content);
|
|
195
|
+
if (testEvidenceText != null) {
|
|
196
|
+
testEvidenceLines.push({ lineIndex: childIndex, text: testEvidenceText, raw: childLine });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const verificationRecipeText = parseVerificationRecipeLine(childBullet.content);
|
|
200
|
+
if (verificationRecipeText != null) {
|
|
201
|
+
verificationRecipeLineIndex = childIndex;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (childBullet.content.startsWith('❌')) {
|
|
205
|
+
explicitPendingItems.push({
|
|
206
|
+
lineIndex: childIndex,
|
|
207
|
+
text: childBullet.content.slice('❌'.length).trim()
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
165
211
|
const warningTextValue = parseWarningLine(childBullet.content);
|
|
166
212
|
if (warningTextValue != null) {
|
|
167
213
|
warningLineIndex = childIndex;
|
|
@@ -190,6 +236,10 @@ function parseRoadmap(content) {
|
|
|
190
236
|
warningLineIndex,
|
|
191
237
|
warningText,
|
|
192
238
|
evidenceLines,
|
|
239
|
+
verifyLines,
|
|
240
|
+
testEvidenceLines,
|
|
241
|
+
verificationRecipeLineIndex,
|
|
242
|
+
explicitPendingItems,
|
|
193
243
|
blockedByIds,
|
|
194
244
|
markerId,
|
|
195
245
|
noTest,
|
package/src/sync/index.js
CHANGED
|
@@ -112,6 +112,23 @@ function shouldPreserveExistingWarning(existingReason, newReason) {
|
|
|
112
112
|
return cleanNew === 'validation failed' && cleanExisting && cleanExisting !== cleanNew;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
function formatVerificationRecipe(indent, recipe) {
|
|
116
|
+
return `${indent} - Verification recipe: ${recipe}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function formatTestEvidence(indent, evidence) {
|
|
120
|
+
return `${indent} - Test evidence: ${evidence}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function findVerificationRecipeIndex(lines, taskLineIndex) {
|
|
124
|
+
for (let index = taskLineIndex + 1; index < lines.length; index += 1) {
|
|
125
|
+
const line = lines[index];
|
|
126
|
+
if (!line.trim() || /^\s*#{2,}\s/.test(line) || /^\s*- \[[ xX]\]\s/.test(line)) break;
|
|
127
|
+
if (/^\s*- Verification recipe:/i.test(line)) return index;
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
115
132
|
function applySync(content, parsedTasks, results) {
|
|
116
133
|
const parsed = parseRoadmap(content);
|
|
117
134
|
const lines = [...parsed.lines];
|
|
@@ -142,6 +159,19 @@ function applySync(content, parsedTasks, results) {
|
|
|
142
159
|
lines.splice(warningIndex, 1);
|
|
143
160
|
offset -= 1;
|
|
144
161
|
}
|
|
162
|
+
const recipeIndex = findVerificationRecipeIndex(lines, lineIndex);
|
|
163
|
+
if (recipeIndex != null && recipeIndex >= 0 && recipeIndex < lines.length) {
|
|
164
|
+
lines.splice(recipeIndex, 1);
|
|
165
|
+
offset -= 1;
|
|
166
|
+
}
|
|
167
|
+
if (
|
|
168
|
+
result.generatedTestEvidence &&
|
|
169
|
+
(!Array.isArray(task.testEvidenceLines) || task.testEvidenceLines.length === 0)
|
|
170
|
+
) {
|
|
171
|
+
const insertionIndex = Math.max(lineIndex + 1, (task.lastChildLineIndex + offset) + 1);
|
|
172
|
+
lines.splice(insertionIndex, 0, formatTestEvidence(task.indent || '', result.generatedTestEvidence));
|
|
173
|
+
offset += 1;
|
|
174
|
+
}
|
|
145
175
|
if (
|
|
146
176
|
result.staleEvidenceResolved &&
|
|
147
177
|
(!Array.isArray(task.evidenceLines) || task.evidenceLines.length === 0) &&
|
|
@@ -154,10 +184,7 @@ function applySync(content, parsedTasks, results) {
|
|
|
154
184
|
lines.splice(insertionIndex, 0, `${task.indent || ''} - Evidence: ${result.discoveredEvidence}`);
|
|
155
185
|
offset += 1;
|
|
156
186
|
}
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (warningIndex != null && warningIndex >= 0 && warningIndex < lines.length) {
|
|
187
|
+
} else if (warningIndex != null && warningIndex >= 0 && warningIndex < lines.length) {
|
|
161
188
|
const existingReason = normalizeWarningReason(lines[warningIndex]);
|
|
162
189
|
const newReason = reason || 'validation failed';
|
|
163
190
|
if (!shouldPreserveExistingWarning(existingReason, newReason)) {
|
|
@@ -167,6 +194,23 @@ function applySync(content, parsedTasks, results) {
|
|
|
167
194
|
lines.splice(lastChildLineIndex + 1, 0, warningText);
|
|
168
195
|
offset += 1;
|
|
169
196
|
}
|
|
197
|
+
|
|
198
|
+
const recipeIndex = findVerificationRecipeIndex(lines, lineIndex);
|
|
199
|
+
if (result.passed) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (result.verificationRecipe) {
|
|
203
|
+
const recipeLine = formatVerificationRecipe(task.indent || '', result.verificationRecipe);
|
|
204
|
+
if (recipeIndex != null && recipeIndex >= 0 && recipeIndex < lines.length) {
|
|
205
|
+
lines[recipeIndex] = recipeLine;
|
|
206
|
+
} else {
|
|
207
|
+
lines.splice(lastChildLineIndex + 1, 0, recipeLine);
|
|
208
|
+
offset += 1;
|
|
209
|
+
}
|
|
210
|
+
} else if (recipeIndex != null && recipeIndex >= 0 && recipeIndex < lines.length) {
|
|
211
|
+
lines.splice(recipeIndex, 1);
|
|
212
|
+
offset -= 1;
|
|
213
|
+
}
|
|
170
214
|
}
|
|
171
215
|
|
|
172
216
|
return ensureTrailingNewline(lines.join('\n'));
|
package/src/validator/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const CODE_EXTENSIONS = new Set([
|
|
|
13
13
|
]);
|
|
14
14
|
const TRANSLATION_DIR_SEGMENTS = ['locale', 'locales', 'i18n', 'translations'];
|
|
15
15
|
const DEFAULT_EXCLUDED_PATH_PREFIXES = ['.claude/', '.agent/', 'roadmap-skill/'];
|
|
16
|
+
const GENERATED_OUTPUT_PREFIXES = ['dist-electron/', 'dist/', 'build/', 'out/', '.next/', 'coverage/'];
|
|
16
17
|
|
|
17
18
|
// "docs" omitted from DOC_HINTS — it is a path prefix in scan tasks, not a doc-authoring keyword.
|
|
18
19
|
const DOC_HINTS = ['readme', 'changelog', 'documentation', 'spec', 'diagram', 'runbook'];
|
|
@@ -151,6 +152,11 @@ function isMostlyUiStrings(content) {
|
|
|
151
152
|
return stringLikeLines / lines.length > 0.8;
|
|
152
153
|
}
|
|
153
154
|
|
|
155
|
+
function isGeneratedOutputPath(relativePath) {
|
|
156
|
+
const normalized = normalizePathForMatch(relativePath);
|
|
157
|
+
return GENERATED_OUTPUT_PREFIXES.some((prefix) => normalized.startsWith(prefix));
|
|
158
|
+
}
|
|
159
|
+
|
|
154
160
|
function readFileIndex(projectRoot, files, config) {
|
|
155
161
|
const index = [];
|
|
156
162
|
for (const relativePath of files) {
|
|
@@ -180,6 +186,7 @@ function readFileIndex(projectRoot, files, config) {
|
|
|
180
186
|
absolutePath,
|
|
181
187
|
ext,
|
|
182
188
|
content,
|
|
189
|
+
generatedOutput: isGeneratedOutputPath(relativePath),
|
|
183
190
|
isTestFile: /(^|\/)(__tests__|tests)\//.test(relativePath) || /\.test\.|\.spec\.|_test\.go$/.test(relativePath)
|
|
184
191
|
});
|
|
185
192
|
}
|
|
@@ -631,6 +638,7 @@ function findFilesBySymbols(symbolHints, fileIndex) {
|
|
|
631
638
|
for (const symbol of symbolHints) {
|
|
632
639
|
const regex = new RegExp(`\\b${escapeRegExp(symbol)}\\b`, 'i');
|
|
633
640
|
for (const file of fileIndex) {
|
|
641
|
+
if (file.generatedOutput) continue;
|
|
634
642
|
if (!CODE_EXTENSIONS.has(file.ext)) {
|
|
635
643
|
continue;
|
|
636
644
|
}
|
|
@@ -650,6 +658,7 @@ function findFilesByTaskPathTokens(taskText, fileIndex, pathDerivedTokens = new
|
|
|
650
658
|
|
|
651
659
|
const matches = new Set();
|
|
652
660
|
for (const file of fileIndex) {
|
|
661
|
+
if (file.generatedOutput) continue;
|
|
653
662
|
const pathSegments = normalizePathForMatch(file.relativePath).split('/').filter(Boolean);
|
|
654
663
|
for (const token of tokens) {
|
|
655
664
|
if (pathSegments.some((segment) => segment === token || segment.includes(token))) {
|
|
@@ -744,7 +753,7 @@ function findCodeEvidence(taskText, fileIndex, pathDerivedTokens = new Set()) {
|
|
|
744
753
|
|
|
745
754
|
const matches = [];
|
|
746
755
|
for (const file of fileIndex) {
|
|
747
|
-
if (!CODE_EXTENSIONS.has(file.ext) || file.isTestFile) {
|
|
756
|
+
if (file.generatedOutput || !CODE_EXTENSIONS.has(file.ext) || file.isTestFile) {
|
|
748
757
|
continue;
|
|
749
758
|
}
|
|
750
759
|
|
|
@@ -819,7 +828,7 @@ function findTestEvidence(taskText, fileIndex, referencedPaths = []) {
|
|
|
819
828
|
const matches = [];
|
|
820
829
|
|
|
821
830
|
for (const file of fileIndex) {
|
|
822
|
-
if (!file.isTestFile) continue;
|
|
831
|
+
if (file.generatedOutput || !file.isTestFile) continue;
|
|
823
832
|
|
|
824
833
|
// A test file counts as evidence only when it imports a module whose path contains
|
|
825
834
|
// one of the task's meaningful tokens. Content-keyword matching is intentionally absent:
|
|
@@ -897,6 +906,7 @@ function findArtifactEvidence(taskText, fileIndex) {
|
|
|
897
906
|
];
|
|
898
907
|
|
|
899
908
|
for (const file of fileIndex) {
|
|
909
|
+
if (file.generatedOutput) continue;
|
|
900
910
|
if (artifactPatterns.some((pattern) => pattern.test(file.relativePath)) && !files.includes(file.relativePath)) {
|
|
901
911
|
files.push(file.relativePath);
|
|
902
912
|
}
|
|
@@ -1273,6 +1283,303 @@ function evaluateRule(rule, task, context) {
|
|
|
1273
1283
|
};
|
|
1274
1284
|
}
|
|
1275
1285
|
|
|
1286
|
+
function parseVerificationFields(text) {
|
|
1287
|
+
const fields = {};
|
|
1288
|
+
for (const part of String(text || '').split(';')) {
|
|
1289
|
+
const separator = part.indexOf('=');
|
|
1290
|
+
if (separator <= 0) continue;
|
|
1291
|
+
const key = part.slice(0, separator).trim().toLowerCase();
|
|
1292
|
+
let value = part.slice(separator + 1).trim();
|
|
1293
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
1294
|
+
value = value.slice(1, -1);
|
|
1295
|
+
}
|
|
1296
|
+
if (key && value) fields[key] = value;
|
|
1297
|
+
}
|
|
1298
|
+
return fields;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function stripCodeComments(content) {
|
|
1302
|
+
const source = String(content || '');
|
|
1303
|
+
let result = '';
|
|
1304
|
+
let quote = null;
|
|
1305
|
+
let escaped = false;
|
|
1306
|
+
let lineHasContent = false;
|
|
1307
|
+
|
|
1308
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
1309
|
+
const char = source[index];
|
|
1310
|
+
const next = source[index + 1];
|
|
1311
|
+
|
|
1312
|
+
if (quote) {
|
|
1313
|
+
result += char;
|
|
1314
|
+
if (escaped) {
|
|
1315
|
+
escaped = false;
|
|
1316
|
+
} else if (char === '\\') {
|
|
1317
|
+
escaped = true;
|
|
1318
|
+
} else if (char === quote) {
|
|
1319
|
+
quote = null;
|
|
1320
|
+
}
|
|
1321
|
+
if (char === '\n') lineHasContent = false;
|
|
1322
|
+
continue;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
1326
|
+
quote = char;
|
|
1327
|
+
result += char;
|
|
1328
|
+
lineHasContent = true;
|
|
1329
|
+
continue;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
if (char === '/' && next === '*') {
|
|
1333
|
+
const closeIndex = source.indexOf('*/', index + 2);
|
|
1334
|
+
index = closeIndex < 0 ? source.length : closeIndex + 1;
|
|
1335
|
+
continue;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
if (char === '/' && next === '/') {
|
|
1339
|
+
const newlineIndex = source.indexOf('\n', index + 2);
|
|
1340
|
+
if (newlineIndex < 0) break;
|
|
1341
|
+
result += '\n';
|
|
1342
|
+
lineHasContent = false;
|
|
1343
|
+
index = newlineIndex;
|
|
1344
|
+
continue;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
if (char === '#' && !lineHasContent) {
|
|
1348
|
+
const newlineIndex = source.indexOf('\n', index + 1);
|
|
1349
|
+
if (newlineIndex < 0) break;
|
|
1350
|
+
result += '\n';
|
|
1351
|
+
lineHasContent = false;
|
|
1352
|
+
index = newlineIndex;
|
|
1353
|
+
continue;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
result += char;
|
|
1357
|
+
if (char === '\n') {
|
|
1358
|
+
lineHasContent = false;
|
|
1359
|
+
} else if (!/\s/.test(char)) {
|
|
1360
|
+
lineHasContent = true;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
return result;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
function findIndexedFile(relativePath, context) {
|
|
1368
|
+
const normalized = normalizeReferencedPath(relativePath);
|
|
1369
|
+
return context.fileIndex.find((file) => !file.generatedOutput && (
|
|
1370
|
+
normalizeReferencedPath(file.relativePath) === normalized ||
|
|
1371
|
+
normalizeReferencedPath(file.relativePath).endsWith('/' + normalized)
|
|
1372
|
+
));
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
function readTestReportRecords(projectRoot, validationConfig) {
|
|
1376
|
+
const reports = Array.isArray(validationConfig && validationConfig.testReports)
|
|
1377
|
+
? validationConfig.testReports
|
|
1378
|
+
: [];
|
|
1379
|
+
const records = [];
|
|
1380
|
+
|
|
1381
|
+
function visit(value, reportPath, mtimeMs) {
|
|
1382
|
+
if (!value || typeof value !== 'object') return;
|
|
1383
|
+
if (Array.isArray(value)) {
|
|
1384
|
+
value.forEach((entry) => visit(entry, reportPath, mtimeMs));
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
const status = String(value.status || value.state || value.result || '').toLowerCase();
|
|
1388
|
+
const file = value.file || value.filepath || value.testFile || value.nameFile;
|
|
1389
|
+
const name = value.fullName || value.fullname || value.name || value.title;
|
|
1390
|
+
if (file && name && /^(pass|passed|success)$/.test(status)) {
|
|
1391
|
+
records.push({ file: String(file), name: String(name), reportPath, mtimeMs });
|
|
1392
|
+
}
|
|
1393
|
+
Object.values(value).forEach((entry) => visit(entry, reportPath, mtimeMs));
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
for (const report of reports) {
|
|
1397
|
+
if (!report || report.format !== 'vitest-json' || !report.path) continue;
|
|
1398
|
+
const reportPath = path.resolve(projectRoot, report.path);
|
|
1399
|
+
try {
|
|
1400
|
+
const payload = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
|
|
1401
|
+
const mtimeMs = fs.statSync(reportPath).mtimeMs;
|
|
1402
|
+
visit(payload, reportPath, mtimeMs);
|
|
1403
|
+
} catch {
|
|
1404
|
+
// A missing or malformed optional report is simply unavailable evidence.
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
return records;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
function timestampIsFresh(timestamp, files) {
|
|
1411
|
+
const verifiedAt = Date.parse(timestamp || '');
|
|
1412
|
+
if (!Number.isFinite(verifiedAt)) return false;
|
|
1413
|
+
return files.every((file) => {
|
|
1414
|
+
try {
|
|
1415
|
+
return verifiedAt >= fs.statSync(file.absolutePath).mtimeMs;
|
|
1416
|
+
} catch {
|
|
1417
|
+
return false;
|
|
1418
|
+
}
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
function findFreshTestProof(task, fields, context) {
|
|
1423
|
+
const testFile = findIndexedFile(fields.test || fields.file, context);
|
|
1424
|
+
const sourceFile = fields.source ? findIndexedFile(fields.source, context) : null;
|
|
1425
|
+
const caseName = fields.case;
|
|
1426
|
+
if (!testFile || !caseName) return { passed: false, available: false };
|
|
1427
|
+
const freshnessFiles = [testFile, ...(sourceFile ? [sourceFile] : [])];
|
|
1428
|
+
const evidenceLines = Array.isArray(task.testEvidenceLines) ? task.testEvidenceLines : [];
|
|
1429
|
+
for (const line of evidenceLines) {
|
|
1430
|
+
const evidence = parseVerificationFields(line.text);
|
|
1431
|
+
if (String(evidence.status || '').toUpperCase() !== 'PASS') continue;
|
|
1432
|
+
if (normalizeReferencedPath(evidence.file) !== normalizeReferencedPath(testFile.relativePath)) continue;
|
|
1433
|
+
if (evidence.case !== caseName) continue;
|
|
1434
|
+
if (timestampIsFresh(evidence.verifiedat, freshnessFiles)) {
|
|
1435
|
+
return { passed: true, available: true };
|
|
1436
|
+
}
|
|
1437
|
+
return { passed: false, available: true, stale: true };
|
|
1438
|
+
}
|
|
1439
|
+
for (const record of context.testReportRecords || []) {
|
|
1440
|
+
if (!referencedPathMatches(record.file, testFile.relativePath) || !record.name.includes(caseName)) continue;
|
|
1441
|
+
if (record.mtimeMs >= Math.max(...freshnessFiles.map((file) => fs.statSync(file.absolutePath).mtimeMs))) {
|
|
1442
|
+
return {
|
|
1443
|
+
passed: true,
|
|
1444
|
+
available: true,
|
|
1445
|
+
generatedEvidence: `file=${testFile.relativePath}; case=${caseName}; status=PASS; verifiedAt=${new Date(record.mtimeMs).toISOString()}`
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
return { passed: false, available: true, stale: true };
|
|
1449
|
+
}
|
|
1450
|
+
return { passed: false, available: false };
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
function testCoversEndpoint(testFile, route, context) {
|
|
1454
|
+
const normalizedRoute = String(route).replace(/\[([^\]]+)\]/g, '$1').toLowerCase();
|
|
1455
|
+
const normalizedContent = testFile.content.replace(/\[([^\]]+)\]/g, '$1').toLowerCase();
|
|
1456
|
+
if (normalizedContent.includes(normalizedRoute)) return true;
|
|
1457
|
+
const source = findIndexedFile(`src/app${route}/route.ts`, context);
|
|
1458
|
+
if (source && normalizedContent.includes(path.basename(source.relativePath, source.ext).toLowerCase())) return true;
|
|
1459
|
+
const segments = normalizedRoute.split('/').filter((segment) => segment.length >= 3);
|
|
1460
|
+
return segments.length > 0 && segments.every((segment) => normalizedContent.includes(segment));
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
function findVerificationRecipe(task, context) {
|
|
1464
|
+
const patterns = [
|
|
1465
|
+
/disabled\s*=\s*\{[^}]+\}/i,
|
|
1466
|
+
/<(?:dialog|alertdialog)\b[^>]*\bopen\s*=/i,
|
|
1467
|
+
/abortcontroller|abortsignal\.timeout/i,
|
|
1468
|
+
/router\.push\s*\(/i
|
|
1469
|
+
];
|
|
1470
|
+
for (const file of context.fileIndex) {
|
|
1471
|
+
if (file.generatedOutput || file.isTestFile || !CODE_EXTENSIONS.has(file.ext)) continue;
|
|
1472
|
+
const match = patterns.map((pattern) => pattern.exec(file.content)).find(Boolean);
|
|
1473
|
+
if (!match) continue;
|
|
1474
|
+
const line = file.content.slice(0, match.index).split(/\r?\n/).length;
|
|
1475
|
+
const command = context.config.validation && context.config.validation.recipeCommand
|
|
1476
|
+
? `; run ${String(context.config.validation.recipeCommand).replace('{testFile}', '<test-file>')}`
|
|
1477
|
+
: '';
|
|
1478
|
+
return `${file.relativePath}:${line} inspect ${match[0].trim()}${command}`;
|
|
1479
|
+
}
|
|
1480
|
+
return null;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
function isBehavioralTask(taskText) {
|
|
1484
|
+
return /\b(mostrar|deshabilitar|confirmar|notificar|redirigir|imprimir|show|disable|confirm|notify|redirect|print)\b/i.test(String(taskText || ''));
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
function evaluateDeterministicVerification(task, context) {
|
|
1488
|
+
const verifyLines = Array.isArray(task.verifyLines) ? task.verifyLines : [];
|
|
1489
|
+
if (verifyLines.length === 0) {
|
|
1490
|
+
if (!isBehavioralTask(task.text)) {
|
|
1491
|
+
return { applicable: false, passed: false, reasons: [], diagnostics: [], recipe: null };
|
|
1492
|
+
}
|
|
1493
|
+
const recipe = findVerificationRecipe(task, context);
|
|
1494
|
+
return {
|
|
1495
|
+
applicable: false,
|
|
1496
|
+
passed: false,
|
|
1497
|
+
reasons: [],
|
|
1498
|
+
diagnostics: [{
|
|
1499
|
+
code: recipe ? 'REQUIRES_HUMAN_EVIDENCE' : 'NO_STATIC_SIGNAL',
|
|
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
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
const reasons = [];
|
|
1507
|
+
const diagnostics = [];
|
|
1508
|
+
let generatedTestEvidence = null;
|
|
1509
|
+
|
|
1510
|
+
for (const line of verifyLines) {
|
|
1511
|
+
const fields = parseVerificationFields(line.text);
|
|
1512
|
+
const kind = fields.kind;
|
|
1513
|
+
if (kind === 'contains' || kind === 'property') {
|
|
1514
|
+
const file = findIndexedFile(fields.file, context);
|
|
1515
|
+
if (!file) {
|
|
1516
|
+
reasons.push(`missing referenced file(s): ${fields.file || '<unspecified>'}`);
|
|
1517
|
+
continue;
|
|
1518
|
+
}
|
|
1519
|
+
const content = stripCodeComments(file.content);
|
|
1520
|
+
if (kind === 'contains') {
|
|
1521
|
+
if (!fields.expected || !content.includes(fields.expected)) {
|
|
1522
|
+
reasons.push(`no content match in ${file.relativePath}: expected ${fields.expected || '<unspecified>'}`);
|
|
1523
|
+
}
|
|
1524
|
+
continue;
|
|
1525
|
+
}
|
|
1526
|
+
const keyPattern = new RegExp(`\\b${escapeRegExp(fields.key || '')}\\s*:`);
|
|
1527
|
+
const exactPattern = new RegExp(`\\b${escapeRegExp(fields.key || '')}\\s*:\\s*${escapeRegExp(fields.equals || '')}(?=\\s*[,}\\n])`);
|
|
1528
|
+
if (!exactPattern.test(content)) {
|
|
1529
|
+
reasons.push(keyPattern.test(content)
|
|
1530
|
+
? `wrong value for ${fields.key} in ${file.relativePath}: expected ${fields.equals}`
|
|
1531
|
+
: `no content match in ${file.relativePath}: expected ${fields.key}: ${fields.equals}`);
|
|
1532
|
+
}
|
|
1533
|
+
continue;
|
|
1534
|
+
}
|
|
1535
|
+
if (kind === 'endpoints') {
|
|
1536
|
+
const routes = String(fields.routes || '').split(',').map((value) => value.trim()).filter(Boolean);
|
|
1537
|
+
const tests = context.fileIndex.filter((file) => !file.generatedOutput && file.isTestFile);
|
|
1538
|
+
const covered = routes.filter((route) => tests.some((file) => testCoversEndpoint(file, route, context)));
|
|
1539
|
+
if (covered.length !== routes.length) {
|
|
1540
|
+
const missing = routes.filter((route) => !covered.includes(route));
|
|
1541
|
+
reasons.push(`partial endpoint coverage ${covered.length}/${routes.length}: missing ${missing.join(', ')}`);
|
|
1542
|
+
}
|
|
1543
|
+
continue;
|
|
1544
|
+
}
|
|
1545
|
+
if (kind === 'behavior') {
|
|
1546
|
+
const source = findIndexedFile(fields.source, context);
|
|
1547
|
+
const testFile = findIndexedFile(fields.test, context);
|
|
1548
|
+
const testContent = testFile ? testFile.content : '';
|
|
1549
|
+
const sourceReference = source && testContent.includes(path.basename(source.relativePath, source.ext));
|
|
1550
|
+
const exercise = fields.trigger && testContent.includes(fields.trigger);
|
|
1551
|
+
const assertion = fields.assertion && testContent.includes(fields.assertion);
|
|
1552
|
+
const namedCase = fields.case && testContent.includes(fields.case);
|
|
1553
|
+
const proof = findFreshTestProof(task, fields, context);
|
|
1554
|
+
if (!source || !testFile || !sourceReference || !exercise || !assertion || !namedCase) {
|
|
1555
|
+
diagnostics.push({ code: 'REQUIRES_HUMAN_EVIDENCE', severity: 'warning', message: 'behavior verification needs a source reference, named test, trigger, and assertion' });
|
|
1556
|
+
continue;
|
|
1557
|
+
}
|
|
1558
|
+
if (proof.stale) {
|
|
1559
|
+
diagnostics.push({ code: 'STALE_TEST_REPORT', severity: 'warning', message: `test result for ${testFile.relativePath} predates the verified source` });
|
|
1560
|
+
continue;
|
|
1561
|
+
}
|
|
1562
|
+
if (!proof.passed) {
|
|
1563
|
+
diagnostics.push({ code: 'REQUIRES_HUMAN_EVIDENCE', severity: 'warning', message: `no fresh passing result for ${testFile.relativePath}` });
|
|
1564
|
+
continue;
|
|
1565
|
+
}
|
|
1566
|
+
generatedTestEvidence = proof.generatedEvidence || generatedTestEvidence;
|
|
1567
|
+
continue;
|
|
1568
|
+
}
|
|
1569
|
+
reasons.push(`unsupported verification kind: ${kind || '<unspecified>'}`);
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
const passed = reasons.length === 0 && diagnostics.length === 0;
|
|
1573
|
+
return {
|
|
1574
|
+
applicable: true,
|
|
1575
|
+
passed,
|
|
1576
|
+
reasons,
|
|
1577
|
+
diagnostics,
|
|
1578
|
+
generatedTestEvidence,
|
|
1579
|
+
recipe: passed ? null : findVerificationRecipe(task, context)
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1276
1583
|
function buildValidationContext(projectRoot, config, plugins) {
|
|
1277
1584
|
const files = walkFiles(projectRoot);
|
|
1278
1585
|
const fileIndex = readFileIndex(projectRoot, files, config);
|
|
@@ -1286,7 +1593,8 @@ function buildValidationContext(projectRoot, config, plugins) {
|
|
|
1286
1593
|
files,
|
|
1287
1594
|
fileIndex,
|
|
1288
1595
|
pathHintResolver,
|
|
1289
|
-
testFrameworks
|
|
1596
|
+
testFrameworks,
|
|
1597
|
+
testReportRecords: readTestReportRecords(projectRoot, config.validation)
|
|
1290
1598
|
};
|
|
1291
1599
|
}
|
|
1292
1600
|
|
|
@@ -1298,6 +1606,15 @@ function diagnosticCodeForReason(reason) {
|
|
|
1298
1606
|
if (normalized.includes('missing test evidence')) {
|
|
1299
1607
|
return 'NO_TEST';
|
|
1300
1608
|
}
|
|
1609
|
+
if (normalized.includes('wrong value')) {
|
|
1610
|
+
return 'WRONG_VALUE';
|
|
1611
|
+
}
|
|
1612
|
+
if (normalized.includes('partial endpoint coverage')) {
|
|
1613
|
+
return 'PARTIAL';
|
|
1614
|
+
}
|
|
1615
|
+
if (normalized.includes('no content match')) {
|
|
1616
|
+
return 'NOT_IMPLEMENTED';
|
|
1617
|
+
}
|
|
1301
1618
|
if (
|
|
1302
1619
|
normalized.includes('no code, test, or artifact evidence found') ||
|
|
1303
1620
|
normalized.includes('implementation task requires evidence line') ||
|
|
@@ -1327,6 +1644,10 @@ function buildDiagnostics(reasons, options = {}) {
|
|
|
1327
1644
|
message: 'historical validation warning conflicts with fresh repository evidence'
|
|
1328
1645
|
});
|
|
1329
1646
|
}
|
|
1647
|
+
for (const diagnostic of options.extra || []) {
|
|
1648
|
+
if (!diagnostic || !diagnostic.code || diagnostics.some((item) => item.code === diagnostic.code)) continue;
|
|
1649
|
+
diagnostics.push(diagnostic);
|
|
1650
|
+
}
|
|
1330
1651
|
return diagnostics;
|
|
1331
1652
|
}
|
|
1332
1653
|
|
|
@@ -1348,6 +1669,8 @@ function validateTask(task, context, config, plugins) {
|
|
|
1348
1669
|
const standaloneFilenames = extractStandaloneFilenames(task.text);
|
|
1349
1670
|
const symbolHints = extractSymbolHints(task.text);
|
|
1350
1671
|
const authoritativeEvidence = evaluateAuthoritativeEvidence(task, context.pathHintResolver);
|
|
1672
|
+
const deterministicVerification = evaluateDeterministicVerification(task, context);
|
|
1673
|
+
const hasExplicitPendingItems = Array.isArray(task.explicitPendingItems) && task.explicitPendingItems.length > 0;
|
|
1351
1674
|
|
|
1352
1675
|
const filesFromPaths = findFilesByPathHints(pathHints, context.pathHintResolver);
|
|
1353
1676
|
const filesFromPurePathHints = findFilesByPathHints(purePathHints, context.pathHintResolver);
|
|
@@ -1424,7 +1747,8 @@ function validateTask(task, context, config, plugins) {
|
|
|
1424
1747
|
context.testFrameworks.length > 0 &&
|
|
1425
1748
|
isCodeTask(task.text) &&
|
|
1426
1749
|
!isDocTask(task.text) &&
|
|
1427
|
-
!isHttpExpectationTask(task.text)
|
|
1750
|
+
!isHttpExpectationTask(task.text) &&
|
|
1751
|
+
!(task.verifyLines || []).some((line) => ['contains', 'property', 'endpoints', 'behavior'].includes(parseVerificationFields(line.text).kind));
|
|
1428
1752
|
const configuredRules = Array.isArray(config.validators) ? config.validators : [];
|
|
1429
1753
|
const pluginRules = collectPluginContributions(plugins || [], 'registerValidators', context);
|
|
1430
1754
|
let overrideResult = null;
|
|
@@ -1458,6 +1782,18 @@ function validateTask(task, context, config, plugins) {
|
|
|
1458
1782
|
|
|
1459
1783
|
let uniqueReasons = Array.from(new Set(reasons));
|
|
1460
1784
|
|
|
1785
|
+
if (deterministicVerification.passed) {
|
|
1786
|
+
uniqueReasons = uniqueReasons.filter((reason) => (
|
|
1787
|
+
reason !== 'no code, test, or artifact evidence found' &&
|
|
1788
|
+
reason !== 'missing test evidence' &&
|
|
1789
|
+
!reason.startsWith('weak path-')
|
|
1790
|
+
));
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
if (deterministicVerification.applicable && deterministicVerification.reasons.length > 0) {
|
|
1794
|
+
uniqueReasons = Array.from(new Set([...uniqueReasons, ...deterministicVerification.reasons]));
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1461
1797
|
if (overrideResult) {
|
|
1462
1798
|
uniqueReasons = Array.isArray(overrideResult.reasons) ? Array.from(new Set(overrideResult.reasons)) : [];
|
|
1463
1799
|
}
|
|
@@ -1516,7 +1852,15 @@ function validateTask(task, context, config, plugins) {
|
|
|
1516
1852
|
const hasFreshRepositoryEvidence = hasStrongEvidence || hasWeakEvidence;
|
|
1517
1853
|
let staleEvidenceDetected = false;
|
|
1518
1854
|
let staleEvidenceResolved = false;
|
|
1519
|
-
|
|
1855
|
+
// Heuristic proximity remains useful to locate candidates, but is never sufficient to
|
|
1856
|
+
// complete an unchecked task. Completion requires human Evidence, a trusted rule, or a
|
|
1857
|
+
// typed deterministic verifier.
|
|
1858
|
+
let passed = authoritativeEvidence.passed || hasTrustedRuleEvidencePass || deterministicVerification.passed;
|
|
1859
|
+
|
|
1860
|
+
if (!task.checked && !authoritativeEvidence.passed && !hasTrustedRuleEvidencePass && !deterministicVerification.passed && hasHighConfidenceImplementationEvidence) {
|
|
1861
|
+
uniqueReasons.push('implementation task requires deterministic Verify metadata or explicit Evidence to be marked complete');
|
|
1862
|
+
uniqueReasons = Array.from(new Set(uniqueReasons));
|
|
1863
|
+
}
|
|
1520
1864
|
|
|
1521
1865
|
if (task.warningText && !task.checked && hasFreshRepositoryEvidence && !authoritativeEvidence.passed) {
|
|
1522
1866
|
staleEvidenceDetected = true;
|
|
@@ -1531,7 +1875,7 @@ function validateTask(task, context, config, plugins) {
|
|
|
1531
1875
|
|
|
1532
1876
|
// Historical warnings are only cleared by independent, high-confidence repository evidence.
|
|
1533
1877
|
if (task.warningText && !task.checked && passed && !authoritativeEvidence.passed) {
|
|
1534
|
-
if (hasHighConfidenceImplementationEvidence && negativeSignalMatches.length === 0 && uniqueReasons.length === 0) {
|
|
1878
|
+
if ((deterministicVerification.passed || hasHighConfidenceImplementationEvidence) && negativeSignalMatches.length === 0 && uniqueReasons.length === 0) {
|
|
1535
1879
|
staleEvidenceResolved = true;
|
|
1536
1880
|
} else {
|
|
1537
1881
|
passed = false;
|
|
@@ -1544,6 +1888,16 @@ function validateTask(task, context, config, plugins) {
|
|
|
1544
1888
|
passed = false;
|
|
1545
1889
|
}
|
|
1546
1890
|
|
|
1891
|
+
const extraDiagnostics = [...deterministicVerification.diagnostics];
|
|
1892
|
+
if (hasExplicitPendingItems) {
|
|
1893
|
+
passed = false;
|
|
1894
|
+
extraDiagnostics.push({
|
|
1895
|
+
code: 'HAS_EXPLICIT_PENDING',
|
|
1896
|
+
severity: 'warning',
|
|
1897
|
+
message: `task declares pending item(s): ${task.explicitPendingItems.map((item) => item.text || 'pending').join(', ')}`
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1547
1901
|
// Unchecked implementation tasks need explicit evidence or high-confidence implementation
|
|
1548
1902
|
// evidence. Weak token overlap, direct file references, or code-only matches are not enough.
|
|
1549
1903
|
if (
|
|
@@ -1552,6 +1906,7 @@ function validateTask(task, context, config, plugins) {
|
|
|
1552
1906
|
isImplementationTask(task.text) &&
|
|
1553
1907
|
!authoritativeEvidence.passed &&
|
|
1554
1908
|
!hasTrustedRuleEvidencePass &&
|
|
1909
|
+
!deterministicVerification.passed &&
|
|
1555
1910
|
!hasArtifactTaskPass &&
|
|
1556
1911
|
!hasHighConfidenceImplementationEvidence
|
|
1557
1912
|
) {
|
|
@@ -1588,13 +1943,13 @@ function validateTask(task, context, config, plugins) {
|
|
|
1588
1943
|
// Used by auditValidation to flag implementation tasks that pass solely via documentation.
|
|
1589
1944
|
const evidenceIsDocOnly = !evidence.code && !evidence.test && evidence.artifact && !isDocTask(task.text);
|
|
1590
1945
|
|
|
1591
|
-
const finalPassed = overrideResult ? overrideResult.passed !== false : (passed && uniqueReasons.length === 0);
|
|
1946
|
+
const finalPassed = overrideResult ? (overrideResult.passed !== false && !hasExplicitPendingItems) : (passed && uniqueReasons.length === 0 && !hasExplicitPendingItems);
|
|
1592
1947
|
return {
|
|
1593
1948
|
taskId: task.id,
|
|
1594
1949
|
passed: finalPassed,
|
|
1595
1950
|
confidence,
|
|
1596
1951
|
reasons: uniqueReasons,
|
|
1597
|
-
diagnostics: buildDiagnostics(uniqueReasons, { staleEvidence: staleEvidenceDetected }),
|
|
1952
|
+
diagnostics: buildDiagnostics(uniqueReasons, { staleEvidence: staleEvidenceDetected, extra: extraDiagnostics }),
|
|
1598
1953
|
evidence,
|
|
1599
1954
|
evidenceIsDocOnly,
|
|
1600
1955
|
requiresTest,
|
|
@@ -1602,7 +1957,9 @@ function validateTask(task, context, config, plugins) {
|
|
|
1602
1957
|
attempted,
|
|
1603
1958
|
preservedCheckedState,
|
|
1604
1959
|
staleEvidenceResolved,
|
|
1605
|
-
discoveredEvidence: staleEvidenceResolved ? buildDiscoveredEvidenceLine(evidence) : null
|
|
1960
|
+
discoveredEvidence: staleEvidenceResolved ? buildDiscoveredEvidenceLine(evidence) : null,
|
|
1961
|
+
verificationRecipe: deterministicVerification.recipe,
|
|
1962
|
+
generatedTestEvidence: deterministicVerification.generatedTestEvidence || null
|
|
1606
1963
|
};
|
|
1607
1964
|
}
|
|
1608
1965
|
|