roadmapsmith 0.9.24 → 0.9.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +2 -2
- package/README.md +5 -3
- package/bin/cli.js +4 -3
- package/package.json +2 -2
- package/skills/roadmap-sync/agents/openai.yaml +1 -1
- package/skills.json +13 -13
- package/src/host.js +23 -4
- package/src/parser/index.js +21 -3
- package/src/slash.js +1 -1
- package/src/sync/index.js +19 -9
- package/src/validator/index.js +314 -78
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roadmapsmith",
|
|
3
|
-
"version": "0.9.
|
|
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.",
|
|
3
|
+
"version": "0.9.26",
|
|
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"
|
|
7
7
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roadmapsmith",
|
|
3
|
-
"version": "0.9.
|
|
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.",
|
|
3
|
+
"version": "0.9.26",
|
|
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"
|
|
7
7
|
},
|
package/README.md
CHANGED
|
@@ -120,7 +120,8 @@ roadmapsmith init [--roadmap-file <path>] [--agents-file <path>] [--dry-run]
|
|
|
120
120
|
roadmapsmith generate [--project-root <path>] [--config <path>] [--roadmap-file <path>] [--dry-run] [--audit] [--full-regen]
|
|
121
121
|
roadmapsmith sync [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--dry-run] [--audit]
|
|
122
122
|
roadmapsmith validate [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--task <id|text>] [--json]
|
|
123
|
-
roadmapsmith
|
|
123
|
+
roadmapsmith status [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--json]
|
|
124
|
+
roadmapsmith doctor [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--json] # compatibility alias
|
|
124
125
|
```
|
|
125
126
|
|
|
126
127
|
## Claude Code native slash commands
|
|
@@ -153,7 +154,7 @@ Then restart Codex, open the plugin directory, install `roadmapsmith` from the `
|
|
|
153
154
|
|
|
154
155
|
Codex native plugin support means install/enable discovery inside Codex. It is separate from Claude-specific `/reload-skills` behavior, and the VS Code task layer remains the fallback/manual workflow when you are not using the plugin directory.
|
|
155
156
|
|
|
156
|
-
`roadmapsmith
|
|
157
|
+
`roadmapsmith status --json` now reports native slash surfaces separately from the VS Code task layer (`doctor --json` remains a compatibility alias):
|
|
157
158
|
|
|
158
159
|
- `claudeGui`
|
|
159
160
|
- `claudeCli`
|
|
@@ -179,7 +180,8 @@ The repo does not remove user-global skills automatically. Use the `doctor` outp
|
|
|
179
180
|
- code OR test OR artifact evidence required.
|
|
180
181
|
- test evidence required for code tasks when test frameworks are detected.
|
|
181
182
|
- Validation failures in sync write warning lines:
|
|
182
|
-
- `- ⚠️ attempted but validation failed: <reason>`
|
|
183
|
+
- `- ⚠️ attempted but validation failed: <reason>` when there is concrete attempt evidence
|
|
184
|
+
- `- ⚠️ no implementation evidence found yet: <reason>` when there is not
|
|
183
185
|
- Preserves unmanaged markdown content by updating only the managed roadmap block.
|
|
184
186
|
|
|
185
187
|
## Defaults
|
package/bin/cli.js
CHANGED
|
@@ -31,7 +31,8 @@ function printHelp() {
|
|
|
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
33
|
' roadmapsmith validate [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--task <id|text>] [--json]',
|
|
34
|
-
' roadmapsmith
|
|
34
|
+
' roadmapsmith status [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--json]',
|
|
35
|
+
' roadmapsmith doctor [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--json] # compatibility alias'
|
|
35
36
|
].join('\n'));
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -245,7 +246,7 @@ function printHumanStatus(payload) {
|
|
|
245
246
|
function runStatusCommand(projectRoot, config, flags, options = {}) {
|
|
246
247
|
const roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
|
|
247
248
|
const agentsFile = resolveAgentsFile(projectRoot, config, flags['agents-file']);
|
|
248
|
-
const payload = inspectHostSetup(projectRoot, { roadmapFile, agentsFile });
|
|
249
|
+
const payload = inspectHostSetup(projectRoot, { roadmapFile, agentsFile, currentCliPath: __filename });
|
|
249
250
|
|
|
250
251
|
if (options.json) {
|
|
251
252
|
process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
|
|
@@ -443,7 +444,7 @@ async function run() {
|
|
|
443
444
|
return;
|
|
444
445
|
}
|
|
445
446
|
|
|
446
|
-
if (effectiveCommand === 'doctor') {
|
|
447
|
+
if (effectiveCommand === 'status' || effectiveCommand === 'doctor') {
|
|
447
448
|
const projectRoot = path.resolve(String(flags['project-root'] || process.cwd()));
|
|
448
449
|
let ok = true;
|
|
449
450
|
const jsonMode = isEnabled(flags.json);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roadmapsmith",
|
|
3
|
-
"version": "0.9.
|
|
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.",
|
|
3
|
+
"version": "0.9.26",
|
|
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": {
|
|
7
7
|
"roadmapsmith": "bin/cli.js"
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"interface": {
|
|
3
3
|
"display_name": "Roadmap Sync",
|
|
4
4
|
"short_description": "Legacy root plus policy for evidence-backed ROADMAP.md slash workflows.",
|
|
5
|
-
"default_prompt": "Prefer /roadmap
|
|
5
|
+
"default_prompt": "Prefer /roadmap, /roadmap-status, /roadmap-maintain, and /roadmap-update."
|
|
6
6
|
}
|
|
7
7
|
}
|
package/skills.json
CHANGED
|
@@ -22,74 +22,74 @@
|
|
|
22
22
|
"command": "npx skills add PapiScholz/roadmapsmith --skill '*' -a claude-code",
|
|
23
23
|
"source": "PapiScholz/roadmapsmith",
|
|
24
24
|
"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. The legacy /roadmap-sync root remains available for compatibility, especially as /roadmap-sync <action>. Install the roadmapsmith CLI separately for actual command execution
|
|
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. The legacy /roadmap-sync root remains available for compatibility, especially as /roadmap-sync <action>. 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
26
|
},
|
|
27
27
|
"skills": [
|
|
28
28
|
{
|
|
29
29
|
"name": "roadmap",
|
|
30
30
|
"path": "skills/roadmap",
|
|
31
31
|
"description": "Native slash palette for RoadmapSmith commands and recommended entrypoints across supported hosts.",
|
|
32
|
-
"version": "0.9.
|
|
32
|
+
"version": "0.9.26"
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"name": "roadmap-zero",
|
|
36
36
|
"path": "skills/roadmap-zero",
|
|
37
37
|
"description": "Native slash entrypoint for the one-command Zero Mode CLI workflow.",
|
|
38
|
-
"version": "0.9.
|
|
38
|
+
"version": "0.9.26"
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
41
|
"name": "roadmap-maintain",
|
|
42
42
|
"path": "skills/roadmap-maintain",
|
|
43
43
|
"description": "Native slash entrypoint for the preserve-first generate + sync + audit flow.",
|
|
44
|
-
"version": "0.9.
|
|
44
|
+
"version": "0.9.26"
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
47
|
"name": "roadmap-status",
|
|
48
48
|
"path": "skills/roadmap-status",
|
|
49
|
-
"description": "Native slash readiness check grounded in roadmapsmith
|
|
50
|
-
"version": "0.9.
|
|
49
|
+
"description": "Native slash readiness check grounded in roadmapsmith status JSON.",
|
|
50
|
+
"version": "0.9.26"
|
|
51
51
|
},
|
|
52
52
|
{
|
|
53
53
|
"name": "roadmap-init",
|
|
54
54
|
"path": "skills/roadmap-init",
|
|
55
55
|
"description": "Native slash entrypoint for creating ROADMAP.md and AGENTS.md.",
|
|
56
|
-
"version": "0.9.
|
|
56
|
+
"version": "0.9.26"
|
|
57
57
|
},
|
|
58
58
|
{
|
|
59
59
|
"name": "roadmap-generate",
|
|
60
60
|
"path": "skills/roadmap-generate",
|
|
61
61
|
"description": "Native slash entrypoint for managed roadmap updates that require --full-regen before destructive replacement.",
|
|
62
|
-
"version": "0.9.
|
|
62
|
+
"version": "0.9.26"
|
|
63
63
|
},
|
|
64
64
|
{
|
|
65
65
|
"name": "roadmap-validate",
|
|
66
66
|
"path": "skills/roadmap-validate",
|
|
67
67
|
"description": "Native slash entrypoint for evidence-backed roadmap validation.",
|
|
68
|
-
"version": "0.9.
|
|
68
|
+
"version": "0.9.26"
|
|
69
69
|
},
|
|
70
70
|
{
|
|
71
71
|
"name": "roadmap-update",
|
|
72
72
|
"path": "skills/roadmap-update",
|
|
73
73
|
"description": "Native slash entrypoint for applying evidence-backed checklist sync.",
|
|
74
|
-
"version": "0.9.
|
|
74
|
+
"version": "0.9.26"
|
|
75
75
|
},
|
|
76
76
|
{
|
|
77
77
|
"name": "roadmap-sync",
|
|
78
78
|
"path": "skills/roadmap-sync",
|
|
79
79
|
"description": "Legacy namespaced root plus policy guidance for RoadmapSmith slash workflows.",
|
|
80
|
-
"version": "0.9.
|
|
80
|
+
"version": "0.9.26"
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
83
|
"name": "roadmap-audit",
|
|
84
84
|
"path": "skills/roadmap-audit",
|
|
85
85
|
"description": "Native slash entrypoint for the current sync-plus-audit workflow.",
|
|
86
|
-
"version": "0.9.
|
|
86
|
+
"version": "0.9.26"
|
|
87
87
|
},
|
|
88
88
|
{
|
|
89
89
|
"name": "roadmap-setup",
|
|
90
90
|
"path": "skills/roadmap-setup",
|
|
91
91
|
"description": "Native slash entrypoint for generating RoadmapSmith host integration files.",
|
|
92
|
-
"version": "0.9.
|
|
92
|
+
"version": "0.9.26"
|
|
93
93
|
}
|
|
94
94
|
]
|
|
95
95
|
}
|
package/src/host.js
CHANGED
|
@@ -921,7 +921,7 @@ function renderVsCodeLauncher() {
|
|
|
921
921
|
'function explain() {',
|
|
922
922
|
' console.log(\'RoadmapSmith layers:\\n\');',
|
|
923
923
|
' console.log(\'1. The roadmap-sync skill guides the agent. It does not add VS Code buttons or install the CLI.\');',
|
|
924
|
-
' console.log(\'2. The roadmapsmith CLI executes zero/maintain plus init/generate/validate/sync/setup/
|
|
924
|
+
' console.log(\'2. The roadmapsmith CLI executes zero/maintain plus init/generate/validate/sync/setup/status, with doctor kept as a compatibility alias and --full-regen reserved for destructive replacement.\');',
|
|
925
925
|
' console.log(\'3. roadmapsmith setup makes the CLI visible in VS Code through tasks and optional Claude hook wiring.\\n\');',
|
|
926
926
|
' console.log(\'Typical VS Code workflow:\');',
|
|
927
927
|
' console.log(\'- Run "RoadmapSmith: Status" to inspect readiness.\');',
|
|
@@ -1020,7 +1020,7 @@ function renderVsCodeLauncher() {
|
|
|
1020
1020
|
'}',
|
|
1021
1021
|
'',
|
|
1022
1022
|
'function status() {',
|
|
1023
|
-
' const result = runCli([\'
|
|
1023
|
+
' const result = runCli([\'status\', \'--project-root\', PROJECT_ROOT, \'--json\'], { capture: true, allowMissingCli: true });',
|
|
1024
1024
|
' if (!result || result.missingCli) {',
|
|
1025
1025
|
' printMissingCliStatus();',
|
|
1026
1026
|
' return;',
|
|
@@ -1143,7 +1143,26 @@ function findGlobalRoadmapsmith() {
|
|
|
1143
1143
|
return findCommandPath('roadmapsmith');
|
|
1144
1144
|
}
|
|
1145
1145
|
|
|
1146
|
-
function
|
|
1146
|
+
function isRoadmapsmithCliEntrypoint(filePath) {
|
|
1147
|
+
const normalized = String(filePath || '').replace(/\\/g, '/').toLowerCase();
|
|
1148
|
+
if (!normalized) {
|
|
1149
|
+
return false;
|
|
1150
|
+
}
|
|
1151
|
+
return normalized.endsWith('/roadmap-skill/bin/cli.js')
|
|
1152
|
+
|| normalized.endsWith('/roadmapsmith/bin/cli.js')
|
|
1153
|
+
|| /(?:^|[\\/])roadmapsmith(?:\.(?:cmd|exe|bat|ps1))?$/.test(normalized);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function detectCliResolution(projectRoot, options = {}) {
|
|
1157
|
+
const currentCliPath = options.currentCliPath || process.argv[1] || null;
|
|
1158
|
+
if (currentCliPath && isRoadmapsmithCliEntrypoint(currentCliPath)) {
|
|
1159
|
+
return {
|
|
1160
|
+
ready: true,
|
|
1161
|
+
kind: 'current-process',
|
|
1162
|
+
path: currentCliPath
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1147
1166
|
const workspaceDevCli = path.join(projectRoot, 'roadmap-skill', 'bin', 'cli.js');
|
|
1148
1167
|
if (fs.existsSync(workspaceDevCli)) {
|
|
1149
1168
|
return {
|
|
@@ -1244,7 +1263,7 @@ function inspectHostSetup(projectRoot, options = {}) {
|
|
|
1244
1263
|
const roadmapFile = options.roadmapFile;
|
|
1245
1264
|
const agentsFile = options.agentsFile;
|
|
1246
1265
|
const runtime = detectNodeRuntime(options.env || process.env);
|
|
1247
|
-
const cli = detectCliResolution(projectRoot);
|
|
1266
|
+
const cli = detectCliResolution(projectRoot, { currentCliPath: options.currentCliPath });
|
|
1248
1267
|
const vscode = inspectVsCodeTasks(projectRoot);
|
|
1249
1268
|
const claude = inspectClaudeSetup(projectRoot);
|
|
1250
1269
|
const bundle = inspectSharedBundleSurface();
|
package/src/parser/index.js
CHANGED
|
@@ -4,7 +4,12 @@ const { slugify } = require('../utils');
|
|
|
4
4
|
|
|
5
5
|
const MANAGED_START = '<!-- rs:managed:start -->';
|
|
6
6
|
const MANAGED_END = '<!-- rs:managed:end -->';
|
|
7
|
-
const WARNING_PREFIX = '⚠️
|
|
7
|
+
const WARNING_PREFIX = '⚠️';
|
|
8
|
+
const WARNING_REASON_PREFIXES = [
|
|
9
|
+
'attempted but validation failed:',
|
|
10
|
+
'no implementation evidence found yet:',
|
|
11
|
+
'validation failed:'
|
|
12
|
+
];
|
|
8
13
|
|
|
9
14
|
function getIndentWidth(text) {
|
|
10
15
|
return String(text || '').replace(/\t/g, ' ').length;
|
|
@@ -86,7 +91,14 @@ function parseEvidenceLine(content) {
|
|
|
86
91
|
|
|
87
92
|
function parseWarningLine(content) {
|
|
88
93
|
if (!content.startsWith(WARNING_PREFIX)) return null;
|
|
89
|
-
|
|
94
|
+
let normalized = content.slice(WARNING_PREFIX.length).trim();
|
|
95
|
+
for (const prefix of WARNING_REASON_PREFIXES) {
|
|
96
|
+
if (normalized.startsWith(prefix)) {
|
|
97
|
+
normalized = normalized.slice(prefix.length).trim();
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return normalized;
|
|
90
102
|
}
|
|
91
103
|
|
|
92
104
|
function parseBlockedByLine(content) {
|
|
@@ -98,6 +110,7 @@ function parseRoadmap(content) {
|
|
|
98
110
|
const lines = String(content || '').split(/\r?\n/);
|
|
99
111
|
const managedRange = findManagedRange(lines);
|
|
100
112
|
const tasks = [];
|
|
113
|
+
const implicitIdCounts = new Map();
|
|
101
114
|
let section = '';
|
|
102
115
|
|
|
103
116
|
for (let index = 0; index < lines.length; index += 1) {
|
|
@@ -162,7 +175,12 @@ function parseRoadmap(content) {
|
|
|
162
175
|
}
|
|
163
176
|
}
|
|
164
177
|
|
|
165
|
-
const
|
|
178
|
+
const baseId = markerId || slugify(text);
|
|
179
|
+
const nextImplicitCount = markerId ? 1 : (implicitIdCounts.get(baseId) || 0) + 1;
|
|
180
|
+
if (!markerId) {
|
|
181
|
+
implicitIdCounts.set(baseId, nextImplicitCount);
|
|
182
|
+
}
|
|
183
|
+
const id = markerId || (nextImplicitCount === 1 ? baseId : `${baseId}-${nextImplicitCount}`);
|
|
166
184
|
tasks.push({
|
|
167
185
|
id,
|
|
168
186
|
text,
|
package/src/slash.js
CHANGED
|
@@ -16,7 +16,7 @@ const SLASH_ACTIONS = [
|
|
|
16
16
|
{
|
|
17
17
|
id: 'status',
|
|
18
18
|
description: 'Inspect CLI, roadmap, VS Code task, Codex, and Claude readiness.',
|
|
19
|
-
classicCliExample: 'roadmapsmith
|
|
19
|
+
classicCliExample: 'roadmapsmith status --json',
|
|
20
20
|
taskLabel: 'RoadmapSmith: Status'
|
|
21
21
|
},
|
|
22
22
|
{
|
package/src/sync/index.js
CHANGED
|
@@ -3,14 +3,21 @@
|
|
|
3
3
|
const { parseRoadmap } = require('../parser');
|
|
4
4
|
const { ensureTrailingNewline } = require('../utils');
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const ATTEMPTED_WARNING_REASON_PREFIX = 'attempted but validation failed:';
|
|
7
|
+
const NO_EVIDENCE_WARNING_REASON_PREFIX = 'no implementation evidence found yet:';
|
|
8
|
+
const WARNING_REASON_PREFIXES = [
|
|
9
|
+
ATTEMPTED_WARNING_REASON_PREFIX,
|
|
10
|
+
NO_EVIDENCE_WARNING_REASON_PREFIX,
|
|
11
|
+
'validation failed:'
|
|
12
|
+
];
|
|
7
13
|
|
|
8
14
|
function setChecklistState(line, checked) {
|
|
9
15
|
return line.replace(/- \[( |x|X)\]/, `- [${checked ? 'x' : ' '}]`);
|
|
10
16
|
}
|
|
11
17
|
|
|
12
|
-
function formatWarning(indent, reason) {
|
|
13
|
-
|
|
18
|
+
function formatWarning(indent, reason, attempted) {
|
|
19
|
+
const prefix = attempted ? ATTEMPTED_WARNING_REASON_PREFIX : NO_EVIDENCE_WARNING_REASON_PREFIX;
|
|
20
|
+
return `${indent} - ⚠️ ${prefix} ${reason}`;
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
function isWhitespaceCharacter(char) {
|
|
@@ -72,9 +79,12 @@ function normalizeWarningReason(reason) {
|
|
|
72
79
|
}
|
|
73
80
|
|
|
74
81
|
normalized = stripLeadingWarningMarker(normalized).trim();
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
82
|
+
for (const prefix of WARNING_REASON_PREFIXES) {
|
|
83
|
+
const prefixIndex = normalized.indexOf(prefix);
|
|
84
|
+
if (prefixIndex >= 0) {
|
|
85
|
+
normalized = normalized.slice(prefixIndex + prefix.length).trim();
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
78
88
|
}
|
|
79
89
|
|
|
80
90
|
return normalized;
|
|
@@ -122,12 +132,12 @@ function applySync(content, parsedTasks, results) {
|
|
|
122
132
|
lines[lineIndex] = setChecklistState(lines[lineIndex], result.passed);
|
|
123
133
|
|
|
124
134
|
const reason = normalizeWarningReasons(result.reasons).join('; ');
|
|
125
|
-
const warningText = formatWarning(task.indent || '', reason || 'validation failed');
|
|
135
|
+
const warningText = formatWarning(task.indent || '', reason || 'validation failed', result.attempted);
|
|
126
136
|
const hasWarning = task.warningLineIndex != null;
|
|
127
137
|
const warningIndex = hasWarning ? task.warningLineIndex + offset : null;
|
|
128
138
|
const lastChildLineIndex = (task.lastChildLineIndex != null ? task.lastChildLineIndex : task.lineIndex) + offset;
|
|
129
139
|
|
|
130
|
-
if (result.passed
|
|
140
|
+
if (result.passed) {
|
|
131
141
|
if (warningIndex != null && warningIndex >= 0 && warningIndex < lines.length) {
|
|
132
142
|
lines.splice(warningIndex, 1);
|
|
133
143
|
offset -= 1;
|
|
@@ -136,7 +146,7 @@ function applySync(content, parsedTasks, results) {
|
|
|
136
146
|
}
|
|
137
147
|
|
|
138
148
|
if (warningIndex != null && warningIndex >= 0 && warningIndex < lines.length) {
|
|
139
|
-
const existingReason = lines[warningIndex]
|
|
149
|
+
const existingReason = normalizeWarningReason(lines[warningIndex]);
|
|
140
150
|
const newReason = reason || 'validation failed';
|
|
141
151
|
if (!shouldPreserveExistingWarning(existingReason, newReason)) {
|
|
142
152
|
lines[warningIndex] = warningText;
|
package/src/validator/index.js
CHANGED
|
@@ -197,18 +197,9 @@ function hasFileExtension(token) {
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
function isLikelyPath(token) {
|
|
200
|
-
if (
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
// Bare "/" or "./" with nothing after is not a real path (e.g. "API / ESC-POS" → "/")
|
|
204
|
-
return /[A-Za-z0-9_]/.test(token);
|
|
205
|
-
}
|
|
206
|
-
if (hasFileExtension(token)) return true;
|
|
207
|
-
if (KNOWN_PATH_ROOTS.some((root) => token.startsWith(root))) return true;
|
|
208
|
-
// The ">= 2 slashes" rule was intentionally removed: it caused conceptual slash phrases
|
|
209
|
-
// like "code/test/artifact" or "build/test/deploy" to be treated as file paths.
|
|
210
|
-
// Real multi-segment paths are caught by the extension or known-root rules above.
|
|
211
|
-
return false;
|
|
200
|
+
if (isRealFilePath(token)) return true;
|
|
201
|
+
// Preserve the legacy extension-only fallback for standalone path-looking tokens.
|
|
202
|
+
return hasFileExtension(token);
|
|
212
203
|
}
|
|
213
204
|
|
|
214
205
|
// Matches standalone filenames without a slash — e.g. "roadmap-skill.config.json",
|
|
@@ -230,21 +221,83 @@ function hasKnownFileExtension(token) {
|
|
|
230
221
|
return KNOWN_FILE_EXTENSIONS.has(token.slice(lastDot).toLowerCase());
|
|
231
222
|
}
|
|
232
223
|
|
|
224
|
+
function startsWithKnownPathRoot(token) {
|
|
225
|
+
const normalized = String(token || '').replace(/\\/g, '/').toLowerCase();
|
|
226
|
+
return KNOWN_PATH_ROOTS.some((root) => normalized.startsWith(root.toLowerCase()));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function isHttpRouteToken(token) {
|
|
230
|
+
const normalized = String(token || '').trim();
|
|
231
|
+
if (!normalized) return false;
|
|
232
|
+
if (/^(GET|POST|PUT|PATCH|DELETE)\s+\/\S+$/i.test(normalized)) {
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
return /^\/api\//i.test(normalized);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function isMimeTypeToken(token) {
|
|
239
|
+
return /^[A-Za-z0-9.+-]+\/[A-Za-z0-9.+-]+$/.test(String(token || '').trim());
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function looksLikeFormulaToken(token) {
|
|
243
|
+
return /[=×÷]/.test(String(token || '').trim());
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function isRealFilePath(token) {
|
|
247
|
+
const normalized = String(token || '').trim().replace(/\\/g, '/');
|
|
248
|
+
if (!normalized) return false;
|
|
249
|
+
if (normalized.includes('*') || normalized.includes('?')) return false;
|
|
250
|
+
if (/\s/.test(normalized)) return false;
|
|
251
|
+
if (looksLikeFormulaToken(normalized)) return false;
|
|
252
|
+
if (isHttpRouteToken(normalized)) return false;
|
|
253
|
+
|
|
254
|
+
const looksLikePath =
|
|
255
|
+
hasKnownFileExtension(normalized) ||
|
|
256
|
+
startsWithKnownPathRoot(normalized) ||
|
|
257
|
+
/^\.{1,2}\//.test(normalized) ||
|
|
258
|
+
/^\//.test(normalized);
|
|
259
|
+
if (!looksLikePath) return false;
|
|
260
|
+
|
|
261
|
+
if (!hasKnownFileExtension(normalized) && !startsWithKnownPathRoot(normalized) && isMimeTypeToken(normalized)) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (/^\.{1,2}\/|^\//.test(normalized)) {
|
|
266
|
+
return /[A-Za-z0-9_]/.test(normalized);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
|
|
233
272
|
function isAsciiAlphaNumeric(char) {
|
|
234
273
|
if (!char || char.length === 0) return false;
|
|
235
274
|
const code = char.charCodeAt(0);
|
|
236
275
|
return (code >= 48 && code <= 57) || (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
|
|
237
276
|
}
|
|
238
277
|
|
|
239
|
-
function isPathTokenCharacter(char) {
|
|
240
|
-
|
|
278
|
+
function isPathTokenCharacter(char, current) {
|
|
279
|
+
if (char === '~') {
|
|
280
|
+
return !current;
|
|
281
|
+
}
|
|
282
|
+
return isAsciiAlphaNumeric(char) || char === '.' || char === '_' || char === '-' || char === '/' || char === '\\' || char === ':';
|
|
241
283
|
}
|
|
242
284
|
|
|
243
285
|
function stripTrailingPathPunctuation(token) {
|
|
244
286
|
let result = String(token || '');
|
|
245
287
|
while (result.length > 0) {
|
|
246
288
|
const lastChar = result[result.length - 1];
|
|
247
|
-
if (
|
|
289
|
+
if (
|
|
290
|
+
lastChar !== '.' &&
|
|
291
|
+
lastChar !== ',' &&
|
|
292
|
+
lastChar !== ';' &&
|
|
293
|
+
lastChar !== ':' &&
|
|
294
|
+
lastChar !== '!' &&
|
|
295
|
+
lastChar !== '?' &&
|
|
296
|
+
lastChar !== ')' &&
|
|
297
|
+
lastChar !== ']' &&
|
|
298
|
+
lastChar !== '>' &&
|
|
299
|
+
lastChar !== '`'
|
|
300
|
+
) {
|
|
248
301
|
break;
|
|
249
302
|
}
|
|
250
303
|
result = result.slice(0, -1);
|
|
@@ -255,22 +308,41 @@ function stripTrailingPathPunctuation(token) {
|
|
|
255
308
|
function collectPathishTokens(text) {
|
|
256
309
|
const tokens = [];
|
|
257
310
|
let current = '';
|
|
311
|
+
let tokenStart = -1;
|
|
258
312
|
const source = String(text || '');
|
|
259
313
|
for (let index = 0; index < source.length; index += 1) {
|
|
260
314
|
const char = source[index];
|
|
261
|
-
if (isPathTokenCharacter(char)) {
|
|
315
|
+
if (isPathTokenCharacter(char, current)) {
|
|
316
|
+
if (!current) {
|
|
317
|
+
tokenStart = index;
|
|
318
|
+
}
|
|
262
319
|
current += char;
|
|
263
320
|
continue;
|
|
264
321
|
}
|
|
265
322
|
if (current) {
|
|
266
|
-
|
|
323
|
+
const value = stripTrailingPathPunctuation(current);
|
|
324
|
+
if (value) {
|
|
325
|
+
tokens.push({
|
|
326
|
+
value,
|
|
327
|
+
start: tokenStart,
|
|
328
|
+
end: tokenStart + value.length
|
|
329
|
+
});
|
|
330
|
+
}
|
|
267
331
|
current = '';
|
|
332
|
+
tokenStart = -1;
|
|
268
333
|
}
|
|
269
334
|
}
|
|
270
335
|
if (current) {
|
|
271
|
-
|
|
336
|
+
const value = stripTrailingPathPunctuation(current);
|
|
337
|
+
if (value) {
|
|
338
|
+
tokens.push({
|
|
339
|
+
value,
|
|
340
|
+
start: tokenStart,
|
|
341
|
+
end: tokenStart + value.length
|
|
342
|
+
});
|
|
343
|
+
}
|
|
272
344
|
}
|
|
273
|
-
return tokens
|
|
345
|
+
return tokens;
|
|
274
346
|
}
|
|
275
347
|
|
|
276
348
|
// LINE_REF_RE matches "path/file.ext:NN" or "path/file.ext:NN-MM" — indicates WHERE
|
|
@@ -278,46 +350,142 @@ function collectPathishTokens(text) {
|
|
|
278
350
|
// to lineReferenceHints and excluded from hasDirectReferencePass scoring.
|
|
279
351
|
const LINE_REF_RE = /^(.+?):(\d+)(?:-\d+)?$/;
|
|
280
352
|
|
|
353
|
+
function normalizePathCandidateToken(rawToken) {
|
|
354
|
+
const stripped = stripTrailingPathPunctuation(String(rawToken || '').trim());
|
|
355
|
+
if (!stripped) {
|
|
356
|
+
return '';
|
|
357
|
+
}
|
|
358
|
+
const normalized = stripped.replace(/\\/g, '/');
|
|
359
|
+
if (/^~\//.test(normalized)) {
|
|
360
|
+
return normalized;
|
|
361
|
+
}
|
|
362
|
+
return normalized.replace(/^~(?=\/)/, '~');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function isExternalPathToken(token) {
|
|
366
|
+
return /^~\//.test(String(token || '').trim().replace(/\\/g, '/'));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function classifyExplicitPathCandidate(rawToken) {
|
|
370
|
+
const clean = normalizePathCandidateToken(rawToken);
|
|
371
|
+
if (!clean || clean.includes('*') || clean.includes('?')) {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const lineMatch = LINE_REF_RE.exec(clean);
|
|
376
|
+
if (lineMatch) {
|
|
377
|
+
const linePath = normalizePathCandidateToken(lineMatch[1]);
|
|
378
|
+
if (isExternalPathToken(linePath)) {
|
|
379
|
+
return { path: linePath, kind: 'external', isLineReference: true };
|
|
380
|
+
}
|
|
381
|
+
if (isRealFilePath(linePath)) {
|
|
382
|
+
return { path: linePath, kind: 'repo', isLineReference: true };
|
|
383
|
+
}
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (isExternalPathToken(clean)) {
|
|
388
|
+
return { path: clean, kind: 'external', isLineReference: false };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (!isRealFilePath(clean)) {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return { path: clean, kind: 'repo', isLineReference: false };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function addClassifiedPath(classified, results, externalPaths, lineReferenceHints) {
|
|
399
|
+
if (!classified) return;
|
|
400
|
+
if (classified.kind === 'external') {
|
|
401
|
+
externalPaths.add(classified.path);
|
|
402
|
+
} else {
|
|
403
|
+
results.add(classified.path);
|
|
404
|
+
if (classified.isLineReference) {
|
|
405
|
+
lineReferenceHints.add(classified.path);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function findHttpRequestRouteRanges(text) {
|
|
411
|
+
const ranges = [];
|
|
412
|
+
const pattern = /\b(?:GET|POST|PUT|PATCH|DELETE)\s+(\/[^\s`]+)/gi;
|
|
413
|
+
let match = pattern.exec(String(text || ''));
|
|
414
|
+
while (match) {
|
|
415
|
+
const routeToken = match[1];
|
|
416
|
+
const routeStart = match.index + match[0].length - routeToken.length;
|
|
417
|
+
ranges.push({ start: routeStart, end: routeStart + routeToken.length });
|
|
418
|
+
match = pattern.exec(String(text || ''));
|
|
419
|
+
}
|
|
420
|
+
return ranges;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function isTokenInsideRanges(token, ranges) {
|
|
424
|
+
return ranges.some((range) => token.start >= range.start && token.end <= range.end);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function addPathTokensFromPlainText(text, results, externalPaths, lineReferenceHints) {
|
|
428
|
+
const ignoredRanges = findHttpRequestRouteRanges(text);
|
|
429
|
+
for (const token of collectPathishTokens(text)) {
|
|
430
|
+
if (!token.value.includes('/') && !isExternalPathToken(token.value)) {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (isTokenInsideRanges(token, ignoredRanges)) {
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
addClassifiedPath(classifyExplicitPathCandidate(token.value), results, externalPaths, lineReferenceHints);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function addPathTokensFromBacktickSpan(text, results, externalPaths, lineReferenceHints) {
|
|
441
|
+
const wholeSpan = classifyExplicitPathCandidate(text);
|
|
442
|
+
if (wholeSpan) {
|
|
443
|
+
addClassifiedPath(wholeSpan, results, externalPaths, lineReferenceHints);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (!/[;,]/.test(text)) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
for (const part of text.split(/[;,]/)) {
|
|
452
|
+
addClassifiedPath(classifyExplicitPathCandidate(part), results, externalPaths, lineReferenceHints);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
281
456
|
function extractExplicitPaths(text) {
|
|
282
457
|
const results = new Set();
|
|
458
|
+
const externalPaths = new Set();
|
|
283
459
|
const lineReferenceHints = new Set();
|
|
460
|
+
const source = String(text || '');
|
|
461
|
+
let cursor = 0;
|
|
284
462
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
// Require a slash or a known file extension — rejects property access like err.message,
|
|
291
|
-
// fs.readFileSync, error.stack (whose extensions are not in KNOWN_FILE_EXTENSIONS).
|
|
292
|
-
if (!hasSlash && !hasKnownFileExtension(clean)) continue;
|
|
293
|
-
const lineMatch = LINE_REF_RE.exec(clean);
|
|
294
|
-
if (lineMatch && hasKnownFileExtension(lineMatch[1])) {
|
|
295
|
-
lineReferenceHints.add(lineMatch[1]);
|
|
296
|
-
results.add(lineMatch[1]);
|
|
297
|
-
} else {
|
|
298
|
-
results.add(clean);
|
|
463
|
+
while (cursor < source.length) {
|
|
464
|
+
const openTick = source.indexOf('`', cursor);
|
|
465
|
+
if (openTick < 0) {
|
|
466
|
+
addPathTokensFromPlainText(source.slice(cursor), results, externalPaths, lineReferenceHints);
|
|
467
|
+
break;
|
|
299
468
|
}
|
|
300
|
-
}
|
|
301
469
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (lineMatch && hasKnownFileExtension(lineMatch[1])) {
|
|
308
|
-
if (isLikelyPath(lineMatch[1])) {
|
|
309
|
-
lineReferenceHints.add(lineMatch[1]);
|
|
310
|
-
results.add(lineMatch[1]);
|
|
311
|
-
}
|
|
312
|
-
} else if (isLikelyPath(token)) {
|
|
313
|
-
results.add(token);
|
|
470
|
+
addPathTokensFromPlainText(source.slice(cursor, openTick), results, externalPaths, lineReferenceHints);
|
|
471
|
+
const closeTick = source.indexOf('`', openTick + 1);
|
|
472
|
+
if (closeTick < 0) {
|
|
473
|
+
addPathTokensFromPlainText(source.slice(openTick), results, externalPaths, lineReferenceHints);
|
|
474
|
+
break;
|
|
314
475
|
}
|
|
476
|
+
|
|
477
|
+
addPathTokensFromBacktickSpan(source.slice(openTick + 1, closeTick), results, externalPaths, lineReferenceHints);
|
|
478
|
+
cursor = closeTick + 1;
|
|
315
479
|
}
|
|
316
480
|
|
|
317
481
|
const paths = Array.from(results)
|
|
318
482
|
.filter((p) => !p.includes('*') && !p.includes('?'))
|
|
319
483
|
.sort((left, right) => left.localeCompare(right));
|
|
320
|
-
return {
|
|
484
|
+
return {
|
|
485
|
+
paths,
|
|
486
|
+
externalPaths: Array.from(externalPaths).sort((left, right) => left.localeCompare(right)),
|
|
487
|
+
lineReferenceHints
|
|
488
|
+
};
|
|
321
489
|
}
|
|
322
490
|
|
|
323
491
|
// Standalone filenames (no slash) mentioned in task prose — e.g. "roadmap-skill.config.json",
|
|
@@ -363,6 +531,14 @@ function isCodeTask(taskText) {
|
|
|
363
531
|
return CODE_HINTS.some((hint) => normalized.includes(hint));
|
|
364
532
|
}
|
|
365
533
|
|
|
534
|
+
function isHttpExpectationTask(taskText) {
|
|
535
|
+
const text = String(taskText || '');
|
|
536
|
+
if (!/(?:->|→)/.test(text) || !/\bHTTP\s+\d{3}\b/i.test(text)) {
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
return /\b(?:GET|POST|PUT|PATCH|DELETE)\b/i.test(text) || /\/api\//i.test(text);
|
|
540
|
+
}
|
|
541
|
+
|
|
366
542
|
function isDocTask(taskText) {
|
|
367
543
|
const normalized = String(taskText).toLowerCase();
|
|
368
544
|
// Use word-boundary matching to avoid substring false positives (e.g. "specific" ≠ "spec").
|
|
@@ -377,10 +553,59 @@ function isImplementationTask(taskText) {
|
|
|
377
553
|
return !isDocTask(taskText) && (isCodeTask(taskText) || taskDescribesChange(taskText));
|
|
378
554
|
}
|
|
379
555
|
|
|
380
|
-
function
|
|
556
|
+
function deriveNextAppRouteAlias(relativePath) {
|
|
557
|
+
const normalized = normalizePathForMatch(relativePath);
|
|
558
|
+
const match = normalized.match(/^(?:src\/)?app(?:\/(.*))?\/(page|route)\.(?:js|jsx|ts|tsx)$/);
|
|
559
|
+
if (!match) {
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const routePath = match[1] || '';
|
|
564
|
+
const segments = routePath ? routePath.split('/').filter(Boolean) : [];
|
|
565
|
+
const visibleSegments = [];
|
|
566
|
+
for (const segment of segments) {
|
|
567
|
+
if (/^\([^)]*\)$/.test(segment)) {
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
if (segment.includes('(') || segment.includes(')') || segment.includes('[') || segment.includes(']') || segment.startsWith('@')) {
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
visibleSegments.push(segment);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return visibleSegments.length > 0 ? `/${visibleSegments.join('/')}` : '/';
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function buildPathHintResolver(fileIndex) {
|
|
580
|
+
const routeAliasIndex = new Map();
|
|
581
|
+
for (const file of fileIndex) {
|
|
582
|
+
const alias = deriveNextAppRouteAlias(file.relativePath);
|
|
583
|
+
if (!alias) {
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
const existing = routeAliasIndex.get(alias) || [];
|
|
587
|
+
existing.push(file.relativePath);
|
|
588
|
+
routeAliasIndex.set(alias, existing);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
for (const [alias, matches] of routeAliasIndex.entries()) {
|
|
592
|
+
routeAliasIndex.set(alias, Array.from(new Set(matches)).sort((left, right) => left.localeCompare(right)));
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return {
|
|
596
|
+
fileIndex,
|
|
597
|
+
routeAliasIndex
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function findFilesByPathHints(pathHints, pathHintResolver) {
|
|
602
|
+
const resolver = Array.isArray(pathHintResolver)
|
|
603
|
+
? buildPathHintResolver(pathHintResolver)
|
|
604
|
+
: pathHintResolver;
|
|
605
|
+
const fileIndex = resolver.fileIndex;
|
|
381
606
|
const matches = [];
|
|
382
607
|
for (const hint of pathHints) {
|
|
383
|
-
const normalizedHint = hint
|
|
608
|
+
const normalizedHint = normalizePathCandidateToken(hint);
|
|
384
609
|
const direct = fileIndex.find((file) => file.relativePath === normalizedHint);
|
|
385
610
|
if (direct) {
|
|
386
611
|
matches.push(direct.relativePath);
|
|
@@ -392,6 +617,11 @@ function findFilesByPathHints(pathHints, fileIndex) {
|
|
|
392
617
|
matches.push(file.relativePath);
|
|
393
618
|
}
|
|
394
619
|
}
|
|
620
|
+
|
|
621
|
+
const routeMatches = resolver.routeAliasIndex.get(normalizedHint);
|
|
622
|
+
if (routeMatches && routeMatches.length > 0) {
|
|
623
|
+
matches.push(...routeMatches);
|
|
624
|
+
}
|
|
395
625
|
}
|
|
396
626
|
return Array.from(new Set(matches)).sort((left, right) => left.localeCompare(right));
|
|
397
627
|
}
|
|
@@ -683,19 +913,12 @@ function isTestPath(relativePath) {
|
|
|
683
913
|
return /(^|\/)(__tests__|tests)\//.test(relativePath) || /\.test\.|\.spec\.|_test\.go$/.test(relativePath);
|
|
684
914
|
}
|
|
685
915
|
|
|
686
|
-
function
|
|
687
|
-
const
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
}
|
|
693
|
-
if (!hasKnownFileExtension(candidate)) {
|
|
694
|
-
continue;
|
|
695
|
-
}
|
|
696
|
-
paths.add(candidate.replace(/^\.\//, ''));
|
|
697
|
-
}
|
|
698
|
-
return Array.from(paths).sort((left, right) => left.localeCompare(right));
|
|
916
|
+
function extractReferencedPaths(text) {
|
|
917
|
+
const extracted = extractExplicitPaths(text);
|
|
918
|
+
return {
|
|
919
|
+
repoPaths: extracted.paths,
|
|
920
|
+
externalPaths: extracted.externalPaths
|
|
921
|
+
};
|
|
699
922
|
}
|
|
700
923
|
|
|
701
924
|
function evidenceLineHasPassingSummary(evidenceText) {
|
|
@@ -714,7 +937,7 @@ function evidenceSummaryImpliesTests(evidenceText) {
|
|
|
714
937
|
/\b(?:vitest|jest|npm test|pnpm test|yarn test|bun test)\b/i.test(String(evidenceText || ''));
|
|
715
938
|
}
|
|
716
939
|
|
|
717
|
-
function evaluateAuthoritativeEvidence(task,
|
|
940
|
+
function evaluateAuthoritativeEvidence(task, pathHintResolver) {
|
|
718
941
|
const evidenceLines = Array.isArray(task.evidenceLines) ? task.evidenceLines : [];
|
|
719
942
|
if (evidenceLines.length === 0) {
|
|
720
943
|
return {
|
|
@@ -729,8 +952,9 @@ function evaluateAuthoritativeEvidence(task, fileIndex) {
|
|
|
729
952
|
};
|
|
730
953
|
}
|
|
731
954
|
|
|
732
|
-
const
|
|
733
|
-
const
|
|
955
|
+
const extractedReferences = evidenceLines.map((line) => extractReferencedPaths(line.text));
|
|
956
|
+
const referencedPaths = unionArrays(...extractedReferences.map((entry) => entry.repoPaths));
|
|
957
|
+
const matchedPaths = referencedPaths.length > 0 ? findFilesByPathHints(referencedPaths, pathHintResolver) : [];
|
|
734
958
|
const summaryMatches = evidenceLines
|
|
735
959
|
.filter((line) => evidenceLineHasPassingSummary(line.text))
|
|
736
960
|
.map((line) => line.text);
|
|
@@ -1053,6 +1277,7 @@ function buildValidationContext(projectRoot, config, plugins) {
|
|
|
1053
1277
|
const files = walkFiles(projectRoot);
|
|
1054
1278
|
const fileIndex = readFileIndex(projectRoot, files, config);
|
|
1055
1279
|
const testFrameworks = detectTestFrameworks(projectRoot, files);
|
|
1280
|
+
const pathHintResolver = buildPathHintResolver(fileIndex);
|
|
1056
1281
|
|
|
1057
1282
|
return {
|
|
1058
1283
|
projectRoot,
|
|
@@ -1060,26 +1285,31 @@ function buildValidationContext(projectRoot, config, plugins) {
|
|
|
1060
1285
|
plugins,
|
|
1061
1286
|
files,
|
|
1062
1287
|
fileIndex,
|
|
1288
|
+
pathHintResolver,
|
|
1063
1289
|
testFrameworks
|
|
1064
1290
|
};
|
|
1065
1291
|
}
|
|
1066
1292
|
|
|
1067
1293
|
function validateTask(task, context, config, plugins) {
|
|
1068
|
-
const {
|
|
1294
|
+
const {
|
|
1295
|
+
paths: pathHints,
|
|
1296
|
+
externalPaths,
|
|
1297
|
+
lineReferenceHints
|
|
1298
|
+
} = extractExplicitPaths(task.text);
|
|
1069
1299
|
// Paths that are line-reference hints (file.ts:NN) indicate WHERE to implement,
|
|
1070
1300
|
// not that implementation exists. They are excluded from hasDirectReferencePass.
|
|
1071
1301
|
const purePathHints = pathHints.filter((p) => !lineReferenceHints.has(p));
|
|
1072
1302
|
const standaloneFilenames = extractStandaloneFilenames(task.text);
|
|
1073
1303
|
const symbolHints = extractSymbolHints(task.text);
|
|
1074
|
-
const authoritativeEvidence = evaluateAuthoritativeEvidence(task, context.
|
|
1304
|
+
const authoritativeEvidence = evaluateAuthoritativeEvidence(task, context.pathHintResolver);
|
|
1075
1305
|
|
|
1076
|
-
const filesFromPaths = findFilesByPathHints(pathHints, context.
|
|
1077
|
-
const filesFromPurePathHints = findFilesByPathHints(purePathHints, context.
|
|
1306
|
+
const filesFromPaths = findFilesByPathHints(pathHints, context.pathHintResolver);
|
|
1307
|
+
const filesFromPurePathHints = findFilesByPathHints(purePathHints, context.pathHintResolver);
|
|
1078
1308
|
const filesFromSymbols = findFilesBySymbols(symbolHints, context.fileIndex);
|
|
1079
1309
|
// Combine path hints AND standalone filenames for token exclusion so that tokens
|
|
1080
1310
|
// derived from any referenced filename (e.g. "roadmap-skill" from
|
|
1081
1311
|
// "roadmap-skill.config.json") are excluded from code evidence scoring.
|
|
1082
|
-
const pathDerivedTokens = extractPathDerivedTokens([...pathHints, ...standaloneFilenames]);
|
|
1312
|
+
const pathDerivedTokens = extractPathDerivedTokens([...pathHints, ...externalPaths, ...standaloneFilenames]);
|
|
1083
1313
|
const filesFromCode = findCodeEvidence(task.text, context.fileIndex, pathDerivedTokens);
|
|
1084
1314
|
const filesFromWeakPathTokens = findFilesByTaskPathTokens(task.text, context.fileIndex, pathDerivedTokens);
|
|
1085
1315
|
const weakPathContentTokens = findWeakPathContentSpecificTokens(task.text, context.fileIndex, filesFromWeakPathTokens, pathDerivedTokens);
|
|
@@ -1143,7 +1373,12 @@ function validateTask(task, context, config, plugins) {
|
|
|
1143
1373
|
}
|
|
1144
1374
|
}
|
|
1145
1375
|
|
|
1146
|
-
const requiresTest =
|
|
1376
|
+
const requiresTest =
|
|
1377
|
+
!task.noTest &&
|
|
1378
|
+
context.testFrameworks.length > 0 &&
|
|
1379
|
+
isCodeTask(task.text) &&
|
|
1380
|
+
!isDocTask(task.text) &&
|
|
1381
|
+
!isHttpExpectationTask(task.text);
|
|
1147
1382
|
const configuredRules = Array.isArray(config.validators) ? config.validators : [];
|
|
1148
1383
|
const pluginRules = collectPluginContributions(plugins || [], 'registerValidators', context);
|
|
1149
1384
|
let overrideResult = null;
|
|
@@ -1181,13 +1416,14 @@ function validateTask(task, context, config, plugins) {
|
|
|
1181
1416
|
uniqueReasons = Array.isArray(overrideResult.reasons) ? Array.from(new Set(overrideResult.reasons)) : [];
|
|
1182
1417
|
}
|
|
1183
1418
|
|
|
1184
|
-
const hasConcreteReferenceEvidence =
|
|
1185
|
-
const
|
|
1186
|
-
||
|
|
1187
|
-
||
|
|
1188
|
-
||
|
|
1189
|
-
||
|
|
1190
|
-
|
|
1419
|
+
const hasConcreteReferenceEvidence = filesFromPaths.length > 0 || filesFromSymbols.length > 0;
|
|
1420
|
+
const hasConcreteAttemptEvidence =
|
|
1421
|
+
authoritativeEvidence.active ||
|
|
1422
|
+
hasRuleGrantedEvidence ||
|
|
1423
|
+
filesFromTests.length > 0 ||
|
|
1424
|
+
hasConcreteReferenceEvidence ||
|
|
1425
|
+
(isDocTask(task.text) && filesFromArtifacts.length > 0);
|
|
1426
|
+
const attempted = hasConcreteAttemptEvidence;
|
|
1191
1427
|
const { categories: strongEvidenceCategories } = countStrongEvidenceCategories(task.text, evidence);
|
|
1192
1428
|
const strongEvidenceCount = strongEvidenceCategories.length;
|
|
1193
1429
|
// Only pure path hints (not line-reference hints like file.ts:169) count as direct evidence.
|