roadmapsmith 0.9.27 → 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 +24 -5
- package/bin/cli.js +91 -3
- package/package.json +1 -1
- package/skills/roadmap/SKILL.md +1 -1
- package/skills/roadmap-maintain/SKILL.md +3 -2
- package/skills/roadmap-status/SKILL.md +1 -1
- package/skills/roadmap-sync/SKILL.md +4 -4
- package/skills/roadmap-sync/agents/openai.yaml +2 -2
- package/skills/roadmap-update/SKILL.md +6 -5
- package/skills.json +15 -16
- package/src/config.js +7 -2
- package/src/io.js +2 -0
- package/src/parser/index.js +50 -0
- package/src/sync/index.js +60 -4
- package/src/validator/index.js +429 -13
|
@@ -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
|
@@ -28,7 +28,7 @@ npx skills add PapiScholz/roadmapsmith --skill '*' -a claude-code
|
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
This is the recommended Claude Code install path for native GUI slash commands such as `/roadmap`, `/roadmap-zero`, `/roadmap-maintain`, `/roadmap-status`, `/roadmap-init`, `/roadmap-generate`, `/roadmap-validate`, `/roadmap-update`, `/roadmap-audit`, and `/roadmap-setup`.
|
|
31
|
-
|
|
31
|
+
`roadmap-sync` is deprecated compatibility only; install the full bundle for new workflows and use `/roadmap-maintain` or `/roadmap-update`.
|
|
32
32
|
The skill bundle does not install the CLI and it does not create visible VS Code actions by itself.
|
|
33
33
|
The published `roadmapsmith` package/plugin surface now also ships the shared bundle files for both hosts (`skills.json`, `skills/*`, `.codex-plugin/plugin.json`, `.claude-plugin/plugin.json`) for downstream host installers, but consuming the CLI alone still does not auto-register native Codex or Claude GUI commands.
|
|
34
34
|
|
|
@@ -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 |
|
|
@@ -101,7 +114,11 @@ Use the lower-level commands only when you want manual control over generation,
|
|
|
101
114
|
| CI | Use disposable checkouts if you run `sync --audit`, because it still mutates the roadmap today. |
|
|
102
115
|
| Other hosts | Use the skill plus manual CLI commands. |
|
|
103
116
|
|
|
104
|
-
If Node is installed outside PATH, set `ROADMAPSMITH_NODE` to a working `node` executable before using the generated VS Code tasks.
|
|
117
|
+
If Node is installed outside PATH, set `ROADMAPSMITH_NODE` to a working `node` executable before using the generated VS Code tasks. If the globally installed `roadmapsmith` shim itself cannot resolve Node, bypass it in PowerShell with:
|
|
118
|
+
|
|
119
|
+
```powershell
|
|
120
|
+
& "C:\Program Files\nodejs\node.exe" "$env:APPDATA\npm\node_modules\roadmapsmith\bin\cli.js" <command>
|
|
121
|
+
```
|
|
105
122
|
|
|
106
123
|
---
|
|
107
124
|
|
|
@@ -119,6 +136,7 @@ roadmapsmith maintain [--project-root <path>] [--config <path>] [--roadmap-file
|
|
|
119
136
|
roadmapsmith init [--roadmap-file <path>] [--agents-file <path>] [--dry-run]
|
|
120
137
|
roadmapsmith generate [--project-root <path>] [--config <path>] [--roadmap-file <path>] [--dry-run] [--audit] [--full-regen]
|
|
121
138
|
roadmapsmith sync [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--dry-run] [--audit]
|
|
139
|
+
roadmapsmith update --task <stable-id> --evidence <text> [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--dry-run]
|
|
122
140
|
roadmapsmith validate [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--task <id|text>] [--json]
|
|
123
141
|
roadmapsmith status [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--json]
|
|
124
142
|
roadmapsmith doctor [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--json] # compatibility alias
|
|
@@ -176,13 +194,14 @@ The repo does not remove user-global skills automatically. Use the `doctor` outp
|
|
|
176
194
|
- Uses stable task IDs: `<!-- rs:task=<slug> -->`.
|
|
177
195
|
- Sync marks `[x]` only when validation passes.
|
|
178
196
|
- `sync --audit` currently runs sync and then prints a mismatch summary; it is not yet a dedicated read-only audit mode.
|
|
179
|
-
-
|
|
180
|
-
|
|
181
|
-
- 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.
|
|
182
199
|
- Validation failures in sync write warning lines:
|
|
183
200
|
- `- ⚠️ attempted but validation failed: <reason>` when there is concrete attempt evidence
|
|
184
201
|
- `- ⚠️ no implementation evidence found yet: <reason>` when there is not
|
|
185
202
|
- Preserves unmanaged markdown content by updating only the managed roadmap block.
|
|
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`.
|
|
204
|
+
- `update --task <id> --evidence <text>` writes only when the evidence validates at high confidence; otherwise the roadmap is unchanged.
|
|
186
205
|
|
|
187
206
|
## Defaults
|
|
188
207
|
|
package/bin/cli.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const readline = require('node:readline/promises');
|
|
7
|
-
const { parseArgv } = require('../src/utils');
|
|
7
|
+
const { ensureTrailingNewline, parseArgv } = require('../src/utils');
|
|
8
8
|
const { loadConfig, resolveRoadmapFile, resolveAgentsFile, loadPlugins, readUserConfig, resolveConfigPath } = require('../src/config');
|
|
9
9
|
const { readTextIfExists, writeText, printDryRunDiff } = require('../src/io');
|
|
10
10
|
const { buildSetupFiles, applySetupFiles, inspectHostSetup, parseHosts, assertSupportedEditor } = require('../src/host');
|
|
@@ -30,6 +30,7 @@ function printHelp() {
|
|
|
30
30
|
' roadmapsmith setup [--project-root <path>] [--config <path>] [--editor vscode] [--hosts <codex,claude>] [--dry-run]',
|
|
31
31
|
' roadmapsmith generate [--project-root <path>] [--config <path>] [--roadmap-file <path>] [--dry-run] [--audit] [--full-regen]',
|
|
32
32
|
' roadmapsmith sync [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--dry-run] [--audit]',
|
|
33
|
+
' roadmapsmith update --task <stable-id> --evidence <text> [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--dry-run]',
|
|
33
34
|
' roadmapsmith validate [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--task <id|text>] [--json]',
|
|
34
35
|
' roadmapsmith status [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--json]',
|
|
35
36
|
' roadmapsmith doctor [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--json] # compatibility alias'
|
|
@@ -44,8 +45,15 @@ function isEnabled(value) {
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
function formatResultLine(task, result) {
|
|
47
|
-
const
|
|
48
|
-
const
|
|
48
|
+
const diagnostics = Array.isArray(result.diagnostics) ? result.diagnostics : [];
|
|
49
|
+
const primaryError = diagnostics.find((item) => item.severity === 'error');
|
|
50
|
+
const warnings = diagnostics.filter((item) => item.severity === 'warning');
|
|
51
|
+
const status = primaryError
|
|
52
|
+
? `FAIL:${primaryError.code}`
|
|
53
|
+
: (result.passed ? 'PASS' : (warnings[0] ? `WARN:${warnings[0].code}` : 'FAIL'));
|
|
54
|
+
const parts = [...result.reasons];
|
|
55
|
+
warnings.forEach((item) => parts.push(`WARN:${item.code} ${item.message}`));
|
|
56
|
+
const reason = parts.length > 0 ? ` :: ${parts.join('; ')}` : '';
|
|
49
57
|
return `${status} [${task.id}] ${task.text}${reason}`;
|
|
50
58
|
}
|
|
51
59
|
|
|
@@ -215,6 +223,77 @@ function runSyncCommand(projectRoot, config, flags, options = {}) {
|
|
|
215
223
|
}
|
|
216
224
|
}
|
|
217
225
|
|
|
226
|
+
function addEvidenceToTask(content, task, evidenceText) {
|
|
227
|
+
const lines = String(content || '').split(/\r?\n/);
|
|
228
|
+
const evidenceLine = `${task.indent || ''} - Evidence: ${evidenceText}`;
|
|
229
|
+
if (Array.isArray(task.evidenceLines) && task.evidenceLines.length > 0) {
|
|
230
|
+
lines[task.evidenceLines[0].lineIndex] = evidenceLine;
|
|
231
|
+
return ensureTrailingNewline(lines.join('\n'));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const insertionIndex = task.warningLineIndex != null
|
|
235
|
+
? task.warningLineIndex
|
|
236
|
+
: task.lastChildLineIndex + 1;
|
|
237
|
+
lines.splice(insertionIndex, 0, evidenceLine);
|
|
238
|
+
return ensureTrailingNewline(lines.join('\n'));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function runUpdateCommand(projectRoot, config, flags) {
|
|
242
|
+
const hasTask = flags.task != null;
|
|
243
|
+
const hasEvidence = flags.evidence != null;
|
|
244
|
+
if (!hasTask && !hasEvidence) {
|
|
245
|
+
runSyncCommand(projectRoot, config, flags);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (!hasTask || !hasEvidence || Array.isArray(flags.task) || Array.isArray(flags.evidence)) {
|
|
249
|
+
throw new Error('update requires exactly one --task <stable-id> and one --evidence <text> value');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const taskId = String(flags.task).trim();
|
|
253
|
+
const evidenceText = String(flags.evidence).trim();
|
|
254
|
+
if (!taskId || !evidenceText || /[\r\n]/.test(evidenceText)) {
|
|
255
|
+
throw new Error('update requires a non-empty single-line --task and --evidence value');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
|
|
259
|
+
const content = readTextIfExists(roadmapFile);
|
|
260
|
+
if (content == null) {
|
|
261
|
+
throw new Error(`Roadmap not found: ${roadmapFile}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const parsedRoadmap = parseRoadmap(content);
|
|
265
|
+
const matches = tasksInManagedBlock(parsedRoadmap).filter((task) => task.id === taskId);
|
|
266
|
+
if (matches.length !== 1) {
|
|
267
|
+
throw new Error(matches.length === 0
|
|
268
|
+
? `Roadmap task not found: ${taskId}`
|
|
269
|
+
: `Roadmap task ID is ambiguous: ${taskId}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const draft = addEvidenceToTask(content, matches[0], evidenceText);
|
|
273
|
+
const draftTask = tasksInManagedBlock(parseRoadmap(draft)).find((task) => task.id === taskId);
|
|
274
|
+
const validationContext = buildValidationContext(projectRoot, config, loadPlugins(projectRoot, config.plugins));
|
|
275
|
+
const result = validateTasks([draftTask], validationContext, config, validationContext.plugins)[taskId];
|
|
276
|
+
const errors = (result.diagnostics || []).filter((item) => item.severity === 'error');
|
|
277
|
+
const suppliedEvidenceResolved = result.evidence.authoritative && result.evidence.authoritativeFiles.length > 0;
|
|
278
|
+
if (!suppliedEvidenceResolved || !result.passed || result.confidence !== 'high' || errors.length > 0) {
|
|
279
|
+
const reasons = result.reasons.length > 0 ? `: ${result.reasons.join('; ')}` : '';
|
|
280
|
+
throw new Error(`Task ${taskId} was not updated; supplied evidence must resolve in the repository and validate at high confidence${reasons}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const next = applySync(draft, [draftTask], { [taskId]: result });
|
|
284
|
+
const dryRun = isEnabled(flags['dry-run']);
|
|
285
|
+
const writeResult = writeText(roadmapFile, next, { dryRun });
|
|
286
|
+
if (dryRun) {
|
|
287
|
+
if (writeResult.changed) {
|
|
288
|
+
printDryRunDiff(roadmapFile, writeResult.before, writeResult.after);
|
|
289
|
+
} else {
|
|
290
|
+
console.log(`No changes for ${roadmapFile}`);
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
console.log(writeResult.changed ? `Updated ${roadmapFile}` : `No changes for ${roadmapFile}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
218
297
|
function printHumanStatus(payload) {
|
|
219
298
|
console.log('RoadmapSmith status\n');
|
|
220
299
|
console.log(`Project root: ${payload.projectRoot}`);
|
|
@@ -343,6 +422,8 @@ async function run() {
|
|
|
343
422
|
if (slashAction.id === 'audit') {
|
|
344
423
|
flags.audit = true;
|
|
345
424
|
effectiveCommand = 'sync';
|
|
425
|
+
} else if (slashAction.id === 'sync' && String(command).trim().toLowerCase() === '/roadmap-update') {
|
|
426
|
+
effectiveCommand = 'update';
|
|
346
427
|
} else {
|
|
347
428
|
effectiveCommand = slashAction.id;
|
|
348
429
|
}
|
|
@@ -411,6 +492,13 @@ async function run() {
|
|
|
411
492
|
return;
|
|
412
493
|
}
|
|
413
494
|
|
|
495
|
+
if (effectiveCommand === 'update') {
|
|
496
|
+
const projectRoot = path.resolve(String(flags['project-root'] || process.cwd()));
|
|
497
|
+
const config = loadConfig({ projectRoot, configPath: flags.config });
|
|
498
|
+
runUpdateCommand(projectRoot, config, flags);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
414
502
|
if (effectiveCommand === 'validate') {
|
|
415
503
|
const projectRoot = path.resolve(String(flags['project-root'] || process.cwd()));
|
|
416
504
|
const config = loadConfig({ projectRoot, configPath: flags.config });
|
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": {
|
package/skills/roadmap/SKILL.md
CHANGED
|
@@ -13,7 +13,7 @@ Use this command as the native discovery entrypoint for the shared RoadmapSmith
|
|
|
13
13
|
2. When working inside the RoadmapSmith repository itself and `roadmap-skill/bin/cli.js` exists, prefer the local engine:
|
|
14
14
|
- `node roadmap-skill/bin/cli.js /roadmap`
|
|
15
15
|
- on this Windows machine, prefer `C:\Program Files\nodejs\node.exe roadmap-skill/bin/cli.js /roadmap` if `node` is not in PATH
|
|
16
|
-
3. Otherwise, if the `roadmapsmith` CLI is available, you may run `roadmapsmith /roadmap` from the project root and use that output directly.
|
|
16
|
+
3. Otherwise, if the `roadmapsmith` CLI is available, you may run `roadmapsmith /roadmap` from the project root and use that output directly. If its global shim cannot resolve `node`, use `& "C:\Program Files\nodejs\node.exe" "$env:APPDATA\npm\node_modules\roadmapsmith\bin\cli.js" /roadmap`.
|
|
17
17
|
4. If the CLI is missing, provide the palette manually and explain the install path:
|
|
18
18
|
- `npm install -g roadmapsmith`
|
|
19
19
|
- `npx skills add PapiScholz/roadmapsmith --skill '*' -a claude-code`
|
|
@@ -12,7 +12,8 @@ Use this command when the repository already has code, tests, docs, or an existi
|
|
|
12
12
|
1. Prefer the local engine inside this repository:
|
|
13
13
|
- `node roadmap-skill/bin/cli.js maintain --project-root .`
|
|
14
14
|
- on this Windows machine, prefer `C:\Program Files\nodejs\node.exe roadmap-skill/bin/cli.js maintain --project-root .` if `node` is not in PATH
|
|
15
|
-
2. Otherwise prefer `roadmapsmith maintain --project-root .`.
|
|
15
|
+
2. Otherwise prefer `roadmapsmith maintain --project-root .`. If that global shim fails because `node` is not in PATH, run `& "C:\Program Files\nodejs\node.exe" "$env:APPDATA\npm\node_modules\roadmapsmith\bin\cli.js" maintain --project-root .`.
|
|
16
16
|
3. Treat this command as CLI-backed. Do not silently replace it with manual reasoning when the CLI is unavailable.
|
|
17
17
|
4. Mention that maintain runs preserve-first generate, sync, and audit in one invocation.
|
|
18
|
-
5.
|
|
18
|
+
5. After a successful maintain cycle, do not propose generate, sync, or audit separately unless the user needs manual control or inspection.
|
|
19
|
+
6. Mention that `roadmapsmith maintain --full-regen` or `roadmapsmith generate --full-regen` is the explicit destructive rebuild path when the user truly wants a full managed-block replacement.
|
|
@@ -12,6 +12,6 @@ Use this command to inspect whether the shared bundle, native host surfaces, CLI
|
|
|
12
12
|
1. Prefer the local engine inside this repository:
|
|
13
13
|
- `node roadmap-skill/bin/cli.js doctor --json --project-root .`
|
|
14
14
|
- on this Windows machine, prefer `C:\Program Files\nodejs\node.exe roadmap-skill/bin/cli.js doctor --json --project-root .` if `node` is not in PATH
|
|
15
|
-
2. Otherwise prefer `roadmapsmith doctor --json --project-root .`.
|
|
15
|
+
2. Otherwise prefer `roadmapsmith doctor --json --project-root .`. If its global shim cannot resolve `node`, use `& "C:\Program Files\nodejs\node.exe" "$env:APPDATA\npm\node_modules\roadmapsmith\bin\cli.js" doctor --json --project-root .`.
|
|
16
16
|
3. Parse and summarize the JSON output in plain language.
|
|
17
17
|
4. Explicitly call out missing commands or duplicate `/roadmap-sync` registration when doctor reports them.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: roadmap-sync
|
|
3
|
-
description:
|
|
3
|
+
description: DEPRECATED legacy root for RoadmapSmith slash workflows; use roadmap-maintain or roadmap-update.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# RoadmapSmith Legacy Root
|
|
6
|
+
# RoadmapSmith Legacy Root (Deprecated)
|
|
7
7
|
|
|
8
|
-
Use this skill when the host exposes or the user invokes `/roadmap-sync
|
|
8
|
+
Use this skill only when the host exposes or the user explicitly invokes `/roadmap-sync`. For new work, use `/roadmap-maintain` for the daily flow or `/roadmap-update` for evidence-backed completion.
|
|
9
9
|
|
|
10
10
|
## Required behavior
|
|
11
11
|
|
|
@@ -17,4 +17,4 @@ Use this skill when the host exposes or the user invokes `/roadmap-sync`, or whe
|
|
|
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.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"interface": {
|
|
3
|
-
"display_name": "Roadmap Sync",
|
|
4
|
-
"short_description": "
|
|
3
|
+
"display_name": "Roadmap Sync (Deprecated)",
|
|
4
|
+
"short_description": "DEPRECATED legacy root; use /roadmap-maintain or /roadmap-update.",
|
|
5
5
|
"default_prompt": "Prefer /roadmap, /roadmap-status, /roadmap-maintain, and /roadmap-update."
|
|
6
6
|
}
|
|
7
7
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: roadmap-update
|
|
3
|
-
description: Apply evidence-backed checklist sync through the RoadmapSmith CLI.
|
|
3
|
+
description: Apply evidence-backed checklist sync or complete one task with verified evidence through the RoadmapSmith CLI.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# RoadmapSmith Update
|
|
@@ -10,8 +10,9 @@ Use this command when the user wants the direct sync surface without routing thr
|
|
|
10
10
|
## Required behavior
|
|
11
11
|
|
|
12
12
|
1. Prefer the local engine inside this repository:
|
|
13
|
-
- `node roadmap-skill/bin/cli.js
|
|
14
|
-
- on this Windows machine, prefer `C:\Program Files\nodejs\node.exe roadmap-skill/bin/cli.js
|
|
15
|
-
2. Otherwise prefer `roadmapsmith
|
|
13
|
+
- `node roadmap-skill/bin/cli.js update --project-root .`
|
|
14
|
+
- on this Windows machine, prefer `C:\Program Files\nodejs\node.exe roadmap-skill/bin/cli.js update --project-root .` if `node` is not in PATH
|
|
15
|
+
2. Otherwise prefer `roadmapsmith update --project-root .`. If that global shim fails because `node` is not in PATH, run `& "C:\Program Files\nodejs\node.exe" "$env:APPDATA\npm\node_modules\roadmapsmith\bin\cli.js" update --project-root .`.
|
|
16
16
|
3. Explain that `/roadmap-update` is the visible namespaced sync command, while `/roadmap-sync <action>` remains legacy compatibility.
|
|
17
|
-
4. Keep the evidence-backed sync semantics unchanged:
|
|
17
|
+
4. Keep the evidence-backed sync semantics unchanged: no-argument update syncs the roadmap from repository evidence; it is not a full regeneration path.
|
|
18
|
+
5. To complete one task, run `roadmapsmith update --task <stable-id> --evidence "<single-line evidence>"`. It writes only after the supplied evidence validates at high confidence; use `--dry-run` to preview it.
|
package/skills.json
CHANGED
|
@@ -10,86 +10,85 @@
|
|
|
10
10
|
],
|
|
11
11
|
"usageExamples": [
|
|
12
12
|
"npx skills add PapiScholz/roadmapsmith --skill '*' -a claude-code",
|
|
13
|
-
"npx skills add PapiScholz/roadmapsmith --skill roadmap-sync",
|
|
14
13
|
"roadmapsmith setup",
|
|
15
14
|
"roadmapsmith zero",
|
|
16
15
|
"roadmapsmith maintain",
|
|
17
16
|
"roadmapsmith /roadmap",
|
|
18
|
-
"roadmapsmith
|
|
17
|
+
"roadmapsmith update --task p2-customer-history --evidence \"src/app/api/customers/route.ts, test/customers.test.js\"",
|
|
19
18
|
"roadmapsmith validate --json"
|
|
20
19
|
],
|
|
21
20
|
"install": {
|
|
22
21
|
"command": "npx skills add PapiScholz/roadmapsmith --skill '*' -a claude-code",
|
|
23
22
|
"source": "PapiScholz/roadmapsmith",
|
|
24
23
|
"skill": "*",
|
|
25
|
-
"notes": "Recommended Claude Code install path for native GUI slash commands like /roadmap, /roadmap-zero, /roadmap-maintain, /roadmap-status, /roadmap-init, /roadmap-generate, /roadmap-validate, /roadmap-update, /roadmap-audit, and /roadmap-setup.
|
|
24
|
+
"notes": "Recommended Claude Code install path for native GUI slash commands like /roadmap, /roadmap-zero, /roadmap-maintain, /roadmap-status, /roadmap-init, /roadmap-generate, /roadmap-validate, /roadmap-update, /roadmap-audit, and /roadmap-setup. DEPRECATED: /roadmap-sync remains only as a compatibility root for existing automation; use /roadmap-maintain or /roadmap-update for new workflows. Install the roadmapsmith CLI separately for actual command execution; roadmapsmith status is the visible readiness command and roadmapsmith doctor remains a compatibility alias. Then run /reload-skills and, if applicable, /reload-plugins. Codex native plugin installs use the repo/package .codex-plugin surface instead of this Claude-specific skills CLI path."
|
|
26
25
|
},
|
|
27
26
|
"skills": [
|
|
28
27
|
{
|
|
29
28
|
"name": "roadmap",
|
|
30
29
|
"path": "skills/roadmap",
|
|
31
30
|
"description": "Native slash palette for RoadmapSmith commands and recommended entrypoints across supported hosts.",
|
|
32
|
-
"version": "0.9.
|
|
31
|
+
"version": "0.9.29"
|
|
33
32
|
},
|
|
34
33
|
{
|
|
35
34
|
"name": "roadmap-zero",
|
|
36
35
|
"path": "skills/roadmap-zero",
|
|
37
36
|
"description": "Native slash entrypoint for the one-command Zero Mode CLI workflow.",
|
|
38
|
-
"version": "0.9.
|
|
37
|
+
"version": "0.9.29"
|
|
39
38
|
},
|
|
40
39
|
{
|
|
41
40
|
"name": "roadmap-maintain",
|
|
42
41
|
"path": "skills/roadmap-maintain",
|
|
43
42
|
"description": "Native slash entrypoint for the preserve-first generate + sync + audit flow.",
|
|
44
|
-
"version": "0.9.
|
|
43
|
+
"version": "0.9.29"
|
|
45
44
|
},
|
|
46
45
|
{
|
|
47
46
|
"name": "roadmap-status",
|
|
48
47
|
"path": "skills/roadmap-status",
|
|
49
48
|
"description": "Native slash readiness check grounded in roadmapsmith status JSON.",
|
|
50
|
-
"version": "0.9.
|
|
49
|
+
"version": "0.9.29"
|
|
51
50
|
},
|
|
52
51
|
{
|
|
53
52
|
"name": "roadmap-init",
|
|
54
53
|
"path": "skills/roadmap-init",
|
|
55
54
|
"description": "Native slash entrypoint for creating ROADMAP.md and AGENTS.md.",
|
|
56
|
-
"version": "0.9.
|
|
55
|
+
"version": "0.9.29"
|
|
57
56
|
},
|
|
58
57
|
{
|
|
59
58
|
"name": "roadmap-generate",
|
|
60
59
|
"path": "skills/roadmap-generate",
|
|
61
60
|
"description": "Native slash entrypoint for managed roadmap updates that require --full-regen before destructive replacement.",
|
|
62
|
-
"version": "0.9.
|
|
61
|
+
"version": "0.9.29"
|
|
63
62
|
},
|
|
64
63
|
{
|
|
65
64
|
"name": "roadmap-validate",
|
|
66
65
|
"path": "skills/roadmap-validate",
|
|
67
66
|
"description": "Native slash entrypoint for evidence-backed roadmap validation.",
|
|
68
|
-
"version": "0.9.
|
|
67
|
+
"version": "0.9.29"
|
|
69
68
|
},
|
|
70
69
|
{
|
|
71
70
|
"name": "roadmap-update",
|
|
72
71
|
"path": "skills/roadmap-update",
|
|
73
|
-
"description": "Native slash entrypoint for
|
|
74
|
-
"version": "0.9.
|
|
72
|
+
"description": "Native slash entrypoint for evidence-backed sync and verified single-task completion.",
|
|
73
|
+
"version": "0.9.29"
|
|
75
74
|
},
|
|
76
75
|
{
|
|
77
76
|
"name": "roadmap-sync",
|
|
78
77
|
"path": "skills/roadmap-sync",
|
|
79
|
-
"description": "
|
|
80
|
-
"version": "0.9.
|
|
78
|
+
"description": "DEPRECATED legacy compatibility root; use roadmap-maintain or roadmap-update.",
|
|
79
|
+
"version": "0.9.29"
|
|
81
80
|
},
|
|
82
81
|
{
|
|
83
82
|
"name": "roadmap-audit",
|
|
84
83
|
"path": "skills/roadmap-audit",
|
|
85
84
|
"description": "Native slash entrypoint for the current sync-plus-audit workflow.",
|
|
86
|
-
"version": "0.9.
|
|
85
|
+
"version": "0.9.29"
|
|
87
86
|
},
|
|
88
87
|
{
|
|
89
88
|
"name": "roadmap-setup",
|
|
90
89
|
"path": "skills/roadmap-setup",
|
|
91
90
|
"description": "Native slash entrypoint for generating RoadmapSmith host integration files.",
|
|
92
|
-
"version": "0.9.
|
|
91
|
+
"version": "0.9.29"
|
|
93
92
|
}
|
|
94
93
|
]
|
|
95
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,10 +159,32 @@ function applySync(content, parsedTasks, results) {
|
|
|
142
159
|
lines.splice(warningIndex, 1);
|
|
143
160
|
offset -= 1;
|
|
144
161
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
+
}
|
|
175
|
+
if (
|
|
176
|
+
result.staleEvidenceResolved &&
|
|
177
|
+
(!Array.isArray(task.evidenceLines) || task.evidenceLines.length === 0) &&
|
|
178
|
+
result.discoveredEvidence
|
|
179
|
+
) {
|
|
180
|
+
const insertionIndex = Math.max(
|
|
181
|
+
lineIndex + 1,
|
|
182
|
+
(task.lastChildLineIndex + offset) + 1
|
|
183
|
+
);
|
|
184
|
+
lines.splice(insertionIndex, 0, `${task.indent || ''} - Evidence: ${result.discoveredEvidence}`);
|
|
185
|
+
offset += 1;
|
|
186
|
+
}
|
|
187
|
+
} else if (warningIndex != null && warningIndex >= 0 && warningIndex < lines.length) {
|
|
149
188
|
const existingReason = normalizeWarningReason(lines[warningIndex]);
|
|
150
189
|
const newReason = reason || 'validation failed';
|
|
151
190
|
if (!shouldPreserveExistingWarning(existingReason, newReason)) {
|
|
@@ -155,6 +194,23 @@ function applySync(content, parsedTasks, results) {
|
|
|
155
194
|
lines.splice(lastChildLineIndex + 1, 0, warningText);
|
|
156
195
|
offset += 1;
|
|
157
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
|
+
}
|
|
158
214
|
}
|
|
159
215
|
|
|
160
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,10 +1593,70 @@ 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
|
|
|
1601
|
+
function diagnosticCodeForReason(reason) {
|
|
1602
|
+
const normalized = String(reason || '').toLowerCase();
|
|
1603
|
+
if (normalized.includes('missing referenced file') || normalized.includes('evidence file(s) not found')) {
|
|
1604
|
+
return 'MISSING_REFERENCE';
|
|
1605
|
+
}
|
|
1606
|
+
if (normalized.includes('missing test evidence')) {
|
|
1607
|
+
return 'NO_TEST';
|
|
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
|
+
}
|
|
1618
|
+
if (
|
|
1619
|
+
normalized.includes('no code, test, or artifact evidence found') ||
|
|
1620
|
+
normalized.includes('implementation task requires evidence line') ||
|
|
1621
|
+
normalized.includes('weak path') ||
|
|
1622
|
+
normalized.includes('file reference shows implementation location')
|
|
1623
|
+
) {
|
|
1624
|
+
return 'NOT_IMPLEMENTED';
|
|
1625
|
+
}
|
|
1626
|
+
return null;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
function buildDiagnostics(reasons, options = {}) {
|
|
1630
|
+
const diagnostics = [];
|
|
1631
|
+
const seen = new Set();
|
|
1632
|
+
for (const reason of Array.isArray(reasons) ? reasons : []) {
|
|
1633
|
+
const code = diagnosticCodeForReason(reason);
|
|
1634
|
+
if (!code || seen.has(code)) {
|
|
1635
|
+
continue;
|
|
1636
|
+
}
|
|
1637
|
+
seen.add(code);
|
|
1638
|
+
diagnostics.push({ code, severity: 'error', message: reason });
|
|
1639
|
+
}
|
|
1640
|
+
if (options.staleEvidence) {
|
|
1641
|
+
diagnostics.push({
|
|
1642
|
+
code: 'STALE_EVIDENCE',
|
|
1643
|
+
severity: 'warning',
|
|
1644
|
+
message: 'historical validation warning conflicts with fresh repository evidence'
|
|
1645
|
+
});
|
|
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
|
+
}
|
|
1651
|
+
return diagnostics;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
function buildDiscoveredEvidenceLine(evidence) {
|
|
1655
|
+
const files = unionArrays(evidence.codeFiles, evidence.testFiles)
|
|
1656
|
+
.sort((left, right) => left.localeCompare(right));
|
|
1657
|
+
return files.length > 0 ? files.join(', ') : null;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1293
1660
|
function validateTask(task, context, config, plugins) {
|
|
1294
1661
|
const {
|
|
1295
1662
|
paths: pathHints,
|
|
@@ -1302,6 +1669,8 @@ function validateTask(task, context, config, plugins) {
|
|
|
1302
1669
|
const standaloneFilenames = extractStandaloneFilenames(task.text);
|
|
1303
1670
|
const symbolHints = extractSymbolHints(task.text);
|
|
1304
1671
|
const authoritativeEvidence = evaluateAuthoritativeEvidence(task, context.pathHintResolver);
|
|
1672
|
+
const deterministicVerification = evaluateDeterministicVerification(task, context);
|
|
1673
|
+
const hasExplicitPendingItems = Array.isArray(task.explicitPendingItems) && task.explicitPendingItems.length > 0;
|
|
1305
1674
|
|
|
1306
1675
|
const filesFromPaths = findFilesByPathHints(pathHints, context.pathHintResolver);
|
|
1307
1676
|
const filesFromPurePathHints = findFilesByPathHints(purePathHints, context.pathHintResolver);
|
|
@@ -1378,7 +1747,8 @@ function validateTask(task, context, config, plugins) {
|
|
|
1378
1747
|
context.testFrameworks.length > 0 &&
|
|
1379
1748
|
isCodeTask(task.text) &&
|
|
1380
1749
|
!isDocTask(task.text) &&
|
|
1381
|
-
!isHttpExpectationTask(task.text)
|
|
1750
|
+
!isHttpExpectationTask(task.text) &&
|
|
1751
|
+
!(task.verifyLines || []).some((line) => ['contains', 'property', 'endpoints', 'behavior'].includes(parseVerificationFields(line.text).kind));
|
|
1382
1752
|
const configuredRules = Array.isArray(config.validators) ? config.validators : [];
|
|
1383
1753
|
const pluginRules = collectPluginContributions(plugins || [], 'registerValidators', context);
|
|
1384
1754
|
let overrideResult = null;
|
|
@@ -1412,6 +1782,18 @@ function validateTask(task, context, config, plugins) {
|
|
|
1412
1782
|
|
|
1413
1783
|
let uniqueReasons = Array.from(new Set(reasons));
|
|
1414
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
|
+
|
|
1415
1797
|
if (overrideResult) {
|
|
1416
1798
|
uniqueReasons = Array.isArray(overrideResult.reasons) ? Array.from(new Set(overrideResult.reasons)) : [];
|
|
1417
1799
|
}
|
|
@@ -1467,7 +1849,22 @@ function validateTask(task, context, config, plugins) {
|
|
|
1467
1849
|
// evidence, artifact evidence, or strong code+test threshold to pass.
|
|
1468
1850
|
// Already-checked tasks with found path hints are preserved via shouldPreserveCheckedTask.
|
|
1469
1851
|
const hasHighConfidenceImplementationEvidence = meetsStrongThreshold && evidence.code && evidence.test;
|
|
1470
|
-
|
|
1852
|
+
const hasFreshRepositoryEvidence = hasStrongEvidence || hasWeakEvidence;
|
|
1853
|
+
let staleEvidenceDetected = false;
|
|
1854
|
+
let staleEvidenceResolved = false;
|
|
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
|
+
}
|
|
1864
|
+
|
|
1865
|
+
if (task.warningText && !task.checked && hasFreshRepositoryEvidence && !authoritativeEvidence.passed) {
|
|
1866
|
+
staleEvidenceDetected = true;
|
|
1867
|
+
}
|
|
1471
1868
|
|
|
1472
1869
|
if (!passed && !task.checked && hasDirectReferencePass) {
|
|
1473
1870
|
const locationReason = 'file reference shows implementation location, not confirmed completion';
|
|
@@ -1476,19 +1873,31 @@ function validateTask(task, context, config, plugins) {
|
|
|
1476
1873
|
}
|
|
1477
1874
|
}
|
|
1478
1875
|
|
|
1479
|
-
//
|
|
1480
|
-
// explicitly confirms implementation. meetsStrongThreshold (token match) cannot override a
|
|
1481
|
-
// human/agent judgment that the feature is incomplete.
|
|
1876
|
+
// Historical warnings are only cleared by independent, high-confidence repository evidence.
|
|
1482
1877
|
if (task.warningText && !task.checked && passed && !authoritativeEvidence.passed) {
|
|
1483
|
-
passed
|
|
1484
|
-
|
|
1485
|
-
|
|
1878
|
+
if ((deterministicVerification.passed || hasHighConfidenceImplementationEvidence) && negativeSignalMatches.length === 0 && uniqueReasons.length === 0) {
|
|
1879
|
+
staleEvidenceResolved = true;
|
|
1880
|
+
} else {
|
|
1881
|
+
passed = false;
|
|
1882
|
+
if (uniqueReasons.length === 0) {
|
|
1883
|
+
uniqueReasons.push('validation failed');
|
|
1884
|
+
}
|
|
1486
1885
|
}
|
|
1487
1886
|
}
|
|
1488
1887
|
if (negativeSignalMatches.length > 0) {
|
|
1489
1888
|
passed = false;
|
|
1490
1889
|
}
|
|
1491
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
|
+
|
|
1492
1901
|
// Unchecked implementation tasks need explicit evidence or high-confidence implementation
|
|
1493
1902
|
// evidence. Weak token overlap, direct file references, or code-only matches are not enough.
|
|
1494
1903
|
if (
|
|
@@ -1497,6 +1906,7 @@ function validateTask(task, context, config, plugins) {
|
|
|
1497
1906
|
isImplementationTask(task.text) &&
|
|
1498
1907
|
!authoritativeEvidence.passed &&
|
|
1499
1908
|
!hasTrustedRuleEvidencePass &&
|
|
1909
|
+
!deterministicVerification.passed &&
|
|
1500
1910
|
!hasArtifactTaskPass &&
|
|
1501
1911
|
!hasHighConfidenceImplementationEvidence
|
|
1502
1912
|
) {
|
|
@@ -1533,17 +1943,23 @@ function validateTask(task, context, config, plugins) {
|
|
|
1533
1943
|
// Used by auditValidation to flag implementation tasks that pass solely via documentation.
|
|
1534
1944
|
const evidenceIsDocOnly = !evidence.code && !evidence.test && evidence.artifact && !isDocTask(task.text);
|
|
1535
1945
|
|
|
1946
|
+
const finalPassed = overrideResult ? (overrideResult.passed !== false && !hasExplicitPendingItems) : (passed && uniqueReasons.length === 0 && !hasExplicitPendingItems);
|
|
1536
1947
|
return {
|
|
1537
1948
|
taskId: task.id,
|
|
1538
|
-
passed:
|
|
1949
|
+
passed: finalPassed,
|
|
1539
1950
|
confidence,
|
|
1540
1951
|
reasons: uniqueReasons,
|
|
1952
|
+
diagnostics: buildDiagnostics(uniqueReasons, { staleEvidence: staleEvidenceDetected, extra: extraDiagnostics }),
|
|
1541
1953
|
evidence,
|
|
1542
1954
|
evidenceIsDocOnly,
|
|
1543
1955
|
requiresTest,
|
|
1544
1956
|
hasEvidence: hasStrongEvidence || hasWeakEvidence,
|
|
1545
1957
|
attempted,
|
|
1546
|
-
preservedCheckedState
|
|
1958
|
+
preservedCheckedState,
|
|
1959
|
+
staleEvidenceResolved,
|
|
1960
|
+
discoveredEvidence: staleEvidenceResolved ? buildDiscoveredEvidenceLine(evidence) : null,
|
|
1961
|
+
verificationRecipe: deterministicVerification.recipe,
|
|
1962
|
+
generatedTestEvidence: deterministicVerification.generatedTestEvidence || null
|
|
1547
1963
|
};
|
|
1548
1964
|
}
|
|
1549
1965
|
|