roadmapsmith 0.7.1 → 0.9.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/bin/cli.js CHANGED
@@ -65,6 +65,12 @@ async function run() {
65
65
  const command = parsed.command;
66
66
  const flags = parsed.flags;
67
67
 
68
+ if (isEnabled(flags.version) || isEnabled(flags.v)) {
69
+ const pkg = require(path.join(__dirname, '..', 'package.json'));
70
+ process.stdout.write(pkg.version + '\n');
71
+ process.exit(0);
72
+ }
73
+
68
74
  if (!command || isEnabled(flags.help) || isEnabled(flags.h)) {
69
75
  printHelp();
70
76
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roadmapsmith",
3
- "version": "0.7.1",
3
+ "version": "0.9.0",
4
4
  "description": "Evidence-backed ROADMAP.md generator and sync tool for AI coding agents.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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 };
@@ -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