flaglint 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,54 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ## [0.4.1] - 2026-05-25
11
+
12
+ ### Fixed
13
+
14
+ - Detail evaluation methods are classified as manual review and excluded from safe auto-transform counts.
15
+ - Generated LaunchDarkly OpenFeature provider setup now uses the SDK key constructor correctly.
16
+ - Evaluation-context guidance now states that either OpenFeature `targetingKey` or existing LaunchDarkly `key` is accepted.
17
+ - Default one-file flags no longer trigger staleness solely because they occur in one file; explicit `minFileCount: 1` remains available.
18
+ - Reporter output now says `Flags with review signals` instead of implying flags are safe to remove.
19
+ - Public early-preview messaging now states the Node.js server-side migration scope and review/testing requirement.
20
+
21
+ ## [0.4.0] - 2026-05-24
22
+
23
+ ### Added
24
+
25
+ - Release preparation: bump package version to 0.4.0, normalize repository URL, and adjust release workflow to publish only from manual GitHub Releases. No publish or tag created by this change.
26
+
27
+ ### Added
28
+
29
+ - **`flaglint migrate --dry-run`**: Generates reviewable before/after diffs for every automatable
30
+ call-site, including inline provider setup guidance (packages, bootstrap file, `targetingKey`
31
+ context requirement). Does not write any files; output is to stdout.
32
+
33
+ - **Docs**: Repositioned public copy and website messaging to explicitly state scope (LaunchDarkly Node.js server SDK only), clarify that `--apply` is guarded, confirm provider/bootstrap setup is manual, and limit precision/recall claims to the 120 deterministic benchmark cases within that supported scope.
34
+
35
+ - **`flaglint migrate --apply`**: Applies only guarded, provably automatable transformations
36
+ in-place. Safety contracts: refuses on a dirty git working tree (override with `--allow-dirty`);
37
+ skips any file without a proven `openFeatureClient = OpenFeature.getClient()` binding from
38
+ `@openfeature/server-sdk` (AST-grounded, not regex); never rewrites detail methods, dynamic
39
+ keys, unknown fallbacks, or bulk calls; preserves `await` and all call arguments exactly;
40
+ idempotent (re-running a stale analysis is a no-op via range-content guard).
41
+
42
+ - **`flaglint validate [dir]`**: New command for CI enforcement.
43
+ - Without `--no-direct-launchdarkly`: reports usages, always exits 0.
44
+ - `--no-direct-launchdarkly`: exits 1 if any direct LaunchDarkly Node server evaluation call
45
+ is found (static, dynamic, detail, or bulk — all count as violations).
46
+ - `--bootstrap-exclude <glob>` (repeatable): exclude provider bootstrap files from violations.
47
+ Supports exact paths, `*` (within one directory), `**` (across directories), and `?` wildcards.
48
+ - Never claims flags are stale or safe to delete.
49
+
50
+ ### Scope clarification
51
+
52
+ Current scope: **LaunchDarkly Node.js server-side SDK** (`launchdarkly-node-server-sdk`).
53
+ React hooks, HOC, and client-side SDK patterns are detected by `scan` but are not automatically
54
+ migrated by `--apply`.
55
+
8
56
  ## [0.3.0] - 2026-05-23
9
57
 
10
58
  ### Added
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  </p>
4
4
 
5
5
  <p align="center">
6
- <strong>Your LaunchDarkly codebase has flag debt. FlagLint tells you exactly what and where.</strong>
6
+ <strong>LaunchDarkly Node.js server SDK -> OpenFeature migration</strong>
7
7
  </p>
8
8
 
9
9
  <p align="center">
@@ -21,35 +21,44 @@
21
21
  </a>
22
22
  </p>
23
23
 
24
+ > [!WARNING]
25
+ > FlagLint is currently an early preview.
26
+ >
27
+ > Automatic migration currently supports LaunchDarkly Node.js server-side SDK evaluation calls only. Generated changes must be reviewed and tested before merging.
28
+ >
29
+ > React hooks, higher-order components, browser/client-side SDK usage, bulk flag-state calls, detail evaluations, dynamic keys, and custom wrappers are not automatically migrated in this release.
24
30
 
