Skip to main content
Glama
ruleService.ts7.64 kB
/** * Rule service - business logic for querying and applying rules */ import { Rule, RuleViolation, RuleSeverity, RuleArea } from '../model/rule.js'; import { Project } from '../model/project.js'; import { RulesetConfig } from '../model/config.js'; import { scanProjectRules } from '../scanner/rulesScanner.js'; export class RuleService { private ruleCache: Map<string, Rule[]> = new Map(); private config: RulesetConfig; constructor(config: RulesetConfig) { this.config = config; } /** * Load rules for a specific project */ async loadProjectRules(projectId: string): Promise<Rule[]> { // Check cache first if (this.ruleCache.has(projectId)) { return this.ruleCache.get(projectId)!; } const project = this.config.projects.find(p => p.id === projectId); if (!project) { throw new Error(`Project not found: ${projectId}`); } const { rules, filesScanned, errors } = await scanProjectRules(project); console.log(`Loaded ${rules.length} rules from ${filesScanned} files for project "${projectId}"`); if (errors.length > 0) { console.warn(`Encountered ${errors.length} errors while scanning rules`); } this.ruleCache.set(projectId, rules); return rules; } /** * Get rules with optional filtering */ async getRules(params: { projectId: string; area?: RuleArea | RuleArea[]; tags?: string[]; severity?: RuleSeverity | RuleSeverity[]; limit?: number; }): Promise<Rule[]> { const allRules = await this.loadProjectRules(params.projectId); let filtered = allRules; // Filter by area if (params.area) { const areas = Array.isArray(params.area) ? params.area : [params.area]; filtered = filtered.filter(r => areas.includes(r.area)); } // Filter by tags if (params.tags && params.tags.length > 0) { filtered = filtered.filter(r => params.tags!.some(tag => r.tags.includes(tag)) ); } // Filter by severity if (params.severity) { const severities = Array.isArray(params.severity) ? params.severity : [params.severity]; filtered = filtered.filter(r => severities.includes(r.severity)); } // Apply limit if (params.limit && params.limit > 0) { filtered = filtered.slice(0, params.limit); } return filtered; } /** * Validate a snippet against project rules */ async validateSnippet(params: { projectId: string; area?: RuleArea; snippet: string; path?: string; }): Promise<RuleViolation[]> { const rules = await this.getRules({ projectId: params.projectId, area: params.area, }); const violations: RuleViolation[] = []; for (const rule of rules) { // Check if rule applies to this file path if (params.path && rule.appliesTo) { const matches = rule.appliesTo.some(pattern => this.matchesPattern(params.path!, pattern) ); if (!matches) continue; } // Simple pattern matching for MVP if (rule.pattern) { try { const regex = new RegExp(rule.pattern, 'gim'); if (regex.test(params.snippet)) { violations.push({ ruleId: rule.id, message: `Violation: ${rule.title}. ${rule.description}`, severity: rule.severity, }); } } catch (error) { console.warn(`Invalid regex pattern in rule ${rule.id}:`, error); } } // Simple SQL casing check for demo if (rule.area === 'sql' && rule.id.includes('format') && rule.id.includes('001')) { const hasLowerKeywords = /\b(select|from|where|join|and|or|order|group|having)\b/.test(params.snippet); if (hasLowerKeywords) { const suggested = params.snippet.replace( /\b(select|from|where|join|and|or|order|group|having|by|on|as|in|not|null|is|like|between|case|when|then|else|end|insert|update|delete|create|alter|drop|table|index|view|procedure|function|trigger|database|schema)\b/gi, match => match.toUpperCase() ); violations.push({ ruleId: rule.id, message: `SQL keywords must be UPPER-CASE. ${rule.description}`, severity: rule.severity, suggestedSnippet: suggested, }); } } } return violations; } /** * Generate a task-oriented summary of relevant rules */ async summarizeRulesForTask(params: { projectId: string; taskDescription: string; areasHint?: RuleArea[]; }): Promise<{ summary: string; rulesReferenced: string[] }> { // Extract potential areas from task description const areas = params.areasHint || this.inferAreasFromTask(params.taskDescription); const rules = await this.getRules({ projectId: params.projectId, area: areas.length > 0 ? areas : undefined, }); if (rules.length === 0) { return { summary: `No specific rules found for project "${params.projectId}" in areas: ${areas.join(', ')}`, rulesReferenced: [], }; } // Group rules by area const rulesByArea = new Map<string, Rule[]>(); for (const rule of rules) { if (!rulesByArea.has(rule.area)) { rulesByArea.set(rule.area, []); } rulesByArea.get(rule.area)!.push(rule); } // Build summary let summary = `For the "${params.projectId}" project, when working on: "${params.taskDescription}"\n\n`; summary += `Relevant rules:\n\n`; for (const [area, areaRules] of rulesByArea.entries()) { summary += `**${area.toUpperCase()} Rules:**\n`; for (const rule of areaRules.slice(0, 5)) { // Top 5 per area summary += `- [${rule.severity}] ${rule.title}: ${rule.description}\n`; } summary += `\n`; } const rulesReferenced = rules.map(r => r.id); return { summary, rulesReferenced }; } /** * Reload rules for a project (clear cache) */ async reloadRules(projectId: string): Promise<{ status: string; message: string }> { this.ruleCache.delete(projectId); const rules = await this.loadProjectRules(projectId); return { status: 'ok', message: `Reloaded ${rules.length} rules for project "${projectId}"`, }; } /** * Simple pattern matching (supports wildcards) */ private matchesPattern(path: string, pattern: string): boolean { const regexPattern = pattern .replace(/\./g, '\\.') .replace(/\*/g, '.*') .replace(/\?/g, '.'); return new RegExp(`^${regexPattern}$`, 'i').test(path); } /** * Infer areas from task description (simple keyword matching) */ private inferAreasFromTask(task: string): RuleArea[] { const lower = task.toLowerCase(); const areas: RuleArea[] = []; if (lower.includes('sql') || lower.includes('stored procedure') || lower.includes('query')) { areas.push('sql'); } if (lower.includes('api') || lower.includes('endpoint') || lower.includes('rest')) { areas.push('api'); } if (lower.includes('security') || lower.includes('auth') || lower.includes('permission')) { areas.push('security'); } if (lower.includes('test') || lower.includes('testing')) { areas.push('testing'); } if (lower.includes('c#') || lower.includes('csharp') || lower.includes('.net')) { areas.push('csharp'); } if (lower.includes('typescript') || lower.includes('ts')) { areas.push('typescript'); } if (lower.includes('javascript') || lower.includes('js')) { areas.push('javascript'); } return areas; } }

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/n8daniels/RulesetMCP'

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