@quonfig/cli 0.0.50 → 0.0.51

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.
@@ -0,0 +1,402 @@
1
+ /**
2
+ * Flagsmith REST API client — Phase-1 config snapshot.
3
+ *
4
+ * Flagsmith has no bulk-export endpoint, so the fetcher stitches per-resource
5
+ * calls: project → environments → features → per-env featurestates → segments
6
+ * → feature-segments (priority) → edge identities → edge identity overrides
7
+ * → tags.
8
+ *
9
+ * Auth is `Authorization: Api-Key <token>` — note the `Api-Key` prefix (not
10
+ * `Bearer`, and not bare-token like LaunchDarkly). Rate-limiting headers are
11
+ * unpublished; we program against the response: back off on 429 using
12
+ * `Retry-After` (standard) or `X-RateLimit-Reset` (Flagsmith's variant), and
13
+ * step the inter-request throttle up briefly when we hit one (mirrors the
14
+ * Phase-2 generator's empirical backoff loop). On any non-429 error we throw
15
+ * immediately — only rate limiting is retried.
16
+ *
17
+ * Pagination is standard Django REST `{count, next, previous, results}` with
18
+ * absolute `next` URLs. Edge-identities listing is the one exception: it uses
19
+ * `{results, last_evaluated_key}` cursor pagination via a query-string param.
20
+ */
21
+ const PROD_BASE_URL = 'https://api.flagsmith.com/api/v1';
22
+ /** Page size for Django REST endpoints that accept `page_size`. Conservative for memory + 429 risk. */
23
+ const PAGE_LIMIT = 100;
24
+ /** Fallback wait when a 429 carries no usable headers. Mirrors the Phase-2 generator's base. */
25
+ const DEFAULT_BACKOFF_MS = 2000;
26
+ /** Hard cap on retries per request before we give up. */
27
+ const MAX_RETRIES = 6;
28
+ let baseUrl = PROD_BASE_URL;
29
+ export function setFlagsmithBaseUrl(url) {
30
+ baseUrl = url;
31
+ }
32
+ export function selectFlagsmithBaseUrl() {
33
+ return process.env.FLAGSMITH_API_URL ?? PROD_BASE_URL;
34
+ }
35
+ export function applyFlagsmithBaseUrl() {
36
+ baseUrl = selectFlagsmithBaseUrl();
37
+ }
38
+ /** Real timer-backed sleep — swapped out in tests so MSW runs don't actually wait. */
39
+ function timerSleep(ms) {
40
+ return new Promise((resolve) => {
41
+ setTimeout(resolve, ms);
42
+ });
43
+ }
44
+ /** Test seam: a no-op sleep is swapped in so MSW tests don't actually wait. */
45
+ let sleepImpl = timerSleep;
46
+ export function __setSleepForTests(fn) {
47
+ sleepImpl = fn;
48
+ }
49
+ export function __resetSleepForTests() {
50
+ sleepImpl = timerSleep;
51
+ }
52
+ export class FlagsmithApiError extends Error {
53
+ status;
54
+ constructor(status, statusText, url, body) {
55
+ super(`Flagsmith API request failed: ${status} ${statusText} — ${url}\n${body}`);
56
+ this.name = 'FlagsmithApiError';
57
+ this.status = status;
58
+ }
59
+ }
60
+ /**
61
+ * Compute the wait derived from a 429 response's headers.
62
+ *
63
+ * Flagsmith honours the standard `Retry-After` (seconds) and also sometimes
64
+ * sends `X-RateLimit-Reset` — which the Phase-2 generator observed as either
65
+ * seconds-from-now OR an epoch-ms timestamp. Heuristic: > 10^10 means
66
+ * epoch-ms. We bump the wait UP for either signal (never down) so a tight
67
+ * 429 storm doesn't burn through the retry budget faster than the API wants.
68
+ */
69
+ function backoffMsFromHeaders(headers, attempt) {
70
+ // Exponential base: 2s, 4s, 8s, 16s, 32s, 60s (clamped).
71
+ let wait = Math.min(60_000, 2000 * 2 ** attempt);
72
+ const retryAfter = headers.get('Retry-After');
73
+ if (retryAfter !== null) {
74
+ const sec = Number(retryAfter);
75
+ if (Number.isFinite(sec))
76
+ wait = Math.max(wait, sec * 1000);
77
+ }
78
+ const reset = headers.get('X-RateLimit-Reset') ?? headers.get('X-Ratelimit-Reset');
79
+ if (reset !== null) {
80
+ const n = Number(reset);
81
+ if (Number.isFinite(n)) {
82
+ // > 10^10 ⇒ epoch ms; ≤ 10^10 ⇒ seconds-from-now.
83
+ const ms = n > 1e10 ? Math.max(0, n - Date.now()) : n * 1000;
84
+ wait = Math.max(wait, ms + 500);
85
+ }
86
+ }
87
+ return Math.min(60_000, wait);
88
+ }
89
+ /**
90
+ * Single GET with `Api-Key` auth, 429 backoff against `Retry-After` /
91
+ * `X-RateLimit-Reset`, and an exponential fallback. Non-429 errors throw
92
+ * immediately — only rate limiting is retried.
93
+ */
94
+ export async function apiFetch(path, apiKey) {
95
+ const url = path.startsWith('http') ? path : `${baseUrl}${path}`;
96
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
97
+ // eslint-disable-next-line no-await-in-loop
98
+ const res = await fetch(url, {
99
+ headers: {
100
+ Accept: 'application/json',
101
+ Authorization: `Api-Key ${apiKey}`,
102
+ },
103
+ });
104
+ if (res.status === 429) {
105
+ if (attempt === MAX_RETRIES) {
106
+ // eslint-disable-next-line no-await-in-loop
107
+ const body = await res.text().catch(() => '(no body)');
108
+ throw new FlagsmithApiError(429, res.statusText, url, body);
109
+ }
110
+ // eslint-disable-next-line no-await-in-loop
111
+ await sleepImpl(backoffMsFromHeaders(res.headers, attempt) || DEFAULT_BACKOFF_MS);
112
+ continue;
113
+ }
114
+ if (!res.ok) {
115
+ // eslint-disable-next-line no-await-in-loop
116
+ const body = await res.text().catch(() => '(no body)');
117
+ throw new FlagsmithApiError(res.status, res.statusText, url, body);
118
+ }
119
+ return res.json();
120
+ }
121
+ // Unreachable — the loop either returns or throws.
122
+ throw new FlagsmithApiError(429, 'Too Many Requests', url, 'retries exhausted');
123
+ }
124
+ /**
125
+ * Walks a Django-REST paginated collection. The first page is fetched at
126
+ * `firstPath`; subsequent pages follow the absolute `next` URL until null.
127
+ * Flagsmith's `next` is a full `https://api.flagsmith.com/...` URL; `apiFetch`
128
+ * treats absolute URLs as-is, so this works under the configurable base URL
129
+ * for tests (we strip the production host off so the test base URL applies).
130
+ */
131
+ async function paginate(firstPath, apiKey) {
132
+ const collected = [];
133
+ let nextPath = firstPath;
134
+ const seen = new Set();
135
+ while (nextPath && !seen.has(nextPath)) {
136
+ seen.add(nextPath);
137
+ // eslint-disable-next-line no-await-in-loop
138
+ const page = (await apiFetch(nextPath, apiKey));
139
+ if (Array.isArray(page.results))
140
+ collected.push(...page.results);
141
+ nextPath = page.next ? rewriteNextUrl(page.next) : undefined;
142
+ }
143
+ return collected;
144
+ }
145
+ /**
146
+ * Flagsmith's `next` field is the full production URL. For tests we point at
147
+ * `https://flagsmith.test/api/v1` via setFlagsmithBaseUrl; strip the prod
148
+ * host so the next-page fetch goes through the test server too.
149
+ */
150
+ function rewriteNextUrl(next) {
151
+ if (baseUrl === PROD_BASE_URL)
152
+ return next;
153
+ if (next.startsWith(PROD_BASE_URL))
154
+ return baseUrl + next.slice(PROD_BASE_URL.length);
155
+ return next;
156
+ }
157
+ /** `GET /projects/{id}/` — used as the cheap auth probe AND as a source of `use_edge_identities`. */
158
+ export async function fetchProject(apiKey, projectId) {
159
+ return apiFetch(`/projects/${encodeURIComponent(String(projectId))}/`, apiKey);
160
+ }
161
+ /** `GET /environments/?project={id}` — paginated env list. */
162
+ export async function fetchEnvironments(apiKey, projectId) {
163
+ const qs = new URLSearchParams({ page_size: String(PAGE_LIMIT), project: String(projectId) });
164
+ return paginate(`/environments/?${qs}`, apiKey);
165
+ }
166
+ /**
167
+ * `GET /projects/{pk}/features/` — paginated feature list. Each result
168
+ * includes inline `multivariate_options[]` which we sort ASC by id (API6) so
169
+ * downstream code sees variations in definition order, not the reverse order
170
+ * the API returns.
171
+ */
172
+ export async function fetchFeatures(apiKey, projectId) {
173
+ const qs = new URLSearchParams({ page_size: String(PAGE_LIMIT) });
174
+ const features = await paginate(`/projects/${encodeURIComponent(String(projectId))}/features/?${qs}`, apiKey);
175
+ for (const feature of features) {
176
+ if (Array.isArray(feature.multivariate_options) && feature.multivariate_options.length > 1) {
177
+ feature.multivariate_options = [...feature.multivariate_options].sort((a, b) => a.id - b.id);
178
+ }
179
+ }
180
+ return features;
181
+ }
182
+ /**
183
+ * `GET /features/featurestates/?environment={env_id}` — per-env featurestates.
184
+ *
185
+ * This is the unified listing that returns BOTH env-default rows
186
+ * (`feature_segment == null && identity == null`) AND segment-override rows
187
+ * (`feature_segment != null`). The plan §4.1 suggests
188
+ * `/environments/{api_key}/featurestates/`, but empirically on v2-versioned
189
+ * envs that endpoint silently omits segment-override rows. The
190
+ * `/features/featurestates/?environment=` path is the only read endpoint that
191
+ * returns both on a v2 env, so we use it uniformly across v1 and v2 envs.
192
+ *
193
+ * Identity overrides are NOT in this listing on edge-enabled projects — see
194
+ * `fetchEdgeIdentityOverrides`.
195
+ */
196
+ export async function fetchEnvFeatureStates(apiKey, envId) {
197
+ const qs = new URLSearchParams({ environment: String(envId), page_size: String(PAGE_LIMIT) });
198
+ return paginate(`/features/featurestates/?${qs}`, apiKey);
199
+ }
200
+ /** `GET /projects/{pk}/segments/` — project-scoped segment pool. */
201
+ export async function fetchSegments(apiKey, projectId) {
202
+ const qs = new URLSearchParams({ page_size: String(PAGE_LIMIT) });
203
+ return paginate(`/projects/${encodeURIComponent(String(projectId))}/segments/?${qs}`, apiKey);
204
+ }
205
+ /**
206
+ * `GET /features/feature-segments/?environment={env_id}&feature={feature_id}` —
207
+ * the per-env, per-feature segment priority list. Required by the converter
208
+ * to order the segment-override rules (Quonfig is a strict first-match list).
209
+ *
210
+ * The endpoint REJECTS calls without `feature` (`{feature: ["This field is
211
+ * required."]}`), so we have to walk every feature × env combo. This is N×E
212
+ * calls in the worst case — acceptable for the corpus (~110 features × 2 envs
213
+ * = ~220 calls) and roughly the same order as fetchEnvFeatureStates.
214
+ */
215
+ export async function fetchFeatureSegments(apiKey, envId, featureId) {
216
+ const qs = new URLSearchParams({
217
+ environment: String(envId),
218
+ feature: String(featureId),
219
+ page_size: String(PAGE_LIMIT),
220
+ });
221
+ return paginate(`/features/feature-segments/?${qs}`, apiKey);
222
+ }
223
+ /**
224
+ * `GET /environments/{api_key}/edge-identities/` — paginated edge-identity
225
+ * listing. Pagination here is CURSOR-based (`last_evaluated_key`) not page
226
+ * numbers — the Flagsmith dynamo-backed edge store can't offset-paginate.
227
+ *
228
+ * The fetcher uses this only to surface identifiers for the report; the
229
+ * actual identity-override featurestates come from
230
+ * `fetchEdgeIdentityOverrides`, which is a one-call-per-env shortcut.
231
+ */
232
+ export async function fetchEdgeIdentities(apiKey, envApiKey) {
233
+ const collected = [];
234
+ let cursor = null;
235
+ const seen = new Set();
236
+ for (;;) {
237
+ const qs = new URLSearchParams({ page_size: String(PAGE_LIMIT) });
238
+ if (cursor !== null)
239
+ qs.set('last_evaluated_key', cursor);
240
+ // eslint-disable-next-line no-await-in-loop
241
+ const page = (await apiFetch(`/environments/${encodeURIComponent(envApiKey)}/edge-identities/?${qs}`, apiKey));
242
+ if (Array.isArray(page.results))
243
+ collected.push(...page.results);
244
+ if (!page.last_evaluated_key || seen.has(page.last_evaluated_key))
245
+ break;
246
+ seen.add(page.last_evaluated_key);
247
+ cursor = page.last_evaluated_key;
248
+ }
249
+ return collected;
250
+ }
251
+ /**
252
+ * `GET /environments/{api_key}/edge-identity-overrides` — every identity
253
+ * override in the env, flat. Each row is `{identifier, identity_uuid,
254
+ * feature_state}` so we can index by feature without walking N identities.
255
+ *
256
+ * Note: the path has NO trailing slash (unlike most Flagsmith paths). This
257
+ * endpoint does not paginate in the verified shape — it returns
258
+ * `{results: [...]}` directly.
259
+ */
260
+ export async function fetchEdgeIdentityOverrides(apiKey, envApiKey) {
261
+ const data = (await apiFetch(`/environments/${encodeURIComponent(envApiKey)}/edge-identity-overrides`, apiKey));
262
+ return Array.isArray(data.results) ? data.results : [];
263
+ }
264
+ /** `GET /projects/{pk}/tags/` — project-scoped tag pool. */
265
+ export async function fetchTags(apiKey, projectId) {
266
+ const qs = new URLSearchParams({ page_size: String(PAGE_LIMIT) });
267
+ return paginate(`/projects/${encodeURIComponent(String(projectId))}/tags/?${qs}`, apiKey);
268
+ }
269
+ /**
270
+ * Stitch the whole Phase-1 snapshot: project → environments → features →
271
+ * per-env featurestates (split into env-default + segment-overrides) → segments
272
+ * → feature-segments (per env, per feature with segment overrides) → edge
273
+ * identity overrides → tags.
274
+ *
275
+ * Walk count grows as 1 (project) + 1 (envs) + 1 (segments) + 1 (tags) + E ×
276
+ * (1 featurestates + 1 edge-id-overrides + S features-with-segov ×
277
+ * feature-segments). For the 110-feature, 2-env, 5 features-with-segov live
278
+ * corpus: ~1 + 1 + 1 + 1 + 2 × (1 + 1 + 5) = ~17 calls. The featurestates
279
+ * endpoint is paginated under 100/page so larger projects multiply that
280
+ * featurestates leg.
281
+ *
282
+ * `sinceEpochMs` is currently unused — decision D2 says Phase 1 is always a
283
+ * full re-snapshot. It survives in the signature so reporting (Epic 5) can
284
+ * compute "what's new since last run" against the same snapshot.
285
+ */
286
+ export async function fetchSnapshot(apiKey, projectId) {
287
+ const project = await fetchProject(apiKey, projectId);
288
+ const environments = await fetchEnvironments(apiKey, projectId);
289
+ const features = await fetchFeatures(apiKey, projectId);
290
+ const segments = await fetchSegments(apiKey, projectId);
291
+ const tags = await fetchTags(apiKey, projectId);
292
+ // Index featurestates by env_api_key, then by feature_id. We split each env's
293
+ // featurestate listing into env-default rows (feature_segment == null) and
294
+ // segment-override rows (feature_segment != null) up-front so the per-feature
295
+ // bundle assembly below is a lookup, not a scan.
296
+ const fsByEnvByFeature = new Map();
297
+ for (const env of environments) {
298
+ // eslint-disable-next-line no-await-in-loop
299
+ const featureStates = await fetchEnvFeatureStates(apiKey, env.id);
300
+ const perFeature = new Map();
301
+ for (const fs of featureStates) {
302
+ const bucket = perFeature.get(fs.feature) ?? { default: null, segment_overrides: [] };
303
+ if (fs.feature_segment === null) {
304
+ bucket.default = fs;
305
+ }
306
+ else {
307
+ bucket.segment_overrides.push(fs);
308
+ }
309
+ perFeature.set(fs.feature, bucket);
310
+ }
311
+ fsByEnvByFeature.set(env.api_key, perFeature);
312
+ }
313
+ // Edge-identity overrides indexed by env_api_key → feature_id.
314
+ const idovByEnvByFeature = new Map();
315
+ if (project.use_edge_identities) {
316
+ for (const env of environments) {
317
+ // eslint-disable-next-line no-await-in-loop
318
+ const overrides = await fetchEdgeIdentityOverrides(apiKey, env.api_key);
319
+ const byFeature = new Map();
320
+ for (const ov of overrides) {
321
+ const list = byFeature.get(ov.feature_state.feature) ?? [];
322
+ list.push(ov);
323
+ byFeature.set(ov.feature_state.feature, list);
324
+ }
325
+ idovByEnvByFeature.set(env.api_key, byFeature);
326
+ }
327
+ }
328
+ // feature-segments listing (per env, per feature) for priority. Only fetched
329
+ // for features that actually have segment-override featurestates in this env
330
+ // — saves N×E calls on the common case where most features have no segovs.
331
+ const fsegByEnvByFeature = new Map();
332
+ for (const env of environments) {
333
+ const perFeature = fsByEnvByFeature.get(env.api_key) ?? new Map();
334
+ const featuresWithSegov = [];
335
+ for (const [featureId, bucket] of perFeature) {
336
+ if (bucket.segment_overrides.length > 0)
337
+ featuresWithSegov.push(featureId);
338
+ }
339
+ const byFeature = new Map();
340
+ for (const featureId of featuresWithSegov) {
341
+ // eslint-disable-next-line no-await-in-loop
342
+ const rows = await fetchFeatureSegments(apiKey, env.id, featureId);
343
+ byFeature.set(featureId, rows);
344
+ }
345
+ fsegByEnvByFeature.set(env.api_key, byFeature);
346
+ }
347
+ // Stitch the per-feature bundles together.
348
+ const stitched = features.map((feature) => stitchFeatureBundle(feature, environments, fsByEnvByFeature, idovByEnvByFeature, fsegByEnvByFeature));
349
+ return { environments, features: stitched, project, segments, tags };
350
+ }
351
+ /**
352
+ * For one feature, assemble its per-env bundle. Sorts segment-override
353
+ * featurestates by their `feature_segment` priority (ASC) using the
354
+ * feature-segments lookup, so the converter sees them in evaluation order.
355
+ */
356
+ function stitchFeatureBundle(feature, environments, fsByEnvByFeature, idovByEnvByFeature, fsegByEnvByFeature) {
357
+ const featurestates_by_env = {};
358
+ const feature_segments_by_env = {};
359
+ for (const env of environments) {
360
+ const perFeature = fsByEnvByFeature.get(env.api_key) ?? new Map();
361
+ const bucket = perFeature.get(feature.id) ?? { default: null, segment_overrides: [] };
362
+ const idov = idovByEnvByFeature.get(env.api_key)?.get(feature.id) ?? [];
363
+ const fseg = fsegByEnvByFeature.get(env.api_key)?.get(feature.id) ?? [];
364
+ // Sort segment-override featurestates by priority ASC, using the
365
+ // feature_segments table. Each fs.feature_segment is either a number ID or
366
+ // an inline {id, priority, ...} object — handle both.
367
+ const priorityById = new Map();
368
+ for (const fs of fseg)
369
+ priorityById.set(fs.id, fs.priority);
370
+ const sortedSegov = [...bucket.segment_overrides].sort((a, b) => {
371
+ const pa = priorityFor(a, priorityById);
372
+ const pb = priorityFor(b, priorityById);
373
+ return pa - pb;
374
+ });
375
+ featurestates_by_env[env.api_key] = {
376
+ default: bucket.default,
377
+ identity_overrides: idov,
378
+ segment_overrides: sortedSegov,
379
+ };
380
+ feature_segments_by_env[env.api_key] = fseg;
381
+ }
382
+ return { feature, feature_segments_by_env, featurestates_by_env };
383
+ }
384
+ /**
385
+ * Resolve a segment-override featurestate's priority. `feature_segment` on the
386
+ * row comes as either a numeric ID (legacy endpoint) or an inline `{id,
387
+ * priority, ...}` object (the `/features/featurestates/` endpoint we use).
388
+ * Fall back to a huge number so an unresolved row sorts last instead of NaN.
389
+ */
390
+ function priorityFor(fs, priorityById) {
391
+ const fsRef = fs.feature_segment;
392
+ if (fsRef === null || fsRef === undefined)
393
+ return Number.MAX_SAFE_INTEGER;
394
+ if (typeof fsRef === 'object' && typeof fsRef.priority === 'number')
395
+ return fsRef.priority;
396
+ if (typeof fsRef === 'number') {
397
+ const p = priorityById.get(fsRef);
398
+ if (typeof p === 'number')
399
+ return p;
400
+ }
401
+ return Number.MAX_SAFE_INTEGER;
402
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Flagsmith converter — Flagsmith feature/segment JSON → `QuonfigFile`-ready
3
+ * objects (plain JS objects ready for `JSON.stringify`).
4
+ *
5
+ * Provider-specific topology collapse (plan §5, Epic 3). Calls the shared
6
+ * `quonfig-target/` verb library where the structure is identical to LD
7
+ * (value coercion, criterion + rule shape, report wiring) and handles the
8
+ * Flagsmith-shaped bits here: synthesizing the ordered rule list from
9
+ * `feature_state` + `segment_overrides` + `identity_overrides` (§5.1),
10
+ * `enabled=false` disposition per D-F1, operator mapping with `:semver`
11
+ * suffix detection (§5.3), multivariate weighted-values shape (§5.4),
12
+ * cross-env value-type coercion per D-F5.
13
+ *
14
+ * This module is a pure function — no I/O, no env reads, no module-level
15
+ * mutable state. Report accumulation flows through the `ConversionReport`
16
+ * argument supplied by the caller (the `flagsmith.ts` source).
17
+ */
18
+ import type { ConversionReport } from '../../quonfig-target/report.js';
19
+ import type { QuonfigCriterion } from '../../quonfig-target/ruleset.js';
20
+ import type { FlagsmithFeatureStateValueEnvelope, FlagsmithFeatureWithStates, FlagsmithSegment, FlagsmithSegmentCondition, FlagsmithTag } from './types.js';
21
+ /**
22
+ * Operator-mapping result. A clean criterion translation OR a whole-rule drop
23
+ * with a precise reason. Returned per condition; the rule is dropped if any
24
+ * condition drops.
25
+ */
26
+ type ConditionMapping = {
27
+ criterion: QuonfigCriterion;
28
+ } | {
29
+ drop: true;
30
+ reason: string;
31
+ };
32
+ /**
33
+ * Flagsmith feature names land directly under `feature-flags/` — Flagsmith
34
+ * enforces a lower-case-hyphenated naming policy on most projects, so the
35
+ * key is already filesystem-safe. We still defensively replace any `/` so
36
+ * a name can never spawn a nested directory.
37
+ */
38
+ export declare function flagOutputPath(name: string): string;
39
+ export declare function segmentOutputPath(name: string): string;
40
+ /**
41
+ * Unwrap the `feature_state_value` field as it appears on the
42
+ * `/features/featurestates/` endpoint (envelope shape on env-default and
43
+ * segment-override rows; scalar on `/edge-identity-overrides` per API5).
44
+ * Returns the raw JS value Flagsmith would have served before any Quonfig
45
+ * type coercion.
46
+ */
47
+ export declare function coerceEnvelopeOrScalar(v: FlagsmithFeatureStateValueEnvelope | boolean | null | number | string | undefined): boolean | null | number | string;
48
+ /**
49
+ * Map one Flagsmith condition to a Quonfig criterion, or signal a whole-rule
50
+ * drop with a reason. Implements plan §5.3 verbatim.
51
+ */
52
+ export declare function mapCondition(condition: FlagsmithSegmentCondition): ConditionMapping;
53
+ /**
54
+ * Translate one Flagsmith feature (with all its per-env state) into a Quonfig
55
+ * feature-flag config object.
56
+ */
57
+ export declare function translateFeature(bundle: FlagsmithFeatureWithStates, report: ConversionReport, options?: {
58
+ /** Map from env api_key → user-facing env name. The converter slugifies the name for the Quonfig env id. */
59
+ envNameByApiKey?: Map<string, string>;
60
+ segmentNameById?: Map<number, string>;
61
+ tags?: FlagsmithTag[];
62
+ }): Record<string, unknown>;
63
+ /**
64
+ * Translate one Flagsmith segment into a Quonfig segment config object.
65
+ *
66
+ * Quonfig segments are boolean ("is the context in the segment?") and have no
67
+ * per-environment state, so all rules collapse into `default.rules`,
68
+ * first-match-wins. A rule whose conditions all map yields a true-valued rule;
69
+ * a trailing ALWAYS_TRUE serves false (not in segment).
70
+ */
71
+ export declare function translateSegment(segment: FlagsmithSegment, report: ConversionReport): Record<string, unknown>;
72
+ export {};