<?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: Boris Grinshpun</title>
    <description>The latest articles on DEV Community by Boris Grinshpun (@borisgri).</description>
    <link>https://dev.to/borisgri</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%2F4066133%2F0053b176-bb42-4051-ac2c-616e6abc2c65.png</url>
      <title>DEV Community: Boris Grinshpun</title>
      <link>https://dev.to/borisgri</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/borisgri"/>
    <language>en</language>
    <item>
      <title>Flow-to-TypeScript AI Migration</title>
      <dc:creator>Boris Grinshpun</dc:creator>
      <pubDate>Thu, 06 Aug 2026 15:28:25 +0000</pubDate>
      <link>https://dev.to/borisgri/flow-to-typescript-ai-migration-obc</link>
      <guid>https://dev.to/borisgri/flow-to-typescript-ai-migration-obc</guid>
      <description>&lt;p&gt;Everyone thinks migrating a codebase with AI means pointing a model at your files and saying "convert these."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's not a plan. That's a wish.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Migrating a 10-year-old product isn't a refactor; it's an excavation. We had ~1,100 files deeply entrenched in Flow (last real release: 2019) that we had to safely move to TypeScript without breaking the app.did &lt;/p&gt;

&lt;p&gt;Before writing a single prompt, we sat with why the "just let the AI do it" approach fails:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Context is bounded:&lt;/strong&gt; Feed a model a slice of 1,100 interdependent files, and it's blind to the rest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No memory:&lt;/strong&gt; A type in File A is invisible when looking at File B, leading to broken imports and hallucinations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No ground truth:&lt;/strong&gt; AI feels confident even when wrong. Without compile/test checks, you stack silent breakages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model was never the hard part. &lt;strong&gt;The system around it was.&lt;/strong&gt; Over a hackathon, a team of 3 built an AI migration engine. Here is how we did it:&lt;/p&gt;

&lt;h2&gt;
  
  
  1. We migrated the build first (Webpack → Vite)
&lt;/h2&gt;

&lt;p&gt;You can't convert file-by-file if the app only builds when it's all done. Moving to Vite + esbuild allowed legacy &lt;code&gt;.js&lt;/code&gt; and new &lt;code&gt;.ts&lt;/code&gt; to coexist, keeping the app green.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. We mapped the graph, then split it into groups
&lt;/h2&gt;

&lt;p&gt;None of this works if you throw random files at the model. So before any conversion, two static-analysis passes ran over the codebase:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dependency graph:&lt;/strong&gt; the full import graph — who imports whom, plus every circular dependency (the traps that break naive migration).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type-ownership map:&lt;/strong&gt; for every exported type, &lt;em&gt;which file owns it&lt;/em&gt; and &lt;em&gt;which files consume it&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then we split the ~1,100 files into small &lt;strong&gt;atomic groups&lt;/strong&gt;: a type's owner file plus every file that consumes that type migrate together — so no import ever straddles the Flow/TS boundary mid-migration.&lt;/p&gt;

&lt;p&gt;Finally, we sorted those ~720 groups &lt;strong&gt;leaf-first&lt;/strong&gt; — dependencies always migrated before the things that depended on them. The output is a single ordered plan the loop just walks down.&lt;/p&gt;

&lt;p&gt;One rule the model forced on us: &lt;strong&gt;you can't one-shot a monolith.&lt;/strong&gt; Files like &lt;code&gt;actions.js&lt;/code&gt; (4,300+ lines) were too big to convert reliably in a single pass, so we split them into ~14 focused domain modules &lt;em&gt;first&lt;/em&gt;, then fed those to the loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. We built an AI loop with a referee
&lt;/h2&gt;

&lt;p&gt;Instead of a single prompt, we built a pipeline: Planner → Implementer (Claude) → Reviewer (tsc + tests).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   Planner  ──►  Implementer  ──►  Reviewer  ──► ✅ commit
  (next group    (Claude writes    (tsc + tests)
   from plan)      the TS)              │
      ▲                                 │ errors?
      │                                 ▼
      └────────  retry (feed errors back in)  ◄──┘
                          │
                   stuck after 3 tries?
                          ▼
                  🧠 Architect (bigger model)
             → retry smarter / reorder / flag a human
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Reviewer only commits on green. Failures feed exact errors back for a retry. If it gets stuck after 3 tries, the system escalates or flags a human.&lt;/p&gt;

