Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 | 1x 1x 1x 1x 1x 1x 1x 1x 18x 18x 18x 18x 18x 18x 18x 8x 8x 8x 3x 3x 2x 1x 3x 5x 8x 8x 1x 8x 4x 4x 4x 4x 4x 3x 2x 2x 2x 1x 1x 3x 3x 9x 2x 7x 7x 7x 7x 15x 15x 4x 11x 3x 3x 8x 4x 1x 3x 4x 2x 2x 2x 2x 2x 2x 2x | import * as fs from 'fs/promises';
import * as path from 'path';
import dotenv from 'dotenv';
import { AIService } from '../services/aiService';
// Load environment variables
dotenv.config();
const DEFAULT_CONTEXT_DIR = './lamplighter_context';
const FEATURE_TASKS_DIR = 'feature_tasks';
export class FeatureSpecProcessor {
private contextDir: string;
private featureTasksDir: string;
constructor() {
this.contextDir = process.env.LAMPLIGHTER_CONTEXT_DIR || DEFAULT_CONTEXT_DIR;
this.featureTasksDir = path.join(this.contextDir, FEATURE_TASKS_DIR);
// Ensure the feature tasks directory exists
this.ensureDirectories();
console.log(`[FeatureSpecProcessor] Initialized. Task files will be saved to: ${this.featureTasksDir}`);
}
/**
* Create necessary directories if they don't exist
*/
private async ensureDirectories(): Promise<void> {
try {
await fs.mkdir(this.contextDir, { recursive: true });
await fs.mkdir(this.featureTasksDir, { recursive: true });
} catch (error) {
console.error('[FeatureSpecProcessor] Error creating directories:', error);
throw new Error(`Failed to create required directories: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Derive a feature ID from a URL or title
*/
deriveFeatureId(urlOrTitle: string): string {
let featureId = '';
try {
// Try to extract a feature ID from a URL
const url = new URL(urlOrTitle);
// Get the last part of the path (usually the page title in Confluence URLs)
const pathParts = url.pathname.split('/').filter(Boolean);
if (pathParts.length > 0) {
featureId = pathParts[pathParts.length - 1];
} else {
// If no path parts, use the hostname
featureId = url.hostname.split('.')[0];
}
// Decode URL encoding like %20 or +
featureId = decodeURIComponent(featureId.replace(/\+/g, ' '));
} catch (error) {
// If not a valid URL, use the string as is
featureId = urlOrTitle;
}
// Sanitize: remove special characters, replace spaces/hyphens with underscores
featureId = featureId
.trim()
.replace(/\s+/g, '_') // Replace spaces (and now decoded +/ %20) with underscores
.replace(/-+/g, '_') // Replace hyphens with underscores
.replace(/[^a-z0-9_]/gi, '') // Remove remaining non-alphanumeric chars except underscore
.replace(/_+/g, '_') // Collapse multiple underscores
.replace(/^_+|_+$/g, '') // Trim leading/trailing underscores
.toLowerCase(); // Convert to lowercase
// Ensure we have something valid
if (!featureId) {
// Generate a timestamp-based ID as fallback
featureId = `feature_${Date.now()}`;
}
return featureId;
}
/**
* Process a specification into tasks
*/
async processSpecification(
specText: string,
codebaseSummary: string,
featureIdentifier: string
): Promise<string> {
console.log(`[FeatureSpecProcessor] Processing specification for feature: ${featureIdentifier}`);
try {
// Construct prompt for the LLM
const prompt = this.constructPrompt(specText, codebaseSummary);
// Generate task list using AI
console.log('[FeatureSpecProcessor] Generating task list with AI...');
const rawTaskListMarkdown = await AIService.generateText(prompt, { temperature: 0.3 });
// Validate and clean the AI response
const validatedTaskListMarkdown = this.validateAndCleanTaskList(rawTaskListMarkdown);
// Format the final markdown content
const finalMarkdown = this.formatFinalMarkdown(specText, validatedTaskListMarkdown, featureIdentifier);
// Write to file
const filePath = path.join(this.featureTasksDir, `feature_${featureIdentifier}_tasks.md`);
await fs.writeFile(filePath, finalMarkdown, 'utf-8');
console.log(`[FeatureSpecProcessor] Task list saved to: ${filePath}`);
return filePath;
} catch (error) {
console.error('[FeatureSpecProcessor] Error processing specification:', error);
throw new Error(`Failed to process feature specification: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Validate and clean the raw markdown string from the AI to ensure it's a checklist.
*/
private validateAndCleanTaskList(rawMarkdown: string): string {
if (!rawMarkdown || typeof rawMarkdown !== 'string') {
throw new Error('AI response was empty or invalid.');
}
const lines = rawMarkdown.split('\n');
const validLines: string[] = [];
const taskRegex = /^- \[ \] .+$/; // Matches "- [ ] Task description"
for (const line of lines) {
const trimmedLine = line.trim();
if (trimmedLine === '') {
continue; // Skip empty lines
}
if (!taskRegex.test(trimmedLine)) {
console.error(`[FeatureSpecProcessor] Invalid task list format from AI. Line: "${trimmedLine}"`);
throw new Error('AI did not return a valid Markdown task checklist. Please check the AI response or prompt.');
}
validLines.push(trimmedLine); // Keep the trimmed, valid line
}
if (validLines.length === 0) {
throw new Error('AI response did not contain any valid task list items.');
}
return validLines.join('\n');
}
/**
* Construct the prompt for the AI
*/
private constructPrompt(specText: string, codebaseSummary: string): string {
return `
You are a software development task analyzer. Your job is to break down a feature specification into
a well-structured list of actionable development tasks.
## Codebase Context
The following is a summary of the codebase structure. Use this information to reference specific modules,
components, or patterns when creating tasks:
${codebaseSummary}
## Feature Specification
${specText}
## Instructions
1. Analyze the feature specification.
2. Break it down into a logical sequence of development tasks.
3. Format the tasks as a Markdown checklist using GitHub-style "- [ ] Task description" syntax.
4. Ensure tasks are specific, actionable, and sized appropriately (not too broad or too narrow).
5. Reference relevant modules, patterns, or components from the codebase summary when appropriate.
6. Include tasks for tests if testing is implied by the feature.
7. Order tasks in a logical implementation sequence.
8. DO NOT include any explanatory text before or after the task list - ONLY output the task checklist.
Example output format:
- [ ] Initialize the XYZ module in the authentication system
- [ ] Implement the database schema changes for user preferences
- [ ] Create API endpoints in the UserController
Now, please create a task checklist for implementing the feature:
`;
}
/**
* Format the final markdown document
*/
private formatFinalMarkdown(specText: string, taskListMarkdown: string, featureId: string): string {
// Extract a title from the first few lines of the spec
const specLines = specText.split('\n');
const title = specLines[0]?.trim() || `Feature: ${featureId}`;
// Create a timestamp
const timestamp = new Date().toISOString();
// Format the complete markdown document
return `# ${title}
## Feature Specification Summary
*Generated at: ${timestamp}*
${this.createSpecSummary(specText)}
## Implementation Tasks
${taskListMarkdown}
---
*This task list was automatically generated from the feature specification by Lamplighter-MCP.*
`;
}
/**
* Create a summary from the spec text
*/
private createSpecSummary(specText: string): string {
// For simplicity, take the first few lines of the spec
// In a more advanced implementation, you could use the AI to generate a summary
const specLines = specText.split('\n');
const summary = specLines.slice(0, 5).join('\n');
return summary + (specLines.length > 5 ? '\n...' : '');
}
} |