diff --git a/docs/work-items/ORCH-069/06-adr/ADR-001-configurable-qg0-title-limit.md b/docs/work-items/ORCH-069/06-adr/ADR-001-configurable-qg0-title-limit.md new file mode 100644 index 0000000..49e1fba --- /dev/null +++ b/docs/work-items/ORCH-069/06-adr/ADR-001-configurable-qg0-title-limit.md @@ -0,0 +1,143 @@ +# 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` + + diff --git a/docs/work-items/ORCH-069/10-tech-risks.md b/docs/work-items/ORCH-069/10-tech-risks.md new file mode 100644 index 0000000..b6bde09 --- /dev/null +++ b/docs/work-items/ORCH-069/10-tech-risks.md @@ -0,0 +1,21 @@ +# Технические риски — ORCH-069 + +Work Item ID: ORCH-069 +Уровень общего риска: **низкий** (аддитивное, обратносовместимое, изолированное изменение). + +| # | Риск | Вероятность | Влияние | Митигация | +|---|------|-------------|---------|-----------| +| R-1 | `ValidationError` на старте при мусоре в `ORCH_QG0_TITLE_MAX` → краш прод-процесса (self-hosting) | Средняя (опечатка в env) | Высокое (падение инструмента всех проектов) | `field_validator(mode="before")` гасит невалидный вход → дефолт 200 (ADR Р-2, AC-3). never-raise. | +| R-2 | Чтение лимита один раз на module-import вместо per-call → тесты не смогут патчить settings | Низкая | Среднее (нетестируемость AC-2) | `_qg0_errors` читает `settings.qg0_title_max` динамически на каждый вызов (ADR Р-3). | +| R-3 | Off-by-one на границе (`>=` вместо `>`) | Низкая | Низкое (1 символ) | Явная семантика `len > limit` зафиксирована (ТЗ §4, AC-1/AC-2); тесты на 200/201, 120/121. | +| R-4 | Регресс нижних лимитов (`< 5` title, `< 20` description) при правке функции | Низкая | Среднее | Трогать только верхний лимит; AC-4 покрывает нижние; диф минимален. | +| R-5 | Тихое проглатывание невалидного env → оператор не заметит опечатку | Средняя | Низкое (лимит молча = 200, конвейер работает) | Осознанный trade-off (ADR «Минусы»): устойчивость > громкость. Опц. `logger.warning` — вне объёма. | +| R-6 | Случайное затрагивание вне-объёмных элементов (slug `[:30]`, БД, реестры, `handle_*`, soft-QG-0) | Низкая | Среднее | AC-8 — изоляция; reviewer проверяет диф; ADR Р-4 фиксирует инварианты. | +| R-7 | Документация не обновлена в том же PR (`.env.example`, `.env.staging.example`, `CHANGELOG.md`) | Средняя | Среднее (reviewer REQUEST_CHANGES) | AC-6 чек-лист; документация = golden source (правило 2 CLAUDE.md). | + +## Не-риски (явно) +- Схема БД — не меняется (`tasks.title TEXT` без ограничения). +- API/эндпоинты — не меняются. +- Топология/контейнеры/порты — не меняются. +- Откат/миграция — не требуется (дефолт 200 > 80, чисто аддитивно). +