flaglint 0.2.2 → 0.4.0

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,7 +5,49 @@ 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
- ## [0.2.2] - 2026-05-23
8
+ ## [Unreleased]
9
+
10
+ ## [0.4.0] - 2026-05-24
11
+
12
+ ### Added
13
+
14
+ - 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.
15
+
16
+ ### Added
17
+
18
+ - **`flaglint migrate --dry-run`**: Generates reviewable before/after diffs for every automatable
19
+ call-site, including inline provider setup guidance (packages, bootstrap file, `targetingKey`
20
+ context requirement). Does not write any files; output is to stdout.
21
+
22
+ - **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.
23
+
24
+ - **`flaglint migrate --apply`**: Applies only guarded, provably automatable transformations
25
+ in-place. Safety contracts: refuses on a dirty git working tree (override with `--allow-dirty`);
26
+ skips any file without a proven `openFeatureClient = OpenFeature.getClient()` binding from
27
+ `@openfeature/server-sdk` (AST-grounded, not regex); never rewrites detail methods, dynamic
28
+ keys, unknown fallbacks, or bulk calls; preserves `await` and all call arguments exactly;
29
+ idempotent (re-running a stale analysis is a no-op via range-content guard).
30
+
31
+ - **`flaglint validate [dir]`**: New command for CI enforcement.
32
+ - Without `--no-direct-launchdarkly`: reports usages, always exits 0.
33
+ - `--no-direct-launchdarkly`: exits 1 if any direct LaunchDarkly Node server evaluation call
34
+ is found (static, dynamic, detail, or bulk — all count as violations).
35
+ - `--bootstrap-exclude <glob>` (repeatable): exclude provider bootstrap files from violations.
36
+ Supports exact paths, `*` (within one directory), `**` (across directories), and `?` wildcards.
37
+ - Never claims flags are stale or safe to delete.
38
+
39
+ ### Scope clarification
40
+
41
+ Current scope: **LaunchDarkly Node.js server-side SDK** (`launchdarkly-node-server-sdk`).
42
+ React hooks, HOC, and client-side SDK patterns are detected by `scan` but are not automatically
43
+ migrated by `--apply`.
44
+
45
+ ## [0.3.0] - 2026-05-23
46
+
47
+ ### Added
48
+
49
+ - **SARIF output**: `flaglint scan --format sarif --output flaglint.sarif` now emits SARIF 2.1.0 for GitHub Code Scanning / PR annotations.
50
+ - **Persistent scan metadata**: `ScanResult` now includes `scannedAt` and `scanRoot`, giving JSON/SARIF/HTML reports a stable scan timestamp and source-root context.
9
51
 
10
52
  ### Fixed
11
53
 
@@ -13,6 +55,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
13
55
  - **Typed scan warnings**: `ScanResult.warnings` is now a typed `ScanWarning` union (`read-failure` | `parse-failure`) instead of opaque strings, preserving structured data at the domain boundary.
14
56
  - **StalenessEvaluator wired**: The `StalenessEvaluator` interface now has a call site in `scan()` — pass an `evaluator` to inject API-based staleness signals without touching core scanner logic.
15
57
  - **ScanConfig boundary**: `scan()` now accepts `ScanConfig` (scan-relevant fields only) rather than the full `FlagLintConfig`, decoupling the scanner from CLI output concerns (`reportTitle`, `outputDir`).
58
+ - **Report count consistency**: Markdown and HTML stale candidate counts now exclude wildcard (`*`) usages, matching the CLI summary.
16
59
 
17
60
  ## [0.2.1] - 2026-05-23
18
61
 
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  </p>
4
4
 
5
5
  <p align="center">
6
- <strong>Find stale feature flags. Detect flag debt. Plan your OpenFeature migration.</strong>
6
+ <strong>LaunchDarkly Node.js server SDK -> OpenFeature migration</strong>
7
7
  </p>
8
8
 
9
9
  <p align="center">
@@ -21,29 +21,68 @@
21
21
  </a>
22
22
  </p>
23
23
 
24
+ > ⚠️ **Early preview.** Current scope: **LaunchDarkly Node.js server-side SDK** only.
25
+ > React hooks, HOC, and client-side SDK patterns are detected by `scan` but are not
26
+ > automatically migrated.
24
27
 
25
28
  # FlagLint
26
29
 
