Skip to main content
Glama
validate.ts8.46 kB
import { z } from 'zod'; import * as fs from 'fs/promises'; import * as path from 'path'; import { findSpecsDirectory } from '../speckit/detector.js'; import { resolveWorkspacePath } from './workspace.js'; import { validateSpec, ValidationReport } from '../speckit/validators.js'; /** * Spec Validation & Quality Gates * * Implements GitHub Spec Kit's /analyze command for automated quality checks. */ // Schema for spec_validate tool export const SpecValidateSchema = z.object({ checks: z.object({ completeness: z.boolean().optional().describe('Check if all required sections are present'), acceptanceCriteria: z.boolean().optional().describe('Check if features have testable criteria'), clarifications: z.boolean().optional().describe('Check for unresolved [NEEDS CLARIFICATION] markers'), prematureImplementation: z.boolean().optional().describe('Check for HOW details in WHAT sections') }).optional().describe('Which checks to run (default: all)'), specPath: z.string().optional().describe('Path to specific spec file (auto-detect if not provided)'), workspacePath: z.string().optional().describe('Workspace directory path'), }); // Schema for artifacts_analyze tool export const ArtifactsAnalyzeSchema = z.object({ artifacts: z.array(z.enum(['spec', 'plan', 'tasks'])).optional().describe('Which artifacts to analyze (default: all)'), workspacePath: z.string().optional().describe('Workspace directory path'), }); /** * Find the most recent spec file in the workspace */ async function findSpecFile(workspacePath: string, specPath?: string): Promise<string> { if (specPath) { const absolutePath = path.isAbsolute(specPath) ? specPath : path.join(workspacePath, specPath); // Verify file exists try { await fs.access(absolutePath); return absolutePath; } catch { throw new Error(`Spec file not found: ${specPath}`); } } // Auto-detect: find most recent feature directory const specsDir = await findSpecsDirectory(workspacePath); const entries = await fs.readdir(specsDir); const featureDirs = entries .filter(entry => /^\d{3}-/.test(entry)) .sort() .reverse(); if (featureDirs.length === 0) { throw new Error('No feature directory found. Run specify_start or specify_describe first.'); } const specFilePath = path.join(specsDir, featureDirs[0], 'spec.md'); try { await fs.access(specFilePath); return specFilePath; } catch { throw new Error(`No spec.md found in ${featureDirs[0]}`); } } /** * Validate a specification file */ export async function specValidate(params: z.infer<typeof SpecValidateSchema>) { const { checks = {}, specPath, workspacePath } = params; const resolvedPath = resolveWorkspacePath(workspacePath); try { // Find spec file const specFilePath = await findSpecFile(resolvedPath, specPath); // Load spec content const specContent = await fs.readFile(specFilePath, 'utf-8'); // Run validation const report: ValidationReport = validateSpec(specContent, checks); // Generate summary message const totalIssues = report.errors.length + report.warnings.length; const status = report.passed ? 'PASSED' : 'FAILED'; const result = { success: true, status, specFile: path.basename(path.dirname(specFilePath)), passed: report.passed, summary: { errors: report.errors.length, warnings: report.warnings.length, total: totalIssues }, errors: report.errors, warnings: report.warnings, message: report.passed ? `Specification validation passed! No critical errors found.` : `Specification validation failed with ${report.errors.length} error(s) and ${report.warnings.length} warning(s).`, nextSteps: report.passed ? [ 'Specification looks good!', 'Use plan_create to proceed with technical planning', 'Or use spec_refine to make improvements' ] : [ `Fix ${report.errors.length} error(s) before proceeding`, 'Review warnings for quality improvements', 'Use clarify_add for any ambiguities', 'Use spec_refine to update the specification' ] }; return { content: [ { type: 'text' as const, text: JSON.stringify(result, null, 2) } ] }; } catch (error) { throw new Error(`Failed to validate specification: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Analyze consistency across artifacts (spec, plan, tasks) */ export async function artifactsAnalyze(params: z.infer<typeof ArtifactsAnalyzeSchema>) { const { artifacts = ['spec', 'plan', 'tasks'], workspacePath } = params; const resolvedPath = resolveWorkspacePath(workspacePath); try { // Find feature directory const specsDir = await findSpecsDirectory(resolvedPath); const entries = await fs.readdir(specsDir); const featureDirs = entries .filter(entry => /^\d{3}-/.test(entry)) .sort() .reverse(); if (featureDirs.length === 0) { throw new Error('No feature directory found'); } const featurePath = path.join(specsDir, featureDirs[0]); const issues: ValidationReport['errors'] = []; const loadedArtifacts: { [key: string]: string } = {}; // Load requested artifacts for (const artifact of artifacts) { const filePath = path.join(featurePath, `${artifact}.md`); try { loadedArtifacts[artifact] = await fs.readFile(filePath, 'utf-8'); } catch { issues.push({ rule: 'artifact_missing', message: `${artifact}.md file not found`, suggestion: artifact === 'spec' ? 'Run specify_describe to create spec.md' : artifact === 'plan' ? 'Run plan_create to create plan.md' : 'Run tasks_generate to create tasks.md' }); } } // Analyze spec vs plan consistency if (loadedArtifacts['spec'] && loadedArtifacts['plan']) { const specGoals = extractSectionContent(loadedArtifacts['spec'], 'Goals'); const planPhases = extractSectionContent(loadedArtifacts['plan'], 'Implementation'); if (!specGoals || specGoals.trim().length < 50) { issues.push({ rule: 'spec_incomplete', message: 'Spec has minimal or missing Goals section', suggestion: 'Expand Goals section with clear objectives' }); } if (!planPhases || planPhases.trim().length < 50) { issues.push({ rule: 'plan_incomplete', message: 'Plan has minimal or missing Implementation section', suggestion: 'Use plan_create to generate a detailed implementation plan' }); } } // Analyze plan vs tasks consistency if (loadedArtifacts['plan'] && loadedArtifacts['tasks']) { const hasTaskList = /^[\s]*(\d+\.|[-*])\s+/m.test(loadedArtifacts['tasks']); if (!hasTaskList) { issues.push({ rule: 'tasks_incomplete', message: 'tasks.md exists but has no task list', suggestion: 'Use tasks_generate to create a task list from the plan' }); } } const result = { success: true, featureDirectory: featureDirs[0], artifactsChecked: artifacts, artifactsFound: Object.keys(loadedArtifacts), issues, passed: issues.length === 0, message: issues.length === 0 ? `All artifacts are consistent!` : `Found ${issues.length} issue(s) across artifacts`, nextSteps: issues.length === 0 ? [ 'Artifacts are aligned!', 'Proceed with implementation using tasks.md' ] : issues.map(issue => issue.suggestion || issue.message) }; return { content: [ { type: 'text' as const, text: JSON.stringify(result, null, 2) } ] }; } catch (error) { throw new Error(`Failed to analyze artifacts: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Helper: Extract section content from markdown */ function extractSectionContent(markdown: string, sectionName: string): string | null { const sectionPattern = new RegExp(`##\\s+${sectionName}([\\s\\S]*?)(?=##|$)`, 'i'); const match = markdown.match(sectionPattern); return match ? match[1].trim() : null; }

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/flight505/MCP_DinCoder'

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