architect(ET): auto-commit from architect run_id=369
This commit is contained in:
@@ -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`
|
||||
</content>
|
||||
</invoke>
|
||||
21
docs/work-items/ORCH-069/10-tech-risks.md
Normal file
21
docs/work-items/ORCH-069/10-tech-risks.md
Normal 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>
|
||||
Reference in New Issue
Block a user