# 11 · Разработка и тесты

## Раскладка

```text
fornex-control/
  index.php                  вход (он же точка входа `php -S`)
  backend/
    bootstrap.php            настройки → база → сервисы → маршруты; единственная сборка
    bin/fornex               CLI, те же сервисы
    config/defaults.php      значения по умолчанию
    config/routes.php        таблица маршрутов
    config/presets.json      пресеты и шаги мастера (данные, не код)
    src/Kernel/              настройки, env, запрос/ответ, роутер, сессия, журнал
    src/Fornex/              клиент API
    src/Service/             зона, планировщик, пресеты, проверка, NS, токены, аудит
    src/Http/                контроллеры и оболочка SPA
    src/Security/            вход и CSRF
    src/Cli/                 командная строка
  assets/                    css/js без сборки
  tests/plan_test.php        весь набор тестов
  tools/lint.php             синтаксис PHP и чистота файлов
  tools/secret-scan.php      поиск строк, похожих на учётные данные
```

## Четыре проверки перед каждым изменением

```bash
php tools/lint.php
php tools/lint.php --text
php tests/plan_test.php
php tools/secret-scan.php
```

CI запускает ровно их плюс smoke-проверку сборки: `FORNEX_API_KEY=ci-dummy … php backend/bin/fornex
help` — это доказывает, что приложение собирается без веб-сервера и без файла настроек.

## Почему тесты именно такие

Набор проверяет правила, ошибка в которых молчит:

| Правило | Что сломается без теста |
|---|---|
| повторный пресет не копит дубли SPF/DMARC/DKIM | `permerror` для всего домена |
| MX на ту же цель с другим приоритетом — это правка, а не вторая запись | половина писем уходит мимо |
| слияние SPF сохраняет сторонний `ip4:`/`include:` | письма сторонних сервисов перестают проходить |
| DMARC не ослабляется | политика, выставленная человеком, тихо понижается |
| нормализация DKIM | ключ, вставленный фрагментами, не становится валидной записью |
| статусы шагов `done`/`todo`/`blocked`/`empty` | интерфейс выключает кнопку, которая должна работать |

Каждая проверка печатает `[ok]` или `[FAIL]` с причиной, поэтому регрессия называет себя сама.

## Куда относится изменение

| Изменение | Файл |
|---|---|
| Правило о дублях, слиянии SPF, политике DMARC | `backend/src/Service/ZonePlanner.php` |
| Пресет или шаг мастера | `backend/config/presets.json`, при новом типе поля — `PresetService` |
| Новый HTTP-маршрут | `backend/config/routes.php` + контроллер |
| Новая команда CLI | `backend/src/Cli/Application.php` + `docs/CLI.md` |
| Поведение интерфейса | `assets/js/app.js` (один файл, hash-маршруты, без фреймворка) |
| Новая настройка | `defaults.php`, `Config::ENV_MAP`, `.env.example`, `docs/CONFIGURATION.md` |

## Как добавить пресет

1. Запись в `presets.json`: плоский пресет (`records`, `params`) или мастер (`steps`).
2. Подстановки в значении: `{domain}`, `{default_ip}`, `{param_key}`.
3. `present_if` превращает «запись уже в зоне» в статус `done`.
4. `confirm: true` — только там, где значение заменяется.
5. Тесты: пустая зона, повторный запуск, зона с чужим `ip4:` и строгим DMARC.
6. Прогнать мастер на своей зоне и приложить к pull request план, а не результат применения.

## Соглашения

- PHP 8.5, `declare(strict_types=1)`, классы `final`, один класс на файл, пространство имён
  повторяет каталог.
- Комментарии и текст интерфейса — по-русски; идентификаторы, коммиты, документация и README — по-английски.
- Зависимостей нет: библиотека на 200 строк не стоит цепочки обновлений.
- Файлы: UTF-8 без BOM, LF, перевод строки в конце. `.gitattributes` объявляет, `tools/lint.php`
  проверяет.

## Выпуск версии

1. `CHANGELOG.md`: `[Unreleased]` → `## [X.Y.Z] - ГГГГ-ММ-ДД` и ссылки сравнения.
2. Поднять `app.version` в `defaults.php` и `APP_VERSION` в `.env.example`.
3. Четыре проверки плюс живой прогон на своей зоне.
4. Коммит, тег `vX.Y.Z`, push, релиз с разделом из changelog.
