@flagshark/core 2.2.1 → 2.3.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.
Files changed (47) hide show
  1. package/dist/config/schema.d.ts +10 -10
  2. package/dist/detection/detectors/typescript.d.ts.map +1 -1
  3. package/dist/detection/detectors/typescript.js +62 -21
  4. package/dist/detection/detectors/typescript.js.map +1 -1
  5. package/dist/output/json.d.ts.map +1 -1
  6. package/dist/output/json.js +37 -0
  7. package/dist/output/json.js.map +1 -1
  8. package/dist/output/text.d.ts.map +1 -1
  9. package/dist/output/text.js +14 -2
  10. package/dist/output/text.js.map +1 -1
  11. package/dist/providers/cache.d.ts +2 -0
  12. package/dist/providers/cache.d.ts.map +1 -1
  13. package/dist/providers/cache.js +10 -1
  14. package/dist/providers/cache.js.map +1 -1
  15. package/dist/providers/cross-reference.d.ts +36 -13
  16. package/dist/providers/cross-reference.d.ts.map +1 -1
  17. package/dist/providers/cross-reference.js +166 -52
  18. package/dist/providers/cross-reference.js.map +1 -1
  19. package/dist/providers/index.d.ts +2 -0
  20. package/dist/providers/index.d.ts.map +1 -1
  21. package/dist/providers/index.js.map +1 -1
  22. package/dist/providers/interface.d.ts +91 -3
  23. package/dist/providers/interface.d.ts.map +1 -1
  24. package/dist/providers/launchdarkly/client.d.ts +8 -0
  25. package/dist/providers/launchdarkly/client.d.ts.map +1 -1
  26. package/dist/providers/launchdarkly/client.js +84 -2
  27. package/dist/providers/launchdarkly/client.js.map +1 -1
  28. package/dist/providers/launchdarkly/definition.d.ts +41 -4
  29. package/dist/providers/launchdarkly/definition.d.ts.map +1 -1
  30. package/dist/providers/launchdarkly/definition.js +36 -7
  31. package/dist/providers/launchdarkly/definition.js.map +1 -1
  32. package/dist/providers/launchdarkly/types.d.ts +443 -0
  33. package/dist/providers/launchdarkly/types.d.ts.map +1 -1
  34. package/dist/providers/launchdarkly/types.js +52 -0
  35. package/dist/providers/launchdarkly/types.js.map +1 -1
  36. package/dist/providers/orchestrate.d.ts +38 -5
  37. package/dist/providers/orchestrate.d.ts.map +1 -1
  38. package/dist/providers/orchestrate.js +130 -22
  39. package/dist/providers/orchestrate.js.map +1 -1
  40. package/dist/scan-repo.d.ts.map +1 -1
  41. package/dist/scan-repo.js +2 -1
  42. package/dist/scan-repo.js.map +1 -1
  43. package/dist/staleness.d.ts +42 -2
  44. package/dist/staleness.d.ts.map +1 -1
  45. package/dist/staleness.js +16 -4
  46. package/dist/staleness.js.map +1 -1
  47. package/package.json +1 -1
