---
title: "Зашитая конфигурация"
type: "ai-smell"
slug: "hardcoded-configuration"
url: "http://localhost:3000/ru/ai-smells/hardcoded-configuration.md"
category: "Сопровождение"
description: "ИИ-ассистенты вшивают зависящие от окружения значения — URL, порты, пути, тайм-ауты, ключи и магические числа — прямо в логику вместо чтения их из конфигурации или окружения, потому что литерал является наиболее вероятным следующим токеном, а модель не знает о вашем существующем слое конфигурации."
---
# Зашитая конфигурация

> ИИ-ассистенты вшивают зависящие от окружения значения — URL, порты, пути, тайм-ауты, ключи и магические числа — прямо в логику вместо чтения их из конфигурации или окружения, потому что литерал является наиболее вероятным следующим токеном, а модель не знает о вашем существующем слое конфигурации.

## Signs and Symptoms

ИИ-ассистент склонен выдавать _правдоподобный литерал_ ровно там, где должна быть ссылка. Код работает в демонстрации, поэтому запах переживает ревью, если его не искать специально.

Характерные признаки:

* **Встроенные эндпоинты, порты, пути и тайм-ауты**, разбросанные по бизнес-логике вместо модуля конфигурации: `http://localhost:3000`, `/tmp/cache`, `5432`, `setTimeout(..., 30000)`.
* **Магические числа/строки** без именованной константы — счётчики повторов, размеры страниц, ограничения частоты, пороги функций.
* **Один и тот же литерал, повторяющийся в разных файлах**, потому что модель сгенерировала его заново, а не импортировала существующую константу (тот же паттерн GitClear «копипаст вверх, рефакторинг вниз» в миниатюре).
* **Зашитые учётные данные / ключи** — API-токены, ключи шифрования или строки `Basic`\-авторизации, вставленные из обучающих данных.
* **Существующий `config.ts` / `.env` / объект настроек, который новый код полностью игнорирует**.
* **Значения, вшитые под окружение**: URL dev/стейджинга или ID тестового аккаунта, жёстко прописанные в пути, который уезжает в продакшен.

```ts
// 🚩 Сгенерировано ИИ: каждая настройка — литерал, dev-значения вшиты
export async function syncOrders() {
  const res = await fetch("https://api.staging.acme.dev/v1/orders", {
    headers: { Authorization: "Bearer sk_test_4eC39HqLyjWDarjtT1zdp7dc" },
    signal: AbortSignal.timeout(30000),
  });
  const orders = (await res.json()).slice(0, 50); // почему 50?
  for (let i = 0; i < 3; i++) { /* повтор 3 раза... почему 3? */ }
}

```

Строка Authorization — это срабатывание gitleaks/Semgrep; URL, `30000`, `50` и `3` — запахи магических значений; а `api.staging.acme.dev` — это dev-значение, которое вот-вот достигнет продакшена.

## Reasons for the Problem

**Почему модели это порождают**

* **Литерал — самый вероятный следующий токен.** Имея `fetch(`, самым дешёвым продолжением, удовлетворяющим непосредственному запросу, является конкретная строка URL, а не `config.apiBaseUrl`. Разрешение _ссылки_ требует знания о том, что где-то существует символ; выдача _значения_ не требует ничего. Модели оптимизируют локально правдоподобный, запускаемый фрагмент.
* **Обучающий корпус состоит из самодостаточных фрагментов.** Учебники, примеры из README и ответы на Stack Overflow подставляют значения прямо в код, чтобы они работали автономно. Модель усвоила, что «хороший пример кода» зашивает значения — противоположность гигиене продакшена.
* **Нет контекста репозитория / ограниченное окно.** Модель обычно не читала ваш `config/`, схему окружения или файл констант, поэтому не может их переиспользовать. GitClear связывает именно это с ростом дублирования: ассистенты «реже предлагают переиспользовать похожую функцию… отчасти из-за ограниченного размера контекста», при этом дублированных блоков по 5+ строк в 2024 году стало примерно в 8× больше, тогда как доля отрефакторенных («перемещённых») строк упала с \~24 % до \~9,5 %.
* **Угодничество / склонность отвечать буквально на запрос.** Если попросить «добавить синхронизацию заказов», модель выдаёт что-то, что работает _сейчас_; вынесение конфигурации — это лишний обвес, который она не предложит по своей инициативе, если не сказать.
* **Устаревание из-за обучающего среза.** Вшитые литералы — это зачастую _устаревшие_ литералы: старые эндпоинты, устаревшие версии API, порты по умолчанию или слабые криптоконстанты (MD5, зашитые ключи). OX Security обнаружила, что 62 % сгенерированного ИИ кода поставляется с проблемами, и связывает зашитые ключи/секреты/пути с паттернами, «усвоенными из легаси-кодовых баз… \[при\] нулевом понимании того, что стандарты безопасности изменились».

**Чем это вредит**

