roadmapsmith 0.9.15 → 0.9.22
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 +76 -20
- 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 +35 -0
- package/src/generator/index.js +219 -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,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: roadmap-update
|
|
3
|
+
description: Apply evidence-backed checklist sync through the RoadmapSmith CLI.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# RoadmapSmith Update
|
|
7
|
+
|
|
8
|
+
Use this command when the user wants the direct sync surface without routing through the legacy `/roadmap-sync <action>` root.
|
|
9
|
+
|
|
10
|
+
## Required behavior
|
|
11
|
+
|
|
12
|
+
1. Prefer the local engine inside this repository:
|
|
13
|
+
- `node roadmap-skill/bin/cli.js sync --project-root .`
|
|
14
|
+
- on this Windows machine, prefer `C:\Program Files\nodejs\node.exe roadmap-skill/bin/cli.js sync --project-root .` if `node` is not in PATH
|
|
15
|
+
2. Otherwise prefer `roadmapsmith sync --project-root .`.
|
|
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: sync updates checklist state from repository evidence; it is not a full regeneration path.
|
|
@@ -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.22"
|
|
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.22"
|
|
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.22"
|
|
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.22"
|
|
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.22"
|
|
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.22"
|
|
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.22"
|
|
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.22"
|
|
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.22"
|
|
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.22"
|
|
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.22"
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
}
|
package/src/classifier/index.js
CHANGED
|
@@ -12,6 +12,14 @@ 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;
|
|
16
24
|
|
|
17
25
|
function readPackageDeps(projectRoot) {
|
|
@@ -59,9 +67,14 @@ function classifyProject({ projectRoot, files }) {
|
|
|
59
67
|
|
|
60
68
|
let webScore = 0;
|
|
61
69
|
let landingScore = 0;
|
|
70
|
+
let electronScore = 0;
|
|
62
71
|
const deps = readPackageDeps(projectRoot);
|
|
63
72
|
|
|
64
73
|
for (const dep of deps) {
|
|
74
|
+
if (ELECTRON_DEPS.has(dep)) {
|
|
75
|
+
electronScore += 3;
|
|
76
|
+
signals.push(`dependency: ${dep}`);
|
|
77
|
+
}
|
|
65
78
|
if (WEB_DEPS.has(dep)) {
|
|
66
79
|
webScore += 2;
|
|
67
80
|
signals.push(`dependency: ${dep}`);
|
|
@@ -82,6 +95,23 @@ function classifyProject({ projectRoot, files }) {
|
|
|
82
95
|
}
|
|
83
96
|
}
|
|
84
97
|
|
|
98
|
+
if (hasDir(files, 'electron/')) {
|
|
99
|
+
electronScore += 3;
|
|
100
|
+
signals.push('directory: electron');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (files.some((file) => /^electron\/.+\.(js|ts|cjs|mjs)$/.test(file))) {
|
|
104
|
+
electronScore += 2;
|
|
105
|
+
signals.push('electron main/preload sources');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (const cfg of ELECTRON_CONFIGS) {
|
|
109
|
+
if (hasFilename(files, cfg)) {
|
|
110
|
+
electronScore += 2;
|
|
111
|
+
signals.push(`config: ${cfg}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
85
115
|
for (const cfg of WEB_CONFIGS) {
|
|
86
116
|
if (hasFilename(files, cfg)) {
|
|
87
117
|
webScore += 3;
|
|
@@ -122,6 +152,11 @@ function classifyProject({ projectRoot, files }) {
|
|
|
122
152
|
return { type: 'npm-package', confidence: 'low', signals };
|
|
123
153
|
}
|
|
124
154
|
|
|
155
|
+
if (electronScore >= 3) {
|
|
156
|
+
const confidence = electronScore >= 6 ? 'high' : 'medium';
|
|
157
|
+
return { type: 'electron-app', confidence, signals };
|
|
158
|
+
}
|
|
159
|
+
|
|
125
160
|
if (webScore >= 3) {
|
|
126
161
|
const type = landingScore >= 3 ? 'landing-site' : 'frontend-web';
|
|
127
162
|
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,54 @@ 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 buildManagedAdditionsLines(tasks, options = {}) {
|
|
211
|
+
const groups = groupByPhase(tasks);
|
|
212
|
+
const lines = [];
|
|
213
|
+
const includeSectionHeading = options.includeSectionHeading !== false;
|
|
214
|
+
|
|
215
|
+
if (includeSectionHeading) {
|
|
216
|
+
lines.push(`## ${ADDITIONS_SECTION_TITLE}`);
|
|
217
|
+
lines.push('');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
for (const phase of PHASE_ORDER) {
|
|
221
|
+
if (!groups[phase] || groups[phase].length === 0) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
lines.push(`### Phase ${phase}`);
|
|
225
|
+
for (const task of groups[phase]) {
|
|
226
|
+
lines.push(renderAdditionTask(task));
|
|
227
|
+
}
|
|
228
|
+
lines.push('');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return stripTrailingBlankLines(lines);
|
|
232
|
+
}
|
|
233
|
+
|
|
184
234
|
const WEB_CANDIDATES_COMMON = [
|
|
185
235
|
{ text: 'Add SEO metadata: title, description, and canonical URL for all pages', phase: 'P0' },
|
|
186
236
|
{ text: 'Implement responsive and mobile-first layout across all breakpoints', phase: 'P0' },
|
|
@@ -285,6 +335,9 @@ function applyTaskMatchers(scan, config) {
|
|
|
285
335
|
}
|
|
286
336
|
|
|
287
337
|
function inferPhase(existingTask) {
|
|
338
|
+
const textPhaseMatch = String(existingTask.text || '').match(PHASE_LABEL_RE);
|
|
339
|
+
if (textPhaseMatch) return textPhaseMatch[1].toUpperCase();
|
|
340
|
+
|
|
288
341
|
const section = String(existingTask.section || '').toUpperCase();
|
|
289
342
|
if (section.includes('P0')) return 'P0';
|
|
290
343
|
if (section.includes('P1')) return 'P1';
|
|
@@ -292,12 +345,134 @@ function inferPhase(existingTask) {
|
|
|
292
345
|
return 'P1';
|
|
293
346
|
}
|
|
294
347
|
|
|
295
|
-
function
|
|
348
|
+
function sortTasksByPhaseAndText(tasks) {
|
|
349
|
+
return tasks.slice().sort((left, right) => {
|
|
350
|
+
const leftPhaseIndex = PHASE_ORDER.indexOf(left.phase);
|
|
351
|
+
const rightPhaseIndex = PHASE_ORDER.indexOf(right.phase);
|
|
352
|
+
if (leftPhaseIndex !== rightPhaseIndex) {
|
|
353
|
+
return leftPhaseIndex - rightPhaseIndex;
|
|
354
|
+
}
|
|
355
|
+
return left.text.localeCompare(right.text);
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function findPhaseSectionRange(lines, managedRange, phase) {
|
|
360
|
+
const headingPattern = /^(#{2,4})\s+(.*)$/;
|
|
361
|
+
const phasePattern = new RegExp(`\\b${phase}\\b`, 'i');
|
|
362
|
+
let sectionStart = -1;
|
|
363
|
+
let sectionLevel = 0;
|
|
364
|
+
|
|
365
|
+
for (let index = managedRange.start + 1; index < managedRange.end; index += 1) {
|
|
366
|
+
const match = lines[index].trim().match(headingPattern);
|
|
367
|
+
if (!match) {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (!phasePattern.test(match[2])) {
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
sectionStart = index;
|
|
374
|
+
sectionLevel = match[1].length;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (sectionStart < 0) {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
let sectionEnd = managedRange.end;
|
|
382
|
+
for (let index = sectionStart + 1; index < managedRange.end; index += 1) {
|
|
383
|
+
const match = lines[index].trim().match(headingPattern);
|
|
384
|
+
if (!match) {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (match[1].length <= sectionLevel) {
|
|
388
|
+
sectionEnd = index;
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
start: sectionStart,
|
|
395
|
+
end: sectionEnd
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function buildPreserveModeInsertions(parsedRoadmap, tasks) {
|
|
400
|
+
const managedTasks = sortTasksByPhaseAndText(tasksInManagedBlock(parsedRoadmap));
|
|
401
|
+
const groups = groupByPhase(tasks);
|
|
402
|
+
const lines = parsedRoadmap.lines;
|
|
403
|
+
const insertions = [];
|
|
404
|
+
const fallbackTasks = [];
|
|
405
|
+
|
|
406
|
+
for (const phase of PHASE_ORDER) {
|
|
407
|
+
const phaseTasks = sortTasksByPhaseAndText(groups[phase] || []);
|
|
408
|
+
if (phaseTasks.length === 0) {
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const samePhaseTasks = managedTasks.filter((task) => inferPhase(task) === phase);
|
|
413
|
+
if (samePhaseTasks.length > 0) {
|
|
414
|
+
const anchor = samePhaseTasks.reduce((latest, task) => {
|
|
415
|
+
if (!latest || task.lastChildLineIndex > latest.lastChildLineIndex) {
|
|
416
|
+
return task;
|
|
417
|
+
}
|
|
418
|
+
return latest;
|
|
419
|
+
}, null);
|
|
420
|
+
insertions.push({
|
|
421
|
+
index: anchor.lastChildLineIndex + 1,
|
|
422
|
+
lines: phaseTasks.map(renderAdditionTask)
|
|
423
|
+
});
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const phaseSection = findPhaseSectionRange(lines, parsedRoadmap.managedRange, phase);
|
|
428
|
+
if (phaseSection) {
|
|
429
|
+
let insertionIndex = phaseSection.end;
|
|
430
|
+
while (insertionIndex > phaseSection.start + 1 && !lines[insertionIndex - 1].trim()) {
|
|
431
|
+
insertionIndex -= 1;
|
|
432
|
+
}
|
|
433
|
+
insertions.push({
|
|
434
|
+
index: insertionIndex,
|
|
435
|
+
lines: phaseTasks.map(renderAdditionTask)
|
|
436
|
+
});
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
fallbackTasks.push(...phaseTasks);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (fallbackTasks.length > 0) {
|
|
444
|
+
const fallbackLines = buildManagedAdditionsLines(fallbackTasks, { includeSectionHeading: true });
|
|
445
|
+
insertions.push({
|
|
446
|
+
index: parsedRoadmap.managedRange.end,
|
|
447
|
+
lines: ['', ...fallbackLines]
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return insertions.sort((left, right) => right.index - left.index);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function insertPreserveModeTasks(existingContent, parsedRoadmap, tasks) {
|
|
455
|
+
if (!parsedRoadmap || !parsedRoadmap.managedRange || tasks.length === 0) {
|
|
456
|
+
return existingContent;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const nextLines = parsedRoadmap.lines.slice();
|
|
460
|
+
const insertions = buildPreserveModeInsertions(parsedRoadmap, tasks);
|
|
461
|
+
for (const insertion of insertions) {
|
|
462
|
+
nextLines.splice(insertion.index, 0, ...insertion.lines);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return nextLines.join('\n');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function mergeWithExisting(candidates, existingTasks, options = {}) {
|
|
296
469
|
const matchedExistingIds = new Set();
|
|
297
470
|
const merged = [];
|
|
298
471
|
|
|
299
472
|
for (const candidate of candidates) {
|
|
300
|
-
const match = findBestTaskMatch(candidate, existingTasks
|
|
473
|
+
const match = findBestTaskMatch(candidate, existingTasks, {
|
|
474
|
+
allowFuzzy: options.allowFuzzy !== false
|
|
475
|
+
});
|
|
301
476
|
if (match) {
|
|
302
477
|
matchedExistingIds.add(match.task.id);
|
|
303
478
|
merged.push({
|
|
@@ -311,20 +486,22 @@ function mergeWithExisting(candidates, existingTasks) {
|
|
|
311
486
|
merged.push(candidate);
|
|
312
487
|
}
|
|
313
488
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
489
|
+
if (options.includeUnmatchedExisting) {
|
|
490
|
+
for (const existing of existingTasks) {
|
|
491
|
+
if (matchedExistingIds.has(existing.id)) {
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
318
494
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
495
|
+
const phase = inferPhase(existing);
|
|
496
|
+
merged.push({
|
|
497
|
+
id: existing.id,
|
|
498
|
+
text: existing.text,
|
|
499
|
+
phase,
|
|
500
|
+
priority: phase,
|
|
501
|
+
checked: existing.checked,
|
|
502
|
+
source: 'existing'
|
|
503
|
+
});
|
|
504
|
+
}
|
|
328
505
|
}
|
|
329
506
|
|
|
330
507
|
return dedupeTasks(merged);
|
|
@@ -566,6 +743,8 @@ function generateRoadmapDocument(options) {
|
|
|
566
743
|
const zeroModeConfig = config.zeroMode || {};
|
|
567
744
|
const plugins = options.plugins || [];
|
|
568
745
|
const existingContent = options.existingContent || '';
|
|
746
|
+
const preserveManagedBlock = options.preserveManagedBlock === true;
|
|
747
|
+
const forceFullRegenerate = options.forceFullRegenerate === true;
|
|
569
748
|
|
|
570
749
|
const scan = scanProject(projectRoot);
|
|
571
750
|
const existing = parseRoadmap(existingContent);
|
|
@@ -573,7 +752,7 @@ function generateRoadmapDocument(options) {
|
|
|
573
752
|
for (const task of existing.tasks) {
|
|
574
753
|
existingCheckedById[task.id] = task.checked;
|
|
575
754
|
}
|
|
576
|
-
const
|
|
755
|
+
const existingManagedTasks = tasksInManagedBlock(existing);
|
|
577
756
|
|
|
578
757
|
const pluginTaskCandidates = collectPluginContributions(plugins, 'registerTaskDetectors', {
|
|
579
758
|
projectRoot,
|
|
@@ -627,7 +806,28 @@ function generateRoadmapDocument(options) {
|
|
|
627
806
|
|
|
628
807
|
const baseCandidates = buildDefaultCandidates(scan, config);
|
|
629
808
|
const matcherCandidates = applyTaskMatchers(scan, config);
|
|
630
|
-
const
|
|
809
|
+
const allCandidates = dedupeTasks([...baseCandidates, ...matcherCandidates, ...pluginTaskCandidates]);
|
|
810
|
+
|
|
811
|
+
if (hasSubstantiveManagedBlock(existing) && preserveManagedBlock && !forceFullRegenerate) {
|
|
812
|
+
const unmatchedCandidates = allCandidates.filter((candidate) => {
|
|
813
|
+
return !findBestTaskMatch(candidate, existingManagedTasks, { allowFuzzy: false });
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
if (unmatchedCandidates.length === 0) {
|
|
817
|
+
return existingContent;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
return insertPreserveModeTasks(existingContent, existing, unmatchedCandidates);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (hasSubstantiveManagedBlock(existing) && !forceFullRegenerate) {
|
|
824
|
+
throw new Error('Refusing to regenerate a substantive managed roadmap block. Rerun with --full-regen to replace it explicitly.');
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const merged = mergeWithExisting(allCandidates, existingManagedTasks, {
|
|
828
|
+
allowFuzzy: true,
|
|
829
|
+
includeUnmatchedExisting: false
|
|
830
|
+
});
|
|
631
831
|
const model = createModel(scan, merged, config, [profileSection, ...generatedZeroModeSection, ...configSections, ...pluginSections], existingCheckedById);
|
|
632
832
|
const profile = config.roadmapProfile || 'compact';
|
|
633
833
|
const managedBody = renderBody(model, profile);
|