From df3053c665d7cf0e1006212e0b74f0fe917f2633 Mon Sep 17 00:00:00 2001 From: Vapi Tasker Date: Tue, 27 Jan 2026 07:08:33 +0000 Subject: [PATCH] fix: add atlas parity - regex patterns, rules guidance, dotenv loading - Add support for raw regex patterns in coverage scope config (prefix with "regex:") - Add vapi-nextjs preset with atlas-matching coverage scope patterns - Add detailed Vapi-specific rules guidance (API routes, React components, hooks, tests) - Add dotenv loading for .env.local in guardian initialization - Export RulesGuidance interface and DEFAULT_VAPI_RULES_GUIDANCE constant Fixes VAP-11522 Co-Authored-By: Claude --- src/claude/prompt.ts | 74 ++++++++++++++++--- src/config.ts | 170 ++++++++++++++++++++++++++++++++++++++----- src/guardian.ts | 4 + 3 files changed, 216 insertions(+), 32 deletions(-) diff --git a/src/claude/prompt.ts b/src/claude/prompt.ts index 4a07737..46bd9c0 100644 --- a/src/claude/prompt.ts +++ b/src/claude/prompt.ts @@ -3,7 +3,7 @@ */ import type { DiffAnalysis } from '../diff/analyzer.js' -import type { BulletproofConfig } from '../config.js' +import type { BulletproofConfig, RulesGuidance } from '../config.js' /** * Generate the system prompt for Claude @@ -27,6 +27,66 @@ Do NOT read files from other users' directories.` return base } +/** + * Generate the rules guidance section based on config + */ +function generateRulesGuidanceSection( + guidance: RulesGuidance | undefined, + rulesFile: string +): string { + if (!guidance) { + return `Run \`git diff HEAD~1 --name-only\` to see changed files, then review them against ${rulesFile}.` + } + + const sections: string[] = [] + + sections.push( + `Run \`git diff HEAD~1 --name-only\` to see changed files, then review them against ${rulesFile}:` + ) + + if (guidance.apiRoutes && guidance.apiRoutes.length > 0) { + sections.push('') + sections.push('**For API routes, verify:**') + guidance.apiRoutes.forEach((rule) => { + sections.push(`- ${rule}`) + }) + } + + if (guidance.reactComponents && guidance.reactComponents.length > 0) { + sections.push('') + sections.push('**For React components, verify:**') + guidance.reactComponents.forEach((rule) => { + sections.push(`- ${rule}`) + }) + } + + if (guidance.hooks && guidance.hooks.length > 0) { + sections.push('') + sections.push('**For hooks, verify:**') + guidance.hooks.forEach((rule) => { + sections.push(`- ${rule}`) + }) + } + + if (guidance.testFiles && guidance.testFiles.length > 0) { + sections.push('') + sections.push('**For test files, verify:**') + guidance.testFiles.forEach((rule) => { + sections.push(`- ${rule}`) + }) + } + + if (guidance.allFiles && guidance.allFiles.length > 0) { + sections.push('') + sections.push('**For ALL files, verify:**') + guidance.allFiles.forEach((rule) => { + sections.push(`- ${rule}`) + }) + } + + return sections.join('\n') +} + /** * Generate the merge conflict resolution prompt */ @@ -151,17 +211,7 @@ ${checksToRun.join('\n')} ${coverageInstructions} ## RULES COMPLIANCE CHECK (Step 1): -Run \`git diff HEAD~1 --name-only\` to see changed files, then review them against ${config.rulesFile}: - -**For API routes, verify:** -- Uses proper authentication -- Returns proper HTTP status codes (200, 201, 400, 401, 403, 404, 500) -- Has corresponding tests - -**For ALL files, verify:** -- No hardcoded values (use constants) -- TypeScript types are explicit (no \`any\`) -- Follows existing code patterns +${generateRulesGuidanceSection(config.rulesGuidance, config.rulesFile)} Report after compliance check: ✓ RULES COMPLIANCE PASSED or ✗ RULES COMPLIANCE: [list violations] diff --git a/src/config.ts b/src/config.ts index 9c65d41..a9b6989 100644 --- a/src/config.ts +++ b/src/config.ts @@ -47,14 +47,74 @@ export interface CommandsConfig { testCoverageRelated: string } +/** + * Pattern that can be either a glob string or a regex pattern string + * Regex patterns should be prefixed with "regex:" (e.g., "regex:^src/app/.*\\.(ts|tsx)$") + */ +export type CoveragePattern = string + /** * Coverage scope configuration - patterns for files that require coverage */ export interface CoverageScopeConfig { - /** Glob patterns for files that should be included in coverage */ - include: string[] - /** Glob patterns for files that should be excluded from coverage */ - exclude: string[] + /** + * Patterns for files that should be included in coverage. + * Can be glob patterns (e.g., "src/**\/*.ts") or regex patterns prefixed with "regex:" + * (e.g., "regex:^src/app/.*\\.(ts|tsx)$") + */ + include: CoveragePattern[] + /** + * Patterns for files that should be excluded from coverage. + * Can be glob patterns or regex patterns prefixed with "regex:" + */ + exclude: CoveragePattern[] +} + +/** + * Preset configurations for common project types + */ +export type CoveragePreset = 'default' | 'vapi-nextjs' + +/** + * Vapi Next.js preset coverage scope patterns (matching atlas implementation) + */ +export const VAPI_NEXTJS_COVERAGE_SCOPE: CoverageScopeConfig = { + include: [ + 'regex:^src/app/.*\\.(ts|tsx)$', + 'regex:^src/components/.*\\.(ts|tsx)$', + 'regex:^src/hooks/.*\\.(ts|tsx)$', + 'regex:^src/lib/.*\\.ts$', + ], + exclude: [ + 'regex:^src/test/', + 'regex:\\.test\\.(ts|tsx)$', + 'regex:\\.spec\\.(ts|tsx)$', + 'regex:/types/', + 'regex:\\.d\\.ts$', + 'regex:/layout\\.tsx$', + 'regex:/page\\.tsx$', + 'regex:^src/components/ui/[^/]+\\.tsx$', + 'regex:^src/lib/api/generate-mcp-tools\\.ts$', + 'regex:/index\\.ts$', + 'regex:^src/app/api/auth/\\[\\.\\.\\.nextauth\\]/route\\.ts$', + ], +} + +/** + * Detailed rules guidance for the Claude agent + * These provide specific conventions for different file types + */ +export interface RulesGuidance { + /** Rules for API routes */ + apiRoutes?: string[] + /** Rules for React components */ + reactComponents?: string[] + /** Rules for hooks */ + hooks?: string[] + /** Rules for test files */ + testFiles?: string[] + /** Rules for all files */ + allFiles?: string[] } /** @@ -73,6 +133,9 @@ export interface BulletproofConfig { /** Coverage scope patterns */ coverageScope: CoverageScopeConfig + /** Use a preset for coverage scope (overrides coverageScope if set) */ + coveragePreset?: CoveragePreset + /** Default checks to run */ checks: ChecksConfig @@ -87,6 +150,46 @@ export interface BulletproofConfig { /** Additional prompt instructions */ additionalInstructions?: string + + /** Detailed rules guidance for different file types */ + rulesGuidance?: RulesGuidance +} + +/** + * Default Vapi-specific rules guidance (matching atlas implementation) + */ +export const DEFAULT_VAPI_RULES_GUIDANCE: RulesGuidance = { + apiRoutes: [ + 'Uses `authenticateRequestWithPermissions` for auth', + 'Emits SSE events for all create/update/delete operations (`emitEvent`)', + 'Returns proper HTTP status codes (200, 201, 400, 401, 403, 404, 500)', + 'Has corresponding tests in src/test/api/', + ], + reactComponents: [ + 'Uses existing UI components from src/components/ui/ (Button, Card, Badge, etc.)', + 'Uses `cn()` for className merging', + 'Uses `@/` path aliases (never relative imports like ../../)', + 'Loading states use skeleton components', + 'Detail/nested pages use `DetailPageHeader` with back button', + 'Mutations use `useOptimisticMutation` hook', + 'Links prefetch on hover', + ], + hooks: [ + 'Custom hooks follow use* naming convention', + 'Uses proper TypeScript types (no `any`)', + ], + testFiles: [ + 'Located in src/test/ mirroring source path', + 'Tests auth (401), validation (400), happy path, and error cases', + 'Uses vi.mock() before imports', + 'Has beforeEach with vi.resetAllMocks()', + ], + allFiles: [ + 'No hardcoded values (use constants)', + 'Console logs use context prefixes like [API], [SSE], [Auth]', + 'TypeScript types are explicit (no `any`)', + 'Import order follows: React > Next > External > Internal hooks > Components > Utils > Types', + ], } /** @@ -130,6 +233,7 @@ export const DEFAULT_CONFIG: BulletproofConfig = { testCoverageRelated: 'npm run test:coverage:related', }, rulesFile: '.cursorrules', + rulesGuidance: DEFAULT_VAPI_RULES_GUIDANCE, } /** @@ -174,6 +278,25 @@ export function loadConfigFile(cwd: string): Partial | null { return null } +/** + * Get coverage scope based on preset or explicit config + */ +function getCoverageScope( + partial: Partial | null, + defaults: BulletproofConfig +): CoverageScopeConfig { + // If preset is specified, use it + if (partial?.coveragePreset === 'vapi-nextjs') { + return VAPI_NEXTJS_COVERAGE_SCOPE + } + + // Otherwise use explicit config or defaults + return { + include: partial?.coverageScope?.include ?? defaults.coverageScope.include, + exclude: partial?.coverageScope?.exclude ?? defaults.coverageScope.exclude, + } +} + /** * Merge partial config with defaults */ @@ -192,10 +315,8 @@ export function mergeConfig( ...defaults.coverageThresholds, ...partial.coverageThresholds, }, - coverageScope: { - include: partial.coverageScope?.include ?? defaults.coverageScope.include, - exclude: partial.coverageScope?.exclude ?? defaults.coverageScope.exclude, - }, + coverageScope: getCoverageScope(partial, defaults), + coveragePreset: partial.coveragePreset, checks: { ...defaults.checks, ...partial.checks, @@ -207,6 +328,7 @@ export function mergeConfig( rulesFile: partial.rulesFile ?? defaults.rulesFile, systemPrompt: partial.systemPrompt, additionalInstructions: partial.additionalInstructions, + rulesGuidance: partial.rulesGuidance ?? defaults.rulesGuidance, } } @@ -218,6 +340,24 @@ export function loadConfig(cwd: string = process.cwd()): BulletproofConfig { return mergeConfig(fileConfig) } +/** + * Convert a coverage pattern (glob or regex) to a RegExp + */ +function patternToRegex(pattern: CoveragePattern): RegExp { + // Check if it's a regex pattern (prefixed with "regex:") + if (pattern.startsWith('regex:')) { + return new RegExp(pattern.slice(6)) + } + + // Otherwise, convert glob to regex + const escaped = pattern + .replace(/[.+^${}()|[\]\\]/g, '\\$&') + .replace(/\*\*/g, '<<>>') + .replace(/\*/g, '[^/]*') + .replace(/<<>>/g, '.*') + return new RegExp(`^${escaped}$`) +} + /** * Check if a file matches coverage scope patterns */ @@ -225,19 +365,9 @@ export function isInCoverageScope( file: string, scope: CoverageScopeConfig ): boolean { - // Convert glob patterns to regex for matching - const toRegex = (pattern: string): RegExp => { - const escaped = pattern - .replace(/[.+^${}()|[\]\\]/g, '\\$&') - .replace(/\*\*/g, '<<>>') - .replace(/\*/g, '[^/]*') - .replace(/<<>>/g, '.*') - return new RegExp(`^${escaped}$`) - } - // Check if file matches any include pattern const matchesInclude = scope.include.some((pattern) => - toRegex(pattern).test(file) + patternToRegex(pattern).test(file) ) if (!matchesInclude) { @@ -246,7 +376,7 @@ export function isInCoverageScope( // Check if file matches any exclude pattern const matchesExclude = scope.exclude.some((pattern) => - toRegex(pattern).test(file) + patternToRegex(pattern).test(file) ) return !matchesExclude diff --git a/src/guardian.ts b/src/guardian.ts index bad63aa..c108856 100644 --- a/src/guardian.ts +++ b/src/guardian.ts @@ -4,7 +4,11 @@ * Main orchestrator class that coordinates all checks and Claude agent. */ +import * as dotenv from 'dotenv' import { execSync } from 'child_process' + +// Load environment variables from .env.local (matching atlas behavior) +dotenv.config({ path: '.env.local' }) import type { BulletproofConfig } from './config.js' import { loadConfig } from './config.js' import { analyzeDiff, type DiffAnalysis } from './diff/analyzer.js'