flaglint 0.8.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.
Files changed (2) hide show
  1. package/dist/bin/flaglint.js +33 -13
  2. package/package.json +14 -4
@@ -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.8.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.8.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.8.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">
@@ -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.8.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.8.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",
@@ -52,6 +61,7 @@
52
61
  "test:package": "tsx scripts/test-package.ts",
53
62
  "test:smoke": "playwright test",
54
63
  "metrics": "node scripts/metrics/collect.mjs",
64
+ "metrics:report": "node scripts/metrics/weekly-report.mjs",
55
65
  "new-branch": "tsx scripts/new-branch.ts",
56
66
  "release:patch": "tsx scripts/release.ts patch",
57
67
  "release:minor": "tsx scripts/release.ts minor",