<?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: Ali Sadeghi</title>
    <description>The latest articles on DEV Community by Ali Sadeghi (@thisissadeghi).</description>
    <link>https://dev.to/thisissadeghi</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%2F4000111%2F2cc484a2-4ce4-4c2c-aa89-036a12e003e4.jpg</url>
      <title>DEV Community: Ali Sadeghi</title>
      <link>https://dev.to/thisissadeghi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thisissadeghi"/>
    <language>en</language>
    <item>
      <title>Design a screen, get a Clean Architecture feature — Spec-Driven Development that keeps AI-generated KMP code from drifting</title>
      <dc:creator>Ali Sadeghi</dc:creator>
      <pubDate>Wed, 24 Jun 2026 08:44:35 +0000</pubDate>
      <link>https://dev.to/thisissadeghi/design-a-screen-get-a-clean-architecture-feature-spec-driven-development-that-keeps-ai-generated-4po4</link>
      <guid>https://dev.to/thisissadeghi/design-a-screen-get-a-clean-architecture-feature-spec-driven-development-that-keeps-ai-generated-4po4</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8fetxa2xrynmc2ksi4mo.png" class="article-body-image-wrapper"&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8fetxa2xrynmc2ksi4mo.png" alt="Kickoff26, a Kotlin Multiplatform World Cup app built feature by feature with KMPilot." width="800" height="512"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A living spec, a few guardrails, and an architecture the model isn't allowed to break.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Describe a feature in plain English — get it designed, built, tested, and reviewed across Android + iOS, with Clean Architecture enforced, not hoped for.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the catch nobody warns you about when you generate features with an AI: each one looks fine on its own. The first screen is clean. So is the second. By the fifth, the app has two different state conventions, a repository quietly reaching into a ViewModel, and a folder of screens that don't agree on how anything is laid out. No single prompt was wrong. The codebase drifted anyway, because nothing held the architecture in place &lt;em&gt;between&lt;/em&gt; prompts.&lt;/p&gt;

&lt;p&gt;You don't fix that with a better prompt. You fix it by making the structure a constraint the model has to satisfy, not a suggestion it's free to reinterpret tomorrow.&lt;/p&gt;

&lt;p&gt;That's what &lt;strong&gt;&lt;a href="https://github.com/ThisIsSadeghi/KMPilot" rel="noopener noreferrer"&gt;KMPilot&lt;/a&gt;&lt;/strong&gt; is: a template I built to hold AI to exactly that. I put it to work on a real app: &lt;a href="https://github.com/ThisIsSadeghi/Kickoff26" rel="noopener noreferrer"&gt;Kickoff26&lt;/a&gt;, a 2026 World Cup companion built feature by feature, design first. This post is what it taught me about keeping AI-generated code in shape.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why AI-generated code drifts
&lt;/h2&gt;

&lt;p&gt;Four things go wrong when you build an app one prompt at a time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Patterns drift.&lt;/strong&gt; Prompt one produces a &lt;code&gt;UiState&lt;/code&gt; sealed class. Prompt five "improves" on it, and now two state conventions live in the same app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layers leak.&lt;/strong&gt; The fastest path from A to B is often a shortcut through a layer that wasn't meant to know about the other. The model takes it, because the shortcut compiles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tests evaporate.&lt;/strong&gt; They're the first casualty of "just make it work," and nobody notices until there's nothing left to run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The design never matches the screen.&lt;/strong&gt; A mockup has 24dp corners and a specific tinted header. What ships is close-ish. Multiply that across twenty screens and the app stops looking designed.&lt;/p&gt;

&lt;p&gt;None of these are intelligence problems. They're &lt;em&gt;memory&lt;/em&gt; and &lt;em&gt;enforcement&lt;/em&gt; problems. The model has no durable record of how this codebase does things, and nothing stops it from doing them differently today.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The model was never the problem. Nothing was holding its work in place.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  The pattern: spec-driven, applied to KMP
&lt;/h2&gt;