25
31
  # FlagLint
26
32
 
27
- Find zombie flags. Eliminate flag debt. Generate your OpenFeature
28
- migration plan.
33
+ FlagLint inventories direct LaunchDarkly Node.js server SDK calls in your TypeScript/JavaScript
34
+ codebase, generates reviewable OpenFeature migration diffs, applies only guarded transformations,
35
+ and enforces migration state in CI.
29
36
 
30
- ---
31
-
32
- ## The problem
37
+ **LaunchDarkly remains your provider. OpenFeature becomes the evaluation API your application code calls.**
33
38
 
34
- LaunchDarkly flags accumulate. Teams add them, forget to clean them up, and gradually build flag debt — dead code paths controlled by flags nobody manages. When you finally want to migrate to OpenFeature, you don't even know what you have.
39
+ ---
35
40
 
36
- Like Uber's Piranha — for any JS/TS codebase.
41
+ ## Workflow
37
42
 
38
- **FlagLint fixes this.** It scans your codebase, maps every flag usage, identifies stale candidates, and generates a step-by-step OpenFeature migration plan.
43
+ | Step | Command | Purpose |
44
+ |------|---------|---------|
45
+ | 1 | `flaglint scan` | AST inventory of every direct LD Node server SDK call |
46
+ | 2 | `flaglint migrate --dry-run` | Reviewable before/after diffs with provider setup guidance |
47
+ | 3 | `flaglint migrate --apply` | Apply only guarded, provably automatable transformations |
48
+ | 4 | `flaglint validate --no-direct-launchdarkly` | CI gate: exit 1 if direct LD calls remain |
39
49
 
40
50
  ---
41
51
 
42
52
  ## Quick start
43
53
 
44
54
  ```bash
45
- npx flaglint scan
55
+ npx flaglint scan ./src
46
56
  ```
47
57
 
48
58
  Example output:
49
59
 
50
60
  ```text
51
61
  ✓ 15 flag usages found across 6 unique flags (48ms)
52
- ⚠ 5 potentially stale flag(s) — review recommended
53
62
  ℹ 1 dynamic flag key(s) require manual review
54
63
  ```
55
64
 
@@ -57,34 +66,25 @@ Markdown report excerpt:
57
66
 
58
67
  ```markdown
59
68
  ## Flag Inventory
60
- | Flag Key | Usages | Files | Call Types | Status |
61
- |----------|--------|-------|------------|--------|
62
- | show-banner | 1 | 1 | variation | ✓ Active |
63
- | old-checkout | 1 | 1 | variation | ⚠ Stale |
64
- | temp-debug-mode | 1 | 1 | variation | ⚠ Stale |
65
-
66
- ## ⚠ Stale Flag Candidates
67
- | Flag Key | Reason | Location |
68
- |----------|--------|----------|
69
- | old-checkout | Contains "old" in key | ld-stale.ts:1 |
69
+ | Flag Key | Usages | Files | Call Types |
70
+ |----------|--------|-------|------------|
71
+ | checkout-v2 | 3 | 2 | boolVariation |
72
+ | color-theme | 1 | 1 | stringVariation |
73
+ | timeout-ms | 1 | 1 | numberVariation |
70
74
  ```
71
75
 
72
76
  ### JSON output (`--format json`)
73
77
 
74
- Pipe-friendly. Every usage includes file, line, call type,
75
- and structured staleness signals:
78
+ Pipe-friendly. Every usage includes file, line, call type, and staleness signals:
76
79
 
77
80
  ```json
78
81
  {
79
- "flagKey": "old-checkout",
82
+ "flagKey": "checkout-v2",
80
83
  "isDynamic": false,
81
- "file": "src/components/Checkout.tsx",
84
+ "file": "src/services/checkout.ts",
82
85
  "line": 14,
83
- "callType": "variation",
84
- "stalenessSignals": [
85
- { "source": "keyword", "keyword": "old" },
86
- { "source": "minFileCount", "fileCount": 1, "threshold": 1 }
87
- ]
86
+ "callType": "boolVariation",
87
+ "stalenessSignals": []
88
88
  }
89
89
  ```
