roadmapsmith 0.1.1 → 0.4.0
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/README.md +46 -0
- package/package.json +1 -1
- package/src/config.js +21 -1
- package/src/generator/index.js +197 -13
- package/src/model.js +7 -2
- package/src/renderer/compact.js +89 -0
- package/src/renderer/helpers.js +19 -0
- package/src/renderer/index.js +18 -0
- package/src/renderer/professional.js +467 -0
package/README.md
CHANGED
|
@@ -18,6 +18,31 @@ npx skills add PapiScholz/roadmapsmith --skill roadmap-sync
|
|
|
18
18
|
|
|
19
19
|
This adds the `roadmap-sync` agent skill. It does not install the CLI package.
|
|
20
20
|
|
|
21
|
+
## Operating Modes
|
|
22
|
+
|
|
23
|
+
### Zero Mode
|
|
24
|
+
|
|
25
|
+
Agent-guided discovery for empty or low-context repositories. The developer has a product idea but no implementation files, no stack decision, and no ROADMAP.md yet.
|
|
26
|
+
|
|
27
|
+
The CLI creates governance files. The AI agent (using the `roadmap-sync` skill) performs the discovery interview before generating the roadmap.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
roadmapsmith init
|
|
31
|
+
roadmapsmith generate --project-root .
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Sync/Audit Mode
|
|
35
|
+
|
|
36
|
+
Repository-backed roadmap generation, validation, and synchronization. Use when the repository already has code, tests, docs, TODOs, or an existing ROADMAP.md.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
roadmapsmith generate --project-root .
|
|
40
|
+
roadmapsmith validate --json
|
|
41
|
+
roadmapsmith sync --audit
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
21
46
|
## Commands
|
|
22
47
|
|
|
23
48
|
```bash
|
|
@@ -61,6 +86,27 @@ Create `roadmap-skill.config.json`:
|
|
|
61
86
|
{
|
|
62
87
|
"roadmapFile": "./ROADMAP.md",
|
|
63
88
|
"agentsFile": "./AGENTS.md",
|
|
89
|
+
|
|
90
|
+
// Forward-compatible fields — recognized by the skill/agent for Zero Mode discovery context,
|
|
91
|
+
// but not yet read by the generator or validator. Safe to add now; they will be wired in a future release.
|
|
92
|
+
"northStar": "Ship a self-hosted CLI tool for website capture and AI-readable design analysis.",
|
|
93
|
+
"targetUser": "Frontend developers, full-stack developers, and AI coding agents.",
|
|
94
|
+
"problemStatement": "Developers lack a unified tool to capture screenshots, crawl pages, extract assets, and export structured context.",
|
|
95
|
+
"v1Outcome": "A stable CLI that captures full-page screenshots, crawls internal links, exports metadata, and produces an AI-readable report.",
|
|
96
|
+
"antiGoals": [
|
|
97
|
+
"Do not bypass authentication",
|
|
98
|
+
"Do not target private systems without authorization"
|
|
99
|
+
],
|
|
100
|
+
"risks": [
|
|
101
|
+
"Browser automation instability",
|
|
102
|
+
"Scope creep into generic scraping"
|
|
103
|
+
],
|
|
104
|
+
"exitCriteria": [
|
|
105
|
+
"CLI works against a public test site",
|
|
106
|
+
"Screenshots and metadata are exported deterministically",
|
|
107
|
+
"README documents safe and authorized usage"
|
|
108
|
+
],
|
|
109
|
+
|
|
64
110
|
"taskMatchers": [
|
|
65
111
|
{
|
|
66
112
|
"pattern": "src/payments/",
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -7,10 +7,23 @@ const { readTextIfExists } = require('./io');
|
|
|
7
7
|
const DEFAULT_CONFIG = {
|
|
8
8
|
roadmapFile: './ROADMAP.md',
|
|
9
9
|
agentsFile: './AGENTS.md',
|
|
10
|
+
roadmapProfile: 'compact',
|
|
10
11
|
taskMatchers: [],
|
|
11
12
|
validators: [],
|
|
12
13
|
customSections: [],
|
|
13
14
|
plugins: [],
|
|
15
|
+
product: {
|
|
16
|
+
name: '',
|
|
17
|
+
northStar: '',
|
|
18
|
+
positioning: '',
|
|
19
|
+
primaryUser: '',
|
|
20
|
+
targetOutcome: '',
|
|
21
|
+
antiGoals: [],
|
|
22
|
+
risks: [],
|
|
23
|
+
successCriteria: [],
|
|
24
|
+
steps: [],
|
|
25
|
+
phases: []
|
|
26
|
+
},
|
|
14
27
|
milestones: [
|
|
15
28
|
{ version: 'v0.1', goal: 'Foundation baseline complete' },
|
|
16
29
|
{ version: 'v0.2', goal: 'Core feature coverage stabilized' },
|
|
@@ -53,6 +66,13 @@ function mergeConfig(userConfig) {
|
|
|
53
66
|
phaseTemplates: {
|
|
54
67
|
...DEFAULT_CONFIG.phaseTemplates,
|
|
55
68
|
...((userConfig && userConfig.phaseTemplates) || {})
|
|
69
|
+
},
|
|
70
|
+
product: {
|
|
71
|
+
...DEFAULT_CONFIG.product,
|
|
72
|
+
...((userConfig && userConfig.product) || {}),
|
|
73
|
+
phases: (userConfig && userConfig.product && Array.isArray(userConfig.product.phases))
|
|
74
|
+
? userConfig.product.phases
|
|
75
|
+
: DEFAULT_CONFIG.product.phases
|
|
56
76
|
}
|
|
57
77
|
};
|
|
58
78
|
}
|
|
@@ -185,4 +205,4 @@ module.exports = {
|
|
|
185
205
|
loadPlugins,
|
|
186
206
|
resolveAgentsFile,
|
|
187
207
|
resolveRoadmapFile
|
|
188
|
-
};
|
|
208
|
+
};
|
package/src/generator/index.js
CHANGED
|
@@ -8,6 +8,7 @@ const { slugify, ensureTrailingNewline } = require('../utils');
|
|
|
8
8
|
const { parseRoadmap, upsertManagedBlock } = require('../parser');
|
|
9
9
|
const { findBestTaskMatch, dedupeTasks } = require('../match');
|
|
10
10
|
const { collectPluginContributions } = require('../config');
|
|
11
|
+
const { renderBody } = require('../renderer');
|
|
11
12
|
|
|
12
13
|
function detectModules(files) {
|
|
13
14
|
const modules = new Set();
|
|
@@ -73,6 +74,37 @@ function collectTodoHints(projectRoot, files) {
|
|
|
73
74
|
return hints;
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
function collectCodeTodoHints(projectRoot, files) {
|
|
78
|
+
const hints = [];
|
|
79
|
+
const codeFiles = files.filter((file) => /\.(js|ts|tsx|py|go|rs)$/.test(file)).slice(0, 120);
|
|
80
|
+
|
|
81
|
+
for (const file of codeFiles) {
|
|
82
|
+
const absolutePath = path.resolve(projectRoot, file);
|
|
83
|
+
let content = '';
|
|
84
|
+
try {
|
|
85
|
+
content = fs.readFileSync(absolutePath, 'utf8');
|
|
86
|
+
} catch {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const lines = content.split(/\r?\n/);
|
|
91
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
92
|
+
if (/TODO|FIXME/i.test(lines[i])) {
|
|
93
|
+
hints.push({
|
|
94
|
+
file,
|
|
95
|
+
line: i + 1,
|
|
96
|
+
text: lines[i].trim()
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (hints.length >= 6) {
|
|
100
|
+
return hints;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return hints;
|
|
106
|
+
}
|
|
107
|
+
|
|
76
108
|
function scanProject(projectRoot) {
|
|
77
109
|
const files = walkFiles(projectRoot);
|
|
78
110
|
const languages = detectLanguages(files);
|
|
@@ -80,17 +112,20 @@ function scanProject(projectRoot) {
|
|
|
80
112
|
const modules = detectModules(files);
|
|
81
113
|
const commands = detectCommands(files);
|
|
82
114
|
const todos = collectTodoHints(projectRoot, files);
|
|
115
|
+
const codeTodos = collectCodeTodoHints(projectRoot, files);
|
|
83
116
|
|
|
84
117
|
const implementedFiles = files.filter((file) => /\.(js|ts|tsx|py|go|rs|java|kt|cs)$/.test(file));
|
|
85
118
|
const testFiles = files.filter((file) => /(^|\/)(__tests__|tests)\//.test(file) || /\.test\.|\.spec\.|_test\.go$/.test(file));
|
|
86
119
|
|
|
87
120
|
return {
|
|
121
|
+
projectRoot,
|
|
88
122
|
files,
|
|
89
123
|
languages,
|
|
90
124
|
testFrameworks,
|
|
91
125
|
modules,
|
|
92
126
|
commands,
|
|
93
127
|
todos,
|
|
128
|
+
codeTodos,
|
|
94
129
|
implementedCount: implementedFiles.length,
|
|
95
130
|
testCount: testFiles.length
|
|
96
131
|
};
|
|
@@ -328,10 +363,131 @@ function renderManagedBody(model) {
|
|
|
328
363
|
return ensureTrailingNewline(lines.join('\n')).trimEnd();
|
|
329
364
|
}
|
|
330
365
|
|
|
366
|
+
function inferProjectName(projectRoot) {
|
|
367
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
368
|
+
try {
|
|
369
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
370
|
+
if (pkg.name) return pkg.name;
|
|
371
|
+
} catch {
|
|
372
|
+
// ignore — try other manifests
|
|
373
|
+
}
|
|
374
|
+
return path.basename(projectRoot);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function buildPhasesDetailed(phases, config) {
|
|
378
|
+
const configPhases = config.product && Array.isArray(config.product.phases)
|
|
379
|
+
? config.product.phases : [];
|
|
380
|
+
if (configPhases.length > 0) return configPhases;
|
|
381
|
+
|
|
382
|
+
return [
|
|
383
|
+
{
|
|
384
|
+
phaseNumber: 1,
|
|
385
|
+
title: 'Foundation Baseline',
|
|
386
|
+
priority: 'P0',
|
|
387
|
+
objective: 'Establish a stable, testable baseline that unblocks all downstream delivery.',
|
|
388
|
+
steps: [{
|
|
389
|
+
stepNumber: 1,
|
|
390
|
+
title: 'Core Implementation',
|
|
391
|
+
priority: 'P0',
|
|
392
|
+
dependsOn: [],
|
|
393
|
+
objective: 'Close critical path items.',
|
|
394
|
+
tasks: phases.P0,
|
|
395
|
+
exitCriteria: [
|
|
396
|
+
{ text: 'All P0 tasks validated by evidence', priority: 'P0' },
|
|
397
|
+
{ text: 'CI is green on main', priority: 'P0' }
|
|
398
|
+
],
|
|
399
|
+
risks: []
|
|
400
|
+
}]
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
phaseNumber: 2,
|
|
404
|
+
title: 'Feature Completeness',
|
|
405
|
+
priority: 'P1',
|
|
406
|
+
objective: 'Expand functionality and reduce operational risk.',
|
|
407
|
+
steps: [{
|
|
408
|
+
stepNumber: 1,
|
|
409
|
+
title: 'Feature Delivery',
|
|
410
|
+
priority: 'P1',
|
|
411
|
+
dependsOn: [1],
|
|
412
|
+
objective: 'Deliver planned P1 features.',
|
|
413
|
+
tasks: phases.P1,
|
|
414
|
+
exitCriteria: [
|
|
415
|
+
{ text: 'All P1 tasks validated by evidence', priority: 'P1' },
|
|
416
|
+
{ text: 'No regressions on Phase 1 functionality', priority: 'P0' }
|
|
417
|
+
],
|
|
418
|
+
risks: []
|
|
419
|
+
}]
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
phaseNumber: 3,
|
|
423
|
+
title: 'Release Hardening',
|
|
424
|
+
priority: 'P2',
|
|
425
|
+
objective: 'Complete hardening and production readiness for v1.0.',
|
|
426
|
+
steps: [{
|
|
427
|
+
stepNumber: 1,
|
|
428
|
+
title: 'Hardening',
|
|
429
|
+
priority: 'P2',
|
|
430
|
+
dependsOn: [2],
|
|
431
|
+
objective: 'Close P2 items and harden release.',
|
|
432
|
+
tasks: phases.P2,
|
|
433
|
+
exitCriteria: [
|
|
434
|
+
{ text: 'All P2 tasks validated by evidence', priority: 'P2' },
|
|
435
|
+
{ text: 'Release candidate checklist complete', priority: 'P0' }
|
|
436
|
+
],
|
|
437
|
+
risks: []
|
|
438
|
+
}]
|
|
439
|
+
}
|
|
440
|
+
];
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function buildSteps(phases, config) {
|
|
444
|
+
const configSteps = config.product && Array.isArray(config.product.steps) ? config.product.steps : [];
|
|
445
|
+
if (configSteps.length > 0) return configSteps;
|
|
446
|
+
|
|
447
|
+
const stepDefs = [
|
|
448
|
+
{ stepNumber: 1, title: 'Foundation Baseline', priority: 'P0', dependsOn: [], phaseKey: 'P0',
|
|
449
|
+
objective: 'Establish a stable, testable baseline that unblocks all downstream delivery.' },
|
|
450
|
+
{ stepNumber: 2, title: 'Feature Completeness', priority: 'P1', dependsOn: [1], phaseKey: 'P1',
|
|
451
|
+
objective: 'Expand functionality, improve reliability, and reduce operational risk.' },
|
|
452
|
+
{ stepNumber: 3, title: 'Release Hardening', priority: 'P2', dependsOn: [2], phaseKey: 'P2',
|
|
453
|
+
objective: 'Complete hardening, final validation, and production readiness for v1.0.' }
|
|
454
|
+
];
|
|
455
|
+
|
|
456
|
+
const defaultExitCriteria = {
|
|
457
|
+
1: ['All P0 tasks validated by evidence', 'CI is green on main'],
|
|
458
|
+
2: ['All P1 tasks validated by evidence', 'No regressions on P0 functionality'],
|
|
459
|
+
3: ['All P2 tasks validated by evidence', 'Release candidate checklist complete']
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
return stepDefs.map((def) => ({
|
|
463
|
+
stepNumber: def.stepNumber,
|
|
464
|
+
title: def.title,
|
|
465
|
+
priority: def.priority,
|
|
466
|
+
dependsOn: def.dependsOn,
|
|
467
|
+
objective: def.objective,
|
|
468
|
+
deliverables: phases[def.phaseKey] || [],
|
|
469
|
+
exitCriteria: defaultExitCriteria[def.stepNumber] || [],
|
|
470
|
+
risks: []
|
|
471
|
+
}));
|
|
472
|
+
}
|
|
473
|
+
|
|
331
474
|
function createModel(scan, tasks, config, customSections, checkedById) {
|
|
332
475
|
const phases = groupByPhase(tasks);
|
|
333
476
|
|
|
477
|
+
const implemented = [
|
|
478
|
+
`${scan.implementedCount} implementation files across ${scan.languages.join(', ') || 'detected stack'}`
|
|
479
|
+
];
|
|
480
|
+
|
|
481
|
+
const scaffold = scan.modules.length > 0
|
|
482
|
+
? scan.modules.slice(0, 6).map((m) => `Module "${m}" partially implemented — coverage unknown`)
|
|
483
|
+
: [];
|
|
484
|
+
|
|
485
|
+
const knownLimitations = (scan.codeTodos || []).slice(0, 6).map((t) => `${t.file}:${t.line} — ${t.text.slice(0, 80)}`);
|
|
486
|
+
|
|
334
487
|
const currentState = {
|
|
488
|
+
implemented,
|
|
489
|
+
scaffold,
|
|
490
|
+
knownLimitations,
|
|
335
491
|
implementedSummary: `${scan.implementedCount} implementation files detected`,
|
|
336
492
|
todoSummary: `${scan.todos.length} TODO/FIXME markers detected`,
|
|
337
493
|
stackSummary: scan.languages.length > 0 ? scan.languages.join(', ') : 'No language-specific stack detected'
|
|
@@ -351,23 +507,50 @@ function createModel(scan, tasks, config, customSections, checkedById) {
|
|
|
351
507
|
commandBreakdown.push(`Command: ${command}`);
|
|
352
508
|
}
|
|
353
509
|
|
|
510
|
+
const productConfig = config.product || {};
|
|
511
|
+
const inferredName = inferProjectName(scan.projectRoot || process.cwd());
|
|
512
|
+
const product = {
|
|
513
|
+
name: productConfig.name || inferredName,
|
|
514
|
+
northStar: productConfig.northStar || '',
|
|
515
|
+
positioning: productConfig.positioning || '',
|
|
516
|
+
primaryUser: productConfig.primaryUser || '',
|
|
517
|
+
targetOutcome: productConfig.targetOutcome || ''
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const defaultRisks = [
|
|
521
|
+
'Roadmap drift if checklist state diverges from repository evidence',
|
|
522
|
+
'Silent regressions when tasks are marked complete without tests',
|
|
523
|
+
'Scope creep that delays the v1.0 milestone path'
|
|
524
|
+
];
|
|
525
|
+
const defaultAntiGoals = [
|
|
526
|
+
'Do not mark tasks complete without repository evidence',
|
|
527
|
+
'Do not introduce non-deterministic roadmap formatting',
|
|
528
|
+
'Do not hide validation failures from roadmap consumers'
|
|
529
|
+
];
|
|
530
|
+
|
|
531
|
+
const risks = (productConfig.risks && productConfig.risks.length > 0) ? productConfig.risks : defaultRisks;
|
|
532
|
+
const antiGoals = (productConfig.antiGoals && productConfig.antiGoals.length > 0) ? productConfig.antiGoals : defaultAntiGoals;
|
|
533
|
+
const successCriteria = productConfig.successCriteria || [];
|
|
534
|
+
|
|
535
|
+
const northStar = productConfig.northStar
|
|
536
|
+
|| 'Ship validated, high-impact increments with deterministic delivery and transparent completion evidence.';
|
|
537
|
+
|
|
538
|
+
const steps = buildSteps(phases, config);
|
|
539
|
+
const phasesDetailed = buildPhasesDetailed(phases, config);
|
|
540
|
+
|
|
354
541
|
return createRoadmapModel({
|
|
355
|
-
northStar
|
|
542
|
+
northStar,
|
|
543
|
+
product,
|
|
356
544
|
currentState,
|
|
357
545
|
phases,
|
|
546
|
+
steps,
|
|
547
|
+
phasesDetailed,
|
|
358
548
|
milestones: config.milestones,
|
|
359
549
|
commandBreakdown,
|
|
360
550
|
exitCriteria,
|
|
361
|
-
risks
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
'Scope creep that delays the v1.0 milestone path'
|
|
365
|
-
],
|
|
366
|
-
antiGoals: [
|
|
367
|
-
'Do not mark tasks complete without repository evidence',
|
|
368
|
-
'Do not introduce non-deterministic roadmap formatting',
|
|
369
|
-
'Do not hide validation failures from roadmap consumers'
|
|
370
|
-
],
|
|
551
|
+
risks,
|
|
552
|
+
antiGoals,
|
|
553
|
+
successCriteria,
|
|
371
554
|
customSections,
|
|
372
555
|
checkedById
|
|
373
556
|
});
|
|
@@ -424,7 +607,8 @@ function generateRoadmapDocument(options) {
|
|
|
424
607
|
const matcherCandidates = applyTaskMatchers(scan, config);
|
|
425
608
|
const merged = mergeWithExisting([...baseCandidates, ...matcherCandidates, ...pluginTaskCandidates], existingPhaseTasks);
|
|
426
609
|
const model = createModel(scan, merged, config, [...configSections, ...pluginSections], existingCheckedById);
|
|
427
|
-
const
|
|
610
|
+
const profile = config.roadmapProfile || 'compact';
|
|
611
|
+
const managedBody = renderBody(model, profile);
|
|
428
612
|
|
|
429
613
|
return upsertManagedBlock(existingContent, managedBody);
|
|
430
614
|
}
|
|
@@ -433,4 +617,4 @@ module.exports = {
|
|
|
433
617
|
generateRoadmapDocument,
|
|
434
618
|
renderManagedBody,
|
|
435
619
|
scanProject
|
|
436
|
-
};
|
|
620
|
+
};
|
package/src/model.js
CHANGED
|
@@ -10,14 +10,19 @@ function phaseWeight(phase) {
|
|
|
10
10
|
function createRoadmapModel(input) {
|
|
11
11
|
return {
|
|
12
12
|
northStar: input.northStar,
|
|
13
|
+
product: input.product || {},
|
|
13
14
|
currentState: input.currentState,
|
|
14
15
|
phases: input.phases,
|
|
16
|
+
steps: input.steps || [],
|
|
17
|
+
phasesDetailed: input.phasesDetailed || [],
|
|
15
18
|
milestones: input.milestones,
|
|
16
19
|
commandBreakdown: input.commandBreakdown,
|
|
17
20
|
exitCriteria: input.exitCriteria,
|
|
18
21
|
risks: input.risks,
|
|
19
22
|
antiGoals: input.antiGoals,
|
|
20
|
-
|
|
23
|
+
successCriteria: input.successCriteria || [],
|
|
24
|
+
customSections: input.customSections || [],
|
|
25
|
+
checkedById: input.checkedById || {}
|
|
21
26
|
};
|
|
22
27
|
}
|
|
23
28
|
|
|
@@ -25,4 +30,4 @@ module.exports = {
|
|
|
25
30
|
PHASE_ORDER,
|
|
26
31
|
createRoadmapModel,
|
|
27
32
|
phaseWeight
|
|
28
|
-
};
|
|
33
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { slugify, ensureTrailingNewline } = require('../utils');
|
|
4
|
+
const { taskLine, checkedState } = require('./helpers');
|
|
5
|
+
|
|
6
|
+
function renderCompact(model) {
|
|
7
|
+
const lines = [];
|
|
8
|
+
|
|
9
|
+
lines.push('# Project Roadmap');
|
|
10
|
+
lines.push('');
|
|
11
|
+
lines.push('## Product North Star');
|
|
12
|
+
lines.push(model.northStar);
|
|
13
|
+
lines.push('');
|
|
14
|
+
|
|
15
|
+
lines.push('## Current State');
|
|
16
|
+
lines.push(`- Implemented surface: ${model.currentState.implementedSummary}`);
|
|
17
|
+
lines.push(`- TODO surface: ${model.currentState.todoSummary}`);
|
|
18
|
+
lines.push(`- Detected stacks: ${model.currentState.stackSummary}`);
|
|
19
|
+
lines.push('');
|
|
20
|
+
|
|
21
|
+
lines.push('## Phased Roadmap');
|
|
22
|
+
lines.push('');
|
|
23
|
+
lines.push('### Phase P0 (Critical)');
|
|
24
|
+
for (const task of model.phases.P0) {
|
|
25
|
+
lines.push(taskLine(task));
|
|
26
|
+
}
|
|
27
|
+
lines.push('');
|
|
28
|
+
lines.push('### Phase P1 (Important)');
|
|
29
|
+
for (const task of model.phases.P1) {
|
|
30
|
+
lines.push(taskLine(task));
|
|
31
|
+
}
|
|
32
|
+
lines.push('');
|
|
33
|
+
lines.push('### Phase P2 (Optimization)');
|
|
34
|
+
for (const task of model.phases.P2) {
|
|
35
|
+
lines.push(taskLine(task));
|
|
36
|
+
}
|
|
37
|
+
lines.push('');
|
|
38
|
+
|
|
39
|
+
lines.push('## Release Milestones');
|
|
40
|
+
for (const milestone of model.milestones) {
|
|
41
|
+
const id = `milestone-${slugify(milestone.version)}`;
|
|
42
|
+
lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] ${milestone.version}: ${milestone.goal} <!-- rs:task=${id} -->`);
|
|
43
|
+
}
|
|
44
|
+
lines.push('');
|
|
45
|
+
|
|
46
|
+
lines.push('## Command/Module Breakdown');
|
|
47
|
+
if (model.commandBreakdown.length === 0) {
|
|
48
|
+
const id = 'identify-command-module-boundaries';
|
|
49
|
+
lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] Identify command/module boundaries for the next increment <!-- rs:task=${id} -->`);
|
|
50
|
+
} else {
|
|
51
|
+
for (const item of model.commandBreakdown) {
|
|
52
|
+
const id = `module-${slugify(item)}`;
|
|
53
|
+
lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] ${item} <!-- rs:task=${id} -->`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
lines.push('');
|
|
57
|
+
|
|
58
|
+
lines.push('## Exit Criteria Per Phase');
|
|
59
|
+
for (const item of model.exitCriteria) {
|
|
60
|
+
const id = `exit-${slugify(item)}`;
|
|
61
|
+
lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] ${item} <!-- rs:task=${id} -->`);
|
|
62
|
+
}
|
|
63
|
+
lines.push('');
|
|
64
|
+
|
|
65
|
+
for (const section of model.customSections) {
|
|
66
|
+
lines.push(`## ${section.title}`);
|
|
67
|
+
for (const line of section.items) {
|
|
68
|
+
lines.push(line);
|
|
69
|
+
}
|
|
70
|
+
lines.push('');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
lines.push('## Risks and Anti-goals');
|
|
74
|
+
lines.push('### Risks');
|
|
75
|
+
for (const risk of model.risks) {
|
|
76
|
+
const id = `risk-${slugify(risk)}`;
|
|
77
|
+
lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] ${risk} <!-- rs:task=${id} -->`);
|
|
78
|
+
}
|
|
79
|
+
lines.push('');
|
|
80
|
+
lines.push('### Anti-goals');
|
|
81
|
+
for (const antiGoal of model.antiGoals) {
|
|
82
|
+
const id = `anti-goal-${slugify(antiGoal)}`;
|
|
83
|
+
lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] ${antiGoal} <!-- rs:task=${id} -->`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return ensureTrailingNewline(lines.join('\n')).trimEnd();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = { renderCompact };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function taskLine(task) {
|
|
4
|
+
return `- [${task.checked ? 'x' : ' '}] ${task.text} <!-- rs:task=${task.id} -->`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function sectionHeader(n, title) {
|
|
8
|
+
return `## ${n}. ${title}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function checkedState(model, id) {
|
|
12
|
+
return Boolean(model.checkedById && model.checkedById[id]);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function priorityLabel(priority) {
|
|
16
|
+
return priority ? `\`[${priority}]\`` : '';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { taskLine, sectionHeader, checkedState, priorityLabel };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { renderCompact } = require('./compact');
|
|
4
|
+
const { renderProfessional } = require('./professional');
|
|
5
|
+
|
|
6
|
+
function renderBody(model, profile) {
|
|
7
|
+
if (profile === 'professional') return renderProfessional(model);
|
|
8
|
+
if (profile === 'enterprise') {
|
|
9
|
+
throw new Error(
|
|
10
|
+
'roadmapProfile "enterprise" is not yet implemented. ' +
|
|
11
|
+
'Use "professional" instead, or contribute via the plugin registry. ' +
|
|
12
|
+
'See docs/use-cases/ for the extension guide.'
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
return renderCompact(model);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { renderBody };
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { slugify, ensureTrailingNewline } = require('../utils');
|
|
4
|
+
const { sectionHeader, checkedState, priorityLabel } = require('./helpers');
|
|
5
|
+
|
|
6
|
+
function taskLineWithPriority(task, model) {
|
|
7
|
+
const pri = task.priority ? `${priorityLabel(task.priority)} ` : '';
|
|
8
|
+
const id = task.id || `prof-task-${slugify(task.text || String(task))}`;
|
|
9
|
+
const text = task.text || String(task);
|
|
10
|
+
const checked = task.checked || checkedState(model, id);
|
|
11
|
+
return `- [${checked ? 'x' : ' '}] ${pri}${text} <!-- rs:task=${id} -->`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function exitLine(item, phN, stN, model) {
|
|
15
|
+
const text = typeof item === 'string' ? item : item.text;
|
|
16
|
+
const pri = (typeof item === 'object' && item.priority) ? `${priorityLabel(item.priority)} ` : '';
|
|
17
|
+
const id = `prof-ph${phN}-st${stN}-exit-${slugify(text)}`;
|
|
18
|
+
const checked = checkedState(model, id);
|
|
19
|
+
return `- [${checked ? 'x' : ' '}] ${pri}${text} <!-- rs:task=${id} -->`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function renderSection1NorthStar(model, lines) {
|
|
23
|
+
lines.push(sectionHeader(1, 'Product North Star'));
|
|
24
|
+
lines.push('');
|
|
25
|
+
lines.push(model.product.northStar || model.northStar);
|
|
26
|
+
lines.push('');
|
|
27
|
+
if (model.product.primaryUser) {
|
|
28
|
+
lines.push(`**Primary user:** ${model.product.primaryUser}`);
|
|
29
|
+
lines.push('');
|
|
30
|
+
}
|
|
31
|
+
if (model.product.targetOutcome) {
|
|
32
|
+
lines.push(`**Target outcome:** ${model.product.targetOutcome}`);
|
|
33
|
+
lines.push('');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function renderSection2Positioning(model, lines) {
|
|
38
|
+
lines.push(sectionHeader(2, 'Positioning and Competitive Advantage'));
|
|
39
|
+
lines.push('');
|
|
40
|
+
if (model.product.positioning) {
|
|
41
|
+
lines.push(model.product.positioning);
|
|
42
|
+
} else {
|
|
43
|
+
lines.push('_No positioning statement configured. Add `product.positioning` to roadmap-skill.config.json._');
|
|
44
|
+
}
|
|
45
|
+
lines.push('');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function renderSection3CurrentState(model, lines) {
|
|
49
|
+
lines.push(sectionHeader(3, 'Explicit Current State'));
|
|
50
|
+
lines.push('');
|
|
51
|
+
|
|
52
|
+
lines.push('### Implemented');
|
|
53
|
+
lines.push('');
|
|
54
|
+
if (model.currentState.implemented && model.currentState.implemented.length > 0) {
|
|
55
|
+
for (const item of model.currentState.implemented) {
|
|
56
|
+
lines.push(`- [x] ${item} <!-- rs:task=prof-state-impl-${slugify(item)} -->`);
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
lines.push(`- Detected implementation surface: ${model.currentState.implementedSummary}`);
|
|
60
|
+
lines.push(`- Detected stacks: ${model.currentState.stackSummary}`);
|
|
61
|
+
}
|
|
62
|
+
lines.push('');
|
|
63
|
+
|
|
64
|
+
lines.push('### Scaffold / Partial');
|
|
65
|
+
lines.push('');
|
|
66
|
+
if (model.currentState.scaffold && model.currentState.scaffold.length > 0) {
|
|
67
|
+
for (const item of model.currentState.scaffold) {
|
|
68
|
+
const id = `prof-state-scaffold-${slugify(item)}`;
|
|
69
|
+
lines.push(`- [ ] ${item} <!-- rs:task=${id} -->`);
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
lines.push('_No scaffold modules detected. Improve detection by adding `product.steps` to config._');
|
|
73
|
+
}
|
|
74
|
+
lines.push('');
|
|
75
|
+
|
|
76
|
+
lines.push('### Known Limitations');
|
|
77
|
+
lines.push('');
|
|
78
|
+
if (model.currentState.knownLimitations && model.currentState.knownLimitations.length > 0) {
|
|
79
|
+
for (const item of model.currentState.knownLimitations) {
|
|
80
|
+
const id = `prof-state-limit-${slugify(item)}`;
|
|
81
|
+
lines.push(`- [ ] ${item} <!-- rs:task=${id} -->`);
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
lines.push(`- Code-level TODO/FIXME surface: ${model.currentState.todoSummary}`);
|
|
85
|
+
}
|
|
86
|
+
lines.push('');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function renderSection4PhasedExecution(model, lines) {
|
|
90
|
+
lines.push(sectionHeader(4, 'Phased Execution Roadmap'));
|
|
91
|
+
lines.push('');
|
|
92
|
+
|
|
93
|
+
const detailedPhases = [...(model.phasesDetailed || [])].sort((a, b) => a.phaseNumber - b.phaseNumber);
|
|
94
|
+
|
|
95
|
+
for (const phase of detailedPhases) {
|
|
96
|
+
lines.push(`### Phase ${phase.phaseNumber}: ${phase.title}`);
|
|
97
|
+
lines.push('');
|
|
98
|
+
lines.push(`**Phase Priority:** ${priorityLabel(phase.priority)}`);
|
|
99
|
+
lines.push('');
|
|
100
|
+
if (phase.objective) {
|
|
101
|
+
lines.push(`**Objective:** ${phase.objective}`);
|
|
102
|
+
lines.push('');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const steps = [...(phase.steps || [])].sort((a, b) => a.stepNumber - b.stepNumber);
|
|
106
|
+
for (const step of steps) {
|
|
107
|
+
const stepLabel = `${phase.phaseNumber}.${step.stepNumber}`;
|
|
108
|
+
lines.push(`#### Step ${stepLabel}: ${step.title}`);
|
|
109
|
+
lines.push('');
|
|
110
|
+
lines.push(`**Step Priority:** ${priorityLabel(step.priority)}`);
|
|
111
|
+
const deps = step.dependsOn && step.dependsOn.length > 0
|
|
112
|
+
? step.dependsOn.map((n) => `Phase ${n}`).join(', ')
|
|
113
|
+
: 'None';
|
|
114
|
+
lines.push(`**Depends on:** ${deps}`);
|
|
115
|
+
lines.push('');
|
|
116
|
+
if (step.objective) {
|
|
117
|
+
lines.push(`**Objective:** ${step.objective}`);
|
|
118
|
+
lines.push('');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (step.tasks && step.tasks.length > 0) {
|
|
122
|
+
lines.push('**Tasks:**');
|
|
123
|
+
lines.push('');
|
|
124
|
+
for (const task of step.tasks) {
|
|
125
|
+
lines.push(taskLineWithPriority(task, model));
|
|
126
|
+
}
|
|
127
|
+
lines.push('');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (step.exitCriteria && step.exitCriteria.length > 0) {
|
|
131
|
+
lines.push('**Exit Criteria:**');
|
|
132
|
+
lines.push('');
|
|
133
|
+
for (const item of step.exitCriteria) {
|
|
134
|
+
lines.push(exitLine(item, phase.phaseNumber, step.stepNumber, model));
|
|
135
|
+
}
|
|
136
|
+
lines.push('');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (step.risks && step.risks.length > 0) {
|
|
140
|
+
lines.push(`**Notable Risks:** ${step.risks.join('; ')}`);
|
|
141
|
+
lines.push('');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function renderSection5Milestones(model, lines) {
|
|
148
|
+
lines.push(sectionHeader(5, 'Versioned Milestones'));
|
|
149
|
+
lines.push('');
|
|
150
|
+
|
|
151
|
+
for (const milestone of model.milestones) {
|
|
152
|
+
const msSlug = slugify(milestone.version);
|
|
153
|
+
lines.push(`### ${milestone.version}`);
|
|
154
|
+
lines.push('');
|
|
155
|
+
lines.push(`**Goal:** ${milestone.goal}`);
|
|
156
|
+
lines.push('');
|
|
157
|
+
|
|
158
|
+
if (milestone.mustExist && milestone.mustExist.length > 0) {
|
|
159
|
+
lines.push('**What Must Exist:**');
|
|
160
|
+
lines.push('');
|
|
161
|
+
for (const item of milestone.mustExist) {
|
|
162
|
+
const id = `prof-ms-${msSlug}-exist-${slugify(item)}`;
|
|
163
|
+
lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] \`[P0]\` ${item} <!-- rs:task=${id} -->`);
|
|
164
|
+
}
|
|
165
|
+
lines.push('');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (milestone.mustBeStable && milestone.mustBeStable.length > 0) {
|
|
169
|
+
lines.push('**What Must Be Stable:**');
|
|
170
|
+
lines.push('');
|
|
171
|
+
for (const item of milestone.mustBeStable) {
|
|
172
|
+
const id = `prof-ms-${msSlug}-stable-${slugify(item)}`;
|
|
173
|
+
lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] \`[P1]\` ${item} <!-- rs:task=${id} -->`);
|
|
174
|
+
}
|
|
175
|
+
lines.push('');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (milestone.outOfScope && milestone.outOfScope.length > 0) {
|
|
179
|
+
lines.push('**Intentionally Out of Scope:**');
|
|
180
|
+
lines.push('');
|
|
181
|
+
for (const item of milestone.outOfScope) {
|
|
182
|
+
lines.push(`- ${item}`);
|
|
183
|
+
}
|
|
184
|
+
lines.push('');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!milestone.mustExist && !milestone.mustBeStable && !milestone.outOfScope) {
|
|
188
|
+
const id = `prof-ms-${msSlug}`;
|
|
189
|
+
lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] \`[P0]\` ${milestone.version}: ${milestone.goal} <!-- rs:task=${id} -->`);
|
|
190
|
+
lines.push('');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const MODULE_METADATA = {
|
|
196
|
+
generator: {
|
|
197
|
+
state: 'Compact and professional profiles supported; Phase→Step→Task model implemented.',
|
|
198
|
+
tasks: [
|
|
199
|
+
{ text: 'Improve Phase→Step→Task model inference quality', priority: 'P0', id: 'prof-mat-generator-improve-phase-step-task-inference' },
|
|
200
|
+
{ text: 'Add scan-driven task suggestions per detected module', priority: 'P1', id: 'prof-mat-generator-scan-driven-task-suggestions' }
|
|
201
|
+
]
|
|
202
|
+
},
|
|
203
|
+
parser: {
|
|
204
|
+
state: 'Parses managed blocks, rs:task IDs, and checked state.',
|
|
205
|
+
tasks: [
|
|
206
|
+
{ text: 'Add parser validation for Phase→Step hierarchy markers', priority: 'P1', id: 'prof-mat-parser-phase-hierarchy-validation' },
|
|
207
|
+
{ text: 'Improve section boundary detection for professional format', priority: 'P1', id: 'prof-mat-parser-professional-section-detection' }
|
|
208
|
+
]
|
|
209
|
+
},
|
|
210
|
+
renderer: {
|
|
211
|
+
state: 'Dispatcher supports compact, professional, and enterprise (error) profiles.',
|
|
212
|
+
tasks: [
|
|
213
|
+
{ text: 'Add snapshot regression fixtures for compact and professional', priority: 'P0', id: 'prof-mat-renderer-snapshot-regression-fixtures' },
|
|
214
|
+
{ text: 'Harden priority label rendering for edge cases', priority: 'P1', id: 'prof-mat-renderer-priority-label-edge-cases' }
|
|
215
|
+
]
|
|
216
|
+
},
|
|
217
|
+
validator: {
|
|
218
|
+
state: 'Evidence-based validation against file, symbol, and test presence.',
|
|
219
|
+
tasks: [
|
|
220
|
+
{ text: 'Extend validator to verify Phase→Step→Task IDs survive sync', priority: 'P1', id: 'prof-mat-validator-phase-step-task-id-sync' },
|
|
221
|
+
{ text: 'Add validation coverage for professional profile task IDs', priority: 'P1', id: 'prof-mat-validator-professional-task-id-coverage' }
|
|
222
|
+
]
|
|
223
|
+
},
|
|
224
|
+
match: {
|
|
225
|
+
state: 'Task similarity matching with edit-distance threshold.',
|
|
226
|
+
tasks: [
|
|
227
|
+
{ text: 'Tune similarity threshold to reduce false-positive merges', priority: 'P0', id: 'prof-mat-match-tune-similarity-threshold' }
|
|
228
|
+
]
|
|
229
|
+
},
|
|
230
|
+
config: {
|
|
231
|
+
state: 'Supports roadmapProfile, product block, milestones, phaseTemplates, plugins.',
|
|
232
|
+
tasks: [
|
|
233
|
+
{ text: 'Add JSON schema validation for roadmap-skill.config.json', priority: 'P1', id: 'prof-mat-config-json-schema-validation' }
|
|
234
|
+
]
|
|
235
|
+
},
|
|
236
|
+
io: {
|
|
237
|
+
state: 'Scans files, detects languages, test frameworks, commands, modules.',
|
|
238
|
+
tasks: [
|
|
239
|
+
{ text: 'Improve module detection for monorepo workspace layouts', priority: 'P2', id: 'prof-mat-io-monorepo-workspace-detection' }
|
|
240
|
+
]
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
function renderSection6MaturityPath(model, lines) {
|
|
245
|
+
lines.push(sectionHeader(6, 'Command-by-Command / Module-by-Module Maturity Path'));
|
|
246
|
+
lines.push('');
|
|
247
|
+
|
|
248
|
+
const allAreas = [...model.commandBreakdown];
|
|
249
|
+
|
|
250
|
+
if (allAreas.length === 0) {
|
|
251
|
+
const id = 'prof-mat-identify-boundaries';
|
|
252
|
+
lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] \`[P1]\` Identify command/module boundaries for the next increment <!-- rs:task=${id} -->`);
|
|
253
|
+
lines.push('');
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
for (const area of allAreas) {
|
|
258
|
+
const rawName = area.replace(/^(Module:|Command:)\s*/i, '').trim();
|
|
259
|
+
const meta = MODULE_METADATA[rawName.toLowerCase()];
|
|
260
|
+
const displayName = rawName;
|
|
261
|
+
|
|
262
|
+
lines.push(`### ${displayName}`);
|
|
263
|
+
lines.push('');
|
|
264
|
+
if (meta) {
|
|
265
|
+
lines.push(`**Current state:** ${meta.state}`);
|
|
266
|
+
lines.push('');
|
|
267
|
+
for (const task of meta.tasks) {
|
|
268
|
+
lines.push(`- [${checkedState(model, task.id) ? 'x' : ' '}] ${priorityLabel(task.priority)} ${task.text} <!-- rs:task=${task.id} -->`);
|
|
269
|
+
}
|
|
270
|
+
} else {
|
|
271
|
+
const isCommand = /^Command:/i.test(area);
|
|
272
|
+
const kind = isCommand ? 'command' : 'module';
|
|
273
|
+
const nextId = `prof-mat-${slugify(rawName)}-define-maturity-criteria`;
|
|
274
|
+
lines.push(`**Current state:** ${kind} detected in scan.`);
|
|
275
|
+
lines.push('');
|
|
276
|
+
lines.push(`- [${checkedState(model, nextId) ? 'x' : ' '}] \`[P1]\` Define maturity criteria and testability gates for ${displayName} <!-- rs:task=${nextId} -->`);
|
|
277
|
+
}
|
|
278
|
+
lines.push('');
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function renderSection7OutputContract(model, lines) {
|
|
283
|
+
lines.push(sectionHeader(7, 'Output Contract Roadmap'));
|
|
284
|
+
lines.push('');
|
|
285
|
+
|
|
286
|
+
lines.push('### Output Format');
|
|
287
|
+
lines.push('');
|
|
288
|
+
const formatItems = [
|
|
289
|
+
{ text: 'Define stable public output format (stdout, files, exit codes)', priority: 'P0' },
|
|
290
|
+
{ text: 'Version output format alongside package version', priority: 'P1' }
|
|
291
|
+
];
|
|
292
|
+
for (const item of formatItems) {
|
|
293
|
+
const id = `prof-out-${slugify(item.text)}`;
|
|
294
|
+
lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] ${priorityLabel(item.priority)} ${item.text} <!-- rs:task=${id} -->`);
|
|
295
|
+
}
|
|
296
|
+
lines.push('');
|
|
297
|
+
|
|
298
|
+
lines.push('### Breaking Changes');
|
|
299
|
+
lines.push('');
|
|
300
|
+
const breakingItems = [
|
|
301
|
+
{ text: 'Document breaking vs. non-breaking output changes', priority: 'P1' },
|
|
302
|
+
{ text: 'Add output schema validation to CI', priority: 'P1' }
|
|
303
|
+
];
|
|
304
|
+
for (const item of breakingItems) {
|
|
305
|
+
const id = `prof-out-${slugify(item.text)}`;
|
|
306
|
+
lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] ${priorityLabel(item.priority)} ${item.text} <!-- rs:task=${id} -->`);
|
|
307
|
+
}
|
|
308
|
+
lines.push('');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function renderSection8Testing(model, lines) {
|
|
312
|
+
lines.push(sectionHeader(8, 'Testing and Quality-Gate Roadmap'));
|
|
313
|
+
lines.push('');
|
|
314
|
+
|
|
315
|
+
lines.push('### Test Coverage');
|
|
316
|
+
lines.push('');
|
|
317
|
+
const coverageItems = [
|
|
318
|
+
{ text: 'Unit test coverage for all core modules', priority: 'P0' },
|
|
319
|
+
{ text: 'Integration tests covering the full generate → sync → validate pipeline', priority: 'P0' },
|
|
320
|
+
{ text: 'Regression fixtures for compact and professional profile output', priority: 'P1' },
|
|
321
|
+
{ text: 'Edge case coverage: empty repo, no config, large monorepo scan', priority: 'P1' }
|
|
322
|
+
];
|
|
323
|
+
for (const item of coverageItems) {
|
|
324
|
+
const id = `prof-test-${slugify(item.text)}`;
|
|
325
|
+
lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] ${priorityLabel(item.priority)} ${item.text} <!-- rs:task=${id} -->`);
|
|
326
|
+
}
|
|
327
|
+
lines.push('');
|
|
328
|
+
|
|
329
|
+
lines.push('### Quality Gates');
|
|
330
|
+
lines.push('');
|
|
331
|
+
const gateItems = [
|
|
332
|
+
{ text: 'CI quality gate: tests must pass before merge', priority: 'P0' },
|
|
333
|
+
{ text: 'Block merge when generated roadmap loses checked state', priority: 'P0' },
|
|
334
|
+
{ text: 'Add professional renderer snapshot tests', priority: 'P1' }
|
|
335
|
+
];
|
|
336
|
+
for (const item of gateItems) {
|
|
337
|
+
const id = `prof-test-${slugify(item.text)}`;
|
|
338
|
+
lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] ${priorityLabel(item.priority)} ${item.text} <!-- rs:task=${id} -->`);
|
|
339
|
+
}
|
|
340
|
+
lines.push('');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function renderSection9Distribution(model, lines) {
|
|
344
|
+
lines.push(sectionHeader(9, 'Distribution Roadmap'));
|
|
345
|
+
lines.push('');
|
|
346
|
+
|
|
347
|
+
lines.push('### npm Registry');
|
|
348
|
+
lines.push('');
|
|
349
|
+
const npmItems = [
|
|
350
|
+
{ text: 'Publish to npm registry with stable semver', priority: 'P0' },
|
|
351
|
+
{ text: 'Ensure CLI binary is correctly linked in package.json `bin`', priority: 'P0' }
|
|
352
|
+
];
|
|
353
|
+
for (const item of npmItems) {
|
|
354
|
+
const id = `prof-dist-${slugify(item.text)}`;
|
|
355
|
+
lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] ${priorityLabel(item.priority)} ${item.text} <!-- rs:task=${id} -->`);
|
|
356
|
+
}
|
|
357
|
+
lines.push('');
|
|
358
|
+
|
|
359
|
+
lines.push('### Release Process');
|
|
360
|
+
lines.push('');
|
|
361
|
+
const releaseItems = [
|
|
362
|
+
{ text: 'Tag git releases aligned with npm publish', priority: 'P1' },
|
|
363
|
+
{ text: 'Document install instructions for npm global and npx usage', priority: 'P1' }
|
|
364
|
+
];
|
|
365
|
+
for (const item of releaseItems) {
|
|
366
|
+
const id = `prof-dist-${slugify(item.text)}`;
|
|
367
|
+
lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] ${priorityLabel(item.priority)} ${item.text} <!-- rs:task=${id} -->`);
|
|
368
|
+
}
|
|
369
|
+
lines.push('');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function renderSection10Documentation(model, lines) {
|
|
373
|
+
lines.push(sectionHeader(10, 'Documentation Roadmap'));
|
|
374
|
+
lines.push('');
|
|
375
|
+
|
|
376
|
+
lines.push('### Core Docs');
|
|
377
|
+
lines.push('');
|
|
378
|
+
const coreItems = [
|
|
379
|
+
{ text: 'README.md covers install, commands, and profile selection', priority: 'P0' },
|
|
380
|
+
{ text: 'SKILL.md reflects current feature set and guardrails', priority: 'P0' },
|
|
381
|
+
{ text: 'CHANGELOG.md maintained for each release', priority: 'P1' }
|
|
382
|
+
];
|
|
383
|
+
for (const item of coreItems) {
|
|
384
|
+
const id = `prof-doc-${slugify(item.text)}`;
|
|
385
|
+
lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] ${priorityLabel(item.priority)} ${item.text} <!-- rs:task=${id} -->`);
|
|
386
|
+
}
|
|
387
|
+
lines.push('');
|
|
388
|
+
|
|
389
|
+
lines.push('### Showcase');
|
|
390
|
+
lines.push('');
|
|
391
|
+
const showcaseItems = [
|
|
392
|
+
{ text: 'docs/ use-cases cover compact and professional profiles', priority: 'P1' },
|
|
393
|
+
{ text: 'Generated ROADMAP.md showcases professional Phase→Step→Task output', priority: 'P1' }
|
|
394
|
+
];
|
|
395
|
+
for (const item of showcaseItems) {
|
|
396
|
+
const id = `prof-doc-${slugify(item.text)}`;
|
|
397
|
+
lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] ${priorityLabel(item.priority)} ${item.text} <!-- rs:task=${id} -->`);
|
|
398
|
+
}
|
|
399
|
+
lines.push('');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function renderSection11Risks(model, lines) {
|
|
403
|
+
lines.push(sectionHeader(11, 'Risks, Constraints, and Anti-Goals'));
|
|
404
|
+
lines.push('');
|
|
405
|
+
|
|
406
|
+
lines.push('### Risks');
|
|
407
|
+
lines.push('');
|
|
408
|
+
for (let i = 0; i < model.risks.length; i += 1) {
|
|
409
|
+
const risk = model.risks[i];
|
|
410
|
+
const pri = i === 0 ? 'P0' : 'P1';
|
|
411
|
+
const id = `prof-risk-${slugify(risk)}`;
|
|
412
|
+
lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] ${priorityLabel(pri)} ${risk} <!-- rs:task=${id} -->`);
|
|
413
|
+
}
|
|
414
|
+
lines.push('');
|
|
415
|
+
|
|
416
|
+
lines.push('### Anti-Goals');
|
|
417
|
+
lines.push('');
|
|
418
|
+
for (const antiGoal of model.antiGoals) {
|
|
419
|
+
lines.push(`- ${antiGoal}`);
|
|
420
|
+
}
|
|
421
|
+
lines.push('');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function renderSection12SuccessCriteria(model, lines) {
|
|
425
|
+
lines.push(sectionHeader(12, '1.0 Measurable Success Criteria'));
|
|
426
|
+
lines.push('');
|
|
427
|
+
|
|
428
|
+
const criteria = model.successCriteria && model.successCriteria.length > 0
|
|
429
|
+
? model.successCriteria
|
|
430
|
+
: [
|
|
431
|
+
'All roadmap sections render without errors for compact and professional profiles',
|
|
432
|
+
'Checked task state is preserved across regeneration',
|
|
433
|
+
'npm test passes with no failures',
|
|
434
|
+
'ROADMAP.md is generated by RoadmapSmith itself'
|
|
435
|
+
];
|
|
436
|
+
|
|
437
|
+
for (const criterion of criteria) {
|
|
438
|
+
const id = `prof-sc-${slugify(criterion)}`;
|
|
439
|
+
lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] \`[P0]\` ${criterion} <!-- rs:task=${id} -->`);
|
|
440
|
+
}
|
|
441
|
+
lines.push('');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function renderProfessional(model) {
|
|
445
|
+
const projectName = (model.product && model.product.name) || 'Project';
|
|
446
|
+
const lines = [];
|
|
447
|
+
|
|
448
|
+
lines.push(`# ${projectName} Roadmap`);
|
|
449
|
+
lines.push('');
|
|
450
|
+
|
|
451
|
+
renderSection1NorthStar(model, lines);
|
|
452
|
+
renderSection2Positioning(model, lines);
|
|
453
|
+
renderSection3CurrentState(model, lines);
|
|
454
|
+
renderSection4PhasedExecution(model, lines);
|
|
455
|
+
renderSection5Milestones(model, lines);
|
|
456
|
+
renderSection6MaturityPath(model, lines);
|
|
457
|
+
renderSection7OutputContract(model, lines);
|
|
458
|
+
renderSection8Testing(model, lines);
|
|
459
|
+
renderSection9Distribution(model, lines);
|
|
460
|
+
renderSection10Documentation(model, lines);
|
|
461
|
+
renderSection11Risks(model, lines);
|
|
462
|
+
renderSection12SuccessCriteria(model, lines);
|
|
463
|
+
|
|
464
|
+
return ensureTrailingNewline(lines.join('\n')).trimEnd();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
module.exports = { renderProfessional };
|