roadmapsmith 0.2.0 → 0.5.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.
@@ -2,28 +2,60 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
- const { walkFiles, detectLanguages, detectTestFrameworks } = require('../io');
5
+ const { walkFiles, detectLanguages, detectTestFrameworks, detectWorkspaces } = require('../io');
6
6
  const { createRoadmapModel, PHASE_ORDER } = require('../model');
7
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');
12
+
13
+ const IMPL_PATTERN_RE = /[/|]TODO|TODO[|/]|[/|]FIXME|FIXME[|/]/;
14
+ const COMMENT_TODO_RE = /(?:\/\/|#|\*\s*).*\b(?:TODO|FIXME)\b/;
15
+
16
+ function isTodoMarker(line) {
17
+ return COMMENT_TODO_RE.test(line) && !IMPL_PATTERN_RE.test(line);
18
+ }
19
+
20
+ const GENERIC_MODULE_NAMES = new Set(['index', 'main', 'utils', 'common', 'helpers', 'types', 'constants', 'model']);
11
21
 
12
22
  function detectModules(files) {
13
23
  const modules = new Set();
14
- const roots = ['src/', 'apps/', 'packages/', 'lib/', 'cmd/', 'internal/'];
24
+ const rootPrefixes = ['src/', 'apps/', 'packages/', 'lib/', 'cmd/', 'internal/'];
15
25
 
16
26
  for (const file of files) {
17
- const root = roots.find((candidate) => file.startsWith(candidate));
18
- if (!root) {
19
- continue;
27
+ let relative;
28
+
29
+ const directRoot = rootPrefixes.find((r) => file.startsWith(r));
30
+ if (directRoot) {
31
+ relative = file.slice(directRoot.length);
32
+ } else {
33
+ let found = false;
34
+ for (const r of rootPrefixes) {
35
+ const idx = file.indexOf('/' + r);
36
+ if (idx !== -1) {
37
+ // Only accept nested prefix when it appears within the first two path segments
38
+ // (e.g. "wrapper/src/..." is fine; "a/b/c/src/..." is too deep and likely a fixture or dependency)
39
+ if (file.slice(0, idx).split('/').length > 2) continue;
40
+ relative = file.slice(idx + 1 + r.length);
41
+ found = true;
42
+ break;
43
+ }
44
+ }
45
+ if (!found) continue;
20
46
  }
21
- const relative = file.slice(root.length);
47
+
22
48
  const first = relative.split('/')[0];
23
- if (!first || first.includes('.')) {
24
- continue;
49
+ if (!first) continue;
50
+
51
+ if (first.includes('.')) {
52
+ const name = first.slice(0, first.lastIndexOf('.'));
53
+ if (name && !GENERIC_MODULE_NAMES.has(name)) {
54
+ modules.add(name);
55
+ }
56
+ } else {
57
+ modules.add(first);
25
58
  }
26
- modules.add(first);
27
59
  }
28
60
 
29
61
  return Array.from(modules).sort((left, right) => left.localeCompare(right));
@@ -57,7 +89,7 @@ function collectTodoHints(projectRoot, files) {
57
89
 
58
90
  const lines = content.split(/\r?\n/);
59
91
  for (let i = 0; i < lines.length; i += 1) {
60
- if (/TODO|FIXME/i.test(lines[i])) {
92
+ if (isTodoMarker(lines[i])) {
61
93
  hints.push({
62
94
  file,
63
95
  line: i + 1,
@@ -73,6 +105,37 @@ function collectTodoHints(projectRoot, files) {
73
105
  return hints;
74
106
  }
75
107
 
108
+ function collectCodeTodoHints(projectRoot, files) {
109
+ const hints = [];
110
+ const codeFiles = files.filter((file) => /\.(js|ts|tsx|py|go|rs)$/.test(file)).slice(0, 120);
111
+
112
+ for (const file of codeFiles) {
113
+ const absolutePath = path.resolve(projectRoot, file);
114
+ let content = '';
115
+ try {
116
+ content = fs.readFileSync(absolutePath, 'utf8');
117
+ } catch {
118
+ continue;
119
+ }
120
+
121
+ const lines = content.split(/\r?\n/);
122
+ for (let i = 0; i < lines.length; i += 1) {
123
+ if (isTodoMarker(lines[i])) {
124
+ hints.push({
125
+ file,
126
+ line: i + 1,
127
+ text: lines[i].trim()
128
+ });
129
+ }
130
+ if (hints.length >= 6) {
131
+ return hints;
132
+ }
133
+ }
134
+ }
135
+
136
+ return hints;
137
+ }
138
+
76
139
  function scanProject(projectRoot) {
77
140
  const files = walkFiles(projectRoot);
78
141
  const languages = detectLanguages(files);
@@ -80,17 +143,22 @@ function scanProject(projectRoot) {
80
143
  const modules = detectModules(files);
81
144
  const commands = detectCommands(files);
82
145
  const todos = collectTodoHints(projectRoot, files);
146
+ const codeTodos = collectCodeTodoHints(projectRoot, files);
147
+ const workspaces = detectWorkspaces(projectRoot, files);
83
148
 
84
149
  const implementedFiles = files.filter((file) => /\.(js|ts|tsx|py|go|rs|java|kt|cs)$/.test(file));
85
150
  const testFiles = files.filter((file) => /(^|\/)(__tests__|tests)\//.test(file) || /\.test\.|\.spec\.|_test\.go$/.test(file));
86
151
 
87
152
  return {
153
+ projectRoot,
88
154
  files,
89
155
  languages,
90
156
  testFrameworks,
91
157
  modules,
92
158
  commands,
93
159
  todos,
160
+ codeTodos,
161
+ workspaces,
94
162
  implementedCount: implementedFiles.length,
95
163
  testCount: testFiles.length
96
164
  };
@@ -237,101 +305,132 @@ function groupByPhase(tasks) {
237
305
  return groups;
238
306
  }
239
307
 
240
- function taskLine(task) {
241
- return `- [${task.checked ? 'x' : ' '}] ${task.text} <!-- rs:task=${task.id} -->`;
242
- }
243
-
244
- function checkedState(model, id) {
245
- return Boolean(model.checkedById && model.checkedById[id]);
246
- }
247
-
248
- function renderManagedBody(model) {
249
- const lines = [];
250
-
251
- lines.push('# Project Roadmap');
252
- lines.push('');
253
- lines.push('## Product North Star');
254
- lines.push(model.northStar);
255
- lines.push('');
256
-
257
- lines.push('## Current State');
258
- lines.push(`- Implemented surface: ${model.currentState.implementedSummary}`);
259
- lines.push(`- TODO surface: ${model.currentState.todoSummary}`);
260
- lines.push(`- Detected stacks: ${model.currentState.stackSummary}`);
261
- lines.push('');
262
-
263
- lines.push('## Phased Roadmap');
264
- lines.push('');
265
- lines.push('### Phase P0 (Critical)');
266
- for (const task of model.phases.P0) {
267
- lines.push(taskLine(task));
268
- }
269
- lines.push('');
270
- lines.push('### Phase P1 (Important)');
271
- for (const task of model.phases.P1) {
272
- lines.push(taskLine(task));
308
+ function inferProjectName(projectRoot) {
309
+ const pkgPath = path.join(projectRoot, 'package.json');
310
+ try {
311
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
312
+ if (pkg.name) return pkg.name;
313
+ } catch {
314
+ // ignore — try other manifests
273
315
  }
274
- lines.push('');
275
- lines.push('### Phase P2 (Optimization)');
276
- for (const task of model.phases.P2) {
277
- lines.push(taskLine(task));
278
- }
279
- lines.push('');
316
+ return path.basename(projectRoot);
317
+ }
280
318
 
281
- lines.push('## Release Milestones');
282
- for (const milestone of model.milestones) {
283
- const id = `milestone-${slugify(milestone.version)}`;
284
- lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] ${milestone.version}: ${milestone.goal} <!-- rs:task=${id} -->`);
285
- }
286
- lines.push('');
287
-
288
- lines.push('## Command/Module Breakdown');
289
- if (model.commandBreakdown.length === 0) {
290
- const id = 'identify-command-module-boundaries';
291
- lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] Identify command/module boundaries for the next increment <!-- rs:task=${id} -->`);
292
- } else {
293
- for (const item of model.commandBreakdown) {
294
- const id = `module-${slugify(item)}`;
295
- lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] ${item} <!-- rs:task=${id} -->`);
319
+ function buildPhasesDetailed(phases, config) {
320
+ const configPhases = config.product && Array.isArray(config.product.phases)
321
+ ? config.product.phases : [];
322
+ if (configPhases.length > 0) return configPhases;
323
+
324
+ return [
325
+ {
326
+ phaseNumber: 1,
327
+ title: 'Foundation Baseline',
328
+ priority: 'P0',
329
+ objective: 'Establish a stable, testable baseline that unblocks all downstream delivery.',
330
+ steps: [{
331
+ stepNumber: 1,
332
+ title: 'Core Implementation',
333
+ priority: 'P0',
334
+ dependsOn: [],
335
+ objective: 'Close critical path items.',
336
+ tasks: phases.P0,
337
+ exitCriteria: [
338
+ { text: 'All P0 tasks validated by evidence', priority: 'P0' },
339
+ { text: 'CI is green on main', priority: 'P0' }
340
+ ],
341
+ risks: []
342
+ }]
343
+ },
344
+ {
345
+ phaseNumber: 2,
346
+ title: 'Feature Completeness',
347
+ priority: 'P1',
348
+ objective: 'Expand functionality and reduce operational risk.',
349
+ steps: [{
350
+ stepNumber: 1,
351
+ title: 'Feature Delivery',
352
+ priority: 'P1',
353
+ dependsOn: [1],
354
+ objective: 'Deliver planned P1 features.',
355
+ tasks: phases.P1,
356
+ exitCriteria: [
357
+ { text: 'All P1 tasks validated by evidence', priority: 'P1' },
358
+ { text: 'No regressions on Phase 1 functionality', priority: 'P0' }
359
+ ],
360
+ risks: []
361
+ }]
362
+ },
363
+ {
364
+ phaseNumber: 3,
365
+ title: 'Release Hardening',
366
+ priority: 'P2',
367
+ objective: 'Complete hardening and production readiness for v1.0.',
368
+ steps: [{
369
+ stepNumber: 1,
370
+ title: 'Hardening',
371
+ priority: 'P2',
372
+ dependsOn: [2],
373
+ objective: 'Close P2 items and harden release.',
374
+ tasks: phases.P2,
375
+ exitCriteria: [
376
+ { text: 'All P2 tasks validated by evidence', priority: 'P2' },
377
+ { text: 'Release candidate checklist complete', priority: 'P0' }
378
+ ],
379
+ risks: []
380
+ }]
296
381
  }
297
- }
298
- lines.push('');
299
-
300
- lines.push('## Exit Criteria Per Phase');
301
- for (const item of model.exitCriteria) {
302
- const id = `exit-${slugify(item)}`;
303
- lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] ${item} <!-- rs:task=${id} -->`);
304
- }
305
- lines.push('');
382
+ ];
383
+ }
306
384
 
307
- for (const section of model.customSections) {
308
- lines.push(`## ${section.title}`);
309
- for (const line of section.items) {
310
- lines.push(line);
311
- }
312
- lines.push('');
313
- }
385
+ function buildSteps(phases, config) {
386
+ const configSteps = config.product && Array.isArray(config.product.steps) ? config.product.steps : [];
387
+ if (configSteps.length > 0) return configSteps;
388
+
389
+ const stepDefs = [
390
+ { stepNumber: 1, title: 'Foundation Baseline', priority: 'P0', dependsOn: [], phaseKey: 'P0',
391
+ objective: 'Establish a stable, testable baseline that unblocks all downstream delivery.' },
392
+ { stepNumber: 2, title: 'Feature Completeness', priority: 'P1', dependsOn: [1], phaseKey: 'P1',
393
+ objective: 'Expand functionality, improve reliability, and reduce operational risk.' },
394
+ { stepNumber: 3, title: 'Release Hardening', priority: 'P2', dependsOn: [2], phaseKey: 'P2',
395
+ objective: 'Complete hardening, final validation, and production readiness for v1.0.' }
396
+ ];
314
397
 
315
- lines.push('## Risks and Anti-goals');
316
- lines.push('### Risks');
317
- for (const risk of model.risks) {
318
- const id = `risk-${slugify(risk)}`;
319
- lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] ${risk} <!-- rs:task=${id} -->`);
320
- }
321
- lines.push('');
322
- lines.push('### Anti-goals');
323
- for (const antiGoal of model.antiGoals) {
324
- const id = `anti-goal-${slugify(antiGoal)}`;
325
- lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] ${antiGoal} <!-- rs:task=${id} -->`);
326
- }
398
+ const defaultExitCriteria = {
399
+ 1: ['All P0 tasks validated by evidence', 'CI is green on main'],
400
+ 2: ['All P1 tasks validated by evidence', 'No regressions on P0 functionality'],
401
+ 3: ['All P2 tasks validated by evidence', 'Release candidate checklist complete']
402
+ };
327
403
 
328
- return ensureTrailingNewline(lines.join('\n')).trimEnd();
404
+ return stepDefs.map((def) => ({
405
+ stepNumber: def.stepNumber,
406
+ title: def.title,
407
+ priority: def.priority,
408
+ dependsOn: def.dependsOn,
409
+ objective: def.objective,
410
+ deliverables: phases[def.phaseKey] || [],
411
+ exitCriteria: defaultExitCriteria[def.stepNumber] || [],
412
+ risks: []
413
+ }));
329
414
  }
330
415
 
331
416
  function createModel(scan, tasks, config, customSections, checkedById) {
332
417
  const phases = groupByPhase(tasks);
333
418
 
419
+ const implemented = [
420
+ `${scan.implementedCount} implementation files across ${scan.languages.join(', ') || 'detected stack'}`
421
+ ];
422
+
423
+ const scaffold = scan.modules.length > 0
424
+ ? scan.modules.slice(0, 6).map((m) => `Module "${m}" partially implemented — coverage unknown`)
425
+ : [];
426
+
427
+ const knownLimitations = (scan.codeTodos || []).slice(0, 6).map((t) => `${t.file}:${t.line} — ${t.text.slice(0, 80)}`);
428
+
334
429
  const currentState = {
430
+ implemented,
431
+ scaffold,
432
+ knownLimitations,
433
+ workspaces: scan.workspaces || [],
335
434
  implementedSummary: `${scan.implementedCount} implementation files detected`,
336
435
  todoSummary: `${scan.todos.length} TODO/FIXME markers detected`,
337
436
  stackSummary: scan.languages.length > 0 ? scan.languages.join(', ') : 'No language-specific stack detected'
@@ -351,23 +450,50 @@ function createModel(scan, tasks, config, customSections, checkedById) {
351
450
  commandBreakdown.push(`Command: ${command}`);
352
451
  }
353
452
 
453
+ const productConfig = config.product || {};
454
+ const inferredName = inferProjectName(scan.projectRoot || process.cwd());
455
+ const product = {
456
+ name: productConfig.name || inferredName,
457
+ northStar: productConfig.northStar || '',
458
+ positioning: productConfig.positioning || '',
459
+ primaryUser: productConfig.primaryUser || '',
460
+ targetOutcome: productConfig.targetOutcome || ''
461
+ };
462
+
463
+ const defaultRisks = [
464
+ 'Roadmap drift if checklist state diverges from repository evidence',
465
+ 'Silent regressions when tasks are marked complete without tests',
466
+ 'Scope creep that delays the v1.0 milestone path'
467
+ ];
468
+ const defaultAntiGoals = [
469
+ 'Do not mark tasks complete without repository evidence',
470
+ 'Do not introduce non-deterministic roadmap formatting',
471
+ 'Do not hide validation failures from roadmap consumers'
472
+ ];
473
+
474
+ const risks = (productConfig.risks && productConfig.risks.length > 0) ? productConfig.risks : defaultRisks;
475
+ const antiGoals = (productConfig.antiGoals && productConfig.antiGoals.length > 0) ? productConfig.antiGoals : defaultAntiGoals;
476
+ const successCriteria = productConfig.successCriteria || [];
477
+
478
+ const northStar = productConfig.northStar
479
+ || 'Ship validated, high-impact increments with deterministic delivery and transparent completion evidence.';
480
+
481
+ const steps = buildSteps(phases, config);
482
+ const phasesDetailed = buildPhasesDetailed(phases, config);
483
+
354
484
  return createRoadmapModel({
355
- northStar: 'Ship validated, high-impact increments with deterministic delivery and transparent completion evidence.',
485
+ northStar,
486
+ product,
356
487
  currentState,
357
488
  phases,
489
+ steps,
490
+ phasesDetailed,
358
491
  milestones: config.milestones,
359
492
  commandBreakdown,
360
493
  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
- ],
494
+ risks,
495
+ antiGoals,
496
+ successCriteria,
371
497
  customSections,
372
498
  checkedById
373
499
  });
@@ -424,13 +550,13 @@ function generateRoadmapDocument(options) {
424
550
  const matcherCandidates = applyTaskMatchers(scan, config);
425
551
  const merged = mergeWithExisting([...baseCandidates, ...matcherCandidates, ...pluginTaskCandidates], existingPhaseTasks);
426
552
  const model = createModel(scan, merged, config, [...configSections, ...pluginSections], existingCheckedById);
427
- const managedBody = renderManagedBody(model);
553
+ const profile = config.roadmapProfile || 'compact';
554
+ const managedBody = renderBody(model, profile);
428
555
 
429
556
  return upsertManagedBlock(existingContent, managedBody);
430
557
  }
431
558
 
432
559
  module.exports = {
433
560
  generateRoadmapDocument,
434
- renderManagedBody,
435
561
  scanProject
436
- };
562
+ };
package/src/index.js CHANGED
@@ -1,8 +1,11 @@
1
- 'use strict';
2
-
3
- module.exports = {
4
- generator: require('./generator'),
5
- parser: require('./parser'),
6
- sync: require('./sync'),
7
- validator: require('./validator')
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ generator: require('./generator'),
5
+ parser: require('./parser'),
6
+ sync: require('./sync'),
7
+ validator: require('./validator'),
8
+ config: require('./config'),
9
+ model: require('./model'),
10
+ renderer: require('./renderer')
8
11
  };