<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: kanugu rajesh</title>
    <description>The latest articles on DEV Community by kanugu rajesh (@kanugurajesh).</description>
    <link>https://dev.to/kanugurajesh</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1200971%2Fb06266b8-5958-4cee-9b91-b8023be25531.jpg</url>
      <title>DEV Community: kanugu rajesh</title>
      <link>https://dev.to/kanugurajesh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kanugurajesh"/>
    <language>en</language>
    <item>
      <title>Building AI-Native Infrastructure with Specmatic: How I Eliminated Integration Uncertainty in a Multi-Service System</title>
      <dc:creator>kanugu rajesh</dc:creator>
      <pubDate>Thu, 18 Jun 2026 07:15:29 +0000</pubDate>
      <link>https://dev.to/kanugurajesh/building-ai-native-infrastructure-with-specmatic-how-i-eliminated-integration-uncertainty-in-a-21m6</link>
      <guid>https://dev.to/kanugurajesh/building-ai-native-infrastructure-with-specmatic-how-i-eliminated-integration-uncertainty-in-a-21m6</guid>
      <description>&lt;p&gt;&lt;em&gt;A hands-on walkthrough of contract-first development, async event contracts, and building the feedback loops that AI coding agents actually need.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;There's a gap between what AI coding agents can generate and what production systems actually need.&lt;/p&gt;

&lt;p&gt;An agent can write a Flask route. It can write an OpenAPI spec. It can even write tests. But when you have two services built by two different agents — or two different humans — that need to talk to each other, the integration is where everything silently breaks.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;specmatic-taskflow&lt;/strong&gt; to explore this exact problem. It's a multi-service task management system where a Task Service, a User Service, a Kafka event bus, and a Kanban frontend all talk to each other. But more importantly, I integrated &lt;strong&gt;Specmatic&lt;/strong&gt; into every layer — turning OpenAPI and AsyncAPI specifications into living, executable tests that both humans &lt;em&gt;and&lt;/em&gt; AI coding agents can use as a feedback signal.&lt;/p&gt;

&lt;p&gt;This post is a walkthrough of what I built, how I designed the contract-first workflow, and what I learned about making contracts enforceable rather than aspirational.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: Integration Uncertainty at Scale
&lt;/h2&gt;

&lt;p&gt;When you build a single-service application, failures are visible. When you build a multi-service system, failures are &lt;em&gt;silent&lt;/em&gt; until production.&lt;/p&gt;

