Skip to main content
Glama
solution-service.ts11.9 kB
import { v4 as uuidv4 } from 'uuid'; import type { FileStorage } from '../../infrastructure/file-storage.js'; import type { PlanService } from './plan-service.js'; import type { Solution, SolutionStatus, Tradeoff, EffortEstimate, Tag } from '../entities/types.js'; import { validateEffortEstimate, validateTags } from './validators.js'; // Input types export interface ProposeSolutionInput { planId: string; solution: { title: string; description: string; approach: string; implementationNotes?: string; tradeoffs: Tradeoff[]; addressing: string[]; evaluation: { effortEstimate: EffortEstimate; technicalFeasibility: 'high' | 'medium' | 'low'; riskAssessment: string; dependencies?: string[]; performanceImpact?: string; }; tags?: Tag[]; }; } export interface CompareSolutionsInput { planId: string; solutionIds: string[]; aspects?: string[]; } export interface SelectSolutionInput { planId: string; solutionId: string; reason?: string; } export interface UpdateSolutionInput { planId: string; solutionId: string; updates: Partial<ProposeSolutionInput['solution']>; } export interface ListSolutionsInput { planId: string; filters?: { status?: SolutionStatus; addressingRequirement?: string; tags?: Tag[]; }; limit?: number; offset?: number; } export interface DeleteSolutionInput { planId: string; solutionId: string; force?: boolean; } export interface GetSolutionInput { planId: string; solutionId: string; } export interface GetSolutionResult { solution: Solution; } // Output types export interface ProposeSolutionResult { solutionId: string; solution: Solution; } export interface CompareSolutionsResult { comparison: { solutions: Solution[]; matrix: Array<{ aspect: string; solutions: Array<{ solutionId: string; solutionTitle: string; pros: string[]; cons: string[]; score?: number; }>; winner?: string; }>; summary: { bestOverall?: string; recommendations: string[]; }; }; } export interface SelectSolutionResult { success: boolean; solution: Solution; deselected?: Solution[]; } export interface UpdateSolutionResult { success: boolean; solution: Solution; } export interface ListSolutionsResult { solutions: Solution[]; total: number; hasMore: boolean; } export interface DeleteSolutionResult { success: boolean; message: string; } export class SolutionService { constructor( private storage: FileStorage, private planService: PlanService ) {} async getSolution(input: GetSolutionInput): Promise<GetSolutionResult> { const solutions = await this.storage.loadEntities<Solution>(input.planId, 'solutions'); const solution = solutions.find((s) => s.id === input.solutionId); if (!solution) { throw new Error('Solution not found'); } return { solution }; } async proposeSolution(input: ProposeSolutionInput): Promise<ProposeSolutionResult> { // Validate tradeoffs format this.validateTradeoffs(input.solution.tradeoffs); // Validate effortEstimate format validateEffortEstimate(input.solution.evaluation?.effortEstimate); // Validate tags format validateTags(input.solution.tags || []); const solutionId = uuidv4(); const now = new Date().toISOString(); const solution: Solution = { id: solutionId, type: 'solution', createdAt: now, updatedAt: now, version: 1, metadata: { createdBy: 'claude-code', tags: input.solution.tags || [], annotations: [], }, title: input.solution.title, description: input.solution.description, approach: input.solution.approach, implementationNotes: input.solution.implementationNotes, tradeoffs: input.solution.tradeoffs, addressing: input.solution.addressing, evaluation: input.solution.evaluation, status: 'proposed', }; const solutions = await this.storage.loadEntities<Solution>(input.planId, 'solutions'); solutions.push(solution); await this.storage.saveEntities(input.planId, 'solutions', solutions); await this.planService.updateStatistics(input.planId); return { solutionId, solution }; } async compareSolutions(input: CompareSolutionsInput): Promise<CompareSolutionsResult> { const allSolutions = await this.storage.loadEntities<Solution>(input.planId, 'solutions'); const solutions = allSolutions.filter((s) => input.solutionIds.includes(s.id)); // Collect all aspects const aspectsSet = new Set<string>(); solutions.forEach((s) => s.tradeoffs.forEach((t) => aspectsSet.add(t.aspect))); let aspects = Array.from(aspectsSet); if (input.aspects && input.aspects.length > 0) { aspects = aspects.filter((a) => input.aspects!.includes(a)); } // Build comparison matrix const matrix = aspects.map((aspect) => { const solutionData = solutions.map((s) => { const tradeoff = s.tradeoffs.find((t) => t.aspect === aspect); return { solutionId: s.id, solutionTitle: s.title, pros: tradeoff?.pros || [], cons: tradeoff?.cons || [], score: tradeoff?.score, }; }); // Find winner for this aspect const withScores = solutionData.filter((d) => d.score !== undefined); let winner: string | undefined; if (withScores.length > 0) { const best = withScores.reduce((a, b) => (a.score || 0) > (b.score || 0) ? a : b ); winner = best.solutionId; } return { aspect, solutions: solutionData, winner }; }); // Calculate overall best const scores: Record<string, number[]> = {}; solutions.forEach((s) => { scores[s.id] = s.tradeoffs.filter((t) => t.score !== undefined).map((t) => t.score!); }); let bestOverall: string | undefined; let bestAvg = -1; for (const [id, scoreList] of Object.entries(scores)) { if (scoreList.length > 0) { const avg = scoreList.reduce((a, b) => a + b, 0) / scoreList.length; if (avg > bestAvg) { bestAvg = avg; bestOverall = id; } } } return { comparison: { solutions, matrix, summary: { bestOverall, recommendations: bestOverall ? [`${solutions.find((s) => s.id === bestOverall)?.title} has the highest average score`] : [], }, }, }; } async selectSolution(input: SelectSolutionInput): Promise<SelectSolutionResult> { const solutions = await this.storage.loadEntities<Solution>(input.planId, 'solutions'); const index = solutions.findIndex((s) => s.id === input.solutionId); if (index === -1) { throw new Error('Solution not found'); } const solution = solutions[index]; const now = new Date().toISOString(); // Deselect other solutions that address the same requirements const deselected: Solution[] = []; for (const s of solutions) { if ( s.id !== input.solutionId && s.status === 'selected' && s.addressing.some((r) => solution.addressing.includes(r)) ) { s.status = 'rejected'; s.updatedAt = now; s.version += 1; deselected.push(s); } } // Select this solution solution.status = 'selected'; solution.selectionReason = input.reason; solution.updatedAt = now; solution.version += 1; solutions[index] = solution; await this.storage.saveEntities(input.planId, 'solutions', solutions); return { success: true, solution, deselected: deselected.length > 0 ? deselected : undefined, }; } async updateSolution(input: UpdateSolutionInput): Promise<UpdateSolutionResult> { const solutions = await this.storage.loadEntities<Solution>(input.planId, 'solutions'); const index = solutions.findIndex((s) => s.id === input.solutionId); if (index === -1) { throw new Error('Solution not found'); } const solution = solutions[index]; const now = new Date().toISOString(); // Apply updates if (input.updates.title !== undefined) solution.title = input.updates.title; if (input.updates.description !== undefined) solution.description = input.updates.description; if (input.updates.approach !== undefined) solution.approach = input.updates.approach; if (input.updates.implementationNotes !== undefined) solution.implementationNotes = input.updates.implementationNotes; if (input.updates.tradeoffs !== undefined) { this.validateTradeoffs(input.updates.tradeoffs); solution.tradeoffs = input.updates.tradeoffs; } if (input.updates.addressing !== undefined) solution.addressing = input.updates.addressing; if (input.updates.evaluation !== undefined) { validateEffortEstimate(input.updates.evaluation.effortEstimate); solution.evaluation = input.updates.evaluation; } if (input.updates.tags !== undefined) { validateTags(input.updates.tags); solution.metadata.tags = input.updates.tags; } solution.updatedAt = now; solution.version += 1; solutions[index] = solution; await this.storage.saveEntities(input.planId, 'solutions', solutions); return { success: true, solution }; } async listSolutions(input: ListSolutionsInput): Promise<ListSolutionsResult> { let solutions = await this.storage.loadEntities<Solution>(input.planId, 'solutions'); if (input.filters) { if (input.filters.status) { solutions = solutions.filter((s) => s.status === input.filters!.status); } if (input.filters.addressingRequirement) { solutions = solutions.filter((s) => s.addressing.includes(input.filters!.addressingRequirement!) ); } } const total = solutions.length; const offset = input.offset || 0; const limit = input.limit || 50; const paginated = solutions.slice(offset, offset + limit); return { solutions: paginated, total, hasMore: offset + limit < total, }; } async deleteSolution(input: DeleteSolutionInput): Promise<DeleteSolutionResult> { const solutions = await this.storage.loadEntities<Solution>(input.planId, 'solutions'); const index = solutions.findIndex((s) => s.id === input.solutionId); if (index === -1) { throw new Error('Solution not found'); } solutions.splice(index, 1); await this.storage.saveEntities(input.planId, 'solutions', solutions); await this.planService.updateStatistics(input.planId); return { success: true, message: 'Solution deleted' }; } private validateTradeoffs(tradeoffs: unknown[]): void { if (!Array.isArray(tradeoffs)) { return; // Empty or not array is OK } for (let i = 0; i < tradeoffs.length; i++) { const t = tradeoffs[i] as Record<string, unknown>; // Check for invalid { pro, con } format if ('pro' in t || 'con' in t) { throw new Error( `Invalid tradeoff format at index ${i}: found { pro, con } format. ` + `Expected { aspect: string, pros: string[], cons: string[] }` ); } // Validate required fields if (typeof t.aspect !== 'string' || !t.aspect) { throw new Error( `Invalid tradeoff at index ${i}: 'aspect' must be a non-empty string` ); } if (!Array.isArray(t.pros)) { throw new Error( `Invalid tradeoff at index ${i}: 'pros' must be an array of strings` ); } if (!Array.isArray(t.cons)) { throw new Error( `Invalid tradeoff at index ${i}: 'cons' must be an array of strings` ); } } } } export default SolutionService;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cppmyjob/cpp-mcp-planner'

If you have feedback or need assistance with the MCP directory API, please join our Discord server