flaglint 0.7.0 → 0.9.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,15 +5,41 @@ 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.8.0] - 2026-06-13
9
+
10
+ ### Changed
11
+
12
+ - **`--cost-estimate` renamed to `--effort-estimate`** (breaking) — the flag outputs
13
+ hours, not dollars; "effort" is more accurate. Update any scripts or CI pipelines
14
+ that use `--cost-estimate`.
15
+
16
+ ### Fixed
17
+
18
+ - Homepage email-capture / Loops signup section removed.
19
+ - Version displayed on homepage now read from `package.json` at build time — can no
20
+ longer drift from the published npm version.
21
+ - `robots.txt` sitemap URL updated to `/sitemap-index.xml` (was `/sitemap.xml`, which 404'd).
22
+ - Audit sample numbers corrected to real fixture output: 13 unique flags across 19 call
23
+ sites, readiness 53/100, estimate 20.8h–40h.
24
+
25
+ ### Infrastructure
26
+
27
+ - Vitest `testTimeout`/`hookTimeout` raised to 30 s to fix flaky Windows/Node 22 CI timeouts.
28
+ - `npm publish --provenance` re-enabled in release workflow.
29
+ - `scripts/metrics/collect.mjs` added: fetches npm downloads + GitHub stars/forks/issues
30
+ and appends a JSON line to `.agent-output/metrics/history.jsonl`.
31
+
32
+ ---
33
+
8
34
  ## [0.7.0] - 2026-06-07
9
35
 
10
36
  ### Added
11
37
 
12
- - `flaglint audit --cost-estimate` — adds a directional migration-effort estimate to
38
+ - `flaglint audit --effort-estimate` — adds a directional migration-effort estimate to
13
39
  audit output. Computes low/high hour ranges from automatable and manual-review call
14
40
  counts using configurable planning heuristics.
15
41
  - `flaglint audit --hourly-rate <rate>` — adds engineering cost projection
16
- (`costLow`/`costHigh`) to the estimate. Requires `--cost-estimate`.
42
+ (`costLow`/`costHigh`) to the estimate. Requires `--effort-estimate`.
17
43
  - Migration readiness score displayed in `flaglint audit` terminal output: 0–100 ratio
18
44
  of safely automatable calls to total detected LaunchDarkly calls, with grade
19
45
  (`ready` | `moderate` | `complex` | `not-applicable`) and progress bar.
package/README.md CHANGED
@@ -30,27 +30,27 @@ npx flaglint audit ./src
30
30
  ```
31
31
 
32
32
  ```
33
- ✓ Audit complete: 13 flags — 3 high risk, 10 medium risk
33
+ ✓ Audit complete: 13 unique flags across 19 call sites — 3 high risk, 10 medium risk
34
34
 
35
- Migration readiness: 50/100 · moderate
36
- [█████████████░░░░░░░░░░░░] 50%
37
- 10 safely automatable · 10 require manual review
35
+ Migration readiness: 53/100 · moderate
36
+ [█████████████░░░░░░░░░░░░] 53%
37
+ 10 of 19 call sites safely automatable · 9 require manual review
38
38
  ```
39
39
 
40
- Add `--cost-estimate` to include a directional planning estimate:
40
+ Add `--effort-estimate` to include a directional planning estimate:
41
41
 
42
42
  ```bash
43
- npx flaglint audit ./src --cost-estimate
43
+ npx flaglint audit ./src --effort-estimate
44
44
  ```
45
45
 
46
46
  ```
47
- ✓ Audit complete: 13 flags — 3 high risk, 10 medium risk
47
+ ✓ Audit complete: 13 unique flags across 19 call sites — 3 high risk, 10 medium risk
48
48
 
49
- Migration readiness: 50/100 · moderate
50
- [█████████████░░░░░░░░░░░░] 50%
51
- 10 safely automatable · 10 require manual review
49
+ Migration readiness: 53/100 · moderate
50
+ [█████████████░░░░░░░░░░░░] 53%
51
+ 10 of 19 call sites safely automatable · 9 require manual review
52
52
 
53
- Estimated migration effort: 22.8h – 43.9h
53
+ Estimated migration effort: 20.8h – 40h
54
54
  Estimates are directional. See the report for assumptions.
55
55
  ```
56
56
 
@@ -58,6 +58,8 @@ For the full report see `--format html`. [Documentation](https://flaglint.dev/do
58
58
 
59
59
  No API key. No source upload. LaunchDarkly stays your provider — OpenFeature becomes the evaluation API your application calls.
60
60
 
61
+ Migrating an existing Node.js service? Read the [complete LaunchDarkly-to-OpenFeature migration guide](https://flaglint.dev/guides/launchdarkly-to-openfeature-nodejs/).
62
+
61
63
  ---
62
64
 
63
65
  ## The problem FlagLint solves
@@ -789,7 +789,7 @@ function formatHTML(result, options) {
789
789
  <div class="card"><div class="card-num orange">${manualCount}</div><div class="card-label">Manual Review (${manualPct}%)</div></div>
790
790
  <div class="card"><div class="card-num blue">${detailBulkCount}</div><div class="card-label">Detail/Bulk Calls</div></div>` : "";
791
791
  const title = options.title ? esc(options.title) : "FlagLint Scan Report";
792
- const version = true ? "0.7.0" : "0.1.0";
792
+ const version = true ? "0.9.0" : "0.1.0";
793
793
  return `<!DOCTYPE html>
794
794
  <html lang="en">
795
795
  <head>
@@ -1322,7 +1322,7 @@ function formatMigrationReport(analysis) {
1322
1322
  unsupportedUnknownCount
1323
1323
  } = analysis;
1324
1324
  const date = (/* @__PURE__ */ new Date()).toLocaleDateString();
1325
- const version = true ? "0.7.0" : "0.1.0";
1325
+ const version = true ? "0.9.0" : "0.1.0";
1326
1326
  const lines = [];
1327
1327
  lines.push("# OpenFeature Migration Inventory");
1328
1328
  lines.push(`Generated by FlagLint v${version} on ${date}`);
@@ -2154,9 +2154,12 @@ function buildAuditReport(scanResult, inventoryItems) {
2154
2154
  const { riskLevel, riskReasons } = scoreFlag(usages, items);
2155
2155
  const files = [...new Set(usages.map((u) => u.file))];
2156
2156
  const callTypes = [...new Set(usages.map((u) => u.callType))];
2157
+ const isAutomatableTier = riskLevel === "medium" && riskReasons.length === 1 && riskReasons[0] === "safely automatable";
2158
+ const displayTier = isAutomatableTier ? "automatable" : riskLevel;
2157
2159
  flags.push({
2158
2160
  flagKey,
2159
2161
  riskLevel,
2162
+ displayTier,
2160
2163
  riskReasons,
2161
2164
  callTypes,
2162
2165
  fileCount: files.length,
@@ -2176,23 +2179,28 @@ function buildAuditReport(scanResult, inventoryItems) {
2176
2179
  const highRisk = flags.filter((f) => f.riskLevel === "high").length;
2177
2180
  const mediumRisk = flags.filter((f) => f.riskLevel === "medium").length;
2178
2181
  const lowRisk = flags.filter((f) => f.riskLevel === "low").length;
2182
+ const automatableFlags = flags.filter((f) => f.displayTier === "automatable").length;
2183
+ const staleSignals = scanResult.usages.filter((u) => u.stalenessSignals.length > 0).length;
2184
+ const stalenessNote = staleSignals === 0 ? "No staleness signals detected. Heuristics checked: keyword match (flag key contains old/deprecated/legacy/temp/tmp/test/demo), path pattern (test/spec/mock files, deprecated/old/legacy directories), and minFileCount threshold. Git-history-based staleness (last evaluation date) requires git metadata and is not available in a pure static scan." : void 0;
2179
2185
  const summary = {
2180
2186
  totalFlags: flags.length,
2181
2187
  highRisk,
2182
2188
  mediumRisk,
2183
2189
  lowRisk,
2190
+ automatableFlags,
2184
2191
  totalUsages: scanResult.totalUsages,
2185
2192
  dynamicKeys: scanResult.usages.filter((u) => u.isDynamic).length,
2186
2193
  detailEvaluations: inventoryItems.filter((i) => i.manualReviewReason === "detail-method").length,
2187
2194
  bulkCalls: inventoryItems.filter((i) => i.manualReviewReason === "bulk-inventory-call").length,
2188
2195
  wrapperUsages: scanResult.usages.filter((u) => u.callType === "variation").length,
2189
- staleSignals: scanResult.usages.filter((u) => u.stalenessSignals.length > 0).length,
2196
+ staleSignals,
2190
2197
  safelyAutomatable: inventoryItems.filter((i) => i.safelyAutomatable).length,
2191
2198
  manualReview: inventoryItems.filter((i) => !i.safelyAutomatable).length,
2192
2199
  scannedFiles: scanResult.scannedFiles,
2193
2200
  scanDurationMs: scanResult.scanDurationMs,
2194
2201
  scannedAt: scanResult.scannedAt,
2195
- scanRoot: scanResult.scanRoot
2202
+ scanRoot: scanResult.scanRoot,
2203
+ ...stalenessNote !== void 0 ? { stalenessNote } : {}
2196
2204
  };
2197
2205
  const readiness = computeReadiness(scanResult.usages, inventoryItems);
2198
2206
  return { summary, flags, readiness };
@@ -2256,6 +2264,10 @@ function formatAuditMarkdown(report, options) {
2256
2264
  `| ${summary.dynamicKeys} | ${summary.detailEvaluations} | ${summary.bulkCalls} | ${summary.staleSignals} | ${summary.safelyAutomatable} | ${summary.manualReview} |`
2257
2265
  );
2258
2266
  lines.push("");
2267
+ if (summary.stalenessNote !== void 0) {
2268
+ lines.push(`> **Staleness:** ${summary.stalenessNote}`);
2269
+ lines.push("");
2270
+ }
2259
2271
  lines.push("## Migration Readiness");
2260
2272
  lines.push("");
2261
2273
  if (readiness.grade === "not-applicable") {
@@ -2300,16 +2312,17 @@ function formatAuditMarkdown(report, options) {
2300
2312
  lines.push("");
2301
2313
  lines.push("| Flag Key | Risk | Usages | Files | Call Types | Reasons |");
2302
2314
  lines.push("|----------|------|--------|-------|------------|---------|");
2303
- const riskLabel = {
2315
+ const tierLabel = {
2304
2316
  high: "\u{1F534} High",
2305
2317
  medium: "\u{1F7E1} Medium",
2318
+ automatable: "\u{1F7E2} Automatable",
2306
2319
  low: "\u{1F7E2} Low"
2307
2320
  };
2308
2321
  for (const flag of flags) {
2309
2322
  const reasons = flag.riskReasons.join(", ") || "\u2014";
2310
2323
  const flagKey = displayFlagKey(flag);
2311
2324
  lines.push(
2312
- `| \`${flagKey}\` | ${riskLabel[flag.riskLevel]} | ${flag.usageCount} | ${flag.fileCount} | ${flag.callTypes.join(", ")} | ${reasons} |`
2325
+ `| \`${flagKey}\` | ${tierLabel[flag.displayTier]} | ${flag.usageCount} | ${flag.fileCount} | ${flag.callTypes.join(", ")} | ${reasons} |`
2313
2326
  );
