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.
- package/package.json +40 -41
- package/src/config.js +211 -187
- package/src/generator/index.js +233 -107
- package/src/index.js +10 -7
- package/src/io.js +263 -227
- package/src/match.js +85 -85
- package/src/model.js +31 -27
- package/src/parser/index.js +106 -108
- 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 +474 -0
- package/src/utils.js +141 -142
- package/src/validator/index.js +22 -3
package/src/generator/index.js
CHANGED
|
@@ -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
|
|
24
|
+
const rootPrefixes = ['src/', 'apps/', 'packages/', 'lib/', 'cmd/', 'internal/'];
|
|
15
25
|
|
|
16
26
|
for (const file of files) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
47
|
+
|
|
22
48
|
const first = relative.split('/')[0];
|
|
23
|
-
if (!first
|
|
24
|
-
|
|
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 (
|
|
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
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
275
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
],
|
|
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
|
|
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
|
};
|