API Design Reviewer Tier: POWERFUL Category: Engineering / Architecture Maintainer: Claude Skills Team Overview The API Design Reviewer skill provides comprehensive analysis and review of API designs, focusing on REST conventions, best practices, and industry standards. This skill helps engineering teams build consistent, maintainable, and well-designed APIs through automated linting, breaking change detection, and design scorecards. Quick Start — run the tools first Review flow: run all three, report linter findings + breaking changes + grade to the user, fix, then re-run until the linter is…

)\n self.camel_case_pattern = re.compile(r'^[a-z][a-zA-Z0-9]*

API Design Reviewer Tier: POWERFUL Category: Engineering / Architecture Maintainer: Claude Skills Team Overview The API Design Reviewer skill provides comprehensive analysis and review of API designs, focusing on REST conventions, best practices, and industry standards. This skill helps engineering teams build consistent, maintainable, and well-designed APIs through automated linting, breaking change detection, and design scorecards. Quick Start — run the tools first Review flow: run all three, report linter findings + breaking changes + grade to the user, fix, then re-run until the linter is…

)\n self.snake_case_pattern = re.compile(r'^[a-z]+(?:_[a-z0-9]+)*

API Design Reviewer Tier: POWERFUL Category: Engineering / Architecture Maintainer: Claude Skills Team Overview The API Design Reviewer skill provides comprehensive analysis and review of API designs, focusing on REST conventions, best practices, and industry standards. This skill helps engineering teams build consistent, maintainable, and well-designed APIs through automated linting, breaking change detection, and design scorecards. Quick Start — run the tools first Review flow: run all three, report linter findings + breaking changes + grade to the user, fix, then re-run until the linter is…

)\n self.pascal_case_pattern = re.compile(r'^[A-Z][a-zA-Z0-9]*

API Design Reviewer Tier: POWERFUL Category: Engineering / Architecture Maintainer: Claude Skills Team Overview The API Design Reviewer skill provides comprehensive analysis and review of API designs, focusing on REST conventions, best practices, and industry standards. This skill helps engineering teams build consistent, maintainable, and well-designed APIs through automated linting, breaking change detection, and design scorecards. Quick Start — run the tools first Review flow: run all three, report linter findings + breaking changes + grade to the user, fix, then re-run until the linter is…

)\n \n # Standard HTTP methods\n self.http_methods = {'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'}\n \n # Standard HTTP status codes by method\n self.standard_status_codes = {\n 'GET': {200, 304, 404},\n 'POST': {200, 201, 400, 409, 422},\n 'PUT': {200, 204, 400, 404, 409},\n 'PATCH': {200, 204, 400, 404, 409},\n 'DELETE': {200, 204, 404},\n 'HEAD': {200, 404},\n 'OPTIONS': {200}\n }\n \n # Common error status codes\n self.common_error_codes = {400, 401, 403, 404, 405, 409, 422, 429, 500, 502, 503}\n\n def lint_openapi_spec(self, spec: Dict[str, Any]) -> LintReport:\n \"\"\"Lint an OpenAPI/Swagger specification.\"\"\"\n self.openapi_spec = spec\n self.report = LintReport()\n \n # Basic structure validation\n self._validate_openapi_structure()\n \n # Info section validation\n self._validate_info_section()\n \n # Server section validation\n self._validate_servers_section()\n \n # Paths validation (main linting logic)\n self._validate_paths_section()\n \n # Components validation\n self._validate_components_section()\n \n # Security validation\n self._validate_security_section()\n \n # Calculate final score\n self.report.calculate_score()\n \n return self.report\n\n def lint_raw_endpoints(self, endpoints: Dict[str, Any]) -> LintReport:\n \"\"\"Lint raw endpoint definitions.\"\"\"\n self.raw_endpoints = endpoints\n self.report = LintReport()\n \n # Validate raw endpoint structure\n self._validate_raw_endpoint_structure()\n \n # Lint each endpoint\n for endpoint_path, endpoint_data in endpoints.get('endpoints', {}).items():\n self._lint_raw_endpoint(endpoint_path, endpoint_data)\n \n self.report.calculate_score()\n return self.report\n\n def _validate_openapi_structure(self) -> None:\n \"\"\"Validate basic OpenAPI document structure.\"\"\"\n required_fields = ['openapi', 'info', 'paths']\n \n for field in required_fields:\n if field not in self.openapi_spec:\n self.report.add_issue(LintIssue(\n severity='error',\n category='structure',\n message=f\"Missing required field: {field}\",\n path=f\"/{field}\",\n suggestion=f\"Add the '{field}' field to the root of your OpenAPI specification\"\n ))\n\n def _validate_info_section(self) -> None:\n \"\"\"Validate the info section of OpenAPI spec.\"\"\"\n if 'info' not in self.openapi_spec:\n return\n \n info = self.openapi_spec['info']\n required_info_fields = ['title', 'version']\n recommended_info_fields = ['description', 'contact']\n \n for field in required_info_fields:\n if field not in info:\n self.report.add_issue(LintIssue(\n severity='error',\n category='documentation',\n message=f\"Missing required info field: {field}\",\n path=f\"/info/{field}\",\n suggestion=f\"Add a '{field}' field to the info section\"\n ))\n \n for field in recommended_info_fields:\n if field not in info:\n self.report.add_issue(LintIssue(\n severity='warning',\n category='documentation',\n message=f\"Missing recommended info field: {field}\",\n path=f\"/info/{field}\",\n suggestion=f\"Consider adding a '{field}' field to improve API documentation\"\n ))\n \n # Validate version format\n if 'version' in info:\n version = info['version']\n if not re.match(r'^\\d+\\.\\d+(\\.\\d+)?(-\\w+)?

API Design Reviewer Tier: POWERFUL Category: Engineering / Architecture Maintainer: Claude Skills Team Overview The API Design Reviewer skill provides comprehensive analysis and review of API designs, focusing on REST conventions, best practices, and industry standards. This skill helps engineering teams build consistent, maintainable, and well-designed APIs through automated linting, breaking change detection, and design scorecards. Quick Start — run the tools first Review flow: run all three, report linter findings + breaking changes + grade to the user, fix, then re-run until the linter is…