90
90
 
@@ -104,7 +104,7 @@ npx flaglint
104
104
 
105
105
  ### `flaglint scan [dir]`
106
106
 
107
- Scans a directory for LaunchDarkly SDK usage.
107
+ AST-based inventory of direct LaunchDarkly Node.js server SDK calls.
108
108
 
109
109
  ```bash
110
110
  flaglint scan ./src
@@ -120,7 +120,8 @@ flaglint scan --format sarif --output flaglint.sarif
120
120
  | `--config` | auto-detect | Path to a config file |
121
121
  | `--exclude-tests` | — | Exclude test files from scan results |
122
122
 
123
- Exit code `0` when no stale flags found, `1` when stale flags exist enabling CI blocking.
123
+ Exit code `0` when no staleness signals detected, `1` when staleness signals are present
124
+ enabling CI visibility into flag usage patterns.
124
125
 
125
126
  ---
126
127
 
@@ -129,17 +130,144 @@ Exit code `0` when no stale flags found, `1` when stale flags exist — enabling
129
130
  Analyzes migration readiness and generates an OpenFeature migration plan.
130
131
 
131
132
  ```bash
132
- flaglint migrate ./src
133
- flaglint migrate --dry-run
134
- flaglint migrate --output MIGRATION.md
133
+ flaglint migrate ./src # write MIGRATION.md
134
+ flaglint migrate --dry-run # reviewable diffs to stdout
135
+ flaglint migrate --apply # guarded: apply only provably automatable transformations in-place
136
+ flaglint migrate --apply --allow-dirty # apply even on a dirty working tree
137
+ flaglint migrate --output plan.md # write to custom file
138
+ flaglint migrate --exclude-tests # skip test and spec files
135
139
  ```
136
140
 
137
141
  | Option | Default | Description |
138
142
  |--------|---------|-------------|
139
143
  | `--output` | `MIGRATION.md` | Write migration plan to file |
140
- | `--dry-run` | — | Print plan to stdout, do not write file |
144
+ | `--dry-run` | — | Print reviewable diffs to stdout; includes provider setup guidance |
145
+ | `--apply` | — | Apply automatable transformations in-place (requires clean git tree) |
146
+ | `--allow-dirty` | — | Override dirty-tree guard for `--apply` |
147
+ | `--config` | auto-detect | Path to a config file |
148
+ | `--exclude-tests` | — | Skip `*.test.*`, `*.spec.*`, `__tests__/`, `tests/` |
149
+
150
+ **`--apply` safety contracts:**
151
+ - Refuses on a dirty git working tree unless `--allow-dirty`
152
+ - Skips any file that does not already contain a proven `openFeatureClient` binding
153
+ (`openFeatureClient = OpenFeature.getClient()` from `@openfeature/server-sdk`)
154
+ - Never touches detail methods, dynamic keys, unknown fallbacks, or bulk calls
155
+ - Preserves `await` and original call arguments exactly
156
+ - Idempotent: re-running with the same analysis has no effect
157
+
158
+ ---
159
+
160
+ ### `flaglint validate [dir]`
161
+
162
+ Validates that your codebase complies with feature flag policy rules.
163
+ Designed for CI enforcement after migration is complete.
164
+
165
+ ```bash
166
+ flaglint validate # report usages, always exits 0
167
+ flaglint validate --no-direct-launchdarkly # exit 1 on any direct LD eval call
168
+ flaglint validate --no-direct-launchdarkly \
169
+ --bootstrap-exclude src/provider/setup.ts # allow specific bootstrap file
170
+ flaglint validate --no-direct-launchdarkly \
171
+ --bootstrap-exclude "src/provider/**" # allow all provider-directory files
172
+ ```
173
+
174
+ | Option | Default | Description |
175
+ |--------|---------|-------------|
176
+ | `--no-direct-launchdarkly` | — | Exit 1 if any direct LD Node server evaluation calls found |
177
+ | `--bootstrap-exclude <glob>` | — | Repeatable glob; matching files excluded from violations |
141
178
  | `--config` | auto-detect | Path to a config file |
142
179
 
180
+ Exit codes: `0` = passed, `1` = violations found, `130` = SIGINT.
181
+
182
+ **Example pass output:**
183
+ ```
184
+ ✓ validate --no-direct-launchdarkly: no direct LaunchDarkly evaluation calls found.
185
+ Scanned 42 file(s).
186
+ ```
187
+
188
+ **Example fail output:**
189
+ ```
190
+ ✗ validate --no-direct-launchdarkly: 2 direct LaunchDarkly evaluation call(s) found.
191
+
192
+ src/services/checkout.ts:42:8 — boolVariation("checkout-v2")
193
+ src/services/pricing.ts:17:4 — boolVariation(dynamic key — manual review required)
194
+
195
+ These files must migrate to OpenFeature before this rule passes.
196
+ Run `flaglint migrate --dry-run` to review the migration plan.
197
+ ```
198
+
199
+ ---
200
+
201
+ ## Supported API matrix
202
+
203
+ **Scope: LaunchDarkly Node.js server-side SDK** (`launchdarkly-node-server-sdk`).
204
+
205
+ | LaunchDarkly call | Automatable | OpenFeature equivalent |
206
+ |---|---|---|
207
+ | `ldClient.boolVariation(key, ctx, false)` | ✓ | `openFeatureClient.getBooleanValue(key, false, ctx)` |
208
+ | `ldClient.stringVariation(key, ctx, "")` | ✓ | `openFeatureClient.getStringValue(key, "", ctx)` |
209
+ | `ldClient.numberVariation(key, ctx, 0)` | ✓ | `openFeatureClient.getNumberValue(key, 0, ctx)` |
210
+ | `ldClient.jsonVariation(key, ctx, {})` | ✓ | `openFeatureClient.getObjectValue(key, {}, ctx)` |
211
+ | `ldClient.*VariationDetail(...)` | ✗ manual | Detail result shapes differ — requires manual review |
212
+ | Dynamic flag key | ✗ manual | Key must be a static string literal |
213
+ | `ldClient.allFlags()` / `allFlagsState()` | ✗ manual | Bulk calls — no single-flag codemod |
214
+ | Unknown fallback type | ✗ manual | Fallback type must be determinable statically |
215
+
216
+ `flaglint scan` and `flaglint migrate --dry-run` report detected LaunchDarkly Node.js server-side SDK patterns, including manual-review cases.
217
+ `flaglint migrate --apply` rewrites only the ✓ rows above.
218
+
219
+ ---
220
+
221
+ ## Provider setup (one-time manual step)
222
+
223
+ `flaglint migrate --dry-run` includes this guidance inline. **Complete provider setup in
224
+ one dedicated file before running `--apply`.**
225
+
226
+ ```bash
227
+ npm install @openfeature/server-sdk \
228
+ @launchdarkly/node-server-sdk \
229
+ @launchdarkly/openfeature-node-server
230
+ ```
231
+
232
+ Bootstrap file (do not apply automatically — bootstrap is intentionally manual):
233
+
234
+ ```typescript
235
+ import { LaunchDarklyProvider } from "@launchdarkly/openfeature-node-server";
236
+ import { OpenFeature } from "@openfeature/server-sdk";
237
+
238
+ const ldProvider = new LaunchDarklyProvider(process.env.LD_SDK_KEY!);
239
+ await OpenFeature.setProviderAndWait(ldProvider);
240
+
241
+ // Evaluation context needs a targeting key.
242
+ // Use OpenFeature `targetingKey` or keep an existing LaunchDarkly `key`:
243
+ // { targetingKey: user.id } or { key: user.id }
244
+ export const openFeatureClient = OpenFeature.getClient();
245
+ ```
246
+
247
+ **Do not remove any LaunchDarkly packages.** LaunchDarkly remains your feature flag provider;
248
+ `@openfeature/server-sdk` becomes the evaluation interface your application code calls.
249
+
250
+ ---
251
+
252
+ ## Example transformation
253
+
254
+ **Before — direct LaunchDarkly Node.js server SDK:**
255
+ ```typescript
256
+ const enabled = await ldClient.boolVariation("checkout-v2", { key: user.id }, false);
257
+ const theme = await ldClient.stringVariation("color-theme", { key: user.id }, "light");
258
+ const timeout = await ldClient.numberVariation("timeout-ms", { key: user.id }, 5000);
259
+ ```
260
+
261
+ **After — OpenFeature via LaunchDarkly provider:**
262
+ ```typescript
263
+ const enabled = await openFeatureClient.getBooleanValue("checkout-v2", false, { targetingKey: user.id });
264
+ const theme = await openFeatureClient.getStringValue("color-theme", "light", { targetingKey: user.id });
265
+ const timeout = await openFeatureClient.getNumberValue("timeout-ms", 5000, { targetingKey: user.id });
266
+ ```
267
+
268
+ Flag key, fallback value, `await`, and evaluation context are preserved exactly.
269
+ LaunchDarkly continues to serve the flags — only the call-site API changes.
270
+
143
271
  ---
144
272
 
145
273
  ## Configuration
@@ -151,7 +279,7 @@ Create `.flaglintrc`, `.flaglintrc.json`, or `flaglint.config.json` in your proj
151
279
  "include": ["**/*.{ts,tsx,js,jsx}"],
152
280
  "exclude": ["**/node_modules/**", "**/dist/**"],
153
281
  "provider": "launchdarkly",
154
- "minFileCount": 1,
282
+ "minFileCount": 0,
155
283
  "reportTitle": "My Project Flag Report"
156
284
  }
157
285
  ```
