architect(ET): auto-commit from architect run_id=369

This commit is contained in:
2026-06-08 11:05:36 +00:00
parent 59603f6e92
commit 7d99782673
2 changed files with 164 additions and 0 deletions

View File

@@ -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 хранят/экранируют сами).
Валидные заголовки 81200 символов отклоняются на входе без бизнес-причины.
Требуется:
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>

View File

@@ -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, чисто аддитивно).
</content>