From d5f11e5caad0b8b446868747eb9d18d99530b7ea Mon Sep 17 00:00:00 2001 From: Slava Date: Mon, 8 Jun 2026 08:09:57 +0300 Subject: [PATCH 01/12] docs: init ORCH-069 business request --- docs/work-items/ORCH-069/00-business-request.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 docs/work-items/ORCH-069/00-business-request.md diff --git a/docs/work-items/ORCH-069/00-business-request.md b/docs/work-items/ORCH-069/00-business-request.md new file mode 100644 index 0000000..dbce07c --- /dev/null +++ b/docs/work-items/ORCH-069/00-business-request.md @@ -0,0 +1,7 @@ +# Business Request: QG-0 title-лимит → параметр ORCH_QG0_TITLE_MAX (дефолт 200) + +Work Item ID: ORCH-069 + +## Description + +TBD From 59603f6e92c71c44850db578b0501296fc5ce8f4 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Mon, 8 Jun 2026 05:12:49 +0000 Subject: [PATCH 02/12] analyst(ET): auto-commit from analyst run_id=350 --- docs/work-items/ORCH-069/01-brd.md | 76 ++++++++++++ docs/work-items/ORCH-069/02-trz.md | 95 +++++++++++++++ .../ORCH-069/03-acceptance-criteria.md | 56 +++++++++ docs/work-items/ORCH-069/04-test-plan.yaml | 112 ++++++++++++++++++ 4 files changed, 339 insertions(+) create mode 100644 docs/work-items/ORCH-069/01-brd.md create mode 100644 docs/work-items/ORCH-069/02-trz.md create mode 100644 docs/work-items/ORCH-069/03-acceptance-criteria.md create mode 100644 docs/work-items/ORCH-069/04-test-plan.yaml diff --git a/docs/work-items/ORCH-069/01-brd.md b/docs/work-items/ORCH-069/01-brd.md new file mode 100644 index 0000000..99e1a38 --- /dev/null +++ b/docs/work-items/ORCH-069/01-brd.md @@ -0,0 +1,76 @@ +# BRD — ORCH-069: QG-0 title-лимит → параметр ORCH_QG0_TITLE_MAX (дефолт 200) + +Work Item ID: ORCH-069 +Тип: Enhancement (QoL / конфигурируемость) +Источник: Слава, 2026-06-08 +Связано с: QG-0 (gate входа конвейера, `_qg0_errors`) + +## 1. Проблема (As-Is) +QG-0 — первый quality gate конвейера. Он валидирует заголовок и описание задачи +до старта pipeline (`start_pipeline`) и в soft-режиме на `work_item.created`. + +Верхний лимит длины заголовка задачи **захардкожен** в +`src/webhooks/plane.py:362`: + +```python +if len(name) > 80: + errors.append("Title слишком длинный (максимум 80 символов)") +``` + +Лимит 80 — «гигиенический», а не структурный. Проверено, что **ниже по течению +ничего от значения 80 не зависит**: +- slug ветки режется независимо: `re.sub(...)[:30]` (`src/webhooks/plane.py:478`); +- БД `tasks.title TEXT` — без ограничения длины; +- Telegram-карточка использует `html.escape(title)` без обрезки; +- Plane хранит `name` самостоятельно. + +Следствие: вполне валидные осмысленные заголовки длиной 81–200 символов +отклоняются на входе конвейера без бизнес-причины. + +## 2. Цель (To-Be) +Вынести верхний лимит длины заголовка QG-0 в конфигурируемый параметр со +значением по умолчанию **200** (вместо текущего хардкода 80). Расширить лимит +безопасно, сохранив возможность регулировать его через окружение, как и +остальные `ORCH_*` настройки. + +## 3. Бизнес-ценность +- Меньше ложных отклонений валидных задач на входе конвейера (QoL для постановщика). +- Лимит становится операционно настраиваемым без правки кода и редеплоя + (изменение env-переменной). +- Изменение чисто аддитивное и обратносовместимое: дефолт 200 > прежних 80, поэтому + все заголовки, проходившие раньше, проходят и теперь. + +## 4. Объём (Scope) +### В объёме +- Новый параметр Settings `qg0_title_max` (env `ORCH_QG0_TITLE_MAX`, дефолт 200). +- Замена хардкода `> 80` на `> settings.qg0_title_max` в `_qg0_errors`. +- Динамический текст ошибки с подстановкой актуального лимита. +- Graceful-поведение при невалидном/пустом значении env → дефолт 200, без падения процесса. +- Документация: `.env.example`, `.env.staging.example`, `CHANGELOG.md`, + при необходимости README-таблица конфигов / `CLAUDE.md`. +- Юнит-тесты на `_qg0_errors` с разными лимитами. + +### Вне объёма (Out of scope) +- Slug-логика `[:30]` (`src/webhooks/plane.py:478`) — самодостаточна, не трогать. +- Нижний лимит заголовка (`< 5`) и лимит description (`< 20`) — оставить как есть. +- Схема БД, реестры `STAGE_TRANSITIONS` / `QG_CHECKS`, контракты `handle_*`. +- Soft-QG-0 на `work_item.created` (там только warning) — логика валидации общая + (`_qg0_errors`), отдельных изменений не требует и не вносит. + +## 5. Заинтересованные стороны +- Owner / постановщик задач (Слава) — снижение ложных отклонений. +- Агенты конвейера — поведение QG-0 при старте pipeline. + +## 6. Ограничения и риски (self-hosting) +- Правка касается работающего в проде инструмента (self-hosting). Прод-контейнер + `orchestrator` в рамках задачи **не рестартить**; обязательна страховка + `deploy-staging` (8501). +- Риск минимален: изменение обратносовместимо, изолировано в одной функции и одном + новом параметре config. + +## 7. Допущения +- Механизм чтения env — стандартный `pydantic_settings.BaseSettings` с + `env_prefix = "ORCH_"`, как у остальных параметров. +- «Невалидное/пустое значение → дефолт 200» — требование graceful-деградации: + процесс не должен падать на старте из-за мусора в `ORCH_QG0_TITLE_MAX` + (нюанс реализации pydantic-валидации передаётся архитектору, см. 02-trz §5). diff --git a/docs/work-items/ORCH-069/02-trz.md b/docs/work-items/ORCH-069/02-trz.md new file mode 100644 index 0000000..7ffb071 --- /dev/null +++ b/docs/work-items/ORCH-069/02-trz.md @@ -0,0 +1,95 @@ +# ТЗ — ORCH-069: QG-0 title-лимит → параметр ORCH_QG0_TITLE_MAX (дефолт 200) + +Work Item ID: ORCH-069 + +## 1. Задействованные модули `src/` +| Файл | Текущее состояние | Требуемое изменение | +|------|-------------------|---------------------| +| `src/config.py` | `Settings(BaseSettings)`, `env_prefix = "ORCH_"` (строки 4, 347-349) | Добавить поле `qg0_title_max: int = 200` с комментарием-описанием. | +| `src/webhooks/plane.py` | `_qg0_errors` (строки 357-367), хардкод `if len(name) > 80:` (строка 362); `from ..config import settings` уже импортирован (строка 11) | Заменить хардкод `> 80` на `> settings.qg0_title_max`; текст ошибки — динамический с подстановкой лимита. | + +Других модулей изменение не затрагивает. + +## 2. Изменение config.py +Добавить в класс `Settings` новое поле (рядом с другими `ORCH_*` группами, +рекомендуется отдельный блок с комментарием): + +```python +# ORCH-069: QG-0 upper title-length limit (entry gate _qg0_errors). The 80-char +# cap was a hygiene limit, not structural (slug is cut to [:30] independently, +# DB title TEXT is unbounded). Configurable via env ORCH_QG0_TITLE_MAX; default +# 200 (was hardcoded 80). Invalid/empty value -> default (graceful, no crash). +qg0_title_max: int = 200 +``` + +- Env-переменная: `ORCH_QG0_TITLE_MAX` (автоматически из `env_prefix = "ORCH_"`). +- Тип `int`, дефолт `200`. + +## 3. Изменение `_qg0_errors` (src/webhooks/plane.py) +Текущий блок (строки 362-363): +```python +if len(name) > 80: + errors.append("Title слишком длинный (максимум 80 символов)") +``` + +Требуемое: +```python +if len(name) > settings.qg0_title_max: + errors.append( + f"Title слишком длинный (максимум {settings.qg0_title_max} символов)" + ) +``` + +Требования: +- Лимит берётся из `settings.qg0_title_max` (динамически, на каждый вызов — чтобы + тесты могли подменять значение через мок/патч settings). +- Текст ошибки содержит актуальное число лимита (для AC-1/AC-2: текст упоминает + 200 / 120 соответственно). +- Нижний лимит заголовка `< 5` (строка 360-361) и проверка description `< 20` + (строка 364-365) — **не трогать**. +- Сигнатура `_qg0_errors(name, description) -> list` не меняется. + +## 4. Поведение границы (точная семантика) +- Условие fail — строго `len(name) > limit`. То есть `len == limit` → PASS, + `len == limit + 1` → FAIL. +- При дефолте: 200 символов → PASS, 201 → FAIL. +- При `ORCH_QG0_TITLE_MAX=120`: 120 → PASS, 121 → FAIL. + +## 5. Graceful-обработка невалидного значения (требование AC-3) +Требование: невалидное/отсутствующее `ORCH_QG0_TITLE_MAX` → используется дефолт 200, +процесс не падает. + +Нюанс для архитектора/разработчика: `pydantic_settings` по умолчанию при +непарсящемся в `int` значении env (например `ORCH_QG0_TITLE_MAX=abc` или пустая +строка) выбрасывает `ValidationError` на инстанцировании `Settings()` — +т.е. падение на старте процесса. Это противоречит требованию graceful. +Реализация должна обеспечить, что: +- отсутствие переменной → дефолт 200 (это стандартное поведение, ОК «из коробки»); +- пустая строка / нечисловое значение → дефолт 200 без исключения. + +Способ (на усмотрение архитектора, без предписания со стороны аналитика) — +например field-validator с `mode="before"`, который при невалидном входе +возвращает дефолт. Конкретный механизм фиксируется в ADR на стадии architecture. + +## 6. Изменения API +Нет. Эндпоинты не меняются. + +## 7. Изменения схемы БД +Нет. `tasks.title TEXT` остаётся без ограничения длины. + +## 8. Новые QG checks +Нет. Реестр `QG_CHECKS` и `STAGE_TRANSITIONS` не меняются. QG-0 — не зарегистрированный +stage-gate, а inline-валидация входа (`_qg0_errors`), её контракт сохраняется. + +## 9. Артефакты pipeline, которые должны быть созданы/обновлены +- `.env.example` — добавить `ORCH_QG0_TITLE_MAX=200` с комментарием. +- `.env.staging.example` — добавить `ORCH_QG0_TITLE_MAX` (дефолт/комментарий). +- `CHANGELOG.md` — запись об ORCH-069. +- README-таблица конфигов / `CLAUDE.md` — обновить при наличии релевантной таблицы + параметров (по требованию reviewer; документация = golden source). +- Юнит-тесты (`tests/`) — см. `04-test-plan.yaml`. + +## 10. Обратная совместимость +- Дефолт 200 > прежних 80 → все ранее проходившие заголовки проходят и теперь. +- Поведение при не заданном env идентично «как было», но с порогом 200 вместо 80. +- Изменение чисто аддитивное; откатов/миграций не требует. diff --git a/docs/work-items/ORCH-069/03-acceptance-criteria.md b/docs/work-items/ORCH-069/03-acceptance-criteria.md new file mode 100644 index 0000000..a76e91f --- /dev/null +++ b/docs/work-items/ORCH-069/03-acceptance-criteria.md @@ -0,0 +1,56 @@ +# Критерии приёмки — ORCH-069 + +Work Item ID: ORCH-069 + +Формат: каждый критерий имеет чёткое условие PASS/FAIL. + +## AC-1 — Дефолтный лимит 200, граница на 201 +**Дано:** env `ORCH_QG0_TITLE_MAX` не задан (используется дефолт 200), description валиден (≥ 20 символов). +**Тогда:** +- заголовок длиной 200 символов → `_qg0_errors` НЕ содержит ошибки про длину title (PASS); +- заголовок длиной 201 символ → `_qg0_errors` содержит ошибку про длину title, и текст ошибки упоминает «200». +**FAIL если:** на 200 появляется ошибка длины, либо на 201 ошибки нет, либо текст не упоминает 200. + +## AC-2 — Настраиваемый лимит 120, граница на 121 +**Дано:** `ORCH_QG0_TITLE_MAX=120` (через мок/патч settings в тесте), description валиден. +**Тогда:** +- заголовок 120 символов → нет ошибки длины title (PASS); +- заголовок 121 символ → есть ошибка длины title, текст упоминает «120». +**FAIL если:** граница срабатывает не на 121, либо текст ошибки упоминает не 120. + +## AC-3 — Graceful при невалидном/пустом значении +**Дано:** `ORCH_QG0_TITLE_MAX` пустой (`""`) или нечисловой (`"abc"`). +**Тогда:** +- инстанцирование `Settings()` / импорт приложения НЕ выбрасывает исключение (процесс не падает); +- эффективное значение лимита = дефолт 200 (поведение AC-1 сохраняется). +**FAIL если:** старт процесса падает с `ValidationError`, либо лимит != 200. + +## AC-4 — Нижние лимиты не сломаны +**Дано:** любое валидное значение `ORCH_QG0_TITLE_MAX`. +**Тогда:** +- заголовок длиной < 5 символов → `_qg0_errors` содержит ошибку «Title слишком короткий»; +- description длиной < 20 символов → `_qg0_errors` содержит ошибку «Description слишком короткий». +**FAIL если:** нижний лимит title или лимит description перестал срабатывать. + +## AC-5 — Юнит-тесты зелёные +**Дано:** реализованные юнит-тесты на `_qg0_errors` с разными значениями лимита (мок settings). +**Тогда:** `pytest tests/ -q` проходит полностью (зелёный), включая новые тесты ORCH-069 и существующий набор. +**FAIL если:** хотя бы один тест падает. + +## AC-6 — Документация обновлена в том же PR +**Дано:** PR с изменениями кода. +**Тогда в том же PR:** +- `.env.example` содержит `ORCH_QG0_TITLE_MAX` с дефолтом и комментарием; +- `.env.staging.example` содержит `ORCH_QG0_TITLE_MAX`; +- `CHANGELOG.md` содержит запись об ORCH-069; +- при наличии релевантной таблицы конфигов в README / `CLAUDE.md` — она обновлена. +**FAIL если:** какой-либо из обязательных файлов документации не обновлён (reviewer → REQUEST_CHANGES). + +## AC-7 — Обратная совместимость +**Дано:** env не задан. +**Тогда:** любой заголовок, который проходил QG-0 при прежнем лимите 80 (len ≤ 80), проходит и теперь (len ≤ 200). +**FAIL если:** ранее валидный заголовок отклоняется. + +## AC-8 — Изоляция изменений +**Тогда:** не изменены slug-логика (`[:30]`), схема БД, реестры `STAGE_TRANSITIONS` / `QG_CHECKS`, контракты `handle_*`, soft-QG-0 поведение (warning на `work_item.created`). +**FAIL если:** затронут любой из перечисленных вне-объёмных элементов. diff --git a/docs/work-items/ORCH-069/04-test-plan.yaml b/docs/work-items/ORCH-069/04-test-plan.yaml new file mode 100644 index 0000000..b99bcb0 --- /dev/null +++ b/docs/work-items/ORCH-069/04-test-plan.yaml @@ -0,0 +1,112 @@ +work_item: ORCH-069 +description: > + Юнит-тесты для конфигурируемого верхнего лимита длины заголовка QG-0 + (_qg0_errors) через параметр settings.qg0_title_max (env ORCH_QG0_TITLE_MAX, + дефолт 200). Тесты патчат settings.qg0_title_max (monkeypatch на объекте + src.config.settings, который импортирован в src.webhooks.plane) и проверяют + границы и тексты ошибок. Файл тестов: tests/test_qg0_title_limit.py. + +tests: + - id: TC-01 + type: unit + description: "Дефолтный лимит 200: заголовок ровно 200 символов -> нет ошибки длины title (PASS на границе)." + module: tests/test_qg0_title_limit.py + setup: "settings.qg0_title_max=200 (дефолт); name='x'*200; description валиден (>=20 символов)." + assert: "В списке _qg0_errors нет элемента про длину title." + covers: [AC-1] + expected: PASS + + - id: TC-02 + type: unit + description: "Дефолтный лимит 200: заголовок 201 символ -> ошибка длины title, текст упоминает '200'." + module: tests/test_qg0_title_limit.py + setup: "settings.qg0_title_max=200; name='x'*201; description валиден." + assert: "В _qg0_errors есть ошибка длины title и её текст содержит подстроку '200'." + covers: [AC-1] + expected: PASS + + - id: TC-03 + type: unit + description: "Настраиваемый лимит 120: заголовок 120 символов -> нет ошибки длины title." + module: tests/test_qg0_title_limit.py + setup: "monkeypatch settings.qg0_title_max=120; name='x'*120; description валиден." + assert: "Нет ошибки длины title." + covers: [AC-2] + expected: PASS + + - id: TC-04 + type: unit + description: "Настраиваемый лимит 120: заголовок 121 символ -> ошибка длины title, текст упоминает '120'." + module: tests/test_qg0_title_limit.py + setup: "monkeypatch settings.qg0_title_max=120; name='x'*121; description валиден." + assert: "Есть ошибка длины title и её текст содержит подстроку '120' (и НЕ '80')." + covers: [AC-2] + expected: PASS + + - id: TC-05 + type: unit + description: "Graceful: невалидное (нечисловое) значение env ORCH_QG0_TITLE_MAX не роняет инстанцирование Settings и даёт дефолт 200." + module: tests/test_qg0_title_limit.py + setup: "monkeypatch.setenv('ORCH_QG0_TITLE_MAX','abc'); создать новый экземпляр Settings()." + assert: "Settings() не выбрасывает исключение; settings.qg0_title_max == 200." + covers: [AC-3] + expected: PASS + + - id: TC-06 + type: unit + description: "Graceful: пустая строка env ORCH_QG0_TITLE_MAX -> дефолт 200, без исключения." + module: tests/test_qg0_title_limit.py + setup: "monkeypatch.setenv('ORCH_QG0_TITLE_MAX',''); создать новый экземпляр Settings()." + assert: "Settings() не падает; settings.qg0_title_max == 200." + covers: [AC-3] + expected: PASS + + - id: TC-07 + type: unit + description: "Корректное числовое env -> применяется заданное значение (sanity положительного пути)." + module: tests/test_qg0_title_limit.py + setup: "monkeypatch.setenv('ORCH_QG0_TITLE_MAX','150'); создать новый экземпляр Settings()." + assert: "settings.qg0_title_max == 150." + covers: [AC-2, AC-3] + expected: PASS + + - id: TC-08 + type: unit + description: "Нижний лимит title не сломан: заголовок < 5 символов -> ошибка 'Title слишком короткий' при любом верхнем лимите." + module: tests/test_qg0_title_limit.py + setup: "settings.qg0_title_max=200; name='abc' (3 символа); description валиден." + assert: "В _qg0_errors есть ошибка короткого title." + covers: [AC-4] + expected: PASS + + - id: TC-09 + type: unit + description: "Лимит description не сломан: description < 20 символов -> ошибка 'Description слишком короткий'." + module: tests/test_qg0_title_limit.py + setup: "settings.qg0_title_max=200; name валиден (>=5, <=200); description='short'." + assert: "В _qg0_errors есть ошибка короткого description." + covers: [AC-4] + expected: PASS + + - id: TC-10 + type: unit + description: "Обратная совместимость: заголовок длиной 81-200 (ранее отклонялся лимитом 80) теперь проходит при дефолте." + module: tests/test_qg0_title_limit.py + setup: "settings.qg0_title_max=200; name='x'*100; description валиден." + assert: "Нет ошибки длины title (раньше при лимите 80 была бы)." + covers: [AC-7] + expected: PASS + + - id: TC-11 + type: unit + description: "Полный набор тестов зелёный (регрессия не внесена)." + module: tests/ + command: "pytest tests/ -q" + assert: "Все тесты проходят." + covers: [AC-5] + expected: PASS + +notes: + - "settings импортирован в src.webhooks.plane как 'from ..config import settings', _qg0_errors читает settings.qg0_title_max динамически -> monkeypatch на src.config.settings.qg0_title_max (или импортируемом объекте) меняет поведение в рамках теста." + - "Для TC-05/06/07 нужен СВЕЖИЙ экземпляр Settings(): глобальный src.config.settings создаётся один раз на импорт, поэтому env-тесты инстанцируют Settings() локально, а не полагаются на готовый синглтон." + - "Тесты не требуют сети, БД, агентов или FastAPI TestClient — чистая проверка leaf-функции _qg0_errors и парсинга Settings." From 7d99782673ed9302094b02daa8956e8f1234d9a9 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Mon, 8 Jun 2026 11:05:36 +0000 Subject: [PATCH 03/12] architect(ET): auto-commit from architect run_id=369 --- .../ADR-001-configurable-qg0-title-limit.md | 143 ++++++++++++++++++ docs/work-items/ORCH-069/10-tech-risks.md | 21 +++ 2 files changed, 164 insertions(+) create mode 100644 docs/work-items/ORCH-069/06-adr/ADR-001-configurable-qg0-title-limit.md create mode 100644 docs/work-items/ORCH-069/10-tech-risks.md 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, чисто аддитивно). + From 0ed05417e623e012b62daf0b2ddd125c8d4bb0e6 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Mon, 8 Jun 2026 11:09:06 +0000 Subject: [PATCH 04/12] feat(qg0): configurable QG-0 title limit via ORCH_QG0_TITLE_MAX (default 200) Replace the hardcoded `len(name) > 80` cap in the QG-0 entry validation (_qg0_errors) with a configurable Settings.qg0_title_max (env ORCH_QG0_TITLE_MAX, default 200). The 80-char cap was a hygiene limit, not structural, so valid 81-200 char titles were rejected without a business reason. The limit is read dynamically per call and the error text interpolates the active value. Graceful degradation (AC-3, self-hosting safety): an empty/non-numeric env value no longer crashes the process on startup. A field_validator(mode="before") intercepts the raw env before int-parsing and falls back to 200 (never raises), suppressing pydantic ValidationError. Additive and backward-compatible (default 200 > old 80). Invariants unchanged: STAGE_TRANSITIONS, QG_CHECKS registry, DB schema, slug [:30], lower limits, soft-QG-0 warning path, API. Refs: ORCH-069 Co-Authored-By: Claude Opus 4.7 --- .env.example | 7 ++ .env.staging.example | 3 + CHANGELOG.md | 1 + src/config.py | 19 ++++++ src/webhooks/plane.py | 7 +- tests/test_qg0_title_limit.py | 117 ++++++++++++++++++++++++++++++++++ 6 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 tests/test_qg0_title_limit.py diff --git a/.env.example b/.env.example index 40423d9..37f8fa3 100644 --- a/.env.example +++ b/.env.example @@ -199,3 +199,10 @@ ORCH_POST_DEPLOY_FAIL_THRESHOLD=3 ORCH_POST_DEPLOY_5XX_THRESHOLD=0.5 ORCH_POST_DEPLOY_AUTO_ROLLBACK=false ORCH_POST_DEPLOY_BASE_URL=http://localhost:8500 + +# ── QG-0 entry validation (ORCH-069) ────────────────────────────────────────── +# Upper title-length limit for the QG-0 entry gate (_qg0_errors). The old 80-char +# cap was a hygiene limit, not structural (slug is cut to [:30] independently, the +# DB title TEXT is unbounded). Default 200. An invalid/empty value gracefully +# degrades to 200 (the process never crashes on startup). +ORCH_QG0_TITLE_MAX=200 diff --git a/.env.staging.example b/.env.staging.example index f3af589..722ed25 100644 --- a/.env.staging.example +++ b/.env.staging.example @@ -50,3 +50,6 @@ ORCH_QUEUE_POLL_INTERVAL=2.0 DEPLOY_SSH_USER=slin DEPLOY_SSH_HOST=127.0.0.1 DEPLOY_HOOK_SCRIPT=/home/slin/bin/enduro-deploy-hook.sh + +# QG-0 entry title-length limit (ORCH-069). Default 200; invalid/empty -> 200. +ORCH_QG0_TITLE_MAX=200 diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f5361f..2692a99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ## [Unreleased] ### Added +- **Конфигурируемый верхний лимит длины заголовка QG-0 (`ORCH_QG0_TITLE_MAX`, дефолт 200)** (ORCH-069): хардкод `if len(name) > 80` во входной валидации `_qg0_errors` (`src/webhooks/plane.py`) вынесен в настраиваемый параметр `Settings.qg0_title_max` (env `ORCH_QG0_TITLE_MAX`, дефолт 200). Лимит 80 был гигиеническим, а не структурным (slug режется независимо `[:30]`, `tasks.title TEXT` без ограничения), поэтому валидные заголовки 81–200 символов отклонялись на входе без бизнес-причины. Лимит читается из `settings.qg0_title_max` динамически на каждый вызов (тесты патчат значение), текст ошибки подставляет актуальное число; граница строгая (`len > limit` → FAIL, `len == limit` → PASS). **Graceful-деградация (AC-3, self-hosting safety):** пустое/нечисловое значение env не роняет процесс на старте — `field_validator(mode="before")` `_qg0_title_max_default` в `src/config.py` перехватывает сырое env ДО `int`-парсинга pydantic и при невалидном/пустом входе возвращает дефолт 200 (never-raise), гася `ValidationError`. Чисто аддитивно и обратносовместимо: дефолт 200 > прежних 80 → все ранее проходившие заголовки проходят (AC-7). Инварианты НЕ менялись: `STAGE_TRANSITIONS`, реестр `QG_CHECKS` (QG-0 — inline-валидация входа, не зарегистрированный stage-gate), схема БД, slug-логика `[:30]`, нижние лимиты (`< 5` title, `< 20` description), soft-QG-0 поведение (warning на `work_item.created`), API. ADR `docs/work-items/ORCH-069/06-adr/ADR-001-configurable-qg0-title-limit.md`. Документация: `.env.example`, `.env.staging.example`. Тесты: `tests/test_qg0_title_limit.py`. - **Merge-в-`main` + пост-деплой верификация как обязательное условие `done` (фикс «фантомного merge»)** (ORCH-071): задача могла дойти до `done`, хотя ветка фактически НЕ влита в `main` («фантомный merge») — конвейер рапортовал успех без реального состояния репозитория. Введён под-гейт ребра `deploy → done`: единственная точка перехода `advance_stage` теперь гейтится `_handle_merge_verify` (`src/stage_engine.py`), который покрывает ВСЕ пути финализации (finalizer Phase C, reconciler F-1, job-reaper). Добавлены детерминированный merge-актор и пост-деплой верификатор (`src/merge_gate.py`): merge выполняется ТОЛЬКО через PR-merge API (без push/force-push, INV-4) в restart-surviving Phase C, верификация подтверждает фактическое слияние в `main` прежде чем разрешить переход в `done`. Раскат условный и снабжён kill-switch (`src/config.py`, `src/main.py`, по образцу условности ORCH-35/43/58), never-raise контракты соблюдены. Документация: глобальный `docs/architecture/adr/adr-0013-merge-verify-gate.md`, детальный `docs/work-items/ORCH-071/06-adr/ADR-001-merge-verify-gate.md` (D1–D9), раздел в `docs/architecture/README.md`, runbook постмортема `docs/operations/PHANTOM_MERGE_RUNBOOK.md` (4 проверки + критерий «фантом подтверждён» + remediation). Тесты: `tests/test_merge_actor.py`, `tests/test_merge_verify.py`, `tests/test_deploy_finalizer_merge_gate.py`, `tests/test_deploy_restart_merge_recovery.py`, `tests/test_qg_checks.py`, `tests/test_stages.py`. - **Security-гейт: secret-scanning (gitleaks) + dependency audit (pip-audit) перед мержем** (ORCH-022): автономный конвейер вливал ветку в `main` без проверки на утёкший секрет (ключ/токен/пароль/приватный ключ) и уязвимую зависимость (известный CVE) — для self-hosting `orchestrator` это особенно остро: один общий прод-инстанс обслуживает все проекты из общей БД, поэтому секрет/CVE, проскочивший через одну задачу, уезжает в прод всех проектов (CLAUDE.md §self-hosting, §8). ORCH-022 вводит детерминированный (без LLM) **security-гейт как под-гейт ребра `deploy-staging → deploy`**, исполняемый **ПЕРВЫМ** среди edge-под-гейтов (ДО merge-gate ORCH-043 и image-freshness ORCH-058) — дёшево фейлить до дорогих rebase/rebuild, а скан ветки ДО rebase не «обвиняет» задачу в CVE из обновившегося `main`. Паттерн соседей: новый leaf-модуль `src/security_gate.py` (контракт «never-raise», по образцу `merge_gate`/`image_freshness`/`staging_verdict`) + тонкая обёртка `check_security_gate` в реестре `QG_CHECKS` (`src/qg/checks.py`, lazy-import → нет цикла) + врезка `_handle_security_gate` в `src/stage_engine.py` в блок `current_stage == "deploy-staging"` ПЕРВОЙ. `STAGE_TRANSITIONS` и схема БД — **без изменений**. **Secret-scanning (`gitleaks`, offline):** скан диапазона `origin/main..HEAD` (ровно коммиты задачи); любой секрет вне аллоулиста версионируемого `.gitleaks.toml` → вклад в FAIL. Полностью оффлайн (локальные правила) → гарантия «секрет всегда блокирует» (BR-2) безусловна, не зависит от сети; **fail-closed** при ошибке инструмента/отсутствии бинаря/таймауте (нельзя доказать «секретов нет» → FAIL). Контракт exit-кодов: 0=чисто, 1=найдено, ≥2=ошибка. **Dependency audit (`pip-audit`, OSV/PyPI):** аудит `requirements.txt`; severity ≥ `security_dep_block_severity` (дефолт `HIGH`, порядок CRITICAL>HIGH>MEDIUM>LOW) → вклад в FAIL (`deps_blocking`); ниже порога / UNKNOWN → warning (`deps_warning`, анти-петля Р-4, не авто-блок). Источник advisory требует сети → недоступность фида **fail-open + громкий warning** по умолчанию (`deps_audit_degraded: true` + Telegram + лог; прецедент анти-петли ORCH-061), флаг `security_dep_audit_fail_closed` переводит в строгий режим без редеплоя кода. **Артефакт `17-security-report.md`** (YAML-frontmatter `security_status`/`secrets_found`/`deps_blocking`/`deps_warning`/`deps_audit_degraded` + тело-списки находок); машинный вердикт читается ТОЛЬКО из frontmatter (гейт пишет → читает обратно через `parse_security_status` → возвращает ровно то, что записал: единый источник истины, AC-8), negative-токен (FAIL) авторитетен, нет frontmatter/битый YAML/нет поля → **fail-closed** на чтении; значения секретов в артефакте маскируются (не ре-лик). **FAIL → откат на `development`** + developer-retry (общий `_developer_retry_count`, cap `MAX_DEVELOPER_RETRIES`=3, затем `set_issue_blocked` + Telegram, без бесконечного баунса); `task_desc` перезапущенного developer'а несёт дословные находки (`extract_security_findings`, паттерн ORCH-046) + ссылку на артефакт. **Self-hosting safety:** гейт только читает/сканирует/пишет артефакт — не вызывает деплой-хук, не рестартит прод-контейнер (под-гейт исполняется ДО захвата merge-lease → при FAIL lease освобождать не нужно). **Условность как ORCH-35/43/58:** `security_gate_enabled` (kill-switch) + `security_gate_repos` (CSV; пусто → только self-hosting `orchestrator`); таймаут `security_scan_timeout_s`; never-raise. v1 — Python-only стек; SAST/мульти-стек — follow-up (BR-14). Инфраструктура: pinned `gitleaks` (статический Go-бинарь) в `Dockerfile` (+ `curl`/`ca-certificates`), `pip-audit` (pinned) в `requirements.txt`, `.gitleaks.toml` в корне репо. Новые настройки: `ORCH_SECURITY_GATE_ENABLED` (true), `ORCH_SECURITY_GATE_REPOS` (""), `ORCH_SECURITY_DEP_BLOCK_SEVERITY` (HIGH), `ORCH_SECURITY_SCAN_TIMEOUT_S` (300), `ORCH_SECURITY_DEP_AUDIT_FAIL_CLOSED` (false), `ORCH_SECURITY_SECRETS_BLOCK` (true). Инварианты НЕ менялись: `STAGE_TRANSITIONS` (9 стадий), `check_branch_mergeable`/`check_staging_image_fresh` и их под-гейты, БАГ-8 откат, terminal-sync, схема БД (без миграций). ADR `docs/work-items/ORCH-022/06-adr/ADR-001-security-gate.md`, глобальный `docs/architecture/adr/adr-0012-security-gate.md`. Документация: `docs/architecture/README.md`, `CLAUDE.md`, `.env.example`. Тесты: `tests/test_security_gate.py`, `tests/test_qg_security.py`, `tests/test_stage_engine_security_gate.py`, `tests/test_qg_registry_snapshot.py`, `tests/test_config.py`. - **Выделенный статус-триггер прод-деплоя «Confirm Deploy»** (ORCH-059): жест запуска прод-деплоя отделён от человеческого гейта одобрения. Раньше один Plane-статус `Approved` был перегружен: на `analysis` он работал как человеческий гейт BRD (`check_analysis_approved`), а на `deploy` — молча триггерил Фазу B прод-деплоя ORCH-036 (`advance_stage(deploy, finished_agent=None) → _handle_self_deploy_phase_b → detached host-рестарт прод-контейнера 8500`). Привычный жест approve = групповой self-hosting риск (прод обслуживает ВСЕ проекты из одного инстанса). ORCH-059 вводит отдельный логический статус `confirm_deploy` («Confirm Deploy»), который триггерит **ТОЛЬКО** Фазу B на `deploy`; `Approved` остаётся исключительно гейтом конвейера. Четыре точечные правки в трёх модулях: (1) `src/plane_sync.py` — маппинг `"Confirm Deploy" → "confirm_deploy"` в `_PLANE_NAME_TO_KEY`; ключ намеренно НЕ добавлен в `_DEFAULT_STATES` (нет UUID для enduro/fallback) → **fail-closed**: для проекта ORCH резолвится из живого Plane API (`get_project_states(orch)["confirm_deploy"]` → реальный UUID), для сред без статуса (enduro / недоступный API / доска без статуса) ключ просто отсутствует, доступ через `.get("confirm_deploy")` → `None`, без `KeyError`. (2) `src/webhooks/plane.py` — `handle_issue_updated` ДО ветки `approved` добавляет fail-closed-ветку `confirm_state = proj_states.get("confirm_deploy"); if confirm_state and new_state == confirm_state: handle_confirm_deploy(...)`; новый `handle_confirm_deploy` резолвит задачу, гард `stage == "deploy"` (иначе no-op с логом — защищает прочие гейты от случайного триггера), иначе → `_try_advance_stage(..., confirm_deploy=True)`. `handle_verdict(approved=True)` не изменён (продолжает звать `_try_advance_stage` с дефолтным `confirm_deploy=False`). (3) `src/stage_engine.py` — `advance_stage` получил keyword-only параметр `confirm_deploy: bool = False` (обратносовместимо: все существующие вызовы из launcher/reconciler/finalizer передают `finished_agent`); блок Фазы B теперь **всегда возвращается рано** для `deploy + finished_agent is None` self-hosting, но `_handle_self_deploy_phase_b` вызывается ТОЛЬКО при `confirm_deploy=True`, иначе (обычный `Approved`) — детерминированный **no-op** (`result.note = "approved-on-deploy-noop"`): возврат ДО блока Quality Gate → `check_deploy_status` не запускается → нет ложного отката БАГ-8 (вердикта ещё нет, R-2). (4) CTA Фазы A (`_handle_self_deploy_phase_a`) — Plane-коммент и Telegram просят перевести задачу в статус «Confirm Deploy» (а не «Approved»). Следствие для reconciler F-1 на `deploy` (ORCH-053): попадает в no-op-ветку вместо неявного запуска Фазы B → прод-деплой нельзя инициировать автоматически, только явным человеческим «Confirm Deploy» (усиление safety). Условность как ORCH-35/36 (реально только для `self_deploy.self_deploy_applies("orchestrator")`; прочие репо — прежний синхронный ssh-деплой агентом, статус не нужен и не влияет). Контракты НЕ менялись: `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, `check_deploy_status`/`_parse_deploy_status`, exit-код-контракт хука (0/1/2), Фазы A/C, merge-gate, terminal-sync, схема БД (статусы — на стороне Plane; restart-safe состояние деплоя — существующие sentinel-файлы ORCH-036). Эксплуатационное предусловие: в Plane-проекте ORCH создать статус доски «Confirm Deploy» (точное имя, регистр) + сброс кэша состояний — `docs/work-items/ORCH-059/07-infra-requirements.md`. До создания статуса прод-деплой через approve не запустится (желаемое fail-closed-поведение). ADR `docs/work-items/ORCH-059/06-adr/ADR-001-confirm-deploy-status.md` (уточняет триггер Фазы B относительно adr-0007). Тесты: `tests/test_plane_states.py`, `tests/test_plane_confirm_deploy.py`, `tests/test_stage_engine_phase_b.py`, `tests/test_stage_engine_phase_a_cta.py`, `tests/test_confirm_deploy_integration.py`, `tests/test_deploy_approve.py` (обновлён под новый триггер). diff --git a/src/config.py b/src/config.py index b9ad1e3..6f36681 100644 --- a/src/config.py +++ b/src/config.py @@ -1,3 +1,4 @@ +from pydantic import field_validator from pydantic_settings import BaseSettings @@ -407,6 +408,24 @@ class Settings(BaseSettings): # Неизвестное/пустое значение трактуется как edit (см. notifications). tracker_mode: str = "edit" + # ORCH-069: QG-0 upper title-length limit (entry gate _qg0_errors). The 80-char + # cap was a hygiene limit, not structural (slug is cut to [:30] independently, + # DB title TEXT is unbounded). Configurable via env ORCH_QG0_TITLE_MAX; default + # 200 (was hardcoded 80). Invalid/empty value -> default (graceful, no crash). + qg0_title_max: int = 200 + + @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, the + # process must not crash on startup. Never raises (self-hosting safety). + try: + if v is None or (isinstance(v, str) and v.strip() == ""): + return 200 + return int(v) + except (TypeError, ValueError): + return 200 + class Config: env_prefix = "ORCH_" env_file = ".env" diff --git a/src/webhooks/plane.py b/src/webhooks/plane.py index 875f54a..4bdaf0c 100644 --- a/src/webhooks/plane.py +++ b/src/webhooks/plane.py @@ -416,8 +416,11 @@ def _qg0_errors(name: str, description: str) -> list: errors = [] if not name or len(name) < 5: errors.append("Title \u0441\u043b\u0438\u0448\u043a\u043e\u043c \u043a\u043e\u0440\u043e\u0442\u043a\u0438\u0439 (\u043d\u0443\u0436\u043d\u043e >= 5 \u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432)") - if len(name) > 80: - errors.append("Title \u0441\u043b\u0438\u0448\u043a\u043e\u043c \u0434\u043b\u0438\u043d\u043d\u044b\u0439 (\u043c\u0430\u043a\u0441\u0438\u043c\u0443\u043c 80 \u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432)") + if len(name) > settings.qg0_title_max: + errors.append( + f"Title \u0441\u043b\u0438\u0448\u043a\u043e\u043c \u0434\u043b\u0438\u043d\u043d\u044b\u0439 " + f"(\u043c\u0430\u043a\u0441\u0438\u043c\u0443\u043c {settings.qg0_title_max} \u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432)" + ) if not description or len(description.strip()) < 20: errors.append("Description \u0441\u043b\u0438\u0448\u043a\u043e\u043c \u043a\u043e\u0440\u043e\u0442\u043a\u0438\u0439 (\u043d\u0443\u0436\u043d\u043e >= 20 \u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432)") diff --git a/tests/test_qg0_title_limit.py b/tests/test_qg0_title_limit.py new file mode 100644 index 0000000..44b2962 --- /dev/null +++ b/tests/test_qg0_title_limit.py @@ -0,0 +1,117 @@ +"""ORCH-069: unit tests for the configurable QG-0 title-length limit. + +Covers `_qg0_errors` (src/webhooks/plane.py) reading the upper title limit +dynamically from `settings.qg0_title_max` (env `ORCH_QG0_TITLE_MAX`, default 200), +plus the graceful env-degradation field-validator on `Settings`. + +The tests patch `src.config.settings.qg0_title_max` (the same object imported into +`src.webhooks.plane`) and assert boundary behaviour and error texts. For env-driven +cases a FRESH `Settings()` instance is created locally, since the module-level +singleton is built once on import. +""" + +import re + +import pytest + +from src.config import Settings, settings +from src.webhooks.plane import _qg0_errors + +VALID_DESCRIPTION = "x" * 30 # >= 20 chars, always passes the description check + + +def _title_length_error(errors): + """Return the title length-limit error string, or None if absent. + + The short-title error ('нужно >= 5') and the description error are excluded; + only the 'too long' title error is matched (it contains 'максимум'). + """ + for e in errors: + if "Title" in e and "максимум" in e: + return e + return None + + +# --- AC-1: default limit 200, boundary at 201 ------------------------------ + +def test_tc01_default_limit_200_boundary_pass(monkeypatch): + """TC-01: title of exactly 200 chars -> no title length error (PASS).""" + monkeypatch.setattr(settings, "qg0_title_max", 200) + errors = _qg0_errors("x" * 200, VALID_DESCRIPTION) + assert _title_length_error(errors) is None + + +def test_tc02_default_limit_200_boundary_fail(monkeypatch): + """TC-02: title of 201 chars -> length error mentioning '200'.""" + monkeypatch.setattr(settings, "qg0_title_max", 200) + errors = _qg0_errors("x" * 201, VALID_DESCRIPTION) + err = _title_length_error(errors) + assert err is not None + assert "200" in err + + +# --- AC-2: configurable limit 120, boundary at 121 ------------------------- + +def test_tc03_custom_limit_120_boundary_pass(monkeypatch): + """TC-03: with limit 120, a 120-char title passes.""" + monkeypatch.setattr(settings, "qg0_title_max", 120) + errors = _qg0_errors("x" * 120, VALID_DESCRIPTION) + assert _title_length_error(errors) is None + + +def test_tc04_custom_limit_120_boundary_fail(monkeypatch): + """TC-04: with limit 120, a 121-char title fails; text mentions 120 not 80.""" + monkeypatch.setattr(settings, "qg0_title_max", 120) + errors = _qg0_errors("x" * 121, VALID_DESCRIPTION) + err = _title_length_error(errors) + assert err is not None + assert "120" in err + assert "80" not in err + + +# --- AC-3: graceful handling of invalid/empty env -------------------------- + +def test_tc05_graceful_non_numeric_env(monkeypatch): + """TC-05: non-numeric env -> Settings() does not raise, limit == 200.""" + monkeypatch.setenv("ORCH_QG0_TITLE_MAX", "abc") + s = Settings() + assert s.qg0_title_max == 200 + + +def test_tc06_graceful_empty_env(monkeypatch): + """TC-06: empty-string env -> default 200, no exception.""" + monkeypatch.setenv("ORCH_QG0_TITLE_MAX", "") + s = Settings() + assert s.qg0_title_max == 200 + + +def test_tc07_valid_numeric_env(monkeypatch): + """TC-07: valid numeric env -> the given value is applied (positive path).""" + monkeypatch.setenv("ORCH_QG0_TITLE_MAX", "150") + s = Settings() + assert s.qg0_title_max == 150 + + +# --- AC-4: lower limits unchanged ------------------------------------------ + +def test_tc08_short_title_still_errors(monkeypatch): + """TC-08: title < 5 chars still raises the short-title error.""" + monkeypatch.setattr(settings, "qg0_title_max", 200) + errors = _qg0_errors("abc", VALID_DESCRIPTION) + assert any("Title" in e and "нужно >= 5" in e for e in errors) + + +def test_tc09_short_description_still_errors(monkeypatch): + """TC-09: description < 20 chars still raises the short-description error.""" + monkeypatch.setattr(settings, "qg0_title_max", 200) + errors = _qg0_errors("Valid title", "short") + assert any("Description" in e for e in errors) + + +# --- AC-7: backward compatibility ------------------------------------------ + +def test_tc10_backward_compat_titles_81_to_200(monkeypatch): + """TC-10: a title previously rejected by the 80-char cap now passes at 200.""" + monkeypatch.setattr(settings, "qg0_title_max", 200) + errors = _qg0_errors("x" * 100, VALID_DESCRIPTION) + assert _title_length_error(errors) is None From c56672aabfb2c9a6f8f741cfd5d3c6fb7b388361 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Mon, 8 Jun 2026 11:14:07 +0000 Subject: [PATCH 05/12] reviewer(ET): auto-commit from reviewer run_id=371 --- docs/work-items/ORCH-069/12-review.md | 71 +++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 docs/work-items/ORCH-069/12-review.md diff --git a/docs/work-items/ORCH-069/12-review.md b/docs/work-items/ORCH-069/12-review.md new file mode 100644 index 0000000..efa95f6 --- /dev/null +++ b/docs/work-items/ORCH-069/12-review.md @@ -0,0 +1,71 @@ +--- +type: review +work_item_id: ORCH-069 +verdict: REQUEST_CHANGES +version: 1 +--- + +# Review ORCH-069 + +## Summary +Реализация QG-0 title-лимита как конфигурируемого параметра `ORCH_QG0_TITLE_MAX` +(дефолт 200) выполнена качественно и **по ТЗ/ADR дословно**: поле +`Settings.qg0_title_max`, graceful `field_validator(mode="before")` (never-raise → +дефолт 200), динамическое чтение `settings.qg0_title_max` в `_qg0_errors` с +f-string-текстом ошибки. Код изолирован (затронуты только `src/config.py` и +`src/webhooks/plane.py`), инварианты не нарушены, нижние лимиты сохранены. Полный +тест-сет зелёный: `pytest tests/ -q` → **757 passed** (включая 10 новых). + +Единственная причина REQUEST_CHANGES — **неполная документация**: при наличии в +`README.md` таблицы конфигов «Все переменные с префиксом `ORCH_`» новый параметр +`ORCH_QG0_TITLE_MAX` в неё не внесён. Это прямо требуется AC-6 и ТЗ §9. + +## Соответствие ТЗ / ADR +- `src/config.py` — поле `qg0_title_max: int = 200` + валидатор `_qg0_title_max_default` + (mode="before", try/except → 200): 1:1 с ADR Р-1/Р-2 и ТЗ §2/§5. ✓ +- `src/webhooks/plane.py:362` — хардкод `> 80` заменён на `> settings.qg0_title_max`, + текст ошибки динамический; сигнатура `_qg0_errors`, нижний лимит `< 5`, проверка + description `< 20` не тронуты: ADR Р-3, ТЗ §3/§4. ✓ +- Граница строгая (`len == limit` PASS, `limit+1` FAIL) — подтверждена tc01–tc04. ✓ +- Инварианты (AC-8): `STAGE_TRANSITIONS`, `QG_CHECKS`, схема БД, slug `[:30]`, + soft-QG-0, API — НЕ изменены (diff = только 2 файла src). ✓ + +## Acceptance criteria +- AC-1 (дефолт 200, граница 201, текст упоминает 200) — tc01/tc02 ✓ +- AC-2 (лимит 120, граница 121, текст 120 не 80) — tc03/tc04 ✓ +- AC-3 (graceful пустое/`abc` → 200 без краха) — tc05/tc06/tc07 + валидатор ✓ +- AC-4 (нижние лимиты title<5 / desc<20) — tc08/tc09 ✓ +- AC-5 (pytest зелёный) — 757 passed ✓ +- AC-6 (документация в том же PR) — **частично: README-таблица не обновлена** ✗ (см. P1) +- AC-7 (обратная совместимость, ≤80 проходит) — tc10 ✓ +- AC-8 (изоляция изменений) — ✓ + +## Findings + +### P0 — Blocker +- (нет) + +### P1 — Must fix +- [ ] **AC-6 / ТЗ §9 не выполнен полностью: README-таблица конфигов не обновлена.** + В `README.md` (строки 105–138) есть таблица «Все переменные с префиксом `ORCH_`», + куда внесены даже свежие переменные (ORCH-053/ORCH-060 reconcile). Новый + пользовательский параметр `ORCH_QG0_TITLE_MAX` в ней отсутствует — таблица стала + неконсистентна собственному заголовку («все переменные»). AC-6 явно требует: + «при наличии релевантной таблицы конфигов в README / CLAUDE.md — она обновлена… + FAIL если какой-либо из обязательных файлов документации не обновлён (reviewer → + REQUEST_CHANGES)». **Действие:** добавить строку в таблицу `README.md`, напр.: + `| `ORCH_QG0_TITLE_MAX` | Верхний лимит длины заголовка QG-0 (вход `_qg0_errors`); невалидное/пустое → дефолт (ORCH-069) | `200` |`. + +### P2 — Should fix +- (нет) + +## Документация +- `.env.example` — добавлен `ORCH_QG0_TITLE_MAX=200` с комментарием. ✓ +- `.env.staging.example` — добавлен `ORCH_QG0_TITLE_MAX=200`. ✓ +- `CHANGELOG.md` — подробная запись об ORCH-069 (Added). ✓ +- ADR `06-adr/ADR-001-configurable-qg0-title-limit.md` — присутствует, согласован с кодом. ✓ +- **`README.md` таблица env-конфигов — НЕ обновлена** (новый `ORCH_QG0_TITLE_MAX` + отсутствует) → блокирующее замечание P1, нужно дополнить в этом же PR. +- `docs/architecture/README.md` / `CLAUDE.md` — обновления не требуют (QG-0 — inline + soft/hard-валидация входа, не зарегистрированный stage-gate; API/стадии/QG-реестр + не менялись). ОК. From 0b9ae514c92896eadd9edd10dcb9e23bef7ffa92 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Mon, 8 Jun 2026 11:15:04 +0000 Subject: [PATCH 06/12] docs(qg0): add ORCH_QG0_TITLE_MAX to README config table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewer P1 fix (attempt 2/3): новый параметр отсутствовал в таблице «Все переменные с префиксом ORCH_», делая её неконсистентной заголовку. Закрывает AC-6 / ТЗ §9. Refs: ORCH-069 Co-Authored-By: Claude Opus 4.7 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e7de2cd..4036e94 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,7 @@ uvicorn src.main:app --reload --port 8500 | `ORCH_RECONCILE_GRACE_OVERRIDES_JSON` | Per-stage пороги, напр. `{"development":300}` | `""` | | `ORCH_RECONCILE_NOTIFY_UNBLOCK` | Telegram при разблокировке застрявшей задачи | `true` | | `ORCH_RECONCILE_SKIP_BLOCKED_ENABLED` | F-1 Guard 2 (ORCH-060): пропуск задач в Plane-статусе Blocked / Needs Input; `false` глушит только сетевой Guard 2 (Guard 1 escalated всегда активен) | `true` | +| `ORCH_QG0_TITLE_MAX` | Верхний лимит длины заголовка QG-0 (вход `_qg0_errors`); невалидное/пустое значение → дефолт (ORCH-069) | `200` | ## Очередь задач (ORCH-1 / F-2b) From 37ef58f21fee8e1731c0c16c449766ed34574fe0 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Mon, 8 Jun 2026 11:17:30 +0000 Subject: [PATCH 07/12] reviewer(ET): auto-commit from reviewer run_id=373 --- docs/work-items/ORCH-069/12-review.md | 52 ++++++++++++--------------- 1 file changed, 23 insertions(+), 29 deletions(-) diff --git a/docs/work-items/ORCH-069/12-review.md b/docs/work-items/ORCH-069/12-review.md index efa95f6..6e00470 100644 --- a/docs/work-items/ORCH-069/12-review.md +++ b/docs/work-items/ORCH-069/12-review.md @@ -1,34 +1,35 @@ --- type: review work_item_id: ORCH-069 -verdict: REQUEST_CHANGES -version: 1 +verdict: APPROVED +version: 2 --- # Review ORCH-069 ## Summary -Реализация QG-0 title-лимита как конфигурируемого параметра `ORCH_QG0_TITLE_MAX` -(дефолт 200) выполнена качественно и **по ТЗ/ADR дословно**: поле -`Settings.qg0_title_max`, graceful `field_validator(mode="before")` (never-raise → -дефолт 200), динамическое чтение `settings.qg0_title_max` в `_qg0_errors` с -f-string-текстом ошибки. Код изолирован (затронуты только `src/config.py` и -`src/webhooks/plane.py`), инварианты не нарушены, нижние лимиты сохранены. Полный -тест-сет зелёный: `pytest tests/ -q` → **757 passed** (включая 10 новых). +Реализация конфигурируемого QG-0 title-лимита `ORCH_QG0_TITLE_MAX` (дефолт 200) +выполнена **дословно по ТЗ/ADR** и качественно: поле `Settings.qg0_title_max`, +graceful `field_validator(mode="before")` (never-raise → дефолт 200), динамическое +чтение `settings.qg0_title_max` в `_qg0_errors` с f-string-текстом ошибки. Код +изолирован (затронуты только `src/config.py` и `src/webhooks/plane.py`), инварианты +не нарушены, нижние лимиты сохранены. Полный тест-сет зелёный: `pytest tests/ -q` +→ **757 passed** (включая 10 новых тестов ORCH-069). -Единственная причина REQUEST_CHANGES — **неполная документация**: при наличии в -`README.md` таблицы конфигов «Все переменные с префиксом `ORCH_`» новый параметр -`ORCH_QG0_TITLE_MAX` в неё не внесён. Это прямо требуется AC-6 и ТЗ §9. +Единственное замечание прошлой ревизии (v1) — отсутствие строки `ORCH_QG0_TITLE_MAX` +в README-таблице конфигов — **устранено** коммитом `bce287e` (строка добавлена +в таблицу «Все переменные с префиксом `ORCH_`»). Блокирующих и must-fix findings +не осталось → **APPROVED**. ## Соответствие ТЗ / ADR - `src/config.py` — поле `qg0_title_max: int = 200` + валидатор `_qg0_title_max_default` - (mode="before", try/except → 200): 1:1 с ADR Р-1/Р-2 и ТЗ §2/§5. ✓ + (`mode="before"`, try/except → 200): 1:1 с ADR Р-1/Р-2 и ТЗ §2/§5. ✓ - `src/webhooks/plane.py:362` — хардкод `> 80` заменён на `> settings.qg0_title_max`, - текст ошибки динамический; сигнатура `_qg0_errors`, нижний лимит `< 5`, проверка - description `< 20` не тронуты: ADR Р-3, ТЗ §3/§4. ✓ + текст ошибки динамический (f-string с актуальным числом); сигнатура `_qg0_errors`, + нижний лимит `< 5`, проверка description `< 20` не тронуты: ADR Р-3, ТЗ §3/§4. ✓ - Граница строгая (`len == limit` PASS, `limit+1` FAIL) — подтверждена tc01–tc04. ✓ - Инварианты (AC-8): `STAGE_TRANSITIONS`, `QG_CHECKS`, схема БД, slug `[:30]`, - soft-QG-0, API — НЕ изменены (diff = только 2 файла src). ✓ + soft-QG-0, API — НЕ изменены (diff src = только 2 файла). ✓ ## Acceptance criteria - AC-1 (дефолт 200, граница 201, текст упоминает 200) — tc01/tc02 ✓ @@ -36,7 +37,7 @@ f-string-текстом ошибки. Код изолирован (затрон - AC-3 (graceful пустое/`abc` → 200 без краха) — tc05/tc06/tc07 + валидатор ✓ - AC-4 (нижние лимиты title<5 / desc<20) — tc08/tc09 ✓ - AC-5 (pytest зелёный) — 757 passed ✓ -- AC-6 (документация в том же PR) — **частично: README-таблица не обновлена** ✗ (см. P1) +- AC-6 (документация в том же PR) — **выполнен полностью** ✓ (README-таблица дополнена) - AC-7 (обратная совместимость, ≤80 проходит) — tc10 ✓ - AC-8 (изоляция изменений) — ✓ @@ -46,15 +47,7 @@ f-string-текстом ошибки. Код изолирован (затрон - (нет) ### P1 — Must fix -- [ ] **AC-6 / ТЗ §9 не выполнен полностью: README-таблица конфигов не обновлена.** - В `README.md` (строки 105–138) есть таблица «Все переменные с префиксом `ORCH_`», - куда внесены даже свежие переменные (ORCH-053/ORCH-060 reconcile). Новый - пользовательский параметр `ORCH_QG0_TITLE_MAX` в ней отсутствует — таблица стала - неконсистентна собственному заголовку («все переменные»). AC-6 явно требует: - «при наличии релевантной таблицы конфигов в README / CLAUDE.md — она обновлена… - FAIL если какой-либо из обязательных файлов документации не обновлён (reviewer → - REQUEST_CHANGES)». **Действие:** добавить строку в таблицу `README.md`, напр.: - `| `ORCH_QG0_TITLE_MAX` | Верхний лимит длины заголовка QG-0 (вход `_qg0_errors`); невалидное/пустое → дефолт (ORCH-069) | `200` |`. +- (нет) ### P2 — Should fix - (нет) @@ -63,9 +56,10 @@ f-string-текстом ошибки. Код изолирован (затрон - `.env.example` — добавлен `ORCH_QG0_TITLE_MAX=200` с комментарием. ✓ - `.env.staging.example` — добавлен `ORCH_QG0_TITLE_MAX=200`. ✓ - `CHANGELOG.md` — подробная запись об ORCH-069 (Added). ✓ -- ADR `06-adr/ADR-001-configurable-qg0-title-limit.md` — присутствует, согласован с кодом. ✓ -- **`README.md` таблица env-конфигов — НЕ обновлена** (новый `ORCH_QG0_TITLE_MAX` - отсутствует) → блокирующее замечание P1, нужно дополнить в этом же PR. +- `README.md` — таблица env-конфигов дополнена строкой `ORCH_QG0_TITLE_MAX` + (коммит `bce287e`). ✓ +- ADR `06-adr/ADR-001-configurable-qg0-title-limit.md` — присутствует, согласован + с кодом. ✓ - `docs/architecture/README.md` / `CLAUDE.md` — обновления не требуют (QG-0 — inline soft/hard-валидация входа, не зарегистрированный stage-gate; API/стадии/QG-реестр не менялись). ОК. From 18f887c8869301b8930ca54a8dd1660e934bb357 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Mon, 8 Jun 2026 11:18:50 +0000 Subject: [PATCH 08/12] tester(ET): auto-commit from tester run_id=374 --- docs/work-items/ORCH-069/13-test-report.md | 80 ++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 docs/work-items/ORCH-069/13-test-report.md diff --git a/docs/work-items/ORCH-069/13-test-report.md b/docs/work-items/ORCH-069/13-test-report.md new file mode 100644 index 0000000..9919513 --- /dev/null +++ b/docs/work-items/ORCH-069/13-test-report.md @@ -0,0 +1,80 @@ +--- +type: test-report +work_item_id: ORCH-069 +result: PASS +--- + +# Test Report — ORCH-069 + +QG-0 title-лимит → конфигурируемый параметр `ORCH_QG0_TITLE_MAX` (дефолт 200). + +## Окружение +- Python: 3.12.13 +- pytest: 8.3.3 +- Ветка: `feature/ORCH-069-qg-0-title-orch-qg0-title-max-` +- Дата: 2026-06-08 + +## Smoke test API (prod, read-only) +| Endpoint | Результат | +|----------|-----------| +| `GET /health` | PASS — `{"status":"ok","service":"orchestrator"}` | +| `GET /status` | PASS — отдаёт активные задачи (ORCH-069 виден, stage=testing) | +| `GET /queue` | PASS — counts/resilience/reconcile/reaper/post_deploy в норме (breaker closed, preflight_ok) | + +## Результаты (по test-plan 04-test-plan.yaml) + +| TC ID | Описание | Покрывает | Результат | +|-------|----------|-----------|-----------| +| TC-01 | Дефолт 200: заголовок 200 символов → нет ошибки длины (граница PASS) | AC-1 | PASS | +| TC-02 | Дефолт 200: заголовок 201 → ошибка длины, текст упоминает «200» | AC-1 | PASS | +| TC-03 | Лимит 120: заголовок 120 → нет ошибки длины | AC-2 | PASS | +| TC-04 | Лимит 120: заголовок 121 → ошибка длины, текст «120» (не «80») | AC-2 | PASS | +| TC-05 | Graceful: `ORCH_QG0_TITLE_MAX=abc` → Settings() не падает, лимит 200 | AC-3 | PASS | +| TC-06 | Graceful: пустая строка env → дефолт 200 без исключения | AC-3 | PASS | +| TC-07 | Корректное env `150` → применяется значение 150 (sanity) | AC-2, AC-3 | PASS | +| TC-08 | Нижний лимит title не сломан: < 5 символов → «Title слишком короткий» | AC-4 | PASS | +| TC-09 | Лимит description не сломан: < 20 символов → «Description слишком короткий» | AC-4 | PASS | +| TC-10 | Обратная совместимость: заголовок 81–200 проходит при дефолте | AC-7 | PASS | +| TC-11 | Полный набор тестов зелёный (регрессия не внесена) | AC-5 | PASS | + +## Сопоставление с критериями приёмки (03-acceptance-criteria.md) +- AC-1 (дефолт 200, граница 201, текст «200») — TC-01/TC-02 — PASS +- AC-2 (лимит 120, граница 121, текст «120») — TC-03/TC-04/TC-07 — PASS +- AC-3 (graceful пустое/нечисловое → 200 без краха) — TC-05/TC-06/TC-07 — PASS +- AC-4 (нижние лимиты title<5 / desc<20) — TC-08/TC-09 — PASS +- AC-5 (pytest зелёный) — TC-11 (757 passed) — PASS +- AC-6 (документация в том же PR) — подтверждено reviewer (`12-review.md` APPROVED) — PASS +- AC-7 (обратная совместимость ≤80) — TC-10 — PASS +- AC-8 (изоляция изменений: STAGE_TRANSITIONS/QG_CHECKS/БД/slug не тронуты) — подтверждено reviewer — PASS + +## Вывод pytest + +Целевой набор ORCH-069 (`tests/test_qg0_title_limit.py`): +``` +collected 10 items + +tests/test_qg0_title_limit.py::test_tc01_default_limit_200_boundary_pass PASSED [ 10%] +tests/test_qg0_title_limit.py::test_tc02_default_limit_200_boundary_fail PASSED [ 20%] +tests/test_qg0_title_limit.py::test_tc03_custom_limit_120_boundary_pass PASSED [ 30%] +tests/test_qg0_title_limit.py::test_tc04_custom_limit_120_boundary_fail PASSED [ 40%] +tests/test_qg0_title_limit.py::test_tc05_graceful_non_numeric_env PASSED [ 50%] +tests/test_qg0_title_limit.py::test_tc06_graceful_empty_env PASSED [ 60%] +tests/test_qg0_title_limit.py::test_tc07_valid_numeric_env PASSED [ 70%] +tests/test_qg0_title_limit.py::test_tc08_short_title_still_errors PASSED [ 80%] +tests/test_qg0_title_limit.py::test_tc09_short_description_still_errors PASSED [ 90%] +tests/test_qg0_title_limit.py::test_tc10_backward_compat_titles_81_to_200 PASSED [100%] + +======================== 10 passed, 1 warning in 0.32s ========================= +``` + +Полный регресс (`pytest tests/ -q`): +``` +757 passed, 1 warning in 15.37s +``` + +(Единственный warning — PydanticDeprecatedSince20 о class-based config в `src/config.py`, +предсуществующий, не связан с ORCH-069, на результат не влияет.) + +## Итог +**PASS** — все 11 тест-кейсов зелёные (757 passed в полном наборе), smoke API в норме, +все критерии приёмки AC-1…AC-8 покрыты. Задача готова к стадии deploy-staging. From 2e84813c13f9165c0795c801a963d3dc33caad00 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Mon, 8 Jun 2026 11:25:09 +0000 Subject: [PATCH 09/12] developer(ET): auto-commit from developer run_id=376 --- .../work-items/ORCH-069/17-security-report.md | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 docs/work-items/ORCH-069/17-security-report.md diff --git a/docs/work-items/ORCH-069/17-security-report.md b/docs/work-items/ORCH-069/17-security-report.md new file mode 100644 index 0000000..243c333 --- /dev/null +++ b/docs/work-items/ORCH-069/17-security-report.md @@ -0,0 +1,25 @@ +--- +security_status: PASS +secrets_found: 0 +deps_blocking: 0 +deps_warning: 4 +deps_audit_degraded: false +--- +# Security Report — ORCH-069 + +Детерминированный security-гейт (ORCH-022): secret-scanning (gitleaks, offline) + dependency audit (pip-audit). Машинный вердикт читается ТОЛЬКО из frontmatter выше. + +## Verdict +clean: 0 secrets, 0 blocking CVE(s) + +## Secrets +- None + +## Dependencies (blocking) +- None + +## Dependencies (warning) +- `pytest==8.3.3` — GHSA-6w46-j5rx-g56g severity=UNKNOWN fix=9.0.3 +- `starlette==0.38.6` — PYSEC-2026-161 severity=UNKNOWN fix=1.0.1 +- `starlette==0.38.6` — GHSA-f96h-pmfr-66vw severity=UNKNOWN fix=0.40.0 +- `starlette==0.38.6` — GHSA-2c2j-9gv5-cj73 severity=UNKNOWN fix=0.47.2 From e1055861b5263b6e48526b6b2d21d2662d76c38d Mon Sep 17 00:00:00 2001 From: claude-bot Date: Mon, 8 Jun 2026 11:28:16 +0000 Subject: [PATCH 10/12] reviewer(ET): auto-commit from reviewer run_id=377 --- docs/work-items/ORCH-069/12-review.md | 45 ++++++++++++++------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/docs/work-items/ORCH-069/12-review.md b/docs/work-items/ORCH-069/12-review.md index 6e00470..ecd719b 100644 --- a/docs/work-items/ORCH-069/12-review.md +++ b/docs/work-items/ORCH-069/12-review.md @@ -2,43 +2,41 @@ type: review work_item_id: ORCH-069 verdict: APPROVED -version: 2 +version: 3 --- # Review ORCH-069 ## Summary Реализация конфигурируемого QG-0 title-лимита `ORCH_QG0_TITLE_MAX` (дефолт 200) -выполнена **дословно по ТЗ/ADR** и качественно: поле `Settings.qg0_title_max`, +выполнена **дословно по ТЗ/ADR** и качественно. Поле `Settings.qg0_title_max`, graceful `field_validator(mode="before")` (never-raise → дефолт 200), динамическое чтение `settings.qg0_title_max` в `_qg0_errors` с f-string-текстом ошибки. Код изолирован (затронуты только `src/config.py` и `src/webhooks/plane.py`), инварианты -не нарушены, нижние лимиты сохранены. Полный тест-сет зелёный: `pytest tests/ -q` -→ **757 passed** (включая 10 новых тестов ORCH-069). - -Единственное замечание прошлой ревизии (v1) — отсутствие строки `ORCH_QG0_TITLE_MAX` -в README-таблице конфигов — **устранено** коммитом `bce287e` (строка добавлена -в таблицу «Все переменные с префиксом `ORCH_`»). Блокирующих и must-fix findings -не осталось → **APPROVED**. +не нарушены, нижние лимиты сохранены. Свежий полный прогон на текущем состоянии +ветки: `pytest tests/ -q` → **863 passed** (включая 10 новых тестов ORCH-069, +файл `tests/test_qg0_title_limit.py`, все зелёные). Документация обновлена в том же +PR полностью. Блокирующих и must-fix findings нет → **APPROVED**. ## Соответствие ТЗ / ADR - `src/config.py` — поле `qg0_title_max: int = 200` + валидатор `_qg0_title_max_default` - (`mode="before"`, try/except → 200): 1:1 с ADR Р-1/Р-2 и ТЗ §2/§5. ✓ -- `src/webhooks/plane.py:362` — хардкод `> 80` заменён на `> settings.qg0_title_max`, + (`mode="before"`, try/except → 200 при `None`/пустой/нечисловой): 1:1 с ADR Р-1/Р-2 + и ТЗ §2/§5. ✓ +- `src/webhooks/plane.py` — хардкод `> 80` заменён на `> settings.qg0_title_max`, текст ошибки динамический (f-string с актуальным числом); сигнатура `_qg0_errors`, - нижний лимит `< 5`, проверка description `< 20` не тронуты: ADR Р-3, ТЗ §3/§4. ✓ + нижний лимит title `< 5`, проверка description `< 20` не тронуты: ADR Р-3, ТЗ §3/§4. ✓ - Граница строгая (`len == limit` PASS, `limit+1` FAIL) — подтверждена tc01–tc04. ✓ -- Инварианты (AC-8): `STAGE_TRANSITIONS`, `QG_CHECKS`, схема БД, slug `[:30]`, - soft-QG-0, API — НЕ изменены (diff src = только 2 файла). ✓ +- Инварианты (ADR Р-4 / AC-8): `STAGE_TRANSITIONS`, `QG_CHECKS`, схема БД, slug `[:30]`, + soft-QG-0, API — НЕ изменены (diff `src/` = только 2 файла). ✓ ## Acceptance criteria - AC-1 (дефолт 200, граница 201, текст упоминает 200) — tc01/tc02 ✓ - AC-2 (лимит 120, граница 121, текст 120 не 80) — tc03/tc04 ✓ -- AC-3 (graceful пустое/`abc` → 200 без краха) — tc05/tc06/tc07 + валидатор ✓ +- AC-3 (graceful пустое/`abc` → 200 без краха) — tc05/tc06 + позитив tc07 + валидатор ✓ - AC-4 (нижние лимиты title<5 / desc<20) — tc08/tc09 ✓ -- AC-5 (pytest зелёный) — 757 passed ✓ -- AC-6 (документация в том же PR) — **выполнен полностью** ✓ (README-таблица дополнена) -- AC-7 (обратная совместимость, ≤80 проходит) — tc10 ✓ +- AC-5 (pytest зелёный) — 863 passed ✓ +- AC-6 (документация в том же PR) — выполнен полностью ✓ +- AC-7 (обратная совместимость, ≤80 проходит при 200) — tc10 ✓ - AC-8 (изоляция изменений) — ✓ ## Findings @@ -52,12 +50,17 @@ graceful `field_validator(mode="before")` (never-raise → дефолт 200), д ### P2 — Should fix - (нет) +### P3 — Nice-to-have (не блокирует) +- В конце `06-adr/ADR-001-configurable-qg0-title-limit.md` присутствуют артефактные + хвостовые теги (``, ``). Косметика в артефакте стадии architecture; + на корректность кода/контракта не влияет. Править артефакт чужой стадии в рамках + ревью не уполномочен — отмечено для будущей чистки. + ## Документация - `.env.example` — добавлен `ORCH_QG0_TITLE_MAX=200` с комментарием. ✓ - `.env.staging.example` — добавлен `ORCH_QG0_TITLE_MAX=200`. ✓ -- `CHANGELOG.md` — подробная запись об ORCH-069 (Added). ✓ -- `README.md` — таблица env-конфигов дополнена строкой `ORCH_QG0_TITLE_MAX` - (коммит `bce287e`). ✓ +- `CHANGELOG.md` — подробная запись об ORCH-069 (раздел Added). ✓ +- `README.md` — таблица env-конфигов дополнена строкой `ORCH_QG0_TITLE_MAX`. ✓ - ADR `06-adr/ADR-001-configurable-qg0-title-limit.md` — присутствует, согласован с кодом. ✓ - `docs/architecture/README.md` / `CLAUDE.md` — обновления не требуют (QG-0 — inline From de70ee811d038a01d16ff11d97fa68ba2d6392af Mon Sep 17 00:00:00 2001 From: claude-bot Date: Mon, 8 Jun 2026 11:30:17 +0000 Subject: [PATCH 11/12] tester(ET): auto-commit from tester run_id=378 --- docs/work-items/ORCH-069/13-test-report.md | 112 ++++++++++++--------- 1 file changed, 65 insertions(+), 47 deletions(-) diff --git a/docs/work-items/ORCH-069/13-test-report.md b/docs/work-items/ORCH-069/13-test-report.md index 9919513..f440e91 100644 --- a/docs/work-items/ORCH-069/13-test-report.md +++ b/docs/work-items/ORCH-069/13-test-report.md @@ -6,75 +6,93 @@ result: PASS # Test Report — ORCH-069 -QG-0 title-лимит → конфигурируемый параметр `ORCH_QG0_TITLE_MAX` (дефолт 200). +QG-0 title-лимит → параметр `ORCH_QG0_TITLE_MAX` (дефолт 200) ## Окружение - Python: 3.12.13 -- pytest: 8.3.3 +- pytest: 8.3.3 (plugins: anyio-4.13.0, asyncio-0.23.8; asyncio mode=auto) - Ветка: `feature/ORCH-069-qg-0-title-orch-qg0-title-max-` +- Worktree: `/repos/_wt/orchestrator/feature_ORCH-069-qg-0-title-orch-qg0-title-max-` +- Prod-health (8500): `{"status":"ok","service":"orchestrator"}` — не трогался (self-hosting safety) - Дата: 2026-06-08 -## Smoke test API (prod, read-only) -| Endpoint | Результат | -|----------|-----------| -| `GET /health` | PASS — `{"status":"ok","service":"orchestrator"}` | -| `GET /status` | PASS — отдаёт активные задачи (ORCH-069 виден, stage=testing) | -| `GET /queue` | PASS — counts/resilience/reconcile/reaper/post_deploy в норме (breaker closed, preflight_ok) | +## Предусловия +- Review-вердикт `12-review.md`: **APPROVED** (version 3) ✓ +- Изменения изолированы: `src/config.py`, `src/webhooks/plane.py` (+ тесты, + документация) -## Результаты (по test-plan 04-test-plan.yaml) +## Результаты по тест-плану (04-test-plan.yaml) | TC ID | Описание | Покрывает | Результат | |-------|----------|-----------|-----------| -| TC-01 | Дефолт 200: заголовок 200 символов → нет ошибки длины (граница PASS) | AC-1 | PASS | -| TC-02 | Дефолт 200: заголовок 201 → ошибка длины, текст упоминает «200» | AC-1 | PASS | -| TC-03 | Лимит 120: заголовок 120 → нет ошибки длины | AC-2 | PASS | -| TC-04 | Лимит 120: заголовок 121 → ошибка длины, текст «120» (не «80») | AC-2 | PASS | -| TC-05 | Graceful: `ORCH_QG0_TITLE_MAX=abc` → Settings() не падает, лимит 200 | AC-3 | PASS | -| TC-06 | Graceful: пустая строка env → дефолт 200 без исключения | AC-3 | PASS | -| TC-07 | Корректное env `150` → применяется значение 150 (sanity) | AC-2, AC-3 | PASS | -| TC-08 | Нижний лимит title не сломан: < 5 символов → «Title слишком короткий» | AC-4 | PASS | -| TC-09 | Лимит description не сломан: < 20 символов → «Description слишком короткий» | AC-4 | PASS | -| TC-10 | Обратная совместимость: заголовок 81–200 проходит при дефолте | AC-7 | PASS | -| TC-11 | Полный набор тестов зелёный (регрессия не внесена) | AC-5 | PASS | +| TC-01 | Дефолт 200: title=200 → нет ошибки длины (граница PASS) | AC-1 | PASS | +| TC-02 | Дефолт 200: title=201 → ошибка длины, текст упоминает «200» | AC-1 | PASS | +| TC-03 | Лимит 120: title=120 → нет ошибки длины | AC-2 | PASS | +| TC-04 | Лимит 120: title=121 → ошибка, текст «120» (не «80») | AC-2 | PASS | +| TC-05 | Graceful: env `abc` → дефолт 200, без краха `Settings()` | AC-3 | PASS | +| TC-06 | Graceful: пустой env `""` → дефолт 200, без исключения | AC-3 | PASS | +| TC-07 | Валидный env `150` → применяется 150 (позитивный путь) | AC-2, AC-3 | PASS | +| TC-08 | Нижний лимит title < 5 не сломан | AC-4 | PASS | +| TC-09 | Лимит description < 20 не сломан | AC-4 | PASS | +| TC-10 | Обратная совместимость: title 81–200 проходит при дефолте | AC-7 | PASS | +| TC-11 | Полный набор тестов зелёный (нет регрессии) | AC-5 | PASS | ## Сопоставление с критериями приёмки (03-acceptance-criteria.md) -- AC-1 (дефолт 200, граница 201, текст «200») — TC-01/TC-02 — PASS -- AC-2 (лимит 120, граница 121, текст «120») — TC-03/TC-04/TC-07 — PASS -- AC-3 (graceful пустое/нечисловое → 200 без краха) — TC-05/TC-06/TC-07 — PASS -- AC-4 (нижние лимиты title<5 / desc<20) — TC-08/TC-09 — PASS -- AC-5 (pytest зелёный) — TC-11 (757 passed) — PASS -- AC-6 (документация в том же PR) — подтверждено reviewer (`12-review.md` APPROVED) — PASS -- AC-7 (обратная совместимость ≤80) — TC-10 — PASS -- AC-8 (изоляция изменений: STAGE_TRANSITIONS/QG_CHECKS/БД/slug не тронуты) — подтверждено reviewer — PASS -## Вывод pytest +| AC | Критерий | Статус | +|----|----------|--------| +| AC-1 | Дефолт 200, граница на 201, текст упоминает 200 | PASS (TC-01/02) | +| AC-2 | Настраиваемый лимит 120, граница 121, текст 120 | PASS (TC-03/04/07) | +| AC-3 | Graceful при пустом/нечисловом значении → 200 | PASS (TC-05/06) | +| AC-4 | Нижние лимиты title<5 / description<20 не сломаны | PASS (TC-08/09) | +| AC-5 | Юнит-тесты зелёные (весь набор) | PASS (863 passed) | +| AC-6 | Документация в том же PR (.env.example, .env.staging.example, CHANGELOG, README) | PASS (подтверждено review) | +| AC-7 | Обратная совместимость (≤80 проходит при 200) | PASS (TC-10) | +| AC-8 | Изоляция: slug `[:30]`, БД, STAGE_TRANSITIONS/QG_CHECKS, handle_* не тронуты | PASS (diff = 2 файла src/) | -Целевой набор ORCH-069 (`tests/test_qg0_title_limit.py`): +## Smoke test API (prod 8500, read-only) +- `GET /health` → `{"status":"ok","service":"orchestrator"}` — OK +- `GET /status` → отдаёт активные задачи (ORCH-069 в стадии `testing`) — OK +- `GET /queue` → `counts: queued=0 running=1 done=459 failed=4 cancelled=1`; breaker `closed`, preflight ok — OK + +## Целевой прогон ORCH-069 (tests/test_qg0_title_limit.py) ``` collected 10 items -tests/test_qg0_title_limit.py::test_tc01_default_limit_200_boundary_pass PASSED [ 10%] -tests/test_qg0_title_limit.py::test_tc02_default_limit_200_boundary_fail PASSED [ 20%] -tests/test_qg0_title_limit.py::test_tc03_custom_limit_120_boundary_pass PASSED [ 30%] -tests/test_qg0_title_limit.py::test_tc04_custom_limit_120_boundary_fail PASSED [ 40%] -tests/test_qg0_title_limit.py::test_tc05_graceful_non_numeric_env PASSED [ 50%] -tests/test_qg0_title_limit.py::test_tc06_graceful_empty_env PASSED [ 60%] -tests/test_qg0_title_limit.py::test_tc07_valid_numeric_env PASSED [ 70%] -tests/test_qg0_title_limit.py::test_tc08_short_title_still_errors PASSED [ 80%] -tests/test_qg0_title_limit.py::test_tc09_short_description_still_errors PASSED [ 90%] -tests/test_qg0_title_limit.py::test_tc10_backward_compat_titles_81_to_200 PASSED [100%] +tests/test_qg0_title_limit.py::test_tc01_default_limit_200_boundary_pass PASSED +tests/test_qg0_title_limit.py::test_tc02_default_limit_200_boundary_fail PASSED +tests/test_qg0_title_limit.py::test_tc03_custom_limit_120_boundary_pass PASSED +tests/test_qg0_title_limit.py::test_tc04_custom_limit_120_boundary_fail PASSED +tests/test_qg0_title_limit.py::test_tc05_graceful_non_numeric_env PASSED +tests/test_qg0_title_limit.py::test_tc06_graceful_empty_env PASSED +tests/test_qg0_title_limit.py::test_tc07_valid_numeric_env PASSED +tests/test_qg0_title_limit.py::test_tc08_short_title_still_errors PASSED +tests/test_qg0_title_limit.py::test_tc09_short_description_still_errors PASSED +tests/test_qg0_title_limit.py::test_tc10_backward_compat_titles_81_to_200 PASSED -======================== 10 passed, 1 warning in 0.32s ========================= +======================== 10 passed, 1 warning in 0.31s ========================= ``` -Полный регресс (`pytest tests/ -q`): +## Полный прогон (pytest tests/ -q) ``` -757 passed, 1 warning in 15.37s +........................................................................ [ 8%] +........................................................................ [ 16%] +........................................................................ [ 25%] +........................................................................ [ 33%] +........................................................................ [ 41%] +........................................................................ [ 50%] +........................................................................ [ 58%] +........................................................................ [ 66%] +........................................................................ [ 75%] +........................................................................ [ 83%] +........................................................................ [ 91%] +....................................................................... [100%] +863 passed, 1 warning in 21.49s ``` -(Единственный warning — PydanticDeprecatedSince20 о class-based config в `src/config.py`, -предсуществующий, не связан с ORCH-069, на результат не влияет.) +(Единственный warning — PydanticDeprecatedSince20 в `src/config.py:5`, существующий +class-based config; к ORCH-069 не относится, не является ошибкой.) ## Итог -**PASS** — все 11 тест-кейсов зелёные (757 passed в полном наборе), smoke API в норме, -все критерии приёмки AC-1…AC-8 покрыты. Задача готова к стадии deploy-staging. +**PASS** — все 11 TC из тест-плана пройдены, все 8 критериев приёмки выполнены, +полный регресс зелёный (863 passed), smoke-тесты API OK. Регрессии не внесены. +Задача готова к переходу на стадию `deploy-staging`. From d33e0ded2e82943811cfa6c1ad855f021c4269d9 Mon Sep 17 00:00:00 2001 From: deploy-finalizer Date: Mon, 8 Jun 2026 11:44:38 +0000 Subject: [PATCH 12/12] deploy(ORCH-036): finalize SUCCESS for ORCH-069 --- docs/work-items/ORCH-069/14-deploy-log.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 docs/work-items/ORCH-069/14-deploy-log.md diff --git a/docs/work-items/ORCH-069/14-deploy-log.md b/docs/work-items/ORCH-069/14-deploy-log.md new file mode 100644 index 0000000..2ca83f2 --- /dev/null +++ b/docs/work-items/ORCH-069/14-deploy-log.md @@ -0,0 +1,12 @@ +--- +deploy_status: SUCCESS +work_item: ORCH-069 +hook_exit_code: 0 +deployed_by: deploy-finalizer +--- + +# Deploy log — ORCH-036 executable self-deploy + +Прод-деплой завершён хост-хуком с exit-code `0` -> `deploy_status: SUCCESS`. + +Вердикт зафиксирован детерминированным finalizer'ом (Фаза C), не LLM.