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

This commit is contained in:
2026-06-17 14:07:25 +03:00
parent 0f3649c5d3
commit a8b896b27f
3 changed files with 248 additions and 0 deletions

View File

@@ -0,0 +1,162 @@
---
work_item: ORCH-119
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-17
model_used: claude-opus-4-8
---
# ADR-001: Durable-персист `description` для source-backed `00-business-request.md`
Work Item: **ORCH-119**`00-business-request.md` всегда `TBD`, теряется source-backed контекст запроса
Стадия: **architecture**
Сквозная регистрация: **N/A, локальное решение задачи** (аддитивная скалярная колонка по
прецеденту `tasks.title`; не новый QG / стадия / компонент / смена БД-движка → сквозной
`docs/architecture/adr/adr-NNNN-*` не заводится).
> **Примечание по треку (Bug, ORCH-019).** Задача классифицирована как Bug и по BRD должна была
> идти укороченным маршрутом (пропуск `architecture`). Фактически задача **дошла до стадии
> `architecture`** (routing-override ORCH-019 не сработал — нет метки `Bug` в Plane / выключен
> `bug_fast_track`), а exit-гейт `check_architecture_done` (`src/qg/checks.py:62`) требует ≥1 файла
> в `06-adr/` или `07-infra-requirements.md`. Поэтому архитектурный выход выпускается штатно. ADR
> намеренно компактный: он фиксирует **локальное** решение по data-flow и аддитивной схеме, без
> сквозных последствий.
## Статус
Proposed
## Контекст
`src/webhooks/plane.py::_create_initial_docs` (`src/webhooks/plane.py:925`) **хардкодит** тело
раздела «Description»:
```python
content = f"# Business Request: {name}\n\nWork Item ID: {work_item_id}\n\n## Description\n\nTBD\n"
```
Сигнатура `_create_initial_docs(repo, branch, work_item_id, name)` (`:917`) **не принимает**
`description`, хотя у вызывающего `start_pipeline` оно есть в области видимости (`:518`, обогащается
из Plane API `:540551`) и уже используется для analyst-job (`task_desc`, `:723725`). Данные есть —
в durable-артефакт не доходят.
Есть **два** пути создания (сверено по коду):
- **Путь A (прямой)** — `serial_gate` не применим к репо: `start_pipeline` сам зовёт
`_create_initial_docs(repo, branch, work_item_id, name)` (`src/webhooks/plane.py:710`). `description`
здесь в области видимости — достаточно дотянуть аргумент.
- **Путь B (отложенный, доминирует на self-hosting `orchestrator`)** — `serial_gate_applies(repo)`
(ORCH-088, анти-stale-base): `start_pipeline` **НЕ** режет ветку/доки (`:697717`); срез
релоцирован на claim analyst-job в `src/agents/launcher.py::_materialize_deferred_branch`
(`:514538`), который располагает только `title` из строки `tasks`
(`SELECT branch, work_item_id, title FROM tasks`, `:561`). **`description` в таблице `tasks` не
персистится** (базовый `CREATE TABLE tasks`, `src/db.py:3142`, его не содержит) → путь B физически
не имеет доступа к описанию на момент claim.
Предусловие истинности данных: QG-0 (`_qg0_errors`, `src/webhooks/plane.py:490500`) отклоняет
создание при `description` короче 20 символов ⇒ нормальный путь всегда имеет осмысленный текст; терять
его недопустимо.
Прямой прецедент: `tasks.title` — аддитивная колонка (`_ensure_column(conn,"tasks","title","TEXT")`,
`src/db.py:125`), записываемая атомарно при создании задачи (`create_task_atomic`, `src/db.py:678683`)
и читаемая в `_spawn`/`_materialize_deferred_branch`. ORCH-119 решается **точным зеркалированием**
этого прецедента для `description`.
## Решение
### Сводка
Персистить `description` durable при создании задачи как **аддитивную колонку `tasks.description TEXT`**
(зеркало `tasks.title`), записываемую **внутри того же атомарного INSERT** `create_task_atomic`
(ORCH-053). На обоих путях создания тело `00-business-request.md` рендерится из фактического описания
через выделенный чистый хелпер с fail-safe fallback. `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` /
machine-verdict-ключи / базовый `CREATE TABLE tasks` — не трогаются.
### D1 — Где персистить описание (ключевое решение)
**Решение: аддитивная колонка `tasks.description TEXT` через `_ensure_column`, записываемая атомарно
в `create_task_atomic`.**
Путь B (отложенный срез) читает данные задачи из строки `tasks` на момент claim — единственное
durable-место, доступное и пути A, и пути B без сети. Колонка добавляется идемпотентным
`_ensure_column(conn, "tasks", "description", "TEXT")` рядом с `tasks.title` (`src/db.py:125`); базовый
`CREATE TABLE tasks` не меняется (no-op на боевой общей БД, restart-safe). Запись `description`
**встраивается в существующий INSERT** `create_task_atomic` (расширяется список колонок/значений,
`src/db.py:678683`), а не отдельным `UPDATE` — чтобы персист был атомарен и race-safe относительно
анти-dup-claim ORCH-053 (никакого окна, в котором задача создана, но описание ещё не записано).
Сигнатура `create_task_atomic` расширяется аддитивным параметром `description` с дефолтом → обратная
совместимость прочих вызывающих (напр. F-2 reconciler) сохранена. Привязка: FR-3, AC-3, NFR-3, NFR-4.
### D2 — Чистый рендер-хелпер + проброс на обоих путях
**Решение: выделить чистую функцию рендера тела и пробросить `description` в `_create_initial_docs`
на обоих путях.**
`_create_initial_docs` принимает аддитивный аргумент `description` и делегирует формирование тела
чистому хелперу (напр. `_render_business_request(work_item_id, name, description) -> str`,
unit-тестируемому без сети — TC-01/TC-02/TC-06). Заголовок (`# Business Request: {name}`) и
`Work Item ID` — без изменений; меняется только тело раздела «Description».
- **Путь A:** `start_pipeline` передаёт `description` в `_create_initial_docs` (`:710`).
- **Путь B:** `_spawn` расширяет `SELECT` до `..., description` (`src/agents/launcher.py:561`),
`_materialize_deferred_branch` принимает `description` 5-м аргументом (`:514516`, `:582584`) и
передаёт в `_create_initial_docs` (`:538`).
Привязка: FR-1, FR-2, AC-1, AC-2.
### D3 — Fail-safe fallback и идемпотентность (never-break)
**Решение: пустое/None/нечитаемое описание → явный безопасный маркер; любая ошибка рендера/чтения →
деградация на маркер, не отказ создания.**
При `description` пустом/whitespace/`None` (включая `NULL` у исторических задач, для которых колонка
не заполнялась) хелпер возвращает явный маркер (напр. `_(описание отсутствует в источнике)_`), а не
голый `TBD`. Создание задачи **никогда** не падает из-за рендера/персиста — все обогащения изолированы
(`try/except` → fallback). Идемпотентность сохранена: `_create_initial_docs` на Gitea-`422`
(файл существует) → no-op, ранее записанное тело не перезаписывается (повторная материализация после
рестарта/реклейма безопасна). Привязка: FR-4, AC-4, AC-6, NFR-1.
### D4 — Что НЕ трогаем (инварианты)
- **ORCH-088 (анти-stale-base):** момент и условие отложенного среза ветки не меняются — только
источник данных дополняется durable-колонкой; `_materialize_deferred_branch` по-прежнему режет ветку
из свежего `origin/main` на claim. Перед правкой блока ORCH-088 свериться с
`docs/work-items/ORCH-088/06-adr/ADR-001-serial-gate.md` (TRACEABILITY). NFR-3.
- **ORCH-053 (атомарный анти-dup-claim):** запись `description` — в том же INSERT под
`_CREATE_TASK_LOCK`; семантика `(row, created)` не меняется.
- **Гейты:** `00-business-request.md` — информационный док (гейтом не парсится, `PIPELINE_DOCS.md`
§2§3); `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / имена/семантика `check_*` / machine-verdict —
байт-в-байт. AC-5.
- **Без kill-switch:** это исправление дефекта (улучшение «всегда»), не рискованная фича; безопасность
обеспечивается fail-safe fallback и never-break-контрактом (TRZ §7). Обратимость — revert PR
(колонка остаётся инертной).
## Альтернативы
- **Чинить только путь A (без персиста)** — отвергнуто: путь B (self-hosting `orchestrator`, доминирует)
останется `TBD`; нарушает BR-2 (приоритетный сценарий).
- **Хранить описание в `task_content` / payload job'а** — отвергнуто: эфемерно, привязано к жизненному
циклу job'а; `_materialize_deferred_branch` читает данные из строки `tasks`, а не из job'а; нет
durable-доступа на момент claim.
- **Re-fetch описания из Plane API в `_materialize_deferred_branch`** — отвергнуто: добавляет сетевой
вызов рядом с горячим путём claim (нарушает NFR-4) и вводит зависимость материализации от
доступности Plane; ORCH-088 сознательно сделал claim детерминированным.
- **Отдельная таблица `task_metadata`** — отвергнуто: оверинжиниринг; прецедент `tasks.title` уже
канонизирует аддитивную скалярную колонку per-task.
- **Эскалация в full-cycle (`arch:major-change` / `back-to:analysis`)** — отвергнуто: решение
аддитивно, по установленному прецеденту, без нового компонента/QG/стадии/смены БД-движка и без
нарушения принципов; ТЗ удовлетворяется штатно.
## Последствия
- **+** Durable source-backed контекст в `00-business-request.md` на обоих путях; зеркало проверенного
прецедента (низкий риск).
- **+** Ноль изменений машинерии конвейера (гейты/переходы/вердикты/базовая схема) → ноль риска
регресса конвейера; enduro-trails не затронут (для него тоже просто рендерится его описание).
- **** Схема общей прод-БД растёт на одну колонку → митигировано аддитивным `_ensure_column` (no-op
при наличии, без переписывания базового `CREATE TABLE`), обратная совместимость (`NULL` у
существующих строк, fallback-маркер при рендере).
- **** Уже созданные задачи не ретро-генерируются (вне объёма, принято; колонка `NULL` → fallback).
- **Откат:** revert PR полностью возвращает прежнее поведение; аддитивная колонка остаётся инертной
(без обязательной down-миграции на общей БД).
## Ссылки
- BRD: `docs/work-items/ORCH-119/01-brd.md`
- TRZ: `docs/work-items/ORCH-119/02-trz.md`
- Acceptance: `docs/work-items/ORCH-119/03-acceptance-criteria.md`
- Data-requirements: `docs/work-items/ORCH-119/08-data-requirements.md`
- Tech-risks: `docs/work-items/ORCH-119/10-tech-risks.md`
- Сверено по коду: `src/webhooks/plane.py:482,490,518,710,917,925` · `src/agents/launcher.py:514,531,538,561,582` · `src/db.py:31,125,647,678` · `src/qg/checks.py:62`
- Инвариант ORCH-088: `docs/work-items/ORCH-088/06-adr/ADR-001-serial-gate.md`
- Стандарт документов / ADR-naming: `docs/_standards/PIPELINE_DOCS.md` §4

View File

@@ -0,0 +1,49 @@
---
work_item: ORCH-119
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-17
model_used: claude-opus-4-8
---
# 08 — Требования к данным: ORCH-119 — durable-персист `description` задачи
Work Item: **ORCH-119** · Repo: **orchestrator** · Стадия: architecture
> When-applicable / информационный (гейтом не парсится). Применимо: фикс требует durable-хранения
> `description` в строке `tasks` для пути B (отложенный срез ветки, ORCH-088).
## Изменения схемы БД
- **Новая колонка `tasks.description TEXT`** — добавляется идемпотентным
`_ensure_column(conn, "tasks", "description", "TEXT")` (`src/db.py`, рядом с `tasks.title`,
`src/db.py:125`). Прецедент 1:1 — `tasks.title` / `tasks.track` / `tasks.cancelled_at`.
- Базовый `CREATE TABLE tasks` (`src/db.py:3142`) **не трогается**.
- Индексы **не требуются** (колонка не участвует в выборках/JOIN; читается по PK `tasks.id`).
- `NULL` по умолчанию; для уже существующих задач остаётся `NULL` (ретро-генерация вне объёма).
## Новые/изменённые сущности
- **`tasks.description`** — plain-text описание запроса (предпочтительно `description_stripped`
Plane-issue), записывается **при создании задачи** внутри атомарного INSERT
`create_task_atomic` (`src/db.py:678683`; список колонок/значений расширяется, параметр
`description` аддитивен с дефолтом). Читается на пути B в `_spawn`
(`SELECT ..., description FROM tasks`, `src/agents/launcher.py:561`) и передаётся в
`_materialize_deferred_branch``_create_initial_docs`.
- Инвариант данных: значение пишется **как есть**, без обрезки/искажения; многострочный текст и
markdown-спецсимволы сохраняются (`00-business-request.md` гейтом не парсится — спецсимволы
безопасны, NFR-2). Пустое/`NULL` → рендер деградирует на fallback-маркер (ADR-001 D3), не на
отказ.
## Совместимость данных / миграции
- **Аддитивность:** только `ADD COLUMN` через `_ensure_column`; существующая боевая ОБЩАЯ БД и
enduro-trails не затронуты (для них `description` тоже просто рендерится — улучшение, не регресс).
- **Идемпотентность:** `_ensure_column` — no-op при наличии колонки; повторный `init_db` безопасен
(TC-05). `_create_initial_docs` на Gitea-`422` — no-op (тело не перезаписывается, TC-06).
- **Restart-safe / атомарность:** запись `description` — в том же INSERT под `_CREATE_TASK_LOCK`
(ORCH-053), без окна «задача создана, описание отсутствует»; реклейм/материализация после
рестарта безопасны.
- **Down-миграция:** не требуется — revert PR оставляет колонку инертной (без обязательного DROP на
общей прод-БД).
- **Влияние на общую прод-БД (self-hosting):** одна аддитивная колонка, без рестарта прода в рамках
схемы (применяется на следующем `init_db`); без новых сетевых вызовов в горячем `claim_next_job`
(NFR-4).

View File

@@ -0,0 +1,37 @@
---
work_item: ORCH-119
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-17
model_used: claude-opus-4-8
---
# 10 — Технические риски: ORCH-119 — source-backed `00-business-request.md`
Work Item: **ORCH-119** · Repo: **orchestrator** · Стадия: architecture
> Информационный (гейтом не парсится). Перечисляет риски реализации и их митигейшн.
## Реестр рисков
| ID | Риск | Вер. | Влия. | Митигейшн |
|----|------|------|-------|-----------|
| TR-1 | Чинят только прямой путь A, путь B (отложенный срез, self-hosting) забыт → на `orchestrator` баг остаётся (`TBD`) | Сред. | Выс. | Обязательный integration-тест пути B (TC-03): персист в `tasks` + рендер в `_materialize_deferred_branch`; ADR-001 D1/D2 явно фиксирует оба пути |
| TR-2 | Регресс схемы/создания задачи при добавлении персиста на ОБЩЕЙ прод-БД | Низ. | Выс. | Аддитивный идемпотентный `_ensure_column` (no-op при наличии), базовый `CREATE TABLE` не трогается; тест обратной совместимости (TC-05) |
| TR-3 | Падение создания задачи из-за рендера/чтения описания (нарушение never-break) | Низ. | Выс. | Fail-safe fallback-маркер + изоляция обогащения (`try/except`); создание задачи не зависит от рендера (ADR-001 D3, TC-02) |
| TR-4 | Гонка ORCH-053: окно «задача создана, описание ещё не записано» при отдельном `UPDATE` | Низ. | Сред. | Запись `description` встроена в тот же атомарный INSERT `create_task_atomic` под `_CREATE_TASK_LOCK`, отдельного `UPDATE` нет (ADR-001 D1) |
| TR-5 | Повторная материализация (рестарт/реклейм) перезаписывает/дублирует тело артефакта | Низ. | Сред. | Идемпотентность `_create_initial_docs` (Gitea `422` → no-op), тело не перезаписывается (TC-06) |
| TR-6 | Нарушение анти-stale-base инварианта ORCH-088 при правке отложенного среза | Низ. | Выс. | Момент/условие среза НЕ меняются — только источник данных дополняется durable-колонкой; сверка с `docs/work-items/ORCH-088/06-adr/` перед правкой (ADR-001 D4, NFR-3) |
| TR-7 | Случайная правка `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict | Низ. | Выс. | `00-business-request.md` информационный (не гейтится); анти-регресс-тест импорта гейтов без изменений (TC-07, AC-5) |
| TR-8 | Markdown-спецсимволы/многострочность описания искажают артефакт | Низ. | Низ. | Рендер plain-text «как есть» без обрезки; артефакт гейтом не парсится → спецсимволы безопасны (NFR-2, TC-06) |
## Сводный вывод
Доминирующий класс — **операционные риски реализации** (полнота покрытия обоих путей + сохранение
never-break/идемпотентности/инвариантов ORCH-088/ORCH-053), не архитектурные. Все они закрываются
обязательными регресс- и edge-тестами (`04-test-plan.yaml` TC-01…TC-07) и точным следованием
прецеденту `tasks.title`. Эскалация **не требуется**: решение аддитивно, обратимо (revert PR),
без нового компонента/QG/стадии/смены БД-движка → `arch:major-change` не выставляется, возврат в
анализ (`back-to:analysis`) не нужен. Остаточный риск для прод-конвейера (self-hosting) — **низкий**:
фикс не деплоит/не рестартит прод, не трогает `main`, не добавляет сетевых вызовов в горячий
`claim_next_job` (NFR-4).