&lt;p&gt;Spec-Driven Development isn't something I came up with. Tools like GitHub's &lt;a href="https://github.com/github/spec-kit" rel="noopener noreferrer"&gt;spec-kit&lt;/a&gt; and &lt;a href="https://github.com/Fission-AI/OpenSpec" rel="noopener noreferrer"&gt;OpenSpec&lt;/a&gt; have already made it popular for general codebases: write the spec first and make it the artifact the AI builds from, instead of planning in a chat that scrolls away. The idea is simple: put the contract in writing, keep it beside the code, make the code answer to it. What I did was aim it at one domain, Kotlin Multiplatform, with three parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The architecture is the constraint.&lt;/strong&gt; Clean Architecture, the same shape for every feature, and non-negotiable. Not by politeness, either: a hook physically blocks raw edits to feature code, so the model &lt;em&gt;can't&lt;/em&gt; quietly reshape it. It executes inside the structure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The design is an input, not an afterthought.&lt;/strong&gt; A screen begins as an approved mockup, and the mockup's tokens flow into the code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A living &lt;code&gt;spec.md&lt;/code&gt; is the contract.&lt;/strong&gt; One per feature, versioned, updated as the code changes. It's the memory the model reads before it touches anything.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Under the hood, KMPilot is a set of &lt;strong&gt;skills and agents&lt;/strong&gt; running on top of &lt;a href="https://claude.com/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt;. That's what built every screen in Kickoff26.&lt;/p&gt;