@@ -161,8 +289,8 @@ Create `.flaglintrc`, `.flaglintrc.json`, or `flaglint.config.json` in your proj
161
289
  | `include` | `string[]` | `["**/*.{ts,tsx,js,jsx}"]` | Glob patterns to scan |
162
290
  | `exclude` | `string[]` | `["**/node_modules/**", ...]` | Glob patterns to ignore |
163
291
  | `provider` | `string` | `"launchdarkly"` | Feature flag provider |
164
- | `minFileCount` | `number` | `1` | A flag is stale if it appears in ≤ N files (default: 1) |
165
- | `wrappers` | `string[]` | `[]` | Function names that wrap LD SDK calls. FlagLint will detect calls to these functions as flag usages. Example: `["flagPredicate", "useFlag", "getFlag", "isEnabled"]` |
292
+ | `minFileCount` | `number` | `0` | Opt-in staleness heuristic. When set above 0, a flag is a staleness candidate if it appears in ≤ N files |
293
+ | `wrappers` | `string[]` | `[]` | Function names wrapping LD SDK calls. Example: `["flagPredicate", "useFlag"]` |
166
294
  | `reportTitle` | `string` | — | Custom title for generated reports |
167
295
  | `outputDir` | `string` | `"."` | Default output directory |
168
296
 