@@ -1,30 +1,61 @@
1
1
  /**
2
- * Pure function: joins detected flag keys against a platform's flag list,
3
- * emits PlatformSignals based on the platform's view of each flag.
2
+ * Render an env attribution suffix for a multi-env signal description.
4
3
  *
5
- * Signal precedence (most-specific wins; only one primary signal per flag):
6
- * 1. missing-in-platform — flag not in platform at all (error)
7
- * 2. archived-in-platform — flag archived (warning)
8
- * 3. platform-launched — LD says single variation for 7+ days (error)
9
- * 4. platform-inactive — LD says no eval events for 7+ days (warning)
10
- * 5. platform-permanent — user marked permanent (control signal)
11
- * 6. platform-too-old — created > thresholdDays ago (warning)
4
+ * fmtEnvs(['production'], ['production', 'staging']) // 'in production'
5
+ * fmtEnvs(['production', 'staging'], ['production', 'staging']) // 'everywhere'
12
6
  *
13
- * Permanent + too-old can coexist; permanent + inactive can coexist;
14
- * permanent is the strongest CONTROL signal (it suppresses code-side
15
- * heuristics) but doesn't displace platform-side activity signals.
7
+ * Order of envs in the output follows the order of `all` (i.e. the
8
+ * config-declared order, since that's the order the orchestrator builds
9
+ * the inner perEnv map in).
10
+ */
11
+ function fmtEnvs(triggered, all) {
12
+ // Both empty would render 'everywhere' without the second guard, which
13
+ // is semantically wrong. Current call sites all gate on triggered.length > 0
14
+ // before invoking; the guard is defensive insurance for future callers.
15
+ if (triggered.length === all.length && triggered.length > 0)
16
+ return 'everywhere';
17
+ const ordered = all.filter((e) => triggered.includes(e));
18
+ return `in ${ordered.join(', ')}`;
19
+ }
20
+ /**
21
+ * Pure function: joins detected flag keys against per-env platform flag data,
22
+ * emits PlatformSignals based on each platform's view of each flag.
23
+ *
24
+ * Two early-exit signals are sole signals when they fire:
25
+ * - missing-in-platform — flag absent from EVERY env (error)
26
+ * - archived-in-platform — flag archived in the platform (warning)
27
+ *
28
+ * All other signals stack — a single flag can carry several at once:
29
+ * - platform-permanent — user marked the flag permanent (info,
30
+ * control signal; filtered from user output)
31
+ * - platform-too-old — created > thresholdDays ago (warning)
32
+ * - platform-launched — at least one env reports the flag as
33
+ * served-single-variation for 7+ days (error)
34
+ * - platform-inactive — at least one env reports no evaluations in
35
+ * 7+ days, AND no env is platform-launched
36
+ * (warning)
37
+ * - platform-zero-evaluations — at least one env has 0 evaluations over
38
+ * the 30-day window (error)
39
+ * - platform-low-evaluations — at least one env is below the threshold,
40
+ * AND no env is at zero (warning)
41
+ * - platform-untouched-stale — EVERY env's audit log confirmed zero
42
+ * activity within the window (warning, the
43
+ * one strict-all-envs rule)
44
+ *
45
+ * For multi-env scans, signal descriptions carry env attribution
46
+ * ('everywhere' when all envs agree, 'in env1, env2' otherwise) via the
47
+ * fmtEnvs helper above.
16
48
  *
17
49
  * Does NOT surface platform flags with no code reference — that's a separate
18
50
  * "orphan platform flags" feature, out of scope.
19
51
  */
