<?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: Krishan Sharma</title>
    <description>The latest articles on DEV Community by Krishan Sharma (@krishan_sharma_561a52817e).</description>
    <link>https://dev.to/krishan_sharma_561a52817e</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%2F3956537%2F877d2c42-8569-4852-a8cf-d7891425c664.png</url>
      <title>DEV Community: Krishan Sharma</title>
      <link>https://dev.to/krishan_sharma_561a52817e</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/krishan_sharma_561a52817e"/>
    <language>en</language>
    <item>
      <title>Why LaunchDarkly OpenFeature Migrations Break in Production</title>
      <dc:creator>Krishan Sharma</dc:creator>
      <pubDate>Sun, 28 Jun 2026 20:40:51 +0000</pubDate>
      <link>https://dev.to/krishan_sharma_561a52817e/why-launchdarkly-openfeature-migrations-break-in-production-52mk</link>
      <guid>https://dev.to/krishan_sharma_561a52817e/why-launchdarkly-openfeature-migrations-break-in-production-52mk</guid>
      <description>&lt;p&gt;LaunchDarkly and OpenFeature both evaluate flags with three arguments, but the&lt;br&gt;
fallback and context positions are reversed. A naive codemod can produce&lt;br&gt;
valid-looking code that silently changes runtime behavior.&lt;/p&gt;

&lt;p&gt;This article shows the argument-order trap and why FlagLint uses AST analysis&lt;br&gt;
before rewriting any call site.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Agreement That Takes 30 Minutes
&lt;/h2&gt;

&lt;p&gt;Teams agree on OpenFeature quickly. It makes sense — vendor-neutral, &lt;br&gt;
CNCF-backed, clean abstraction. The decision is easy.&lt;/p&gt;

&lt;p&gt;The migration is not.&lt;/p&gt;

&lt;p&gt;Halfway through, most teams hit a bug that looks like this in production:&lt;br&gt;
a subset of users sees the wrong feature state. The flag evaluation &lt;br&gt;
is returning unexpected values. Everything looked correct in code review.&lt;/p&gt;

&lt;p&gt;The cause is almost always the same thing.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Argument-Order Trap
&lt;/h2&gt;

&lt;p&gt;LaunchDarkly and OpenFeature share method names but differ in argument order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// LaunchDarkly&lt;/span&gt;
&lt;span class="nx"&gt;ldClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolVariation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flagKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// OpenFeature  &lt;/span&gt;
&lt;span class="nx"&gt;openFeatureClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBooleanValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flagKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;context&lt;/code&gt; and &lt;code&gt;fallback&lt;/code&gt; are swapped.&lt;/p&gt;

&lt;p&gt;A search-and-replace migration silently puts &lt;code&gt;context&lt;/code&gt; where &lt;code&gt;fallback&lt;/code&gt; &lt;br&gt;
should be, and &lt;code&gt;fallback&lt;/code&gt; where &lt;code&gt;context&lt;/code&gt; should be — across every call &lt;br&gt;
site in your codebase.&lt;/p&gt;

&lt;p&gt;In production: users in your evaluation context see the fallback value.&lt;br&gt;
In code review: the signature looks correct because the argument count matches.&lt;br&gt;
In the post-mortem: nobody can identify when it was introduced.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Grep Misses It
&lt;/h2&gt;

&lt;p&gt;The typical manual approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Search for &lt;code&gt;launchdarkly-node-server-sdk&lt;/code&gt; imports&lt;/li&gt;
&lt;li&gt;Find all &lt;code&gt;ldClient&lt;/code&gt; references
&lt;/li&gt;
&lt;li&gt;Search-and-replace method names&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This finds the calls. It does not understand argument semantics.&lt;/p&gt;

&lt;p&gt;A grep-based migration will correctly rename &lt;code&gt;boolVariation&lt;/code&gt; to &lt;br&gt;
&lt;code&gt;getBooleanValue&lt;/code&gt; and miss the argument order entirely. The test suite &lt;br&gt;
often misses it too because the values are both valid types — &lt;code&gt;context&lt;/code&gt; &lt;br&gt;
and &lt;code&gt;fallback&lt;/code&gt; are both objects.&lt;/p&gt;
&lt;h2&gt;
  
  
  What AST Analysis Catches
&lt;/h2&gt;

&lt;p&gt;Abstract Syntax Tree (AST) analysis parses your code the same way a &lt;br&gt;
compiler does. It doesn't match text — it understands structure.&lt;/p&gt;