@@ -172,18 +300,17 @@ FlagLint searches for config in this order: `--config` path → `.flaglintrc`
172
300
 
173
301
  ## CI Integration
174
302
 
175
- ### Basic block PRs on stale flags
303
+ ### Enforce OpenFeature migration: block PRs with direct LD calls
176
304
 
177
305
  ```yaml
178
- - name: Check for stale flags
179
- run: npx flaglint scan --format json --output flaglint-report.json
180
- # exits 1 if stale flags found, blocking the PR
306
+ - name: Validate no direct LaunchDarkly evaluations
307
+ run: |
308
+ npx flaglint validate --no-direct-launchdarkly \
309
+ --bootstrap-exclude "src/provider/setup.ts"
310
+ # exits 1 if any direct LD evaluation calls remain outside the bootstrap file
181
311
  ```
182
312
 
183
- ### GitHub PR annotations via SARIF
184
-
185
- Stale flags appear as warnings directly in the PR diff —
186
- no dashboard, no separate tool.
313
+ ### Full migration CI pipeline with SARIF annotations
187
314
 
188
315
  ```yaml
189
316
  name: FlagLint
@@ -200,46 +327,32 @@ jobs:
200
327
  - uses: actions/setup-node@v4
201
328
  with:
202
329
  node-version: 20
203
- - name: Scan for flag debt
330
+
331
+ - name: Scan for LaunchDarkly SDK usage
204
332
  run: npx flaglint scan --format sarif --output flaglint.sarif
205
333
  continue-on-error: true
334
+
206
335
  - name: Upload to GitHub Code Scanning
207
336
  uses: github/codeql-action/upload-sarif@v3
208
337
  with:
209
338
  sarif_file: flaglint.sarif
210
- ```
211
-
212
- Stale flags show up as Code Scanning alerts on the exact file
213
- and line where the flag is used — reviewers see them in the PR
214
- without running anything locally.
215
339
 
