api-diff-analyzer
You are api-diff-analyzer - a specialized skill for comparing API specifications and detecting breaking changes, ensuring SDK compatibility and safe API evolution.
Overview
This skill enables AI-powered API diff analysis including:
- Comparing OpenAPI spec versions
- Categorizing changes by severity
- Detecting breaking changes automatically
- Generating migration guides
- Blocking breaking changes in CI
- Supporting multiple spec formats (OpenAPI, GraphQL, gRPC)
- Creating detailed change reports
Prerequisites
- OpenAPI, GraphQL, or Protobuf specifications
- Version control with spec history
- oasdiff, openapi-diff, or similar tools
- CI/CD pipeline for automated checks
Capabilities
1. OpenAPI Diff Analysis
Compare OpenAPI specifications:
// src/analyzer/openapi-diff.ts
import { parseSpec, diffSpecs } from './parser';
interface ApiChange {
type: 'breaking' | 'non-breaking' | 'info';
category: string;
path: string;
method?: string;
description: string;
oldValue?: unknown;
newValue?: unknown;
migration?: string;
}
interface DiffResult {
hasBreakingChanges: boolean;
changes: ApiChange[];
summary: {
breaking: number;
nonBreaking: number;
info: number;
};
report: string;
}
export async function analyzeApiDiff(
oldSpec: string,
newSpec: string,
options: DiffOptions = {}
): Promise<DiffResult> {
const oldApi = await parseSpec(oldSpec);
const newApi = await parseSpec(newSpec);
const changes: ApiChange[] = [];
// Analyze paths
for (const [path, oldPathItem] of Object.entries(oldApi.paths)) {
const newPathItem = newApi.paths[path];
if (!newPathItem) {
changes.push({
type: 'breaking',
category: 'endpoint-removed',
path,
description: `Endpoint ${path} was removed`,
migration: `Update SDK to remove calls to ${path}`
});
continue;
}
// Analyze methods
for (const method of ['get', 'post', 'put', 'patch', 'delete']) {
const oldOp = oldPathItem[method];
const newOp = newPathItem[method];
if (oldOp && !newOp) {
changes.push({
type: 'breaking',
category: 'method-removed',
path,
method,
description: `${method.toUpperCase()} ${path} was removed`
});
continue;
}
if (oldOp && newOp) {
// Check parameters
analyzeParameters(path, method, oldOp, newOp, changes);
// Check request body
analyzeRequestBody(path, method, oldOp, newOp, changes);
// Check responses
analyzeResponses(path, method, oldOp, newOp, changes);
}
}
}
// Check for new endpoints (non-breaking)
for (const [path, newPathItem] of Object.entries(newApi.paths)) {
if (!oldApi.paths[path]) {
changes.push({
type: 'non-breaking',
category: 'endpoint-added',
path,
description: `New endpoint ${path} was added`
});
}
}
// Analyze components/schemas
analyzeSchemas(oldApi.components?.schemas, newApi.components?.schemas, changes);
const summary = {
breaking: changes.filter(c => c.type === 'breaking').length,
nonBreaking: changes.filter(c => c.type === 'non-breaking').length,
info: changes.filter(c => c.type === 'info').length
};
return {
hasBreakingChanges: summary.breaking > 0,
changes,
summary,
report: generateReport(changes, summary)
};
}
function analyzeParameters(
path: string,
method: string,
oldOp: Operation,
newOp: Operation,
changes: ApiChange[]
): void {
const oldParams = new Map(oldOp.parameters?.map(p => [p.name, p]) || []);
const newParams = new Map(newOp.parameters?.map(p => [p.name, p]) || []);
// Check for removed parameters
for (const [name, oldParam] of oldParams) {
if (!newParams.has(name)) {
changes.push({
type: oldParam.required ? 'breaking' : 'info',
category: 'parameter-removed',
path,
method,
description: `Parameter '${name}' was removed from ${method.toUpperCase()} ${path}`,
oldValue: oldParam
});
}
}
// Check for new required parameters
for (const [name, newParam] of newParams) {
const oldParam = oldParams.get(name);
if (!oldParam && newParam.required) {
changes.push({
type: 'breaking',
category: 'required-parameter-added',
path,
method,
description: `New required parameter '${name}' added to ${method.toUpperCase()} ${path}`,
newValue: newParam,
migration: `Update SDK calls to include '${name}' parameter`
});
}
if (oldParam && !oldParam.required && newParam.required) {
changes.push({
type: 'breaking',
category: 'parameter-required',
path,
method,
description: `Parameter '${name}' is now required in ${method.toUpperCase()} ${path}`,
oldValue: oldParam,
newValue: newParam
});
}
// Check type changes
if (oldParam && oldParam.schema?.type !== newParam.schema?.type) {
changes.push({
type: 'breaking',
category: 'parameter-type-changed',
path,
method,
description: `Parameter '${name}' type changed from '${oldParam.schema?.type}' to '${newParam.schema?.type}'`,
oldValue: oldParam,
newValue: newParam
});
}
}
}
function analyzeSchemas(
oldSchemas: Record<string, Schema> | undefined,
newSchemas: Record<string, Schema> | undefined,
changes: ApiChange[]
): void {
if (!oldSchemas || !newSchemas) return;
for (const [name, oldSchema] of Object.entries(oldSchemas)) {
const newSchema = newSchemas[name];
if (!newSchema) {
changes.push({
type: 'breaking',
category: 'schema-removed',
path: `#/components/schemas/${name}`,
description: `Schema '${name}' was removed`
});
continue;
}
// Check for removed properties
if (oldSchema.properties && newSchema.properties) {
for (const prop of Object.keys(oldSchema.properties)) {
if (!(prop in newSchema.properties)) {
changes.push({
type: 'breaking',
category: 'property-removed',
path: `#/components/schemas/${name}/${prop}`,
description: `Property '${prop}' was removed from schema '${name}'`
});
}
}
// Check for new required properties
const oldRequired = new Set(oldSchema.required || []);
const newRequired = new Set(newSchema.required || []);
for (const prop of newRequired) {
if (!oldRequired.has(prop) && oldSchema.properties[prop]) {
changes.push({
type: 'breaking',
category: 'property-required',
path: `#/components/schemas/${name}/${prop}`,
description: `Property '${prop}' is now required in schema '${name}'`
});
}
}
}
}
}
2. Breaking Change Categories
Comprehensive breaking change detection:
// src/rules/breaking-changes.ts
export const BREAKING_CHANGE_RULES = {
// Endpoint changes
'endpoint-removed': {
severity: 'major',
description: 'Removing an endpoint breaks all consumers',
autoFix: false
},
'method-removed': {
severity: 'major',
description: 'Removing an HTTP method breaks consumers using it',
autoFix: false
},
// Parameter changes
'required-parameter-added': {
severity: 'major',
description: 'Adding required parameter breaks existing calls',
autoFix: false
},
'parameter-removed': {
severity: 'minor',
description: 'Removing parameter may break consumers expecting it',
autoFix: 'Make parameter optional first'
},
'parameter-type-changed': {
severity: 'major',
description: 'Changing parameter type breaks serialization',
autoFix: false
},
'parameter-required': {
severity: 'm