&lt;p&gt;For a call like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ldClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolVariation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkout-v2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AST analysis identifies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The import binding (&lt;code&gt;ldClient&lt;/code&gt; → &lt;code&gt;launchdarkly-node-server-sdk&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The method name (&lt;code&gt;boolVariation&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The argument at position 0: flag key (&lt;code&gt;'checkout-v2'&lt;/code&gt; — string literal)&lt;/li&gt;
&lt;li&gt;The argument at position 1: context (&lt;code&gt;ctx&lt;/code&gt; — object reference)&lt;/li&gt;
&lt;li&gt;The argument at position 2: fallback (&lt;code&gt;false&lt;/code&gt; — boolean literal)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When generating the OpenFeature equivalent, it knows to produce:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;openFeatureClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBooleanValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkout-v2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Argument 2 goes to position 1. Argument 1 goes to position 2.&lt;br&gt;
Argument order corrected. Type preserved. Await preserved.&lt;/p&gt;
&lt;h2&gt;
  
  
  When AST Analysis Refuses to Rewrite
&lt;/h2&gt;

&lt;p&gt;Not every call can be safely automated. FlagLint identifies these &lt;br&gt;
and routes them to manual review instead of silently rewriting them:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dynamic flag keys:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;flagKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`feature-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;featureName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ldClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolVariation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flagKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key is not statically knowable. Automated rewrite could produce &lt;br&gt;
incorrect OpenFeature client binding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Detail methods:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ldClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolVariationDetail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkout-v2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;boolVariationDetail&lt;/code&gt; returns metadata not directly equivalent in &lt;br&gt;
OpenFeature. Requires a different migration pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bulk state calls:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ldClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allFlagsState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No direct OpenFeature equivalent. Architecture decision required.&lt;/p&gt;

&lt;p&gt;For all of these, FlagLint reports the location and reason — &lt;br&gt;
it never silently rewrites them.&lt;/p&gt;
&lt;h2&gt;
  
  
  The CI Gate
&lt;/h2&gt;

&lt;p&gt;Generating diffs is only half the problem. Migration rot is the other half.&lt;/p&gt;

&lt;p&gt;After a phased migration, new engineers joining the codebase don't &lt;br&gt;
know the rule. They reach for &lt;code&gt;ldClient&lt;/code&gt; because that's what they know. &lt;br&gt;
Six months later, you have new direct LD calls and the migration has &lt;br&gt;
partially reversed.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;flaglint validate --no-direct-launchdarkly&lt;/code&gt; exits 1 if any direct &lt;br&gt;
LaunchDarkly evaluation call appears outside the bootstrap file.&lt;/p&gt;

&lt;p&gt;Add it to your GitHub Actions workflow:&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="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;Enforce OpenFeature boundary&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx flaglint@latest validate ./src --no-direct-launchdarkly&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any new &lt;code&gt;ldClient.boolVariation()&lt;/code&gt; call fails the build. The boundary holds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Now
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx flaglint audit ./src
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Runs locally, no SDK key needed, nothing changes. Gives you a risk-ranked&lt;br&gt;
inventory of every direct LaunchDarkly SDK call in your codebase —&lt;br&gt;
dynamic keys, detail methods, and bulk state calls included — with a&lt;br&gt;
migration readiness score.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx flaglint migrate ./src &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shows the before/after diff for every safely automatable call site.&lt;br&gt;
Dynamic keys and detail methods are reported separately for manual review.&lt;/p&gt;

&lt;p&gt;FlagLint is free, open source, MIT licensed.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://github.com/flaglint/flaglint" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;br&gt;&lt;br&gt;
→ &lt;a href="https://www.npmjs.com/package/flaglint" rel="noopener noreferrer"&gt;npm&lt;/a&gt;&lt;br&gt;&lt;br&gt;
→ &lt;a href="https://dev.to/docs/quickstart"&gt;Quickstart&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Related:&lt;/strong&gt; &lt;a href="https://dev.to/blog/after-launchdarkly-outage-vendor-neutral-abstraction/"&gt;After the LaunchDarkly Outage: Adding a Vendor-Neutral Abstraction Without a Full Migration →&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://flaglint.dev/blog/launchdarkly-openfeature-argument-order-bug/" rel="noopener noreferrer"&gt;flaglint.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>launchdarkly</category>
      <category>openfeature</category>
      <category>typescript</category>
      <category>devops</category>
    </item>
    <item>
      <title>Five LaunchDarkly SDK Patterns That Block Automatic Migration to OpenFeature</title>
      <dc:creator>Krishan Sharma</dc:creator>
      <pubDate>Sun, 28 Jun 2026 20:40:27 +0000</pubDate>
      <link>https://dev.to/krishan_sharma_561a52817e/five-launchdarkly-sdk-patterns-that-block-automatic-migration-to-openfeature-5aho</link>
      <guid>https://dev.to/krishan_sharma_561a52817e/five-launchdarkly-sdk-patterns-that-block-automatic-migration-to-openfeature-5aho</guid>
      <description>&lt;p&gt;Run &lt;code&gt;flaglint migrate ./src --dry-run&lt;/code&gt; and you will see two kinds of results: call&lt;br&gt;
sites with a generated diff and call sites marked &lt;code&gt;skip — manual review required&lt;/code&gt;.&lt;br&gt;
The skipped calls are not bugs in the tool. They are patterns where a mechanical&lt;br&gt;
rewrite would change runtime behavior in ways the tool cannot prove are safe.&lt;/p&gt;

&lt;p&gt;This article covers the five patterns that produce skips and what you need to do for each.&lt;/p&gt;


&lt;h2&gt;
  
  
  What makes a call automatable
&lt;/h2&gt;

&lt;p&gt;FlagLint rewrites a call automatically only when it can prove three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The flag key is a static string literal.&lt;/li&gt;
&lt;li&gt;The fallback value type is known (&lt;code&gt;boolean&lt;/code&gt;, &lt;code&gt;string&lt;/code&gt;, &lt;code&gt;number&lt;/code&gt;, or &lt;code&gt;json&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;A verified OpenFeature client binding is present in scope.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If any proof fails, the call is left unchanged and flagged for manual review. The&lt;br&gt;
conservative stance is intentional: a wrong rewrite is worse than no rewrite.&lt;/p&gt;


&lt;h2&gt;
  
  
  Pattern 1 — Dynamic flag keys
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it looks like:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// key comes from a variable&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;experimentGroup&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkout-v2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkout-v1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ldClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolVariation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// key comes from a function call&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;discount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ldClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;numberVariation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getFlagKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// key is assembled at runtime&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;variant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ldClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringVariation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`feature-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-rollout`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it blocks migration:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;OpenFeature's evaluation methods are structurally identical to LaunchDarkly's for&lt;br&gt;
this case — both take &lt;code&gt;(key, defaultValue, context)&lt;/code&gt;. But if the key is dynamic,&lt;br&gt;
FlagLint cannot know which flag is being evaluated, which means it cannot verify&lt;br&gt;
that the OpenFeature provider has that flag configured, cannot determine the correct&lt;br&gt;
return type, and cannot guarantee the fallback default is the right type for that&lt;br&gt;
specific flag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to resolve:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Extract the dynamic selection into an explicit switch or map, then call evaluation&lt;br&gt;
with a known static key per branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;discount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ldClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;numberVariation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getFlagKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// After&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FLAG_BY_TIER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;free&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;discount-free-tier&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;pro&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;discount-pro-tier&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;enterprise&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;discount-enterprise-tier&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;flagKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;FLAG_BY_TIER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;discount-free-tier&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;discount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;openFeatureClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getNumberValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flagKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the key is static in each branch, the surrounding calls become automatable in&lt;br&gt;
the next &lt;code&gt;migrate&lt;/code&gt; run.&lt;/p&gt;


&lt;h2&gt;
  
  
  Pattern 2 — Detail evaluations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it looks like:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;detail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ldClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolVariationDetail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;show-banner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// detail.value — the evaluated value&lt;/span&gt;
&lt;span class="c1"&gt;// detail.reason — WHY it was that value (RULE_MATCH, FALLTHROUGH, etc.)&lt;/span&gt;
&lt;span class="c1"&gt;// detail.variationIndex — index into the flag's variation list&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ldClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;variationDetail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkout-experiment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it blocks migration:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;OpenFeature has an equivalent — &lt;code&gt;getBooleanDetails()&lt;/code&gt; returns a &lt;code&gt;{ value, reason,&lt;br&gt;
errorCode }&lt;/code&gt; object. But the reason structure is different. LaunchDarkly's&lt;br&gt;
&lt;code&gt;EvaluationReason&lt;/code&gt; includes &lt;code&gt;ruleId&lt;/code&gt;, &lt;code&gt;ruleIndex&lt;/code&gt;, &lt;code&gt;bigSegmentsStatus&lt;/code&gt;, and&lt;br&gt;
&lt;code&gt;prerequisiteKey&lt;/code&gt;. OpenFeature's &lt;code&gt;ResolutionDetails&lt;/code&gt; uses a smaller vocabulary&lt;br&gt;
(&lt;code&gt;CACHED&lt;/code&gt;, &lt;code&gt;DEFAULT&lt;/code&gt;, &lt;code&gt;ERROR&lt;/code&gt;, &lt;code&gt;SPLIT&lt;/code&gt;, &lt;code&gt;STATIC&lt;/code&gt;, &lt;code&gt;TARGETING_MATCH&lt;/code&gt;, &lt;code&gt;UNKNOWN&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Code that consumes &lt;code&gt;detail.reason.kind === 'RULE_MATCH'&lt;/code&gt; or &lt;code&gt;detail.reason.ruleId&lt;/code&gt;&lt;br&gt;
will need to be updated alongside the evaluation call. FlagLint cannot safely&lt;br&gt;
generate that transformation because the consuming code varies too much.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to resolve:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Migrate these calls manually. For each &lt;code&gt;variationDetail&lt;/code&gt; call:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Replace the evaluation with &lt;code&gt;getBooleanDetails()&lt;/code&gt; / &lt;code&gt;getStringDetails()&lt;/code&gt; etc.&lt;/li&gt;
&lt;li&gt;Update any consumers of &lt;code&gt;reason.kind&lt;/code&gt;, &lt;code&gt;reason.ruleId&lt;/code&gt;, or &lt;code&gt;variationIndex&lt;/code&gt; to use OpenFeature's reason vocabulary.&lt;/li&gt;
&lt;li&gt;Add tests that cover the specific reason codes your business logic depends on.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;detail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ldClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolVariationDetail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;show-banner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RULE_MATCH&lt;/span&gt;&lt;span class="dl"&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;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rule-matched&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// After&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;detail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;openFeatureClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBooleanDetails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;show-banner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TARGETING_MATCH&lt;/span&gt;&lt;span class="dl"&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;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rule-matched&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Pattern 3 — Bulk state calls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it looks like:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Server-side bootstrap — sends all flag values to the client&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allFlags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ldClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allFlags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ldClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allFlagsState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Common in SSR: inject all flags into the page for client-side hydration&lt;/span&gt;
&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ldClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allFlags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it blocks migration:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;OpenFeature has no equivalent to &lt;code&gt;allFlags()&lt;/code&gt; or &lt;code&gt;allFlagsState()&lt;/code&gt;. The OpenFeature&lt;br&gt;
specification deliberately does not include bulk evaluation — providers are expected&lt;br&gt;
to surface individual flags, and bulk retrieval is a vendor-specific concern.&lt;/p&gt;

&lt;p&gt;The LaunchDarkly OpenFeature provider does not expose &lt;code&gt;allFlags&lt;/code&gt; through the&lt;br&gt;
OpenFeature client interface. If your code relies on bulk evaluation to bootstrap a&lt;br&gt;
client-side SDK or build a flag snapshot, that architecture requires rethinking, not&lt;br&gt;
just rewriting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to resolve:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option A — Enumerate the flags explicitly.&lt;/strong&gt; If you know which flags are needed&lt;br&gt;
at the injection point, evaluate them individually and bundle the results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FLAGS_TO_BOOTSTRAP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;show-banner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkout-v2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;new-pricing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;flagSnapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEntries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;FLAGS_TO_BOOTSTRAP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;openFeatureClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBooleanValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Option B — Use the LaunchDarkly provider directly for bootstrapping.&lt;/strong&gt; The&lt;br&gt;
OpenFeature provider wraps the LD Node.js SDK. You can access the underlying client&lt;br&gt;
for the specific bootstrap call while migrating all other evaluation calls to&lt;br&gt;
OpenFeature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LaunchDarklyProvider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@launchdarkly/openfeature-node-server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LaunchDarklyProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LD_SDK_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;OpenFeature&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProviderAndWait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Access the underlying LD client only for bulk bootstrap&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ldClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;getClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// type-cast needed; provider internals are not public API&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allFlags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ldClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allFlags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Option B is a transitional pattern. The goal is to eliminate it once you've&lt;br&gt;
enumerated the flags that actually need bootstrapping.&lt;/p&gt;


&lt;h2&gt;
  
  
  Pattern 4 — Configured wrappers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it looks like:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A shared evaluation helper used across services&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isFeatureEnabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flagKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LDContext&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;ldClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolVariation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flagKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Custom wrapper that adds logging and metrics&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;evaluateFlag&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LDContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ldClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;variation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flag.evaluation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it blocks migration:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;FlagLint detects wrappers configured in &lt;code&gt;.flaglintrc&lt;/code&gt; under the &lt;code&gt;wrappers&lt;/code&gt; key.&lt;br&gt;
When a wrapper is detected, the call is surfaced in the audit and scan output but&lt;br&gt;
never auto-rewritten — because rewriting the call site does not solve the problem.&lt;br&gt;
The wrapper itself contains the direct LD SDK call that needs to change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to resolve:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Migrate the wrapper implementation, not the call sites. The call sites stay the same;&lt;br&gt;
only the internals of the wrapper change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isFeatureEnabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flagKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LDContext&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;ldClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolVariation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flagKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// After — wrapper now delegates to OpenFeature&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isFeatureEnabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flagKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EvaluationContext&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;openFeatureClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBooleanValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flagKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the wrapper implementation is migrated, configure FlagLint to recognise the&lt;br&gt;
wrapper's result type so downstream &lt;code&gt;scan&lt;/code&gt; output is accurate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&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;"wrappers"&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="s2"&gt;"isFeatureEnabled"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"evaluateFlag"&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;/div&gt;



&lt;p&gt;Wrappers that accept a dynamic &lt;code&gt;flagKey&lt;/code&gt; parameter will still appear in reports —&lt;br&gt;
that is correct behaviour. The scanner surfaces them for the same reason it surfaces&lt;br&gt;
dynamic keys: it cannot prove which flag is being evaluated.&lt;/p&gt;


&lt;h2&gt;
  
  
  Pattern 5 — Unknown fallback types (&lt;code&gt;jsonVariation&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it looks like:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Untyped JSON — fallback type is object but the shape is unknown&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ldClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;jsonVariation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pricing-config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;

&lt;span class="c1"&gt;// Typed JSON with a complex or union shape&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ldClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;jsonVariation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;routing-rules&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it blocks migration:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;OpenFeature's equivalent is &lt;code&gt;getObjectValue()&lt;/code&gt;, which returns &lt;code&gt;JsonValue&lt;/code&gt; — a union&lt;br&gt;
of &lt;code&gt;string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }&lt;/code&gt;.&lt;br&gt;
When the fallback is &lt;code&gt;{}&lt;/code&gt; or another untyped object, FlagLint cannot determine the&lt;br&gt;
correct generic type to use, and it cannot verify that the calling code handles the&lt;br&gt;
&lt;code&gt;JsonValue&lt;/code&gt; type correctly rather than a narrower application-specific type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to resolve:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add explicit type annotations to the fallback and the result, then migrate manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Define the expected shape&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PricingConfig&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;basePrice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;tiers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;discount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_CONFIG&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PricingConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;basePrice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tiers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Before&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ldClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;jsonVariation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pricing-config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_CONFIG&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// After&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;openFeatureClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getObjectValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pricing-config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;DEFAULT_CONFIG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;PricingConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The explicit cast is safe because the provider returns whatever LaunchDarkly sends,&lt;br&gt;
and the schema is defined in the LaunchDarkly dashboard. If the shape might not&lt;br&gt;
match, add a runtime validator (Zod works well here) at the call site.&lt;/p&gt;


&lt;h2&gt;
  
  
  Seeing the full breakdown before you start
&lt;/h2&gt;

&lt;p&gt;Before migrating, run &lt;code&gt;flaglint audit ./src&lt;/code&gt; to see how many calls fall into each&lt;br&gt;
category and why:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✓ Audit complete: 18 flags — 5 high risk, 13 medium risk

| Flag Key              | Risk   | Usages | Reasons                           |
|-----------------------|--------|--------|-----------------------------------|
| &amp;lt;dynamic key&amp;gt;         | High   | 9      | key cannot be resolved statically |
| checkout-experiment   | High   | 1      | detail evaluation                 |
| *                     | High   | 1      | bulk call                         |
| &amp;lt;wrappers&amp;gt;            | High   | 4      | configured wrapper                |
| pricing-config        | Medium | 1      | json — unknown shape              |
| checkout-v2           | Medium | 1      | safely automatable                |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The five patterns above account for every high-risk category. Resolving them&lt;br&gt;
one at a time — starting with wrappers, then dynamic keys — progressively reduces&lt;br&gt;
the manual review surface until &lt;code&gt;migrate --apply&lt;/code&gt; can handle the rest automatically.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://flaglint.dev/blog/five-patterns-that-block-migration/" rel="noopener noreferrer"&gt;flaglint.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>launchdarkly</category>
      <category>openfeature</category>
      <category>typescript</category>
      <category>devops</category>
    </item>
    <item>
      <title>Feature Flag Technical Debt in TypeScript: Find, Measure, and Clear It</title>
      <dc:creator>Krishan Sharma</dc:creator>
      <pubDate>Sun, 28 Jun 2026 20:40:16 +0000</pubDate>
      <link>https://dev.to/krishan_sharma_561a52817e/feature-flag-technical-debt-in-typescript-find-measure-and-clear-it-35cf</link>
      <guid>https://dev.to/krishan_sharma_561a52817e/feature-flag-technical-debt-in-typescript-find-measure-and-clear-it-35cf</guid>
      <description>&lt;p&gt;TypeScript's type system enforces interface contracts and catches argument-type mismatches at compile time — but it cannot see which of your modules still depend on the LaunchDarkly SDK, which of those call sites can be automatically rewritten, or how many engineer-hours the migration backlog represents.&lt;/p&gt;

&lt;p&gt;Feature flag technical debt in TypeScript codebases compounds quietly. A team ships a boolean flag behind &lt;code&gt;ldClient.boolVariation&lt;/code&gt;, the rollout succeeds, and the code moves on. Six months later the flag is still evaluated on every request. The surrounding code has grown around it. The LaunchDarkly SDK is a locked-in transitive dependency for the entire module that contains it. And no one has a reliable count of how many of these exist, because the only tool most teams reach for is a &lt;code&gt;grep&lt;/code&gt; that conflates static flag keys, wrapper functions, and bulk calls into one undifferentiated list.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://flaglint.dev" rel="noopener noreferrer"&gt;FlagLint&lt;/a&gt; is a free open-source CLI that parses TypeScript source files using an AST scanner to enumerate every LaunchDarkly SDK call site, classify each one by call type and risk, compute a readiness score, and output a migration plan to OpenFeature. No LaunchDarkly API key required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why grep misses TypeScript feature flag technical debt
&lt;/h2&gt;

&lt;p&gt;Running &lt;code&gt;grep -r "ldClient" ./src&lt;/code&gt; gives you a count. It does not give you a classification. Every result looks equivalent in grep output, but three structurally different situations hide behind the same pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Static flag key, direct call&lt;/strong&gt; — the flag key is a string literal; the call type is &lt;code&gt;boolVariation&lt;/code&gt;, &lt;code&gt;stringVariation&lt;/code&gt;, or &lt;code&gt;numberVariation&lt;/code&gt;; the return type is known. FlagLint can generate a safe rewrite for this automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrapper function with a dynamic key&lt;/strong&gt; — the function accepts a &lt;code&gt;flagKey&lt;/code&gt; parameter and calls the LaunchDarkly SDK internally. FlagLint cannot statically determine which flag is being evaluated, verify the call type, or confirm the return type. This is a high-risk call type.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detail evaluation or bulk call&lt;/strong&gt; — &lt;code&gt;boolVariationDetail&lt;/code&gt; and &lt;code&gt;allFlagsState&lt;/code&gt; have no direct OpenFeature provider equivalent and cannot be safely transformed by a static rewriter.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All three groups require completely different migration approaches — but grep cannot distinguish them.&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;flaglint scan&lt;/code&gt; against your source directory to get the AST-based inventory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx flaglint scan ./src
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real output from the enterprise checkout service included with FlagLint (5 source files):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;-&lt;/span&gt; Scanning examples/enterprise-checkout-service/src/...
✓ 19 flag usages found across 11 unique flags (65ms)
ℹ  1 dynamic flag key(s) require manual review
&lt;span class="gh"&gt;# FlagLint Scan Report&lt;/span&gt;

&lt;span class="gs"&gt;**Scanned:**&lt;/span&gt; 5 files in 65ms
&lt;span class="gs"&gt;**Flag usages:**&lt;/span&gt; 19 across 11 unique flags
&lt;span class="gs"&gt;**Stale candidates:**&lt;/span&gt; 0 flags flagged for review

&lt;span class="gu"&gt;## Flag Inventory&lt;/span&gt;
| Flag Key              | Usages | Files | Call Types                                                 | Status   |
|-----------------------|--------|-------|------------------------------------------------------------|----------|
| (dynamic key)         | 7      | 3     | variationDetail, boolVariation, stringVariation, ...       | ✓ Active |
| checkout-experiment   | 1      | 1     | boolVariationDetail                                        | ✓ Active |
| (dynamic key)         | 1      | 1     | allFlagsState                                              | ✓ Active |
| checkout-v2           | 1      | 1     | boolVariation                                              | ✓ Active |
| payment-provider      | 1      | 1     | stringVariation                                            | ✓ Active |
| one-click-checkout    | 1      | 1     | boolVariation                                              | ✓ Active |
| checkout-currency     | 1      | 1     | stringVariation                                            | ✓ Active |
| discount-percentage   | 1      | 1     | numberVariation                                            | ✓ Active |
| max-discount-amount   | 1      | 1     | numberVariation                                            | ✓ Active |
| discount-config       | 1      | 1     | jsonVariation                                              | ✓ Active |
| pricing-tier-config   | 1      | 1     | jsonVariation                                              | ✓ Active |
| recommendations-variant | 1    | 1     | stringVariation                                            | ✓ Active |
| bulk-discount-enabled | 1      | 1     | boolVariation                                              | ✓ Active |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven of the nineteen usages resolve to a dynamic flag key. All seven originate from &lt;code&gt;flags-wrapper.ts&lt;/code&gt;, which accepts &lt;code&gt;flagKey&lt;/code&gt; as a parameter and proxies calls to the LaunchDarkly SDK. Grep would list those seven as equivalent entries alongside the statically-keyed calls in &lt;code&gt;checkout.ts&lt;/code&gt; and &lt;code&gt;pricing.ts&lt;/code&gt;. The AST scanner surfaces the wrapper boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring the flag debt
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;flaglint scan&lt;/code&gt; gives you the inventory. &lt;code&gt;flaglint audit&lt;/code&gt; adds risk classification, a readiness score, and an optional effort estimate in engineer-hours:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx flaglint audit examples/enterprise-checkout-service/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;-&lt;/span&gt; Auditing examples/enterprise-checkout-service/...
&lt;span class="gh"&gt;# FlagLint Audit Report&lt;/span&gt;

&lt;span class="gs"&gt;**Files scanned:**&lt;/span&gt; 16
&lt;span class="gs"&gt;**Duration:**&lt;/span&gt; 97ms

&lt;span class="gu"&gt;## Summary&lt;/span&gt;

| Total Flags | High Risk | Medium Risk | Total Usages |
|-------------|-----------|-------------|--------------|
| 13          | 3         | 10          | 27           |

| Dynamic Keys | Detail Evals | Bulk Calls | Stale Signals | Safely Automatable | Manual Review |
|--------------|--------------|------------|---------------|--------------------|---------------|
| 7            | 1            | 1          | 0             | 18                 | 9             |
&lt;span class="gt"&gt;
&amp;gt; **Staleness:** No staleness signals detected. Heuristics checked: keyword match&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; (flag key contains old/deprecated/legacy/temp/tmp/test/demo), path pattern&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; (test/spec/mock files, deprecated/old/legacy directories), and minFileCount threshold.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; Git-history-based staleness (last evaluation date) requires git metadata and is not&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; available in a pure static scan.&lt;/span&gt;

&lt;span class="gu"&gt;## Migration Readiness&lt;/span&gt;

Migration readiness: &lt;span class="gs"&gt;**67/100**&lt;/span&gt; · moderate

[█████████████████░░░░░░░░] 67%

18 safely automatable  ·  9 require manual review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The readiness score is the fraction of direct LaunchDarkly SDK call sites that FlagLint can rewrite automatically. A score of 67 means 18 of the 27 call sites are safely transformable. The remaining 9 require a human to resolve before an automated pass can run on those files.&lt;/p&gt;

&lt;p&gt;The staleness signal column surfaces flag keys whose names carry heuristic staleness signal — keywords like &lt;code&gt;old&lt;/code&gt;, &lt;code&gt;deprecated&lt;/code&gt;, &lt;code&gt;legacy&lt;/code&gt;, or &lt;code&gt;tmp&lt;/code&gt; in the flag key itself. Zero here means no staleness signal at the source level. Staleness detection does not require a LaunchDarkly API key or runtime data.&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;--effort-estimate&lt;/code&gt; to convert the count into a planning number:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx flaglint audit ./src &lt;span class="nt"&gt;--effort-estimate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This appends a three-phase estimate: automatable call sites at approximately 0.25 engineer-hours each, manual review call sites at 1.5–3 hours each, plus 30% overhead for validation and testing. Supplying &lt;code&gt;--hourly-rate 150&lt;/code&gt; appends a dollar range to the summary. The estimate is a planning heuristic calibrated to call-site complexity, not a billing projection.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three risk tiers in the flag debt inventory
&lt;/h2&gt;

&lt;p&gt;Every flag key in the audit report lands in one of three tiers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;High risk — cannot be automated:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;dynamic key&amp;gt;&lt;/code&gt; (7 usages across 3 files) — the flag key is a runtime variable, not a string literal. FlagLint marks every dynamic flag key as high risk because it cannot statically determine which flag is being evaluated, verify the call type, or confirm the return type. The resolution is to trace back to the call sites that supply the key parameter, then extract each unique flag key to a named constant. Re-running &lt;code&gt;flaglint audit&lt;/code&gt; after that change will reclassify the previously-dynamic entries as automatable.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;checkout-experiment&lt;/code&gt; (1 usage) — &lt;code&gt;boolVariationDetail&lt;/code&gt; is a detail evaluation call type. OpenFeature has a &lt;code&gt;getBooleanDetails&lt;/code&gt; equivalent, but the reason vocabulary differs: the LaunchDarkly SDK returns &lt;code&gt;TARGETING_MATCH&lt;/code&gt; and &lt;code&gt;RULE_MATCH&lt;/code&gt;; OpenFeature uses its own reason strings. Code that inspects &lt;code&gt;reason.kind&lt;/code&gt; or &lt;code&gt;reason.ruleId&lt;/code&gt; must be updated by hand alongside the call site.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;*&lt;/code&gt; (1 usage) — &lt;code&gt;allFlagsState&lt;/code&gt; is a bulk call with no OpenFeature provider equivalent. The resolution is to enumerate the specific flag keys the bulk call feeds and replace them with individual named-key calls. If full flag state at application startup is genuinely required, retain the LaunchDarkly SDK client for that bootstrap path while migrating all other call sites.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Medium risk — automatable with review:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;discount-config&lt;/code&gt; and &lt;code&gt;pricing-tier-config&lt;/code&gt; are &lt;code&gt;jsonVariation&lt;/code&gt; call types. They are safely automatable, but OpenFeature's object value API returns &lt;code&gt;unknown&lt;/code&gt;. After the rewrite, confirm that any code that casts or destructures the return value still compiles and behaves correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automatable — safe to transform:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Eight flag keys — &lt;code&gt;checkout-v2&lt;/code&gt;, &lt;code&gt;payment-provider&lt;/code&gt;, &lt;code&gt;one-click-checkout&lt;/code&gt;, &lt;code&gt;checkout-currency&lt;/code&gt;, &lt;code&gt;discount-percentage&lt;/code&gt;, &lt;code&gt;max-discount-amount&lt;/code&gt;, &lt;code&gt;recommendations-variant&lt;/code&gt;, and &lt;code&gt;bulk-discount-enabled&lt;/code&gt; — are called with &lt;code&gt;boolVariation&lt;/code&gt;, &lt;code&gt;stringVariation&lt;/code&gt;, or &lt;code&gt;numberVariation&lt;/code&gt; using static string literal flag keys. FlagLint can rewrite all of these.&lt;/p&gt;

&lt;h2&gt;
  
  
  The argument order inversion
&lt;/h2&gt;

&lt;p&gt;The automatable rewrite is not a text substitution. The LaunchDarkly SDK and OpenFeature provider place the fallback value and evaluation context in different argument positions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// LaunchDarkly SDK — (flagKey, context, fallback)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ldClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolVariation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checkout-v2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// OpenFeature provider — (flagKey, fallback, context)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;openFeatureClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBooleanValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checkout-v2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The flag key is identical. The fallback value and evaluation context swap positions. A naive find-and-replace migration that does not track argument order evaluates every flag with the wrong context on the first request and returns the wrong result silently. FlagLint's AST rewriter moves all three arguments to the correct positions for each automatable call type.&lt;/p&gt;

&lt;p&gt;Preview every transformation before any file is touched:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx flaglint migrate &lt;span class="nt"&gt;--dry-run&lt;/span&gt; ./src
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dry-run output shows a reviewable diff for each automatable call site alongside the OpenFeature provider setup steps. No files are modified.&lt;/p&gt;

&lt;h2&gt;
  
  
  Applying the migration plan and enforcing the boundary
&lt;/h2&gt;

&lt;p&gt;Once you have reviewed the dry-run output and set up the OpenFeature provider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx flaglint migrate &lt;span class="nt"&gt;--apply&lt;/span&gt; ./src
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This applies all safe rewrites in-place. Run your test suite after the apply. Then lock the boundary in CI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx flaglint validate &lt;span class="nt"&gt;--no-direct-launchdarkly&lt;/span&gt; ./src
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;flaglint validate&lt;/code&gt; exits non-zero when any direct LaunchDarkly SDK call is detected. Add it to your GitHub Actions workflow and direct LaunchDarkly SDK calls become a build failure from that point forward, blocking regressions as the migration lands across multiple PRs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clearing the manual review backlog incrementally
&lt;/h2&gt;

&lt;p&gt;Work through the high-risk items in batches. After each batch, re-run &lt;code&gt;flaglint audit&lt;/code&gt; to watch the readiness score climb. At 80 or above, the remaining feature flag technical debt in TypeScript can be handled in a single automated pass — &lt;code&gt;flaglint migrate --apply&lt;/code&gt; clears it and &lt;code&gt;flaglint validate --no-direct-launchdarkly&lt;/code&gt; confirms the boundary is clean.&lt;/p&gt;

&lt;p&gt;The audit plus the CI gate is a closed loop: audit measures what exists, migrate rewrites what is safe, validate blocks regressions, audit confirms progress. You can run the full cycle on a large codebase before writing a single line of migration code. The readiness score tells you up front whether you are looking at a two-sprint effort or a six-month program, and the migration plan tells you exactly which call sites require which kind of attention.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/docs/guides/launchdarkly-to-openfeature-nodejs/"&gt;LaunchDarkly to OpenFeature Node.js migration guide&lt;/a&gt; — the six-step workflow: audit, provider setup, dry-run, apply, validate&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/docs/guides/manual-review-patterns/"&gt;Manual review patterns&lt;/a&gt; — resolving dynamic flag keys, detail evaluations, and bulk calls before running migrate&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/docs/cli/audit/"&gt;&lt;code&gt;flaglint audit&lt;/code&gt; CLI reference&lt;/a&gt; — all options, output formats (JSON, Markdown, HTML), and exit codes&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/docs/concepts/migration-readiness/"&gt;Migration readiness concept&lt;/a&gt; — grade thresholds and the formula behind the readiness score&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/docs/tutorials/enforce-in-github-actions/"&gt;Enforce in GitHub Actions&lt;/a&gt; — CI workflow to block direct LaunchDarkly SDK regressions&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://flaglint.dev/blog/feature-flag-technical-debt-typescript/" rel="noopener noreferrer"&gt;flaglint.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>launchdarkly</category>
      <category>openfeature</category>
      <category>typescript</category>
      <category>devops</category>
    </item>
    <item>
      <title>Enforcing Your LaunchDarkly to OpenFeature Migration in GitHub Actions</title>
      <dc:creator>Krishan Sharma</dc:creator>
      <pubDate>Sun, 28 Jun 2026 20:39:50 +0000</pubDate>
      <link>https://dev.to/krishan_sharma_561a52817e/enforcing-your-launchdarkly-to-openfeature-migration-in-github-actions-1o3p</link>
      <guid>https://dev.to/krishan_sharma_561a52817e/enforcing-your-launchdarkly-to-openfeature-migration-in-github-actions-1o3p</guid>
      <description>&lt;p&gt;You started your LaunchDarkly to OpenFeature migration three weeks ago. The first sprint went well—five files converted, OpenFeature provider wired in, existing tests green. Then a teammate opened a PR for a new service. Inside it: two fresh &lt;code&gt;ldClient.boolVariation()&lt;/code&gt; calls. Not malicious. They just forgot. You merge it anyway because it is not worth blocking the PR over. Two weeks later there are six more.&lt;/p&gt;

&lt;p&gt;This is migration drift. It is the most common reason LaunchDarkly to OpenFeature migration projects stall: there is no gate on new direct LaunchDarkly SDK calls landing in main. Without a CI check that fails on any new call site, every PR can quietly add to the flag debt you are actively paying down.&lt;/p&gt;

&lt;p&gt;FlagLint addresses this with two commands—&lt;code&gt;audit&lt;/code&gt; to measure the existing flag debt and &lt;code&gt;validate&lt;/code&gt; to enforce the boundary—and a one-step GitHub Actions integration that adds the gate with two lines of YAML.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Baseline your flag debt before you gate
&lt;/h2&gt;

&lt;p&gt;Before you block anything in CI, run &lt;code&gt;flaglint audit&lt;/code&gt; against your source directory. This produces a readiness score and a per-flag-key inventory—the snapshot you will measure progress against, and the list you need when deciding what to exclude during the transition period.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx flaglint audit ./src
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real output from the &lt;code&gt;src/&lt;/code&gt; directory of the enterprise checkout service shipped with FlagLint examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;-&lt;/span&gt; Auditing examples/enterprise-checkout-service/src/...
&lt;span class="gh"&gt;# FlagLint Audit Report&lt;/span&gt;

&lt;span class="gs"&gt;**Scanned at:**&lt;/span&gt; 2026-06-24T03:20:27.050Z
&lt;span class="gs"&gt;**Scan root:**&lt;/span&gt; /home/user/flaglint/examples/enterprise-checkout-service/src
&lt;span class="gs"&gt;**Files scanned:**&lt;/span&gt; 5
&lt;span class="gs"&gt;**Duration:**&lt;/span&gt; 63ms

&lt;span class="gu"&gt;## Summary&lt;/span&gt;

| Total Flags | High Risk | Medium Risk | Total Usages |
|-------------|-----------|-------------|--------------|
| 13 | 3 | 10 | 19 |

| Dynamic Keys | Detail Evals | Bulk Calls | Stale Signals | Safely Automatable | Manual Review |
|--------------|--------------|------------|---------------|-------------------|---------------|
| 7 | 1 | 1 | 0 | 10 | 9 |

&lt;span class="gu"&gt;## Migration Readiness&lt;/span&gt;

Migration readiness: &lt;span class="gs"&gt;**53/100**&lt;/span&gt; · moderate

[█████████████░░░░░░░░░░░░] 53%

10 safely automatable  ·  9 require manual review

&lt;span class="gu"&gt;## Flag Debt Inventory&lt;/span&gt;

| Flag Key | Risk | Usages | Files | Call Types | Reasons |
|----------|------|--------|-------|------------|---------|
| &lt;span class="sb"&gt;`&amp;lt;dynamic key&amp;gt;`&lt;/span&gt; | 🔴 High | 7 | 3 | variationDetail, boolVariation, stringVariation, numberVariation, jsonVariation | dynamic key |
| &lt;span class="sb"&gt;`checkout-experiment`&lt;/span&gt; | 🔴 High | 1 | 1 | boolVariationDetail | detail evaluation |
| &lt;span class="sb"&gt;`*`&lt;/span&gt; | 🔴 High | 1 | 1 | allFlagsState | bulk call |
| &lt;span class="sb"&gt;`checkout-v2`&lt;/span&gt; | 🟢 Automatable | 1 | 1 | boolVariation | safely automatable |
| &lt;span class="sb"&gt;`payment-provider`&lt;/span&gt; | 🟢 Automatable | 1 | 1 | stringVariation | safely automatable |
| &lt;span class="sb"&gt;`one-click-checkout`&lt;/span&gt; | 🟢 Automatable | 1 | 1 | boolVariation | safely automatable |
| &lt;span class="sb"&gt;`checkout-currency`&lt;/span&gt; | 🟢 Automatable | 1 | 1 | stringVariation | safely automatable |
| &lt;span class="sb"&gt;`discount-percentage`&lt;/span&gt; | 🟢 Automatable | 1 | 1 | numberVariation | safely automatable |
| &lt;span class="sb"&gt;`max-discount-amount`&lt;/span&gt; | 🟢 Automatable | 1 | 1 | numberVariation | safely automatable |
| &lt;span class="sb"&gt;`discount-config`&lt;/span&gt; | 🟡 Medium | 1 | 1 | jsonVariation | safely automatable, json variation |
| &lt;span class="sb"&gt;`pricing-tier-config`&lt;/span&gt; | 🟡 Medium | 1 | 1 | jsonVariation | safely automatable, json variation |
| &lt;span class="sb"&gt;`recommendations-variant`&lt;/span&gt; | 🟢 Automatable | 1 | 1 | stringVariation | safely automatable |
| &lt;span class="sb"&gt;`bulk-discount-enabled`&lt;/span&gt; | 🟢 Automatable | 1 | 1 | boolVariation | safely automatable |

✓ Audit complete: 13 flags — 3 high risk, 10 medium risk

Migration readiness: 53/100  ·  moderate
[█████████████░░░░░░░░░░░░] 53%
10 safely automatable  ·  9 require manual review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The readiness score of 53 means 10 of the 19 direct LaunchDarkly SDK call sites can be automatically rewritten by &lt;code&gt;flaglint migrate --apply&lt;/code&gt;. The remaining 9 require manual work: 7 use a dynamic flag key (a variable, not a string literal), 1 is a detail evaluation returning reason metadata, and 1 is a bulk &lt;code&gt;allFlagsState&lt;/code&gt; call with no single-flag OpenFeature equivalent. The staleness signal count of zero means no flag keys carry source-level stale signal—no keys contain &lt;code&gt;old&lt;/code&gt;, &lt;code&gt;deprecated&lt;/code&gt;, &lt;code&gt;legacy&lt;/code&gt;, or &lt;code&gt;tmp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Save this output as your progress baseline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Run the validate command locally
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;flaglint validate --no-direct-launchdarkly&lt;/code&gt; exits non-zero when any direct LaunchDarkly SDK call is found in the scanned directory. Before wiring it into CI, run it locally so you know exactly what the gate will report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx flaglint validate ./src &lt;span class="nt"&gt;--no-direct-launchdarkly&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real output from the same &lt;code&gt;src/&lt;/code&gt; directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- Scanning examples/enterprise-checkout-service/src/...
✗ validate --no-direct-launchdarkly: 19 direct LaunchDarkly evaluation call(s) found.

  analytics.ts:51:43 — variationDetail("(dynamic key)")
  analytics.ts:76:23 — boolVariationDetail("checkout-experiment")
  analytics.ts:104:22 — allFlagsState(bulk inventory)
  checkout.ts:40:9 — boolVariation("checkout-v2")
  checkout.ts:49:9 — stringVariation("payment-provider")
  checkout.ts:58:9 — boolVariation("one-click-checkout")
  checkout.ts:67:9 — stringVariation("checkout-currency")
  flags-wrapper.ts:48:9 — boolVariation("(dynamic key)")
  flags-wrapper.ts:67:11 — boolVariation("(dynamic key)")
  flags-wrapper.ts:70:11 — stringVariation("(dynamic key)")
  flags-wrapper.ts:73:11 — numberVariation("(dynamic key)")
  flags-wrapper.ts:75:9 — jsonVariation("(dynamic key)")
  pricing.ts:46:9 — numberVariation("discount-percentage")
  pricing.ts:55:9 — numberVariation("max-discount-amount")
  pricing.ts:69:9 — jsonVariation("discount-config")
  pricing.ts:83:9 — jsonVariation("pricing-tier-config")
  product.ts:52:9 — boolVariation("(dynamic key)")
  product.ts:61:9 — stringVariation("recommendations-variant")
  product.ts:70:9 — boolVariation("bulk-discount-enabled")

These files must migrate to OpenFeature before this rule passes.
Run `flaglint migrate --dry-run` to review the migration plan.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each finding shows file path, line number, column, call type, and flag key. All call types are tracked: &lt;code&gt;boolVariation&lt;/code&gt;, &lt;code&gt;stringVariation&lt;/code&gt;, &lt;code&gt;numberVariation&lt;/code&gt;, &lt;code&gt;jsonVariation&lt;/code&gt;, &lt;code&gt;variationDetail&lt;/code&gt;, &lt;code&gt;boolVariationDetail&lt;/code&gt;, and &lt;code&gt;allFlagsState&lt;/code&gt;. Dynamic flag keys appear as &lt;code&gt;(dynamic key)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The gate exits non-zero the moment any direct LaunchDarkly SDK call is detected, blocking any new flag from bypassing the OpenFeature provider. When validate finds zero violations, it exits cleanly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✓ validate --no-direct-launchdarkly: no direct LaunchDarkly evaluation calls found.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That line is what you are working toward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Add the GitHub Actions gate
&lt;/h2&gt;

&lt;p&gt;FlagLint ships a composite GitHub Actions action. The minimum setup is two lines:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FlagLint&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;validate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;flaglint/flaglint@main&lt;/span&gt;
        &lt;span class="na"&gt;with&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;./src&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The action runs &lt;code&gt;flaglint validate ./src --no-direct-launchdarkly&lt;/code&gt; and exits 1 when any direct call is found. Do not set &lt;code&gt;continue-on-error: true&lt;/code&gt; on the FlagLint step. The job failing is the mechanism—that is what blocks the PR.&lt;/p&gt;

&lt;h3&gt;
  
  
  Excluding the bootstrap file
&lt;/h3&gt;

&lt;p&gt;Your OpenFeature provider setup module legitimately imports from the LaunchDarkly SDK to instantiate the provider. Exclude it with &lt;code&gt;--bootstrap-exclude&lt;/code&gt; so the gate does not fire on it:&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="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;flaglint/flaglint@main&lt;/span&gt;
  &lt;span class="na"&gt;with&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;./src&lt;/span&gt;
    &lt;span class="na"&gt;extra-args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--bootstrap-exclude&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"src/provider/setup.ts"'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can pass multiple exclusion patterns:&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;extra-args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
  &lt;span class="s"&gt;--bootstrap-exclude "src/provider/setup.ts"&lt;/span&gt;
  &lt;span class="s"&gt;--bootstrap-exclude "src/bootstrap/**"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The excluded files can call the LaunchDarkly SDK directly. Everything else cannot. The &lt;code&gt;--bootstrap-exclude&lt;/code&gt; flag accepts glob patterns, so a single &lt;code&gt;"src/provider/**"&lt;/code&gt; covers a provider directory with multiple files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Add SARIF annotations for inline PR diff visibility
&lt;/h2&gt;

&lt;p&gt;Job-level failure tells engineers something is wrong. SARIF annotations tell them exactly which line is wrong, directly in the PR diff. Add the SARIF upload step alongside the gate:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FlagLint Policy&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;validate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
      &lt;span class="na"&gt;security-events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&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;Validate no direct LaunchDarkly calls&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;flaglint&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;flaglint/flaglint@main&lt;/span&gt;
        &lt;span class="na"&gt;with&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;./src&lt;/span&gt;
          &lt;span class="na"&gt;extra-args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
            &lt;span class="s"&gt;--bootstrap-exclude "src/provider/setup.ts"&lt;/span&gt;
            &lt;span class="s"&gt;--format sarif&lt;/span&gt;
            &lt;span class="s"&gt;--output flaglint-validation.sarif&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;Upload validation SARIF&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github/codeql-action/upload-sarif@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;sarif_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;flaglint-validation.sarif&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SARIF findings use rule id &lt;code&gt;flaglint.direct-launchdarkly&lt;/code&gt;. With &lt;code&gt;security-events: write&lt;/code&gt;, GitHub annotates each violation inline on the relevant PR diff line as a code scanning alert. Set &lt;code&gt;if: always()&lt;/code&gt; on the upload step—not on the validate step—so GitHub receives the SARIF file even after the job fails, and annotations appear regardless of whether the PR passes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Managing the transition period
&lt;/h2&gt;

&lt;p&gt;If your service already has 19 direct LaunchDarkly SDK calls when you add the gate, CI will immediately fail. Two approaches handle the transition:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start with SARIF-only, then harden.&lt;/strong&gt; Set &lt;code&gt;continue-on-error: true&lt;/code&gt; temporarily on the validate step so violations surface as code scanning alerts without blocking merges. Remove &lt;code&gt;continue-on-error&lt;/code&gt; once you have migrated the bulk of existing call sites.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exclude directories that are mid-migration.&lt;/strong&gt; Use &lt;code&gt;--bootstrap-exclude&lt;/code&gt; patterns to allow files already in the migration queue through the gate while blocking any new file from adding a direct LaunchDarkly SDK call. Remove each exclusion as you migrate that directory.&lt;/p&gt;

&lt;p&gt;Re-run the audit after each sprint to track how the readiness score moves. The goal is a validate run that exits cleanly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✓ validate --no-direct-launchdarkly: no direct LaunchDarkly evaluation calls found.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When that is the consistent CI result, the LaunchDarkly to OpenFeature migration is structurally complete. No new direct call sites can land, and the codebase no longer carries flag debt pointing at the LaunchDarkly SDK.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/docs/guides/launchdarkly-to-openfeature-nodejs/"&gt;LaunchDarkly to OpenFeature Node.js migration guide&lt;/a&gt; — the full end-to-end workflow: audit, provider setup, dry-run, apply, validate&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/docs/guides/manual-review-patterns/"&gt;Manual Review Patterns&lt;/a&gt; — how to resolve dynamic flag keys, detail evaluations, and bulk calls before &lt;code&gt;migrate --apply&lt;/code&gt; can handle them automatically&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/docs/cli/audit/"&gt;&lt;code&gt;flaglint audit&lt;/code&gt; CLI reference&lt;/a&gt; — all options, output formats (JSON, Markdown, HTML), and exit behavior&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/docs/concepts/migration-readiness/"&gt;Migration Readiness concept&lt;/a&gt; — how the readiness score is calculated and what the grade thresholds mean&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/docs/integrations/github-actions/"&gt;GitHub Actions integration reference&lt;/a&gt; — full option table for the composite action, SARIF configuration, and rule id reference&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://flaglint.dev/blog/enforce-launchdarkly-migration-github-actions/" rel="noopener noreferrer"&gt;flaglint.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>launchdarkly</category>
      <category>openfeature</category>
      <category>typescript</category>
      <category>devops</category>
    </item>
    <item>
      <title>A provider outage can expose how deeply application code depends on a single feature-flag SDK.</title>
      <dc:creator>Krishan Sharma</dc:creator>
      <pubDate>Sun, 28 Jun 2026 20:36:52 +0000</pubDate>
      <link>https://dev.to/krishan_sharma_561a52817e/founder-and-maintainer-of-flaglint-5adb</link>
      <guid>https://dev.to/krishan_sharma_561a52817e/founder-and-maintainer-of-flaglint-5adb</guid>
      <description>&lt;p&gt;A provider outage can expose how deeply application code depends on a single&lt;br&gt;
feature-flag SDK. OpenFeature creates a neutral application boundary without&lt;br&gt;
forcing teams to abandon LaunchDarkly.&lt;/p&gt;

&lt;p&gt;This article walks through the local audit, migration preview, and CI enforcement&lt;br&gt;
path that lets teams add that boundary incrementally.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Vendor Lock-In Problem
&lt;/h2&gt;

&lt;p&gt;Direct SDK calls look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;LaunchDarkly&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;launchdarkly-node-server-sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ldClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;LaunchDarkly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LD_SDK_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ldClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolVariation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkout-v2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your application code is coupled to LaunchDarkly's API surface.&lt;br&gt;
Switching providers means rewriting every evaluation call.&lt;/p&gt;

&lt;p&gt;OpenFeature decouples this. Your application calls the OpenFeature API.&lt;br&gt;
The provider (LaunchDarkly, or anything else) is a configuration detail.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Migration That Stalls
&lt;/h2&gt;

&lt;p&gt;Most teams agree to add OpenFeature. Most migrations take 6+ weeks&lt;br&gt;
and nearly break production at least once.&lt;/p&gt;

&lt;p&gt;The reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No inventory of where direct SDK calls live&lt;/li&gt;
&lt;li&gt;Argument-order differences cause subtle production bugs&lt;/li&gt;
&lt;li&gt;Phased migrations partially reverse when new engineers join&lt;/li&gt;
&lt;li&gt;No CI enforcement to prevent new direct calls&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One of those reasons is an API difference that breaks even careful rewrites — &lt;a href="https://dev.to/blog/launchdarkly-openfeature-argument-order-bug/"&gt;the argument-order trap between LaunchDarkly and OpenFeature →&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What FlagLint Does
&lt;/h2&gt;

&lt;p&gt;Before you can migrate, you need to know what you're migrating.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx flaglint scan ./src
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AST-based inventory of every direct LaunchDarkly SDK call.&lt;br&gt;
File, line, call type, flag key, whether it's safely automatable.&lt;/p&gt;

&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```bash frame="none"&lt;br&gt;
flaglint migrate ./src --dry-run&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


Reviewable diffs for safe call sites. Argument order corrected.
Dynamic keys, detail methods, and bulk calls reported for manual review.

Then:



```bash frame="none"
flaglint validate ./src --no-direct-launchdarkly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CI gate. Fails the build if any new direct LD call appears.&lt;br&gt;
The migration doesn't rot.&lt;/p&gt;

&lt;h2&gt;
  
  
  LaunchDarkly Stays As Your Provider
&lt;/h2&gt;

&lt;p&gt;This is not a migration away from LaunchDarkly.&lt;/p&gt;

&lt;p&gt;LaunchDarkly offers an official OpenFeature provider. After the migration,&lt;br&gt;
LaunchDarkly still evaluates your flags — you're just calling the &lt;br&gt;
OpenFeature API instead of the LaunchDarkly SDK directly.&lt;/p&gt;

&lt;p&gt;The difference: if you need to switch providers, you change the provider &lt;br&gt;
configuration. Your application code doesn't change.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://dev.to/docs/quickstart"&gt;Start with the scan&lt;/a&gt;&lt;br&gt;&lt;br&gt;
→ &lt;a href="https://github.com/flaglint/flaglint" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Related:&lt;/strong&gt; &lt;a href="https://dev.to/blog/launchdarkly-openfeature-argument-order-bug/"&gt;Why LaunchDarkly → OpenFeature Migrations Break in Production →&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://flaglint.dev/blog/after-launchdarkly-outage-vendor-neutral-abstraction/" rel="noopener noreferrer"&gt;flaglint.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>launchdarkly</category>
      <category>openfeature</category>
      <category>typescript</category>
      <category>devops</category>
    </item>
    <item>
      <title>Your Node.js Codebase Has Flag Debt. Here's How to Find It in 30 Seconds.</title>
      <dc:creator>Krishan Sharma</dc:creator>
      <pubDate>Tue, 02 Jun 2026 15:54:10 +0000</pubDate>
      <link>https://dev.to/krishan_sharma_561a52817e/your-nodejs-codebase-has-flag-debt-heres-how-to-find-it-in-30-seconds-1pg1</link>
      <guid>https://dev.to/krishan_sharma_561a52817e/your-nodejs-codebase-has-flag-debt-heres-how-to-find-it-in-30-seconds-1pg1</guid>
      <description>&lt;p&gt;Most teams don't know how many feature flags are in their codebase.&lt;/p&gt;

&lt;p&gt;They know they have &lt;em&gt;some&lt;/em&gt;. They think they cleaned up &lt;em&gt;most&lt;/em&gt;. They're not sure about the rest.&lt;/p&gt;

&lt;p&gt;One command changes that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx flaglint audit ./src
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No API key. No credentials. No dashboard to sign up for. Just your source code and an honest answer.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;According to LaunchDarkly's best practices, most release flags should live for only days to weeks — yet many remain in codebases for months or years.&lt;/p&gt;

&lt;p&gt;That's not a LaunchDarkly problem. It's a universal one.&lt;/p&gt;

&lt;p&gt;Flags accumulate because adding one is fast and removing one is work. You ship the feature, move on, and the flag stays. Six months later a new engineer asks "is this safe to delete?" and nobody knows. So it stays another six months.&lt;/p&gt;

&lt;p&gt;Stale flags make code "more complex and harder to maintain," as developers spend extra time navigating obsolete conditionals. Unused toggles may degrade performance or even inadvertently expose features or data.&lt;/p&gt;

&lt;p&gt;And here's the part that stings: developers spend 33–42% of their time dealing with technical debt and maintenance. Feature flag debt is a quiet contributor to that number.&lt;/p&gt;




&lt;h2&gt;
  
  
  What "Flag Debt" Actually Looks Like
&lt;/h2&gt;

&lt;p&gt;Here's a real checkout service. Nothing exotic — a Node.js backend with LaunchDarkly calls spread across five files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// checkout.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isCheckoutV2Enabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;targetingKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;ldClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolVariation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checkout-v2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// discounts.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;flagKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`discount-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;experimentName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ldClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolVariation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flagKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// analytics.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ldClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allFlagsState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three different patterns. Three very different levels of risk.&lt;/p&gt;

&lt;p&gt;The first one is fine — static key, known type, safely removable.&lt;/p&gt;

&lt;p&gt;The second one is a problem — the key is a template literal. You can't statically know which flag it evaluates at runtime.&lt;/p&gt;

&lt;p&gt;The third one is a migration blocker — &lt;code&gt;allFlagsState&lt;/code&gt; has no OpenFeature equivalent. It requires an architecture decision before you touch it.&lt;/p&gt;

&lt;p&gt;Most teams treat all three the same. They shouldn't.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Audit Command
&lt;/h2&gt;

&lt;p&gt;FlagLint v0.6.0 ships with a new command: &lt;code&gt;flaglint audit&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It scans your codebase, classifies every flag call by risk level, and tells you exactly what you're dealing with — before you touch anything.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx flaglint@latest audit ./src
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running it against the enterprise checkout service above produces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✓ Audit complete: 13 flags — 3 high risk, 10 medium risk, 0 low risk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the full table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag Key&lt;/th&gt;
&lt;th&gt;Risk&lt;/th&gt;
&lt;th&gt;Usages&lt;/th&gt;
&lt;th&gt;Reasons&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dynamic&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;🔴 High&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;dynamic key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;checkout-experiment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;🔴 High&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;detail evaluation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;🔴 High&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;bulk call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;checkout-v2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;🟡 Medium&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;safely automatable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;payment-provider&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;🟡 Medium&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;safely automatable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;discount-config&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;🟡 Medium&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;safely automatable, json variation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&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;p&gt;&lt;strong&gt;High risk&lt;/strong&gt; means the call needs manual review before anything happens:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic key&lt;/strong&gt; — the flag key is a variable or template literal. FlagLint can't tell which flag you're evaluating.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detail evaluation&lt;/strong&gt; — &lt;code&gt;boolVariationDetail&lt;/code&gt; returns metadata with no direct OpenFeature equivalent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bulk call&lt;/strong&gt; — &lt;code&gt;allFlagsState&lt;/code&gt; is an architecture decision, not a line-by-line migration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Medium risk&lt;/strong&gt; means FlagLint can automate the migration safely, but the flag is still a direct vendor call that will need to move eventually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Low risk&lt;/strong&gt; means the flag is already migrated or has no debt signals.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three Output Formats
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Markdown — good for PRs and docs&lt;/span&gt;
npx flaglint audit ./src &lt;span class="nt"&gt;--format&lt;/span&gt; markdown

&lt;span class="c"&gt;# JSON — good for CI pipelines and dashboards&lt;/span&gt;
npx flaglint audit ./src &lt;span class="nt"&gt;--format&lt;/span&gt; json &lt;span class="nt"&gt;--output&lt;/span&gt; audit.json

&lt;span class="c"&gt;# HTML — shareable report for engineering reviews&lt;/span&gt;
npx flaglint audit ./src &lt;span class="nt"&gt;--format&lt;/span&gt; html &lt;span class="nt"&gt;--output&lt;/span&gt; flag-debt.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The HTML report is the one worth sharing with your team or your manager. It's a single self-contained file — no server needed, just open it in a browser. Drop it in a PR, a Jira ticket, or an email. It shows exactly which flags need attention and why.&lt;/p&gt;




&lt;h2&gt;
  
  
  From Audit to Action
&lt;/h2&gt;

&lt;p&gt;The audit is informational. It doesn't touch your code.&lt;/p&gt;

&lt;p&gt;Once you've seen your report, FlagLint has two more commands for when you're ready to act:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Preview the migration:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx flaglint migrate ./src &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This shows before/after diffs for every safely automatable call — the Medium risk flags from your audit. Nothing is written to disk.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;--- checkout.ts
&lt;/span&gt;&lt;span class="gi"&gt;+++ checkout.ts
&lt;/span&gt;&lt;span class="gd"&gt;-  return ldClient.boolVariation("checkout-v2", ctx, false);
&lt;/span&gt;&lt;span class="gi"&gt;+  return openFeatureClient.getBooleanValue("checkout-v2", false, ctx);
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the argument order. LaunchDarkly is &lt;code&gt;(flagKey, context, fallback)&lt;/code&gt;. OpenFeature is &lt;code&gt;(flagKey, fallback, context)&lt;/code&gt;. A manual find-and-replace migration silently swaps them — that's a production bug waiting to happen. FlagLint gets it right because it uses AST analysis, not text matching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apply the migration:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx flaglint migrate ./src &lt;span class="nt"&gt;--apply&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rewrites only the calls that are proven safe. Dynamic keys, detail methods, and bulk calls are skipped and reported for manual review. Won't run on a dirty git tree. Idempotent — safe to run twice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lock the boundary in CI:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx flaglint validate ./src &lt;span class="nt"&gt;--no-direct-launchdarkly&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exits 1 if any direct LaunchDarkly evaluation call appears in your codebase. Add this to your GitHub Actions workflow and the migration can't silently reverse.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why No API Key?
&lt;/h2&gt;

&lt;p&gt;Every major feature flag platform has a tool that connects to their API to show you flag health. Those tools are useful, but they only see flags that exist in their own system. They won't show you a LaunchDarkly flag if you're asking a Statsig dashboard.&lt;/p&gt;

&lt;p&gt;More importantly — they're built to keep you in their platform. No vendor builds the exit ramp for their own tool.&lt;/p&gt;

&lt;p&gt;FlagLint only looks at your source code. It doesn't know what's in your LaunchDarkly dashboard. It doesn't need to. The question it answers is: &lt;em&gt;what is your code actually doing right now?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That's a different question, and it has a different answer.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Full Workflow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Step 1: Understand what you have&lt;/span&gt;
npx flaglint audit ./src &lt;span class="nt"&gt;--format&lt;/span&gt; html &lt;span class="nt"&gt;--output&lt;/span&gt; flag-debt.html

&lt;span class="c"&gt;# Step 2: Preview safe migrations&lt;/span&gt;
npx flaglint migrate ./src &lt;span class="nt"&gt;--dry-run&lt;/span&gt;

&lt;span class="c"&gt;# Step 3: Apply the safe ones&lt;/span&gt;
npx flaglint migrate ./src &lt;span class="nt"&gt;--apply&lt;/span&gt;

&lt;span class="c"&gt;# Step 4: Lock it in CI&lt;/span&gt;
npx flaglint validate ./src &lt;span class="nt"&gt;--no-direct-launchdarkly&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four commands. Covers the full lifecycle from "what do we have?" to "this can never regress."&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx flaglint@latest audit ./src
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No install required. Works on any Node.js 20+ project using the LaunchDarkly Node.js server SDK (both &lt;code&gt;launchdarkly-node-server-sdk&lt;/code&gt; and &lt;code&gt;@launchdarkly/node-server-sdk&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://github.com/flaglint/flaglint" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; — MIT licensed, open source&lt;br&gt;&lt;br&gt;
→ &lt;a href="https://www.npmjs.com/package/flaglint" rel="noopener noreferrer"&gt;npm&lt;/a&gt;&lt;br&gt;&lt;br&gt;
→ &lt;a href="https://flaglint.dev/docs/cli/audit" rel="noopener noreferrer"&gt;Docs&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;FlagLint is a vendor-neutral CLI for LaunchDarkly → OpenFeature migrations. v0.6.0 adds flag debt auditing. &lt;a href="https://github.com/flaglint/flaglint/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;Read the changelog →&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; &lt;code&gt;node&lt;/code&gt; &lt;code&gt;devops&lt;/code&gt; &lt;code&gt;javascript&lt;/code&gt; &lt;code&gt;typescript&lt;/code&gt; &lt;code&gt;openfeature&lt;/code&gt; &lt;code&gt;launchdarkly&lt;/code&gt; &lt;code&gt;featureflags&lt;/code&gt; &lt;code&gt;technicaldebt&lt;/code&gt; &lt;code&gt;opensource&lt;/code&gt; &lt;code&gt;featureflags&lt;/code&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Standardizing Feature Flags Is Easy to Agree On. Migrating Safely Is the Hard Part.</title>
      <dc:creator>Krishan Sharma</dc:creator>
      <pubDate>Thu, 28 May 2026 12:02:08 +0000</pubDate>
      <link>https://dev.to/krishan_sharma_561a52817e/standardizing-feature-flags-is-easy-to-agree-on-migrating-safely-is-the-hard-part-1k39</link>
      <guid>https://dev.to/krishan_sharma_561a52817e/standardizing-feature-flags-is-easy-to-agree-on-migrating-safely-is-the-hard-part-1k39</guid>
      <description>&lt;h2&gt;
  
  
  Why I built FlagLint, an open-source CLI for moving direct LaunchDarkly Node.js usage behind an OpenFeature boundary
&lt;/h2&gt;

&lt;p&gt;Feature flags usually begin as a simple engineering decision.&lt;/p&gt;

&lt;p&gt;A team needs to release gradually. A developer adds a flag. The application evaluates it through the provider SDK. The rollout succeeds.&lt;/p&gt;

&lt;p&gt;Then the pattern repeats.&lt;/p&gt;

&lt;p&gt;One feature becomes ten. One service becomes dozens. Over time, application code starts to accumulate direct calls to a provider-specific API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ldClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolVariation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checkout-v2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is nothing wrong with that call by itself. LaunchDarkly is doing exactly what it is supposed to do: providing feature-flag management and evaluation.&lt;/p&gt;

&lt;p&gt;The problem appears later, when a platform team wants a consistent application-facing abstraction across services.&lt;/p&gt;

&lt;p&gt;Maybe the organization wants to standardize on &lt;a href="https://openfeature.dev/" rel="noopener noreferrer"&gt;OpenFeature&lt;/a&gt;, the CNCF-incubating, vendor-agnostic feature-flag API. Maybe teams want feature-flag instrumentation, governance, or shared patterns implemented once at a platform boundary instead of separately inside each service.&lt;/p&gt;

&lt;p&gt;At that point, the difficult question is no longer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Should we standardize feature-flag evaluation?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The difficult questions are:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Where are all the direct SDK calls? Which ones can be migrated safely? Which ones require human review? And how do we stop new direct calls from returning after migration?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the problem I built &lt;strong&gt;&lt;a href="https://flaglint.dev/" rel="noopener noreferrer"&gt;FlagLint&lt;/a&gt;&lt;/strong&gt; to solve.&lt;/p&gt;




&lt;h2&gt;
  
  
  OpenFeature Does Not Mean Replacing LaunchDarkly
&lt;/h2&gt;

&lt;p&gt;A common misconception in migration conversations is that introducing OpenFeature means replacing the current feature-flag provider.&lt;/p&gt;

&lt;p&gt;It does not.&lt;/p&gt;

&lt;p&gt;OpenFeature standardizes the evaluation API that application code uses. A provider still performs the actual evaluation. LaunchDarkly offers an official OpenFeature provider for its Node.js server-side SDK, so a Node.js service can continue using LaunchDarkly as its feature-flag provider while application code evaluates flags through OpenFeature.&lt;/p&gt;

&lt;p&gt;Conceptually, the change looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Direct provider-specific application usage&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ldClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolVariation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checkout-v2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;becoming:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Standard application-facing evaluation API&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;openFeatureClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBooleanValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checkout-v2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;LaunchDarkly remains behind the provider boundary. The application code moves toward a standard interface.&lt;/p&gt;

&lt;p&gt;That distinction matters. Platform standardization should not require an unnecessary provider replacement project.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Migration Is More Dangerous Than It Looks
&lt;/h2&gt;

&lt;p&gt;At first glance, this appears to be a simple codemod problem: find a method call and rename it.&lt;/p&gt;

&lt;p&gt;But feature-flag migrations carry runtime behavior. An incorrect transformation can change what users see in production.&lt;/p&gt;

&lt;p&gt;For example, the LaunchDarkly and OpenFeature method signatures differ in argument order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// LaunchDarkly&lt;/span&gt;
&lt;span class="nx"&gt;ldClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolVariation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// OpenFeature&lt;/span&gt;
&lt;span class="nx"&gt;openFeatureClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBooleanValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A careless rewrite can silently swap the fallback value and context.&lt;/p&gt;

&lt;p&gt;There are additional complications:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a flag key may be a dynamic expression rather than a static string;&lt;/li&gt;
&lt;li&gt;a call may request evaluation details rather than only a flag value;&lt;/li&gt;
&lt;li&gt;code may use bulk flag-state APIs;&lt;/li&gt;
&lt;li&gt;a fallback type may not be statically clear;&lt;/li&gt;
&lt;li&gt;a service may import a shared platform-owned OpenFeature client using a local alias;&lt;/li&gt;
&lt;li&gt;the provider bootstrap file may legitimately reference LaunchDarkly directly;&lt;/li&gt;
&lt;li&gt;asynchronous behavior must be preserved exactly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In a migration involving feature flags, “we rewrote most of it automatically” is not enough. The important question is whether the automation knows when &lt;strong&gt;not&lt;/strong&gt; to rewrite code.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Design Principle Behind FlagLint: Conservative Automation
&lt;/h2&gt;

&lt;p&gt;FlagLint is an open-source CLI focused on a specific workflow:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Help Node.js teams inventory direct LaunchDarkly server SDK evaluations, migrate only provably safe call sites to OpenFeature, and enforce the new boundary in CI.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Its scope is deliberately narrow today. FlagLint supports JavaScript and TypeScript code using the LaunchDarkly Node.js server-side SDK package forms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@launchdarkly/node-server-sdk&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;launchdarkly-node-server-sdk&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is not trying to claim every language, every SDK, or every feature-flag lifecycle problem.&lt;/p&gt;

&lt;p&gt;FlagLint uses AST-based analysis to identify direct LaunchDarkly Node.js evaluation calls. It can automatically transform typed static evaluations only when the important parts are explicit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the flag key;&lt;/li&gt;
&lt;li&gt;the fallback value and type;&lt;/li&gt;
&lt;li&gt;the evaluation context;&lt;/li&gt;
&lt;li&gt;a proven OpenFeature client binding.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ldClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolVariation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checkout-v2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can safely become:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// After&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;openFeatureClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBooleanValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checkout-v2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But FlagLint does &lt;strong&gt;not&lt;/strong&gt; automatically rewrite uncertain patterns. Dynamic keys, detail evaluation methods, bulk calls, unknown fallbacks, browser or React SDK usage, and ambiguous bindings remain visible for human review.&lt;/p&gt;

&lt;p&gt;This is not a limitation to hide. It is the safety model.&lt;/p&gt;

&lt;p&gt;A migration tool should automate the obvious cases and make uncertainty impossible to ignore.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Workflow: Scan, Migrate, Enforce
&lt;/h2&gt;

&lt;p&gt;FlagLint is designed around three steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Inventory direct SDK coupling
&lt;/h3&gt;

&lt;p&gt;Before a migration starts, teams need to understand what exists in the codebase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx flaglint scan ./src
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scan identifies direct LaunchDarkly Node.js server SDK evaluation calls, their files, flag keys, call types, and patterns that need manual review. Reports can be emitted in formats including Markdown, JSON, HTML, and SARIF.&lt;/p&gt;

&lt;p&gt;This is useful for both developers and platform teams. Before changing code, you can see whether a service is mostly straightforward or full of patterns that require careful migration planning.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Preview and apply safe transformations
&lt;/h3&gt;

&lt;p&gt;Next, teams can generate a migration plan and inspect diffs before touching source code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx flaglint migrate ./src &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;FlagLint generates reviewable before/after diffs. When a file already contains a proven OpenFeature client binding, including an approved imported shared client binding, the preview uses that exact binding.&lt;/p&gt;

&lt;p&gt;For example, a service may already import a platform-owned client with an alias:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;openFeatureClient&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;flags&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../platform/feature-flags.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;FlagLint preserves that architecture and previews the migration using &lt;code&gt;flags.getBooleanValue(...)&lt;/code&gt; rather than inventing a new local client.&lt;/p&gt;

&lt;p&gt;When ready, safe transformations can be applied:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx flaglint migrate ./src &lt;span class="nt"&gt;--apply&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The apply mode is guarded. It requires a proven OpenFeature client binding, refuses to write into a dirty Git working tree unless explicitly overridden, does not insert provider bootstrap setup automatically, and does not rewrite uncertain patterns.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Prevent the migration from reversing
&lt;/h3&gt;

&lt;p&gt;Migration is not complete if new direct provider SDK calls can quietly appear in future pull requests.&lt;/p&gt;

&lt;p&gt;Once a service has completed the boundary migration, FlagLint can enforce it in CI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx flaglint validate ./src &lt;span class="nt"&gt;--no-direct-launchdarkly&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Validation can emit SARIF findings so direct LaunchDarkly policy violations can appear as annotations in code scanning and pull-request review flows.&lt;/p&gt;

&lt;p&gt;This turns a one-time refactor into an enforceable engineering standard.&lt;/p&gt;




&lt;h2&gt;
  
  
  An Important Boundary: FlagLint Is Not a Stale-Flag Platform
&lt;/h2&gt;

&lt;p&gt;There is a broader category of feature-flag debt: flags that are fully rolled out, inactive, unused at runtime, or ready for deletion in the provider platform.&lt;/p&gt;

&lt;p&gt;That is an important problem, but it requires lifecycle and runtime information that source code alone cannot prove.&lt;/p&gt;

&lt;p&gt;FlagLint does not currently claim to delete stale flags, inspect production evaluations, replace LaunchDarkly lifecycle tooling, or reduce feature-flag billing.&lt;/p&gt;

&lt;p&gt;Its current problem is more precise:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Where is application code directly coupled to the LaunchDarkly Node.js SDK, what can move safely behind OpenFeature, and can we enforce that boundary afterward?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That focus is intentional. Tools earn trust by being accurate about what they know and what they do not know.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned Building the First Release
&lt;/h2&gt;

&lt;p&gt;The technical work was only one part of the project. The harder part was defining safe behavior.&lt;/p&gt;

&lt;p&gt;A migration CLI for application infrastructure cannot be aggressive by default. It needs to be explainable. Every automated rewrite should be reviewable. Every skipped pattern should make sense to the engineer reading the report.&lt;/p&gt;

&lt;p&gt;During post-release validation of FlagLint v0.5.0, I found a good example of why that matters. A dry-run using a proven aliased OpenFeature client binding correctly previewed a safe transformation, but the CLI still printed global guidance implying provider setup was required. The transformation itself was correct, but the message was contradictory.&lt;/p&gt;

&lt;p&gt;That mattered because trust in migration tools is not only about whether they edit code correctly. It is also about whether their explanations match their behavior.&lt;/p&gt;

&lt;p&gt;In v0.5.1, I fixed that messaging so proven bindings are described accurately, missing bindings still receive setup guidance, and mixed cases clearly scope guidance only to diffs that need it.&lt;/p&gt;

&lt;p&gt;No safety boundary was weakened to make the tool appear more capable.&lt;/p&gt;

&lt;p&gt;That is the standard I want FlagLint to maintain as it grows.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Comes Next
&lt;/h2&gt;

&lt;p&gt;The next challenge is not just transforming code. It is helping real teams adopt the boundary incrementally.&lt;/p&gt;

&lt;p&gt;In a small service, a team may be able to migrate all direct SDK calls and enable strict CI enforcement immediately.&lt;/p&gt;

&lt;p&gt;In an established codebase, that may not be realistic. A platform team may find hundreds of existing direct evaluations spread across services. They still need a way to prevent &lt;em&gt;new&lt;/em&gt; vendor-coupled access while reducing existing usage over time.&lt;/p&gt;

&lt;p&gt;That leads to the next direction for FlagLint: team adoption and governance workflows, including the ability to measure migration readiness across services and support gradual CI rollout without requiring a big-bang refactor.&lt;/p&gt;

&lt;p&gt;The goal remains simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Make the safer architecture easier to adopt than the shortcut.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Try FlagLint
&lt;/h2&gt;

&lt;p&gt;FlagLint is open source and available today as v0.5.1.&lt;/p&gt;

&lt;p&gt;Run it against a Node.js service using the LaunchDarkly server-side SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx flaglint scan &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then explore a reviewable migration preview:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx flaglint migrate &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Resources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Website: &lt;a href="https://flaglint.dev/" rel="noopener noreferrer"&gt;flaglint.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/flaglint/flaglint" rel="noopener noreferrer"&gt;github.com/flaglint/flaglint&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/flaglint" rel="noopener noreferrer"&gt;npmjs.com/package/flaglint&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenFeature: &lt;a href="https://openfeature.dev/" rel="noopener noreferrer"&gt;openfeature.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;LaunchDarkly OpenFeature Node.js provider documentation: &lt;a href="https://launchdarkly.com/docs/sdk/openfeature/node-js" rel="noopener noreferrer"&gt;LaunchDarkly docs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I would especially value feedback from Node.js backend engineers and platform teams already using LaunchDarkly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What direct SDK patterns exist in your repositories?&lt;/li&gt;
&lt;li&gt;Do you use internal wrappers or shared feature-flag clients?&lt;/li&gt;
&lt;li&gt;What would make an OpenFeature migration safe enough to evaluate in a real service?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The best developer tools are shaped by the real codebases they have to survive.&lt;/p&gt;

&lt;h1&gt;
  
  
  devops #typescript #javascript #opensource #featureflags
&lt;/h1&gt;

</description>
    </item>
  </channel>
</rss>