&lt;h2&gt;
  
  
  How a feature actually gets built
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4kw2zmb6jnakjpny5wce.png" class="article-body-image-wrapper"&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4kw2zmb6jnakjpny5wce.png" alt="Kickoff26, a Kotlin Multiplatform World Cup app built feature by feature with KMPilot." width="552" height="1038"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Take the Matches tab: a group-stage browser plus a knockout bracket, Round of 32 to the Final. No single prompt built it. It came together as a short sequence of skills, each with one job, each leaving behind an artifact the next one reads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It starts with the design, with &lt;code&gt;/ui-designer&lt;/code&gt;.&lt;/strong&gt; You describe the screen in plain language:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;/ui-designer Matches — a tab that switches between a group-stage fixtures list, filterable by matchday and group, and a knockout bracket running from the Round of 32 to the Final&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It drives &lt;a href="https://stitch.withgoogle.com" rel="noopener noreferrer"&gt;Stitch&lt;/a&gt; (Google's AI design tool) over an MCP connection: it generates a mockup, and you refine it by talking to it (&lt;em&gt;make the live badge red, tighten the bracket spacing&lt;/em&gt;) until it's right. Approving it is where the interesting part happens. The skill pulls the finished screen back from Stitch and runs it through a token-extraction script that lifts every color, radius, font, and spacing value straight from the design instead of eyeballing them, then downloads the exact icons and images the mockup uses. All of it lands in a &lt;strong&gt;blueprint&lt;/strong&gt;: the design captured as explicit Compose instructions, down to a negative goal difference turning &lt;code&gt;error&lt;/code&gt; red and the bracket's connectors becoming a &lt;code&gt;Canvas&lt;/code&gt;. The screen leaves Stitch as a contract, not a screenshot, so the next step builds it token-for-token, not approximately.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqz6jdorj7l7cg4aiswid.png" class="article-body-image-wrapper"&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqz6jdorj7l7cg4aiswid.png" alt="The Kickoff26 Matches screen, built by KMPilot from a Stitch design." width="800" height="787"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The shipped Matches screen: Group Stage / Knockout, matchday and group filters, real flags and scores. Every color, radius, font, and spacing value came from the Stitch mockup, not eyeballing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then the build, with &lt;code&gt;/creating-kmp-feature&lt;/code&gt;.&lt;/strong&gt; This skill is the heart of the system. It already has the design, from the blueprint; what you hand it is the data contract, the API endpoint and the shape that comes back:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;/creating-kmp-feature matches — fixtures from GET /get/games, where each game has home_team_id, away_team_id, local_date, stadium_id, matchday, type, finished and time_elapsed&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;From there it works in stages, stopping for your sign-off between each:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It turns the request into a short &lt;strong&gt;PRD&lt;/strong&gt; (what the feature does, its screens, its data, the edge cases), then waits.&lt;/li&gt;
&lt;li&gt;Once you approve, it breaks the PRD into discrete &lt;strong&gt;tasks&lt;/strong&gt; (data layer, UI, wiring), then waits again.&lt;/li&gt;
&lt;li&gt;Only after that second confirmation does it hand the tasks to specialized agents that run &lt;strong&gt;in parallel&lt;/strong&gt;, each owning one layer:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;data&lt;/strong&gt;: a serializable model for that JSON, the repository, the Ktor call&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ui&lt;/strong&gt;: the ViewModel, the Compose screen and its components&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;integration&lt;/strong&gt;: dependency injection, navigation, the Gradle wiring&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;platform&lt;/strong&gt;: per-platform code behind a shared interface, only when a feature reaches for a device capability (GPS, camera, biometrics)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Matches is plain network, so the first three covered it. Because each agent owns a separate layer, they never collide, and what comes back isn't a sketch you finish by hand. It's a complete, wired feature module, laid out the same way every feature is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;feature/matches/
├── data/
│   ├── model/MatchesDtos.kt          # @Serializable, mirrors the API JSON
│   └── repository/                    # MatchesRepository + Impl
├── presentation/
│   ├── MatchesViewModel.kt
│   ├── MatchesUiModel.kt             # one state container
│   ├── ui/
│   │   ├── MatchesScreen.kt          # screen + screen-root, nothing else
│   │   ├── MatchesUtils.kt
│   │   ├── motion/MatchesMotion.kt
│   │   └── components/               # 20 files — one composable each
│   │       ├── SegmentedControl.kt
│   │       ├── MatchCard.kt
│   │       ├── BracketColumn.kt
│   │       └── …
│   └── navigation/MatchesNavigation.kt
└── di/MatchesModules.kt              # Koin module
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Thirty-two files across data, presentation, and DI, every one generated from a single design and a single build command, laid out exactly like every other feature in the app. (&lt;a href="https://github.com/ThisIsSadeghi/Kickoff26/tree/master/feature/matches" rel="noopener noreferrer"&gt;Browse it on GitHub&lt;/a&gt;.)&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Predictable structure means you review behavior, not boilerplate.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But the module is only half of what &lt;code&gt;/creating-kmp-feature&lt;/code&gt; produces. Alongside it the skill writes a &lt;strong&gt;&lt;code&gt;spec.md&lt;/code&gt;&lt;/strong&gt; and stores it outside the feature tree, at &lt;code&gt;.claude/docs/matches/spec.md&lt;/code&gt;, versioned and committed with the project. That spec is the feature's memory, structured rather than freeform: a metadata header (version, status, dates), the feature's goals and non-goals, a table of design decisions with the rationale and the alternatives rejected, and requirements written as GIVEN / WHEN / THEN scenarios. A trimmed slice of the Matches spec:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Matches Specification
Version 1.2.2  ·  Status: Active  ·  Updated 2026-06-16

## Design Decisions
| Decision              | Choice                                 | Rationale                        |
| Re-filter, no refetch | cache games/teams, recompute in memory | a chip tap shouldn't hit the API |

## Requirement: Group Stage fixtures browsing
The system SHALL let users browse group-stage fixtures, filtered by matchday and group.

  Scenario: filters narrow the list
  - GIVEN dataState = Success and the Group Stage tab is active
  - WHEN the user picks a different matchday or group chip
  - THEN MatchesDto.dateSections MUST be recomputed from cached data
  - AND if the result is empty, EmptyContent MUST render

## Last Updated
- 2026-06-16  v1.2.2  Knockout tab: render the real 16/8/4/2/1 bracket
- 2026-06-15  v1.2.1  Live detection: API field is "live", not "playing"
- 2026-06-15  v1.2.0  Add Persian (fa) locale — 22 strings translated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;That's the short version. The &lt;a href="https://github.com/ThisIsSadeghi/Kickoff26/blob/master/.claude/docs/matches/spec.md" rel="noopener noreferrer"&gt;full &lt;code&gt;matches/spec.md&lt;/code&gt;&lt;/a&gt; lives in the repo.&lt;/em&gt; The version number and the dated changelog make each feature's history trackable at a glance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Changing it later, with &lt;code&gt;/modifying-kmp-feature&lt;/code&gt;.&lt;/strong&gt; Once a feature exists you never hand-edit it; you describe the change:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;/modifying-kmp-feature matches — add a "Live" filter chip that shows only in-progress matches&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It reads the spec first, plans the change against the decisions already recorded there, and edits the feature through the same agents, never by hand: a &lt;a href="https://github.com/ThisIsSadeghi/KMPilot/blob/main/.claude/hooks/protect-feature-files.sh" rel="noopener noreferrer"&gt;hook&lt;/a&gt; physically blocks raw edits to files under &lt;code&gt;feature/&lt;/code&gt;. When it's done it writes the spec back: a new version, and a fresh dated line in that changelog. Because it starts from the spec, it builds on the existing design instead of relitigating it, and the code and the spec are never updated apart, so neither drifts from the other.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Asking the model nicely isn't enforcement. Blocking the write is.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The remaining skills are gates, run the same way. &lt;code&gt;/verify-ui matches&lt;/code&gt; re-checks the built screen against the design tokens, &lt;code&gt;/feature-test matches&lt;/code&gt; writes the test suite (fixtures, repository, ViewModel, UI, and an end-to-end pass), and &lt;code&gt;/feature-review matches&lt;/code&gt; audits the result against the architecture rules. Any of them can hand the work back. That's the core loop; the &lt;a href="https://github.com/ThisIsSadeghi/KMPilot/wiki/Skills" rel="noopener noreferrer"&gt;full skill catalog&lt;/a&gt; covers the rest.&lt;/p&gt;




&lt;h2&gt;
  
  
  The honest part
&lt;/h2&gt;

&lt;p&gt;I didn't trust this at first. For weeks I half-expected to open the project and find the usual AI sprawl: three ways of doing the same thing, a layer quietly leaking into another, tests I'd end up writing myself anyway. It never showed up. The closest I came to a mess was my own: I'd let each feature keep its own copy of the network layer, and by the fourth one the duplication was impossible to ignore. That's normally the kind of cleanup you keep putting off, because it touches everything. Here I described the change once, the specs told me exactly what each feature had decided and why, and it was done in an afternoon without breaking a thing.&lt;/p&gt;

&lt;p&gt;It's young: &lt;a href="https://github.com/ThisIsSadeghi/Kickoff26" rel="noopener noreferrer"&gt;Kickoff26&lt;/a&gt; still says &lt;em&gt;under development&lt;/em&gt;, and KMPilot has rough edges I haven't sanded. But after months of watching AI-assisted codebases rot in fast-forward, the part I keep coming back to is that this one hasn't. Nothing drifted. That was the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;If you write Kotlin Multiplatform and you've watched a codebase drift under AI-generated code, the pattern is worth stealing even without the template. Make the architecture a constraint. Make the design an input. Give the model a living spec to read.&lt;/p&gt;

&lt;p&gt;KMPilot is the version I actually use, MIT-licensed, one command to start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/ThisIsSadeghi/KMPilot/main/install.sh &lt;span class="se"&gt;\&lt;/span&gt;
  | bash &lt;span class="nt"&gt;-s&lt;/span&gt; &amp;lt;MyApp&amp;gt; &amp;lt;com.acme.myapp&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repo and the full pipeline: &lt;strong&gt;&lt;a href="https://github.com/ThisIsSadeghi/KMPilot" rel="noopener noreferrer"&gt;github.com/ThisIsSadeghi/KMPilot&lt;/a&gt;&lt;/strong&gt;. If the idea resonates, a star is the cheapest way to tell me to keep building it.&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>jetpackcompose</category>
      <category>sdd</category>
      <category>android</category>
    </item>
  </channel>
</rss>
