roadmapsmith 0.7.1 → 0.8.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 +1 -1
- package/src/classifier/index.js +134 -0
- package/src/generator/index.js +53 -2
- package/src/renderer/professional.js +8 -0
package/package.json
CHANGED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const WEB_DIRS = ['app/', 'pages/', 'components/', 'src/app/', 'src/pages/', 'src/components/'];
|
|
7
|
+
const ASSET_DIRS = ['public/', 'assets/', 'static/'];
|
|
8
|
+
const WEB_CONFIGS = [
|
|
9
|
+
'next.config.js', 'next.config.ts', 'next.config.mjs',
|
|
10
|
+
'vite.config.js', 'vite.config.ts',
|
|
11
|
+
'astro.config.mjs', 'astro.config.ts'
|
|
12
|
+
];
|
|
13
|
+
const STYLE_CONFIGS = ['tailwind.config.js', 'tailwind.config.ts', 'tailwind.config.cjs'];
|
|
14
|
+
const WEB_DEPS = new Set(['next', 'react', 'vue', 'svelte', 'astro', 'vite', 'nuxt', 'gatsby', 'remix', '@remix-run/react']);
|
|
15
|
+
const LANDING_ROUTE_RE = /(?:^|\/)(?:contact|services|about|pricing|hero|cta|landing)(?:\/|\.)/i;
|
|
16
|
+
|
|
17
|
+
function readPackageDeps(projectRoot) {
|
|
18
|
+
if (!projectRoot) return [];
|
|
19
|
+
try {
|
|
20
|
+
const raw = fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8');
|
|
21
|
+
const pkg = JSON.parse(raw);
|
|
22
|
+
return Object.keys({ ...pkg.dependencies, ...pkg.devDependencies });
|
|
23
|
+
} catch {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function hasDir(files, prefix) {
|
|
29
|
+
return files.some((f) => f.startsWith(prefix));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function hasFilename(files, name) {
|
|
33
|
+
return files.some((f) => f === name || f.endsWith('/' + name));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function hasWorkspaces(projectRoot) {
|
|
37
|
+
if (!projectRoot) return false;
|
|
38
|
+
try {
|
|
39
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8'));
|
|
40
|
+
return Array.isArray(pkg.workspaces) && pkg.workspaces.length > 0;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function classifyProject({ projectRoot, files }) {
|
|
47
|
+
const signals = [];
|
|
48
|
+
|
|
49
|
+
if (hasWorkspaces(projectRoot)) {
|
|
50
|
+
signals.push('package.json workspaces field');
|
|
51
|
+
return { type: 'monorepo', confidence: 'high', signals };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const hasPy = hasFilename(files, 'pyproject.toml') || hasFilename(files, 'setup.py');
|
|
55
|
+
if (hasPy && !files.some((f) => /\.[jt]sx?$/.test(f))) {
|
|
56
|
+
signals.push('pyproject.toml / setup.py, no JS files');
|
|
57
|
+
return { type: 'python-package', confidence: 'high', signals };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let webScore = 0;
|
|
61
|
+
let landingScore = 0;
|
|
62
|
+
const deps = readPackageDeps(projectRoot);
|
|
63
|
+
|
|
64
|
+
for (const dep of deps) {
|
|
65
|
+
if (WEB_DEPS.has(dep)) {
|
|
66
|
+
webScore += 2;
|
|
67
|
+
signals.push(`dependency: ${dep}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const dir of WEB_DIRS) {
|
|
72
|
+
if (hasDir(files, dir)) {
|
|
73
|
+
webScore += 2;
|
|
74
|
+
signals.push(`directory: ${dir.replace(/\/$/, '')}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const dir of ASSET_DIRS) {
|
|
79
|
+
if (hasDir(files, dir)) {
|
|
80
|
+
webScore += 1;
|
|
81
|
+
signals.push(`directory: ${dir.replace(/\/$/, '')}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const cfg of WEB_CONFIGS) {
|
|
86
|
+
if (hasFilename(files, cfg)) {
|
|
87
|
+
webScore += 3;
|
|
88
|
+
signals.push(`config: ${cfg}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const cfg of STYLE_CONFIGS) {
|
|
93
|
+
if (hasFilename(files, cfg)) {
|
|
94
|
+
webScore += 1;
|
|
95
|
+
signals.push(`config: ${cfg}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (files.some((f) => /\.css$/.test(f))) {
|
|
100
|
+
webScore += 1;
|
|
101
|
+
signals.push('CSS files present');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const landingRoutes = files.filter((f) => LANDING_ROUTE_RE.test(f));
|
|
105
|
+
if (landingRoutes.length > 0) {
|
|
106
|
+
landingScore += landingRoutes.length * 2;
|
|
107
|
+
signals.push(`landing/service routes: ${landingRoutes.length}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (hasFilename(files, 'favicon.ico') || hasFilename(files, 'logo.png') || hasFilename(files, 'logo.svg')) {
|
|
111
|
+
landingScore += 1;
|
|
112
|
+
signals.push('branding asset in public/');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (webScore === 0 && (files.some((f) => f.startsWith('bin/')) || hasFilename(files, 'cli.js'))) {
|
|
116
|
+
signals.push('bin/ directory or cli.js');
|
|
117
|
+
return { type: 'cli-tool', confidence: 'medium', signals };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (webScore === 0 && hasFilename(files, 'package.json')) {
|
|
121
|
+
signals.push('package.json, no web signals');
|
|
122
|
+
return { type: 'npm-package', confidence: 'low', signals };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (webScore >= 3) {
|
|
126
|
+
const type = landingScore >= 3 ? 'landing-site' : 'frontend-web';
|
|
127
|
+
const confidence = webScore >= 7 ? 'high' : 'medium';
|
|
128
|
+
return { type, confidence, signals };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { type: 'unknown-generic', confidence: 'low', signals: [] };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = { classifyProject };
|
package/src/generator/index.js
CHANGED
|
@@ -9,6 +9,7 @@ const { parseRoadmap, upsertManagedBlock } = require('../parser');
|
|
|
9
9
|
const { findBestTaskMatch, dedupeTasks } = require('../match');
|
|
10
10
|
const { collectPluginContributions } = require('../config');
|
|
11
11
|
const { renderBody } = require('../renderer');
|
|
12
|
+
const { classifyProject } = require('../classifier');
|
|
12
13
|
|
|
13
14
|
const IMPL_PATTERN_RE = /[/|]TODO|TODO[|/]|[/|]FIXME|FIXME[|/]/;
|
|
14
15
|
const COMMENT_TODO_RE = /(?:\/\/|#|\*\s*).*\b(?:TODO|FIXME)\b/;
|
|
@@ -149,6 +150,8 @@ function scanProject(projectRoot) {
|
|
|
149
150
|
const implementedFiles = files.filter((file) => /\.(js|ts|tsx|py|go|rs|java|kt|cs)$/.test(file));
|
|
150
151
|
const testFiles = files.filter((file) => /(^|\/)(__tests__|tests)\//.test(file) || /\.test\.|\.spec\.|_test\.go$/.test(file));
|
|
151
152
|
|
|
153
|
+
const classifier = classifyProject({ projectRoot, files });
|
|
154
|
+
|
|
152
155
|
return {
|
|
153
156
|
projectRoot,
|
|
154
157
|
files,
|
|
@@ -160,7 +163,10 @@ function scanProject(projectRoot) {
|
|
|
160
163
|
codeTodos,
|
|
161
164
|
workspaces,
|
|
162
165
|
implementedCount: implementedFiles.length,
|
|
163
|
-
testCount: testFiles.length
|
|
166
|
+
testCount: testFiles.length,
|
|
167
|
+
projectType: classifier.type,
|
|
168
|
+
classifierConfidence: classifier.confidence,
|
|
169
|
+
classifierSignals: classifier.signals
|
|
164
170
|
};
|
|
165
171
|
}
|
|
166
172
|
|
|
@@ -175,6 +181,35 @@ function toCandidate(text, phase, priority, source = 'default') {
|
|
|
175
181
|
};
|
|
176
182
|
}
|
|
177
183
|
|
|
184
|
+
const WEB_CANDIDATES_COMMON = [
|
|
185
|
+
{ text: 'Add SEO metadata: title, description, and canonical URL for all pages', phase: 'P0' },
|
|
186
|
+
{ text: 'Implement responsive and mobile-first layout across all breakpoints', phase: 'P0' },
|
|
187
|
+
{ text: 'Establish accessibility baseline (semantic HTML, ARIA labels, keyboard navigation)', phase: 'P0' },
|
|
188
|
+
{ text: 'Add OpenGraph and Twitter card metadata for social sharing', phase: 'P1' },
|
|
189
|
+
{ text: 'Achieve Lighthouse performance score ≥ 90 and resolve critical findings', phase: 'P1' },
|
|
190
|
+
{ text: 'Validate branding consistency: typography, color tokens, and logo usage', phase: 'P1' },
|
|
191
|
+
{ text: 'Configure deployment and hosting pipeline (CI/CD to production)', phase: 'P2' },
|
|
192
|
+
{ text: 'Add web security headers: Content-Security-Policy, X-Frame-Options, HSTS', phase: 'P2' }
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
const LANDING_CANDIDATES = [
|
|
196
|
+
{ text: 'Complete services and content sections with clear value proposition', phase: 'P1' },
|
|
197
|
+
{ text: 'Implement contact form and conversion flow with input validation', phase: 'P1' },
|
|
198
|
+
{ text: 'Set up analytics and conversion event tracking', phase: 'P2' }
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
function buildWebCandidates(scan) {
|
|
202
|
+
const candidates = WEB_CANDIDATES_COMMON.map(({ text, phase }) =>
|
|
203
|
+
toCandidate(text, phase, phase, 'classifier')
|
|
204
|
+
);
|
|
205
|
+
if (scan.projectType === 'landing-site') {
|
|
206
|
+
for (const { text, phase } of LANDING_CANDIDATES) {
|
|
207
|
+
candidates.push(toCandidate(text, phase, phase, 'classifier'));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return candidates;
|
|
211
|
+
}
|
|
212
|
+
|
|
178
213
|
function buildDefaultCandidates(scan, config) {
|
|
179
214
|
const languageLabel = scan.languages.length > 0 ? scan.languages.join(', ') : 'current stack';
|
|
180
215
|
const candidates = [];
|
|
@@ -223,6 +258,10 @@ function buildDefaultCandidates(scan, config) {
|
|
|
223
258
|
candidates.push(toCandidate(`Resolve backlog note in ${hint.file}`, 'P0', 'P0', 'todo-hint'));
|
|
224
259
|
}
|
|
225
260
|
|
|
261
|
+
if (scan.projectType === 'frontend-web' || scan.projectType === 'landing-site') {
|
|
262
|
+
candidates.push(...buildWebCandidates(scan));
|
|
263
|
+
}
|
|
264
|
+
|
|
226
265
|
return candidates;
|
|
227
266
|
}
|
|
228
267
|
|
|
@@ -547,10 +586,22 @@ function generateRoadmapDocument(options) {
|
|
|
547
586
|
items: section.items || []
|
|
548
587
|
}));
|
|
549
588
|
|
|
589
|
+
const evidenceLine = scan.classifierSignals.length > 0
|
|
590
|
+
? scan.classifierSignals.slice(0, 5).join(', ')
|
|
591
|
+
: 'general file scan';
|
|
592
|
+
const profileSection = {
|
|
593
|
+
title: 'Detected Project Profile',
|
|
594
|
+
items: [
|
|
595
|
+
`- **Type:** ${scan.projectType}`,
|
|
596
|
+
`- **Confidence:** ${scan.classifierConfidence}`,
|
|
597
|
+
`- **Evidence:** ${evidenceLine}`
|
|
598
|
+
]
|
|
599
|
+
};
|
|
600
|
+
|
|
550
601
|
const baseCandidates = buildDefaultCandidates(scan, config);
|
|
551
602
|
const matcherCandidates = applyTaskMatchers(scan, config);
|
|
552
603
|
const merged = mergeWithExisting([...baseCandidates, ...matcherCandidates, ...pluginTaskCandidates], existingPhaseTasks);
|
|
553
|
-
const model = createModel(scan, merged, config, [...configSections, ...pluginSections], existingCheckedById);
|
|
604
|
+
const model = createModel(scan, merged, config, [profileSection, ...configSections, ...pluginSections], existingCheckedById);
|
|
554
605
|
const profile = config.roadmapProfile || 'compact';
|
|
555
606
|
const managedBody = renderBody(model, profile);
|
|
556
607
|
|
|
@@ -530,6 +530,14 @@ function renderProfessional(model) {
|
|
|
530
530
|
renderSection12SuccessCriteria(model, lines);
|
|
531
531
|
renderSection13CustomPhases(model, lines);
|
|
532
532
|
|
|
533
|
+
for (const section of (model.customSections || [])) {
|
|
534
|
+
lines.push(`## ${section.title}`);
|
|
535
|
+
for (const line of section.items) {
|
|
536
|
+
lines.push(line);
|
|
537
|
+
}
|
|
538
|
+
lines.push('');
|
|
539
|
+
}
|
|
540
|
+
|
|
533
541
|
return ensureTrailingNewline(lines.join('\n')).trimEnd();
|
|
534
542
|
}
|
|
535
543
|
|