roadmapsmith 0.9.16 → 0.9.23
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 +34 -0
- package/.codex-plugin/plugin.json +55 -0
- package/README.md +55 -18
- package/assets/palette.png +0 -0
- package/assets/roadmapsmith-logo.png +0 -0
- package/bin/cli.js +75 -17
- package/package.json +20 -4
- package/skills/roadmap/SKILL.md +33 -0
- package/skills/roadmap-audit/SKILL.md +16 -0
- package/skills/roadmap-generate/SKILL.md +18 -0
- package/skills/roadmap-init/SKILL.md +16 -0
- package/skills/roadmap-maintain/SKILL.md +18 -0
- package/skills/roadmap-setup/SKILL.md +17 -0
- package/skills/roadmap-status/SKILL.md +17 -0
- package/skills/roadmap-sync/SKILL.md +20 -0
- package/skills/roadmap-sync/agents/openai.yaml +7 -0
- package/skills/roadmap-update/SKILL.md +17 -0
- package/skills/roadmap-validate/SKILL.md +16 -0
- package/skills/roadmap-zero/SKILL.md +17 -0
- package/skills.json +95 -0
- package/src/classifier/index.js +50 -11
- package/src/generator/index.js +231 -19
- package/src/host.js +446 -58
- package/src/io.js +45 -4
- package/src/match.js +18 -2
- package/src/parser/index.js +13 -0
- package/src/slash.js +148 -69
- package/src/utils.js +1 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: roadmap-validate
|
|
3
|
+
description: Inspect evidence-backed roadmap validation through the RoadmapSmith CLI.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# RoadmapSmith Validate
|
|
7
|
+
|
|
8
|
+
Use this command when the user wants per-task evidence status without mutating the roadmap.
|
|
9
|
+
|
|
10
|
+
## Required behavior
|
|
11
|
+
|
|
12
|
+
1. Prefer the local engine inside this repository:
|
|
13
|
+
- `node roadmap-skill/bin/cli.js validate --json --project-root .`
|
|
14
|
+
- on this Windows machine, prefer `C:\Program Files\nodejs\node.exe roadmap-skill/bin/cli.js validate --json --project-root .` if `node` is not in PATH
|
|
15
|
+
2. Otherwise prefer `roadmapsmith validate --json --project-root .`.
|
|
16
|
+
3. Treat this command as CLI-backed and non-mutating.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: roadmap-zero
|
|
3
|
+
description: Run the one-command Zero Mode workflow through the RoadmapSmith CLI.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# RoadmapSmith Zero
|
|
7
|
+
|
|
8
|
+
Use this command when the repository is empty or low-context and the user needs the discovery interview plus first roadmap generation.
|
|
9
|
+
|
|
10
|
+
## Required behavior
|
|
11
|
+
|
|
12
|
+
1. Prefer the local engine inside this repository:
|
|
13
|
+
- `node roadmap-skill/bin/cli.js zero --project-root .`
|
|
14
|
+
- on this Windows machine, prefer `C:\Program Files\nodejs\node.exe roadmap-skill/bin/cli.js zero --project-root .` if `node` is not in PATH
|
|
15
|
+
2. Otherwise prefer `roadmapsmith zero --project-root .`.
|
|
16
|
+
3. Treat this command as CLI-backed and interactive.
|
|
17
|
+
4. If the CLI is missing, explain the install path instead of improvising the workflow manually.
|
package/skills.json
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "roadmapsmith",
|
|
3
|
+
"description": "One-command roadmap generation and maintenance for coding agents, with a shared native slash bundle for Claude and Codex plus the RoadmapSmith CLI.",
|
|
4
|
+
"triggers": [
|
|
5
|
+
"roadmap",
|
|
6
|
+
"sync roadmap",
|
|
7
|
+
"validate task completion",
|
|
8
|
+
"project milestones",
|
|
9
|
+
"agent roadmap"
|
|
10
|
+
],
|
|
11
|
+
"usageExamples": [
|
|
12
|
+
"npx skills add PapiScholz/roadmapsmith --skill '*' -a claude-code",
|
|
13
|
+
"npx skills add PapiScholz/roadmapsmith --skill roadmap-sync",
|
|
14
|
+
"roadmapsmith setup",
|
|
15
|
+
"roadmapsmith zero",
|
|
16
|
+
"roadmapsmith maintain",
|
|
17
|
+
"roadmapsmith /roadmap",
|
|
18
|
+
"roadmapsmith /roadmap-update",
|
|
19
|
+
"roadmapsmith validate --json"
|
|
20
|
+
],
|
|
21
|
+
"install": {
|
|
22
|
+
"command": "npx skills add PapiScholz/roadmapsmith --skill '*' -a claude-code",
|
|
23
|
+
"source": "PapiScholz/roadmapsmith",
|
|
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, 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
|
+
},
|
|
27
|
+
"skills": [
|
|
28
|
+
{
|
|
29
|
+
"name": "roadmap",
|
|
30
|
+
"path": "skills/roadmap",
|
|
31
|
+
"description": "Native slash palette for RoadmapSmith commands and recommended entrypoints across supported hosts.",
|
|
32
|
+
"version": "0.9.23"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "roadmap-zero",
|
|
36
|
+
"path": "skills/roadmap-zero",
|
|
37
|
+
"description": "Native slash entrypoint for the one-command Zero Mode CLI workflow.",
|
|
38
|
+
"version": "0.9.23"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "roadmap-maintain",
|
|
42
|
+
"path": "skills/roadmap-maintain",
|
|
43
|
+
"description": "Native slash entrypoint for the preserve-first generate + sync + audit flow.",
|
|
44
|
+
"version": "0.9.23"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"name": "roadmap-status",
|
|
48
|
+
"path": "skills/roadmap-status",
|
|
49
|
+
"description": "Native slash readiness check grounded in roadmapsmith doctor JSON.",
|
|
50
|
+
"version": "0.9.23"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"name": "roadmap-init",
|
|
54
|
+
"path": "skills/roadmap-init",
|
|
55
|
+
"description": "Native slash entrypoint for creating ROADMAP.md and AGENTS.md.",
|
|
56
|
+
"version": "0.9.23"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"name": "roadmap-generate",
|
|
60
|
+
"path": "skills/roadmap-generate",
|
|
61
|
+
"description": "Native slash entrypoint for managed roadmap updates that require --full-regen before destructive replacement.",
|
|
62
|
+
"version": "0.9.23"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"name": "roadmap-validate",
|
|
66
|
+
"path": "skills/roadmap-validate",
|
|
67
|
+
"description": "Native slash entrypoint for evidence-backed roadmap validation.",
|
|
68
|
+
"version": "0.9.23"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"name": "roadmap-update",
|
|
72
|
+
"path": "skills/roadmap-update",
|
|
73
|
+
"description": "Native slash entrypoint for applying evidence-backed checklist sync.",
|
|
74
|
+
"version": "0.9.23"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"name": "roadmap-sync",
|
|
78
|
+
"path": "skills/roadmap-sync",
|
|
79
|
+
"description": "Legacy namespaced root plus policy guidance for RoadmapSmith slash workflows.",
|
|
80
|
+
"version": "0.9.23"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"name": "roadmap-audit",
|
|
84
|
+
"path": "skills/roadmap-audit",
|
|
85
|
+
"description": "Native slash entrypoint for the current sync-plus-audit workflow.",
|
|
86
|
+
"version": "0.9.23"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"name": "roadmap-setup",
|
|
90
|
+
"path": "skills/roadmap-setup",
|
|
91
|
+
"description": "Native slash entrypoint for generating RoadmapSmith host integration files.",
|
|
92
|
+
"version": "0.9.23"
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
}
|
package/src/classifier/index.js
CHANGED
|
@@ -12,7 +12,16 @@ const WEB_CONFIGS = [
|
|
|
12
12
|
];
|
|
13
13
|
const STYLE_CONFIGS = ['tailwind.config.js', 'tailwind.config.ts', 'tailwind.config.cjs'];
|
|
14
14
|
const WEB_DEPS = new Set(['next', 'react', 'vue', 'svelte', 'astro', 'vite', 'nuxt', 'gatsby', 'remix', '@remix-run/react']);
|
|
15
|
+
const ELECTRON_DEPS = new Set(['electron', 'electron-builder', 'electron-forge', '@electron-forge/cli', 'electron-updater']);
|
|
16
|
+
const ELECTRON_CONFIGS = [
|
|
17
|
+
'electron-builder.json',
|
|
18
|
+
'electron-builder.yml',
|
|
19
|
+
'electron-builder.yaml',
|
|
20
|
+
'forge.config.js',
|
|
21
|
+
'forge.config.ts'
|
|
22
|
+
];
|
|
15
23
|
const LANDING_ROUTE_RE = /(?:^|\/)(?:contact|services|about|pricing|hero|cta|landing)(?:\/|\.)/i;
|
|
24
|
+
const FIXTURE_PATH_RE = /(^|\/)(?:test|tests)\/fixtures\//i;
|
|
16
25
|
|
|
17
26
|
function readPackageDeps(projectRoot) {
|
|
18
27
|
if (!projectRoot) return [];
|
|
@@ -44,6 +53,9 @@ function hasWorkspaces(projectRoot) {
|
|
|
44
53
|
}
|
|
45
54
|
|
|
46
55
|
function classifyProject({ projectRoot, files }) {
|
|
56
|
+
const candidateFiles = Array.isArray(files)
|
|
57
|
+
? files.filter((file) => !FIXTURE_PATH_RE.test(String(file || '')))
|
|
58
|
+
: [];
|
|
47
59
|
const signals = [];
|
|
48
60
|
|
|
49
61
|
if (hasWorkspaces(projectRoot)) {
|
|
@@ -51,17 +63,22 @@ function classifyProject({ projectRoot, files }) {
|
|
|
51
63
|
return { type: 'monorepo', confidence: 'high', signals };
|
|
52
64
|
}
|
|
53
65
|
|
|
54
|
-
const hasPy = hasFilename(
|
|
55
|
-
if (hasPy && !
|
|
66
|
+
const hasPy = hasFilename(candidateFiles, 'pyproject.toml') || hasFilename(candidateFiles, 'setup.py');
|
|
67
|
+
if (hasPy && !candidateFiles.some((f) => /\.[jt]sx?$/.test(f))) {
|
|
56
68
|
signals.push('pyproject.toml / setup.py, no JS files');
|
|
57
69
|
return { type: 'python-package', confidence: 'high', signals };
|
|
58
70
|
}
|
|
59
71
|
|
|
60
72
|
let webScore = 0;
|
|
61
73
|
let landingScore = 0;
|
|
74
|
+
let electronScore = 0;
|
|
62
75
|
const deps = readPackageDeps(projectRoot);
|
|
63
76
|
|
|
64
77
|
for (const dep of deps) {
|
|
78
|
+
if (ELECTRON_DEPS.has(dep)) {
|
|
79
|
+
electronScore += 3;
|
|
80
|
+
signals.push(`dependency: ${dep}`);
|
|
81
|
+
}
|
|
65
82
|
if (WEB_DEPS.has(dep)) {
|
|
66
83
|
webScore += 2;
|
|
67
84
|
signals.push(`dependency: ${dep}`);
|
|
@@ -69,59 +86,81 @@ function classifyProject({ projectRoot, files }) {
|
|
|
69
86
|
}
|
|
70
87
|
|
|
71
88
|
for (const dir of WEB_DIRS) {
|
|
72
|
-
if (hasDir(
|
|
89
|
+
if (hasDir(candidateFiles, dir)) {
|
|
73
90
|
webScore += 2;
|
|
74
91
|
signals.push(`directory: ${dir.replace(/\/$/, '')}`);
|
|
75
92
|
}
|
|
76
93
|
}
|
|
77
94
|
|
|
78
95
|
for (const dir of ASSET_DIRS) {
|
|
79
|
-
if (hasDir(
|
|
96
|
+
if (hasDir(candidateFiles, dir)) {
|
|
80
97
|
webScore += 1;
|
|
81
98
|
signals.push(`directory: ${dir.replace(/\/$/, '')}`);
|
|
82
99
|
}
|
|
83
100
|
}
|
|
84
101
|
|
|
102
|
+
if (hasDir(candidateFiles, 'electron/')) {
|
|
103
|
+
electronScore += 3;
|
|
104
|
+
signals.push('directory: electron');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (candidateFiles.some((file) => /^electron\/.+\.(js|ts|cjs|mjs)$/.test(file))) {
|
|
108
|
+
electronScore += 2;
|
|
109
|
+
signals.push('electron main/preload sources');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const cfg of ELECTRON_CONFIGS) {
|
|
113
|
+
if (hasFilename(candidateFiles, cfg)) {
|
|
114
|
+
electronScore += 2;
|
|
115
|
+
signals.push(`config: ${cfg}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
85
119
|
for (const cfg of WEB_CONFIGS) {
|
|
86
|
-
if (hasFilename(
|
|
120
|
+
if (hasFilename(candidateFiles, cfg)) {
|
|
87
121
|
webScore += 3;
|
|
88
122
|
signals.push(`config: ${cfg}`);
|
|
89
123
|
}
|
|
90
124
|
}
|
|
91
125
|
|
|
92
126
|
for (const cfg of STYLE_CONFIGS) {
|
|
93
|
-
if (hasFilename(
|
|
127
|
+
if (hasFilename(candidateFiles, cfg)) {
|
|
94
128
|
webScore += 1;
|
|
95
129
|
signals.push(`config: ${cfg}`);
|
|
96
130
|
}
|
|
97
131
|
}
|
|
98
132
|
|
|
99
|
-
if (
|
|
133
|
+
if (candidateFiles.some((f) => /\.css$/.test(f))) {
|
|
100
134
|
webScore += 1;
|
|
101
135
|
signals.push('CSS files present');
|
|
102
136
|
}
|
|
103
137
|
|
|
104
|
-
const landingRoutes =
|
|
138
|
+
const landingRoutes = candidateFiles.filter((f) => LANDING_ROUTE_RE.test(f));
|
|
105
139
|
if (landingRoutes.length > 0) {
|
|
106
140
|
landingScore += landingRoutes.length * 2;
|
|
107
141
|
signals.push(`landing/service routes: ${landingRoutes.length}`);
|
|
108
142
|
}
|
|
109
143
|
|
|
110
|
-
if (hasFilename(
|
|
144
|
+
if (hasFilename(candidateFiles, 'favicon.ico') || hasFilename(candidateFiles, 'logo.png') || hasFilename(candidateFiles, 'logo.svg')) {
|
|
111
145
|
landingScore += 1;
|
|
112
146
|
signals.push('branding asset in public/');
|
|
113
147
|
}
|
|
114
148
|
|
|
115
|
-
if (webScore === 0 && (
|
|
149
|
+
if (webScore === 0 && (candidateFiles.some((f) => f.startsWith('bin/')) || hasFilename(candidateFiles, 'cli.js'))) {
|
|
116
150
|
signals.push('bin/ directory or cli.js');
|
|
117
151
|
return { type: 'cli-tool', confidence: 'medium', signals };
|
|
118
152
|
}
|
|
119
153
|
|
|
120
|
-
if (webScore === 0 && hasFilename(
|
|
154
|
+
if (webScore === 0 && hasFilename(candidateFiles, 'package.json')) {
|
|
121
155
|
signals.push('package.json, no web signals');
|
|
122
156
|
return { type: 'npm-package', confidence: 'low', signals };
|
|
123
157
|
}
|
|
124
158
|
|
|
159
|
+
if (electronScore >= 3) {
|
|
160
|
+
const confidence = electronScore >= 6 ? 'high' : 'medium';
|
|
161
|
+
return { type: 'electron-app', confidence, signals };
|
|
162
|
+
}
|
|
163
|
+
|
|
125
164
|
if (webScore >= 3) {
|
|
126
165
|
const type = landingScore >= 3 ? 'landing-site' : 'frontend-web';
|
|
127
166
|
const confidence = webScore >= 7 ? 'high' : 'medium';
|
package/src/generator/index.js
CHANGED
|
@@ -4,8 +4,8 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { walkFiles, detectLanguages, detectTestFrameworks, detectWorkspaces } = require('../io');
|
|
6
6
|
const { createRoadmapModel, PHASE_ORDER } = require('../model');
|
|
7
|
-
const { slugify
|
|
8
|
-
const { parseRoadmap, upsertManagedBlock } = require('../parser');
|
|
7
|
+
const { slugify } = require('../utils');
|
|
8
|
+
const { parseRoadmap, tasksInManagedBlock, upsertManagedBlock } = require('../parser');
|
|
9
9
|
const { findBestTaskMatch, dedupeTasks } = require('../match');
|
|
10
10
|
const { collectPluginContributions } = require('../config');
|
|
11
11
|
const { renderBody } = require('../renderer');
|
|
@@ -13,6 +13,8 @@ const { classifyProject } = require('../classifier');
|
|
|
13
13
|
|
|
14
14
|
const IMPL_PATTERN_RE = /[/|]TODO|TODO[|/]|[/|]FIXME|FIXME[|/]/;
|
|
15
15
|
const COMMENT_TODO_RE = /(?:\/\/|#|\*\s*).*\b(?:TODO|FIXME)\b/;
|
|
16
|
+
const ADDITIONS_SECTION_TITLE = 'RoadmapSmith Additions';
|
|
17
|
+
const PHASE_LABEL_RE = /`?\[(P[0-2])\]`?/i;
|
|
16
18
|
|
|
17
19
|
function isTodoMarker(line) {
|
|
18
20
|
return COMMENT_TODO_RE.test(line) && !IMPL_PATTERN_RE.test(line);
|
|
@@ -181,6 +183,58 @@ function toCandidate(text, phase, priority, source = 'default') {
|
|
|
181
183
|
};
|
|
182
184
|
}
|
|
183
185
|
|
|
186
|
+
function hasSubstantiveManagedBlock(parsedRoadmap) {
|
|
187
|
+
if (!parsedRoadmap || !parsedRoadmap.managedRange) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const managedLines = parsedRoadmap.lines.slice(
|
|
192
|
+
parsedRoadmap.managedRange.start + 1,
|
|
193
|
+
parsedRoadmap.managedRange.end
|
|
194
|
+
);
|
|
195
|
+
return managedLines.some((line) => line.trim().length > 0);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function stripTrailingBlankLines(lines) {
|
|
199
|
+
const next = Array.isArray(lines) ? lines.slice() : [];
|
|
200
|
+
while (next.length > 0 && !next[next.length - 1].trim()) {
|
|
201
|
+
next.pop();
|
|
202
|
+
}
|
|
203
|
+
return next;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function renderAdditionTask(task) {
|
|
207
|
+
return `- [ ] ${task.text} <!-- rs:task=${task.id} -->`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function isGenericPreserveModeCandidate(candidate) {
|
|
211
|
+
return candidate && ['default', 'classifier', 'todo-hint'].includes(candidate.source);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function buildManagedAdditionsLines(tasks, options = {}) {
|
|
215
|
+
const groups = groupByPhase(tasks);
|
|
216
|
+
const lines = [];
|
|
217
|
+
const includeSectionHeading = options.includeSectionHeading !== false;
|
|
218
|
+
|
|
219
|
+
if (includeSectionHeading) {
|
|
220
|
+
lines.push(`## ${ADDITIONS_SECTION_TITLE}`);
|
|
221
|
+
lines.push('');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
for (const phase of PHASE_ORDER) {
|
|
225
|
+
if (!groups[phase] || groups[phase].length === 0) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
lines.push(`### Phase ${phase}`);
|
|
229
|
+
for (const task of groups[phase]) {
|
|
230
|
+
lines.push(renderAdditionTask(task));
|
|
231
|
+
}
|
|
232
|
+
lines.push('');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return stripTrailingBlankLines(lines);
|
|
236
|
+
}
|
|
237
|
+
|
|
184
238
|
const WEB_CANDIDATES_COMMON = [
|
|
185
239
|
{ text: 'Add SEO metadata: title, description, and canonical URL for all pages', phase: 'P0' },
|
|
186
240
|
{ text: 'Implement responsive and mobile-first layout across all breakpoints', phase: 'P0' },
|
|
@@ -285,6 +339,9 @@ function applyTaskMatchers(scan, config) {
|
|
|
285
339
|
}
|
|
286
340
|
|
|
287
341
|
function inferPhase(existingTask) {
|
|
342
|
+
const textPhaseMatch = String(existingTask.text || '').match(PHASE_LABEL_RE);
|
|
343
|
+
if (textPhaseMatch) return textPhaseMatch[1].toUpperCase();
|
|
344
|
+
|
|
288
345
|
const section = String(existingTask.section || '').toUpperCase();
|
|
289
346
|
if (section.includes('P0')) return 'P0';
|
|
290
347
|
if (section.includes('P1')) return 'P1';
|
|
@@ -292,12 +349,138 @@ function inferPhase(existingTask) {
|
|
|
292
349
|
return 'P1';
|
|
293
350
|
}
|
|
294
351
|
|
|
295
|
-
function
|
|
352
|
+
function sortTasksByPhaseAndText(tasks) {
|
|
353
|
+
return tasks.slice().sort((left, right) => {
|
|
354
|
+
const leftPhaseIndex = PHASE_ORDER.indexOf(left.phase);
|
|
355
|
+
const rightPhaseIndex = PHASE_ORDER.indexOf(right.phase);
|
|
356
|
+
if (leftPhaseIndex !== rightPhaseIndex) {
|
|
357
|
+
return leftPhaseIndex - rightPhaseIndex;
|
|
358
|
+
}
|
|
359
|
+
return left.text.localeCompare(right.text);
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function findPhaseSectionRange(lines, managedRange, phase) {
|
|
364
|
+
const headingPattern = /^(#{2,4})\s+(.*)$/;
|
|
365
|
+
const phasePattern = new RegExp(`\\b${phase}\\b`, 'i');
|
|
366
|
+
let sectionStart = -1;
|
|
367
|
+
let sectionLevel = 0;
|
|
368
|
+
|
|
369
|
+
for (let index = managedRange.start + 1; index < managedRange.end; index += 1) {
|
|
370
|
+
const match = lines[index].trim().match(headingPattern);
|
|
371
|
+
if (!match) {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (!phasePattern.test(match[2])) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
sectionStart = index;
|
|
378
|
+
sectionLevel = match[1].length;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (sectionStart < 0) {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
let sectionEnd = managedRange.end;
|
|
386
|
+
for (let index = sectionStart + 1; index < managedRange.end; index += 1) {
|
|
387
|
+
const match = lines[index].trim().match(headingPattern);
|
|
388
|
+
if (!match) {
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (match[1].length <= sectionLevel) {
|
|
392
|
+
sectionEnd = index;
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
start: sectionStart,
|
|
399
|
+
end: sectionEnd
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function buildPreserveModeInsertions(parsedRoadmap, tasks) {
|
|
404
|
+
const managedTasks = sortTasksByPhaseAndText(tasksInManagedBlock(parsedRoadmap));
|
|
405
|
+
const groups = groupByPhase(tasks);
|
|
406
|
+
const lines = parsedRoadmap.lines;
|
|
407
|
+
const insertions = [];
|
|
408
|
+
const fallbackTasks = [];
|
|
409
|
+
|
|
410
|
+
for (const phase of PHASE_ORDER) {
|
|
411
|
+
const phaseTasks = sortTasksByPhaseAndText(groups[phase] || []);
|
|
412
|
+
if (phaseTasks.length === 0) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const samePhaseTasks = managedTasks.filter((task) => inferPhase(task) === phase);
|
|
417
|
+
if (samePhaseTasks.length > 0) {
|
|
418
|
+
const anchor = samePhaseTasks.reduce((latest, task) => {
|
|
419
|
+
if (!latest || task.lastChildLineIndex > latest.lastChildLineIndex) {
|
|
420
|
+
return task;
|
|
421
|
+
}
|
|
422
|
+
return latest;
|
|
423
|
+
}, null);
|
|
424
|
+
insertions.push({
|
|
425
|
+
index: anchor.lastChildLineIndex + 1,
|
|
426
|
+
lines: phaseTasks.map(renderAdditionTask)
|
|
427
|
+
});
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const phaseSection = findPhaseSectionRange(lines, parsedRoadmap.managedRange, phase);
|
|
432
|
+
if (phaseSection) {
|
|
433
|
+
let insertionIndex = phaseSection.end;
|
|
434
|
+
while (insertionIndex > phaseSection.start + 1 && !lines[insertionIndex - 1].trim()) {
|
|
435
|
+
insertionIndex -= 1;
|
|
436
|
+
}
|
|
437
|
+
insertions.push({
|
|
438
|
+
index: insertionIndex,
|
|
439
|
+
lines: phaseTasks.map(renderAdditionTask)
|
|
440
|
+
});
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
fallbackTasks.push(...phaseTasks);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (fallbackTasks.length > 0) {
|
|
448
|
+
const fallbackLines = buildManagedAdditionsLines(fallbackTasks, { includeSectionHeading: true });
|
|
449
|
+
insertions.push({
|
|
450
|
+
index: parsedRoadmap.managedRange.end,
|
|
451
|
+
lines: ['', ...fallbackLines]
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return insertions.sort((left, right) => right.index - left.index);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function insertPreserveModeTasks(existingContent, parsedRoadmap, tasks) {
|
|
459
|
+
if (!parsedRoadmap || !parsedRoadmap.managedRange || tasks.length === 0) {
|
|
460
|
+
return existingContent;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const nextLines = parsedRoadmap.lines.slice();
|
|
464
|
+
const insertions = buildPreserveModeInsertions(parsedRoadmap, tasks);
|
|
465
|
+
for (const insertion of insertions) {
|
|
466
|
+
nextLines.splice(insertion.index, 0, ...insertion.lines);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return nextLines.join('\n');
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function filterPreserveModeCandidates(candidates) {
|
|
473
|
+
return candidates.filter((candidate) => !isGenericPreserveModeCandidate(candidate));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function mergeWithExisting(candidates, existingTasks, options = {}) {
|
|
296
477
|
const matchedExistingIds = new Set();
|
|
297
478
|
const merged = [];
|
|
298
479
|
|
|
299
480
|
for (const candidate of candidates) {
|
|
300
|
-
const match = findBestTaskMatch(candidate, existingTasks
|
|
481
|
+
const match = findBestTaskMatch(candidate, existingTasks, {
|
|
482
|
+
allowFuzzy: options.allowFuzzy !== false
|
|
483
|
+
});
|
|
301
484
|
if (match) {
|
|
302
485
|
matchedExistingIds.add(match.task.id);
|
|
303
486
|
merged.push({
|
|
@@ -311,20 +494,22 @@ function mergeWithExisting(candidates, existingTasks) {
|
|
|
311
494
|
merged.push(candidate);
|
|
312
495
|
}
|
|
313
496
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
497
|
+
if (options.includeUnmatchedExisting) {
|
|
498
|
+
for (const existing of existingTasks) {
|
|
499
|
+
if (matchedExistingIds.has(existing.id)) {
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
318
502
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
503
|
+
const phase = inferPhase(existing);
|
|
504
|
+
merged.push({
|
|
505
|
+
id: existing.id,
|
|
506
|
+
text: existing.text,
|
|
507
|
+
phase,
|
|
508
|
+
priority: phase,
|
|
509
|
+
checked: existing.checked,
|
|
510
|
+
source: 'existing'
|
|
511
|
+
});
|
|
512
|
+
}
|
|
328
513
|
}
|
|
329
514
|
|
|
330
515
|
return dedupeTasks(merged);
|
|
@@ -566,6 +751,8 @@ function generateRoadmapDocument(options) {
|
|
|
566
751
|
const zeroModeConfig = config.zeroMode || {};
|
|
567
752
|
const plugins = options.plugins || [];
|
|
568
753
|
const existingContent = options.existingContent || '';
|
|
754
|
+
const preserveManagedBlock = options.preserveManagedBlock === true;
|
|
755
|
+
const forceFullRegenerate = options.forceFullRegenerate === true;
|
|
569
756
|
|
|
570
757
|
const scan = scanProject(projectRoot);
|
|
571
758
|
const existing = parseRoadmap(existingContent);
|
|
@@ -573,7 +760,7 @@ function generateRoadmapDocument(options) {
|
|
|
573
760
|
for (const task of existing.tasks) {
|
|
574
761
|
existingCheckedById[task.id] = task.checked;
|
|
575
762
|
}
|
|
576
|
-
const
|
|
763
|
+
const existingManagedTasks = tasksInManagedBlock(existing);
|
|
577
764
|
|
|
578
765
|
const pluginTaskCandidates = collectPluginContributions(plugins, 'registerTaskDetectors', {
|
|
579
766
|
projectRoot,
|
|
@@ -627,7 +814,32 @@ function generateRoadmapDocument(options) {
|
|
|
627
814
|
|
|
628
815
|
const baseCandidates = buildDefaultCandidates(scan, config);
|
|
629
816
|
const matcherCandidates = applyTaskMatchers(scan, config);
|
|
630
|
-
const
|
|
817
|
+
const allCandidates = dedupeTasks([...baseCandidates, ...matcherCandidates, ...pluginTaskCandidates]);
|
|
818
|
+
|
|
819
|
+
if (hasSubstantiveManagedBlock(existing) && preserveManagedBlock && !forceFullRegenerate) {
|
|
820
|
+
const unmatchedCandidates = allCandidates.filter((candidate) => {
|
|
821
|
+
return !findBestTaskMatch(candidate, existingManagedTasks, {
|
|
822
|
+
allowFuzzy: true,
|
|
823
|
+
minScore: 0.72
|
|
824
|
+
});
|
|
825
|
+
});
|
|
826
|
+
const preserveModeCandidates = filterPreserveModeCandidates(unmatchedCandidates);
|
|
827
|
+
|
|
828
|
+
if (preserveModeCandidates.length === 0) {
|
|
829
|
+
return existingContent;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
return insertPreserveModeTasks(existingContent, existing, preserveModeCandidates);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
if (hasSubstantiveManagedBlock(existing) && !forceFullRegenerate) {
|
|
836
|
+
throw new Error('Refusing to regenerate a substantive managed roadmap block. Rerun with --full-regen to replace it explicitly.');
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const merged = mergeWithExisting(allCandidates, existingManagedTasks, {
|
|
840
|
+
allowFuzzy: true,
|
|
841
|
+
includeUnmatchedExisting: false
|
|
842
|
+
});
|
|
631
843
|
const model = createModel(scan, merged, config, [profileSection, ...generatedZeroModeSection, ...configSections, ...pluginSections], existingCheckedById);
|
|
632
844
|
const profile = config.roadmapProfile || 'compact';
|
|
633
845
|
const managedBody = renderBody(model, profile);
|