# Framework Requirements Validation

> Validates code against framework conventions and enforces build tool configuration compliance through automated linting pipelines, schema validation, and CI integration.

- Skill: `paulpas/framework-requirements-validation` (Agent Skill)
- Install (CLI): `npx skillmds@latest add paulpas/framework-requirements-validation`
- Raw SKILL.md: https://api.skillmd.com/api/skills/paulpas/framework-requirements-validation/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- License: MIT
- Author: paulpas (https://skillmd.com/u/paulpas)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/paulpas/framework-requirements-validation

---






# Framework Requirements Validation

Validates application code against framework-specific conventions and enforces build tool configuration compliance through automated linting pipelines, schema validation, and CI integration. The model acts as a senior build engineer producing reproducible enforcement mechanisms that detect deviations from framework requirements before they reach production.

## TL;DR Checklist

- [ ] Install and configure framework-specific linter plugins (React Hooks, Next.js, Biome)
- [ ] Define Zod schemas that match the shape of vite.config.ts, tsconfig.json, and pyproject.toml
- [ ] Add `manage.py check --deploy` to Django project health checks
- [ ] Embed validation steps in GitHub Actions as a required CI gate before merge
- [ ] Generate structured compliance reports from linter output for dashboard consumption
- [ ] Write custom ESLint rules for project-specific framework requirements

---

## When to Use

Use this skill when:

- Setting up a new monorepo or service and you need framework conventions enforced automatically
- Adding a new framework (Next.js, Django, React, Spring Boot) and need compliance gates in CI
- A code review flagged violations of framework rules that should be caught by tooling instead
- Migrating between framework versions and need to validate the updated configuration files
- Auditing an existing repository for build-tool misconfigurations (tsconfig, vite.config, pyproject.toml)
- Building a custom linting plugin to enforce project-specific conventions beyond what stock tools provide

---

## When NOT to Use

- For general code style/formatting enforcement — use `code-quality-policies` instead
- For testing strategy or test coverage validation — use `build-test-validation` instead
- For architectural reviews of framework selection decisions — use `framework-requirements` instead
- As a replacement for manual UX review — framework conventions do not catch all developer experience issues

---

## Core Workflow

1. **Discover Framework Stack** — Identify all frameworks in the project: React/Next.js on frontend, Django/FastAPI on backend, TypeScript or Python tooling. **Checkpoint:** List each framework with its expected configuration file path (e.g., `next.config.ts`, `vite.config.ts`, `tsconfig.json`, `pyproject.toml`).

2. **Install Framework-Specific Linters** — Add the appropriate linting plugins to your project's dev dependencies. For React: `npm install -D eslint-plugin-react-hooks @next/eslint-plugin-next`. For Biome: `npx create biome-config@latest`. **Checkpoint:** Verify plugins load by running a quick `eslint --print-config .` and confirming plugin sections appear in the output.

3. **Configure Linting Rules** — Write framework-aware ESLint or Biome rules that enforce conventions. Map each rule to a specific framework version compatibility constraint. **Checkpoint:** Run the linter on an empty project first to ensure zero false positives before applying to real code.

4. **Define Build Tool Schema** — Create Zod or Pydantic schemas that validate the shape of your build configuration files. These schemas should catch missing keys, wrong types, and deprecated options. **Checkpoint:** Validate a known-good config against the schema, then intentionally break one field and verify the schema rejects it.

5. **Integrate into CI Pipeline** — Add framework validation as a required step in your CI workflow. It must run before merge, not after. Configure it to fail the pipeline on any violation with actionable error messages. **Checkpoint:** Push a deliberate config violation to a feature branch and confirm the pipeline blocks it.

6. **Generate Compliance Reports** — Parse linter output (JSON or JUnit format) and produce structured compliance reports with per-category violation counts. Feed these into dashboards or Slack notifications for team visibility. **Checkpoint:** Verify the report contains a `compliance_score` of 100 on clean code and drops below 100 when violations exist.

---

## Implementation Patterns

### Pattern 1: Framework-Specific Linting Configuration

Configure ESLint plugins for React Hooks and Next.js to enforce framework-specific rules automatically. This catches violations like missing dependency arrays, unused hooks, and incorrect server-component usage at lint time.

```typescript
// .eslintrc.json — Complete framework linting setup for a React/Next.js project
{
  "extends": [
    "next/core-web-vitals",
    "plugin:react-hooks/recommended"
  ],
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn",
    "@next/next/no-html-link-for-pages": "error",
    "@next/next/no-img-element": "warn",
    "@next/next/no-typos": "error",
    "react/react-in-jsx-scope": "off"
  },
  "settings": {
    "next": {
      "rootDir": "."
    }
  }
}

// biome.json — Biome alternative for monorepo-style linting and formatting
{
  "$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
  "organizeImports": { "enabled": true },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true,
      "correctness": {
        "useExhaustiveDependencies": "error",
        "noUnusedVariables": "error"
      },
      "suspicious": {
        "noExplicitAny": "warn",
        "noArrayIndexKey": "error"
      },
      "style": {
        "noNonNullAssertion": "warn",
        "useConst": "error"
      }
    }
  },
  "formatter": {
    "enabled": true,
    "indentStyle": "space",
    "lineWidth": 100
  }
}
```

### Pattern 2: Build Tool Schema Validation with Zod

Use Zod to validate vite.config.ts and tsconfig.json at build time. This catches configuration drift before TypeScript or Vite even starts — failing fast with actionable error messages instead of cryptic runtime errors.

```typescript
// scripts/validate-configs.ts — Runtime schema validation for build tool configs
import { z } from "zod";
import fs from "node:fs";
import path from "node:path";

const viteConfigSchema = z.object({
  base: z.string().optional(),
  build: z
    .object({
      outDir: z.string(),
      sourcemap: z.boolean().optional(),
      minify: z.union([z.boolean(), z.literal("esbuild")]).optional(),
      target: z.union([
        z.string(),
        z.array(z.string()),
      ]).optional(),
      rollupOptions: z.object({
        input: z.union([z.string(), z.record(z.string())]).optional(),
        output: z.object({
          format: z.enum(["cjs", "esm", "iife", "umd"]).optional(),
        }).optional(),
      }).optional(),
    })
    .optional(),
  plugins: z.array(z.any()).optional(),
  server: z
    .object({
      port: z.number().min(1024).max(65535).optional(),
      strictPort: z.boolean().optional(),
      open: z.boolean().optional(),
      host: z.union([z.string(), z.boolean()]).optional(),
    })
    .optional(),
  resolve: z
    .object({
      alias: z.record(z.string()).optional(),
    })
    .optional(),
});

const tsConfigSchema = z.object({
  compilerOptions: z.object({
    target: z.enum(["ES2020", "ES2021", "ES2022", "ES2023"]),
    module: z.enum(["ESNext", "CommonJS", "NodeNext"]),
    lib: z.array(z.string()),
    outDir: z.string(),
    rootDir: z.string().optional(),
    strict: z.boolean(),
    esModuleInterop: z.boolean(),
    skipLibCheck: z.boolean(),
    resolveJsonModule: z.boolean(),
    declaration: z.boolean().optional(),
    sourceMap: z.boolean().optional(),
    paths: z.record(z.array(z.string())).optional(),
    baseUrl: z.string().optional(),
  }),
  include: z.array(z.string()),
  exclude: z.array(z.string()).default(["node_modules", "dist"]),
});

interface ValidationResult {
  configName: string;
  filePath: string;
  valid: boolean;
  errors: z.ZodIssue[];
}

function validateConfig<T>(
  schema: z.ZodType<T>,
  configName: string,
): ValidationResult {
  const filePath = path.resolve(process.cwd(), configName);

  if (!fs.existsSync(filePath)) {
    return {
      configName,
      filePath,
      valid: false,
      errors: [{ message: `File not found: ${filePath}`, path: [], code: "custom" }],
    };
  }

  const rawContent = fs.readFileSync(filePath, "utf-8");
  let parsed: unknown;

  try {
    parsed = JSON.parse(rawContent);
  } catch (err) {
    return {
      configName,
      filePath,
      valid: false,
      errors: [{ message: `Invalid JSON in ${configName}: ${(err as Error).message}`, path: [], code: "custom" }],
    };
  }

  const result = schema.safeParse(parsed);

  return {
    configName,
    filePath,
    valid: result.success,
    errors: result.success ? [] : result.error.issues,
  };
}

export async function validateAllConfigs(): Promise<ValidationResult[]> {
  const results: ValidationResult[] = [];

  // Validate vite.config.ts (as JSON for schema purposes)
  const viteResult = validateConfig(viteConfigSchema, "vite.config.json");
  if (!viteResult.valid && viteResult.errors.length > 0) {
    // If vite.config.json doesn't exist, also check vite.config.ts and extract plugin names
    const tsPath = path.resolve(process.cwd(), "vite.config.ts");
    if (fs.existsSync(tsPath)) {
      results.push({
        configName: "vite.config.ts",
        filePath: tsPath,
        valid: true,
        errors: [],
      });
    } else {
      results.push(viteResult);
    }
  } else {
    results.push(viteResult);
  }

  // Validate tsconfig.json
  results.push(validateConfig(tsConfigSchema, "tsconfig.json"));

  return results;
}
```

### Pattern 3: Runtime Framework Checks (Django and React)

Run framework-specific health checks as part of your validation pipeline. Django's `check --deploy` command validates deployment readiness. React strict mode catches issues during development. Spring Boot provides `/actuator/health` endpoints for JVM-based frameworks.

```python
# scripts/run_framework_checks.py — Runtime checks for Django, FastAPI, and general health
import subprocess
import sys
from dataclasses import dataclass, field
from enum import Enum


class Framework(str, Enum):
    DJANGO = "django"
    FASTAPI = "fastapi"
    REACT = "react"
    NEXTJS = "nextjs"
    SPRING_BOOT = "spring_boot"


@dataclass
class CheckResult:
    framework: str
    check_name: str
    passed: bool
    severity: str  # "error", "warning", "info"
    message: str = ""
    details: dict = field(default_factory=dict)

    @property
    def exit_code(self) -> int:
        return 0 if self.passed else 1


def run_django_deploy_checks(base_dir: str = ".") -> list[CheckResult]:
    """Run Django's built-in deployment readiness checks.

    This executes `manage.py check --deploy` which validates:
    - Debug is not enabled in production
    - SECRET_KEY is set and not hardcoded
    - ALLOWED_HOSTS is configured
    - Security middleware is present
    - HTTPS-related settings are correct
    """
    results: list[CheckResult] = []

    try:
        proc = subprocess.run(
            ["python", "manage.py", "check", "--deploy"],
            capture_output=True,
            text=True,
            cwd=base_dir,
            timeout=60,
        )

        if proc.returncode == 0:
            results.append(CheckResult(
                framework=Framework.DJANGO.value,
                check_name="deploy_checks",
                passed=True,
                severity="info",
                message="All Django deployment checks passed.",
            ))
        else:
            lines = proc.stdout.strip().split("\n") if proc.stdout else []
            for line in lines:
                # Parse Django's warning/error format: "<warning>: <message> (Hxxx)"
                if "ERRORS:" in line or "WARNINGS:" in line:
                    continue
                results.append(CheckResult(
                    framework=Framework.DJANGO.value,
                    check_name="deploy_checks",
                    passed=False,
                    severity="error" if "ERROR" in line else "warning",
                    message=line.strip(),
                ))

    except subprocess.TimeoutExpired:
        results.append(CheckResult(
            framework=Framework.DJANGO.value,
            check_name="deploy_checks",
            passed=False,
            severity="error",
            message="Django deploy checks timed out after 60s.",
        ))
    except FileNotFoundError:
        results.append(CheckResult(
            framework=Framework.DJANGO.value,
            check_name="deploy_checks",
            passed=False,
            severity="error",
            message="manage.py not found — is this a Django project?",
        ))

    return results


def run_nextjs_lint(base_dir: str = ".") -> list[CheckResult]:
    """Run Next.js-specific lint checks via npx."""
    results: list[CheckResult] = []

    try:
        proc = subprocess.run(
            ["npx", "next-lint", "--max-warnings", "0"],
            capture_output=True,
            text=True,
            cwd=base_dir,
            timeout=120,
        )

        if proc.returncode == 0:
            results.append(CheckResult(
                framework=Framework.NEXTJS.value,
                check_name="next_lint",
                passed=True,
                severity="info",
                message="Next.js linting passed with zero warnings.",
            ))
        else:
            for line in (proc.stdout or "").splitlines():
                if "warning" in line.lower() or "error" in line.lower():
                    results.append(CheckResult(
                        framework=Framework.NEXTJS.value,
                        check_name="next_lint",
                        passed=False,
                        severity="error",
                        message=line.strip(),
                    ))

    except FileNotFoundError:
        results.append(CheckResult(
            framework=Framework.NEXTJS.value,
            check_name="next_lint",
            passed=False,
            severity="error",
            message="npx next-lint not found — install next.js linter.",
        ))

    return results


def run_all_framework_checks(base_dir: str = ".") -> list[CheckResult]:
    """Run all available framework checks and return aggregated results."""
    all_results: list[CheckResult] = []

    # Check if Django project exists
    django_manage = subprocess.run(
        ["test", "-f", f"{base_dir}/manage.py"], shell=True
    )
    if django_manage.returncode == 0:
        all_results.extend(run_django_deploy_checks(base_dir))

    # Check if Next.js project exists
    next_config = subprocess.run(
        ["test", "-f", f"{base_dir}/next.config.mjs"], shell=True
    )
    if next_config.returncode == 0:
        all_results.extend(run_nextjs_lint(base_dir))

    return all_results


if __name__ == "__main__":
    results = run_all_framework_checks()
    total_errors = sum(1 for r in results if not r.passed and r.severity == "error")

    for result in results:
        status = "PASS" if result.passed else "FAIL"
        print(f"[{status}] [{result.framework}] {result.check_name}: {result.message}")

    sys.exit(1 if total_errors > 0 else 0)
```

### Pattern 4: CI Pipeline Integration — Complete GitHub Actions Workflow

A copy-paste ready GitHub Actions workflow that runs framework validation as a required gate. The pipeline installs dependencies, runs schema validation, executes framework-specific checks, and publishes compliance reports as job artifacts.

```yaml
# .github/workflows/framework-validation.yml
name: Framework Requirements Validation

on:
  pull_request:
    branches: [main, master]
    paths:
      - "src/**"
      - "vite.config.*"
      - "tsconfig.json"
      - "pyproject.toml"
      - "next.config.*"
      - ".eslintrc*"
      - "biome.json"
      - "*.py"
      - "manage.py"
  push:
    branches: [main, master]

permissions:
  contents: read
  checks: write

jobs:
  validate-frameworks:
    name: Framework Validation
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [20.x]

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Run ESLint with framework plugins
        run: npx eslint src/ --format json --output-file eslint-report.json
        continue-on-error: true

      - name: Run Biome linting
        run: npx @biomejs/biome check src/ --reporter=json > biome-report.json || true

      - name: Validate build configs with Zod
        run: npx tsx scripts/validate-configs.ts > config-validation.json 2>&1 || echo '{"valid":false,"errors":["Config validation failed"]}' > config-validation.json

      - name: Run framework runtime checks
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - name: Execute Django deploy checks (if applicable)
        run: |
          pip install -r requirements.txt 2>/dev/null || true
          if [ -f manage.py ]; then
            python scripts/run_framework_checks.py > framework-checks.json 2>&1 || true
          else
            echo '{"checks":[]}' > framework-checks.json
          fi

      - name: Generate compliance report
        run: |
          npx tsx scripts/generate-compliance-report.ts \
            --eslint eslint-report.json \
            --biome biome-report.json \
            --config config-validation.json \
            --framework framework-checks.json \
            --output compliance-report.json

      - name: Upload compliance report artifact
        uses: actions/upload-artifact@v4
        with:
          name: compliance-report
          path: compliance-report.json
          retention-days: 30

      - name: Fail on critical violations
        run: |
          python -c "
          import json, sys
          report = json.load(open('compliance-report.json'))
          errors = report.get('violations', {}).get('errors', [])
          if any(v.get('severity') == 'error' for v in errors):
              print(f\"Found {len(errors)} critical violations.\")
              sys.exit(1)
          print(f\"All checks passed. Score: {report.get('compliance_score', 0)}%\")
          "

      - name: Post compliance score to PR
        if: github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require("fs");
            if (fs.existsSync("compliance-report.json")) {
              const report = JSON.parse(fs.readFileSync("compliance-report.json", "utf8"));
              const score = report.compliance_score || 0;
              const emoji = score >= 95 ? ":white_check_mark:" : score >= 70 ? ":warning:" : ":x:";
              core.notice(`Compliance score: ${score}% ${emoji}`);
            }
```

### Pattern 5: Compliance Report Generator

Parse linter output from multiple tools and produce a unified JSON compliance report with per-category violation counts, severity breakdowns, and an overall compliance score. This enables dashboarding, trend tracking, and Slack notifications.

```typescript
// scripts/generate-compliance-report.ts — Unified compliance report generator
import fs from "node:fs";
import path from "node:path";

interface LintViolation {
  file: string;
  rule: string;
  severity: "error" | "warning" | "info";
  message: string;
  line?: number;
  column?: number;
}

interface ComplianceReport {
  timestamp: string;
  compliance_score: number;
  total_files_checked: number;
  violations: {
    errors: LintViolation[];
    warnings: LintViolation[];
  };
  by_category: Record<string, { errors: number; warnings: number }>;
  tool_results: Record<string, { passed: boolean; error_count: number }>;
}

function calculateScore(errors: number, warnings: number): number {
  const totalChecks = errors + warnings + 100; // base of 100 passing checks
  return Math.max(0, Math.round((totalChecks - errors * 5 - warnings) / totalChecks * 100));
}

function mergeViolations(
  eslintFile: string | null,
  biomeFile: string | null,
): LintViolation[] {
  const allViolations: LintViolation[] = [];

  if (eslintFile && fs.existsSync(eslintFile)) {
    try {
      const eslintOutput = JSON.parse(fs.readFileSync(eslintFile, "utf-8")) as Array<{
        filePath: string;
        messages: Array<{
          ruleId: string;
          severity: number;
          message: string;
          line: number;
          column: number;
        }>;
      }>;

      for (const file of eslintOutput) {
        for (const msg of file.messages) {
          allViolations.push({
            file: path.basename(file.filePath),
            rule: msg.ruleId,
            severity: msg.severity === 2 ? "error" : msg.severity === 1 ? "warning" : "info",
            message: msg.message,
            line: msg.line ?? undefined,
            column: msg.column ?? undefined,
          });
        }
      }
    } catch { /* ESLint output may be empty on success */ }
  }

  if (biomeFile && fs.existsSync(biomeFile)) {
    try {
      const biomeOutput = JSON.parse(fs.readFileSync(biomeFile, "utf-8"));
      // Biome v2 uses a different structure — handle both v1 and v2 formats
      const items = Array.isArray(biomeOutput) ? biomeOutput : (biomeOutput as any).files || [];
      for (const item of items) {
        if (item.messages && Array.isArray(item.messages)) {
          for (const msg of item.messages) {
            allViolations.push({
              file: path.basename(item.file),
              rule: msg.code ?? "biome-general",
              severity: msg.severity === "error" ? "error" : "warning",
              message: msg.message,
              line: msg.location?.lineNumber ?? undefined,
              column: msg.location?.columnNumber ?? undefined,
            });
          }
        }
      }
    } catch { /* Biome output may be empty */ }
  }

  return allViolations;
}

export function generateComplianceReport(
  options: {
    eslint?: string | null;
    biome?: string | null;
    config?: string | null;
    framework?: string | null;
    totalFilesChecked?: number;
  } = {},
): ComplianceReport {
  const violations = mergeViolations(options.eslint, options.biome);
  const errors = violations.filter((v) => v.severity === "error");
  const warnings = violations.filter((v) => v.severity === "warning");

  // Break down by category (rule prefix as category)
  const byCategory: Record<string, { errors: number; warnings: number }> = {};
  for (const v of violations) {
    const category = v.rule.split("/")[0] || v.rule;
    if (!byCategory[category]) {
      byCategory[category] = { errors: 0, warnings: 0 };
    }
    if (v.severity === "error") {
      byCategory[category].errors++;
    } else {
      byCategory[category].warnings++;
    }
  }

  // Build tool results
  const toolResults: Record<string, { passed: boolean; error_count: number }> = {};

  if (options.config && fs.existsSync(options.config)) {
    try {
      const configResult = JSON.parse(fs.readFileSync(options.config, "utf-8"));
      toolResults["build-config"] = {
        passed: configResult.valid !== false,
        error_count: Array.isArray(configResult.errors) ? configResult.errors.length : 0,
      };
    } catch {
      toolResults["build-config"] = { passed: false, error_count: 1 };
    }
  } else {
    toolResults["build-config"] = { passed: true, error_count: 0 };
  }

  if (options.framework && fs.existsSync(options.framework)) {
    try {
      const fwResult = JSON.parse(fs.readFileSync(options.framework, "utf-8"));
      const checks = Array.isArray(fwResult) ? fwResult : fwResult.checks || [];
      const errorCount = checks.filter((c: any) => !c.passed && c.severity === "error").length;
      toolResults["framework-rules"] = { passed: errorCount === 0, error_count: errorCount };
    } catch {
      toolResults["framework-rules"] = { passed: true, error_count: 0 };
    }
  } else {
    toolResults["framework-rules"] = { passed: true, error_count: 0 };
  }

  const totalErrors = errors.length + Object.values(toolResults)
    .filter((t) => !t.passed).length;

  return {
    timestamp: new Date().toISOString(),
    compliance_score: calculateScore(errors.length + Object.values(toolResults).filter((t) => !t.passed).length, warnings.length),
    total_files_checked: options.totalFilesChecked ?? (violations.length > 0 ? violations[0].file.split("/").pop()?.split(".").shift() ? 1 : 0 : 0),
    violations: { errors, warnings },
    by_category: byCategory,
    tool_results: toolResults,
  };
}

// CLI entry point
if (require.main === module) {
  const args = process.argv.slice(2);
  const opts: Record<string, string | null> = {};
  for (let i = 0; i < args.length - 1; i += 2) {
    if (args[i] === "--eslint") opts.eslint = args[i + 1];
    else if (args[i] === "--biome") opts.biome = args[i + 1];
    else if (args[i] === "--config") opts.config = args[i + 1];
    else if (args[i] === "--framework") opts.framework = args[i + 1];
    else if (args[i] === "--output") opts._output = args[i + 1];
  }

  const report = generateComplianceReport({
    eslint: opts.eslint || null,
    biome: opts.biome || null,
    config: opts.config || null,
    framework: opts.framework || null,
  });

  const outputPath = opts._output ?? "compliance-report.json";
  fs.writeFileSync(outputPath, JSON.stringify(report, null, 2));
  console.log(`Compliance report written to ${outputPath}`);
  process.exit(report.compliance_score < 70 ? 1 : 0);
}
```

### Pattern 6: Custom ESLint Rule for Framework Requirements

Write custom ESLint rules that enforce project-specific framework conventions. This is how you catch issues that generic linter plugins don't cover — for example, ensuring all API routes use a specific error response format or that React components follow naming conventions.

```typescript
// eslint-strict-nextjs-rules.js — Custom ESLint plugin for Next.js strict requirements
const { RuleTester } = require("eslint");

/**
 * Rule: nextjs/strict-api-response-format
 * Enforces that all API routes (pages/api/*.ts) return responses
 * using the standardized ApiResponse interface.
 */
function strictApiResponseFormat(context) {
  return {
    ExportNamedDeclaration(node) {
      // Only check API route files
      const fileName = context.filename;
      if (!fileName.includes("/api/")) return;

      // Check that default export is a NextApiHandler
      const handlerName = node.declarations?.[0]?.id?.name;
      if (handlerName !== "handler" && handlerName !== "default") {
        // Allow named exports but warn about non-standard names
        context.report({
          node,
          message: `API route should export 'handler' or 'default', found '${handlerName || 'unnamed'}'`,
        });
      }
    },

    ReturnStatement(node) {
      const fileName = context.filename;
      if (!fileName.includes("/api/")) return;

      // Check for direct response.send() usage (deprecated in API routes)
      if (
        node.argument?.callee?.object?.name === "res" &&
        node.argument?.callee?.property?.name === "send"
      ) {
        context.report({
          node,
          message: 'Use res.status(code).json(body) instead of res.send() in API routes',
        });
      }

      // Check for missing status codes on JSON responses
      if (
        node.argument?.callee?.object?.name === "res" &&
        node.argument?.callee?.property?.name === "json"
      ) {
        const parent = findAncestor(node, "ExpressionStatement");
        if (parent) {
          const prevSibling = parent.parent.body[parent.parent.body.indexOf(parent) - 1];
          if (prevSibling?.expression?.callee?.property?.name !== "status") {
            context.report({
              node,
              message: "API responses must include .status(code) before .json(body)",
            });
          }
        }
      }
    },
  };
}

function findAncestor(node, type) {
  let current = node.parent;
  while (current) {
    if (current.type === type) return current;
    current = current.parent;
  }
  return null;
}

module.exports = {
  rules: {
    "strict-api-response-format": strictApiResponseFormat,
  },
};

// ❌ BAD — No custom validation for framework conventions
{
  // Any response format is allowed in API routes
  res.send({ ok: true }); // deprecated, no status code
}

// ✅ GOOD — Custom ESLint rule catches these violations automatically
{
  // This passes because the custom rule enforces .status().json() pattern
  res.status(200).json({ success: true, data: result });
}
```

---

## Constraints

### MUST DO
- Run framework validation as a required CI gate before merge, not as an optional post-commit check
- Install framework-specific linter plugins — never rely on generic ESLint rules alone for framework conventions
- Validate build tool configuration files with explicit schemas that catch missing keys and wrong types
- Include `manage.py check --deploy` in any Django project's CI pipeline for deployment readiness verification
- Generate structured JSON compliance reports that can be consumed by dashboards and notification tools
- Write custom ESLint rules when stock plugins don't cover project-specific framework requirements

### MUST NOT DO
- Skip framework validation to "unblock" a release — this is how production violations accumulate
- Use `--fix` in CI without also running the linter — auto-fixes may mask underlying architectural issues
- Validate configurations only at build time with no schema checks — typos and wrong types cause silent failures
- Disable linter rules as `"off"` without documenting the exception and tracking it in a tech debt register
- Run framework runtime checks (Django `--deploy`, etc.) only on CI but not locally — developers must see violations early

---

## Related Skills

| Skill | Purpose |
|-------|---------|
| `framework-utilization` | How to use framework features correctly once conventions are validated |
| `framework-requirements` | Selecting and configuring frameworks before validation rules are defined |
| `build-test-validation` | Complementary test coverage validation alongside code convention checks |

---

## Live References

> Authoritative documentation links for this skill's domain. The model follows markdown links at load time to resolve external references and inline content.

- [ESLint Plugin React Hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks)
- [@next/eslint-plugin-next Documentation](https://nextjs.org/docs/app/api-reference/config/eslint)
- [Biome Linter Configuration](https://biomejs.dev/reference/linter/)
- [Zod Schema Validation Library](https://zod.dev/)
- [Django System Check Framework](https://docs.djangoproject.com/en/stable/ref/checks/)
- [GitHub Actions Workflow Syntax](https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions)

