diff --git a/kits/api-schema-drift-sentinel/.env.example b/kits/api-schema-drift-sentinel/.env.example new file mode 100644 index 000000000..ab9197fa1 --- /dev/null +++ b/kits/api-schema-drift-sentinel/.env.example @@ -0,0 +1,4 @@ +LAMATIC_API_KEY=your_lamatic_api_key_here +LAMATIC_API_URL=https://api.lamatic.ai +LAMATIC_DRIFT_FLOW_ID=your_id +LAMATIC_PROJECT_ID=your_project_id_here diff --git a/kits/api-schema-drift-sentinel/.gitignore b/kits/api-schema-drift-sentinel/.gitignore new file mode 100644 index 000000000..d39a532da --- /dev/null +++ b/kits/api-schema-drift-sentinel/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +.next/ +.env +.env.* +!.env.example +.DS_Store +*.log +*.tsbuildinfo diff --git a/kits/api-schema-drift-sentinel/README.md b/kits/api-schema-drift-sentinel/README.md new file mode 100644 index 000000000..03847d59c --- /dev/null +++ b/kits/api-schema-drift-sentinel/README.md @@ -0,0 +1,253 @@ +# API Schema Drift Sentinel + +A **breaking-change detection and migration orchestration kit** for OpenAPI-based services. It uses deterministic structural comparison to identify schema changes, supplements the diff with direct path-parameter comparison where necessary, classifies the resulting changes by severity, and sends the confirmed change facts to a Lamatic workflow for executive impact analysis and migration guidance — exposed through a single `/api/analyze-drift` API call. + +--- + +## The Problem + +API schema drift is silent and expensive. When a service team renames a field, removes a response property, or changes a parameter type, the breakage shows up in downstream clients — frontends, SDKs, mobile apps — long after the deploy. Most teams catch this through manual spec review or at runtime during integration testing. + +There is no easy way to: +1. Automatically detect what broke between two spec versions +2. Know which client systems will be affected +3. Get a concrete migration plan without reading the full spec diff manually + +--- + +## The Solution + +API Schema Drift Sentinel combines two layers: + +1. **Deterministic AST diff** — `openapi-diff` performs structural comparison of the two OpenAPI specs and returns typed, structured breaking and non-breaking changes. This is computed locally, with no AI involved, so the facts are always accurate. + +2. **AI narrative synthesis** — the structured facts are forwarded to a Lamatic workflow where an LLM reasons about downstream impact, classifies deployment risk, and produces a migration guide grounded in the actual detected changes. + +The result is exposed through a Next.js API endpoint and a minimal dashboard UI. + +--- + +## Why the Two-Layer Architecture + +Using AI alone to diff specs is unreliable — models hallucinate field names, miss subtle type changes, and produce inconsistent severity ratings. Using a pure diff tool alone gives you a machine-readable change list but no actionable guidance. + +This kit separates the concerns: + +| Layer | What it does | Why | +|---|---|---| +| `openapi-diff` (deterministic) | Structural AST diff | Deterministic and reproducible; no LLM hallucination risk | +| Lamatic LLM workflow | Narrative, impact, migration | Produces human-readable output grounded in confirmed facts | + +The LLM receives a plain-text fact list derived from the deterministic layer — not the raw specs. This keeps the LLM grounded in the deterministic fact list and reduces the risk of unsupported claims. + +--- + +## Architecture + +``` +Browser / test harness + │ + │ POST /api/analyze-drift { specA, specB } + ▼ +apps/app/api/analyze-drift/route.ts + │ + ├─ 1. runOpenApiDiff(specA, specB) ← openapi-diff AST comparison + │ + ├─ 2. normalizeDiff(rawDiff, specA, specB) ← typed SemanticChange[] facts + │ breaking: CRITICAL severity + │ non-breaking: INFO severity + │ + ├─ 3. Format fact lines for AI context + │ "Endpoint: GET /users/{id} | Field: email | Action: remove | ..." + │ + ├─ 4. triggerLamaticWorkflow({ sampleInput }) + │ Lamatic.executeFlow(flowId, payload) + │ │ + │ Lamatic Studio + │ ┌─────────────────────────────┐ + │ │ LLM Node (system prompt: │ + │ │ prompts/analyze-schema- │ + │ │ drift_llm-node_system.md) │ + │ │ │ + │ │ Returns JSON: │ + │ │ { executiveSummary, │ + │ │ detailedImpact[], │ + │ │ migrationGuide[], │ + │ │ deploymentRisk } │ + │ └─────────────────────────────┘ + │ + └─ 5. Merge AI output + deterministic counts → NextResponse + { breakingCount, nonBreakingCount, riskLevel, changes[], ... } +``` + +--- + +## API + +### `POST /api/analyze-drift` + +**Request body:** +```json +{ + "specA": "", + "specB": "" +} +``` + +Both `specA` and `specB` are required. They must be valid OpenAPI 3.0 JSON (as a string or parsed object). + +**Response:** +```json +{ + "success": true, + "data": { + "executiveSummary": { "recommendation": "...", "deploymentRisk": "HIGH" }, + "detailedImpact": ["...", "..."], + "migrationGuide": ["...", "..."], + "breakingCount": 3, + "nonBreakingCount": 0, + "riskLevel": "HIGH", + "changes": [ + { + "endpoint": "GET /users/{id}", + "field": "email", + "action": "remove", + "severity": "CRITICAL", + "code": "response.body.scope.remove", + "before": "string", + "after": "—", + "description": "Removed field 'email'", + "isBreaking": true + } + ] + } +} +``` + +`breakingCount`, `nonBreakingCount`, and dashboard `riskLevel` are derived deterministically from the change classification. The narrative fields (`executiveSummary`, `detailedImpact`, `migrationGuide`) are produced by the Lamatic workflow. + +--- + +## Environment Variables + +| Variable | Description | Where to find it | +|---|---|---| +| `LAMATIC_API_KEY` | Lamatic project API key | Studio → API Keys | +| `LAMATIC_PROJECT_ID` | Lamatic project UUID | Studio → Project Settings | +| `LAMATIC_API_URL` | Lamatic project API endpoint | Studio → Settings → API | +| `LAMATIC_DRIFT_FLOW_ID` | Deployed flow ID for the drift analysis flow | Studio → open flow → copy Flow ID | + +--- + +## Setup + +### 1. Build the Lamatic flow + +1. Log in to [Lamatic Studio](https://studio.lamatic.ai) +2. Create a new flow with a trigger that accepts `sampleInput` (string) +3. Add an LLM node — use the system prompt from [`prompts/analyze-schema-drift_llm-node_system.md`](./prompts/analyze-schema-drift_llm-node_system.md) +4. Configure the LLM node to return a JSON object with: `executiveSummary`, `detailedImpact`, `migrationGuide` +5. Deploy the flow and copy the **Flow ID** + +### 2. Configure environment variables + +```bash +cd kits/api-schema-drift-sentinel/apps +cp .env.example .env.local +``` + +Fill in `.env.local` with your values: +``` +LAMATIC_API_KEY=lt-... +LAMATIC_PROJECT_ID=... +LAMATIC_API_URL=https://your-project.lamatic.dev +LAMATIC_DRIFT_FLOW_ID=... +``` + +### 3. Install and run + +```bash +npm install +npm run dev +# App available at http://localhost:3000 +``` + +--- + +## Test Cases and Results + +The end-to-end test harness is in [`apps/test-orchestrate.js`](./apps/test-orchestrate.js). + +```bash +node apps/test-orchestrate.js +``` + +> **Credentials not required for deterministic checks.** Steps 1 and 2 (normalization correctness and path-parameter regression) run entirely locally — no Lamatic credentials are needed and no network calls are made. +> +> **Lamatic credentials required only for live integration.** Steps 3 A and B trigger the deployed Lamatic workflow and require all four `LAMATIC_*` environment variables to be set in `.env.local`. When credentials are absent the test harness detects this and skips the live workflow steps automatically, so the deterministic assertions still pass. + +### Test A — Additive (non-breaking) + +**Input:** Base spec has `GET /users/{id}` returning `{ id, name, email }`, target spec adds optional response property `full_name: { type: "string" }`. + +**Expected result:** +- `changesCount: 1` +- `breakingChangesCount: 0` +- `deploymentRisk: LOW` +- One non-breaking change: `FIELD_ADDED full_name` + +### Test B — Breaking (field removal + type change) + +**Input:** V1 spec vs V2 spec that removes `email` and `name` from the response body, and changes the `id` path parameter type from `integer` to `string`. + +**Expected result:** +- `FIELD_REMOVED email` +- `FIELD_REMOVED name` +- `TYPE_CHANGED id integer → string` +- `changesCount: 3` +- `breakingChangesCount: 3` +- `deploymentRisk: HIGH` +- `detailedImpact` — 3 grounded impact descriptions +- `migrationGuide` — 3 specific migration actions + +--- + +## Lamatic Workflow / Configuration + +- **Flow:** [`flows/analyze-schema-drift.ts`](./flows/analyze-schema-drift.ts) contains the checked-in Lamatic flow definition (trigger → LLM → response). +- **Prompt:** [`prompts/analyze-schema-drift_llm-node_system.md`](./prompts/analyze-schema-drift_llm-node_system.md) contains the LLM system prompt. +- **Model configuration:** [`model-configs/analyze-schema-drift_llm-node_generative-model-name.ts`](./model-configs/analyze-schema-drift_llm-node_generative-model-name.ts) contains the checked-in model configuration used by the kit (`gemini-2.5-flash`). +- **Constitution:** [`constitutions/default.md`](./constitutions/default.md) contains the safety and data handling guidelines referenced by the flow. +- The deployed flow is configured and tested in Lamatic Studio. +- The workflow receives deterministic schema-drift facts through `sampleInput` and uses the LLM to generate grounded impact analysis and migration guidance. +- **Kit config:** [`lamatic.config.ts`](./lamatic.config.ts) contains kit metadata and the required `LAMATIC_DRIFT_FLOW_ID`. + +--- + +## Design Decisions and Tradeoffs + +**Why `openapi-diff` instead of a pure LLM diff?** +`openapi-diff` gives deterministic, reproducible, structured output. The LLM layer only receives confirmed facts — it cannot contradict or fabricate changes. This is the key correctness guarantee. + +**Why does `detectParameterTypeChanges` exist?** +`openapi-diff` does not consistently surface path-parameter type changes. `detectParameterTypeChanges()` is now part of the production deterministic normalization layer in [`apps/lib/sentinel.ts`](./apps/lib/sentinel.ts). It supplements `openapi-diff` by directly comparing path parameters between the two specs. This is why the production browser test correctly detects `id: integer → string` on `GET /users/{id}`. + +**Why `Lamatic.executeFlow(flowId, payload)` via the Lamatic SDK?** +The integration uses the official `@lamatic/sdk` `executeFlow` API. The SDK manages flow execution, payload transmission, and polling internally, providing a robust, typed execution path directly against the deployed flow ID. + +**Why is the LLM output merged with deterministic counts?** +`breakingCount`, `nonBreakingCount`, and dashboard `riskLevel` are derived deterministically from the change classification, independent of the LLM. This means the dashboard's risk badge and change counters are always correct even if the AI narrative fails or is degraded. + +--- + +## Limitations + +- **YAML spec support:** Both specs must be valid OpenAPI 3.0 JSON. YAML input is not currently parsed. +- **Lamatic dependency:** AI narrative synthesis requires a configured and deployed Lamatic flow. If the flow is unreachable, the API returns deterministic facts with a fallback `executiveSummary` string instead of failing. +- **Path parameter type changes:** Path parameter type changes are supplemented by a direct comparator because `openapi-diff` may not consistently surface them. +- **Single endpoint scope:** The current implementation treats the entire spec as a single analysis unit. It does not segment analysis per-endpoint for large multi-path specs. +- **No authentication on the API:** The `/api/analyze-drift` endpoint has no authentication. Suitable for local/internal use; add middleware before public deployment. + +--- + +Built on [Lamatic](https://lamatic.ai). diff --git a/kits/api-schema-drift-sentinel/agent.md b/kits/api-schema-drift-sentinel/agent.md new file mode 100644 index 000000000..134685721 --- /dev/null +++ b/kits/api-schema-drift-sentinel/agent.md @@ -0,0 +1,15 @@ +# API Schema Drift Sentinel + +## Overview + +API Schema Drift Sentinel detects breaking changes between OpenAPI specifications and produces grounded migration guidance. + +## Purpose + +The goal of this kit is to prevent breaking API drift by combining deterministic AST diffing with an AI reasoning layer. + +## Flows + +### 1. Analyze Schema Drift + +- **Flow ID / Env key mapping:** `analyze-schema-drift` (configured via `LAMATIC_DRIFT_FLOW_ID`) diff --git a/kits/api-schema-drift-sentinel/apps/.env.example b/kits/api-schema-drift-sentinel/apps/.env.example new file mode 100644 index 000000000..ab9197fa1 --- /dev/null +++ b/kits/api-schema-drift-sentinel/apps/.env.example @@ -0,0 +1,4 @@ +LAMATIC_API_KEY=your_lamatic_api_key_here +LAMATIC_API_URL=https://api.lamatic.ai +LAMATIC_DRIFT_FLOW_ID=your_id +LAMATIC_PROJECT_ID=your_project_id_here diff --git a/kits/api-schema-drift-sentinel/apps/actions/orchestrate.ts b/kits/api-schema-drift-sentinel/apps/actions/orchestrate.ts new file mode 100644 index 000000000..bb35f8cc7 --- /dev/null +++ b/kits/api-schema-drift-sentinel/apps/actions/orchestrate.ts @@ -0,0 +1,28 @@ +"use server"; + +import { runOpenApiDiff, normalizeDiff, triggerLamaticWorkflow } from '../lib/sentinel'; + +export async function analyzeSchemaDrift( + oldSpecContent: string, + newSpecContent: string, + apiName = "Target API", + oldVersion = "1.0.0", + newVersion = "2.0.0" +) { + try { + const rawDiff = await runOpenApiDiff(oldSpecContent, newSpecContent); + const normalizedChanges = normalizeDiff(rawDiff, oldSpecContent, newSpecContent); + const payload = { + apiName, + oldVersion, + newVersion, + changesCount: normalizedChanges.allChanges.length, + changes: normalizedChanges.allChanges + }; + + const data = await triggerLamaticWorkflow(payload); + return { success: true, data }; + } catch (error: any) { + return { success: false, error: error.message }; + } +} \ No newline at end of file diff --git a/kits/api-schema-drift-sentinel/apps/app/api/analyze-drift/route.ts b/kits/api-schema-drift-sentinel/apps/app/api/analyze-drift/route.ts new file mode 100644 index 000000000..4c330ca62 --- /dev/null +++ b/kits/api-schema-drift-sentinel/apps/app/api/analyze-drift/route.ts @@ -0,0 +1,135 @@ +import { NextResponse } from 'next/server'; +import { runOpenApiDiff, normalizeDiff, triggerLamaticWorkflow } from '@/lib/sentinel'; + +export const maxDuration = 120; + +export async function POST(req: Request) { + try { + const body = await req.json().catch(() => ({})); + + if ( + typeof body !== 'object' || + body === null || + Array.isArray(body) + ) { + return NextResponse.json( + { success: false, error: 'Request body must be a JSON object.' }, + { status: 400 } + ); + } + + const { specA, specB } = body; + + if (!specA || !specB) { + return NextResponse.json( + { success: false, error: 'Both specA and specB are required.' }, + { status: 400 } + ); + } + + const typeA = typeof specA; + const typeB = typeof specB; + + const isJsonObject = (value: unknown) => + typeof value === 'object' && value !== null && !Array.isArray(value); + + if ( + (typeA !== 'string' && !isJsonObject(specA)) || + (typeB !== 'string' && !isJsonObject(specB)) + ) { + return NextResponse.json( + { success: false, error: 'specA and specB must be string or JSON object.' }, + { status: 400 } + ); + } + + const strA = typeA === 'string' ? specA : JSON.stringify(specA); + const strB = typeB === 'string' ? specB : JSON.stringify(specB); + const MAX_SIZE = 2 * 1024 * 1024; // 2 MB limit + + const encoder = new TextEncoder(); + + if ( + encoder.encode(strA).byteLength > MAX_SIZE || + encoder.encode(strB).byteLength > MAX_SIZE + ) { + return NextResponse.json( + { success: false, error: 'Spec payload exceeds 2 MB size limit.' }, + { status: 400 } + ); + } + + // 1. Run local AST diff + const rawDiff = await runOpenApiDiff(specA, specB); + + // 2. Normalize deterministic facts + const facts = normalizeDiff(rawDiff, specA, specB); + console.log( + `[analyze-drift] normalized changeCount=${facts.allChanges.length} breaking=${facts.totalBreaking} risk=${facts.calculatedRisk}` + ); + + // 3. Build the sampleInput payload in the exact shape the LLM node's + // system prompt documents: { apiName, oldVersion, newVersion, changesCount, changes[] } + const parseSpecInfo = (raw: any): { title?: string; version?: string } => { + try { + const obj = typeof raw === 'string' ? JSON.parse(raw) : raw; + return { title: obj?.info?.title, version: obj?.info?.version }; + } catch { + return {}; + } + }; + const infoA = parseSpecInfo(specA); + const infoB = parseSpecInfo(specB); + + const sampleInput = JSON.stringify({ + apiName: infoA.title || infoB.title || 'Target API', + oldVersion: infoA.version || '1.0.0', + newVersion: infoB.version || '2.0.0', + changesCount: facts.allChanges.length, + changes: facts.allChanges.map((c) => ({ + endpoint: c.endpoint, + changeType: c.changeType, + affectedField: c.affectedField, + description: c.description, + })), + }); + + // 4. Call Lamatic with error isolation + let aiResult: any = {}; + try { + aiResult = await triggerLamaticWorkflow({ sampleInput }); + } catch (lamaticError: any) { + console.error('--- LAMATIC WORKFLOW ERROR ---', lamaticError?.message || lamaticError); + aiResult = { + executiveSummary: 'AI narrative synthesis failed. Displaying deterministic facts.', + }; + } + + // 5. Parse response safely + let formattedAiData = aiResult; + if (typeof aiResult === 'string') { + try { + formattedAiData = JSON.parse(aiResult); + } catch { + formattedAiData = { executiveSummary: aiResult }; + } + } + + return NextResponse.json({ + success: true, + data: { + ...formattedAiData, + breakingCount: facts.totalBreaking, + nonBreakingCount: facts.totalNonBreaking, + riskLevel: facts.calculatedRisk, + changes: facts.allChanges, + }, + }); + } catch (error: any) { + console.error('--- ANALYZE DRIFT ROUTE ERROR ---', error); + return NextResponse.json( + { success: false, error: error.message || 'Internal Server Error' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/kits/api-schema-drift-sentinel/apps/app/globals.css b/kits/api-schema-drift-sentinel/apps/app/globals.css new file mode 100644 index 000000000..a90d63cf0 --- /dev/null +++ b/kits/api-schema-drift-sentinel/apps/app/globals.css @@ -0,0 +1,14 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --font-sans: 'Inter', system-ui, sans-serif; + --background: #020617; + --foreground: #f1f5f9; +} + +* { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} \ No newline at end of file diff --git a/kits/api-schema-drift-sentinel/apps/app/layout.tsx b/kits/api-schema-drift-sentinel/apps/app/layout.tsx new file mode 100644 index 000000000..5d531eaf7 --- /dev/null +++ b/kits/api-schema-drift-sentinel/apps/app/layout.tsx @@ -0,0 +1,20 @@ +import './globals.css'; + +export const metadata = { + title: 'API Schema Drift Sentinel', + description: 'Deterministic AST diffing paired with AI-driven migration orchestration.', +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {children} + + + ); +} \ No newline at end of file diff --git a/kits/api-schema-drift-sentinel/apps/app/page.tsx b/kits/api-schema-drift-sentinel/apps/app/page.tsx new file mode 100644 index 000000000..462f5e1d5 --- /dev/null +++ b/kits/api-schema-drift-sentinel/apps/app/page.tsx @@ -0,0 +1,489 @@ +'use client'; + +import React, { useState } from 'react'; +import { Hexagon, GitBranch, ScanSearch, TriangleAlert, Table2, Check, ArrowRight } from 'lucide-react'; + +// ── Example specs (breaking scenario) for quick reviewer demo ────────────── +const EXAMPLE_V1 = JSON.stringify({ + openapi: '3.0.0', + info: { title: 'User Service API', version: '1.0.0' }, + paths: { + '/users/{id}': { + get: { + summary: 'Get user details', + parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'integer' } }], + responses: { + '200': { + description: 'User found', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + id: { type: 'integer' }, + name: { type: 'string' }, + email: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }, + }, + }, +}, null, 2); + +const EXAMPLE_V2_BREAKING = JSON.stringify({ + openapi: '3.0.0', + info: { title: 'User Service API', version: '2.0.0' }, + paths: { + '/users/{id}': { + get: { + summary: 'Get user details', + parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }], + responses: { + '200': { + description: 'User found', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + id: { type: 'integer' }, + }, + }, + }, + }, + }, + }, + }, + }, + }, +}, null, 2); + +// ── Small reusable components ────────────────────────────────────────────── + +function RiskBadge({ risk }: { risk: string }) { + const r = risk?.toUpperCase() ?? 'LOW'; + const cls: Record = { + CRITICAL: 'bg-red-600/15 text-red-300 border-red-600/30', + HIGH: 'bg-red-500/15 text-red-400 border-red-500/30', + MEDIUM: 'bg-amber-500/15 text-amber-400 border-amber-500/30', + LOW: 'bg-emerald-500/15 text-emerald-400 border-emerald-500/30', + }; + return ( + + + {r} RISK + + ); +} + +function SeverityPip({ severity }: { severity: string }) { + return ( + + {severity} + + ); +} + +function ActionPip({ action }: { action: string }) { + const map: Record = { + remove: { label: 'REMOVED', cls: 'bg-red-500/10 text-red-400 border-red-500/25' }, + add: { label: 'ADDED', cls: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/25' }, + change: { label: 'CHANGED', cls: 'bg-amber-500/10 text-amber-400 border-amber-500/25' }, + }; + const { label, cls } = map[action] ?? { label: action.toUpperCase(), cls: 'bg-slate-800 text-slate-500 border-slate-700/50' }; + return ( + + {label} + + ); +} + +function TypeCell({ value }: { value: string }) { + if (!value || value === '—') return ; + return {value}; +} + +// ── Main dashboard ───────────────────────────────────────────────────────── + +export default function SentinelDashboard() { + const [specA, setSpecA] = useState(''); + const [specB, setSpecB] = useState(''); + const [loading, setLoading] = useState(false); + const [analysis, setAnalysis] = useState(null); + const [error, setError] = useState(''); + + const handleAnalyze = async () => { + if (!specA.trim() || !specB.trim()) { + setError('Both Base Spec and Target Spec are required.'); + return; + } + setError(''); + setLoading(true); + setAnalysis(null); + + try { + const res = await fetch('/api/analyze-drift', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ specA, specB }), + }); + const json = await res.json(); + if (!json.success) throw new Error(json.error ?? 'Analysis failed'); + setAnalysis(json.data); + } catch (err: any) { + setError(err.message ?? 'Unexpected error'); + } finally { + setLoading(false); + } + }; + + const loadExample = () => { + setSpecA(EXAMPLE_V1); + setSpecB(EXAMPLE_V2_BREAKING); + setAnalysis(null); + setError(''); + }; + + const toStringList = (v: unknown): string[] => + Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string') : []; + const summary = analysis?.executiveSummary; + + const risk = analysis?.riskLevel ?? (typeof summary?.deploymentRisk === 'string' ? summary.deploymentRisk : 'LOW'); + const recommendation = + typeof summary === 'string' + ? summary + : typeof summary?.recommendation === 'string' + ? summary.recommendation + : typeof analysis?.summary === 'string' + ? analysis.summary + : ''; + const breakingCount = typeof analysis?.breakingCount === 'number' ? analysis.breakingCount : 0; + const nonBreakingCount = typeof analysis?.nonBreakingCount === 'number' ? analysis.nonBreakingCount : 0; + const changes: any[] = Array.isArray(analysis?.changes) ? analysis.changes : []; + const detailedImpact: string[] = toStringList(analysis?.detailedImpact); + const migrationGuide: string[] = toStringList(analysis?.migrationGuide); + + return ( +
+ + {/* ── Header ──────────────────────────────────────────────────────── */} +
+
+
+ +
+

API Schema Drift Sentinel

+

Deterministic diff · AI migration intelligence

+
+
+
+ v1.0.0 + + +
+
+
+ +
+ + {/* ── Spec input section ────────────────────────────────────────── */} +
+
+
+

Compare OpenAPI Specifications

+

+ Paste valid OpenAPI 3.0 JSON for both versions. The local diff engine runs first — AI synthesizes the narrative. +

+
+ +
+ +
+ {/* Base spec */} +
+
+
+ + Base Spec +
+ v1 · source of truth +
+