Skip to main content
Glama
query-service.ts36.5 kB
import type { RepositoryFactory, PlanRepository } from '../repositories/interfaces.js'; import type { PlanService } from './plan-service.js'; import type { LinkingService } from './linking-service.js'; import * as fs from 'fs/promises'; import type { Entity, EntityType, Requirement, Solution, Decision, Phase, Artifact, Tag, PlanManifest, } from '../entities/types.js'; // ============================================================================ // Sprint 8: Trace Depth Constants // ============================================================================ /** * Trace depth levels for hierarchical requirement tracing: * - SOLUTIONS (1): requirement → solutions only * - PHASES (2): requirement → solutions → phases * - ARTIFACTS (3): requirement → solutions → phases → artifacts (full trace) */ const DEFAULT_SEARCH_RESULTS_LIMIT = 50; const TRACE_DEPTH = { SOLUTIONS: 1, PHASES: 2, ARTIFACTS: 3, MIN: 1, MAX: 3, DEFAULT: 3, } as const; // Input types export interface SearchEntitiesInput { planId: string; query: string; entityTypes?: EntityType[]; filters?: { tags?: Tag[]; status?: string; }; limit?: number; offset?: number; } export interface TraceRequirementInput { planId: string; requirementId: string; depth?: number; includePhases?: boolean; includeArtifacts?: boolean; limit?: number; fields?: string[]; solutionFields?: string[]; phaseFields?: string[]; excludeMetadata?: boolean; } export interface ValidatePlanInput { planId: string; checks?: string[]; validationLevel?: 'basic' | 'strict'; } export interface ExportPlanInput { planId: string; format: 'markdown' | 'json'; sections?: string[]; includeVersionHistory?: boolean; } // Output types export interface SearchResult { entityId: string; entityType: EntityType; entity: Entity; relevanceScore: number; matchedFields: string[]; } export interface SearchEntitiesResult { results: SearchResult[]; total: number; hasMore: boolean; } export interface TraceRequirementResult { requirement: Requirement; trace: { proposedSolutions: Solution[]; selectedSolution: Solution | null; alternativeSolutions: Solution[]; decisions: Decision[]; implementingPhases?: Phase[]; artifacts?: Artifact[]; completionStatus: { isAddressed: boolean; isImplemented: boolean; completionPercentage: number; }; }; } export interface ValidationIssue { severity: 'error' | 'warning' | 'info'; type: string; entityId?: string; entityType?: EntityType; message: string; suggestion?: string; filePath?: string; } export interface ValidatePlanResult { isValid: boolean; issues: ValidationIssue[]; summary: { totalIssues: number; errors: number; warnings: number; infos: number; }; checksPerformed: string[]; } export interface ExportPlanResult { format: string; content: string; filePath?: string; sizeBytes: number; } export class QueryService { private readonly planRepo: PlanRepository; constructor( private readonly repositoryFactory: RepositoryFactory, private readonly planService: PlanService, private readonly linkingService: LinkingService ) { this.planRepo = repositoryFactory.createPlanRepository(); } public async searchEntities(input: SearchEntitiesInput): Promise<SearchEntitiesResult> { const planData = await this.planService.getPlan({ planId: input.planId, includeEntities: true, }); const entities = planData.plan.entities; if (!entities) { throw new Error('Plan entities not loaded'); } const allEntities: Entity[] = [ ...entities.requirements, ...entities.solutions, ...entities.decisions, ...entities.phases, ...entities.artifacts, ]; // Convert SQL LIKE pattern to regex const searchRegex = this.convertSqlLikeToRegex(input.query); const results: SearchResult[] = []; for (const entity of allEntities) { // Filter by entity type if (input.entityTypes && !input.entityTypes.includes(entity.type)) { continue; } // Search in entity fields const matchedFields: string[] = []; let score = 0; const searchableText = this.getSearchableText(entity); for (const [field, text] of Object.entries(searchableText)) { if (searchRegex.test(text)) { matchedFields.push(field); score += 1; } } if (matchedFields.length > 0) { results.push({ entityId: entity.id, entityType: entity.type, entity, relevanceScore: score / Object.keys(searchableText).length, matchedFields, }); } } // Sort by relevance results.sort((a, b) => b.relevanceScore - a.relevanceScore); // Pagination const total = results.length; const offset = input.offset ?? 0; const limit = input.limit ?? DEFAULT_SEARCH_RESULTS_LIMIT; const paginated = results.slice(offset, offset + limit); return { results: paginated, total, hasMore: offset + limit < total, }; } public async traceRequirement(input: TraceRequirementInput): Promise<TraceRequirementResult> { // ======================================================================== // Sprint 8: Parameter Validation // ======================================================================== // Validate depth parameter if (input.depth !== undefined) { if (typeof input.depth !== 'number') { throw new Error('depth must be a number'); } if (!Number.isInteger(input.depth)) { throw new Error('depth must be an integer'); } if (input.depth < TRACE_DEPTH.MIN || input.depth > TRACE_DEPTH.MAX) { throw new Error(`depth must be between ${String(TRACE_DEPTH.MIN)} and ${String(TRACE_DEPTH.MAX)}`); } } // Validate limit parameter if (input.limit !== undefined) { if (typeof input.limit !== 'number') { throw new Error('limit must be a number'); } if (!Number.isInteger(input.limit)) { throw new Error('limit must be an integer'); } if (input.limit <= 0) { throw new Error('limit must be greater than 0'); } } // Validate includePhases parameter if (input.includePhases !== undefined && typeof input.includePhases !== 'boolean') { throw new Error('includePhases must be a boolean'); } // Validate includeArtifacts parameter (null is allowed and treated as default true) // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (input.includeArtifacts !== undefined && input.includeArtifacts !== null && typeof input.includeArtifacts !== 'boolean') { throw new Error('includeArtifacts must be a boolean'); } // ======================================================================== // Load Plan Data // ======================================================================== const planData = await this.planService.getPlan({ planId: input.planId, includeEntities: true, }); const entities = planData.plan.entities; const links = planData.plan.links; if (!entities || !links) { throw new Error('Plan entities and links not loaded'); } const requirement = entities.requirements.find((r) => r.id === input.requirementId); if (!requirement) { throw new Error('Requirement not found'); } // ======================================================================== // Sprint 8: Depth and Include Flags (default behavior) // ======================================================================== const depth = input.depth ?? TRACE_DEPTH.DEFAULT; const includePhases = input.includePhases ?? true; const includeArtifacts = input.includeArtifacts ?? true; const { limit, excludeMetadata } = input; // ======================================================================== // Level 1: Find Solutions (always included at any depth) // ======================================================================== const implementsLinks = links.filter( (l) => l.targetId === input.requirementId && l.relationType === 'implements' ); const solutionIds = new Set(implementsLinks.map((l) => l.sourceId)); const rawSolutions = entities.solutions.filter((s) => solutionIds.has(s.id)); // IMPORTANT: Find selectedSolution BEFORE applying limit // selectedSolution is a semantic singleton and must always be found // from the full set of solutions, regardless of pagination const rawSelected = rawSolutions.find((s) => s.status === 'selected'); const rawAlternatives = rawSolutions.filter((s) => s.status !== 'selected'); // Apply filters (limit, fields, excludeMetadata) only to alternatives const filteredAlternatives = this.applyEntityFilters(rawAlternatives, { limit, fields: input.solutionFields ?? input.fields, excludeMetadata, }); // Apply fields/metadata filters to selectedSolution (but NOT limit) let selectedSolution: Solution | null = null; if (rawSelected) { const filtered = this.applyEntityFilters([rawSelected], { fields: input.solutionFields ?? input.fields, excludeMetadata, }); selectedSolution = filtered[0] ?? null; } const alternativeSolutions = filteredAlternatives; const allSolutions = selectedSolution ? [selectedSolution, ...alternativeSolutions] : alternativeSolutions; // ======================================================================== // Level 2: Find Phases (depth >= PHASES and includePhases) // ======================================================================== let implementingPhases: Phase[] = []; let allPhaseIds = new Set<string>(); // For artifact discovery (before limit) let phasesForCompletion: Phase[] = []; // For completion calculation (unfiltered) // BUGFIX: Compute allPhaseIds if depth >= PHASES, regardless of includePhases flag // This is needed for artifact discovery (Level 3), which depends on phase relationships // even when phases are excluded from the output if (depth >= TRACE_DEPTH.PHASES) { const addressesLinks = links.filter( (l) => l.targetId === input.requirementId && l.relationType === 'addresses' ); allPhaseIds = new Set(addressesLinks.map((l) => l.sourceId)); // BUGFIX: Always load phases for completion calculation, regardless of includePhases // Completion status should reflect actual implementation state, independent of output filtering const rawPhases = entities.phases.filter((p) => allPhaseIds.has(p.id)); phasesForCompletion = rawPhases; // Only populate implementingPhases if they should be included in the result if (includePhases) { // Apply filters (limit, fields, excludeMetadata) implementingPhases = this.applyEntityFilters(rawPhases, { limit, fields: input.phaseFields ?? input.fields, excludeMetadata, }); } } // ======================================================================== // Level 3: Find Artifacts (depth >= ARTIFACTS and includeArtifacts) // ======================================================================== let artifacts: Artifact[] = []; if (depth >= TRACE_DEPTH.ARTIFACTS && includeArtifacts) { // IMPORTANT: Use ALL phases (allPhaseIds) for artifact discovery, not limited implementingPhases // This follows the principle "limit applies per entity type independently" // Artifact discovery should consider all phases addressing the requirement, // then artifacts are limited separately // Find artifacts related to this requirement or ANY of its phases (before limit) const rawArtifacts = entities.artifacts.filter((a) => { // Artifact directly related to requirement if (a.relatedRequirementIds?.includes(input.requirementId) === true) { return true; } // Artifact related to ANY phase addressing the requirement (O(1) lookup) if (a.relatedPhaseId !== undefined && a.relatedPhaseId !== '' && allPhaseIds.has(a.relatedPhaseId)) { return true; } return false; }); // Apply filters (limit, fields, excludeMetadata) artifacts = this.applyEntityFilters(rawArtifacts, { limit, fields: input.fields, excludeMetadata, }); } // ======================================================================== // Find Related Decisions (always included) // ======================================================================== // Create Set of decision IDs for O(1) lookup const decisionIdSet = new Set(entities.decisions.map((d) => d.id)); const decisionLinks = links.filter( (l) => (l.sourceId === input.requirementId || l.targetId === input.requirementId) && (decisionIdSet.has(l.sourceId) || decisionIdSet.has(l.targetId)) ); const relatedDecisionIds = new Set( decisionLinks.flatMap((l) => [l.sourceId, l.targetId]) ); const decisions = entities.decisions.filter((d) => relatedDecisionIds.has(d.id)); // ======================================================================== // Calculate Completion Status // ======================================================================== const isAddressed = allSolutions.length > 0; // BUGFIX: Use phasesForCompletion (unfiltered) for accurate completion status, // not implementingPhases which may be empty when includePhases=false const isImplemented = phasesForCompletion.some((p) => p.status === 'completed'); const completionPercentage = this.calculateAverageProgress(phasesForCompletion); // ======================================================================== // Build Result (conditionally include fields based on depth and flags) // ======================================================================== const trace: TraceRequirementResult['trace'] = { // BUGFIX: proposedSolutions should include ALL solutions (selected + alternatives) // for backward compatibility and correct API semantics proposedSolutions: allSolutions, selectedSolution, alternativeSolutions, decisions, completionStatus: { isAddressed, isImplemented, completionPercentage, }, }; // Include implementingPhases if depth >= PHASES AND includePhases is true if (depth >= TRACE_DEPTH.PHASES && includePhases) { trace.implementingPhases = implementingPhases; } // Include artifacts if depth >= ARTIFACTS AND includeArtifacts is true if (depth >= TRACE_DEPTH.ARTIFACTS && includeArtifacts) { trace.artifacts = artifacts; } return { requirement, trace }; } /** * Calculate average progress percentage from phases * @param phases - Array of phases with progress values * @returns Average progress percentage (0-100), or 0 if no phases */ private calculateAverageProgress(phases: Phase[]): number { if (phases.length === 0) return 0; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const totalProgress = phases.reduce((sum, p) => sum + (p.progress ?? 0), 0); return Math.round(totalProgress / phases.length); } public async validatePlan(input: ValidatePlanInput): Promise<ValidatePlanResult> { const planData = await this.planService.getPlan({ planId: input.planId, includeEntities: true, }); const entities = planData.plan.entities; const links = planData.plan.links; if (!entities || !links) { throw new Error('Plan entities and links not loaded'); } const issues: ValidationIssue[] = []; const checksPerformed: string[] = []; // Check: Uncovered requirements checksPerformed.push('uncovered_requirements'); for (const req of entities.requirements) { const hasImplementation = links.some( (l) => l.targetId === req.id && l.relationType === 'implements' ); if (!hasImplementation) { issues.push({ severity: 'error', type: 'uncovered_requirement', entityId: req.id, entityType: 'requirement', message: `Requirement '${req.title}' has no proposed solutions`, suggestion: 'Use propose_solution to address this requirement', }); } } // Check: Orphan solutions checksPerformed.push('orphan_solutions'); for (const sol of entities.solutions) { const hasLinks = links.some( (l) => l.sourceId === sol.id && l.relationType === 'implements' ); if (!hasLinks && sol.addressing.length === 0) { issues.push({ severity: 'warning', type: 'orphan_solution', entityId: sol.id, entityType: 'solution', message: `Solution '${sol.title}' is not linked to any requirement`, suggestion: 'Link to requirement or delete if not needed', }); } } // Check: Missing decisions checksPerformed.push('missing_decisions'); const reqsWithMultipleSolutions = new Map<string, Solution[]>(); for (const sol of entities.solutions) { for (const reqId of sol.addressing) { if (!reqsWithMultipleSolutions.has(reqId)) { reqsWithMultipleSolutions.set(reqId, []); } const solutions = reqsWithMultipleSolutions.get(reqId); if (solutions) { solutions.push(sol); } } } for (const [reqId, solutions] of reqsWithMultipleSolutions) { if (solutions.length > 1) { const hasSelected = solutions.some((s) => s.status === 'selected'); if (!hasSelected) { issues.push({ severity: 'info', type: 'no_decision_recorded', entityId: reqId, entityType: 'requirement', message: `Multiple solutions for requirement but none selected`, suggestion: 'Use select_solution to choose one', }); } } } // Check: Broken links checksPerformed.push('broken_links'); const allIds = new Set([ ...entities.requirements.map((r) => r.id), ...entities.solutions.map((s) => s.id), ...entities.decisions.map((d) => d.id), ...entities.phases.map((p) => p.id), ...entities.artifacts.map((a) => a.id), ]); for (const link of links) { if (!allIds.has(link.sourceId)) { issues.push({ severity: 'error', type: 'broken_link', message: `Link references non-existent source: ${link.sourceId}`, }); } if (!allIds.has(link.targetId)) { issues.push({ severity: 'error', type: 'broken_link', message: `Link references non-existent target: ${link.targetId}`, }); } } // Check: Phase status logic checksPerformed.push('phase_status_logic'); const phaseMap = new Map(entities.phases.map((p) => [p.id, p])); for (const phase of entities.phases) { // Check child-parent status consistency if (phase.parentId !== null && phase.parentId !== '') { const parent = phaseMap.get(phase.parentId); if (parent?.status === 'planned') { if (phase.status === 'completed') { issues.push({ severity: 'error', type: 'invalid_phase_status', entityId: phase.id, entityType: 'phase', message: `Phase '${phase.title}' has status 'completed' but parent '${parent.title}' is still 'planned'`, suggestion: 'Update parent phase status before completing children', }); } else if (phase.status === 'in_progress') { issues.push({ severity: 'warning', type: 'invalid_phase_status', entityId: phase.id, entityType: 'phase', message: `Phase '${phase.title}' is in progress but parent '${parent.title}' is still planned`, suggestion: 'Consider updating parent phase status', }); } } } // Check if all children are completed but parent is not if (phase.status !== 'completed') { const children = entities.phases.filter((p) => p.parentId === phase.id); if (children.length > 0 && children.every((c) => c.status === 'completed')) { issues.push({ severity: 'info', type: 'parent_should_complete', entityId: phase.id, entityType: 'phase', message: `Phase '${phase.title}' has all children completed but is not marked as completed`, suggestion: 'Consider completing this phase', }); } } } // Check: File existence checksPerformed.push('file_existence'); for (const artifact of entities.artifacts) { if (artifact.targets && artifact.targets.length > 0) { for (const file of artifact.targets) { // Skip files that are being created if (file.action === 'create') { continue; } // Check if file exists try { await fs.access(file.path); } catch { issues.push({ severity: 'warning', type: 'missing_file', entityId: artifact.id, entityType: 'artifact', filePath: file.path, message: `File '${file.path}' referenced in artifact '${artifact.title}' (action: ${file.action}) does not exist`, suggestion: 'Verify file path or update artifact', }); } } } } // Check: Solution implementation checksPerformed.push('solution_implementation'); const selectedSolutions = entities.solutions.filter((s) => s.status === 'selected'); for (const solution of selectedSolutions) { // Find requirements implemented by this solution const implementedReqIds = solution.addressing; // Find phases that address these requirements const addressesLinks = links.filter( (l) => l.relationType === 'addresses' && implementedReqIds.includes(l.targetId) && entities.phases.some((p) => p.id === l.sourceId) ); if (addressesLinks.length === 0) { issues.push({ severity: 'warning', type: 'unimplemented_solution', entityId: solution.id, entityType: 'solution', message: `Selected solution '${solution.title}' has no implementing phases`, suggestion: 'Create phases that address the requirements or link existing phases', }); } } // Apply validation level filtering (default: basic) let filteredIssues = issues; if (input.validationLevel !== 'strict') { filteredIssues = issues.filter((i) => i.severity === 'error'); } const errors = filteredIssues.filter((i) => i.severity === 'error').length; const warnings = filteredIssues.filter((i) => i.severity === 'warning').length; const infos = filteredIssues.filter((i) => i.severity === 'info').length; return { isValid: errors === 0, issues: filteredIssues, summary: { totalIssues: filteredIssues.length, errors, warnings, infos, }, checksPerformed, }; } public async exportPlan(input: ExportPlanInput): Promise<ExportPlanResult> { const planData = await this.planService.getPlan({ planId: input.planId, includeEntities: true, }); let content: string; if (input.format === 'json') { content = JSON.stringify(planData.plan, null, 2); } else { const entities = planData.plan.entities; if (!entities) { throw new Error('Plan entities not loaded'); } content = this.generateMarkdown(planData.plan.manifest, entities); } const sizeBytes = Buffer.byteLength(content, 'utf-8'); // Save export file const filename = input.format === 'json' ? 'plan-export.json' : 'plan-export.md'; const filePath = await this.planRepo.saveExport(input.planId, filename, content); return { format: input.format, content, filePath, sizeBytes, }; } private getSearchableText(entity: Entity): Record<string, string> { const base: Record<string, string> = { id: entity.id, tags: entity.metadata.tags.map((t) => `${t.key}:${t.value}`).join(' '), }; switch (entity.type) { case 'requirement': { const req = entity as Requirement; return { ...base, title: req.title, description: req.description, rationale: req.rationale ?? '', acceptanceCriteria: req.acceptanceCriteria.join(' '), }; } case 'solution': { const sol = entity as Solution; return { ...base, title: sol.title, // LEGACY SUPPORT: Keep defensive guards for backward compatibility with old data // TODO Sprint 3: Remove after data migration applies defaults to all existing entities // New solutions created with proposeSolution() will always have description='' and approach='' // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition description: sol.description ?? '', // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition approach: sol.approach ?? '', }; } case 'decision': { const dec = entity as Decision; return { ...base, title: dec.title, question: dec.question, decision: dec.decision, // LEGACY SUPPORT: Keep defensive guard for backward compatibility with old data // TODO Sprint 3: Remove after data migration applies defaults to all existing entities // New decisions created with recordDecision() will always have context='' // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition context: dec.context ?? '', }; } case 'phase': { const phase = entity as Phase; return { ...base, title: phase.title, // LEGACY SUPPORT: Keep defensive guards for backward compatibility with old data // TODO Sprint 3: Remove after data migration applies defaults to all existing entities // New phases created with addPhase() will always have description='', objectives=[], deliverables=[] // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition description: phase.description ?? '', // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition objectives: (phase.objectives ?? []).join(' '), // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition deliverables: (phase.deliverables ?? []).join(' '), }; } case 'artifact': { const art = entity as Artifact; return { ...base, title: art.title, description: art.description, content: art.content.sourceCode ?? '', filename: art.content.filename ?? '', }; } default: return base; } } private generateMarkdown( manifest: PlanManifest, entities: { requirements: Requirement[]; solutions: Solution[]; decisions: Decision[]; phases: Phase[]; artifacts: Artifact[]; } ): string { const lines: string[] = []; lines.push(`# ${manifest.name}`); lines.push(''); lines.push(manifest.description); lines.push(''); lines.push(`**Status**: ${manifest.status}`); lines.push(`**Progress**: ${String(manifest.statistics.completionPercentage)}%`); lines.push(''); // Requirements if (entities.requirements.length > 0) { lines.push('## Requirements'); lines.push(''); for (const req of entities.requirements) { lines.push(`### ${req.title}`); lines.push(''); lines.push(req.description); lines.push(''); lines.push(`**Priority**: ${req.priority} | **Category**: ${req.category}`); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const acceptanceCriteria = req.acceptanceCriteria ?? []; if (acceptanceCriteria.length > 0) { lines.push(''); lines.push('**Acceptance Criteria**:'); for (const ac of acceptanceCriteria) { lines.push(`- ${ac}`); } } lines.push(''); } } // Solutions if (entities.solutions.length > 0) { lines.push('## Solutions'); lines.push(''); for (const sol of entities.solutions) { const badge = sol.status === 'selected' ? ' [SELECTED]' : ''; lines.push(`### ${sol.title}${badge}`); lines.push(''); lines.push(sol.description); lines.push(''); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const tradeoffs = sol.tradeoffs ?? []; if (tradeoffs.length > 0) { lines.push('**Trade-offs**:'); for (const t of tradeoffs) { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const pros = (t.pros ?? []).join(', '); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const cons = (t.cons ?? []).join(', '); lines.push(`- **${t.aspect}**: +${pros} / -${cons}`); } } lines.push(''); } } // Phases if (entities.phases.length > 0) { lines.push('## Phases'); lines.push(''); const sortedPhases = [...entities.phases].sort((a, b) => a.path.localeCompare(b.path) ); for (const phase of sortedPhases) { const indent = ' '.repeat(phase.depth); const status = phase.status === 'completed' ? ' [DONE]' : ''; lines.push(`${indent}- **${phase.path}. ${phase.title}**${status}`); if (phase.progress > 0 && phase.progress < 100) { lines.push(`${indent} Progress: ${String(phase.progress)}%`); } } lines.push(''); } // Artifacts if (entities.artifacts.length > 0) { lines.push('## Artifacts'); lines.push(''); for (const artifact of entities.artifacts) { const statusBadge = artifact.status !== 'draft' ? ` [${artifact.status.toUpperCase()}]` : ''; lines.push(`### ${artifact.title}${statusBadge}`); lines.push(''); lines.push(artifact.description); lines.push(''); lines.push(`**Type**: ${artifact.artifactType}`); if (artifact.content.language !== undefined && artifact.content.language !== '') { lines.push(` | **Language**: ${artifact.content.language}`); } if (artifact.content.filename !== undefined && artifact.content.filename !== '') { lines.push(` | **File**: ${artifact.content.filename}`); } lines.push(''); // File targets if (artifact.targets !== undefined && artifact.targets.length > 0) { lines.push('**Files**:'); for (const file of artifact.targets) { const desc = file.description !== undefined && file.description !== '' ? ` - ${file.description}` : ''; lines.push(`- \`${file.path}\` [${file.action}]${desc}`); } lines.push(''); } // Source code (truncated if too long) if (artifact.content.sourceCode !== undefined && artifact.content.sourceCode !== '') { const maxLength = 500; const code = artifact.content.sourceCode.length > maxLength ? artifact.content.sourceCode.substring(0, maxLength) + '\n... (truncated)' : artifact.content.sourceCode; lines.push('```' + (artifact.content.language ?? '')); lines.push(code); lines.push('```'); lines.push(''); } } } return lines.join('\n'); } // ======================================================================== // Sprint 8: Helper Methods for Fields Filtering and Metadata Removal // ======================================================================== /** * Apply limit, fields filtering, and excludeMetadata to an array of entities * Consolidates repeated filtering logic into a single reusable method * @param entities - Array of entities to filter * @param options - Filtering options (limit, fields, excludeMetadata) * @returns Filtered array of entities */ private applyEntityFilters<T extends Entity>( entities: T[], options: { limit?: number; fields?: string[]; excludeMetadata?: boolean; } ): T[] { let result = entities; // Apply limit if (options.limit !== undefined) { result = result.slice(0, options.limit); } // Apply fields filtering if (options.fields !== undefined && options.fields.length > 0) { result = result.map((e) => this.filterFields(e, options.fields)); } // Apply excludeMetadata if (options.excludeMetadata === true) { result = result.map((e) => this.removeMetadataFields(e)); } return result; } /** * Filter entity fields based on provided field list * @param entity - Entity to filter * @param fields - Array of field names to include (empty array or undefined returns all fields) * @returns Filtered entity with only specified fields */ private filterFields<T extends Entity>(entity: T, fields?: string[]): T { if (!fields || fields.length === 0) { return entity; // No filtering, return all fields } const filtered: Partial<T> = {}; for (const field of fields) { if (field in entity) { (filtered as Record<string, unknown>)[field] = (entity as Record<string, unknown>)[field]; } // Silently ignore non-existent fields } return filtered as T; } /** * Remove metadata fields from entity * Removes: metadata, createdAt, updatedAt, version, type * @param entity - Entity to process * @returns Entity without metadata fields */ private removeMetadataFields<T extends Entity>(entity: T): T { const { metadata: excludedMetadata, createdAt: excludedCreatedAt, updatedAt: excludedUpdatedAt, version: excludedVersion, type: excludedType, ...rest } = entity as Record<string, unknown>; void excludedMetadata; void excludedCreatedAt; void excludedUpdatedAt; void excludedVersion; void excludedType; return rest as unknown as T; } /** * Convert SQL LIKE pattern to JavaScript RegExp * Supports SQL LIKE wildcards: * - % (percent) matches any sequence of characters (including empty) * - _ (underscore) matches exactly one character * * All other regex special characters are escaped to be treated as literals. * Pattern matching is case-insensitive and searches anywhere in the text. * * @param pattern - SQL LIKE pattern (e.g., "%service%", "test_", "API%") * @returns RegExp for pattern matching * * @example * convertSqlLikeToRegex("%service%") // matches "User Service", "ServiceLayer" * convertSqlLikeToRegex("test_") // matches "test1", "testA" * convertSqlLikeToRegex("API%") // matches "API Gateway", "API" * convertSqlLikeToRegex("") // matches everything */ private convertSqlLikeToRegex(pattern: string): RegExp { // Empty pattern matches everything if (pattern === '') { return /./i; // Match any character (case-insensitive) } // Escape all regex special characters except % and _ // Special chars to escape: . * + ? ^ $ { } ( ) | [ ] \ let regexPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&'); // Convert SQL LIKE wildcards to regex: // % -> .* (zero or more of any character) // _ -> . (exactly one character) regexPattern = regexPattern.replace(/%/g, '.*').replace(/_/g, '.'); // Create case-insensitive regex // Note: We don't anchor (^$) because SQL LIKE searches within text return new RegExp(regexPattern, 'i'); } }

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