Skip to main content
Glama

grade_component

Analyze UI component source code to assess compliance with accessibility and specification rules, providing scores, violations, and actionable improvement suggestions.

Instructions

Grade a component against all rules. Returns a score, grade, violations, and suggestions for fixes.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
codeYesThe component source code to grade
verboseNoInclude detailed violation information

Implementation Reference

  • The main handler function for the 'grade_component' tool. Extracts 'code' and 'verbose' from args, calls gradeComponent(code), formats the result into a detailed markdown report including grade, score, violations with suggestions, good examples (if verbose), and passed rules.
    function handleGradeComponent(args: Record<string, unknown>): ToolResult {
      const code = args.code as string;
      const verbose = args.verbose as boolean;
    
      const result = gradeComponent(code);
    
      let text = `# Component Grade: ${result.grade} (${result.score}/100)
    
    ${result.summary}
    
    `;
    
      if (result.violations.length > 0) {
        text += `## Violations (${result.violations.length})
    
    `;
        for (const v of result.violations) {
          const rule = getRule(v.ruleId);
          text += `### ❌ ${rule?.name || v.ruleId}
    - **Message:** ${v.message}
    ${v.line ? `- **Line:** ${v.line}` : ''}
    ${v.suggestion ? `- **Suggestion:** ${v.suggestion}` : ''}
    
    `;
          if (verbose && rule) {
            text += `**Good example:**
    \`\`\`tsx
    ${rule.example.good}
    \`\`\`
    
    `;
          }
        }
      }
    
      if (result.passes.length > 0) {
        text += `## Passed Rules (${result.passes.length})
    
    ${result.passes.map((p) => `✅ ${p}`).join('\n')}
    `;
      }
    
      return {
        content: [{ type: 'text', text }],
      };
    }
  • Core grading logic: Iterates over all RULES, runs each rule.check(code), computes total score by summing weights of failed rules, determines letter grade, categorizes violations by severity, generates summary.
    export function gradeComponent(code: string): GradeResult {
      const violations: RuleViolation[] = [];
      const passes: string[] = [];
      let totalWeight = 0;
      let lostPoints = 0;
    
      for (const rule of RULES) {
        totalWeight += rule.weight;
        const ruleViolations = rule.check(code);
    
        if (ruleViolations.length === 0) {
          passes.push(rule.id);
        } else {
          violations.push(...ruleViolations);
          lostPoints += rule.weight;
        }
      }
    
      const score = Math.max(0, Math.round(((totalWeight - lostPoints) / totalWeight) * 100));
    
      let grade: string;
      if (score >= 95) grade = 'A+';
      else if (score >= 90) grade = 'A';
      else if (score >= 85) grade = 'B+';
      else if (score >= 80) grade = 'B';
      else if (score >= 75) grade = 'C+';
      else if (score >= 70) grade = 'C';
      else if (score >= 65) grade = 'D+';
      else if (score >= 60) grade = 'D';
      else grade = 'F';
    
      const errorCount = violations.filter(v =>
        RULES.find(r => r.id === v.ruleId)?.severity === 'error'
      ).length;
    
      const warningCount = violations.filter(v =>
        RULES.find(r => r.id === v.ruleId)?.severity === 'warning'
      ).length;
    
      const summary = `Score: ${score}/100 (${grade}) | ${passes.length} passed | ${errorCount} errors | ${warningCount} warnings`;
    
      return {
        score,
        grade,
        violations,
        passes,
        summary
      };
    }
  • Tool registration in the executeTool switch statement: Routes 'grade_component' calls to the handleGradeComponent handler.
    case 'grade_component':
      return handleGradeComponent(args);
  • Tool schema and registration in getToolDefinitions(): Defines name, description, inputSchema with 'code' (required string) and optional 'verbose' boolean.
    {
      name: 'grade_component',
      description: 'Grade a component against all rules. Returns a score, grade, violations, and suggestions for fixes.',
      inputSchema: {
        type: 'object',
        properties: {
          code: {
            type: 'string',
            description: 'The component source code to grade',
          },
          verbose: {
            type: 'boolean',
            description: 'Include detailed violation information',
          },
        },
        required: ['code'],
      },
    },
  • Array of all RULES used by gradeComponent. Each rule has id, check function (regex/heuristic based), weight, severity, examples. Covers types, styling, data attrs, a11y, polymorphism, etc.
    export const RULES: Rule[] = [
      // ============================================
      // TYPES RULES
      // ============================================
      {
        id: 'extends-html-props',
        name: 'Extend HTML Props',
        description: 'Components must extend native HTML attributes using React.ComponentProps',
        category: 'types',
        severity: 'error',
        weight: 15,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Check if there's a type definition
          const hasTypeDefinition = /type\s+\w+Props\s*=/.test(code) || /interface\s+\w+Props/.test(code);
    
          if (hasTypeDefinition) {
            // Check if it extends React.ComponentProps or ComponentProps
            const extendsComponentProps = /React\.ComponentProps<['"`]\w+['"`]>/.test(code) ||
                                           /ComponentProps<['"`]\w+['"`]>/.test(code);
    
            if (!extendsComponentProps) {
              violations.push({
                ruleId: 'extends-html-props',
                message: 'Component props should extend React.ComponentProps<"element">',
                suggestion: 'Add React.ComponentProps<"div"> or appropriate element type'
              });
            }
          }
    
          return violations;
        },
        example: {
          bad: `type ButtonProps = {
      variant?: 'primary' | 'secondary';
      onClick?: () => void;
    };`,
          good: `type ButtonProps = React.ComponentProps<'button'> & {
      variant?: 'primary' | 'secondary';
    };`
        }
      },
    
      {
        id: 'exports-types',
        name: 'Export Types',
        description: 'Component prop types must be exported',
        category: 'types',
        severity: 'error',
        weight: 10,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Remove import statements to avoid matching imported types like `type VariantProps`
          const codeWithoutImports = code.replace(/^import\s+.*$/gm, '');
    
          // Find all Props type definitions (only in actual type/interface declarations, not imports)
          const propsMatch = codeWithoutImports.match(/(?:type|interface)\s+(\w+Props)/g);
    
          if (propsMatch) {
            for (const match of propsMatch) {
              const typeName = match.replace(/(?:type|interface)\s+/, '');
              // Check if it's exported
              const isExported = new RegExp(`export\\s+(?:type|interface)\\s+${typeName}`).test(code);
    
              if (!isExported) {
                violations.push({
                  ruleId: 'exports-types',
                  message: `Type "${typeName}" should be exported`,
                  suggestion: `Add "export" before the type definition`
                });
              }
            }
          }
    
          return violations;
        },
        example: {
          bad: `type ButtonProps = React.ComponentProps<'button'>;`,
          good: `export type ButtonProps = React.ComponentProps<'button'>;`
        }
      },
    
      {
        id: 'props-spread-last',
        name: 'Props Spread Last',
        description: 'Spread props (...props) must come last on the element to allow user overrides',
        category: 'types',
        severity: 'error',
        weight: 10,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Find JSX elements with spread props
          const jsxWithSpread = code.match(/<\w+[^>]*\{\.\.\.props\}[^>]*>/g);
    
          if (jsxWithSpread) {
            for (const jsx of jsxWithSpread) {
              // Check if there are props AFTER the spread
              const afterSpread = jsx.split('{...props}')[1];
              if (afterSpread && /\w+=/.test(afterSpread)) {
                violations.push({
                  ruleId: 'props-spread-last',
                  message: 'Props are defined after {...props}, which prevents user overrides',
                  line: findLineNumber(code, jsx),
                  suggestion: 'Move {...props} to the end of the element'
                });
              }
            }
          }
    
          return violations;
        },
        example: {
          bad: `<div {...props} className="default-class" />`,
          good: `<div className="default-class" {...props} />`
        }
      },
    
      {
        id: 'single-element-wrap',
        name: 'Single Element Wrapping',
        description: 'Each component should wrap a single HTML/JSX element for maximum composability',
        category: 'composition',
        severity: 'warning',
        weight: 5,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // This is a heuristic - check for multiple root elements in return
          const returnMatch = code.match(/return\s*\(\s*(<[\s\S]*?>[\s\S]*?<\/[\s\S]*?>)\s*\)/);
    
          if (returnMatch) {
            const jsx = returnMatch[1];
            // Count top-level elements (simplified check)
            const fragmentWrapper = /<>|<React\.Fragment>|<Fragment>/.test(jsx);
            if (fragmentWrapper) {
              violations.push({
                ruleId: 'single-element-wrap',
                message: 'Component returns a fragment with multiple elements. Consider breaking into sub-components.',
                suggestion: 'Use composable sub-components (Root, Header, Content, etc.)'
              });
            }
          }
    
          return violations;
        },
        example: {
          bad: `return (
      <>
        <div className="header">{title}</div>
        <div className="content">{content}</div>
      </>
    );`,
          good: `// CardHeader.tsx
    return <div className="header" {...props} />;
    
    // CardContent.tsx
    return <div className="content" {...props} />;`
        }
      },
    
      // ============================================
      // STYLING RULES
      // ============================================
      {
        id: 'uses-cn-utility',
        name: 'Uses cn() Utility',
        description: 'Use the cn() utility for class merging (clsx + tailwind-merge)',
        category: 'styling',
        severity: 'warning',
        weight: 8,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Check if component has className but doesn't use cn()
          const hasClassName = /className=/.test(code);
          const usesCn = /cn\(/.test(code);
          const hasConditionalClasses = /className=\{.*\?.*:/.test(code) ||
                                         /className=\{`/.test(code);
    
          if (hasClassName && hasConditionalClasses && !usesCn) {
            violations.push({
              ruleId: 'uses-cn-utility',
              message: 'Use cn() utility for conditional class names instead of template literals or ternaries',
              suggestion: 'Import cn from @/lib/utils and use cn(baseClasses, conditionalClasses, className)'
            });
          }
    
          return violations;
        },
        example: {
          bad: `className={\`base-class \${isActive ? 'active' : ''}\`}`,
          good: `className={cn('base-class', isActive && 'active', className)}`
        }
      },
    
      {
        id: 'class-order',
        name: 'Class Order',
        description: 'Classes should be ordered: base, variants, conditionals, user overrides (className last)',
        category: 'styling',
        severity: 'warning',
        weight: 5,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Match cn() calls, handling nested parentheses for variant functions
          const cnCallRegex = /cn\(([^)]*(?:\([^)]*\)[^)]*)*)\)/g;
          let match;
    
          while ((match = cnCallRegex.exec(code)) !== null) {
            const args = match[1];
    
            // Check 1: className should be last
            // className followed by a comma AND then another argument means it's NOT last
            // But trailing commas are OK: cn("base", className,) - className IS last here
            // We check: is there actual content (not just whitespace) after "className,"?
            const classNameNotLast = /className\s*,\s*(?!\s*$)(?!\s*\))[\w'"&|{]/.test(args);
            if (classNameNotLast) {
              violations.push({
                ruleId: 'class-order',
                message: 'className should be the last argument in cn() to allow user overrides',
                line: findLineNumber(code, match[0]),
                suggestion: 'Move className to the end: cn(baseClasses, variants, conditionals, className)'
              });
            }
    
            // Check 2: Base styles (string literals) should come first
            // If we see a conditional (&&) or variable before a string literal, flag it
            const firstArg = args.split(',')[0]?.trim();
            if (firstArg && /^[a-zA-Z]/.test(firstArg) && !firstArg.startsWith("'") && !firstArg.startsWith('"')) {
              // First arg is a variable/conditional, not a string literal
              // Check if there are string literals later
              const hasLaterStringLiteral = /,\s*['"][^'"]+['"]/.test(args);
              if (hasLaterStringLiteral) {
                violations.push({
                  ruleId: 'class-order',
                  message: 'Base styles (string literals) should come before variables and conditionals',
                  line: findLineNumber(code, match[0]),
                  suggestion: 'Order: cn("base-styles", variantStyles, conditional && "active", className)'
                });
              }
            }
    
            // Check 3: CVA variants function should come before conditionals
            // Look for pattern: conditional && before variantFunction(
            if (/\w+\s*&&\s*['"][^'"]*['"].*\w+Variants?\(/.test(args)) {
              violations.push({
                ruleId: 'class-order',
                message: 'Variant styles should come before conditional styles',
                line: findLineNumber(code, match[0]),
                suggestion: 'Order: cn("base", variants({ size }), isActive && "active", className)'
              });
            }
          }
    
          return violations;
        },
        example: {
          bad: `cn(className, 'base-styles', isActive && 'active')
    cn(isActive && 'active', 'base-styles')`,
          good: `cn(
      'base-styles',            // 1. Base (always applied)
      variants({ size }),       // 2. Variants (based on props)
      isActive && 'active',     // 3. Conditionals (based on state)
      className                 // 4. User overrides (last!)
    )`
        }
      },
    
      {
        id: 'uses-design-tokens',
        name: 'Uses Design Tokens',
        description: 'Use CSS variables/design tokens instead of hardcoded colors',
        category: 'styling',
        severity: 'warning',
        weight: 5,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Check for Tailwind arbitrary hex values: bg-[#5B9BD5], text-[#2B5BA8], from-[#hex], etc.
          const arbitraryHexColors = code.match(/(?:bg|text|border|fill|stroke|from|to|via|ring|outline|shadow|accent|caret|decoration)-\[#[0-9A-Fa-f]{3,8}(?:\/\d+)?\]/g);
    
          if (arbitraryHexColors && arbitraryHexColors.length > 0) {
            violations.push({
              ruleId: 'uses-design-tokens',
              message: `Found ${arbitraryHexColors.length} hardcoded hex color(s) in Tailwind arbitrary values: ${arbitraryHexColors.slice(0, 3).join(', ')}`,
              suggestion: `Use CSS variables instead: bg-[var(--custom-color)] or define semantic tokens.
    
    Bad:  bg-[#5B9BD5] text-[#2B5BA8] dark:bg-[#5B9BD5]/10
    Good: bg-primary text-primary-foreground (with CSS vars handling dark mode)`,
              line: findLineNumber(code, arbitraryHexColors[0])
            });
          }
    
          // Check for manual dark mode color overrides (sign of missing design tokens)
          const darkModeColorOverrides = code.match(/dark:(?:bg|text|border|fill|stroke|from|to|via|ring|outline|shadow|accent|caret|decoration)-\[#[0-9A-Fa-f]{3,8}/g);
    
          if (darkModeColorOverrides && darkModeColorOverrides.length > 0) {
            violations.push({
              ruleId: 'uses-design-tokens',
              message: `Found ${darkModeColorOverrides.length} manual dark mode color overrides - this should be handled by CSS variables`,
              suggestion: `Instead of: bg-[#5B9BD5] dark:bg-[#7BA3D9]
    Use: bg-[var(--brand-color)] where --brand-color changes in .dark`
            });
          }
    
          // Check for hardcoded color values in className (Tailwind palette colors)
          const hardcodedTailwindColors = code.match(/(?:bg|text|border)-(?:red|blue|green|yellow|purple|pink|gray|slate|zinc|neutral|stone|amber|lime|emerald|teal|cyan|sky|indigo|violet|fuchsia|rose)-\d{2,3}/g);
    
          if (hardcodedTailwindColors && hardcodedTailwindColors.length > 3) {
            violations.push({
              ruleId: 'uses-design-tokens',
              message: 'Consider using semantic design tokens (primary, secondary, destructive) instead of hardcoded Tailwind colors',
              suggestion: 'Use bg-primary, text-foreground, border-border, etc.'
            });
          }
    
          // Check for hardcoded hex colors in style objects or JSX
          // Matches: color: "#8B5CF6", background: '#fff', etc.
          const hexInStyles = code.match(/(?:color|background|backgroundColor|borderColor|fill|stroke)\s*:\s*["']#[0-9A-Fa-f]{3,8}["']/g);
    
          if (hexInStyles && hexInStyles.length > 0) {
            violations.push({
              ruleId: 'uses-design-tokens',
              message: `Found ${hexInStyles.length} hardcoded hex color(s) in style objects`,
              suggestion: 'Use CSS variables: style={{ color: "var(--primary)" }} or define as CSS custom properties',
              line: findLineNumber(code, hexInStyles[0])
            });
          }
    
          // Check for hex values assigned to variables (config objects)
          // Matches: primary: "#8B5CF6", color: '#fff', etc.
          const hexInConfig = code.match(/\w+\s*:\s*["']#[0-9A-Fa-f]{3,8}["']/g);
    
          // Filter out CSS variable definitions which are fine
          const nonCssVarHex = hexInConfig?.filter(match => !match.includes('--'));
    
          if (nonCssVarHex && nonCssVarHex.length > 3) {
            violations.push({
              ruleId: 'uses-design-tokens',
              message: `Found ${nonCssVarHex.length} hardcoded hex colors in config/variables`,
              suggestion: 'Define colors as CSS variables: "--palette-primary": "#8B5CF6" then use var(--palette-primary)'
            });
          }
    
          // Check for rgb/rgba/hsl/hsla hardcoded values
          const hardcodedRgbHsl = code.match(/(?:rgb|rgba|hsl|hsla)\s*\([^)]+\)/g);
    
          if (hardcodedRgbHsl && hardcodedRgbHsl.length > 3) {
            violations.push({
              ruleId: 'uses-design-tokens',
              message: `Found ${hardcodedRgbHsl.length} hardcoded rgb/hsl colors`,
              suggestion: 'Use CSS variables for colors to enable theming'
            });
          }
    
          return violations;
        },
        example: {
          bad: `<Badge className="bg-[#5B9BD5]/20 text-[#2B5BA8] dark:bg-[#5B9BD5]/10 dark:text-[#7BA3D9]">
    const config = { primary: "#8B5CF6" };`,
          good: `const config = { "--primary": "#8B5CF6" };
    <div style={config as React.CSSProperties} className="text-[var(--primary)]">`
        }
      },
    
      {
        id: 'uses-semantic-tokens',
        name: 'Uses Semantic Design Tokens',
        description: 'Use semantic token names (background, foreground, primary) instead of color names',
        category: 'styling',
        severity: 'warning',
        weight: 8,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Semantic tokens that SHOULD be used
          const semanticTokens = [
            'background', 'foreground',
            'primary', 'primary-foreground',
            'secondary', 'secondary-foreground',
            'muted', 'muted-foreground',
            'accent', 'accent-foreground',
            'destructive', 'destructive-foreground',
            'border', 'input', 'ring',
            'card', 'card-foreground',
            'popover', 'popover-foreground'
          ];
    
          // Check if component uses Tailwind classes
          const hasTailwindClasses = /className=.*(?:bg-|text-|border-)/.test(code);
    
          if (hasTailwindClasses) {
            // Check for non-semantic color usage (e.g., bg-white, text-black, bg-gray-100)
            const nonSemanticColors = code.match(/(?:bg|text|border)-(?:white|black|transparent)/g) || [];
            const paletteColors = code.match(/(?:bg|text|border)-(?:gray|slate|zinc|neutral|stone)-\d{2,3}/g) || [];
    
            // Check if semantic tokens are used
            const usesSemanticTokens = semanticTokens.some(token =>
              new RegExp(`(?:bg|text|border)-${token}(?:-foreground)?`).test(code)
            );
    
            // If using lots of non-semantic colors and no semantic tokens, flag it
            if ((nonSemanticColors.length + paletteColors.length) > 3 && !usesSemanticTokens) {
              violations.push({
                ruleId: 'uses-semantic-tokens',
                message: 'Use semantic design tokens instead of color names for better theming support',
                suggestion: `Replace bg-white → bg-background, text-black → text-foreground, bg-gray-100 → bg-muted, etc.
    
    Semantic tokens to use:
    - background/foreground: Page background and main text
    - primary/primary-foreground: Brand color and its text
    - secondary/secondary-foreground: Secondary actions
    - muted/muted-foreground: Subtle backgrounds and text
    - accent/accent-foreground: Highlights
    - destructive/destructive-foreground: Errors/warnings
    - border, input, ring: Borders and focus states
    - card/card-foreground: Card surfaces`
              });
            }
    
            // Check for direct color values that should use tokens
            const directWhiteBlack = code.match(/(?:bg|text|border)-(?:white|black)/g);
            if (directWhiteBlack && directWhiteBlack.length > 0) {
              violations.push({
                ruleId: 'uses-semantic-tokens',
                message: `Found ${directWhiteBlack.length} uses of bg-white/black - these break dark mode`,
                suggestion: 'bg-white → bg-background or bg-card, text-black → text-foreground'
              });
            }
          }
    
          // Check for CSS variable usage without semantic naming
          const cssVarUsage = code.match(/var\(--[\w-]+\)/g);
          if (cssVarUsage) {
            const nonSemanticVars = cssVarUsage.filter(v => {
              const varName = v.match(/--[\w-]+/)?.[0] || '';
              // Check if it's a color-based name instead of semantic
              return /--(?:red|blue|green|yellow|purple|pink|gray|white|black|color-\d)/.test(varName);
            });
    
            if (nonSemanticVars.length > 0) {
              violations.push({
                ruleId: 'uses-semantic-tokens',
                message: `Found CSS variables with color names instead of semantic names: ${nonSemanticVars.slice(0, 3).join(', ')}`,
                suggestion: 'Use semantic names: --primary instead of --blue, --destructive instead of --red'
              });
            }
          }
    
          return violations;
        },
        example: {
          bad: `// Hardcoded colors break dark mode
    <div className="bg-white text-black border-gray-200">
      <button className="bg-blue-500 text-white">Click</button>
    </div>`,
          good: `// Semantic tokens adapt to theme
    <div className="bg-background text-foreground border-border">
      <button className="bg-primary text-primary-foreground">Click</button>
    </div>
    
    /* In globals.css */
    :root {
      --background: oklch(1 0 0);
      --foreground: oklch(0.145 0 0);
      --primary: oklch(0.205 0 0);
      --primary-foreground: oklch(0.985 0 0);
    }
    .dark {
      --background: oklch(0.145 0 0);
      --foreground: oklch(0.985 0 0);
    }`
        }
      },
    
      // ============================================
      // DATA ATTRIBUTES RULES
      // ============================================
      {
        id: 'has-data-slot',
        name: 'Has data-slot Attribute',
        description: 'Components should have a data-slot attribute for parent-aware styling',
        category: 'styling',
        severity: 'warning',
        weight: 8,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Check if this looks like a component (has export and returns JSX)
          const isComponent = /export\s+(?:const|function)/.test(code) && /<\w+/.test(code);
          const hasDataSlot = /data-slot=/.test(code);
    
          // Skip blocks - they are compositions, not components
          // Blocks use components that have data-slot, but the block itself doesn't need one
          const isBlock = /(?:Hero|Pricing|Features|Footer|Header|Auth|Onboarding|Dashboard|Settings|Billing|Chat|Landing|Block|Page|Section)(?:Block|Page|Section)?/i.test(code);
    
          if (isComponent && !hasDataSlot && !isBlock) {
            violations.push({
              ruleId: 'has-data-slot',
              message: 'Component should have a data-slot attribute for identification',
              suggestion: 'Add data-slot="component-name" to the root element'
            });
          }
    
          return violations;
        },
        example: {
          bad: `<div className="card" {...props} />`,
          good: `<div data-slot="card" className="card" {...props} />`
        }
      },
    
      {
        id: 'uses-data-state',
        name: 'Uses data-state for Visual States',
        description: 'Use data-state attribute to expose component state for styling',
        category: 'styling',
        severity: 'info',
        weight: 3,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Check if component has state that should be exposed
          const hasOpenState = /isOpen|open|setOpen/.test(code);
          const hasActiveState = /isActive|active|setActive/.test(code);
          const hasDataState = /data-state=/.test(code);
    
          if ((hasOpenState || hasActiveState) && !hasDataState) {
            violations.push({
              ruleId: 'uses-data-state',
              message: 'Component has state that could be exposed via data-state attribute',
              suggestion: 'Add data-state={isOpen ? "open" : "closed"} for styling hooks'
            });
          }
    
          return violations;
        },
        example: {
          bad: `<div className={isOpen ? 'opacity-100' : 'opacity-0'} />`,
          good: `<div data-state={isOpen ? 'open' : 'closed'} className="data-[state=open]:opacity-100 data-[state=closed]:opacity-0" />`
        }
      },
    
      // ============================================
      // ACCESSIBILITY RULES
      // ============================================
      {
        id: 'button-has-type',
        name: 'Button Has Type',
        description: 'Button elements must have an explicit type attribute',
        category: 'accessibility',
        severity: 'error',
        weight: 10,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Find button elements without type
          const buttonMatches = code.match(/<button[^>]*>/g);
    
          if (buttonMatches) {
            for (const match of buttonMatches) {
              if (!/type=/.test(match)) {
                violations.push({
                  ruleId: 'button-has-type',
                  message: 'Button element missing type attribute',
                  line: findLineNumber(code, match),
                  suggestion: 'Add type="button" or type="submit"'
                });
              }
            }
          }
    
          return violations;
        },
        example: {
          bad: `<button onClick={handleClick}>Click me</button>`,
          good: `<button type="button" onClick={handleClick}>Click me</button>`
        }
      },
    
      {
        id: 'interactive-has-keyboard',
        name: 'Interactive Elements Have Keyboard Support',
        description: 'Interactive non-button elements must have keyboard event handlers',
        category: 'accessibility',
        severity: 'error',
        weight: 12,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Find div/span with onClick but no onKeyDown/onKeyUp
          const interactiveMatches = code.match(/<(?:div|span)[^>]*onClick[^>]*>/g);
    
          if (interactiveMatches) {
            for (const match of interactiveMatches) {
              const hasKeyHandler = /onKey(?:Down|Up|Press)/.test(match);
              const hasRole = /role=/.test(match);
    
              if (!hasKeyHandler) {
                violations.push({
                  ruleId: 'interactive-has-keyboard',
                  message: 'Interactive element with onClick needs keyboard handler',
                  line: findLineNumber(code, match),
                  suggestion: 'Add onKeyDown handler for Enter/Space activation, or use a <button>'
                });
              }
    
              if (!hasRole) {
                violations.push({
                  ruleId: 'interactive-has-keyboard',
                  message: 'Interactive element needs role="button"',
                  line: findLineNumber(code, match),
                  suggestion: 'Add role="button" and tabIndex={0}'
                });
              }
            }
          }
    
          return violations;
        },
        example: {
          bad: `<div onClick={handleClick}>Click me</div>`,
          good: `<div
      role="button"
      tabIndex={0}
      onClick={handleClick}
      onKeyDown={(e) => e.key === 'Enter' && handleClick()}
    >Click me</div>`
        }
      },
    
      {
        id: 'icon-button-has-label',
        name: 'Icon Buttons Have Labels',
        description: 'Icon-only buttons must have aria-label or visually hidden text',
        category: 'accessibility',
        severity: 'error',
        weight: 12,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Check for buttons that only contain icons (simplified heuristic)
          const iconButtonPattern = /<button[^>]*>\s*<(?:svg|Icon|\w+Icon)[^>]*\/>\s*<\/button>/g;
          const matches = code.match(iconButtonPattern);
    
          if (matches) {
            for (const match of matches) {
              const hasAriaLabel = /aria-label=/.test(match);
              const hasSrOnly = /sr-only|visually-hidden/.test(match);
    
              if (!hasAriaLabel && !hasSrOnly) {
                violations.push({
                  ruleId: 'icon-button-has-label',
                  message: 'Icon-only button needs aria-label for screen readers',
                  line: findLineNumber(code, match),
                  suggestion: 'Add aria-label="Description" to the button'
                });
              }
            }
          }
    
          return violations;
        },
        example: {
          bad: `<button><TrashIcon /></button>`,
          good: `<button aria-label="Delete item"><TrashIcon aria-hidden="true" /></button>`
        }
      },
    
      {
        id: 'uses-semantic-html',
        name: 'Uses Semantic HTML',
        description: 'Use semantic HTML elements instead of divs with roles',
        category: 'accessibility',
        severity: 'warning',
        weight: 5,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Check for div with button role (should be button)
          if (/<div[^>]*role=["']button["']/.test(code)) {
            violations.push({
              ruleId: 'uses-semantic-html',
              message: 'Use <button> instead of <div role="button">',
              suggestion: 'Replace with semantic <button> element'
            });
          }
    
          // Check for div with nav role (should be nav)
          if (/<div[^>]*role=["']navigation["']/.test(code)) {
            violations.push({
              ruleId: 'uses-semantic-html',
              message: 'Use <nav> instead of <div role="navigation">',
              suggestion: 'Replace with semantic <nav> element'
            });
          }
    
          return violations;
        },
        example: {
          bad: `<div role="button" onClick={handleClick}>Click</div>`,
          good: `<button onClick={handleClick}>Click</button>`
        }
      },
    
      {
        id: 'aria-expanded-with-controls',
        name: 'aria-expanded Has aria-controls',
        description: 'Elements with aria-expanded should have aria-controls pointing to the controlled element',
        category: 'accessibility',
        severity: 'warning',
        weight: 5,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Find elements with aria-expanded
          const expandedMatches = code.match(/<\w+[^>]*aria-expanded[^>]*>/g);
    
          if (expandedMatches) {
            for (const match of expandedMatches) {
              if (!/aria-controls=/.test(match)) {
                violations.push({
                  ruleId: 'aria-expanded-with-controls',
                  message: 'Element with aria-expanded should have aria-controls',
                  line: findLineNumber(code, match),
                  suggestion: 'Add aria-controls="content-id" pointing to the expandable content'
                });
              }
            }
          }
    
          return violations;
        },
        example: {
          bad: `<button aria-expanded={isOpen}>Toggle</button>`,
          good: `<button aria-expanded={isOpen} aria-controls="panel-1">Toggle</button>
    <div id="panel-1">{content}</div>`
        }
      },
    
      // ============================================
      // STATE RULES
      // ============================================
      {
        id: 'supports-controlled-uncontrolled',
        name: 'Supports Controlled and Uncontrolled',
        description: 'Stateful components should support both controlled and uncontrolled usage',
        category: 'state',
        severity: 'info',
        weight: 5,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Check if component has useState but no controlled props
          const hasUseState = /useState/.test(code);
          const hasValueProp = /value.*:/.test(code) || /\bvalue\b/.test(code);
          const hasDefaultValueProp = /defaultValue/.test(code);
          const hasOnChangeProp = /onValueChange|onChange/.test(code);
    
          if (hasUseState && !hasDefaultValueProp && !hasOnChangeProp) {
            violations.push({
              ruleId: 'supports-controlled-uncontrolled',
              message: 'Stateful component should support controlled usage',
              suggestion: 'Add value, defaultValue, and onValueChange props. Consider using useControllableState.'
            });
          }
    
          return violations;
        },
        example: {
          bad: `const Stepper = () => {
      const [value, setValue] = useState(0);
      return <div>{value}</div>;
    };`,
          good: `const Stepper = ({ value: controlledValue, defaultValue, onValueChange }) => {
      const [value, setValue] = useControllableState({
        prop: controlledValue,
        defaultProp: defaultValue,
        onChange: onValueChange,
      });
      return <div>{value}</div>;
    };`
        }
      },
    
      // ============================================
      // NAMING RULES
      // ============================================
      {
        id: 'composable-naming',
        name: 'Composable Naming Convention',
        description: 'Sub-components should follow naming conventions: Root, Trigger, Content, Header, Footer, Title, Description',
        category: 'naming',
        severity: 'info',
        weight: 3,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Check if file exports multiple components (compound component pattern)
          const exports = code.match(/export\s+(?:const|function)\s+(\w+)/g);
    
          if (exports && exports.length > 1) {
            const validNames = ['Root', 'Item', 'Trigger', 'Content', 'Header', 'Body', 'Footer', 'Title', 'Description', 'Close', 'Overlay', 'Portal'];
    
            for (const exp of exports) {
              const name = exp.replace(/export\s+(?:const|function)\s+/, '');
              const hasValidSuffix = validNames.some(valid => name.endsWith(valid) || name === valid);
    
              if (!hasValidSuffix && name !== 'default') {
                // This is informational, not an error
              }
            }
          }
    
          return violations;
        },
        example: {
          bad: `export const AccordionContainer = ...
    export const AccordionButton = ...
    export const AccordionPanel = ...`,
          good: `export const Root = ...
    export const Trigger = ...
    export const Content = ...`
        }
      },
    
      {
        id: 'data-slot-naming',
        name: 'data-slot Naming Convention',
        description: 'data-slot values should use kebab-case and be descriptive',
        category: 'naming',
        severity: 'info',
        weight: 2,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Find data-slot values
          const dataSlotMatches = code.match(/data-slot=["']([^"']+)["']/g);
    
          if (dataSlotMatches) {
            for (const match of dataSlotMatches) {
              const value = match.match(/data-slot=["']([^"']+)["']/)?.[1];
    
              if (value) {
                // Check for camelCase (should be kebab-case)
                if (/[a-z][A-Z]/.test(value)) {
                  violations.push({
                    ruleId: 'data-slot-naming',
                    message: `data-slot value "${value}" should use kebab-case`,
                    suggestion: `Change to "${value.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()}"`
                  });
                }
              }
            }
          }
    
          return violations;
        },
        example: {
          bad: `data-slot="cardHeader"`,
          good: `data-slot="card-header"`
        }
      },
    
      // ============================================
      // POLYMORPHISM RULES
      // ============================================
      {
        id: 'supports-polymorphism',
        name: 'Supports Polymorphism',
        description: 'Interactive and layout components should support polymorphism via as or asChild prop',
        category: 'composition',
        severity: 'warning',
        weight: 8,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Check if this is an interactive component (Button, Link, etc.)
          const isInteractiveComponent = /export\s+(?:const|function)\s+(?:Button|Link|Clickable|Interactive|Action|Trigger)/i.test(code);
    
          // Check if this is a layout/container component
          const isLayoutComponent = /export\s+(?:const|function)\s+(?:Box|Container|Flex|Grid|Stack|Card|Section|Article|Main|Header|Footer|Aside|Nav)/i.test(code);
    
          // Check if component uses Slot from Radix
          const usesSlot = /import.*Slot.*from\s+['"]@radix-ui\/react-slot['"]/.test(code);
          const hasAsChild = /asChild/.test(code);
    
          // Check if component accepts 'as' prop
          const hasAsProp = /\bas\s*[?:]/.test(code) || /as\s*=/.test(code) || /as:\s*Element/.test(code);
    
          // Check for compound component sub-parts that render links/buttons without polymorphism
          // Matches: const NavItem = ..., const Logo = ..., function MobileNavItem, etc.
          const subComponentPatterns = [
            // Navigation items - should support button OR link
            { pattern: /(?:const|function)\s+(NavItem|NavigationItem|MenuItem|MobileNavItem|NavLink)/g, type: 'navigation' },
            // Logo - might be link or static
            { pattern: /(?:const|function)\s+(Logo|Brand|LogoLink)/g, type: 'logo' },
            // Root/Container components - should support semantic elements
            { pattern: /(?:const|function)\s+(Root|Wrapper|Container|Section|Layout)/g, type: 'container' },
            // Action items
            { pattern: /(?:const|function)\s+(Action|CTA|ActionButton)/g, type: 'action' },
          ];
    
          for (const { pattern, type } of subComponentPatterns) {
            const matches = code.match(pattern);
            if (matches && !hasAsChild && !hasAsProp && !usesSlot) {
              // Check if the component renders a hardcoded <a> or <button>
              const rendersHardcodedLink = /<a\s/.test(code) && !/<a\s[^>]*\{/.test(code);
              const rendersHardcodedButton = /<button\s/.test(code) && !/<button\s[^>]*\{/.test(code);
    
              if ((type === 'navigation' || type === 'logo' || type === 'action') && (rendersHardcodedLink || rendersHardcodedButton)) {
                violations.push({
                  ruleId: 'supports-polymorphism',
                  message: `${matches[0].replace(/(?:const|function)\s+/, '')} renders hardcoded ${rendersHardcodedLink ? '<a>' : '<button>'} - add asChild support for Next.js Link / React Router compatibility`,
                  suggestion: `Add asChild prop for router integration:
    
    // Before (hardcoded anchor)
    const NavItem = ({ href, children }) => (
      <a href={href}>{children}</a>
    )
    
    // After (supports any link component)
    import { Slot } from "@radix-ui/react-slot"
    
    const NavItem = ({ href, asChild = false, children, ...props }) => {
      const Comp = asChild ? Slot : "a"
      return <Comp href={!asChild ? href : undefined} {...props}>{children}</Comp>
    }
    
    // Usage with Next.js:
    <NavItem asChild>
      <Link href="/features">Features</Link>
    </NavItem>`
                });
              }
    
              if (type === 'container') {
                violations.push({
                  ruleId: 'supports-polymorphism',
                  message: `${matches[0].replace(/(?:const|function)\s+/, '')} should support 'as' prop for semantic HTML (<section>, <article>, etc.)`,
                  suggestion: `Add 'as' prop:
    
    const Root = <E extends React.ElementType = 'div'>({
      as,
      ...props
    }: { as?: E } & React.ComponentPropsWithoutRef<E>) => {
      const Element = as || 'div';
      return <Element {...props} />;
    }
    
    // Usage:
    <Hero.Root as="section" aria-labelledby="hero-title">
    <Hero.Root as="article">`
                });
              }
            }
          }
    
          // Interactive components SHOULD support polymorphism
          if (isInteractiveComponent && !usesSlot && !hasAsChild && !hasAsProp) {
            violations.push({
              ruleId: 'supports-polymorphism',
              message: 'Interactive component should support polymorphism (as or asChild prop)',
              suggestion: `Use Radix Slot for asChild pattern:
    
    import { Slot } from "@radix-ui/react-slot"
    
    function Button({ asChild = false, ...props }) {
      const Comp = asChild ? Slot : "button"
      return <Comp {...props} />
    }
    
    // Usage: <Button asChild><a href="/">Link Button</a></Button>`
            });
          }
    
          // Layout components SHOULD support 'as' prop for semantic HTML
          if (isLayoutComponent && !hasAsProp && !hasAsChild) {
            violations.push({
              ruleId: 'supports-polymorphism',
              message: 'Layout component should support the "as" prop for semantic HTML',
              suggestion: `Add 'as' prop for semantic flexibility:
    
    type BoxProps<E extends React.ElementType = 'div'> = {
      as?: E;
    } & React.ComponentPropsWithoutRef<E>;
    
    function Box<E extends React.ElementType = 'div'>({
      as,
      ...props
    }: BoxProps<E>) {
      const Element = as || 'div';
      return <Element {...props} />;
    }
    
    // Usage:
    <Box as="section">Content</Box>
    <Box as="nav">Navigation</Box>
    <Box as="article">Article</Box>`
            });
          }
    
          // Check for hardcoded elements that could be polymorphic
          // e.g., a component that always renders <div> but could be semantic
          const rendersDiv = /<div[^>]*data-slot=/.test(code);
          const hasSemanticProps = /role=["'](?:navigation|main|banner|contentinfo|complementary|article|region)["']/.test(code);
    
          if (rendersDiv && hasSemanticProps && !hasAsProp) {
            violations.push({
              ruleId: 'supports-polymorphism',
              message: 'Component uses role attribute - consider supporting "as" prop for semantic HTML instead',
              suggestion: 'Replace <div role="navigation"> with as="nav" support'
            });
          }
    
          return violations;
        },
        example: {
          bad: `// Hardcoded element - no flexibility
    function Button({ children, ...props }) {
      return <button {...props}>{children}</button>
    }
    
    // Hardcoded div with role - should use semantic element
    function Nav({ children }) {
      return <div role="navigation">{children}</div>
    }`,
          good: `// With asChild (Radix Slot pattern)
    import { Slot } from "@radix-ui/react-slot"
    
    function Button({ asChild = false, ...props }) {
      const Comp = asChild ? Slot : "button"
      return <Comp data-slot="button" {...props} />
    }
    
    // With as prop (for layout components)
    function Container<E extends React.ElementType = 'div'>({
      as,
      ...props
    }: { as?: E } & React.ComponentPropsWithoutRef<E>) {
      const Element = as || 'div';
      return <Element data-slot="container" {...props} />;
    }
    
    // Usage:
    <Button asChild><a href="/">Link</a></Button>
    <Container as="section">Content</Container>
    <Container as="nav">Navigation</Container>`
        }
      },
    
      {
        id: 'semantic-default-element',
        name: 'Semantic Default Element',
        description: 'Polymorphic components should default to semantic elements, not generic divs',
        category: 'composition',
        severity: 'warning',
        weight: 5,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Check for components with 'as' prop that default to 'div' when they should be semantic
          const componentDefaults = [
            // Component name patterns and their expected semantic defaults
            { pattern: /(?:const|function)\s+Article.*as:\s*Element\s*=\s*['"]div['"]/, expected: 'article' },
            { pattern: /(?:const|function)\s+Navigation.*as:\s*Element\s*=\s*['"]div['"]/, expected: 'nav' },
            { pattern: /(?:const|function)\s+Nav\b.*as:\s*Element\s*=\s*['"]div['"]/, expected: 'nav' },
            { pattern: /(?:const|function)\s+Header\b.*as:\s*Element\s*=\s*['"]div['"]/, expected: 'header' },
            { pattern: /(?:const|function)\s+Footer\b.*as:\s*Element\s*=\s*['"]div['"]/, expected: 'footer' },
            { pattern: /(?:const|function)\s+Main\b.*as:\s*Element\s*=\s*['"]div['"]/, expected: 'main' },
            { pattern: /(?:const|function)\s+Aside\b.*as:\s*Element\s*=\s*['"]div['"]/, expected: 'aside' },
            { pattern: /(?:const|function)\s+Section\b.*as:\s*Element\s*=\s*['"]div['"]/, expected: 'section' },
            { pattern: /(?:const|function)\s+Heading.*as:\s*Element\s*=\s*['"]div['"]/, expected: 'h2' },
            { pattern: /(?:const|function)\s+Title.*as:\s*Element\s*=\s*['"]div['"]/, expected: 'h1 or h2' },
          ];
    
          for (const { pattern, expected } of componentDefaults) {
            if (pattern.test(code)) {
              const match = code.match(/(?:const|function)\s+(\w+)/);
              const componentName = match ? match[1] : 'Component';
              violations.push({
                ruleId: 'semantic-default-element',
                message: `${componentName} defaults to 'div' but should default to '${expected}'`,
                suggestion: `Change default from 'div' to '${expected}':
    
    // ❌ Too generic
    function ${componentName}({ as: Element = 'div', ...props }) { }
    
    // ✅ Semantic default
    function ${componentName}({ as: Element = '${expected}', ...props }) { }`
              });
            }
          }
    
          return violations;
        },
        example: {
          bad: `// Too generic defaults
    function Article({ as: Element = 'div', ...props }) { }
    function Navigation({ as: Element = 'div', ...props }) { }
    function Heading({ as: Element = 'div', ...props }) { }`,
          good: `// Semantic defaults
    function Article({ as: Element = 'article', ...props }) { }
    function Navigation({ as: Element = 'nav', ...props }) { }
    function Heading({ as: Element = 'h2', ...props }) { }`
        }
      },
    
      {
        id: 'polymorphic-type-safety',
        name: 'Polymorphic Type Safety',
        description: 'Polymorphic components should use proper TypeScript generics for type safety',
        category: 'types',
        severity: 'warning',
        weight: 5,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Check for 'as' prop with 'any' type
          if (/as\s*\??\s*:\s*any/.test(code)) {
            violations.push({
              ruleId: 'polymorphic-type-safety',
              message: "The 'as' prop is typed as 'any' - this loses type safety",
              suggestion: `Use proper generic typing:
    
    type PolymorphicProps<E extends React.ElementType> = {
      as?: E;
    } & React.ComponentPropsWithoutRef<E>;
    
    function Component<E extends React.ElementType = 'div'>({
      as,
      ...props
    }: PolymorphicProps<E>) {
      const Element = as || 'div';
      return <Element {...props} />;
    }`
            });
          }
    
          // Check for 'as' prop with string type instead of ElementType
          if (/as\s*\??\s*:\s*string/.test(code)) {
            violations.push({
              ruleId: 'polymorphic-type-safety',
              message: "The 'as' prop is typed as 'string' - use React.ElementType for proper inference",
              suggestion: `Use React.ElementType:
    
    // ❌ No inference
    as?: string
    
    // ✅ Full type inference
    <E extends React.ElementType = 'div'>
    as?: E`
            });
          }
    
          // Check for polymorphic component without generic type parameter
          const hasAsProp = /\bas\s*[?:]/.test(code);
          const hasGeneric = /<\s*E\s+extends\s+React\.ElementType/.test(code);
    
          if (hasAsProp && !hasGeneric && !/asChild/.test(code)) {
            // Only flag if it looks like a polymorphic component definition
            if (/(?:const|function)\s+\w+.*as\s*[?:]/.test(code)) {
              violations.push({
                ruleId: 'polymorphic-type-safety',
                message: "Polymorphic component lacks generic type parameter - props won't be inferred correctly",
                suggestion: `Add generic type parameter:
    
    function Component<E extends React.ElementType = 'div'>({
      as,
      ...props
    }: { as?: E } & React.ComponentPropsWithoutRef<E>) {
      const Element = as || 'div';
      return <Element {...props} />;
    }
    
    // Now TypeScript infers props based on 'as':
    <Component as="a" href="/" />  // ✅ href is valid
    <Component as="button" href="/" />  // ❌ Error: href not on button`
              });
            }
          }
    
          return violations;
        },
        example: {
          bad: `// No type safety
    function Component({ as: Element = 'div', ...props }: any) {
      return <Element {...props} />;
    }
    
    // String type loses inference
    function Box({ as }: { as?: string }) { }`,
          good: `// Full type safety with generics
    type PolymorphicProps<E extends React.ElementType> = {
      as?: E;
    } & React.ComponentPropsWithoutRef<E>;
    
    function Component<E extends React.ElementType = 'div'>({
      as,
      ...props
    }: PolymorphicProps<E>) {
      const Element = as || 'div';
      return <Element {...props} />;
    }
    
    // Props are inferred from element type:
    <Component as="a" href="/home" />  // ✅ href valid
    <Component as="button" type="submit" />  // ✅ type valid`
        }
      },
    
      {
        id: 'polymorphic-keyboard-handler',
        name: 'Polymorphic Keyboard Handler',
        description: 'Non-button interactive elements need keyboard event handlers for accessibility',
        category: 'accessibility',
        severity: 'warning',
        weight: 8,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Check for onClick without onKeyDown on polymorphic components
          const hasAsProp = /\bas\s*[?:]/.test(code) || /as\s*=/.test(code);
          const hasOnClick = /onClick/.test(code);
          const hasKeyboardHandler = /onKeyDown|onKeyUp|onKeyPress/.test(code);
    
          // If component supports 'as' and has onClick but no keyboard handler
          if (hasAsProp && hasOnClick && !hasKeyboardHandler) {
            violations.push({
              ruleId: 'polymorphic-keyboard-handler',
              message: "Polymorphic component with onClick needs keyboard handler for when rendered as non-button",
              suggestion: `Add keyboard support for accessibility:
    
    function Interactive({ as: Element = 'button', onClick, ...props }) {
      const handleKeyDown = (e: React.KeyboardEvent) => {
        // Only needed for non-button elements
        if (Element !== 'button' && (e.key === 'Enter' || e.key === ' ')) {
          e.preventDefault();
          onClick?.(e as any);
        }
      };
    
      return (
        <Element
          onClick={onClick}
          onKeyDown={Element !== 'button' ? handleKeyDown : undefined}
          tabIndex={Element !== 'button' && Element !== 'a' ? 0 : undefined}
          role={Element !== 'button' && Element !== 'a' ? 'button' : undefined}
          {...props}
        />
      );
    }`
            });
          }
    
          return violations;
        },
        example: {
          bad: `// Missing keyboard support
    function Clickable({ as: Element = 'div', onClick, ...props }) {
      return <Element onClick={onClick} {...props} />;
    }
    
    // When rendered as div, Enter/Space won't work!`,
          good: `// With keyboard support
    function Clickable({ as: Element = 'button', onClick, ...props }) {
      const handleKeyDown = (e: React.KeyboardEvent) => {
        if (Element !== 'button' && (e.key === 'Enter' || e.key === ' ')) {
          e.preventDefault();
          onClick?.(e as any);
        }
      };
    
      return (
        <Element
          onClick={onClick}
          onKeyDown={Element !== 'button' ? handleKeyDown : undefined}
          tabIndex={Element !== 'button' ? 0 : undefined}
          role={Element !== 'button' ? 'button' : undefined}
          {...props}
        />
      );
    }`
        }
      },
    
      {
        id: 'as-prop-documented',
        name: 'As Prop Documented',
        description: 'The as prop should have JSDoc documenting valid element choices',
        category: 'types',
        severity: 'info',
        weight: 3,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Check for 'as' prop without JSDoc
          const hasAsProp = /\bas\s*\??\s*:/.test(code);
          const hasAsJsDoc = /\/\*\*[\s\S]*?@(?:example|default)[\s\S]*?\*\/[\s\n]*.*as\s*\??\s*:/.test(code);
    
          if (hasAsProp && !hasAsJsDoc) {
            violations.push({
              ruleId: 'as-prop-documented',
              message: "The 'as' prop should have JSDoc documenting valid elements",
              suggestion: `Add JSDoc to 'as' prop:
    
    interface BoxProps {
      /**
       * The HTML element to render as
       * @default 'div'
       * @example 'section', 'article', 'aside', 'main'
       */
      as?: 'div' | 'section' | 'article' | 'aside' | 'main' | 'header' | 'footer';
    }`
            });
          }
    
          return violations;
        },
        example: {
          bad: `interface BoxProps {
      as?: React.ElementType;  // No documentation
    }`,
          good: `interface BoxProps {
      /**
       * The HTML element to render as
       * @default 'div'
       * @example 'section', 'article', 'aside', 'main'
       */
      as?: 'div' | 'section' | 'article' | 'aside' | 'main' | 'header' | 'footer';
    }`
        }
      },
    
      // ============================================
      // DOCUMENTATION RULES
      // ============================================
      {
        id: 'variants-documented',
        name: 'Variants Are Documented',
        description: 'Variant props should have JSDoc comments explaining their purpose',
        category: 'types',
        severity: 'info',
        weight: 3,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Check for variant/size props without JSDoc
          const variantProps = code.match(/(?:variant|size)\??\s*:\s*['"][^'"]+['"]\s*\|/g);
    
          if (variantProps && variantProps.length > 0) {
            // Check if there's JSDoc before the type definition
            const hasJsDoc = /\/\*\*[\s\S]*?\*\/\s*(?:export\s+)?(?:type|interface)\s+\w+Props/.test(code);
            const hasInlineJsDoc = /@(?:default|description)/.test(code);
    
            if (!hasJsDoc && !hasInlineJsDoc) {
              violations.push({
                ruleId: 'variants-documented',
                message: 'Variant props should have JSDoc comments',
                suggestion: 'Add /** @default "primary" */ or descriptive comments above variant props'
              });
            }
          }
    
          return violations;
        },
        example: {
          bad: `type ButtonProps = {
      variant?: 'primary' | 'secondary';
      size?: 'sm' | 'md' | 'lg';
    };`,
          good: `type ButtonProps = {
      /**
       * The visual style of the button
       * @default "primary"
       */
      variant?: 'primary' | 'secondary';
      /**
       * The size of the button
       * @default "md"
       */
      size?: 'sm' | 'md' | 'lg';
    };`
        }
      },
    
      {
        id: 'extracts-repeated-patterns',
        name: 'Extracts Repeated Patterns',
        description: 'Repeated style patterns should be extracted into shared utilities',
        category: 'styling',
        severity: 'info',
        weight: 3,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Common patterns that should be extracted
          const commonPatterns = [
            { pattern: /focus-visible:outline-none\s+focus-visible:ring/g, name: 'focus ring' },
            { pattern: /disabled:pointer-events-none\s+disabled:opacity/g, name: 'disabled state' },
            { pattern: /transition-(?:all|colors|opacity|transform)/g, name: 'transition' },
          ];
    
          for (const { pattern, name } of commonPatterns) {
            const matches = code.match(pattern);
            if (matches && matches.length >= 3) {
              violations.push({
                ruleId: 'extracts-repeated-patterns',
                message: `The ${name} pattern appears ${matches.length} times - consider extracting to a shared utility`,
                suggestion: `Create a utility: export const ${name.replace(' ', '')} = '${matches[0]}'; and import it`
              });
            }
          }
    
          return violations;
        },
        example: {
          bad: `// Same focus ring copied everywhere
    <Button className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500" />
    <Input className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500" />`,
          good: `// utils/styles.ts
    export const focusRing = 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring';
    export const disabled = 'disabled:pointer-events-none disabled:opacity-50';
    
    // In components
    <Button className={cn(focusRing, disabled, className)} />`
        }
      },
    
      // ============================================
      // ASCHILD PATTERN RULES
      // ============================================
      {
        id: 'aschild-spreads-props',
        name: 'asChild Component Spreads Props',
        description: 'Components used with asChild must spread props to receive parent behavior',
        category: 'composition',
        severity: 'error',
        weight: 10,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Check for components that DON'T spread props
          // Pattern: function/const Component = ({ children }) => ... (no ...props)
          const noSpreadPattern = /(?:const|function)\s+(\w+)\s*=?\s*\(\s*\{\s*(?:children|className)(?:\s*,\s*(?:children|className))*\s*\}\s*\)/g;
    
          let match;
          while ((match = noSpreadPattern.exec(code)) !== null) {
            const componentName = match[1];
            // Skip if this looks like a type definition
            if (code.includes(`${componentName}Props`)) {
              violations.push({
                ruleId: 'aschild-spreads-props',
                message: `${componentName} doesn't spread props - won't work correctly with asChild`,
                suggestion: `Always spread props to receive parent behavior:
    
    // ❌ Won't receive trigger behavior
    const ${componentName} = ({ children }) => <div>{children}</div>;
    
    // ✅ Properly receives all props
    const ${componentName} = ({ children, ...props }) => (
      <div {...props}>{children}</div>
    );`
              });
            }
          }
    
          return violations;
        },
        example: {
          bad: `// Won't work with asChild - props not spread
    const Button = ({ children }) => <button>{children}</button>;`,
          good: `// Works with asChild - props are spread
    const Button = ({ children, ...props }) => (
      <button {...props}>{children}</button>
    );`
        }
      },
    
      // ============================================
      // ACCESSIBILITY ADVANCED RULES
      // ============================================
      {
        id: 'nav-has-label',
        name: 'Navigation Has Label',
        description: 'Navigation elements should have aria-label for screen readers',
        category: 'accessibility',
        severity: 'warning',
        weight: 5,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Check for <nav> without aria-label
          const navWithoutLabel = /<nav(?![^>]*aria-label)/g;
          const matches = code.match(navWithoutLabel);
    
          if (matches && matches.length > 0) {
            violations.push({
              ruleId: 'nav-has-label',
              message: `Found ${matches.length} <nav> element(s) without aria-label`,
              suggestion: `Add aria-label to describe the navigation:
    
    <nav aria-label="Main navigation">
    <nav aria-label="Breadcrumb">
    <nav aria-label="Footer links">`
            });
          }
    
          return violations;
        },
        example: {
          bad: `<nav>
      <ul>...</ul>
    </nav>`,
          good: `<nav aria-label="Main navigation">
      <ul>...</ul>
    </nav>`
        }
      },
    
      {
        id: 'focus-visible-styles',
        name: 'Uses Focus Visible',
        description: 'Use focus-visible instead of focus for keyboard-only focus styles',
        category: 'accessibility',
        severity: 'info',
        weight: 3,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Check for :focus without :focus-visible
          const focusWithoutVisible = /(?:^|[^-])focus:/g;
          const hasFocusVisible = /focus-visible:/.test(code);
    
          const matches = code.match(focusWithoutVisible);
          if (matches && matches.length > 2 && !hasFocusVisible) {
            violations.push({
              ruleId: 'focus-visible-styles',
              message: 'Using focus: instead of focus-visible: - focus styles will show on mouse click',
              suggestion: `Use focus-visible: to only show focus styles for keyboard navigation:
    
    // ❌ Shows focus ring on mouse click too
    className="focus:ring-2 focus:ring-blue-500"
    
    // ✅ Only shows focus ring for keyboard users
    className="focus-visible:ring-2 focus-visible:ring-blue-500"`
            });
          }
    
          return violations;
        },
        example: {
          bad: `<button className="focus:outline-none focus:ring-2">`,
          good: `<button className="focus-visible:outline-none focus-visible:ring-2">`
        }
      },
    
      {
        id: 'input-has-label',
        name: 'Input Has Associated Label',
        description: 'Form inputs should have associated labels or aria-label',
        category: 'accessibility',
        severity: 'warning',
        weight: 8,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Check for input without aria-label and not near a label
          const inputPattern = /<input(?![^>]*(?:aria-label|aria-labelledby|id=))[^>]*>/g;
          const matches = code.match(inputPattern);
    
          // Only flag if there's no <label> in the code at all
          const hasLabel = /<label/.test(code);
    
          if (matches && matches.length > 0 && !hasLabel) {
            violations.push({
              ruleId: 'input-has-label',
              message: `Found input(s) without labels or aria-label`,
              suggestion: `Associate inputs with labels:
    
    // Option 1: Wrapping label
    <label>
      Email
      <input type="email" />
    </label>
    
    // Option 2: Using htmlFor/id
    <label htmlFor="email">Email</label>
    <input id="email" type="email" />
    
    // Option 3: aria-label for icon inputs
    <input type="search" aria-label="Search" />`
            });
          }
    
          return violations;
        },
        example: {
          bad: `<input type="email" placeholder="Email" />`,
          good: `<label>
      Email
      <input type="email" />
    </label>`
        }
      },
    
      {
        id: 'prefers-aria-disabled',
        name: 'Prefers aria-disabled Over disabled',
        description: 'Consider aria-disabled over disabled to maintain focusability',
        category: 'accessibility',
        severity: 'info',
        weight: 3,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Check for disabled without aria-disabled explanation
          const hasDisabled = /disabled(?:\s*=\s*\{|\s*\})/.test(code);
          const hasAriaDisabled = /aria-disabled/.test(code);
          const hasExplanation = /aria-describedby|disabled.*help|disabled.*explain/i.test(code);
    
          if (hasDisabled && !hasAriaDisabled && !hasExplanation) {
            violations.push({
              ruleId: 'prefers-aria-disabled',
              message: 'Using disabled attribute - users cannot focus to understand why',
              suggestion: `Consider aria-disabled with explanation:
    
    // ❌ User can't understand why button is disabled
    <button disabled={!isValid}>Submit</button>
    
    // ✅ User can focus and learn why
    <button
      aria-disabled={!isValid}
      aria-describedby="submit-help"
      onClick={isValid ? handleSubmit : undefined}
      className={!isValid ? 'opacity-50 cursor-not-allowed' : ''}
    >
      Submit
    </button>
    <span id="submit-help">
      {!isValid && 'Please fill in all required fields'}
    </span>`
            });
          }
    
          return violations;
        },
        example: {
          bad: `<button disabled={!isValid}>Submit</button>`,
          good: `<button
      aria-disabled={!isValid}
      aria-describedby="submit-help"
    >Submit</button>
    <span id="submit-help">{!isValid && 'Complete required fields'}</span>`
        }
      },
    
      {
        id: 'cva-outside-component',
        name: 'CVA Defined Outside Component',
        description: 'CVA variants should be defined outside the component to avoid recreation',
        category: 'styling',
        severity: 'warning',
        weight: 5,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Check for cva() inside a function component
          const componentPattern = /(?:function|const)\s+\w+\s*(?:=|[\(<])[\s\S]*?cva\(/g;
    
          // More specific: cva inside the component body
          const cvaInsideComponent = /(?:function|const)\s+\w+\s*(?:=\s*\([^)]*\)\s*=>|[\(<])[^}]*\breturn\b[^}]*\bcva\(/;
    
          if (cvaInsideComponent.test(code)) {
            violations.push({
              ruleId: 'cva-outside-component',
              message: 'CVA variants defined inside component - recreated on every render',
              suggestion: `Define CVA variants outside the component:
    
    // ❌ Recreated on every render
    function Button({ variant }) {
      const buttonVariants = cva('...', { variants: {...} });
      return <button className={buttonVariants({ variant })} />;
    }
    
    // ✅ Defined once, reused
    const buttonVariants = cva('...', { variants: {...} });
    
    function Button({ variant }) {
      return <button className={buttonVariants({ variant })} />;
    }`
            });
          }
    
          return violations;
        },
        example: {
          bad: `function Button({ variant }) {
      const buttonVariants = cva('base', { variants: {} });
      return <button className={buttonVariants({ variant })} />;
    }`,
          good: `const buttonVariants = cva('base', { variants: {} });
    
    function Button({ variant }) {
      return <button className={buttonVariants({ variant })} />;
    }`
        }
      },
    
      {
        id: 'live-region-for-dynamic',
        name: 'Live Region for Dynamic Content',
        description: 'Dynamic content changes should use aria-live for screen reader announcements',
        category: 'accessibility',
        severity: 'info',
        weight: 3,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Check for loading states without aria-live
          const hasLoadingState = /isLoading|loading|\.loading/i.test(code);
          const hasErrorState = /error|Error|isError/i.test(code);
          const hasAriaLive = /aria-live|role="alert"|role="status"/.test(code);
    
          if ((hasLoadingState || hasErrorState) && !hasAriaLive) {
            violations.push({
              ruleId: 'live-region-for-dynamic',
              message: 'Dynamic loading/error states should announce to screen readers',
              suggestion: `Add aria-live for dynamic content:
    
    // Loading states
    <div aria-live="polite" aria-busy={isLoading}>
      {isLoading ? "Loading..." : \`\${items.length} items\`}
    </div>
    
    // Error messages
    <div role="alert" aria-live="assertive">
      {error && \`Error: \${error.message}\`}
    </div>
    
    // Status updates
    <div role="status" aria-live="polite">
      {saved && "Changes saved"}
    </div>`
            });
          }
    
          return violations;
        },
        example: {
          bad: `{isLoading && <span>Loading...</span>}
    {error && <span className="text-red-500">{error}</span>}`,
          good: `<div aria-live="polite" aria-busy={isLoading}>
      {isLoading ? "Loading..." : "Content loaded"}
    </div>
    <div role="alert">{error}</div>`
        }
      },
    
      {
        id: 'touch-target-size',
        name: 'Touch Target Size',
        description: 'Interactive elements should have minimum 44x44px touch targets',
        category: 'accessibility',
        severity: 'info',
        weight: 3,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Check for small size classes on interactive elements
          const smallInteractive = /(?:<button|<a\s|onClick)[^>]*(?:size-\d|w-\d|h-\d|p-1|p-0\.5)/g;
          const matches = code.match(smallInteractive);
    
          // Check for size utilities smaller than min touch target (44px ≈ h-11, w-11)
          const tooSmall = /(?:size-[1-9]|w-[1-9]|h-[1-9]|size-10|w-10|h-10)(?![0-9])/;
    
          if (matches && matches.some(m => tooSmall.test(m))) {
            violations.push({
              ruleId: 'touch-target-size',
              message: 'Interactive element may be too small for touch (min 44x44px)',
              suggestion: `Ensure minimum 44x44px touch targets:
    
    // ❌ Too small for touch
    <button className="size-6">
      <Icon />
    </button>
    
    // ✅ Minimum touch target
    <button className="size-11 p-2">
      <Icon className="size-6" />
    </button>
    
    // ✅ Or use invisible touch area
    <button className="relative size-6">
      <span className="absolute -inset-2" /> <!-- Extends touch area -->
      <Icon />
    </button>`
            });
          }
    
          return violations;
        },
        example: {
          bad: `<button className="size-6" onClick={...}>
      <XIcon />
    </button>`,
          good: `<button className="size-11 p-2" onClick={...}>
      <XIcon className="size-6" />
    </button>`
        }
      },
    
      // ============================================
      // ARTIFACT CLASSIFICATION RULES
      // ============================================
      {
        id: 'block-vs-component',
        name: 'Block vs Component Classification',
        description: 'Blocks should be production-ready compositions, not reusable components',
        category: 'composition',
        severity: 'warning',
        weight: 5,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Indicators that this is a BLOCK (correct for blocks):
          // - Multiple component imports
          // - Hardcoded content/copy
          // - Multiple sections/layouts
          // - Specific use-case names (Hero, Pricing, Features, etc.)
          const isBlock = /(?:Hero|Pricing|Features|Footer|Header|Auth|Onboarding|Dashboard|Settings|Billing|Chat|Landing)/i.test(code);
          const hasMultipleImports = (code.match(/import.*from/g) || []).length > 5;
          const hasHardcodedContent = /"[A-Z][^"]{20,}"|'[A-Z][^']{20,}'/.test(code); // Long strings with capital start
    
          // Indicators that this should be a COMPONENT (problem if in a block):
          // - Exports multiple sub-components (compound pattern)
          // - Has variants/CVA
          // - No hardcoded content
          // - Generic naming (Button, Card, Input)
          const exportsMultiple = (code.match(/export\s+(?:const|function)/g) || []).length > 3;
          const hasCVA = /cva\(/.test(code);
          const hasVariants = /variants?\s*[:=]/.test(code);
    
          // If it looks like a block but exports like a component library
          if (isBlock && exportsMultiple && hasCVA) {
            violations.push({
              ruleId: 'block-vs-component',
              message: 'This appears to be a Block but exports multiple components with CVA variants',
              suggestion: `Blocks should:
    - Be copy-paste friendly compositions
    - Import components, not define them
    - Have opinionated content/layout
    - Not export reusable sub-components with variants
    
    If this is meant to be reusable, it should be a Component.
    If it's a Block, move the sub-components to separate files.
    
    From definitions.mdx:
    "Blocks are typically not reusable like a component. You don't import them,
    but they typically import components and primitives."`
            });
          }
    
          // If it looks like a component but has hardcoded content like a block
          if (!isBlock && hasHardcodedContent && !hasMultipleImports && hasCVA) {
            violations.push({
              ruleId: 'block-vs-component',
              message: 'Component has hardcoded content - should this be a Block instead?',
              suggestion: `Components should:
    - Accept content via props/children
    - Not contain hardcoded copy/text
    - Be reusable across different contexts
    
    If this has specific content for a use-case, it's a Block, not a Component.`
            });
          }
    
          return violations;
        },
        example: {
          bad: `// ❌ Block with component-style exports
    export const HeroTitle = ({ variant }) => cva(...)
    export const HeroDescription = ({ variant }) => cva(...)
    export const HeroButton = ({ variant }) => cva(...)
    
    // ❌ Component with hardcoded content
    export const Button = () => (
      <button>Get Started for Free Today!</button>
    )`,
          good: `// ✅ Block imports components, has opinionated content
    import { Button } from '@/components/ui/button';
    
    export function HeroBlock() {
      return (
        <section>
          <h1>Get Started for Free Today!</h1>
          <Button>Sign Up</Button>
        </section>
      );
    }
    
    // ✅ Component accepts content via props
    export const Button = ({ children, ...props }) => (
      <button {...props}>{children}</button>
    )`
        }
      },
    
      {
        id: 'component-single-responsibility',
        name: 'Component Single Responsibility',
        description: 'Components should wrap a single element, not multiple unrelated elements',
        category: 'composition',
        severity: 'warning',
        weight: 5,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Check for components that render multiple root-level elements without composition
          // Pattern: return ( <div>...<div>...<div>... ) without clear compound structure
          const hasMultipleRootElements = /<(?:div|section|article)[^>]*>[\s\S]*<(?:div|section|article)[^>]*>[\s\S]*<(?:div|section|article)[^>]*>/;
    
          // Check if it's using compound component pattern (which is OK)
          const isCompound = /\w+\.\w+\s*=/.test(code) || /export\s+const\s+\w+\s*=.*Object\.assign/.test(code);
    
          // Check if component accepts children (compositional)
          const acceptsChildren = /children/.test(code);
    
          // If it has many nested divs but isn't compound and doesn't accept children
          if (hasMultipleRootElements.test(code) && !isCompound && !acceptsChildren) {
            // Check if it's a block (which can have multiple elements)
            const isBlock = /(?:Hero|Pricing|Features|Footer|Header|Block|Section|Page)/i.test(code);
    
            if (!isBlock) {
              violations.push({
                ruleId: 'component-single-responsibility',
                message: 'Component renders multiple elements - consider composition pattern',
                suggestion: `From definitions.mdx: Components should wrap a single element.
    
    Either:
    1. Break into compound sub-components (Card.Header, Card.Content)
    2. Accept children for flexible composition
    3. If this is a full section/layout, it's a Block, not a Component
    
    // ❌ Too much responsibility
    const Card = ({ title, description, footer }) => (
      <div>
        <div className="header"><h2>{title}</h2></div>
        <div className="body"><p>{description}</p></div>
        <div className="footer">{footer}</div>
      </div>
    );
    
    // ✅ Composable
    const Card = ({ children }) => <div>{children}</div>;
    const CardHeader = ({ children }) => <div>{children}</div>;
    const CardContent = ({ children }) => <div>{children}</div>;`
              });
            }
          }
    
          return violations;
        },
        example: {
          bad: `// Component doing too much
    const Card = ({ title, desc, footer, image }) => (
      <div>
        <img src={image} />
        <h2>{title}</h2>
        <p>{desc}</p>
        <div>{footer}</div>
      </div>
    );`,
          good: `// Composable compound components
    const Card = ({ children }) => <div>{children}</div>;
    Card.Image = ({ src }) => <img src={src} />;
    Card.Title = ({ children }) => <h2>{children}</h2>;
    Card.Description = ({ children }) => <p>{children}</p>;`
        }
      },
    
      {
        id: 'primitive-should-be-unstyled',
        name: 'Primitive Should Be Unstyled',
        description: 'If building a primitive, it should not include styling',
        category: 'composition',
        severity: 'info',
        weight: 3,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Check if this is named like a primitive
          const isPrimitiveName = /(?:Primitive|Headless|Unstyled|Base)/.test(code);
    
          // Check for styling
          const hasStyling = /className=|style=|styled\.|css`/.test(code);
          const hasTailwind = /(?:bg-|text-|p-|m-|flex|grid|rounded|border)/.test(code);
    
          if (isPrimitiveName && (hasStyling || hasTailwind)) {
            violations.push({
              ruleId: 'primitive-should-be-unstyled',
              message: 'This appears to be a Primitive but includes styling',
              suggestion: `From definitions.mdx:
    "A primitive is the lowest-level building block that provides
    behavior and accessibility WITHOUT any styling."
    
    Primitives should:
    - Be completely unstyled (headless)
    - Encapsulate behavior, a11y, keyboard interaction
    - Let consumers provide all styling
    
    If you need styling, this is a Component, not a Primitive.`
            });
          }
    
          return violations;
        },
        example: {
          bad: `// Primitive with styling - contradiction
    const PrimitiveButton = ({ children }) => (
      <button className="bg-blue-500 rounded-lg p-2">
        {children}
      </button>
    );`,
          good: `// True primitive - behavior only
    const PrimitiveButton = ({ children, ...props }) => (
      <button
        type="button"
        role="button"
        {...props}
      >
        {children}
      </button>
    );`
        }
      },
    
      {
        id: 'utility-should-not-render',
        name: 'Utility Should Not Render UI',
        description: 'Utilities should be non-visual helpers, not components',
        category: 'composition',
        severity: 'info',
        weight: 3,
        check: (code) => {
          const violations: RuleViolation[] = [];
    
          // Check if this is named like a utility
          const isUtilityName = /(?:use[A-Z]|util|helper|hook)/i.test(code);
          const isExportedFunction = /export\s+(?:const|function)\s+(?:use[A-Z]|create|get|format|parse|is[A-Z])/.test(code);
    
          // Check if it renders JSX
          const rendersJSX = /<[A-Z]|<[a-z]+[^>]*>/.test(code);
          const hasReturn = /return\s*\(?\s*</.test(code);
    
          if ((isUtilityName || isExportedFunction) && (rendersJSX && hasReturn)) {
            // Exception: hooks can return elements
            const isHookReturningElement = /use[A-Z]\w*.*return.*</.test(code);
    
            if (!isHookReturningElement) {
              violations.push({
                ruleId: 'utility-should-not-render',
                message: 'This appears to be a Utility but renders UI',
                suggestion: `From definitions.mdx:
    "A utility is a helper exported for developer ergonomics
    or composition; NOT rendered UI."
    
    Utilities should:
    - Be side-effect free
    - Not render any JSX
    - Be testable in isolation
    
    If this renders UI, it's a Component, not a Utility.`
              });
            }
          }
    
          return violations;
        },
        example: {
          bad: `// Utility that renders - contradiction
    export function formatDate(date) {
      return <span>{date.toLocaleDateString()}</span>;
    }`,
          good: `// True utility - returns data, not UI
    export function formatDate(date: Date): string {
      return date.toLocaleDateString();
    }
    
    // Component uses the utility
    const DateDisplay = ({ date }) => (
      <span>{formatDate(date)}</span>
    );`
        }
      }
    ];
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries full burden. It discloses the return values (score, grade, violations, suggestions) but lacks details on behavioral traits such as performance implications, error handling, or whether grading is idempotent. For a tool with no annotations, this leaves significant gaps in understanding how it behaves beyond basic output.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence that front-loads the core action and output, with zero wasted words. It directly communicates the tool's purpose and results without unnecessary elaboration, making it easy to parse quickly.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given no annotations, no output schema, and 2 parameters with full schema coverage, the description is minimally adequate. It covers the basic purpose and output but lacks depth on usage context, behavioral details, or integration with sibling tools. For a grading tool with potential complexity, more completeness would be beneficial.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents both parameters ('code' and 'verbose'). The description adds no additional meaning beyond what the schema provides, such as explaining the grading process or how 'verbose' affects output. Baseline 3 is appropriate when the schema handles parameter documentation adequately.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action ('Grade') and resource ('a component against all rules'), specifying what the tool does. It distinguishes from siblings like 'check_compliance' or 'get_rule' by focusing on grading with comprehensive rule evaluation. However, it doesn't explicitly differentiate from 'check_compliance' which might have overlapping functionality.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives like 'check_compliance' or 'list_rules'. It mentions grading against 'all rules' but doesn't specify prerequisites, context, or exclusions, leaving the agent to infer usage scenarios without explicit direction.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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/getlokiui/components-build-mcp'

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