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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roadmapsmith",
3
- "version": "0.1.1",
3
+ "version": "0.4.0",
4
4
  "description": "Generate, sync, and validate deterministic project roadmaps for agent-driven execution.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
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
+ };
@@ -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: 'Ship validated, high-impact increments with deterministic delivery and transparent completion evidence.',
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
- 'Roadmap drift if checklist state diverges from repository evidence',
363
- 'Silent regressions when tasks are marked complete without tests',
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 managedBody = renderManagedBody(model);
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
- customSections: input.customSections || []
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 };