roadmapsmith 0.4.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 +212 -208
- package/src/generator/index.js +45 -103
- package/src/index.js +10 -7
- package/src/io.js +263 -227
- package/src/match.js +85 -85
- package/src/model.js +32 -33
- package/src/parser/index.js +106 -108
- package/src/renderer/professional.js +7 -0
- package/src/utils.js +141 -142
- package/src/validator/index.js +22 -3
package/src/generator/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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');
|
|
@@ -10,21 +10,52 @@ const { findBestTaskMatch, dedupeTasks } = require('../match');
|
|
|
10
10
|
const { collectPluginContributions } = require('../config');
|
|
11
11
|
const { renderBody } = require('../renderer');
|
|
12
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']);
|
|
21
|
+
|
|
13
22
|
function detectModules(files) {
|
|
14
23
|
const modules = new Set();
|
|
15
|
-
const
|
|
24
|
+
const rootPrefixes = ['src/', 'apps/', 'packages/', 'lib/', 'cmd/', 'internal/'];
|
|
16
25
|
|
|
17
26
|
for (const file of files) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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;
|
|
21
46
|
}
|
|
22
|
-
|
|
47
|
+
|
|
23
48
|
const first = relative.split('/')[0];
|
|
24
|
-
if (!first
|
|
25
|
-
|
|
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);
|
|
26
58
|
}
|
|
27
|
-
modules.add(first);
|
|
28
59
|
}
|
|
29
60
|
|
|
30
61
|
return Array.from(modules).sort((left, right) => left.localeCompare(right));
|
|
@@ -58,7 +89,7 @@ function collectTodoHints(projectRoot, files) {
|
|
|
58
89
|
|
|
59
90
|
const lines = content.split(/\r?\n/);
|
|
60
91
|
for (let i = 0; i < lines.length; i += 1) {
|
|
61
|
-
if (
|
|
92
|
+
if (isTodoMarker(lines[i])) {
|
|
62
93
|
hints.push({
|
|
63
94
|
file,
|
|
64
95
|
line: i + 1,
|
|
@@ -89,7 +120,7 @@ function collectCodeTodoHints(projectRoot, files) {
|
|
|
89
120
|
|
|
90
121
|
const lines = content.split(/\r?\n/);
|
|
91
122
|
for (let i = 0; i < lines.length; i += 1) {
|
|
92
|
-
if (
|
|
123
|
+
if (isTodoMarker(lines[i])) {
|
|
93
124
|
hints.push({
|
|
94
125
|
file,
|
|
95
126
|
line: i + 1,
|
|
@@ -113,6 +144,7 @@ function scanProject(projectRoot) {
|
|
|
113
144
|
const commands = detectCommands(files);
|
|
114
145
|
const todos = collectTodoHints(projectRoot, files);
|
|
115
146
|
const codeTodos = collectCodeTodoHints(projectRoot, files);
|
|
147
|
+
const workspaces = detectWorkspaces(projectRoot, files);
|
|
116
148
|
|
|
117
149
|
const implementedFiles = files.filter((file) => /\.(js|ts|tsx|py|go|rs|java|kt|cs)$/.test(file));
|
|
118
150
|
const testFiles = files.filter((file) => /(^|\/)(__tests__|tests)\//.test(file) || /\.test\.|\.spec\.|_test\.go$/.test(file));
|
|
@@ -126,6 +158,7 @@ function scanProject(projectRoot) {
|
|
|
126
158
|
commands,
|
|
127
159
|
todos,
|
|
128
160
|
codeTodos,
|
|
161
|
+
workspaces,
|
|
129
162
|
implementedCount: implementedFiles.length,
|
|
130
163
|
testCount: testFiles.length
|
|
131
164
|
};
|
|
@@ -272,97 +305,6 @@ function groupByPhase(tasks) {
|
|
|
272
305
|
return groups;
|
|
273
306
|
}
|
|
274
307
|
|
|
275
|
-
function taskLine(task) {
|
|
276
|
-
return `- [${task.checked ? 'x' : ' '}] ${task.text} <!-- rs:task=${task.id} -->`;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function checkedState(model, id) {
|
|
280
|
-
return Boolean(model.checkedById && model.checkedById[id]);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function renderManagedBody(model) {
|
|
284
|
-
const lines = [];
|
|
285
|
-
|
|
286
|
-
lines.push('# Project Roadmap');
|
|
287
|
-
lines.push('');
|
|
288
|
-
lines.push('## Product North Star');
|
|
289
|
-
lines.push(model.northStar);
|
|
290
|
-
lines.push('');
|
|
291
|
-
|
|
292
|
-
lines.push('## Current State');
|
|
293
|
-
lines.push(`- Implemented surface: ${model.currentState.implementedSummary}`);
|
|
294
|
-
lines.push(`- TODO surface: ${model.currentState.todoSummary}`);
|
|
295
|
-
lines.push(`- Detected stacks: ${model.currentState.stackSummary}`);
|
|
296
|
-
lines.push('');
|
|
297
|
-
|
|
298
|
-
lines.push('## Phased Roadmap');
|
|
299
|
-
lines.push('');
|
|
300
|
-
lines.push('### Phase P0 (Critical)');
|
|
301
|
-
for (const task of model.phases.P0) {
|
|
302
|
-
lines.push(taskLine(task));
|
|
303
|
-
}
|
|
304
|
-
lines.push('');
|
|
305
|
-
lines.push('### Phase P1 (Important)');
|
|
306
|
-
for (const task of model.phases.P1) {
|
|
307
|
-
lines.push(taskLine(task));
|
|
308
|
-
}
|
|
309
|
-
lines.push('');
|
|
310
|
-
lines.push('### Phase P2 (Optimization)');
|
|
311
|
-
for (const task of model.phases.P2) {
|
|
312
|
-
lines.push(taskLine(task));
|
|
313
|
-
}
|
|
314
|
-
lines.push('');
|
|
315
|
-
|
|
316
|
-
lines.push('## Release Milestones');
|
|
317
|
-
for (const milestone of model.milestones) {
|
|
318
|
-
const id = `milestone-${slugify(milestone.version)}`;
|
|
319
|
-
lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] ${milestone.version}: ${milestone.goal} <!-- rs:task=${id} -->`);
|
|
320
|
-
}
|
|
321
|
-
lines.push('');
|
|
322
|
-
|
|
323
|
-
lines.push('## Command/Module Breakdown');
|
|
324
|
-
if (model.commandBreakdown.length === 0) {
|
|
325
|
-
const id = 'identify-command-module-boundaries';
|
|
326
|
-
lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] Identify command/module boundaries for the next increment <!-- rs:task=${id} -->`);
|
|
327
|
-
} else {
|
|
328
|
-
for (const item of model.commandBreakdown) {
|
|
329
|
-
const id = `module-${slugify(item)}`;
|
|
330
|
-
lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] ${item} <!-- rs:task=${id} -->`);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
lines.push('');
|
|
334
|
-
|
|
335
|
-
lines.push('## Exit Criteria Per Phase');
|
|
336
|
-
for (const item of model.exitCriteria) {
|
|
337
|
-
const id = `exit-${slugify(item)}`;
|
|
338
|
-
lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] ${item} <!-- rs:task=${id} -->`);
|
|
339
|
-
}
|
|
340
|
-
lines.push('');
|
|
341
|
-
|
|
342
|
-
for (const section of model.customSections) {
|
|
343
|
-
lines.push(`## ${section.title}`);
|
|
344
|
-
for (const line of section.items) {
|
|
345
|
-
lines.push(line);
|
|
346
|
-
}
|
|
347
|
-
lines.push('');
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
lines.push('## Risks and Anti-goals');
|
|
351
|
-
lines.push('### Risks');
|
|
352
|
-
for (const risk of model.risks) {
|
|
353
|
-
const id = `risk-${slugify(risk)}`;
|
|
354
|
-
lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] ${risk} <!-- rs:task=${id} -->`);
|
|
355
|
-
}
|
|
356
|
-
lines.push('');
|
|
357
|
-
lines.push('### Anti-goals');
|
|
358
|
-
for (const antiGoal of model.antiGoals) {
|
|
359
|
-
const id = `anti-goal-${slugify(antiGoal)}`;
|
|
360
|
-
lines.push(`- [${checkedState(model, id) ? 'x' : ' '}] ${antiGoal} <!-- rs:task=${id} -->`);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
return ensureTrailingNewline(lines.join('\n')).trimEnd();
|
|
364
|
-
}
|
|
365
|
-
|
|
366
308
|
function inferProjectName(projectRoot) {
|
|
367
309
|
const pkgPath = path.join(projectRoot, 'package.json');
|
|
368
310
|
try {
|
|
@@ -488,6 +430,7 @@ function createModel(scan, tasks, config, customSections, checkedById) {
|
|
|
488
430
|
implemented,
|
|
489
431
|
scaffold,
|
|
490
432
|
knownLimitations,
|
|
433
|
+
workspaces: scan.workspaces || [],
|
|
491
434
|
implementedSummary: `${scan.implementedCount} implementation files detected`,
|
|
492
435
|
todoSummary: `${scan.todos.length} TODO/FIXME markers detected`,
|
|
493
436
|
stackSummary: scan.languages.length > 0 ? scan.languages.join(', ') : 'No language-specific stack detected'
|
|
@@ -615,6 +558,5 @@ function generateRoadmapDocument(options) {
|
|
|
615
558
|
|
|
616
559
|
module.exports = {
|
|
617
560
|
generateRoadmapDocument,
|
|
618
|
-
renderManagedBody,
|
|
619
561
|
scanProject
|
|
620
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
|
};
|