&lt;p&gt;We ran &lt;strong&gt;two models on purpose&lt;/strong&gt;: a fast, cheap one (Claude Sonnet) does the routine ~95% of conversions; a heavier one (Claude Opus) only steps in as the Architect on stuck cases — to reason about &lt;em&gt;why&lt;/em&gt; it's stuck and decide the next move (retry smarter, reorder, or escalate). And not everything even needs the loop: the interdependent source files ran through the full pipeline, but hundreds of mechanical test-file conversions were handled by a plain batch script — no orchestrator overhead.&lt;/p&gt;

&lt;p&gt;And we &lt;strong&gt;constrained the model, hard.&lt;/strong&gt; It wasn't handed a blank check — every prompt forbade &lt;code&gt;any&lt;/code&gt; and forbade &lt;code&gt;as unknown as X&lt;/code&gt; casts that silence errors instead of fixing them; it had to import real library types rather than invent shapes, and trace prop types from how a component is &lt;em&gt;actually&lt;/em&gt; used by its parents. Most importantly: the model only ever &lt;em&gt;edits files&lt;/em&gt; — no shell, no git. All verification lives outside it, in the Reviewer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Orchestration:&lt;/strong&gt; LangGraph JS — the Planner, Implementer, Reviewer, and Architect are nodes in a state machine, not one big prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engine:&lt;/strong&gt; Claude Code CLI, driven by two models — Claude Sonnet (routine) + Claude Opus (stuck cases).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The retry pattern:&lt;/strong&gt; a "Ralph Loop" — keep re-running the Implementer, feeding the exact &lt;code&gt;tsc&lt;/code&gt;/test errors back in, until it's green or declared stuck.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static analysis:&lt;/strong&gt; &lt;code&gt;madge&lt;/code&gt; (dependency graph), &lt;code&gt;@babel/parser&lt;/code&gt; (type-ownership map), Kahn's algorithm (leaf-first topological sort).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verification gate:&lt;/strong&gt; &lt;code&gt;tsc --noEmit&lt;/code&gt; + Jest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build &amp;amp; types:&lt;/strong&gt; Vite 6 + esbuild; &lt;code&gt;openapi-typescript&lt;/code&gt; generating API types from Swagger/OpenAPI specs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State:&lt;/strong&gt; a JSON checkpoint on disk with file-based locking — which is what makes it killable, resumable, and parallelizable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Runtime: Node + TypeScript throughout.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you're pointing AI at a big migration, here are your takeaways:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fix the build first:&lt;/strong&gt; You need mixed-state compilation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feed it a graph:&lt;/strong&gt; Give it self-contained units with resolved dependencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decompose the giants first:&lt;/strong&gt; AI can't reliably one-shot a 4,000-line file. Split monoliths into focused modules before you migrate them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Order is correctness:&lt;/strong&gt; Leaf-first prevents downstream breaks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix structure, not symptoms:&lt;/strong&gt; A circular-dependency chain caused 318 test failures. We broke it with &lt;em&gt;layer separation&lt;/em&gt; — pulling pure data out of component-coupled code — not a lazy &lt;code&gt;require()&lt;/code&gt; hack. One structural fix cleared hundreds of errors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supply ground truth:&lt;/strong&gt; Wire your tests as a hard commit gate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Errors are prompts:&lt;/strong&gt; Feed failures back in to create a self-correcting loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Escalate on purpose:&lt;/strong&gt; Cheap models for 95%, expensive for 5%, humans for the rest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match the tool to the task:&lt;/strong&gt; Full agent loop for the hard, interdependent code; a plain batch script for mechanical bulk. Don't pay for judgment you don't need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guardrail the agent:&lt;/strong&gt; Forbid &lt;code&gt;any&lt;/code&gt;, forbid error-silencing casts, force real library types. Give it edit-only power — no shell, no git — and keep verification external.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make it resumable:&lt;/strong&gt; Checkpoint state to disk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A migration is a free audit:&lt;/strong&gt; Generating types from your API specs reveals every place your contracts have drifted from reality — and every service that has no contract at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result: ~1,100 files migrated, Flow gone, the app green throughout. And it wasn't a hackathon toy that died on Monday — &lt;strong&gt;the migration shipped to production the following sprint.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The meta-lesson: &lt;strong&gt;AI doesn't replace engineering judgment — it scales it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What's the migration you keep putting off because it's "too big to do by hand"?&lt;/p&gt;

&lt;p&gt;&lt;code&gt;#AI&lt;/code&gt; &lt;code&gt;#TypeScript&lt;/code&gt; &lt;code&gt;#SoftwareEngineering&lt;/code&gt; &lt;code&gt;#DeveloperTools&lt;/code&gt; &lt;code&gt;#Claude&lt;/code&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>softwareengineering</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
