From 2e10346f45f8bb108605abfef3f3c7616af04b54 Mon Sep 17 00:00:00 2001 From: Paco Cartones <253313177+pacocartones@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:41:43 +0200 Subject: [PATCH] docs(reference): sync FEATURE_FLAGS.md with featureFlagDefinitions (#12552) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The doc promised a 1:1 catalog and had drifted to 20 missing keys, two rows that were not flags at all, and a port number that disagreed with the code. Good call adding a static sync test — it caught its own drift immediately: I added the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after you wrote this. --- Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs. - `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK - complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline - 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately. Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written. ⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch). Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass. --- .../12552-feature-flags-reference-sync.md | 1 + docs/reference/FEATURE_FLAGS.md | 133 +++++++++++------- scripts/check/check-fabricated-docs.mjs | 24 +++- .../feature-flags-doc-sync-static.test.ts | 100 +++++++++++++ 4 files changed, 199 insertions(+), 59 deletions(-) create mode 100644 changelog.d/fixes/12552-feature-flags-reference-sync.md create mode 100644 tests/unit/feature-flags-doc-sync-static.test.ts diff --git a/changelog.d/fixes/12552-feature-flags-reference-sync.md b/changelog.d/fixes/12552-feature-flags-reference-sync.md new file mode 100644 index 0000000000..62b599ac6d --- /dev/null +++ b/changelog.d/fixes/12552-feature-flags-reference-sync.md @@ -0,0 +1 @@ +- **docs(reference):** bring the `FEATURE_FLAGS.md` catalog back to 1:1 with `featureFlagDefinitions.ts` — 20 missing flags added, the two `*_BLOCK_THRESHOLD` env-only knobs moved out of the flag tables, category/total counts and the Live WS port corrected, guarded by a static test (#12552 — thanks @pacocartones) diff --git a/docs/reference/FEATURE_FLAGS.md b/docs/reference/FEATURE_FLAGS.md index f7e046d169..65a5bcac12 100644 --- a/docs/reference/FEATURE_FLAGS.md +++ b/docs/reference/FEATURE_FLAGS.md @@ -1,7 +1,7 @@ --- title: "Feature Flags" -version: 3.8.40 -lastUpdated: 2026-06-28 +version: 3.8.51 +lastUpdated: 2026-09-03 --- # Feature Flags @@ -46,66 +46,85 @@ A boolean flag is considered **enabled** when its effective value is `"true"`, ## Flag Catalog -37 flags across 6 categories. **Default** is the definition default — the value +55 flags across 6 categories. **Default** is the definition default — the value used when neither a DB override nor an environment variable is present. -### Security (7) +### Security (10) -| Key | Type | Default | Description | -| --------------------------------- | ------- | --------- | ------------------------------------------------------------------------------------------------------------------- | -| `REQUIRE_API_KEY` | boolean | `false` | Require an API key for all incoming requests. | -| `INPUT_SANITIZER_ENABLED` | boolean | `true` | Enable input sanitization for all requests. | -| `INJECTION_GUARD_MODE` | enum | `off` | Prompt injection guard mode. Values: `off`, `warn`, `block`, `redact`. | -| `INPUT_SANITIZER_BLOCK_THRESHOLD` | enum | `high` | Minimum severity blocked when mode is `block` (`high`/`medium`/`low`). Medium families are observe-only at default. | -| `INJECTION_GUARD_BLOCK_THRESHOLD` | enum | _(unset)_ | Legacy alias for `INPUT_SANITIZER_BLOCK_THRESHOLD`. | -| `PII_REDACTION_ENABLED` | boolean | `false` | Redact PII from requests (independent of `INPUT_SANITIZER_MODE`). | -| `PII_RESPONSE_SANITIZATION` | boolean | `false` | Sanitize PII from provider responses. | -| `PII_RESPONSE_SANITIZATION_MODE` | enum | `redact` | Mode for PII response sanitization. Values: `redact`, `warn`, `block`, `off`. | -| `OUTBOUND_SSRF_GUARD_ENABLED` | boolean | `true` | Block outbound requests to private/internal IP ranges. | +| Key | Type | Default | Description | +| --------------------------------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `REQUIRE_API_KEY` | boolean | `false` | Require an API key for all incoming requests. | +| `INPUT_SANITIZER_ENABLED` | boolean | `true` | Enable input sanitization for all requests. | +| `INJECTION_GUARD_MODE` | enum | `off` | Prompt injection guard mode. Values: `off`, `warn`, `block`, `redact`. | +| `PII_REDACTION_ENABLED` | boolean | `false` | Redact PII from requests (independent of `INPUT_SANITIZER_MODE`). | +| `PII_RESPONSE_SANITIZATION` | boolean | `false` | Sanitize PII from provider responses. | +| `PII_RESPONSE_SANITIZATION_MODE` | enum | `redact` | Mode for PII response sanitization. Values: `redact`, `warn`, `block`, `off`. | +| `OUTBOUND_SSRF_GUARD_ENABLED` | boolean | `true` | Block outbound requests to private/internal IP ranges. | +| `ALLOW_API_KEY_REVEAL` | boolean | `false` | Allow authenticated dashboard users to reveal stored API keys instead of only seeing masked values. | +| `AUTH_LOG_INCLUDE_ACCOUNT_ID` | boolean | `false` | Include account prefix in AUTH log lines (e.g. "Using account: abc12345..."). Disabled by default so account identifiers are redacted from shared/multi-tenant process logs. Independent from Debug Mode; flipping Debug Mode does not reveal this. | +| `OMNIROUTE_OIDC_DISABLE_PASSWORD_LOGIN` | boolean | `false` | When OIDC is enabled, disable password login so users can only authenticate via OIDC Single Sign-On. When disabled (default), both password login and OIDC are available. | -### Network (8) +### Network (9) -| Key | Type | Default | Restart | Description | -| ----------------------------------------------- | ------- | ------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `ENABLE_TLS_FINGERPRINT` | boolean | `false` | ✓ | Enable TLS fingerprint stealth mode. | -| `PROXY_AUTO_SELECT_ENABLED` | boolean | `false` | | When no proxy is assigned to a connection, auto-select the first working proxy from the registry. Off by default (otherwise any registry proxy becomes a global fallback — #3332). | -| `OMNIROUTE_CONTROL_PLANE_PROXY_DIRECT_FALLBACK` | boolean | `false` | | Allow OAuth and provider validation flows to bypass a pinned proxy and connect directly when proxy reachability pre-checks fail. Off by default because this can change egress IP. | -| `MITM_DISABLE_TLS_VERIFY` | boolean | `false` | ✓ | Disable TLS certificate verification for the MITM proxy. **Danger.** | -| `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` | boolean | `false` | | Allow provider URLs pointing to private/internal networks. | -| `OMNIROUTE_ALLOW_LOCAL_PROVIDER_URLS` | boolean | `true` | | Allow adding/validating providers on local/private addresses (127.0.0.1, localhost, LAN). On by default (local-first); disable for strict public-only blocking. Cloud-metadata stays blocked. | -| `ENABLE_CC_COMPATIBLE_PROVIDER` | boolean | `false` | ✓ | Enable Claude Code compatible provider mode. | +| Key | Type | Default | Restart | Description | +| ----------------------------------------------- | ------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ENABLE_TLS_FINGERPRINT` | boolean | `false` | ✓ | Enable TLS fingerprint stealth mode. | +| `AUDIO_REMOTE_PROVIDER_NODES` | boolean | `false` | | Allow the /v1/audio/* routes to use OpenAI-compatible provider nodes hosted outside localhost. Off by default — routing audio to a remote host changes egress identity and must be an explicit operator decision. Loopback nodes are always allowed and unaffected. | +| `PROXY_AUTO_SELECT_ENABLED` | boolean | `false` | | When no proxy is assigned to a connection, auto-select the first working proxy from the registry. Off by default (otherwise any registry proxy becomes a global fallback — #3332). | +| `OMNIROUTE_CONTROL_PLANE_PROXY_DIRECT_FALLBACK` | boolean | `false` | | Allow OAuth and provider validation flows to bypass a pinned proxy and connect directly when proxy reachability pre-checks fail. Off by default because this can change egress IP. | +| `NETWORK_ROTATION_SHARED_EGRESS_GUARD` | boolean | `true` | | On a network exception (timeout, connection refused/reset) for a multi-account rotation executor, when the failing account has no dedicated proxy, apply a short cooldown and skip other proxy-less accounts for the rest of the request instead of retrying each one. On by default (safe: no egress IP change, only reduces latency/cooldown risk on shared-egress accounts). Disable to restore immediate propagation on the first proxy-less throw. | +| `MITM_DISABLE_TLS_VERIFY` | boolean | `false` | ✓ | Disable TLS certificate verification for the MITM proxy. **Danger.** | +| `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` | boolean | `false` | | Allow provider URLs pointing to private/internal networks. | +| `OMNIROUTE_ALLOW_LOCAL_PROVIDER_URLS` | boolean | `true` | | Allow adding/validating providers on local/private addresses (127.0.0.1, localhost, LAN). On by default (local-first); disable for strict public-only blocking. Cloud-metadata stays blocked. | +| `ENABLE_CC_COMPATIBLE_PROVIDER` | boolean | `false` | ✓ | Enable Claude Code compatible provider mode. | -### Policies (3) +### Policies (5) -| Key | Type | Default | Restart | Description | -| ------------------------------- | ------- | ---------- | ------- | ---------------------------------------------------------------------------------------------------------------------------- | -| `TOOL_POLICY_MODE` | enum | `disabled` | | Tool-use policy enforcement mode. Values: `disabled`, `warn`, `block`. | -| `RATE_LIMIT_AUTO_ENABLE` | boolean | `false` | | Automatically enable rate limiting based on usage patterns. | -| `DISABLE_CONTEXT_WINDOW_CHECKS` | boolean | `false` | | Skip OmniRoute's local context-window / max-input-token check for direct single-model requests. Upstream limits still apply. | +| Key | Type | Default | Description | +| ------------------------------- | ------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `TOOL_POLICY_MODE` | enum | `disabled` | Tool-use policy enforcement mode. Values: `disabled`, `warn`, `block`. | +| `RATE_LIMIT_AUTO_ENABLE` | boolean | `false` | Automatically enable rate limiting based on usage patterns. | +| `DISABLE_CONTEXT_WINDOW_CHECKS` | boolean | `false` | Skip OmniRoute's local context-window / max-input-token check for direct single-model requests. Upstream limits still apply. | +| `CAPABILITY_FILTER_ENABLED` | boolean | `false` | Reject requests before dispatch when the target model lacks required capabilities (vision, tools, structured output, context window). Protects direct single-provider requests that bypass the combo-layer compatibility filter. | +| `RADAR_ENABLED` | boolean | `false` | Enable the OmniRoute Radar module (catalog feed screens and sync). Off by default; enabling only unlocks the UI — data sync remains a separate opt-in. | -### Runtime (11) +### Runtime (23) -| Key | Type | Default | Restart | Description | -| ------------------------------------------- | ------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `EXPOSE_CC_DISCOVERY_ALIASES` | boolean | `false` | | Advertise `claude//` mirror ids on `/v1/models` so Claude Code gateway model discovery lists non-Claude models. Global level of the three-level gate (env wins over the dashboard override). See [Claude Code configuration](../guides/CLAUDE-CODE-CONFIGURATION.md#discovery-aliases--surface-non-claude-models-in-the-model-picker). | -| `OMNIROUTE_MCP_ENFORCE_SCOPES` | boolean | `true` | | Enforce scope restrictions on MCP tool access. | -| `OMNIROUTE_MCP_COMPRESS_DESCRIPTIONS` | boolean | `false` | | Compress MCP tool descriptions to reduce token usage. | -| `OMNIROUTE_ENABLE_RUNTIME_BACKGROUND_TASKS` | boolean | `false` | | Enable background task processing at runtime. | -| `OMNIROUTE_DISABLE_BACKGROUND_SERVICES` | boolean | `false` | ✓ | Disable all background services (quota refresh, sync, etc). | -| `OMNIROUTE_RTK_TRUST_PROJECT_FILTERS` | boolean | `false` | | Trust project-level RTK filters without validation. | -| `OMNIROUTE_ENABLE_LIVE_WS` | boolean | `true` | ✓ | Start the real-time dashboard WebSocket server on import (port 20129 by default). | -| `OMNIROUTE_CODEX_WS_ENABLED` | boolean | `true` | | Allow Codex to use the Responses-over-WebSocket transport. When off, Codex falls back to HTTP Responses. | -| `OMNIROUTE_EMERGENCY_FALLBACK` | boolean | `true` | | Route budget-exhausted requests to the emergency free fallback provider/model. (See [Emergency Budget Fallback](#emergency-budget-fallback) below.) | -| `MODEL_CATALOG_INCLUDE_NAMES` | boolean | `true` | | Include display-friendly name fields in `/v1/models` responses. Disable for clients that expect model IDs only. | -| `ARENA_ELO_SYNC_ENABLED` | boolean | `true` | | Enable periodic Arena AI leaderboard ELO sync for model intelligence rankings. | +| Key | Type | Default | Restart | Description | +| ------------------------------------------- | ------- | ------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `UNIVERSAL_CONTEXT_HANDOFF_ENABLED` | boolean | `true` | | Generate and inject conversation summaries when combo routing switches models. Disable to treat model switches independently and prevent background handoff requests for all existing and future combos. | +| `RESPONSES_PASSTHROUGH_DROP_COMMENTARY` | boolean | `true` | | Drop internal commentary-phase output items from Responses API passthrough streams before forwarding to clients. Disable to receive raw upstream commentary. | +| `OMNIROUTE_MCP_ENFORCE_SCOPES` | boolean | `true` | | Enforce scope restrictions on MCP tool access. | +| `OMNIROUTE_MCP_COMPRESS_DESCRIPTIONS` | boolean | `false` | | Compress MCP tool descriptions to reduce token usage. | +| `OMNIROUTE_ENABLE_RUNTIME_BACKGROUND_TASKS` | boolean | `false` | | Enable background task processing at runtime. | +| `OMNIROUTE_DISABLE_BACKGROUND_SERVICES` | boolean | `false` | ✓ | Disable all background services (quota refresh, sync, etc). | +| `OMNIROUTE_RTK_TRUST_PROJECT_FILTERS` | boolean | `false` | | Trust project-level RTK filters without validation. | +| `OMNIROUTE_ENABLE_LIVE_WS` | boolean | `true` | ✓ | Start the real-time dashboard WebSocket server on import (port 20132 by default). | +| `OMNIROUTE_CODEX_WS_ENABLED` | boolean | `true` | | Allow Codex to use the Responses-over-WebSocket transport. When off, Codex falls back to HTTP Responses. | +| `OMNIROUTE_CODEX_APP_SERVER_ENABLED` | boolean | `true` | | Allow Codex to use the local app-server WebSocket JSON-RPC transport (codexTransport=app-server). When off, connections opted into app-server fall back to Codex's other transports. | +| `OMNIROUTE_EMERGENCY_FALLBACK` | boolean | `true` | | Route budget-exhausted requests to the emergency free fallback provider/model. (See [Emergency Budget Fallback](#emergency-budget-fallback) below.) | +| `STREAM_RECOVERY_ENABLED` | boolean | `false` | | Enable transparent early retry for truncated upstream SSE streams before any response bytes reach the client. | +| `STREAM_RECOVERY_MIDSTREAM_ENABLED` | boolean | `false` | | Allow stream recovery to re-request and stitch a response after bytes have already reached the client. | +| `MODEL_CATALOG_INCLUDE_NAMES` | boolean | `true` | | Include display-friendly name fields in `/v1/models` responses. Disable for clients that expect model IDs only. | +| `MODELS_CATALOG_PREFIX_MODE` | enum | `dual` | | Controls how model IDs are prefixed in /v1/models. 'dual' (default) emits both alias and canonical provider-id prefixes for backward compatibility. 'alias' emits only the short alias prefix (e.g. ds-web/model, not deepseek-web/model). 'canonical' emits only the full provider-id prefix. Values: `dual`, `alias`, `canonical`. | +| `ARENA_ELO_SYNC_ENABLED` | boolean | `true` | | Enable periodic Arena AI leaderboard ELO sync for model intelligence rankings. | +| `EXPOSE_CC_DISCOVERY_ALIASES` | boolean | `false` | | Advertise `claude//` mirror ids on `/v1/models` so Claude Code gateway model discovery lists non-Claude models. Global level of the three-level gate (env wins over the dashboard override). See [Claude Code configuration](../guides/CLAUDE-CODE-CONFIGURATION.md#discovery-aliases--surface-non-claude-models-in-the-model-picker). | +| `NO_THINKING_ALIAS_ENABLED` | boolean | `true` | | Master switch for the no-think// gateway aliases. On (default): /v1/models advertises a no-thinking variant for every eligible thinking-capable Claude model, and a no-think/ id sent on a request resolves back to the real model with reasoning suppressed. Off: no variants are advertised and a no-think/ id is treated like any other unknown model id. The per-model ModelSpec.noThinkingAlias opt-in/opt-out still applies while this is on. | +| `OMNIROUTE_DISABLE_THINKING_LEVEL_VARIANTS` | boolean | `false` | | Disable the generation of thinking level variants (e.g. -low, -medium, -high) in the /v1/models catalog. | +| `OMNIROUTE_CHAT_VIRTUAL_LANES` | boolean | `false` | ✓ | Enable per-tenant adaptive virtual admission lanes for provider dispatch (#9654): one tenant's burst no longer 503s another. The OMNIROUTE_CHAT_VIRTUAL_LANES env var wins over this dashboard override; changes take effect at server restart. | +| `EXPOSE_FUNCTIONAL_GATEWAY_MIRRORS` | boolean | `false` | | Advertise / mirror ids on /v1/models for models whose canonical owner has no active credential but a passthrough gateway with an active credential routes them. Warning: adds catalog entries for all clients when enabled globally. | +| `NEWAPI_AGGREGATOR_BALANCE` | boolean | `false` | | Enable balance detection for New-API / One-API / Sub2API aggregator compatible nodes. When enabled, compatible nodes with the aggregator flag set will report their balance in the dashboard and quota-preflight routing. | +| `SERVER_OWNED_TOOL_LOOP_ENABLED` | boolean | `false` | | Continue non-streaming server-owned tool calls until the model returns a client-usable response. | -### CLI (3) +### CLI (5) -| Key | Type | Default | Restart | Description | -| ---------------------------- | ------- | ------- | ------- | -------------------------------------------------------------------------------------------------------------- | -| `CLI_COMPAT_ALL` | boolean | `false` | ✓ | Enable compatibility mode for all CLI clients. | -| `MODEL_ALIAS_COMPAT_ENABLED` | boolean | `false` | | Enable model alias compatibility layer. | -| `PRICING_SYNC_ENABLED` | boolean | `false` | | Enable automatic pricing data synchronization (also requires the `PRICING_SYNC_ENABLED` environment variable). | +| Key | Type | Default | Restart | Description | +| ------------------------------------- | ------- | ------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `CLI_COMPAT_ALL` | boolean | `false` | ✓ | Enable compatibility mode for all CLI clients. | +| `MODEL_ALIAS_COMPAT_ENABLED` | boolean | `false` | | Enable model alias compatibility layer. | +| `PRICING_SYNC_ENABLED` | boolean | `false` | | Enable automatic pricing data synchronization (also requires the `PRICING_SYNC_ENABLED` environment variable). | +| `OMNIROUTE_AUTO_SYNC_CODEX_PROFILES` | boolean | `false` | | After a provider model sync, automatically (re)write ~/.codex/*.config.toml profile files from the live catalog. Never changes the active/default Codex config. Off by default. | +| `OMNIROUTE_AUTO_SYNC_CLAUDE_PROFILES` | boolean | `false` | | After a provider model sync, automatically (re)write ~/.claude/profiles//settings.json Claude Code profiles from the live catalog. Never changes the active/default Claude config. Off by default. | ### Health (3) @@ -115,6 +134,14 @@ used when neither a DB override nor an environment variable is present. | `OMNIROUTE_DISABLE_TOKEN_HEALTHCHECK` | boolean | `false` | Disable the token validation health check. | | `SKILLS_SANDBOX_NETWORK_ENABLED` | boolean | `false` | Enable network access in the skills sandbox environment. | +> [!NOTE] +> `INPUT_SANITIZER_BLOCK_THRESHOLD` and its legacy alias +> `INJECTION_GUARD_BLOCK_THRESHOLD` tune the `block` mode of +> `INJECTION_GUARD_MODE`, but they are plain environment variables read by +> [`src/shared/utils/injectionSeverity.ts`](../../src/shared/utils/injectionSeverity.ts), +> not feature flags: they have no DB override and no dashboard toggle. See +> [`ENVIRONMENT.md`](./ENVIRONMENT.md#4-security--authentication). + > [!NOTE] > The `Restart` column marks flags with `requiresRestart: true` — the value is > persisted instantly but only takes effect after the process reloads. Enum @@ -168,10 +195,10 @@ Returns every flag with its effective value, source, and a summary. "requiresRestart": false, "warningLevel": "caution", }, - // ... all 33 flags + // ... all 55 flags ], "summary": { - "total": 33, + "total": 54, "active": 0, "inactive": 0, "overriddenByDb": 0, diff --git a/scripts/check/check-fabricated-docs.mjs b/scripts/check/check-fabricated-docs.mjs index 567a9610da..c74965a8cb 100644 --- a/scripts/check/check-fabricated-docs.mjs +++ b/scripts/check/check-fabricated-docs.mjs @@ -128,12 +128,6 @@ const ENV_VAR_ALLOWLIST = new Set([ "LINUX_GPG_KEY", // electron AppImage signing key, CI/build only (ELECTRON_GUIDE.md) "BRANCH_LOCK_TOKEN", // release branch-protection ops token (QUALITY_GATE_PLAYBOOK.md) "NEXT_LOCALE", // next-intl locale cookie name (I18N.md) - // Feature flags are resolved by key at runtime — `resolveFeatureFlag()` reads - // `process.env[key]` (src/shared/utils/featureFlags.ts), never a literal - // `process.env.MODELS_CATALOG_PREFIX_MODE`, so this scan cannot see the read. - // The flag is real: defined in featureFlagDefinitions.ts, overridable from the - // dashboard or the environment. (API_REFERENCE.md, VSCODE-COPILOT.md) - "MODELS_CATALOG_PREFIX_MODE", // Telegram Mini App integration (proposal TELEGRAM-MINIAPP.md, not yet implemented): env vars named in the feasibility analysis but no code reads them yet. "TELEGRAM_WEBHOOK_URL", // proposal-only: Telegram webhook public endpoint (TELEGRAM-MINIAPP.md, future feature) "TELEGRAM_WEBHOOK_SECRET", // proposal-only: Telegram webhook HMAC secret (TELEGRAM-MINIAPP.md, future feature) @@ -581,6 +575,24 @@ export function buildCodebaseIndex(root = ROOT) { } readEnvContract(); + // Feature flags are resolved by key at runtime — `resolveFeatureFlag()` reads + // `process.env[definition.key]` (src/shared/utils/featureFlags.ts), never a + // literal `process.env.`, so the code-read index cannot see those reads. + // Every key in FEATURE_FLAG_DEFINITIONS is therefore a real, env-overridable + // knob (docs/reference/FEATURE_FLAGS.md documents the catalog 1:1). + function readFeatureFlagContract() { + try { + const t = fs.readFileSync( + path.join(root, "src", "shared", "constants", "featureFlagDefinitions.ts"), + "utf8" + ); + for (const m of t.matchAll(/^\s*key:\s*"([A-Z][A-Z0-9_]+)"/gm)) envVars.add(m[1]); + } catch { + /* ignore */ + } + } + readFeatureFlagContract(); + // Set of `omniroute ` strings that exist in bin/ const cliCommands = new Set(); function walkCli(dir) { diff --git a/tests/unit/feature-flags-doc-sync-static.test.ts b/tests/unit/feature-flags-doc-sync-static.test.ts new file mode 100644 index 0000000000..8275529e38 --- /dev/null +++ b/tests/unit/feature-flags-doc-sync-static.test.ts @@ -0,0 +1,100 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; +import { FEATURE_FLAG_DEFINITIONS } from "../../src/shared/constants/featureFlagDefinitions.ts"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = join(__dirname, "..", ".."); + +/** + * docs/reference/FEATURE_FLAGS.md promises that its catalog matches + * FEATURE_FLAG_DEFINITIONS "1:1". Keep that promise checkable: every flag the + * code defines must be a table row with the same type and default, every table + * row must be a real flag, and the per-category / total counts must match. + */ +const doc = readFileSync(join(root, "docs/reference/FEATURE_FLAGS.md"), "utf8"); +const catalog = doc.slice(doc.indexOf("## Flag Catalog"), doc.indexOf("## Toggling Flags")); + +interface DocRow { + key: string; + type: string; + defaultValue: string; + restart: boolean; + category: string; +} + +function parseCatalog(): DocRow[] { + const rows: DocRow[] = []; + let category = ""; + for (const line of catalog.split("\n")) { + const heading = line.match(/^### (\w+) \(\d+\)/); + if (heading) { + category = heading[1].toLowerCase(); + continue; + } + const cells = line.match(/^\| `([A-Z0-9_]+)` +\| (\w+) +\| ([^|]+?) +\|(.*)$/); + if (!cells) continue; + rows.push({ + key: cells[1], + type: cells[2], + defaultValue: cells[3].replace(/`/g, ""), + restart: /^ *✓ *\|/.test(cells[4]), + category, + }); + } + return rows; +} + +const docRows = parseCatalog(); +const docByKey = new Map(docRows.map((row) => [row.key, row])); + +test("every defined feature flag has a catalog row in FEATURE_FLAGS.md", () => { + const missing = FEATURE_FLAG_DEFINITIONS.filter((d) => !docByKey.has(d.key)).map((d) => d.key); + assert.deepEqual( + missing, + [], + `flags defined in featureFlagDefinitions.ts but absent from the doc: ${missing.join(", ")}` + ); +}); + +test("every catalog row in FEATURE_FLAGS.md is a defined feature flag", () => { + const known = new Set(FEATURE_FLAG_DEFINITIONS.map((d) => d.key)); + const extra = docRows.filter((row) => !known.has(row.key)).map((row) => row.key); + assert.deepEqual( + extra, + [], + `doc rows that are not feature flags (env-only knobs belong in ENVIRONMENT.md): ${extra.join(", ")}` + ); +}); + +test("catalog rows carry the code's category, type, default and restart hint", () => { + const mismatches: string[] = []; + for (const def of FEATURE_FLAG_DEFINITIONS) { + const row = docByKey.get(def.key); + if (!row) continue; + if (row.category !== def.category) + mismatches.push(`${def.key}: category doc=${row.category} code=${def.category}`); + if (row.type !== def.type) mismatches.push(`${def.key}: type doc=${row.type} code=${def.type}`); + if (row.defaultValue !== def.defaultValue) + mismatches.push(`${def.key}: default doc=${row.defaultValue} code=${def.defaultValue}`); + if (row.restart !== def.requiresRestart) + mismatches.push(`${def.key}: requiresRestart doc=${row.restart} code=${def.requiresRestart}`); + } + assert.deepEqual(mismatches, []); +}); + +test("category headings and the total match the number of defined flags", () => { + const perCategory = new Map(); + for (const def of FEATURE_FLAG_DEFINITIONS) { + perCategory.set(def.category, (perCategory.get(def.category) ?? 0) + 1); + } + for (const [, name, count] of catalog.matchAll(/^### (\w+) \((\d+)\)/gm)) { + assert.equal(Number(count), perCategory.get(name.toLowerCase()), `heading count for ${name}`); + } + const total = catalog.match(/^(\d+) flags across (\d+) categories/m); + assert.ok(total, "expected an ' flags across categories' summary line"); + assert.equal(Number(total[1]), FEATURE_FLAG_DEFINITIONS.length, "total flag count"); + assert.equal(Number(total[2]), perCategory.size, "category count"); +});