@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
495 lines • 19.4 kB
JavaScript
import { StorageManager } from '../../storage/storage-manager.js';
import { promises as fs } from 'fs';
import path from 'path';
export class RoadmapManager {
configManager;
storageManager;
roadmapDataPath;
roadmaps;
themes;
initiatives;
features;
milestones;
releases;
reviews;
constructor(configManager) {
this.configManager = configManager;
this.storageManager = new StorageManager();
this.roadmapDataPath = '';
this.roadmaps = new Map();
this.themes = new Map();
this.initiatives = new Map();
this.features = new Map();
this.milestones = new Map();
this.releases = new Map();
this.reviews = new Map();
}
async initialize() {
const location = await this.storageManager.getStorageLocation();
this.roadmapDataPath = path.join(location.data, 'roadmaps');
await this.loadExistingData();
}
async loadExistingData() {
try {
// Load roadmaps
const roadmapsPath = path.join(this.roadmapDataPath, 'roadmaps.json');
if (await this.fileExists(roadmapsPath)) {
const roadmapsData = JSON.parse(await fs.readFile(roadmapsPath, 'utf-8'));
roadmapsData.forEach((roadmap) => {
roadmap.createdAt = new Date(roadmap.createdAt);
roadmap.updatedAt = new Date(roadmap.updatedAt);
this.roadmaps.set(roadmap.id, roadmap);
});
}
// Load themes
const themesPath = path.join(this.roadmapDataPath, 'themes.json');
if (await this.fileExists(themesPath)) {
const themesData = JSON.parse(await fs.readFile(themesPath, 'utf-8'));
themesData.forEach((theme) => {
this.themes.set(theme.id, theme);
});
}
// Load initiatives
const initiativesPath = path.join(this.roadmapDataPath, 'initiatives.json');
if (await this.fileExists(initiativesPath)) {
const initiativesData = JSON.parse(await fs.readFile(initiativesPath, 'utf-8'));
initiativesData.forEach((initiative) => {
this.initiatives.set(initiative.id, initiative);
});
}
// Load features
const featuresPath = path.join(this.roadmapDataPath, 'features.json');
if (await this.fileExists(featuresPath)) {
const featuresData = JSON.parse(await fs.readFile(featuresPath, 'utf-8'));
featuresData.forEach((feature) => {
this.features.set(feature.id, feature);
});
}
// Load milestones
const milestonesPath = path.join(this.roadmapDataPath, 'milestones.json');
if (await this.fileExists(milestonesPath)) {
const milestonesData = JSON.parse(await fs.readFile(milestonesPath, 'utf-8'));
milestonesData.forEach((milestone) => {
milestone.date = new Date(milestone.date);
this.milestones.set(milestone.id, milestone);
});
}
// Load releases
const releasesPath = path.join(this.roadmapDataPath, 'releases.json');
if (await this.fileExists(releasesPath)) {
const releasesData = JSON.parse(await fs.readFile(releasesPath, 'utf-8'));
releasesData.forEach((release) => {
release.date = new Date(release.date);
this.releases.set(release.id, release);
});
}
}
catch (error) {
console.error('Error loading roadmap data:', error);
}
}
async fileExists(filePath) {
try {
await fs.access(filePath);
return true;
}
catch {
return false;
}
}
async createRoadmap(options) {
const roadmap = {
id: `roadmap-${this.generateId()}`,
name: options.name,
vision: options.vision,
timeHorizon: options.timeHorizon,
status: 'draft',
themes: [],
milestones: [],
releases: [],
createdAt: new Date(),
updatedAt: new Date(),
owner: options.owner,
stakeholders: options.stakeholders || [],
metrics: {
featuresPlanned: 0,
featuresCompleted: 0,
initiativesActive: 0,
velocityTrend: 'stable',
onTimeDelivery: 100,
valueDelivered: 0
}
};
this.roadmaps.set(roadmap.id, roadmap);
await this.saveRoadmaps();
return roadmap;
}
async addTheme(options) {
const roadmap = this.roadmaps.get(options.roadmapId);
if (!roadmap) {
throw new Error(`Roadmap ${options.roadmapId} not found`);
}
const theme = {
id: `theme-${this.generateId()}`,
name: options.name,
description: options.description,
objectives: options.objectives,
initiatives: [],
priority: options.priority,
timeframe: {
startQuarter: options.startQuarter,
endQuarter: options.endQuarter
},
status: 'planned',
metrics: {
initiativesTotal: 0,
initiativesCompleted: 0,
featuresTotal: 0,
featuresCompleted: 0,
progressPercentage: 0,
valueScore: 0
}
};
this.themes.set(theme.id, theme);
roadmap.themes.push(theme);
roadmap.updatedAt = new Date();
await Promise.all([this.saveThemes(), this.saveRoadmaps()]);
return theme;
}
async createInitiative(options) {
const roadmap = this.roadmaps.get(options.roadmapId);
const theme = this.themes.get(options.themeId);
if (!roadmap) {
throw new Error(`Roadmap ${options.roadmapId} not found`);
}
if (!theme) {
throw new Error(`Theme ${options.themeId} not found`);
}
const initiative = {
id: `initiative-${this.generateId()}`,
themeId: options.themeId,
title: options.title,
description: options.description,
features: [],
epicIds: [],
value: options.estimatedValue,
effort: options.estimatedEffort,
risks: options.risks || [],
dependencies: [],
status: 'ideation'
};
this.initiatives.set(initiative.id, initiative);
theme.initiatives.push(initiative);
// Update theme metrics
theme.metrics.initiativesTotal++;
this.updateRoadmapMetrics(roadmap);
await Promise.all([
this.saveInitiatives(),
this.saveThemes(),
this.saveRoadmaps()
]);
return initiative;
}
async addFeature(options) {
const roadmap = this.roadmaps.get(options.roadmapId);
const initiative = this.initiatives.get(options.initiativeId);
if (!roadmap) {
throw new Error(`Roadmap ${options.roadmapId} not found`);
}
if (!initiative) {
throw new Error(`Initiative ${options.initiativeId} not found`);
}
const feature = {
id: `feature-${this.generateId()}`,
initiativeId: options.initiativeId,
name: options.name,
description: options.description,
userStories: [],
priority: 0, // Will be set by prioritization
businessValue: options.businessValue,
technicalComplexity: options.technicalComplexity,
targetRelease: options.targetRelease,
status: 'proposed'
};
this.features.set(feature.id, feature);
initiative.features.push(feature);
// Update metrics
const theme = this.themes.get(initiative.themeId);
if (theme) {
theme.metrics.featuresTotal++;
}
roadmap.metrics.featuresPlanned++;
this.updateRoadmapMetrics(roadmap);
await Promise.all([
this.saveFeatures(),
this.saveInitiatives(),
this.saveThemes(),
this.saveRoadmaps()
]);
return feature;
}
async createMilestone(roadmapId, milestone) {
const roadmap = this.roadmaps.get(roadmapId);
if (!roadmap) {
throw new Error(`Roadmap ${roadmapId} not found`);
}
const newMilestone = {
...milestone,
id: `milestone-${this.generateId()}`,
date: new Date(milestone.date),
status: 'upcoming'
};
this.milestones.set(newMilestone.id, newMilestone);
roadmap.milestones.push(newMilestone);
roadmap.updatedAt = new Date();
await Promise.all([this.saveMilestones(), this.saveRoadmaps()]);
return newMilestone;
}
async planRelease(roadmapId, release) {
const roadmap = this.roadmaps.get(roadmapId);
if (!roadmap) {
throw new Error(`Roadmap ${roadmapId} not found`);
}
const newRelease = {
...release,
id: `release-${this.generateId()}`,
date: new Date(release.date),
status: 'planning'
};
this.releases.set(newRelease.id, newRelease);
roadmap.releases.push(newRelease);
roadmap.updatedAt = new Date();
await Promise.all([this.saveReleases(), this.saveRoadmaps()]);
return newRelease;
}
async updateFeatureStatus(featureId, status) {
const feature = this.features.get(featureId);
if (!feature) {
throw new Error(`Feature ${featureId} not found`);
}
feature.status = status;
// Update metrics if completed
if (status === 'completed') {
const initiative = this.initiatives.get(feature.initiativeId);
if (initiative) {
const theme = this.themes.get(initiative.themeId);
if (theme) {
theme.metrics.featuresCompleted++;
this.updateThemeProgress(theme);
}
}
// Find and update roadmap
for (const [, roadmap] of this.roadmaps) {
if (roadmap.themes.some(t => t.id === initiative?.themeId)) {
roadmap.metrics.featuresCompleted++;
this.updateRoadmapMetrics(roadmap);
break;
}
}
}
await this.saveFeatures();
return feature;
}
async updateInitiativeStatus(initiativeId, status) {
const initiative = this.initiatives.get(initiativeId);
if (!initiative) {
throw new Error(`Initiative ${initiativeId} not found`);
}
initiative.status = status;
// Update theme metrics if completed
if (status === 'launched') {
const theme = this.themes.get(initiative.themeId);
if (theme) {
theme.metrics.initiativesCompleted++;
this.updateThemeProgress(theme);
}
}
await this.saveInitiatives();
return initiative;
}
async generateTimeline(roadmapId, viewType) {
const roadmap = this.roadmaps.get(roadmapId);
if (!roadmap) {
throw new Error(`Roadmap ${roadmapId} not found`);
}
const items = [];
const now = new Date();
// Add themes to timeline
for (const theme of roadmap.themes) {
const startDate = this.parseQuarter(theme.timeframe.startQuarter);
const endDate = this.parseQuarter(theme.timeframe.endQuarter);
items.push({
id: theme.id,
type: 'theme',
name: theme.name,
startDate,
endDate,
status: theme.status,
dependencies: [],
progress: theme.metrics.progressPercentage
});
}
// Add milestones
for (const milestone of roadmap.milestones) {
items.push({
id: milestone.id,
type: 'milestone',
name: milestone.name,
startDate: milestone.date,
endDate: milestone.date,
status: milestone.status,
dependencies: milestone.dependencies
});
}
// Add releases
for (const release of roadmap.releases) {
items.push({
id: release.id,
type: 'release',
name: `${release.version} - ${release.name}`,
startDate: release.date,
endDate: release.date,
status: release.status,
dependencies: []
});
}
// Sort by start date
items.sort((a, b) => a.startDate.getTime() - b.startDate.getTime());
return {
type: viewType,
startDate: items[0]?.startDate || now,
endDate: items[items.length - 1]?.endDate || now,
items
};
}
async getRoadmapHealth(roadmapId) {
const roadmap = this.roadmaps.get(roadmapId);
if (!roadmap) {
throw new Error(`Roadmap ${roadmapId} not found`);
}
const risks = [];
const recommendations = [];
// Analyze on-time delivery
if (roadmap.metrics.onTimeDelivery < 70) {
risks.push('Low on-time delivery rate');
recommendations.push('Review capacity planning and estimates');
}
// Check velocity trend
if (roadmap.metrics.velocityTrend === 'decreasing') {
risks.push('Decreasing velocity trend');
recommendations.push('Investigate blockers and team capacity');
}
// Calculate health score
let healthScore = 100;
if (roadmap.metrics.onTimeDelivery < 80)
healthScore -= 20;
if (roadmap.metrics.velocityTrend === 'decreasing')
healthScore -= 20;
if (roadmap.metrics.featuresCompleted / roadmap.metrics.featuresPlanned < 0.5)
healthScore -= 20;
const health = healthScore >= 80 ? 'excellent' :
healthScore >= 60 ? 'good' :
healthScore >= 40 ? 'at-risk' : 'critical';
return {
health,
metrics: roadmap.metrics,
risks,
recommendations
};
}
async getRoadmaps() {
return Array.from(this.roadmaps.values())
.filter(r => r.status !== 'archived')
.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
}
async getRoadmap(roadmapId) {
return this.roadmaps.get(roadmapId) || null;
}
async getThemeDetails(themeId) {
const theme = this.themes.get(themeId);
if (!theme)
return null;
const initiatives = theme.initiatives.map(i => this.initiatives.get(typeof i === 'string' ? i : i.id)).filter(Boolean);
const features = [];
for (const initiative of initiatives) {
const initiativeFeatures = initiative.features.map(f => this.features.get(typeof f === 'string' ? f : f.id)).filter(Boolean);
features.push(...initiativeFeatures);
}
return { theme, initiatives, features };
}
updateRoadmapMetrics(roadmap) {
// Count active initiatives
let activeInitiatives = 0;
for (const theme of roadmap.themes) {
for (const initiative of theme.initiatives) {
const init = typeof initiative === 'string' ?
this.initiatives.get(initiative) : initiative;
if (init && ['validated', 'scheduled', 'in-development'].includes(init.status)) {
activeInitiatives++;
}
}
}
roadmap.metrics.initiativesActive = activeInitiatives;
// Calculate value delivered
let totalValue = 0;
let completedValue = 0;
for (const feature of this.features.values()) {
if (feature.status === 'completed') {
completedValue += feature.businessValue.score;
}
totalValue += feature.businessValue.score;
}
roadmap.metrics.valueDelivered = totalValue > 0 ?
Math.round((completedValue / totalValue) * 100) : 0;
}
updateThemeProgress(theme) {
const total = theme.metrics.initiativesTotal + theme.metrics.featuresTotal;
const completed = theme.metrics.initiativesCompleted + theme.metrics.featuresCompleted;
theme.metrics.progressPercentage = total > 0 ?
Math.round((completed / total) * 100) : 0;
}
parseQuarter(quarter) {
// Parse "Q1 2024" format
const [q, year] = quarter.split(' ');
const quarterNum = parseInt(q.substring(1));
const yearNum = parseInt(year);
// Q1 = Jan, Q2 = Apr, Q3 = Jul, Q4 = Oct
const month = (quarterNum - 1) * 3;
return new Date(yearNum, month, 1);
}
generateId() {
return `${Date.now().toString(16)}-${Math.random().toString(16).substr(2, 8)}`;
}
// Save methods
async saveRoadmaps() {
const roadmapsPath = path.join(this.roadmapDataPath, 'roadmaps.json');
await fs.mkdir(path.dirname(roadmapsPath), { recursive: true });
await fs.writeFile(roadmapsPath, JSON.stringify(Array.from(this.roadmaps.values()), null, 2));
}
async saveThemes() {
const themesPath = path.join(this.roadmapDataPath, 'themes.json');
await fs.mkdir(path.dirname(themesPath), { recursive: true });
await fs.writeFile(themesPath, JSON.stringify(Array.from(this.themes.values()), null, 2));
}
async saveInitiatives() {
const initiativesPath = path.join(this.roadmapDataPath, 'initiatives.json');
await fs.mkdir(path.dirname(initiativesPath), { recursive: true });
await fs.writeFile(initiativesPath, JSON.stringify(Array.from(this.initiatives.values()), null, 2));
}
async saveFeatures() {
const featuresPath = path.join(this.roadmapDataPath, 'features.json');
await fs.mkdir(path.dirname(featuresPath), { recursive: true });
await fs.writeFile(featuresPath, JSON.stringify(Array.from(this.features.values()), null, 2));
}
async saveMilestones() {
const milestonesPath = path.join(this.roadmapDataPath, 'milestones.json');
await fs.mkdir(path.dirname(milestonesPath), { recursive: true });
await fs.writeFile(milestonesPath, JSON.stringify(Array.from(this.milestones.values()), null, 2));
}
async saveReleases() {
const releasesPath = path.join(this.roadmapDataPath, 'releases.json');
await fs.mkdir(path.dirname(releasesPath), { recursive: true });
await fs.writeFile(releasesPath, JSON.stringify(Array.from(this.releases.values()), null, 2));
}
}
//# sourceMappingURL=roadmap-manager.js.map