# ADR-001: Конфигурируемый QG-0 title-лимит с graceful-деградацией env

## Статус
Accepted

## Контекст
QG-0 — inline-валидация входа конвейера (`_qg0_errors` в `src/webhooks/plane.py`),
вызывается из `start_pipeline` (hard-блок) и из `handle_work_item_created`
(soft-warning). Верхний лимит длины заголовка захардкожен: `if len(name) > 80`.

BRD/ТЗ (ORCH-069) установили, что лимит 80 — гигиенический, а не структурный:
ниже по течению от него ничего не зависит (slug режется независимо `[:30]`,
`tasks.title TEXT` без ограничения, Telegram/Plane хранят/экранируют сами).
Валидные заголовки 81–200 символов отклоняются на входе без бизнес-причины.

Требуется:
1. Вынести лимит в конфигурируемый параметр `ORCH_QG0_TITLE_MAX`, дефолт 200.
2. **Graceful-деградация** (AC-3): пустое/нечисловое значение env → дефолт 200
   **без падения процесса**. Это и есть единственное нетривиальное архитектурное
   решение задачи: `pydantic_settings` v2 по умолчанию при непарсящемся в `int`
   значении env бросает `ValidationError` на инстанцировании `Settings()` —
   т.е. краш на старте контейнера (`settings = Settings()` на module-import,
   `src/config.py:352`). Для self-hosting это означало бы падение прод-инструмента
   из-за опечатки в env — недопустимо.

Стек подтверждён: `pydantic==2.13.4`, `pydantic-settings==2.5.0` (v2 API).

## Решение

### Р-1. Новый параметр Settings
В `src/config.py`, в класс `Settings`, добавить поле (отдельный блок с
комментарием, рядом с прочими `ORCH_*`):

```python
# ORCH-069: QG-0 upper title-length limit (entry gate _qg0_errors).
# 80-char cap was a hygiene limit, not structural. Env ORCH_QG0_TITLE_MAX;
# default 200 (was hardcoded 80). Invalid/empty -> default (graceful, no crash).
qg0_title_max: int = 200
```
Env-имя выводится автоматически из `env_prefix = "ORCH_"` → `ORCH_QG0_TITLE_MAX`.

### Р-2. Механизм graceful-деградации — `field_validator(mode="before")`
Выбран **pydantic v2 `field_validator` с `mode="before"`** как
минимально-инвазивный, локальный для одного поля механизм. Валидатор перехватывает
сырое значение env ДО стандартного `int`-парсинга и при невалидном/пустом входе
возвращает дефолт `200`, гася `ValidationError`:

```python
from pydantic import field_validator

@field_validator("qg0_title_max", mode="before")
@classmethod
def _qg0_title_max_default(cls, v):
    # Graceful (ORCH-069 AC-3): empty / non-numeric env -> default 200,
    # process must not crash on startup. Never raises.
    try:
        if v is None or (isinstance(v, str) and v.strip() == ""):
            return 200
        return int(v)
    except (TypeError, ValueError):
        return 200
```

Семантика:
- переменная не задана → pydantic не вызывает validator с env, берётся дефолт поля
  `200` (стандартное поведение «из коробки»);
- `""`, `"abc"`, мусор → validator возвращает `200`, исключения нет;
- `"120"` → `int("120") == 120`.

**Почему именно так (рассмотренные альтернативы):**
- *`Optional[int] + None-fallback на месте чтения`* — отвергнуто: размазывает
  дефолт по call-site'ам, легко забыть, тип поля перестаёт быть «честным `int`».
- *try/except вокруг `Settings()` на module-level* — отвергнуто: глушит ВСЕ
  ошибки конфигурации (маскирует реальные проблемы других полей), слишком грубо.
- *кастомный тип / `Annotated`-валидатор* — избыточно для одного поля.
- `field_validator(mode="before")` локален, не трогает остальные поля, не меняет
  публичный тип `int`, тестируется напрямую через `Settings(qg0_title_max=...)` и
  env-патч. Контракт «never-raise» консистентен с общим стилем кодовой базы
  (`_qg0_errors`, парсеры — defensive).

### Р-3. Использование лимита в `_qg0_errors`
Хардкод `> 80` → динамическое чтение `settings.qg0_title_max` **на каждый вызов**
(чтобы тест мог патчить `settings`), текст ошибки — f-string с актуальным числом:

```python
if len(name) > settings.qg0_title_max:
    errors.append(
        f"Title слишком длинный (максимум {settings.qg0_title_max} символов)"
    )
```
`settings` уже импортирован в `plane.py`. Сигнатура `_qg0_errors(name, description)
-> list` не меняется. Нижние лимиты (`< 5` title, `< 20` description) — без правок.

Граница (ТЗ §4): fail строго при `len(name) > limit` → `len == limit` PASS,
`limit + 1` FAIL.

### Р-4. Что НЕ меняется (инварианты)
- `STAGE_TRANSITIONS`, `QG_CHECKS` — QG-0 не зарегистрированный stage-gate, а
  inline-валидация; реестры не трогаются.
- Схема БД (`tasks.title TEXT`), API, контракты `handle_*`, slug-логика `[:30]`,
  soft-QG-0 поведение (общая функция `_qg0_errors`, отдельной правки не требует).
- Топология/инфраструктура (`07-infra-requirements.md` — **N/A**) и схема данных
  (`08-data-requirements.md` — **N/A**) не затрагиваются.

## Последствия

### Плюсы
- Лимит операционно настраивается через env без правки кода и редеплоя кода.
- Чисто аддитивно и обратносовместимо: дефолт 200 > прежних 80 → все ранее
  проходившие заголовки проходят (AC-7).
- Опечатка в `ORCH_QG0_TITLE_MAX` не роняет прод-процесс (критично для
  self-hosting): graceful-fallback на 200.
- Изменение изолировано в одной функции + одном поле config + одном валидаторе.

### Минусы / ограничения
- Невалидное env «тихо» проглатывается → оператор не сразу заметит опечатку
  (лимит молча станет 200). Принято как осознанный trade-off: устойчивость
  процесса важнее громкости (consistency с требованием AC-3). Рекомендация:
  при желании усилить наблюдаемость — `logger.warning` в validator; **не вводим**
  по умолчанию, т.к. на этапе валидации settings логгер может быть не сконфигурён,
  и это вне объёма ORCH-069 (можно отдельной QoL-задачей).
- Дефолт 200 — тоже эвристика; структурного верхнего предела по-прежнему нет
  (его и не требуется — БД/slug/UI к длине устойчивы).

### Влияние на self-hosting
Прод-контейнер `orchestrator` **не рестартить** в рамках задачи. Изменение
прокатывается штатно через обязательный `deploy-staging`-гейт (8501) перед
прод-деплоем. Риск отказа на старте после деплоя снят самим механизмом Р-2
(graceful), что дополнительно снижает self-hosting-риск.

### Тестируемость (вход для стадий development/testing)
- `_qg0_errors`: патч `settings.qg0_title_max` → проверка границ 200/201 (AC-1),
  120/121 (AC-2), нижних лимитов (AC-4).
- validator: `Settings(qg0_title_max="abc")` / `=""` / env-патч → значение 200,
  без исключения (AC-3).

## Ссылки
- BRD: `docs/work-items/ORCH-069/01-brd.md`
- ТЗ: `docs/work-items/ORCH-069/02-trz.md`
- Acceptance: `docs/work-items/ORCH-069/03-acceptance-criteria.md`
- Тех-риски: `docs/work-items/ORCH-069/10-tech-risks.md`
</content>
</invoke>