27
- **Find stale feature flags. Detect flag debt. Plan your OpenFeature migration.**
30
+ FlagLint inventories direct LaunchDarkly Node.js server SDK calls in your TypeScript/JavaScript
31
+ codebase, generates reviewable OpenFeature migration diffs, applies only guarded transformations,
32
+ and enforces migration state in CI.
28
33
 
29
- [![CI](https://github.com/flaglint/flaglint/actions/workflows/ci.yml/badge.svg)](https://github.com/flaglint/flaglint/actions/workflows/ci.yml)
30
- [![npm version](https://img.shields.io/npm/v/flaglint.svg)](https://www.npmjs.com/package/flaglint)
31
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
34
+ **LaunchDarkly remains your provider. OpenFeature becomes the evaluation API your application code calls.**
32
35
 
33
36
  ---
34
37
 
35
- ## The problem
38
+ ## Workflow
36
39
 
37
- 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.
38
-
39
- **FlagLint fixes this.** It scans your codebase, maps every flag usage, identifies stale candidates, and generates a step-by-step OpenFeature migration plan.
40
+ | Step | Command | Purpose |
41
+ |------|---------|---------|
42
+ | 1 | `flaglint scan` | AST inventory of every direct LD Node server SDK call |
43
+ | 2 | `flaglint migrate --dry-run` | Reviewable before/after diffs with provider setup guidance |
44
+ | 3 | `flaglint migrate --apply` | Apply only guarded, provably automatable transformations |
45
+ | 4 | `flaglint validate --no-direct-launchdarkly` | CI gate: exit 1 if direct LD calls remain |
40
46
 
41
47
  ---
42
48
 
43
49
  ## Quick start
44
50
 
45
51
  ```bash
46
- npx flaglint scan
52
+ npx flaglint scan ./src
53
+ ```
54
+
55
+ Example output:
56
+
57
+ ```text
58
+ ✓ 15 flag usages found across 6 unique flags (48ms)
59
+ ℹ 1 dynamic flag key(s) require manual review
60
+ ```
61
+
62
+ Markdown report excerpt:
63
+
64
+ ```markdown
65
+ ## Flag Inventory
66
+ | Flag Key | Usages | Files | Call Types |
67
+ |----------|--------|-------|------------|
68
+ | checkout-v2 | 3 | 2 | boolVariation |
69
+ | color-theme | 1 | 1 | stringVariation |
70
+ | timeout-ms | 1 | 1 | numberVariation |
71
+ ```
72
+
73
+ ### JSON output (`--format json`)
74
+
75
+ Pipe-friendly. Every usage includes file, line, call type, and staleness signals:
76
+
77
+ ```json
78
+ {
79
+ "flagKey": "checkout-v2",
80
+ "isDynamic": false,
81
+ "file": "src/services/checkout.ts",
82
+ "line": 14,
83
+ "callType": "boolVariation",
84
+ "stalenessSignals": []
85
+ }
47
86
  ```
48
87
 
49
88
  ---
@@ -62,21 +101,24 @@ npx flaglint
62
101
 
63
102
  ### `flaglint scan [dir]`
64
103
 
65
- Scans a directory for LaunchDarkly SDK usage.
104
+ AST-based inventory of direct LaunchDarkly Node.js server SDK calls.
66
105
 
67
106
  ```bash
68
107
  flaglint scan ./src
69
108
  flaglint scan --format json --output report.json
70
109
  flaglint scan --format html --output report.html
110
+ flaglint scan --format sarif --output flaglint.sarif
71
111
  ```
72
112
 
73
113
  | Option | Default | Description |
74
114
  |--------|---------|-------------|
75
- | `--format` | `markdown` | Output format: `json`, `markdown`, `html` |
115
+ | `--format` | `markdown` | Output format: `json`, `markdown`, `html`, `sarif` |
76
116
  | `--output` | stdout | Write report to file |
77
- | `--config` | auto-detect | Path to `.flaglintrc` |
117
+ | `--config` | auto-detect | Path to a config file |
118
+ | `--exclude-tests` | — | Exclude test files from scan results |
78
119
 
79
- Exit code `0` when no stale flags found, `1` when stale flags exist enabling CI blocking.
120
+ Exit code `0` when no staleness signals detected, `1` when staleness signals are present
121
+ enabling CI visibility into flag usage patterns.
80
122
 
81
123
  ---
82
124
 
@@ -85,22 +127,151 @@ Exit code `0` when no stale flags found, `1` when stale flags exist — enabling
85
127
  Analyzes migration readiness and generates an OpenFeature migration plan.
86
128
 
87
129
  ```bash
88
- flaglint migrate ./src
89
- flaglint migrate --dry-run
90
- flaglint migrate --output MIGRATION.md
130
+ flaglint migrate ./src # write MIGRATION.md
131
+ flaglint migrate --dry-run # reviewable diffs to stdout
132
+ flaglint migrate --apply # guarded: apply only provably automatable transformations in-place
133
+ flaglint migrate --apply --allow-dirty # apply even on a dirty working tree
134
+ flaglint migrate --output plan.md # write to custom file
135
+ flaglint migrate --exclude-tests # skip test and spec files
91
136
  ```
92
137
 
93
138
  | Option | Default | Description |
94
139
  |--------|---------|-------------|
95
140
  | `--output` | `MIGRATION.md` | Write migration plan to file |
96
- | `--dry-run` | — | Print plan to stdout, do not write file |
97
- | `--config` | auto-detect | Path to `.flaglintrc` |
141
+ | `--dry-run` | — | Print reviewable diffs to stdout; includes provider setup guidance |
142
+ | `--apply` | | Apply automatable transformations in-place (requires clean git tree) |
143
+ | `--allow-dirty` | — | Override dirty-tree guard for `--apply` |
144
+ | `--config` | auto-detect | Path to a config file |
145
+ | `--exclude-tests` | — | Skip `*.test.*`, `*.spec.*`, `__tests__/`, `tests/` |
146
+
147
+ **`--apply` safety contracts:**
148
+ - Refuses on a dirty git working tree unless `--allow-dirty`
149
+ - Skips any file that does not already contain a proven `openFeatureClient` binding
150
+ (`openFeatureClient = OpenFeature.getClient()` from `@openfeature/server-sdk`)
151
+ - Never touches detail methods, dynamic keys, unknown fallbacks, or bulk calls
152
+ - Preserves `await` and original call arguments exactly
153
+ - Idempotent: re-running with the same analysis has no effect
154
+
155
+ ---
156
+
157
+ ### `flaglint validate [dir]`
158
+
159
+ Validates that your codebase complies with feature flag policy rules.
160
+ Designed for CI enforcement after migration is complete.
161
+
162
+ ```bash
163
+ flaglint validate # report usages, always exits 0
164
+ flaglint validate --no-direct-launchdarkly # exit 1 on any direct LD eval call
165
+ flaglint validate --no-direct-launchdarkly \
166
+ --bootstrap-exclude src/provider/setup.ts # allow specific bootstrap file
167
+ flaglint validate --no-direct-launchdarkly \
168
+ --bootstrap-exclude "src/provider/**" # allow all provider-directory files
169
+ ```
170
+
171
+ | Option | Default | Description |
172
+ |--------|---------|-------------|
173
+ | `--no-direct-launchdarkly` | — | Exit 1 if any direct LD Node server evaluation calls found |
174
+ | `--bootstrap-exclude <glob>` | — | Repeatable glob; matching files excluded from violations |
175
+ | `--config` | auto-detect | Path to a config file |
176
+
177
+ Exit codes: `0` = passed, `1` = violations found, `130` = SIGINT.
178
+
179
+ **Example pass output:**
180
+ ```
181
+ ✓ validate --no-direct-launchdarkly: no direct LaunchDarkly evaluation calls found.
182
+ Scanned 42 file(s).
183
+ ```
184
+
185
+ **Example fail output:**
186
+ ```
187
+ ✗ validate --no-direct-launchdarkly: 2 direct LaunchDarkly evaluation call(s) found.
188
+
189
+ src/services/checkout.ts:42:8 — boolVariation("checkout-v2")
190
+ src/services/pricing.ts:17:4 — boolVariation(dynamic key — manual review required)
191
+
192
+ These files must migrate to OpenFeature before this rule passes.
193
+ Run `flaglint migrate --dry-run` to review the migration plan.
194
+ ```
195
+
196
+ ---
197
+
198
+ ## Supported API matrix
199
+
200
+ **Scope: LaunchDarkly Node.js server-side SDK** (`launchdarkly-node-server-sdk`).
201
+
202
+ | LaunchDarkly call | Automatable | OpenFeature equivalent |
203
+ |---|---|---|
204
+ | `ldClient.boolVariation(key, ctx, false)` | ✓ | `openFeatureClient.getBooleanValue(key, false, ctx)` |
205
+ | `ldClient.stringVariation(key, ctx, "")` | ✓ | `openFeatureClient.getStringValue(key, "", ctx)` |
206
+ | `ldClient.numberVariation(key, ctx, 0)` | ✓ | `openFeatureClient.getNumberValue(key, 0, ctx)` |
207
+ | `ldClient.jsonVariation(key, ctx, {})` | ✓ | `openFeatureClient.getObjectValue(key, {}, ctx)` |
208
+ | `ldClient.*VariationDetail(...)` | ✗ manual | Detail result shapes differ — requires manual review |
209
+ | Dynamic flag key | ✗ manual | Key must be a static string literal |
210
+ | `ldClient.allFlags()` / `allFlagsState()` | ✗ manual | Bulk calls — no single-flag codemod |
211
+ | Unknown fallback type | ✗ manual | Fallback type must be determinable statically |
212
+ | React `useFlags()`, `useLDClient()` | detect only | Client-side — outside Node.js server SDK scope |
213
+ | React HOC / `<LDProvider>` | detect only | Client-side — outside Node.js server SDK scope |
214
+
215
+ `flaglint scan` and `flaglint migrate --dry-run` report all detected patterns including manual-review cases.
216
+ `flaglint migrate --apply` rewrites only the ✓ rows above.
217
+
218
+ ---
219
+
220
+ ## Provider setup (one-time manual step)
221
+
222
+ `flaglint migrate --dry-run` includes this guidance inline. **Complete provider setup in
223
+ one dedicated file before running `--apply`.**
224
+
225
+ ```bash
226
+ npm install @openfeature/server-sdk \
227
+ @launchdarkly/node-server-sdk \
228
+ @launchdarkly/openfeature-node-server
229
+ ```
230
+
231
+ Bootstrap file (do not apply automatically — bootstrap is intentionally manual):
232
+
233
+ ```typescript
234
+ import LaunchDarkly from "@launchdarkly/node-server-sdk";
235
+ import { LaunchDarklyProvider } from "@launchdarkly/openfeature-node-server";
236
+ import { OpenFeature } from "@openfeature/server-sdk";
237
+
238
+ const ldClient = LaunchDarkly.init(process.env.LD_SDK_KEY!);
239
+ await OpenFeature.setProviderAndWait(new LaunchDarklyProvider(ldClient));
240
+
241
+ // Evaluation context must include targetingKey (or key):
242
+ // { targetingKey: user.id }
243
+ export const openFeatureClient = OpenFeature.getClient();
244
+ ```
245
+
246
+ **Do not remove any LaunchDarkly packages.** LaunchDarkly remains your feature flag provider;
247
+ `@openfeature/server-sdk` becomes the evaluation interface your application code calls.
248
+
249
+ ---
250
+
251
+ ## Example transformation
252
+
253
+ **Before — direct LaunchDarkly Node.js server SDK:**
254
+ ```typescript
255
+ const enabled = await ldClient.boolVariation("checkout-v2", { key: user.id }, false);
256
+ const theme = await ldClient.stringVariation("color-theme", { key: user.id }, "light");
257
+ const timeout = await ldClient.numberVariation("timeout-ms", { key: user.id }, 5000);
258
+ ```
259
+
260
+ **After — OpenFeature via LaunchDarkly provider:**
261
+ ```typescript
262
+ const enabled = await openFeatureClient.getBooleanValue("checkout-v2", false, { targetingKey: user.id });
263
+ const theme = await openFeatureClient.getStringValue("color-theme", "light", { targetingKey: user.id });
264
+ const timeout = await openFeatureClient.getNumberValue("timeout-ms", 5000, { targetingKey: user.id });
265
+ ```
266
+
267
+ Flag key, fallback value, `await`, and evaluation context are preserved exactly.
268
+ LaunchDarkly continues to serve the flags — only the call-site API changes.
98
269
 
99
270
  ---
100
271
 
101
272
  ## Configuration
102
273
 
103
- Create `.flaglintrc` in your project root:
274
+ Create `.flaglintrc`, `.flaglintrc.json`, or `flaglint.config.json` in your project root:
104
275
 
105
276
  ```json
106
277
  {
@@ -117,49 +288,70 @@ Create `.flaglintrc` in your project root:
117
288
  | `include` | `string[]` | `["**/*.{ts,tsx,js,jsx}"]` | Glob patterns to scan |
118
289
  | `exclude` | `string[]` | `["**/node_modules/**", ...]` | Glob patterns to ignore |
119
290
  | `provider` | `string` | `"launchdarkly"` | Feature flag provider |
120
- | `minFileCount` | `number` | `1` | A flag is stale if it appears in ≤ N files (default: 1) |
121
- | `wrappers` | `string[]` | `[]` | Function names that wrap LD SDK calls. FlagLint will detect calls to these functions as flag usages. Example: `["flagPredicate", "useFlag", "getFlag", "isEnabled"]` |
291
+ | `minFileCount` | `number` | `1` | A flag is a staleness candidate if it appears in ≤ N files |
292
+ | `wrappers` | `string[]` | `[]` | Function names wrapping LD SDK calls. Example: `["flagPredicate", "useFlag"]` |
122
293
  | `reportTitle` | `string` | — | Custom title for generated reports |
123
294
  | `outputDir` | `string` | `"."` | Default output directory |
124
295
 
125
- FlagLint searches for config in this order: `--config` flag → `.flaglintrc` → `.flaglintrc.json` → `flaglint.config.json`.
296
+ FlagLint searches for config in this order: `--config` path → `.flaglintrc` → `.flaglintrc.json` → `flaglint.config.json`.
126
297
 
127
298
  ---
128
299
 
129
300
  ## CI Integration
130
301
 
302
+ ### Enforce OpenFeature migration: block PRs with direct LD calls
303
+
131
304
  ```yaml
132
- - name: Check for stale flags
133
- run: npx flaglint scan --format json --output flaglint-report.json
134
- # exits 1 if stale flags found, blocking the PR
305
+ - name: Validate no direct LaunchDarkly evaluations
306
+ run: |
307
+ npx flaglint validate --no-direct-launchdarkly \
308
+ --bootstrap-exclude "src/provider/setup.ts"
309
+ # exits 1 if any direct LD evaluation calls remain outside the bootstrap file
135
310
  ```
136
311
 
137
- ---
138
-
139
- ## What FlagLint detects
312
+ ### Full migration CI pipeline with SARIF annotations
140
313
 
141
- - `ldClient.variation()` and `ldClient.variationDetail()`
142
- - `ldClient.allFlags()`
143
- - `useFlags()`, `useLDClient()` React hooks
144
- - `<LDProvider>` and `withLDConsumer()` patterns
145
- - Dynamic flag keys (runtime-determined, flagged for manual review)
314
+ ```yaml
315
+ name: FlagLint
316
+ on: [pull_request]
317
+
318
+ jobs:
319
+ flaglint:
320
+ runs-on: ubuntu-latest
321
+ permissions:
322
+ security-events: write
323
+ contents: read
324
+ steps:
325
+ - uses: actions/checkout@v4
326
+ - uses: actions/setup-node@v4
327
+ with:
328
+ node-version: 20
329
+
330
+ - name: Scan for LaunchDarkly SDK usage
331
+ run: npx flaglint scan --format sarif --output flaglint.sarif
332
+ continue-on-error: true
333
+
334
+ - name: Upload to GitHub Code Scanning
335
+ uses: github/codeql-action/upload-sarif@v3
336
+ with:
337
+ sarif_file: flaglint.sarif
338
+
339
+ - name: Enforce OpenFeature migration
340
+ run: |
341
+ npx flaglint validate --no-direct-launchdarkly \
342
+ --bootstrap-exclude "src/provider/setup.ts"
343
+ ```
146
344
 
147
- All detections include the **file path**, **line number**, **call type**, and a **stale heuristic** based on key names and file locations.
345
+ Code Scanning alerts show the exact file and line of each direct LD call reviewers see them in the PR without running anything locally.
148
346
 
149
347
  ---
150
348
 
151
- ## OpenFeature Migration
349
+ ## Precision
152
350
 
153
- [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`:
351
+ 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.
154
352
 
155
- | LaunchDarkly | OpenFeature |
156
- |---|---|
157
- | `ldClient.variation(key, ctx, false)` | `client.getBooleanValue(key, false, ctx)` |
158
- | `ldClient.variationDetail(key, ctx, def)` | `client.getBooleanDetails(key, def, ctx)` |
159
- | `useFlags()` | `useFlag(key)` per flag |
160
- | `useLDClient()` | `useOpenFeatureClient()` |
161
- | `<LDProvider>` | `<OpenFeatureProvider provider={...}>` |
162
- | `withLDConsumer()(Component)` | `withOpenFeature()(Component)` |
353
+ Detection is AST-based, not regex: client binding patterns, import aliases, CJS require forms,
354
+ and custom wrappers are all resolved before matching.
163
355
 
164
356
  ---
165
357