2314
2327
  }
2315
2328
  lines.push("");
@@ -2329,12 +2342,14 @@ function formatAuditMarkdown(report, options) {
2329
2342
  }
2330
2343
  function formatAuditHtml(report, options) {
2331
2344
  const { summary, flags, readiness } = report;
2332
- const version = true ? "0.7.0" : "0.1.0";
2345
+ const version = true ? "0.9.0" : "0.1.0";
2333
2346
  const date = new Date(summary.scannedAt).toLocaleString();
2334
- const riskBadge = (level) => {
2335
- if (level === "high")
2347
+ const tierBadge = (tier) => {
2348
+ if (tier === "high")
2336
2349
  return '<span class="badge badge-high">High</span>';
2337
- if (level === "medium")
2350
+ if (tier === "automatable")
2351
+ return '<span class="badge badge-automatable">Automatable</span>';
2352
+ if (tier === "medium")
2338
2353
  return '<span class="badge badge-medium">Medium</span>';
2339
2354
  return '<span class="badge badge-low">Low</span>';
2340
2355
  };
@@ -2343,7 +2358,7 @@ function formatAuditHtml(report, options) {
2343
2358
  const flagKey = displayFlagKey(f);
2344
2359
  return `<tr>
2345
2360
  <td><code>${esc2(flagKey)}</code></td>
2346
- <td>${riskBadge(f.riskLevel)}</td>
2361
+ <td>${tierBadge(f.displayTier)}</td>
2347
2362
  <td>${f.usageCount}</td>
2348
2363
  <td>${f.fileCount}</td>
2349
2364
  <td>${f.callTypes.map(esc2).join(", ")}</td>
@@ -2439,11 +2454,14 @@ function formatAuditHtml(report, options) {
2439
2454
  .badge-high{background:#fee2e2;color:#991b1b}
2440
2455
  .badge-medium{background:#fef3c7;color:#92400e}
2441
2456
  .badge-low{background:#dcfce7;color:#166534}
2457
+ .badge-automatable{background:#d1fae5;color:#065f46}
2442
2458
  @media(prefers-color-scheme:dark){
2443
2459
  .badge-high{background:#7f1d1d;color:#fca5a5}
2444
2460
  .badge-medium{background:#78350f;color:#fcd34d}
2445
2461
  .badge-low{background:#14532d;color:#86efac}
2462
+ .badge-automatable{background:#064e3b;color:#6ee7b7}
2446
2463
  }
2464
+ .staleness-note{margin-top:.75rem;padding:.75rem 1rem;background:var(--surface);border:1px solid var(--border);border-radius:6px;font-size:.8125rem;color:var(--muted)}
2447
2465
  .estimate-block{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:1.25rem;margin-bottom:1.5rem}
2448
2466
  .estimate-total{font-size:1.5rem;font-weight:700;margin-bottom:.75rem}
2449
2467
  .estimate-cost{font-size:1rem;font-weight:600;margin:.75rem 0;color:#3b82f6}
@@ -2463,11 +2481,13 @@ function formatAuditHtml(report, options) {
2463
2481
  <div class="cards">
2464
2482
  <div class="card"><div class="card-num">${summary.totalFlags}</div><div class="card-label">Total Flags</div></div>
2465
2483
  <div class="card"><div class="card-num red">${summary.highRisk}</div><div class="card-label">High Risk</div></div>
2466
- <div class="card"><div class="card-num amber">${summary.mediumRisk}</div><div class="card-label">Medium Risk</div></div>
2484
+ ${summary.automatableFlags > 0 ? `<div class="card"><div class="card-num green">${summary.automatableFlags}</div><div class="card-label">Automatable</div></div>` : ""}
2485
+ ${summary.mediumRisk - summary.automatableFlags > 0 ? `<div class="card"><div class="card-num amber">${summary.mediumRisk - summary.automatableFlags}</div><div class="card-label">Medium Risk</div></div>` : ""}
2467
2486
  ${summary.lowRisk > 0 ? `<div class="card"><div class="card-num green">${summary.lowRisk}</div><div class="card-label">Low Risk</div></div>` : ""}
2468
2487
  <div class="card"><div class="card-num purple">${summary.safelyAutomatable}</div><div class="card-label">Safely Automatable</div></div>
2469
2488
  <div class="card"><div class="card-num orange">${summary.manualReview}</div><div class="card-label">Manual Review</div></div>
2470
2489
  </div>
2490
+ ${summary.stalenessNote !== void 0 ? `<div class="staleness-note"><strong>Staleness:</strong> ${esc2(summary.stalenessNote)}</div>` : ""}
2471
2491
 
2472
2492
  <h2>Migration Readiness</h2>
2473
2493
  <div class="readiness-block">
@@ -2593,15 +2613,15 @@ function computeEstimate(readiness, overrides, hourlyRate) {
2593
2613
  // src/commands/audit.ts
2594
2614
  var VALID_AUDIT_FORMATS = ["json", "markdown", "html"];
2595
2615
  function registerAuditCommand(program2) {
2596
- program2.command("audit").description("Generate a flag debt audit report").argument("[dir]", "directory to audit", process.cwd()).option("-f, --format <format>", "output format: json | markdown | html", "markdown").option("-o, --output <file>", "write report to file").option("-c, --config <path>", "path to .flaglintrc config file").option("--exclude-tests", "exclude test files (*.test.*, *.spec.*, __tests__/, tests/)").option("--cost-estimate", "include a migration effort estimate in the report. The default assumptions are configurable planning heuristics, not observed industry benchmarks.").option("--hourly-rate <rate>", "hourly rate for cost projection (requires --cost-estimate)").addHelpText(
2616
+ program2.command("audit").description("Generate a flag debt audit report").argument("[dir]", "directory to audit", process.cwd()).option("-f, --format <format>", "output format: json | markdown | html", "markdown").option("-o, --output <file>", "write report to file").option("-c, --config <path>", "path to .flaglintrc config file").option("--exclude-tests", "exclude test files (*.test.*, *.spec.*, __tests__/, tests/)").option("--effort-estimate", "include a migration effort estimate in the report. The default assumptions are configurable planning heuristics, not observed industry benchmarks.").option("--hourly-rate <rate>", "hourly rate for cost projection (requires --effort-estimate)").addHelpText(
2597
2617
  "after",
2598
2618
  `
2599
2619
  Examples:
2600
2620
  $ flaglint audit generate flag debt audit report
2601
2621
  $ flaglint audit --format html shareable HTML report
2602
2622
  $ flaglint audit --output audit.md save to file
2603
- $ flaglint audit --cost-estimate include migration effort estimate
2604
- $ flaglint audit --cost-estimate --hourly-rate 125 include cost projection`
2623
+ $ flaglint audit --effort-estimate include migration effort estimate
2624
+ $ flaglint audit --effort-estimate --hourly-rate 125 include cost projection`
2605
2625
  ).action(
2606
2626
  async (dir, options) => {
2607
2627
  if (!VALID_AUDIT_FORMATS.includes(options.format)) {
@@ -2615,9 +2635,9 @@ Examples:
2615
2635
  }
2616
2636
  let hourlyRate;
2617
2637
  if (options.hourlyRate !== void 0) {
2618
- if (!options.costEstimate) {
2638
+ if (!options.effortEstimate) {
2619
2639
  process.stderr.write(
2620
- chalk4.yellow("warn: --hourly-rate has no effect without --cost-estimate\n")
2640
+ chalk4.yellow("warn: --hourly-rate has no effect without --effort-estimate\n")
2621
2641
  );
2622
2642
  } else {
2623
2643
  const parsed = Number(options.hourlyRate);
@@ -2710,11 +2730,11 @@ Examples:
2710
2730
  `
2711
2731
  )
2712
2732
  );
2713
- if (format === "json" && options.costEstimate) {
2733
+ if (format === "json" && options.effortEstimate) {
2714
2734
  const emptyReport = buildAuditReport(scanResult, []);
2715
2735
  process.stdout.write(formatAuditJson(emptyReport, { estimate: null }) + "\n");
2716
2736
  process.stderr.write(chalk4.dim("Migration estimate: N/A\n"));
2717
- } else if (options.costEstimate) {
2737
+ } else if (options.effortEstimate) {
2718
2738
  process.stderr.write(chalk4.dim("Migration estimate: N/A\n"));
2719
2739
  }
2720
2740
  process.exit(0);
@@ -2723,7 +2743,7 @@ Examples:
2723
2743
  scanResult,
2724
2744
  scanResult.migrationInventory ?? []
2725
2745
  );
2726
- const renderOptions = options.costEstimate ? { estimate: computeEstimate(auditReport.readiness, void 0, hourlyRate) } : void 0;
2746
+ const renderOptions = options.effortEstimate ? { estimate: computeEstimate(auditReport.readiness, void 0, hourlyRate) } : void 0;
2727
2747
  let output;
2728
2748
  if (format === "json") {
2729
2749
  output = formatAuditJson(auditReport, renderOptions);
@@ -2762,7 +2782,7 @@ Examples:
2762
2782
  process.stderr.write("\n");
2763
2783
  if (readiness.grade === "not-applicable") {
2764
2784
  process.stderr.write(chalk4.dim("Migration readiness: N/A \u2014 no direct LaunchDarkly calls detected.\n"));
2765
- if (options.costEstimate) {
2785
+ if (options.effortEstimate) {
2766
2786
  process.stderr.write(chalk4.dim("Migration estimate: N/A\n"));
2767
2787
  }
2768
2788
  } else {
@@ -2777,7 +2797,7 @@ Examples:
2777
2797
  `
2778
2798
  )
2779
2799
  );
2780
- if (options.costEstimate && renderOptions?.estimate) {
2800
+ if (options.effortEstimate && renderOptions?.estimate) {
2781
2801
  const est = renderOptions.estimate;
2782
2802
  process.stderr.write("\n");
2783
2803
  process.stderr.write(
@@ -2800,7 +2820,7 @@ Examples:
2800
2820
  // src/cli.ts
2801
2821
  function createCLI() {
2802
2822
  const program2 = new Command();
2803
- program2.name("flaglint").description("Scan LaunchDarkly Node.js SDK usage, generate safe OpenFeature migration diffs, and enforce the vendor boundary in CI.").version("0.7.0", "-v, --version", "output the current version").addHelpText(
2823
+ program2.name("flaglint").description("Find every direct LaunchDarkly SDK call, detect flag debt, and generate safe OpenFeature migration plans. Static analysis \u2014 no API key required.").version("0.9.0", "-v, --version", "output the current version").addHelpText(
2804
2824
  "after",
2805
2825
  `
2806
2826
  Examples:
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "flaglint",
3
- "version": "0.7.0",
4
- "description": "Scan LaunchDarkly Node.js SDK usage, generate safe OpenFeature migration diffs, and enforce the vendor boundary in CI.",
3
+ "version": "0.9.0",
4
+ "description": "Find every direct LaunchDarkly SDK call, detect flag debt, and generate safe OpenFeature migration plans. Static analysis no API key required.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "flaglint": "dist/bin/flaglint.js"
@@ -16,11 +16,20 @@
16
16
  "node": ">=20"
17
17
  },
18
18
  "keywords": [
19
- "feature-flags",
20
19
  "launchdarkly",
21
20
  "openfeature",
22
- "cli",
21
+ "feature-flags",
22
+ "feature-flag-migration",
23
+ "launchdarkly-migration",
24
+ "openfeature-migration",
23
25
  "flag-debt",
26
+ "static-analysis",
27
+ "codemod",
28
+ "cli",
29
+ "typescript",
30
+ "nodejs",
31
+ "cncf",
32
+ "migration-tool",
24
33
  "developer-tools"
25
34
  ],
26
35
  "author": "flaglint",
@@ -51,6 +60,8 @@
51
60
  "preview:docs": "ASTRO_TELEMETRY_DISABLED=1 astro preview --port 4321",
52
61
  "test:package": "tsx scripts/test-package.ts",
53
62
  "test:smoke": "playwright test",
63
+ "metrics": "node scripts/metrics/collect.mjs",
64
+ "metrics:report": "node scripts/metrics/weekly-report.mjs",
54
65
  "new-branch": "tsx scripts/new-branch.ts",
55
66
  "release:patch": "tsx scripts/release.ts patch",
56
67
  "release:minor": "tsx scripts/release.ts minor",