&lt;p&gt;Here's the typical failure mode:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Service A and Service B both implement the same OpenAPI spec.&lt;/li&gt;
&lt;li&gt;A developer changes a field name in Service A (&lt;code&gt;dueDate&lt;/code&gt; → &lt;code&gt;due_date&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The OpenAPI spec isn't updated. Tests pass because they're mocked.&lt;/li&gt;
&lt;li&gt;Service B still expects &lt;code&gt;dueDate&lt;/code&gt;. Everything looks fine in staging.&lt;/li&gt;
&lt;li&gt;Production breaks.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This problem gets &lt;em&gt;worse&lt;/em&gt; with AI coding agents. When you ask an LLM to implement a service, it generates code based on its training — not based on what the &lt;em&gt;other&lt;/em&gt; services in your system expect. Without a feedback mechanism that can tell an AI "your implementation doesn't match the contract," you get plausible-looking code that doesn't integrate.&lt;/p&gt;

&lt;p&gt;The solution isn't more documentation. It's &lt;strong&gt;executable contracts&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built: specmatic-taskflow
&lt;/h2&gt;

&lt;p&gt;specmatic-taskflow is an AI-native task management system with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Task Service&lt;/strong&gt; — Flask REST API (CRUD on tasks, publishes to Kafka)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User Service&lt;/strong&gt; — Flask REST API (user registry, assignee lookup)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apache Kafka&lt;/strong&gt; — Event bus for task lifecycle events (&lt;code&gt;task-created&lt;/code&gt;, &lt;code&gt;task-updated&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kanban Frontend&lt;/strong&gt; — Vanilla JavaScript SPA with no build toolchain overhead&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specmatic Contract Tests&lt;/strong&gt; — REST tests (OpenAPI 3.0.1) + Async tests (AsyncAPI 3.0.0)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema Resiliency Tests&lt;/strong&gt; — Auto-generated positive variations beyond hand-written examples&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specmatic Mock Server&lt;/strong&gt; — Serves contract-driven mock responses before real services exist&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specmatic Studio&lt;/strong&gt; — Visual IDE for contract exploration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything runs via Docker Compose. Bringing up the full stack also runs contract tests — so integration verification is not a separate step, it's built into the startup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;specmatic-taskflow/
├── specs/
│   ├── openapi/
│   │   ├── task-api.yaml          # Task Service contract
│   │   └── user-api.yaml          # User Service contract
│   └── asyncapi/
│       ├── task-events.yaml       # Kafka event contract
│       └── examples/              # Before-hooks that trigger publishes via the REST API
├── services/
│   ├── task-service/              # Flask + Kafka publisher
│   └── user-service/              # Flask user registry
├── frontend/                      # Vanilla JS Kanban board
├── specmatic.yaml                 # Task API REST + async Kafka test configuration
├── specmatic-user.yaml            # User API REST test configuration
└── docker-compose.yaml            # Full orchestration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 1: Write the Contract First
&lt;/h2&gt;

&lt;p&gt;The first principle of this project: &lt;strong&gt;the spec exists before the code.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I wrote &lt;code&gt;task-api.yaml&lt;/code&gt; before writing a single line of Flask. Every endpoint, every status code, every field name was decided in the OpenAPI document. The implementation's job was to satisfy this contract, not to define it.&lt;/p&gt;

&lt;p&gt;Here's a simplified view of the Task API contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# specs/openapi/task-api.yaml&lt;/span&gt;
&lt;span class="na"&gt;openapi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3.0.1&lt;/span&gt;
&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Task Service API&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.0.0&lt;/span&gt;

&lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;/tasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;List all tasks&lt;/span&gt;
      &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;status&lt;/span&gt;
          &lt;span class="na"&gt;in&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;query&lt;/span&gt;
          &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
            &lt;span class="na"&gt;enum&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;pending&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;in-progress&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;completed&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
          &lt;span class="na"&gt;examples&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;TASKS_200_OK&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pending&lt;/span&gt;
      &lt;span class="na"&gt;responses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;200'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;application/json&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;examples&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;TASKS_200_OK&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
                      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Implement&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Login&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Feature"&lt;/span&gt;
                      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;in-progress"&lt;/span&gt;
                      &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;high"&lt;/span&gt;

    &lt;span class="na"&gt;post&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create a task&lt;/span&gt;
      &lt;span class="na"&gt;requestBody&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;application/json&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;examples&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;CREATE_TASK_201&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Write&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Unit&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Tests"&lt;/span&gt;
                  &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;medium"&lt;/span&gt;
              &lt;span class="na"&gt;CREATE_TASK_400&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;medium"&lt;/span&gt;   &lt;span class="c1"&gt;# Missing required 'title'&lt;/span&gt;
      &lt;span class="na"&gt;responses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;201'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;application/json&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;examples&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;CREATE_TASK_201&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
                    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Write&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Unit&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Tests"&lt;/span&gt;
                    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pending"&lt;/span&gt;
                    &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;medium"&lt;/span&gt;
      &lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;400'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;application/json&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;examples&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;CREATE_TASK_400&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                    &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Validation&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;failed"&lt;/span&gt;
                    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;required"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The critical detail: &lt;strong&gt;inline example names must match across parameters, request bodies, and responses.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When Specmatic sees &lt;code&gt;CREATE_TASK_400&lt;/code&gt; in both the request body and the 400 response, it pairs them into a single test scenario: "send this request body, expect this response." This is how examples become test cases without writing any test code.&lt;/p&gt;

&lt;p&gt;The naming convention I used — &lt;code&gt;TASKS_200_OK&lt;/code&gt;, &lt;code&gt;CREATE_TASK_201&lt;/code&gt;, &lt;code&gt;CREATE_TASK_400&lt;/code&gt;, &lt;code&gt;GET_TASK_404&lt;/code&gt; — is the entire test suite. No test files. No mocking frameworks. No setup/teardown.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Run the Contract, Watch It Fail
&lt;/h2&gt;

&lt;p&gt;Before writing any Flask code, I ran Specmatic against a stub service.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up test-task-api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Specmatic hit the (non-existent) endpoints and failed every test. But this failure is meaningful — it's a precise diff between what the contract expects and what the service provides. This is exactly the kind of signal that an AI coding agent can act on.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;specmatic.yaml&lt;/code&gt; configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;

&lt;span class="na"&gt;systemUnderTest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;definitions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;definition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;filesystem&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;specs/openapi&lt;/span&gt;
          &lt;span class="na"&gt;specs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;task-api.yaml&lt;/span&gt;
    &lt;span class="na"&gt;runOptions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;openapi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
        &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://task-service:8080&lt;/span&gt;

&lt;span class="na"&gt;specmatic&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;settings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;schemaResiliencyTests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;
  &lt;span class="na"&gt;governance&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;report&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;formats&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;html&lt;/span&gt;
      &lt;span class="na"&gt;outputDirectory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build/reports/specmatic&lt;/span&gt;
    &lt;span class="na"&gt;successCriteria&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;minCoveragePercentage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
      &lt;span class="na"&gt;maxMissedOperationsInSpec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
      &lt;span class="na"&gt;enforce&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;license&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/specmatic/specmatic-license.txt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three governance settings matter here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;minCoveragePercentage: 100&lt;/code&gt; — Every single endpoint in the spec must be tested. No skipping.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;maxMissedOperationsInSpec: 0&lt;/code&gt; — If the service has endpoints not in the spec, it fails.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;schemaResiliencyTests: all&lt;/code&gt; — Beyond the hand-written examples, Specmatic automatically generates every valid positive schema variation &lt;em&gt;and&lt;/em&gt; every invalid (negative) variation — wrong types, broken enums, missing required fields. More on this in Step 4.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes the contract &lt;strong&gt;enforcing&lt;/strong&gt;, not advisory.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Build the Service to Satisfy the Contract
&lt;/h2&gt;

&lt;p&gt;With the failing tests as my guide, I implemented the Task Service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# services/task-service/main.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jsonify&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask_cors&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;CORS&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;CORS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Three tasks seeded with dedicated IDs — one per mutating operation
# to prevent test ordering conflicts (see Challenges section).
&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Implement Login Feature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;in-progress&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assignee&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priority&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;high&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;createdAt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2024-01-15T10:00:00Z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Write Unit Tests&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assignee&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bob&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priority&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;medium&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;createdAt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2024-01-15T11:00:00Z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Review Pull Request&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assignee&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priority&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;low&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;createdAt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2024-01-15T12:00:00Z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;_next_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;  &lt;span class="c1"&gt;# avoids collision with seed IDs during schema resiliency POST tests
&lt;/span&gt;
&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/actuator/health&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;actuator_health&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UP&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/tasks&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list_tasks&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;status_filter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status_filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;status_filter&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/tasks&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;_next_id&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;silent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Validation failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title is required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;priority&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Validation failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priority is required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;

    &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;_next_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assignee&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;assignee&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priority&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;priority&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;createdAt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;_now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;_next_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;
    &lt;span class="n"&gt;_next_id&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="nf"&gt;_publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task-created&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;taskId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assignee&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assignee&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priority&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priority&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;createdAt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The implementation's shape was dictated by the contract. Field names, status codes, error response format — all copied from the spec, not invented.&lt;/p&gt;

&lt;p&gt;After implementing all five endpoints (&lt;code&gt;GET /tasks&lt;/code&gt;, &lt;code&gt;POST /tasks&lt;/code&gt;, &lt;code&gt;GET /tasks/{taskId}&lt;/code&gt;, &lt;code&gt;PUT /tasks/{taskId}&lt;/code&gt;, &lt;code&gt;DELETE /tasks/{taskId}&lt;/code&gt;), I ran the contract tests again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;--exit-code-from&lt;/span&gt; test-task-api test-task-api
&lt;span class="c"&gt;# Exit code: 0 ✓&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All tests pass. Not because I wrote tests against my own implementation — but because my implementation satisfies a contract that was written independently.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Schema Resiliency — Beyond Hand-Written Examples
&lt;/h2&gt;

&lt;p&gt;One example per endpoint is a floor, not a ceiling.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;schemaResiliencyTests&lt;/code&gt; in &lt;code&gt;specmatic.yaml&lt;/code&gt;, Specmatic automatically expands each example into every variation the schema allows. No extra test files. No test authoring. The schema itself defines the test space. There are three modes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;What it generates&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;positiveOnly&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Every &lt;em&gt;valid&lt;/em&gt; combination of enum values and optional-field presence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;negativeOnly&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Every &lt;em&gt;invalid&lt;/em&gt; combination — wrong types, broken enums, missing required fields&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;all&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Both — the full positive and negative test space&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here's what this means in practice for &lt;code&gt;PUT /tasks/{taskId}&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;UpdateTaskRequest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;object&lt;/span&gt;
  &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
      &lt;span class="na"&gt;enum&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;pending&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;in-progress&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;completed&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# 3 values&lt;/span&gt;
    &lt;span class="na"&gt;assignee&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
    &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
      &lt;span class="na"&gt;enum&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;low&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;medium&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;high&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;                 &lt;span class="c1"&gt;# 3 values&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three fields are optional. The hand-written example only covers &lt;code&gt;{"status": "completed", "assignee": "alice"}&lt;/code&gt;. On the positive side, Specmatic generates every valid combination of enum values and optional field presence. On the negative side, it also throws &lt;code&gt;assignee: 42&lt;/code&gt;, &lt;code&gt;priority: "urgent"&lt;/code&gt;, &lt;code&gt;status: null&lt;/code&gt;, and dozens of similar mutations at the same endpoint — each one expecting a &lt;code&gt;4xx&lt;/code&gt; response back.&lt;/p&gt;

&lt;p&gt;I run with &lt;code&gt;schemaResiliencyTests: all&lt;/code&gt;. The test count jump is real:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Total tests (task-api)&lt;/th&gt;
&lt;th&gt;Total tests (user-api)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Example-only (no resiliency)&lt;/td&gt;
&lt;td&gt;~10&lt;/td&gt;
&lt;td&gt;~5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;positiveOnly&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;all&lt;/code&gt; (positive + negative)&lt;/td&gt;
&lt;td&gt;135&lt;/td&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This catches two different classes of bugs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Positive resiliency&lt;/strong&gt;: a service that handles &lt;code&gt;priority: "high"&lt;/code&gt; but crashes on &lt;code&gt;priority: "low"&lt;/code&gt; — something a single hand-written example would never reveal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Negative resiliency&lt;/strong&gt;: a service that &lt;em&gt;should&lt;/em&gt; reject &lt;code&gt;priority: "urgent"&lt;/code&gt; or &lt;code&gt;assignee: true&lt;/code&gt; with a &lt;code&gt;400&lt;/code&gt;, but instead silently accepts it and corrupts its own state — something no hand-written example was ever written to catch, because nobody writes examples for requests they don't expect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;One important consequence:&lt;/strong&gt; more tests means more mutations, which means test execution order matters. I cover how I solved this in the Challenges section.&lt;/p&gt;




&lt;h2&gt;
  
  
  Turning On &lt;code&gt;schemaResiliencyTests: all&lt;/code&gt; — and Fixing What It Found
&lt;/h2&gt;

&lt;p&gt;Flipping &lt;code&gt;positiveOnly&lt;/code&gt; to &lt;code&gt;all&lt;/code&gt; didn't just add more passing tests — it took the task-api suite from 27 green tests to &lt;strong&gt;133 tests, 96 of them failing&lt;/strong&gt;. Here's the real output from that first run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Tests run: 133, Successes: 37, Failures: 96, WIP: 0, Errors: 0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's not noise. It's every place where the Flask services accepted garbage instead of rejecting it. Three distinct failure patterns showed up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure 1 — No type or enum validation on write endpoints
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;POST /tasks&lt;/code&gt; and &lt;code&gt;PUT /tasks/{taskId}&lt;/code&gt; only checked that required fields were &lt;em&gt;truthy&lt;/em&gt; — they never checked that &lt;code&gt;title&lt;/code&gt; was a string, that &lt;code&gt;priority&lt;/code&gt; was one of &lt;code&gt;low|medium|high&lt;/code&gt;, or that &lt;code&gt;assignee&lt;/code&gt; wasn't a boolean. Specmatic's negative generator throws every wrong type at every field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-ve  Scenario: POST /tasks -&amp;gt; 4xx with the request from the example 'CREATE_TASK_201'
     where REQUEST.BODY contains all the keys AND the key priority is mutated
     from ("low" or "medium" or "high") to number FAILED
Reason:
    &amp;gt;&amp;gt; RESPONSE.STATUS
        Expected 4xx status, but received 201
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;87 of the 96 failures were exactly this shape: &lt;code&gt;Expected 4xx status, but received 200/201&lt;/code&gt;. The fix was real input validation in both services — type checks via &lt;code&gt;isinstance&lt;/code&gt;, enum checks against an explicit set, applied &lt;em&gt;before&lt;/em&gt; anything gets written to the in-memory store:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# services/task-service/main.py
&lt;/span&gt;&lt;span class="n"&gt;TASK_PRIORITIES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;low&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;medium&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;high&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;TASK_STATUSES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;in-progress&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;silent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_validation_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title is required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;priority&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priority&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;TASK_PRIORITIES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_validation_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priority must be one of &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TASK_PRIORITIES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_validation_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description must be a string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assignee&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assignee&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_validation_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assignee must be a string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same pattern applied to &lt;code&gt;PUT /tasks/{taskId}&lt;/code&gt; (status/priority enums, assignee type) and to &lt;code&gt;user-service&lt;/code&gt;'s &lt;code&gt;POST /users&lt;/code&gt; (role enum). One subtlety that cost a second round of failures: checking &lt;code&gt;value is not None and not isinstance(value, str)&lt;/code&gt; lets an explicit &lt;code&gt;null&lt;/code&gt; slip through, because &lt;code&gt;None is not None&lt;/code&gt; is &lt;code&gt;False&lt;/code&gt;. The fix is to drop the &lt;code&gt;is not None&lt;/code&gt; guard entirely — if the key is present, its value must satisfy the type, full stop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure 2 — The contract had no &lt;code&gt;400&lt;/code&gt; to validate against
&lt;/h3&gt;

&lt;p&gt;After adding validation, a different error appeared for &lt;code&gt;PUT /tasks/{taskId}&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reason:
    &amp;gt;&amp;gt; RESPONSE.STATUS
        R0002: HTTP status mismatch
        Specification expected status 404 but response contained status 400
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And for &lt;code&gt;GET /tasks?status=&amp;lt;invalid&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reason:
    &amp;gt;&amp;gt; RESPONSE.STATUS
        Received 400, but the specification does not contain a 4xx or default
        response, hence unable to verify this response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service was now doing the &lt;em&gt;right&lt;/em&gt; thing — returning &lt;code&gt;400&lt;/code&gt; for bad input — but the OpenAPI spec never declared a &lt;code&gt;400&lt;/code&gt; response for these two operations. Only &lt;code&gt;POST /tasks&lt;/code&gt; had one. Specmatic enforces the contract literally: if you return a status code the spec doesn't document, that's a contract violation, even if it's semantically correct. This is the contract-first principle paying for itself — the gap was in the spec, not just the code. The fix was adding the missing response definitions to &lt;code&gt;task-api.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# GET /tasks&lt;/span&gt;
&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;400'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Validation error — invalid status filter&lt;/span&gt;
  &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;application/json&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;$ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#/components/schemas/Error'&lt;/span&gt;
      &lt;span class="na"&gt;examples&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;LIST_TASKS_400&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Validation&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;failed"&lt;/span&gt;
            &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;must&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;be&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;one&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;['completed',&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'in-progress',&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'pending']"&lt;/span&gt;

&lt;span class="c1"&gt;# PUT /tasks/{taskId}&lt;/span&gt;
&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;400'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Validation error — invalid field value&lt;/span&gt;
  &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;application/json&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;$ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#/components/schemas/Error'&lt;/span&gt;
      &lt;span class="na"&gt;examples&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;UPDATE_TASK_400&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Validation&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;failed"&lt;/span&gt;
            &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priority&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;must&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;be&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;one&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;['high',&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'low',&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'medium']"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each new &lt;code&gt;400&lt;/code&gt; needed a matching named example on the parameter/request side too (&lt;code&gt;LIST_TASKS_400&lt;/code&gt; on the &lt;code&gt;status&lt;/code&gt; query param, &lt;code&gt;UPDATE_TASK_400&lt;/code&gt; on both the &lt;code&gt;taskId&lt;/code&gt; path param and the request body) — Specmatic pairs examples by name, the same rule from Challenge 1.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure 3 — Flask's default 404 page broke the error schema
&lt;/h3&gt;

&lt;p&gt;Mutating &lt;code&gt;taskId&lt;/code&gt; from a number to a string or boolean (e.g. &lt;code&gt;DELETE /tasks/true&lt;/code&gt;) doesn't match Flask's &lt;code&gt;&amp;lt;int:task_id&amp;gt;&lt;/code&gt; route converter, so Flask falls back to its own default 404 — an HTML page, not JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt;&amp;gt; RESPONSE.HEADER.Content-Type
    R1002: Value mismatch
    Specification expected application/json but response contained text/html; charset=utf-8

&amp;gt;&amp;gt; RESPONSE.BODY
    R1001: Type mismatch
    Specification expected type json object but response contained value
    "&amp;lt;!doctype html&amp;gt;\n&amp;lt;html lang=en&amp;gt;\n&amp;lt;title&amp;gt;404 Not Found&amp;lt;/title&amp;gt;..."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is a Flask-wide 404 handler that returns JSON matching the &lt;code&gt;Error&lt;/code&gt; schema for any path Flask itself can't route — independent of the explicit &lt;code&gt;404&lt;/code&gt; responses already returned by &lt;code&gt;get_task&lt;/code&gt;/&lt;code&gt;update_task&lt;/code&gt;/&lt;code&gt;delete_task&lt;/code&gt; when a &lt;em&gt;valid&lt;/em&gt; taskId doesn't exist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.errorhandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_not_found&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_exc&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Not Found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The requested resource was not found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Result
&lt;/h3&gt;

&lt;p&gt;After all three fixes — input validation, the missing &lt;code&gt;400&lt;/code&gt; contract responses, and the JSON 404 handler — task-api went from &lt;strong&gt;37/133 passing to 135/135 passing&lt;/strong&gt; (two new explicit &lt;code&gt;400&lt;/code&gt; examples added two tests), and user-api went from &lt;strong&gt;30/49 to 49/49&lt;/strong&gt;. Both numbers were stable across repeated runs, confirming the fixes weren't masking flakiness.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Tests run: 135, Successes: 135, Failures: 0, WIP: 0, Errors: 0
Tests run: 49, Successes: 49, Failures: 0, WIP: 0, Errors: 0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The headline: &lt;strong&gt;96 negative-path bugs existed in services that had 100% positive-path contract coverage and a green CI pipeline.&lt;/strong&gt; Schema resiliency in &lt;code&gt;all&lt;/code&gt; mode is the difference between "the API works when you use it correctly" and "the API is actually safe to expose to a client you don't control" — which, for any service called by another team, another service, or an AI agent generating its own request bodies, is the only question that matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Extend to Async Events with AsyncAPI
&lt;/h2&gt;

&lt;p&gt;REST APIs are only half the story in an event-driven system. When the Task Service creates a task, it publishes to Kafka. Without a contract for those messages, any consumer can break silently.&lt;/p&gt;

&lt;p&gt;I wrote an AsyncAPI 3.0.0 spec for the Kafka events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# specs/asyncapi/task-events.yaml&lt;/span&gt;
&lt;span class="na"&gt;asyncapi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3.0.0&lt;/span&gt;
&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Task Events&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.0.0&lt;/span&gt;

&lt;span class="na"&gt;servers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;kafka&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kafka:9092&lt;/span&gt;
    &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kafka&lt;/span&gt;

&lt;span class="na"&gt;channels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;task-created&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;task-created&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;TaskCreatedMessage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;object&lt;/span&gt;
          &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;taskId&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;timestamp&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
          &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;taskId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;integer&lt;/span&gt;
            &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
            &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
              &lt;span class="na"&gt;enum&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;pending&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;in-progress&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;completed&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
            &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
              &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;date-time&lt;/span&gt;
        &lt;span class="na"&gt;examples&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;taskId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
              &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;New&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;deployment&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;task"&lt;/span&gt;
              &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pending&lt;/span&gt;
              &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2024-01-15T10:00:00Z"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The async contract test works differently from REST tests. Instead of Specmatic acting as a client hitting endpoints, &lt;code&gt;test-task-api&lt;/code&gt; runs both the REST and Kafka contract tests in a single pass. For the Kafka side, it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Subscribes to the Kafka &lt;code&gt;task-created&lt;/code&gt; and &lt;code&gt;task-updated&lt;/code&gt; topics&lt;/li&gt;
&lt;li&gt;Runs a &lt;code&gt;before&lt;/code&gt; hook that issues a real HTTP call against the Task Service (e.g. &lt;code&gt;POST /tasks&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Validates the message that publish produces against the AsyncAPI schema
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# specmatic.yaml — the asyncapi definition and runOptions, alongside&lt;/span&gt;
&lt;span class="c1"&gt;# the openapi ones already shown in Step 2, under the same systemUnderTest.service&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;definition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;filesystem&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;specs/asyncapi&lt;/span&gt;
          &lt;span class="na"&gt;specs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;task-events.yaml&lt;/span&gt;
    &lt;span class="na"&gt;runOptions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;asyncapi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
        &lt;span class="na"&gt;subscriberReadinessWaitTime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5000&lt;/span&gt;
        &lt;span class="na"&gt;servers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kafka:9092&lt;/span&gt;
            &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kafka&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;examples&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;directories&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;specs/asyncapi/examples&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha I hit:&lt;/strong&gt; The AsyncAPI spec initially had &lt;code&gt;host: localhost:9092&lt;/code&gt;. Inside Docker's network, services communicate by service name, not localhost. Changing to &lt;code&gt;host: kafka:9092&lt;/code&gt; (matching the Docker Compose service name) fixed the connection immediately. Always verify your spec's server hosts match your runtime network topology.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gotcha I hit (the expensive one):&lt;/strong&gt; Inline &lt;code&gt;examples&lt;/code&gt; on an AsyncAPI Message are documentation only — nothing actually calls the REST API to make the service publish. I initially put the example payload directly on the &lt;code&gt;operations&lt;/code&gt; entry, which AsyncAPI 3.0 rejects (&lt;code&gt;examples&lt;/code&gt; is only valid on &lt;code&gt;Message&lt;/code&gt;, not &lt;code&gt;Operation&lt;/code&gt;). Moving it to the message fixed parsing, but the test still timed out waiting for a Kafka message, because there was still nothing driving the publish. The actual mechanism is external JSON example files (&lt;code&gt;specs/asyncapi/examples/&lt;/code&gt;, wired up via &lt;code&gt;data.examples.directories&lt;/code&gt;) with a &lt;code&gt;before&lt;/code&gt; array that fires the real HTTP request:&lt;/p&gt;


&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TASK_CREATED_EXAMPLE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"before"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"http-request"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"baseUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://task-service:8080"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/tasks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"http-response"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"send"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"topic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"task-created"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"taskId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"(integer)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"(datetime)"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;&lt;code&gt;(integer)&lt;/code&gt; / &lt;code&gt;(datetime)&lt;/code&gt; are Specmatic's type-matcher placeholders — useful here since the published &lt;code&gt;taskId&lt;/code&gt; and &lt;code&gt;timestamp&lt;/code&gt; are server-generated and can't be hardcoded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gotcha I hit (the one that mattered most):&lt;/strong&gt; Once the wiring was right, the test still failed — every message timed out. The task service's &lt;code&gt;_publish()&lt;/code&gt; wraps the Kafka call in a &lt;code&gt;try/except&lt;/code&gt; (so the service can start before Kafka is ready), and that swallowed an exception every single time: &lt;code&gt;kafka-python==2.0.2&lt;/code&gt; doesn't work on Python 3.12 (&lt;code&gt;No module named 'kafka.vendor.six.moves'&lt;/code&gt;). The Kafka publish had never actually succeeded — it just looked fine because nothing was asserting on it until the async contract test existed. Switching to &lt;code&gt;kafka-python-ng&lt;/code&gt; (a maintained drop-in fork, same &lt;code&gt;import kafka&lt;/code&gt;) fixed it. This is exactly the failure mode contract testing is supposed to catch: a swallowed exception that made the service &lt;em&gt;look&lt;/em&gt; healthy while silently dropping every event.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;test-task-api&lt;/code&gt; runs both suites in one &lt;code&gt;specmatic test&lt;/code&gt; invocation and reports REST and Kafka results together (143/143 REST + 2/2 async, 100% coverage).&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6: Mock Server for Parallel Development
&lt;/h2&gt;

&lt;p&gt;While building the frontend Kanban board, the Task Service wasn't fully implemented yet. Rather than hardcoding fake data or building a separate mock, I used Specmatic's mock server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nt"&gt;--profile&lt;/span&gt; mock up mock-task-api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Specmatic reads &lt;code&gt;task-api.yaml&lt;/code&gt;, extracts the inline examples, and serves them as HTTP responses — with zero additional configuration. &lt;code&gt;GET /tasks&lt;/code&gt; returns the &lt;code&gt;TASKS_200_OK&lt;/code&gt; example. &lt;code&gt;POST /tasks&lt;/code&gt; with a missing title returns the &lt;code&gt;CREATE_TASK_400&lt;/code&gt; example.&lt;/p&gt;

&lt;p&gt;The frontend has a built-in API switcher in the sidebar that toggles between the mock server (&lt;code&gt;:9100&lt;/code&gt;) and the real service (&lt;code&gt;:8080&lt;/code&gt;). A frontend developer can work against realistic API responses before the backend exists, and switching to the real service requires changing exactly one URL.&lt;/p&gt;

&lt;p&gt;This is the pattern that enables &lt;strong&gt;parallel development without integration ceremonies&lt;/strong&gt;: frontend, backend, and mobile teams can all work simultaneously against the same source of truth (the spec), without coordinating implementation timelines.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 7: Governance Enforcement in CI
&lt;/h2&gt;

&lt;p&gt;The final piece is making contracts enforced — not just available.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;docker-compose.yaml&lt;/code&gt; uses &lt;code&gt;--exit-code-from&lt;/code&gt; to make the entire compose stack fail if contract tests fail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# In a CI pipeline&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;--exit-code-from&lt;/span&gt; test-task-api
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Exit code: &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;minCoveragePercentage: 100&lt;/code&gt;, a developer can't ship a partial implementation. If the spec defines &lt;code&gt;DELETE /tasks/{taskId}&lt;/code&gt; and the service doesn't implement it, the entire CI run fails.&lt;/p&gt;

&lt;p&gt;Because &lt;code&gt;schemaResiliencyTests: all&lt;/code&gt; lives in &lt;code&gt;specmatic.yaml&lt;/code&gt; rather than as a CLI flag, every CI run picks it up automatically — &lt;code&gt;docker compose run test-task-api&lt;/code&gt; and &lt;code&gt;docker compose run test-user-api&lt;/code&gt; both read the same config, so the GitHub Actions workflow (&lt;code&gt;.github/workflows/ci.yml&lt;/code&gt;) enforces negative-path validation on every push and pull request with zero pipeline changes beyond the config file itself. There's no separate "resiliency" job to maintain or forget to run — it's the same job, just testing a larger space.&lt;/p&gt;

&lt;p&gt;Specmatic generates HTML reports at &lt;code&gt;build/reports/specmatic/test/html/index.html&lt;/code&gt;. These show exactly which scenarios passed, which failed, and the diff between expected and actual responses — the artifact that goes into pull request reviews.&lt;/p&gt;

&lt;p&gt;The governance model I enforced:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Every operation in the spec = tested&lt;/td&gt;
&lt;td&gt;Can't skip endpoints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Every response = validated against schema&lt;/td&gt;
&lt;td&gt;Field drift caught immediately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Any drift between spec and implementation&lt;/td&gt;
&lt;td&gt;Build failure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This turns the OpenAPI spec from passive documentation into an active quality gate.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 8: Authentication &amp;amp; Authorization with Multiple OpenAPI Security Schemes
&lt;/h2&gt;

&lt;p&gt;Contract testing without auth testing has a blind spot: a service can satisfy every schema and still leak data to anyone who asks. I closed that gap by borrowing the pattern from the neighboring &lt;code&gt;labs/api-security-schemes&lt;/code&gt; lab — vary the security scheme by HTTP method, then let Specmatic prove the service actually enforces whichever one is declared.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;task-api.yaml&lt;/code&gt; ended up with three schemes on one spec:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;securitySchemes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;basicAuth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http&lt;/span&gt;
      &lt;span class="na"&gt;scheme&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;basic&lt;/span&gt;          &lt;span class="c1"&gt;# GET — any valid account may read&lt;/span&gt;
    &lt;span class="na"&gt;oAuth2AuthCode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;oauth2&lt;/span&gt;            &lt;span class="c1"&gt;# POST/PUT — developer, manager, or admin role&lt;/span&gt;
      &lt;span class="na"&gt;flows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;authorizationCode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;authorizationUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:8083/realms/taskflow/protocol/openid-connect/auth&lt;/span&gt;
          &lt;span class="na"&gt;tokenUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:8083/realms/taskflow/protocol/openid-connect/token&lt;/span&gt;
    &lt;span class="na"&gt;apiKeyAuth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apiKey&lt;/span&gt;
      &lt;span class="na"&gt;in&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;header&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;X-API-Key&lt;/span&gt;         &lt;span class="c1"&gt;# DELETE — admin role only&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;user-api.yaml&lt;/code&gt; uses one scheme (&lt;code&gt;apiKeyAuth&lt;/code&gt;) everywhere, with an elevated-role check bolted onto &lt;code&gt;POST /users&lt;/code&gt; only.&lt;/p&gt;

&lt;p&gt;The POST/PUT scheme started life as a static &lt;code&gt;bearerAuth&lt;/code&gt; (&lt;code&gt;type: http, scheme: bearer&lt;/code&gt;) checked against a hardcoded token dictionary — no different in substance from the API key, just a different header name. Gotcha 3 below covers why and how it became a real Keycloak-backed &lt;code&gt;oAuth2AuthCode&lt;/code&gt; scheme instead. Both Flask services enforce authorization with small decorator functions (&lt;code&gt;require_basic_auth&lt;/code&gt;, &lt;code&gt;require_bearer_auth(allowed_roles)&lt;/code&gt;, &lt;code&gt;require_api_key(allowed_roles)&lt;/code&gt;) backed by a demo identity table — four accounts (&lt;code&gt;alice&lt;/code&gt;/developer, &lt;code&gt;bob&lt;/code&gt;/qa, &lt;code&gt;diana&lt;/code&gt;/manager, &lt;code&gt;charlie&lt;/code&gt;/admin). A decorator returns &lt;code&gt;401&lt;/code&gt; for a missing/invalid credential and &lt;code&gt;403&lt;/code&gt; for a valid credential whose role isn't in the allowlist; here's the current, JWT-validating version of the bearer decorator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;require_bearer_auth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;allowed_roles&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decorator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nd"&gt;@wraps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;auth_header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;auth_header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_unauthorized&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer token required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;auth_header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;signing_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_jwks_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_signing_key_from_jwt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;claims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signing_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;algorithms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RS256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verify_aud&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verify_iss&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_unauthorized&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid or expired token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;token_roles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;realm_access&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;roles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]))&lt;/span&gt;
            &lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;developer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qa&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;manager&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;token_roles&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_unauthorized&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Token does not carry a recognized role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;allowed_roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_forbidden&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; is not permitted to perform this action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;wrapper&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;decorator&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Gotcha 1: &lt;code&gt;securitySchemes&lt;/code&gt; only work through &lt;code&gt;systemUnderTest&lt;/code&gt;, not the ad hoc CLI form
&lt;/h3&gt;

&lt;p&gt;This one took real digging through &lt;code&gt;specmatic-schema.json&lt;/code&gt; to resolve. Before this change, &lt;code&gt;test-task-api&lt;/code&gt; and &lt;code&gt;test-user-api&lt;/code&gt; both ran &lt;code&gt;specmatic test &amp;lt;spec-file&amp;gt; --testBaseURL=...&lt;/code&gt; — a positional-file invocation. That form clearly ignores &lt;code&gt;systemUnderTest.service&lt;/code&gt; entirely: it was already working for &lt;code&gt;user-api.yaml&lt;/code&gt; even though the &lt;code&gt;specmatic.yaml&lt;/code&gt; in the repo only ever declared &lt;code&gt;task-api.yaml&lt;/code&gt; under &lt;code&gt;systemUnderTest&lt;/code&gt;. Top-level &lt;code&gt;specmatic:&lt;/code&gt; settings (&lt;code&gt;governance&lt;/code&gt;, &lt;code&gt;schemaResiliencyTests&lt;/code&gt;, &lt;code&gt;license&lt;/code&gt;) are read regardless of invocation style, but the schema shows &lt;code&gt;securitySchemes&lt;/code&gt; lives three levels deeper — &lt;code&gt;systemUnderTest.service.runOptions.openapi.specs[].spec.securitySchemes&lt;/code&gt; — a path that's only consulted when Specmatic resolves the spec &lt;em&gt;through&lt;/em&gt; &lt;code&gt;systemUnderTest&lt;/code&gt;, not via a bare file argument.&lt;/p&gt;

&lt;p&gt;So adding &lt;code&gt;security:&lt;/code&gt; to the specs and a &lt;code&gt;securitySchemes&lt;/code&gt; block to &lt;code&gt;specmatic.yaml&lt;/code&gt; would have been silently ignored by the existing docker-compose commands. The fix: switch both test containers to the bare &lt;code&gt;specmatic test&lt;/code&gt; form, letting &lt;code&gt;systemUnderTest.service.definitions&lt;/code&gt; supply the spec and &lt;code&gt;runOptions.openapi.baseUrl&lt;/code&gt; supply the target. Since &lt;code&gt;systemUnderTest.service&lt;/code&gt; is singular, the User API needed its own &lt;code&gt;specmatic-user.yaml&lt;/code&gt; — bind-mounted over &lt;code&gt;/usr/src/app/specmatic.yaml&lt;/code&gt; for that one container.&lt;/p&gt;

&lt;p&gt;I validated both new config files against &lt;code&gt;specmatic-schema.json&lt;/code&gt; with &lt;code&gt;jsonschema.validate()&lt;/code&gt; before touching docker-compose — it confirmed the shape was structurally legal, but that turned out not to be the whole story.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gotcha 2: schema-valid isn't the same as working — &lt;code&gt;securitySchemes&lt;/code&gt; also needs a matching &lt;code&gt;id&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The Specmatic Enterprise license had lapsed while I was building this, so the first real Docker run only happened after it was renewed. The first attempt still failed: every positive scenario came back &lt;code&gt;401&lt;/code&gt;, including plain &lt;code&gt;GET /tasks&lt;/code&gt;. Grepping the test output for &lt;code&gt;Authorization&lt;/code&gt; showed why — Specmatic was sending fuzzed, auto-generated credentials (&lt;code&gt;Authorization: Bearer molestiae&lt;/code&gt;, &lt;code&gt;Authorization: Basic RUpTWE46TFVUSk4=&lt;/code&gt;) instead of the ones in &lt;code&gt;specmatic.yaml&lt;/code&gt;. My &lt;code&gt;runOptions.openapi.specs[]&lt;/code&gt; entry had no &lt;code&gt;id&lt;/code&gt;, and neither did the &lt;code&gt;definitions.specs&lt;/code&gt; entry it was supposed to override — with nothing to correlate them, Specmatic silently fell back to generated values instead of raising an error. The fix, once I looked back at how the reference lab does it, was to give both sides a matching &lt;code&gt;id&lt;/code&gt; (&lt;code&gt;taskApiSpec&lt;/code&gt; / &lt;code&gt;userApiSpec&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;systemUnderTest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;definitions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;definition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;specs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;taskApiSpec&lt;/span&gt;
                &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;task-api.yaml&lt;/span&gt;
    &lt;span class="na"&gt;runOptions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;openapi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;specs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;taskApiSpec&lt;/span&gt;
              &lt;span class="na"&gt;securitySchemes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;...&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That got every positive scenario back to &lt;code&gt;200&lt;/code&gt;/&lt;code&gt;201&lt;/code&gt;/&lt;code&gt;204&lt;/code&gt; — but the run still exited non-zero. Coverage had dropped to 58%, because the coverage report tracks every response code declared in an operation's &lt;code&gt;responses:&lt;/code&gt; map as its own gradable unit, and the &lt;code&gt;401&lt;/code&gt;/&lt;code&gt;403&lt;/code&gt; responses I'd added (schema-only, no examples) showed up as "not tested" — I'd assumed a contract-test run could only ever attach one configured credential per scheme, so those codes could never actually be triggered. First instinct was to just drop the declarations and lower &lt;code&gt;minCoveragePercentage&lt;/code&gt; to the resulting ceiling (58%/56%) — CI stayed green, but it was a workaround for a problem I hadn't actually diagnosed correctly.&lt;/p&gt;

&lt;p&gt;That assumption turned out to be wrong, and a founder's review of this submission is what surfaced it: the reference &lt;a href="//../api-security-schemes"&gt;&lt;code&gt;api-security-schemes&lt;/code&gt;&lt;/a&gt; lab's actual contract (pulled from &lt;code&gt;specmatic/labs-contracts&lt;/code&gt; at test time, so not visible in this repo's checkout of that lab) has an &lt;code&gt;auth_examples/&lt;/code&gt; directory full of dedicated external example files — &lt;code&gt;post-orders_application_json_401.json&lt;/code&gt; hardcodes a bad bearer token directly in the request; &lt;code&gt;post-orders-create-forbidden.json&lt;/code&gt; uses a &lt;code&gt;before&lt;/code&gt; fixture to log into Keycloak as a real, valid, wrong-role user. Each example carries its &lt;strong&gt;own&lt;/strong&gt; credential, completely independent of the one global credential configured for the positive-path scenarios — so each one executes for real, hits the server, and counts as tested coverage. The "only one credential per run" limitation was a limitation of how I'd wired the global &lt;code&gt;securitySchemes&lt;/code&gt; override, not of Specmatic itself.&lt;/p&gt;

&lt;p&gt;I added the equivalent here — &lt;code&gt;specs/openapi/examples/task-api/&lt;/code&gt; and &lt;code&gt;specs/openapi/examples/user-api/&lt;/code&gt;, wired in via &lt;code&gt;data.examples.directories&lt;/code&gt; — the same mechanism already used for the async Kafka &lt;code&gt;before&lt;/code&gt;-hooks. For &lt;code&gt;basicAuth&lt;/code&gt;/&lt;code&gt;apiKeyAuth&lt;/code&gt; this was simple: one gotcha surfaced — simply omitting the auth header in an example doesn't produce &lt;code&gt;401&lt;/code&gt;, Specmatic still auto-attaches the globally configured &lt;em&gt;valid&lt;/em&gt; credential to fill the gap, so the fix is to set the header to some other explicit (invalid) value, which takes precedence. The &lt;code&gt;bearerAuth&lt;/code&gt;/OAuth2 case needed a much bigger, separate fix — see Gotcha 3. With everything in place, both specs measure genuine &lt;strong&gt;100% coverage with every test passing&lt;/strong&gt; — no lowered ceiling required. Both bugs (the missing &lt;code&gt;id&lt;/code&gt; and the coverage cost) only showed up once I actually ran the containers — &lt;code&gt;jsonschema.validate()&lt;/code&gt; against the config schema and &lt;code&gt;openapi-spec-validator&lt;/code&gt; against the specs both passed the whole time, because the files were legal, just not correctly &lt;em&gt;wired&lt;/em&gt; or &lt;em&gt;exercised&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gotcha 3: migrating to real Keycloak OAuth2 — three failed assumptions before the actual fix
&lt;/h3&gt;

&lt;p&gt;Testing 401/403 "for real" worked cleanly for the static schemes, but &lt;code&gt;bearerAuth&lt;/code&gt; was itself just a static pre-shared string checked against a dictionary — not meaningfully different from the API key, and not actually testing OAuth2. Migrating it to a real Keycloak-backed &lt;code&gt;oAuth2AuthCode&lt;/code&gt; scheme (a new &lt;code&gt;keycloak&lt;/code&gt; service in &lt;code&gt;docker-compose.yaml&lt;/code&gt;, a realm export at &lt;code&gt;keycloak/taskflow-realm.json&lt;/code&gt;) surfaced a chain of problems, each only visible by running the stack:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No curl in the Keycloak image, and the health endpoint isn't on the port you'd guess.&lt;/strong&gt; &lt;code&gt;quay.io/keycloak/keycloak&lt;/code&gt; ships no &lt;code&gt;curl&lt;/code&gt;/&lt;code&gt;wget&lt;/code&gt;. Keycloak 26 also serves &lt;code&gt;/health/ready&lt;/code&gt; on the &lt;em&gt;management&lt;/em&gt; port (9000), not the main port (8080) I first pointed the healthcheck at. Fixed with a &lt;code&gt;CMD-SHELL&lt;/code&gt; healthcheck using bash's built-in &lt;code&gt;/dev/tcp&lt;/code&gt; to speak raw HTTP directly — no external tool needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Account is not fully set up" on login, and it's not about required actions.&lt;/strong&gt; Every imported user had &lt;code&gt;requiredActions: []&lt;/code&gt; explicitly — confirmed via Keycloak's own admin REST API, not just the realm file. The real cause: Keycloak 26's declarative User Profile marks &lt;code&gt;firstName&lt;/code&gt;/&lt;code&gt;lastName&lt;/code&gt; required by default, and &lt;code&gt;VERIFY_PROFILE&lt;/code&gt; gets computed dynamically at login for any user missing them, regardless of what's in &lt;code&gt;requiredActions&lt;/code&gt;. Fixed by adding &lt;code&gt;firstName&lt;/code&gt;/&lt;code&gt;lastName&lt;/code&gt; to every user in the realm export.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The same token has a different &lt;code&gt;iss&lt;/code&gt; claim depending on how you asked for it.&lt;/strong&gt; Keycloak derives the issuer from the request's own Host header, so a token fetched from the browser (&lt;code&gt;localhost:8083&lt;/code&gt;) and one fetched from inside Docker (&lt;code&gt;keycloak:8080&lt;/code&gt;) get different &lt;code&gt;iss&lt;/code&gt; values for the same realm. Pinning it via &lt;code&gt;KC_HOSTNAME&lt;/code&gt;/&lt;code&gt;KC_HOSTNAME_PORT&lt;/code&gt; didn't produce the value I expected either. Since JWKS signature verification already proves a token came from this exact Keycloak instance's private key, I disabled issuer and audience verification in Flask (&lt;code&gt;verify_iss: False&lt;/code&gt;, &lt;code&gt;verify_aud: False&lt;/code&gt;) instead of chasing a fragile pin — a deliberate, documented simplification for a single-realm demo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There's no way to forward a captured value into a later request within a Specmatic example — at least not in this version.&lt;/strong&gt; The natural design: a &lt;code&gt;before&lt;/code&gt; fixture logs into Keycloak, captures &lt;code&gt;"(ACCESS_TOKEN:string)"&lt;/code&gt;, the next request references &lt;code&gt;$(ACCESS_TOKEN)&lt;/code&gt;. I tried the reference lab's exact pattern (no explicit header, relying on undocumented auto-binding) — checked byte-for-byte, their files never reference the captured variable anywhere — and it just fell back to the global credential. I then tried the documented capture/reference syntax explicitly: the literal string &lt;code&gt;$(ACCESS_TOKEN)&lt;/code&gt; was sent verbatim, completely unsubstituted. Tested in both the &lt;code&gt;before&lt;/code&gt;+&lt;code&gt;partial&lt;/code&gt; REST format and the sequential &lt;code&gt;before&lt;/code&gt; array the async hooks use — identical result both times, confirmed with real Docker output, not assumed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The actual fix lives in the test container's entrypoint, not in the example files.&lt;/strong&gt; &lt;code&gt;test-task-api&lt;/code&gt; in &lt;code&gt;docker-compose.yaml&lt;/code&gt; now copies the (read-only-mounted) repo into a scratch directory, fetches real tokens for alice (developer) and bob (qa) straight from Keycloak, and &lt;code&gt;sed&lt;/code&gt;-substitutes them into the two examples and two async hooks that need a &lt;em&gt;specific&lt;/em&gt; role. Everything else that just needs &lt;em&gt;some&lt;/em&gt; valid allowed-role credential has no explicit &lt;code&gt;Authorization&lt;/code&gt; header at all and picks up the freshly-fetched developer token from the global config automatically — same mechanism as &lt;code&gt;basicAuth&lt;/code&gt;/&lt;code&gt;apiKeyAuth&lt;/code&gt;. One last &lt;code&gt;cp&lt;/code&gt; gotcha: Docker auto-creates &lt;code&gt;working_dir&lt;/code&gt; as an empty directory before the entrypoint runs, so &lt;code&gt;cp -r /src /tmp/app&lt;/code&gt; copies &lt;code&gt;/src&lt;/code&gt; as a &lt;em&gt;subdirectory&lt;/em&gt; of the already-existing &lt;code&gt;/tmp/app&lt;/code&gt; rather than populating it — &lt;code&gt;cp -r /src/. /tmp/app/&lt;/code&gt; was the fix.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gotcha 4: the token fetch itself was racing Keycloak's realm import
&lt;/h3&gt;

&lt;p&gt;Even after Gotcha 3 was fixed, the suite was still sporadically flaky — not every run, just occasionally a &lt;code&gt;401&lt;/code&gt; on an async before-fixture or on the &lt;code&gt;403&lt;/code&gt; QA example, with no code change in between runs to explain it.&lt;/p&gt;

&lt;p&gt;The cause was a race, not an auth bug: &lt;code&gt;test-task-api&lt;/code&gt; depends on Keycloak via &lt;code&gt;service_healthy&lt;/code&gt;, but that healthcheck only proves &lt;code&gt;/health/ready&lt;/code&gt; is up — it says nothing about whether &lt;code&gt;--import-realm&lt;/code&gt; has actually finished registering the &lt;code&gt;task-api&lt;/code&gt; client and its users yet. The very first password-grant request could land in that gap and get an error response back, and because the entrypoint script didn't have &lt;code&gt;set -o pipefail&lt;/code&gt;, a failed &lt;code&gt;curl&lt;/code&gt; piped into &lt;code&gt;jq&lt;/code&gt; doesn't fail the pipeline — it just produces an empty string, which then got &lt;code&gt;sed&lt;/code&gt;-substituted into the example files as if it were a real token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Before — one-shot, no protection against losing the realm-import race&lt;/span&gt;
&lt;span class="nv"&gt;DEV_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-sf&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEYCLOAK_BASE_URL&lt;/span&gt;&lt;span class="s2"&gt;/realms/taskflow/protocol/openid-connect/token"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"grant_type=password&amp;amp;client_id=task-api&amp;amp;username=&lt;/span&gt;&lt;span class="nv"&gt;$KEYCLOAK_DEV_USERNAME&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;password=&lt;/span&gt;&lt;span class="nv"&gt;$KEYCLOAK_DEV_PASSWORD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; .access_token&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;QA_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-sf&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEYCLOAK_BASE_URL&lt;/span&gt;&lt;span class="s2"&gt;/realms/taskflow/protocol/openid-connect/token"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"grant_type=password&amp;amp;client_id=task-api&amp;amp;username=&lt;/span&gt;&lt;span class="nv"&gt;$KEYCLOAK_QA_USERNAME&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;password=&lt;/span&gt;&lt;span class="nv"&gt;$KEYCLOAK_QA_PASSWORD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; .access_token&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# After — pipefail on, retry until a real JWT comes back or fail loudly&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; pipefail

fetch_token&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;username&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nv"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; attempt token
  &lt;span class="k"&gt;for &lt;/span&gt;attempt &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 15&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nv"&gt;token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-sf&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEYCLOAK_BASE_URL&lt;/span&gt;&lt;span class="s2"&gt;/realms/taskflow/protocol/openid-connect/token"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"grant_type=password&amp;amp;client_id=task-api&amp;amp;username=&lt;/span&gt;&lt;span class="nv"&gt;$username&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;password=&lt;/span&gt;&lt;span class="nv"&gt;$password&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.access_token // empty'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="k"&gt;return &lt;/span&gt;0
    &lt;span class="k"&gt;fi
    &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;2
  &lt;span class="k"&gt;done
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ERROR: failed to fetch Keycloak token for &lt;/span&gt;&lt;span class="nv"&gt;$username&lt;/span&gt;&lt;span class="s2"&gt; after 15 attempts"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="k"&gt;return &lt;/span&gt;1
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;DEV_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;fetch_token &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEYCLOAK_DEV_USERNAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEYCLOAK_DEV_PASSWORD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;QA_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;fetch_token &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEYCLOAK_QA_USERNAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEYCLOAK_QA_PASSWORD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;jq -r '.access_token // empty'&lt;/code&gt; turns a missing field into an explicit empty string rather than the literal text &lt;code&gt;null&lt;/code&gt;, so the &lt;code&gt;-n "$token"&lt;/code&gt; check works correctly. The retry loop gives realm import up to 30 seconds to finish before giving up, and &lt;code&gt;pipefail&lt;/code&gt; means any other kind of curl failure surfaces as a real script error instead of quietly propagating an empty token downstream. Same class of bug as the swallowed Kafka exception back in Step 5 — a failure that looked like test flakiness but was actually a docker-compose dependency graph that didn't model the thing it needed to wait for.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Specmatic tests here — authentication and authorization, both for real
&lt;/h3&gt;

&lt;p&gt;Running the suite with the securitySchemes correctly wired re-validates every existing example and schema-resiliency scenario against an authenticated backend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;basicAuth&lt;/code&gt; / &lt;code&gt;apiKeyAuth&lt;/code&gt;&lt;/strong&gt; — a static admin (&lt;code&gt;charlie&lt;/code&gt;) credential&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;oAuth2AuthCode&lt;/code&gt;&lt;/strong&gt; — a real token fetched from Keycloak for &lt;code&gt;alice&lt;/code&gt; (developer) at the start of every run&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Verified live:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Suite&lt;/th&gt;
&lt;th&gt;Scenarios&lt;/th&gt;
&lt;th&gt;Coverage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test-task-api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;143/143 REST + 2/2 async Kafka (same run)&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test-user-api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;53/53&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Break the credential (wrong Keycloak password, or a valid-but-wrong-role account) and every scenario relying on that global credential fails — the same signal the reference lab demonstrates.&lt;/p&gt;

&lt;p&gt;Authorization is tested the same way, not just declared:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The dedicated external examples assert &lt;code&gt;bob&lt;/code&gt; (qa) gets &lt;code&gt;403&lt;/code&gt; on &lt;code&gt;POST /tasks&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The positive-path examples assert &lt;code&gt;alice&lt;/code&gt; (developer) gets &lt;code&gt;201&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Both happen in the &lt;em&gt;same&lt;/em&gt; single contract-test run, because each example brings its own credential rather than relying on the one global value&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OpenAPI's &lt;code&gt;security&lt;/code&gt; keyword only models &lt;em&gt;authentication&lt;/em&gt; (who you are) on its own; the per-role authorization rule ("qa can authenticate but can't create tasks") is real, enforced application logic in the Flask services. I verified it twice — once as a counted Specmatic scenario, and once live via curl:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;QA_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8083/realms/taskflow/protocol/openid-connect/token &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"grant_type=password&amp;amp;client_id=task-api&amp;amp;username=bob&amp;amp;password=password234"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; .access_token&lt;span class="si"&gt;)&lt;/span&gt;
curl &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8080/tasks &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$QA_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"title":"x","priority":"low"}'&lt;/span&gt;
&lt;span class="c"&gt;# 403 Forbidden — qa role not permitted&lt;/span&gt;

&lt;span class="nv"&gt;DEV_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8083/realms/taskflow/protocol/openid-connect/token &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"grant_type=password&amp;amp;client_id=task-api&amp;amp;username=alice&amp;amp;password=password123"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; .access_token&lt;span class="si"&gt;)&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8080/tasks &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$DEV_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"title":"x","priority":"low"}'&lt;/span&gt;
&lt;span class="c"&gt;# 201 Created — developer role permitted&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I verified the full matrix (401/403/200/201/204 across every operation and every role) by running both Flask services directly and driving them with &lt;code&gt;curl&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two smaller downstream fixes came out of turning this on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The async &lt;code&gt;before&lt;/code&gt;-hooks (&lt;code&gt;specs/asyncapi/examples/*.json&lt;/code&gt;) call &lt;code&gt;POST /tasks&lt;/code&gt; and &lt;code&gt;PUT /tasks/2&lt;/code&gt; directly — they now carry a placeholder &lt;code&gt;Authorization&lt;/code&gt; header that the test entrypoint substitutes with a real fetched token, or &lt;code&gt;test-task-api&lt;/code&gt; (which runs the async contract test alongside REST, see the update in Step 5) would start failing its setup calls with &lt;code&gt;401&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The frontend's "Real Service" target hits &lt;code&gt;task-service:8080&lt;/code&gt; directly from the browser, so it now fetches its own token from Keycloak on first use (password grant, cached) — clearly commented as a lab/demo shortcut, not a real login flow.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The AI-Native Angle: Building Feedback Loops for Coding Agents
&lt;/h2&gt;

&lt;p&gt;Here's why this architecture matters beyond traditional development.&lt;/p&gt;

&lt;p&gt;When you ask an AI coding agent to implement a service, the typical workflow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Give the agent a task description&lt;/li&gt;
&lt;li&gt;Agent generates code&lt;/li&gt;
&lt;li&gt;Human manually checks if it's right&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With contract-driven development, the workflow becomes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write (or generate) the OpenAPI spec&lt;/li&gt;
&lt;li&gt;Give the agent the spec + the failing Specmatic output&lt;/li&gt;
&lt;li&gt;Agent generates code&lt;/li&gt;
&lt;li&gt;Specmatic runs and produces a precise diff&lt;/li&gt;
&lt;li&gt;Feed the diff back to the agent&lt;/li&gt;
&lt;li&gt;Agent iterates until all tests pass&lt;/li&gt;
&lt;li&gt;No human verification needed at the integration layer&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The contract becomes the &lt;strong&gt;objective ground truth&lt;/strong&gt; that the AI optimizes against. It's not "does this code look right" — it's "does this code satisfy these concrete, machine-verifiable assertions?"&lt;/p&gt;

&lt;p&gt;This is what "AI-native infrastructure" actually means: not using AI to write code, but building the verification substrate that lets AI code generation be &lt;em&gt;reliable at scale&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Full System Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────┐
│                     Docker Compose                       │
│                                                          │
│  ┌──────────────┐      ┌──────────────┐                 │
│  │ task-service │      │ user-service │                 │
│  │  Flask :8080 │      │  Flask :8081 │                 │
│  └──────┬───────┘      └──────────────┘                 │
│         │ publishes events                               │
│  ┌──────▼───────┐                                       │
│  │    Kafka     │                                       │
│  │    :9092     │                                       │
│  └──────────────┘                                       │
│                                                          │
│  ┌────────────────────────────────────────────────────┐ │
│  │  test-task-api                                      │ │
│  │  Specmatic REST + AsyncAPI                          │ │
│  │  (OpenAPI 3.0.1 + Kafka topic tests, one run)        │ │
│  └────────────────────────────────────────────────────┘ │
│                                                          │
│  ┌──────────────┐   ┌─────────────────────────────────┐ │
│  │  frontend    │   │  mock-task-api                  │ │
│  │  Kanban:3000 │   │  Specmatic Mock Server :9100    │ │
│  └──────────────┘   └─────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Contract Coverage at a Glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Spec File&lt;/th&gt;
&lt;th&gt;Endpoints / Channels&lt;/th&gt;
&lt;th&gt;Named Examples&lt;/th&gt;
&lt;th&gt;External 401/403 Examples&lt;/th&gt;
&lt;th&gt;Tests with &lt;code&gt;schemaResiliencyTests: all&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;specs/openapi/task-api.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5 REST endpoints&lt;/td&gt;
&lt;td&gt;12 (200, 201, 204, 400, 404 scenarios)&lt;/td&gt;
&lt;td&gt;8 (&lt;code&gt;specs/openapi/examples/task-api/&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;143&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;specs/openapi/user-api.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3 REST endpoints&lt;/td&gt;
&lt;td&gt;5 (201, 400, 200, 200, 404)&lt;/td&gt;
&lt;td&gt;4 (&lt;code&gt;specs/openapi/examples/user-api/&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;53&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;specs/asyncapi/task-events.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2 Kafka channels&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;task-created&lt;/code&gt;, &lt;code&gt;task-updated&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Real Errors Specmatic Surfaced — and How I Fixed Them
&lt;/h2&gt;

&lt;p&gt;To demonstrate Specmatic's feedback loop concretely, I deliberately introduced a field rename in &lt;code&gt;task-service/main.py&lt;/code&gt; — the kind of silent drift that kills multi-service systems in production — and ran the full contract test suite to capture what Specmatic actually reports.&lt;/p&gt;

&lt;h3&gt;
  
  
  The break: &lt;code&gt;title&lt;/code&gt; → &lt;code&gt;task_title&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;A single-line change in the &lt;code&gt;create_task&lt;/code&gt; handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Before (matches the spec)
&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;_next_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# After (drifted from the spec)
&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;_next_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task_title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;   &lt;span class="c1"&gt;# ← renamed
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What Specmatic reported
&lt;/h3&gt;

&lt;p&gt;Running &lt;code&gt;docker compose up --exit-code-from test-task-api&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure 1 — POST /tasks → 500 instead of 201&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The service crashed immediately. The Kafka publish code still referenced &lt;code&gt;task["title"]&lt;/code&gt;, which no longer existed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;taskflow-task-service | ERROR in app: Exception on /tasks [POST]
taskflow-task-service |   File "/app/main.py", line 106, in create_task
taskflow-task-service |     "title": task["title"],
taskflow-task-service |              ~~~~^^^^^^^^^
taskflow-task-service | KeyError: 'title'
taskflow-task-service | 172.19.0.2 - - [POST /tasks] 500

&lt;/span&gt;&lt;span class="gp"&gt;taskflow-test-task-api | +ve  Scenario: POST /tasks -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;201 ... has FAILED
&lt;span class="go"&gt;taskflow-test-task-api | Reason:
&lt;/span&gt;&lt;span class="gp"&gt;taskflow-test-task-api |   &amp;gt;&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; RESPONSE.STATUS
&lt;span class="go"&gt;taskflow-test-task-api |       R0002: HTTP status mismatch
taskflow-test-task-api |       Specification expected status 201 but response contained status 500
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All 6 schema resiliency variants of &lt;code&gt;POST /tasks → 201&lt;/code&gt; failed the same way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure 2 — GET /tasks → schema violation (cascade)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because the task was inserted into the in-memory store &lt;em&gt;before&lt;/em&gt; the Kafka publish crashed, the malformed object (with &lt;code&gt;task_title&lt;/code&gt; instead of &lt;code&gt;title&lt;/code&gt;) persisted. When &lt;code&gt;GET /tasks&lt;/code&gt; ran next, the list response contained those corrupt items — and Specmatic caught it at the schema layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;taskflow-test-task-api | +ve  Scenario: GET /tasks -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;200 ... has FAILED
&lt;span class="go"&gt;taskflow-test-task-api | Reason:
&lt;/span&gt;&lt;span class="gp"&gt;taskflow-test-task-api |   &amp;gt;&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; RESPONSE.BODY[3].title
&lt;span class="go"&gt;taskflow-test-task-api |       R2001: Missing required property
taskflow-test-task-api |       Specification expected mandatory property "title" to be present
taskflow-test-task-api |       but was missing from the response
taskflow-test-task-api |
&lt;/span&gt;&lt;span class="gp"&gt;taskflow-test-task-api |   &amp;gt;&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; RESPONSE.BODY[3].task_title
&lt;span class="go"&gt;taskflow-test-task-api |       R2003: Unknown property
taskflow-test-task-api |       Property "task_title" in the response was not in the specification
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Final result:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Tests run: 27, Successes: 19, Failures: 8, WIP: 0, Errors: 0
89% API Coverage — FAILED (minCoveragePercentage = 100%)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One field rename caused &lt;strong&gt;8 failures across two endpoints&lt;/strong&gt; — 6 from POST crashing and 2 from GET returning corrupt data — before a single human reviewer saw anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;Two things needed to be consistent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 1. Keep the task dict key matching the spec field name
&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;_next_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;   &lt;span class="c1"&gt;# ← back to "title"
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# 2. The Kafka publish code already referenced task["title"] correctly
#    — so reverting the dict key was the only change needed
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After reverting: &lt;strong&gt;27/27 tests passing, 100% coverage.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What this demonstrates
&lt;/h3&gt;

&lt;p&gt;The contract test caught the break at the &lt;em&gt;exact&lt;/em&gt; moment it was introduced — no integration environment, no manual testing, no waiting for another team to hit the issue. The error output told me precisely which field was wrong (&lt;code&gt;RESPONSE.BODY[3].title&lt;/code&gt;), which rule was violated (&lt;code&gt;R2001&lt;/code&gt;, &lt;code&gt;R2003&lt;/code&gt;), and what the spec expected versus what the service returned.&lt;/p&gt;

&lt;p&gt;This is the feedback loop that makes contract-first development reliable at scale.&lt;/p&gt;




&lt;h2&gt;
  
  
  Challenges and What I Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Example naming symmetry is non-negotiable
&lt;/h3&gt;

&lt;p&gt;Specmatic matches examples across request/response pairs by name. If your parameter example is named &lt;code&gt;GET_TASK_200&lt;/code&gt; but your response example is &lt;code&gt;GET_TASK_SUCCESS&lt;/code&gt;, Specmatic won't pair them into a test. I standardized on a &lt;code&gt;VERB_RESOURCE_STATUSCODE&lt;/code&gt; convention and never had pairing issues after that.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Healthcheck timing in Docker Compose
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;service_completed_successfully&lt;/code&gt; dependency type waits for a container to exit — it doesn't mean the service inside is &lt;em&gt;ready&lt;/em&gt;. I added explicit healthchecks to every service and used &lt;code&gt;service_healthy&lt;/code&gt; dependencies for the test containers, eliminating "connection refused" flakiness:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;import&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;urllib.request;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;urllib.request.urlopen('http://localhost:8080/health')"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2s&lt;/span&gt;
  &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
  &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
  &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Kafka publish blocking service startup
&lt;/h3&gt;

&lt;p&gt;The first implementation blocked Flask startup while connecting to Kafka. If Kafka wasn't ready, the entire service failed. I wrapped the Kafka producer and every publish call in try/except:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;kafka&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;KafkaProducer&lt;/span&gt;
        &lt;span class="n"&gt;producer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KafkaProducer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;bootstrap_servers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;KAFKA_BOOTSTRAP_SERVER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;value_serializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;producer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;producer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Kafka publish skipped (%s): %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service stays healthy and responds to REST calls even if Kafka is temporarily unavailable.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. AsyncAPI server host resolution
&lt;/h3&gt;

&lt;p&gt;The AsyncAPI spec's &lt;code&gt;servers[].host&lt;/code&gt; must match the hostname that the Specmatic container can resolve. Inside Docker Compose, that's the service name (&lt;code&gt;kafka&lt;/code&gt;), not &lt;code&gt;localhost&lt;/code&gt;. Always check your spec's server host against your runtime network topology.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Specmatic Enterprise requires &lt;code&gt;/actuator/health&lt;/code&gt; to execute tests
&lt;/h3&gt;

&lt;p&gt;This one cost me time. Running &lt;code&gt;docker compose up test-user-api&lt;/code&gt; produced an HTML report showing &lt;strong&gt;0% coverage, 4 tests skipped, "Actuator Not Available"&lt;/strong&gt; — even though the Flask service was fully up and responding correctly.&lt;/p&gt;

&lt;p&gt;Specmatic Enterprise checks for a &lt;code&gt;GET /actuator/health&lt;/code&gt; endpoint before executing any tests. Without it, every scenario is marked "Skipped" regardless of service health. The fix is a single route on each Flask service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/actuator/health&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;actuator_health&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UP&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is separate from the application's own &lt;code&gt;/health&lt;/code&gt; endpoint. Once added, all 4 tests ran and coverage showed correctly.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Schema resiliency + stateful in-memory store = test ordering trap
&lt;/h3&gt;

&lt;p&gt;Enabling &lt;code&gt;schemaResiliencyTests: positiveOnly&lt;/code&gt; expanded the task-api suite from ~10 to 27 tests. More tests exposed a subtle ordering bug I hadn't hit before.&lt;/p&gt;

&lt;p&gt;Specmatic runs operations on the same path alphabetically by HTTP method: &lt;strong&gt;DELETE → GET → PUT&lt;/strong&gt;. My spec used &lt;code&gt;taskId=1&lt;/code&gt; for all three success scenarios. When DELETE ran first and removed task 1, the subsequent GET and all 11 PUT schema resiliency variations against &lt;code&gt;taskId=1&lt;/code&gt; returned 404 instead of 200 — 13 cascading failures from a single deletion.&lt;/p&gt;

&lt;p&gt;The fix: assign each mutating operation its own dedicated seed task.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{...},&lt;/span&gt;   &lt;span class="c1"&gt;# GET /tasks/1  — read-only, never touched by other operations
&lt;/span&gt;    &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{...},&lt;/span&gt;   &lt;span class="c1"&gt;# PUT /tasks/2  — mutated but never deleted
&lt;/span&gt;    &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{...},&lt;/span&gt;   &lt;span class="c1"&gt;# DELETE /tasks/3 — disposable
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;_next_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;  &lt;span class="c1"&gt;# POST schema resiliency tests create IDs 100+ — no collision with seeds
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in the spec, update the examples to match:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;GET_TASK_200:   value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;   &lt;span class="c1"&gt;# task 1&lt;/span&gt;
&lt;span class="na"&gt;UPDATE_TASK_200: value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;  &lt;span class="c1"&gt;# task 2&lt;/span&gt;
&lt;span class="na"&gt;DELETE_TASK_204: value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;  &lt;span class="c1"&gt;# task 3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each operation now has its own data slice. Tests pass regardless of execution order.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. &lt;code&gt;schemaResiliencyTests: all&lt;/code&gt; requires the spec and the service to grow together
&lt;/h3&gt;

&lt;p&gt;Switching from &lt;code&gt;positiveOnly&lt;/code&gt; to &lt;code&gt;all&lt;/code&gt; wasn't just a config flag — every negative test that found a missing &lt;code&gt;400&lt;/code&gt; response meant the &lt;em&gt;spec&lt;/em&gt; was incomplete, not just the code. I'd documented &lt;code&gt;400&lt;/code&gt; for &lt;code&gt;POST /tasks&lt;/code&gt; and &lt;code&gt;POST /users&lt;/code&gt; from day one (because "what if a required field is missing" is an obvious case to write an example for), but &lt;code&gt;GET /tasks?status=&lt;/code&gt; and &lt;code&gt;PUT /tasks/{taskId}&lt;/code&gt; had no documented error path for invalid input, because nobody had written a negative example for them. Negative resiliency testing finds exactly the blind spots that example-driven testing structurally can't — the requests nobody thought to write down. The fix pattern is always the same: add the &lt;code&gt;4xx&lt;/code&gt; response to the spec, add a validation check to the service that returns it, in that order.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. &lt;code&gt;securitySchemes&lt;/code&gt; needs &lt;code&gt;systemUnderTest&lt;/code&gt;, not ad hoc CLI args — and authorization isn't authentication
&lt;/h3&gt;

&lt;p&gt;Covered in full in Step 8, but the short version: Specmatic only reads &lt;code&gt;runOptions.openapi.specs[].spec.securitySchemes&lt;/code&gt; when it resolves a spec through &lt;code&gt;systemUnderTest.service&lt;/code&gt; (not a bare &lt;code&gt;specmatic test &amp;lt;file&amp;gt; --testBaseURL=...&lt;/code&gt; invocation), and even then it only applies them once the &lt;code&gt;definitions.specs&lt;/code&gt; entry and the &lt;code&gt;runOptions&lt;/code&gt; override share an explicit matching &lt;code&gt;id&lt;/code&gt; — omit it and Specmatic silently falls back to fuzzed, auto-generated credentials with no error. Declaring &lt;code&gt;401&lt;/code&gt;/&lt;code&gt;403&lt;/code&gt; as formal &lt;code&gt;responses&lt;/code&gt; entries only costs coverage if nothing ever triggers them — my first pass relied solely on the one global credential, so they sat permanently "not tested." Dedicated external examples (&lt;code&gt;specs/openapi/examples/&lt;/code&gt;), each carrying its own hardcoded or wrong-role credential independent of the global config, fix that: they execute for real and get counted, taking both specs to genuine 100% coverage. That same mechanism also closes the authorization gap — OpenAPI's &lt;code&gt;security&lt;/code&gt; keyword only models authentication on its own, but an example with &lt;code&gt;bob&lt;/code&gt;'s real qa-role token asserting &lt;code&gt;403&lt;/code&gt; tests per-role authorization as a real, counted scenario too, not just something verified separately via curl. All of this — the &lt;code&gt;id&lt;/code&gt; requirement, the coverage cost, and the fix — only became fully visible once the Enterprise license was renewed and the suite actually ran in Docker; schema/spec validation alone had passed the whole time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Running It Yourself
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/kanugurajesh/Spectamic-Taskflow
&lt;span class="nb"&gt;cd &lt;/span&gt;Spectamic-Taskflow

&lt;span class="c"&gt;# Copy your Specmatic Enterprise license into the project root first:&lt;/span&gt;
&lt;span class="c"&gt;# cp /path/to/your/license.txt ./license.txt&lt;/span&gt;

&lt;span class="c"&gt;# Full stack + REST contract tests (use --build on first run)&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;--build&lt;/span&gt;

&lt;span class="c"&gt;# Run only the contract tests (REST + async Kafka events, one run)&lt;/span&gt;
docker compose up test-task-api test-user-api &lt;span class="nt"&gt;--build&lt;/span&gt; &lt;span class="nt"&gt;--abort-on-container-exit&lt;/span&gt;

&lt;span class="c"&gt;# Frontend against mock server (no backend needed)&lt;/span&gt;
docker compose &lt;span class="nt"&gt;--profile&lt;/span&gt; mock up mock-task-api frontend

&lt;span class="c"&gt;# Open Specmatic Studio (visual contract editor)&lt;/span&gt;
docker compose &lt;span class="nt"&gt;--profile&lt;/span&gt; studio up studio
&lt;span class="c"&gt;# Visit http://localhost:9000&lt;/span&gt;

&lt;span class="c"&gt;# CI mode: fail fast on contract violations&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;--exit-code-from&lt;/span&gt; test-task-api

&lt;span class="c"&gt;# Tear down&lt;/span&gt;
docker compose down &lt;span class="nt"&gt;--remove-orphans&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test reports are generated at &lt;code&gt;build/reports/specmatic/test/html/index.html&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Always pass &lt;code&gt;--build&lt;/code&gt; after changing any service code (&lt;code&gt;services/*/main.py&lt;/code&gt;). Spec file changes (&lt;code&gt;.yaml&lt;/code&gt;) are picked up immediately via volume mount — no rebuild needed for those.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The most important thing I learned building specmatic-taskflow: &lt;strong&gt;a contract that isn't executable isn't a contract, it's a suggestion.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;OpenAPI specs are everywhere. AsyncAPI specs are increasingly common. But most of them live as YAML files in a &lt;code&gt;/docs&lt;/code&gt; folder that nobody updates after the initial commit. They become lies — specifications that describe a system as it was, not as it is.&lt;/p&gt;

&lt;p&gt;Specmatic changes the relationship. When your spec &lt;em&gt;runs&lt;/em&gt; — when it becomes the test, the mock, the governance gate — it has to stay true. Every CI run verifies it. Every developer who breaks it sees it in the build log.&lt;/p&gt;

&lt;p&gt;For AI coding agents, this matters even more. An agent that generates code against a contract and gets precise, executable feedback can iterate reliably. An agent working against documentation can only guess.&lt;/p&gt;

&lt;p&gt;I built specmatic-taskflow to prove that a single engineer can, in a weekend, wire together a full contract-driven system across REST and async messaging — and end up with more confidence in the integration than most teams get after months of manual testing.&lt;/p&gt;

&lt;p&gt;That's the infrastructure layer AI-native software development needs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Post Notes
&lt;/h2&gt;

&lt;p&gt;This post originally walked through &lt;code&gt;specmatic-async.yaml&lt;/code&gt; and a standalone &lt;code&gt;test-async&lt;/code&gt; container as part of the async testing setup, then explained how they were later merged into &lt;code&gt;specmatic.yaml&lt;/code&gt; and &lt;code&gt;test-task-api&lt;/code&gt;. Those two were removed from the writeup — the post now describes the async setup as it exists today, without the intermediate history. The reasoning: a blog post like this gets used as a reference, and readers following along shouldn't hit instructions for files or containers that no longer exist in the repo.&lt;/p&gt;




&lt;h2&gt;
  
  
  About the Author
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Kanugurajesh&lt;/strong&gt; — Final-year B.E. CS student at MVSR Engineering College. 4100+ GitHub contributions, hackathon winner, 2 years of internship experience at AI and SaaS companies. Building at the intersection of AI agents and reliable software systems.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/kanugurajesh" rel="noopener noreferrer"&gt;github.com/kanugurajesh&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Email: &lt;a href="mailto:kanugurajesh3@gmail.com"&gt;kanugurajesh3@gmail.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>microservices</category>
      <category>testing</category>
    </item>
    <item>
      <title>🚀 AI-Powered README Generator – Automatically Write Beautiful READMEs with Gemini + LangChain</title>
      <dc:creator>kanugu rajesh</dc:creator>
      <pubDate>Mon, 21 Jul 2025 08:50:16 +0000</pubDate>
      <link>https://dev.to/kanugurajesh/ai-powered-readme-generator-automatically-write-beautiful-readmes-with-gemini-langchain-36kl</link>
      <guid>https://dev.to/kanugurajesh/ai-powered-readme-generator-automatically-write-beautiful-readmes-with-gemini-langchain-36kl</guid>
      <description>&lt;p&gt;Creating good documentation is hard — but what if your &lt;code&gt;README.md&lt;/code&gt; wrote itself?&lt;/p&gt;

&lt;p&gt;Introducing the &lt;strong&gt;AI-Powered README Generator&lt;/strong&gt;, a command-line tool that uses &lt;strong&gt;Google's Generative AI (Gemini)&lt;/strong&gt; and &lt;strong&gt;LangChain&lt;/strong&gt; to generate detailed and structured &lt;code&gt;README.md&lt;/code&gt; files for public GitHub repositories.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📖 Say goodbye to boring READMEs. Let AI do the heavy lifting.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🧠 What It Does
&lt;/h2&gt;

&lt;p&gt;Given a GitHub repository URL, this tool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Connects to the GitHub API to fetch repository contents&lt;/li&gt;
&lt;li&gt;Understands the file structure and purpose&lt;/li&gt;
&lt;li&gt;Uses a Large Language Model (via LangChain + Gemini)&lt;/li&gt;
&lt;li&gt;Generates a &lt;strong&gt;professional-quality &lt;code&gt;README.md&lt;/code&gt; file&lt;/strong&gt; — automatically!&lt;/li&gt;
&lt;li&gt;Updates/Creates the &lt;code&gt;README.md&lt;/code&gt; file in the github repo if you own it&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔧 Why Use It?
&lt;/h2&gt;

&lt;p&gt;This tool is perfect for:&lt;/p&gt;

&lt;p&gt;✅ Instantly generating initial READMEs for new projects&lt;br&gt;&lt;br&gt;
✅ Refreshing stale documentation with up-to-date content&lt;br&gt;&lt;br&gt;
✅ Learning how to integrate LLMs with external APIs in a real-world scenario&lt;/p&gt;


&lt;h2&gt;
  
  
  📦 Installation
&lt;/h2&gt;

&lt;p&gt;Here's how you can get it up and running in 5 steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Clone the repo&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   git clone https://github.com/kanugurajesh/DocMind
   &lt;span class="nb"&gt;cd &lt;/span&gt;DocMind
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;Set up a virtual environment
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   python &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv
   &lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate  &lt;span class="c"&gt;# Windows: venv\Scripts\activate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;Install dependencies
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;Setup .env file&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  ⚙️ How to Use
&lt;/h2&gt;

&lt;p&gt;Just run the main.py script with the GitHub repository URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py https://github.com/octocat/Spoon-Knife
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🎉 It will generate a README_generated.md file in your current directory with the AI-crafted content.&lt;/p&gt;

&lt;h2&gt;
  
  
  ✨ Features
&lt;/h2&gt;

&lt;p&gt;🤖 LLM-Powered Documentation – Uses Gemini through LangChain to generate context-aware, high-quality markdown.&lt;/p&gt;

&lt;p&gt;🔗 GitHub Integration – Fetches the file structure using PyGithub.&lt;/p&gt;

&lt;p&gt;💻 CLI Tool – Simple and effective to use right from your terminal.&lt;/p&gt;

&lt;p&gt;🔌 Modular Codebase – Cleanly separated into agents and utils.&lt;/p&gt;

&lt;p&gt;🔐 Environment-Based Secrets – Uses .env for secure API key handling.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧪 Example
&lt;/h2&gt;

&lt;p&gt;Want to try it on a real-world repo?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py https://github.com/&lt;span class="o"&gt;{&lt;/span&gt;username&lt;span class="o"&gt;}&lt;/span&gt;/&lt;span class="o"&gt;{&lt;/span&gt;repo_name&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check your project folder — you'll find a brand new README_generated.md waiting for you ✍️&lt;/p&gt;

&lt;p&gt;Github :- &lt;a href="https://github.com/kanugurajesh/DocMind" rel="noopener noreferrer"&gt;https://github.com/kanugurajesh/DocMind&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtu.be/VZZ0laFaCj4" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F638ismdx560ghx9raitf.png" alt="Smart Doc" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>langchain</category>
      <category>github</category>
    </item>
  </channel>
</rss>