* **Сопровождаемость — «дробовик» (Shotgun Surgery).** Изменение одного тайм-аута или базового URL означает охоту за каждым дублированным литералом по всей кодовой базе; пропустите один — и поведение незаметно разойдётся в зависимости от места вызова.
* **Корректность в разных окружениях.** Зашитый в логику URL стейджинга или ID dev-аккаунта уезжает в продакшен; магические пороги без имени рассинхронизируются со значениями, которые должны были бы отражать.
* **Безопасность.** Зашитые токены/ключи — это утечки учётных данных: будучи однажды закоммиченными, они навсегда остаются в истории git. Исследование сборочного кода с arXiv (2601.16839) обнаружило повторяющиеся в сгенерированных ИИ сборочных файлах зашитые пути/URL и зашитые учётные данные высокой степени критичности.
* **Нагрузка на ревью и техдолг.** Ревьюеры вынуждены проверять происхождение _каждого_ литерала. Работа о специфичных для ИИ запахах (arXiv 2509.20491) отмечает, что модели нормально справляются с «литералами в области видимости», но пасуют в **чувствительных к значению** случаях, где корректность зависит от порогов, распространяющихся через вспомогательные функции, — именно те зашитые значения, которые людям теперь приходится проверять вручную. По словам Эно Рейеса из Factory в интервью Stack Overflow, базовое качество кода — «единственный сигнал» того, ускоряют ли агенты команду или _замедляют_, а разрастание зашитых значений подтачивает именно этот базовый уровень.

## Treatment

**Тактики промптинга / ревью**

* **Направьте модель на вашу конфигурационную поверхность:** «Прочитай `src/config.ts` и `.env.example`; используй `config.*` / `process.env` для каждого URL, порта, тайм-аута и учётных данных. Не вводи литералы.» Модели подставляют значения, потому что не знают о существовании символа, — назовите его.
* **Явно запретите магические значения:** «Никаких магических чисел или строк — выноси именованные константы.» Затем заставьте прогнать `eslint --rule no-magic-numbers` и сканер секретов (gitleaks/Semgrep) и исправить то, что они отметят. Согласно рекомендациям Factory, встройте линтеры/сканеры в цикл, чтобы агент исправлялся сам, а не опирался на ручное ревью.
* **Дайте схему окружения** (zod/envalid/`.env.example`), чтобы модели было куда класть значения, а не угадывать их.
* **Прогрепайте новые диффы** на `http`, `localhost`, IP-литералы, `Bearer ` и голые цифры в аргументах вызовов перед слиянием.

**Рефакторинг** — назовите классические приёмы:

* **Замена магического числа символьной константой** для порогов, счётчиков, размеров.
* **Извлечение функции / Извлечение модуля конфигурации**, чтобы собрать настройки в одно типизированное, валидируемое место.
* **Вынесение в окружение** (12-факторное приложение) для всего, что различается между окружениями или является секретом; никогда не коммитьте секреты.

```ts
// config.ts — единый, валидируемый источник истины
import { z } from "zod";
const env = z.object({
  ORDERS_API_BASE_URL: z.string().url(),
  ORDERS_API_TOKEN: z.string().min(1),
  ORDERS_TIMEOUT_MS: z.coerce.number().default(30_000),
  ORDERS_PAGE_SIZE: z.coerce.number().default(50),
  ORDERS_MAX_RETRIES: z.coerce.number().default(3),
}).parse(process.env);
export const config = env;

```

```ts
// ✅ после: ссылки, а не литералы — одно место для изменений, секрет вне исходников
import { config } from "./config";
export async function syncOrders() {
  const res = await fetch(`${config.ORDERS_API_BASE_URL}/v1/orders`, {
    headers: { Authorization: `Bearer ${config.ORDERS_API_TOKEN}` },
    signal: AbortSignal.timeout(config.ORDERS_TIMEOUT_MS),
  });
  const orders = (await res.json()).slice(0, config.ORDERS_PAGE_SIZE);
  for (let i = 0; i < config.ORDERS_MAX_RETRIES; i++) { /* ... */ }
}

```

Если секрет уже попал в коммит, смените его — удаление строки не очищает историю git.

## Detected by

- **ESLint** `no-magic-numbers` — Запрет магических чисел (https://eslint.org/docs/latest/rules/no-magic-numbers)
- **SonarSource** `typescript:S109 / java:S109 — "Magic numbers should not be used"` — Магические числа (https://rules.sonarsource.com/java/RSPEC-109/)
- **SonarSource** `java:S1075 — "URIs should not be hardcoded"` — Зашитые URI (https://rules.sonarsource.com/java/RSPEC-1075/)
- **SonarSource** `java:S2068 — "Hard-coded credentials are security-sensitive"` — Зашитые учётные данные (https://rules.sonarsource.com/java/RSPEC-2068/)
- **Checkstyle** `MagicNumber` — Проверка магических чисел (https://checkstyle.org/checks/coding/magicnumber.html)
- **PMD** `AvoidUsingHardCodedIP` — Избегать зашитых IP-адресов (https://docs.pmd-code.org/latest/pmd_rules_java_bestpractices.html#avoidusinghardcodedip)
- **Bandit** `B105 hardcoded_password_string / B108 hardcoded_tmp_directory` — Зашитые пароли / временные каталоги (Python) (https://bandit.readthedocs.io/en/latest/plugins/b105_hardcoded_password_string.html)
- **gitleaks** `secret detection (e.g. generic-api-key, regex/entropy rules)` — Сканер зашитых секретов (https://github.com/gitleaks/gitleaks)
- **Semgrep** `generic.secrets / hardcoded-credentials registry rules` — Правила для зашитых секретов/конфигурации (https://semgrep.dev/r?q=hardcoded)
- **eslint-plugin-no-secrets** `no-secrets/no-secrets` — Обнаружение высокоэнтропийных строк (секретов) (https://github.com/nickdeis/eslint-plugin-no-secrets)