20
- export function crossReference(detectedFlags, platformFlags, platformDisplayName, options = {}) {
21
- const platformByKey = new Map(platformFlags.map((f) => [f.key, f]));
52
+ export function crossReference(detectedFlags, platformFlagsByEnv, platformDisplayName, options = {}) {
22
53
  const out = new Map();
23
54
  const now = Date.now();
24
55
  const thresholdMs = options.thresholdDays != null ? options.thresholdDays * 86_400_000 : null;
25
56
  for (const key of detectedFlags.keys()) {
26
- const platform = platformByKey.get(key);
27
- if (!platform) {
57
+ const envMap = platformFlagsByEnv.get(key);
58
+ if (!envMap || envMap.size === 0) {
28
59
  out.set(key, [
29
60
  {
30
61
  type: 'missing-in-platform',
@@ -34,7 +65,11 @@ export function crossReference(detectedFlags, platformFlags, platformDisplayName
34
65
  ]);
35
66
  continue;
36
67
  }
37
- if (platform.archived) {
68
+ // Flag-level fields (archived, permanent, tags, maintainer, createdAt)
69
+ // are identical across envs in LD's data model. Read them from the
70
+ // first env's entry.
71
+ const firstEntry = envMap.values().next().value;
72
+ if (firstEntry.archived) {
38
73
  out.set(key, [
39
74
  {
40
75
  type: 'archived-in-platform',
@@ -44,24 +79,35 @@ export function crossReference(detectedFlags, platformFlags, platformDisplayName
44
79
  ]);
45
80
  continue;
46
81
  }
47
- // Stack-able signals: platform-permanent (control), platform-too-old,
48
- // platform-inactive/launched. A single flag can carry multiple.
49
82
  const signals = [];
50
- if (platform.status === 'launched') {
83
+ const allEnvs = Array.from(envMap.keys());
84
+ // platform-launched: fire-on-any with env attribution. Inactive is
85
+ // handled in a separate block below so we can suppress it when any
86
+ // env is launched (they're contradictory states).
87
+ const launchedEnvs = allEnvs.filter((e) => envMap.get(e).status === 'launched');
88
+ if (launchedEnvs.length > 0) {
89
+ const where = fmtEnvs(launchedEnvs, allEnvs);
51
90
  signals.push({
52
91
  type: 'platform-launched',
53
92
  severity: 'error',
54
- description: `${platformDisplayName} reports this flag has served one variation for 7+ days — likely ready for removal`,
93
+ description: `${platformDisplayName} reports this flag has served one variation for 7+ days ${where} — likely ready for removal`,
55
94
  });
56
95
  }
57
- else if (platform.status === 'inactive') {
58
- signals.push({
59
- type: 'platform-inactive',
60
- severity: 'warning',
61
- description: `no evaluations recorded in ${platformDisplayName} in the last 7+ days`,
62
- });
96
+ // platform-inactive: fire-on-any-env, BUT suppress when any env is
97
+ // launched (they're contradictory; launched is the more severe
98
+ // and more actionable signal).
99
+ if (launchedEnvs.length === 0) {
100
+ const inactiveEnvs = allEnvs.filter((e) => envMap.get(e).status === 'inactive');
101
+ if (inactiveEnvs.length > 0) {
102
+ const where = fmtEnvs(inactiveEnvs, allEnvs);
103
+ signals.push({
104
+ type: 'platform-inactive',
105
+ severity: 'warning',
106
+ description: `no evaluations recorded in ${platformDisplayName} ${where} in the last 7+ days`,
107
+ });
108
+ }
63
109
  }
64
- if (platform.permanent) {
110
+ if (firstEntry.permanent) {
65
111
  // Control signal: tells staleness.ts to suppress age + low-usage
66
112
  // signals. Filtered out of the user-facing StaleFlag.signals array
67
113
  // before display. Kill-switches and other intentionally permanent
@@ -77,11 +123,11 @@ export function crossReference(detectedFlags, platformFlags, platformDisplayName
77
123
  // exposed a createdAt timestamp. Permanent flags suppress this too —
78
124
  // the user explicitly chose to keep a long-lived flag, so don't
79
125
  // contradict them with a too-old warning.
80
- if (!platform.permanent &&
126
+ if (!firstEntry.permanent &&
81
127
  thresholdMs != null &&
82
- platform.createdAt &&
83
- now - platform.createdAt.getTime() > thresholdMs) {
84
- const ageDays = Math.floor((now - platform.createdAt.getTime()) / 86_400_000);
128
+ firstEntry.createdAt &&
129
+ now - firstEntry.createdAt.getTime() > thresholdMs) {
130
+ const ageDays = Math.floor((now - firstEntry.createdAt.getTime()) / 86_400_000);
85
131
  signals.push({
86
132
  type: 'platform-too-old',
87
133
  severity: 'warning',
@@ -97,38 +143,106 @@ export function crossReference(detectedFlags, platformFlags, platformDisplayName
97
143
  // `undefined` means the feature isn't available for this account
98
144
  // (tier-gated / endpoint 404'd); `null` means available but no
99
145
  // window data yet. Both fall through to no signal.
100
- if (!platform.permanent && typeof platform.evaluations30d === 'number') {
101
- if (platform.evaluations30d === 0) {
146
+ //
147
+ // platform-zero-evaluations: fire-on-any with env attribution.
148
+ // platform-low-evaluations: fire-on-any with env attribution, BUT
149
+ // suppress when any env reports zero (zero is the stronger signal;
150
+ // emitting both creates noise without adding information).
151
+ if (!firstEntry.permanent) {
152
+ const zeroEnvs = [];
153
+ const lowByEnv = [];
154
+ const threshold = options.evaluationThreshold ?? 10;
155
+ for (const env of allEnvs) {
156
+ const evals = envMap.get(env).evaluations30d;
157
+ if (typeof evals !== 'number')
158
+ continue;
159
+ if (evals === 0) {
160
+ zeroEnvs.push(env);
161
+ }
162
+ else if (evals < threshold) {
163
+ lowByEnv.push({ env, count: evals });
164
+ }
165
+ }
166
+ if (zeroEnvs.length > 0) {
167
+ const where = fmtEnvs(zeroEnvs, allEnvs);
102
168
  signals.push({
103
169
  type: 'platform-zero-evaluations',
104
170
  severity: 'error',
105
- description: `0 evaluations in ${platformDisplayName} over the last 30 days — code path is unused`,
171
+ description: `${platformDisplayName} reports 0 evaluations ${where} over the last 30 days — code path is unused`,
106
172
  });
107
173
  }
108
- else {
109
- const threshold = options.evaluationThreshold ?? 10;
110
- if (platform.evaluations30d < threshold) {
111
- signals.push({
112
- type: 'platform-low-evaluations',
113
- severity: 'warning',
114
- description: `only ${platform.evaluations30d} evaluation${platform.evaluations30d === 1 ? '' : 's'} in ${platformDisplayName} over the last 30 days (below threshold ${threshold})`,
115
- });
116
- }
174
+ else if (lowByEnv.length > 0) {
175
+ // Pick the env with the LOWEST count for the canonical
176
+ // description; render the env list when multiple envs are low.
177
+ const lowEnvs = lowByEnv.map((e) => e.env);
178
+ const lowest = lowByEnv.reduce((a, b) => (a.count <= b.count ? a : b));
179
+ const where = fmtEnvs(lowEnvs, allEnvs);
180
+ // When multiple envs are low with different counts, "only N" is
181
+ // ambiguous (reads as 'each env has N'). "as few as N" makes the
182
+ // minimum-across-envs framing explicit. With one low env, "only N"
183
+ // is the right phrasing — exactly N evaluations in exactly that env.
184
+ const countPhrase = lowByEnv.length > 1
185
+ ? `as few as ${lowest.count}`
186
+ : `only ${lowest.count}`;
187
+ signals.push({
188
+ type: 'platform-low-evaluations',
189
+ severity: 'warning',
190
+ description: `${platformDisplayName} reports ${countPhrase} evaluation${lowest.count === 1 ? '' : 's'} ${where} over the last 30 days (below threshold ${threshold})`,
191
+ });
117
192
  }
118
193
  }
119
- // platform-untouched-stale: the audit log confirmed no activity
120
- // (toggles, edits, targeting changes) for this flag within the
121
- // platform's lookback window. Unlike most signals, this is NOT
122
- // suppressed for permanent flags a kill switch untouched for
123
- // 3 years should still get a periodic review even if the user
124
- // intends to keep it permanent.
125
- if (platform.lastTouched === null) {
194
+ // platform-untouched-stale: STRICT all-envs rule.
195
+ // Untouched only counts if EVERY env's audit log was successfully
196
+ // fetched AND showed zero activity (lastTouched === null exactly —
197
+ // not undefined, which means the fetch couldn't confirm). A flag
198
+ // toggled in staging counts as touched; a flag whose staging audit
199
+ // log we couldn't read is "unknown", not "untouched".
200
+ //
201
+ // Unlike most signals, this is NOT suppressed for permanent flags —
202
+ // a kill switch untouched for 3 years should still get a periodic
203
+ // review even if the user intends to keep it permanent.
204
+ const allUntouched = allEnvs.length > 0
205
+ && allEnvs.every((e) => envMap.get(e).lastTouched === null);
206
+ if (allUntouched) {
207
+ // TODO: '90+ days' is hardcoded to AUDIT_LOG_WINDOW_DAYS in the LD
208
+ // client (packages/core/src/providers/launchdarkly/client.ts). If
209
+ // the window becomes a config knob, plumb the actual value through
210
+ // here so the description doesn't lie.
126
211
  signals.push({
127
212
  type: 'platform-untouched-stale',
128
213
  severity: 'warning',
129
- description: `no activity in ${platformDisplayName} for 90+ days (audit log)`,
214
+ description: `no activity in ${platformDisplayName} ${fmtEnvs(allEnvs, allEnvs)} for 90+ days (audit log)`,
130
215
  });
131
216
  }
217
+ // coverage-gap-vs-platform: LD's code-refs feature reports more
218
+ // references for this flag than FlagShark detected. The delta is
219
+ // detector blind spots — code patterns LD recognizes that our
220
+ // language detectors missed. Info severity: doesn't make the flag
221
+ // stale, just surfaces a detection-quality issue for triage.
222
+ //
223
+ // Fires only LD-says-more. Reverse direction (FlagShark finds more)
224
+ // is expected: LD's ld-find-code-refs CLI excludes test files,
225
+ // node_modules, etc. by default — FlagShark scans them.
226
+ //
227
+ // Three states of codeReferences (read from firstEntry — the field
228
+ // is flag-level, identical across envs in LD's data model):
229
+ // - undefined → feature unavailable, no signal
230
+ // - null → LD says 0 (no gap possible — FlagShark count is ≥ 0)
231
+ // - { count } → compare count vs FlagShark's detection count
232
+ if (firstEntry.codeReferences && firstEntry.codeReferences.count > 0) {
233
+ /* v8 ignore next — ?? 0 fallback not exercised by current fixtures */
234
+ const detected = detectedFlags.get(key)?.length ?? 0;
235
+ if (firstEntry.codeReferences.count > detected) {
236
+ const gap = firstEntry.codeReferences.count - detected;
237
+ const platCount = firstEntry.codeReferences.count;
238
+ signals.push({
239
+ type: 'coverage-gap-vs-platform',
240
+ severity: 'info',
241
+ description: `${platformDisplayName} detected ${platCount} reference${platCount === 1 ? '' : 's'} ` +
242
+ `for this flag; FlagShark detected ${detected} (gap: ${gap})`,
243
+ });
244
+ }
245
+ }
132
246
  if (signals.length > 0) {
133
247
  out.set(key, signals);
134
248
  }
@@ -1 +1 @@
1
- {"version":3,"file":"cross-reference.js","sourceRoot":"","sources":["../../src/providers/cross-reference.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cross-reference.js","sourceRoot":"","sources":["../../src/providers/cross-reference.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,7 @@
1
1
  export type { PlatformFlag, PlatformClient, PlatformDefinition, PlatformSignal, } from './interface.js';
2
2
  export { platformRegistry, findPlatform } from './registry.js';
3
3
  export { crossReference, mergePlatformSignals } from './cross-reference.js';
4
+ export type { PerEnvFlags } from './cross-reference.js';
5
+ export type { PerFlagEnvironmentData } from './orchestrate.js';
4
6
  export { computeCacheKey, readCache, writeCache, loadPlatformFlagsCached, type CacheOptions, } from './cache.js';
5
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,YAAY,EACZ,cAAc,EACd,kBAAkB,EAClB,cAAc,GACf,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC9D,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAC3E,OAAO,EACL,eAAe,EACf,SAAS,EACT,UAAU,EACV,uBAAuB,EACvB,KAAK,YAAY,GAClB,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,YAAY,EACZ,cAAc,EACd,kBAAkB,EAClB,cAAc,GACf,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC9D,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAC3E,YAAY,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AACvD,YAAY,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAA;AAC9D,OAAO,EACL,eAAe,EACf,SAAS,EACT,UAAU,EACV,uBAAuB,EACvB,KAAK,YAAY,GAClB,MAAM,YAAY,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC9D,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAC3E,OAAO,EACL,eAAe,EACf,SAAS,EACT,UAAU,EACV,uBAAuB,GAExB,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC9D,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAG3E,OAAO,EACL,eAAe,EACf,SAAS,EACT,UAAU,EACV,uBAAuB,GAExB,MAAM,YAAY,CAAA"}
@@ -1,4 +1,18 @@
1
- import type { ZodType } from 'zod';
1
+ import type { ZodType, ZodTypeDef } from 'zod';
2
+ import type { ScanLogger } from '../scan-repo.js';
3
+ /**
4
+ * One variation definition on a flag. The `value` is the runtime value
5
+ * the variation serves (boolean, string, number, JSON object — anything
6
+ * the SDK supports). The `name` is the user-defined label shown in the
7
+ * LD UI ("on", "off", "treatment", etc.).
8
+ *
9
+ * Cleanup pipelines look up the substitute value by index — see
10
+ * PlatformFlag.fallthroughVariation and offVariation.
11
+ */
12
+ export type FlagVariation = {
13
+ value: unknown;
14
+ name?: string;
15
+ };
2
16
  /** A flag entry as reported by a flag-management platform's API. */
3
17
  export interface PlatformFlag {
4
18
  key: string;
@@ -112,6 +126,73 @@ export interface PlatformFlag {
112
126
  * surfacing.
113
127
  */
114
128
  lastTouched?: Date | null;
129
+ /**
130
+ * Variation definitions for this flag — flag-level (identical across envs
131
+ * in LD's data model). Indexed by the same integer the per-env
132
+ * `fallthroughVariation` and `offVariation` fields point at. SaaS-side
133
+ * cleanup pipelines look up the substitute value via these indices
134
+ * instead of guessing from code-side default arguments.
135
+ *
136
+ * Undefined when the platform doesn't expose variation data (other
137
+ * providers without an equivalent concept) or when the flag-list
138
+ * response unexpectedly omits the field.
139
+ */
140
+ variations?: FlagVariation[];
141
+ /**
142
+ * Whether the flag is enabled in the configured env. When false, LD
143
+ * serves `offVariation` regardless of fallthrough/targeting rules.
144
+ * Per-env field — the LD client populates this from the configured env's
145
+ * data on each per-env fetch the orchestrator runs.
146
+ *
147
+ * Undefined when envData itself is absent (archived flags or unexpected
148
+ * API responses).
149
+ */
150
+ on?: boolean;
151
+ /**
152
+ * Variation index served by fallthrough when no targeting/rule matches
153
+ * AND `on: true`.
154
+ * - number → 100% rollout to that variation
155
+ * - null → split rollout (`fallthrough.rollout` shape) or fallthrough
156
+ * absent. **Preserve null literally in output** — SaaS uses
157
+ * it to detect "can't substitute" and fail closed.
158
+ *
159
+ * Non-optional: the LD client's `?? null` normalization guarantees this
160
+ * field is always set (either a number or null) on every returned flag.
161
+ */
162
+ fallthroughVariation: number | null;
163
+ /**
164
+ * Variation index served when `on: false`. Required by LD on every
165
+ * active flag (LD's API rejects flag creation without it). Undefined
166
+ * when the platform omits the field (archived flags or unexpected API
167
+ * responses).
168
+ */
169
+ offVariation?: number;
170
+ /**
171
+ * Cross-check data from LaunchDarkly's own code-references feature.
172
+ * LD's `ld-find-code-refs` CLI scans repos and reports how many code
173
+ * references exist for each flag. FlagShark cross-checks LD's count
174
+ * against its own detection count to surface detector blind spots —
175
+ * flags LD found 12 references for but FlagShark only found 8 indicate
176
+ * 4 hunks our patterns missed.
177
+ *
178
+ * Three-state (matches the evaluations30d convention):
179
+ * - undefined → feature not available (tier-gated, not configured for
180
+ * this project, or the aux fetch errored). No signal
181
+ * emitted; the LD client logs a single advisory per scan.
182
+ * - null → feature available; LD has zero references for this
183
+ * flag. No signal — zero refs cannot be a "gap"; FlagShark's
184
+ * detection count is necessarily ≥ 0.
185
+ * - { count } → LD found `count` references for this flag (sum of
186
+ * hunkCount across all repos LD scanned).
187
+ *
188
+ * Used by cross-reference to emit `coverage-gap-vs-platform` when
189
+ * count > FlagShark's detection count. LD-says-more direction only —
190
+ * reverse direction is expected: FlagShark scans test files that
191
+ * ld-find-code-refs excludes by default.
192
+ */
193
+ codeReferences?: {
194
+ count: number;
195
+ } | null;
115
196
  }
116
197
  /** Runtime client for a configured platform. Returned by PlatformDefinition.createClient. */
117
198
  export interface PlatformClient {
@@ -121,6 +202,7 @@ export interface PlatformClient {
121
202
  displayName: string;
122
203
  listFlags(opts?: {
123
204
  signal?: AbortSignal;
205
+ logger?: ScanLogger;
124
206
  }): Promise<PlatformFlag[]>;
125
207
  }
126
208
  /** Registry entry. Each platform implementation exports exactly one of these. */
@@ -131,7 +213,7 @@ export interface PlatformDefinition<TConfig = unknown> {
131
213
  /** Env var read for the secret token. User can override via token_env. */
132
214
  defaultTokenEnv: string;
133
215
  /** Zod schema validating this platform's config block. */
134
- configSchema: ZodType<TConfig>;
216
+ configSchema: ZodType<TConfig, ZodTypeDef, unknown>;
135
217
  /** Factory — validated config + resolved token → runtime client. No IO until listFlags() is called. */
136
218
  createClient: (config: TConfig, token: string) => PlatformClient;
137
219
  }
@@ -165,8 +247,14 @@ export interface PlatformSignal {
165
247
  * because it's based on actual usage, not heuristics.
166
248
  * - `platform-low-evaluations` (warning): evaluations below the
167
249
  * configured threshold over the window. Default threshold: 10/30d.
250
+ * - `coverage-gap-vs-platform` (info): LD's own code-refs feature reports
251
+ * more references for this flag than FlagShark detected. Surfaces detector
252
+ * blind spots — code patterns LD recognizes that our language detectors
253
+ * missed. Doesn't make the flag stale; informational only. Fires only
254
+ * when LD count > FlagShark count; reverse direction (FlagShark counts
255
+ * more) is expected for projects that test-file-exclude in LD.
168
256
  */
169
- type: 'missing-in-platform' | 'archived-in-platform' | 'platform-permanent' | 'platform-too-old' | 'platform-inactive' | 'platform-launched' | 'platform-zero-evaluations' | 'platform-low-evaluations' | 'platform-untouched-stale';
257
+ type: 'missing-in-platform' | 'archived-in-platform' | 'platform-permanent' | 'platform-too-old' | 'platform-inactive' | 'platform-launched' | 'platform-zero-evaluations' | 'platform-low-evaluations' | 'platform-untouched-stale' | 'coverage-gap-vs-platform';
170
258
  severity: 'error' | 'warning' | 'info';
171
259
  description: string;
172
260
  }
@@ -1 +1 @@
1
- {"version":3,"file":"interface.d.ts","sourceRoot":"","sources":["../../src/providers/interface.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,KAAK,CAAA;AAElC,oEAAoE;AACpE,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAA;IACX,gFAAgF;IAChF,QAAQ,EAAE,OAAO,CAAA;IACjB,YAAY,EAAE,IAAI,GAAG,IAAI,CAAA;IACzB;;;;;;;;;;;;;;;OAeG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IAEnB;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,IAAI,GAAG,IAAI,CAAA;IAEvB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IAEf;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IAEnB;;;;;;;;;;;;;;;OAeG;IACH,MAAM,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,UAAU,GAAG,UAAU,CAAA;IAEnD;;;;;OAKG;IACH,aAAa,CAAC,EAAE,IAAI,GAAG,IAAI,CAAA;IAE3B;;;;;;;;;;;;;;;;;;;OAmBG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAE9B;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,WAAW,CAAC,EAAE,IAAI,GAAG,IAAI,CAAA;CAC1B;AAED,6FAA6F;AAC7F,MAAM,WAAW,cAAc;IAC7B,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAA;IACZ,4EAA4E;IAC5E,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAAA;CACpE;AAED,iFAAiF;AACjF,MAAM,WAAW,kBAAkB,CAAC,OAAO,GAAG,OAAO;IACnD,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,0EAA0E;IAC1E,eAAe,EAAE,MAAM,CAAA;IACvB,0DAA0D;IAC1D,YAAY,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;IAC9B,uGAAuG;IACvG,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,KAAK,cAAc,CAAA;CACjE;AAED,gGAAgG;AAChG,MAAM,WAAW,cAAc;IAC7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACH,IAAI,EACA,qBAAqB,GACrB,sBAAsB,GACtB,oBAAoB,GACpB,kBAAkB,GAClB,mBAAmB,GACnB,mBAAmB,GACnB,2BAA2B,GAC3B,0BAA0B,GAC1B,0BAA0B,CAAA;IAC9B,QAAQ,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAA;IACtC,WAAW,EAAE,MAAM,CAAA;CACpB"}
1
+ {"version":3,"file":"interface.d.ts","sourceRoot":"","sources":["../../src/providers/interface.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import type { PlatformFlag } from '../interface.js';
2
+ import type { ScanLogger } from '../../scan-repo.js';
2
3
  export interface FetchAllFlagsConfig {
3
4
  project: string;
4
5
  environment: string;
@@ -8,6 +9,13 @@ export interface FetchAllFlagsOptions {
8
9
  apiBase?: string;
9
10
  fetch?: typeof globalThis.fetch;
10
11
  signal?: AbortSignal;
12
+ /**
13
+ * Optional logger for one-line advisory messages emitted by aux
14
+ * fetches (e.g. code-refs not configured). When unset, advisories
15
+ * are silently dropped — useful for tests and direct callers that
16
+ * don't go through the orchestrator.
17
+ */
18
+ logger?: ScanLogger;
11
19
  }
12
20
  /**
13
21
  * Fetches every flag in `config.project` (active + archived), then enriches
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/providers/launchdarkly/client.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAgCnD,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;IAC/B,MAAM,CAAC,EAAE,WAAW,CAAA;CACrB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,mBAAmB,EAC3B,IAAI,GAAE,oBAAyB,GAC9B,OAAO,CAAC,YAAY,EAAE,CAAC,CAuHzB"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/providers/launchdarkly/client.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,YAAY,EAAiB,MAAM,iBAAiB,CAAA;AAClE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAgCpD,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;IAC/B,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,UAAU,CAAA;CACpB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,mBAAmB,EAC3B,IAAI,GAAE,oBAAyB,GAC9B,OAAO,CAAC,YAAY,EAAE,CAAC,CAuJzB"}
@@ -1,5 +1,5 @@
1
1
  import pLimit from 'p-limit';
2
- import { AuditLogResponseSchema, EvaluationsResponseSchema, FlagsResponseSchema, FlagStatusesResponseSchema, MembersResponseSchema, } from './types.js';
2
+ import { AuditLogResponseSchema, CodeRefsStatisticsResponseSchema, EvaluationsResponseSchema, FlagsResponseSchema, FlagStatusesResponseSchema, MembersResponseSchema, } from './types.js';
3
3
  import { LdApiError } from './errors.js';
4
4
  const DEFAULT_API_BASE = 'https://app.launchdarkly.com';
5
5
  const LD_API_VERSION = '20240415';
@@ -79,6 +79,14 @@ export async function fetchAllFlags(config, opts = {}) {
79
79
  // Resolved below from the /members lookup; left as the opaque id
80
80
  // for now so the producer/consumer split stays clean.
81
81
  maintainer: item.maintainerId,
82
+ // The cast narrows the inferred type from `VariationSchema.passthrough()`
83
+ // (which infers `Record<string, unknown>` for extra fields) back to the
84
+ // declared `PlatformFlag.variations` shape. The runtime data is
85
+ // identical; the cast is a structural type-narrowing only.
86
+ variations: item.variations,
87
+ on: envData?.on,
88
+ fallthroughVariation: envData?.fallthrough?.variation ?? null,
89
+ offVariation: envData?.offVariation,
82
90
  });
83
91
  }
84
92
  path = parsed._links?.next?.href;
@@ -146,6 +154,29 @@ export async function fetchAllFlags(config, opts = {}) {
146
154
  flag.lastTouched = lastTouched.get(flag.key) ?? null;
147
155
  }
148
156
  }
157
+ // Aux 5: LD code-references (cross-check against our own detection).
158
+ // Single project-scoped call; not env-scoped. Surfaces detector blind
159
+ // spots when LD finds more references than FlagShark detected. Opt-in
160
+ // LD feature; when unavailable we log an advisory pointing customers
161
+ // at the setup docs.
162
+ const codeRefs = await fetchCodeReferences(config, apiBase, headers, fetchFn, opts.signal);
163
+ if (codeRefs === null) {
164
+ // Feature unavailable — single advisory line. info-severity (not
165
+ // warn) because this isn't a failure, just a missed-opportunity
166
+ // nudge. The existing logger threading from Task 1 delivers it
167
+ // through the orchestrator into the scan output.
168
+ opts.logger?.info(`LaunchDarkly code-references not available for this project — enable it in LD ` +
169
+ `(Code references → Connect repository) to get coverage-gap diagnostics. ` +
170
+ `See https://launchdarkly.com/docs/home/observability/code-references for setup.`);
171
+ }
172
+ else {
173
+ for (const flag of out) {
174
+ if (flag.archived)
175
+ continue;
176
+ const count = codeRefs.get(flag.key) ?? 0;
177
+ flag.codeReferences = count > 0 ? { count } : null;
178
+ }
179
+ }
149
180
  return out;
150
181
  }
151
182
  /**
@@ -217,7 +248,12 @@ function buildFirstPath(project, environment, archived = false) {
217
248
  env: environment,
218
249
  limit: '100',
219
250
  offset: '0',
220
- summary: '1',
251
+ // summary=0 returns full flag objects with variations + per-env
252
+ // fallthrough/on/offVariation. summary=1 (the previous default) only
253
+ // returned key/tags/lastModified, which was insufficient for SaaS
254
+ // Piranha to substitute the correct value during cleanup. Payload
255
+ // grows ~1-5KB per flag — well within reasonable for paginated fetch.
256
+ summary: '0',
221
257
  });
222
258
  if (archived)
223
259
  params.set('archived', 'true');
@@ -368,4 +404,50 @@ async function fetchLastTouchedMap(config, apiBase, headers, fetchFn, signal) {
368
404
  return null;
369
405
  return lastTouched;
370
406
  }
407
+ /**
408
+ * Best-effort fetch of LD's per-flag code-references statistics.
409
+ *
410
+ * Returns:
411
+ * - Map<flagKey, totalHunkCount> on success (possibly empty if no flag
412
+ * has refs yet — e.g. CI ran but matched nothing)
413
+ * - null when:
414
+ * - 401/403/404 (tier-gated / not configured for this project)
415
+ * - 5xx / 429 (transient errors)
416
+ * - network or JSON parse failure
417
+ *
418
+ * Caller distinguishes:
419
+ * - null → feature unavailable; leave codeReferences undefined
420
+ * per flag AND emit the "not configured" advisory
421
+ * - empty Map → feature available but no refs yet; flags get null
422
+ * - populated Map → join by flag key; flags absent get null
423
+ */
424
+ async function fetchCodeReferences(config, apiBase, headers, fetchFn, signal) {
425
+ try {
426
+ const url = new URL(`/api/v2/code-refs/statistics/${encodeURIComponent(config.project)}`, apiBase);
427
+ const res = await fetchFn(url, { headers, signal });
428
+ if (res.status === 401 || res.status === 403 || res.status === 404) {
429
+ return null;
430
+ }
431
+ /* v8 ignore next — non-401/403/404 error status; not exercised by current fixtures. */
432
+ if (!res.ok)
433
+ return null;
434
+ const parsed = CodeRefsStatisticsResponseSchema.parse(await res.json());
435
+ const out = new Map();
436
+ for (const [flagKey, repoEntries] of Object.entries(parsed.flags)) {
437
+ let total = 0;
438
+ for (const entry of repoEntries) {
439
+ if (typeof entry.hunkCount === 'number')
440
+ total += entry.hunkCount;
441
+ }
442
+ out.set(flagKey, total);
443
+ }
444
+ return out;
445
+ /* v8 ignore start — defensive catch for malformed JSON / schema
446
+ drift; not exercised by current fixtures. */
447
+ }
448
+ catch {
449
+ return null;
450
+ }
451
+ /* v8 ignore stop */
452
+ }
371
453
  //# sourceMappingURL=client.js.map