216
- ---
217
-
218
- ## What FlagLint detects
219
-
220
- - `ldClient.variation()` and `ldClient.variationDetail()`
221
- - `ldClient.allFlags()`
222
- - `useFlags()`, `useLDClient()` React hooks
223
- - `<LDProvider>` and `withLDConsumer()` patterns
224
- - Custom wrapper calls such as `flagPredicate("my-flag", false)` when configured with `wrappers`
225
- - Dynamic flag keys (runtime-determined, flagged for manual review)
340
+ - name: Enforce OpenFeature migration
341
+ run: |
342
+ npx flaglint validate --no-direct-launchdarkly \
343
+ --bootstrap-exclude "src/provider/setup.ts"
344
+ ```
226
345
 
227
- All detections include the **file path**, **line number**, **call type**, and staleness signals based on key names, file locations, and low file counts.
346
+ Code Scanning alerts show the exact file and line of each direct LD call reviewers see them in the PR without running anything locally.
228
347
 
229
348
  ---
230
349
 
231
- ## OpenFeature Migration
350
+ ## Precision
232
351
 
233
- [OpenFeature](https://openfeature.dev) is the vendor-neutral standard for feature flagging (CNCF project). `flaglint migrate` maps your LaunchDarkly SDK calls to OpenFeature equivalents and generates an actionable `MIGRATION.md`:
352
+ Validated against 120 deterministic benchmark cases within the supported LaunchDarkly Node.js server-side SDK scope. 100% precision and recall are limited to those 120 tested cases and to the Node.js server-side SDK call patterns explicitly listed in the Supported API matrix above.
234
353
 
235
- | LaunchDarkly | OpenFeature |
236
- |---|---|
237
- | `ldClient.variation(key, ctx, false)` | `client.getBooleanValue(key, false, ctx)` |
238
- | `ldClient.variationDetail(key, ctx, def)` | `client.getBooleanDetails(key, def, ctx)` |
239
- | `useFlags()` | `useFlag(key)` per flag |
240
- | `useLDClient()` | `useOpenFeatureClient()` |
241
- | `<LDProvider>` | `<OpenFeatureProvider provider={...}>` |
242
- | `withLDConsumer()(Component)` | `withOpenFeature()(Component)` |
354
+ Detection is AST-based, not regex: client binding patterns, import aliases, CJS require forms,
355
+ and custom wrappers are all resolved before matching.
243
356
 
244
357
  ---
245
358
 
@@ -249,14 +362,6 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md).
249
362
 
250
363
  ---
251
364
 
252
- ## Free flag debt audit
253
-
254
- Running this on a real codebase?
255
- [Book a free 30-minute audit →](https://flaglint.dev#waitlist)
256
- I'll run FlagLint on your repo and walk you through the results.
257
-
258
- ---
259
-
260
365
  ## License
261
366
 
262
367
  MIT — see [LICENSE](./LICENSE).