@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
374 lines • 15.7 kB
JavaScript
export class TimelineGenerator {
generateQuarterlyView(roadmap, startQuarter, endQuarter) {
const items = [];
const startDate = this.parseQuarter(startQuarter);
const endDate = this.parseQuarter(endQuarter);
// Add themes
for (const theme of roadmap.themes) {
const themeStart = this.parseQuarter(theme.timeframe.startQuarter);
const themeEnd = this.parseQuarter(theme.timeframe.endQuarter);
// Only include themes that overlap with the view period
if (themeEnd >= startDate && themeStart <= endDate) {
items.push({
id: theme.id,
type: 'theme',
name: theme.name,
startDate: themeStart,
endDate: themeEnd,
status: theme.status,
dependencies: [],
progress: theme.metrics.progressPercentage
});
}
}
// Add milestones
for (const milestone of roadmap.milestones) {
if (milestone.date >= startDate && milestone.date <= endDate) {
items.push({
id: milestone.id,
type: 'milestone',
name: milestone.name,
startDate: milestone.date,
endDate: milestone.date,
status: milestone.status,
dependencies: milestone.dependencies
});
}
}
// Sort by start date
items.sort((a, b) => a.startDate.getTime() - b.startDate.getTime());
return {
type: 'quarterly',
startDate,
endDate,
items
};
}
generateMonthlyView(roadmap, initiatives, features, startMonth, months) {
const items = [];
const endDate = new Date(startMonth);
endDate.setMonth(endDate.getMonth() + months);
// Add initiatives with estimated timelines
for (const initiative of initiatives) {
const estimatedStart = this.estimateInitiativeStart(initiative, roadmap);
const estimatedEnd = this.estimateInitiativeEnd(initiative, estimatedStart);
if (estimatedEnd >= startMonth && estimatedStart <= endDate) {
items.push({
id: initiative.id,
type: 'initiative',
name: initiative.title,
startDate: estimatedStart,
endDate: estimatedEnd,
status: initiative.status,
dependencies: initiative.dependencies.map(d => d.targetId || '').filter(Boolean),
progress: this.calculateInitiativeProgress(initiative, features)
});
}
}
// Add features
for (const feature of features) {
const featureStart = this.estimateFeatureStart(feature, initiatives);
const featureEnd = this.estimateFeatureEnd(feature, featureStart);
if (featureEnd >= startMonth && featureStart <= endDate) {
items.push({
id: feature.id,
type: 'feature',
name: feature.name,
startDate: featureStart,
endDate: featureEnd,
status: feature.status,
dependencies: [],
progress: feature.status === 'completed' ? 100 :
feature.status === 'in-progress' ? 50 : 0
});
}
}
// Add releases
for (const release of roadmap.releases) {
if (release.date >= startMonth && release.date <= endDate) {
items.push({
id: release.id,
type: 'release',
name: `v${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: 'monthly',
startDate: startMonth,
endDate,
items
};
}
generateReleaseView(roadmap, features) {
const items = [];
const now = new Date();
// Group features by release
const releaseGroups = new Map();
for (const [_, feature] of features) {
if (feature.targetRelease) {
if (!releaseGroups.has(feature.targetRelease)) {
releaseGroups.set(feature.targetRelease, []);
}
releaseGroups.get(feature.targetRelease).push(feature);
}
}
// Add releases with their features
for (const release of roadmap.releases) {
items.push({
id: release.id,
type: 'release',
name: `v${release.version}: ${release.name}`,
startDate: release.date,
endDate: release.date,
status: release.status,
dependencies: [],
progress: this.calculateReleaseProgress(release, releaseGroups.get(release.id) || [])
});
// Add features for this release
const releaseFeatures = releaseGroups.get(release.id) || [];
for (const feature of releaseFeatures) {
const featureStart = new Date(release.date);
featureStart.setMonth(featureStart.getMonth() - 3); // Assume 3 months development
items.push({
id: feature.id,
type: 'feature',
name: feature.name,
startDate: featureStart,
endDate: release.date,
status: feature.status,
dependencies: [],
progress: feature.status === 'completed' ? 100 :
feature.status === 'in-progress' ? 50 : 0
});
}
}
// Sort by date
items.sort((a, b) => a.startDate.getTime() - b.startDate.getTime());
const startDate = items[0]?.startDate || now;
const endDate = items[items.length - 1]?.endDate || now;
return {
type: 'release',
startDate,
endDate,
items
};
}
generateNowNextLaterView(roadmap, themes, initiatives, features) {
const now = new Date();
const nextQuarter = new Date(now);
nextQuarter.setMonth(nextQuarter.getMonth() + 3);
const laterDate = new Date(now);
laterDate.setMonth(laterDate.getMonth() + 9);
const items = [];
// Categorize items into Now, Next, Later
const categorizeByTiming = (startDate) => {
if (startDate <= now)
return 'now';
if (startDate <= nextQuarter)
return 'next';
return 'later';
};
// Process themes
for (const theme of roadmap.themes) {
const themeStart = this.parseQuarter(theme.timeframe.startQuarter);
const timing = categorizeByTiming(themeStart);
items.push({
id: theme.id,
type: 'theme',
name: `[${timing.toUpperCase()}] ${theme.name}`,
startDate: timing === 'now' ? now :
timing === 'next' ? nextQuarter : laterDate,
endDate: this.parseQuarter(theme.timeframe.endQuarter),
status: theme.status,
dependencies: [],
progress: theme.metrics.progressPercentage
});
}
// Process initiatives
for (const [_, initiative] of initiatives) {
const initiativeStart = this.estimateInitiativeStart(initiative, roadmap);
const timing = categorizeByTiming(initiativeStart);
items.push({
id: initiative.id,
type: 'initiative',
name: `[${timing.toUpperCase()}] ${initiative.title}`,
startDate: timing === 'now' ? now :
timing === 'next' ? nextQuarter : laterDate,
endDate: this.estimateInitiativeEnd(initiative, initiativeStart),
status: initiative.status,
dependencies: initiative.dependencies.map(d => d.targetId || '').filter(Boolean),
progress: 0
});
}
// Group by timing for better visualization
items.sort((a, b) => {
// First sort by timing category
const aCategory = a.name.startsWith('[NOW]') ? 0 :
a.name.startsWith('[NEXT]') ? 1 : 2;
const bCategory = b.name.startsWith('[NOW]') ? 0 :
b.name.startsWith('[NEXT]') ? 1 : 2;
if (aCategory !== bCategory)
return aCategory - bCategory;
// Then by type (themes first, then initiatives, then features)
const typeOrder = { theme: 0, initiative: 1, feature: 2, milestone: 3, release: 4 };
return typeOrder[a.type] - typeOrder[b.type];
});
return {
type: 'now-next-later',
startDate: now,
endDate: laterDate,
items
};
}
// Helper methods
parseQuarter(quarter) {
const [q, year] = quarter.split(' ');
const quarterNum = parseInt(q.substring(1));
const yearNum = parseInt(year);
const month = (quarterNum - 1) * 3;
return new Date(yearNum, month, 1);
}
estimateInitiativeStart(initiative, roadmap) {
// Find the theme this initiative belongs to
const theme = roadmap.themes.find(t => t.initiatives.some(i => (typeof i === 'string' ? i : i.id) === initiative.id));
if (theme) {
return this.parseQuarter(theme.timeframe.startQuarter);
}
// Default to current date
return new Date();
}
estimateInitiativeEnd(initiative, startDate) {
const endDate = new Date(startDate);
// Estimate based on effort
const totalWeeks = initiative.effort.developmentWeeks +
initiative.effort.designWeeks +
initiative.effort.qaWeeks;
endDate.setDate(endDate.getDate() + (totalWeeks * 7));
return endDate;
}
estimateFeatureStart(feature, initiatives) {
// Find the initiative this feature belongs to
const initiative = initiatives.find(i => i.features.some(f => (typeof f === 'string' ? f : f.id) === feature.id));
if (initiative) {
// Return current date as we don't have the full roadmap context here
return new Date();
}
return new Date();
}
estimateFeatureEnd(feature, startDate) {
const endDate = new Date(startDate);
// Estimate based on complexity
const complexityWeeks = {
'low': 2,
'medium': 4,
'high': 8,
'very-high': 12
};
const weeks = complexityWeeks[feature.technicalComplexity];
endDate.setDate(endDate.getDate() + (weeks * 7));
return endDate;
}
calculateInitiativeProgress(initiative, features) {
const initiativeFeatures = features.filter(f => initiative.features.some(initiativeFeature => (typeof initiativeFeature === 'string' ? initiativeFeature : initiativeFeature.id) === f.id));
if (initiativeFeatures.length === 0)
return 0;
const completedCount = initiativeFeatures.filter(f => f.status === 'completed').length;
return Math.round((completedCount / initiativeFeatures.length) * 100);
}
calculateReleaseProgress(release, features) {
if (features.length === 0)
return 0;
const completedCount = features.filter(f => f.status === 'completed').length;
return Math.round((completedCount / features.length) * 100);
}
// Generate Gantt chart data
generateGanttData(timelineView) {
return {
tasks: timelineView.items.map((item, index) => ({
id: item.id,
name: item.name,
start: item.startDate.toISOString(),
end: item.endDate.toISOString(),
progress: item.progress || 0,
dependencies: item.dependencies,
type: item.type,
status: item.status,
row: index
})),
viewStart: timelineView.startDate.toISOString(),
viewEnd: timelineView.endDate.toISOString()
};
}
// Critical path analysis
findCriticalPath(items) {
// Build dependency graph
const graph = new Map();
const itemMap = new Map();
for (const item of items) {
itemMap.set(item.id, item);
graph.set(item.id, new Set(item.dependencies));
}
// Find items with no dependencies (start nodes)
const startNodes = items.filter(item => item.dependencies.length === 0);
// Calculate earliest start/finish times
const earliestTimes = new Map();
const calculateEarliestTimes = (itemId) => {
if (earliestTimes.has(itemId)) {
return earliestTimes.get(itemId);
}
const item = itemMap.get(itemId);
const dependencies = Array.from(graph.get(itemId) || []);
let earliestStart = item.startDate.getTime();
if (dependencies.length > 0) {
const depFinishTimes = dependencies.map(depId => {
const depTimes = calculateEarliestTimes(depId);
return depTimes.finish;
});
earliestStart = Math.max(...depFinishTimes);
}
const duration = item.endDate.getTime() - item.startDate.getTime();
const earliestFinish = earliestStart + duration;
earliestTimes.set(itemId, { start: earliestStart, finish: earliestFinish });
return { start: earliestStart, finish: earliestFinish };
};
// Calculate for all items
for (const item of items) {
calculateEarliestTimes(item.id);
}
// Find the critical path (items with no slack)
const criticalPath = [];
// Implementation simplified - in production would need full CPM algorithm
// For now, return the longest dependency chain
let maxDuration = 0;
let longestPath = [];
const findLongestPath = (itemId, currentPath, currentDuration) => {
currentPath.push(itemId);
const item = itemMap.get(itemId);
currentDuration += item.endDate.getTime() - item.startDate.getTime();
const dependents = items.filter(i => i.dependencies.includes(itemId));
if (dependents.length === 0) {
if (currentDuration > maxDuration) {
maxDuration = currentDuration;
longestPath = [...currentPath];
}
}
else {
for (const dependent of dependents) {
findLongestPath(dependent.id, [...currentPath], currentDuration);
}
}
};
for (const startNode of startNodes) {
findLongestPath(startNode.id, [], 0);
}
return longestPath;
}
}
//# sourceMappingURL=timeline-generator.js.map