, version):\n self.report.add_issue(LintIssue(\n severity='warning',\n category='versioning',\n message=f\"Version format '{version}' doesn't follow semantic versioning\",\n path=\"/info/version\",\n suggestion=\"Use semantic versioning format (e.g., '1.0.0', '2.1.3-beta')\"\n ))\n\n def _validate_servers_section(self) -> None:\n \"\"\"Validate the servers section.\"\"\"\n if 'servers' not in self.openapi_spec:\n self.report.add_issue(LintIssue(\n severity='warning',\n category='configuration',\n message=\"Missing servers section\",\n path=\"/servers\",\n suggestion=\"Add a servers section to specify API base URLs\"\n ))\n return\n \n servers = self.openapi_spec['servers']\n if not isinstance(servers, list) or len(servers) == 0:\n self.report.add_issue(LintIssue(\n severity='warning',\n category='configuration',\n message=\"Empty servers section\",\n path=\"/servers\",\n suggestion=\"Add at least one server URL\"\n ))\n\n def _validate_paths_section(self) -> None:\n \"\"\"Validate all API paths and operations.\"\"\"\n if 'paths' not in self.openapi_spec:\n return\n \n paths = self.openapi_spec['paths']\n if not paths:\n self.report.add_issue(LintIssue(\n severity='error',\n category='structure',\n message=\"No paths defined in API specification\",\n path=\"/paths\",\n suggestion=\"Define at least one API endpoint\"\n ))\n return\n \n self.report.total_endpoints = sum(\n len([method for method in path_obj.keys() if method.upper() in self.http_methods])\n for path_obj in paths.values() if isinstance(path_obj, dict)\n )\n \n endpoints_with_issues = set()\n \n for path, path_obj in paths.items():\n if not isinstance(path_obj, dict):\n continue\n \n # Validate path structure\n path_issues = self._validate_path_structure(path)\n if path_issues:\n endpoints_with_issues.add(path)\n \n # Validate each operation in the path\n for method, operation in path_obj.items():\n if method.upper() not in self.http_methods:\n continue\n \n operation_issues = self._validate_operation(path, method.upper(), operation)\n if operation_issues:\n endpoints_with_issues.add(path)\n \n self.report.endpoints_with_issues = len(endpoints_with_issues)\n\n def _validate_path_structure(self, path: str) -> bool:\n \"\"\"Validate REST path structure and naming conventions.\"\"\"\n has_issues = False\n \n # Check if path starts with slash\n if not path.startswith('/'):\n self.report.add_issue(LintIssue(\n severity='error',\n category='url_structure',\n message=f\"Path must start with '/' character: {path}\",\n path=f\"/paths/{path}\",\n suggestion=f\"Change '{path}' to '/{path.lstrip('/')}'\"\n ))\n has_issues = True\n \n # Split path into segments\n segments = [seg for seg in path.split('/') if seg]\n \n # Check for empty segments (double slashes)\n if '//' in path:\n self.report.add_issue(LintIssue(\n severity='error',\n category='url_structure',\n message=f\"Path contains empty segments: {path}\",\n path=f\"/paths/{path}\",\n suggestion=\"Remove double slashes from the path\"\n ))\n has_issues = True\n \n # Validate each segment\n for i, segment in enumerate(segments):\n # Skip parameter segments\n if segment.startswith('{') and segment.endswith('}'):\n # Validate parameter naming\n param_name = segment[1:-1]\n if not self.camel_case_pattern.match(param_name) and not self.kebab_case_pattern.match(param_name):\n self.report.add_issue(LintIssue(\n severity='warning',\n category='naming',\n message=f\"Path parameter '{param_name}' should use camelCase or kebab-case\",\n path=f\"/paths/{path}\",\n suggestion=f\"Use camelCase (e.g., 'userId') or kebab-case (e.g., 'user-id')\"\n ))\n has_issues = True\n continue\n \n # Check for resource naming conventions\n if not self.kebab_case_pattern.match(segment):\n # Allow version segments like 'v1', 'v2'\n if not re.match(r'^v\\d+

API Design Reviewer Tier: POWERFUL Category: Engineering / Architecture Maintainer: Claude Skills Team Overview The API Design Reviewer skill provides comprehensive analysis and review of API designs, focusing on REST conventions, best practices, and industry standards. This skill helps engineering teams build consistent, maintainable, and well-designed APIs through automated linting, breaking change detection, and design scorecards. Quick Start — run the tools first Review flow: run all three, report linter findings + breaking changes + grade to the user, fix, then re-run until the linter is…

, segment):\n self.report.add_issue(LintIssue(\n severity='warning',\n category='naming',\n message=f\"Resource segment '{segment}' should use kebab-case\",\n path=f\"/paths/{path}\",\n suggestion=f\"Use kebab-case for '{segment}' (e.g., 'user-profiles', 'order-items')\"\n ))\n has_issues = True\n \n # Check for verb usage in URLs (anti-pattern)\n common_verbs = {'get', 'post', 'put', 'delete', 'create', 'update', 'remove', 'add'}\n if segment.lower() in common_verbs:\n self.report.add_issue(LintIssue(\n severity='warning',\n category='rest_conventions',\n message=f\"Avoid verbs in URLs: '{segment}' in {path}\",\n path=f\"/paths/{path}\",\n suggestion=\"Use HTTP methods instead of verbs in URLs. Use nouns for resources.\"\n ))\n has_issues = True\n \n # Check path depth (avoid over-nesting)\n if len(segments) > 6:\n self.report.add_issue(LintIssue(\n severity='warning',\n category='url_structure',\n message=f\"Path has excessive nesting ({len(segments)} levels): {path}\",\n path=f\"/paths/{path}\",\n suggestion=\"Consider flattening the resource hierarchy or using query parameters\"\n ))\n has_issues = True\n \n # Check for consistent versioning\n if any('v' + str(i) in segments for i in range(1, 10)):\n version_segments = [seg for seg in segments if re.match(r'^v\\d+

API Design Reviewer Tier: POWERFUL Category: Engineering / Architecture Maintainer: Claude Skills Team Overview The API Design Reviewer skill provides comprehensive analysis and review of API designs, focusing on REST conventions, best practices, and industry standards. This skill helps engineering teams build consistent, maintainable, and well-designed APIs through automated linting, breaking change detection, and design scorecards. Quick Start — run the tools first Review flow: run all three, report linter findings + breaking changes + grade to the user, fix, then re-run until the linter is…

, seg)]\n if len(version_segments) > 1:\n self.report.add_issue(LintIssue(\n severity='error',\n category='versioning',\n message=f\"Multiple version segments in path: {path}\",\n path=f\"/paths/{path}\",\n suggestion=\"Use only one version segment per path\"\n ))\n has_issues = True\n \n return has_issues\n\n def _validate_operation(self, path: str, method: str, operation: Dict[str, Any]) -> bool:\n \"\"\"Validate individual operation (HTTP method + path combination).\"\"\"\n has_issues = False\n operation_path = f\"/paths/{path}/{method.lower()}\"\n \n # Check for required operation fields\n if 'responses' not in operation:\n self.report.add_issue(LintIssue(\n severity='error',\n category='structure',\n message=f\"Missing responses section for {method} {path}\",\n path=f\"{operation_path}/responses\",\n suggestion=\"Define expected responses for this operation\"\n ))\n has_issues = True\n \n # Check for operation documentation\n if 'summary' not in operation:\n self.report.add_issue(LintIssue(\n severity='warning',\n category='documentation',\n message=f\"Missing summary for {method} {path}\",\n path=f\"{operation_path}/summary\",\n suggestion=\"Add a brief summary describing what this operation does\"\n ))\n has_issues = True\n \n if 'description' not in operation:\n self.report.add_issue(LintIssue(\n severity='info',\n category='documentation',\n message=f\"Missing description for {method} {path}\",\n path=f\"{operation_path}/description\",\n suggestion=\"Add a detailed description for better API documentation\"\n ))\n has_issues = True\n \n # Validate HTTP method usage patterns\n method_issues = self._validate_http_method_usage(path, method, operation)\n if method_issues:\n has_issues = True\n \n # Validate responses\n if 'responses' in operation:\n response_issues = self._validate_responses(path, method, operation['responses'])\n if response_issues:\n has_issues = True\n \n # Validate parameters\n if 'parameters' in operation:\n param_issues = self._validate_parameters(path, method, operation['parameters'])\n if param_issues:\n has_issues = True\n \n # Validate request body\n if 'requestBody' in operation:\n body_issues = self._validate_request_body(path, method, operation['requestBody'])\n if body_issues:\n has_issues = True\n \n return has_issues\n\n def _validate_http_method_usage(self, path: str, method: str, operation: Dict[str, Any]) -> bool:\n \"\"\"Validate proper HTTP method usage patterns.\"\"\"\n has_issues = False\n \n # GET requests should not have request body\n if method == 'GET' and 'requestBody' in operation:\n self.report.add_issue(LintIssue(\n severity='error',\n category='rest_conventions',\n message=f\"GET request should not have request body: {method} {path}\",\n path=f\"/paths/{path}/{method.lower()}/requestBody\",\n suggestion=\"Remove requestBody from GET request or use POST if body is needed\"\n ))\n has_issues = True\n \n # DELETE requests typically should not have request body\n if method == 'DELETE' and 'requestBody' in operation:\n self.report.add_issue(LintIssue(\n severity='warning',\n category='rest_conventions',\n message=f\"DELETE request typically should not have request body: {method} {path}\",\n path=f\"/paths/{path}/{method.lower()}/requestBody\",\n suggestion=\"Consider using query parameters or path parameters instead\"\n ))\n has_issues = True\n \n # POST/PUT/PATCH should typically have request body (except for actions)\n if method in ['POST', 'PUT', 'PATCH'] and 'requestBody' not in operation:\n # Check if this is an action endpoint\n if not any(action in path.lower() for action in ['activate', 'deactivate', 'reset', 'confirm']):\n self.report.add_issue(LintIssue(\n severity='info',\n category='rest_conventions',\n message=f\"{method} request typically should have request body: {method} {path}\",\n path=f\"/paths/{path}/{method.lower()}\",\n suggestion=f\"Consider adding requestBody for {method} operation or use GET if no data is being sent\"\n ))\n has_issues = True\n \n return has_issues\n\n def _validate_responses(self, path: str, method: str, responses: Dict[str, Any]) -> bool:\n \"\"\"Validate response definitions.\"\"\"\n has_issues = False\n \n # Check for success response\n success_codes = {'200', '201', '202', '204'}\n has_success = any(code in responses for code in success_codes)\n \n if not has_success:\n self.report.add_issue(LintIssue(\n severity='error',\n category='responses',\n message=f\"Missing success response for {method} {path}\",\n path=f\"/paths/{path}/{method.lower()}/responses\",\n suggestion=\"Define at least one success response (200, 201, 202, or 204)\"\n ))\n has_issues = True\n \n # Check for error responses\n has_error_responses = any(code.startswith('4') or code.startswith('5') for code in responses.keys())\n \n if not has_error_responses:\n self.report.add_issue(LintIssue(\n severity='warning',\n category='responses',\n message=f\"Missing error responses for {method} {path}\",\n path=f\"/paths/{path}/{method.lower()}/responses\",\n suggestion=\"Define common error responses (400, 404, 500, etc.)\"\n ))\n has_issues = True\n \n # Validate individual response codes\n for status_code, response in responses.items():\n if status_code == 'default':\n continue\n \n try:\n code_int = int(status_code)\n except ValueError:\n self.report.add_issue(LintIssue(\n severity='error',\n category='responses',\n message=f\"Invalid status code '{status_code}' for {method} {path}\",\n path=f\"/paths/{path}/{method.lower()}/responses/{status_code}\",\n suggestion=\"Use valid HTTP status codes (e.g., 200, 404, 500)\"\n ))\n has_issues = True\n continue\n \n # Check if status code is appropriate for the method\n expected_codes = self.standard_status_codes.get(method, set())\n common_codes = {400, 401, 403, 404, 429, 500} # Always acceptable\n \n if expected_codes and code_int not in expected_codes and code_int not in common_codes:\n self.report.add_issue(LintIssue(\n severity='info',\n category='responses',\n message=f\"Uncommon status code {status_code} for {method} {path}\",\n path=f\"/paths/{path}/{method.lower()}/responses/{status_code}\",\n suggestion=f\"Consider using standard codes for {method}: {sorted(expected_codes)}\"\n ))\n has_issues = True\n \n return has_issues\n\n def _validate_parameters(self, path: str, method: str, parameters: List[Dict[str, Any]]) -> bool:\n \"\"\"Validate parameter definitions.\"\"\"\n has_issues = False\n \n for i, param in enumerate(parameters):\n param_path = f\"/paths/{path}/{method.lower()}/parameters[{i}]\"\n \n # Check required fields\n if 'name' not in param:\n self.report.add_issue(LintIssue(\n severity='error',\n category='parameters',\n message=f\"Parameter missing name field in {method} {path}\",\n path=f\"{param_path}/name\",\n suggestion=\"Add a name field to the parameter\"\n ))\n has_issues = True\n continue\n \n if 'in' not in param:\n self.report.add_issue(LintIssue(\n severity='error',\n category='parameters',\n message=f\"Parameter '{param['name']}' missing 'in' field in {method} {path}\",\n path=f\"{param_path}/in\",\n suggestion=\"Specify parameter location (query, path, header, cookie)\"\n ))\n has_issues = True\n \n # Validate parameter naming\n param_name = param['name']\n param_location = param.get('in', '')\n \n if param_location == 'query':\n # Query parameters should use camelCase or kebab-case\n if not self.camel_case_pattern.match(param_name) and not self.kebab_case_pattern.match(param_name):\n self.report.add_issue(LintIssue(\n severity='warning',\n category='naming',\n message=f\"Query parameter '{param_name}' should use camelCase or kebab-case in {method} {path}\",\n path=f\"{param_path}/name\",\n suggestion=\"Use camelCase (e.g., 'pageSize') or kebab-case (e.g., 'page-size')\"\n ))\n has_issues = True\n \n elif param_location == 'path':\n # Path parameters should use camelCase or kebab-case\n if not self.camel_case_pattern.match(param_name) and not self.kebab_case_pattern.match(param_name):\n self.report.add_issue(LintIssue(\n severity='warning',\n category='naming',\n message=f\"Path parameter '{param_name}' should use camelCase or kebab-case in {method} {path}\",\n path=f\"{param_path}/name\",\n suggestion=\"Use camelCase (e.g., 'userId') or kebab-case (e.g., 'user-id')\"\n ))\n has_issues = True\n \n # Path parameters must be required\n if not param.get('required', False):\n self.report.add_issue(LintIssue(\n severity='error',\n category='parameters',\n message=f\"Path parameter '{param_name}' must be required in {method} {path}\",\n path=f\"{param_path}/required\",\n suggestion=\"Set required: true for path parameters\"\n ))\n has_issues = True\n \n return has_issues\n\n def _validate_request_body(self, path: str, method: str, request_body: Dict[str, Any]) -> bool:\n \"\"\"Validate request body definition.\"\"\"\n has_issues = False\n \n if 'content' not in request_body:\n self.report.add_issue(LintIssue(\n severity='error',\n category='request_body',\n message=f\"Request body missing content for {method} {path}\",\n path=f\"/paths/{path}/{method.lower()}/requestBody/content\",\n suggestion=\"Define content types for the request body\"\n ))\n has_issues = True\n \n return has_issues\n\n def _validate_components_section(self) -> None:\n \"\"\"Validate the components section.\"\"\"\n if 'components' not in self.openapi_spec:\n self.report.add_issue(LintIssue(\n severity='info',\n category='structure',\n message=\"Missing components section\",\n path=\"/components\",\n suggestion=\"Consider defining reusable components (schemas, responses, parameters)\"\n ))\n return\n \n components = self.openapi_spec['components']\n \n # Validate schemas\n if 'schemas' in components:\n self._validate_schemas(components['schemas'])\n\n def _validate_schemas(self, schemas: Dict[str, Any]) -> None:\n \"\"\"Validate schema definitions.\"\"\"\n for schema_name, schema in schemas.items():\n # Check schema naming (should be PascalCase)\n if not self.pascal_case_pattern.match(schema_name):\n self.report.add_issue(LintIssue(\n severity='warning',\n category='naming',\n message=f\"Schema name '{schema_name}' should use PascalCase\",\n path=f\"/components/schemas/{schema_name}\",\n suggestion=f\"Use PascalCase for schema names (e.g., 'UserProfile', 'OrderItem')\"\n ))\n \n # Validate schema properties\n if isinstance(schema, dict) and 'properties' in schema:\n self._validate_schema_properties(schema_name, schema['properties'])\n\n def _validate_schema_properties(self, schema_name: str, properties: Dict[str, Any]) -> None:\n \"\"\"Validate schema property naming.\"\"\"\n for prop_name, prop_def in properties.items():\n # Properties should use camelCase\n if not self.camel_case_pattern.match(prop_name):\n self.report.add_issue(LintIssue(\n severity='warning',\n category='naming',\n message=f\"Property '{prop_name}' in schema '{schema_name}' should use camelCase\",\n path=f\"/components/schemas/{schema_name}/properties/{prop_name}\",\n suggestion=\"Use camelCase for property names (e.g., 'firstName', 'createdAt')\"\n ))\n\n def _validate_security_section(self) -> None:\n \"\"\"Validate security definitions.\"\"\"\n if 'security' not in self.openapi_spec and 'components' not in self.openapi_spec:\n self.report.add_issue(LintIssue(\n severity='warning',\n category='security',\n message=\"No security configuration found\",\n path=\"/security\",\n suggestion=\"Define security schemes and apply them to operations\"\n ))\n\n def _validate_raw_endpoint_structure(self) -> None:\n \"\"\"Validate structure of raw endpoint definitions.\"\"\"\n if 'endpoints' not in self.raw_endpoints:\n self.report.add_issue(LintIssue(\n severity='error',\n category='structure',\n message=\"Missing 'endpoints' field in raw endpoint definition\",\n path=\"/endpoints\",\n suggestion=\"Provide an 'endpoints' object containing endpoint definitions\"\n ))\n return\n \n endpoints = self.raw_endpoints['endpoints']\n self.report.total_endpoints = len(endpoints)\n\n def _lint_raw_endpoint(self, path: str, endpoint_data: Dict[str, Any]) -> None:\n \"\"\"Lint individual raw endpoint definition.\"\"\"\n # Validate path structure\n self._validate_path_structure(path)\n \n # Check for required fields\n if 'method' not in endpoint_data:\n self.report.add_issue(LintIssue(\n severity='error',\n category='structure',\n message=f\"Missing method field for endpoint {path}\",\n path=f\"/endpoints/{path}/method\",\n suggestion=\"Specify HTTP method (GET, POST, PUT, PATCH, DELETE)\"\n ))\n return\n \n method = endpoint_data['method'].upper()\n if method not in self.http_methods:\n self.report.add_issue(LintIssue(\n severity='error',\n category='structure',\n message=f\"Invalid HTTP method '{method}' for endpoint {path}\",\n path=f\"/endpoints/{path}/method\",\n suggestion=f\"Use valid HTTP methods: {', '.join(sorted(self.http_methods))}\"\n ))\n\n def generate_json_report(self) -> str:\n \"\"\"Generate JSON format report.\"\"\"\n issues_by_severity = self.report.get_issues_by_severity()\n \n report_data = {\n \"summary\": {\n \"total_endpoints\": self.report.total_endpoints,\n \"endpoints_with_issues\": self.report.endpoints_with_issues,\n \"total_issues\": len(self.report.issues),\n \"errors\": len(issues_by_severity['error']),\n \"warnings\": len(issues_by_severity['warning']),\n \"info\": len(issues_by_severity['info']),\n \"score\": round(self.report.score, 2)\n },\n \"issues\": []\n }\n \n for issue in self.report.issues:\n report_data[\"issues\"].append({\n \"severity\": issue.severity,\n \"category\": issue.category,\n \"message\": issue.message,\n \"path\": issue.path,\n \"suggestion\": issue.suggestion\n })\n \n return json.dumps(report_data, indent=2)\n\n def generate_text_report(self) -> str:\n \"\"\"Generate human-readable text report.\"\"\"\n issues_by_severity = self.report.get_issues_by_severity()\n \n report_lines = [\n \"═══════════════════════════════════════════════════════════════\",\n \" API LINTING REPORT\",\n \"═══════════════════════════════════════════════════════════════\",\n \"\",\n \"SUMMARY:\",\n f\" Total Endpoints: {self.report.total_endpoints}\",\n f\" Endpoints with Issues: {self.report.endpoints_with_issues}\",\n f\" Overall Score: {self.report.score:.1f}/100.0\",\n \"\",\n \"ISSUE BREAKDOWN:\",\n f\" 🔴 Errors: {len(issues_by_severity['error'])}\",\n f\" 🟡 Warnings: {len(issues_by_severity['warning'])}\",\n f\" ℹ️ Info: {len(issues_by_severity['info'])}\",\n \"\",\n ]\n \n if not self.report.issues:\n report_lines.extend([\n \"🎉 Congratulations! No issues found in your API specification.\",\n \"\"\n ])\n else:\n # Group issues by category\n issues_by_category = {}\n for issue in self.report.issues:\n if issue.category not in issues_by_category:\n issues_by_category[issue.category] = []\n issues_by_category[issue.category].append(issue)\n \n for category, issues in issues_by_category.items():\n report_lines.append(f\"{'═' * 60}\")\n report_lines.append(f\"CATEGORY: {category.upper().replace('_', ' ')}\")\n report_lines.append(f\"{'═' * 60}\")\n \n for issue in issues:\n severity_icon = {\"error\": \"🔴\", \"warning\": \"🟡\", \"info\": \"ℹ️\"}[issue.severity]\n \n report_lines.extend([\n f\"{severity_icon} {issue.severity.upper()}: {issue.message}\",\n f\" Path: {issue.path}\",\n ])\n \n if issue.suggestion:\n report_lines.append(f\" 💡 Suggestion: {issue.suggestion}\")\n \n report_lines.append(\"\")\n \n # Add scoring breakdown\n report_lines.extend([\n \"═══════════════════════════════════════════════════════════════\",\n \"SCORING DETAILS:\",\n \"═══════════════════════════════════════════════════════════════\",\n f\"Base Score: 100.0\",\n f\"Errors Penalty: -{len(issues_by_severity['error']) * 10} (10 points per error)\",\n f\"Warnings Penalty: -{len(issues_by_severity['warning']) * 3} (3 points per warning)\",\n f\"Info Penalty: -{len(issues_by_severity['info']) * 1} (1 point per info)\",\n f\"Final Score: {self.report.score:.1f}/100.0\",\n \"\"\n ])\n \n # Add recommendations based on score\n if self.report.score >= 90:\n report_lines.append(\"🏆 Excellent! Your API design follows best practices.\")\n elif self.report.score >= 80:\n report_lines.append(\"✅ Good API design with minor areas for improvement.\")\n elif self.report.score >= 70:\n report_lines.append(\"⚠️ Fair API design. Consider addressing warnings and errors.\")\n elif self.report.score >= 50:\n report_lines.append(\"❌ Poor API design. Multiple issues need attention.\")\n else:\n report_lines.append(\"🚨 Critical API design issues. Immediate attention required.\")\n \n return \"\\n\".join(report_lines)\n\n\n# Embedded sample OpenAPI spec — intentionally imperfect (a verb in a URL, a\n# snake_case property) so --sample produces a representative report.\nSAMPLE_OPENAPI_SPEC = {\n \"openapi\": \"3.0.0\",\n \"info\": {\"title\": \"Sample API\", \"version\": \"1.0.0\"},\n \"servers\": [{\"url\": \"https://api.example.com\"}],\n \"paths\": {\n \"/user-profiles/{userId}\": {\n \"get\": {\n \"summary\": \"Get a user profile\",\n \"responses\": {\"200\": {\"description\": \"OK\"}, \"404\": {\"description\": \"Not found\"}},\n \"parameters\": [{\"name\": \"userId\", \"in\": \"path\", \"required\": True}],\n }\n },\n \"/user-profiles/create\": {\n \"post\": {\n \"summary\": \"Create a user profile (verb-in-URL anti-pattern)\",\n \"responses\": {\"201\": {\"description\": \"Created\"}},\n }\n },\n },\n \"components\": {\n \"schemas\": {\n \"UserProfile\": {\"properties\": {\"first_name\": {\"type\": \"string\"}}}\n }\n },\n}\n\n\ndef main():\n \"\"\"Main CLI entry point.\"\"\"\n parser = argparse.ArgumentParser(\n description=\"Analyze OpenAPI/Swagger specifications for REST conventions and best practices\",\n formatter_class=argparse.RawDescriptionHelpFormatter,\n epilog=\"\"\"\nExamples:\n python api_linter.py openapi.json\n python api_linter.py --format json openapi.json > report.json\n python api_linter.py --raw-endpoints endpoints.json\n python api_linter.py --sample --format json\n \"\"\"\n )\n\n parser.add_argument(\n 'input_file',\n nargs='?',\n help='Input file: OpenAPI/Swagger JSON file or raw endpoints JSON'\n )\n\n parser.add_argument(\n '--sample',\n action='store_true',\n help='Lint an embedded sample OpenAPI spec (no input file needed)'\n )\n \n parser.add_argument(\n '--format',\n choices=['text', 'json'],\n default='text',\n help='Output format (default: text)'\n )\n \n parser.add_argument(\n '--raw-endpoints',\n action='store_true',\n help='Treat input as raw endpoint definitions instead of OpenAPI spec'\n )\n \n parser.add_argument(\n '--output',\n help='Output file (default: stdout)'\n )\n \n args = parser.parse_args()\n\n # Load input data — from the embedded sample or the input file\n if args.sample:\n input_data = SAMPLE_OPENAPI_SPEC\n else:\n if not args.input_file:\n parser.error(\"input_file is required (or use --sample)\")\n try:\n with open(args.input_file, 'r') as f:\n input_data = json.load(f)\n except FileNotFoundError:\n print(f\"Error: Input file '{args.input_file}' not found.\", file=sys.stderr)\n return 1\n except json.JSONDecodeError as e:\n print(f\"Error: Invalid JSON in '{args.input_file}': {e}\", file=sys.stderr)\n return 1\n \n # Initialize linter and run analysis\n linter = APILinter()\n \n try:\n if args.raw_endpoints:\n report = linter.lint_raw_endpoints(input_data)\n else:\n report = linter.lint_openapi_spec(input_data)\n except Exception as e:\n print(f\"Error during linting: {e}\", file=sys.stderr)\n return 1\n \n # Generate report\n if args.format == 'json':\n output = linter.generate_json_report()\n else:\n output = linter.generate_text_report()\n \n # Write output\n if args.output:\n try:\n with open(args.output, 'w') as f:\n f.write(output)\n print(f\"Report written to {args.output}\")\n except IOError as e:\n print(f\"Error writing to '{args.output}': {e}\", file=sys.stderr)\n return 1\n else:\n print(output)\n \n # Return appropriate exit code\n error_count = len([i for i in report.issues if i.severity == 'error'])\n return 1 if error_count > 0 else 0\n\n\nif __name__ == '__main__':\n sys.exit(main())","content_type":"text/x-python; charset=utf-8","language":"python","size":40259,"content_sha256":"9acfecc26859e54be84a564fc4edefc552556c83e52f9228cc4b8282f9f617f8"},{"filename":"scripts/api_scorecard.py","content":"#!/usr/bin/env python3\n\"\"\"\nAPI Scorecard - Comprehensive API design quality assessment tool.\n\nThis script evaluates API designs across multiple dimensions and generates\na detailed scorecard with letter grades and improvement recommendations.\n\nScoring Dimensions:\n- Consistency (30%): Naming conventions, response patterns, structural consistency\n- Documentation (20%): Completeness and clarity of API documentation \n- Security (20%): Authentication, authorization, and security best practices\n- Usability (15%): Ease of use, discoverability, and developer experience\n- Performance (15%): Caching, pagination, and efficiency patterns\n\nGenerates letter grades (A-F) with detailed breakdowns and actionable recommendations.\n\"\"\"\n\nimport argparse\nimport json\nimport re\nimport sys\nfrom typing import Any, Dict, List, Optional, Set, Tuple\nfrom dataclasses import dataclass, field\nfrom enum import Enum\nimport math\n\n# Windows consoles often default to a legacy codepage (e.g. cp1252) that cannot\n# encode the Unicode glyphs this script prints (issue #969). Re-encode\n# stdout/stderr as UTF-8 with replacement so output never crashes at print time.\nif hasattr(sys.stdout, \"reconfigure\"):\n sys.stdout.reconfigure(encoding=\"utf-8\", errors=\"replace\")\nif hasattr(sys.stderr, \"reconfigure\"):\n sys.stderr.reconfigure(encoding=\"utf-8\", errors=\"replace\")\n\n\n\nclass ScoreCategory(Enum):\n \"\"\"Scoring categories.\"\"\"\n CONSISTENCY = \"consistency\"\n DOCUMENTATION = \"documentation\"\n SECURITY = \"security\"\n USABILITY = \"usability\"\n PERFORMANCE = \"performance\"\n\n\n@dataclass\nclass CategoryScore:\n \"\"\"Score for a specific category.\"\"\"\n category: ScoreCategory\n score: float # 0-100\n max_score: float # Usually 100\n weight: float # Percentage weight in overall score\n issues: List[str] = field(default_factory=list)\n recommendations: List[str] = field(default_factory=list)\n \n @property\n def letter_grade(self) -> str:\n \"\"\"Convert score to letter grade.\"\"\"\n if self.score >= 90:\n return \"A\"\n elif self.score >= 80:\n return \"B\"\n elif self.score >= 70:\n return \"C\"\n elif self.score >= 60:\n return \"D\"\n else:\n return \"F\"\n \n @property\n def weighted_score(self) -> float:\n \"\"\"Calculate weighted contribution to overall score.\"\"\"\n return (self.score / 100.0) * self.weight\n\n\n@dataclass\nclass APIScorecard:\n \"\"\"Complete API scorecard with all category scores.\"\"\"\n category_scores: Dict[ScoreCategory, CategoryScore] = field(default_factory=dict)\n overall_score: float = 0.0\n overall_grade: str = \"F\"\n total_endpoints: int = 0\n api_info: Dict[str, Any] = field(default_factory=dict)\n \n def calculate_overall_score(self) -> None:\n \"\"\"Calculate overall weighted score and grade.\"\"\"\n self.overall_score = sum(score.weighted_score for score in self.category_scores.values())\n \n if self.overall_score >= 90:\n self.overall_grade = \"A\"\n elif self.overall_score >= 80:\n self.overall_grade = \"B\"\n elif self.overall_score >= 70:\n self.overall_grade = \"C\"\n elif self.overall_score >= 60:\n self.overall_grade = \"D\"\n else:\n self.overall_grade = \"F\"\n \n def get_top_recommendations(self, limit: int = 5) -> List[str]:\n \"\"\"Get top recommendations across all categories.\"\"\"\n all_recommendations = []\n for category_score in self.category_scores.values():\n for rec in category_score.recommendations:\n all_recommendations.append(f\"{category_score.category.value.title()}: {rec}\")\n \n # Sort by category weight (highest impact first)\n weighted_recs = []\n for category_score in sorted(self.category_scores.values(), \n key=lambda x: x.weight, reverse=True):\n for rec in category_score.recommendations[:2]: # Top 2 per category\n weighted_recs.append(f\"{category_score.category.value.title()}: {rec}\")\n \n return weighted_recs[:limit]\n\n\nclass APIScoringEngine:\n \"\"\"Main API scoring engine.\"\"\"\n \n def __init__(self):\n self.scorecard = APIScorecard()\n self.spec: Optional[Dict] = None\n \n # Regex patterns for validation\n self.kebab_case_pattern = re.compile(r'^[a-z]+(?:-[a-z0-9]+)*

API Design Reviewer Tier: POWERFUL Category: Engineering / Architecture Maintainer: Claude Skills Team Overview The API Design Reviewer skill provides comprehensive analysis and review of API designs, focusing on REST conventions, best practices, and industry standards. This skill helps engineering teams build consistent, maintainable, and well-designed APIs through automated linting, breaking change detection, and design scorecards. Quick Start — run the tools first Review flow: run all three, report linter findings + breaking changes + grade to the user, fix, then re-run until the linter is…

)\n self.camel_case_pattern = re.compile(r'^[a-z][a-zA-Z0-9]*

API Design Reviewer Tier: POWERFUL Category: Engineering / Architecture Maintainer: Claude Skills Team Overview The API Design Reviewer skill provides comprehensive analysis and review of API designs, focusing on REST conventions, best practices, and industry standards. This skill helps engineering teams build consistent, maintainable, and well-designed APIs through automated linting, breaking change detection, and design scorecards. Quick Start — run the tools first Review flow: run all three, report linter findings + breaking changes + grade to the user, fix, then re-run until the linter is…

)\n self.pascal_case_pattern = re.compile(r'^[A-Z][a-zA-Z0-9]*

API Design Reviewer Tier: POWERFUL Category: Engineering / Architecture Maintainer: Claude Skills Team Overview The API Design Reviewer skill provides comprehensive analysis and review of API designs, focusing on REST conventions, best practices, and industry standards. This skill helps engineering teams build consistent, maintainable, and well-designed APIs through automated linting, breaking change detection, and design scorecards. Quick Start — run the tools first Review flow: run all three, report linter findings + breaking changes + grade to the user, fix, then re-run until the linter is…

)\n \n # HTTP methods\n self.http_methods = {'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'}\n \n # Category weights (must sum to 100)\n self.category_weights = {\n ScoreCategory.CONSISTENCY: 30.0,\n ScoreCategory.DOCUMENTATION: 20.0,\n ScoreCategory.SECURITY: 20.0,\n ScoreCategory.USABILITY: 15.0,\n ScoreCategory.PERFORMANCE: 15.0\n }\n \n def score_api(self, spec: Dict[str, Any]) -> APIScorecard:\n \"\"\"Generate comprehensive API scorecard.\"\"\"\n self.spec = spec\n self.scorecard = APIScorecard()\n \n # Extract basic API info\n self._extract_api_info()\n \n # Score each category\n self._score_consistency()\n self._score_documentation()\n self._score_security()\n self._score_usability()\n self._score_performance()\n \n # Calculate overall score\n self.scorecard.calculate_overall_score()\n \n return self.scorecard\n \n def _extract_api_info(self) -> None:\n \"\"\"Extract basic API information.\"\"\"\n info = self.spec.get('info', {})\n paths = self.spec.get('paths', {})\n \n self.scorecard.api_info = {\n 'title': info.get('title', 'Unknown API'),\n 'version': info.get('version', ''),\n 'description': info.get('description', ''),\n 'total_paths': len(paths),\n 'openapi_version': self.spec.get('openapi', self.spec.get('swagger', ''))\n }\n \n # Count total endpoints\n endpoint_count = 0\n for path_obj in paths.values():\n if isinstance(path_obj, dict):\n endpoint_count += len([m for m in path_obj.keys() \n if m.upper() in self.http_methods])\n \n self.scorecard.total_endpoints = endpoint_count\n \n def _score_consistency(self) -> None:\n \"\"\"Score API consistency (30% weight).\"\"\"\n category = ScoreCategory.CONSISTENCY\n score = CategoryScore(\n category=category,\n score=0.0,\n max_score=100.0,\n weight=self.category_weights[category]\n )\n \n consistency_checks = [\n self._check_naming_consistency(),\n self._check_response_consistency(),\n self._check_error_format_consistency(),\n self._check_parameter_consistency(),\n self._check_url_structure_consistency(),\n self._check_http_method_consistency(),\n self._check_status_code_consistency()\n ]\n \n # Average the consistency scores\n valid_scores = [s for s in consistency_checks if s is not None]\n if valid_scores:\n score.score = sum(valid_scores) / len(valid_scores)\n \n # Add specific recommendations based on low scores\n if score.score \u003c 70:\n score.recommendations.extend([\n \"Review naming conventions across all endpoints and schemas\",\n \"Standardize response formats and error structures\",\n \"Ensure consistent HTTP method usage patterns\"\n ])\n elif score.score \u003c 85:\n score.recommendations.extend([\n \"Minor consistency improvements needed in naming or response formats\",\n \"Consider creating API design guidelines document\"\n ])\n \n self.scorecard.category_scores[category] = score\n \n def _check_naming_consistency(self) -> float:\n \"\"\"Check naming convention consistency.\"\"\"\n paths = self.spec.get('paths', {})\n schemas = self.spec.get('components', {}).get('schemas', {})\n \n total_checks = 0\n passed_checks = 0\n \n # Check path naming (should be kebab-case)\n for path in paths.keys():\n segments = [seg for seg in path.split('/') if seg and not seg.startswith('{')]\n for segment in segments:\n total_checks += 1\n if self.kebab_case_pattern.match(segment) or re.match(r'^v\\d+

API Design Reviewer Tier: POWERFUL Category: Engineering / Architecture Maintainer: Claude Skills Team Overview The API Design Reviewer skill provides comprehensive analysis and review of API designs, focusing on REST conventions, best practices, and industry standards. This skill helps engineering teams build consistent, maintainable, and well-designed APIs through automated linting, breaking change detection, and design scorecards. Quick Start — run the tools first Review flow: run all three, report linter findings + breaking changes + grade to the user, fix, then re-run until the linter is…

, segment):\n passed_checks += 1\n \n # Check schema naming (should be PascalCase)\n for schema_name in schemas.keys():\n total_checks += 1\n if self.pascal_case_pattern.match(schema_name):\n passed_checks += 1\n \n # Check property naming within schemas\n for schema in schemas.values():\n if isinstance(schema, dict) and 'properties' in schema:\n for prop_name in schema['properties'].keys():\n total_checks += 1\n if self.camel_case_pattern.match(prop_name):\n passed_checks += 1\n \n return (passed_checks / total_checks * 100) if total_checks > 0 else 100\n \n def _check_response_consistency(self) -> float:\n \"\"\"Check response format consistency.\"\"\"\n paths = self.spec.get('paths', {})\n \n response_patterns = []\n total_responses = 0\n \n for path_obj in paths.values():\n if not isinstance(path_obj, dict):\n continue\n \n for method, operation in path_obj.items():\n if method.upper() not in self.http_methods or not isinstance(operation, dict):\n continue\n \n responses = operation.get('responses', {})\n for status_code, response in responses.items():\n if not isinstance(response, dict):\n continue\n \n total_responses += 1\n content = response.get('content', {})\n \n # Analyze response structure\n for media_type, media_obj in content.items():\n schema = media_obj.get('schema', {})\n pattern = self._extract_schema_pattern(schema)\n response_patterns.append(pattern)\n \n # Calculate consistency by comparing patterns\n if not response_patterns:\n return 100\n \n pattern_counts = {}\n for pattern in response_patterns:\n pattern_key = json.dumps(pattern, sort_keys=True)\n pattern_counts[pattern_key] = pattern_counts.get(pattern_key, 0) + 1\n \n # Most common pattern should dominate for good consistency\n max_count = max(pattern_counts.values()) if pattern_counts else 0\n consistency_ratio = max_count / len(response_patterns) if response_patterns else 1\n \n return consistency_ratio * 100\n \n def _extract_schema_pattern(self, schema: Dict[str, Any]) -> Dict[str, Any]:\n \"\"\"Extract a pattern from a schema for consistency checking.\"\"\"\n if not isinstance(schema, dict):\n return {}\n \n pattern = {\n 'type': schema.get('type'),\n 'has_properties': 'properties' in schema,\n 'has_items': 'items' in schema,\n 'required_count': len(schema.get('required', [])),\n 'property_count': len(schema.get('properties', {}))\n }\n \n return pattern\n \n def _check_error_format_consistency(self) -> float:\n \"\"\"Check error response format consistency.\"\"\"\n paths = self.spec.get('paths', {})\n error_responses = []\n \n for path_obj in paths.values():\n if not isinstance(path_obj, dict):\n continue\n \n for method, operation in path_obj.items():\n if method.upper() not in self.http_methods:\n continue\n \n responses = operation.get('responses', {})\n for status_code, response in responses.items():\n try:\n code_int = int(status_code)\n if code_int >= 400: # Error responses\n content = response.get('content', {})\n for media_type, media_obj in content.items():\n schema = media_obj.get('schema', {})\n error_responses.append(self._extract_schema_pattern(schema))\n except ValueError:\n continue\n \n if not error_responses:\n return 80 # No error responses defined - somewhat concerning\n \n # Check consistency of error response formats\n pattern_counts = {}\n for pattern in error_responses:\n pattern_key = json.dumps(pattern, sort_keys=True)\n pattern_counts[pattern_key] = pattern_counts.get(pattern_key, 0) + 1\n \n max_count = max(pattern_counts.values()) if pattern_counts else 0\n consistency_ratio = max_count / len(error_responses) if error_responses else 1\n \n return consistency_ratio * 100\n \n def _check_parameter_consistency(self) -> float:\n \"\"\"Check parameter naming and usage consistency.\"\"\"\n paths = self.spec.get('paths', {})\n \n query_params = []\n path_params = []\n header_params = []\n \n for path_obj in paths.values():\n if not isinstance(path_obj, dict):\n continue\n \n for method, operation in path_obj.items():\n if method.upper() not in self.http_methods:\n continue\n \n parameters = operation.get('parameters', [])\n for param in parameters:\n if not isinstance(param, dict):\n continue\n \n param_name = param.get('name', '')\n param_in = param.get('in', '')\n \n if param_in == 'query':\n query_params.append(param_name)\n elif param_in == 'path':\n path_params.append(param_name)\n elif param_in == 'header':\n header_params.append(param_name)\n \n # Check naming consistency for each parameter type\n scores = []\n \n # Query parameters should be camelCase or kebab-case\n if query_params:\n valid_query = sum(1 for p in query_params \n if self.camel_case_pattern.match(p) or self.kebab_case_pattern.match(p))\n scores.append((valid_query / len(query_params)) * 100)\n \n # Path parameters should be camelCase or kebab-case\n if path_params:\n valid_path = sum(1 for p in path_params \n if self.camel_case_pattern.match(p) or self.kebab_case_pattern.match(p))\n scores.append((valid_path / len(path_params)) * 100)\n \n return sum(scores) / len(scores) if scores else 100\n \n def _check_url_structure_consistency(self) -> float:\n \"\"\"Check URL structure and pattern consistency.\"\"\"\n paths = self.spec.get('paths', {})\n \n total_paths = len(paths)\n if total_paths == 0:\n return 0\n \n structure_score = 0\n \n # Check for consistent versioning\n versioned_paths = 0\n for path in paths.keys():\n if re.search(r'/v\\d+/', path):\n versioned_paths += 1\n \n # Either all or none should be versioned for consistency\n if versioned_paths == 0 or versioned_paths == total_paths:\n structure_score += 25\n elif versioned_paths > total_paths * 0.8:\n structure_score += 20\n \n # Check for reasonable path depth\n reasonable_depth = 0\n for path in paths.keys():\n segments = [seg for seg in path.split('/') if seg]\n if 2 \u003c= len(segments) \u003c= 5: # Reasonable depth\n reasonable_depth += 1\n \n structure_score += (reasonable_depth / total_paths) * 25\n \n # Check for RESTful resource patterns\n restful_patterns = 0\n for path in paths.keys():\n # Look for patterns like /resources/{id} or /resources\n if re.match(r'^/[a-z-]+(/\\{[^}]+\\})?(/[a-z-]+)*

API Design Reviewer Tier: POWERFUL Category: Engineering / Architecture Maintainer: Claude Skills Team Overview The API Design Reviewer skill provides comprehensive analysis and review of API designs, focusing on REST conventions, best practices, and industry standards. This skill helps engineering teams build consistent, maintainable, and well-designed APIs through automated linting, breaking change detection, and design scorecards. Quick Start — run the tools first Review flow: run all three, report linter findings + breaking changes + grade to the user, fix, then re-run until the linter is…

, path):\n restful_patterns += 1\n \n structure_score += (restful_patterns / total_paths) * 30\n \n # Check for consistent trailing slash usage\n with_slash = sum(1 for path in paths.keys() if path.endswith('/'))\n without_slash = total_paths - with_slash\n \n # Either all or none should have trailing slashes\n if with_slash == 0 or without_slash == 0:\n structure_score += 20\n elif min(with_slash, without_slash) \u003c total_paths * 0.1:\n structure_score += 15\n \n return min(structure_score, 100)\n \n def _check_http_method_consistency(self) -> float:\n \"\"\"Check HTTP method usage consistency.\"\"\"\n paths = self.spec.get('paths', {})\n \n method_usage = {}\n total_operations = 0\n \n for path, path_obj in paths.items():\n if not isinstance(path_obj, dict):\n continue\n \n for method in path_obj.keys():\n if method.upper() in self.http_methods:\n method_upper = method.upper()\n total_operations += 1\n \n # Analyze method usage patterns\n if method_upper not in method_usage:\n method_usage[method_upper] = {'count': 0, 'appropriate': 0}\n \n method_usage[method_upper]['count'] += 1\n \n # Check if method usage seems appropriate\n if self._is_method_usage_appropriate(path, method_upper, path_obj[method]):\n method_usage[method_upper]['appropriate'] += 1\n \n if total_operations == 0:\n return 0\n \n # Calculate appropriateness score\n total_appropriate = sum(data['appropriate'] for data in method_usage.values())\n return (total_appropriate / total_operations) * 100\n \n def _is_method_usage_appropriate(self, path: str, method: str, operation: Dict) -> bool:\n \"\"\"Check if HTTP method usage is appropriate for the endpoint.\"\"\"\n # Simple heuristics for method appropriateness\n has_request_body = 'requestBody' in operation\n path_has_id = '{' in path and '}' in path\n \n if method == 'GET':\n return not has_request_body # GET should not have body\n elif method == 'POST':\n return not path_has_id # POST typically for collections\n elif method == 'PUT':\n return path_has_id and has_request_body # PUT for specific resources\n elif method == 'PATCH':\n return path_has_id # PATCH for specific resources\n elif method == 'DELETE':\n return path_has_id # DELETE for specific resources\n \n return True # Default to appropriate for other methods\n \n def _check_status_code_consistency(self) -> float:\n \"\"\"Check HTTP status code usage consistency.\"\"\"\n paths = self.spec.get('paths', {})\n \n method_status_patterns = {}\n total_operations = 0\n \n for path_obj in paths.values():\n if not isinstance(path_obj, dict):\n continue\n \n for method, operation in path_obj.items():\n if method.upper() not in self.http_methods:\n continue\n \n total_operations += 1\n responses = operation.get('responses', {})\n status_codes = set(responses.keys())\n \n if method.upper() not in method_status_patterns:\n method_status_patterns[method.upper()] = []\n \n method_status_patterns[method.upper()].append(status_codes)\n \n if total_operations == 0:\n return 0\n \n # Check consistency within each method type\n consistency_scores = []\n \n for method, status_patterns in method_status_patterns.items():\n if not status_patterns:\n continue\n \n # Find common status codes for this method\n all_codes = set()\n for pattern in status_patterns:\n all_codes.update(pattern)\n \n # Calculate how many operations use the most common codes\n code_usage = {}\n for code in all_codes:\n code_usage[code] = sum(1 for pattern in status_patterns if code in pattern)\n \n # Score based on consistency of common status codes\n if status_patterns:\n avg_consistency = sum(\n len([code for code in pattern if code_usage.get(code, 0) > len(status_patterns) * 0.5]) \n for pattern in status_patterns\n ) / len(status_patterns)\n \n method_consistency = min(avg_consistency / 3.0 * 100, 100) # Expect ~3 common codes\n consistency_scores.append(method_consistency)\n \n return sum(consistency_scores) / len(consistency_scores) if consistency_scores else 100\n \n def _score_documentation(self) -> None:\n \"\"\"Score API documentation quality (20% weight).\"\"\"\n category = ScoreCategory.DOCUMENTATION\n score = CategoryScore(\n category=category,\n score=0.0,\n max_score=100.0,\n weight=self.category_weights[category]\n )\n \n documentation_checks = [\n self._check_api_level_documentation(),\n self._check_endpoint_documentation(),\n self._check_schema_documentation(),\n self._check_parameter_documentation(),\n self._check_response_documentation(),\n self._check_example_coverage()\n ]\n \n valid_scores = [s for s in documentation_checks if s is not None]\n if valid_scores:\n score.score = sum(valid_scores) / len(valid_scores)\n \n # Add recommendations based on score\n if score.score \u003c 60:\n score.recommendations.extend([\n \"Add comprehensive descriptions to all API components\",\n \"Include examples for complex operations and schemas\",\n \"Document all parameters and response fields\"\n ])\n elif score.score \u003c 80:\n score.recommendations.extend([\n \"Improve documentation completeness for some endpoints\",\n \"Add more examples to enhance developer experience\"\n ])\n \n self.scorecard.category_scores[category] = score\n \n def _check_api_level_documentation(self) -> float:\n \"\"\"Check API-level documentation completeness.\"\"\"\n info = self.spec.get('info', {})\n score = 0\n \n # Required fields\n if info.get('title'):\n score += 20\n if info.get('version'):\n score += 20\n if info.get('description') and len(info['description']) > 20:\n score += 30\n \n # Optional but recommended fields\n if info.get('contact'):\n score += 15\n if info.get('license'):\n score += 15\n \n return score\n \n def _check_endpoint_documentation(self) -> float:\n \"\"\"Check endpoint-level documentation completeness.\"\"\"\n paths = self.spec.get('paths', {})\n \n total_operations = 0\n documented_operations = 0\n \n for path_obj in paths.values():\n if not isinstance(path_obj, dict):\n continue\n \n for method, operation in path_obj.items():\n if method.upper() not in self.http_methods:\n continue\n \n total_operations += 1\n doc_score = 0\n \n if operation.get('summary'):\n doc_score += 1\n if operation.get('description') and len(operation['description']) > 20:\n doc_score += 1\n if operation.get('operationId'):\n doc_score += 1\n \n # Consider it documented if it has at least 2/3 elements\n if doc_score >= 2:\n documented_operations += 1\n \n return (documented_operations / total_operations * 100) if total_operations > 0 else 100\n \n def _check_schema_documentation(self) -> float:\n \"\"\"Check schema documentation completeness.\"\"\"\n schemas = self.spec.get('components', {}).get('schemas', {})\n \n if not schemas:\n return 80 # No schemas to document\n \n total_schemas = len(schemas)\n documented_schemas = 0\n \n for schema_name, schema in schemas.items():\n if not isinstance(schema, dict):\n continue\n \n doc_elements = 0\n \n # Schema-level description\n if schema.get('description'):\n doc_elements += 1\n \n # Property descriptions\n properties = schema.get('properties', {})\n if properties:\n described_props = sum(1 for prop in properties.values() \n if isinstance(prop, dict) and prop.get('description'))\n if described_props > len(properties) * 0.5: # At least 50% documented\n doc_elements += 1\n \n # Examples\n if schema.get('example') or any(\n isinstance(prop, dict) and prop.get('example') \n for prop in properties.values()\n ):\n doc_elements += 1\n \n if doc_elements >= 2:\n documented_schemas += 1\n \n return (documented_schemas / total_schemas * 100) if total_schemas > 0 else 100\n \n def _check_parameter_documentation(self) -> float:\n \"\"\"Check parameter documentation completeness.\"\"\"\n paths = self.spec.get('paths', {})\n \n total_params = 0\n documented_params = 0\n \n for path_obj in paths.values():\n if not isinstance(path_obj, dict):\n continue\n \n for method, operation in path_obj.items():\n if method.upper() not in self.http_methods:\n continue\n \n parameters = operation.get('parameters', [])\n for param in parameters:\n if not isinstance(param, dict):\n continue\n \n total_params += 1\n \n doc_score = 0\n if param.get('description'):\n doc_score += 1\n if param.get('example') or (param.get('schema', {}).get('example')):\n doc_score += 1\n \n if doc_score >= 1: # At least description\n documented_params += 1\n \n return (documented_params / total_params * 100) if total_params > 0 else 100\n \n def _check_response_documentation(self) -> float:\n \"\"\"Check response documentation completeness.\"\"\"\n paths = self.spec.get('paths', {})\n \n total_responses = 0\n documented_responses = 0\n \n for path_obj in paths.values():\n if not isinstance(path_obj, dict):\n continue\n \n for method, operation in path_obj.items():\n if method.upper() not in self.http_methods:\n continue\n \n responses = operation.get('responses', {})\n for status_code, response in responses.items():\n if not isinstance(response, dict):\n continue\n \n total_responses += 1\n \n if response.get('description'):\n documented_responses += 1\n \n return (documented_responses / total_responses * 100) if total_responses > 0 else 100\n \n def _check_example_coverage(self) -> float:\n \"\"\"Check example coverage across the API.\"\"\"\n paths = self.spec.get('paths', {})\n schemas = self.spec.get('components', {}).get('schemas', {})\n \n # Check examples in operations\n total_operations = 0\n operations_with_examples = 0\n \n for path_obj in paths.values():\n if not isinstance(path_obj, dict):\n continue\n \n for method, operation in path_obj.items():\n if method.upper() not in self.http_methods:\n continue\n \n total_operations += 1\n \n has_example = False\n \n # Check request body examples\n request_body = operation.get('requestBody', {})\n if self._has_examples(request_body.get('content', {})):\n has_example = True\n \n # Check response examples\n responses = operation.get('responses', {})\n for response in responses.values():\n if isinstance(response, dict) and self._has_examples(response.get('content', {})):\n has_example = True\n break\n \n if has_example:\n operations_with_examples += 1\n \n # Check examples in schemas\n total_schemas = len(schemas)\n schemas_with_examples = 0\n \n for schema in schemas.values():\n if isinstance(schema, dict) and self._schema_has_examples(schema):\n schemas_with_examples += 1\n \n # Combine scores\n operation_score = (operations_with_examples / total_operations * 100) if total_operations > 0 else 100\n schema_score = (schemas_with_examples / total_schemas * 100) if total_schemas > 0 else 100\n \n return (operation_score + schema_score) / 2\n \n def _has_examples(self, content: Dict[str, Any]) -> bool:\n \"\"\"Check if content has examples.\"\"\"\n for media_type, media_obj in content.items():\n if isinstance(media_obj, dict):\n if media_obj.get('example') or media_obj.get('examples'):\n return True\n return False\n \n def _schema_has_examples(self, schema: Dict[str, Any]) -> bool:\n \"\"\"Check if schema has examples.\"\"\"\n if schema.get('example'):\n return True\n \n properties = schema.get('properties', {})\n for prop in properties.values():\n if isinstance(prop, dict) and prop.get('example'):\n return True\n \n return False\n \n def _score_security(self) -> None:\n \"\"\"Score API security implementation (20% weight).\"\"\"\n category = ScoreCategory.SECURITY\n score = CategoryScore(\n category=category,\n score=0.0,\n max_score=100.0,\n weight=self.category_weights[category]\n )\n \n security_checks = [\n self._check_security_schemes(),\n self._check_security_requirements(),\n self._check_https_usage(),\n self._check_authentication_patterns(),\n self._check_sensitive_data_handling()\n ]\n \n valid_scores = [s for s in security_checks if s is not None]\n if valid_scores:\n score.score = sum(valid_scores) / len(valid_scores)\n \n # Add recommendations\n if score.score \u003c 50:\n score.recommendations.extend([\n \"Implement comprehensive security schemes (OAuth2, API keys, etc.)\",\n \"Ensure all endpoints have appropriate security requirements\",\n \"Add input validation and rate limiting patterns\"\n ])\n elif score.score \u003c 80:\n score.recommendations.extend([\n \"Review security coverage for all endpoints\",\n \"Consider additional security measures for sensitive operations\"\n ])\n \n self.scorecard.category_scores[category] = score\n \n def _check_security_schemes(self) -> float:\n \"\"\"Check security scheme definitions.\"\"\"\n security_schemes = self.spec.get('components', {}).get('securitySchemes', {})\n \n if not security_schemes:\n return 20 # Very low score for no security\n \n score = 40 # Base score for having security schemes\n \n scheme_types = set()\n for scheme in security_schemes.values():\n if isinstance(scheme, dict):\n scheme_type = scheme.get('type')\n scheme_types.add(scheme_type)\n \n # Bonus for modern security schemes\n if 'oauth2' in scheme_types:\n score += 30\n if 'apiKey' in scheme_types:\n score += 15\n if 'http' in scheme_types:\n score += 15\n \n return min(score, 100)\n \n def _check_security_requirements(self) -> float:\n \"\"\"Check security requirement coverage.\"\"\"\n paths = self.spec.get('paths', {})\n global_security = self.spec.get('security', [])\n \n total_operations = 0\n secured_operations = 0\n \n for path_obj in paths.values():\n if not isinstance(path_obj, dict):\n continue\n \n for method, operation in path_obj.items():\n if method.upper() not in self.http_methods:\n continue\n \n total_operations += 1\n \n # Check if operation has security requirements\n operation_security = operation.get('security')\n \n if operation_security is not None:\n secured_operations += 1\n elif global_security:\n secured_operations += 1\n \n return (secured_operations / total_operations * 100) if total_operations > 0 else 0\n \n def _check_https_usage(self) -> float:\n \"\"\"Check HTTPS enforcement.\"\"\"\n servers = self.spec.get('servers', [])\n \n if not servers:\n return 60 # No servers defined - assume HTTPS\n \n https_servers = 0\n for server in servers:\n if isinstance(server, dict):\n url = server.get('url', '')\n if url.startswith('https://') or not url.startswith('http://'):\n https_servers += 1\n \n return (https_servers / len(servers) * 100) if servers else 100\n \n def _check_authentication_patterns(self) -> float:\n \"\"\"Check authentication pattern quality.\"\"\"\n security_schemes = self.spec.get('components', {}).get('securitySchemes', {})\n \n if not security_schemes:\n return 0\n \n pattern_scores = []\n \n for scheme in security_schemes.values():\n if not isinstance(scheme, dict):\n continue\n \n scheme_type = scheme.get('type', '').lower()\n \n if scheme_type == 'oauth2':\n # OAuth2 is highly recommended\n flows = scheme.get('flows', {})\n if flows:\n pattern_scores.append(95)\n else:\n pattern_scores.append(80)\n elif scheme_type == 'http':\n scheme_scheme = scheme.get('scheme', '').lower()\n if scheme_scheme == 'bearer':\n pattern_scores.append(85)\n elif scheme_scheme == 'basic':\n pattern_scores.append(60) # Less secure\n else:\n pattern_scores.append(70)\n elif scheme_type == 'apikey':\n location = scheme.get('in', '').lower()\n if location == 'header':\n pattern_scores.append(75)\n else:\n pattern_scores.append(60) # Query/cookie less secure\n else:\n pattern_scores.append(50) # Unknown scheme\n \n return sum(pattern_scores) / len(pattern_scores) if pattern_scores else 0\n \n def _check_sensitive_data_handling(self) -> float:\n \"\"\"Check sensitive data handling patterns.\"\"\"\n # This is a simplified check - in reality would need more sophisticated analysis\n schemas = self.spec.get('components', {}).get('schemas', {})\n \n score = 80 # Default good score\n \n # Look for potential sensitive fields without proper handling\n sensitive_field_names = {'password', 'secret', 'token', 'key', 'ssn', 'credit_card'}\n \n for schema in schemas.values():\n if not isinstance(schema, dict):\n continue\n \n properties = schema.get('properties', {})\n for prop_name, prop_def in properties.items():\n if not isinstance(prop_def, dict):\n continue\n \n # Check for sensitive field names\n if any(sensitive in prop_name.lower() for sensitive in sensitive_field_names):\n # Check if it's marked as sensitive (writeOnly, format: password, etc.)\n if not (prop_def.get('writeOnly') or \n prop_def.get('format') == 'password' or\n 'password' in prop_def.get('description', '').lower()):\n score -= 10 # Penalty for exposed sensitive field\n \n return max(score, 0)\n \n def _score_usability(self) -> None:\n \"\"\"Score API usability and developer experience (15% weight).\"\"\"\n category = ScoreCategory.USABILITY\n score = CategoryScore(\n category=category,\n score=0.0,\n max_score=100.0,\n weight=self.category_weights[category]\n )\n \n usability_checks = [\n self._check_discoverability(),\n self._check_error_handling(),\n self._check_filtering_and_searching(),\n self._check_resource_relationships(),\n self._check_developer_experience()\n ]\n \n valid_scores = [s for s in usability_checks if s is not None]\n if valid_scores:\n score.score = sum(valid_scores) / len(valid_scores)\n \n # Add recommendations\n if score.score \u003c 60:\n score.recommendations.extend([\n \"Improve error messages with actionable guidance\",\n \"Add filtering and search capabilities to list endpoints\",\n \"Enhance resource discoverability with better linking\"\n ])\n elif score.score \u003c 80:\n score.recommendations.extend([\n \"Consider adding HATEOAS links for better discoverability\",\n \"Enhance developer experience with better examples\"\n ])\n \n self.scorecard.category_scores[category] = score\n \n def _check_discoverability(self) -> float:\n \"\"\"Check API discoverability features.\"\"\"\n paths = self.spec.get('paths', {})\n \n # Look for root/discovery endpoints\n has_root = '/' in paths or any(path == '/api' or path.startswith('/api/') for path in paths)\n \n # Look for HATEOAS patterns in responses\n hateoas_score = 0\n total_responses = 0\n \n for path_obj in paths.values():\n if not isinstance(path_obj, dict):\n continue\n \n for method, operation in path_obj.items():\n if method.upper() not in self.http_methods:\n continue\n \n responses = operation.get('responses', {})\n for response in responses.values():\n if not isinstance(response, dict):\n continue\n \n total_responses += 1\n \n # Look for link-like properties in response schemas\n content = response.get('content', {})\n for media_obj in content.values():\n schema = media_obj.get('schema', {})\n if self._has_link_properties(schema):\n hateoas_score += 1\n break\n \n discovery_score = 50 if has_root else 30\n if total_responses > 0:\n hateoas_ratio = hateoas_score / total_responses\n discovery_score += hateoas_ratio * 50\n \n return min(discovery_score, 100)\n \n def _has_link_properties(self, schema: Dict[str, Any]) -> bool:\n \"\"\"Check if schema has link-like properties.\"\"\"\n if not isinstance(schema, dict):\n return False\n \n properties = schema.get('properties', {})\n link_indicators = {'links', '_links', 'href', 'url', 'self', 'next', 'prev'}\n \n return any(prop_name.lower() in link_indicators for prop_name in properties.keys())\n \n def _check_error_handling(self) -> float:\n \"\"\"Check error handling quality.\"\"\"\n paths = self.spec.get('paths', {})\n \n total_operations = 0\n operations_with_errors = 0\n detailed_error_responses = 0\n \n for path_obj in paths.values():\n if not isinstance(path_obj, dict):\n continue\n \n for method, operation in path_obj.items():\n if method.upper() not in self.http_methods:\n continue\n \n total_operations += 1\n responses = operation.get('responses', {})\n \n # Check for error responses\n has_error_responses = any(\n status_code.startswith('4') or status_code.startswith('5')\n for status_code in responses.keys()\n )\n \n if has_error_responses:\n operations_with_errors += 1\n \n # Check for detailed error schemas\n for status_code, response in responses.items():\n if (status_code.startswith('4') or status_code.startswith('5')) and isinstance(response, dict):\n content = response.get('content', {})\n for media_obj in content.values():\n schema = media_obj.get('schema', {})\n if self._has_detailed_error_schema(schema):\n detailed_error_responses += 1\n break\n break\n \n if total_operations == 0:\n return 0\n \n error_coverage = (operations_with_errors / total_operations) * 60\n error_detail = (detailed_error_responses / operations_with_errors * 40) if operations_with_errors > 0 else 0\n \n return error_coverage + error_detail\n \n def _has_detailed_error_schema(self, schema: Dict[str, Any]) -> bool:\n \"\"\"Check if error schema has detailed information.\"\"\"\n if not isinstance(schema, dict):\n return False\n \n properties = schema.get('properties', {})\n error_fields = {'error', 'message', 'details', 'code', 'timestamp'}\n \n matching_fields = sum(1 for field in error_fields if field in properties)\n return matching_fields >= 2 # At least 2 standard error fields\n \n def _check_filtering_and_searching(self) -> float:\n \"\"\"Check filtering and search capabilities.\"\"\"\n paths = self.spec.get('paths', {})\n \n collection_endpoints = 0\n endpoints_with_filtering = 0\n \n for path, path_obj in paths.items():\n if not isinstance(path_obj, dict):\n continue\n \n # Identify collection endpoints (no path parameters)\n if '{' not in path:\n get_operation = path_obj.get('get')\n if get_operation:\n collection_endpoints += 1\n \n # Check for filtering/search parameters\n parameters = get_operation.get('parameters', [])\n filter_params = {'filter', 'search', 'q', 'query', 'limit', 'page', 'offset'}\n \n has_filtering = any(\n isinstance(param, dict) and param.get('name', '').lower() in filter_params\n for param in parameters\n )\n \n if has_filtering:\n endpoints_with_filtering += 1\n \n return (endpoints_with_filtering / collection_endpoints * 100) if collection_endpoints > 0 else 100\n \n def _check_resource_relationships(self) -> float:\n \"\"\"Check resource relationship handling.\"\"\"\n paths = self.spec.get('paths', {})\n schemas = self.spec.get('components', {}).get('schemas', {})\n \n # Look for nested resource patterns\n nested_resources = 0\n total_resource_paths = 0\n \n for path in paths.keys():\n # Skip root paths\n if path.count('/') >= 3: # e.g., /api/users/123/orders\n total_resource_paths += 1\n if '{' in path:\n nested_resources += 1\n \n # Look for relationship fields in schemas\n schemas_with_relations = 0\n for schema in schemas.values():\n if not isinstance(schema, dict):\n continue\n \n properties = schema.get('properties', {})\n relation_indicators = {'id', '_id', 'ref', 'link', 'relationship'}\n \n has_relations = any(\n any(indicator in prop_name.lower() for indicator in relation_indicators)\n for prop_name in properties.keys()\n )\n \n if has_relations:\n schemas_with_relations += 1\n \n nested_score = (nested_resources / total_resource_paths * 50) if total_resource_paths > 0 else 25\n schema_score = (schemas_with_relations / len(schemas) * 50) if schemas else 25\n \n return nested_score + schema_score\n \n def _check_developer_experience(self) -> float:\n \"\"\"Check overall developer experience factors.\"\"\"\n # This is a composite score based on various DX factors\n factors = []\n \n # Factor 1: Consistent response structure\n factors.append(self._check_response_consistency())\n \n # Factor 2: Clear operation IDs\n paths = self.spec.get('paths', {})\n total_operations = 0\n operations_with_ids = 0\n \n for path_obj in paths.values():\n if not isinstance(path_obj, dict):\n continue\n \n for method, operation in path_obj.items():\n if method.upper() not in self.http_methods:\n continue\n \n total_operations += 1\n if isinstance(operation, dict) and operation.get('operationId'):\n operations_with_ids += 1\n \n operation_id_score = (operations_with_ids / total_operations * 100) if total_operations > 0 else 100\n factors.append(operation_id_score)\n \n # Factor 3: Reasonable path complexity\n avg_path_complexity = 0\n if paths:\n complexities = []\n for path in paths.keys():\n segments = [seg for seg in path.split('/') if seg]\n complexities.append(len(segments))\n \n avg_complexity = sum(complexities) / len(complexities)\n # Optimal complexity is 3-4 segments\n if 3 \u003c= avg_complexity \u003c= 4:\n avg_path_complexity = 100\n elif 2 \u003c= avg_complexity \u003c= 5:\n avg_path_complexity = 80\n else:\n avg_path_complexity = 60\n \n factors.append(avg_path_complexity)\n \n return sum(factors) / len(factors) if factors else 0\n \n def _score_performance(self) -> None:\n \"\"\"Score API performance patterns (15% weight).\"\"\"\n category = ScoreCategory.PERFORMANCE\n score = CategoryScore(\n category=category,\n score=0.0,\n max_score=100.0,\n weight=self.category_weights[category]\n )\n \n performance_checks = [\n self._check_caching_headers(),\n self._check_pagination_patterns(),\n self._check_compression_support(),\n self._check_efficiency_patterns(),\n self._check_batch_operations()\n ]\n \n valid_scores = [s for s in performance_checks if s is not None]\n if valid_scores:\n score.score = sum(valid_scores) / len(valid_scores)\n \n # Add recommendations\n if score.score \u003c 60:\n score.recommendations.extend([\n \"Implement pagination for list endpoints\",\n \"Add caching headers for cacheable responses\",\n \"Consider batch operations for bulk updates\"\n ])\n elif score.score \u003c 80:\n score.recommendations.extend([\n \"Review caching strategies for better performance\",\n \"Consider field selection parameters for large responses\"\n ])\n \n self.scorecard.category_scores[category] = score\n \n def _check_caching_headers(self) -> float:\n \"\"\"Check caching header implementation.\"\"\"\n paths = self.spec.get('paths', {})\n \n get_operations = 0\n cacheable_operations = 0\n \n for path_obj in paths.values():\n if not isinstance(path_obj, dict):\n continue\n \n get_operation = path_obj.get('get')\n if get_operation and isinstance(get_operation, dict):\n get_operations += 1\n \n # Check for caching-related headers in responses\n responses = get_operation.get('responses', {})\n for response in responses.values():\n if not isinstance(response, dict):\n continue\n \n headers = response.get('headers', {})\n cache_headers = {'cache-control', 'etag', 'last-modified', 'expires'}\n \n if any(header.lower() in cache_headers for header in headers.keys()):\n cacheable_operations += 1\n break\n \n return (cacheable_operations / get_operations * 100) if get_operations > 0 else 50\n \n def _check_pagination_patterns(self) -> float:\n \"\"\"Check pagination implementation.\"\"\"\n paths = self.spec.get('paths', {})\n \n collection_endpoints = 0\n paginated_endpoints = 0\n \n for path, path_obj in paths.items():\n if not isinstance(path_obj, dict):\n continue\n \n # Identify collection endpoints\n if '{' not in path: # No path parameters = collection\n get_operation = path_obj.get('get')\n if get_operation and isinstance(get_operation, dict):\n collection_endpoints += 1\n \n # Check for pagination parameters\n parameters = get_operation.get('parameters', [])\n pagination_params = {'limit', 'offset', 'page', 'pagesize', 'per_page', 'cursor'}\n \n has_pagination = any(\n isinstance(param, dict) and param.get('name', '').lower() in pagination_params\n for param in parameters\n )\n \n if has_pagination:\n paginated_endpoints += 1\n \n return (paginated_endpoints / collection_endpoints * 100) if collection_endpoints > 0 else 100\n \n def _check_compression_support(self) -> float:\n \"\"\"Check compression support indicators.\"\"\"\n # This is speculative - OpenAPI doesn't directly specify compression\n # Look for indicators that compression is considered\n \n servers = self.spec.get('servers', [])\n \n # Check if any server descriptions mention compression\n compression_mentions = 0\n for server in servers:\n if isinstance(server, dict):\n description = server.get('description', '').lower()\n if any(term in description for term in ['gzip', 'compress', 'deflate']):\n compression_mentions += 1\n \n # Base score - assume compression is handled at server level\n base_score = 70\n \n if compression_mentions > 0:\n return min(base_score + (compression_mentions * 10), 100)\n \n return base_score\n \n def _check_efficiency_patterns(self) -> float:\n \"\"\"Check efficiency patterns like field selection.\"\"\"\n paths = self.spec.get('paths', {})\n \n total_get_operations = 0\n operations_with_selection = 0\n \n for path_obj in paths.values():\n if not isinstance(path_obj, dict):\n continue\n \n get_operation = path_obj.get('get')\n if get_operation and isinstance(get_operation, dict):\n total_get_operations += 1\n \n # Check for field selection parameters\n parameters = get_operation.get('parameters', [])\n selection_params = {'fields', 'select', 'include', 'exclude'}\n \n has_selection = any(\n isinstance(param, dict) and param.get('name', '').lower() in selection_params\n for param in parameters\n )\n \n if has_selection:\n operations_with_selection += 1\n \n return (operations_with_selection / total_get_operations * 100) if total_get_operations > 0 else 60\n \n def _check_batch_operations(self) -> float:\n \"\"\"Check for batch operation support.\"\"\"\n paths = self.spec.get('paths', {})\n \n # Look for batch endpoints\n batch_indicators = ['batch', 'bulk', 'multi']\n batch_endpoints = 0\n \n for path in paths.keys():\n if any(indicator in path.lower() for indicator in batch_indicators):\n batch_endpoints += 1\n \n # Look for array-based request bodies (indicating batch operations)\n array_operations = 0\n total_post_put_operations = 0\n \n for path_obj in paths.values():\n if not isinstance(path_obj, dict):\n continue\n \n for method in ['post', 'put', 'patch']:\n operation = path_obj.get(method)\n if operation and isinstance(operation, dict):\n total_post_put_operations += 1\n \n request_body = operation.get('requestBody', {})\n content = request_body.get('content', {})\n \n for media_obj in content.values():\n schema = media_obj.get('schema', {})\n if schema.get('type') == 'array':\n array_operations += 1\n break\n \n # Score based on presence of batch patterns\n batch_score = min(batch_endpoints * 20, 60) # Up to 60 points for explicit batch endpoints\n \n if total_post_put_operations > 0:\n array_score = (array_operations / total_post_put_operations) * 40\n batch_score += array_score\n \n return min(batch_score, 100)\n \n def generate_json_report(self) -> str:\n \"\"\"Generate JSON format scorecard.\"\"\"\n report_data = {\n \"overall\": {\n \"score\": round(self.scorecard.overall_score, 2),\n \"grade\": self.scorecard.overall_grade,\n \"totalEndpoints\": self.scorecard.total_endpoints\n },\n \"api_info\": self.scorecard.api_info,\n \"categories\": {},\n \"topRecommendations\": self.scorecard.get_top_recommendations()\n }\n \n for category, score in self.scorecard.category_scores.items():\n report_data[\"categories\"][category.value] = {\n \"score\": round(score.score, 2),\n \"grade\": score.letter_grade,\n \"weight\": score.weight,\n \"weightedScore\": round(score.weighted_score, 2),\n \"issues\": score.issues,\n \"recommendations\": score.recommendations\n }\n \n return json.dumps(report_data, indent=2)\n \n def generate_text_report(self) -> str:\n \"\"\"Generate human-readable scorecard report.\"\"\"\n lines = [\n \"═══════════════════════════════════════════════════════════════\",\n \" API DESIGN SCORECARD\",\n \"═══════════════════════════════════════════════════════════════\",\n f\"API: {self.scorecard.api_info.get('title', 'Unknown')}\",\n f\"Version: {self.scorecard.api_info.get('version', 'Unknown')}\",\n f\"Total Endpoints: {self.scorecard.total_endpoints}\",\n \"\",\n f\"🏆 OVERALL GRADE: {self.scorecard.overall_grade} ({self.scorecard.overall_score:.1f}/100.0)\",\n \"\",\n \"═══════════════════════════════════════════════════════════════\",\n \"DETAILED BREAKDOWN:\",\n \"═══════════════════════════════════════════════════════════════\"\n ]\n \n # Sort categories by weight (most important first)\n sorted_categories = sorted(\n self.scorecard.category_scores.items(),\n key=lambda x: x[1].weight,\n reverse=True\n )\n \n for category, score in sorted_categories:\n category_name = category.value.title().replace('_', ' ')\n \n lines.extend([\n \"\",\n f\"📊 {category_name.upper()} - Grade: {score.letter_grade} ({score.score:.1f}/100)\",\n f\" Weight: {score.weight}% | Contribution: {score.weighted_score:.1f} points\",\n \" \" + \"─\" * 50\n ])\n \n if score.recommendations:\n lines.append(\" 💡 Recommendations:\")\n for rec in score.recommendations[:3]: # Top 3 recommendations\n lines.append(f\" • {rec}\")\n else:\n lines.append(\" ✅ No specific recommendations - performing well!\")\n \n # Overall assessment\n lines.extend([\n \"\",\n \"═══════════════════════════════════════════════════════════════\",\n \"OVERALL ASSESSMENT:\",\n \"═══════════════════════════════════════════════════════════════\"\n ])\n \n if self.scorecard.overall_grade == \"A\":\n lines.extend([\n \"🏆 EXCELLENT! Your API demonstrates outstanding design quality.\",\n \" Continue following these best practices and consider sharing\",\n \" your approach as a reference for other teams.\"\n ])\n elif self.scorecard.overall_grade == \"B\":\n lines.extend([\n \"✅ GOOD! Your API follows most best practices with room for\",\n \" minor improvements. Focus on the recommendations above\",\n \" to achieve excellence.\"\n ])\n elif self.scorecard.overall_grade == \"C\":\n lines.extend([\n \"⚠️ FAIR! Your API has a solid foundation but several areas\",\n \" need improvement. Prioritize the high-weight categories\",\n \" for maximum impact.\"\n ])\n elif self.scorecard.overall_grade == \"D\":\n lines.extend([\n \"❌ NEEDS IMPROVEMENT! Your API has significant issues that\",\n \" may impact developer experience and maintainability.\",\n \" Focus on consistency and documentation first.\"\n ])\n else: # Grade F\n lines.extend([\n \"🚨 CRITICAL ISSUES! Your API requires major redesign to meet\",\n \" basic quality standards. Consider comprehensive review\",\n \" of design principles and best practices.\"\n ])\n \n # Top recommendations\n top_recs = self.scorecard.get_top_recommendations(3)\n if top_recs:\n lines.extend([\n \"\",\n \"🎯 TOP PRIORITY RECOMMENDATIONS:\",\n \"\"\n ])\n for i, rec in enumerate(top_recs, 1):\n lines.append(f\" {i}. {rec}\")\n \n lines.extend([\n \"\",\n \"═══════════════════════════════════════════════════════════════\",\n f\"Generated by API Scorecard Tool | Score: {self.scorecard.overall_grade} ({self.scorecard.overall_score:.1f}%)\",\n \"═══════════════════════════════════════════════════════════════\"\n ])\n \n return \"\\n\".join(lines)\n\n\ndef main():\n \"\"\"Main CLI entry point.\"\"\"\n parser = argparse.ArgumentParser(\n description=\"Generate comprehensive API design quality scorecard\",\n formatter_class=argparse.RawDescriptionHelpFormatter,\n epilog=\"\"\"\nExamples:\n python api_scorecard.py openapi.json\n python api_scorecard.py --format json openapi.json > scorecard.json\n python api_scorecard.py --output scorecard.txt openapi.json\n \"\"\"\n )\n \n parser.add_argument(\n 'spec_file',\n help='OpenAPI/Swagger specification file (JSON format)'\n )\n \n parser.add_argument(\n '--format',\n choices=['text', 'json'],\n default='text',\n help='Output format (default: text)'\n )\n \n parser.add_argument(\n '--output',\n help='Output file (default: stdout)'\n )\n \n parser.add_argument(\n '--min-grade',\n choices=['A', 'B', 'C', 'D', 'F'],\n help='Exit with code 1 if grade is below minimum'\n )\n \n args = parser.parse_args()\n \n # Load specification file\n try:\n with open(args.spec_file, 'r') as f:\n spec = json.load(f)\n except FileNotFoundError:\n print(f\"Error: Specification file '{args.spec_file}' not found.\", file=sys.stderr)\n return 1\n except json.JSONDecodeError as e:\n print(f\"Error: Invalid JSON in '{args.spec_file}': {e}\", file=sys.stderr)\n return 1\n \n # Initialize scoring engine and generate scorecard\n engine = APIScoringEngine()\n \n try:\n scorecard = engine.score_api(spec)\n except Exception as e:\n print(f\"Error during scoring: {e}\", file=sys.stderr)\n return 1\n \n # Generate report\n if args.format == 'json':\n output = engine.generate_json_report()\n else:\n output = engine.generate_text_report()\n \n # Write output\n if args.output:\n try:\n with open(args.output, 'w') as f:\n f.write(output)\n print(f\"Scorecard written to {args.output}\")\n except IOError as e:\n print(f\"Error writing to '{args.output}': {e}\", file=sys.stderr)\n return 1\n else:\n print(output)\n \n # Check minimum grade requirement\n if args.min_grade:\n grade_order = ['F', 'D', 'C', 'B', 'A']\n current_grade_index = grade_order.index(scorecard.overall_grade)\n min_grade_index = grade_order.index(args.min_grade)\n \n if current_grade_index \u003c min_grade_index:\n print(f\"Grade {scorecard.overall_grade} is below minimum required grade {args.min_grade}\", file=sys.stderr)\n return 1\n \n return 0\n\n\nif __name__ == '__main__':\n sys.exit(main())","content_type":"text/x-python; charset=utf-8","language":"python","size":66377,"content_sha256":"a0df19fcdb7d4c137d75800d05475f47cf4fede77f33ae21bddda97c8efed302"},{"filename":"scripts/breaking_change_detector.py","content":"#!/usr/bin/env python3\n\"\"\"\nBreaking Change Detector - Compares API specification versions to identify breaking changes.\n\nThis script analyzes two versions of an API specification and detects potentially\nbreaking changes including:\n- Removed endpoints\n- Modified response structures\n- Removed or renamed fields\n- Field type changes\n- New required fields\n- HTTP status code changes\n- Parameter changes\n\nGenerates detailed reports with migration guides for each breaking change.\n\"\"\"\n\nimport argparse\nimport json\nimport sys\nfrom typing import Any, Dict, List, Set, Optional, Tuple, Union\nfrom dataclasses import dataclass, field\nfrom enum import Enum\n\n# Windows consoles often default to a legacy codepage (e.g. cp1252) that cannot\n# encode the Unicode glyphs this script prints (issue #969). Re-encode\n# stdout/stderr as UTF-8 with replacement so output never crashes at print time.\nif hasattr(sys.stdout, \"reconfigure\"):\n sys.stdout.reconfigure(encoding=\"utf-8\", errors=\"replace\")\nif hasattr(sys.stderr, \"reconfigure\"):\n sys.stderr.reconfigure(encoding=\"utf-8\", errors=\"replace\")\n\n\n\nclass ChangeType(Enum):\n \"\"\"Types of API changes.\"\"\"\n BREAKING = \"breaking\"\n POTENTIALLY_BREAKING = \"potentially_breaking\"\n NON_BREAKING = \"non_breaking\"\n ENHANCEMENT = \"enhancement\"\n\n\nclass ChangeSeverity(Enum):\n \"\"\"Severity levels for changes.\"\"\"\n CRITICAL = \"critical\" # Will definitely break clients\n HIGH = \"high\" # Likely to break some clients\n MEDIUM = \"medium\" # May break clients depending on usage\n LOW = \"low\" # Minor impact, unlikely to break clients\n INFO = \"info\" # Informational, no breaking impact\n\n\n@dataclass\nclass Change:\n \"\"\"Represents a detected change between API versions.\"\"\"\n change_type: ChangeType\n severity: ChangeSeverity\n category: str\n path: str\n message: str\n old_value: Any = None\n new_value: Any = None\n migration_guide: str = \"\"\n impact_description: str = \"\"\n \n def to_dict(self) -> Dict[str, Any]:\n \"\"\"Convert change to dictionary for JSON serialization.\"\"\"\n return {\n \"changeType\": self.change_type.value,\n \"severity\": self.severity.value,\n \"category\": self.category,\n \"path\": self.path,\n \"message\": self.message,\n \"oldValue\": self.old_value,\n \"newValue\": self.new_value,\n \"migrationGuide\": self.migration_guide,\n \"impactDescription\": self.impact_description\n }\n\n\n@dataclass\nclass ComparisonReport:\n \"\"\"Complete comparison report between two API versions.\"\"\"\n changes: List[Change] = field(default_factory=list)\n summary: Dict[str, int] = field(default_factory=dict)\n \n def add_change(self, change: Change) -> None:\n \"\"\"Add a change to the report.\"\"\"\n self.changes.append(change)\n \n def calculate_summary(self) -> None:\n \"\"\"Calculate summary statistics.\"\"\"\n self.summary = {\n \"total_changes\": len(self.changes),\n \"breaking_changes\": len([c for c in self.changes if c.change_type == ChangeType.BREAKING]),\n \"potentially_breaking_changes\": len([c for c in self.changes if c.change_type == ChangeType.POTENTIALLY_BREAKING]),\n \"non_breaking_changes\": len([c for c in self.changes if c.change_type == ChangeType.NON_BREAKING]),\n \"enhancements\": len([c for c in self.changes if c.change_type == ChangeType.ENHANCEMENT]),\n \"critical_severity\": len([c for c in self.changes if c.severity == ChangeSeverity.CRITICAL]),\n \"high_severity\": len([c for c in self.changes if c.severity == ChangeSeverity.HIGH]),\n \"medium_severity\": len([c for c in self.changes if c.severity == ChangeSeverity.MEDIUM]),\n \"low_severity\": len([c for c in self.changes if c.severity == ChangeSeverity.LOW]),\n \"info_severity\": len([c for c in self.changes if c.severity == ChangeSeverity.INFO])\n }\n \n def has_breaking_changes(self) -> bool:\n \"\"\"Check if report contains any breaking changes.\"\"\"\n return any(c.change_type in [ChangeType.BREAKING, ChangeType.POTENTIALLY_BREAKING] \n for c in self.changes)\n\n\nclass BreakingChangeDetector:\n \"\"\"Main breaking change detection engine.\"\"\"\n \n def __init__(self):\n self.report = ComparisonReport()\n self.old_spec: Optional[Dict] = None\n self.new_spec: Optional[Dict] = None\n \n def compare_specs(self, old_spec: Dict[str, Any], new_spec: Dict[str, Any]) -> ComparisonReport:\n \"\"\"Compare two API specifications and detect changes.\"\"\"\n self.old_spec = old_spec\n self.new_spec = new_spec\n self.report = ComparisonReport()\n \n # Compare different sections of the API specification\n self._compare_info_section()\n self._compare_servers_section()\n self._compare_paths_section()\n self._compare_components_section()\n self._compare_security_section()\n \n # Calculate summary statistics\n self.report.calculate_summary()\n \n return self.report\n \n def _compare_info_section(self) -> None:\n \"\"\"Compare API info sections.\"\"\"\n old_info = self.old_spec.get('info', {})\n new_info = self.new_spec.get('info', {})\n \n # Version comparison\n old_version = old_info.get('version', '')\n new_version = new_info.get('version', '')\n \n if old_version != new_version:\n self.report.add_change(Change(\n change_type=ChangeType.NON_BREAKING,\n severity=ChangeSeverity.INFO,\n category=\"versioning\",\n path=\"/info/version\",\n message=f\"API version changed from '{old_version}' to '{new_version}'\",\n old_value=old_version,\n new_value=new_version,\n impact_description=\"Version change indicates API evolution\"\n ))\n \n # Title comparison\n old_title = old_info.get('title', '')\n new_title = new_info.get('title', '')\n \n if old_title != new_title:\n self.report.add_change(Change(\n change_type=ChangeType.NON_BREAKING,\n severity=ChangeSeverity.INFO,\n category=\"metadata\",\n path=\"/info/title\",\n message=f\"API title changed from '{old_title}' to '{new_title}'\",\n old_value=old_title,\n new_value=new_title,\n impact_description=\"Title change is cosmetic and doesn't affect functionality\"\n ))\n \n def _compare_servers_section(self) -> None:\n \"\"\"Compare server configurations.\"\"\"\n old_servers = self.old_spec.get('servers', [])\n new_servers = self.new_spec.get('servers', [])\n \n old_urls = {server.get('url', '') for server in old_servers if isinstance(server, dict)}\n new_urls = {server.get('url', '') for server in new_servers if isinstance(server, dict)}\n \n # Removed servers\n removed_urls = old_urls - new_urls\n for url in removed_urls:\n self.report.add_change(Change(\n change_type=ChangeType.BREAKING,\n severity=ChangeSeverity.HIGH,\n category=\"servers\",\n path=\"/servers\",\n message=f\"Server URL removed: {url}\",\n old_value=url,\n new_value=None,\n migration_guide=f\"Update client configurations to use alternative server URLs: {list(new_urls)}\",\n impact_description=\"Clients configured to use removed server URL will fail to connect\"\n ))\n \n # Added servers\n added_urls = new_urls - old_urls\n for url in added_urls:\n self.report.add_change(Change(\n change_type=ChangeType.ENHANCEMENT,\n severity=ChangeSeverity.INFO,\n category=\"servers\",\n path=\"/servers\",\n message=f\"New server URL added: {url}\",\n old_value=None,\n new_value=url,\n impact_description=\"New server option provides additional deployment flexibility\"\n ))\n \n def _compare_paths_section(self) -> None:\n \"\"\"Compare API paths and operations.\"\"\"\n old_paths = self.old_spec.get('paths', {})\n new_paths = self.new_spec.get('paths', {})\n \n # Find removed, added, and modified paths\n old_path_set = set(old_paths.keys())\n new_path_set = set(new_paths.keys())\n \n removed_paths = old_path_set - new_path_set\n added_paths = new_path_set - old_path_set\n common_paths = old_path_set & new_path_set\n \n # Handle removed paths\n for path in removed_paths:\n old_operations = self._extract_operations(old_paths[path])\n for method in old_operations:\n self.report.add_change(Change(\n change_type=ChangeType.BREAKING,\n severity=ChangeSeverity.CRITICAL,\n category=\"endpoints\",\n path=f\"/paths{path}\",\n message=f\"Endpoint removed: {method.upper()} {path}\",\n old_value=f\"{method.upper()} {path}\",\n new_value=None,\n migration_guide=self._generate_endpoint_removal_migration(path, method, new_paths),\n impact_description=\"Clients using this endpoint will receive 404 errors\"\n ))\n \n # Handle added paths\n for path in added_paths:\n new_operations = self._extract_operations(new_paths[path])\n for method in new_operations:\n self.report.add_change(Change(\n change_type=ChangeType.ENHANCEMENT,\n severity=ChangeSeverity.INFO,\n category=\"endpoints\",\n path=f\"/paths{path}\",\n message=f\"New endpoint added: {method.upper()} {path}\",\n old_value=None,\n new_value=f\"{method.upper()} {path}\",\n impact_description=\"New functionality available to clients\"\n ))\n \n # Handle modified paths\n for path in common_paths:\n self._compare_path_operations(path, old_paths[path], new_paths[path])\n \n def _extract_operations(self, path_object: Dict[str, Any]) -> List[str]:\n \"\"\"Extract HTTP operations from a path object.\"\"\"\n http_methods = {'get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'}\n return [method for method in path_object.keys() if method.lower() in http_methods]\n \n def _compare_path_operations(self, path: str, old_path_obj: Dict, new_path_obj: Dict) -> None:\n \"\"\"Compare operations within a specific path.\"\"\"\n old_operations = set(self._extract_operations(old_path_obj))\n new_operations = set(self._extract_operations(new_path_obj))\n \n # Removed operations\n removed_ops = old_operations - new_operations\n for method in removed_ops:\n self.report.add_change(Change(\n change_type=ChangeType.BREAKING,\n severity=ChangeSeverity.CRITICAL,\n category=\"endpoints\",\n path=f\"/paths{path}/{method}\",\n message=f\"HTTP method removed: {method.upper()} {path}\",\n old_value=f\"{method.upper()} {path}\",\n new_value=None,\n migration_guide=self._generate_method_removal_migration(path, method, new_operations),\n impact_description=\"Clients using this method will receive 405 Method Not Allowed errors\"\n ))\n \n # Added operations\n added_ops = new_operations - old_operations\n for method in added_ops:\n self.report.add_change(Change(\n change_type=ChangeType.ENHANCEMENT,\n severity=ChangeSeverity.INFO,\n category=\"endpoints\",\n path=f\"/paths{path}/{method}\",\n message=f\"New HTTP method added: {method.upper()} {path}\",\n old_value=None,\n new_value=f\"{method.upper()} {path}\",\n impact_description=\"New method provides additional functionality for this resource\"\n ))\n \n # Modified operations\n common_ops = old_operations & new_operations\n for method in common_ops:\n self._compare_operation_details(path, method, old_path_obj[method], new_path_obj[method])\n \n def _compare_operation_details(self, path: str, method: str, old_op: Dict, new_op: Dict) -> None:\n \"\"\"Compare details of individual operations.\"\"\"\n operation_path = f\"/paths{path}/{method}\"\n \n # Compare parameters\n self._compare_parameters(operation_path, old_op.get('parameters', []), new_op.get('parameters', []))\n \n # Compare request body\n self._compare_request_body(operation_path, old_op.get('requestBody'), new_op.get('requestBody'))\n \n # Compare responses\n self._compare_responses(operation_path, old_op.get('responses', {}), new_op.get('responses', {}))\n \n # Compare security requirements\n self._compare_security_requirements(operation_path, old_op.get('security'), new_op.get('security'))\n \n def _compare_parameters(self, base_path: str, old_params: List[Dict], new_params: List[Dict]) -> None:\n \"\"\"Compare operation parameters.\"\"\"\n # Create lookup dictionaries\n old_param_map = {(p.get('name'), p.get('in')): p for p in old_params}\n new_param_map = {(p.get('name'), p.get('in')): p for p in new_params}\n \n old_param_keys = set(old_param_map.keys())\n new_param_keys = set(new_param_map.keys())\n \n # Removed parameters\n removed_params = old_param_keys - new_param_keys\n for param_key in removed_params:\n name, location = param_key\n self.report.add_change(Change(\n change_type=ChangeType.BREAKING,\n severity=ChangeSeverity.HIGH,\n category=\"parameters\",\n path=f\"{base_path}/parameters\",\n message=f\"Parameter removed: {name} (in: {location})\",\n old_value=old_param_map[param_key],\n new_value=None,\n migration_guide=f\"Remove '{name}' parameter from {location} when calling this endpoint\",\n impact_description=\"Clients sending this parameter may receive validation errors\"\n ))\n \n # Added parameters\n added_params = new_param_keys - old_param_keys\n for param_key in added_params:\n name, location = param_key\n new_param = new_param_map[param_key]\n is_required = new_param.get('required', False)\n \n if is_required:\n self.report.add_change(Change(\n change_type=ChangeType.BREAKING,\n severity=ChangeSeverity.CRITICAL,\n category=\"parameters\",\n path=f\"{base_path}/parameters\",\n message=f\"New required parameter added: {name} (in: {location})\",\n old_value=None,\n new_value=new_param,\n migration_guide=f\"Add required '{name}' parameter to {location} when calling this endpoint\",\n impact_description=\"Clients not providing this parameter will receive 400 Bad Request errors\"\n ))\n else:\n self.report.add_change(Change(\n change_type=ChangeType.NON_BREAKING,\n severity=ChangeSeverity.INFO,\n category=\"parameters\",\n path=f\"{base_path}/parameters\",\n message=f\"New optional parameter added: {name} (in: {location})\",\n old_value=None,\n new_value=new_param,\n impact_description=\"Optional parameter provides additional functionality\"\n ))\n \n # Modified parameters\n common_params = old_param_keys & new_param_keys\n for param_key in common_params:\n name, location = param_key\n old_param = old_param_map[param_key]\n new_param = new_param_map[param_key]\n self._compare_parameter_details(base_path, name, location, old_param, new_param)\n \n def _compare_parameter_details(self, base_path: str, name: str, location: str, \n old_param: Dict, new_param: Dict) -> None:\n \"\"\"Compare individual parameter details.\"\"\"\n param_path = f\"{base_path}/parameters/{name}\"\n \n # Required status change\n old_required = old_param.get('required', False)\n new_required = new_param.get('required', False)\n \n if old_required != new_required:\n if new_required:\n self.report.add_change(Change(\n change_type=ChangeType.BREAKING,\n severity=ChangeSeverity.HIGH,\n category=\"parameters\",\n path=param_path,\n message=f\"Parameter '{name}' is now required (was optional)\",\n old_value=old_required,\n new_value=new_required,\n migration_guide=f\"Ensure '{name}' parameter is always provided when calling this endpoint\",\n impact_description=\"Clients not providing this parameter will receive validation errors\"\n ))\n else:\n self.report.add_change(Change(\n change_type=ChangeType.NON_BREAKING,\n severity=ChangeSeverity.INFO,\n category=\"parameters\",\n path=param_path,\n message=f\"Parameter '{name}' is now optional (was required)\",\n old_value=old_required,\n new_value=new_required,\n impact_description=\"Parameter is now optional, providing more flexibility to clients\"\n ))\n \n # Schema/type changes\n old_schema = old_param.get('schema', {})\n new_schema = new_param.get('schema', {})\n \n if old_schema != new_schema:\n self._compare_schemas(param_path, old_schema, new_schema, f\"parameter '{name}'\")\n \n def _compare_request_body(self, base_path: str, old_body: Optional[Dict], new_body: Optional[Dict]) -> None:\n \"\"\"Compare request body specifications.\"\"\"\n body_path = f\"{base_path}/requestBody\"\n \n # Request body added\n if old_body is None and new_body is not None:\n is_required = new_body.get('required', False)\n if is_required:\n self.report.add_change(Change(\n change_type=ChangeType.BREAKING,\n severity=ChangeSeverity.HIGH,\n category=\"request_body\",\n path=body_path,\n message=\"Required request body added\",\n old_value=None,\n new_value=new_body,\n migration_guide=\"Include request body with appropriate content type when calling this endpoint\",\n impact_description=\"Clients not providing request body will receive validation errors\"\n ))\n else:\n self.report.add_change(Change(\n change_type=ChangeType.NON_BREAKING,\n severity=ChangeSeverity.INFO,\n category=\"request_body\",\n path=body_path,\n message=\"Optional request body added\",\n old_value=None,\n new_value=new_body,\n impact_description=\"Optional request body provides additional functionality\"\n ))\n \n # Request body removed\n elif old_body is not None and new_body is None:\n self.report.add_change(Change(\n change_type=ChangeType.BREAKING,\n severity=ChangeSeverity.HIGH,\n category=\"request_body\",\n path=body_path,\n message=\"Request body removed\",\n old_value=old_body,\n new_value=None,\n migration_guide=\"Remove request body when calling this endpoint\",\n impact_description=\"Clients sending request body may receive validation errors\"\n ))\n \n # Request body modified\n elif old_body is not None and new_body is not None:\n self._compare_request_body_details(body_path, old_body, new_body)\n \n def _compare_request_body_details(self, base_path: str, old_body: Dict, new_body: Dict) -> None:\n \"\"\"Compare request body details.\"\"\"\n # Required status change\n old_required = old_body.get('required', False)\n new_required = new_body.get('required', False)\n \n if old_required != new_required:\n if new_required:\n self.report.add_change(Change(\n change_type=ChangeType.BREAKING,\n severity=ChangeSeverity.HIGH,\n category=\"request_body\",\n path=base_path,\n message=\"Request body is now required (was optional)\",\n old_value=old_required,\n new_value=new_required,\n migration_guide=\"Always include request body when calling this endpoint\",\n impact_description=\"Clients not providing request body will receive validation errors\"\n ))\n else:\n self.report.add_change(Change(\n change_type=ChangeType.NON_BREAKING,\n severity=ChangeSeverity.INFO,\n category=\"request_body\",\n path=base_path,\n message=\"Request body is now optional (was required)\",\n old_value=old_required,\n new_value=new_required,\n impact_description=\"Request body is now optional, providing more flexibility\"\n ))\n \n # Content type changes\n old_content = old_body.get('content', {})\n new_content = new_body.get('content', {})\n self._compare_content_types(base_path, old_content, new_content, \"request body\")\n \n def _compare_responses(self, base_path: str, old_responses: Dict, new_responses: Dict) -> None:\n \"\"\"Compare response specifications.\"\"\"\n responses_path = f\"{base_path}/responses\"\n \n old_status_codes = set(old_responses.keys())\n new_status_codes = set(new_responses.keys())\n \n # Removed status codes\n removed_codes = old_status_codes - new_status_codes\n for code in removed_codes:\n self.report.add_change(Change(\n change_type=ChangeType.BREAKING,\n severity=ChangeSeverity.HIGH,\n category=\"responses\",\n path=f\"{responses_path}/{code}\",\n message=f\"Response status code {code} removed\",\n old_value=old_responses[code],\n new_value=None,\n migration_guide=f\"Handle alternative status codes: {list(new_status_codes)}\",\n impact_description=f\"Clients expecting status code {code} need to handle different responses\"\n ))\n \n # Added status codes\n added_codes = new_status_codes - old_status_codes\n for code in added_codes:\n self.report.add_change(Change(\n change_type=ChangeType.NON_BREAKING,\n severity=ChangeSeverity.INFO,\n category=\"responses\",\n path=f\"{responses_path}/{code}\",\n message=f\"New response status code {code} added\",\n old_value=None,\n new_value=new_responses[code],\n impact_description=\"New status code provides more specific response information\"\n ))\n \n # Modified responses\n common_codes = old_status_codes & new_status_codes\n for code in common_codes:\n self._compare_response_details(responses_path, code, old_responses[code], new_responses[code])\n \n def _compare_response_details(self, base_path: str, status_code: str, \n old_response: Dict, new_response: Dict) -> None:\n \"\"\"Compare individual response details.\"\"\"\n response_path = f\"{base_path}/{status_code}\"\n \n # Compare content types and schemas\n old_content = old_response.get('content', {})\n new_content = new_response.get('content', {})\n \n self._compare_content_types(response_path, old_content, new_content, f\"response {status_code}\")\n \n def _compare_content_types(self, base_path: str, old_content: Dict, new_content: Dict, context: str) -> None:\n \"\"\"Compare content types and their schemas.\"\"\"\n old_types = set(old_content.keys())\n new_types = set(new_content.keys())\n \n # Removed content types\n removed_types = old_types - new_types\n for content_type in removed_types:\n self.report.add_change(Change(\n change_type=ChangeType.BREAKING,\n severity=ChangeSeverity.HIGH,\n category=\"content_types\",\n path=f\"{base_path}/content\",\n message=f\"Content type '{content_type}' removed from {context}\",\n old_value=content_type,\n new_value=None,\n migration_guide=f\"Use alternative content types: {list(new_types)}\",\n impact_description=f\"Clients expecting '{content_type}' need to handle different formats\"\n ))\n \n # Added content types\n added_types = new_types - old_types\n for content_type in added_types:\n self.report.add_change(Change(\n change_type=ChangeType.ENHANCEMENT,\n severity=ChangeSeverity.INFO,\n category=\"content_types\",\n path=f\"{base_path}/content\",\n message=f\"New content type '{content_type}' added to {context}\",\n old_value=None,\n new_value=content_type,\n impact_description=f\"Additional format option available for {context}\"\n ))\n \n # Modified schemas for common content types\n common_types = old_types & new_types\n for content_type in common_types:\n old_media = old_content[content_type]\n new_media = new_content[content_type]\n \n old_schema = old_media.get('schema', {})\n new_schema = new_media.get('schema', {})\n \n if old_schema != new_schema:\n schema_path = f\"{base_path}/content/{content_type}/schema\"\n self._compare_schemas(schema_path, old_schema, new_schema, f\"{context} ({content_type})\")\n \n def _compare_schemas(self, base_path: str, old_schema: Dict, new_schema: Dict, context: str) -> None:\n \"\"\"Compare schema definitions.\"\"\"\n # Type changes\n old_type = old_schema.get('type')\n new_type = new_schema.get('type')\n \n if old_type != new_type and old_type is not None and new_type is not None:\n self.report.add_change(Change(\n change_type=ChangeType.BREAKING,\n severity=ChangeSeverity.CRITICAL,\n category=\"schema\",\n path=base_path,\n message=f\"Schema type changed from '{old_type}' to '{new_type}' for {context}\",\n old_value=old_type,\n new_value=new_type,\n migration_guide=f\"Update client code to handle {new_type} instead of {old_type}\",\n impact_description=\"Type change will break client parsing and validation\"\n ))\n \n # Property changes for object types\n if old_schema.get('type') == 'object' and new_schema.get('type') == 'object':\n self._compare_object_properties(base_path, old_schema, new_schema, context)\n \n # Array item changes\n if old_schema.get('type') == 'array' and new_schema.get('type') == 'array':\n old_items = old_schema.get('items', {})\n new_items = new_schema.get('items', {})\n if old_items != new_items:\n self._compare_schemas(f\"{base_path}/items\", old_items, new_items, f\"{context} items\")\n \n def _compare_object_properties(self, base_path: str, old_schema: Dict, new_schema: Dict, context: str) -> None:\n \"\"\"Compare object schema properties.\"\"\"\n old_props = old_schema.get('properties', {})\n new_props = new_schema.get('properties', {})\n old_required = set(old_schema.get('required', []))\n new_required = set(new_schema.get('required', []))\n \n old_prop_names = set(old_props.keys())\n new_prop_names = set(new_props.keys())\n \n # Removed properties\n removed_props = old_prop_names - new_prop_names\n for prop_name in removed_props:\n severity = ChangeSeverity.CRITICAL if prop_name in old_required else ChangeSeverity.HIGH\n self.report.add_change(Change(\n change_type=ChangeType.BREAKING,\n severity=severity,\n category=\"schema\",\n path=f\"{base_path}/properties\",\n message=f\"Property '{prop_name}' removed from {context}\",\n old_value=old_props[prop_name],\n new_value=None,\n migration_guide=f\"Remove references to '{prop_name}' property in client code\",\n impact_description=\"Clients expecting this property will receive incomplete data\"\n ))\n \n # Added properties\n added_props = new_prop_names - old_prop_names\n for prop_name in added_props:\n if prop_name in new_required:\n # This is handled separately in required field changes\n pass\n else:\n self.report.add_change(Change(\n change_type=ChangeType.NON_BREAKING,\n severity=ChangeSeverity.INFO,\n category=\"schema\",\n path=f\"{base_path}/properties\",\n message=f\"New optional property '{prop_name}' added to {context}\",\n old_value=None,\n new_value=new_props[prop_name],\n impact_description=\"New property provides additional data without breaking existing clients\"\n ))\n \n # Required field changes\n added_required = new_required - old_required\n removed_required = old_required - new_required\n \n for prop_name in added_required:\n self.report.add_change(Change(\n change_type=ChangeType.BREAKING,\n severity=ChangeSeverity.CRITICAL,\n category=\"schema\",\n path=f\"{base_path}/properties\",\n message=f\"Property '{prop_name}' is now required in {context}\",\n old_value=False,\n new_value=True,\n migration_guide=f\"Ensure '{prop_name}' is always provided when sending {context}\",\n impact_description=\"Clients not providing this property will receive validation errors\"\n ))\n \n for prop_name in removed_required:\n self.report.add_change(Change(\n change_type=ChangeType.NON_BREAKING,\n severity=ChangeSeverity.INFO,\n category=\"schema\",\n path=f\"{base_path}/properties\",\n message=f\"Property '{prop_name}' is no longer required in {context}\",\n old_value=True,\n new_value=False,\n impact_description=\"Property is now optional, providing more flexibility\"\n ))\n \n # Modified properties\n common_props = old_prop_names & new_prop_names\n for prop_name in common_props:\n old_prop = old_props[prop_name]\n new_prop = new_props[prop_name]\n if old_prop != new_prop:\n self._compare_schemas(f\"{base_path}/properties/{prop_name}\", \n old_prop, new_prop, f\"{context}.{prop_name}\")\n \n def _compare_security_requirements(self, base_path: str, old_security: Optional[List], \n new_security: Optional[List]) -> None:\n \"\"\"Compare security requirements.\"\"\"\n # Simplified security comparison - could be expanded\n if old_security != new_security:\n severity = ChangeSeverity.HIGH if new_security else ChangeSeverity.CRITICAL\n change_type = ChangeType.BREAKING\n \n if old_security is None and new_security is not None:\n message = \"Security requirements added\"\n migration_guide = \"Ensure proper authentication/authorization when calling this endpoint\"\n impact = \"Endpoint now requires authentication\"\n elif old_security is not None and new_security is None:\n message = \"Security requirements removed\"\n migration_guide = \"Authentication is no longer required for this endpoint\"\n impact = \"Endpoint is now publicly accessible\"\n severity = ChangeSeverity.MEDIUM # Less severe, more permissive\n else:\n message = \"Security requirements modified\"\n migration_guide = \"Update authentication/authorization method for this endpoint\"\n impact = \"Different authentication method required\"\n \n self.report.add_change(Change(\n change_type=change_type,\n severity=severity,\n category=\"security\",\n path=f\"{base_path}/security\",\n message=message,\n old_value=old_security,\n new_value=new_security,\n migration_guide=migration_guide,\n impact_description=impact\n ))\n \n def _compare_components_section(self) -> None:\n \"\"\"Compare components sections.\"\"\"\n old_components = self.old_spec.get('components', {})\n new_components = self.new_spec.get('components', {})\n \n # Compare schemas\n old_schemas = old_components.get('schemas', {})\n new_schemas = new_components.get('schemas', {})\n \n old_schema_names = set(old_schemas.keys())\n new_schema_names = set(new_schemas.keys())\n \n # Removed schemas\n removed_schemas = old_schema_names - new_schema_names\n for schema_name in removed_schemas:\n self.report.add_change(Change(\n change_type=ChangeType.BREAKING,\n severity=ChangeSeverity.HIGH,\n category=\"components\",\n path=f\"/components/schemas/{schema_name}\",\n message=f\"Schema '{schema_name}' removed from components\",\n old_value=old_schemas[schema_name],\n new_value=None,\n migration_guide=f\"Remove references to schema '{schema_name}' or use alternative schemas\",\n impact_description=\"References to this schema will fail validation\"\n ))\n \n # Added schemas\n added_schemas = new_schema_names - old_schema_names\n for schema_name in added_schemas:\n self.report.add_change(Change(\n change_type=ChangeType.ENHANCEMENT,\n severity=ChangeSeverity.INFO,\n category=\"components\",\n path=f\"/components/schemas/{schema_name}\",\n message=f\"New schema '{schema_name}' added to components\",\n old_value=None,\n new_value=new_schemas[schema_name],\n impact_description=\"New reusable schema available\"\n ))\n \n # Modified schemas\n common_schemas = old_schema_names & new_schema_names\n for schema_name in common_schemas:\n old_schema = old_schemas[schema_name]\n new_schema = new_schemas[schema_name]\n if old_schema != new_schema:\n self._compare_schemas(f\"/components/schemas/{schema_name}\", \n old_schema, new_schema, f\"schema '{schema_name}'\")\n \n def _compare_security_section(self) -> None:\n \"\"\"Compare security definitions.\"\"\"\n old_security_schemes = self.old_spec.get('components', {}).get('securitySchemes', {})\n new_security_schemes = self.new_spec.get('components', {}).get('securitySchemes', {})\n \n if old_security_schemes != new_security_schemes:\n # Simplified comparison - could be more detailed\n self.report.add_change(Change(\n change_type=ChangeType.POTENTIALLY_BREAKING,\n severity=ChangeSeverity.MEDIUM,\n category=\"security\",\n path=\"/components/securitySchemes\",\n message=\"Security scheme definitions changed\",\n old_value=old_security_schemes,\n new_value=new_security_schemes,\n migration_guide=\"Review authentication implementation for compatibility with new security schemes\",\n impact_description=\"Authentication mechanisms may have changed\"\n ))\n \n def _generate_endpoint_removal_migration(self, removed_path: str, method: str, \n remaining_paths: Dict[str, Any]) -> str:\n \"\"\"Generate migration guide for removed endpoints.\"\"\"\n # Look for similar endpoints\n similar_paths = []\n path_segments = removed_path.strip('/').split('/')\n \n for existing_path in remaining_paths.keys():\n existing_segments = existing_path.strip('/').split('/')\n if len(existing_segments) == len(path_segments):\n # Check similarity\n similarity = sum(1 for i, seg in enumerate(path_segments) \n if i \u003c len(existing_segments) and seg == existing_segments[i])\n if similarity >= len(path_segments) * 0.5: # At least 50% similar\n similar_paths.append(existing_path)\n \n if similar_paths:\n return f\"Consider using alternative endpoints: {', '.join(similar_paths[:3])}\"\n else:\n return \"No direct replacement available. Review API documentation for alternative approaches.\"\n \n def _generate_method_removal_migration(self, path: str, removed_method: str, \n remaining_methods: Set[str]) -> str:\n \"\"\"Generate migration guide for removed HTTP methods.\"\"\"\n method_alternatives = {\n 'get': ['head'],\n 'post': ['put', 'patch'],\n 'put': ['post', 'patch'],\n 'patch': ['put', 'post'],\n 'delete': []\n }\n \n alternatives = []\n for alt_method in method_alternatives.get(removed_method.lower(), []):\n if alt_method in remaining_methods:\n alternatives.append(alt_method.upper())\n \n if alternatives:\n return f\"Use alternative methods: {', '.join(alternatives)}\"\n else:\n return f\"No alternative HTTP methods available for {path}\"\n \n def generate_json_report(self) -> str:\n \"\"\"Generate JSON format report.\"\"\"\n report_data = {\n \"summary\": self.report.summary,\n \"hasBreakingChanges\": self.report.has_breaking_changes(),\n \"changes\": [change.to_dict() for change in self.report.changes]\n }\n \n return json.dumps(report_data, indent=2)\n \n def generate_text_report(self) -> str:\n \"\"\"Generate human-readable text report.\"\"\"\n lines = [\n \"═══════════════════════════════════════════════════════════════\",\n \" BREAKING CHANGE ANALYSIS REPORT\",\n \"═══════════════════════════════════════════════════════════════\",\n \"\",\n \"SUMMARY:\",\n f\" Total Changes: {self.report.summary.get('total_changes', 0)}\",\n f\" 🔴 Breaking Changes: {self.report.summary.get('breaking_changes', 0)}\",\n f\" 🟡 Potentially Breaking: {self.report.summary.get('potentially_breaking_changes', 0)}\",\n f\" 🟢 Non-Breaking Changes: {self.report.summary.get('non_breaking_changes', 0)}\",\n f\" ✨ Enhancements: {self.report.summary.get('enhancements', 0)}\",\n \"\",\n \"SEVERITY BREAKDOWN:\",\n f\" 🚨 Critical: {self.report.summary.get('critical_severity', 0)}\",\n f\" ⚠️ High: {self.report.summary.get('high_severity', 0)}\",\n f\" ⚪ Medium: {self.report.summary.get('medium_severity', 0)}\",\n f\" 🔵 Low: {self.report.summary.get('low_severity', 0)}\",\n f\" ℹ️ Info: {self.report.summary.get('info_severity', 0)}\",\n \"\"\n ]\n \n if not self.report.changes:\n lines.extend([\n \"🎉 No changes detected between the API versions!\",\n \"\"\n ])\n else:\n # Group changes by type and severity\n breaking_changes = [c for c in self.report.changes if c.change_type == ChangeType.BREAKING]\n potentially_breaking = [c for c in self.report.changes if c.change_type == ChangeType.POTENTIALLY_BREAKING]\n non_breaking = [c for c in self.report.changes if c.change_type == ChangeType.NON_BREAKING]\n enhancements = [c for c in self.report.changes if c.change_type == ChangeType.ENHANCEMENT]\n \n # Breaking changes section\n if breaking_changes:\n lines.extend([\n \"🔴 BREAKING CHANGES:\",\n \"═\" * 60\n ])\n for change in sorted(breaking_changes, key=lambda x: x.severity.value):\n self._add_change_to_report(lines, change)\n lines.append(\"\")\n \n # Potentially breaking changes section\n if potentially_breaking:\n lines.extend([\n \"🟡 POTENTIALLY BREAKING CHANGES:\",\n \"═\" * 60\n ])\n for change in sorted(potentially_breaking, key=lambda x: x.severity.value):\n self._add_change_to_report(lines, change)\n lines.append(\"\")\n \n # Non-breaking changes section\n if non_breaking:\n lines.extend([\n \"🟢 NON-BREAKING CHANGES:\",\n \"═\" * 60\n ])\n for change in non_breaking:\n self._add_change_to_report(lines, change)\n lines.append(\"\")\n \n # Enhancements section\n if enhancements:\n lines.extend([\n \"✨ ENHANCEMENTS:\",\n \"═\" * 60\n ])\n for change in enhancements:\n self._add_change_to_report(lines, change)\n lines.append(\"\")\n \n # Add overall assessment\n lines.extend([\n \"═══════════════════════════════════════════════════════════════\",\n \"OVERALL ASSESSMENT:\",\n \"═══════════════════════════════════════════════════════════════\"\n ])\n \n if self.report.has_breaking_changes():\n breaking_count = self.report.summary.get('breaking_changes', 0)\n potentially_breaking_count = self.report.summary.get('potentially_breaking_changes', 0)\n \n if breaking_count > 0:\n lines.extend([\n f\"⛔ MAJOR VERSION BUMP REQUIRED\",\n f\" This API version contains {breaking_count} breaking changes that will\",\n f\" definitely break existing clients. A major version bump is required.\",\n \"\"\n ])\n elif potentially_breaking_count > 0:\n lines.extend([\n f\"⚠️ MINOR VERSION BUMP RECOMMENDED\",\n f\" This API version contains {potentially_breaking_count} potentially breaking\",\n f\" changes. Consider a minor version bump and communicate changes to clients.\",\n \"\"\n ])\n else:\n lines.extend([\n \"✅ PATCH VERSION BUMP ACCEPTABLE\",\n \" No breaking changes detected. This version is backward compatible\",\n \" with existing clients.\",\n \"\"\n ])\n \n return \"\\n\".join(lines)\n \n def _add_change_to_report(self, lines: List[str], change: Change) -> None:\n \"\"\"Add a change to the text report.\"\"\"\n severity_icons = {\n ChangeSeverity.CRITICAL: \"🚨\",\n ChangeSeverity.HIGH: \"⚠️ \",\n ChangeSeverity.MEDIUM: \"⚪\",\n ChangeSeverity.LOW: \"🔵\",\n ChangeSeverity.INFO: \"ℹ️ \"\n }\n \n icon = severity_icons.get(change.severity, \"❓\")\n \n lines.extend([\n f\"{icon} {change.severity.value.upper()}: {change.message}\",\n f\" Path: {change.path}\",\n f\" Category: {change.category}\"\n ])\n \n if change.impact_description:\n lines.append(f\" Impact: {change.impact_description}\")\n \n if change.migration_guide:\n lines.append(f\" 💡 Migration: {change.migration_guide}\")\n \n lines.append(\"\")\n\n\ndef main():\n \"\"\"Main CLI entry point.\"\"\"\n parser = argparse.ArgumentParser(\n description=\"Compare API specification versions to detect breaking changes\",\n formatter_class=argparse.RawDescriptionHelpFormatter,\n epilog=\"\"\"\nExamples:\n python breaking_change_detector.py v1.json v2.json\n python breaking_change_detector.py --format json v1.json v2.json > changes.json\n python breaking_change_detector.py --output report.txt v1.json v2.json\n \"\"\"\n )\n \n parser.add_argument(\n 'old_spec',\n help='Old API specification file (JSON format)'\n )\n \n parser.add_argument(\n 'new_spec',\n help='New API specification file (JSON format)'\n )\n \n parser.add_argument(\n '--format',\n choices=['text', 'json'],\n default='text',\n help='Output format (default: text)'\n )\n \n parser.add_argument(\n '--output',\n help='Output file (default: stdout)'\n )\n \n parser.add_argument(\n '--exit-on-breaking',\n action='store_true',\n help='Exit with code 1 if breaking changes are detected'\n )\n \n args = parser.parse_args()\n \n # Load specification files\n try:\n with open(args.old_spec, 'r') as f:\n old_spec = json.load(f)\n except FileNotFoundError:\n print(f\"Error: Old specification file '{args.old_spec}' not found.\", file=sys.stderr)\n return 1\n except json.JSONDecodeError as e:\n print(f\"Error: Invalid JSON in '{args.old_spec}': {e}\", file=sys.stderr)\n return 1\n \n try:\n with open(args.new_spec, 'r') as f:\n new_spec = json.load(f)\n except FileNotFoundError:\n print(f\"Error: New specification file '{args.new_spec}' not found.\", file=sys.stderr)\n return 1\n except json.JSONDecodeError as e:\n print(f\"Error: Invalid JSON in '{args.new_spec}': {e}\", file=sys.stderr)\n return 1\n \n # Initialize detector and compare specifications\n detector = BreakingChangeDetector()\n \n try:\n report = detector.compare_specs(old_spec, new_spec)\n except Exception as e:\n print(f\"Error during comparison: {e}\", file=sys.stderr)\n return 1\n \n # Generate report\n if args.format == 'json':\n output = detector.generate_json_report()\n else:\n output = detector.generate_text_report()\n \n # Write output\n if args.output:\n try:\n with open(args.output, 'w') as f:\n f.write(output)\n print(f\"Breaking change report written to {args.output}\")\n except IOError as e:\n print(f\"Error writing to '{args.output}': {e}\", file=sys.stderr)\n return 1\n else:\n print(output)\n \n # Exit with appropriate code\n if args.exit_on_breaking and report.has_breaking_changes():\n return 1\n \n return 0\n\n\nif __name__ == '__main__':\n sys.exit(main())","content_type":"text/x-python; charset=utf-8","language":"python","size":49468,"content_sha256":"068d4bc0fdab0009f4140baa19e02bd4e255a9ab39c6704f8e7e73c7a550b3fd"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"API Design Reviewer","type":"text"}]},{"type":"paragraph","content":[{"text":"Tier:","type":"text","marks":[{"type":"strong"}]},{"text":" POWERFUL","type":"text"},{"type":"br"},{"text":"Category:","type":"text","marks":[{"type":"strong"}]},{"text":" Engineering / Architecture","type":"text"},{"type":"br"},{"text":"Maintainer:","type":"text","marks":[{"type":"strong"}]},{"text":" Claude Skills Team","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Overview","type":"text"}]},{"type":"paragraph","content":[{"text":"The API Design Reviewer skill provides comprehensive analysis and review of API designs, focusing on REST conventions, best practices, and industry standards. This skill helps engineering teams build consistent, maintainable, and well-designed APIs through automated linting, breaking change detection, and design scorecards.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Quick Start — run the tools first","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# 1. Lint an OpenAPI/Swagger spec for convention violations\npython3 scripts/api_linter.py openapi.json --format json -o lint.json\n\n# 2. Detect breaking changes between two spec versions (gate: exits non-zero with --exit-on-breaking)\npython3 scripts/breaking_change_detector.py openapi-v1.json openapi-v2.json --format json --exit-on-breaking -o breaking.json\n\n# 3. Score overall design quality (gate: --min-grade fails below threshold)\npython3 scripts/api_scorecard.py openapi.json --format json --min-grade B -o scorecard.json","type":"text"}]},{"type":"paragraph","content":[{"text":"Review flow: run all three, report linter findings + breaking changes + grade to the user, fix, then re-run until the linter is clean, ","type":"text"},{"text":"--exit-on-breaking","type":"text","marks":[{"type":"code_inline"}]},{"text":" passes (or breaking changes are version-bumped), and the scorecard meets the agreed ","type":"text"},{"text":"--min-grade","type":"text","marks":[{"type":"code_inline"}]},{"text":". Never sign off an API review on prose alone — attach the tool outputs.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Core Capabilities","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"1. API Linting and Convention Analysis","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Resource Naming Conventions","type":"text","marks":[{"type":"strong"}]},{"text":": Enforces kebab-case for resources, camelCase for fields","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"HTTP Method Usage","type":"text","marks":[{"type":"strong"}]},{"text":": Validates proper use of GET, POST, PUT, PATCH, DELETE","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"URL Structure","type":"text","marks":[{"type":"strong"}]},{"text":": Analyzes endpoint patterns for consistency and RESTful design","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Status Code Compliance","type":"text","marks":[{"type":"strong"}]},{"text":": Ensures appropriate HTTP status codes are used","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Error Response Formats","type":"text","marks":[{"type":"strong"}]},{"text":": Validates consistent error response structures","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Documentation Coverage","type":"text","marks":[{"type":"strong"}]},{"text":": Checks for missing descriptions and documentation gaps","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"2. Breaking Change Detection","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Endpoint Removal","type":"text","marks":[{"type":"strong"}]},{"text":": Detects removed or deprecated endpoints","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Response Shape Changes","type":"text","marks":[{"type":"strong"}]},{"text":": Identifies modifications to response structures","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Field Removal","type":"text","marks":[{"type":"strong"}]},{"text":": Tracks removed or renamed fields in API responses","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Type Changes","type":"text","marks":[{"type":"strong"}]},{"text":": Catches field type modifications that could break clients","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Required Field Additions","type":"text","marks":[{"type":"strong"}]},{"text":": Flags new required fields that could break existing integrations","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Status Code Changes","type":"text","marks":[{"type":"strong"}]},{"text":": Detects changes to expected status codes","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"3. API Design Scoring and Assessment","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Consistency Analysis","type":"text","marks":[{"type":"strong"}]},{"text":" (30%): Evaluates naming conventions, response patterns, and structural consistency","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Documentation Quality","type":"text","marks":[{"type":"strong"}]},{"text":" (20%): Assesses completeness and clarity of API documentation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Security Implementation","type":"text","marks":[{"type":"strong"}]},{"text":" (20%): Reviews authentication, authorization, and security headers","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Usability Design","type":"text","marks":[{"type":"strong"}]},{"text":" (15%): Analyzes ease of use, discoverability, and developer experience","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Performance Patterns","type":"text","marks":[{"type":"strong"}]},{"text":" (15%): Evaluates caching, pagination, and efficiency patterns","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"REST Design Principles","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Resource Naming Conventions","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"✅ Good Examples:\n- /api/v1/users\n- /api/v1/user-profiles\n- /api/v1/orders/123/line-items\n\n❌ Bad Examples:\n- /api/v1/getUsers\n- /api/v1/user_profiles\n- /api/v1/orders/123/lineItems","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"HTTP Method Usage","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"GET","type":"text","marks":[{"type":"strong"}]},{"text":": Retrieve resources (safe, idempotent)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"POST","type":"text","marks":[{"type":"strong"}]},{"text":": Create new resources (not idempotent)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"PUT","type":"text","marks":[{"type":"strong"}]},{"text":": Replace entire resources (idempotent)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"PATCH","type":"text","marks":[{"type":"strong"}]},{"text":": Partial resource updates (not necessarily idempotent)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"DELETE","type":"text","marks":[{"type":"strong"}]},{"text":": Remove resources (idempotent)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"URL Structure Best Practices","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"Collection Resources: /api/v1/users\nIndividual Resources: /api/v1/users/123\nNested Resources: /api/v1/users/123/orders\nActions: /api/v1/users/123/activate (POST)\nFiltering: /api/v1/users?status=active&role=admin","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Versioning Strategies","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"1. URL Versioning (Recommended)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"/api/v1/users\n/api/v2/users","type":"text"}]},{"type":"paragraph","content":[{"text":"Pros","type":"text","marks":[{"type":"strong"}]},{"text":": Clear, explicit, easy to route","type":"text"},{"type":"br"},{"text":"Cons","type":"text","marks":[{"type":"strong"}]},{"text":": URL proliferation, caching complexity","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"2. Header Versioning","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"GET /api/users\nAccept: application/vnd.api+json;version=1","type":"text"}]},{"type":"paragraph","content":[{"text":"Pros","type":"text","marks":[{"type":"strong"}]},{"text":": Clean URLs, content negotiation","type":"text"},{"type":"br"},{"text":"Cons","type":"text","marks":[{"type":"strong"}]},{"text":": Less visible, harder to test manually","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"3. Media Type Versioning","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"GET /api/users\nAccept: application/vnd.myapi.v1+json","type":"text"}]},{"type":"paragraph","content":[{"text":"Pros","type":"text","marks":[{"type":"strong"}]},{"text":": RESTful, supports multiple representations","type":"text"},{"type":"br"},{"text":"Cons","type":"text","marks":[{"type":"strong"}]},{"text":": Complex, harder to implement","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"4. Query Parameter Versioning","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"/api/users?version=1","type":"text"}]},{"type":"paragraph","content":[{"text":"Pros","type":"text","marks":[{"type":"strong"}]},{"text":": Simple to implement","type":"text"},{"type":"br"},{"text":"Cons","type":"text","marks":[{"type":"strong"}]},{"text":": Not RESTful, can be ignored","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Pagination Patterns","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Offset-Based Pagination","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{\n \"data\": [...],\n \"pagination\": {\n \"offset\": 20,\n \"limit\": 10,\n \"total\": 150,\n \"hasMore\": true\n }\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Cursor-Based Pagination","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{\n \"data\": [...],\n \"pagination\": {\n \"nextCursor\": \"eyJpZCI6MTIzfQ==\",\n \"hasMore\": true\n }\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Page-Based Pagination","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{\n \"data\": [...],\n \"pagination\": {\n \"page\": 3,\n \"pageSize\": 10,\n \"totalPages\": 15,\n \"totalItems\": 150\n }\n}","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Error Response Formats","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Standard Error Structure","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{\n \"error\": {\n \"code\": \"VALIDATION_ERROR\",\n \"message\": \"The request contains invalid parameters\",\n \"details\": [\n {\n \"field\": \"email\",\n \"code\": \"INVALID_FORMAT\",\n \"message\": \"Email address is not valid\"\n }\n ],\n \"requestId\": \"req-123456\",\n \"timestamp\": \"2026-02-16T13:00:00Z\"\n }\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"HTTP Status Code Usage","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"400 Bad Request","type":"text","marks":[{"type":"strong"}]},{"text":": Invalid request syntax or parameters","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"401 Unauthorized","type":"text","marks":[{"type":"strong"}]},{"text":": Authentication required","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"403 Forbidden","type":"text","marks":[{"type":"strong"}]},{"text":": Access denied (authenticated but not authorized)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"404 Not Found","type":"text","marks":[{"type":"strong"}]},{"text":": Resource not found","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"409 Conflict","type":"text","marks":[{"type":"strong"}]},{"text":": Resource conflict (duplicate, version mismatch)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"422 Unprocessable Entity","type":"text","marks":[{"type":"strong"}]},{"text":": Valid syntax but semantic errors","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"429 Too Many Requests","type":"text","marks":[{"type":"strong"}]},{"text":": Rate limit exceeded","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"500 Internal Server Error","type":"text","marks":[{"type":"strong"}]},{"text":": Unexpected server error","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Authentication and Authorization Patterns","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Bearer Token Authentication","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"Authorization: Bearer \u003ctoken>","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"API Key Authentication","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"X-API-Key: \u003capi-key>\nAuthorization: Api-Key \u003capi-key>","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"OAuth 2.0 Flow","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"Authorization: Bearer \u003coauth-access-token>","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Role-Based Access Control (RBAC)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{\n \"user\": {\n \"id\": \"123\",\n \"roles\": [\"admin\", \"editor\"],\n \"permissions\": [\"read:users\", \"write:orders\"]\n }\n}","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Rate Limiting Implementation","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Headers","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"X-RateLimit-Limit: 1000\nX-RateLimit-Remaining: 999\nX-RateLimit-Reset: 1640995200","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Response on Limit Exceeded","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{\n \"error\": {\n \"code\": \"RATE_LIMIT_EXCEEDED\",\n \"message\": \"Too many requests\",\n \"retryAfter\": 3600\n }\n}","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"HATEOAS (Hypermedia as the Engine of Application State)","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Example Implementation","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{\n \"id\": \"123\",\n \"name\": \"John Doe\",\n \"email\": \"[email protected]\",\n \"_links\": {\n \"self\": { \"href\": \"/api/v1/users/123\" },\n \"orders\": { \"href\": \"/api/v1/users/123/orders\" },\n \"profile\": { \"href\": \"/api/v1/users/123/profile\" },\n \"deactivate\": { \n \"href\": \"/api/v1/users/123/deactivate\",\n \"method\": \"POST\"\n }\n }\n}","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Idempotency","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Idempotent Methods","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"GET","type":"text","marks":[{"type":"strong"}]},{"text":": Always safe and idempotent","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"PUT","type":"text","marks":[{"type":"strong"}]},{"text":": Should be idempotent (replace entire resource)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"DELETE","type":"text","marks":[{"type":"strong"}]},{"text":": Should be idempotent (same result)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"PATCH","type":"text","marks":[{"type":"strong"}]},{"text":": May or may not be idempotent","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Idempotency Keys","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"POST /api/v1/payments\nIdempotency-Key: 123e4567-e89b-12d3-a456-426614174000","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Backward Compatibility Guidelines","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Safe Changes (Non-Breaking)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Adding optional fields to requests","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Adding fields to responses","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Adding new endpoints","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Making required fields optional","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Adding new enum values (with graceful handling)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Breaking Changes (Require Version Bump)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Removing fields from responses","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Making optional fields required","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Changing field types","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Removing endpoints","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Changing URL structures","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Modifying error response formats","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"OpenAPI/Swagger Validation","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Required Components","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"API Information","type":"text","marks":[{"type":"strong"}]},{"text":": Title, description, version","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Server Information","type":"text","marks":[{"type":"strong"}]},{"text":": Base URLs and descriptions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Path Definitions","type":"text","marks":[{"type":"strong"}]},{"text":": All endpoints with methods","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Parameter Definitions","type":"text","marks":[{"type":"strong"}]},{"text":": Query, path, header parameters","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Request/Response Schemas","type":"text","marks":[{"type":"strong"}]},{"text":": Complete data models","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Security Definitions","type":"text","marks":[{"type":"strong"}]},{"text":": Authentication schemes","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Error Responses","type":"text","marks":[{"type":"strong"}]},{"text":": Standard error formats","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Best Practices","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use consistent naming conventions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Provide detailed descriptions for all components","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Include examples for complex objects","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Define reusable components and schemas","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Validate against OpenAPI specification","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Performance Considerations","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Caching Strategies","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"Cache-Control: public, max-age=3600\nETag: \"123456789\"\nLast-Modified: Wed, 21 Oct 2015 07:28:00 GMT","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Efficient Data Transfer","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use appropriate HTTP methods","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Implement field selection (","type":"text"},{"text":"?fields=id,name,email","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Support compression (gzip)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Implement efficient pagination","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use ETags for conditional requests","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Resource Optimization","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Avoid N+1 queries","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Implement batch operations","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use async processing for heavy operations","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Support partial updates (PATCH)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Security Best Practices","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Input Validation","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Validate all input parameters","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Sanitize user data","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use parameterized queries","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Implement request size limits","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Authentication Security","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use HTTPS everywhere","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Implement secure token storage","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Support token expiration and refresh","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use strong authentication mechanisms","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Authorization Controls","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Implement principle of least privilege","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use resource-based permissions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Support fine-grained access control","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Audit access patterns","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Tools and Scripts","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"api_linter.py","type":"text"}]},{"type":"paragraph","content":[{"text":"Analyzes API specifications for compliance with REST conventions and best practices.","type":"text"}]},{"type":"paragraph","content":[{"text":"Features:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"OpenAPI/Swagger spec validation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Naming convention checks","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"HTTP method usage validation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Error format consistency","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Documentation completeness analysis","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"breaking_change_detector.py","type":"text"}]},{"type":"paragraph","content":[{"text":"Compares API specification versions to identify breaking changes.","type":"text"}]},{"type":"paragraph","content":[{"text":"Features:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Endpoint comparison","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Schema change detection","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Field removal/modification tracking","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Migration guide generation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Impact severity assessment","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"api_scorecard.py","type":"text"}]},{"type":"paragraph","content":[{"text":"Provides comprehensive scoring of API design quality.","type":"text"}]},{"type":"paragraph","content":[{"text":"Features:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Multi-dimensional scoring","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Detailed improvement recommendations","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Letter grade assessment (A-F)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Benchmark comparisons","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Progress tracking","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Integration Examples","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"CI/CD Integration","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"yaml"},"content":[{"text":"- name: \"api-linting\"\n run: python scripts/api_linter.py openapi.json\n\n- name: \"breaking-change-detection\"\n run: python scripts/breaking_change_detector.py openapi-v1.json openapi-v2.json\n\n- name: \"api-scorecard\"\n run: python scripts/api_scorecard.py openapi.json","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Pre-commit Hooks","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"#!/bin/bash\npython engineering/skills/api-design-reviewer/scripts/api_linter.py api/openapi.json\nif [ $? -ne 0 ]; then\n echo \"API linting failed. Please fix the issues before committing.\"\n exit 1\nfi","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Best Practices Summary","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Consistency First","type":"text","marks":[{"type":"strong"}]},{"text":": Maintain consistent naming, response formats, and patterns","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Documentation","type":"text","marks":[{"type":"strong"}]},{"text":": Provide comprehensive, up-to-date API documentation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Versioning","type":"text","marks":[{"type":"strong"}]},{"text":": Plan for evolution with clear versioning strategies","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Error Handling","type":"text","marks":[{"type":"strong"}]},{"text":": Implement consistent, informative error responses","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Security","type":"text","marks":[{"type":"strong"}]},{"text":": Build security into every layer of the API","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Performance","type":"text","marks":[{"type":"strong"}]},{"text":": Design for scale and efficiency from the start","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Backward Compatibility","type":"text","marks":[{"type":"strong"}]},{"text":": Minimize breaking changes and provide migration paths","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Testing","type":"text","marks":[{"type":"strong"}]},{"text":": Implement comprehensive testing including contract testing","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Monitoring","type":"text","marks":[{"type":"strong"}]},{"text":": Add observability for API usage and performance","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Developer Experience","type":"text","marks":[{"type":"strong"}]},{"text":": Prioritize ease of use and clear documentation","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Common Anti-Patterns to Avoid","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Verb-based URLs","type":"text","marks":[{"type":"strong"}]},{"text":": Use nouns for resources, not actions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Inconsistent Response Formats","type":"text","marks":[{"type":"strong"}]},{"text":": Maintain standard response structures","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Over-nesting","type":"text","marks":[{"type":"strong"}]},{"text":": Avoid deeply nested resource hierarchies","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Ignoring HTTP Status Codes","type":"text","marks":[{"type":"strong"}]},{"text":": Use appropriate status codes for different scenarios","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Poor Error Messages","type":"text","marks":[{"type":"strong"}]},{"text":": Provide actionable, specific error information","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Missing Pagination","type":"text","marks":[{"type":"strong"}]},{"text":": Always paginate list endpoints","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No Versioning Strategy","type":"text","marks":[{"type":"strong"}]},{"text":": Plan for API evolution from day one","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Exposing Internal Structure","type":"text","marks":[{"type":"strong"}]},{"text":": Design APIs for external consumption, not internal convenience","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Missing Rate Limiting","type":"text","marks":[{"type":"strong"}]},{"text":": Protect your API from abuse and overload","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Inadequate Testing","type":"text","marks":[{"type":"strong"}]},{"text":": Test all aspects including error cases and edge conditions","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Regular use of the linting, breaking change detection, and scoring tools ensures continuous improvement and helps maintain API quality throughout the development lifecycle.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"More in this skill","type":"text"}]},{"type":"paragraph","content":[{"text":"Load these only when the task needs them.","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/api_antipatterns.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/api_antipatterns.md","title":null}}]},{"text":" — Common API Anti-Patterns and How to Avoid Them","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/rest_design_rules.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/rest_design_rules.md","title":null}}]},{"text":" — REST API Design Rules Reference","type":"text"}]}]}]}]},"metadata":{"date":"2026-11-02","name":"api-design-reviewer","author":"@skillopedia","source":{"stars":29164,"repo_name":"claude-skills","origin_url":"https://github.com/alirezarezvani/claude-skills/blob/HEAD/engineering/skills/api-design-reviewer/SKILL.md","repo_owner":"alirezarezvani","body_sha256":"3c48fe61c03411244a9a12dfe39b438faea7df89b690219cbf244d739c452b2f","cluster_key":"83704da4d3e04558cfb8528899fd50964d257c6bbe62169c6647fd795b98463b","clean_bundle":{"format":"clean-skill-bundle-v1","source":"alirezarezvani/claude-skills/engineering/skills/api-design-reviewer/SKILL.md","text_bytes":180255,"source_commit":"9ef0c3c213932e645cc5cbeeb3c35c3a762a2437","skills_sh_rank":15029,"attachment_count":5,"text_attachments":5,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"engineering/skills/api-design-reviewer/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"software-engineering","category_label":"Engineering"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"software-engineering","import_tag":"skills-sh-top20k-2026-11","description":"Use for any PR or spec that adds or changes REST endpoints, before approving it. Lints conventions with api_linter.py, detects breaking changes with breaking_change_detector.py and scores the design with api_scorecard.py."}},"renderedAt":1793661666069}

API Design Reviewer Tier: POWERFUL Category: Engineering / Architecture Maintainer: Claude Skills Team Overview The API Design Reviewer skill provides comprehensive analysis and review of API designs, focusing on REST conventions, best practices, and industry standards. This skill helps engineering teams build consistent, maintainable, and well-designed APIs through automated linting, breaking change detection, and design scorecards. Quick Start — run the tools first Review flow: run all three, report linter findings + breaking changes + grade to the user, fix, then re-run until the linter is…