Compare commits
28 Commits
feature/OR
...
cb9bfcff12
| Author | SHA1 | Date | |
|---|---|---|---|
| cb9bfcff12 | |||
| d846910ca6 | |||
| 92961d1d32 | |||
| 2030d1627a | |||
| 98c50a094b | |||
| 561f58abe0 | |||
| 95d2c2093a | |||
| 6868e34d1f | |||
| 03c6f2a145 | |||
|
|
da3e6e8acd | ||
| 119b8f2bec | |||
| 138092e040 | |||
| 5e60543232 | |||
| 3251c8c4ed | |||
| 6511ddadbb | |||
| 18e98945dd | |||
| 0f7db904f1 | |||
| 73d936a4c4 | |||
|
|
a16196d68c | ||
| 9b3490ceaa | |||
| 3c407397da | |||
| a6d0ba51c0 | |||
| f7488e9536 | |||
| 0b5fede802 | |||
| cc2f1885e8 | |||
| c9be0eb4c9 | |||
| 21bde85708 | |||
| 7d61c820a7 |
18
CHANGELOG.md
18
CHANGELOG.md
@@ -3,6 +3,24 @@
|
||||
Формат: [Keep a Changelog](https://keepachangelog.com/). Записи — на смысловой PR/задачу.
|
||||
|
||||
## [Unreleased]
|
||||
- **Единый frontmatter-контракт (reader + writer + валидатор) + спека handoff** (ORCH-076 / ORCH-52c, `refactor`/`docs`): слой 2 эпика ORCH-52 — `src/frontmatter.py` из single-key reader превращён в **полный машинный контракт**, а **разрознённое чтение вердиктов** пяти гейтов сведено к **одной точке парсинга**. Строго обратно совместимо, never-raise; `STAGE_TRANSITIONS` / состав `QG_CHECKS` / семантика вердиктов / fallback `worktree→origin/main` / трёх-полевой контракт tester (ORCH-047) — **1:1, без изменений**.
|
||||
- **`src/frontmatter.py` (контракт):** сохранён reader `read_frontmatter_value` (контракт неизменен — внешние вызыватели `usage.py` / `notifications.build_status_comment` не затронуты, INV-3); добавлены единый парс-примитив `parse_frontmatter(content) -> FrontmatterParse` (`data/has_block/malformed/yaml_error` — единственная точка YAML-логики) + ярлыки `parse_frontmatter_dict` / `read_frontmatter`; writer `render_frontmatter`/`write_frontmatter` (формат байт-совместим с `split("---",2)`+`yaml.safe_load`, round-trip render→parse); валидатор схемы `validate_schema`/`SchemaValidation`/`REQUIRED_FIELDS` (`work_item/stage/author_agent/status/created_at/model_used`); общий `strip_frontmatter`. Весь модуль — **never-raise** (NFR-2): любая ошибка I/O/YAML/сериализации → лог + безопасное значение (`{}`/`False`/исходный текст).
|
||||
- **Унифицирован МЕХАНИЗМ, а не семантика (D2):** пять вердикт-парсеров — `check_reviewer_verdict` (`verdict:`), `_parse_tests_verdict` (`result:`/`verdict:`/`status:`, ORCH-047), `_parse_deploy_status` (`deploy_status:`), `_parse_staging_status` (`staging_status:`) в `src/qg/checks.py`; `parse_security_status` (`security_status:`) в `src/security_gate.py` — заменили дублированный блок `startswith/split/safe_load/isinstance` на `parse_frontmatter(content)`; token-логика, upper-casing, приоритет негативного токена, reason-строки — сохранены 1:1. Также сняты дубли в `security_gate.extract_security_findings` и `review_parse._strip_frontmatter` (делегируют `strip_frontmatter`).
|
||||
- **Валидатор не hard-fail по умолчанию (D3, критично для self-hosting):** `maybe_warn_schema` при дефолте только логирует `logger.warning("frontmatter schema incomplete: …")` и **никогда не влияет на boolean-вердикт** гейта (инертен). Жёсткий режим — ТОЛЬКО под kill-switch `frontmatter_validation_strict` (env `ORCH_FRONTMATTER_VALIDATION_STRICT`, дефолт `False`; остаётся `False` в проде/`.env.staging`, иначе ORCH-52c self-block'нулась бы — её доки без полной схемы). Схема **аддитивна**: старый док-вердикт без новых полей читается ровно как раньше (FR-5/AC-4).
|
||||
- **Спека handoff:** новый `docs/_standards/HANDOFF_PROTOCOL.md` — формальный контракт «стадия → обязательные документы + frontmatter-ключи на выходе» + обязательная схема (`REQUIRED_FIELDS`), согласован 1:1 с `PIPELINE_DOCS.md` §2–§3; `PIPELINE_DOCS.md` §5–§6 обновлён (слой 2 реализован, ссылка на спеку и `src/frontmatter.py`).
|
||||
- **Без изменений API / схемы БД** (INV-5). Тесты: `tests/test_frontmatter.py` (TC-01…TC-07: writer/round-trip/валидатор/strict/never-raise/reader), `tests/test_qg_verdicts.py` (TC-08…TC-15: семантика пяти гейтов 1:1, обратная совместимость, fallback origin/main), `tests/test_security_gate.py` (TC-12), `tests/test_stages_invariants.py` (TC-16: `QG_CHECKS`/`STAGE_TRANSITIONS` неизменны). Полный регресс `tests/` зелёный (1212). Конфиг: `src/config.py` (`frontmatter_validation_strict`). Документация: `CLAUDE.md`, `docs/architecture/README.md`. ADR: `docs/work-items/ORCH-076/06-adr/ADR-001-frontmatter-contract.md`, сквозной `docs/architecture/adr/adr-0020-frontmatter-contract.md`.
|
||||
- **Стандарт документов конвейера: `docs/_standards/PIPELINE_DOCS.md` + `docs/_templates/` + ADR-naming** (ORCH-075 / ORCH-52b, `docs`): зафиксирован golden source структуры номерных документов work item (`00-business-request.md` … `17-security-report.md`). **Docs-only**, нулевой рантайм-риск: `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / `src/stage_engine.py` / схема БД — **не трогаются** (изменения только под `docs/**` + `CLAUDE.md`).
|
||||
- **Манифест** `docs/_standards/PIPELINE_DOCS.md` — карта «стадия → агент → документ → категория (`required`/`when-applicable`/`optional`) → гейт/механизм → frontmatter machine-key», сверенная с `src/stages.py` (`STAGE_TRANSITIONS`) и `src/qg/checks.py` (`_parse_*`). Манифест **документирует** поведение гейтов, но НЕ источник истины (источник — код, ADR-001 §D2); честно различает machine-verdict доки (`12`→`verdict:`, `13`→`result:`, `14`→`deploy_status:`, `15`→`staging_status:`, `17`→`security_status:`) и информационные (`00/08/10/16` — гейтом не парсятся). Под-гейты ребра `deploy-staging→deploy` (security/merge/image-freshness) помечены как врезки в `advance_stage`, а не строки `STAGE_TRANSITIONS`.
|
||||
- **Шаблоны** `docs/_templates/*` (15 копируемых скелетов) — для каждого `required`/`when-applicable` дока; машинные доки несут точный frontmatter-ключ из ground-truth (`_parse_*`), чтобы скопированный скелет проходил гейт без угадывания. Служебные каталоги `docs/_standards/` / `docs/_templates/` лежат ВНЕ `docs/work-items/<plane-id>/` → невидимы гейтам наличия файлов (`check_architecture_done`/`check_analysis_complete`).
|
||||
- **ADR-naming** зафиксирован: `docs/work-items/<plane-id>/06-adr/ADR-NNN-<kebab-slug>.md` (NNN с `001`); сквозные решения дублируются в `docs/architecture/adr/adr-NNNN-<slug>.md` (4-значная нумерация). Точки-ссылки: `CLAUDE.md` (раздел «Артефакты задачи» + правило 2), `docs/architecture/README.md` (раздел «Стандарт документов конвейера»). Тесты: `tests/test_orch_52b_docs_standard.py` (TC-01…TC-20, структурные проверки наличия/секций/frontmatter). ADR: `docs/work-items/ORCH-075/06-adr/ADR-001-pipeline-docs-standard.md`, сквозной `docs/architecture/adr/adr-0019-pipeline-docs-standard.md`.
|
||||
- **Авто-режим по лейблам: autoApprove (авто-BRD) + autoDeploy (авто-деплой)** (ORCH-089, `feat`): сняты **два** человеческих гейта конвейера, тормозящих пакетный автономный прогон (эпик ORCH-088) — гейт BRD (`analysis`: ручной `Approved`) и гейт прод-деплоя (`deploy` Phase A: ручной `Confirm Deploy`, ORCH-059). Решение выборочно (лейбл Plane на задаче), декларативно, обратимо и **не трогает ни одной технической проверки**. Аддитивно по образцу условных под-гейтов (ORCH-035/043/058/088): leaf `src/labels.py` (never-raise) + две точечные врезки + флаги; `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/схема БД — **без изменений**.
|
||||
- **`autoApprove`** → врезка в `stage_engine._handle_analysis_approved_flow` (ветка `files_ok`) ПОСЛЕ `In Review`+коммента: `set_issue_approved` (индикация) + лог/Telegram/Plane-коммент + `advance_stage(..., finished_agent=None)` — **тот же путь, что человеческий Approved** (`approved-via-status` → `analysis → architecture` + `mark_brd_review_ended`). Без дублирования переходной логики; re-entrancy безопасна (вложенный вызов идёт с `finished_agent=None`, не входит в analyst-ветку).
|
||||
- **`autoDeploy`** → врезка в `stage_engine._handle_self_deploy_phase_a` сразу после advance на `deploy`+`clear_state` (ДО «ask-human»): лог/Telegram/Plane-коммент + `_handle_self_deploy_phase_b(...)` (idempotency-маркер `INITIATED`, статус `Deploying`, finalizer). Пропускаются лишь индикативно-человеческие шаги (`APPROVE_REQUESTED`+`Awaiting Deploy`+«смените на Confirm Deploy»). **BR-5 структурно:** Phase A достигается только после зелёных под-гейтов ребра `deploy-staging → deploy` (security → merge-gate → image-freshness → staging) → autoDeploy физически не деплоит сломанное.
|
||||
- **Чтение лейблов** — `plane_sync.fetch_issue_labels` (поле `labels` issue; `None` при ошибке ≠ `[]`) + `get_project_labels` (`{normalized_name→uuid}`, TTL-кэш `auto_label_states_ttl_s` по образцу `get_project_states`); сопоставление по нормализованному имени (`strip().casefold()`), неоднозначность (две метки → одно нормализованное имя) → сентинел `__AMBIGUOUS__` → «нет лейбла». Новый сеттер `set_issue_approved` (ключ `approved` уже в `_DEFAULT_STATES`). Источник истины — Plane API, не payload вебхука.
|
||||
- **Флаги** (`config.py`): `auto_label_enabled` (kill-switch), `auto_approve_label`/`auto_deploy_label`, `auto_label_repos` (CSV; **пусто → self-hosting only**), `auto_label_states_ttl_s`. `applies(repo)` (локальный) проверяется ПЕРВЫМ; `has_label` (сеть) — только при `applies==True` → при выключенном флаге нулевой сетевой оверхед, нулевая регрессия для enduro (AC-8).
|
||||
- **Fail-safe (never auto):** любая ошибка/недоступность Plane/неоднозначность → «нет авто» → ручной гейт (never-raise, AC-6). **Прозрачность (AC-7):** лог + Telegram + Plane-коммент + live-карточка через штатный advance. Read-only блок `auto_labels` в `GET /queue`.
|
||||
- **Инфра-предусловие:** создать лейблы `autoApprove`/`autoDeploy` в Plane-проекте ORCH (labels API); их отсутствие = `has_label` False = ручной режим (fail-safe). Детали — `docs/work-items/ORCH-089/07-infra-requirements.md`.
|
||||
- Тесты: `tests/test_labels.py`, `test_plane_sync_labels.py`, `test_auto_approve_brd.py`, `test_auto_deploy.py`, `test_auto_label_combinations.py`, `test_auto_labels_integration.py`, `test_auto_labels_invariants.py` (TC-01…TC-26). ADR: `docs/work-items/ORCH-089/06-adr/ADR-001-auto-label-gates.md`, global `docs/architecture/adr/adr-0018-auto-label-gates.md`.
|
||||
- **Per-repo serial gate: пакетный автономный режим (Этап 1, serial e2e)** (ORCH-088, `feat`): закрыт **логический** stale-анализ — ветка задачи N+1 срезалась на входе в анализ (`start_pipeline._create_gitea_branch`) от `main`, ещё не содержащего код предшественника N (физическое затирание уже закрыто ORCH-026). Новая задача репо не входит в `analysis` (не режет ветку, не запускает analyst), пока в репо есть незавершённая задача или репо заморожен. Аддитивно, под kill-switch, область репо, never-raise, restart-safe; `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*` — **без изменений**.
|
||||
- **Gate-в-claim** (`db.claim_next_job`): analyst-job (`jobs.agent='analyst'`) применимого репо не выбирается, если `EXISTS` **более ранняя** незавершённая задача репо (`t2.id < jobs.task_id`) ИЛИ активна строка `repo_freeze`. Фрагмент строится в leaf `src/serial_gate.py::build_claim_clause` (санитизация repo-токенов `^[A-Za-z0-9._-]+$`, **fail-OPEN** на любой ошибке построения — не заклинить очередь всех проектов, AC-8); только локальная БД (offline hot-path, NFR-2). Job'ы уже активной задачи проходят свободно. **FIFO-уточнение реализации (FR-2):** ADR-001 D1 фиксировал псевдо-SQL `t2.id != jobs.task_id`; при `!=` пакет одновременно созданных свежих задач (все в `analysis`) взаимно блокировался бы → дедлок всей serial-очереди (воспроизведено). `<` допускает ровно самую раннюю задачу и сериализует остальные за ней (строго по одной, FIFO по `jobs.id`), сохраняя AC-1 и не блокируя rework-analyst собственной задачи (R-7).
|
||||
- **Отложенный срез ветки (анти-stale-base, AC-6):** для применимого репо `start_pipeline` создаёт task-row + enqueue analyst, но **не** создаёт Gitea-ветку/docs; срез релоцирован в `launcher._spawn` (новый `_materialize_deferred_branch`, sync через `asyncio.run` в worker-потоке, R-4) на момент claim analyst-job, когда `origin/main` уже содержит предшественника (`done` ⇔ SHA-в-main, ORCH-071/073). `ensure_worktree` режет от свежего `origin/main` ⇒ AC-6 структурно. Идемпотентно (`_create_gitea_branch` 409 / `_create_initial_docs` 422 = no-op) → безопасно при реклейме/рестарте. Ожидающая задача = `queued` analyst-job без ветки; `tasks.branch` хранится как имя (R-5).
|
||||
|
||||
39
CLAUDE.md
39
CLAUDE.md
@@ -78,20 +78,55 @@ created → analysis → architecture → development → review → testing →
|
||||
- Транспорт (`send_telegram`/`edit_telegram`/`delete_telegram`), `disable_notification`
|
||||
(карточка тихая, пингуют только alert-хелперы), схема БД — не трогаются.
|
||||
|
||||
## Авто-режим по лейблам: autoApprove + autoDeploy (ORCH-089)
|
||||
Конвейер имеет два **человеческих** гейта, тормозящих пакетный автономный прогон
|
||||
(эпик ORCH-088): гейт BRD (`analysis`: ручной `Approved`) и гейт прод-деплоя
|
||||
(`deploy` Phase A: ручной `Confirm Deploy`, ORCH-059). ORCH-089 снимает **только эти
|
||||
два человеческих решения** — выборочно (лейбл Plane на задаче), декларативно,
|
||||
обратимо, **не трогая ни одной технической проверки**. Инвариант: авто-режим снимает
|
||||
лишь ожидание человеческого сигнала; `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/схема БД
|
||||
— **не трогаются**. Аддитивно: leaf `src/labels.py` (never-raise) + две точечные врезки.
|
||||
- **`autoApprove`** → врезка в `stage_engine._handle_analysis_approved_flow` (ветка
|
||||
`files_ok`): `set_issue_approved` (индикация) + лог/Telegram/Plane-коммент +
|
||||
`advance_stage(..., finished_agent=None)` — **тот же путь, что человеческий Approved**
|
||||
(`approved-via-status` → `analysis → architecture` + `mark_brd_review_ended`).
|
||||
- **`autoDeploy`** → врезка в `stage_engine._handle_self_deploy_phase_a` после advance
|
||||
на `deploy` + `clear_state`: лог/Telegram/Plane-коммент + `_handle_self_deploy_phase_b`
|
||||
(маркер `INITIATED`, статус `Deploying`, finalizer). Пропускаются лишь
|
||||
индикативно-человеческие шаги. **BR-5 структурно:** Phase A достигается только после
|
||||
зелёных под-гейтов ребра `deploy-staging → deploy` (security → merge-gate →
|
||||
image-freshness → staging) → autoDeploy физически не деплоит сломанное.
|
||||
- **Чтение лейблов** — `plane_sync.fetch_issue_labels` (`None` при ошибке ≠ `[]`) +
|
||||
`get_project_labels` (`{normalized_name→uuid}`, TTL-кэш); сопоставление по
|
||||
нормализованному имени (`strip().casefold()`), неоднозначность → «нет лейбла».
|
||||
Источник истины — Plane API, не payload вебхука. Новый сеттер `set_issue_approved`.
|
||||
- **Флаги** (`config.py`): `auto_label_enabled` (kill-switch), `auto_approve_label`/
|
||||
`auto_deploy_label`, `auto_label_repos` (CSV; **пусто → self-hosting only**),
|
||||
`auto_label_states_ttl_s`. `applies(repo)` (локальный) проверяется ПЕРВЫМ; `has_label`
|
||||
(сеть) — только при `applies==True` → при выключенном флаге нулевой сетевой оверхед.
|
||||
- **Fail-safe (never auto):** любая ошибка/недоступность Plane/неоднозначность →
|
||||
«нет авто» → ручной гейт (never-raise). Прозрачность: лог + Telegram + Plane-коммент +
|
||||
live-карточка; блок `auto_labels` в `GET /queue`. **Инфра-предусловие:** создать лейблы
|
||||
`autoApprove`/`autoDeploy` в Plane-проекте ORCH (их отсутствие = ручной режим, fail-safe).
|
||||
Детали — `docs/work-items/ORCH-089/06-adr/ADR-001-auto-label-gates.md`,
|
||||
`docs/architecture/adr/adr-0018-auto-label-gates.md`.
|
||||
|
||||
## Конвенции
|
||||
- Conventional Commits (`feat:`, `fix:`, `docs:`, `refactor:`, `test:`)
|
||||
- Ветки: `feature/ORCH-NNN-slug`, `fix/ORCH-NNN-slug`
|
||||
- ADR per work-item: `docs/work-items/<plane-id>/06-adr/ADR-NNN-slug.md`
|
||||
- Global ADR (сквозные решения): `docs/architecture/adr/adr-NNNN-slug.md`
|
||||
- Work items: `docs/work-items/<plane-id>/`
|
||||
- Машинные вердикты Quality Gate — строго YAML-frontmatter (`verdict:`, `deploy_status:`, `staging_status:`, `security_status:`), никогда проза
|
||||
- Машинные вердикты Quality Gate — строго YAML-frontmatter (`verdict:`, `deploy_status:`, `staging_status:`, `security_status:`), никогда проза. **ORCH-52c (ORCH-076):** парсинг frontmatter сведён к единому контракту `src/frontmatter.py` (reader `read_frontmatter_value` — BC; единый парс-примитив `parse_frontmatter`; writer `render/write_frontmatter`; валидатор схемы `validate_schema`/`REQUIRED_FIELDS` — warning-only по умолчанию, hard-fail только под kill-switch `frontmatter_validation_strict`, дефолт `False`). Пять вердикт-парсеров (`check_reviewer_verdict`, `_parse_tests_verdict`, `_parse_deploy_status`, `_parse_staging_status`, `parse_security_status`) читают через ОДНУ точку парсинга; семантика вердиктов и `STAGE_TRANSITIONS`/состав `QG_CHECKS` — 1:1. Формальная спека «стадия → обязательный выход» + обязательная frontmatter-схема — `docs/_standards/HANDOFF_PROTOCOL.md`
|
||||
|
||||
## Артефакты задачи (`docs/work-items/<plane-id>/`)
|
||||
`00-business-request.md`, `01-brd.md`, `02-trz.md`, `03-acceptance-criteria.md`, `04-test-plan.yaml`, `06-adr/ADR-NNN-slug.md`, `07-infra-requirements.md`, `08-data-requirements.md`, `10-tech-risks.md`, `12-review.md`, `13-test-report.md`, `14-deploy-log.md`, `15-staging-log.md`, `16-post-deploy-log.md` (post-deploy наблюдение, ORCH-021), `17-security-report.md` (security-гейт: `security_status:`/secrets/deps, ORCH-022).
|
||||
|
||||
**Стандарт документов (ORCH-075, ORCH-52b):** структура каждого дока, карта «стадия→агент→документ→гейт→machine-key» и конвенция ADR-naming зафиксированы в `docs/_standards/PIPELINE_DOCS.md` (golden source); копируемые скелеты — в `docs/_templates/`. Перед написанием номерного дока бери скелет из `docs/_templates/` и не меняй имя machine-key frontmatter (регистр чувствителен — иначе гейт упадёт ложно).
|
||||
|
||||
## Правила для агентов
|
||||
1. Перед любым действием прочесть этот файл и `docs/architecture/README.md`.
|
||||
2. **Документация = golden source наравне с кодом.** Изменил функционал → обнови доку В ТОМ ЖЕ PR. Архитектурное решение → заведи ADR. Обнови `CHANGELOG.md`.
|
||||
2. **Документация = golden source наравне с кодом.** Изменил функционал → обнови доку В ТОМ ЖЕ PR. Архитектурное решение → заведи ADR (формат — `docs/_standards/PIPELINE_DOCS.md` §4). Структура номерных доков и шаблоны — `docs/_standards/PIPELINE_DOCS.md` + `docs/_templates/`. Обнови `CHANGELOG.md`.
|
||||
3. Никогда не править артефакты других этапов.
|
||||
4. Никогда не комментировать ТЗ задним числом — если ТЗ не годится, возвращай в Анализ.
|
||||
5. Никогда не закрывать задачу самостоятельно — это делает CI / финальная стадия.
|
||||
|
||||
118
docs/_standards/HANDOFF_PROTOCOL.md
Normal file
118
docs/_standards/HANDOFF_PROTOCOL.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# HANDOFF_PROTOCOL — формальный контракт handoff «стадия → обязательный выход»
|
||||
|
||||
> **Назначение.** Нормативная спека: что КАЖДАЯ стадия конвейера обязана оставить на выходе —
|
||||
> какие документы и какие frontmatter-ключи. Дополняет [`PIPELINE_DOCS.md`](PIPELINE_DOCS.md)
|
||||
> (карта «документ → агент → стадия → гейт → machine-key») «вертикальным» срезом по стадиям и
|
||||
> вводит **обязательную frontmatter-схему** для машинной проверки.
|
||||
>
|
||||
> **Статус истины (важно).** Источник истины поведения — **код**: `src/stages.py`
|
||||
> (`STAGE_TRANSITIONS`), `src/qg/checks.py` (`QG_CHECKS` / `check_*` / `_parse_*`),
|
||||
> `src/stage_engine.py` (врезки под-гейтов). Машинный контракт чтения/записи/валидации
|
||||
> frontmatter — `src/frontmatter.py`. Эта спека **документирует**; при расхождении первичен код
|
||||
> (правило ORCH-075).
|
||||
|
||||
Введено задачей **ORCH-076** (ORCH-52c — слой 2 эпика ORCH-52: машинный контракт). Слой 1
|
||||
(ORCH-075/52b) дал описательный стандарт документов; ORCH-52c реализовала единый машинный
|
||||
frontmatter-контракт (reader + writer + валидатор) и свела чтение пяти вердиктов к одной точке
|
||||
парсинга. Сквозной ADR: [`adr-0020-frontmatter-contract.md`](../architecture/adr/adr-0020-frontmatter-contract.md);
|
||||
детально — [`ORCH-076/06-adr/ADR-001-frontmatter-contract.md`](../work-items/ORCH-076/06-adr/ADR-001-frontmatter-contract.md).
|
||||
|
||||
---
|
||||
|
||||
## 1. Обязательная frontmatter-схема (машинный источник: `frontmatter.REQUIRED_FIELDS`)
|
||||
|
||||
Forward-looking аддитивная схема: набор полей, которые handoff-документ стадии **должен** нести
|
||||
в ведущем YAML-frontmatter. Машинный источник истины — кортеж
|
||||
[`src/frontmatter.py`](../../src/frontmatter.py) `REQUIRED_FIELDS`:
|
||||
|
||||
| Поле | Смысл |
|
||||
|------|-------|
|
||||
| `work_item` | ID задачи (`ORCH-NNN` / `ET-NNN`) — к какой задаче относится выход |
|
||||
| `stage` | стадия, на выходе которой написан документ (`analysis` … `deploy`) |
|
||||
| `author_agent` | роль-автор (`analyst` / `architect` / `developer` / `reviewer` / `tester` / `deployer`) |
|
||||
| `status` | человеко/машинно-читаемый статус выхода стадии |
|
||||
| `created_at` | дата создания артефакта (YYYY-MM-DD) |
|
||||
| `model_used` | модель агента, сгенерировавшего артефакт (`claude-…`) |
|
||||
|
||||
**Режим проверки (ORCH-52c, критично для self-hosting).** Валидатор схемы
|
||||
`frontmatter.validate_schema` / `maybe_warn_schema` по умолчанию **warning-only** и **никогда не
|
||||
влияет на boolean-вердикт ни одного гейта**: отсутствие полей логируется (`logger.warning`), но не
|
||||
роняет конвейер и не заваливает гейт. Жёсткий режим (hard-fail) зарезервирован на будущее
|
||||
(ORCH-52d) и включается ТОЛЬКО kill-switch'ем `frontmatter_validation_strict`
|
||||
(env `ORCH_FRONTMATTER_VALIDATION_STRICT`, дефолт `False`). Схема **аддитивна**: старый
|
||||
документ-вердикт без этих полей читается гейтом ровно как раньше (см. §3).
|
||||
|
||||
---
|
||||
|
||||
## 2. Контракт handoff по стадиям
|
||||
|
||||
Категории документов — как в `PIPELINE_DOCS.md` §2: **required** (всегда), **when-applicable**
|
||||
(при наличии предмета: инфра / данные / security / post-deploy — отсутствие не нарушение).
|
||||
«Machine-verdict ключ» — поле, которое exit-гейт/под-гейт ребра читает ТОЛЬКО из frontmatter
|
||||
(никогда из прозы). Набор документов/ключей/гейтов **согласован 1:1 с `PIPELINE_DOCS.md` §2–§3**.
|
||||
|
||||
| Стадия (выход) | Агент | Обязательные документы на выходе | Machine-verdict ключ (читает гейт ребра) | Гейт ребра |
|
||||
|----------------|-------|----------------------------------|------------------------------------------|------------|
|
||||
| `created` | система (`_create_initial_docs`) / заказчик | `00-business-request.md` | — (вход, не гейтится) | — |
|
||||
| `analysis` | analyst | `01-brd.md`, `02-trz.md`, `03-acceptance-criteria.md`, `04-test-plan.yaml` | — (гейт проверяет наличие файлов + Approved) | `check_analysis_approved` |
|
||||
| `architecture` | architect | `06-adr/ADR-NNN-<slug>.md` (≥1); `07-infra-requirements.md`, `08-data-requirements.md`, `10-tech-risks.md` (when-applicable/required-info) | — (гейт проверяет наличие `06-adr/` ≥1 ИЛИ `07-…`) | `check_architecture_done` |
|
||||
| `development` | developer | код + тесты в ветке (артефакт-док не пишется; гейт — зелёный CI) | — (гейт читает CI-статус Gitea) | `check_ci_green` |
|
||||
| `review` | reviewer | `12-review.md` | `verdict:` (`APPROVED` \| `REQUEST_CHANGES`) | `check_reviewer_verdict` |
|
||||
| `testing` | tester | `13-test-report.md` | `result:` / `verdict:` / `status:` (`PASS` \| `FAIL` \| `BLOCKED`; три равноранговых, ORCH-047) | `check_tests_passed` |
|
||||
| `deploy-staging` | deployer | `15-staging-log.md` (required для self-hosting); `17-security-report.md` (security-под-гейт, when-applicable) | `staging_status:` (`SUCCESS` \| `FAILED`); `security_status:` (`PASS` \| `FAIL`) | `check_staging_status` (ребро); под-гейты ребра `deploy-staging→deploy`: `check_security_gate` → `check_branch_mergeable` → `check_staging_image_fresh` |
|
||||
| `deploy` | deployer / deploy-finalizer | `14-deploy-log.md` | `deploy_status:` (`SUCCESS` \| `FAILED`) | `check_deploy_status` |
|
||||
| `done` | — | — (терминал) | — | — |
|
||||
| пост-`done` наблюдение | post-deploy-monitor | `16-post-deploy-log.md` (when-applicable, ORCH-021) | `post_deploy_status:` (`HEALTHY` \| `DEGRADED`) — **информационный, не гейт** | — (телеметрия петли уроков / наблюдаемость) |
|
||||
|
||||
### Примечания (нормативные)
|
||||
|
||||
- **Под-гейты ребра `deploy-staging → deploy`** (`check_security_gate` → `check_branch_mergeable`
|
||||
→ `check_staging_image_fresh`) — это **врезки в `advance_stage`**, а НЕ строки
|
||||
`STAGE_TRANSITIONS`. Их порядок и условность раската не меняются этой спекой.
|
||||
- **`15-staging-log.md`** обязателен только для self-hosting репо (`orchestrator`); для прочих
|
||||
репо staging-гейт — N/A (ORCH-35), документ не требуется.
|
||||
- **`16-post-deploy-log.md`** несёт `post_deploy_status:`, но это **информационный** ключ
|
||||
(телеметрия ORCH-8 / наблюдаемость), гейтом он НЕ парсится.
|
||||
- **`09-…` / `05-…` / `11-…`** — зарезервированные/legacy номера; канон reviewer'а — `12-review.md`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Machine-verdict доки vs информационные (честный механизм проверки)
|
||||
|
||||
Полностью согласовано с `PIPELINE_DOCS.md` §3. Machine-verdict док — гейт читает ТОЛЬКО
|
||||
YAML-frontmatter (через единый `frontmatter.parse_frontmatter`), маппит ключ в вердикт; имя ключа
|
||||
чувствительно к регистру, значение парсер приводит к верхнему регистру.
|
||||
|
||||
| Документ | Machine-key | Парсер | Эффект вердикта |
|
||||
|----------|-------------|--------|-----------------|
|
||||
| `12-review.md` | `verdict:` | `check_reviewer_verdict` | `APPROVED` → дальше; `REQUEST_CHANGES` → откат на `development` |
|
||||
| `13-test-report.md` | `result:` / `verdict:` / `status:` | `_parse_tests_verdict` | `PASS` → дальше; `FAIL`/`BLOCKED` → откат (негативный токен авторитетен) |
|
||||
| `14-deploy-log.md` | `deploy_status:` | `_parse_deploy_status` | `SUCCESS` → `done`; `FAILED` → откат (БАГ-8) |
|
||||
| `15-staging-log.md` | `staging_status:` | `_parse_staging_status` | `SUCCESS` → дальше; `FAILED` → откат (self-hosting; иначе N/A) |
|
||||
| `17-security-report.md` | `security_status:` | `check_security_gate` → `parse_security_status` | `PASS` → дальше; `FAIL` → откат |
|
||||
|
||||
**Информационные доки** (гейтом НЕ парсятся): `00-business-request.md`, `08-data-requirements.md`,
|
||||
`10-tech-risks.md`, `16-post-deploy-log.md`.
|
||||
|
||||
**Аддитивность схемы (§1).** Документ-вердикт БЕЗ полей схемы из §1, но с вердикт-ключом, читается
|
||||
гейтом РОВНО как раньше: схема не участвует в вычислении вердикта при дефолтном
|
||||
`frontmatter_validation_strict=False`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Единый машинный контракт — `src/frontmatter.py`
|
||||
|
||||
Все операции с frontmatter сведены в один leaf-модуль (never-raise):
|
||||
|
||||
- `read_frontmatter_value(path, key) -> str | None` — single-key reader (контракт неизменен, BC).
|
||||
- `parse_frontmatter(content) -> FrontmatterParse` — **единственная точка** парсинга YAML-frontmatter
|
||||
(`data` / `has_block` / `malformed` / `yaml_error`); пять вердикт-парсеров делегируют сюда.
|
||||
- `parse_frontmatter_dict` / `read_frontmatter` — ярлыки к распарсенному mapping.
|
||||
- `render_frontmatter` / `write_frontmatter` — writer (формат совместим с существующими парсерами).
|
||||
- `validate_schema` / `REQUIRED_FIELDS` / `maybe_warn_schema` — схема §1 (warning-only по умолчанию).
|
||||
- `strip_frontmatter` — общий хелпер тела (заменил дубли).
|
||||
- Kill-switch жёсткой валидации: `config.frontmatter_validation_strict`
|
||||
(env `ORCH_FRONTMATTER_VALIDATION_STRICT`, дефолт `False`).
|
||||
|
||||
> Перед написанием номерного дока бери скелет из [`docs/_templates/`](../_templates/) и **не меняй
|
||||
> имя machine-key frontmatter** (регистр чувствителен — иначе гейт упадёт ложно).
|
||||
153
docs/_standards/PIPELINE_DOCS.md
Normal file
153
docs/_standards/PIPELINE_DOCS.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# PIPELINE_DOCS — стандарт документов конвейера (golden source структуры)
|
||||
|
||||
> **Назначение.** Единая карта «стадия → агент → документ → категория → гейт/механизм →
|
||||
> frontmatter machine-key» + конвенция ADR-naming. Это **golden source структуры** номерных
|
||||
> документов work item (`00-business-request.md` … `17-security-report.md`), который каждая
|
||||
> агентская роль пишет на своей стадии.
|
||||
>
|
||||
> **Статус истины (важно).** Манифест **документирует** текущее поведение гейтов, но НЕ является
|
||||
> их источником истины. Источник истины — код: `src/stages.py` (`STAGE_TRANSITIONS`),
|
||||
> `src/qg/checks.py` (`QG_CHECKS` / `check_*` / `_parse_*`), `src/stage_engine.py`. При будущей
|
||||
> правке гейта первична правка кода, манифест обновляется следом (ORCH-075 / ADR-001 §D2).
|
||||
>
|
||||
> **Копируемые скелеты** каждого документа — в каталоге [`docs/_templates/`](../_templates/):
|
||||
> «скопировал → заполнил → не угадываешь структуру/ключ».
|
||||
|
||||
Введён задачей **ORCH-075** (ORCH-52b — слой 1 эпика ORCH-52). Сквозной ADR:
|
||||
[`docs/architecture/adr/adr-0019-pipeline-docs-standard.md`](../architecture/adr/adr-0019-pipeline-docs-standard.md);
|
||||
детально — `docs/work-items/ORCH-075/06-adr/ADR-001-pipeline-docs-standard.md`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Конвейер стадий (ground-truth `STAGE_TRANSITIONS`)
|
||||
|
||||
```
|
||||
created → analysis → architecture → development → review → testing → deploy-staging → deploy → done
|
||||
↑ │
|
||||
└──── REQUEST_CHANGES ──────┘ (откат на development, max 3 retries)
|
||||
```
|
||||
|
||||
Каждое ребро несёт ровно один exit-гейт (`src/stages.py`):
|
||||
`check_analysis_approved → check_architecture_done → check_ci_green → check_reviewer_verdict →
|
||||
check_tests_passed → check_staging_status → check_deploy_status`.
|
||||
|
||||
**Под-гейты ребра `deploy-staging → deploy`** (`check_security_gate` → `check_branch_mergeable` →
|
||||
`check_staging_image_fresh`) — это **врезки в `advance_stage`**, а НЕ строки `STAGE_TRANSITIONS`.
|
||||
Аналогично под-гейт ребра `deploy → done` (`_handle_merge_verify`, ORCH-071/073) — врезка, не
|
||||
зарегистрированный QG. Карта стадий о них не «лжёт»: они не являются стадиями.
|
||||
|
||||
---
|
||||
|
||||
## 2. Манифест: документ → агент → категория → стадия → гейт → machine-key
|
||||
|
||||
Категории: **required** (пишется всегда), **when-applicable** (пишется при наличии предмета:
|
||||
инфра / данные / security / post-deploy — отсутствие не нарушение), **optional** / **legacy**.
|
||||
|
||||
| Документ | Владелец-агент | Категория | Стадия написания | Гейт / механизм проверки | Frontmatter machine-key |
|
||||
|----------|----------------|-----------|------------------|--------------------------|-------------------------|
|
||||
| `00-business-request.md` | система (Plane webhook `_create_initial_docs`) / заказчик | required | `created` (инициализация) | не гейтится (вход) | — |
|
||||
| `01-brd.md` | analyst | required | `analysis` | exit-гейт `analysis→architecture` = `check_analysis_approved` (Approved + полнота файлов); helper `check_analysis_complete` (наличие `01/02/03/04`) | — |
|
||||
| `02-trz.md` | analyst | required | `analysis` | то же | — |
|
||||
| `03-acceptance-criteria.md` | analyst | required | `analysis` | то же | — |
|
||||
| `04-test-plan.yaml` | analyst | required | `analysis` | то же | — |
|
||||
| `06-adr/ADR-NNN-<slug>.md` | architect | required | `architecture` | `check_architecture_done` (наличие каталога `06-adr/` ≥1 файл ИЛИ `07-infra-requirements.md`) | — |
|
||||
| `07-infra-requirements.md` | architect | when-applicable | `architecture` | `check_architecture_done` (учитывается при наличии) | — |
|
||||
| `08-data-requirements.md` | architect | when-applicable | `architecture` | информационный (гейтом не парсится) | — |
|
||||
| `10-tech-risks.md` | architect | required | `architecture` | информационный (гейтом не парсится) | — |
|
||||
| `12-review.md` | reviewer | required | `review` | `check_reviewer_verdict` | `verdict:` (`APPROVED` \| `REQUEST_CHANGES`) |
|
||||
| `13-test-report.md` | tester | required | `testing` | `check_tests_passed` (`_parse_tests_verdict`) | `result:` / `verdict:` / `status:` (`PASS` \| `FAIL` \| `BLOCKED`; три равноранговых, ORCH-047) |
|
||||
| `14-deploy-log.md` | deployer / deploy-finalizer | required | `deploy` | `check_deploy_status` (`_parse_deploy_status`) | `deploy_status:` (`SUCCESS` \| `FAILED`) |
|
||||
| `15-staging-log.md` | deployer | required (self-hosting) | `deploy-staging` | `check_staging_status` (self-hosting; иначе N/A — ORCH-35) | `staging_status:` (`SUCCESS` \| `FAILED`) |
|
||||
| `16-post-deploy-log.md` | post-deploy-monitor | when-applicable | пост-`done` наблюдение (ORCH-021; не ребро `STAGE_TRANSITIONS`) | информационный (гейтом не парсится) | `post_deploy_status:` (`HEALTHY` \| `DEGRADED`) |
|
||||
| `17-security-report.md` | security-гейт (детерминированный, ORCH-022) | when-applicable | под-гейт ребра `deploy-staging→deploy` | `check_security_gate` (врезка в `advance_stage`) | `security_status:` (`PASS` \| `FAIL`) |
|
||||
|
||||
### Примечания манифеста (нормативные)
|
||||
|
||||
- **Под-гейты ребра `deploy-staging→deploy`** (`check_security_gate` → `check_branch_mergeable` →
|
||||
`check_staging_image_fresh`) исполняются как врезки в `advance_stage`, а НЕ строки
|
||||
`STAGE_TRANSITIONS`. Не путать с exit-гейтами рёбер.
|
||||
- **`09-review.md`** — legacy fallback от старой нумерации; **канон — `12-review.md`**. В основную
|
||||
таблицу как канон не вносится; reviewer пишет `12-review.md`.
|
||||
- **Категория `when-applicable`** = документ пишется при наличии соответствующего предмета
|
||||
(инфра / данные / security / post-deploy). Его отсутствие — не нарушение приёмки.
|
||||
- **`05-…` / `09-…` / `11-…`** — зарезервированные/legacy номера, в текущем каноне не используются.
|
||||
|
||||
---
|
||||
|
||||
## 3. Machine-verdict доки vs информационные (честный механизм проверки)
|
||||
|
||||
**Machine-verdict доки** — гейт читает ТОЛЬКО YAML-frontmatter (никогда прозу), маппит ключ в
|
||||
вердикт. Имя ключа чувствительно к регистру; значение парсер приводит к верхнему регистру.
|
||||
|
||||
| Документ | Machine-key | Парсер (`src/qg/checks.py`) | Эффект вердикта |
|
||||
|----------|-------------|-----------------------------|-----------------|
|
||||
| `12-review.md` | `verdict:` | `check_reviewer_verdict` | `APPROVED` → дальше; `REQUEST_CHANGES` → откат на `development` |
|
||||
| `13-test-report.md` | `result:` / `verdict:` / `status:` | `_parse_tests_verdict` | `PASS` → дальше; `FAIL`/`BLOCKED` → откат |
|
||||
| `14-deploy-log.md` | `deploy_status:` | `_parse_deploy_status` | `SUCCESS` → `done`; `FAILED` → откат (БАГ-8) |
|
||||
| `15-staging-log.md` | `staging_status:` | `_parse_staging_status` | `SUCCESS` → дальше; `FAILED` → откат (self-hosting; иначе N/A) |
|
||||
| `17-security-report.md` | `security_status:` | `check_security_gate` | `PASS` → дальше; `FAIL` → откат |
|
||||
|
||||
**Информационные доки** — гейтом НЕ парсятся (структура ничего не блокирует):
|
||||
`00-business-request.md` (вход), `08-data-requirements.md`, `10-tech-risks.md`,
|
||||
`16-post-deploy-log.md` (несёт `post_deploy_status:`, но это телеметрия петли уроков ORCH-8 /
|
||||
наблюдаемость, не гейт).
|
||||
|
||||
---
|
||||
|
||||
## 4. Конвенция ADR-naming
|
||||
|
||||
### Per-work-item ADR (основное)
|
||||
|
||||
- **Путь:** `docs/work-items/<plane-id>/06-adr/`
|
||||
- **Имя файла:** `ADR-NNN-<kebab-slug>.md`
|
||||
- `NNN` — 3-значный, начинается с `001`; инкремент при нескольких ADR в одной задаче
|
||||
(`ADR-001-…`, `ADR-002-…`).
|
||||
- `<kebab-slug>` — kebab-case (нижний регистр, слова через дефис), отражает суть решения.
|
||||
- **Стадия:** пишет **architect** на стадии `architecture`; гейтится `check_architecture_done`
|
||||
(наличие каталога `06-adr/` ≥ 1 файла).
|
||||
|
||||
### Сквозной (cross-cutting) ADR
|
||||
|
||||
Решения, затрагивающие несколько компонентов/ролей или поведение всего конвейера, **дублируются**
|
||||
в глобальный реестр:
|
||||
|
||||
- **Путь:** `docs/architecture/adr/`
|
||||
- **Имя файла:** `adr-NNNN-<kebab-slug>.md` (4-значная сквозная нумерация, последовательная по
|
||||
всему репозиторию; на момент ORCH-075 реестр доходит до `adr-0019`).
|
||||
|
||||
### Примеры из репозитория (реальные, проверенные)
|
||||
|
||||
- `docs/work-items/ORCH-088/06-adr/ADR-001-serial-gate.md`
|
||||
- `docs/work-items/ORCH-089/06-adr/ADR-001-auto-label-gates.md`
|
||||
- `docs/work-items/ORCH-071/06-adr/ADR-001-merge-verify-gate.md`
|
||||
- Сквозные: `docs/architecture/adr/adr-0017-serial-gate.md`,
|
||||
`docs/architecture/adr/adr-0018-auto-label-gates.md`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Как пользоваться шаблонами
|
||||
|
||||
1. Скопируй нужный скелет из [`docs/_templates/`](../_templates/) в
|
||||
`docs/work-items/<plane-id>/` под канонным именем (для ADR — `06-adr/ADR-001-<slug>.md`).
|
||||
2. Заполни секции; **не удаляй** machine-key frontmatter у machine-verdict доков и **не меняй имя
|
||||
ключа** (регистр чувствителен — иначе гейт упадёт ложно).
|
||||
3. Сверяйся с манифестом (§2–§3): какой агент, на какой стадии, какой гейт читает документ.
|
||||
|
||||
> Стандарт **описательный** (слой 1). **Машинный слой реализован в ORCH-52c (ORCH-076):** единый
|
||||
> frontmatter-контракт (reader + writer + валидатор) в [`src/frontmatter.py`](../../src/frontmatter.py)
|
||||
> и формальная спека handoff [`HANDOFF_PROTOCOL.md`](HANDOFF_PROTOCOL.md) («стадия → обязательный
|
||||
> выход» + обязательная frontmatter-схема `REQUIRED_FIELDS`). Пять вердикт-парсеров
|
||||
> (`check_reviewer_verdict`, `_parse_tests_verdict`, `_parse_deploy_status`, `_parse_staging_status`,
|
||||
> `parse_security_status`) читают вердикт через ОДНУ точку парсинга (`parse_frontmatter`); семантика
|
||||
> вердиктов 1:1. Валидатор обязательной схемы по умолчанию **warning-only** (kill-switch
|
||||
> `frontmatter_validation_strict`, дефолт `False`) — соблюдение схемы пока на ответственности агента
|
||||
> и reviewer'а, enforcement придёт с ORCH-52d.
|
||||
|
||||
---
|
||||
|
||||
## 6. Спека handoff (машинный контракт, ORCH-52c)
|
||||
|
||||
Вертикальный срез «стадия → обязательные документы + frontmatter-ключи на выходе» и обязательная
|
||||
frontmatter-схема вынесены в отдельную нормативную спеку [`HANDOFF_PROTOCOL.md`](HANDOFF_PROTOCOL.md)
|
||||
(набор документов/ключей/гейтов согласован 1:1 с §2–§3 этого манифеста). Машинный источник
|
||||
обязательной схемы — `frontmatter.REQUIRED_FIELDS`.
|
||||
8
docs/_templates/00-business-request.md
vendored
Normal file
8
docs/_templates/00-business-request.md
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# Business Request: <краткий заголовок задачи>
|
||||
|
||||
Work Item ID: ORCH-NNN
|
||||
|
||||
## Description
|
||||
|
||||
<Что хочет заказчик/Владелец своими словами: проблема, желаемый результат, контекст.
|
||||
Допускается `TBD` на входе — analyst уточняет на стадии `analysis` и формализует в 01-brd.md.>
|
||||
34
docs/_templates/01-brd.md
vendored
Normal file
34
docs/_templates/01-brd.md
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
# 01 — BRD (бизнес-требования): ORCH-NNN — <название>
|
||||
|
||||
Work Item: **ORCH-NNN** · Repo: **<repo>** · Стадия: analysis
|
||||
|
||||
## 1. Бизнес-контекст и проблема
|
||||
<Зачем задача, какую боль/риск закрывает. Установленные факты — не изобретать.>
|
||||
|
||||
## 2. Объём (scope)
|
||||
|
||||
### В объёме
|
||||
- <что делаем>
|
||||
|
||||
### Вне объёма
|
||||
- <что явно НЕ делаем — чтобы исключить расползание>
|
||||
|
||||
## 3. Заинтересованные стороны
|
||||
<Кто заказчик, кого затрагивает, кто принимает результат.>
|
||||
|
||||
## 4. Бизнес-требования (BR)
|
||||
- **BR-1** — <требование, проверяемое>
|
||||
- **BR-2** — …
|
||||
|
||||
## 5. Нефункциональные требования (NFR)
|
||||
- **NFR-1** — <надёжность / совместимость / обратимость / безопасность>
|
||||
- **NFR-2** — …
|
||||
|
||||
## 6. Допущения и ограничения
|
||||
<Допущения, на которых стоит решение; внешние ограничения.>
|
||||
|
||||
## 7. Критерии успеха
|
||||
<Резюме; детальные PASS/FAIL — в 03-acceptance-criteria.md.>
|
||||
|
||||
## 8. Риски
|
||||
<Краткий перечень; детали — 10-tech-risks.md (заполняет архитектор).>
|
||||
30
docs/_templates/02-trz.md
vendored
Normal file
30
docs/_templates/02-trz.md
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
# 02 — ТЗ (TRZ): ORCH-NNN — <название>
|
||||
|
||||
Work Item: **ORCH-NNN** · Repo: **<repo>** · Стадия: analysis
|
||||
|
||||
> ТЗ описывает **конкретные изменения к реализации**, выведенные из BRD и фактического кода.
|
||||
> Архитектурное обоснование/решения — задача архитектора (06-adr).
|
||||
|
||||
## 1. Сводка изменения
|
||||
<Что меняется, в одном-двух абзацах.>
|
||||
|
||||
## 2. Задействованные модули / пути
|
||||
| Путь | Действие |
|
||||
|------|----------|
|
||||
| `src/<module>.py` | изменить / создать |
|
||||
|
||||
## 3. Функциональные требования
|
||||
### FR-1 — <название>
|
||||
<Поведение, контракт, инварианты. Привязать к BR.>
|
||||
|
||||
## 4. Изменения API
|
||||
<Новые/изменённые эндпоинты; либо «Нет.».>
|
||||
|
||||
## 5. Изменения схемы БД
|
||||
<Таблицы/миграции/индексы; либо «Нет.».>
|
||||
|
||||
## 6. Требования к новым/изменённым QG checks
|
||||
<Изменения `QG_CHECKS` / `check_*`; либо «Нет.».>
|
||||
|
||||
## 7. Совместимость / регресс
|
||||
<Обратная совместимость, kill-switch, область раската, обратимость.>
|
||||
31
docs/_templates/03-acceptance-criteria.md
vendored
Normal file
31
docs/_templates/03-acceptance-criteria.md
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
# 03 — Критерии приёмки (Acceptance Criteria): ORCH-NNN — <название>
|
||||
|
||||
Work Item: **ORCH-NNN** · Repo: **<repo>** · Стадия: analysis
|
||||
|
||||
Формат: каждый критерий имеет **PASS** (что должно быть истинно для приёмки) и **FAIL**
|
||||
(что считается провалом). Любой машинный/ручной reviewer проверяет их буквально по файлам
|
||||
репозитория.
|
||||
|
||||
---
|
||||
|
||||
## AC-1 — <краткий заголовок>
|
||||
|
||||
**Условие:** <проверяемое условие>
|
||||
- **PASS:** <что должно быть истинно>
|
||||
- **FAIL:** <что считается провалом>
|
||||
|
||||
---
|
||||
|
||||
## AC-2 — <краткий заголовок>
|
||||
|
||||
**Условие:** <…>
|
||||
- **PASS:** <…>
|
||||
- **FAIL:** <…>
|
||||
|
||||
---
|
||||
|
||||
## Сводная матрица AC ↔ FR/BR
|
||||
| AC | Покрывает |
|
||||
|----|-----------|
|
||||
| AC-1 | BR-1 / FR-1 |
|
||||
| AC-2 | BR-2 / FR-2 |
|
||||
20
docs/_templates/04-test-plan.yaml
vendored
Normal file
20
docs/_templates/04-test-plan.yaml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
work_item: ORCH-NNN
|
||||
title: "<краткое название тест-плана>"
|
||||
framework: pytest
|
||||
scope: "<что покрывается тестами; что вне покрытия>"
|
||||
notes: >
|
||||
<Свободные заметки: окружение, особенности, что считается регрессом.
|
||||
Полный регресс tests/ должен оставаться зелёным.>
|
||||
|
||||
tests:
|
||||
- id: TC-01
|
||||
type: unit # unit | integration
|
||||
description: "<что проверяет тест>"
|
||||
module: tests/test_<feature>.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-02
|
||||
type: integration
|
||||
description: "<…>"
|
||||
module: tests/test_<feature>.py
|
||||
expected: PASS
|
||||
43
docs/_templates/06-adr-ADR-NNN-slug.md
vendored
Normal file
43
docs/_templates/06-adr-ADR-NNN-slug.md
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
# ADR-NNN: <Заголовок решения>
|
||||
|
||||
> **Шаблон ADR.** Скопируй в `docs/work-items/<plane-id>/06-adr/ADR-NNN-<kebab-slug>.md`.
|
||||
> `NNN` начинается с `001`, инкремент при нескольких ADR в задаче. `<kebab-slug>` — нижний
|
||||
> регистр, слова через дефис. Сквозное (cross-cutting) решение дополнительно дублируй в
|
||||
> `docs/architecture/adr/adr-NNNN-<kebab-slug>.md` (4-значная глобальная нумерация).
|
||||
> См. `docs/_standards/PIPELINE_DOCS.md` §4.
|
||||
|
||||
Work Item: **ORCH-NNN** — <короткое описание>
|
||||
Стадия: **architecture**
|
||||
Сквозная регистрация: **`docs/architecture/adr/adr-NNNN-<slug>.md`** (если решение
|
||||
кросс-каттинговое; иначе — «N/A, локальное решение задачи»).
|
||||
|
||||
## Статус
|
||||
Proposed <!-- Proposed | Accepted | Superseded by ADR-… -->
|
||||
|
||||
## Контекст
|
||||
<Какую проблему решаем; факты, сверенные с кодом (`src/…`); почему «как есть» не годится.>
|
||||
|
||||
## Решение
|
||||
|
||||
### Сводка
|
||||
<Суть выбранного решения в одном-двух абзацах.>
|
||||
|
||||
### D1 — <название аспекта решения>
|
||||
<Конкретное решение по аспекту, инварианты, привязка к FR/AC.>
|
||||
|
||||
### D2 — <название аспекта решения>
|
||||
<…>
|
||||
|
||||
## Альтернативы
|
||||
- **<альтернатива>** — отвергнуто: <почему>.
|
||||
|
||||
## Последствия
|
||||
- **+** <положительный эффект>
|
||||
- **−** <издержка / приятый компромисс + митигейшн>
|
||||
- **Откат:** <как полностью откатить изменение>
|
||||
|
||||
## Ссылки
|
||||
- BRD: `docs/work-items/ORCH-NNN/01-brd.md`
|
||||
- TRZ: `docs/work-items/ORCH-NNN/02-trz.md`
|
||||
- Acceptance: `docs/work-items/ORCH-NNN/03-acceptance-criteria.md`
|
||||
- Сверено по коду: `src/…`
|
||||
19
docs/_templates/07-infra-requirements.md
vendored
Normal file
19
docs/_templates/07-infra-requirements.md
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# 07 — Инфра-требования: ORCH-NNN — <название>
|
||||
|
||||
Work Item: **ORCH-NNN** · Repo: **<repo>** · Стадия: architecture
|
||||
|
||||
> When-applicable. Если инфраструктура не затрагивается — оставить явные `N/A` по пунктам
|
||||
> (файл создаётся для аудитопригодности, а не из-за изменения топологии).
|
||||
|
||||
## I-1. Топология / окружения
|
||||
<Контейнеры, порты, сеть, тома, хост; либо `N/A`.>
|
||||
|
||||
## I-2. Переменные окружения / секреты
|
||||
<Новые env-переменные, изменения `.env` / `.env.example`, секреты; либо `N/A`.>
|
||||
|
||||
## I-3. Деплой / рестарт
|
||||
<Требуется ли рестарт прод-контейнера; self-hosting инвариант (не ронять прод вне staging);
|
||||
либо `N/A`.>
|
||||
|
||||
## I-4. CI/CD
|
||||
<Изменения `.gitea/workflows/`, новые тестовые шаги; либо «без изменений».>
|
||||
15
docs/_templates/08-data-requirements.md
vendored
Normal file
15
docs/_templates/08-data-requirements.md
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# 08 — Требования к данным: ORCH-NNN — <название>
|
||||
|
||||
Work Item: **ORCH-NNN** · Repo: **<repo>** · Стадия: architecture
|
||||
|
||||
> When-applicable / информационный (гейтом не парсится). Если данные/схема не затрагиваются —
|
||||
> оставить явные `N/A`.
|
||||
|
||||
## Изменения схемы БД
|
||||
<Новые/изменённые таблицы, индексы, миграции (`init_db`); либо `N/A`.>
|
||||
|
||||
## Новые/изменённые сущности
|
||||
<Поля, колонки, инварианты данных; либо «Нет.».>
|
||||
|
||||
## Совместимость данных / миграции
|
||||
<Аддитивность, идемпотентность миграций, restart-safe, влияние на общую прод-БД; либо `N/A`.>
|
||||
16
docs/_templates/10-tech-risks.md
vendored
Normal file
16
docs/_templates/10-tech-risks.md
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
# 10 — Технические риски: ORCH-NNN — <название>
|
||||
|
||||
Work Item: **ORCH-NNN** · Repo: **<repo>** · Стадия: architecture
|
||||
|
||||
> Информационный (гейтом не парсится). Перечисляет риски реализации и их митигейшн.
|
||||
|
||||
## Реестр рисков
|
||||
|
||||
| ID | Риск | Вер. | Влия. | Митигейшн |
|
||||
|----|------|------|-------|-----------|
|
||||
| TR-1 | <описание риска> | Низ./Сред./Выс. | Низ./Сред./Выс. | <как снижаем> |
|
||||
| TR-2 | <…> | | | |
|
||||
|
||||
## Сводный вывод
|
||||
<Доминирующий класс рисков; нужна ли эскалация `arch:major-change` / возврат в анализ;
|
||||
итоговая оценка остаточного риска для прод-конвейера (self-hosting).>
|
||||
31
docs/_templates/12-review.md
vendored
Normal file
31
docs/_templates/12-review.md
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
type: review
|
||||
work_item_id: ORCH-NNN
|
||||
verdict: APPROVED # APPROVED | REQUEST_CHANGES (machine-key — читает check_reviewer_verdict)
|
||||
version: 1
|
||||
---
|
||||
|
||||
# Review ORCH-NNN
|
||||
|
||||
> Машинный вердикт читается ТОЛЬКО из `verdict:` во frontmatter (никогда из прозы).
|
||||
> `APPROVED` → дальше по конвейеру; `REQUEST_CHANGES` → откат на `development`.
|
||||
|
||||
## Summary
|
||||
<Краткая оценка: реализовано ли по ТЗ/ADR, покрытие тестами, обновлена ли документация.>
|
||||
|
||||
## Оси проверки
|
||||
<Корректность, соответствие ADR/инвариантам, тесты, документация, совместимость/регресс.>
|
||||
|
||||
## Findings
|
||||
|
||||
### P0 — Blocker
|
||||
- (нет)
|
||||
|
||||
### P1 — Must fix
|
||||
- (нет)
|
||||
|
||||
### P2 — Should fix
|
||||
- (нет)
|
||||
|
||||
## Документация
|
||||
<Обновлена ли документация (README/CLAUDE/CHANGELOG/архитектура) в том же PR. Нет → REQUEST_CHANGES.>
|
||||
33
docs/_templates/13-test-report.md
vendored
Normal file
33
docs/_templates/13-test-report.md
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
type: test-report
|
||||
work_item_id: ORCH-NNN
|
||||
result: PASS # PASS | FAIL | BLOCKED (machine-key — читает _parse_tests_verdict)
|
||||
---
|
||||
|
||||
# Test Report — ORCH-NNN
|
||||
|
||||
> Машинный вердикт читается ТОЛЬКО из frontmatter. Канонический ключ — `result:`; равнорангово
|
||||
> допускаются `verdict:` / `status:` (ORCH-047). Любой негативный токен (`FAIL`/`BLOCKED`) —
|
||||
> авторитетен.
|
||||
|
||||
## Окружение
|
||||
- Python: <версия>
|
||||
- pytest: <версия>
|
||||
- Дата: YYYY-MM-DD
|
||||
- Worktree: `feature/ORCH-NNN-<slug>`
|
||||
|
||||
## Результаты
|
||||
|
||||
### Полный регресс
|
||||
<`pytest tests/ -q` — итог (N passed); прод-контейнер не трогается.>
|
||||
|
||||
### Профильные сюиты
|
||||
<Целевые тесты задачи.>
|
||||
|
||||
### Сопоставление с тест-планом
|
||||
| TC ID | Описание | Тест-функция | Результат |
|
||||
|-------|----------|--------------|-----------|
|
||||
| TC-01 | <…> | test_… | PASS |
|
||||
|
||||
### Сопоставление с критериями приёмки
|
||||
<AC-1…AC-N — покрыт каким тестом / результат.>
|
||||
14
docs/_templates/14-deploy-log.md
vendored
Normal file
14
docs/_templates/14-deploy-log.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
deploy_status: SUCCESS # SUCCESS | FAILED (machine-key — читает _parse_deploy_status)
|
||||
work_item: ORCH-NNN
|
||||
hook_exit_code: 0
|
||||
deployed_by: deploy-finalizer
|
||||
---
|
||||
|
||||
# Deploy log — ORCH-NNN
|
||||
|
||||
> Машинный вердикт читается ТОЛЬКО из `deploy_status:` во frontmatter.
|
||||
> `SUCCESS` → `done`; `FAILED` → откат на `development` (БАГ-8).
|
||||
|
||||
<Краткое описание деплоя: что выкачено, exit-code хука, кто/что зафиксировало вердикт
|
||||
(детерминированный finalizer Фаза C, не LLM, для self-hosting).>
|
||||
20
docs/_templates/15-staging-log.md
vendored
Normal file
20
docs/_templates/15-staging-log.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
staging_status: SUCCESS # SUCCESS | FAILED (machine-key — читает _parse_staging_status)
|
||||
timestamp: YYYY-MM-DDTHH:MM:SSZ
|
||||
base_url: http://localhost:8501
|
||||
---
|
||||
|
||||
# Staging Gate Log
|
||||
|
||||
> Машинный вердикт читается ТОЛЬКО из `staging_status:` во frontmatter. Реален для self-hosting
|
||||
> (`orchestrator`); для прочих репо гейт — N/A (ORCH-35). `SUCCESS` → дальше; `FAILED` → откат.
|
||||
|
||||
Staging test suite — итог (например: «All REAL pipeline checks passed»). Запуск канонически
|
||||
внутри контейнера `orchestrator-staging` (8501).
|
||||
|
||||
## Results
|
||||
- **Block A (SMOKE)**: <…>
|
||||
- **Block B (ACCESS)**: <…>
|
||||
- **Block C (E2E)**: <…>
|
||||
|
||||
REAL failed: <none | перечень>.
|
||||
21
docs/_templates/16-post-deploy-log.md
vendored
Normal file
21
docs/_templates/16-post-deploy-log.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
post_deploy_status: HEALTHY # HEALTHY | DEGRADED (информационный, гейтом НЕ парсится — телеметрия ORCH-021)
|
||||
action_taken: NONE # NONE | ALERT_ONLY | ROLLBACK_OK | ROLLBACK_FAILED
|
||||
work_item: ORCH-NNN
|
||||
window_s: 900
|
||||
checks_total: 0
|
||||
checks_failed: 0
|
||||
---
|
||||
|
||||
# Post-deploy log — ORCH-NNN
|
||||
|
||||
> Пост-`done` наблюдение прода (ORCH-021). НЕ ребро `STAGE_TRANSITIONS`, гейтом не парсится —
|
||||
> frontmatter машиночитаем для петли уроков ORCH-8 / наблюдаемости.
|
||||
|
||||
Окно наблюдения: <window_s>s; опросов всего: <checks_total>, с провалом: <checks_failed>.
|
||||
|
||||
## Серия наблюдений
|
||||
<Краткая серия сигналов health / доли 5xx; классификация HEALTHY/DEGRADED.>
|
||||
|
||||
## Решение
|
||||
<Реакция: для self-hosting всегда `ALERT_ONLY` (ручной approve, тик не откатывает прод).>
|
||||
26
docs/_templates/17-security-report.md
vendored
Normal file
26
docs/_templates/17-security-report.md
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
security_status: PASS # PASS | FAIL (machine-key — читает check_security_gate)
|
||||
work_item: ORCH-NNN
|
||||
secrets_found: 0
|
||||
deps_blocking: 0
|
||||
deps_warning: 0
|
||||
deps_audit_degraded: false
|
||||
---
|
||||
|
||||
# Security Report — ORCH-NNN
|
||||
|
||||
> Детерминированный security-гейт (ORCH-022) — под-гейт ребра `deploy-staging→deploy` (врезка в
|
||||
> `advance_stage`, не строка `STAGE_TRANSITIONS`). Машинный вердикт читается ТОЛЬКО из
|
||||
> `security_status:`. `PASS` → дальше; `FAIL` → откат.
|
||||
|
||||
## Verdict
|
||||
<clean / blocking: N secrets, M blocking CVE(s).>
|
||||
|
||||
## Secrets
|
||||
<secret-scanning (gitleaks, offline): None | перечень.>
|
||||
|
||||
## Dependencies (blocking)
|
||||
<dependency audit (pip-audit): None | перечень блокирующих CVE.>
|
||||
|
||||
## Dependencies (warning)
|
||||
<Не блокирующие предупреждения зависимостей.>
|
||||
@@ -39,7 +39,23 @@ created → analysis → architecture → development → review → testing →
|
||||
|
||||
**Реестр QG** (`QG_CHECKS`): check_analysis_approved, check_analysis_complete, check_architecture_done, check_ci_green, check_review_approved, check_tests_passed, check_reviewer_verdict, check_tests_local, check_deploy_status, check_staging_status, check_branch_mergeable (ORCH-043), check_staging_image_fresh (ORCH-058), check_security_gate (ORCH-022).
|
||||
|
||||
**Канон гейтов:** машинные вердикты читаются ТОЛЬКО из YAML-frontmatter, никогда из прозы. Лог-файлы мержатся в `origin/main` отдельным PR; гейт читает из `origin/main`.
|
||||
**Канон гейтов:** машинные вердикты читаются ТОЛЬКО из YAML-frontmatter, никогда из прозы. Лог-файлы мержатся в `origin/main` отдельным PR; гейт читает из `origin/main`. **Единый frontmatter-контракт (ORCH-52c / ORCH-076):** парсинг YAML-frontmatter сведён к одной точке — `src/frontmatter.parse_frontmatter` (структура `data/has_block/malformed/yaml_error`, never-raise); пять вердикт-парсеров (`check_reviewer_verdict`, `_parse_tests_verdict`, `_parse_deploy_status`, `_parse_staging_status`, `parse_security_status`) делегируют ей вместо дублированной ad-hoc логики. Модуль также несёт writer (`render/write_frontmatter`), валидатор обязательной схемы (`validate_schema`/`REQUIRED_FIELDS`, warning-only по умолчанию; hard-fail только под kill-switch `frontmatter_validation_strict`, дефолт `False`) и общий `strip_frontmatter`. Семантика вердиктов / `STAGE_TRANSITIONS` / состав `QG_CHECKS` — без изменений (1:1).
|
||||
|
||||
### Стандарт документов конвейера (ORCH-075, ORCH-52b)
|
||||
Структура номерных документов work item (`00-business-request.md` … `17-security-report.md`),
|
||||
карта «стадия → агент → документ → категория → гейт/механизм → frontmatter machine-key» и
|
||||
конвенция ADR-naming зафиксированы как golden source в
|
||||
[`docs/_standards/PIPELINE_DOCS.md`](../_standards/PIPELINE_DOCS.md); копируемые скелеты — в
|
||||
[`docs/_templates/`](../_templates/). Манифест **документирует** поведение гейтов (источник истины
|
||||
остаётся код: `src/stages.py`, `src/qg/checks.py`), честно различая machine-verdict доки
|
||||
(`12/13/14/15/17` — несут читаемый гейтом ключ) и информационные (`00/08/10/16` — гейтом не
|
||||
парсятся). Это слой 1 (описательный). **Слой 2 (машинный) реализован в ORCH-52c (ORCH-076):**
|
||||
единый frontmatter-контракт `src/frontmatter.py` + формальная спека handoff «стадия → обязательный
|
||||
выход» с обязательной frontmatter-схемой (`REQUIRED_FIELDS`) —
|
||||
[`docs/_standards/HANDOFF_PROTOCOL.md`](../_standards/HANDOFF_PROTOCOL.md). ADR:
|
||||
[adr-0019](adr/adr-0019-pipeline-docs-standard.md) / [adr-0020](adr/adr-0020-frontmatter-contract.md),
|
||||
детально — `docs/work-items/ORCH-075/06-adr/ADR-001-pipeline-docs-standard.md`,
|
||||
`docs/work-items/ORCH-076/06-adr/ADR-001-frontmatter-contract.md`.
|
||||
|
||||
### Модель и эффорт по ролям (ORCH-41, валидация ORCH-74)
|
||||
Модель и `--effort` каждого агента берутся из config (`src/config.py`), резолвятся `launcher.resolve_agent_model` / `resolve_agent_effort` по приоритету **project-override (`projects_json` `agent_models`/`agent_efforts`) > `ORCH_AGENT_MODEL_<AGENT>`/`ORCH_AGENT_EFFORT_<AGENT>` > `*_default` > CLI-дефолт (без флага)**. **Эффорт (ORCH-081):** ниже `*_default` добавлен непустой **per-role floor** — class-default поля `agent_effort_<role>` из `config.py` (его пустой env перебить не может). Floor — строго последний уровень (ниже default) и срабатывает ТОЛЬКО когда все уровни пусты, поэтому пустые прод-`ORCH_AGENT_EFFORT_*=` (которые pydantic трактует как явное `''` и обнуляют дефолт) больше не приводят к запуску без `--effort`: каждая роль получает свой канонический пол (developer=`xhigh`, tester/deployer=`medium`, прочие=`high`). Непустой явный конфиг по-прежнему побеждает floor; опечатка вне `VALID_EFFORTS` дропается валидацией ДО floor (never-break, не маскируется). См. `docs/work-items/ORCH-081/06-adr/ADR-001-effort-resolution-floor.md`. frontmatter `model:` в `.openclaw/agents/*.md` **удалён** (ORCH-74 G1) — он был мёртвой/лживой декларацией (launcher его не читает); config — единственный источник правды о модели. Model-routing (G3) НЕ включён — все 6 агентов на `claude-opus-4-8`.
|
||||
@@ -130,6 +146,46 @@ Self-hosting зацикливался на `deploy-staging`: `scripts/staging_ch
|
||||
`docs/work-items/ORCH-088/06-adr/ADR-001-serial-gate.md`,
|
||||
`docs/work-items/ORCH-088/08-data-requirements.md`.
|
||||
|
||||
### Авто-режим по лейблам: autoApprove + autoDeploy (ORCH-089 — реализовано)
|
||||
Конвейер имеет два **человеческих** гейта, тормозящих пакетный автономный прогон (эпик
|
||||
ORCH-088): гейт BRD (`analysis`: ждёт ручного `Approved`) и гейт прод-деплоя (`deploy`:
|
||||
Phase A ждёт ручного `Confirm Deploy`, ORCH-059). ORCH-089 снимает **только эти два
|
||||
человеческих решения** — выборочно (лейбл Plane на задаче), декларативно, обратимо, **не
|
||||
трогая ни одной технической проверки**. Аддитивно, по образцу условных под-гейтов
|
||||
(ORCH-035/043/058/059/088): leaf `src/labels.py` (never-raise) + точечные врезки + флаги;
|
||||
`STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / схема БД — **не трогаются**.
|
||||
- **`autoApprove`** → врезка в `stage_engine._handle_analysis_approved_flow` (ветка
|
||||
`files_ok`) после `In Review`+коммента: `set_issue_approved` (индикация) +
|
||||
лог/Telegram/Plane-коммент + `advance_stage(..., finished_agent=None)` — **тот же путь, что
|
||||
человеческий Approved** (`approved-via-status` → `analysis → architecture` +
|
||||
`mark_brd_review_ended`). Без дублирования переходной логики.
|
||||
- **`autoDeploy`** → врезка в `stage_engine._handle_self_deploy_phase_a` сразу после advance
|
||||
на `deploy` + `clear_state`: лог/Telegram/Plane-коммент + `_handle_self_deploy_phase_b(...)`
|
||||
(idempotency-маркер `INITIATED`, статус `Deploying`, finalizer). Пропускаются лишь
|
||||
индикативно-человеческие шаги (`Awaiting Deploy` + «ask-human»). **BR-5 структурно:** Phase A
|
||||
достигается только после зелёных под-гейтов ребра `deploy-staging → deploy` (security →
|
||||
merge-gate → image-freshness → staging) → autoDeploy физически не деплоит сломанное.
|
||||
- **Чтение лейблов** — `plane_sync.fetch_issue_labels` (поле `labels` issue, `None` при
|
||||
ошибке ≠ `[]`) + `get_project_labels` (`{normalized_name→uuid}`, TTL-кэш по образцу
|
||||
`get_project_states`); сопоставление по нормализованному имени (`strip().casefold()`),
|
||||
неоднозначность → «нет лейбла». Источник истины — Plane API, не payload вебхука. Новый
|
||||
сеттер `set_issue_approved` (ключ `approved` уже в `_DEFAULT_STATES`).
|
||||
- **Флаги** (`config.py`): `auto_label_enabled` (kill-switch), `auto_approve_label`/
|
||||
`auto_deploy_label`, `auto_label_repos` (CSV; **пусто → self-hosting only**),
|
||||
`auto_label_states_ttl_s`. `applies(repo)` (локальный) проверяется ПЕРВЫМ; `has_label`
|
||||
(сеть) — только при `applies==True` → при выключенном флаге нулевой сетевой оверхед,
|
||||
нулевая регрессия для enduro.
|
||||
- **Fail-safe (never auto):** любая ошибка/недоступность Plane/неоднозначность →
|
||||
«нет авто» → ручной гейт (never-raise). **Идемпотентность:** autoApprove — advance один раз
|
||||
(поздний Approved/F-2 видят `architecture`); autoDeploy — маркер `INITIATED`. **Прозрачность
|
||||
(AC-7):** лог + Telegram + Plane-коммент + live-карточка; блок `auto_labels` в `GET /queue`.
|
||||
- **Инфра-предусловие:** создать лейблы `autoApprove`/`autoDeploy` в Plane-проекте ORCH
|
||||
(labels API); их отсутствие = `has_label` False = ручной режим (fail-safe).
|
||||
|
||||
Подробнее: [adr-0018](adr/adr-0018-auto-label-gates.md), детально —
|
||||
`docs/work-items/ORCH-089/06-adr/ADR-001-auto-label-gates.md`,
|
||||
`docs/work-items/ORCH-089/07-infra-requirements.md`.
|
||||
|
||||
### Исполняемый самодеплой стадии `deploy` (ORCH-36)
|
||||
`deploy` перестаёт быть «бумажной»: для self-hosting (`is_self_hosting_repo`) стадия
|
||||
РЕАЛЬНО деплоит прод (8500) через хост-хук `scripts/orchestrator-deploy-hook.sh`,
|
||||
@@ -590,7 +646,7 @@ Monitoring after Deploy → Done
|
||||
```
|
||||
|
||||
- **Длительность** считается launcher'ом (`_monitor_agent`) и пробрасывается в `_post_usage_comments`; для analyst (коммент строится в `stage_engine`) используется DB-фоллбэк `usage.get_agent_duration(task_id, agent)`.
|
||||
- **Vердикт-парсер** — `src/frontmatter.read_frontmatter_value(...)` (defensive, никогда не raise). Машинные ключи: reviewer → `verdict:` (12-review.md); **testing-гейт `check_tests_passed` (13-test-report.md) → любое из трёх равноправных: `result:` (канон промпта тестера), `verdict:`, `status:`** (ORCH-047, ADR-001); deployer → `deploy_status:` (14-deploy-log.md), `staging_status:` (15-staging-log.md). Negative-токен в любом поле авторитетен (перебивает positive).
|
||||
- **Vердикт-парсер** — единый контракт `src/frontmatter.py` (defensive, never-raise): коммент-хелпер использует `read_frontmatter_value(...)` (single-key, BC), гейты — `parse_frontmatter(...)` (ORCH-52c). Машинные ключи: reviewer → `verdict:` (12-review.md); **testing-гейт `check_tests_passed` (13-test-report.md) → любое из трёх равноправных: `result:` (канон промпта тестера), `verdict:`, `status:`** (ORCH-047, ADR-001); deployer → `deploy_status:` (14-deploy-log.md), `staging_status:` (15-staging-log.md). Negative-токен в любом поле авторитетен (перебивает positive).
|
||||
- Формат коммента **не** меняет реестр гейтов и стадий; коммент — отображение, не управление.
|
||||
|
||||
## База данных (SQLite)
|
||||
|
||||
@@ -23,13 +23,17 @@ Per-work-item решения живут в `docs/work-items/<id>/06-adr/ADR-NNN-
|
||||
| adr-0015 | Зависимости задач (B ждёт A) + сериализация merge внутри репо | accepted | 2026-06-08 | ORCH-026 |
|
||||
| adr-0016 | ensure_open_pr — гарантированный код-PR перед merge-verify | accepted | 2026-06-09 | ORCH-082 |
|
||||
| adr-0017 | Per-repo serial gate (пакетный автономный режим, serial e2e) | proposed | 2026-06-09 | ORCH-088 |
|
||||
| adr-0018 | Авто-режим по лейблам (autoApprove + autoDeploy) | accepted | 2026-06-09 | ORCH-089 |
|
||||
| adr-0019 | Стандарт документов конвейера (PIPELINE_DOCS, слой 1) | accepted | 2026-06-09 | ORCH-075 |
|
||||
| adr-0020 | Единый frontmatter-контракт + спека handoff (reader/writer/валидатор) | accepted | 2026-06-09 | ORCH-076 |
|
||||
|
||||
> ⚠️ Историческая коллизия: номер `0007` занят двумя файлами —
|
||||
> `adr-0007-reconciler.md` (ORCH-053) и `adr-0007-executable-self-deploy.md`
|
||||
> (ORCH-036). Оба accepted; для новых сквозных ADR использовать следующий
|
||||
> свободный номер (текущий максимум — `0017`).
|
||||
> свободный номер (текущий максимум — `0020`).
|
||||
> adr-0014 **amends** adr-0013 (меняет критерий merge-verify на «SHA-в-main»).
|
||||
> adr-0016 **amends** adr-0013/0014 (гарантирует открытый код-PR перед merge_pr, ORCH-082).
|
||||
> adr-0020 реализует машинный слой к adr-0019 (ORCH-52b→52c).
|
||||
|
||||
## Формат
|
||||
**Контекст → Решение → Альтернативы → Последствия → Связи.** Статус: proposed / accepted / superseded.
|
||||
|
||||
59
docs/architecture/adr/adr-0018-auto-label-gates.md
Normal file
59
docs/architecture/adr/adr-0018-auto-label-gates.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# ADR-0018: Авто-режим по лейблам — autoApprove / autoDeploy (ORCH-089)
|
||||
|
||||
## Статус
|
||||
Accepted (реализация — ORCH-089)
|
||||
|
||||
## Контекст
|
||||
Конвейер имеет два **человеческих** гейта, тормозящих пакетный автономный прогон
|
||||
(эпик ORCH-088, «10–20 задач за ночь»):
|
||||
1. **BRD** (`analysis`): ждёт ручного Plane-статуса `Approved` → advance на `architecture`.
|
||||
2. **Прод-деплой** (`deploy`): Phase A ставит `Awaiting Deploy` и ждёт ручного
|
||||
`Confirm Deploy` (ORCH-059) → Phase B (`initiate_deploy`).
|
||||
|
||||
Для доверенных задач оба клика избыточны. Нужно снять **только эти два человеческих
|
||||
решения**, выборочно/декларативно (лейбл Plane на задаче), не ослабляя ни одной
|
||||
технической проверки.
|
||||
|
||||
## Решение
|
||||
Аддитивно, по образцу условных под-гейтов (ORCH-035/043/058/059/088): leaf-модуль чистой
|
||||
логики `src/labels.py` (never-raise) + точечные врезки + флаги. `STAGE_TRANSITIONS`, реестр
|
||||
`QG_CHECKS`, все `check_*`, схема БД — **не трогаются**.
|
||||
|
||||
- **`autoApprove`** (лейбл задачи) → в `_handle_analysis_approved_flow` (ветка `files_ok`)
|
||||
после `In Review`+коммента: `set_issue_approved` (индикация) + лог/Telegram/Plane-коммент +
|
||||
`advance_stage(..., finished_agent=None)` — тот же путь, что человеческий Approved
|
||||
(`approved-via-status` → `analysis → architecture` + `mark_brd_review_ended`). Без
|
||||
дублирования переходной логики.
|
||||
- **`autoDeploy`** (лейбл задачи) → в `_handle_self_deploy_phase_a` сразу после advance на
|
||||
`deploy` + `clear_state`: лог/Telegram/Plane-коммент + `_handle_self_deploy_phase_b(...)`
|
||||
(idempotency-маркер `INITIATED`, `Deploying`, finalizer). Пропускаются лишь
|
||||
индикативно-человеческие шаги (`Awaiting Deploy` + «ask-human»).
|
||||
- **Чтение лейблов** — `plane_sync.fetch_issue_labels` + `get_project_labels` (TTL-кэш,
|
||||
образец `get_project_states`); сопоставление по нормализованному имени; источник истины —
|
||||
Plane API (не payload). Новый сеттер `set_issue_approved` (ключ `approved` уже в states).
|
||||
- **Флаги:** `auto_label_enabled` (kill-switch), `auto_approve_label`/`auto_deploy_label`
|
||||
(имена), `auto_label_repos` (CSV; **пусто → self-hosting only**), `auto_label_states_ttl_s`.
|
||||
`applies(repo)` (локальный) проверяется ПЕРВЫМ; `has_label` (сеть) — только если
|
||||
`applies==True` → при выключенном флаге нулевой сетевой оверхед.
|
||||
|
||||
## Критические инварианты
|
||||
- **Авто-режим снимает ТОЛЬКО человеческое решение**, не ослабляя ни один тех-гейт
|
||||
(CI / staging / security / merge-gate / image-freshness / merge-verify / regression-guard /
|
||||
post-deploy). autoDeploy живёт в точке, где все под-гейты ребра `deploy-staging → deploy`
|
||||
уже зелёные → структурно «никогда не деплоит сломанное».
|
||||
- **Fail-safe (never auto):** любая ошибка/недоступность Plane/неоднозначность имени →
|
||||
«нет авто» → ручной гейт (согласовано с fail-closed-практикой ORCH-059). never-raise.
|
||||
- **Нулевая регрессия:** без лейблов / `auto_label_enabled=False` / репо вне scope →
|
||||
поведение 1:1 как до ORCH-089 (enduro не затронут).
|
||||
- **Идемпотентность:** autoApprove — advance применяется один раз (поздний Approved/F-2
|
||||
видят уже `architecture`); autoDeploy — маркер `INITIATED`.
|
||||
|
||||
## Последствия
|
||||
**+** минимальная поверхность, единый источник истины перехода, декларативно/обратимо,
|
||||
независимые лейблы, безопасный дефолт. **−** Approved-статус транзиентен (durable-аудит —
|
||||
лог/Telegram/коммент); 1–2 GET к Plane на гейт применимого репо (TTL-кэш карты лейблов);
|
||||
требуется однократно создать лейблы в Plane-проекте ORCH (инфра-предусловие; их отсутствие =
|
||||
fail-safe ручной режим).
|
||||
|
||||
Детально: `docs/work-items/ORCH-089/06-adr/ADR-001-auto-label-gates.md`,
|
||||
`07-infra-requirements.md`, `10-tech-risks.md`.
|
||||
49
docs/architecture/adr/adr-0019-pipeline-docs-standard.md
Normal file
49
docs/architecture/adr/adr-0019-pipeline-docs-standard.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# adr-0019: Стандарт документов пайплайна (docs/_standards + docs/_templates + ADR-naming)
|
||||
|
||||
Статус: **proposed** · Дата: 2026-06-09 · Источник: **ORCH-075** (ORCH-52b, слой 1 эпика ORCH-52)
|
||||
Детально: `docs/work-items/ORCH-075/06-adr/ADR-001-pipeline-docs-standard.md`.
|
||||
|
||||
## Контекст
|
||||
Агенты всех ролей пишут номерные доки work item (`00…17`) «по памяти»; каталогов
|
||||
`docs/_standards/` и `docs/_templates/` нет. Следствия: разнобой структуры между задачами; риск
|
||||
рассинхрона критичных frontmatter-ключей машинных доков (`verdict:` / `result:` / `deploy_status:` /
|
||||
`staging_status:` / `security_status:`), которые читает гейт; отсутствует целостная карта «стадия →
|
||||
агент → документ → гейт». Эпик ORCH-52 слоист: слой 1 (52b) фиксирует **договорённость**, машинная
|
||||
проверка/валидатор — отдельный слой 52c.
|
||||
|
||||
## Решение
|
||||
**Документационный стандарт, docs-only, выведенный из фактического кода и эталонных доков:**
|
||||
|
||||
1. `docs/_standards/PIPELINE_DOCS.md` — манифест-карта «стадия → документ → владелец-агент →
|
||||
категория (`required`/`when-applicable`/`optional`) → гейт/механизм → frontmatter machine-key».
|
||||
Манифест **документирует** поведение гейтов (источник истины остаётся `src/`), честно различает
|
||||
machine-verdict доки (`12,13,14,15,17`) и информационные (`00,08,10,16`), и помечает под-гейты
|
||||
ребра `deploy-staging→deploy` (security/merge/image-freshness) как врезки в `advance_stage`, а не
|
||||
строки `STAGE_TRANSITIONS`.
|
||||
2. `docs/_templates/*` — копируемые скелеты для каждого `required`/`when-applicable` дока; секции
|
||||
выведены из эталонов (ORCH-088/073/089/071), новые не изобретаются; машинные доки несут точный
|
||||
frontmatter-ключ из ground-truth.
|
||||
3. **ADR-naming** канонизирован: `docs/work-items/<plane-id>/06-adr/ADR-NNN-<kebab-slug>.md` (NNN с
|
||||
`001`); кросс-каттинговые решения дублируются в этот глобальный реестр `adr-NNNN-<slug>.md`.
|
||||
|
||||
Подключение — ссылки из `CLAUDE.md` и `docs/architecture/README.md` + запись в `CHANGELOG.md`.
|
||||
|
||||
## Альтернативы
|
||||
- Сразу валидатор на гейте — отвергнуто (ORCH-52c; нарушил бы docs-only/NFR-1, групповой риск).
|
||||
- Манифест как источник истины гейтов — отвергнуто (дубль-истина «манифест ≠ код»).
|
||||
- Шаблоны в `docs/work-items/_template/` — отвергнуто (риск для сканеров/гейтов наличия файлов).
|
||||
- Ретро-фит истории доков — отвергнуто (вне scope, отдельный риск).
|
||||
|
||||
## Последствия
|
||||
- **+** Единый golden source структуры доков; меньше ложных падений гейтов из-за неверного
|
||||
frontmatter-ключа; ADR-naming записан; база для ORCH-52c.
|
||||
- **+ Нулевой рантайм-риск:** только `docs/**` + `CLAUDE.md` + `CHANGELOG.md`; `STAGE_TRANSITIONS` /
|
||||
`QG_CHECKS` / `check_*` / `src/stage_engine.py` / схема БД — без изменений; полностью обратимо.
|
||||
- **−** Манифест — снимок поведения гейтов, дрейфует до ORCH-52c (митигейшн: источник истины — код,
|
||||
reviewer-правило, привязка к именам `check_*`); стандарт описательный, не принуждающий.
|
||||
|
||||
## Связи
|
||||
- Источник: ORCH-075 (`docs/work-items/ORCH-075/06-adr/ADR-001-pipeline-docs-standard.md`).
|
||||
- Документирует (не меняет): adr-0003/0006/0008/0012/0013/0014/0016 (гейты и под-гейты ребра),
|
||||
`STAGE_TRANSITIONS` (`src/stages.py`), `QG_CHECKS` (`src/qg/checks.py`).
|
||||
- Downstream: ORCH-52c (frontmatter-валидатор / writer-контракт), ORCH-52d (правка промптов).
|
||||
63
docs/architecture/adr/adr-0020-frontmatter-contract.md
Normal file
63
docs/architecture/adr/adr-0020-frontmatter-contract.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# adr-0020: Единый frontmatter-контракт + спека handoff (reader/writer/валидатор)
|
||||
|
||||
Статус: **Accepted** · Дата: 2026-06-09 · Источник: **ORCH-076** (ORCH-52c)
|
||||
Детально: [`docs/work-items/ORCH-076/06-adr/ADR-001-frontmatter-contract.md`](../../work-items/ORCH-076/06-adr/ADR-001-frontmatter-contract.md)
|
||||
|
||||
## Контекст
|
||||
|
||||
Слой 1 эпика ORCH-52 (ORCH-075/52b) дал **описательный** стандарт документов
|
||||
(`docs/_standards/PIPELINE_DOCS.md`), явно отложив машинную проверку на ORCH-52c. В коде:
|
||||
`src/frontmatter.py` — только single-key reader (never-raise), а ~10-строчный блок парсинга
|
||||
YAML-frontmatter **продублирован** в 5 вердикт-парсерах (`check_reviewer_verdict`,
|
||||
`_parse_tests_verdict`, `_parse_deploy_status`, `_parse_staging_status`, `parse_security_status`)
|
||||
+ в `_strip_frontmatter`/`extract_security_findings`. Единого контракта чтения, writer'а, схемы
|
||||
и формальной спеки handoff — нет. Эти парсеры читают вердикты **на гейтах self-hosting**
|
||||
инструмента, обслуживающего прод других проектов из общего инстанса → любой регресс = стоп
|
||||
конвейера всех проектов.
|
||||
|
||||
## Решение
|
||||
|
||||
1. **`src/frontmatter.py` → полный frontmatter-контракт** (функции в существующем leaf-модуле,
|
||||
контракт **never-raise**): сохранённый `read_frontmatter_value` (без изменений) + единый
|
||||
парс-примитив `parse_frontmatter(content) -> FrontmatterParse` (единственная точка
|
||||
YAML-логики, структура различает no-block / malformed / yaml-error / data) + `render_/
|
||||
write_frontmatter` (writer) + `validate_schema` (обязательная схема
|
||||
`work_item, stage, author_agent, status, created_at, model_used`) + `strip_frontmatter`.
|
||||
2. **Унифицируется механизм парсинга, НЕ семантика.** Все 5 вердикт-парсеров читают YAML через
|
||||
`parse_frontmatter`; token-наборы, upper-casing, приоритет негативного токена, 3-полевой
|
||||
контракт tester'а (ORCH-047), fallback `worktree→origin/main` — **1:1**. Сигнатуры и
|
||||
`tuple[bool, str]` — неизменны. Reason-строки переносятся дословно.
|
||||
3. **Валидатор не hard-fail по умолчанию.** Флаг `frontmatter_validation_strict` (env
|
||||
`ORCH_FRONTMATTER_VALIDATION_STRICT`, дефолт `False`): default — warning/лог, **вне
|
||||
вердикт-пути гейтов** (нулевая регрессия); hard-fail — зарезервированный strict-режим
|
||||
(включение — с ORCH-52d). Иначе ORCH-52c заблокировала бы собственный деплой.
|
||||
4. **Формальная спека handoff** `docs/_standards/HANDOFF_PROTOCOL.md` — «стадия → обязательный
|
||||
выход» (документы + frontmatter-ключи), согласована 1:1 с `PIPELINE_DOCS.md` §2–§3; источник
|
||||
истины — код. `PIPELINE_DOCS.md` обновляется ссылкой + отметкой о реализации машинного слоя.
|
||||
5. **Без изменений** `STAGE_TRANSITIONS`, состава `QG_CHECKS`, API, схемы БД.
|
||||
|
||||
## Альтернативы
|
||||
|
||||
- Общий «умный» verdict-резолвер (поле+токены для всех гейтов) — отклонён: различия token-логики
|
||||
→ риск тонкого регресса на гейте при self-hosting. Унифицируем только парс YAML.
|
||||
- Класс/новый пакет — отклонён: состояния нет, лишний blast radius.
|
||||
- Hard-fail валидатор по умолчанию — отклонён (NFR-3: self-block собственного деплоя).
|
||||
- Сторонняя `python-frontmatter` — отклонена: лишняя зависимость ради ~30 строк.
|
||||
|
||||
## Последствия
|
||||
|
||||
- **+** Конец дублирования/рассинхрона парсинга; writer+валидатор+схема готовы к ORCH-52d;
|
||||
спека handoff закрывает пробел контракта стадий.
|
||||
- **+** Нулевая регрессия по построению: семантика и reason-строки 1:1, валидатор инертен при
|
||||
дефолте, never-raise сохранён, enduro 1:1.
|
||||
- **−** Унификация частичная (парс, не семантика); strict-режим «спящий» до ORCH-52d.
|
||||
- **Обратимость:** `frontmatter_validation_strict=False` ⇒ прежнее поведение; перевод гейтов
|
||||
поведенчески инвариантен.
|
||||
- **Риск:** первый боевой `autoDeploy` орка (ORCH-089) — наблюдение за стадией `deploy`
|
||||
(`docs/work-items/ORCH-076/10-tech-risks.md`).
|
||||
|
||||
## Связи
|
||||
|
||||
- Опирается: adr-0019 (pipeline-docs-standard, ORCH-075), ORCH-016 (reader), ORCH-047
|
||||
(3-полевой tester), adr-0012 (security-гейт), adr-0018 (auto-label/`autoDeploy`).
|
||||
- Готовит: ORCH-52d (эмиссия полной схемы агентами; возможное включение strict).
|
||||
7
docs/work-items/ORCH-075/00-business-request.md
Normal file
7
docs/work-items/ORCH-075/00-business-request.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Business Request: ORCH-52b: стандарт документов (docs/_templates + манифест стадий + ADR-naming)
|
||||
|
||||
Work Item ID: ORCH-075
|
||||
|
||||
## Description
|
||||
|
||||
TBD
|
||||
111
docs/work-items/ORCH-075/01-brd.md
Normal file
111
docs/work-items/ORCH-075/01-brd.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# 01 — BRD: ORCH-075 — ORCH-52b: стандарт документов (docs/_templates + манифест стадий + ADR-naming)
|
||||
|
||||
Work Item: **ORCH-075**
|
||||
Repo: **orchestrator** (self-hosting)
|
||||
Стадия: analysis
|
||||
Заказчик: Owner / команда агентов оркестратора
|
||||
Тип: documentation-standard (слой 1 эпика ORCH-52)
|
||||
|
||||
> Документ фиксирует **бизнес-требования** к созданию стандарта документов пайплайна.
|
||||
> ORCH-52b — слой 1 («стандарт»): только документация (манифест + канонические шаблоны +
|
||||
> конвенция ADR-naming). Любая принудительная проверка/валидатор/правка кода и промптов —
|
||||
> вне scope (это ORCH-52c/52d). Источник истины для манифеста — фактические
|
||||
> `STAGE_TRANSITIONS` / `QG_CHECKS` и реальные эталонные доки в репозитории, а не вымысел.
|
||||
|
||||
---
|
||||
|
||||
## 1. Бизнес-контекст и проблема
|
||||
|
||||
### 1.1. Текущее состояние (проверено в репо)
|
||||
- Каталоги `docs/_templates/` и `docs/_standards/` **не существуют**.
|
||||
- Агенты (`analyst` → `architect` → … → `deployer`) пишут номерные доки work item
|
||||
(`00-business-request.md` … `17-security-report.md`) «с нуля по памяти».
|
||||
- Конвенция ADR-naming `06-adr/ADR-NNN-<kebab-slug>.md` фактически уже сложилась в репо,
|
||||
но **нигде не зафиксирована** как стандарт.
|
||||
|
||||
### 1.2. Боль
|
||||
- **Разнобой структуры** между задачами: набор и порядок секций одного и того же дока
|
||||
плавает от work item к work item (видно при сравнении BRD/ТЗ разных задач).
|
||||
- Машинные доки-вердикты (`12-review.md`, `13-test-report.md`, `14-deploy-log.md`,
|
||||
`15-staging-log.md`, `17-security-report.md`) держат критичный frontmatter-ключ
|
||||
(`verdict:` / `deploy_status:` / `staging_status:` / `security_status:`), читаемый
|
||||
гейтом — но единого канонического скелета с этим ключом нет → риск рассинхрона.
|
||||
- Нет единой карты «какая стадия / какой агент пишет какой документ и на каком гейте он
|
||||
проверяется» — онбординг новых агентских ролей и аудит покрытия затруднён.
|
||||
|
||||
### 1.3. Почему именно стандарт (а не сразу валидатор)
|
||||
Эпик ORCH-52 разбит на слои, чтобы **сначала зафиксировать договорённость (golden source
|
||||
документации)**, а уже потом, отдельной задачей (52c), навешивать машинную проверку
|
||||
frontmatter/шаблонов на гейте. Стандарт без кода — обратимый, низкорисковый, не трогает
|
||||
работающий прод-конвейер (self-hosting). Это снижает групповой риск.
|
||||
|
||||
## 2. Объём (scope)
|
||||
|
||||
### 2.1. В объёме (ORCH-52b)
|
||||
1. **Манифест** `docs/_standards/PIPELINE_DOCS.md`: таблица «стадия → документ» с
|
||||
владельцем-агентом, категорией (`required` / `when-applicable` / `optional`), стадией
|
||||
написания и гейтом/механизмом проверки — **сверенная с фактическими `QG_CHECKS` и
|
||||
`stage_engine`**.
|
||||
2. **Канонические шаблоны** `docs/_templates/*` — скелеты (frontmatter при необходимости +
|
||||
обязательные секции) для всех номерных доков из реального набора.
|
||||
3. **Конвенция ADR-naming** — зафиксировать сложившийся формат `06-adr/ADR-NNN-<kebab-slug>.md`
|
||||
(нумерация с `001`, где живёт, как формируется slug); раздел в манифесте/стандарте.
|
||||
4. Ссылки на новый стандарт в `CLAUDE.md` и `docs/architecture/README.md`; запись в
|
||||
`CHANGELOG.md`.
|
||||
|
||||
### 2.2. Вне объёма (явно — это ORCH-52c / 52d)
|
||||
- Frontmatter-валидатор в коде; writer-контракт; принудительная проверка наличия/структуры
|
||||
шаблонов на Quality Gate.
|
||||
- Любые изменения `QG_CHECKS` / `STAGE_TRANSITIONS` / `check_*` / `src/stage_engine.py` /
|
||||
схемы БД.
|
||||
- Правка системных промптов агентов (`.openclaw/agents/*`) — это слой 52d.
|
||||
- Массовое приведение **уже существующих** доков прошлых задач к новому шаблону (ретро-фит).
|
||||
|
||||
## 3. Заинтересованные стороны
|
||||
| Роль | Интерес |
|
||||
|------|---------|
|
||||
| Owner | Единообразие и аудитопригодность документации проекта |
|
||||
| Агенты analyst/architect | Готовый скелет → меньше расхождений, быстрее старт |
|
||||
| Агенты reviewer/tester/deployer | Предсказуемый frontmatter машинных доков |
|
||||
| ORCH-52c (downstream) | Стандарт = база для frontmatter-валидатора/writer-контракта |
|
||||
|
||||
## 4. Бизнес-требования (BR)
|
||||
| ID | Требование |
|
||||
|----|------------|
|
||||
| BR-1 | Создан `docs/_standards/PIPELINE_DOCS.md` — манифест, покрывающий **все** номерные доки реального набора (00,01,02,03,04,06,07,08,10,12,13,14,15,16,17) с владельцем-агентом и категорией. |
|
||||
| BR-2 | Каждому `required` и `when-applicable` доку соответствует канонический шаблон в `docs/_templates/` (frontmatter при необходимости + обязательные секции). |
|
||||
| BR-3 | Формат ADR-naming `06-adr/ADR-NNN-<kebab-slug>.md` зафиксирован в стандарте и совпадает с реальными ADR в репо. |
|
||||
| BR-4 | Манифест и шаблоны **согласованы с фактическими эталонными доками** (ORCH-088/073/089/071): нет секций, которых никто не пишет, и наоборот; frontmatter-ключи машинных доков совпадают с тем, что реально парсят гейты. |
|
||||
| BR-5 | Обновлены `CLAUDE.md` и `docs/architecture/README.md` со ссылкой на стандарт; добавлена запись в `CHANGELOG.md`. |
|
||||
| BR-6 | Манифест отражает категорию проверки документа фактическим механизмом: какие доки несут machine-verdict frontmatter, читаемый гейтом, а какие информационные (не гейтятся). |
|
||||
|
||||
## 5. Нефункциональные требования (NFR)
|
||||
| ID | Требование |
|
||||
|----|------------|
|
||||
| NFR-1 | **Нулевой риск для прода:** изменения — только под `docs/` (+ `CLAUDE.md`/`CHANGELOG.md`). Ни строки кода/гейтов. |
|
||||
| NFR-2 | **Достоверность:** все утверждения манифеста о стадии/агенте/гейте проверяемы по `src/stages.py` / `src/qg/checks.py` / `src/stage_engine.py`. |
|
||||
| NFR-3 | **Не изобретать:** шаблоны выведены из существующих эталонов, не из фантазии; новые секции не вводятся. |
|
||||
| NFR-4 | **Читаемость:** манифест — на русском, в стиле существующей документации проекта; таблицы машиночитаемо-аккуратные. |
|
||||
| NFR-5 | **Обратимость:** удаление новых файлов полностью откатывает изменение без следов в поведении системы. |
|
||||
|
||||
## 6. Допущения и ограничения
|
||||
- Реальный набор номерных доков и их частота взяты из `docs/work-items/` (факты в описании
|
||||
задачи); считаем его авторитетным на момент задачи.
|
||||
- Эталонные («golden») задачи для извлечения скелетов — ORCH-088, ORCH-073, ORCH-089, ORCH-071.
|
||||
- `09-review.md` — legacy fallback (канон — `12-review.md`); в манифест как канон **не**
|
||||
вводится, при необходимости упоминается примечанием.
|
||||
- Стадия `monitoring` для `16-post-deploy-log.md` — пост-`done` наблюдение (ORCH-021), не
|
||||
ребро `STAGE_TRANSITIONS`; манифест это отражает явно.
|
||||
|
||||
## 7. Критерии успеха
|
||||
- Любой агент, открыв `docs/_standards/PIPELINE_DOCS.md`, понимает: какой документ он пишет
|
||||
на своей стадии, в какой категории, с каким frontmatter и где он проверяется.
|
||||
- Для каждого `required`/`when-applicable` дока существует шаблон, который можно скопировать
|
||||
и заполнить без догадок о структуре.
|
||||
- ADR-naming больше не «устная традиция», а записанная конвенция.
|
||||
- Полный набор уточняющих PASS/FAIL — в `03-acceptance-criteria.md`.
|
||||
|
||||
## 8. Риски
|
||||
Технические риски и митигейшн ведёт архитектор в `10-tech-risks.md`. Ключевой бизнес-риск —
|
||||
**рассинхрон стандарта с кодом** (манифест описал гейт, которого нет, или наоборот): митигируется
|
||||
NFR-2 (сверка с `src/`) и AC-1/AC-4.
|
||||
141
docs/work-items/ORCH-075/02-trz.md
Normal file
141
docs/work-items/ORCH-075/02-trz.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# 02 — ТЗ (TRZ): ORCH-075 — ORCH-52b: стандарт документов
|
||||
|
||||
Work Item: **ORCH-075** · Repo: **orchestrator** · Стадия: analysis
|
||||
|
||||
> ТЗ описывает **конкретные артефакты к созданию** (манифест + шаблоны + раздел ADR-naming) и
|
||||
> их обязательное содержимое, выведенное из фактических `STAGE_TRANSITIONS`/`QG_CHECKS` и
|
||||
> эталонных доков. Это **docs-only** изменение: исходный код, гейты, схема БД, промпты —
|
||||
> НЕ затрагиваются (см. §7). Архитектурное обоснование/решения — задача архитектора (06-adr).
|
||||
|
||||
## 1. Сводка изменения
|
||||
Создать каталоги `docs/_standards/` и `docs/_templates/`, наполнить их манифестом
|
||||
«стадия→документ», каноническими скелетами номерных доков и зафиксировать ADR-naming.
|
||||
Обновить точки-ссылки (`CLAUDE.md`, `docs/architecture/README.md`, `CHANGELOG.md`).
|
||||
|
||||
## 2. Задействованные модули / пути
|
||||
| Путь | Действие |
|
||||
|------|----------|
|
||||
| `docs/_standards/PIPELINE_DOCS.md` | **создать** — манифест стадия→документ + раздел ADR-naming |
|
||||
| `docs/_templates/00-business-request.md` | **создать** — шаблон |
|
||||
| `docs/_templates/01-brd.md` | **создать** |
|
||||
| `docs/_templates/02-trz.md` | **создать** |
|
||||
| `docs/_templates/03-acceptance-criteria.md` | **создать** |
|
||||
| `docs/_templates/04-test-plan.yaml` | **создать** |
|
||||
| `docs/_templates/06-adr-ADR-NNN-slug.md` | **создать** — шаблон ADR (имя файла шаблона без коллизии с реальной нумерацией) |
|
||||
| `docs/_templates/07-infra-requirements.md` | **создать** (when-applicable) |
|
||||
| `docs/_templates/08-data-requirements.md` | **создать** (when-applicable) |
|
||||
| `docs/_templates/10-tech-risks.md` | **создать** |
|
||||
| `docs/_templates/12-review.md` | **создать** (frontmatter `verdict:`) |
|
||||
| `docs/_templates/13-test-report.md` | **создать** (frontmatter `result:`) |
|
||||
| `docs/_templates/14-deploy-log.md` | **создать** (frontmatter `deploy_status:`) |
|
||||
| `docs/_templates/15-staging-log.md` | **создать** (frontmatter `staging_status:`) |
|
||||
| `docs/_templates/16-post-deploy-log.md` | **создать** (frontmatter `post_deploy_status:`) |
|
||||
| `docs/_templates/17-security-report.md` | **создать** (frontmatter `security_status:`) |
|
||||
| `CLAUDE.md` | **изменить** — ссылка на стандарт в разделе «Артефакты задачи» / «Правила для агентов» |
|
||||
| `docs/architecture/README.md` | **изменить** — ссылка на стандарт |
|
||||
| `CHANGELOG.md` | **изменить** — запись в `## [Unreleased]` |
|
||||
|
||||
> Точное имя файла-шаблона ADR оставлено на усмотрение разработчика/архитектора при условии,
|
||||
> что **внутри** шаблона и в манифесте зафиксирован реальный целевой формат
|
||||
> `06-adr/ADR-NNN-<kebab-slug>.md` (см. §3, FR-3).
|
||||
|
||||
## 3. Функциональные требования
|
||||
|
||||
### FR-1 — Манифест `docs/_standards/PIPELINE_DOCS.md`
|
||||
Содержит таблицу-манифест, покрывающую **все** номерные доки реального набора. Для каждого —
|
||||
колонки: `Документ`, `Владелец-агент`, `Категория`, `Стадия написания`, `Гейт/механизм
|
||||
проверки`, `Frontmatter machine-key (если есть)`. Манифест ДОЛЖЕН соответствовать
|
||||
ground-truth ниже (сверено по `src/`):
|
||||
|
||||
| Документ | Владелец-агент | Категория | Стадия написания | Гейт / проверка | Machine-key |
|
||||
|----------|----------------|-----------|------------------|-----------------|-------------|
|
||||
| `00-business-request.md` | система (Plane webhook `_create_initial_docs`) / заказчик | required | `created` (инициализация) | не гейтится (вход) | — |
|
||||
| `01-brd.md` | analyst | required | `analysis` | exit-гейт `analysis→architecture` = `check_analysis_approved` (Approved + полнота файлов); helper `check_analysis_complete` (наличие) | — |
|
||||
| `02-trz.md` | analyst | required | `analysis` | то же | — |
|
||||
| `03-acceptance-criteria.md` | analyst | required | `analysis` | то же | — |
|
||||
| `04-test-plan.yaml` | analyst | required | `analysis` | то же | — |
|
||||
| `06-adr/ADR-NNN-<slug>.md` | architect | required | `architecture` | `check_architecture_done` (наличие каталога/ADR) | — |
|
||||
| `07-infra-requirements.md` | architect | when-applicable | `architecture` | `check_architecture_done` (учитывается при наличии) | — |
|
||||
| `08-data-requirements.md` | architect | when-applicable | `architecture` | информационный (гейтом не парсится) | — |
|
||||
| `10-tech-risks.md` | architect | required | `architecture` | информационный (гейтом не парсится) | — |
|
||||
| `12-review.md` | reviewer | required | `review` | `check_reviewer_verdict` | `verdict:` (APPROVED\|REQUEST_CHANGES) |
|
||||
| `13-test-report.md` | tester | required | `testing` | `check_tests_passed` | `result:`/`verdict:`/`status:` (PASS\|FAIL\|BLOCKED) |
|
||||
| `14-deploy-log.md` | deployer / deploy-finalizer | required | `deploy` | `check_deploy_status` | `deploy_status:` (SUCCESS\|FAILED) |
|
||||
| `15-staging-log.md` | deployer | required (self-hosting) | `deploy-staging` | `check_staging_status` (self-hosting; иначе N/A) | `staging_status:` (SUCCESS\|FAILED) |
|
||||
| `16-post-deploy-log.md` | post-deploy-monitor | when-applicable | пост-`done` наблюдение (ORCH-021, не ребро `STAGE_TRANSITIONS`) | информационный (гейтом не парсится) | `post_deploy_status:` |
|
||||
| `17-security-report.md` | security-гейт (детерминированный, ORCH-022) | when-applicable | под-гейт ребра `deploy-staging→deploy` | `check_security_gate` | `security_status:` (PASS\|FAIL) |
|
||||
|
||||
Примечания манифеста (обязательны):
|
||||
- Под-гейты ребра `deploy-staging→deploy` (`check_security_gate` → `check_branch_mergeable` →
|
||||
`check_staging_image_fresh`) — **не** строки `STAGE_TRANSITIONS`, а врезки в `advance_stage`.
|
||||
- `09-review.md` — legacy fallback; канон — `12-review.md` (упомянуть примечанием, в основную
|
||||
таблицу как канон не вносить).
|
||||
- Категория `when-applicable` = пишется при наличии соответствующего предмета (инфра/данные/
|
||||
security/post-deploy); её отсутствие — не нарушение.
|
||||
|
||||
### FR-2 — Шаблоны `docs/_templates/*`
|
||||
Каждый шаблон — копируемый скелет. Обязательные элементы по типам (выведено из эталонов
|
||||
ORCH-088/073/089/071):
|
||||
|
||||
- **Документы БЕЗ frontmatter** (`00`,`01`,`02`,`03`,`06-adr`,`07`,`08`,`10`): заголовок `#`,
|
||||
строка метаданных `Work Item / Repo / Стадия`, и фиксированные `##`-секции (ниже §FR-2.1).
|
||||
- **YAML-only**: `04-test-plan.yaml` — корневые ключи + список `tests:` (ниже §FR-2.2).
|
||||
- **Документы С YAML-frontmatter** (`12`,`13`,`14`,`15`,`16`,`17`): блок `---…---` с
|
||||
machine-key из таблицы FR-1 + body-секции.
|
||||
|
||||
#### FR-2.1 Обязательные секции по документу (минимальный канон)
|
||||
- `00-business-request.md`: `# Business Request: <subject>`; строка `Work Item ID:`; `## Description`.
|
||||
- `01-brd.md`: `## 1. Бизнес-контекст и проблема`, `## 2. Объём (scope)` (с `### В объёме`/`### Вне объёма`), `## 3. Заинтересованные стороны`, `## 4. Бизнес-требования (BR)`, `## 5. Нефункциональные требования (NFR)`, `## 6. Допущения и ограничения`, `## 7. Критерии успеха`, `## 8. Риски`.
|
||||
- `02-trz.md`: `## 1. Сводка изменения`, `## 2. Задействованные модули`, `## 3. Функциональные требования`, `## 4. Изменения API`, `## 5. Изменения схемы БД`, `## 6. Требования к QG checks`, `## 7. Совместимость / регресс`.
|
||||
- `03-acceptance-criteria.md`: преамбула формата; повторяемый блок `## AC-N — <title>` с `**Условие:**`, `- **PASS:**`, `- **FAIL:**`; опц. `## Сводная матрица AC ↔ FR/BR`.
|
||||
- `06-adr (шаблон)`: `# ADR-NNN: <title>`, метаданные (`Work Item`,`Стадия: architecture`,`Сквозная регистрация:`), `## Статус`, `## Контекст`, `## Решение` (с `### Сводка` и `### D1 — …`), `## Альтернативы`, `## Последствия`, `## Ссылки`.
|
||||
- `07-infra-requirements.md`: `# 07 — Инфра-требования`, нумерованные `## I-N. <topic>`.
|
||||
- `08-data-requirements.md`: `# 08 — Требования к данным`, секции по таблицам/колонкам/миграциям.
|
||||
- `10-tech-risks.md`: `# 10 — Технические риски`, таблица `| ID | Риск | Вер. | Влия. | Митигейшн |`, `## Сводный вывод`.
|
||||
- `12-review.md`: frontmatter `type: review` / `work_item_id:` / `verdict:` / `version:`; body `## Summary`, `## Оси проверки`, `## Findings` (`### P0`/`### P1`/`### P2`), `## Документация`.
|
||||
- `13-test-report.md`: frontmatter `type: test-report` / `work_item_id:` / `result:`; body `## Окружение`, `## Результаты` (`### Полный регресс`, `### Профильные сюиты`, `### Сопоставление с тест-планом`, `### Сопоставление с критериями приёмки`).
|
||||
- `14-deploy-log.md`: frontmatter `deploy_status:` / `work_item:` / `hook_exit_code:` / `deployed_by:`; body — краткое описание деплоя.
|
||||
- `15-staging-log.md`: frontmatter `staging_status:` / `timestamp:` / `base_url:`; body — `# Staging Gate Log` + результаты проверок.
|
||||
- `16-post-deploy-log.md`: frontmatter `post_deploy_status:` / `action_taken:` / `work_item:`; body — окно наблюдения/серия/решение.
|
||||
- `17-security-report.md`: frontmatter `security_status:` / `work_item:`; body — secret-scan + dependency-audit результаты.
|
||||
|
||||
#### FR-2.2 `04-test-plan.yaml`
|
||||
Корень: `work_item:`, `title:`, `framework: pytest`, опц. `scope:`/`notes:`. Список `tests:`
|
||||
с элементами `{ id: TC-NN, type: unit|integration, description, module: tests/…, expected: PASS }`.
|
||||
|
||||
### FR-3 — Конвенция ADR-naming
|
||||
Зафиксировать в `PIPELINE_DOCS.md` отдельным разделом:
|
||||
- Путь: `docs/work-items/<plane-id>/06-adr/`.
|
||||
- Имя: `ADR-NNN-<kebab-slug>.md`, `NNN` с `001`, инкремент при нескольких ADR в одной задаче.
|
||||
- `slug` — kebab-case (нижний регистр, дефисы), отражает суть решения.
|
||||
- Сквозные (cross-cutting) решения дублируются в `docs/architecture/adr/adr-NNNN-<slug>.md`
|
||||
(4-значная глобальная нумерация) — это уже существующая конвенция, лишь зафиксировать.
|
||||
- Примеры из репо: `ADR-001-serial-gate`, `ADR-001-auto-label-gates`, `ADR-001-merge-verify-gate`.
|
||||
|
||||
### FR-4 — Точки-ссылки
|
||||
- `CLAUDE.md`: в разделе «Артефакты задачи» и/или «Правила для агентов» добавить ссылку на
|
||||
`docs/_standards/PIPELINE_DOCS.md` и `docs/_templates/` как golden source структуры доков.
|
||||
- `docs/architecture/README.md`: добавить ссылку/абзац о стандарте документов.
|
||||
- `CHANGELOG.md`: запись в `## [Unreleased]` (`docs:` тип).
|
||||
|
||||
## 4. Изменения API
|
||||
Нет. Эндпоинты не добавляются и не меняются.
|
||||
|
||||
## 5. Изменения схемы БД
|
||||
Нет. Таблицы/миграции не затрагиваются.
|
||||
|
||||
## 6. Требования к новым/изменённым QG checks
|
||||
Нет. `QG_CHECKS` и `check_*` **не трогаются** (это ORCH-52c). Манифест лишь **документирует**
|
||||
текущее поведение гейтов.
|
||||
|
||||
## 7. Совместимость / регресс
|
||||
- Изменения только под `docs/` + `CLAUDE.md` + `CHANGELOG.md`. Поведение рантайма неизменно.
|
||||
- Существующие доки прошлых задач не модифицируются (нет ретро-фита).
|
||||
- `09-review.md` (legacy) сохраняется как fallback; манифест канонизирует `12-review.md`.
|
||||
- Удаление новых файлов → полный откат без следов (NFR-5).
|
||||
|
||||
## 8. Артефакты, создаваемые/обновляемые по pipeline
|
||||
Создаются: `docs/_standards/PIPELINE_DOCS.md`, `docs/_templates/*` (15 шаблонов).
|
||||
Обновляются: `CLAUDE.md`, `docs/architecture/README.md`, `CHANGELOG.md`.
|
||||
Downstream-доки самой задачи ORCH-075 (`06-adr`, `10-tech-risks`, `12-review`, `13-test-report`,
|
||||
`14-deploy-log`, `15-staging-log`) — по штатному конвейеру.
|
||||
104
docs/work-items/ORCH-075/03-acceptance-criteria.md
Normal file
104
docs/work-items/ORCH-075/03-acceptance-criteria.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# 03 — Критерии приёмки (Acceptance Criteria): ORCH-075 — ORCH-52b: стандарт документов
|
||||
|
||||
Work Item: **ORCH-075** · Repo: **orchestrator** · Стадия: analysis
|
||||
|
||||
Формат: каждый критерий имеет **PASS** (что должно быть истинно для приёмки) и **FAIL**
|
||||
(что считается провалом). Скоп — только создание стандарта/шаблонов/манифеста (docs-only).
|
||||
|
||||
> Критерии унаследованы из AC задачи и расширены проверяемыми условиями. Любой машинный/ручной
|
||||
> reviewer проверяет их буквально по файлам репозитория.
|
||||
|
||||
---
|
||||
|
||||
## AC-1 — Манифест создан и покрывает весь реальный набор
|
||||
|
||||
**Условие:** существует `docs/_standards/PIPELINE_DOCS.md` с таблицей-манифестом.
|
||||
- **PASS:** файл существует; манифест содержит строки для **всех** номерных доков реального
|
||||
набора — `00, 01, 02, 03, 04, 06, 07, 08, 10, 12, 13, 14, 15, 16, 17`; для каждого указаны
|
||||
владелец-агент (analyst/architect/developer/reviewer/tester/deployer/система) и категория
|
||||
(`required` / `when-applicable` / `optional`).
|
||||
- **FAIL:** файла нет; пропущен хотя бы один документ набора; у дока отсутствует владелец или
|
||||
категория.
|
||||
|
||||
---
|
||||
|
||||
## AC-2 — Шаблоны созданы для каждого required/when-applicable дока
|
||||
|
||||
**Условие:** существует `docs/_templates/` с каноническими скелетами.
|
||||
- **PASS:** для каждого `required` и `when-applicable` дока есть файл-шаблон; в шаблоне
|
||||
присутствуют (а) frontmatter с machine-key там, где он требуется по FR-1 (`12`→`verdict:`,
|
||||
`13`→`result:`, `14`→`deploy_status:`, `15`→`staging_status:`, `16`→`post_deploy_status:`,
|
||||
`17`→`security_status:`), и (б) обязательные `##`-секции из ТЗ §FR-2.1.
|
||||
- **FAIL:** отсутствует шаблон для какого-либо required/when-applicable дока; в шаблоне
|
||||
машинного дока нет требуемого frontmatter-ключа; набор секций произвольный, не из ТЗ.
|
||||
|
||||
---
|
||||
|
||||
## AC-3 — ADR-naming зафиксирован
|
||||
|
||||
**Условие:** в стандарте есть раздел про ADR-naming.
|
||||
- **PASS:** зафиксирован формат `06-adr/ADR-NNN-<kebab-slug>.md` (NNN с `001`), путь
|
||||
(`docs/work-items/<plane-id>/06-adr/`), правило формирования slug (kebab-case) и связь со
|
||||
сквозным реестром `docs/architecture/adr/adr-NNNN-<slug>.md`; приведён ≥1 реальный пример.
|
||||
- **FAIL:** ADR-naming не описан, либо описанный формат не совпадает с реальными ADR в репо
|
||||
(напр. указана нумерация не с `001`, неверный путь, неверный регистр slug).
|
||||
|
||||
---
|
||||
|
||||
## AC-4 — Согласованность с фактическими эталонами
|
||||
|
||||
**Условие:** манифест и шаблоны соответствуют реальным эталонным докам (ORCH-088/073/089/071)
|
||||
и фактическому коду.
|
||||
- **PASS:** в шаблонах нет секций, которых никто не пишет в эталонах; все секции эталонов,
|
||||
входящие в общий канон, присутствуют; frontmatter-ключи машинных доков совпадают с тем, что
|
||||
реально парсят `src/qg/checks.py` (`verdict:`/`result:`/`deploy_status:`/`staging_status:`/
|
||||
`security_status:`); привязка «документ→стадия→гейт» совпадает с `src/stages.py`
|
||||
(`STAGE_TRANSITIONS`).
|
||||
- **FAIL:** шаблон вводит выдуманную секцию; манифест приписывает доку неверную стадию/гейт/
|
||||
агента; frontmatter-ключ в шаблоне не тот, что читает гейт.
|
||||
|
||||
---
|
||||
|
||||
## AC-5 — Ссылки и CHANGELOG обновлены
|
||||
|
||||
**Условие:** точки-ссылки и журнал изменений отражают новый стандарт.
|
||||
- **PASS:** `CLAUDE.md` и `docs/architecture/README.md` содержат ссылку на
|
||||
`docs/_standards/PIPELINE_DOCS.md` (и/или `docs/_templates/`); в `CHANGELOG.md` добавлена
|
||||
запись в `## [Unreleased]` типа `docs:`.
|
||||
- **FAIL:** хотя бы одна из трёх точек не обновлена.
|
||||
|
||||
---
|
||||
|
||||
## AC-6 — Код гейтов НЕ изменён
|
||||
|
||||
**Условие:** изменение строго docs-only.
|
||||
- **PASS:** `git diff` не содержит изменений в `src/qg/checks.py` (`QG_CHECKS`/`check_*`),
|
||||
`src/stages.py` (`STAGE_TRANSITIONS`), `src/stage_engine.py`, схеме БД и любом коде гейтов;
|
||||
затронуты только `docs/**`, `CLAUDE.md`, `CHANGELOG.md` (+ опционально новые файлы тестов).
|
||||
- **FAIL:** изменён любой из перечисленных кодовых модулей/гейтов/схемы.
|
||||
|
||||
---
|
||||
|
||||
## AC-7 — Манифест различает machine-verdict и информационные доки
|
||||
|
||||
**Условие:** манифест честно отражает механизм проверки.
|
||||
- **PASS:** документы, чей frontmatter читает гейт (`12,13,14,15,17`), помечены своим
|
||||
machine-key и гейтом; информационные (`00,08,10,16`) явно помечены как не гейтящиеся;
|
||||
под-гейты ребра `deploy-staging→deploy` (security/merge/image-freshness) отмечены как врезки
|
||||
в `advance_stage`, а не строки `STAGE_TRANSITIONS`.
|
||||
- **FAIL:** информационный док представлен как гейтящийся (или наоборот); под-гейты выданы за
|
||||
стадии.
|
||||
|
||||
---
|
||||
|
||||
## Сводная матрица AC ↔ BR
|
||||
|
||||
| AC | Покрывает BR |
|
||||
|----|--------------|
|
||||
| AC-1 | BR-1 |
|
||||
| AC-2 | BR-2 |
|
||||
| AC-3 | BR-3 |
|
||||
| AC-4 | BR-4, BR-6 |
|
||||
| AC-5 | BR-5 |
|
||||
| AC-6 | NFR-1, NFR-5 |
|
||||
| AC-7 | BR-6, NFR-2 |
|
||||
136
docs/work-items/ORCH-075/04-test-plan.yaml
Normal file
136
docs/work-items/ORCH-075/04-test-plan.yaml
Normal file
@@ -0,0 +1,136 @@
|
||||
work_item: ORCH-075
|
||||
title: "ORCH-52b — стандарт документов (docs/_standards + docs/_templates + ADR-naming)"
|
||||
scope: "docs-only: проверяется НАЛИЧИЕ и СТРУКТУРА новых файлов-стандартов/шаблонов; код гейтов не трогается"
|
||||
framework: pytest
|
||||
notes: >
|
||||
Изменение документационное. Тесты — лёгкие структурные проверки (existence + наличие
|
||||
обязательных секций/frontmatter-ключей), новый файл tests/test_orch_52b_docs_standard.py.
|
||||
Тесты НЕ меняют QG_CHECKS/STAGE_TRANSITIONS и не вводят новый гейт (это ORCH-52c). Полный
|
||||
регресс tests/ должен остаться зелёным (отсутствие регресса от docs-изменения).
|
||||
|
||||
tests:
|
||||
- id: TC-01
|
||||
type: integration
|
||||
description: "docs/_standards/PIPELINE_DOCS.md существует и непустой"
|
||||
module: tests/test_orch_52b_docs_standard.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-02
|
||||
type: integration
|
||||
description: "Манифест PIPELINE_DOCS.md упоминает все номерные доки набора: 00,01,02,03,04,06,07,08,10,12,13,14,15,16,17"
|
||||
module: tests/test_orch_52b_docs_standard.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-03
|
||||
type: integration
|
||||
description: "Манифест указывает владельца-агента для каждого дока (analyst/architect/reviewer/tester/deployer/система упомянуты)"
|
||||
module: tests/test_orch_52b_docs_standard.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-04
|
||||
type: integration
|
||||
description: "Манифест содержит категории required / when-applicable / optional"
|
||||
module: tests/test_orch_52b_docs_standard.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-05
|
||||
type: integration
|
||||
description: "Каталог docs/_templates/ существует и содержит шаблоны для всех required/when-applicable доков"
|
||||
module: tests/test_orch_52b_docs_standard.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-06
|
||||
type: integration
|
||||
description: "Шаблон 12-review содержит frontmatter-ключ verdict:"
|
||||
module: tests/test_orch_52b_docs_standard.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-07
|
||||
type: integration
|
||||
description: "Шаблон 13-test-report содержит frontmatter-ключ result:"
|
||||
module: tests/test_orch_52b_docs_standard.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-08
|
||||
type: integration
|
||||
description: "Шаблон 14-deploy-log содержит frontmatter-ключ deploy_status:"
|
||||
module: tests/test_orch_52b_docs_standard.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-09
|
||||
type: integration
|
||||
description: "Шаблон 15-staging-log содержит frontmatter-ключ staging_status:"
|
||||
module: tests/test_orch_52b_docs_standard.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-10
|
||||
type: integration
|
||||
description: "Шаблон 17-security-report содержит frontmatter-ключ security_status:"
|
||||
module: tests/test_orch_52b_docs_standard.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-11
|
||||
type: integration
|
||||
description: "Шаблон 16-post-deploy-log содержит frontmatter-ключ post_deploy_status:"
|
||||
module: tests/test_orch_52b_docs_standard.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-12
|
||||
type: integration
|
||||
description: "Шаблон 01-brd содержит обязательные секции: Бизнес-контекст, Объём, Бизнес-требования, NFR"
|
||||
module: tests/test_orch_52b_docs_standard.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-13
|
||||
type: integration
|
||||
description: "Шаблон 02-trz содержит обязательные секции: Задействованные модули, Изменения API, Изменения схемы БД, QG checks"
|
||||
module: tests/test_orch_52b_docs_standard.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-14
|
||||
type: integration
|
||||
description: "Шаблон 03-acceptance-criteria содержит блок AC-N с метками PASS и FAIL"
|
||||
module: tests/test_orch_52b_docs_standard.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-15
|
||||
type: integration
|
||||
description: "Шаблон 04-test-plan.yaml — валидный YAML с ключами work_item и tests (список с id/type/description/module/expected)"
|
||||
module: tests/test_orch_52b_docs_standard.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-16
|
||||
type: integration
|
||||
description: "Раздел ADR-naming присутствует и фиксирует формат ADR-NNN-<slug>.md с нумерацией с 001 и kebab-slug"
|
||||
module: tests/test_orch_52b_docs_standard.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-17
|
||||
type: integration
|
||||
description: "ADR-naming в стандарте совпадает с реальными ADR в репо (напр. существует docs/work-items/ORCH-088/06-adr/ADR-001-*.md)"
|
||||
module: tests/test_orch_52b_docs_standard.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-18
|
||||
type: integration
|
||||
description: "CLAUDE.md содержит ссылку на docs/_standards/PIPELINE_DOCS.md"
|
||||
module: tests/test_orch_52b_docs_standard.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-19
|
||||
type: integration
|
||||
description: "docs/architecture/README.md содержит ссылку на стандарт документов"
|
||||
module: tests/test_orch_52b_docs_standard.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-20
|
||||
type: integration
|
||||
description: "CHANGELOG.md содержит запись об ORCH-52b/ORCH-075 в разделе Unreleased"
|
||||
module: tests/test_orch_52b_docs_standard.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-21
|
||||
type: integration
|
||||
description: "Регресс: полный прогон pytest tests/ зелёный (docs-изменение не ломает существующие тесты)"
|
||||
module: tests/
|
||||
expected: PASS
|
||||
@@ -0,0 +1,160 @@
|
||||
# ADR-001: Стандарт документов пайплайна (docs/_standards + docs/_templates + ADR-naming)
|
||||
|
||||
Work Item: **ORCH-075** (ORCH-52b — слой 1 эпика ORCH-52)
|
||||
Стадия: **architecture**
|
||||
Сквозная регистрация: **`docs/architecture/adr/adr-0019-pipeline-docs-standard.md`** (решение
|
||||
кросс-каттинговое — задаёт правила доко-письма для ВСЕХ агентских ролей).
|
||||
|
||||
## Статус
|
||||
Proposed
|
||||
|
||||
## Контекст
|
||||
Агенты конвейера (`analyst → architect → developer → reviewer → tester → deployer` + системные
|
||||
акторы deploy-finalizer / post-deploy-monitor / security-гейт) пишут номерные документы work item
|
||||
(`00-business-request.md` … `17-security-report.md`) «с нуля по памяти». Каталогов
|
||||
`docs/_standards/` и `docs/_templates/` не существует (проверено в репо). Следствия:
|
||||
|
||||
- **Разнобой структуры** одного и того же документа от задачи к задаче (набор/порядок секций
|
||||
плавает) — затрудняет ревью и онбординг новых ролей.
|
||||
- **Риск рассинхрона машинных вердиктов.** Доки `12-review.md` / `13-test-report.md` /
|
||||
`14-deploy-log.md` / `15-staging-log.md` / `17-security-report.md` несут frontmatter-ключ,
|
||||
который читает гейт (`verdict:` / `result:` / `deploy_status:` / `staging_status:` /
|
||||
`security_status:`), но единого канонического скелета с этим ключом нет → агент может выдать
|
||||
ключ не того имени/регистра и уронить гейт ложно.
|
||||
- **Нет карты «стадия → агент → документ → гейт».** Какая роль пишет какой документ и где он
|
||||
проверяется — нигде не зафиксировано целостно.
|
||||
|
||||
Эпик ORCH-52 намеренно разбит на слои: **сначала зафиксировать договорённость** (golden source
|
||||
структуры доков), и лишь потом, отдельной задачей (52c), навесить машинную проверку
|
||||
frontmatter/шаблонов на гейте. Слой 1 (эта задача) — **только документация**: манифест,
|
||||
канонические шаблоны, конвенция ADR-naming. Любой валидатор/правка кода/правка промптов — вне
|
||||
scope. Ключевое архитектурное ограничение задачи (NFR-1): **ни строки кода/гейтов** — изменения
|
||||
строго под `docs/**` (+ `CLAUDE.md` / `CHANGELOG.md`).
|
||||
|
||||
Ground-truth для манифеста — **фактические** `STAGE_TRANSITIONS` (`src/stages.py`), `QG_CHECKS` /
|
||||
`check_*` (`src/qg/checks.py`), `src/stage_engine.py` и реальные эталонные доки (ORCH-088/073/089/071),
|
||||
а не вымысел. Сверка проведена на стадии architecture (см. §Решение D5).
|
||||
|
||||
## Решение
|
||||
|
||||
### Сводка
|
||||
Создать **документационный стандарт** из трёх артефактов, выведенный из фактического кода и
|
||||
эталонных доков, и подключить его ссылками из точек-онбординга. Никаких рантайм-изменений.
|
||||
|
||||
- `docs/_standards/PIPELINE_DOCS.md` — манифест-карта «стадия → документ → агент → категория →
|
||||
гейт/механизм → frontmatter machine-key» + раздел ADR-naming.
|
||||
- `docs/_templates/*` — копируемые скелеты для каждого `required` / `when-applicable` дока.
|
||||
- Ссылки из `CLAUDE.md` и `docs/architecture/README.md`; запись в `CHANGELOG.md`.
|
||||
|
||||
### D1 — Местоположение и разделение «стандарт» vs «шаблон»
|
||||
Два каталога с раздельной ответственностью: `docs/_standards/` — **описательный** golden source
|
||||
(манифест, конвенции, человекочитаемая карта); `docs/_templates/` — **копируемые** скелеты для
|
||||
заполнения. Префикс `_` выводит служебные каталоги наверх листинга и визуально отделяет их от
|
||||
`docs/work-items/` / `docs/architecture/` / `docs/operations/`. Шаблоны — НЕ work item, не имеют
|
||||
`<plane-id>`, не парсятся гейтами (живут вне `docs/work-items/`), поэтому не влияют на
|
||||
`check_architecture_done` / `check_analysis_complete` и не попадают под ретро-фит.
|
||||
|
||||
### D2 — Манифест как производная от кода, а не параллельный источник истины
|
||||
`PIPELINE_DOCS.md` **документирует** текущее поведение гейтов, но НЕ становится их источником
|
||||
истины (источник остаётся `src/`). Это устраняет класс «манифест разошёлся с кодом»: при будущем
|
||||
изменении гейта (ORCH-52c+) правка кода первична, манифест — следом. Манифест честно различает:
|
||||
- **machine-verdict доки** (`12,13,14,15,17`) — несут frontmatter-ключ, читаемый гейтом; в
|
||||
манифесте помечены ключом и именем `check_*`;
|
||||
- **информационные доки** (`00,08,10,16`) — гейтом не парсятся; помечены явно как не-гейтящиеся
|
||||
(чтобы не возникало ложного ожидания, что их структура что-то блокирует).
|
||||
|
||||
Под-гейты ребра `deploy-staging → deploy` (`check_security_gate` → `check_branch_mergeable` →
|
||||
`check_staging_image_fresh`) в манифесте отмечаются как **врезки в `advance_stage`**, а НЕ строки
|
||||
`STAGE_TRANSITIONS` — иначе карта соврёт о топологии машины стадий (AC-7).
|
||||
|
||||
### D3 — Шаблоны выведены из эталонов, новые секции не изобретаются
|
||||
Скелеты извлекаются из реальных «golden» задач (ORCH-088/073/089/071) и текущей задачи ORCH-075.
|
||||
Инвариант (NFR-3): **в шаблоне нет секции, которой никто не пишет в эталонах**, и наоборот — общий
|
||||
канон секций эталона присутствует. Минимальный обязательный набор секций по каждому документу
|
||||
зафиксирован в TRZ §FR-2.1 и является контрактом приёмки (AC-2/AC-4). Документы с машинным
|
||||
вердиктом несут в шаблоне точный frontmatter-ключ из ground-truth таблицы (D5), чтобы скопированный
|
||||
скелет проходил гейт без догадок.
|
||||
|
||||
### D4 — ADR-naming: канонизация сложившейся традиции, не новый формат
|
||||
Зафиксировать **уже существующий** формат, не вводя нового:
|
||||
- Путь: `docs/work-items/<plane-id>/06-adr/`.
|
||||
- Имя: `ADR-NNN-<kebab-slug>.md`; `NNN` с `001`, инкремент при нескольких ADR в одной задаче.
|
||||
- `slug` — kebab-case (нижний регистр, дефисы), отражает суть решения.
|
||||
- Кросс-каттинговые решения **дублируются** в глобальном реестре
|
||||
`docs/architecture/adr/adr-NNNN-<slug>.md` (4-значная сквозная нумерация) — это уже действующая
|
||||
конвенция (подтверждено: реестр идёт до `adr-0018`), лишь записывается.
|
||||
- Примеры из репо (проверены): `ADR-001-serial-gate`, `ADR-001-auto-label-gates`,
|
||||
`ADR-001-merge-verify-gate`.
|
||||
|
||||
Сам этот ADR следует конвенции и дублируется как `adr-0019` — стандарт демонстрирует себя.
|
||||
|
||||
### D5 — Достоверность: сверка манифеста с `src/` на стадии architecture (NFR-2)
|
||||
Перед фиксацией манифеста ground-truth сверен с кодом. Подтверждено:
|
||||
|
||||
| Документ | Гейт / механизм | Machine-key | Подтверждено в |
|
||||
|----------|-----------------|-------------|----------------|
|
||||
| `01–04` | `check_analysis_approved` (exit `analysis→architecture`); helper `check_analysis_complete` (наличие `01/02/03/04`) | — | `stages.py`, `qg/checks.py:check_analysis_complete` |
|
||||
| `06-adr/` | `check_architecture_done` (наличие каталога `06-adr/` ≥1 файл ИЛИ `07-infra-requirements.md`) | — | `qg/checks.py:check_architecture_done` |
|
||||
| `12-review.md` | `check_reviewer_verdict` | `verdict:` | `qg/checks.py` |
|
||||
| `13-test-report.md` | `check_tests_passed` | `result:`/`verdict:`/`status:` (три равноранговых, ORCH-047) | `qg/checks.py:_parse_tests_verdict` |
|
||||
| `14-deploy-log.md` | `check_deploy_status` | `deploy_status:` | `qg/checks.py:_parse_deploy_status` |
|
||||
| `15-staging-log.md` | `check_staging_status` (self-hosting; иначе N/A — ORCH-35) | `staging_status:` | `qg/checks.py:_parse_staging_status` |
|
||||
| `17-security-report.md` | `check_security_gate` (под-гейт ребра `deploy-staging→deploy`) | `security_status:` | `qg/checks.py` |
|
||||
| `16-post-deploy-log.md` | информационный (пост-`done` наблюдение ORCH-021, не ребро) | `post_deploy_status:` (не гейтится) | `stage_engine.run_post_deploy_monitor` |
|
||||
| `00/08/10` | не гейтятся (вход / информационные) | — | — |
|
||||
|
||||
`STAGE_TRANSITIONS` (проверено): `analysis→architecture→development→review→testing→deploy-staging
|
||||
→deploy→done`; рёбра несут ровно `check_analysis_approved / check_architecture_done / check_ci_green
|
||||
/ check_reviewer_verdict / check_tests_passed / check_staging_status / check_deploy_status`.
|
||||
Под-гейты `security/merge/image-freshness` в `STAGE_TRANSITIONS` **отсутствуют** (врезки в
|
||||
`advance_stage`) — подтверждает D2.
|
||||
|
||||
### D6 — Разграничение ответственности стадий (что пишет архитектор vs разработчик)
|
||||
Эта стадия (architecture) производит **только** ADR + tech-risks (+ N/A infra/data). Сами артефакты
|
||||
стандарта (`PIPELINE_DOCS.md`, `docs/_templates/*`) и правки точек-ссылок
|
||||
(`CLAUDE.md` / `docs/architecture/README.md` / `CHANGELOG.md`) создаёт стадия development по TRZ §2 —
|
||||
чтобы не было двойного авторства и конфликтов. Архитектор фиксирует **контракт** (что и где должно
|
||||
появиться, по каким инвариантам), разработчик его **реализует**.
|
||||
|
||||
## Альтернативы
|
||||
- **Один файл-стандарт без каталога шаблонов** — отвергнуто: шаблон должен быть копируемым
|
||||
отдельным файлом (UX «скопировал и заполнил», AC-2), а не вырезкой из прозы манифеста.
|
||||
- **Сразу валидатор frontmatter на гейте** — отвергнуто намеренно (это ORCH-52c): нарушило бы
|
||||
NFR-1 (правка кода/гейтов) и подняло бы групповой self-hosting риск без предварительной фиксации
|
||||
договорённости. Слой «стандарт» обязан предшествовать слою «проверка».
|
||||
- **Манифест как источник истины для гейтов** — отвергнуто: породило бы дубль-истину и класс
|
||||
«манифест ≠ код». Источник остаётся `src/`; манифест — производная (D2).
|
||||
- **Положить шаблоны в `docs/work-items/_template/`** — отвергнуто: попадание под `docs/work-items/`
|
||||
с `<plane-id>`-семантикой риск-фактор для гейтов наличия файлов и сканеров; служебный каталог
|
||||
должен быть вне дерева work item (D1).
|
||||
- **Ретро-фит существующих доков под новый шаблон** — отвергнуто (вне scope, BRD §2.2): массовая
|
||||
правка истории — отдельный риск и шум; стандарт применяется к новым задачам вперёд.
|
||||
- **Не заводить глобальный `adr-0019`** — отвергнуто: решение кросс-каттинговое (правила
|
||||
доко-письма для всех ролей), а FR-3 сам канонизирует дублирование сквозных решений в глобальный
|
||||
реестр — стандарт обязан следовать собственному правилу.
|
||||
|
||||
## Последствия
|
||||
- **+** Единая карта «стадия → агент → документ → гейт → machine-key»; копируемые скелеты →
|
||||
меньше разнобоя и ложных падений гейтов из-за неверного frontmatter-ключа; ADR-naming перестаёт
|
||||
быть устной традицией; готовая база для ORCH-52c (валидатор).
|
||||
- **+ Нулевой рантайм-риск (NFR-1/NFR-5):** изменения только под `docs/**` + `CLAUDE.md` +
|
||||
`CHANGELOG.md`. `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / `src/stage_engine.py` / схема БД —
|
||||
**не трогаются**. Удаление новых файлов полностью откатывает изменение без следов в поведении
|
||||
системы (обратимость).
|
||||
- **− Дрейф во времени:** манифест — снимок поведения гейтов; при будущей правке гейта его нужно
|
||||
обновлять вручную (до ORCH-52c, где появится проверка). Митигейшн: D2 (источник истины — код) +
|
||||
reviewer-правило «обновлена ли документация» + явная привязка манифеста к именам `check_*`.
|
||||
- **−** Стандарт описательный, не принуждающий: агент может его проигнорировать (форсинг — 52c).
|
||||
Осознанно принято как цена слоистого подхода.
|
||||
- **Откат:** удалить `docs/_standards/PIPELINE_DOCS.md`, `docs/_templates/*`, снять ссылки и запись
|
||||
CHANGELOG — система ведёт себя в точности как до ORCH-075.
|
||||
|
||||
## Ссылки
|
||||
- BRD: `docs/work-items/ORCH-075/01-brd.md`
|
||||
- TRZ: `docs/work-items/ORCH-075/02-trz.md` (ground-truth таблица FR-1, секции FR-2.1)
|
||||
- Acceptance: `docs/work-items/ORCH-075/03-acceptance-criteria.md`
|
||||
- Tech-risks: `docs/work-items/ORCH-075/10-tech-risks.md`
|
||||
- Глобальный реестр: `docs/architecture/adr/adr-0019-pipeline-docs-standard.md`
|
||||
- Эталоны скелетов: ORCH-088 / ORCH-073 / ORCH-089 / ORCH-071 (`docs/work-items/*/`)
|
||||
- Сверено по коду: `src/stages.py` (`STAGE_TRANSITIONS`), `src/qg/checks.py` (`QG_CHECKS`,
|
||||
`_parse_*`), `src/stage_engine.py`.
|
||||
25
docs/work-items/ORCH-075/07-infra-requirements.md
Normal file
25
docs/work-items/ORCH-075/07-infra-requirements.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# 07 — Инфра-требования: ORCH-075 (ORCH-52b — стандарт документов)
|
||||
|
||||
Work Item: **ORCH-075** · Repo: **orchestrator** · Стадия: architecture
|
||||
|
||||
## I-1. Топология / окружения
|
||||
**N/A.** Изменение docs-only: создаются `docs/_standards/PIPELINE_DOCS.md`, `docs/_templates/*` и
|
||||
правятся `CLAUDE.md` / `docs/architecture/README.md` / `CHANGELOG.md`. Контейнеры (`orchestrator`
|
||||
8500, `orchestrator-staging` 8501), Docker Compose, сеть, тома, хост mva154 — **не затрагиваются**.
|
||||
|
||||
## I-2. Переменные окружения / секреты
|
||||
**N/A.** Новые env-переменные не вводятся; `.env` / `.env.staging` / `.env.example` не меняются;
|
||||
секретов не добавляется.
|
||||
|
||||
## I-3. Деплой / рестарт
|
||||
**N/A.** Рантайм-поведение не меняется → прод-рестарт не требуется. Изменение проходит штатный
|
||||
self-hosting путь (`deploy-staging` 8501 → `deploy` 8500) как обычный PR, но эффект деплоя — лишь
|
||||
появление новых docs-файлов в образе; функциональной нагрузки на рестарт нет. Self-hosting инвариант
|
||||
соблюдён: **не ронять / не рестартить прод вне staging-гейта** — здесь это и не нужно.
|
||||
|
||||
## I-4. CI/CD
|
||||
Без изменений в `.gitea/workflows/`. Добавляется один тестовый файл
|
||||
`tests/test_orch_52b_docs_standard.py` (структурные проверки), исполняемый существующим pytest-шагом.
|
||||
|
||||
> Вывод: инфраструктурных требований нет. Файл создан для аудитопригодности (явное N/A), а не из-за
|
||||
> изменения топологии.
|
||||
28
docs/work-items/ORCH-075/08-data-requirements.md
Normal file
28
docs/work-items/ORCH-075/08-data-requirements.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# 08 — Требования к данным: ORCH-075 (ORCH-52b — стандарт документов)
|
||||
|
||||
Work Item: **ORCH-075** · Repo: **orchestrator** · Стадия: architecture
|
||||
|
||||
## Изменения схемы БД
|
||||
**N/A.** Изменение docs-only. Таблицы SQLite (`jobs`, `tasks`, `job_deps`, `repo_freeze`,
|
||||
`agent_runs`, `tracker_messages`, …), индексы, миграции (`init_db`) — **не затрагиваются**.
|
||||
|
||||
## Новые/изменённые сущности
|
||||
**Нет.** Манифест и шаблоны — статические Markdown/YAML-файлы под `docs/`, вне модели данных
|
||||
рантайма. Гейты наличия файлов (`check_analysis_complete` / `check_architecture_done`) сканируют
|
||||
только `docs/work-items/<plane-id>/` и служебные каталоги `docs/_standards/` / `docs/_templates/` не
|
||||
видят (см. ADR-001 §D1, риск TR-6).
|
||||
|
||||
## Frontmatter machine-keys (документируются, не вводятся)
|
||||
Стандарт лишь **фиксирует** уже существующие машиночитаемые ключи, которые парсят гейты — это НЕ
|
||||
новые поля данных и не изменение хранения:
|
||||
|
||||
| Документ | Ключ | Парсер (`src/qg/checks.py`) |
|
||||
|----------|------|-----------------------------|
|
||||
| `12-review.md` | `verdict:` | `check_reviewer_verdict` |
|
||||
| `13-test-report.md` | `result:` / `verdict:` / `status:` | `_parse_tests_verdict` |
|
||||
| `14-deploy-log.md` | `deploy_status:` | `_parse_deploy_status` |
|
||||
| `15-staging-log.md` | `staging_status:` | `_parse_staging_status` |
|
||||
| `17-security-report.md` | `security_status:` | `check_security_gate` |
|
||||
| `16-post-deploy-log.md` | `post_deploy_status:` | информационный (не гейтится) |
|
||||
|
||||
> Вывод: требований к данным/схеме нет. Файл создан для аудитопригодности (явное N/A).
|
||||
28
docs/work-items/ORCH-075/10-tech-risks.md
Normal file
28
docs/work-items/ORCH-075/10-tech-risks.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# 10 — Технические риски: ORCH-075 (ORCH-52b — стандарт документов)
|
||||
|
||||
Work Item: **ORCH-075** · Repo: **orchestrator** · Стадия: architecture
|
||||
|
||||
> Изменение docs-only (NFR-1): только `docs/**` + `CLAUDE.md` + `CHANGELOG.md`. Рантайм-рисков
|
||||
> деградации прода нет по построению. Основные риски — **достоверность** манифеста и **дрейф**
|
||||
> стандарта относительно кода.
|
||||
|
||||
## Реестр рисков
|
||||
|
||||
| ID | Риск | Вер. | Влия. | Митигейшн |
|
||||
|----|------|------|-------|-----------|
|
||||
| TR-1 | **Рассинхрон манифеста с кодом** — манифест приписывает доку неверную стадию/гейт/агента или неверный frontmatter-ключ (напр. `12-review` → не `verdict:`). | Сред. | Сред. (вводит агентов в заблуждение, ложные ожидания) | NFR-2: ground-truth сверен с `src/stages.py` / `src/qg/checks.py` / `src/stage_engine.py` на стадии architecture (ADR-001 §D5, таблица сверки). AC-4 проверяет привязку буквально по файлам. Источник истины остаётся код (D2). |
|
||||
| TR-2 | **Дрейф во времени** — будущая правка гейта (ORCH-52c+) не отражается в манифесте, т.к. форсинга нет. | Сред. | Низ. (до 52c — описательный документ) | D2 (источник истины — код, манифест производный); reviewer-правило «обновлена ли документация»; явная привязка строк манифеста к именам `check_*`; ORCH-52c добавит машинную проверку. |
|
||||
| TR-3 | **Шаблон вводит выдуманную секцию** или, наоборот, упускает секцию общего канона эталонов. | Сред. | Низ. | NFR-3: скелеты выведены строго из эталонов ORCH-088/073/089/071 + ORCH-075; TRZ §FR-2.1 фиксирует минимальный обязательный набор секций; AC-2/AC-4 проверяют. |
|
||||
| TR-4 | **Неверный machine-key в шаблоне машинного дока** — скопированный скелет уронит гейт ложно (напр. `deploy_status` написан `Deploy-Status`/иной регистр). | Низ. | Выс. (если бы дошло до прода — ложный откат БАГ-8) | Ключи в шаблонах берутся ДОСЛОВНО из `_parse_*` (`deploy_status`/`staging_status`/`security_status`/`verdict`/`result`); парсеры делают `.upper()` на значении, но имя ключа чувствительно — шаблон фиксирует точное имя. AC-2 проверяет наличие ключа. Гейты при этом **не трогаются** (docs-only). |
|
||||
| TR-5 | **Коллизия имени файла-шаблона ADR** с реальной нумерацией (`06-adr/ADR-NNN-…`). | Низ. | Низ. | TRZ §2: имя шаблона ADR без `<plane-id>`-контекста и вне `docs/work-items/` (напр. `docs/_templates/06-adr-ADR-NNN-slug.md`); внутри фиксируется реальный целевой путь/формат. |
|
||||
| TR-6 | **Шаблоны парсятся гейтами наличия файлов** (`check_architecture_done` / `check_analysis_complete` ловят `docs/_templates/*`). | Оч.низ. | Сред. | D1: служебные каталоги `docs/_standards/` / `docs/_templates/` лежат ВНЕ `docs/work-items/<plane-id>/`; гейты сканируют только путь work item → шаблоны структурно невидимы гейтам. |
|
||||
| TR-7 | **Регресс существующих тестов** от docs-изменения. | Оч.низ. | Сред. | Изменения не трогают `src/`; новый тест `tests/test_orch_52b_docs_standard.py` — только структурные проверки наличия/секций; TC-21 требует зелёного полного `pytest tests/`. |
|
||||
| TR-8 | **CHANGELOG-конфликт при merge** (`## [Unreleased]` правят параллельные задачи). | Низ. | Низ. | Корневой `.gitattributes` `CHANGELOG.md merge=union` (ORCH-073 FR-4) авто-сливает append-правки без конфликта. |
|
||||
|
||||
## Сводный вывод
|
||||
Риск для прод-конвейера (self-hosting) — **отсутствует по построению**: изменение docs-only,
|
||||
полностью обратимо (NFR-5), `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / схема БД не затрагиваются
|
||||
(AC-6). Доминирующий класс рисков — **достоверность и дрейф** манифеста (TR-1/TR-2); закрыт сверкой
|
||||
с `src/` на стадии architecture (ADR-001 §D5) и принципом «источник истины — код, манифест —
|
||||
производная». Эскалация `arch:major-change` не требуется (нет новой стадии/QG/компонента/смены БД).
|
||||
Возврат в анализ не требуется — ТЗ удовлетворяется без нарушения принципов архитектуры.
|
||||
71
docs/work-items/ORCH-075/12-review.md
Normal file
71
docs/work-items/ORCH-075/12-review.md
Normal file
@@ -0,0 +1,71 @@
|
||||
---
|
||||
type: review
|
||||
work_item_id: ORCH-075
|
||||
verdict: APPROVED
|
||||
version: 1
|
||||
---
|
||||
|
||||
# Review ORCH-075 — ORCH-52b: стандарт документов конвейера
|
||||
|
||||
## Summary
|
||||
Docs-only задача: создан golden source структуры номерных документов (`docs/_standards/PIPELINE_DOCS.md`),
|
||||
15 копируемых шаблонов (`docs/_templates/*`), зафиксирована конвенция ADR-naming, заведён сквозной
|
||||
ADR `adr-0019`, обновлены точки-ссылки (CLAUDE.md, architecture/README.md, CHANGELOG.md).
|
||||
Манифест и шаблоны **сверены с фактическим кодом** — соответствие подтверждено. Все 7 критериев
|
||||
приёмки выполнены. P0/P1/P2 findings нет → **APPROVED**.
|
||||
|
||||
## Оси проверки
|
||||
|
||||
### 1. Соответствие ТЗ (02-trz.md)
|
||||
- FR-1 (манифест) — таблица покрывает весь реальный набор `00/01/02/03/04/06/07/08/10/12/13/14/15/16/17`,
|
||||
колонки владелец/категория/стадия/гейт/machine-key присутствуют. ✓
|
||||
- FR-2 (шаблоны) — все 15 шаблонов созданы; секции совпадают с FR-2.1 (спот-чек 01-brd, 02-trz,
|
||||
06-adr, 04-test-plan). ✓
|
||||
- FR-3 (ADR-naming) — §4 фиксирует путь, `ADR-NNN-<kebab-slug>`, связь с глобальным реестром, примеры. ✓
|
||||
- FR-4 (точки-ссылки) — CLAUDE.md (раздел «Артефакты задачи» + правило 2), README §«Стандарт
|
||||
документов конвейера», CHANGELOG `## [Unreleased]` (`docs`-тип). ✓
|
||||
|
||||
### 2. Соответствие ADR (06-adr/ADR-001 + adr-0019)
|
||||
- D2 «манифест документирует, источник истины — код» отражён в самом манифесте (блок «Статус истины»). ✓
|
||||
- D5 ground-truth сверка соответствует тому, что реально читает код (проверено независимо). ✓
|
||||
- Стандарт следует собственной конвенции (заведён `adr-0019`). ✓
|
||||
|
||||
### 3. Качество кода (docs-only) — сверка с `src/`
|
||||
Независимо подтверждено по источнику истины:
|
||||
- `STAGE_TRANSITIONS` (`src/stages.py`) — рёбра и exit-гейты совпадают с манифестом 1:1.
|
||||
- Frontmatter-ключи совпадают с парсерами: `verdict:`→`check_reviewer_verdict`; `result:`/`verdict:`/
|
||||
`status:`→`_parse_tests_verdict`; `deploy_status:`→`_parse_deploy_status`; `staging_status:`→
|
||||
`_parse_staging_status`; `security_status:`→`check_security_gate`/`security_gate.py`.
|
||||
- `check_analysis_complete` (01/02/03/04) и `check_architecture_done` (06-adr ≥1 файл ИЛИ 07-infra) —
|
||||
формулировки манифеста точны.
|
||||
- Под-гейты ребра `deploy-staging→deploy` корректно помечены как врезки в `advance_stage`, не строки
|
||||
`STAGE_TRANSITIONS` (AC-7).
|
||||
- AC-6: `git diff` по `src/` пуст — код/гейты/схема БД не тронуты.
|
||||
|
||||
### 4. Качество тестов
|
||||
`tests/test_orch_52b_docs_standard.py` — 20 содержательных структурных тестов (наличие манифеста,
|
||||
покрытие всех доков, владельцы/категории, frontmatter-ключи каждого машинного шаблона, ADR-naming
|
||||
против реального репо, валидность YAML тест-плана, точки-ссылки, CHANGELOG). Прогон: **20 passed**.
|
||||
|
||||
## Findings
|
||||
|
||||
### P0 — Blocker
|
||||
- нет
|
||||
|
||||
### P1 — Must fix
|
||||
- нет
|
||||
|
||||
### P2 — Should fix
|
||||
- нет
|
||||
|
||||
### P3 — Nice-to-have
|
||||
- [ ] В `06-adr/ADR-001` §D4 формулировка «реестр идёт до `adr-0018`» описывает состояние ДО добавления
|
||||
текущего `adr-0019` (что верно), тогда как `PIPELINE_DOCS.md` §4 говорит «доходит до `adr-0019`».
|
||||
Несоответствие безвредно (разные срезы времени), правка не требуется.
|
||||
|
||||
## Документация
|
||||
Это docs-only задача — документация **является** деливерейблом. `src/` не изменён, поэтому правило
|
||||
CLAUDE.md «изменил src → обнови доку» неприменимо в блокирующем смысле. Сама документация проверена на
|
||||
достоверность против кода (`src/stages.py`, `src/qg/checks.py`, `src/security_gate.py`) и эталонных
|
||||
доков — расхождений нет. Точки-онбординга (CLAUDE.md, architecture/README.md) и CHANGELOG обновлены.
|
||||
Статус документации: **полностью обновлена и верифицирована**.
|
||||
85
docs/work-items/ORCH-075/13-test-report.md
Normal file
85
docs/work-items/ORCH-075/13-test-report.md
Normal file
@@ -0,0 +1,85 @@
|
||||
---
|
||||
type: test-report
|
||||
work_item_id: ORCH-075
|
||||
result: PASS
|
||||
---
|
||||
|
||||
# Test Report — ORCH-075 (ORCH-52b: стандарт документов конвейера)
|
||||
|
||||
## Окружение
|
||||
- Python: 3.12.13
|
||||
- pytest: 8.3.3
|
||||
- Дата: 2026-06-09
|
||||
- Ветка: `feature/ORCH-075-orch-52b-docs-templates-adr-na`
|
||||
- Prod health (`http://localhost:8500/health`): `{"status":"ok","service":"orchestrator"}`
|
||||
- Review verdict (12-review.md): **APPROVED** (предусловие выполнено)
|
||||
|
||||
## Smoke-тест API (read-only, прод не трогался)
|
||||
| Endpoint | Результат |
|
||||
|----------|-----------|
|
||||
| `GET /health` | `{"status":"ok","service":"orchestrator"}` — OK |
|
||||
| `GET /status` | OK — активная задача ORCH-075 (id 68) на стадии `testing` |
|
||||
| `GET /queue` | OK — counts {running:1, done:871, failed:4}, breaker `closed`, reconcile/reaper enabled |
|
||||
|
||||
## Результаты
|
||||
|
||||
### Полный регресс
|
||||
`python -m pytest tests/ -q` → **1177 passed, 1 warning in 38.08s** (warning — Pydantic V2 deprecation в `src/config.py`, не относится к задаче). Регресса от docs-изменения нет.
|
||||
|
||||
### Профильная сюита
|
||||
`python -m pytest tests/test_orch_52b_docs_standard.py -v` → **20 passed in 0.39s**.
|
||||
|
||||
### Сопоставление с тест-планом (04-test-plan.yaml)
|
||||
|
||||
| TC ID | Описание | Результат |
|
||||
|-------|----------|-----------|
|
||||
| TC-01 | PIPELINE_DOCS.md существует и непустой | PASS |
|
||||
| TC-02 | Манифест упоминает все номерные доки (00..17) | PASS |
|
||||
| TC-03 | Манифест указывает владельца-агента для каждого дока | PASS |
|
||||
| TC-04 | Манифест содержит категории required/when-applicable/optional | PASS |
|
||||
| TC-05 | docs/_templates/ содержит шаблоны всех required/when-applicable доков | PASS |
|
||||
| TC-06 | Шаблон 12-review содержит `verdict:` | PASS |
|
||||
| TC-07 | Шаблон 13-test-report содержит `result:` | PASS |
|
||||
| TC-08 | Шаблон 14-deploy-log содержит `deploy_status:` | PASS |
|
||||
| TC-09 | Шаблон 15-staging-log содержит `staging_status:` | PASS |
|
||||
| TC-10 | Шаблон 17-security-report содержит `security_status:` | PASS |
|
||||
| TC-11 | Шаблон 16-post-deploy-log содержит `post_deploy_status:` | PASS |
|
||||
| TC-12 | Шаблон 01-brd содержит обязательные секции | PASS |
|
||||
| TC-13 | Шаблон 02-trz содержит обязательные секции | PASS |
|
||||
| TC-14 | Шаблон 03-acceptance-criteria содержит блок AC-N с PASS/FAIL | PASS |
|
||||
| TC-15 | Шаблон 04-test-plan.yaml — валидный YAML с work_item/tests | PASS |
|
||||
| TC-16 | Раздел ADR-naming фиксирует формат ADR-NNN-<slug>.md (с 001, kebab) | PASS |
|
||||
| TC-17 | ADR-naming совпадает с реальными ADR в репо | PASS |
|
||||
| TC-18 | CLAUDE.md ссылается на docs/_standards/PIPELINE_DOCS.md | PASS |
|
||||
| TC-19 | docs/architecture/README.md ссылается на стандарт | PASS |
|
||||
| TC-20 | CHANGELOG.md содержит запись ORCH-52b/ORCH-075 в Unreleased | PASS |
|
||||
| TC-21 | Регресс: полный прогон pytest tests/ зелёный | PASS |
|
||||
|
||||
### Сопоставление с критериями приёмки (03-acceptance-criteria.md)
|
||||
|
||||
| AC | Критерий | Результат |
|
||||
|----|----------|-----------|
|
||||
| AC-1 | Манифест создан, покрывает весь набор + владелец/категория | PASS (TC-01..04) |
|
||||
| AC-2 | Шаблоны для каждого required/when-applicable + frontmatter-ключи + секции | PASS (TC-05..14) |
|
||||
| AC-3 | ADR-naming зафиксирован | PASS (TC-16) |
|
||||
| AC-4 | Согласованность с эталонами и кодом | PASS (TC-15,17; reviewer сверил с src/) |
|
||||
| AC-5 | Ссылки + CHANGELOG обновлены | PASS (TC-18..20) |
|
||||
| AC-6 | Код гейтов НЕ изменён (docs-only) | PASS — `git diff origin/main...HEAD -- src/` пуст; затронуты только `docs/**`, `CLAUDE.md`, `CHANGELOG.md`, `tests/test_orch_52b_docs_standard.py` |
|
||||
| AC-7 | Манифест различает machine-verdict и информационные доки | PASS (reviewer подтвердил врезки `advance_stage` и разметку гейтов) |
|
||||
|
||||
## Вывод pytest
|
||||
```
|
||||
........................................................................ [ 97%]
|
||||
......................... [100%]
|
||||
=============================== warnings summary ===============================
|
||||
src/config.py:5: PydanticDeprecatedSince20: ...
|
||||
1177 passed, 1 warning in 38.08s
|
||||
```
|
||||
```
|
||||
tests/test_orch_52b_docs_standard.py — 20 passed, 1 warning in 0.39s
|
||||
```
|
||||
|
||||
## Итог
|
||||
**PASS** — полный регресс зелёный (1177 passed), профильная сюита зелёная (20 passed),
|
||||
smoke API OK, изменение строго docs-only (AC-6 подтверждён: `src/` не тронут).
|
||||
Задача готова к стадии `deploy-staging`.
|
||||
12
docs/work-items/ORCH-075/14-deploy-log.md
Normal file
12
docs/work-items/ORCH-075/14-deploy-log.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
deploy_status: SUCCESS
|
||||
work_item: ORCH-075
|
||||
hook_exit_code: 0
|
||||
deployed_by: deploy-finalizer
|
||||
---
|
||||
|
||||
# Deploy log — ORCH-036 executable self-deploy
|
||||
|
||||
Прод-деплой завершён хост-хуком с exit-code `0` -> `deploy_status: SUCCESS`.
|
||||
|
||||
Вердикт зафиксирован детерминированным finalizer'ом (Фаза C), не LLM.
|
||||
49
docs/work-items/ORCH-075/15-staging-log.md
Normal file
49
docs/work-items/ORCH-075/15-staging-log.md
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
staging_status: SUCCESS
|
||||
timestamp: 2026-06-09T10:22:40Z
|
||||
base_url: http://localhost:8501
|
||||
---
|
||||
|
||||
# Staging Gate Log
|
||||
|
||||
Staging test suite completed against the live `orchestrator-staging` instance (8501).
|
||||
Result: **8/10 checks PASS**, exit code **0** → `staging_status: SUCCESS`.
|
||||
|
||||
All REAL (pipeline) checks are green. The only two failures are the known
|
||||
sandbox-infra checks **C9a / C9b**, which depend on SANDBOX bot accounts being
|
||||
project members (infra precondition), not on the pipeline. Per ORCH-061 they are
|
||||
tolerated when every REAL check is green; the suite printed an `INFRA-WAIVED:` line
|
||||
and exited 0 (fail-closed for real checks preserved).
|
||||
|
||||
## Execution
|
||||
|
||||
- Command: `python3 /repos/orchestrator/scripts/staging_check.py --base-url http://localhost:8501 --mode stub`
|
||||
- Ran inside the `orchestrator-staging` container (canonical, ADR-001 / ORCH-048),
|
||||
so the B6 registry-isolation check reads the running instance's own process-env.
|
||||
- Note: the `docker` CLI is not installed in this environment; the exec was issued
|
||||
through the mounted Docker Engine API socket (`/var/run/docker.sock`), which is
|
||||
functionally equivalent to `docker exec orchestrator-staging …`.
|
||||
|
||||
## Observability — waiver line (ORCH-061)
|
||||
|
||||
```
|
||||
INFRA-WAIVED: C9a Branch appears in orchestrator-sandbox, C9b Analyst job enqueued in staging queue (known sandbox-infra; real checks green)
|
||||
VERDICT: SUCCESS (exit 0) — SUCCESS (infra-waived): ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue'] are known sandbox-infra checks; all real checks green
|
||||
```
|
||||
|
||||
## Check summary
|
||||
|
||||
| Block | Check | Result |
|
||||
|-------|-------|--------|
|
||||
| A SMOKE | A1 GET /health → 200 status=ok | ✓ PASS |
|
||||
| A SMOKE | A2 GET /queue → 200 with counts/max_concurrency/resilience | ✓ PASS |
|
||||
| A SMOKE | A3 ORCH_STAGING=true (not prod) | ✓ PASS |
|
||||
| B ACCESS | B4 Plane: sandbox project accessible | ✓ PASS |
|
||||
| B ACCESS | B5 Gitea: orchestrator-sandbox accessible, push=true | ✓ PASS |
|
||||
| B ACCESS | B6 Registry: sandbox present, prod ET/ORCH absent | ✓ PASS |
|
||||
| C E2E | C7 Create issue in Plane SANDBOX | ✓ PASS |
|
||||
| C E2E | C8 Trigger pipeline via /webhook/plane | ✓ PASS |
|
||||
| C E2E | C9a Branch appears in orchestrator-sandbox | ✗ FAIL (SANDBOX_INFRA, waived) |
|
||||
| C E2E | C9b Analyst job enqueued in staging queue | ✗ FAIL (SANDBOX_INFRA, waived) |
|
||||
|
||||
REAL failed: none. SANDBOX_INFRA failed: C9a, C9b (waived). Exit code: 0.
|
||||
7
docs/work-items/ORCH-076/00-business-request.md
Normal file
7
docs/work-items/ORCH-076/00-business-request.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Business Request: ORCH-52c: протокол handoff + frontmatter-контракт (writer/валидатор/схема)
|
||||
|
||||
Work Item ID: ORCH-076
|
||||
|
||||
## Description
|
||||
|
||||
TBD
|
||||
151
docs/work-items/ORCH-076/01-brd.md
Normal file
151
docs/work-items/ORCH-076/01-brd.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# 01 — BRD (бизнес-требования): ORCH-076 — ORCH-52c: протокол handoff + frontmatter-контракт (writer/валидатор/схема)
|
||||
|
||||
Work Item: **ORCH-076** · Repo: **orchestrator** · Стадия: analysis
|
||||
|
||||
## 1. Бизнес-контекст и проблема
|
||||
|
||||
Это **слой 2 эпика ORCH-52** (стандартизация документного конвейера). Слой 1 (ORCH-52b /
|
||||
ORCH-075) уже в `main`: создан **описательный** стандарт `docs/_standards/PIPELINE_DOCS.md`
|
||||
+ копируемые скелеты `docs/_templates/*`. Стандарт честно фиксирует карту «стадия → агент →
|
||||
документ → гейт → frontmatter machine-key», но прямо помечен как слой описательный:
|
||||
«Машинная проверка соответствия шаблонам/frontmatter — отдельная задача ORCH-52c».
|
||||
|
||||
Установленные факты (проверено в репо на ветке задачи):
|
||||
|
||||
- **`src/frontmatter.py` = ТОЛЬКО reader.** Единственная функция
|
||||
`read_frontmatter_value(path, key) -> str | None` (single-key, ~2.6 KB). В docstring
|
||||
модуля прямой коммент: *«merging into a single parser is a follow-up task»* — это и есть
|
||||
ORCH-52c. Контракт reader — **never raises** (любая ошибка → `None` + `logger.debug`).
|
||||
- **Протокол вердиктов размазан по отдельным парсерам с дублированной ~10-строчной
|
||||
YAML-frontmatter-логикой:**
|
||||
- `src/qg/checks.py::check_reviewer_verdict` — читает `verdict:` из `12-review.md`;
|
||||
- `src/qg/checks.py::_parse_tests_verdict` — читает `result:`/`verdict:`/`status:` из
|
||||
`13-test-report.md` (три равноранговых поля, ORCH-047);
|
||||
- `src/qg/checks.py::_parse_deploy_status` — читает `deploy_status:` из `14-deploy-log.md`;
|
||||
- `src/qg/checks.py::_parse_staging_status` — читает `staging_status:` из `15-staging-log.md`;
|
||||
- `src/security_gate.py::parse_security_status` — читает `security_status:` из `17-security-report.md`;
|
||||
- `src/post_deploy.py` — пишет/читает `post_deploy_status:` в `16-post-deploy-log.md`;
|
||||
- `src/review_parse.py` — defensive-извлечение прозы (`_strip_frontmatter`).
|
||||
Каждый парсер заново реализует `content.startswith("---")` → `split("---", 2)` →
|
||||
`yaml.safe_load`. Единого контракта нет → риск рассинхрона (разная обработка ошибок,
|
||||
разный набор токенов, разный регистр).
|
||||
- **Нет формальной спеки handoff:** нигде не зафиксировано «что КАЖДАЯ стадия ОБЯЗАНА
|
||||
оставить на выходе» (полный список артефактов + обязательные frontmatter-ключи) как
|
||||
единый контракт передачи между стадиями.
|
||||
|
||||
**Боль/риск:** без единого контракта чтения вердиктов и без обязательной схемы frontmatter
|
||||
каждая правка одного парсера может разойтись с остальными; новый агентский документ легко
|
||||
написать с неверным ключом/регистром (гейт упадёт ложно), а отсутствие машинной проверки
|
||||
схемы оставляет соблюдение стандарта на ручную дисциплину reviewer'а.
|
||||
|
||||
**⚠️ Self-hosting.** Задача меняет КОД, читающий вердикты НА ГЕЙТАХ (review/staging/security/
|
||||
tester/deploy) в инструменте, который сейчас обслуживает прод (enduro-trails) из общего
|
||||
инстанса. Любой регресс чтения вердикта = остановка конвейера всех проектов. Поэтому
|
||||
рефакторинг обязан быть строго обратно совместимым и fail-safe.
|
||||
|
||||
## 2. Объём (scope)
|
||||
|
||||
### В объёме
|
||||
- **Спека handoff** в `docs/_standards/` (рядом с `PIPELINE_DOCS.md`): формальный контракт
|
||||
«стадия → обязательный выход» (какие документы + какие frontmatter-ключи обязательны на
|
||||
выходе каждой стадии), согласованный с манифестом ORCH-52b.
|
||||
- **Расширение `src/frontmatter.py`:** к существующему reader добавить **writer** (запись
|
||||
YAML-frontmatter) и **валидатор** обязательной схемы. Обязательная схема:
|
||||
`work_item`, `stage`, `author_agent`, `status`, `created_at`, `model_used`.
|
||||
- **Единый контракт вердиктов в одном месте** (док + единый frontmatter-API): гейты
|
||||
(reviewer→`verdict:`, tester→`result:`, deployer→`deploy_status:`, staging→`staging_status:`,
|
||||
security→`security_status:`) читают СТАНДАРТНЫЕ поля через единый frontmatter-API, а не
|
||||
через разрознённые ad-hoc парсеры.
|
||||
- Обновление документации (CLAUDE.md, architecture/README, ADR — глобальный и per-work-item,
|
||||
CHANGELOG).
|
||||
|
||||
### Вне объёма
|
||||
- **Правка промптов агентов** (`.openclaw/agents/*.md`), чтобы те эмитили новую полную схему
|
||||
— это **ORCH-52d** (слой 3).
|
||||
- **Ретро-фит старых документов** (дописывание новой схемы в уже существующие work-items).
|
||||
- Изменение `STAGE_TRANSITIONS` и **состава** `QG_CHECKS` (какие гейты существуют).
|
||||
- Изменение **семантики** вердиктов (какое значение → какой переход) — только КАК они
|
||||
читаются.
|
||||
- Включение hard-fail валидации схемы по умолчанию (дефолт — warning; hard-fail только под
|
||||
явно включённым kill-switch).
|
||||
|
||||
## 3. Заинтересованные стороны
|
||||
|
||||
- **Заказчик / Owner** — Слава (homenet542): подтверждает BRD (ручной гейт остаётся ручным).
|
||||
- **Самообслуживаемый инструмент (self-hosting)** — оркестратор правит сам себя; задача —
|
||||
первый боевой тест `autoDeploy` (см. примечание ниже).
|
||||
- **Затрагиваемые роли конвейера** — reviewer / tester / deployer / security-гейт (их
|
||||
вердикты теперь читаются через единый API); architect/analyst (новая обязательная схема
|
||||
для будущих документов, фактическое внедрение — ORCH-52d).
|
||||
- **Другие проекты (enduro-trails)** — НЕ должны почувствовать изменений (нулевая регрессия).
|
||||
|
||||
## 4. Бизнес-требования (BR)
|
||||
|
||||
- **BR-1** — `src/frontmatter.py` предоставляет полный набор операций над YAML-frontmatter:
|
||||
**reader** (сохранён без изменения контракта), **writer** (сериализация frontmatter в
|
||||
документ), **валидатор** (проверка обязательной схемы).
|
||||
- **BR-2** — Обязательная схема frontmatter определена и проверяема: поля `work_item`,
|
||||
`stage`, `author_agent`, `status`, `created_at`, `model_used`.
|
||||
- **BR-3** — Создана формальная спека handoff в `docs/_standards/`, согласованная с
|
||||
`PIPELINE_DOCS.md`: для каждой стадии указано, какие документы и какие frontmatter-ключи
|
||||
она обязана оставить на выходе.
|
||||
- **BR-4** — Контракт вердиктов сведён в ОДНО место; все пять гейтов-вердиктов
|
||||
(review/staging/security/tester/deploy) читают стандартные поля через единый
|
||||
frontmatter-API, а не через разрознённые парсеры.
|
||||
- **BR-5** — Семантика вердиктов неизменна: то же значение → тот же переход/откат, что и
|
||||
сейчас (включая трёх-полевой контракт tester'а ORCH-047 и токен-логику BLOCKED/FAILED).
|
||||
|
||||
## 5. Нефункциональные требования (NFR)
|
||||
|
||||
- **NFR-1 (обратная совместимость, критично self-hosting)** — Существующие документы-вердикты
|
||||
БЕЗ новой полной схемы ПРОДОЛЖАЮТ читаться гейтами (fallback на текущее поведение). Старый
|
||||
`12/13/14/15/17`-док без `work_item/stage/...` парсится по вердикт-ключу как раньше.
|
||||
- **NFR-2 (never-raise / fail-safe)** — Ошибка writer'а или валидатора НЕ роняет конвейер
|
||||
(тот же контракт, что у reader: любая ошибка → лог + безопасное значение, исключение
|
||||
наружу не выходит).
|
||||
- **NFR-3 (валидатор не self-block)** — Валидатор обязательной схемы НЕ является hard-fail
|
||||
на гейте по умолчанию (иначе сама ORCH-52c заблокировала бы себя на собственном деплое,
|
||||
т.к. её документы и документы соседей ещё без полной схемы). Дефолт — warning/лог;
|
||||
жёсткость — под kill-switch (флаг).
|
||||
- **NFR-4 (нулевая регрессия для enduro)** — Поведение для не-self-hosting репозиториев и
|
||||
всех существующих гейтов остаётся 1:1; полный регресс `tests/` зелёный.
|
||||
- **NFR-5 (обратимость)** — Поведенческие изменения (если есть, напр. строгая валидация)
|
||||
закрываются kill-switch с дефолтом, эквивалентным прежнему поведению.
|
||||
|
||||
## 6. Допущения и ограничения
|
||||
|
||||
- Frontmatter везде в каноне — ведущий YAML-блок между `---` … `---` (как в `qg/checks.py`
|
||||
и `frontmatter.py`).
|
||||
- Источник истины о поведении гейтов остаётся КОД (`src/stages.py`, `src/qg/checks.py`,
|
||||
`src/stage_engine.py`); спека/манифест документируют, а не управляют (правило ORCH-075).
|
||||
- `model_used` в схеме — это модель, которой документ создан; фактический источник значения
|
||||
для агентских доков — резолв `resolve_agent_model` (ORCH-41); проставление в реальные
|
||||
документы агентами — ORCH-52d, вне scope.
|
||||
- `pyyaml` уже зависимость проекта (используется во всех существующих парсерах).
|
||||
- Реализационные решения (одна функция-парсер vs класс, точная сигнатура writer/валидатора,
|
||||
имя модуля контракта вердиктов) — прерогатива архитектора (06-adr), здесь не предрешаются.
|
||||
|
||||
## 7. Критерии успеха
|
||||
|
||||
Задача успешна, если: `src/frontmatter.py` несёт reader+writer+валидатор обязательной схемы;
|
||||
спека handoff создана и согласована с `PIPELINE_DOCS.md`; все пять гейтов-вердиктов читают
|
||||
через единый frontmatter-API; старые доки-вердикты продолжают проходить гейты (анти-регресс);
|
||||
ошибка writer/валидатора не роняет конвейер, hard-fail валидации под kill-switch (дефолт —
|
||||
warning); `STAGE_TRANSITIONS` и состав `QG_CHECKS` не изменены, семантика вердиктов неизменна;
|
||||
документация обновлена; **сама ORCH-52c проходит свои гейты** (включая первый боевой
|
||||
`autoDeploy`). Детальные PASS/FAIL — `03-acceptance-criteria.md`.
|
||||
|
||||
## 8. Риски
|
||||
|
||||
- **Регресс чтения вердикта на гейте** → остановка конвейера всех проектов (главный риск
|
||||
self-hosting). Митигация — строгая обратная совместимость + полный регресс тестов гейтов.
|
||||
- **Самоблокировка валидатором** на собственном деплое (документы без полной схемы).
|
||||
Митигация — NFR-3 (валидатор не hard-fail по умолчанию).
|
||||
- **Расхождение спеки handoff с фактом кода** → «лживый» стандарт. Митигация — согласование
|
||||
с `PIPELINE_DOCS.md` и явная пометка «источник истины — код».
|
||||
- **Первый боевой `autoDeploy`** — авто-подтверждение прод-деплоя орка (см. примечание).
|
||||
Детали митигации/наблюдения — задача архитектора (`10-tech-risks.md`).
|
||||
|
||||
> **Примечание (АВТО-ДЕПЛОЙ).** На этой задаче выставлен лейбл `autoDeploy` (ORCH-089): орк
|
||||
> САМ подтверждает прод-деплой после зелёного staging + всех тех-гейтов. BRD-гейт остаётся
|
||||
> ручным (Слава подтверждает BRD). Это первый боевой тест `autoDeploy`.
|
||||
124
docs/work-items/ORCH-076/02-trz.md
Normal file
124
docs/work-items/ORCH-076/02-trz.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# 02 — ТЗ (TRZ): ORCH-076 — ORCH-52c: протокол handoff + frontmatter-контракт (writer/валидатор/схема)
|
||||
|
||||
Work Item: **ORCH-076** · Repo: **orchestrator** · Стадия: analysis
|
||||
|
||||
> ТЗ описывает **конкретные изменения к реализации**, выведенные из BRD и фактического кода.
|
||||
> Архитектурное обоснование/решения (как именно структурировать модуль контракта вердиктов,
|
||||
> точные сигнатуры) — задача архитектора (06-adr).
|
||||
|
||||
## 1. Сводка изменения
|
||||
|
||||
ORCH-52c превращает `src/frontmatter.py` из single-key reader в полный frontmatter-контракт
|
||||
(**reader + writer + валидатор обязательной схемы**) и сводит **разрознённое чтение вердиктов**
|
||||
гейтов к **единому frontmatter-API**, не меняя ни состав гейтов, ни семантику вердиктов.
|
||||
Дополнительно создаётся **формальная спека handoff** в `docs/_standards/`, согласованная с
|
||||
манифестом ORCH-52b (`PIPELINE_DOCS.md`). Всё строго обратно совместимо (старые доки читаются
|
||||
как раньше), never-raise, валидатор не hard-fail по умолчанию (kill-switch).
|
||||
|
||||
## 2. Задействованные модули / пути
|
||||
|
||||
| Путь | Действие |
|
||||
|------|----------|
|
||||
| `src/frontmatter.py` | **изменить** — добавить writer + валидатор + чтение всего frontmatter (multi-key/dict); reader `read_frontmatter_value` сохранить (контракт неизменен) |
|
||||
| `src/qg/checks.py` | **изменить** — `check_reviewer_verdict`, `_parse_tests_verdict`, `_parse_deploy_status`, `_parse_staging_status` перевести на чтение через единый frontmatter-API (поведение/токены/семантика 1:1) |
|
||||
| `src/security_gate.py` | **изменить** — `parse_security_status` читает `security_status:` через единый API (семантика 1:1) |
|
||||
| `src/post_deploy.py` | **изменить (по решению архитектора)** — чтение `post_deploy_status:` через единый API (информационный, не гейт) |
|
||||
| `src/review_parse.py` | **возможно изменить** — `_strip_frontmatter` может использовать общий хелпер; контракт «never raise → ""» сохранить |
|
||||
| `src/config.py` | **изменить** — добавить kill-switch строгой валидации (напр. `frontmatter_validation_strict: bool = False`) |
|
||||
| `docs/_standards/HANDOFF_PROTOCOL.md` (имя — на усмотрение архитектора/стандарта) | **создать** — формальная спека handoff «стадия → обязательный выход» |
|
||||
| `docs/_standards/PIPELINE_DOCS.md` | **изменить** — связать со спекой handoff, отметить что ORCH-52c реализовала машинный контракт |
|
||||
| `tests/test_frontmatter.py` | **создать** — unit на reader/writer/валидатор/round-trip |
|
||||
| `tests/` (гейты) | **изменить/создать** — анти-регресс тесты чтения вердиктов через новый API |
|
||||
| `CLAUDE.md`, `docs/architecture/README.md`, `CHANGELOG.md`, ADR | **изменить/создать** — документация |
|
||||
|
||||
## 3. Функциональные требования
|
||||
|
||||
### FR-1 — Writer frontmatter (BR-1)
|
||||
В `src/frontmatter.py` добавить функцию записи: принимает данные frontmatter (mapping
|
||||
ключ→значение) и тело документа, возвращает/записывает строку с каноничным ведущим
|
||||
YAML-блоком `---\n…\n---\n<body>`. Формат на 100% совместим с существующими парсерами
|
||||
(`split("---", 2)` + `yaml.safe_load`). **never-raise** (NFR-2): ошибка сериализации/записи →
|
||||
лог + безопасный результат, исключение наружу не выходит. Точная сигнатура (in-memory render
|
||||
vs запись в файл, перезапись существующего frontmatter) — решение архитектора.
|
||||
|
||||
### FR-2 — Валидатор обязательной схемы (BR-2, NFR-3)
|
||||
В `src/frontmatter.py` добавить валидатор, проверяющий наличие обязательных полей схемы:
|
||||
`work_item`, `stage`, `author_agent`, `status`, `created_at`, `model_used`. Возвращает
|
||||
структурированный результат (список отсутствующих/невалидных полей + признак валидности).
|
||||
**Поведение по умолчанию — warning/лог, НЕ blocker** (NFR-3): отсутствие полей не роняет
|
||||
конвейер и не заваливает гейт. Жёсткость (hard-fail) включается ТОЛЬКО kill-switch'ем
|
||||
`frontmatter_validation_strict` (дефолт `False`). never-raise.
|
||||
|
||||
### FR-3 — Полночтение frontmatter / единый reader-API (BR-1, BR-4)
|
||||
В `src/frontmatter.py` добавить чтение ВСЕГО frontmatter как mapping (а не только single-key),
|
||||
поверх которого строится единый доступ к вердикт-полям. Существующий
|
||||
`read_frontmatter_value(path, key)` сохраняется без изменения контракта (обратная
|
||||
совместимость вызывающих — `notifications.build_status_comment` и т.п.). never-raise.
|
||||
|
||||
### FR-4 — Единый контракт чтения вердиктов (BR-4, BR-5, NFR-1)
|
||||
Пять гейтов-вердиктов читают свои стандартные поля через единый frontmatter-API:
|
||||
|
||||
| Гейт / парсер | Документ | Стандартное поле | Семантика (НЕИЗМЕННА) |
|
||||
|---------------|----------|------------------|------------------------|
|
||||
| `check_reviewer_verdict` | `12-review.md` | `verdict:` | `APPROVED`→дальше; `REQUEST_CHANGES`→откат на development |
|
||||
| `_parse_tests_verdict` | `13-test-report.md` | `result:` / `verdict:` / `status:` (3 равноранговых, ORCH-047) | `PASS`→дальше; `FAIL`/`BLOCKED`→откат; негативный токен авторитетен |
|
||||
| `_parse_deploy_status` | `14-deploy-log.md` | `deploy_status:` | `SUCCESS`→done; `FAILED`→откат (БАГ-8) |
|
||||
| `_parse_staging_status` | `15-staging-log.md` | `staging_status:` | `SUCCESS`→дальше; `FAILED`→откат (self-hosting; иначе N/A) |
|
||||
| `parse_security_status` | `17-security-report.md` | `security_status:` | `PASS`→дальше; `FAIL`→откат |
|
||||
|
||||
Требование: **только механизм чтения** унифицируется (одна точка парсинга YAML-frontmatter);
|
||||
наборы токенов (`_TESTS_NEGATIVE_TOKENS`/`_TESTS_POSITIVE_TOKENS`), приведение к верхнему
|
||||
регистру, обработка «no frontmatter / bad YAML / missing key», fallback `worktree → origin/main`
|
||||
для deploy/staging — сохраняются 1:1. Возврат каждого `check_*` — прежний `tuple[bool, str]`.
|
||||
|
||||
### FR-5 — Обратная совместимость старых доков (NFR-1, критично)
|
||||
Документ-вердикт БЕЗ новых полей схемы (`work_item/stage/author_agent/status/created_at/
|
||||
model_used`), но с вердикт-ключом (`verdict:`/`result:`/`deploy_status:`/…) ДОЛЖЕН читаться
|
||||
гейтом ровно как сейчас. Новая схема — аддитивна; её отсутствие не влияет на чтение вердикта.
|
||||
|
||||
### FR-6 — Спека handoff (BR-3)
|
||||
Создать в `docs/_standards/` формальную спеку «стадия → обязательный выход»: для каждой стадии
|
||||
(`created`→`analysis`→`architecture`→`development`→`review`→`testing`→`deploy-staging`→`deploy`
|
||||
→`done`) перечислить обязательные документы и обязательные frontmatter-ключи на выходе.
|
||||
Согласовать с таблицей §2 `PIPELINE_DOCS.md` (тот же набор документов/ключей/гейтов), явно
|
||||
указать «источник истины — код». Различать machine-verdict доки и информационные (как в
|
||||
`PIPELINE_DOCS.md` §3).
|
||||
|
||||
## 4. Изменения API
|
||||
|
||||
Нет. HTTP-эндпоинты не добавляются/не меняются. (Опционально архитектор может предложить блок
|
||||
наблюдаемости в `GET /queue` для счётчика валидации — НЕ требование данной задачи.)
|
||||
|
||||
## 5. Изменения схемы БД
|
||||
|
||||
Нет. Таблицы/миграции/индексы не затрагиваются. Контракт работает на файлах
|
||||
(YAML-frontmatter) и in-memory.
|
||||
|
||||
## 6. Требования к новым/изменённым QG checks
|
||||
|
||||
- **Состав `QG_CHECKS` НЕ изменяется** (никаких новых/удалённых зарегистрированных гейтов) —
|
||||
AC-6 / правило CLAUDE.md.
|
||||
- Изменяется только **внутренняя реализация чтения вердикта** существующих `check_*`/`_parse_*`
|
||||
(делегирование единому frontmatter-API). Сигнатуры и возвращаемые значения (`tuple[bool,str]`)
|
||||
— неизменны.
|
||||
- Новый kill-switch `frontmatter_validation_strict` (config) управляет жёсткостью валидатора
|
||||
схемы; дефолт `False` (warning-only) → нулевая поведенческая регрессия.
|
||||
|
||||
## 7. Совместимость / регресс
|
||||
|
||||
- **Обратная совместимость (NFR-1):** старые доки-вердикты без новой схемы читаются как
|
||||
раньше; контракт `read_frontmatter_value` неизменен; формат writer'а совместим с
|
||||
существующими парсерами.
|
||||
- **never-raise (NFR-2):** writer/валидатор/единый reader не выбрасывают исключений в
|
||||
конвейер (паттерн текущего `frontmatter.py`).
|
||||
- **kill-switch / обратимость (NFR-3, NFR-5):** `frontmatter_validation_strict=False` (дефолт)
|
||||
→ валидация только логирует; `True` → строгий режим (на будущее). Поведение деградирует к
|
||||
прежнему при дефолтном флаге.
|
||||
- **Неизменность контрактов (AC-6):** `STAGE_TRANSITIONS`, состав `QG_CHECKS`, семантика
|
||||
вердиктов, fallback `worktree→origin/main`, трёх-полевой контракт tester (ORCH-047),
|
||||
токен-логика BLOCKED/FAILED — без изменений.
|
||||
- **Нулевая регрессия enduro (NFR-4):** для не-self-hosting репо поведение 1:1; условные гейты
|
||||
(ORCH-35/43/58) не затрагиваются по существу.
|
||||
- **Полный регресс `tests/` зелёный** перед мержем.
|
||||
- **self-hosting:** не перезапускать прод-контейнер вручную; деплой через штатный путь;
|
||||
первый боевой `autoDeploy` (наблюдение — за стадией deploy).
|
||||
104
docs/work-items/ORCH-076/03-acceptance-criteria.md
Normal file
104
docs/work-items/ORCH-076/03-acceptance-criteria.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# 03 — Критерии приёмки (Acceptance Criteria): ORCH-076 — ORCH-52c: протокол handoff + frontmatter-контракт
|
||||
|
||||
Work Item: **ORCH-076** · Repo: **orchestrator** · Стадия: analysis
|
||||
|
||||
Формат: каждый критерий имеет **PASS** (что должно быть истинно для приёмки) и **FAIL**
|
||||
(что считается провалом). Любой машинный/ручной reviewer проверяет их буквально по файлам
|
||||
репозитория. Критерии прямо отражают AC из постановки задачи (AC-1…AC-7).
|
||||
|
||||
---
|
||||
|
||||
## AC-1 — frontmatter: reader + writer + валидатор
|
||||
|
||||
**Условие:** `src/frontmatter.py` несёт полный контракт.
|
||||
- **PASS:** в `src/frontmatter.py` есть (а) сохранённый reader `read_frontmatter_value` с
|
||||
прежним контрактом; (б) **writer** (запись/рендер YAML-frontmatter); (в) **валидатор**
|
||||
обязательной схемы, проверяющий поля `work_item`, `stage`, `author_agent`, `status`,
|
||||
`created_at`, `model_used`. Все три покрыты unit-тестами.
|
||||
- **FAIL:** отсутствует writer ИЛИ валидатор; или валидатор не проверяет полный список из
|
||||
6 обязательных полей; или контракт reader сломан (изменена сигнатура/поведение).
|
||||
|
||||
---
|
||||
|
||||
## AC-2 — спека handoff создана и согласована
|
||||
|
||||
**Условие:** формальный контракт handoff в `docs/_standards/`.
|
||||
- **PASS:** в `docs/_standards/` создан документ-спека, где для КАЖДОЙ стадии указано, какие
|
||||
документы и какие frontmatter-ключи она обязана оставить на выходе; набор документов/ключей/
|
||||
гейтов согласован с `PIPELINE_DOCS.md` §2–§3 (нет противоречий); `PIPELINE_DOCS.md`
|
||||
обновлён ссылкой на спеку и отметкой о реализации машинного контракта в ORCH-52c.
|
||||
- **FAIL:** спека отсутствует, не в `docs/_standards/`, покрывает не все стадии, или
|
||||
противоречит `PIPELINE_DOCS.md` (другой набор ключей/документов).
|
||||
|
||||
---
|
||||
|
||||
## AC-3 — единый контракт вердиктов
|
||||
|
||||
**Условие:** гейты читают вердикты через единый frontmatter-API.
|
||||
- **PASS:** контракт вердиктов сведён в ОДНО место (единый frontmatter-API); все пять
|
||||
вердикт-точек — `check_reviewer_verdict` (`verdict:`), `_parse_tests_verdict`
|
||||
(`result:`/`verdict:`/`status:`), `_parse_deploy_status` (`deploy_status:`),
|
||||
`_parse_staging_status` (`staging_status:`), `parse_security_status` (`security_status:`) —
|
||||
парсят YAML-frontmatter через этот API, а не дублированной ad-hoc логикой.
|
||||
- **FAIL:** хотя бы один из пяти гейтов по-прежнему содержит собственную дублированную
|
||||
реализацию парсинга YAML-frontmatter вместо единого API.
|
||||
|
||||
---
|
||||
|
||||
## AC-4 — анти-регресс: старые доки читаются, ORCH-52c проходит свои гейты (критично self-hosting)
|
||||
|
||||
**Условие:** обратная совместимость + самопрохождение.
|
||||
- **PASS:** документ-вердикт БЕЗ новой полной схемы (только с вердикт-ключом) читается гейтом
|
||||
ровно как до задачи (подтверждено тестом для каждого из пяти гейтов); полный регресс
|
||||
`tests/` зелёный; **сама ORCH-52c проходит свои гейты** (review→testing→staging→deploy)
|
||||
и доезжает до `done`.
|
||||
- **FAIL:** любой старый док-вердикт перестал читаться/изменил вердикт; регресс `tests/`
|
||||
красный; задача застряла/откатилась на собственном гейте из-за нового контракта.
|
||||
|
||||
---
|
||||
|
||||
## AC-5 — never-raise + валидатор не hard-fail по умолчанию (kill-switch)
|
||||
|
||||
**Условие:** fail-safe и не-самоблокирующая валидация.
|
||||
- **PASS:** ошибка writer'а/валидатора логируется и НЕ роняет конвейер (исключение наружу не
|
||||
выходит, подтверждено тестом на битом вводе); валидация обязательной схемы по умолчанию —
|
||||
warning/лог, НЕ blocker; hard-fail доступен ТОЛЬКО под kill-switch
|
||||
(`frontmatter_validation_strict`, дефолт `False`).
|
||||
- **FAIL:** ошибка writer/валидатора пробрасывается в конвейер; ИЛИ отсутствие полей схемы
|
||||
по умолчанию заваливает гейт/останавливает задачу; ИЛИ нет kill-switch для строгого режима.
|
||||
|
||||
---
|
||||
|
||||
## AC-6 — STAGE_TRANSITIONS и состав QG_CHECKS не изменены; семантика неизменна
|
||||
|
||||
**Условие:** инварианты конвейера.
|
||||
- **PASS:** `src/stages.py::STAGE_TRANSITIONS` и реестр `QG_CHECKS` (`src/qg/checks.py`) —
|
||||
без изменений по составу (те же стадии, те же зарегистрированные гейты); семантика каждого
|
||||
вердикта (значение → переход/откат) идентична прежней, включая ORCH-047 (3 равноранговых
|
||||
поля tester) и приоритет негативного токена.
|
||||
- **FAIL:** изменён состав `STAGE_TRANSITIONS`/`QG_CHECKS`; или хоть один вердикт даёт другой
|
||||
переход при том же значении, что до задачи.
|
||||
|
||||
---
|
||||
|
||||
## AC-7 — документация обновлена
|
||||
|
||||
**Условие:** golden-source документации синхронна с кодом.
|
||||
- **PASS:** обновлены `CLAUDE.md`, `docs/architecture/README.md`, `CHANGELOG.md`; заведён
|
||||
ADR per-work-item (`docs/work-items/ORCH-076/06-adr/ADR-001-*.md`) и сквозной
|
||||
(`docs/architecture/adr/adr-NNNN-*.md`); спека handoff и `PIPELINE_DOCS.md` согласованы.
|
||||
- **FAIL:** функционал изменён, но доки/ADR/CHANGELOG не обновлены (reviewer →
|
||||
REQUEST_CHANGES по правилу CLAUDE.md №6).
|
||||
|
||||
---
|
||||
|
||||
## Сводная матрица AC ↔ FR/BR
|
||||
| AC | Покрывает |
|
||||
|----|-----------|
|
||||
| AC-1 | BR-1 / BR-2 / FR-1 / FR-2 / FR-3 |
|
||||
| AC-2 | BR-3 / FR-6 |
|
||||
| AC-3 | BR-4 / FR-4 |
|
||||
| AC-4 | NFR-1 / NFR-4 / FR-5 |
|
||||
| AC-5 | NFR-2 / NFR-3 / NFR-5 / FR-2 |
|
||||
| AC-6 | BR-5 / NFR-1 |
|
||||
| AC-7 | правило CLAUDE.md №2/№6 |
|
||||
122
docs/work-items/ORCH-076/04-test-plan.yaml
Normal file
122
docs/work-items/ORCH-076/04-test-plan.yaml
Normal file
@@ -0,0 +1,122 @@
|
||||
work_item: ORCH-076
|
||||
title: "ORCH-52c — handoff-протокол + frontmatter writer/валидатор/единый контракт вердиктов"
|
||||
framework: pytest
|
||||
scope: >
|
||||
Покрывается: writer/валидатор/единое чтение frontmatter (src/frontmatter.py);
|
||||
чтение пяти гейтов-вердиктов через единый API при семантике 1:1; обратная
|
||||
совместимость старых доков без новой схемы; never-raise; kill-switch строгой
|
||||
валидации. Вне покрытия: правка промптов агентов (ORCH-52d), ретро-фит старых
|
||||
документов, изменение STAGE_TRANSITIONS/состава QG_CHECKS.
|
||||
notes: >
|
||||
Полный регресс tests/ должен оставаться зелёным (анти-регресс гейтов, AC-4/AC-6).
|
||||
Регресс = любой существующий тест гейтов (review/tester/deploy/staging/security),
|
||||
ставший красным, или изменение вердикта при том же входном значении.
|
||||
Тесты не должны требовать сети (frontmatter — файловый/in-memory контракт).
|
||||
|
||||
tests:
|
||||
# --- frontmatter.py: writer / валидатор / reader (AC-1, AC-5) ---
|
||||
- id: TC-01
|
||||
type: unit
|
||||
description: "Writer сериализует mapping в каноничный ведущий YAML-frontmatter (--- ... ---), читаемый существующими парсерами"
|
||||
module: tests/test_frontmatter.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-02
|
||||
type: unit
|
||||
description: "Round-trip: writer записал frontmatter -> reader read_frontmatter_value возвращает те же значения по ключам"
|
||||
module: tests/test_frontmatter.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-03
|
||||
type: unit
|
||||
description: "Валидатор: полная схема (work_item/stage/author_agent/status/created_at/model_used) -> valid=True, нет отсутствующих полей"
|
||||
module: tests/test_frontmatter.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-04
|
||||
type: unit
|
||||
description: "Валидатор: отсутствие части обязательных полей -> valid=False со списком отсутствующих, но БЕЗ исключения (warning-only по умолчанию)"
|
||||
module: tests/test_frontmatter.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-05
|
||||
type: unit
|
||||
description: "never-raise: writer и валидатор на битом вводе (None/не-mapping/нечитаемый путь/битый YAML) не выбрасывают исключение, возвращают безопасное значение + лог"
|
||||
module: tests/test_frontmatter.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-06
|
||||
type: unit
|
||||
description: "reader read_frontmatter_value сохраняет прежний контракт (single-key, None на ошибку/отсутствие, strip, регистр сохранён)"
|
||||
module: tests/test_frontmatter.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-07
|
||||
type: unit
|
||||
description: "kill-switch frontmatter_validation_strict: False -> отсутствие полей не блокирует; True -> строгий режим сигнализирует невалидность"
|
||||
module: tests/test_frontmatter.py
|
||||
expected: PASS
|
||||
|
||||
# --- единый контракт вердиктов: чтение через общий API, семантика 1:1 (AC-3, AC-6) ---
|
||||
- id: TC-08
|
||||
type: unit
|
||||
description: "check_reviewer_verdict через единый API: verdict: APPROVED -> (True); REQUEST_CHANGES -> (False); отсутствие -> (False) — как до задачи"
|
||||
module: tests/test_qg_verdicts.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-09
|
||||
type: unit
|
||||
description: "_parse_tests_verdict через единый API: ORCH-047 три равноранговых поля (result/verdict/status), приоритет негативного токена (BLOCKED/FAILED) сохранён"
|
||||
module: tests/test_qg_verdicts.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-10
|
||||
type: unit
|
||||
description: "_parse_deploy_status через единый API: deploy_status SUCCESS -> (True); FAILED -> (False); missing/bad YAML -> (False) — семантика БАГ-8 неизменна"
|
||||
module: tests/test_qg_verdicts.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-11
|
||||
type: unit
|
||||
description: "_parse_staging_status через единый API: SUCCESS/FAILED семантика и условность ORCH-35 (non-self -> N/A pass) сохранены"
|
||||
module: tests/test_qg_verdicts.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-12
|
||||
type: unit
|
||||
description: "parse_security_status через единый API: security_status PASS -> (True); FAIL -> (False) — семантика неизменна"
|
||||
module: tests/test_security_gate.py
|
||||
expected: PASS
|
||||
|
||||
# --- обратная совместимость / анти-регресс (AC-4) ---
|
||||
- id: TC-13
|
||||
type: unit
|
||||
description: "Старый док-вердикт БЕЗ новой схемы (только verdict/result/deploy_status/staging_status/security_status) читается каждым из пяти гейтов как до задачи"
|
||||
module: tests/test_qg_verdicts.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-14
|
||||
type: unit
|
||||
description: "Док С новой полной схемой + вердикт-ключом читается гейтом с тем же вердиктом, что и без схемы (схема аддитивна, не влияет на вердикт)"
|
||||
module: tests/test_qg_verdicts.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-15
|
||||
type: integration
|
||||
description: "fallback worktree -> origin/main для check_deploy_status/check_staging_status сохранён при чтении через единый API"
|
||||
module: tests/test_qg_verdicts.py
|
||||
expected: PASS
|
||||
|
||||
# --- инварианты конвейера (AC-6) ---
|
||||
- id: TC-16
|
||||
type: unit
|
||||
description: "Состав QG_CHECKS и STAGE_TRANSITIONS не изменён (тот же набор ключей/стадий, что эталон)"
|
||||
module: tests/test_stages_invariants.py
|
||||
expected: PASS
|
||||
|
||||
# --- полный регресс ---
|
||||
- id: TC-17
|
||||
type: integration
|
||||
description: "Полный прогон tests/ зелёный (нет регресса существующих тестов гейтов и конвейера)"
|
||||
module: tests/
|
||||
expected: PASS
|
||||
248
docs/work-items/ORCH-076/06-adr/ADR-001-frontmatter-contract.md
Normal file
248
docs/work-items/ORCH-076/06-adr/ADR-001-frontmatter-contract.md
Normal file
@@ -0,0 +1,248 @@
|
||||
# ADR-001: Единый frontmatter-контракт (reader+writer+валидатор) и унификация чтения вердиктов
|
||||
|
||||
Work Item: **ORCH-076** (ORCH-52c, слой 2 эпика ORCH-52) · Repo: **orchestrator** · Стадия: architecture
|
||||
Дата: 2026-06-09 · Статус: **Accepted**
|
||||
|
||||
> Сквозная версия — [`docs/architecture/adr/adr-0020-frontmatter-contract.md`](../../../architecture/adr/adr-0020-frontmatter-contract.md).
|
||||
|
||||
---
|
||||
|
||||
## Статус
|
||||
Accepted
|
||||
|
||||
## Контекст
|
||||
|
||||
(Подробно — `01-brd.md` §1, `02-trz.md`.) Слой 1 эпика (ORCH-075/52b) дал **описательный**
|
||||
стандарт `docs/_standards/PIPELINE_DOCS.md`. ORCH-52c — **машинный** слой. Установлено в коде
|
||||
на ветке задачи:
|
||||
|
||||
- `src/frontmatter.py` = **только reader** (`read_frontmatter_value(path, key) -> str | None`,
|
||||
never-raise → `None`). В docstring прямой коммент: *«merging into a single parser is a
|
||||
follow-up task»* — это и есть данная задача.
|
||||
- **Парсинг YAML-frontmatter дублируется** в 5+ местах (~10 строк
|
||||
`content.startswith("---")` → `split("---", 2)` → `yaml.safe_load` → `isinstance(dict)`):
|
||||
`qg/checks.py::check_reviewer_verdict`, `_parse_tests_verdict`, `_parse_deploy_status`,
|
||||
`_parse_staging_status`; `security_gate.py::parse_security_status`; плюс `_strip_frontmatter`
|
||||
в `review_parse.py` и `security_gate.extract_security_findings`. Каждый — своя обработка
|
||||
ошибок и свои reason-строки → риск рассинхрона.
|
||||
- **Нет машинно-проверяемой схемы** обязательного frontmatter и **нет формальной спеки
|
||||
handoff** «что каждая стадия обязана оставить на выходе».
|
||||
|
||||
**⚠️ Self-hosting (главное ограничение проектирования).** Затрагиваемый код читает вердикты
|
||||
**на гейтах** в инструменте, который прямо сейчас обслуживает прод (enduro-trails) из общего
|
||||
инстанса с общей БД/очередью. Любой регресс чтения вердикта = остановка конвейера ВСЕХ
|
||||
проектов. Рефакторинг обязан быть **строго обратно совместимым, never-raise, нулевая
|
||||
регрессия**. Плюс на задаче выставлен лейбл `autoDeploy` (ORCH-089) — это **первый боевой
|
||||
автодеплой** орка (детали риска — `10-tech-risks.md`).
|
||||
|
||||
## Движущие силы (требования)
|
||||
|
||||
BR-1…BR-5, NFR-1…NFR-5 (`01-brd.md`), FR-1…FR-6 (`02-trz.md`), AC-1…AC-7
|
||||
(`03-acceptance-criteria.md`). Ключевые инварианты-ограничители:
|
||||
|
||||
- **INV-1** `STAGE_TRANSITIONS` и **состав** `QG_CHECKS` — не меняются (AC-6).
|
||||
- **INV-2** Семантика каждого вердикта (значение → переход/откат) — 1:1, включая 3-полевой
|
||||
контракт tester'а (ORCH-047) и приоритет негативного токена (AC-6, FR-4).
|
||||
- **INV-3** Контракт `read_frontmatter_value` — неизменен (внешние вызыватели: `usage.py`,
|
||||
`notifications.build_status_comment`) (FR-3).
|
||||
- **INV-4** Валидатор схемы **не hard-fail по умолчанию** — иначе ORCH-52c заблокировала бы
|
||||
собственный деплой (её доки и доки соседей ещё без полной схемы) (NFR-3).
|
||||
- **INV-5** Никаких изменений API и схемы БД (TRZ §4–§5).
|
||||
|
||||
---
|
||||
|
||||
## Решение
|
||||
|
||||
### D1. `src/frontmatter.py` становится единым frontmatter-контрактом (1 модуль, функции)
|
||||
|
||||
Выбран **набор функций в существующем leaf-модуле** (не класс, не новый пакет): модуль уже
|
||||
есть, не зависит ни от чего проектного (только `logging` + ленивый `yaml`), импортируем без
|
||||
циклов из `qg/checks.py`, `security_gate.py`, `post_deploy.py`, `review_parse.py`. Класс/состояние
|
||||
не нужны — операции чистые. Это минимизирует blast radius (требование self-hosting).
|
||||
|
||||
**Публичный API (имена канонические; точные дефолты — в реализации, контракт фиксирован здесь):**
|
||||
|
||||
```python
|
||||
# --- константы схемы ---
|
||||
REQUIRED_FIELDS = ("work_item", "stage", "author_agent", "status", "created_at", "model_used")
|
||||
|
||||
# --- reader: СОХРАНЁН без изменения контракта (INV-3) ---
|
||||
def read_frontmatter_value(path: str, key: str) -> str | None: ...
|
||||
|
||||
# --- единый парс-примитив (единственная точка YAML-логики) ---
|
||||
@dataclass(frozen=True)
|
||||
class FrontmatterParse:
|
||||
data: dict # {} если нет/битый/не-mapping
|
||||
has_block: bool # присутствовал ведущий ---…--- блок
|
||||
malformed: bool # был "---", но < 3 сегментов (незакрытый блок)
|
||||
yaml_error: str | None # текст ошибки yaml.safe_load, иначе None
|
||||
|
||||
def parse_frontmatter(content: str) -> FrontmatterParse: ... # never-raise
|
||||
def parse_frontmatter_dict(content: str) -> dict: ... # ярлык → .data; never-raise → {}
|
||||
def read_frontmatter(path: str) -> dict: ... # файл → parse; never-raise → {}
|
||||
|
||||
# --- writer ---
|
||||
def render_frontmatter(data: Mapping[str, object], body: str = "") -> str: ...
|
||||
# → "---\n<yaml>\n---\n<body>"; формат совместим со split("---",2)+safe_load; never-raise → body
|
||||
def write_frontmatter(path: str, data: Mapping, body: str = "") -> bool: ...
|
||||
# персист render_frontmatter; never-raise → False (ошибка логируется)
|
||||
|
||||
# --- валидатор схемы ---
|
||||
@dataclass(frozen=True)
|
||||
class SchemaValidation:
|
||||
valid: bool
|
||||
missing: list[str] # отсутствующие/пустые обязательные поля
|
||||
def validate_schema(data: Mapping, *, required=REQUIRED_FIELDS) -> SchemaValidation: ... # never-raise
|
||||
|
||||
# --- общий хелпер тела (заменяет дубли _strip_frontmatter) ---
|
||||
def strip_frontmatter(content: str) -> str: ... # never-raise → content
|
||||
```
|
||||
|
||||
**Контракт всего модуля — never-raise** (NFR-2), как у действующего reader: любая ошибка
|
||||
(I/O, YAML, сериализация) → `logger.debug/warning` + безопасное значение (`{}` / `False` /
|
||||
исходный текст), исключение наружу **не выходит**.
|
||||
|
||||
`parse_frontmatter` возвращает **структуру** (а не голый dict), чтобы каждый гейт мог
|
||||
**воспроизвести свои текущие reason-строки 1:1** (см. D2) — это и есть способ сохранить
|
||||
семантику без переписывания сообщений (INV-2).
|
||||
|
||||
### D2. Унифицируется МЕХАНИЗМ парсинга, а НЕ семантика вердиктов
|
||||
|
||||
AC-3/FR-4 требуют «читать через единый frontmatter-API, а не дублированной ad-hoc логикой».
|
||||
Унифицируется **ровно повторяющийся блок** `startswith/split/safe_load/isinstance` →
|
||||
замена на `parse_frontmatter(content)`. **Token-логика, upper-casing, набор полей, приоритет
|
||||
негативного токена, fallback `worktree → origin/main` — остаются в каждом гейте без изменений.**
|
||||
Это сознательное ограничение объёма унификации: общий «умный» verdict-резолвер увеличил бы
|
||||
риск тонкого регресса на гейтах (недопустимо при self-hosting). Каждый `check_*`/`_parse_*`
|
||||
сохраняет сигнатуру и `tuple[bool, str]`.
|
||||
|
||||
Маппинг состояний `FrontmatterParse` → существующие reason-строки (пример для tester'а,
|
||||
остальные аналогично):
|
||||
|
||||
| Состояние | Прежняя ветка | Сохраняемая reason-строка |
|
||||
|-----------|---------------|---------------------------|
|
||||
| `not has_block` | `not content.startswith("---")` | "No YAML frontmatter in test report …" |
|
||||
| `malformed` | `len(parts) < 3` | "Malformed YAML frontmatter in test report" |
|
||||
| `yaml_error` | `except yaml.YAMLError` | "Invalid YAML frontmatter in test report: {e}" |
|
||||
| `data` (dict) | `fm.get(...)` | прежняя token-логика поверх `parse.data` |
|
||||
|
||||
Точки перевода (FR-4):
|
||||
|
||||
| Парсер | Файл | Поле(я) | Семантика — НЕ менять |
|
||||
|--------|------|---------|----------------------|
|
||||
| `check_reviewer_verdict` | `12-review.md` | `verdict:` | APPROVED→дальше; REQUEST_CHANGES→откат |
|
||||
| `_parse_tests_verdict` | `13-test-report.md` | `result:`/`verdict:`/`status:` (3 равноранг., ORCH-047) | PASS→дальше; FAIL/BLOCKED→откат; негативный токен авторитетен |
|
||||
| `_parse_deploy_status` | `14-deploy-log.md` | `deploy_status:` | SUCCESS→done; FAILED→откат (БАГ-8) |
|
||||
| `_parse_staging_status` | `15-staging-log.md` | `staging_status:` | SUCCESS→дальше; FAILED→откат (self-hosting) |
|
||||
| `parse_security_status` | `17-security-report.md` | `security_status:` | PASS→дальше; FAIL→откат (FAIL авторитетен) |
|
||||
|
||||
`post_deploy.py` (`post_deploy_status:`, информационный) и `review_parse._strip_frontmatter`/
|
||||
`security_gate.extract_security_findings` (извлечение прозы) переводятся на
|
||||
`parse_frontmatter_dict` / `strip_frontmatter` соответственно — снимает оставшиеся дубли без
|
||||
изменения их «never-raise → пусто» контрактов.
|
||||
|
||||
### D3. Валидатор: библиотека + warning-only, hard-fail строго под kill-switch
|
||||
|
||||
`validate_schema` — **чистая библиотечная функция** (INV-4, NFR-3). Чтобы гарантировать
|
||||
**нулевую регрессию гейтов**, в default-режиме валидатор **не участвует в вычислении
|
||||
boolean-вердикта** ни одного гейта. Вместо этого:
|
||||
|
||||
- Новый флаг `config.frontmatter_validation_strict: bool = False`
|
||||
(env `ORCH_FRONTMATTER_VALIDATION_STRICT`).
|
||||
- **Default (`False`):** опциональный warning-emit — при чтении machine-verdict дока, не
|
||||
несущего полной схемы, единый хелпер `maybe_warn_schema(content, doc_label)` пишет
|
||||
`logger.warning("frontmatter schema incomplete: missing …")` и **возвращает управление без
|
||||
влияния на вердикт** (чистый no-op для `tuple[bool,str]`). Это удовлетворяет «по умолчанию
|
||||
warning/лог» (FR-2), оставаясь поведенчески инертным.
|
||||
- **Strict (`True`):** зарезервированный режим будущего ужесточения (ORCH-52d+). Когда
|
||||
включён, тот же хелпер может вернуть гейту вето. На ORCH-52c флаг **остаётся `False`** в
|
||||
проде и в `.env.staging` — иначе задача self-block'нется (её доки без полной схемы). Strict
|
||||
покрывается unit-тестом, но не включается.
|
||||
|
||||
Решение «валидатор вне вердикт-пути по умолчанию» — осознанный выбор в пользу безопасности
|
||||
self-hosting: машинная проверка схемы **существует и тестируется**, но **физически не может**
|
||||
завалить гейт при дефолте.
|
||||
|
||||
### D4. Формальная спека handoff — `docs/_standards/HANDOFF_PROTOCOL.md`
|
||||
|
||||
Создаётся (на стадии development, как doc-deliverable) рядом с `PIPELINE_DOCS.md`. Структура
|
||||
(нормативно для разработчика):
|
||||
|
||||
1. **Назначение + статус истины** — «источник истины поведения = код (`stages.py`,
|
||||
`qg/checks.py`, `stage_engine.py`); спека документирует» (правило ORCH-075).
|
||||
2. **Обязательная frontmatter-схема** — таблица 6 полей (`work_item`, `stage`, `author_agent`,
|
||||
`status`, `created_at`, `model_used`) + смысл каждого; ссылка на `frontmatter.REQUIRED_FIELDS`
|
||||
как на машинный источник.
|
||||
3. **Контракт handoff по стадиям** — для каждой стадии (`created`→…→`done`): какие документы
|
||||
**обязан** оставить выход стадии и какие frontmatter-ключи (machine-verdict ключ + будущая
|
||||
общая схема). **Согласовано 1:1 с `PIPELINE_DOCS.md` §2–§3** (тот же набор
|
||||
документов/ключей/гейтов; различие machine-verdict vs информационные сохранено).
|
||||
4. **Перекрёстная ссылка** на единый API `src/frontmatter.py` и на флаг
|
||||
`frontmatter_validation_strict`.
|
||||
|
||||
`PIPELINE_DOCS.md` обновляется: блок «слой 1 описательный → ORCH-52c реализовала машинный
|
||||
контракт» + ссылка на `HANDOFF_PROTOCOL.md` и на `src/frontmatter.py` (закрывает явную метку
|
||||
«машинная проверка — отдельная задача ORCH-52c» в §5).
|
||||
|
||||
### D5. Без изменений API/БД/состава гейтов
|
||||
|
||||
Подтверждено INV-1/INV-5: HTTP-эндпоинты, `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, схема БД —
|
||||
не трогаются. Опциональный счётчик валидации в `GET /queue` — **не вводим** (TRZ §4: не
|
||||
требование; добавил бы поверхность без нужды).
|
||||
|
||||
---
|
||||
|
||||
## Альтернативы (отклонены)
|
||||
|
||||
- **A1. Общий «умный» verdict-резолвер** (одна функция читает поле+токены для всех 5 гейтов).
|
||||
Отклонено: token-наборы и правила различаются (особенно ORCH-047 3-поля + приоритет
|
||||
негатива); единая абстракция повысила бы риск тонкого регресса на гейте → недопустимо при
|
||||
self-hosting. Унифицируем только парс YAML (D2).
|
||||
- **A2. Класс `Frontmatter`/новый пакет.** Отклонено: состояния нет, операции чистые; класс —
|
||||
лишняя церемония и больший blast radius. Функции в существующем leaf-модуле проще и
|
||||
безопаснее.
|
||||
- **A3. Валидатор как hard-fail на гейте по умолчанию.** Отклонено прямо BRD/NFR-3: заблокирует
|
||||
собственный деплой ORCH-52c. Default — warning-only, hard-fail под флагом (D3).
|
||||
- **A4. Сторонняя библиотека `python-frontmatter`.** Отклонено: новая зависимость ради ~30
|
||||
строк; `pyyaml` уже в проекте, формат тривиален, контроль над never-raise важнее.
|
||||
- **A5. Ретро-фит схемы в существующие доки / правка промптов агентов.** Вне scope (это
|
||||
ORCH-52d, слой 3). Схема аддитивна и forward-looking.
|
||||
|
||||
---
|
||||
|
||||
## Последствия
|
||||
|
||||
**Плюсы**
|
||||
- Единственная точка YAML-парсинга → конец рассинхрона обработки ошибок между гейтами.
|
||||
- Writer + валидатор + полная схема готовы к ORCH-52d (агенты начнут эмитить схему).
|
||||
- Спека handoff закрывает пробел «что стадия обязана оставить», согласована с манифестом.
|
||||
- Нулевая поведенческая регрессия по построению: семантика и reason-строки 1:1, валидатор вне
|
||||
вердикт-пути при дефолте, never-raise сохранён.
|
||||
|
||||
**Минусы / ограничения**
|
||||
- Унификация частичная (только парс, не семантика) — token-логика всё ещё живёт в каждом
|
||||
гейте. Это сознательный компромисс безопасности; полная унификация семантики — возможная
|
||||
будущая задача с отдельным риск-бюджетом.
|
||||
- Strict-режим валидатора пока «спящий» (тестируется, но не включён) — реальная польза от
|
||||
enforcement появится только с ORCH-52d.
|
||||
- Reason-строки нужно перенести **дословно** — за этим следит reviewer и анти-регресс-тесты.
|
||||
|
||||
**Обратимость**
|
||||
- `frontmatter_validation_strict=False` (дефолт) ⇒ поведение эквивалентно прежнему.
|
||||
- Перевод гейтов на `parse_frontmatter` поведенчески инвариантен; откат — точечный возврат
|
||||
inline-блока (но не требуется при зелёном регрессе).
|
||||
|
||||
**Тестирование (обязательно перед мержем)**
|
||||
- `tests/test_frontmatter.py` (новый): reader (контракт неизменен), writer (round-trip
|
||||
`render → parse`), валидатор (полный/неполный набор, strict on/off), битый ввод → never-raise.
|
||||
- Анти-регресс на каждый из 5 гейтов: старый док-вердикт **без** новой схемы → тот же
|
||||
`tuple[bool,str]`, что до задачи (NFR-1/AC-4); negative-token-приоритет tester'а (ORCH-047).
|
||||
- Полный `pytest tests/ -q` зелёный.
|
||||
|
||||
## Связи
|
||||
- Реализует: BR-1…BR-5, FR-1…FR-6, AC-1…AC-7.
|
||||
- Опирается на: ORCH-075/52b (`PIPELINE_DOCS.md`, манифест), ORCH-016 (`frontmatter.py` reader),
|
||||
ORCH-047 (3-полевой tester-вердикт), ORCH-022 (security-гейт), ORCH-089 (`autoDeploy`).
|
||||
- Готовит почву: ORCH-52d (агенты эмитят полную схему; возможное включение strict).
|
||||
- Сквозной ADR: `docs/architecture/adr/adr-0020-frontmatter-contract.md`.
|
||||
- Риски/инфра/данные: `10-tech-risks.md`, `07-infra-requirements.md`, `08-data-requirements.md`.
|
||||
50
docs/work-items/ORCH-076/07-infra-requirements.md
Normal file
50
docs/work-items/ORCH-076/07-infra-requirements.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# 07 — Требования к инфраструктуре: ORCH-076 (ORCH-52c)
|
||||
|
||||
Work Item: **ORCH-076** · Repo: **orchestrator** · Стадия: architecture
|
||||
|
||||
## Сводка
|
||||
|
||||
ORCH-52c — чисто кодово-документная задача (frontmatter-контракт + спека handoff). **Топология
|
||||
инфраструктуры не меняется**: ни контейнеров, ни портов, ни volume, ни сети, ни CI-workflow.
|
||||
Деплой — штатным путём конвейера через staging (8501) → прод (8500). Раздел существует для
|
||||
фиксации двух операционных предусловий и одного конфиг-флага.
|
||||
|
||||
## Изменения инфраструктуры
|
||||
|
||||
- **Нет.** Compose-сервисы, порты (8500/8501), volume (`./data`, `./data/staging`), Gitea
|
||||
Actions — без изменений.
|
||||
- БД/миграции — нет (см. `08-data-requirements.md`).
|
||||
- HTTP API — нет новых/изменённых эндпоинтов.
|
||||
|
||||
## Конфигурация (env)
|
||||
|
||||
| Ключ | Значение по умолчанию | Где | Назначение |
|
||||
|------|----------------------|-----|------------|
|
||||
| `ORCH_FRONTMATTER_VALIDATION_STRICT` | `false` | `.env` / `.env.staging` | Kill-switch строгой валидации схемы frontmatter. **На ORCH-52c держать `false`** (иначе self-block: доки ещё без полной схемы). Включается не раньше ORCH-52d. |
|
||||
|
||||
> Флаг **аддитивный**; его отсутствие в окружении эквивалентно `false` (pydantic-дефолт
|
||||
> `frontmatter_validation_strict: bool = False`). Явная установка не требуется на этой задаче;
|
||||
> строка в `.env.example` добавляется документации ради.
|
||||
|
||||
## Операционные предусловия
|
||||
|
||||
### П-1. Лейбл `autoDeploy` (первый боевой автодеплой — ORCH-089)
|
||||
На задаче выставлен лейбл `autoDeploy`: после зелёного staging и всех тех-гейтов орк **сам**
|
||||
подтверждает прод-деплой (Фаза B ORCH-036/059), без ручного «Confirm Deploy».
|
||||
- Предусловие: лейбл `autoDeploy` существует в Plane-проекте ORCH и проставлен на ORCH-076
|
||||
(инфра-предусловие ORCH-089). Его отсутствие = fail-safe → ручной гейт (деплой не сорвётся,
|
||||
просто потребует ручного «Confirm Deploy»).
|
||||
- BRD-гейт остаётся **ручным** (Слава подтверждает BRD) — `autoApprove` НЕ выставлен.
|
||||
- Наблюдение: стадия `deploy` орка должна пройти через зелёные под-гейты ребра
|
||||
`deploy-staging → deploy` (security → merge-gate → image-freshness → staging) до Фазы B —
|
||||
`autoDeploy` физически не деплоит сломанное (BR-5 ORCH-089). Детали реакции на сбой —
|
||||
`10-tech-risks.md` (R-3).
|
||||
|
||||
### П-2. Self-hosting рестарт-дисциплина
|
||||
Прод-контейнер `orchestrator` (8500) — общий для всех проектов. Деплой ORCH-52c проходит через
|
||||
штатный detached host-хук (ORCH-036), **не** ручным `docker compose`. Ручной рестарт прод-
|
||||
контейнера в рамках задачи **запрещён** (встанет конвейер enduro). Откат — `orchestrator-deploy-hook.sh --rollback` (стандартный путь), не предмет этой задачи.
|
||||
|
||||
## Вне инфра-объёма
|
||||
- Изменения промптов агентов, ретро-фит схемы в старые доки — ORCH-52d.
|
||||
- Любые новые сервисы/демоны/cron — не вводятся.
|
||||
34
docs/work-items/ORCH-076/08-data-requirements.md
Normal file
34
docs/work-items/ORCH-076/08-data-requirements.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# 08 — Требования к данным / схеме БД: ORCH-076 (ORCH-52c)
|
||||
|
||||
Work Item: **ORCH-076** · Repo: **orchestrator** · Стадия: architecture
|
||||
|
||||
## Сводка
|
||||
|
||||
**Изменений схемы БД нет.** Контракт frontmatter работает исключительно на **файлах**
|
||||
(YAML-frontmatter номерных документов `docs/work-items/<id>/*.md`) и **in-memory** строках.
|
||||
SQLite (`src/db.py`) — таблицы, индексы, миграции — **не затрагиваются** (TRZ §5).
|
||||
|
||||
## Детали
|
||||
|
||||
| Аспект | Состояние |
|
||||
|--------|-----------|
|
||||
| Новые таблицы | нет |
|
||||
| Изменённые таблицы / колонки | нет |
|
||||
| Индексы | нет |
|
||||
| Миграции | нет (restart-safe без миграции) |
|
||||
| Persistent state | нет (writer пишет в файлы доков, не в БД) |
|
||||
|
||||
## Модель данных контракта (файлы, не БД)
|
||||
|
||||
- **Обязательная frontmatter-схема** (машинный источник — `frontmatter.REQUIRED_FIELDS`):
|
||||
`work_item`, `stage`, `author_agent`, `status`, `created_at`, `model_used`. Это контракт
|
||||
**документа**, не строки БД. Фактическое проставление полей агентами — ORCH-52d (вне scope).
|
||||
- **Вердикт-ключи** (читаются единым API, семантика 1:1): `verdict:` (12), `result:`/`verdict:`/
|
||||
`status:` (13, ORCH-047), `deploy_status:` (14), `staging_status:` (15), `security_status:`
|
||||
(17), `post_deploy_status:` (16, информационный). Формат — ведущий YAML-блок `---…---`.
|
||||
|
||||
## Совместимость данных
|
||||
- Старые документы-вердикты **без** новой схемы остаются валидными (схема аддитивна; её
|
||||
отсутствие не влияет на чтение вердикта — NFR-1).
|
||||
- Формат writer'а (`render_frontmatter`) совместим с существующим
|
||||
`split("---", 2)` + `yaml.safe_load` — старые и новые парсеры читают единообразно.
|
||||
25
docs/work-items/ORCH-076/10-tech-risks.md
Normal file
25
docs/work-items/ORCH-076/10-tech-risks.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# 10 — Технические риски: ORCH-076 (ORCH-52c)
|
||||
|
||||
Work Item: **ORCH-076** · Repo: **orchestrator** · Стадия: architecture
|
||||
|
||||
Информационный документ (гейтом не парсится). Источник истины по решениям — `06-adr/ADR-001`.
|
||||
|
||||
| ID | Риск | Вероятн. | Влияние | Митигация | Остаточно |
|
||||
|----|------|----------|---------|-----------|-----------|
|
||||
| **R-1** | **Регресс чтения вердикта на гейте** (review/testing/staging/deploy/security) при переводе на единый `parse_frontmatter` → ложный откат/застревание → **остановка конвейера ВСЕХ проектов** (главный self-hosting риск). | средняя | критическое | Унифицируется только парс YAML, НЕ семантика (ADR D2); сигнатуры/`tuple[bool,str]`/токены/upper-case/fallback `worktree→origin/main` 1:1; reason-строки переносятся дословно через `FrontmatterParse`-состояния; **анти-регресс-тест на каждый из 5 гейтов** (старый док → тот же вердикт) + полный `pytest tests/` зелёный до мержа (AC-4/AC-6). | низкое |
|
||||
| **R-2** | **Самоблокировка валидатором** на собственном деплое: доки ORCH-52c (и соседей) ещё без полной 6-польной схемы → strict-валидатор завалил бы гейт. | высокая (если включить strict) | высокое | `frontmatter_validation_strict` дефолт `False`; валидатор в default-режиме **вне вердикт-пути** гейтов (warning-only, чистый no-op для boolean); strict тестируется, но НЕ включается до ORCH-52d; `false` в `.env`/`.env.staging` (NFR-3, ADR D3). | очень низкое |
|
||||
| **R-3** | **Первый боевой `autoDeploy`** (ORCH-089): орк сам подтверждает прод-рестарт после staging → если регресс R-1 проскользнул мимо тестов, автодеплой выкатит его без человеческой паузы. | низкая | высокое | `autoDeploy` достигает Фазы B только после зелёных под-гейтов ребра `deploy-staging→deploy` (security→merge-gate→image-freshness→staging) — не деплоит сломанное (BR-5 ORCH-089); обязательная страховка staging (8501); пост-деплой мониторинг ORCH-021 (`ALERT_ONLY` для self) ловит «зелёный деплой, красный прод» с откатом durable-freeze (ORCH-088); ручной BRD-гейт сохранён. Наблюдать стадию `deploy` вживую (Telegram-карточка). | низкое |
|
||||
| **R-4** | **Дрейф reason-строк / сообщений гейтов** при рефакторе → тесты, ассертящие текст, краснеют; логи/Plane-комменты меняют формулировку. | средняя | низкое | Маппинг `FrontmatterParse → прежняя reason-строка` зафиксирован в ADR D2; переносить дословно; ассерты в анти-регресс-тестах фиксируют текущий текст. | низкое |
|
||||
| **R-5** | **Расхождение спеки handoff с фактом кода** → «лживый» стандарт. | средняя | среднее | `HANDOFF_PROTOCOL.md` согласован 1:1 с `PIPELINE_DOCS.md` §2–§3 (тот же набор документов/ключей/гейтов); явная пометка «источник истины — код» (правило ORCH-075); reviewer сверяет (CLAUDE.md №2/№6). | низкое |
|
||||
| **R-6** | **Скрытое исключение из writer/валидатора** прорывается в конвейер (нарушение never-raise) на битом вводе. | низкая | высокое | Контракт всего модуля never-raise (как действующий reader): любая ошибка → лог + безопасное значение; **тест на битом вводе** (невалидный YAML, не-mapping, I/O-ошибка) подтверждает отсутствие проброса (AC-5/NFR-2). | очень низкое |
|
||||
| **R-7** | **Циклический импорт** при использовании `frontmatter` из `qg/checks.py`/`security_gate.py`/`post_deploy.py`/`review_parse.py`. | низкая | среднее | `frontmatter.py` — leaf без проектных зависимостей (только `logging` + ленивый `yaml`); импортируется, не импортирует проектные модули — циклов нет. | очень низкое |
|
||||
| **R-8** | **Частичная унификация** (token-логика осталась в каждом гейте) воспринимается reviewer'ом как недовыполнение AC-3. | низкая | низкое | AC-3 требует «парсить YAML через единый API, а не дублированной логикой» — выполнено (D2); неунификация семантики — осознанный выбор безопасности, зафиксирован в ADR (альтернатива A1 отклонена). | очень низкое |
|
||||
|
||||
## Сводные митигации (обязательные перед мержем)
|
||||
1. Анти-регресс-тест на каждый из 5 вердикт-гейтов (старый док без схемы → прежний вердикт).
|
||||
2. `tests/test_frontmatter.py`: reader (контракт неизменен) / writer (round-trip) / валидатор
|
||||
(полный/неполный, strict on/off) / битый ввод → never-raise.
|
||||
3. Полный `pytest tests/ -q` зелёный.
|
||||
4. `frontmatter_validation_strict=False` в прод/staging env.
|
||||
5. Живое наблюдение стадии `deploy` (первый `autoDeploy`); готовность к
|
||||
`orchestrator-deploy-hook.sh --rollback` штатным путём (не ручной рестарт).
|
||||
93
docs/work-items/ORCH-076/12-review.md
Normal file
93
docs/work-items/ORCH-076/12-review.md
Normal file
@@ -0,0 +1,93 @@
|
||||
---
|
||||
type: review
|
||||
work_item_id: ORCH-076
|
||||
verdict: APPROVED
|
||||
version: 1
|
||||
---
|
||||
|
||||
# Review ORCH-076 — ORCH-52c: единый frontmatter-контракт + спека handoff
|
||||
|
||||
## Summary
|
||||
|
||||
Изменение реализует слой 2 эпика ORCH-52: `src/frontmatter.py` превращён из single-key
|
||||
reader'а в полный машинный контракт (reader + writer + валидатор схемы + единый парс-примитив),
|
||||
а дублированное чтение YAML-frontmatter в пяти вердикт-парсерах сведено к одной точке
|
||||
(`parse_frontmatter`). Дополнительно создана формальная спека handoff
|
||||
(`docs/_standards/HANDOFF_PROTOCOL.md`).
|
||||
|
||||
Реализация **полностью соответствует ТЗ и ADR-001/adr-0020**: унифицирован только МЕХАНИЗМ
|
||||
парсинга (D2), семантика вердиктов, token-логика, приоритет негативного токена, fallback
|
||||
`worktree→origin/main` и трёх-полевой контракт tester (ORCH-047) сохранены 1:1. Валидатор
|
||||
warning-only по умолчанию, hard-fail только под kill-switch `frontmatter_validation_strict`
|
||||
(дефолт `False`) — критично для self-hosting (задача не self-block'ится). Весь модуль
|
||||
never-raise. `STAGE_TRANSITIONS` и состав `QG_CHECKS` не тронуты (подтверждено TC-16).
|
||||
|
||||
Проверка по осям:
|
||||
- **Соответствие ТЗ:** FR-1…FR-6 реализованы (writer, валидатор, полночтение, единый контракт
|
||||
вердиктов, BC старых доков, спека handoff). API/БД не тронуты (TRZ §4–§5). ✓
|
||||
- **Соответствие AC:** AC-1…AC-7 выполнены (см. ниже). ✓
|
||||
- **Соответствие ADR:** D1–D5 реализованы как спроектировано (функции в leaf-модуле,
|
||||
unify-механизм-не-семантику, warning-only validator, спека handoff, без API/БД). ✓
|
||||
- **Качество кода:** docstrings на всех публичных функциях; never-raise контракт выдержан
|
||||
(broad-except + лог + безопасный возврат); leaf-модуль без проектных импортов (cycle-free).
|
||||
- **Тесты:** содержательные, покрывают writer/round-trip/валидатор/strict/never-raise/reader
|
||||
(`test_frontmatter.py`), семантику пяти гейтов + BC + origin/main fallback
|
||||
(`test_qg_verdicts.py`), security-гейт (`test_security_gate.py`), инварианты реестра
|
||||
(`test_stages_invariants.py`). Полный регресс **1212 passed**.
|
||||
|
||||
Проверка AC:
|
||||
- **AC-1** (reader+writer+валидатор, unit-tested): ✓ `read_frontmatter_value` (BC),
|
||||
`render/write_frontmatter`, `validate_schema` с 6 полями `REQUIRED_FIELDS`; TC-01…TC-07.
|
||||
- **AC-2** (спека handoff в `docs/_standards/`, согласована): ✓ покрывает все стадии
|
||||
`created`→`done`, набор документов/ключей/гейтов 1:1 с `PIPELINE_DOCS.md` §2–§3;
|
||||
`PIPELINE_DOCS.md` обновлён ссылкой + отметкой реализации (§5–§6).
|
||||
- **AC-3** (единый контракт вердиктов): ✓ все 5 (`check_reviewer_verdict`,
|
||||
`_parse_tests_verdict`, `_parse_deploy_status`, `_parse_staging_status`,
|
||||
`parse_security_status`) делегируют `parse_frontmatter`; ad-hoc блоки удалены.
|
||||
- **AC-4** (BC старых доков + регресс зелёный): ✓ TC-13/TC-14 для старых доков без схемы;
|
||||
1212 tests green.
|
||||
- **AC-5** (never-raise + warning-only + kill-switch): ✓ TC-05 (битый ввод), `maybe_warn_schema`
|
||||
инертен при дефолте, `frontmatter_validation_strict` в `config.py`.
|
||||
- **AC-6** (`STAGE_TRANSITIONS`/`QG_CHECKS` неизменны, семантика 1:1): ✓ TC-16; вердикт-логика
|
||||
не тронута.
|
||||
- **AC-7** (документация): ✓ см. раздел «Документация».
|
||||
|
||||
## Findings
|
||||
|
||||
### P0 — Blocker
|
||||
- Нет.
|
||||
|
||||
### P1 — Must fix
|
||||
- Нет.
|
||||
|
||||
### P2 — Should fix
|
||||
- Нет.
|
||||
|
||||
### P3 — Nice-to-have
|
||||
- [ ] `_parse_tests_verdict` (`src/qg/checks.py`): для редкого случая «валидный YAML, но не
|
||||
mapping» во frontmatter старая ветка возвращала reason `"Malformed YAML frontmatter in test
|
||||
report (not a mapping)"`, новая реализация маршрутизирует этот ввод в путь пустых данных →
|
||||
reason `"No machine-readable verdict/status/result in test report frontmatter"`.
|
||||
**Boolean-вердикт идентичен (`False` в обоих случаях) → семантика и STAGE_TRANSITIONS не
|
||||
затронуты (AC-6 соблюдён).** Расхождение только в reason-строке (лог/коммент). ADR D2 заявляет
|
||||
«reason-строки 1:1» — здесь незначительное отклонение в крайне редком кейсе. Можно при желании
|
||||
добавить явную ветку для паритета, но это не обязательно.
|
||||
- [ ] `parse_security_status` (`src/security_gate.py`) не вызывает `maybe_warn_schema`, тогда как
|
||||
4 из 5 вердикт-парсеров его вызывают. Поскольку warning инертен (не влияет на вердикт), это
|
||||
чисто косметическая несогласованность наблюдаемости. Для единообразия можно добавить вызов.
|
||||
|
||||
## Документация
|
||||
|
||||
Обновлено в том же PR (golden source синхронен с кодом, правило CLAUDE.md №2/№6):
|
||||
- `CLAUDE.md` — блок про единый frontmatter-контракт в «Конвенциях».
|
||||
- `docs/architecture/README.md` — «Канон гейтов» (единый контракт), компонент frontmatter,
|
||||
ссылки на спеку handoff и adr-0020.
|
||||
- `docs/_standards/HANDOFF_PROTOCOL.md` — **создан** (спека handoff, все стадии, обязательная
|
||||
схема `REQUIRED_FIELDS`).
|
||||
- `docs/_standards/PIPELINE_DOCS.md` — обновлён (слой 2 реализован, §5–§6 + ссылки).
|
||||
- `CHANGELOG.md` — детальная запись `[Unreleased]`.
|
||||
- ADR: per-work-item `docs/work-items/ORCH-076/06-adr/ADR-001-frontmatter-contract.md` +
|
||||
сквозной `docs/architecture/adr/adr-0020-frontmatter-contract.md`; индекс
|
||||
`docs/architecture/adr/README.md` обновлён (adr-0018/0019/0020, max=0020).
|
||||
|
||||
Документация полная и согласованная — претензий нет.
|
||||
85
docs/work-items/ORCH-076/13-test-report.md
Normal file
85
docs/work-items/ORCH-076/13-test-report.md
Normal file
@@ -0,0 +1,85 @@
|
||||
---
|
||||
type: test-report
|
||||
work_item_id: ORCH-076
|
||||
result: PASS
|
||||
---
|
||||
|
||||
# Test Report — ORCH-076
|
||||
|
||||
ORCH-52c: протокол handoff + единый frontmatter-контракт (writer/валидатор/схема).
|
||||
|
||||
## Окружение
|
||||
- Python: 3.12.13
|
||||
- pytest: 8.3.3
|
||||
- Ветка: `feature/ORCH-076-orch-52c-handoff-frontmatter-w`
|
||||
- Worktree: `/repos/_wt/orchestrator/feature_ORCH-076-orch-52c-handoff-frontmatter-w`
|
||||
- Дата: 2026-06-09
|
||||
|
||||
## Предусловия
|
||||
- Review-вердикт `12-review.md`: **APPROVED** (P0/P1/P2 — нет; два P3 nice-to-have, не блокирующие).
|
||||
- Prod health (8500): `{"status":"ok","service":"orchestrator"}` — конвейер прочих проектов не тронут.
|
||||
|
||||
## Smoke test API (prod 8500, read-only)
|
||||
| Endpoint | Результат |
|
||||
|----------|-----------|
|
||||
| `GET /health` | PASS — `{"status":"ok","service":"orchestrator"}` |
|
||||
| `GET /status` | PASS — задача ORCH-076 видна на стадии `testing` (id 69) |
|
||||
| `GET /queue` | PASS — counts/reconcile/reaper/post_deploy/merge_verify в норме (done=897, failed=4) |
|
||||
|
||||
## Результаты по тест-плану (04-test-plan.yaml)
|
||||
|
||||
| TC ID | Описание | Модуль | Результат |
|
||||
|-------|----------|--------|-----------|
|
||||
| TC-01 | Writer сериализует mapping в каноничный YAML-frontmatter | tests/test_frontmatter.py | PASS |
|
||||
| TC-02 | Round-trip writer → read_frontmatter_value | tests/test_frontmatter.py | PASS |
|
||||
| TC-03 | Валидатор: полная схема → valid=True | tests/test_frontmatter.py | PASS |
|
||||
| TC-04 | Валидатор: неполная схема → valid=False, без исключения | tests/test_frontmatter.py | PASS |
|
||||
| TC-05 | never-raise: writer/валидатор на битом вводе | tests/test_frontmatter.py | PASS |
|
||||
| TC-06 | reader read_frontmatter_value: прежний контракт (BC) | tests/test_frontmatter.py | PASS |
|
||||
| TC-07 | kill-switch frontmatter_validation_strict (False/True) | tests/test_frontmatter.py | PASS |
|
||||
| TC-08 | check_reviewer_verdict через единый API (APPROVED/REQUEST_CHANGES/missing) | tests/test_qg_verdicts.py | PASS |
|
||||
| TC-09 | _parse_tests_verdict: ORCH-047 3 поля + приоритет негативного токена | tests/test_qg_verdicts.py | PASS |
|
||||
| TC-10 | _parse_deploy_status: SUCCESS/FAILED/missing (БАГ-8 1:1) | tests/test_qg_verdicts.py | PASS |
|
||||
| TC-11 | _parse_staging_status: SUCCESS/FAILED + условность ORCH-35 | tests/test_qg_verdicts.py | PASS |
|
||||
| TC-12 | parse_security_status: PASS/FAIL семантика 1:1 | tests/test_security_gate.py | PASS |
|
||||
| TC-13 | Старый док-вердикт без новой схемы читается всеми 5 гейтами | tests/test_qg_verdicts.py | PASS |
|
||||
| TC-14 | Док с полной схемой + вердикт-ключом — тот же вердикт (схема аддитивна) | tests/test_qg_verdicts.py | PASS |
|
||||
| TC-15 | fallback worktree → origin/main сохранён через единый API | tests/test_qg_verdicts.py | PASS |
|
||||
| TC-16 | Состав QG_CHECKS и STAGE_TRANSITIONS не изменён | tests/test_stages_invariants.py | PASS |
|
||||
| TC-17 | Полный прогон tests/ зелёный (анти-регресс) | tests/ | PASS |
|
||||
|
||||
Все 17 тест-кейсов плана покрыты и зелёные. TC-таргетные модули
|
||||
(`test_frontmatter.py`, `test_qg_verdicts.py`, `test_security_gate.py`,
|
||||
`test_stages_invariants.py`) — **49 passed**.
|
||||
|
||||
## Покрытие критериев приёмки (03-acceptance-criteria.md)
|
||||
| AC | Подтверждено | Результат |
|
||||
|----|--------------|-----------|
|
||||
| AC-1 reader+writer+валидатор, unit-tested | TC-01…TC-07 | PASS |
|
||||
| AC-2 спека handoff в docs/_standards/ согласована | (review/doc-check) | PASS |
|
||||
| AC-3 единый контракт вердиктов (5 точек) | TC-08…TC-12 | PASS |
|
||||
| AC-4 BC старых доков + регресс зелёный + самопрохождение | TC-13/TC-14 + полный регресс + задача на testing | PASS |
|
||||
| AC-5 never-raise + warning-only + kill-switch | TC-05/TC-07 | PASS |
|
||||
| AC-6 STAGE_TRANSITIONS/QG_CHECKS неизменны | TC-16 | PASS |
|
||||
| AC-7 документация обновлена | review «Документация» | PASS |
|
||||
|
||||
## Вывод pytest
|
||||
|
||||
Полный регресс:
|
||||
```
|
||||
1212 passed, 1 warning in 34.97s
|
||||
```
|
||||
|
||||
TC-таргетные модули:
|
||||
```
|
||||
49 passed, 1 warning in 0.44s
|
||||
```
|
||||
|
||||
Единственное предупреждение — `PydanticDeprecatedSince20` (class-based config в
|
||||
`src/config.py`), предсуществующее, не связано с ORCH-076, не влияет на результат.
|
||||
Сетевых зависимостей в тестах нет (frontmatter — файловый/in-memory контракт).
|
||||
|
||||
## Итог
|
||||
**PASS** — полный регресс зелёный (1212 passed), все 17 TC плана PASS, smoke API OK,
|
||||
prod-контейнер не тронут. Регрессий гейтов (review/tester/deploy/staging/security) нет,
|
||||
семантика вердиктов 1:1. Задача готова к переходу на `deploy-staging`.
|
||||
42
docs/work-items/ORCH-076/15-staging-log.md
Normal file
42
docs/work-items/ORCH-076/15-staging-log.md
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
staging_status: SUCCESS
|
||||
timestamp: 2026-06-09T11:13:04Z
|
||||
base_url: http://localhost:8501
|
||||
---
|
||||
|
||||
# Staging Gate Log
|
||||
|
||||
Staging test suite completed against the live `orchestrator-staging` instance (8501).
|
||||
Run canonically **inside** the `orchestrator-staging` container (ORCH-048 / ADR-001) via the
|
||||
Docker Engine API `exec` (equivalent to `docker exec`; the host `docker` CLI was unavailable,
|
||||
but the script still executed in-container so B6 reads the instance's own `.env.staging`
|
||||
process-env — no false registry FAIL).
|
||||
|
||||
Command:
|
||||
```
|
||||
python3 /repos/orchestrator/scripts/staging_check.py --base-url http://localhost:8501 --mode stub
|
||||
```
|
||||
|
||||
## Result
|
||||
|
||||
```
|
||||
RESULT: 8/10 checks PASS
|
||||
REAL failed : none
|
||||
SANDBOX_INFRA failed: ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue']
|
||||
tolerance: staging_infra_tolerance_enabled=True
|
||||
```
|
||||
|
||||
INFRA-WAIVED: C9a Branch appears in orchestrator-sandbox, C9b Analyst job enqueued in staging queue (known sandbox-infra; real checks green)
|
||||
|
||||
VERDICT: SUCCESS (exit 0) — SUCCESS (infra-waived): ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue'] are known sandbox-infra checks; all real checks green
|
||||
|
||||
## Notes
|
||||
|
||||
- **Exit code: 0** → `staging_status: SUCCESS` (trusting the exit code per ORCH-061; waived
|
||||
checks are not re-judged).
|
||||
- All REAL pipeline checks passed: A1/A2/A3 (smoke), B4/B5/B6 (access + registry isolation),
|
||||
C7/C8 (E2E issue create + webhook trigger).
|
||||
- The only failures are the two SANDBOX_INFRA checks **C9a/C9b**, which depend on SANDBOX bot
|
||||
accounts being members of the sandbox Plane project — an infra precondition, not a pipeline
|
||||
regression. They are tolerated (`ORCH_STAGING_INFRA_TOLERANCE_ENABLED=true`) while every REAL
|
||||
check is green; the script printed the `INFRA-WAIVED:` / `VERDICT:` lines above and exited 0.
|
||||
7
docs/work-items/ORCH-089/00-business-request.md
Normal file
7
docs/work-items/ORCH-089/00-business-request.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Business Request: Авто-режим по лейблам: autoApprove (орк сам подтверждает BRD) + autoDeploy (орк сам деплоит)
|
||||
|
||||
Work Item ID: ORCH-089
|
||||
|
||||
## Description
|
||||
|
||||
TBD
|
||||
123
docs/work-items/ORCH-089/01-brd.md
Normal file
123
docs/work-items/ORCH-089/01-brd.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# 01 — BRD: Авто-режим по лейблам (autoApprove + autoDeploy)
|
||||
|
||||
**Work Item:** ORCH-089
|
||||
**Repo:** orchestrator (self-hosting)
|
||||
**Стадия:** analysis
|
||||
**Приоритет:** Бэклог (запуск по решению Славы, serial e2e после ORCH-88)
|
||||
|
||||
> ⚠️ Прошлый подход (09.06) ОТМЕНЁН: «Стрим ревьюит и апрувит BRD» — НЕ реализовывать.
|
||||
> Актуальная модель: автономность управляется **лейблами Plane на задаче**, без участия людей.
|
||||
|
||||
## 1. Проблема / зачем
|
||||
|
||||
В конвейере два **человеческих** гейта — точки, где конвейер останавливается и ждёт
|
||||
ручного клика человека (Слава/Стрим):
|
||||
|
||||
1. **Гейт BRD** (стадия `analysis`): после завершения analyst задача переводится в
|
||||
`In Review` и ждёт, пока человек вручную выставит Plane-статус **Approved**, чтобы
|
||||
уйти на `architecture`.
|
||||
2. **Гейт деплоя** (стадия `deploy`): после зелёного staging задача переводится в
|
||||
`Awaiting Deploy` (Phase A, ORCH-036/059) и ждёт, пока человек вручную выставит
|
||||
статус **Confirm Deploy**, чтобы запустить прод-деплой (Phase B).
|
||||
|
||||
Для задач, которым **доверяем**, оба ручных решения избыточны и тормозят пакетный
|
||||
автономный прогон («10–20 задач за ночь», эпик ORCH-088). Нужно снять эти два
|
||||
человеческих решения **выборочно и декларативно** — через лейблы на конкретной задаче.
|
||||
|
||||
## 2. Бизнес-цель
|
||||
|
||||
Дать оператору возможность пометить задачу лейблом и тем самым **разрешить орку
|
||||
самому пройти соответствующий человеческий гейт**, не трогая ни одну техническую
|
||||
проверку. Доверие выражается лейблом — на уровне отдельной задачи, обратимо, прозрачно.
|
||||
|
||||
## 3. Модель (решение Славы, 09.06)
|
||||
|
||||
| Лейбл на задаче | Эффект |
|
||||
|-----------------|--------|
|
||||
| `autoApprove` | Орк САМ подтверждает BRD (гейт 1: `In Review → Approved`), без человека. Конвейер идёт на `architecture`. |
|
||||
| `autoDeploy` | Орк САМ подтверждает прод-деплой (гейт 2: `Confirm Deploy`) и деплоит в прод после зелёного staging + всех тех-гейтов, без человека. |
|
||||
|
||||
**Лейблы независимы:**
|
||||
- только `autoApprove` → BRD авто, деплой вручную;
|
||||
- только `autoDeploy` → BRD вручную, деплой авто;
|
||||
- оба → полная автономность (анализ → деплой без единого ручного клика);
|
||||
- без лейблов → **текущее поведение** (оба гейта ручные, нулевая регрессия).
|
||||
|
||||
## 4. Критический инвариант — авто-режим снимает ТОЛЬКО человеческое решение
|
||||
|
||||
Авто-режим **не отключает и не ослабляет ни одну техническую проверку**. Все
|
||||
тех-гейты остаются на месте и блокируют при провале ровно как сейчас:
|
||||
|
||||
- `check_ci_green` (CI зелёный);
|
||||
- `check_staging_status` (staging healthy, ORCH-035);
|
||||
- security-гейт (gitleaks + pip-audit, ORCH-022);
|
||||
- merge-gate / re-test / merge-lease (ORCH-043);
|
||||
- image-freshness / provenance guard (ORCH-058);
|
||||
- merge-verify + regression-guard (ORCH-071/073);
|
||||
- post-deploy monitor (ORCH-021).
|
||||
|
||||
`autoDeploy` **никогда не деплоит сломанное** — он лишь заменяет ручной клик
|
||||
«Confirm Deploy» на авто-проход, и только когда все тех-гейты на ребре
|
||||
`deploy-staging → deploy` уже зелёные. `autoApprove` заменяет ручной клик «Approved»,
|
||||
но артефакты анализа (BRD/TRZ/AC/test-plan) должны существовать (`check_analysis_complete`).
|
||||
|
||||
## 5. Fail-safe (безопасность по умолчанию)
|
||||
|
||||
При любой неоднозначности — **откат к ручному гейту** (never auto):
|
||||
|
||||
- лейбл не распознан / Plane API недоступен / ошибка чтения лейблов;
|
||||
- неоднозначность сопоставления имени лейбла;
|
||||
- любое исключение в логике определения авто-режима.
|
||||
|
||||
Лучше подождать человека, чем авто-пропустить гейт по ошибке. Это согласуется с
|
||||
fail-closed-практикой проекта (ORCH-059 «нет статуса → нет деплоя»).
|
||||
|
||||
## 6. Прозрачность (обязательно)
|
||||
|
||||
Каждый авто-проход гейта **логируется и виден** оператору:
|
||||
|
||||
- запись в лог (кто/почему: `label autoApprove → auto-approved BRD` /
|
||||
`label autoDeploy → auto-confirmed prod deploy`);
|
||||
- Telegram-уведомление + строка/обновление в live-карточке задачи (ORCH-042/087);
|
||||
- Plane-коммент в задаче (как при ручном проходе гейта).
|
||||
|
||||
Слава должен по карточке/Telegram видеть, что задача прошла гейт автоматически (а не
|
||||
руками), и какой именно лейбл это разрешил.
|
||||
|
||||
## 7. Бизнес-требования (BR)
|
||||
|
||||
- **BR-1.** Лейбл `autoApprove` на задаче → BRD подтверждается автоматически
|
||||
(`In Review → Approved`) сразу после успешного analyst (артефакты готовы),
|
||||
конвейер идёт на `architecture`. Закрывается клок `brd_review_ended_at`.
|
||||
- **BR-2.** Лейбл `autoDeploy` на задаче → после зелёного staging и всех тех-гейтов
|
||||
прод-деплой (Phase B) триггерится автоматически, без ручного `Confirm Deploy`.
|
||||
- **BR-3.** Лейблы независимы; комбинация обоих даёт полную автономность анализ→деплой.
|
||||
- **BR-4.** Без лейблов поведение конвейера **не меняется** (оба гейта ручные).
|
||||
- **BR-5.** Тех-гейт красный → авто-режим НЕ проходит гейт; задача встаёт/заворачивается
|
||||
ровно как сейчас (авто-режим не маскирует провал тех-проверки).
|
||||
- **BR-6.** Нераспознанный/спорный лейбл / ошибка чтения → fail-safe к ручному гейту.
|
||||
- **BR-7.** Каждый авто-проход гейта логируется и виден в карточке/Telegram + Plane.
|
||||
- **BR-8.** Лейблы `autoApprove` и `autoDeploy` должны существовать в Plane-проекте ORCH
|
||||
(сейчас их нет — создать через labels API; инфра-предусловие).
|
||||
- **BR-9.** Раскат под kill-switch (как ORCH-035/043/059/088); выключенный флаг →
|
||||
строго прежнее поведение (нулевая регрессия для enduro-trails и для самого ORCH).
|
||||
- **BR-10.** Авто-проходы — только для self-hosting/applicable репо по тому же
|
||||
условному принципу, что и self-deploy (Phase A/B существуют только для self-hosting).
|
||||
Гейт BRD логически применим к любому репо, но раскат гейтится флагом/scope.
|
||||
|
||||
## 8. Вне scope (НЕ делаем в этой задаче)
|
||||
|
||||
- Любая логика «Стрим/человек ревьюит BRD» (отменённый подход).
|
||||
- Управление лейблами из UI оркестратора.
|
||||
- Авто-режим для REQUEST_CHANGES / откатов reviewer/tester (это не человеческие гейты —
|
||||
это технические вердикты, они и так автоматические).
|
||||
- Снятие/ослабление любого технического гейта.
|
||||
- Авто-снятие per-repo freeze (ORCH-088) — freeze остаётся ручным.
|
||||
|
||||
## 9. Допущения и зависимости
|
||||
|
||||
- Plane labels API v1 работает (`POST /labels/` подтверждён в бизнес-запросе; GET
|
||||
лейблов проекта и поле `labels` issue — проверить на этапе архитектуры/разработки).
|
||||
- Идёт поверх ORCH-088 (serial gate) — авто-режим совместим с serial e2e: serial-gate
|
||||
сериализует задачи, авто-режим убирает человеческие паузы внутри прохода одной задачи.
|
||||
- Self-deploy Phase A/B/C (ORCH-036/059/071) — точки врезки авто-деплоя.
|
||||
210
docs/work-items/ORCH-089/02-trz.md
Normal file
210
docs/work-items/ORCH-089/02-trz.md
Normal file
@@ -0,0 +1,210 @@
|
||||
# 02 — ТЗ: Авто-режим по лейблам (autoApprove + autoDeploy)
|
||||
|
||||
**Work Item:** ORCH-089
|
||||
**Базируется на BRD:** `01-brd.md`
|
||||
|
||||
> ТЗ фиксирует **что** должно измениться (модули, API, БД, гейты, артефакты,
|
||||
> флаги) и предметные требования к поведению. Архитектурное **как** (структура
|
||||
> leaf-модуля, стратегия кэша лейблов, точная сигнатура хелперов) — за архитектором
|
||||
> (ADR `06-adr/`). ТЗ задаёт границы, которые архитектура обязана соблюсти.
|
||||
|
||||
---
|
||||
|
||||
## 1. Обзор изменения
|
||||
|
||||
Ввести два независимых авто-прохода человеческих гейтов, управляемых лейблами Plane
|
||||
на конкретной задаче:
|
||||
|
||||
- **autoApprove** — авто-проход гейта BRD (`analysis`: `In Review → Approved`).
|
||||
- **autoDeploy** — авто-проход гейта прод-деплоя (`deploy`: `Confirm Deploy` → Phase B).
|
||||
|
||||
Принцип врезки — аддитивно, по образцу условных под-гейтов (ORCH-035/043/058/088):
|
||||
leaf-модуль чистой логики (never-raise) + точечные врезки в существующие точки
|
||||
принятия решений + флаги в `config.py`. **`STAGE_TRANSITIONS` и реестр `QG_CHECKS`
|
||||
НЕ трогаются** — авто-режим переиспользует уже существующие переходы и гейты, лишь
|
||||
устраняя ожидание человеческого сигнала.
|
||||
|
||||
---
|
||||
|
||||
## 2. Задействованные модули `src/`
|
||||
|
||||
| Модуль | Роль изменения |
|
||||
|--------|----------------|
|
||||
| `src/labels.py` (**новый, leaf**) | Чистая логика авто-режима: `auto_approve_applies(repo)`, `auto_deploy_applies(repo)`, `has_label(work_item_id, label, project_id) -> bool/None`, нормализация имён лейблов, fail-safe. never-raise. |
|
||||
| `src/plane_sync.py` | Новая функция чтения лейблов issue из Plane API (`fetch_issue_labels`) + резолв карты лейблов проекта (имя↔uuid, с кэшем по образцу `get_project_states`). Новый сеттер статуса `set_issue_approved` (PATCH в Approved-UUID) для индикации авто-аппрува. |
|
||||
| `src/stage_engine.py` | Врезка autoApprove в `_handle_analysis_approved_flow` (ветка `files_ok`, после `set_issue_in_review`). Врезка autoDeploy в `_handle_self_deploy_phase_a` (после advance на `deploy`, перед возвратом). |
|
||||
| `src/config.py` | Новые флаги `auto_label_enabled`, `auto_approve_label`, `auto_deploy_label`, `auto_label_repos` (+ при необходимости TTL кэша лейблов). |
|
||||
| `src/main.py` (`GET /queue`) | Аддитивный блок наблюдаемости `auto_labels` (опционально: счётчики авто-проходов). |
|
||||
| `src/webhooks/plane.py` | (Опц.) если payload вебхука несёт `labels` — использовать как быстрый путь; иначе чтение через `fetch_issue_labels`. Источник истины лейблов — Plane API (надёжнее payload). |
|
||||
|
||||
> Точные имена функций/флагов — ориентир; финальные сигнатуры закрепляет ADR.
|
||||
> Обязательное требование: вся логика определения авто-режима — **never-raise** и
|
||||
> при ошибке возвращает «нет авто» (fail-safe к ручному гейту, BR-6).
|
||||
|
||||
---
|
||||
|
||||
## 3. Точки врезки (insertion points) — предметные требования
|
||||
|
||||
### 3.1 Гейт BRD (autoApprove)
|
||||
|
||||
**Текущее поведение** (`src/stage_engine.py::_handle_analysis_approved_flow`, ветка
|
||||
`files_ok`, ~стр. 584–599):
|
||||
1. `set_issue_in_review(work_item_id)`;
|
||||
2. Plane-коммент «артефакты готовы»;
|
||||
3. `notify_approve_requested(task_id)`;
|
||||
4. `return` — **без advance**; ждёт ручного Approved через webhook
|
||||
(`handle_verdict(approved=True)` → `_try_advance_stage` → advance на `architecture`).
|
||||
|
||||
**Требуемое поведение при `autoApprove`:**
|
||||
- ПОСЛЕ установки `In Review` и коммента (для прозрачности и клока) проверить лейбл
|
||||
`autoApprove` на задаче;
|
||||
- если лейбл есть И `auto_approve_applies(repo)` И `auto_label_enabled`:
|
||||
- выставить Plane-статус **Approved** (индикация; `set_issue_approved`);
|
||||
- залогировать авто-проход (`label autoApprove → BRD auto-approved`);
|
||||
- отправить Telegram + Plane-коммент о факте авто-аппрува (BR-7, прозрачность);
|
||||
- инициировать тот же advance, что делает ручной Approved, т.е. переход
|
||||
`analysis → architecture` через штатный путь (`advance_stage(..., finished_agent=None)`
|
||||
с `qg_passed`/`approved-via-status`-семантикой), чтобы:
|
||||
- закрылся клок `brd_review_ended_at` (`mark_brd_review_ended`),
|
||||
- выполнились все стандартные пост-переходные эффекты (карточка, plane-sync);
|
||||
- если лейбла нет / ошибка чтения → **прежнее поведение** (return, ждём человека).
|
||||
|
||||
> Требование к реализации advance: НЕ дублировать переходную логику. Авто-аппрув
|
||||
> обязан идти через тот же advance-путь, что и человеческий Approved (единый источник
|
||||
> истины перехода). Защита от двойного advance/гонки с реальным webhook — идемпотентность
|
||||
> (advance применяется один раз; повторный сигнал — no-op).
|
||||
|
||||
### 3.2 Гейт прод-деплоя (autoDeploy)
|
||||
|
||||
**Текущее поведение** (`src/stage_engine.py::_handle_self_deploy_phase_a`, ~стр. 1151):
|
||||
вызывается на ребре `deploy-staging → deploy` ПОСЛЕ зелёных под-гейтов (security →
|
||||
merge-gate → image-freshness → staging). Делает:
|
||||
1. `update_task_stage(task_id, "deploy")` + `notify_stage_change`;
|
||||
2. `set_issue_awaiting_deploy`;
|
||||
3. `write_marker(APPROVE_REQUESTED)`;
|
||||
4. Plane-коммент + Telegram «смените статус на Confirm Deploy»;
|
||||
5. `return` — ждёт ручного `Confirm Deploy` → `handle_confirm_deploy` →
|
||||
`advance_stage(confirm_deploy=True)` → `_handle_self_deploy_phase_b` (initiate_deploy).
|
||||
|
||||
**Требуемое поведение при `autoDeploy`:**
|
||||
- Все тех-гейты ребра `deploy-staging → deploy` уже зелёные к моменту Phase A
|
||||
(иначе сюда не дошли бы) — это структурно гарантирует BR-5 (авто не деплоит сломанное);
|
||||
- ПОСЛЕ advance на `deploy` (шаг 1) проверить лейбл `autoDeploy`;
|
||||
- если лейбл есть И `auto_deploy_applies(repo)` И `auto_label_enabled`:
|
||||
- залогировать авто-проход (`label autoDeploy → prod deploy auto-confirmed`);
|
||||
- Telegram + Plane-коммент о факте авто-деплоя (BR-7);
|
||||
- инициировать Phase B тем же путём, что ручной Confirm Deploy
|
||||
(`_handle_self_deploy_phase_b(...)`), сохранив идемпотентность (маркер `INITIATED`);
|
||||
- индикация статуса — `Deploying` (ставит уже сам Phase B);
|
||||
- если лейбла нет / ошибка → **прежнее поведение** (Phase A ждёт человека).
|
||||
|
||||
> Требование: НЕ обходить и НЕ дублировать тех-гейты. autoDeploy запускается строго
|
||||
> в точке, где Phase A уже прошёл все под-гейты. Phase C (finalizer) + merge-verify +
|
||||
> regression-guard + post-deploy monitor остаются неизменны и продолжают верифицировать
|
||||
> результат деплоя.
|
||||
|
||||
---
|
||||
|
||||
## 4. Изменения Plane API
|
||||
|
||||
Новых endpoint оркестратора (FastAPI) — **нет**. Изменяется только клиентское
|
||||
взаимодействие с Plane API v1:
|
||||
|
||||
| Действие | Endpoint Plane | Назначение |
|
||||
|----------|----------------|------------|
|
||||
| Чтение лейблов issue | `GET /workspaces/{slug}/projects/{pid}/issues/{issue_id}/` → поле `labels` (список uuid) | Узнать, какие лейблы навешены на задачу. |
|
||||
| Карта лейблов проекта | `GET /workspaces/{slug}/projects/{pid}/labels/` → `[{id,name}]` | Сопоставить uuid лейбла ↔ имя (`autoApprove`/`autoDeploy`). Кэшировать (TTL, образец `get_project_states`/`plane_states_ttl_s`). |
|
||||
| Установка Approved | `PATCH /…/issues/{issue_id}/` `{"state": <approved_uuid>}` | Индикация авто-аппрува BRD (`set_issue_approved`, через `get_project_states(...)["approved"]`). |
|
||||
| (Инфра) создание лейблов | `POST /…/labels/` | Однократно создать `autoApprove` и `autoDeploy` в проекте ORCH (см. `07-infra-requirements.md`). |
|
||||
|
||||
**Требования:**
|
||||
- Все GET/PATCH к Plane — через существующие `PLANE_HEADERS`/`_resolve_project_id`,
|
||||
таймаут как у соседей (10с), never-raise.
|
||||
- Сопоставление лейбла — по **имени** (нормализованному: регистр/пробелы), резолвенному
|
||||
из карты лейблов проекта; неоднозначность/нет совпадения → «нет лейбла» (fail-safe).
|
||||
- Чтение лейблов НЕ должно блокировать конвейер при недоступности Plane: ошибка →
|
||||
«нет авто» → ручной гейт.
|
||||
|
||||
---
|
||||
|
||||
## 5. Изменения схемы БД
|
||||
|
||||
**Не требуются.** Авто-режим — stateless относительно БД:
|
||||
- источник истины лейблов — Plane (читается на гейте);
|
||||
- идемпотентность авто-деплоя обеспечена существующими sentinel-маркерами
|
||||
(`APPROVE_REQUESTED`/`INITIATED`, ORCH-036), а не новой колонкой;
|
||||
- клок `brd_review_*` уже существует (ORCH-087).
|
||||
|
||||
Если архитектура решит кэшировать факт авто-прохода для наблюдаемости — допускается
|
||||
**аддитивная** идемпотентная миграция (`_ensure_column`, образец ORCH-065 `jobs.pid`),
|
||||
но это не требование ТЗ (предпочтительно без миграции, restart-safe через Plane/маркеры).
|
||||
|
||||
---
|
||||
|
||||
## 6. Новые QG checks
|
||||
|
||||
**Не вводятся.** Авто-режим не добавляет проверок качества — он устраняет ожидание
|
||||
человеческого сигнала на существующих гейтах. Реестр `QG_CHECKS` и
|
||||
`check_analysis_approved` / `check_deploy_status` / `check_staging_status` —
|
||||
**без изменений**. (Это сознательно: добавление QG-чека усложнило бы матрицу и нарушило
|
||||
инвариант «STAGE_TRANSITIONS/QG_CHECKS не трогаются», характерный для соседних под-гейтов.)
|
||||
|
||||
---
|
||||
|
||||
## 7. Конфигурация (флаги `src/config.py`)
|
||||
|
||||
По образцу ORCH-035/043/059/088 (kill-switch + CSV scope):
|
||||
|
||||
| Флаг | Тип / дефолт | Назначение |
|
||||
|------|--------------|------------|
|
||||
| `auto_label_enabled` | `bool = True` (env `ORCH_AUTO_LABEL_ENABLED`) | Глобальный kill-switch обоих авто-режимов. `False` → строго прежнее поведение (оба гейта ручные). |
|
||||
| `auto_approve_label` | `str = "autoApprove"` | Имя лейбла гейта BRD. |
|
||||
| `auto_deploy_label` | `str = "autoDeploy"` | Имя лейбла гейта деплоя. |
|
||||
| `auto_label_repos` | `str = ""` (CSV) | Scope. Пусто → self-hosting only (как ORCH-035/043), либо «все репо» — выбор фиксирует ADR; дефолт безопасный (self-hosting). |
|
||||
| `auto_label_states_ttl_s` | `int` (опц.) | TTL кэша карты лейблов проекта (образец `plane_states_ttl_s`). |
|
||||
|
||||
**Требование:** при `auto_label_enabled=False` — нулевая регрессия (ни одного нового
|
||||
сетевого вызова на гейтах, поведение 1:1 как до ORCH-089).
|
||||
|
||||
---
|
||||
|
||||
## 8. Наблюдаемость
|
||||
|
||||
- Каждый авто-проход → `logger.info` с причиной (label X → действие).
|
||||
- Telegram-уведомление + обновление live-карточки (ORCH-042/087, never-raise).
|
||||
- Plane-коммент в задаче (автор — `analyst` для BRD, `deployer` для деплоя — по образцу
|
||||
существующих комментов гейтов).
|
||||
- (Опц.) аддитивный блок `auto_labels` в `GET /queue` (enabled, label-имена, scope,
|
||||
счётчики `auto_approved_total`/`auto_deployed_total`) — образец блоков
|
||||
`reconcile`/`serial_gate`.
|
||||
|
||||
---
|
||||
|
||||
## 9. Артефакты pipeline
|
||||
|
||||
Новых обязательных артефактов задачи **нет**. Авто-проходы отражаются в:
|
||||
- Plane-комментах и Telegram/карточке (прозрачность, BR-7);
|
||||
- существующих логах деплоя (`14-deploy-log.md` для autoDeploy — пишется Phase C как сейчас).
|
||||
|
||||
Документация golden-source (обязательно в этом же PR):
|
||||
- `CLAUDE.md` — раздел про авто-режим по лейблам (флаги, инвариант «снимает только
|
||||
человеческое решение»);
|
||||
- `docs/architecture/README.md` — описание врезок autoApprove/autoDeploy + флаги;
|
||||
- `06-adr/ADR-001-*.md` — архитектурное решение (точки врезки, fail-safe, чтение лейблов);
|
||||
- `07-infra-requirements.md` — создание лейблов `autoApprove`/`autoDeploy` в Plane ORCH;
|
||||
- `CHANGELOG.md` — `## [Unreleased]`.
|
||||
|
||||
---
|
||||
|
||||
## 10. Инварианты (что НЕ должно измениться)
|
||||
|
||||
- `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, `check_analysis_approved`,
|
||||
`check_deploy_status`, `check_staging_status` — без изменений.
|
||||
- Все технические под-гейты (security/merge-gate/image-freshness/merge-verify/
|
||||
regression-guard/post-deploy) — без изменений; авто-режим их не обходит.
|
||||
- Ручной путь (без лейблов) — 1:1 как сейчас.
|
||||
- Схема БД, exit-коды deploy-хука, merge-lease, sentinel-маркеры self-deploy — без изменений.
|
||||
- never-raise: ни одна ошибка авто-режима не роняет конвейер и не пропускает гейт
|
||||
ошибочно (fail-safe к ручному).
|
||||
- Self-hosting: авто-режим НЕ рестартит/не роняет прод вне штатного Phase B (который
|
||||
и так есть); autoDeploy лишь авто-инициирует существующий путь деплоя.
|
||||
153
docs/work-items/ORCH-089/03-acceptance-criteria.md
Normal file
153
docs/work-items/ORCH-089/03-acceptance-criteria.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# 03 — Критерии приёмки: Авто-режим по лейблам (ORCH-089)
|
||||
|
||||
Каждый критерий — чёткое условие PASS/FAIL. Маппинг на BR (`01-brd.md`) и AC
|
||||
бизнес-запроса указан в скобках.
|
||||
|
||||
---
|
||||
|
||||
## AC-1 — autoApprove проходит гейт BRD (BR-1 / BizAC-1)
|
||||
|
||||
**Дано:** задача с лейблом `autoApprove`, analyst успешно завершился (артефакты
|
||||
BRD/TRZ/AC/test-plan на месте), `auto_label_enabled=True`, репо в scope.
|
||||
**Когда:** срабатывает `_handle_analysis_approved_flow` (ветка `files_ok`).
|
||||
**Тогда:**
|
||||
- задача автоматически переходит `analysis → architecture` без человеческого Approved;
|
||||
- Plane-статус выставлен в `Approved` (индикация);
|
||||
- клок `brd_review_ended_at` закрыт (`mark_brd_review_ended`);
|
||||
- авто-проход залогирован + Telegram/карточка/Plane-коммент уведомляют о факте.
|
||||
|
||||
**PASS:** стадия задачи стала `architecture` без внешнего webhook Approved; клок закрыт.
|
||||
**FAIL:** задача осталась в `In Review`/`analysis` ИЛИ advance прошёл без индикации/лога.
|
||||
|
||||
---
|
||||
|
||||
## AC-2 — autoDeploy триггерит прод-деплой (BR-2 / BizAC-2)
|
||||
|
||||
**Дано:** задача с лейблом `autoDeploy` дошла до ребра `deploy-staging → deploy`,
|
||||
все тех-гейты (security, merge-gate, image-freshness, staging) зелёные, Phase A advance
|
||||
на `deploy` выполнен, `auto_label_enabled=True`, репо в scope (self-hosting).
|
||||
**Когда:** срабатывает `_handle_self_deploy_phase_a`.
|
||||
**Тогда:**
|
||||
- Phase B (`_handle_self_deploy_phase_b`) инициируется автоматически, без ручного
|
||||
`Confirm Deploy`;
|
||||
- маркер `INITIATED` выставлен (идемпотентность), finalizer-job (Phase C) поставлен;
|
||||
- Plane-статус → `Deploying`; авто-проход залогирован + Telegram/Plane-коммент.
|
||||
|
||||
**PASS:** прод-деплой инициирован без статуса Confirm Deploy от человека; Phase C армлен.
|
||||
**FAIL:** задача застряла в `Awaiting Deploy`, ожидая ручного Confirm Deploy.
|
||||
|
||||
---
|
||||
|
||||
## AC-3 — оба лейбла → полная автономность (BR-3 / BizAC-3)
|
||||
|
||||
**Дано:** задача с лейблами `autoApprove` И `autoDeploy`, все тех-гейты по пути зелёные.
|
||||
**Когда:** задача проходит конвейер `analysis → … → deploy`.
|
||||
**Тогда:** задача проходит от анализа до прод-деплоя без единого ручного клика
|
||||
(ни Approved, ни Confirm Deploy).
|
||||
|
||||
**PASS:** ноль человеческих гейт-кликов; задача достигла `deploy`/`done` автономно.
|
||||
**FAIL:** конвейер остановился хотя бы на одном из двух человеческих гейтов.
|
||||
|
||||
---
|
||||
|
||||
## AC-4 — без лейблов поведение НЕ меняется (BR-4 / BizAC-4)
|
||||
|
||||
**Дано:** задача без лейблов `autoApprove`/`autoDeploy`.
|
||||
**Когда:** проходит гейты BRD и деплоя.
|
||||
**Тогда:** оба гейта остаются ручными — задача ждёт `In Review → Approved` (человек) и
|
||||
`Awaiting Deploy → Confirm Deploy` (человек), ровно как до ORCH-089.
|
||||
|
||||
**PASS:** на гейте BRD задача в `In Review` ждёт человека; на гейте деплоя — в
|
||||
`Awaiting Deploy` ждёт человека. Нулевая регрессия.
|
||||
**FAIL:** задача без лейблов авто-прошла любой гейт.
|
||||
|
||||
---
|
||||
|
||||
## AC-5 — красный тех-гейт блокирует авто-режим (BR-5 / BizAC-5)
|
||||
|
||||
**Дано:** задача с лейблом `autoDeploy`, но один из тех-гейтов на ребре
|
||||
`deploy-staging → deploy` красный (например, staging unhealthy / merge-gate конфликт /
|
||||
security FAIL / image-freshness mismatch).
|
||||
**Когда:** конвейер достигает ребра деплоя.
|
||||
**Тогда:** Phase A НЕ достигается (под-гейт завернул задачу) → autoDeploy НЕ
|
||||
инициирует Phase B; задача откатывается/встаёт ровно как при ручном режиме.
|
||||
|
||||
**PASS:** при красном тех-гейте прод-деплой НЕ инициирован, несмотря на лейбл; поведение
|
||||
тождественно ручному режиму при том же провале.
|
||||
**FAIL:** autoDeploy инициировал прод-деплой при красном тех-гейте.
|
||||
|
||||
> Аналогично для autoApprove: при отсутствии артефактов (`check_analysis_complete` FAIL)
|
||||
> авто-аппрув не срабатывает (нет advance), задача не уходит на architecture.
|
||||
|
||||
---
|
||||
|
||||
## AC-6 — fail-safe к ручному гейту (BR-6 / BizAC-6)
|
||||
|
||||
**Дано:** одно из: лейбл не распознан; Plane API недоступен при чтении лейблов;
|
||||
неоднозначное сопоставление имени; исключение в логике авто-режима.
|
||||
**Когда:** гейт BRD или деплоя.
|
||||
**Тогда:** авто-режим НЕ срабатывает → откат к ручному гейту (задача ждёт человека);
|
||||
конвейер НЕ падает.
|
||||
|
||||
**PASS:** при ошибке/неоднозначности задача переходит в ручное ожидание (никогда не
|
||||
авто-проход по ошибке); ни одно исключение не всплывает наружу (never-raise).
|
||||
**FAIL:** ошибка чтения лейблов привела к авто-проходу ИЛИ к падению/застреванию конвейера.
|
||||
|
||||
---
|
||||
|
||||
## AC-7 — прозрачность каждого авто-прохода (BR-7 / BizAC-7)
|
||||
|
||||
**Дано:** любой сработавший авто-проход (autoApprove или autoDeploy).
|
||||
**Когда:** гейт пройден автоматически.
|
||||
**Тогда:** факт виден в: (а) логе с причиной (label X → действие); (б) Telegram +
|
||||
live-карточке задачи; (в) Plane-комменте.
|
||||
|
||||
**PASS:** все три канала несут отметку об авто-проходе и о том, какой лейбл его разрешил.
|
||||
**FAIL:** авто-проход произошёл «молча» (нет отметки хотя бы в одном из обязательных
|
||||
каналов: лог + Telegram/карточка + Plane).
|
||||
|
||||
---
|
||||
|
||||
## AC-8 — kill-switch и scope (BR-9 / BR-10)
|
||||
|
||||
**Дано:** `auto_label_enabled=False` (или репо вне `auto_label_repos`).
|
||||
**Когда:** задача с лейблами проходит гейты.
|
||||
**Тогда:** авто-режим полностью отключён — оба гейта ручные, никаких новых сетевых
|
||||
вызовов на гейтах; поведение 1:1 как до ORCH-089 (включая нулевую регрессию для enduro).
|
||||
|
||||
**PASS:** при выключенном флаге лейблы игнорируются, поведение прежнее.
|
||||
**FAIL:** при `False` авто-режим сработал ИЛИ появилась регрессия для не-scope репо.
|
||||
|
||||
---
|
||||
|
||||
## AC-9 — независимость лейблов (BR-3)
|
||||
|
||||
**Дано:** задача только с `autoApprove` (без `autoDeploy`) — и симметрично наоборот.
|
||||
**Тогда:**
|
||||
- только `autoApprove`: BRD авто-проходит, деплой ждёт ручного Confirm Deploy;
|
||||
- только `autoDeploy`: BRD ждёт ручного Approved, деплой авто-проходит.
|
||||
|
||||
**PASS:** каждый лейбл влияет строго на свой гейт, второй гейт остаётся ручным.
|
||||
**FAIL:** один лейбл повлиял на оба гейта.
|
||||
|
||||
---
|
||||
|
||||
## AC-10 — инварианты неизменны (TRZ §10)
|
||||
|
||||
**Тогда:** `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_analysis_approved`,
|
||||
`check_deploy_status`, `check_staging_status`, схема БД, все технические под-гейты,
|
||||
sentinel-маркеры self-deploy, exit-коды deploy-хука — **не изменены**.
|
||||
|
||||
**PASS:** diff не затрагивает перечисленные контракты; существующие тесты этих
|
||||
компонентов зелёные.
|
||||
**FAIL:** любой из инвариантных контрактов изменён.
|
||||
|
||||
---
|
||||
|
||||
## AC-11 — документация обновлена (CLAUDE.md §правила 2/6)
|
||||
|
||||
**Тогда:** в том же PR обновлены `CLAUDE.md`, `docs/architecture/README.md`,
|
||||
заведён `06-adr/ADR-001-*`, `07-infra-requirements.md` (создание лейблов), `CHANGELOG.md`.
|
||||
|
||||
**PASS:** документация-golden-source синхронна с кодом.
|
||||
**FAIL:** функционал изменён, документация — нет (reviewer → REQUEST_CHANGES).
|
||||
172
docs/work-items/ORCH-089/04-test-plan.yaml
Normal file
172
docs/work-items/ORCH-089/04-test-plan.yaml
Normal file
@@ -0,0 +1,172 @@
|
||||
work_item: ORCH-089
|
||||
title: "Авто-режим по лейблам: autoApprove (авто-BRD) + autoDeploy (авто-деплой)"
|
||||
description: >
|
||||
План тестов авто-прохода двух человеческих гейтов по лейблам Plane.
|
||||
Фокус юнит-тестов — чистая логика src/labels.py (never-raise, fail-safe) и
|
||||
врезки в stage_engine (autoApprove в _handle_analysis_approved_flow,
|
||||
autoDeploy в _handle_self_deploy_phase_a). Сеть Plane — мокается.
|
||||
Инвариант: STAGE_TRANSITIONS/QG_CHECKS/тех-гейты не трогаются.
|
||||
|
||||
tests:
|
||||
# --- src/labels.py: чистая логика авто-режима (never-raise, fail-safe) -----
|
||||
- id: TC-01
|
||||
type: unit
|
||||
description: "has_label возвращает True, когда лейбл присутствует на issue (мок Plane labels)"
|
||||
module: tests/test_labels.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-02
|
||||
type: unit
|
||||
description: "has_label возвращает False, когда лейбла нет на issue"
|
||||
module: tests/test_labels.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-03
|
||||
type: unit
|
||||
description: "has_label при ошибке Plane API / таймауте → fail-safe (нет авто, never-raise)"
|
||||
module: tests/test_labels.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-04
|
||||
type: unit
|
||||
description: "Сопоставление имени лейбла нормализовано (регистр/пробелы); неоднозначность → нет авто"
|
||||
module: tests/test_labels.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-05
|
||||
type: unit
|
||||
description: "auto_approve_applies/auto_deploy_applies: scope CSV + self-hosting; пустой scope по дефолту"
|
||||
module: tests/test_labels.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-06
|
||||
type: unit
|
||||
description: "auto_label_enabled=False → has_label/applies дают 'нет авто' без сетевых вызовов"
|
||||
module: tests/test_labels.py
|
||||
expected: PASS
|
||||
|
||||
# --- plane_sync: чтение лейблов + сеттер Approved ---------------------------
|
||||
- id: TC-07
|
||||
type: unit
|
||||
description: "fetch_issue_labels парсит поле labels issue и резолвит uuid→имя по карте проекта (мок httpx)"
|
||||
module: tests/test_plane_sync_labels.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-08
|
||||
type: unit
|
||||
description: "Карта лейблов проекта кэшируется с TTL (повтор в окне TTL не делает второй GET)"
|
||||
module: tests/test_plane_sync_labels.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-09
|
||||
type: unit
|
||||
description: "set_issue_approved PATCHит issue в Approved-UUID (get_project_states['approved']); never-raise при ошибке"
|
||||
module: tests/test_plane_sync_labels.py
|
||||
expected: PASS
|
||||
|
||||
# --- autoApprove: врезка в _handle_analysis_approved_flow ------------------
|
||||
- id: TC-10
|
||||
type: unit
|
||||
description: "autoApprove + артефакты готовы → авто-advance analysis→architecture, Approved выставлен, клок brd_review_ended закрыт"
|
||||
module: tests/test_auto_approve_brd.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-11
|
||||
type: unit
|
||||
description: "Без лейбла autoApprove → прежнее поведение: In Review, return без advance (ждёт человека)"
|
||||
module: tests/test_auto_approve_brd.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-12
|
||||
type: unit
|
||||
description: "autoApprove, но артефактов нет (check_analysis_complete FAIL) → НЕ advance (AC-5 для BRD)"
|
||||
module: tests/test_auto_approve_brd.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-13
|
||||
type: unit
|
||||
description: "autoApprove идёт через тот же advance-путь, что ручной Approved (нет дублирования логики; идемпотентно при повторе)"
|
||||
module: tests/test_auto_approve_brd.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-14
|
||||
type: unit
|
||||
description: "autoApprove: авто-проход логируется + Telegram/карточка/Plane-коммент вызваны (прозрачность AC-7)"
|
||||
module: tests/test_auto_approve_brd.py
|
||||
expected: PASS
|
||||
|
||||
# --- autoDeploy: врезка в _handle_self_deploy_phase_a ----------------------
|
||||
- id: TC-15
|
||||
type: unit
|
||||
description: "autoDeploy + Phase A advance на deploy → автоматически вызывается _handle_self_deploy_phase_b (initiate_deploy)"
|
||||
module: tests/test_auto_deploy.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-16
|
||||
type: unit
|
||||
description: "Без лейбла autoDeploy → прежнее поведение: Awaiting Deploy, ждёт ручного Confirm Deploy"
|
||||
module: tests/test_auto_deploy.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-17
|
||||
type: unit
|
||||
description: "autoDeploy идемпотентен: маркер INITIATED уже стоит → повторный авто-триггер = no-op"
|
||||
module: tests/test_auto_deploy.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-18
|
||||
type: unit
|
||||
description: "autoDeploy не-self репо / вне scope → no-op (Phase A/B существуют только для self-hosting)"
|
||||
module: tests/test_auto_deploy.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-19
|
||||
type: unit
|
||||
description: "autoDeploy: авто-проход логируется + Telegram + Plane-коммент (прозрачность AC-7)"
|
||||
module: tests/test_auto_deploy.py
|
||||
expected: PASS
|
||||
|
||||
# --- независимость лейблов + kill-switch -----------------------------------
|
||||
- id: TC-20
|
||||
type: unit
|
||||
description: "Только autoApprove (без autoDeploy): BRD авто, деплой ждёт человека (AC-9)"
|
||||
module: tests/test_auto_label_combinations.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-21
|
||||
type: unit
|
||||
description: "Только autoDeploy (без autoApprove): BRD ждёт человека, деплой авто (AC-9)"
|
||||
module: tests/test_auto_label_combinations.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-22
|
||||
type: unit
|
||||
description: "auto_label_enabled=False → оба гейта ручные при наличии обоих лейблов (kill-switch AC-8)"
|
||||
module: tests/test_auto_label_combinations.py
|
||||
expected: PASS
|
||||
|
||||
# --- интеграция: сквозной авто-проход на ребрах конвейера ------------------
|
||||
- id: TC-23
|
||||
type: integration
|
||||
description: "Оба лейбла + все тех-гейты зелёные → задача проходит analysis→deploy без ручных кликов (AC-3)"
|
||||
module: tests/test_auto_labels_integration.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-24
|
||||
type: integration
|
||||
description: "autoDeploy + красный staging/merge-gate → Phase A не достигнут, Phase B не инициирован (AC-5)"
|
||||
module: tests/test_auto_labels_integration.py
|
||||
expected: PASS
|
||||
|
||||
# --- инварианты / регрессия ------------------------------------------------
|
||||
- id: TC-25
|
||||
type: integration
|
||||
description: "Регресс: задача без лейблов проходит оба гейта ровно как до ORCH-089 (AC-4)"
|
||||
module: tests/test_auto_labels_integration.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-26
|
||||
type: unit
|
||||
description: "Инвариант: STAGE_TRANSITIONS и реестр QG_CHECKS не изменены ORCH-089 (snapshot-сверка)"
|
||||
module: tests/test_auto_labels_invariants.py
|
||||
expected: PASS
|
||||
220
docs/work-items/ORCH-089/06-adr/ADR-001-auto-label-gates.md
Normal file
220
docs/work-items/ORCH-089/06-adr/ADR-001-auto-label-gates.md
Normal file
@@ -0,0 +1,220 @@
|
||||
# ADR-001: Авто-режим по лейблам — autoApprove (гейт BRD) + autoDeploy (гейт прод-деплоя)
|
||||
|
||||
## Статус
|
||||
Accepted
|
||||
|
||||
## Контекст
|
||||
|
||||
В конвейере два **человеческих** гейта (точки, где конвейер останавливается и ждёт
|
||||
ручного клика человека):
|
||||
|
||||
1. **Гейт BRD** (`analysis`): после успешного analyst задача переводится в `In Review`
|
||||
(`_handle_analysis_approved_flow`, ветка `files_ok`) и ждёт ручного Plane-статуса
|
||||
**Approved**. Approved прилетает вебхуком → `handle_verdict(approved=True)` →
|
||||
`_try_advance_stage` → `advance_stage(..., finished_agent=None)` (ветка
|
||||
`check_analysis_approved` / `approved-via-status`) → advance `analysis → architecture`
|
||||
+ `mark_brd_review_ended`.
|
||||
2. **Гейт прод-деплоя** (`deploy`): на ребре `deploy-staging → deploy` после зелёных
|
||||
под-гейтов (security → merge-gate → image-freshness → staging) выполняется Phase A
|
||||
(`_handle_self_deploy_phase_a`): advance на `deploy` + `Awaiting Deploy` + маркер
|
||||
`APPROVE_REQUESTED` + просьба сменить статус на **Confirm Deploy**. Confirm Deploy
|
||||
прилетает вебхуком → `handle_confirm_deploy` → `advance_stage(..., confirm_deploy=True)`
|
||||
→ `_handle_self_deploy_phase_b` (`initiate_deploy` + маркер `INITIATED` + finalizer).
|
||||
|
||||
Для задач, которым **доверяем** (пакетный автономный прогон, эпик ORCH-088), оба ручных
|
||||
клика избыточны и тормозят прогон «10–20 задач за ночь». Нужно снять **только эти два
|
||||
человеческих решения** — выборочно (на уровне отдельной задачи), декларативно (лейблом
|
||||
Plane), обратимо, прозрачно и **не трогая ни одной технической проверки** (BRD §4).
|
||||
|
||||
Прошлый подход «Стрим ревьюит и апрувит BRD» (09.06) ОТМЕНЁН. Актуальная модель —
|
||||
лейблы на задаче (`autoApprove`, `autoDeploy`), независимые, без участия людей.
|
||||
|
||||
## Решение
|
||||
|
||||
Аддитивная врезка по образцу условных под-гейтов проекта (ORCH-035/043/058/059/088):
|
||||
**leaf-модуль чистой логики (never-raise) + точечные врезки в существующие точки принятия
|
||||
решений + флаги в `config.py`**. `STAGE_TRANSITIONS`, реестр `QG_CHECKS` и все `check_*`
|
||||
**НЕ трогаются** — авто-режим переиспользует уже существующие переходы и гейты, лишь
|
||||
устраняя ожидание человеческого сигнала.
|
||||
|
||||
### D1. Новый leaf-модуль `src/labels.py` (чистая логика, never-raise)
|
||||
|
||||
Контракт «никогда не падает; при любой ошибке/неоднозначности → "нет авто"»
|
||||
(fail-safe к ручному гейту, BR-6/AC-6). Публичная поверхность:
|
||||
|
||||
| Функция | Контракт |
|
||||
|---------|----------|
|
||||
| `auto_approve_applies(repo) -> bool` | scope autoApprove (см. D5). False при kill-switch/ошибке. |
|
||||
| `auto_deploy_applies(repo) -> bool` | scope autoDeploy (см. D5). False при kill-switch/ошибке. |
|
||||
| `has_label(work_item_id, label_name, project_id=None) -> bool` | True ⇔ на issue навешен лейбл с именем `label_name` (нормализованным). **Любая** ошибка/неоднозначность/недоступность Plane → **False**. |
|
||||
| `snapshot() -> dict` | read-only для `GET /queue` (enabled, имена лейблов, scope). never-raise. |
|
||||
|
||||
`has_label` резолвит так (всё внутри одного `try/except → False`):
|
||||
1. `labels = plane_sync.fetch_issue_labels(work_item_id, project_id)` — список uuid
|
||||
лейблов issue (None при ошибке → `has_label=False`);
|
||||
2. `name_map = plane_sync.get_project_labels(project_id)` — `{normalized_name → uuid}`
|
||||
карта лейблов проекта (кэш с TTL, см. D4);
|
||||
3. нормализация искомого имени (`_normalize`: `strip().casefold()`);
|
||||
4. `target_uuid = name_map.get(normalized)`; если нет совпадения **или** имя
|
||||
неоднозначно (две записи проекта свелись к одному нормализованному имени) →
|
||||
**False** (fail-safe);
|
||||
5. `return target_uuid in set(labels)`.
|
||||
|
||||
> Источник истины лейблов — **Plane API**, не payload вебхука: обе точки врезки —
|
||||
> launcher-path (analyst-finished / staging-deployer-finished), где payload недоступен;
|
||||
> API надёжнее и единообразен. (Подтверждено: `src/webhooks/plane.py` не несёт `labels`.)
|
||||
|
||||
### D2. Чтение лейблов в `src/plane_sync.py`
|
||||
|
||||
- `fetch_issue_labels(work_item_id, project_id=None) -> list[str] | None` —
|
||||
`GET …/issues/{issue_id}/` → поле `labels` (список uuid). Через
|
||||
`_resolve_project_id` + `find_issue_id` + `PLANE_HEADERS`, таймаут 10с (как соседи).
|
||||
Ошибка/issue-not-found → `None` (отличимо от пустого списка `[]` = «лейблов нет»).
|
||||
- `get_project_labels(project_id) -> dict[str,str]` —
|
||||
`GET …/projects/{pid}/labels/` → `{normalized_name → uuid}`. **Кэш по образцу
|
||||
`get_project_states`** (`_LABELS_CACHE` per-project + TTL `_cache_record_fresh`),
|
||||
чтобы не бить API на каждом гейте. Стейл-кэш при сетевой ошибке отдаётся как у
|
||||
`get_project_states` (safer-than-empty). Пустой результат / ошибка без кэша → `{}`
|
||||
→ `has_label=False`.
|
||||
- `set_issue_approved(work_item_id, project_id=None)` — новый сеттер, 1:1 калька
|
||||
`set_issue_in_review`: `state_id = get_project_states(pid)["approved"]` →
|
||||
`_set_issue_state_direct`. Ключ `approved` уже существует в `_DEFAULT_STATES`
|
||||
и `_PLANE_NAME_TO_KEY` (`"Approved" → "approved"`), отдельная инфра-настройка не нужна.
|
||||
|
||||
### D3. Врезка autoApprove — `_handle_analysis_approved_flow`, ветка `files_ok`
|
||||
|
||||
После существующих шагов (`set_issue_in_review` + analyst-коммент + `notify_approve_requested`
|
||||
— оставлены ради клока/прозрачности/симметрии с ручным путём), ДО `return`:
|
||||
|
||||
```
|
||||
if labels.auto_approve_applies(repo) and labels.has_label(work_item_id, settings.auto_approve_label):
|
||||
plane_sync.set_issue_approved(work_item_id) # индикация (AC-1), транзиентна*
|
||||
logger.info("Task …: label autoApprove → BRD auto-approved")
|
||||
plane_add_comment(work_item_id, "<auto-approve via label autoApprove>", author="analyst")
|
||||
send_telegram("✅ <ORC-NNN>: BRD авто-подтверждён (лейбл autoApprove)")
|
||||
auto = advance_stage(task_id, current_stage, repo, work_item_id, branch, finished_agent=None)
|
||||
result.advanced = auto.advanced; result.to_stage = auto.to_stage
|
||||
result.note = "auto-approved-via-label"
|
||||
return
|
||||
# (нет лейбла / fail-safe) → прежнее поведение: return, ждём человека.
|
||||
```
|
||||
|
||||
**Ключевое требование — НЕ дублировать переходную логику.** Авто-аппрув идёт через тот
|
||||
же `advance_stage(..., finished_agent=None)`, что и человеческий Approved-вебхук: ветка
|
||||
`check_analysis_approved` с `agent is None` → `qg_passed=True` (`approved-via-status`) →
|
||||
advance `analysis → architecture` → `mark_brd_review_ended` (клок) → штатные
|
||||
post-эффекты (карточка, plane-sync, enqueue architect). Единый источник истины перехода.
|
||||
|
||||
> *Транзиентность Approved-статуса:* сразу после advance `plane_notify_stage` выставит
|
||||
> статус `Architecture`, перекрыв `Approved`. Это ожидаемо — `set_issue_approved` даёт
|
||||
> мгновенную индикацию/симметрию, а **durable**-прозрачность несут лог + Telegram + Plane-коммент
|
||||
> (AC-7). Re-entrancy безопасна: вложенный `advance_stage` не возвращается в
|
||||
> `_handle_analysis_approved_flow` (та ветка требует `agent=='analyst'`; вложенный вызов
|
||||
> идёт с `finished_agent=None`) — рекурсии нет.
|
||||
|
||||
### D4. Врезка autoDeploy — `_handle_self_deploy_phase_a`, ранняя ветка
|
||||
|
||||
Сразу после `update_task_stage(task_id, "deploy")` + `notify_stage_change` +
|
||||
`self_deploy.clear_state(repo, work_item_id)` (всегда — wipe стейл-маркеров), ДО
|
||||
«ask-human» блока:
|
||||
|
||||
```
|
||||
if labels.auto_deploy_applies(repo) and labels.has_label(work_item_id, settings.auto_deploy_label):
|
||||
logger.info("Task …: label autoDeploy → prod deploy auto-confirmed")
|
||||
plane_add_comment(work_item_id, "<auto-confirm prod deploy via label autoDeploy>", author="deployer")
|
||||
send_telegram("🚀 <ORC-NNN>: прод-деплой авто-подтверждён (лейбл autoDeploy)")
|
||||
_handle_self_deploy_phase_b(task_id, repo, work_item_id, branch, result) # INITIATED + Deploying + finalizer
|
||||
return
|
||||
# (нет лейбла / fail-safe) → прежний Phase A: set_issue_awaiting_deploy + APPROVE_REQUESTED + «смените на Confirm Deploy».
|
||||
```
|
||||
|
||||
При autoDeploy пропускаются ТОЛЬКО индикативно-человеческие шаги (`set_issue_awaiting_deploy`
|
||||
+ `APPROVE_REQUESTED` + «ask-human» коммент/Telegram) — статус `Deploying` выставит сам
|
||||
Phase B. Идемпотентность прод-деплоя обеспечена существующим маркером `INITIATED` внутри
|
||||
`_handle_self_deploy_phase_b` (повторный заход — no-op). Phase B/C, merge-verify,
|
||||
regression-guard, post-deploy monitor — **неизменны**.
|
||||
|
||||
**Почему BR-5/AC-5 выполнены структурно:** Phase A достигается ТОЛЬКО после зелёных
|
||||
под-гейтов ребра `deploy-staging → deploy` (security → merge-gate → image-freshness →
|
||||
staging — они исполняются ВЫШЕ в `advance_stage` и при FAIL откатывают/возвращают БЕЗ
|
||||
выхода на Phase A). autoDeploy лишь заменяет ручной клик в точке, где все тех-проверки
|
||||
уже зелёные — он физически не может задеплоить сломанное.
|
||||
|
||||
### D5. Scope и kill-switch (флаги `src/config.py`)
|
||||
|
||||
| Флаг | Тип / дефолт | Назначение |
|
||||
|------|--------------|------------|
|
||||
| `auto_label_enabled` | `bool=True` (`ORCH_AUTO_LABEL_ENABLED`) | Глобальный kill-switch обоих авто-режимов. `False` → строго прежнее поведение, **ни одного нового сетевого вызова на гейтах** (AC-8). |
|
||||
| `auto_approve_label` | `str="autoApprove"` | Имя лейбла гейта BRD. |
|
||||
| `auto_deploy_label` | `str="autoDeploy"` | Имя лейбла гейта деплоя. |
|
||||
| `auto_label_repos` | `str=""` (CSV) | Scope. **Пусто → self-hosting only** (`orchestrator`). |
|
||||
| `auto_label_states_ttl_s` | `int=300` | TTL кэша карты лейблов проекта (образец `plane_states_ttl_s`). |
|
||||
|
||||
`auto_approve_applies`/`auto_deploy_applies` — калька `self_deploy_applies`:
|
||||
`auto_label_enabled=False` → всегда False; непустой `auto_label_repos` → только
|
||||
перечисленные репо; пустой → **self-hosting only** (`is_self_hosting_repo`). Решение
|
||||
по дефолту scope (BRD оставил выбор): **self-hosting only** — безопасный дефолт (BR-10),
|
||||
к тому же autoDeploy-врезка живёт в Phase A, которая существует только для self-hosting.
|
||||
Единый scope-флаг на оба лейбла (минимальная матрица); раздельные репо-скоупы — follow-up
|
||||
при необходимости.
|
||||
|
||||
**Порядок проверки на гейте (важно для AC-8):** `applies(repo)` проверяется ПЕРВЫМ
|
||||
(локальный, без сети). Только если `applies==True` вызывается `has_label` (сеть). При
|
||||
выключенном флаге `applies` сразу False → `has_label` не вызывается → нулевой сетевой
|
||||
оверхед, нулевая регрессия для enduro.
|
||||
|
||||
### D6. Идемпотентность и взаимодействие с reconciler/serial-gate
|
||||
|
||||
- **autoApprove vs реальный Approved-вебхук / reconciler F-2:** после авто-advance стадия
|
||||
= `architecture`. Поздний человеческий Approved или F-2 (plane-side) увидят уже
|
||||
`architecture` → не повторят analysis-advance (тот же эффект, что и человеческий
|
||||
double-click сегодня). Advance применяется один раз.
|
||||
- **autoDeploy:** идемпотентность — существующий маркер `INITIATED` (Phase B).
|
||||
- **serial-gate (ORCH-088):** сериализует claim analyst-job на уровне FIFO — авто-режим
|
||||
ортогонален (убирает паузы ВНУТРИ прохода одной задачи), не конфликтует.
|
||||
- **reconciler F-1** analysis не трогает (человеческий гейт) — авто-аппрув идёт через
|
||||
launcher-path, не через F-1.
|
||||
|
||||
### D7. Наблюдаемость (AC-7)
|
||||
|
||||
Каждый авто-проход → `logger.info` (label X → действие) + Telegram + Plane-коммент
|
||||
(автор `analyst` для BRD, `deployer` для деплоя — образец существующих гейт-комментов) +
|
||||
обновление live-карточки через штатный advance/notify. Аддитивный read-only блок
|
||||
`auto_labels` в `GET /queue` (`labels.snapshot()`: enabled, имена лейблов, scope) — образец
|
||||
блоков `reconcile`/`serial_gate`. Счётчики авто-проходов — best-effort/опционально (v1
|
||||
можно in-memory или опустить; БД не трогаем).
|
||||
|
||||
### D8. Схема БД — без изменений
|
||||
|
||||
Авто-режим stateless относительно БД: источник истины лейблов — Plane (читается на гейте);
|
||||
идемпотентность autoDeploy — существующие sentinel-маркеры (`APPROVE_REQUESTED`/`INITIATED`);
|
||||
клок `brd_review_*` уже существует (ORCH-087). Миграции нет (restart-safe через Plane/маркеры).
|
||||
|
||||
## Последствия
|
||||
|
||||
**Плюсы:**
|
||||
- Минимальная, аддитивная поверхность изменения; `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`
|
||||
неприкосновенны (AC-10). Единый источник истины перехода (переиспользование advance/Phase B).
|
||||
- Все тех-гейты на месте; autoDeploy структурно не может задеплоить сломанное (BR-5/AC-5).
|
||||
- Декларативно и обратимо (снял лейбл → ручной режим). Независимые лейблы (AC-9).
|
||||
- Fail-safe by default (never auto при любой неоднозначности, AC-6); kill-switch + scope
|
||||
→ нулевая регрессия для enduro (AC-8).
|
||||
|
||||
**Минусы / ограничения:**
|
||||
- Approved-статус при autoApprove транзиентен (перекрывается `Architecture`) — durable-аудит
|
||||
несут лог/Telegram/коммент, не Plane-статус.
|
||||
- Чтение лейблов добавляет 1–2 GET к Plane на каждом из двух гейтов применимого репо (с TTL-кэшем
|
||||
карты лейблов; вызывается только когда `applies==True`). При недоступности Plane → fail-safe
|
||||
к ручному гейту (не блок конвейера).
|
||||
- Доверие выражается лейблом — оператор отвечает за то, что autoDeploy навешан осознанно
|
||||
(тех-гейты страхуют от поломки, но не от «не той фичи»).
|
||||
|
||||
**Инфра-предусловие:** лейблы `autoApprove`/`autoDeploy` должны существовать в Plane-проекте
|
||||
ORCH (создать однократно через labels API) — см. `07-infra-requirements.md`. Нет лейбла в
|
||||
проекте → `has_label` всегда False → ручной режим (fail-safe), без ошибок.
|
||||
|
||||
## Связанные
|
||||
- BRD/ТЗ/AC: `docs/work-items/ORCH-089/{01-brd,02-trz,03-acceptance-criteria}.md`
|
||||
- Образцы условной врезки: ADR-0003 (staging), 0006 (merge-gate), 0007 (self-deploy),
|
||||
0017 (serial-gate); ORCH-059 (Confirm Deploy status).
|
||||
- Глобальный ADR: `docs/architecture/adr/adr-0018-auto-label-gates.md`.
|
||||
63
docs/work-items/ORCH-089/07-infra-requirements.md
Normal file
63
docs/work-items/ORCH-089/07-infra-requirements.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# 07 — Инфра-требования: ORCH-089 (авто-режим по лейблам)
|
||||
|
||||
## I-1. Создать лейблы в Plane-проекте ORCH (однократно, обязательно)
|
||||
|
||||
Авто-режим управляется лейблами на задаче. В Plane-проекте ORCH сейчас лейблов
|
||||
`autoApprove`/`autoDeploy` **нет** — их нужно создать один раз через labels API:
|
||||
|
||||
```
|
||||
POST {PLANE_BASE}/workspaces/{WORKSPACE}/projects/{ORCH_PROJECT_ID}/labels/
|
||||
Headers: PLANE_HEADERS
|
||||
Body: {"name": "autoApprove"}
|
||||
|
||||
POST {PLANE_BASE}/workspaces/{WORKSPACE}/projects/{ORCH_PROJECT_ID}/labels/
|
||||
Body: {"name": "autoDeploy"}
|
||||
```
|
||||
|
||||
Имена должны **точно** соответствовать `auto_approve_label` / `auto_deploy_label`
|
||||
(дефолты `autoApprove` / `autoDeploy`). Сопоставление в коде — по нормализованному имени
|
||||
(`strip().casefold()`), т.е. регистр/пробелы не критичны, но рекомендуется создать ровно
|
||||
как в дефолте.
|
||||
|
||||
**Fail-safe при отсутствии:** если лейбл в проекте не создан, `labels.has_label` всегда
|
||||
вернёт `False` → задача идёт ручным путём (нулевой риск, без ошибок). То есть создание
|
||||
лейблов — предусловие активации фичи, а не условие стабильности конвейера.
|
||||
|
||||
## I-2. Сброс кэша состояний/лейблов после создания (рекомендуется)
|
||||
|
||||
`get_project_labels` кэширует карту лейблов проекта с TTL `auto_label_states_ttl_s`
|
||||
(дефолт 300с). После создания новых лейблов карта подтянется автоматически в течение TTL;
|
||||
для немедленного эффекта — рестарт не требуется, достаточно дождаться TTL или (если будет
|
||||
добавлен) вызвать reload-хелпер кэша лейблов по образцу `reload_project_states`.
|
||||
|
||||
## I-3. Конфигурация (env, хост mva154)
|
||||
|
||||
По умолчанию фича включена (`auto_label_enabled=True`) и применима только к self-hosting
|
||||
репо (`auto_label_repos=""` → `orchestrator`). Управляющие env (опционально, в `.env`):
|
||||
|
||||
| Env | Дефолт | Эффект |
|
||||
|-----|--------|--------|
|
||||
| `ORCH_AUTO_LABEL_ENABLED` | `true` | Глобальный kill-switch. `false` → оба гейта ручные, нулевой сетевой оверхед. |
|
||||
| `ORCH_AUTO_APPROVE_LABEL` | `autoApprove` | Имя лейбла гейта BRD. |
|
||||
| `ORCH_AUTO_DEPLOY_LABEL` | `autoDeploy` | Имя лейбла гейта деплоя. |
|
||||
| `ORCH_AUTO_LABEL_REPOS` | `` (пусто) | CSV scope. Пусто → self-hosting only. |
|
||||
| `ORCH_AUTO_LABEL_STATES_TTL_S` | `300` | TTL кэша карты лейблов проекта. |
|
||||
|
||||
## I-4. Сетевые/доступ
|
||||
|
||||
Новых endpoint оркестратора нет. Дополнительные **исходящие** вызовы к Plane API v1
|
||||
(те же креды `PLANE_HEADERS`, таймаут 10с):
|
||||
- `GET …/issues/{issue_id}/` (поле `labels`) — чтение лейблов задачи на гейте;
|
||||
- `GET …/projects/{pid}/labels/` — карта лейблов проекта (кэш с TTL);
|
||||
- `PATCH …/issues/{issue_id}/` `{"state": <approved_uuid>}` — индикация авто-аппрува.
|
||||
|
||||
Вызовы — только когда `applies(repo)==True` (kill-switch off / репо вне scope → нет
|
||||
вызовов). Недоступность Plane → fail-safe к ручному гейту (конвейер не блокируется).
|
||||
|
||||
## I-5. Топология / прод-риск
|
||||
|
||||
Self-hosting не меняется: autoDeploy лишь авто-инициирует **существующий** Phase B
|
||||
(detached host-деплой через `scripts/orchestrator-deploy-hook.sh`). Никакого нового пути
|
||||
рестарта прод-контейнера не вводится. Phase C / merge-verify / regression-guard /
|
||||
post-deploy monitor продолжают верифицировать результат. Раскат — под kill-switch
|
||||
(`ORCH_AUTO_LABEL_ENABLED`), деплой self — через обязательный staging-гейт (8501), как всегда.
|
||||
20
docs/work-items/ORCH-089/10-tech-risks.md
Normal file
20
docs/work-items/ORCH-089/10-tech-risks.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# 10 — Технические риски: ORCH-089 (авто-режим по лейблам)
|
||||
|
||||
| # | Риск | Вероятность / Impact | Митигация |
|
||||
|---|------|----------------------|-----------|
|
||||
| R-1 | **Ложный авто-проход гейта** при ошибке чтения лейблов (Plane вернул мусор/частичный ответ) → задача авто-проходит, хотя лейбла нет. | Низк. / **Критич.** (групповой self-hosting риск). | `has_label` обёрнут в единый `try/except → False`; `fetch_issue_labels` различает `None` (ошибка) и `[]` (нет лейблов); неоднозначность имени → False. Любая неопределённость → ручной гейт (BR-6/AC-6). Дополнительно: тех-гейты страхуют от деплоя сломанного даже при ложном autoDeploy. |
|
||||
| R-2 | **Двойной advance / гонка** автоApprove с реальным Approved-вебхуком или reconciler F-2. | Сред. / Низк. | Advance применяется один раз: после авто-advance стадия = `architecture`; поздний Approved/F-2 видят `architecture` и не повторяют analysis-переход (как человеческий double-click сегодня). |
|
||||
| R-3 | **Двойной прод-деплой** при autoDeploy (повторный заход Phase A / дубль staging-deployer-finished). | Низк. / Высок. | Идемпотентность Phase B по маркеру `INITIATED`. Phase A после первого прохода advance'ит стадию на `deploy` → guard `current_stage=="deploy-staging"` больше не матчится, повторный Phase A не запускается. `clear_state` в Phase A wipe'ит маркеры только при входе в свежий проход. |
|
||||
| R-4 | **Re-entrancy** вложенного `advance_stage` из `_handle_analysis_approved_flow` → рекурсия. | Низк. / Сред. | Вложенный вызов идёт с `finished_agent=None` и попадает в ветку `approved-via-status`, НЕ в `_handle_analysis_approved_flow` (та требует `agent=='analyst'`). Рекурсии нет. |
|
||||
| R-5 | **Регрессия для enduro / при выключенном флаге** (лишние сетевые вызовы, изменение поведения). | Низк. / Высок. | `applies(repo)` (локальный, без сети) проверяется ПЕРВЫМ; `has_label` (сеть) — только при `applies==True`. `auto_label_enabled=False` или репо вне scope → `applies==False` → нулевой сетевой оверхед, поведение 1:1 (AC-8). |
|
||||
| R-6 | **Лейбл не создан в Plane-проекте** → фича «молча не работает». | Сред. / Низк. | `has_label==False` → ручной гейт (fail-safe, не ошибка). Инфра-предусловие задокументировано (`07-infra-requirements.md` I-1). Прозрачность: отсутствие авто-прохода видно по тому, что задача встала на ручном гейте. |
|
||||
| R-7 | **Транзиентность Approved-статуса** (перекрывается `Architecture` сразу после advance) → оператор не увидит, что прошёл именно авто-аппрув. | Сред. / Низк. | Durable-прозрачность — лог + Telegram + Plane-коммент («auto-approved via label autoApprove») + live-карточка (AC-7). Plane-статус Approved — лишь мгновенная индикация. |
|
||||
| R-8 | **Stale-кэш карты лейблов** (`get_project_labels`) → недавно созданный/снятый лейбл не виден. | Низк. / Низк. | TTL `auto_label_states_ttl_s` (300с) — самозалечивание без рестарта (образец `plane_states_ttl_s`/ORCH-068). Окно ≤ TTL. |
|
||||
| R-9 | **Plane API недоступен на гейте** → задержка/блок конвейера. | Низк. / Сред. | Таймаут 10с (как соседи), never-raise → «нет авто» → ручной гейт. Конвейер не блокируется; задача просто ждёт человека (прежнее поведение). |
|
||||
| R-10 | **Доверие выражено лейблом ошибочно** (autoDeploy навешан не на ту задачу). | Сред. / Сред. | Тех-гейты блокируют поломку (не «не ту фичу»). Лейбл обратим (снять → ручной режим). Зона ответственности оператора; прозрачность авто-прохода (AC-7) даёт раннее обнаружение. |
|
||||
|
||||
## Вывод
|
||||
Доминирующий риск — **R-1 (ложный авто-проход)**; закрывается строгим never-raise / fail-safe
|
||||
контрактом leaf-модуля и тем, что тех-гейты остаются последней линией защиты. Все риски
|
||||
укладываются в установленные проектом паттерны (условный под-гейт + kill-switch + scope +
|
||||
fail-safe), новых классов риска фича не вводит.
|
||||
91
docs/work-items/ORCH-089/12-review.md
Normal file
91
docs/work-items/ORCH-089/12-review.md
Normal file
@@ -0,0 +1,91 @@
|
||||
---
|
||||
type: review
|
||||
work_item_id: ORCH-089
|
||||
verdict: APPROVED
|
||||
version: 1
|
||||
---
|
||||
|
||||
# Review ORCH-089
|
||||
|
||||
## Summary
|
||||
|
||||
Авто-режим по лейблам Plane (`autoApprove` + `autoDeploy`) реализован строго по ТЗ
|
||||
и ADR: аддитивно, по образцу условных под-гейтов (ORCH-035/043/058/088). Снимаются
|
||||
**только два человеческих решения** (гейт BRD `Approved`, гейт прод-деплоя
|
||||
`Confirm Deploy`); ни одна техническая проверка не тронута. Соответствие всем осям
|
||||
(ТЗ, ADR, качество кода, тесты) — полное; документация-golden-source обновлена в том
|
||||
же PR. Блокирующих findings нет. **Вердикт: APPROVED.**
|
||||
|
||||
## Проверка по осям
|
||||
|
||||
### 1. Соответствие ТЗ (`02-trz.md`)
|
||||
- ✅ Leaf `src/labels.py` (never-raise): `auto_approve_applies`/`auto_deploy_applies`
|
||||
(локальный scope-чек ПЕРВЫМ), `has_label` (единственный сетевой вызов, только при
|
||||
`applies==True` → нулевой оверхед при выключенном флаге, §7/AC-8), `snapshot`.
|
||||
- ✅ `src/plane_sync.py`: `fetch_issue_labels` (`None` при ошибке ≠ `[]`),
|
||||
`get_project_labels` (`{normalized→uuid}`, TTL-кэш `auto_label_states_ttl_s` по
|
||||
образцу `get_project_states`, сентинел `__AMBIGUOUS__` при коллизии имён),
|
||||
`set_issue_approved` (1:1 зеркало `set_issue_in_review`).
|
||||
- ✅ Врезка autoApprove — `_handle_analysis_approved_flow` (ветка `files_ok`) ПОСЛЕ
|
||||
`In Review`+коммента; advance идёт через тот же `advance_stage(..., finished_agent=None)`,
|
||||
что человеческий Approved (без дублирования переходной логики, §3.1).
|
||||
- ✅ Врезка autoDeploy — `_handle_self_deploy_phase_a` после advance на `deploy`+
|
||||
`clear_state`, ДО «ask-human»; Phase B запускается тем же `_handle_self_deploy_phase_b`
|
||||
(§3.2).
|
||||
- ✅ Флаги (`config.py`): `auto_label_enabled`, `auto_approve_label`, `auto_deploy_label`,
|
||||
`auto_label_repos` (пусто → self-hosting only), `auto_label_states_ttl_s` (§7).
|
||||
- ✅ Блок наблюдаемости `auto_labels` в `GET /queue` (§8).
|
||||
- ✅ БД-схема и QG-реестр не трогаются (§5/§6) — подтверждено: `stages.py`,
|
||||
`qg/checks.py`, `db.py` отсутствуют в diff feat-коммита.
|
||||
|
||||
### 2. Соответствие ADR (`06-adr/ADR-001`, global `adr-0018`)
|
||||
- ✅ Реализация 1:1 с решениями ADR: D1 (поверхность leaf + порядок резолва `has_label`),
|
||||
D5 (scope: пусто → self-hosting), fail-safe «never auto on doubt», ambiguity-сентинел.
|
||||
- ✅ Глобальный сквозной ADR `adr-0018-auto-label-gates.md` заведён.
|
||||
- ✅ Подтверждена корректность пути advance: `advance_stage` с `agent=None` идёт в
|
||||
ветку `approved-via-status` (qg_passed, без повторного `check_analysis_approved`) →
|
||||
`analysis → architecture` + `mark_brd_review_ended`. Re-entrancy безопасна
|
||||
(вложенный вызов с `finished_agent=None` не входит в analyst-ветку).
|
||||
|
||||
### 3. Качество кода
|
||||
- ✅ never-raise соблюдён во всех публичных функциях (`labels.py`, новые `plane_sync`-хелперы).
|
||||
- ✅ Нет дублирования переходной логики — переиспользованы `advance_stage` и
|
||||
`_handle_self_deploy_phase_b` (включая существующую идемпотентность `INITIATED`).
|
||||
- ✅ Прозрачность (AC-7) во всех трёх каналах: лог + Telegram (`send_telegram`) +
|
||||
Plane-коммент (`plane_add_comment`), плюс live-карточка через штатный advance.
|
||||
- ✅ Docstrings содержательные; кликабельный номер задачи (`link_for`) в уведомлениях.
|
||||
|
||||
### 4. Тесты
|
||||
- ✅ 43 целевых теста (TC-01…TC-26, 7 модулей) — все зелёные.
|
||||
- ✅ Регрессия: 377 релевантных тестов (stage/plane/analysis/deploy/self_deploy/webhook)
|
||||
— все зелёные. AC-10 (инварианты) подтверждён.
|
||||
|
||||
## Документация
|
||||
Обновлена полностью в том же PR (AC-11):
|
||||
- `CLAUDE.md` — раздел «Авто-режим по лейблам» (флаги, инвариант «снимает только
|
||||
человеческое решение»);
|
||||
- `docs/architecture/README.md` — описание врезок autoApprove/autoDeploy + флаги;
|
||||
- `CHANGELOG.md` — запись в `## [Unreleased]`;
|
||||
- `06-adr/ADR-001-auto-label-gates.md` + global `docs/architecture/adr/adr-0018-auto-label-gates.md`;
|
||||
- `07-infra-requirements.md` — предусловие создания лейблов `autoApprove`/`autoDeploy`
|
||||
в Plane-проекте ORCH.
|
||||
|
||||
Статус: документация синхронна с кодом. Требование CLAUDE.md §2/§6 выполнено.
|
||||
|
||||
## Findings
|
||||
|
||||
### P0 — Blocker
|
||||
- Нет.
|
||||
|
||||
### P1 — Must fix
|
||||
- Нет.
|
||||
|
||||
### P2 — Should fix
|
||||
- Нет.
|
||||
|
||||
### P3 — Nice-to-have
|
||||
- `set_issue_approved` обращается к `get_project_states(pid)["approved"]` прямым
|
||||
индексом (потенциальный `KeyError`, если ключ отсутствует). На практике защищено:
|
||||
ключ `approved` гарантирован в `_DEFAULT_STATES`, паттерн 1:1 повторяет
|
||||
существующий `set_issue_in_review`, а вызов обёрнут внешним `try/except` в
|
||||
`advance_stage` (деградирует к ручному гейту). Косметика, не блокер.
|
||||
88
docs/work-items/ORCH-089/13-test-report.md
Normal file
88
docs/work-items/ORCH-089/13-test-report.md
Normal file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
type: test-report
|
||||
work_item_id: ORCH-089
|
||||
result: PASS
|
||||
---
|
||||
|
||||
# Test Report — ORCH-089
|
||||
|
||||
Авто-режим по лейблам: autoApprove (авто-BRD) + autoDeploy (авто-деплой).
|
||||
|
||||
## Окружение
|
||||
- Python: 3.12.13
|
||||
- pytest: 8.3.3
|
||||
- Branch: feature/ORCH-089-autoapprove-brd-autodeploy
|
||||
- Дата: 2026-06-09
|
||||
|
||||
## Предусловия
|
||||
- Review verdict: **APPROVED** (`12-review.md`, P0/P1 — нет).
|
||||
- Prod health (8500): `{"status":"ok"}` — конвейер всех проектов жив, деструктивные операции не выполнялись.
|
||||
|
||||
## Результаты (test-plan `04-test-plan.yaml`)
|
||||
|
||||
Все 26 TC из плана покрыты 43 целевыми тестами (7 модулей). Сопоставление с критериями приёмки (`03-acceptance-criteria.md`):
|
||||
|
||||
| TC ID | Описание | AC | Результат |
|
||||
|-------|----------|-----|-----------|
|
||||
| TC-01 | has_label=True когда лейбл присутствует | AC-1 | PASS |
|
||||
| TC-02 | has_label=False когда лейбла нет | AC-4 | PASS |
|
||||
| TC-03 | has_label при ошибке Plane/таймауте → fail-safe, never-raise | AC-6 | PASS |
|
||||
| TC-04 | Нормализация имени лейбла; неоднозначность → нет авто | AC-6 | PASS |
|
||||
| TC-05 | applies: scope CSV + self-hosting; пустой scope по дефолту | AC-8 | PASS |
|
||||
| TC-06 | auto_label_enabled=False → нет авто без сетевых вызовов | AC-8 | PASS |
|
||||
| TC-07 | fetch_issue_labels парсит labels + резолв uuid→имя | AC-1 | PASS |
|
||||
| TC-08 | Карта лейблов проекта кэшируется с TTL | AC-8 | PASS |
|
||||
| TC-09 | set_issue_approved PATCH в Approved-UUID; never-raise | AC-1 | PASS |
|
||||
| TC-10 | autoApprove → авто-advance analysis→architecture, Approved, клок закрыт | AC-1 | PASS |
|
||||
| TC-11 | Без лейбла autoApprove → In Review, return без advance | AC-4 | PASS |
|
||||
| TC-12 | autoApprove без артефактов → НЕ advance | AC-5 | PASS |
|
||||
| TC-13 | autoApprove через тот же advance-путь; идемпотентно | AC-1 | PASS |
|
||||
| TC-14 | autoApprove: лог + Telegram/карточка + Plane-коммент | AC-7 | PASS |
|
||||
| TC-15 | autoDeploy + Phase A → авто _handle_self_deploy_phase_b | AC-2 | PASS |
|
||||
| TC-16 | Без лейбла autoDeploy → Awaiting Deploy, ждёт человека | AC-4 | PASS |
|
||||
| TC-17 | autoDeploy идемпотентен: маркер INITIATED → no-op | AC-2 | PASS |
|
||||
| TC-18 | autoDeploy не-self/вне scope → no-op | AC-8 | PASS |
|
||||
| TC-19 | autoDeploy: лог + Telegram + Plane-коммент | AC-7 | PASS |
|
||||
| TC-20 | Только autoApprove: BRD авто, деплой ждёт человека | AC-9 | PASS |
|
||||
| TC-21 | Только autoDeploy: BRD ждёт человека, деплой авто | AC-9 | PASS |
|
||||
| TC-22 | auto_label_enabled=False → оба гейта ручные | AC-8 | PASS |
|
||||
| TC-23 | Оба лейбла + зелёные тех-гейты → analysis→deploy автономно | AC-3 | PASS |
|
||||
| TC-24 | autoDeploy + красный staging/merge-gate → Phase B НЕ инициирован | AC-5 | PASS |
|
||||
| TC-25 | Регресс: без лейблов оба гейта как до ORCH-089 | AC-4 | PASS |
|
||||
| TC-26 | Инвариант: STAGE_TRANSITIONS и QG_CHECKS не изменены | AC-10 | PASS |
|
||||
|
||||
## Smoke test API (prod 8500)
|
||||
- `GET /health` → `{"status":"ok","service":"orchestrator"}` — OK
|
||||
- `GET /status` → 200, активные задачи перечислены (ORCH-089 = `testing`) — OK
|
||||
- `GET /queue` → 200, блоки наблюдаемости присутствуют — OK
|
||||
|
||||
> Примечание: блок `auto_labels` в `GET /queue` на 8500 пока отсутствует — это ожидаемо:
|
||||
> прод-контейнер исполняет код до ORCH-089 (задача ещё в `testing`, не задеплоена).
|
||||
> Блок добавляется кодом ветки и покрыт юнит-тестами (snapshot/observability) выше.
|
||||
|
||||
## Вывод pytest (полный регресс)
|
||||
|
||||
```
|
||||
======================= 1157 passed, 1 warning in 37.99s =======================
|
||||
```
|
||||
|
||||
Целевой набор ORCH-089 (7 модулей):
|
||||
|
||||
```
|
||||
tests/test_labels.py ................ (14)
|
||||
tests/test_plane_sync_labels.py ..... (11)
|
||||
tests/test_auto_approve_brd.py ...... (5)
|
||||
tests/test_auto_deploy.py ........... (5)
|
||||
tests/test_auto_label_combinations.py (3)
|
||||
tests/test_auto_labels_integration.py (3)
|
||||
tests/test_auto_labels_invariants.py . (2)
|
||||
======================== 43 passed, 1 warning in 1.09s =========================
|
||||
```
|
||||
|
||||
Единственный warning — `PydanticDeprecatedSince20` (class-based config в `src/config.py`),
|
||||
не связан с ORCH-089, присутствует в baseline.
|
||||
|
||||
## Итог
|
||||
|
||||
**PASS** — все 26 TC плана зелёные, полный регресс 1157/1157 пройден, smoke-тесты OK,
|
||||
инварианты (STAGE_TRANSITIONS/QG_CHECKS, AC-10) подтверждены. Задача готова к `deploy-staging`.
|
||||
12
docs/work-items/ORCH-089/14-deploy-log.md
Normal file
12
docs/work-items/ORCH-089/14-deploy-log.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
deploy_status: SUCCESS
|
||||
work_item: ORCH-089
|
||||
hook_exit_code: 0
|
||||
deployed_by: deploy-finalizer
|
||||
---
|
||||
|
||||
# Deploy log — ORCH-036 executable self-deploy
|
||||
|
||||
Прод-деплой завершён хост-хуком с exit-code `0` -> `deploy_status: SUCCESS`.
|
||||
|
||||
Вердикт зафиксирован детерминированным finalizer'ом (Фаза C), не LLM.
|
||||
39
docs/work-items/ORCH-089/15-staging-log.md
Normal file
39
docs/work-items/ORCH-089/15-staging-log.md
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
staging_status: SUCCESS
|
||||
timestamp: 2026-06-09T09:29:58Z
|
||||
base_url: http://localhost:8501
|
||||
---
|
||||
|
||||
# Staging Gate Log
|
||||
|
||||
Staging test suite completed against the live `orchestrator-staging` stand (8501),
|
||||
run canonically inside the container (`docker exec orchestrator-staging python3
|
||||
/repos/orchestrator/scripts/staging_check.py --base-url http://localhost:8501 --mode stub`).
|
||||
|
||||
**Result: 8/10 checks PASS — exit code 0 → SUCCESS.**
|
||||
|
||||
All REAL (pipeline) checks are green. The two failures are the known sandbox-infra
|
||||
checks C9a/C9b (branch in `orchestrator-sandbox` / analyst job enqueued), which depend
|
||||
on SANDBOX bot accounts being members of the SANDBOX project — not on the pipeline.
|
||||
They are waived per ORCH-061 (`staging_infra_tolerance_enabled=True`), so the script
|
||||
still exits 0 (fail-closed for any REAL check).
|
||||
|
||||
```
|
||||
INFRA-WAIVED: C9a Branch appears in orchestrator-sandbox, C9b Analyst job enqueued in staging queue (known sandbox-infra; real checks green)
|
||||
VERDICT: SUCCESS (exit 0) — SUCCESS (infra-waived): ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue'] are known sandbox-infra checks; all real checks green
|
||||
```
|
||||
|
||||
## Check breakdown
|
||||
|
||||
- ✓ A1 GET /health → 200 status=ok
|
||||
- ✓ A2 GET /queue → 200 with counts/max_concurrency/resilience
|
||||
- ✓ A3 ORCH_STAGING=true (not prod)
|
||||
- ✓ B4 Plane: sandbox project accessible
|
||||
- ✓ B5 Gitea: orchestrator-sandbox accessible, push=true
|
||||
- ✓ B6 Registry: sandbox present, prod ET/ORCH absent
|
||||
- ✓ C7 Create issue in Plane SANDBOX
|
||||
- ✓ C8 Trigger pipeline via /webhook/plane
|
||||
- ✗ C9a Branch appears in orchestrator-sandbox (SANDBOX_INFRA — waived)
|
||||
- ✗ C9b Analyst job enqueued in staging queue (SANDBOX_INFRA — waived)
|
||||
|
||||
CLEANUP: test Plane issue deleted (HTTP 204); no branch to delete.
|
||||
@@ -487,6 +487,37 @@ class Settings(BaseSettings):
|
||||
# *_repos, since auto-create is semantically inseparable from merge-verify.
|
||||
merge_verify_autocreate_pr_enabled: bool = True
|
||||
|
||||
# ORCH-089: auto-mode by Plane labels — autoApprove (BRD gate) + autoDeploy
|
||||
# (prod-deploy gate). Two HUMAN gates of the pipeline (analysis: wait for a
|
||||
# manual Approved; deploy Phase A: wait for a manual Confirm Deploy) are the
|
||||
# only blockers of an autonomous batch run (epic ORCH-088). ORCH-089 lifts ONLY
|
||||
# those two human decisions — selectively (a Plane label on the issue),
|
||||
# declaratively, reversibly, WITHOUT touching a single technical check. Additive
|
||||
# leaf (src/labels.py, never-raise) + two point insertions + flags;
|
||||
# STAGE_TRANSITIONS / QG_CHECKS / check_* / DB schema are NOT touched. See
|
||||
# docs/work-items/ORCH-089/06-adr/ADR-001-auto-label-gates.md.
|
||||
# auto_label_enabled -> global kill-switch for BOTH auto-modes (env
|
||||
# ORCH_AUTO_LABEL_ENABLED). False -> strictly the prior
|
||||
# behaviour (both gates manual), AND no new network call
|
||||
# on the gates (applies() returns False first, before
|
||||
# has_label is consulted) — zero regression (AC-8).
|
||||
# auto_approve_label -> Plane label name for the BRD gate (env
|
||||
# ORCH_AUTO_APPROVE_LABEL).
|
||||
# auto_deploy_label -> Plane label name for the deploy gate (env
|
||||
# ORCH_AUTO_DEPLOY_LABEL).
|
||||
# auto_label_repos -> CSV scope (env ORCH_AUTO_LABEL_REPOS). Empty ->
|
||||
# self-hosting only (orchestrator), the safe default
|
||||
# (the autoDeploy insertion lives in Phase A, which only
|
||||
# exists for the self-hosting repo). Non-empty -> only
|
||||
# the listed repos.
|
||||
# auto_label_states_ttl_s -> TTL (seconds) of the per-project label-map cache
|
||||
# (mirrors plane_states_ttl_s); 0 -> lifetime cache.
|
||||
auto_label_enabled: bool = True
|
||||
auto_approve_label: str = "autoApprove"
|
||||
auto_deploy_label: str = "autoDeploy"
|
||||
auto_label_repos: str = ""
|
||||
auto_label_states_ttl_s: int = 300
|
||||
|
||||
# Telegram notifications
|
||||
telegram_bot_token: str = ""
|
||||
telegram_chat_id: str = ""
|
||||
@@ -522,6 +553,17 @@ class Settings(BaseSettings):
|
||||
# ORCH_TRACKER_BRD_REVIEW_CAP_S; default 7200s (2h). 0/negative -> no cap.
|
||||
tracker_brd_review_cap_s: int = 7200
|
||||
|
||||
# ORCH-076 (ORCH-52c, FR-2 / D3): kill-switch for STRICT frontmatter-schema
|
||||
# validation. The unified frontmatter contract (src/frontmatter.py) ships a
|
||||
# machine-checkable schema validator (REQUIRED_FIELDS), but by DEFAULT it is
|
||||
# warning-only and never influences any gate's boolean verdict (maybe_warn_schema
|
||||
# is inert). This flag is RESERVED for a future tightening (ORCH-52d, when agents
|
||||
# start emitting the full schema). It MUST stay False in prod / .env.staging —
|
||||
# otherwise ORCH-52c would self-block its own deploy (its docs predate the
|
||||
# schema). Env ORCH_FRONTMATTER_VALIDATION_STRICT; default False (zero behaviour
|
||||
# change). See docs/_standards/HANDOFF_PROTOCOL.md.
|
||||
frontmatter_validation_strict: bool = False
|
||||
|
||||
# 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
|
||||
|
||||
@@ -1,27 +1,144 @@
|
||||
"""Safe single-key YAML frontmatter reader (ORCH-016 / ADR-001 §5).
|
||||
"""Unified YAML-frontmatter contract — reader + writer + schema validator (ORCH-52c).
|
||||
|
||||
The status-comment builder (build_status_comment) needs to surface verdict /
|
||||
deploy_status / staging_status from the per-stage artifact files (12-review.md,
|
||||
13-test-report.md, 14-deploy-log.md, 15-staging-log.md). Those files share the
|
||||
same leading-YAML-frontmatter convention used by the quality gates — but the
|
||||
comment hot-path must NEVER raise: a missing file, malformed YAML, or absent
|
||||
key should simply suppress the verdict line, not break the run.
|
||||
History
|
||||
-------
|
||||
ORCH-016 introduced this module as a *single-key reader* (``read_frontmatter_value``)
|
||||
for the status-comment hot path, intentionally duplicating ~10 lines of
|
||||
YAML-frontmatter logic already present in ``src/qg/checks.py`` and
|
||||
``src/security_gate.py`` to keep that PR's blast radius small. Its docstring noted
|
||||
*"merging into a single parser is a follow-up task"* — this module (ORCH-52c /
|
||||
ORCH-076) is that follow-up.
|
||||
|
||||
This module is a tiny defensive helper:
|
||||
- `read_frontmatter_value(path, key)` -> str | None
|
||||
- swallows every exception, logs to logger.debug, returns None.
|
||||
What this module now provides (ADR-001 / adr-0020)
|
||||
--------------------------------------------------
|
||||
A **single point of YAML-frontmatter parsing** that every verdict gate delegates to,
|
||||
plus a writer and a (warning-only by default) schema validator:
|
||||
|
||||
It intentionally duplicates ~10 lines of YAML-frontmatter logic that already
|
||||
exist in `src/qg/checks.py` (S-5 / БАГ 8 / ET-013 fixes). ADR-001 §5 accepts
|
||||
this duplication to keep the blast radius of ORCH-016 small (no QG refactor in
|
||||
this PR); merging into a single parser is a follow-up task.
|
||||
* ``read_frontmatter_value(path, key)`` — UNCHANGED single-key reader (INV-3): the
|
||||
external callers (``usage.py``, ``notifications.build_status_comment``) keep the
|
||||
exact same contract (``str | None``, never-raise, strip, case preserved).
|
||||
* ``parse_frontmatter(content)`` — the ONE YAML parse primitive; returns a
|
||||
structured :class:`FrontmatterParse` so each gate can reproduce its current
|
||||
reason-strings 1:1 (no-block / malformed / yaml-error / data).
|
||||
* ``parse_frontmatter_dict`` / ``read_frontmatter`` — convenience shortcuts to the
|
||||
parsed mapping (in-memory / from a file).
|
||||
* ``render_frontmatter`` / ``write_frontmatter`` — canonical writer; the output is
|
||||
byte-compatible with the existing ``split("---", 2)`` + ``yaml.safe_load`` parsers.
|
||||
* ``validate_schema`` + ``REQUIRED_FIELDS`` + ``maybe_warn_schema`` — the machine
|
||||
schema (``work_item / stage / author_agent / status / created_at / model_used``).
|
||||
By default it is **warning-only** and never influences any gate's boolean verdict
|
||||
(NFR-3 / INV-4); the strict mode is reserved for ORCH-52d and gated behind the
|
||||
``frontmatter_validation_strict`` kill-switch (default ``False``).
|
||||
* ``strip_frontmatter(content)`` — shared body-only helper (replaces the duplicated
|
||||
``_strip_frontmatter`` in ``review_parse``).
|
||||
|
||||
Contract — the WHOLE module is **never-raise** (NFR-2), exactly like the original
|
||||
reader: any error (I/O, YAML, serialization) is logged to ``logger.debug/warning``
|
||||
and degrades to a safe value (``{}`` / ``False`` / the input text); an exception
|
||||
never escapes into the pipeline. This is a hard self-hosting requirement: these
|
||||
functions read verdicts ON THE GATES of the instance that serves prod for every
|
||||
project from one shared DB/queue, so a regression here would stall every project.
|
||||
|
||||
This module is a **leaf**: it imports only ``logging`` (and lazily ``yaml``); it does
|
||||
not import anything project-specific, so it stays cycle-free for ``qg/checks.py``,
|
||||
``security_gate.py``, ``post_deploy.py`` and ``review_parse.py``.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Mapping
|
||||
|
||||
logger = logging.getLogger("orchestrator.frontmatter")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Schema constants (the machine-checkable required frontmatter — FR-2 / D3)
|
||||
# ---------------------------------------------------------------------------
|
||||
#: The required frontmatter fields a stage handoff document is expected to carry.
|
||||
#: Source of truth for HANDOFF_PROTOCOL.md §2. The validator is warning-only by
|
||||
#: default (D3) — its presence does NOT gate the pipeline unless the
|
||||
#: ``frontmatter_validation_strict`` kill-switch is flipped on (reserved, ORCH-52d).
|
||||
REQUIRED_FIELDS = ("work_item", "stage", "author_agent", "status", "created_at", "model_used")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parse primitive — the SINGLE point of YAML-frontmatter logic (D1 / D2)
|
||||
# ---------------------------------------------------------------------------
|
||||
@dataclass(frozen=True)
|
||||
class FrontmatterParse:
|
||||
"""Structured outcome of parsing a document's leading YAML frontmatter.
|
||||
|
||||
The structure (not a bare dict) lets each gate reproduce its EXISTING
|
||||
reason-strings 1:1 (ADR-001 D2): a gate can tell "no block" from "malformed"
|
||||
from "yaml error" from "valid data" without re-implementing the parse.
|
||||
|
||||
Attributes:
|
||||
data: the parsed mapping; ``{}`` when absent / malformed / not a mapping.
|
||||
has_block: a leading ``---`` … ``---`` block was present.
|
||||
malformed: the content started with ``---`` but had < 3 ``---``-split segments
|
||||
(an unterminated frontmatter block).
|
||||
yaml_error: the ``yaml.safe_load`` error text, else ``None``.
|
||||
"""
|
||||
|
||||
data: dict = field(default_factory=dict)
|
||||
has_block: bool = False
|
||||
malformed: bool = False
|
||||
yaml_error: str | None = None
|
||||
|
||||
|
||||
def parse_frontmatter(content: str) -> FrontmatterParse:
|
||||
"""Parse the leading YAML frontmatter of ``content`` into a :class:`FrontmatterParse`.
|
||||
|
||||
The single canonical implementation of the block that used to be duplicated in
|
||||
every verdict gate (``content.startswith("---")`` -> ``split("---", 2)`` ->
|
||||
``yaml.safe_load`` -> ``isinstance(dict)``). Never raises:
|
||||
|
||||
* not a string / no leading ``---`` -> ``has_block=False``, ``data={}``.
|
||||
* ``---`` but < 3 segments (unterminated) -> ``malformed=True``, ``data={}``.
|
||||
* ``yaml.safe_load`` error -> ``yaml_error=<text>``, ``data={}``.
|
||||
* parsed value is not a mapping -> ``data={}`` (``has_block=True``).
|
||||
* valid mapping -> ``data=<dict>``.
|
||||
"""
|
||||
try:
|
||||
if not isinstance(content, str) or not content.startswith("---"):
|
||||
return FrontmatterParse()
|
||||
|
||||
parts = content.split("---", 2)
|
||||
if len(parts) < 3:
|
||||
# Unterminated frontmatter block.
|
||||
return FrontmatterParse(has_block=True, malformed=True)
|
||||
|
||||
try:
|
||||
import yaml
|
||||
loaded = yaml.safe_load(parts[1])
|
||||
except Exception as e: # yaml.YAMLError + anything pyyaml may surface
|
||||
logger.debug(f"parse_frontmatter: yaml parse failed: {e}")
|
||||
return FrontmatterParse(has_block=True, yaml_error=str(e))
|
||||
|
||||
if not isinstance(loaded, dict):
|
||||
return FrontmatterParse(has_block=True)
|
||||
return FrontmatterParse(data=loaded, has_block=True)
|
||||
except Exception as e: # noqa: BLE001 - never-raise contract
|
||||
logger.debug(f"parse_frontmatter: unexpected error: {e}")
|
||||
return FrontmatterParse()
|
||||
|
||||
|
||||
def parse_frontmatter_dict(content: str) -> dict:
|
||||
"""Shortcut: the parsed mapping of ``content``'s frontmatter. Never raises -> ``{}``."""
|
||||
return parse_frontmatter(content).data
|
||||
|
||||
|
||||
def read_frontmatter(path: str) -> dict:
|
||||
"""Read ``path`` and return its parsed frontmatter mapping. Never raises -> ``{}``."""
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
||||
content = f.read()
|
||||
except OSError as e:
|
||||
logger.debug(f"read_frontmatter: cannot open {path}: {e}")
|
||||
return {}
|
||||
return parse_frontmatter(content).data
|
||||
|
||||
|
||||
def read_frontmatter_value(path: str, key: str) -> str | None:
|
||||
"""Return the value of `key` from the leading YAML frontmatter of `path`.
|
||||
|
||||
@@ -42,34 +159,158 @@ def read_frontmatter_value(path: str, key: str) -> str | None:
|
||||
|
||||
The returned value is stringified and stripped (whitespace removed); casing
|
||||
is preserved so the caller decides whether to upper/lower for matching.
|
||||
|
||||
ORCH-52c: reimplemented on top of the unified ``read_frontmatter`` primitive.
|
||||
The external contract (``str | None``, never-raise, strip, case preserved) is
|
||||
UNCHANGED — external callers (``usage.py``, ``notifications``) are unaffected
|
||||
(INV-3 / FR-3).
|
||||
"""
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
||||
content = f.read()
|
||||
except OSError as e:
|
||||
logger.debug(f"read_frontmatter_value: cannot open {path}: {e}")
|
||||
return None
|
||||
|
||||
if not content.startswith("---"):
|
||||
return None
|
||||
|
||||
parts = content.split("---", 2)
|
||||
if len(parts) < 3:
|
||||
# Unterminated frontmatter.
|
||||
return None
|
||||
|
||||
try:
|
||||
import yaml
|
||||
fm = yaml.safe_load(parts[1]) or {}
|
||||
except Exception as e: # yaml.YAMLError + anything pyyaml may surface
|
||||
logger.debug(f"read_frontmatter_value: yaml parse failed for {path}: {e}")
|
||||
return None
|
||||
|
||||
if not isinstance(fm, dict):
|
||||
return None
|
||||
|
||||
fm = read_frontmatter(path)
|
||||
raw = fm.get(key)
|
||||
if raw is None:
|
||||
return None
|
||||
value = str(raw).strip()
|
||||
return value or None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Body helper — replaces the duplicated review_parse._strip_frontmatter (D2)
|
||||
# ---------------------------------------------------------------------------
|
||||
def strip_frontmatter(content: str) -> str:
|
||||
"""Return ``content`` with a leading ``--- … ---`` YAML block removed, if present.
|
||||
|
||||
Mirrors the previous ``review_parse._strip_frontmatter`` exactly: only a
|
||||
well-formed (>= 3 ``---``-split segments) leading block is stripped; otherwise
|
||||
the input is returned unchanged. Never raises -> the input text.
|
||||
"""
|
||||
try:
|
||||
if isinstance(content, str) and content.startswith("---"):
|
||||
parts = content.split("---", 2)
|
||||
if len(parts) >= 3:
|
||||
return parts[2]
|
||||
except Exception as e: # noqa: BLE001 - never-raise contract
|
||||
logger.debug(f"strip_frontmatter: unexpected error: {e}")
|
||||
return content
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Writer — canonical render/persist (FR-1 / D1)
|
||||
# ---------------------------------------------------------------------------
|
||||
def render_frontmatter(data: Mapping[str, object], body: str = "") -> str:
|
||||
"""Render ``data`` as a canonical leading YAML-frontmatter block + ``body``.
|
||||
|
||||
Output shape: ``"---\\n<yaml>\\n---\\n<body>"``. The YAML is emitted with
|
||||
``yaml.safe_dump`` (block style, keys unsorted) so it is byte-compatible with
|
||||
the existing readers (``split("---", 2)`` + ``yaml.safe_load``): a round-trip
|
||||
``render_frontmatter`` -> ``parse_frontmatter`` returns the same mapping.
|
||||
|
||||
never-raise (NFR-2): a serialization error is logged and the function degrades
|
||||
to returning ``body`` unchanged (a document with no frontmatter is read by the
|
||||
gates exactly as "no machine verdict", never an exception).
|
||||
"""
|
||||
try:
|
||||
import yaml
|
||||
dumped = yaml.safe_dump(
|
||||
dict(data or {}), default_flow_style=False, sort_keys=False, allow_unicode=True
|
||||
).strip("\n")
|
||||
return f"---\n{dumped}\n---\n{body}"
|
||||
except Exception as e: # noqa: BLE001 - never-raise contract
|
||||
logger.warning(f"render_frontmatter: serialization failed: {e}")
|
||||
return body
|
||||
|
||||
|
||||
def write_frontmatter(path: str, data: Mapping[str, object], body: str = "") -> bool:
|
||||
"""Persist ``render_frontmatter(data, body)`` to ``path``. Returns True on success.
|
||||
|
||||
never-raise (NFR-2): any I/O / serialization error is logged and returns
|
||||
``False`` (the caller decides how to degrade); an exception never escapes.
|
||||
"""
|
||||
try:
|
||||
content = render_frontmatter(data, body)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
return True
|
||||
except Exception as e: # noqa: BLE001 - never-raise contract
|
||||
logger.warning(f"write_frontmatter: cannot write {path}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Schema validator — warning-only by default; strict reserved (FR-2 / D3)
|
||||
# ---------------------------------------------------------------------------
|
||||
@dataclass(frozen=True)
|
||||
class SchemaValidation:
|
||||
"""Outcome of :func:`validate_schema`.
|
||||
|
||||
valid: all required fields are present and non-empty.
|
||||
missing: the required fields that are absent / None / blank (order = REQUIRED_FIELDS).
|
||||
"""
|
||||
|
||||
valid: bool
|
||||
missing: list
|
||||
|
||||
|
||||
def validate_schema(data: Mapping, *, required=REQUIRED_FIELDS) -> SchemaValidation:
|
||||
"""Validate that ``data`` carries every required schema field, non-empty.
|
||||
|
||||
Pure library function (INV-4). A field counts as MISSING when it is absent, or
|
||||
its value is ``None`` or — after ``str(...).strip()`` — empty. Returns a
|
||||
:class:`SchemaValidation`; never raises (a non-mapping input -> all fields
|
||||
missing -> ``valid=False``). This function NEVER influences a gate verdict by
|
||||
itself — see :func:`maybe_warn_schema` and the ``frontmatter_validation_strict``
|
||||
flag for how strict enforcement is (and is not) wired.
|
||||
"""
|
||||
missing: list = []
|
||||
try:
|
||||
mapping = data if isinstance(data, Mapping) else {}
|
||||
for fld in required:
|
||||
raw = mapping.get(fld)
|
||||
if raw is None or str(raw).strip() == "":
|
||||
missing.append(fld)
|
||||
except Exception as e: # noqa: BLE001 - never-raise contract
|
||||
logger.warning(f"validate_schema: unexpected error: {e}")
|
||||
# Conservatively report everything missing rather than raise.
|
||||
missing = list(required)
|
||||
return SchemaValidation(valid=not missing, missing=missing)
|
||||
|
||||
|
||||
def maybe_warn_schema(content: str, doc_label: str = "document") -> SchemaValidation:
|
||||
"""Best-effort schema check used at verdict-read sites — warning-only by default.
|
||||
|
||||
Parses ``content``'s frontmatter and validates it against :data:`REQUIRED_FIELDS`.
|
||||
Behaviour is governed by the ``frontmatter_validation_strict`` kill-switch
|
||||
(default ``False``):
|
||||
|
||||
* **default (False)** — when fields are missing, emit a single
|
||||
``logger.warning("frontmatter schema incomplete: missing …")`` and return.
|
||||
The result is **inert**: callers that pass it through a gate must NOT change
|
||||
their ``tuple[bool, str]`` verdict (FR-2 "warning/лог, не blocker"). This
|
||||
keeps a machine-verdict doc that lacks the (forward-looking, additive) schema
|
||||
readable exactly as before (FR-5 / AC-4) — critical so ORCH-52c does not
|
||||
self-block its own deploy (its docs predate the schema).
|
||||
* **strict (True)** — RESERVED for a future tightening (ORCH-52d+). The
|
||||
validation result is returned the same way; the flag merely documents intent
|
||||
and lets a future caller veto. It stays ``False`` in prod and ``.env.staging``.
|
||||
|
||||
Never raises (NFR-2): a config-read or parse error degrades to ``valid=True``
|
||||
(no false warning, no influence on the verdict).
|
||||
"""
|
||||
try:
|
||||
data = parse_frontmatter(content).data
|
||||
result = validate_schema(data)
|
||||
if not result.valid:
|
||||
try:
|
||||
from .config import settings
|
||||
strict = bool(getattr(settings, "frontmatter_validation_strict", False))
|
||||
except Exception: # noqa: BLE001 - config read must never raise here
|
||||
strict = False
|
||||
logger.warning(
|
||||
"frontmatter schema incomplete in %s: missing %s%s",
|
||||
doc_label,
|
||||
", ".join(result.missing),
|
||||
" [strict]" if strict else "",
|
||||
)
|
||||
return result
|
||||
except Exception as e: # noqa: BLE001 - never-raise; inert on error
|
||||
logger.debug(f"maybe_warn_schema: unexpected error for {doc_label}: {e}")
|
||||
return SchemaValidation(valid=True, missing=[])
|
||||
|
||||
133
src/labels.py
Normal file
133
src/labels.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""ORCH-089: auto-mode by Plane labels — autoApprove + autoDeploy (pure logic).
|
||||
|
||||
Leaf module — pure, unit-testable logic over the config flags + the Plane label
|
||||
helpers in ``plane_sync``. Mirrors the leaf pattern of ``src/serial_gate.py`` /
|
||||
``src/self_deploy.py``: imports only ``config`` (and lazily ``plane_sync`` /
|
||||
``qg.checks`` / ``projects``), never ``stage_engine`` / ``launcher``.
|
||||
|
||||
What it decides (ADR-001 D1):
|
||||
* Whether the auto-mode is in scope for a repo (``auto_approve_applies`` /
|
||||
``auto_deploy_applies``) — a LOCAL, network-free check evaluated FIRST.
|
||||
* Whether a given Plane label is present on an issue (``has_label``) — the only
|
||||
network call, made ONLY after ``applies()`` is True, so a disabled kill-switch
|
||||
costs zero network and yields zero regression (AC-8).
|
||||
|
||||
never-raise contract (BR-6/AC-6, fail-safe to the MANUAL gate): every public
|
||||
function degrades to "no auto" on ANY error / ambiguity / Plane unavailability.
|
||||
There is NO fail-open here — the conservative default is always "no auto"
|
||||
(human gate stays), so an error can never auto-pass a gate.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from .config import settings
|
||||
|
||||
logger = logging.getLogger("orchestrator.labels")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scope / kill-switch (mirrors self_deploy_applies / serial_gate_applies)
|
||||
# ---------------------------------------------------------------------------
|
||||
def _auto_label_applies(repo: str) -> bool:
|
||||
"""Shared scope check for both auto-modes (ADR-001 D5).
|
||||
|
||||
* ``auto_label_enabled=False`` -> always False (kill-switch; both gates 1:1
|
||||
as before ORCH-089, and — crucially — ``has_label`` is never consulted, so
|
||||
no new network call on the gate, AC-8).
|
||||
* ``auto_label_repos`` (CSV) non-empty -> real only for the listed repos.
|
||||
* empty CSV -> self-hosting only (``orchestrator``) — the safe default
|
||||
(the autoDeploy insertion lives in Phase A, which only exists for the
|
||||
self-hosting repo).
|
||||
Never raises -> False on error (degrade to "no auto" = manual gate).
|
||||
"""
|
||||
try:
|
||||
if not getattr(settings, "auto_label_enabled", False):
|
||||
return False
|
||||
raw = (getattr(settings, "auto_label_repos", "") or "").strip()
|
||||
if raw:
|
||||
allowed = {r.strip().lower() for r in raw.split(",") if r.strip()}
|
||||
return (repo or "").strip().lower() in allowed
|
||||
# Lazy import keeps this module a leaf (avoids importing qg at load).
|
||||
from .qg.checks import is_self_hosting_repo
|
||||
return is_self_hosting_repo(repo)
|
||||
except Exception as e: # noqa: BLE001 - never-raise
|
||||
logger.warning("_auto_label_applies error for %s: %s", repo, e)
|
||||
return False
|
||||
|
||||
|
||||
def auto_approve_applies(repo: str) -> bool:
|
||||
"""Whether the autoApprove (BRD gate) auto-mode is in scope for ``repo``."""
|
||||
return _auto_label_applies(repo)
|
||||
|
||||
|
||||
def auto_deploy_applies(repo: str) -> bool:
|
||||
"""Whether the autoDeploy (prod-deploy gate) auto-mode is in scope for ``repo``."""
|
||||
return _auto_label_applies(repo)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Label presence (the ONLY network call; ADR-001 D1)
|
||||
# ---------------------------------------------------------------------------
|
||||
def has_label(work_item_id: str, label_name: str, project_id: str | None = None) -> bool:
|
||||
"""True iff the issue carries a label whose name == ``label_name`` (normalized).
|
||||
|
||||
Resolution (all inside one ``try/except -> False``):
|
||||
1. ``plane_sync.fetch_issue_labels`` — the issue's label uuids (None on error
|
||||
-> False);
|
||||
2. ``plane_sync.get_project_labels`` — {normalized_name -> uuid} project map
|
||||
(TTL-cached);
|
||||
3. normalize the sought name and look it up in the project map;
|
||||
4. no match, OR an ambiguous name (the project map maps it to the
|
||||
``__AMBIGUOUS__`` sentinel) -> False (fail-safe);
|
||||
5. ``return target_uuid in set(labels)``.
|
||||
|
||||
Any error / unavailability / ambiguity -> **False** (never auto on doubt).
|
||||
"""
|
||||
if not label_name:
|
||||
return False
|
||||
try:
|
||||
from . import plane_sync
|
||||
labels = plane_sync.fetch_issue_labels(work_item_id, project_id)
|
||||
if labels is None:
|
||||
# Could not read the issue's labels -> fail-safe to manual.
|
||||
return False
|
||||
if not labels:
|
||||
return False
|
||||
name_map = plane_sync.get_project_labels(
|
||||
plane_sync._resolve_project_id(work_item_id, project_id)
|
||||
)
|
||||
if not name_map:
|
||||
return False
|
||||
normalized = plane_sync._normalize_label(label_name)
|
||||
target_uuid = name_map.get(normalized)
|
||||
if not target_uuid or target_uuid == "__AMBIGUOUS__":
|
||||
return False
|
||||
return target_uuid in set(labels)
|
||||
except Exception as e: # noqa: BLE001 - never-raise -> no auto
|
||||
logger.warning(
|
||||
"has_label error for %s/%s -> fail-safe (no auto): %s",
|
||||
work_item_id, label_name, e,
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Observability snapshot for GET /queue (ADR-001 D7)
|
||||
# ---------------------------------------------------------------------------
|
||||
def snapshot() -> dict:
|
||||
"""Read-only auto-label summary for GET /queue (additive block). never-raise."""
|
||||
try:
|
||||
enabled = bool(getattr(settings, "auto_label_enabled", False))
|
||||
except Exception: # noqa: BLE001
|
||||
enabled = False
|
||||
try:
|
||||
return {
|
||||
"enabled": enabled,
|
||||
"approve_label": getattr(settings, "auto_approve_label", ""),
|
||||
"deploy_label": getattr(settings, "auto_deploy_label", ""),
|
||||
"repos": getattr(settings, "auto_label_repos", "") or "",
|
||||
}
|
||||
except Exception as e: # noqa: BLE001 - never-raise -> minimal dict
|
||||
logger.warning("labels snapshot error: %s", e)
|
||||
return {"enabled": enabled, "approve_label": "", "deploy_label": "", "repos": ""}
|
||||
@@ -150,6 +150,7 @@ async def queue():
|
||||
from . import merge_gate
|
||||
from . import task_deps
|
||||
from . import serial_gate
|
||||
from . import labels
|
||||
return {
|
||||
"counts": job_status_counts(),
|
||||
"max_concurrency": worker.max_concurrency,
|
||||
@@ -165,6 +166,9 @@ async def queue():
|
||||
# ORCH-088 (D9 / AC-10): per-repo serial-gate observability (read-only) —
|
||||
# active task, queued/waiting analyst-jobs, freeze state. Additive block.
|
||||
"serial_gate": serial_gate.snapshot(),
|
||||
# ORCH-089 (D7): auto-mode-by-label observability (read-only) — kill-switch,
|
||||
# label names, scope. Additive block.
|
||||
"auto_labels": labels.snapshot(),
|
||||
"recent": recent_jobs(10),
|
||||
}
|
||||
|
||||
|
||||
@@ -326,6 +326,160 @@ def reload_project_states(project_id: str = None) -> None:
|
||||
logger.debug(f"reload_project_states: evicted project {project_id[:8]}...")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ORCH-089: label reading (auto-mode by Plane labels) + Approved setter.
|
||||
#
|
||||
# Source of truth for an issue's labels is the Plane API, NOT the webhook payload
|
||||
# (both auto-mode insertion points are launcher-path events where the payload is
|
||||
# absent; src/webhooks/plane.py does not carry `labels`). All three helpers honour
|
||||
# a never-raise contract: a failure degrades to "no label" / "no-op", so the
|
||||
# auto-mode falls back to the manual gate (fail-safe, BR-6/AC-6).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Per-project label-map cache (mirrors _STATES_CACHE / ORCH-068 TTL self-heal).
|
||||
# Each entry: {"map": {normalized_name -> uuid}, "ts": monotonic timestamp}.
|
||||
_LABELS_CACHE: dict[str, dict] = {}
|
||||
|
||||
|
||||
def _normalize_label(name: str) -> str:
|
||||
"""Normalize a label name for matching (case/whitespace-insensitive)."""
|
||||
return (name or "").strip().casefold()
|
||||
|
||||
|
||||
def _labels_record_fresh(record: dict) -> bool:
|
||||
"""ORCH-089: is a label-map cache record still within its TTL?
|
||||
|
||||
``auto_label_states_ttl_s <= 0`` disables the TTL (lifetime cache, escape
|
||||
hatch mirroring ``_cache_record_fresh`` / ``plane_states_ttl_s``).
|
||||
"""
|
||||
try:
|
||||
ttl = settings.auto_label_states_ttl_s
|
||||
except Exception: # noqa: BLE001
|
||||
ttl = 0
|
||||
if ttl <= 0:
|
||||
return True
|
||||
ts = record.get("ts", 0.0)
|
||||
return (time.monotonic() - ts) <= ttl
|
||||
|
||||
|
||||
def reload_project_labels(project_id: str = None) -> None:
|
||||
"""ORCH-089: clear the per-project label-map cache (tests / config reload)."""
|
||||
global _LABELS_CACHE
|
||||
if project_id is None:
|
||||
_LABELS_CACHE = {}
|
||||
else:
|
||||
_LABELS_CACHE.pop(project_id, None)
|
||||
|
||||
|
||||
def get_project_labels(project_id: str) -> dict[str, str]:
|
||||
"""ORCH-089: resolve {normalized_label_name -> uuid} for a Plane project.
|
||||
|
||||
Source of truth: GET /projects/<pid>/labels/. Cached per project_id with a
|
||||
TTL (``auto_label_states_ttl_s``, default 300s) mirroring
|
||||
``get_project_states`` so we do not hit the API on every gate. On a transient
|
||||
API failure a stale-but-correct cached map is served (safer-than-empty); with
|
||||
nothing cached -> ``{}`` (caller resolves to "no label" -> manual gate).
|
||||
|
||||
Ambiguity guard (D1.4): if two distinct project labels normalise to the SAME
|
||||
name, that name is mapped to a sentinel so ``has_label`` treats it as "no
|
||||
match" (fail-safe) instead of silently picking one uuid. never-raise -> ``{}``.
|
||||
"""
|
||||
if not project_id:
|
||||
return {}
|
||||
|
||||
cached = _LABELS_CACHE.get(project_id)
|
||||
if cached is not None and _labels_record_fresh(cached):
|
||||
return cached["map"]
|
||||
|
||||
url = f"{PLANE_BASE}/workspaces/{WORKSPACE}/projects/{project_id}/labels/"
|
||||
try:
|
||||
resp = httpx.get(url, headers=PLANE_HEADERS, timeout=10)
|
||||
resp.raise_for_status()
|
||||
body = resp.json()
|
||||
items = body.get("results", body) if isinstance(body, dict) else body
|
||||
if not isinstance(items, list):
|
||||
raise ValueError(f"unexpected labels response shape: {type(items)}")
|
||||
|
||||
name_map: dict[str, str] = {}
|
||||
ambiguous: set[str] = set()
|
||||
for item in items:
|
||||
uid = item.get("id", "")
|
||||
norm = _normalize_label(item.get("name", ""))
|
||||
if not (uid and norm):
|
||||
continue
|
||||
if norm in name_map and name_map[norm] != uid:
|
||||
# Two distinct labels collide on the normalized name -> ambiguous.
|
||||
ambiguous.add(norm)
|
||||
name_map[norm] = uid
|
||||
for norm in ambiguous:
|
||||
# AMBIGUOUS sentinel: never equals a real issue-label uuid, so
|
||||
# has_label's membership test is False -> fail-safe to the manual gate.
|
||||
name_map[norm] = "__AMBIGUOUS__"
|
||||
logger.warning(
|
||||
"get_project_labels: ambiguous label name %r in project %s "
|
||||
"-> treated as no-match (fail-safe)", norm, project_id[:8],
|
||||
)
|
||||
|
||||
_LABELS_CACHE[project_id] = {"map": name_map, "ts": time.monotonic()}
|
||||
logger.debug(
|
||||
"get_project_labels: cached %d labels for project %s...",
|
||||
len(name_map), project_id[:8],
|
||||
)
|
||||
return name_map
|
||||
except Exception as e: # noqa: BLE001 - never-raise
|
||||
if cached is not None:
|
||||
logger.warning(
|
||||
"get_project_labels: API refresh failed for project %s..., "
|
||||
"serving stale cached map. Error: %s", project_id[:8], e,
|
||||
)
|
||||
return cached["map"]
|
||||
logger.warning(
|
||||
"get_project_labels: API failed for project %s..., no cache -> {}. "
|
||||
"Error: %s", project_id[:8], e,
|
||||
)
|
||||
return {}
|
||||
|
||||
|
||||
def fetch_issue_labels(work_item_id: str, project_id: str = None) -> list[str] | None:
|
||||
"""ORCH-089: GET the issue and return its ``labels`` (a list of label uuids).
|
||||
|
||||
Returns ``None`` on any error / issue-not-found (DISTINCT from ``[]`` = "the
|
||||
issue has no labels") so the caller can distinguish "could not read" (fail-safe
|
||||
to manual) from "definitely no labels". never-raise.
|
||||
"""
|
||||
project_id = _resolve_project_id(work_item_id, project_id)
|
||||
issue_id = find_issue_id(work_item_id, project_id)
|
||||
if not issue_id:
|
||||
logger.debug("fetch_issue_labels: issue not found for %s", work_item_id)
|
||||
return None
|
||||
url = f"{PLANE_BASE}/workspaces/{WORKSPACE}/projects/{project_id}/issues/{issue_id}/"
|
||||
try:
|
||||
resp = httpx.get(url, headers=PLANE_HEADERS, timeout=10)
|
||||
resp.raise_for_status()
|
||||
labels = resp.json().get("labels", [])
|
||||
if not isinstance(labels, list):
|
||||
return None
|
||||
return [str(x) for x in labels]
|
||||
except Exception as e: # noqa: BLE001 - never-raise
|
||||
logger.warning("fetch_issue_labels failed for %s: %s", work_item_id, e)
|
||||
return None
|
||||
|
||||
|
||||
def set_issue_approved(work_item_id: str, project_id: str = None):
|
||||
"""ORCH-089: set issue to 'Approved' — indication of an auto-approved BRD.
|
||||
|
||||
1:1 mirror of ``set_issue_in_review``: resolve the per-project Approved UUID
|
||||
(``get_project_states(pid)["approved"]`` — the key already exists in
|
||||
``_DEFAULT_STATES`` / ``_PLANE_NAME_TO_KEY``) and PATCH the issue. never-raise
|
||||
(via ``_set_issue_state_direct``). The status is transient — the immediately
|
||||
following advance to ``architecture`` overrides it; durable transparency is
|
||||
carried by the log + Telegram + Plane comment (AC-7).
|
||||
"""
|
||||
project_id = _resolve_project_id(work_item_id, project_id)
|
||||
state_id = get_project_states(project_id)["approved"]
|
||||
_set_issue_state_direct(work_item_id, state_id, project_id)
|
||||
|
||||
|
||||
# Feature 3: map an orchestrator stage -> the Plane status to show on the board
|
||||
# when the pipeline ENTERS that stage. ORCH-066: analysis -> Analysis and
|
||||
# review -> Code-Review now have dedicated statuses. deploy keeps in_progress
|
||||
|
||||
@@ -239,22 +239,26 @@ def _parse_tests_verdict(content: str) -> tuple[bool, str]:
|
||||
beats a positive token in another field).
|
||||
- Otherwise a positive token (PASS/PASSED/READY-TO-DEPLOY/...) in ANY field -> (True).
|
||||
- Anything else (fields set but unrecognized) -> (False, reason).
|
||||
|
||||
ORCH-52c: the YAML-frontmatter parse is now delegated to the unified
|
||||
``frontmatter.parse_frontmatter`` primitive (single source of parse logic); the
|
||||
token-logic, upper-casing, three-field set and negative-token priority are
|
||||
UNCHANGED (semantics 1:1, AC-3/AC-6). Reason-strings are reproduced from the
|
||||
structured parse states.
|
||||
"""
|
||||
import yaml
|
||||
from ..frontmatter import parse_frontmatter, maybe_warn_schema
|
||||
|
||||
if not content.startswith("---"):
|
||||
parse = parse_frontmatter(content)
|
||||
if not parse.has_block:
|
||||
return False, "No YAML frontmatter in test report (cannot read machine verdict)"
|
||||
|
||||
parts = content.split("---", 2)
|
||||
if len(parts) < 3:
|
||||
if parse.malformed:
|
||||
return False, "Malformed YAML frontmatter in test report"
|
||||
|
||||
try:
|
||||
fm = yaml.safe_load(parts[1]) or {}
|
||||
except yaml.YAMLError as e:
|
||||
return False, f"Invalid YAML frontmatter in test report: {e}"
|
||||
if not isinstance(fm, dict):
|
||||
return False, "Malformed YAML frontmatter in test report (not a mapping)"
|
||||
if parse.yaml_error is not None:
|
||||
return False, f"Invalid YAML frontmatter in test report: {parse.yaml_error}"
|
||||
fm = parse.data
|
||||
# Warning-only schema check (FR-2/D3): inert — never changes the verdict.
|
||||
if fm:
|
||||
maybe_warn_schema(content, "test report")
|
||||
|
||||
verdict = str(fm.get("verdict", "") or "").upper().strip()
|
||||
status = str(fm.get("status", "") or "").upper().strip()
|
||||
@@ -338,8 +342,12 @@ def check_reviewer_verdict(repo: str, work_item_id: str, branch: str | None = No
|
||||
cause false positives/negatives. Returns:
|
||||
(True, ...) -> verdict: APPROVED
|
||||
(False, ...) -> verdict: REQUEST_CHANGES, missing verdict, or no frontmatter
|
||||
|
||||
ORCH-52c: the YAML-frontmatter parse is delegated to the unified
|
||||
``frontmatter.parse_frontmatter`` primitive; the verdict semantics
|
||||
(APPROVED/REQUEST_CHANGES) are UNCHANGED (1:1, AC-3/AC-6).
|
||||
"""
|
||||
import yaml
|
||||
from ..frontmatter import parse_frontmatter, maybe_warn_schema
|
||||
repo_path = _repo_path(repo, branch)
|
||||
review_path = os.path.join(repo_path, f"docs/work-items/{work_item_id}/12-review.md")
|
||||
|
||||
@@ -350,15 +358,14 @@ def check_reviewer_verdict(repo: str, work_item_id: str, branch: str | None = No
|
||||
with open(review_path, "r") as f:
|
||||
content = f.read()
|
||||
|
||||
parse = parse_frontmatter(content)
|
||||
if parse.yaml_error is not None:
|
||||
return False, f"Invalid YAML frontmatter in review: {parse.yaml_error}"
|
||||
verdict = None
|
||||
if content.startswith("---"):
|
||||
parts = content.split("---", 2)
|
||||
if len(parts) >= 3:
|
||||
try:
|
||||
fm = yaml.safe_load(parts[1]) or {}
|
||||
except yaml.YAMLError as e:
|
||||
return False, f"Invalid YAML frontmatter in review: {e}"
|
||||
verdict = str(fm.get("verdict", "")).upper().strip()
|
||||
if parse.has_block and not parse.malformed:
|
||||
if parse.data:
|
||||
maybe_warn_schema(content, "review report")
|
||||
verdict = str(parse.data.get("verdict", "")).upper().strip()
|
||||
|
||||
if verdict == "APPROVED":
|
||||
return True, "Reviewer verdict: APPROVED"
|
||||
@@ -410,17 +417,19 @@ def _parse_deploy_status(content: str) -> tuple[bool, str]:
|
||||
deploy_status: SUCCESS -> (True, "Deploy status: SUCCESS")
|
||||
deploy_status: FAILED -> (False, "Deploy status: FAILED")
|
||||
missing field / no frontmatter / bad YAML -> (False, <reason>)
|
||||
|
||||
ORCH-52c: parse delegated to the unified ``frontmatter.parse_frontmatter``;
|
||||
the deploy_status semantics (БАГ-8) are UNCHANGED (1:1).
|
||||
"""
|
||||
import yaml
|
||||
from ..frontmatter import parse_frontmatter, maybe_warn_schema
|
||||
parse = parse_frontmatter(content)
|
||||
if parse.yaml_error is not None:
|
||||
return False, f"Invalid YAML frontmatter in deploy log: {parse.yaml_error}"
|
||||
status = None
|
||||
if content.startswith("---"):
|
||||
parts = content.split("---", 2)
|
||||
if len(parts) >= 3:
|
||||
try:
|
||||
fm = yaml.safe_load(parts[1]) or {}
|
||||
except yaml.YAMLError as e:
|
||||
return False, f"Invalid YAML frontmatter in deploy log: {e}"
|
||||
status = str(fm.get("deploy_status", "")).upper().strip()
|
||||
if parse.has_block and not parse.malformed:
|
||||
if parse.data:
|
||||
maybe_warn_schema(content, "deploy log")
|
||||
status = str(parse.data.get("deploy_status", "")).upper().strip()
|
||||
if status == "SUCCESS":
|
||||
return True, "Deploy status: SUCCESS"
|
||||
if status == "FAILED":
|
||||
@@ -525,17 +534,19 @@ def _parse_staging_status(content: str) -> tuple[bool, str]:
|
||||
staging_status: SUCCESS -> (True, "Staging status: SUCCESS")
|
||||
staging_status: FAILED -> (False, "Staging status: FAILED")
|
||||
missing field / no frontmatter / bad YAML -> (False, <reason>)
|
||||
|
||||
ORCH-52c: parse delegated to the unified ``frontmatter.parse_frontmatter``;
|
||||
the staging_status semantics (self-hosting) are UNCHANGED (1:1).
|
||||
"""
|
||||
import yaml
|
||||
from ..frontmatter import parse_frontmatter, maybe_warn_schema
|
||||
parse = parse_frontmatter(content)
|
||||
if parse.yaml_error is not None:
|
||||
return False, f"Invalid YAML frontmatter in staging log: {parse.yaml_error}"
|
||||
status = None
|
||||
if content.startswith("---"):
|
||||
parts = content.split("---", 2)
|
||||
if len(parts) >= 3:
|
||||
try:
|
||||
fm = yaml.safe_load(parts[1]) or {}
|
||||
except yaml.YAMLError as e:
|
||||
return False, f"Invalid YAML frontmatter in staging log: {e}"
|
||||
status = str(fm.get("staging_status", "")).upper().strip()
|
||||
if parse.has_block and not parse.malformed:
|
||||
if parse.data:
|
||||
maybe_warn_schema(content, "staging log")
|
||||
status = str(parse.data.get("staging_status", "")).upper().strip()
|
||||
if status == "SUCCESS":
|
||||
return True, "Staging status: SUCCESS"
|
||||
if status == "FAILED":
|
||||
|
||||
@@ -44,12 +44,15 @@ def _read(path: str) -> str | None:
|
||||
|
||||
|
||||
def _strip_frontmatter(content: str) -> str:
|
||||
"""Drop a leading ``--- … ---`` YAML frontmatter block, if present."""
|
||||
if content.startswith("---"):
|
||||
parts = content.split("---", 2)
|
||||
if len(parts) >= 3:
|
||||
return parts[2]
|
||||
return content
|
||||
"""Drop a leading ``--- … ---`` YAML frontmatter block, if present.
|
||||
|
||||
ORCH-52c: delegates to the unified ``frontmatter.strip_frontmatter`` helper
|
||||
(single source of frontmatter logic). Behaviour is identical (only a well-formed
|
||||
>= 3-segment leading block is stripped) and the never-raise -> input contract is
|
||||
preserved.
|
||||
"""
|
||||
from .frontmatter import strip_frontmatter
|
||||
return strip_frontmatter(content)
|
||||
|
||||
|
||||
def _truncate(text: str, limit: int) -> str:
|
||||
|
||||
@@ -559,19 +559,19 @@ def parse_security_status(content: str) -> tuple[bool, str]:
|
||||
* ``security_status: FAIL`` -> ``(False, "Security status: FAIL")``
|
||||
* missing field / no frontmatter / bad YAML -> ``(False, <reason>)`` (fail-closed
|
||||
on the verdict read, AC-9).
|
||||
"""
|
||||
import yaml
|
||||
|
||||
ORCH-52c: parse delegated to the unified ``frontmatter.parse_frontmatter``
|
||||
primitive (single source of YAML-frontmatter logic); the security_status
|
||||
semantics (FAIL authoritative) are UNCHANGED (1:1).
|
||||
"""
|
||||
from .frontmatter import parse_frontmatter
|
||||
|
||||
parse = parse_frontmatter(content)
|
||||
if parse.yaml_error is not None:
|
||||
return False, f"Invalid YAML frontmatter in security report: {parse.yaml_error}"
|
||||
status = None
|
||||
if content.startswith("---"):
|
||||
parts = content.split("---", 2)
|
||||
if len(parts) >= 3:
|
||||
try:
|
||||
fm = yaml.safe_load(parts[1]) or {}
|
||||
except yaml.YAMLError as e:
|
||||
return False, f"Invalid YAML frontmatter in security report: {e}"
|
||||
if isinstance(fm, dict):
|
||||
status = str(fm.get("security_status", "")).upper().strip()
|
||||
if parse.has_block and not parse.malformed:
|
||||
status = str(parse.data.get("security_status", "")).upper().strip()
|
||||
if status == "FAIL":
|
||||
return False, "Security status: FAIL"
|
||||
if status == "PASS":
|
||||
@@ -593,11 +593,9 @@ def extract_security_findings(report_path: str) -> str:
|
||||
return ""
|
||||
with open(report_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
# Drop the frontmatter; keep the human body.
|
||||
if content.startswith("---"):
|
||||
parts = content.split("---", 2)
|
||||
if len(parts) >= 3:
|
||||
content = parts[2]
|
||||
# Drop the frontmatter; keep the human body (ORCH-52c: shared helper).
|
||||
from .frontmatter import strip_frontmatter
|
||||
content = strip_frontmatter(content)
|
||||
wanted = ("## Verdict", "## Secrets", "## Dependencies (blocking)")
|
||||
lines = content.splitlines()
|
||||
out = []
|
||||
|
||||
@@ -39,6 +39,7 @@ from .qg.checks import QG_CHECKS
|
||||
from . import merge_gate
|
||||
from . import self_deploy
|
||||
from . import post_deploy
|
||||
from . import labels
|
||||
from .notifications import (
|
||||
notify_stage_change,
|
||||
notify_qg_failure,
|
||||
@@ -59,6 +60,7 @@ from .plane_sync import (
|
||||
set_issue_awaiting_deploy,
|
||||
set_issue_deploying,
|
||||
set_issue_monitoring,
|
||||
set_issue_approved,
|
||||
)
|
||||
from .config import settings
|
||||
|
||||
@@ -596,6 +598,47 @@ def _handle_analysis_approved_flow(
|
||||
logger.info(
|
||||
f"Task {task_id}: analyst finished, requested Approved status in Plane"
|
||||
)
|
||||
|
||||
# --- ORCH-089 autoApprove: auto-pass the BRD human gate by label --------
|
||||
# After In Review + the analyst comment + the approve-request (kept for the
|
||||
# BRD-review clock, transparency and symmetry with the manual path), if the
|
||||
# issue carries the autoApprove label AND the repo is in scope, auto-advance
|
||||
# via the SAME path a human Approved takes — never duplicating the
|
||||
# transition logic. applies() (local, network-free) is checked FIRST so a
|
||||
# disabled kill-switch / out-of-scope repo costs zero network (AC-8); any
|
||||
# error / no-label -> fall through to the prior behaviour (return, wait for
|
||||
# a human, AC-4/AC-6).
|
||||
if labels.auto_approve_applies(repo) and labels.has_label(
|
||||
work_item_id, settings.auto_approve_label
|
||||
):
|
||||
set_issue_approved(work_item_id) # indication (AC-1), transient
|
||||
logger.info(
|
||||
f"Task {task_id}: label {settings.auto_approve_label} -> "
|
||||
f"BRD auto-approved (analysis -> architecture)"
|
||||
)
|
||||
plane_add_comment(
|
||||
work_item_id,
|
||||
f"✅ BRD авто-подтверждён (лейбл {settings.auto_approve_label}). "
|
||||
"Переход на architecture без ручного Approved.",
|
||||
author="analyst",
|
||||
)
|
||||
send_telegram(
|
||||
f"✅ {link_for(work_item_id)}: BRD авто-подтверждён "
|
||||
f"(лейбл {settings.auto_approve_label})."
|
||||
)
|
||||
# Same advance the human Approved webhook uses: finished_agent=None ->
|
||||
# check_analysis_approved approved-via-status -> advance analysis ->
|
||||
# architecture + mark_brd_review_ended (clock) + standard post-effects.
|
||||
# Re-entrancy is safe: the nested call passes finished_agent=None, so it
|
||||
# does NOT re-enter this analyst branch (which requires agent=='analyst').
|
||||
auto = advance_stage(
|
||||
task_id, current_stage, repo, work_item_id, branch, finished_agent=None
|
||||
)
|
||||
result.advanced = auto.advanced
|
||||
result.to_stage = auto.to_stage
|
||||
result.enqueued_agent = auto.enqueued_agent
|
||||
result.enqueued_job_id = auto.enqueued_job_id
|
||||
result.note = "auto-approved-via-label"
|
||||
return
|
||||
|
||||
questions_path = os.path.join(
|
||||
@@ -1179,6 +1222,40 @@ def _handle_self_deploy_phase_a(
|
||||
# (e.g. after a crash/manual intervention), so `initiated`/`result` from an
|
||||
# earlier attempt can never leak into this one.
|
||||
self_deploy.clear_state(repo, work_item_id)
|
||||
|
||||
# --- ORCH-089 autoDeploy: auto-confirm the prod-deploy human gate by label --
|
||||
# After advancing onto `deploy` + wiping stale markers and BEFORE the ask-human
|
||||
# block, if the issue carries the autoDeploy label AND the repo is in scope,
|
||||
# initiate Phase B via the SAME path a human Confirm Deploy takes. Only the
|
||||
# indicative human steps are skipped (APPROVE_REQUESTED marker +
|
||||
# set_issue_awaiting_deploy + the "flip to Confirm Deploy" comment/Telegram) —
|
||||
# status Deploying is set by Phase B itself. BR-5/AC-5 hold STRUCTURALLY: Phase A
|
||||
# is reached ONLY after the green edge sub-gates (security -> merge-gate ->
|
||||
# image-freshness -> staging), so autoDeploy cannot deploy a broken build.
|
||||
# Idempotency is the existing INITIATED marker inside _handle_self_deploy_phase_b.
|
||||
# applies() FIRST (network-free); any error / no-label -> the prior Phase A
|
||||
# ask-human flow (AC-4/AC-6).
|
||||
if labels.auto_deploy_applies(repo) and labels.has_label(
|
||||
work_item_id, settings.auto_deploy_label
|
||||
):
|
||||
logger.info(
|
||||
f"Task {task_id}: label {settings.auto_deploy_label} -> "
|
||||
f"prod deploy auto-confirmed (Phase B without manual Confirm Deploy)"
|
||||
)
|
||||
if work_item_id:
|
||||
plane_add_comment(
|
||||
work_item_id,
|
||||
f"🚀 Прод-деплой авто-подтверждён (лейбл {settings.auto_deploy_label}). "
|
||||
"Запуск Phase B без ручного «Confirm Deploy».",
|
||||
author="deployer",
|
||||
)
|
||||
send_telegram(
|
||||
f"🚀 {link_for(work_item_id)}: прод-деплой авто-подтверждён "
|
||||
f"(лейбл {settings.auto_deploy_label})."
|
||||
)
|
||||
_handle_self_deploy_phase_b(task_id, repo, work_item_id, branch, result)
|
||||
return
|
||||
|
||||
self_deploy.write_marker(
|
||||
repo, work_item_id, self_deploy.APPROVE_REQUESTED, content=str(time.time())
|
||||
)
|
||||
|
||||
190
tests/test_auto_approve_brd.py
Normal file
190
tests/test_auto_approve_brd.py
Normal file
@@ -0,0 +1,190 @@
|
||||
"""ORCH-089 — autoApprove врезка in _handle_analysis_approved_flow.
|
||||
|
||||
Covers (04-test-plan.yaml):
|
||||
TC-10 autoApprove + artifacts ready -> auto-advance analysis->architecture,
|
||||
Approved set, brd_review_ended clock closed.
|
||||
TC-11 no autoApprove label -> prior behaviour: In Review, return w/o advance.
|
||||
TC-12 autoApprove but artifacts missing (check_analysis_complete FAIL) -> NO
|
||||
advance (AC-5 for BRD).
|
||||
TC-13 autoApprove goes through the SAME advance path as a manual Approved (no
|
||||
duplicated transition logic; idempotent — stage lands on architecture).
|
||||
TC-14 autoApprove logged + Telegram + Plane comment (transparency AC-7).
|
||||
"""
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
_test_db = os.path.join(tempfile.gettempdir(), "test_auto_approve_brd.db")
|
||||
os.environ["ORCH_DB_PATH"] = _test_db
|
||||
os.environ["ORCH_REPOS_DIR"] = tempfile.gettempdir()
|
||||
os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token")
|
||||
os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token")
|
||||
|
||||
from unittest.mock import MagicMock # noqa: E402
|
||||
|
||||
import src.db as _db # noqa: E402
|
||||
from src.db import init_db, get_db # noqa: E402
|
||||
from src import stage_engine # noqa: E402
|
||||
from src import labels # noqa: E402
|
||||
from src.stage_engine import advance_stage # noqa: E402
|
||||
|
||||
|
||||
def _files_ok(*a, **k):
|
||||
return (True, "ok")
|
||||
|
||||
|
||||
def _files_fail(*a, **k):
|
||||
return (False, "missing artifacts")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def fresh(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(_db.settings, "db_path", _test_db)
|
||||
if os.path.exists(_test_db):
|
||||
os.unlink(_test_db)
|
||||
init_db()
|
||||
# Silence Plane/Telegram side effects; capture the transparency channels.
|
||||
for name in ("notify_stage_change", "notify_qg_failure", "plane_notify_stage",
|
||||
"plane_notify_qg", "set_issue_in_review", "set_issue_needs_input",
|
||||
"set_issue_approved", "notify_approve_requested"):
|
||||
monkeypatch.setattr(stage_engine, name, MagicMock(), raising=False)
|
||||
# Avoid worktree access in the analyst "ready" comment builder.
|
||||
monkeypatch.setattr(stage_engine, "_build_analyst_ready_comment",
|
||||
lambda *a, **k: "ready", raising=False)
|
||||
monkeypatch.setattr(stage_engine, "plane_add_comment", MagicMock())
|
||||
monkeypatch.setattr(stage_engine, "send_telegram", MagicMock())
|
||||
yield
|
||||
|
||||
|
||||
def _make_task(stage="analysis", repo="orchestrator", branch="feature/ORCH-089-x",
|
||||
wi="ORCH-089"):
|
||||
conn = get_db()
|
||||
cur = conn.execute(
|
||||
"INSERT INTO tasks (plane_id, work_item_id, repo, branch, stage) "
|
||||
"VALUES (?, ?, ?, ?, ?)",
|
||||
(f"plane-{wi}", wi, repo, branch, stage),
|
||||
)
|
||||
tid = cur.lastrowid
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return tid
|
||||
|
||||
|
||||
def _stage_of(task_id):
|
||||
conn = get_db()
|
||||
row = conn.execute("SELECT stage FROM tasks WHERE id=?", (task_id,)).fetchone()
|
||||
conn.close()
|
||||
return row[0]
|
||||
|
||||
|
||||
def _brd_ended(task_id):
|
||||
conn = get_db()
|
||||
row = conn.execute(
|
||||
"SELECT brd_review_ended_at FROM tasks WHERE id=?", (task_id,)
|
||||
).fetchone()
|
||||
conn.close()
|
||||
return row[0]
|
||||
|
||||
|
||||
def _patch_complete_gate(monkeypatch, ok=True):
|
||||
gate = _files_ok if ok else _files_fail
|
||||
monkeypatch.setattr(
|
||||
stage_engine, "QG_CHECKS",
|
||||
{**stage_engine.QG_CHECKS, "check_analysis_complete": gate},
|
||||
)
|
||||
|
||||
|
||||
def _label(monkeypatch, present=True, applies=True):
|
||||
monkeypatch.setattr(labels, "auto_approve_applies", lambda repo: applies)
|
||||
monkeypatch.setattr(labels, "has_label", lambda w, name, p=None: present)
|
||||
|
||||
|
||||
# --- TC-10 -----------------------------------------------------------------
|
||||
def test_tc10_auto_approve_advances(monkeypatch):
|
||||
_patch_complete_gate(monkeypatch, ok=True)
|
||||
_label(monkeypatch, present=True)
|
||||
tid = _make_task()
|
||||
# The BRD-review clock was started when the task entered In Review; the
|
||||
# advance closes it (mark_brd_review_ended only stamps when a start exists).
|
||||
conn = get_db()
|
||||
conn.execute(
|
||||
"UPDATE tasks SET brd_review_started_at=datetime('now') WHERE id=?", (tid,)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
res = advance_stage(tid, "analysis", "orchestrator", "ORCH-089",
|
||||
"feature/ORCH-089-x", finished_agent="analyst")
|
||||
assert res.note == "auto-approved-via-label"
|
||||
assert res.advanced is True
|
||||
assert _stage_of(tid) == "architecture"
|
||||
assert _brd_ended(tid) is not None # clock closed by mark_brd_review_ended
|
||||
stage_engine.set_issue_approved.assert_called_once() # Approved indication
|
||||
|
||||
|
||||
# --- TC-11 -----------------------------------------------------------------
|
||||
def test_tc11_no_label_waits_for_human(monkeypatch):
|
||||
_patch_complete_gate(monkeypatch, ok=True)
|
||||
_label(monkeypatch, present=False)
|
||||
tid = _make_task()
|
||||
res = advance_stage(tid, "analysis", "orchestrator", "ORCH-089",
|
||||
"feature/ORCH-089-x", finished_agent="analyst")
|
||||
assert res.note == "analysis-in-review"
|
||||
assert res.advanced is False
|
||||
assert _stage_of(tid) == "analysis" # still waiting for a human
|
||||
stage_engine.set_issue_in_review.assert_called_once()
|
||||
stage_engine.set_issue_approved.assert_not_called()
|
||||
|
||||
|
||||
# --- TC-12 -----------------------------------------------------------------
|
||||
def test_tc12_missing_artifacts_no_auto(monkeypatch):
|
||||
# autoApprove present, but artifacts incomplete -> files_ok False -> the
|
||||
# autoApprove block (inside `if files_ok`) is never reached.
|
||||
_patch_complete_gate(monkeypatch, ok=False)
|
||||
_label(monkeypatch, present=True)
|
||||
tid = _make_task()
|
||||
res = advance_stage(tid, "analysis", "orchestrator", "ORCH-089",
|
||||
"feature/ORCH-089-x", finished_agent="analyst")
|
||||
assert res.advanced is False
|
||||
assert _stage_of(tid) == "analysis"
|
||||
assert res.note != "auto-approved-via-label"
|
||||
stage_engine.set_issue_approved.assert_not_called()
|
||||
|
||||
|
||||
# --- TC-13: same advance path / idempotent ---------------------------------
|
||||
def test_tc13_same_advance_path_idempotent(monkeypatch):
|
||||
_patch_complete_gate(monkeypatch, ok=True)
|
||||
_label(monkeypatch, present=True)
|
||||
tid = _make_task()
|
||||
res = advance_stage(tid, "analysis", "orchestrator", "ORCH-089",
|
||||
"feature/ORCH-089-x", finished_agent="analyst")
|
||||
# The advance went through the unified path -> architect enqueued exactly once.
|
||||
assert res.enqueued_agent == "architect"
|
||||
conn = get_db()
|
||||
n = conn.execute(
|
||||
"SELECT COUNT(*) FROM jobs WHERE task_id=? AND agent='architect'", (tid,)
|
||||
).fetchone()[0]
|
||||
conn.close()
|
||||
assert n == 1
|
||||
# A later real Approved (webhook path, finished_agent=None) sees architecture,
|
||||
# not analysis -> it cannot re-run the analysis advance (idempotent).
|
||||
assert _stage_of(tid) == "architecture"
|
||||
|
||||
|
||||
# --- TC-14: transparency ---------------------------------------------------
|
||||
def test_tc14_transparency_channels(monkeypatch, caplog):
|
||||
_patch_complete_gate(monkeypatch, ok=True)
|
||||
_label(monkeypatch, present=True)
|
||||
tid = _make_task()
|
||||
import logging
|
||||
with caplog.at_level(logging.INFO, logger="orchestrator.stage_engine"):
|
||||
advance_stage(tid, "analysis", "orchestrator", "ORCH-089",
|
||||
"feature/ORCH-089-x", finished_agent="analyst")
|
||||
# (a) log mentions the label + auto-approve.
|
||||
assert any("auto-approved" in r.message.lower() or "autoApprove" in r.message
|
||||
for r in caplog.records)
|
||||
# (b) Telegram fired; (c) a Plane comment authored by analyst about the auto-pass.
|
||||
assert stage_engine.send_telegram.called
|
||||
comment_calls = [c for c in stage_engine.plane_add_comment.call_args_list
|
||||
if "авто-подтверждён" in c.args[1]]
|
||||
assert comment_calls, "expected an auto-approve Plane comment"
|
||||
182
tests/test_auto_deploy.py
Normal file
182
tests/test_auto_deploy.py
Normal file
@@ -0,0 +1,182 @@
|
||||
"""ORCH-089 — autoDeploy врезка in _handle_self_deploy_phase_a.
|
||||
|
||||
Covers (04-test-plan.yaml):
|
||||
TC-15 autoDeploy + Phase A advance on `deploy` -> Phase B (initiate_deploy)
|
||||
is auto-invoked.
|
||||
TC-16 no autoDeploy label -> prior Phase A: Awaiting Deploy, wait for Confirm
|
||||
Deploy.
|
||||
TC-17 idempotent: INITIATED marker already present -> repeat auto-trigger no-op.
|
||||
TC-18 non-self repo / out of scope -> no auto (Phase A/B only for self-hosting).
|
||||
TC-19 autoDeploy logged + Telegram + Plane comment (transparency AC-7).
|
||||
"""
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
_test_db = os.path.join(tempfile.gettempdir(), "test_auto_deploy.db")
|
||||
os.environ["ORCH_DB_PATH"] = _test_db
|
||||
os.environ["ORCH_REPOS_DIR"] = tempfile.gettempdir()
|
||||
os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token")
|
||||
os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token")
|
||||
|
||||
from unittest.mock import MagicMock # noqa: E402
|
||||
|
||||
import src.db as _db # noqa: E402
|
||||
from src.db import init_db, get_db # noqa: E402
|
||||
from src import stage_engine # noqa: E402
|
||||
from src import self_deploy # noqa: E402
|
||||
from src import labels # noqa: E402
|
||||
from src.stage_engine import advance_stage # noqa: E402
|
||||
|
||||
|
||||
def _pass(*a, **k):
|
||||
return (True, "ok")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def fresh_db(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(_db.settings, "db_path", _test_db)
|
||||
if os.path.exists(_test_db):
|
||||
os.unlink(_test_db)
|
||||
init_db()
|
||||
monkeypatch.setattr(self_deploy.settings, "repos_dir", str(tmp_path))
|
||||
monkeypatch.setattr(self_deploy.settings, "host_repos_dir", str(tmp_path))
|
||||
monkeypatch.setattr(stage_engine.settings, "deploy_require_manual_approve", True)
|
||||
# Pass all edge sub-gates so the deploy-staging -> deploy edge reaches Phase A.
|
||||
monkeypatch.setattr(
|
||||
stage_engine, "QG_CHECKS",
|
||||
{**stage_engine.QG_CHECKS,
|
||||
"check_staging_status": _pass,
|
||||
"check_branch_mergeable": _pass,
|
||||
"check_security_gate": _pass,
|
||||
"check_staging_image_fresh": _pass},
|
||||
)
|
||||
# Default auto-mode flags ON (overridden per-test).
|
||||
monkeypatch.setattr(stage_engine.settings, "auto_label_enabled", True, raising=False)
|
||||
monkeypatch.setattr(stage_engine.settings, "auto_deploy_label", "autoDeploy", raising=False)
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def silence(monkeypatch):
|
||||
for name in ("notify_stage_change", "notify_qg_failure", "plane_notify_stage",
|
||||
"plane_notify_qg", "set_issue_in_review", "set_issue_awaiting_deploy",
|
||||
"set_issue_deploying"):
|
||||
monkeypatch.setattr(stage_engine, name, MagicMock(), raising=False)
|
||||
monkeypatch.setattr(stage_engine, "plane_add_comment", MagicMock())
|
||||
monkeypatch.setattr(stage_engine, "send_telegram", MagicMock())
|
||||
|
||||
|
||||
def _make_task(stage="deploy-staging", repo="orchestrator",
|
||||
branch="feature/ORCH-089-x", wi="ORCH-089"):
|
||||
conn = get_db()
|
||||
cur = conn.execute(
|
||||
"INSERT INTO tasks (plane_id, work_item_id, repo, branch, stage) "
|
||||
"VALUES (?, ?, ?, ?, ?)",
|
||||
(f"plane-{wi}", wi, repo, branch, stage),
|
||||
)
|
||||
tid = cur.lastrowid
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return tid
|
||||
|
||||
|
||||
def _label(monkeypatch, present=True, applies=True):
|
||||
monkeypatch.setattr(labels, "auto_deploy_applies", lambda repo: applies)
|
||||
monkeypatch.setattr(labels, "has_label", lambda w, name, p=None: present)
|
||||
|
||||
|
||||
def _advance(tid, repo="orchestrator", wi="ORCH-089"):
|
||||
return advance_stage(tid, "deploy-staging", repo, wi,
|
||||
"feature/ORCH-089-x", finished_agent="deployer")
|
||||
|
||||
|
||||
# --- TC-15 -----------------------------------------------------------------
|
||||
def test_tc15_auto_deploy_initiates_phase_b(monkeypatch):
|
||||
_label(monkeypatch, present=True)
|
||||
initiate = MagicMock(return_value=(True, "ok"))
|
||||
monkeypatch.setattr(stage_engine.self_deploy, "initiate_deploy", initiate)
|
||||
tid = _make_task()
|
||||
res = _advance(tid)
|
||||
# Phase B ran via the same path a human Confirm Deploy takes.
|
||||
initiate.assert_called_once()
|
||||
assert res.note == "self-deploy-initiated"
|
||||
assert self_deploy.has_marker("orchestrator", "ORCH-089", self_deploy.INITIATED)
|
||||
# APPROVE_REQUESTED (the human ask) was SKIPPED on the auto path.
|
||||
assert not self_deploy.has_marker(
|
||||
"orchestrator", "ORCH-089", self_deploy.APPROVE_REQUESTED
|
||||
)
|
||||
|
||||
|
||||
# --- TC-16 -----------------------------------------------------------------
|
||||
def test_tc16_no_label_waits_for_human(monkeypatch):
|
||||
_label(monkeypatch, present=False)
|
||||
initiate = MagicMock(return_value=(True, "ok"))
|
||||
monkeypatch.setattr(stage_engine.self_deploy, "initiate_deploy", initiate)
|
||||
tid = _make_task()
|
||||
res = _advance(tid)
|
||||
# Prior Phase A behaviour: approval-pending, no deploy initiated.
|
||||
assert res.note == "self-deploy-approval-pending"
|
||||
initiate.assert_not_called()
|
||||
assert self_deploy.has_marker(
|
||||
"orchestrator", "ORCH-089", self_deploy.APPROVE_REQUESTED
|
||||
)
|
||||
stage_engine.set_issue_awaiting_deploy.assert_called_once()
|
||||
|
||||
|
||||
# --- TC-17: idempotency ----------------------------------------------------
|
||||
def test_tc17_idempotent_initiated_marker(monkeypatch):
|
||||
"""autoDeploy delegates prod-deploy to _handle_self_deploy_phase_b, whose
|
||||
INITIATED marker makes a repeat a no-op. Phase A always clears stale state
|
||||
first (ADR D4), so the guard that protects against a double prod deploy is the
|
||||
INITIATED marker WRITTEN by Phase B — verify the auto path initiates exactly
|
||||
once and a subsequent Phase B re-entry (duplicate confirm / reaper re-drive) is
|
||||
a no-op."""
|
||||
_label(monkeypatch, present=True)
|
||||
initiate = MagicMock(return_value=(True, "ok"))
|
||||
monkeypatch.setattr(stage_engine.self_deploy, "initiate_deploy", initiate)
|
||||
tid = _make_task()
|
||||
res = _advance(tid)
|
||||
assert res.note == "self-deploy-initiated"
|
||||
assert initiate.call_count == 1
|
||||
assert self_deploy.has_marker("orchestrator", "ORCH-089", self_deploy.INITIATED)
|
||||
# A repeat Phase B (e.g. duplicate Confirm Deploy webhook / reaper re-drive)
|
||||
# with INITIATED already set is a no-op — no second prod deploy.
|
||||
res2 = stage_engine._handle_self_deploy_phase_b(
|
||||
tid, "orchestrator", "ORCH-089", "feature/ORCH-089-x",
|
||||
stage_engine.AdvanceResult(from_stage="deploy"),
|
||||
)
|
||||
assert initiate.call_count == 1 # still exactly one
|
||||
|
||||
|
||||
# --- TC-18: non-self / out of scope ----------------------------------------
|
||||
def test_tc18_non_self_repo_no_phase_a(monkeypatch):
|
||||
# For a non-self repo Phase A is not reached at all (self_deploy_applies False),
|
||||
# so autoDeploy is a structural no-op. The edge advances normally to `deploy`.
|
||||
_label(monkeypatch, present=True, applies=False)
|
||||
initiate = MagicMock(return_value=(True, "ok"))
|
||||
monkeypatch.setattr(stage_engine.self_deploy, "initiate_deploy", initiate)
|
||||
tid = _make_task(repo="enduro-trails", wi="ET-1")
|
||||
res = advance_stage(tid, "deploy-staging", "enduro-trails", "ET-1",
|
||||
"feature/ORCH-089-x", finished_agent="deployer")
|
||||
initiate.assert_not_called()
|
||||
# No Phase A / Phase B for non-self repo.
|
||||
assert res.note != "self-deploy-initiated"
|
||||
|
||||
|
||||
# --- TC-19: transparency ---------------------------------------------------
|
||||
def test_tc19_transparency_channels(monkeypatch, caplog):
|
||||
_label(monkeypatch, present=True)
|
||||
monkeypatch.setattr(stage_engine.self_deploy, "initiate_deploy",
|
||||
MagicMock(return_value=(True, "ok")))
|
||||
tid = _make_task()
|
||||
import logging
|
||||
with caplog.at_level(logging.INFO, logger="orchestrator.stage_engine"):
|
||||
_advance(tid)
|
||||
assert any("auto-confirmed" in r.message.lower() or "autoDeploy" in r.message
|
||||
for r in caplog.records)
|
||||
assert stage_engine.send_telegram.called
|
||||
comment_calls = [c for c in stage_engine.plane_add_comment.call_args_list
|
||||
if "авто-подтверждён" in c.args[1]]
|
||||
assert comment_calls, "expected an auto-deploy Plane comment"
|
||||
146
tests/test_auto_label_combinations.py
Normal file
146
tests/test_auto_label_combinations.py
Normal file
@@ -0,0 +1,146 @@
|
||||
"""ORCH-089 — label independence + kill-switch (AC-8/AC-9).
|
||||
|
||||
Covers (04-test-plan.yaml):
|
||||
TC-20 only autoApprove: BRD auto, deploy waits for a human (AC-9).
|
||||
TC-21 only autoDeploy: BRD waits for a human, deploy auto (AC-9).
|
||||
TC-22 auto_label_enabled=False: both gates manual even with both labels (AC-8).
|
||||
"""
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
_test_db = os.path.join(tempfile.gettempdir(), "test_auto_combos.db")
|
||||
os.environ["ORCH_DB_PATH"] = _test_db
|
||||
os.environ["ORCH_REPOS_DIR"] = tempfile.gettempdir()
|
||||
os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token")
|
||||
os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token")
|
||||
|
||||
from unittest.mock import MagicMock # noqa: E402
|
||||
|
||||
import src.db as _db # noqa: E402
|
||||
from src.db import init_db, get_db # noqa: E402
|
||||
from src import stage_engine # noqa: E402
|
||||
from src import self_deploy # noqa: E402
|
||||
from src import labels # noqa: E402
|
||||
from src.stage_engine import advance_stage # noqa: E402
|
||||
|
||||
|
||||
def _pass(*a, **k):
|
||||
return (True, "ok")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def fresh(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(_db.settings, "db_path", _test_db)
|
||||
if os.path.exists(_test_db):
|
||||
os.unlink(_test_db)
|
||||
init_db()
|
||||
monkeypatch.setattr(self_deploy.settings, "repos_dir", str(tmp_path))
|
||||
monkeypatch.setattr(self_deploy.settings, "host_repos_dir", str(tmp_path))
|
||||
monkeypatch.setattr(stage_engine.settings, "deploy_require_manual_approve", True)
|
||||
monkeypatch.setattr(
|
||||
stage_engine, "QG_CHECKS",
|
||||
{**stage_engine.QG_CHECKS,
|
||||
"check_analysis_complete": _pass,
|
||||
"check_staging_status": _pass,
|
||||
"check_branch_mergeable": _pass,
|
||||
"check_security_gate": _pass,
|
||||
"check_staging_image_fresh": _pass},
|
||||
)
|
||||
monkeypatch.setattr(stage_engine, "_build_analyst_ready_comment",
|
||||
lambda *a, **k: "ready", raising=False)
|
||||
# Real auto-mode scope/flags (kill-switch exercised per-test).
|
||||
monkeypatch.setattr(stage_engine.settings, "auto_label_enabled", True, raising=False)
|
||||
monkeypatch.setattr(stage_engine.settings, "auto_label_repos", "", raising=False)
|
||||
monkeypatch.setattr(stage_engine.settings, "auto_approve_label", "autoApprove", raising=False)
|
||||
monkeypatch.setattr(stage_engine.settings, "auto_deploy_label", "autoDeploy", raising=False)
|
||||
monkeypatch.setattr(stage_engine.self_deploy, "initiate_deploy",
|
||||
MagicMock(return_value=(True, "ok")))
|
||||
for name in ("notify_stage_change", "notify_qg_failure", "plane_notify_stage",
|
||||
"plane_notify_qg", "set_issue_in_review", "set_issue_needs_input",
|
||||
"set_issue_awaiting_deploy", "set_issue_deploying", "set_issue_approved",
|
||||
"notify_approve_requested"):
|
||||
monkeypatch.setattr(stage_engine, name, MagicMock(), raising=False)
|
||||
monkeypatch.setattr(stage_engine, "plane_add_comment", MagicMock())
|
||||
monkeypatch.setattr(stage_engine, "send_telegram", MagicMock())
|
||||
yield
|
||||
|
||||
|
||||
def _make_task(stage, repo="orchestrator", branch="feature/ORCH-089-x", wi="ORCH-089"):
|
||||
conn = get_db()
|
||||
cur = conn.execute(
|
||||
"INSERT INTO tasks (plane_id, work_item_id, repo, branch, stage, brd_review_started_at) "
|
||||
"VALUES (?, ?, ?, ?, ?, datetime('now'))",
|
||||
(f"plane-{wi}", wi, repo, branch, stage),
|
||||
)
|
||||
tid = cur.lastrowid
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return tid
|
||||
|
||||
|
||||
def _stage_of(task_id):
|
||||
conn = get_db()
|
||||
row = conn.execute("SELECT stage FROM tasks WHERE id=?", (task_id,)).fetchone()
|
||||
conn.close()
|
||||
return row[0]
|
||||
|
||||
|
||||
def _present_labels(monkeypatch, names):
|
||||
"""has_label True only for the given normalized label names (real applies())."""
|
||||
want = {n.casefold() for n in names}
|
||||
monkeypatch.setattr(labels, "has_label",
|
||||
lambda w, name, p=None: name.casefold() in want)
|
||||
|
||||
|
||||
def _run_brd(wi="ORCH-089"):
|
||||
tid = _make_task("analysis", wi=wi)
|
||||
res = advance_stage(tid, "analysis", "orchestrator", wi,
|
||||
"feature/ORCH-089-x", finished_agent="analyst")
|
||||
return tid, res
|
||||
|
||||
|
||||
def _run_deploy(wi="ORCH-089"):
|
||||
tid = _make_task("deploy-staging", wi=wi)
|
||||
res = advance_stage(tid, "deploy-staging", "orchestrator", wi,
|
||||
"feature/ORCH-089-x", finished_agent="deployer")
|
||||
return tid, res
|
||||
|
||||
|
||||
# --- TC-20: only autoApprove -----------------------------------------------
|
||||
def test_tc20_only_auto_approve(monkeypatch):
|
||||
_present_labels(monkeypatch, ["autoApprove"])
|
||||
tid_brd, res_brd = _run_brd()
|
||||
assert res_brd.note == "auto-approved-via-label"
|
||||
assert _stage_of(tid_brd) == "architecture"
|
||||
# Deploy gate still manual (autoDeploy absent).
|
||||
_tid_dep, res_dep = _run_deploy()
|
||||
assert res_dep.note == "self-deploy-approval-pending"
|
||||
|
||||
|
||||
# --- TC-21: only autoDeploy ------------------------------------------------
|
||||
def test_tc21_only_auto_deploy(monkeypatch):
|
||||
_present_labels(monkeypatch, ["autoDeploy"])
|
||||
tid_brd, res_brd = _run_brd()
|
||||
# BRD gate still manual (autoApprove absent).
|
||||
assert res_brd.note == "analysis-in-review"
|
||||
assert _stage_of(tid_brd) == "analysis"
|
||||
# Deploy auto-confirmed.
|
||||
_tid_dep, res_dep = _run_deploy()
|
||||
assert res_dep.note == "self-deploy-initiated"
|
||||
|
||||
|
||||
# --- TC-22: kill-switch -> both manual -------------------------------------
|
||||
def test_tc22_killswitch_both_manual(monkeypatch):
|
||||
monkeypatch.setattr(stage_engine.settings, "auto_label_enabled", False, raising=False)
|
||||
# Both labels "present", but the kill-switch makes applies() False FIRST, so
|
||||
# has_label is never consulted -> both gates stay manual.
|
||||
spy = MagicMock(return_value=True)
|
||||
monkeypatch.setattr(labels, "has_label", spy)
|
||||
tid_brd, res_brd = _run_brd()
|
||||
assert res_brd.note == "analysis-in-review"
|
||||
assert _stage_of(tid_brd) == "analysis"
|
||||
_tid_dep, res_dep = _run_deploy()
|
||||
assert res_dep.note == "self-deploy-approval-pending"
|
||||
spy.assert_not_called() # zero network — AC-8
|
||||
147
tests/test_auto_labels_integration.py
Normal file
147
tests/test_auto_labels_integration.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""ORCH-089 — integration: end-to-end auto-pass across pipeline edges.
|
||||
|
||||
Covers (04-test-plan.yaml):
|
||||
TC-23 both labels + all tech-gates green -> analysis -> deploy with no manual
|
||||
clicks (AC-3).
|
||||
TC-24 autoDeploy + a RED edge sub-gate -> Phase A not reached, Phase B not
|
||||
initiated (AC-5).
|
||||
TC-25 regression: no labels -> both gates manual exactly as before ORCH-089
|
||||
(AC-4).
|
||||
"""
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
_test_db = os.path.join(tempfile.gettempdir(), "test_auto_integ.db")
|
||||
os.environ["ORCH_DB_PATH"] = _test_db
|
||||
os.environ["ORCH_REPOS_DIR"] = tempfile.gettempdir()
|
||||
os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token")
|
||||
os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token")
|
||||
|
||||
from unittest.mock import MagicMock # noqa: E402
|
||||
|
||||
import src.db as _db # noqa: E402
|
||||
from src.db import init_db, get_db # noqa: E402
|
||||
from src import stage_engine # noqa: E402
|
||||
from src import self_deploy # noqa: E402
|
||||
from src import labels # noqa: E402
|
||||
from src.stage_engine import advance_stage # noqa: E402
|
||||
|
||||
|
||||
def _pass(*a, **k):
|
||||
return (True, "ok")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def fresh(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(_db.settings, "db_path", _test_db)
|
||||
if os.path.exists(_test_db):
|
||||
os.unlink(_test_db)
|
||||
init_db()
|
||||
monkeypatch.setattr(self_deploy.settings, "repos_dir", str(tmp_path))
|
||||
monkeypatch.setattr(self_deploy.settings, "host_repos_dir", str(tmp_path))
|
||||
monkeypatch.setattr(stage_engine.settings, "deploy_require_manual_approve", True)
|
||||
monkeypatch.setattr(stage_engine.settings, "auto_label_enabled", True, raising=False)
|
||||
monkeypatch.setattr(stage_engine.settings, "auto_label_repos", "", raising=False)
|
||||
monkeypatch.setattr(stage_engine.settings, "auto_approve_label", "autoApprove", raising=False)
|
||||
monkeypatch.setattr(stage_engine.settings, "auto_deploy_label", "autoDeploy", raising=False)
|
||||
monkeypatch.setattr(stage_engine, "_build_analyst_ready_comment",
|
||||
lambda *a, **k: "ready", raising=False)
|
||||
monkeypatch.setattr(stage_engine.self_deploy, "initiate_deploy",
|
||||
MagicMock(return_value=(True, "ok")))
|
||||
for name in ("notify_stage_change", "notify_qg_failure", "plane_notify_stage",
|
||||
"plane_notify_qg", "set_issue_in_review", "set_issue_needs_input",
|
||||
"set_issue_awaiting_deploy", "set_issue_deploying", "set_issue_approved",
|
||||
"set_issue_blocked", "notify_approve_requested"):
|
||||
monkeypatch.setattr(stage_engine, name, MagicMock(), raising=False)
|
||||
monkeypatch.setattr(stage_engine, "plane_add_comment", MagicMock())
|
||||
monkeypatch.setattr(stage_engine, "send_telegram", MagicMock())
|
||||
yield
|
||||
|
||||
|
||||
def _gates(monkeypatch, **overrides):
|
||||
base = {
|
||||
"check_analysis_complete": _pass,
|
||||
"check_staging_status": _pass,
|
||||
"check_branch_mergeable": _pass,
|
||||
"check_security_gate": _pass,
|
||||
"check_staging_image_fresh": _pass,
|
||||
}
|
||||
base.update(overrides)
|
||||
monkeypatch.setattr(stage_engine, "QG_CHECKS", {**stage_engine.QG_CHECKS, **base})
|
||||
|
||||
|
||||
def _make_task(stage, wi="ORCH-089", repo="orchestrator"):
|
||||
conn = get_db()
|
||||
cur = conn.execute(
|
||||
"INSERT INTO tasks (plane_id, work_item_id, repo, branch, stage, brd_review_started_at) "
|
||||
"VALUES (?, ?, ?, ?, ?, datetime('now'))",
|
||||
(f"plane-{wi}", wi, repo, "feature/ORCH-089-x", stage),
|
||||
)
|
||||
tid = cur.lastrowid
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return tid
|
||||
|
||||
|
||||
def _stage_of(task_id):
|
||||
conn = get_db()
|
||||
row = conn.execute("SELECT stage FROM tasks WHERE id=?", (task_id,)).fetchone()
|
||||
conn.close()
|
||||
return row[0]
|
||||
|
||||
|
||||
# --- TC-23: both labels, all green -> autonomous ---------------------------
|
||||
def test_tc23_both_labels_autonomous(monkeypatch):
|
||||
_gates(monkeypatch)
|
||||
monkeypatch.setattr(labels, "has_label", lambda w, name, p=None: True)
|
||||
|
||||
# BRD edge: analyst finished -> auto-approve -> architecture.
|
||||
brd = _make_task("analysis")
|
||||
res_brd = advance_stage(brd, "analysis", "orchestrator", "ORCH-089",
|
||||
"feature/ORCH-089-x", finished_agent="analyst")
|
||||
assert res_brd.note == "auto-approved-via-label"
|
||||
assert _stage_of(brd) == "architecture"
|
||||
|
||||
# Deploy edge: staging-deployer finished -> Phase A advances to deploy -> auto
|
||||
# Phase B initiates the prod deploy. No human Approved nor Confirm Deploy.
|
||||
dep = _make_task("deploy-staging", wi="ORCH-089b")
|
||||
res_dep = advance_stage(dep, "deploy-staging", "orchestrator", "ORCH-089b",
|
||||
"feature/ORCH-089-x", finished_agent="deployer")
|
||||
assert res_dep.note == "self-deploy-initiated"
|
||||
assert stage_engine.self_deploy.initiate_deploy.called
|
||||
assert _stage_of(dep) == "deploy"
|
||||
|
||||
|
||||
# --- TC-24: red sub-gate blocks autoDeploy ---------------------------------
|
||||
def test_tc24_red_staging_blocks_auto_deploy(monkeypatch):
|
||||
# staging RED -> the edge fails BEFORE Phase A -> autoDeploy cannot fire.
|
||||
_gates(monkeypatch, check_staging_status=lambda *a, **k: (False, "FAILED"))
|
||||
monkeypatch.setattr(labels, "has_label", lambda w, name, p=None: True)
|
||||
|
||||
dep = _make_task("deploy-staging")
|
||||
res = advance_stage(dep, "deploy-staging", "orchestrator", "ORCH-089",
|
||||
"feature/ORCH-089-x", finished_agent="deployer")
|
||||
# Phase B never initiated despite the autoDeploy label.
|
||||
assert not stage_engine.self_deploy.initiate_deploy.called
|
||||
assert res.note != "self-deploy-initiated"
|
||||
assert _stage_of(dep) != "deploy" # did not advance onto deploy
|
||||
|
||||
|
||||
# --- TC-25: regression — no labels -> manual gates -------------------------
|
||||
def test_tc25_no_labels_manual(monkeypatch):
|
||||
_gates(monkeypatch)
|
||||
monkeypatch.setattr(labels, "has_label", lambda w, name, p=None: False)
|
||||
|
||||
brd = _make_task("analysis")
|
||||
res_brd = advance_stage(brd, "analysis", "orchestrator", "ORCH-089",
|
||||
"feature/ORCH-089-x", finished_agent="analyst")
|
||||
assert res_brd.note == "analysis-in-review"
|
||||
assert _stage_of(brd) == "analysis"
|
||||
|
||||
dep = _make_task("deploy-staging", wi="ORCH-089b")
|
||||
res_dep = advance_stage(dep, "deploy-staging", "orchestrator", "ORCH-089b",
|
||||
"feature/ORCH-089-x", finished_agent="deployer")
|
||||
assert res_dep.note == "self-deploy-approval-pending"
|
||||
assert not stage_engine.self_deploy.initiate_deploy.called
|
||||
33
tests/test_auto_labels_invariants.py
Normal file
33
tests/test_auto_labels_invariants.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""ORCH-089 — TC-26: invariant registries are NOT touched by the auto-label work.
|
||||
|
||||
The auto-mode reuses existing transitions/gates and only removes the wait for a
|
||||
human signal; it must not add a stage, a transition, or a QG check (TRZ §10 / AC-10).
|
||||
"""
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
os.environ.setdefault("ORCH_DB_PATH", os.path.join(tempfile.gettempdir(), "test_auto_inv.db"))
|
||||
os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token")
|
||||
os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token")
|
||||
|
||||
|
||||
def test_tc26_stage_transitions_unchanged():
|
||||
from src.stages import STAGE_TRANSITIONS
|
||||
assert set(STAGE_TRANSITIONS) == {
|
||||
"created", "analysis", "architecture", "development", "review",
|
||||
"testing", "deploy-staging", "deploy", "done",
|
||||
}
|
||||
# The two human gates still use their existing QG names (unchanged).
|
||||
assert STAGE_TRANSITIONS["analysis"]["qg"] == "check_analysis_approved"
|
||||
|
||||
|
||||
def test_tc26_no_new_qg_check():
|
||||
from src.qg.checks import QG_CHECKS
|
||||
# No auto-label / auto-approve / auto-deploy QG check was introduced — the
|
||||
# auto-mode is a decision врезка, not a registered quality gate.
|
||||
assert not any(
|
||||
tok in k for k in QG_CHECKS for tok in ("auto_label", "autoapprove", "autodeploy")
|
||||
), "ORCH-089 must not register a new QG check"
|
||||
# The gates it reuses are present and untouched.
|
||||
for k in ("check_analysis_approved", "check_deploy_status", "check_staging_status"):
|
||||
assert k in QG_CHECKS
|
||||
244
tests/test_frontmatter.py
Normal file
244
tests/test_frontmatter.py
Normal file
@@ -0,0 +1,244 @@
|
||||
"""ORCH-076 (ORCH-52c): unit tests for the unified frontmatter contract.
|
||||
|
||||
Covers TC-01..TC-07 of docs/work-items/ORCH-076/04-test-plan.yaml:
|
||||
* writer (render/round-trip), validator (full / partial schema, strict on/off),
|
||||
* reader contract preserved (read_frontmatter_value), never-raise on bad input.
|
||||
|
||||
The whole module honours a never-raise contract (NFR-2): no input shape may raise.
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token")
|
||||
os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token")
|
||||
|
||||
from src import frontmatter as fm # noqa: E402
|
||||
from src.frontmatter import ( # noqa: E402
|
||||
REQUIRED_FIELDS,
|
||||
FrontmatterParse,
|
||||
SchemaValidation,
|
||||
maybe_warn_schema,
|
||||
parse_frontmatter,
|
||||
parse_frontmatter_dict,
|
||||
read_frontmatter,
|
||||
read_frontmatter_value,
|
||||
render_frontmatter,
|
||||
strip_frontmatter,
|
||||
validate_schema,
|
||||
write_frontmatter,
|
||||
)
|
||||
|
||||
|
||||
def _full_schema():
|
||||
return {
|
||||
"work_item": "ORCH-076",
|
||||
"stage": "review",
|
||||
"author_agent": "reviewer",
|
||||
"status": "APPROVED",
|
||||
"created_at": "2026-06-09",
|
||||
"model_used": "claude-opus-4-8",
|
||||
}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# TC-01 — writer serialises a mapping into canonical leading YAML-frontmatter
|
||||
# readable by the existing parsers (split("---", 2) + yaml.safe_load).
|
||||
# --------------------------------------------------------------------------- #
|
||||
def test_tc01_render_frontmatter_is_canonical_and_reparseable():
|
||||
out = render_frontmatter({"verdict": "APPROVED", "work_item": "ORCH-076"}, "body text")
|
||||
assert out.startswith("---\n")
|
||||
# Existing parser shape: split on '---' into 3 segments + yaml.safe_load.
|
||||
parts = out.split("---", 2)
|
||||
assert len(parts) == 3
|
||||
import yaml
|
||||
data = yaml.safe_load(parts[1])
|
||||
assert data["verdict"] == "APPROVED"
|
||||
assert data["work_item"] == "ORCH-076"
|
||||
# Body is preserved verbatim after the closing fence.
|
||||
assert parts[2].lstrip("\n") == "body text"
|
||||
# And our own primitive round-trips it.
|
||||
assert parse_frontmatter(out).data == {"verdict": "APPROVED", "work_item": "ORCH-076"}
|
||||
|
||||
|
||||
def test_tc01_render_empty_body_default():
|
||||
out = render_frontmatter({"a": 1})
|
||||
assert out == "---\na: 1\n---\n"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# TC-02 — round-trip: writer -> reader read_frontmatter_value yields same values.
|
||||
# --------------------------------------------------------------------------- #
|
||||
def test_tc02_write_then_read_roundtrip():
|
||||
data = _full_schema()
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
path = os.path.join(d, "12-review.md")
|
||||
assert write_frontmatter(path, data, "# Review body") is True
|
||||
for key, val in data.items():
|
||||
assert read_frontmatter_value(path, key) == val
|
||||
# Whole-mapping read matches too.
|
||||
assert read_frontmatter(path) == data
|
||||
|
||||
|
||||
def test_tc02_render_parse_dict_roundtrip():
|
||||
data = _full_schema()
|
||||
rendered = render_frontmatter(data, "body")
|
||||
assert parse_frontmatter_dict(rendered) == data
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# TC-03 — validator: full schema -> valid=True, no missing fields.
|
||||
# --------------------------------------------------------------------------- #
|
||||
def test_tc03_validate_full_schema_valid():
|
||||
res = validate_schema(_full_schema())
|
||||
assert isinstance(res, SchemaValidation)
|
||||
assert res.valid is True
|
||||
assert res.missing == []
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# TC-04 — validator: partial schema -> valid=False with the missing list,
|
||||
# WITHOUT raising (warning-only by default).
|
||||
# --------------------------------------------------------------------------- #
|
||||
def test_tc04_validate_partial_schema_lists_missing():
|
||||
res = validate_schema({"work_item": "ORCH-076", "stage": "review"})
|
||||
assert res.valid is False
|
||||
# The four absent required fields are reported (order = REQUIRED_FIELDS).
|
||||
assert set(res.missing) == set(REQUIRED_FIELDS) - {"work_item", "stage"}
|
||||
assert res.missing == [f for f in REQUIRED_FIELDS if f in res.missing]
|
||||
|
||||
|
||||
def test_tc04_blank_and_none_count_as_missing():
|
||||
data = _full_schema()
|
||||
data["status"] = "" # blank -> missing
|
||||
data["model_used"] = None # None -> missing
|
||||
res = validate_schema(data)
|
||||
assert res.valid is False
|
||||
assert set(res.missing) == {"status", "model_used"}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# TC-05 — never-raise: writer + validator on broken input return a safe value.
|
||||
# --------------------------------------------------------------------------- #
|
||||
def test_tc05_validate_non_mapping_never_raises():
|
||||
for bad in (None, "not a mapping", 123, ["a", "b"]):
|
||||
res = validate_schema(bad) # type: ignore[arg-type]
|
||||
assert res.valid is False
|
||||
assert set(res.missing) == set(REQUIRED_FIELDS)
|
||||
|
||||
|
||||
def test_tc05_parse_broken_inputs_never_raise():
|
||||
# No frontmatter.
|
||||
p = parse_frontmatter("just prose, no fence")
|
||||
assert p == FrontmatterParse()
|
||||
assert p.data == {} and p.has_block is False
|
||||
# Unterminated block.
|
||||
p = parse_frontmatter("---\nkey: val\nno closing fence")
|
||||
assert p.has_block is True and p.malformed is True and p.data == {}
|
||||
# Bad YAML.
|
||||
p = parse_frontmatter("---\nkey: : :\n bad\n---\n")
|
||||
assert p.has_block is True and p.yaml_error is not None and p.data == {}
|
||||
# Non-mapping scalar frontmatter.
|
||||
p = parse_frontmatter("---\njust a string\n---\nbody")
|
||||
assert p.has_block is True and p.data == {}
|
||||
# Non-string input.
|
||||
assert parse_frontmatter(None).data == {} # type: ignore[arg-type]
|
||||
assert parse_frontmatter_dict(12345) == {} # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_tc05_write_to_unwritable_path_returns_false():
|
||||
# A path under a non-existent directory cannot be opened -> False, no raise.
|
||||
ok = write_frontmatter("/nonexistent-dir-xyz/cannot/12-review.md", {"a": 1})
|
||||
assert ok is False
|
||||
|
||||
|
||||
def test_tc05_render_unserialisable_degrades_to_body():
|
||||
class Bad:
|
||||
pass
|
||||
|
||||
out = render_frontmatter({"x": Bad()}, "fallback-body")
|
||||
# yaml cannot serialise an arbitrary object -> degrade to the body, never raise.
|
||||
assert out == "fallback-body"
|
||||
|
||||
|
||||
def test_tc05_read_missing_file_returns_empty():
|
||||
assert read_frontmatter("/no/such/file.md") == {}
|
||||
assert read_frontmatter_value("/no/such/file.md", "verdict") is None
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# TC-06 — reader read_frontmatter_value keeps its previous contract.
|
||||
# --------------------------------------------------------------------------- #
|
||||
def test_tc06_reader_contract_preserved():
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
path = os.path.join(d, "doc.md")
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write("---\nverdict: Approved\nempty:\n---\nbody\n")
|
||||
# strip + case preserved.
|
||||
assert read_frontmatter_value(path, "verdict") == "Approved"
|
||||
# empty value -> None.
|
||||
assert read_frontmatter_value(path, "empty") is None
|
||||
# absent key -> None.
|
||||
assert read_frontmatter_value(path, "missing") is None
|
||||
|
||||
# No frontmatter -> None.
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
path = os.path.join(d, "doc.md")
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write("no frontmatter here\n")
|
||||
assert read_frontmatter_value(path, "verdict") is None
|
||||
|
||||
|
||||
def test_tc06_reader_strips_whitespace():
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
path = os.path.join(d, "doc.md")
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write('---\nverdict: " PASS "\n---\n')
|
||||
assert read_frontmatter_value(path, "verdict") == "PASS"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# TC-07 — kill-switch: strict False (default) is inert; strict True signals
|
||||
# invalidity. maybe_warn_schema never changes a verdict either way.
|
||||
# --------------------------------------------------------------------------- #
|
||||
def test_tc07_maybe_warn_schema_default_warning_only(monkeypatch, caplog):
|
||||
monkeypatch.setattr(fm, "logger", fm.logger)
|
||||
from src.config import settings
|
||||
monkeypatch.setattr(settings, "frontmatter_validation_strict", False)
|
||||
incomplete = render_frontmatter({"verdict": "APPROVED"}, "body")
|
||||
res = maybe_warn_schema(incomplete, "review report")
|
||||
# Validation still reports invalidity (the data IS incomplete)...
|
||||
assert res.valid is False
|
||||
assert "model_used" in res.missing
|
||||
# ...but the helper is inert: it returns a value, it does not raise / block.
|
||||
|
||||
|
||||
def test_tc07_strict_flag_visible_to_helper(monkeypatch):
|
||||
from src.config import settings
|
||||
# Full schema -> valid regardless of the flag.
|
||||
monkeypatch.setattr(settings, "frontmatter_validation_strict", True)
|
||||
res_full = maybe_warn_schema(render_frontmatter(_full_schema(), "b"), "doc")
|
||||
assert res_full.valid is True
|
||||
# Incomplete -> invalid; strict True does not raise, just signals.
|
||||
res_partial = maybe_warn_schema(render_frontmatter({"stage": "review"}, "b"), "doc")
|
||||
assert res_partial.valid is False
|
||||
|
||||
|
||||
def test_tc07_maybe_warn_schema_on_garbage_is_inert():
|
||||
# Never-raise: a non-string / no-frontmatter input returns a SchemaValidation
|
||||
# (reporting the missing fields) WITHOUT raising — the gate verdict is untouched.
|
||||
for bad in ("no frontmatter", None, 123):
|
||||
res = maybe_warn_schema(bad, "doc") # type: ignore[arg-type]
|
||||
assert isinstance(res, SchemaValidation)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# strip_frontmatter helper parity.
|
||||
# --------------------------------------------------------------------------- #
|
||||
def test_strip_frontmatter_parity():
|
||||
assert strip_frontmatter("---\na: 1\n---\nbody") == "\nbody"
|
||||
# No well-formed block -> unchanged.
|
||||
assert strip_frontmatter("no fence") == "no fence"
|
||||
assert strip_frontmatter("---\nunterminated") == "---\nunterminated"
|
||||
# Never-raise on non-string.
|
||||
assert strip_frontmatter(None) is None # type: ignore[arg-type]
|
||||
150
tests/test_labels.py
Normal file
150
tests/test_labels.py
Normal file
@@ -0,0 +1,150 @@
|
||||
"""ORCH-089 — src/labels.py: auto-mode pure logic (never-raise, fail-safe).
|
||||
|
||||
Covers (04-test-plan.yaml):
|
||||
TC-01 has_label True when the label is present on the issue.
|
||||
TC-02 has_label False when the label is absent.
|
||||
TC-03 has_label fail-safe (no auto, never-raise) on Plane API error/timeout.
|
||||
TC-04 label-name matching is normalized (case/space); ambiguity -> no auto.
|
||||
TC-05 auto_approve_applies / auto_deploy_applies: CSV scope + self-hosting.
|
||||
TC-06 auto_label_enabled=False -> applies() False -> has_label never reached
|
||||
(no network call on the gate).
|
||||
"""
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
os.environ.setdefault("ORCH_DB_PATH", os.path.join(tempfile.gettempdir(), "test_labels.db"))
|
||||
os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token")
|
||||
os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token")
|
||||
|
||||
from src import labels # noqa: E402
|
||||
from src import plane_sync # noqa: E402
|
||||
from src import config as cfg # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def enabled_self_hosting(monkeypatch):
|
||||
monkeypatch.setattr(cfg.settings, "auto_label_enabled", True, raising=False)
|
||||
monkeypatch.setattr(cfg.settings, "auto_approve_label", "autoApprove", raising=False)
|
||||
monkeypatch.setattr(cfg.settings, "auto_deploy_label", "autoDeploy", raising=False)
|
||||
monkeypatch.setattr(cfg.settings, "auto_label_repos", "", raising=False)
|
||||
# Keep _resolve_project_id offline-deterministic.
|
||||
monkeypatch.setattr(plane_sync, "_resolve_project_id", lambda w=None, p=None: "proj-1")
|
||||
yield
|
||||
|
||||
|
||||
# --- TC-01 / TC-02 ---------------------------------------------------------
|
||||
def test_tc01_has_label_present(monkeypatch):
|
||||
monkeypatch.setattr(plane_sync, "fetch_issue_labels", lambda w, p=None: ["uuid-A"])
|
||||
monkeypatch.setattr(plane_sync, "get_project_labels", lambda pid: {"autoapprove": "uuid-A"})
|
||||
assert labels.has_label("ORCH-1", "autoApprove") is True
|
||||
|
||||
|
||||
def test_tc02_has_label_absent(monkeypatch):
|
||||
# Issue carries a different label uuid than the project's autoApprove uuid.
|
||||
monkeypatch.setattr(plane_sync, "fetch_issue_labels", lambda w, p=None: ["uuid-OTHER"])
|
||||
monkeypatch.setattr(plane_sync, "get_project_labels", lambda pid: {"autoapprove": "uuid-A"})
|
||||
assert labels.has_label("ORCH-1", "autoApprove") is False
|
||||
|
||||
|
||||
def test_tc02_has_label_empty_issue_labels(monkeypatch):
|
||||
monkeypatch.setattr(plane_sync, "fetch_issue_labels", lambda w, p=None: [])
|
||||
monkeypatch.setattr(plane_sync, "get_project_labels", lambda pid: {"autoapprove": "uuid-A"})
|
||||
assert labels.has_label("ORCH-1", "autoApprove") is False
|
||||
|
||||
|
||||
# --- TC-03: fail-safe / never-raise ----------------------------------------
|
||||
def test_tc03_fetch_none_failsafe(monkeypatch):
|
||||
# fetch returns None (could-not-read) -> False, no auto.
|
||||
monkeypatch.setattr(plane_sync, "fetch_issue_labels", lambda w, p=None: None)
|
||||
monkeypatch.setattr(plane_sync, "get_project_labels", lambda pid: {"autoapprove": "uuid-A"})
|
||||
assert labels.has_label("ORCH-1", "autoApprove") is False
|
||||
|
||||
|
||||
def test_tc03_fetch_raises_failsafe(monkeypatch):
|
||||
def boom(*a, **k):
|
||||
raise RuntimeError("plane down")
|
||||
monkeypatch.setattr(plane_sync, "fetch_issue_labels", boom)
|
||||
monkeypatch.setattr(plane_sync, "get_project_labels", lambda pid: {"autoapprove": "uuid-A"})
|
||||
# Never raises; degrades to no auto.
|
||||
assert labels.has_label("ORCH-1", "autoApprove") is False
|
||||
|
||||
|
||||
def test_tc03_project_map_empty_failsafe(monkeypatch):
|
||||
monkeypatch.setattr(plane_sync, "fetch_issue_labels", lambda w, p=None: ["uuid-A"])
|
||||
monkeypatch.setattr(plane_sync, "get_project_labels", lambda pid: {})
|
||||
assert labels.has_label("ORCH-1", "autoApprove") is False
|
||||
|
||||
|
||||
# --- TC-04: normalization + ambiguity --------------------------------------
|
||||
def test_tc04_normalized_match(monkeypatch):
|
||||
# Issue label uuid-A; project maps a differently-cased/spaced name to it.
|
||||
monkeypatch.setattr(plane_sync, "fetch_issue_labels", lambda w, p=None: ["uuid-A"])
|
||||
monkeypatch.setattr(plane_sync, "get_project_labels", lambda pid: {"autoapprove": "uuid-A"})
|
||||
# Sought name has different case + surrounding spaces.
|
||||
assert labels.has_label("ORCH-1", " AUTOapprove ") is True
|
||||
|
||||
|
||||
def test_tc04_ambiguous_name_no_auto(monkeypatch):
|
||||
# Two distinct project labels collapse to the same normalized name -> ambiguous
|
||||
# sentinel from get_project_labels -> has_label False.
|
||||
monkeypatch.setattr(plane_sync, "fetch_issue_labels", lambda w, p=None: ["uuid-A"])
|
||||
monkeypatch.setattr(
|
||||
plane_sync, "get_project_labels",
|
||||
lambda pid: {"autoapprove": "__AMBIGUOUS__"},
|
||||
)
|
||||
assert labels.has_label("ORCH-1", "autoApprove") is False
|
||||
|
||||
|
||||
def test_tc04_empty_label_name(monkeypatch):
|
||||
monkeypatch.setattr(plane_sync, "fetch_issue_labels", lambda w, p=None: ["uuid-A"])
|
||||
monkeypatch.setattr(plane_sync, "get_project_labels", lambda pid: {"autoapprove": "uuid-A"})
|
||||
assert labels.has_label("ORCH-1", "") is False
|
||||
|
||||
|
||||
# --- TC-05: scope (CSV + self-hosting) -------------------------------------
|
||||
def test_tc05_empty_csv_self_hosting_only(monkeypatch):
|
||||
monkeypatch.setattr(cfg.settings, "auto_label_repos", "", raising=False)
|
||||
assert labels.auto_approve_applies("orchestrator") is True
|
||||
assert labels.auto_deploy_applies("orchestrator") is True
|
||||
# Non-self repo with empty CSV -> not in scope.
|
||||
assert labels.auto_approve_applies("enduro-trails") is False
|
||||
assert labels.auto_deploy_applies("enduro-trails") is False
|
||||
|
||||
|
||||
def test_tc05_csv_membership(monkeypatch):
|
||||
monkeypatch.setattr(cfg.settings, "auto_label_repos", "enduro-trails, foo", raising=False)
|
||||
assert labels.auto_approve_applies("enduro-trails") is True
|
||||
assert labels.auto_deploy_applies("foo") is True
|
||||
# orchestrator is NOT in the explicit CSV -> out of scope.
|
||||
assert labels.auto_approve_applies("orchestrator") is False
|
||||
|
||||
|
||||
# --- TC-06: kill-switch -> applies False, no network -----------------------
|
||||
def test_tc06_killswitch_applies_false(monkeypatch):
|
||||
monkeypatch.setattr(cfg.settings, "auto_label_enabled", False, raising=False)
|
||||
assert labels.auto_approve_applies("orchestrator") is False
|
||||
assert labels.auto_deploy_applies("orchestrator") is False
|
||||
|
||||
|
||||
def test_tc06_killswitch_gate_makes_no_network(monkeypatch):
|
||||
"""The gate idiom `applies(repo) and has_label(...)` short-circuits before any
|
||||
network call when the kill-switch is off (AC-8)."""
|
||||
monkeypatch.setattr(cfg.settings, "auto_label_enabled", False, raising=False)
|
||||
called = {"fetch": 0}
|
||||
|
||||
def spy(*a, **k):
|
||||
called["fetch"] += 1
|
||||
return ["uuid-A"]
|
||||
monkeypatch.setattr(plane_sync, "fetch_issue_labels", spy)
|
||||
|
||||
repo = "orchestrator"
|
||||
fired = labels.auto_approve_applies(repo) and labels.has_label("ORCH-1", "autoApprove")
|
||||
assert fired is False
|
||||
assert called["fetch"] == 0 # has_label never reached -> zero network
|
||||
|
||||
|
||||
def test_snapshot_never_raises():
|
||||
snap = labels.snapshot()
|
||||
assert set(snap) >= {"enabled", "approve_label", "deploy_label", "repos"}
|
||||
214
tests/test_orch_52b_docs_standard.py
Normal file
214
tests/test_orch_52b_docs_standard.py
Normal file
@@ -0,0 +1,214 @@
|
||||
"""ORCH-075 (ORCH-52b) — структурные проверки стандарта документов конвейера.
|
||||
|
||||
Docs-only задача: проверяется НАЛИЧИЕ и СТРУКТУРА новых файлов-стандартов/шаблонов
|
||||
(`docs/_standards/PIPELINE_DOCS.md`, `docs/_templates/*`) и обновление точек-ссылок
|
||||
(`CLAUDE.md`, `docs/architecture/README.md`, `CHANGELOG.md`). Тесты НЕ меняют
|
||||
`QG_CHECKS`/`STAGE_TRANSITIONS` и не вводят новый гейт (это ORCH-52c).
|
||||
|
||||
Покрытие тест-плана 04-test-plan.yaml: TC-01…TC-20 (TC-21 — полный регресс `pytest tests/`).
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
STANDARDS = REPO_ROOT / "docs" / "_standards"
|
||||
TEMPLATES = REPO_ROOT / "docs" / "_templates"
|
||||
MANIFEST = STANDARDS / "PIPELINE_DOCS.md"
|
||||
|
||||
# Все номерные доки реального набора (TRZ §FR-1, AC-1).
|
||||
NUMBERED_DOCS = ["00", "01", "02", "03", "04", "06", "07", "08", "10",
|
||||
"12", "13", "14", "15", "16", "17"]
|
||||
|
||||
# Шаблоны required/when-applicable доков (TRZ §2, AC-2).
|
||||
TEMPLATE_FILES = [
|
||||
"00-business-request.md",
|
||||
"01-brd.md",
|
||||
"02-trz.md",
|
||||
"03-acceptance-criteria.md",
|
||||
"04-test-plan.yaml",
|
||||
"06-adr-ADR-NNN-slug.md",
|
||||
"07-infra-requirements.md",
|
||||
"08-data-requirements.md",
|
||||
"10-tech-risks.md",
|
||||
"12-review.md",
|
||||
"13-test-report.md",
|
||||
"14-deploy-log.md",
|
||||
"15-staging-log.md",
|
||||
"16-post-deploy-log.md",
|
||||
"17-security-report.md",
|
||||
]
|
||||
|
||||
|
||||
def _read(path: Path) -> str:
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
# --- TC-01 -----------------------------------------------------------------
|
||||
def test_tc01_manifest_exists_and_nonempty():
|
||||
"""docs/_standards/PIPELINE_DOCS.md существует и непустой."""
|
||||
assert MANIFEST.is_file(), f"манифест не найден: {MANIFEST}"
|
||||
assert len(_read(MANIFEST).strip()) > 0, "манифест пустой"
|
||||
|
||||
|
||||
# --- TC-02 -----------------------------------------------------------------
|
||||
def test_tc02_manifest_mentions_all_numbered_docs():
|
||||
"""Манифест упоминает все номерные доки набора."""
|
||||
content = _read(MANIFEST)
|
||||
missing = []
|
||||
for num in NUMBERED_DOCS:
|
||||
# Документ упоминается как `NN-...` или `NN-adr` — ищем по префиксу `NN-`.
|
||||
if f"{num}-" not in content:
|
||||
missing.append(num)
|
||||
assert not missing, f"в манифесте не упомянуты доки: {missing}"
|
||||
|
||||
|
||||
# --- TC-03 -----------------------------------------------------------------
|
||||
def test_tc03_manifest_lists_owner_agents():
|
||||
"""Манифест указывает владельцев-агентов конвейера (TC-03: analyst/architect/
|
||||
reviewer/tester/deployer/система). `developer` не владеет номерным доком — пишет код+PR."""
|
||||
content = _read(MANIFEST).lower()
|
||||
for agent in ["analyst", "architect", "reviewer", "tester", "deployer"]:
|
||||
assert agent in content, f"в манифесте нет владельца-агента: {agent}"
|
||||
assert "систем" in content, "в манифесте нет системного владельца (Plane webhook)"
|
||||
|
||||
|
||||
# --- TC-04 -----------------------------------------------------------------
|
||||
def test_tc04_manifest_has_categories():
|
||||
"""Манифест содержит категории required / when-applicable / optional."""
|
||||
content = _read(MANIFEST)
|
||||
for category in ["required", "when-applicable", "optional"]:
|
||||
assert category in content, f"в манифесте нет категории: {category}"
|
||||
|
||||
|
||||
# --- TC-05 -----------------------------------------------------------------
|
||||
def test_tc05_templates_dir_has_all_templates():
|
||||
"""docs/_templates/ существует и содержит шаблоны для всех required/when-applicable доков."""
|
||||
assert TEMPLATES.is_dir(), f"каталог шаблонов не найден: {TEMPLATES}"
|
||||
missing = [name for name in TEMPLATE_FILES if not (TEMPLATES / name).is_file()]
|
||||
assert not missing, f"отсутствуют шаблоны: {missing}"
|
||||
|
||||
|
||||
# --- TC-06..TC-11 — frontmatter machine-keys -------------------------------
|
||||
def _assert_frontmatter_key(template_name: str, key: str):
|
||||
content = _read(TEMPLATES / template_name)
|
||||
assert content.lstrip().startswith("---"), f"{template_name}: нет YAML-frontmatter"
|
||||
head = content.split("---", 2)
|
||||
assert len(head) >= 3, f"{template_name}: frontmatter не закрыт"
|
||||
assert f"{key}:" in head[1], f"{template_name}: нет machine-key `{key}:` во frontmatter"
|
||||
|
||||
|
||||
def test_tc06_review_template_has_verdict():
|
||||
"""Шаблон 12-review содержит frontmatter-ключ verdict:."""
|
||||
_assert_frontmatter_key("12-review.md", "verdict")
|
||||
|
||||
|
||||
def test_tc07_test_report_template_has_result():
|
||||
"""Шаблон 13-test-report содержит frontmatter-ключ result:."""
|
||||
_assert_frontmatter_key("13-test-report.md", "result")
|
||||
|
||||
|
||||
def test_tc08_deploy_log_template_has_deploy_status():
|
||||
"""Шаблон 14-deploy-log содержит frontmatter-ключ deploy_status:."""
|
||||
_assert_frontmatter_key("14-deploy-log.md", "deploy_status")
|
||||
|
||||
|
||||
def test_tc09_staging_log_template_has_staging_status():
|
||||
"""Шаблон 15-staging-log содержит frontmatter-ключ staging_status:."""
|
||||
_assert_frontmatter_key("15-staging-log.md", "staging_status")
|
||||
|
||||
|
||||
def test_tc10_security_report_template_has_security_status():
|
||||
"""Шаблон 17-security-report содержит frontmatter-ключ security_status:."""
|
||||
_assert_frontmatter_key("17-security-report.md", "security_status")
|
||||
|
||||
|
||||
def test_tc11_post_deploy_template_has_post_deploy_status():
|
||||
"""Шаблон 16-post-deploy-log содержит frontmatter-ключ post_deploy_status:."""
|
||||
_assert_frontmatter_key("16-post-deploy-log.md", "post_deploy_status")
|
||||
|
||||
|
||||
# --- TC-12 -----------------------------------------------------------------
|
||||
def test_tc12_brd_template_has_mandatory_sections():
|
||||
"""Шаблон 01-brd содержит обязательные секции (TRZ §FR-2.1)."""
|
||||
content = _read(TEMPLATES / "01-brd.md")
|
||||
for section in ["Бизнес-контекст", "Объём", "Бизнес-требования", "Нефункциональные требования"]:
|
||||
assert section in content, f"01-brd: нет секции `{section}`"
|
||||
|
||||
|
||||
# --- TC-13 -----------------------------------------------------------------
|
||||
def test_tc13_trz_template_has_mandatory_sections():
|
||||
"""Шаблон 02-trz содержит обязательные секции (TRZ §FR-2.1)."""
|
||||
content = _read(TEMPLATES / "02-trz.md")
|
||||
for section in ["Задействованные модули", "Изменения API", "Изменения схемы БД",
|
||||
"QG checks"]:
|
||||
assert section in content, f"02-trz: нет секции `{section}`"
|
||||
|
||||
|
||||
# --- TC-14 -----------------------------------------------------------------
|
||||
def test_tc14_ac_template_has_pass_fail_block():
|
||||
"""Шаблон 03-acceptance-criteria содержит блок AC-N с метками PASS и FAIL."""
|
||||
content = _read(TEMPLATES / "03-acceptance-criteria.md")
|
||||
assert "AC-1" in content, "03-ac: нет блока AC-N"
|
||||
assert "**PASS:**" in content, "03-ac: нет метки PASS"
|
||||
assert "**FAIL:**" in content, "03-ac: нет метки FAIL"
|
||||
|
||||
|
||||
# --- TC-15 -----------------------------------------------------------------
|
||||
def test_tc15_test_plan_template_is_valid_yaml():
|
||||
"""Шаблон 04-test-plan.yaml — валидный YAML с work_item и списком tests."""
|
||||
data = yaml.safe_load(_read(TEMPLATES / "04-test-plan.yaml"))
|
||||
assert isinstance(data, dict), "04-test-plan: корень не словарь"
|
||||
assert "work_item" in data, "04-test-plan: нет ключа work_item"
|
||||
assert isinstance(data.get("tests"), list) and data["tests"], "04-test-plan: tests не список"
|
||||
first = data["tests"][0]
|
||||
for key in ["id", "type", "description", "module", "expected"]:
|
||||
assert key in first, f"04-test-plan: в элементе tests нет ключа `{key}`"
|
||||
|
||||
|
||||
# --- TC-16 -----------------------------------------------------------------
|
||||
def test_tc16_adr_naming_section_present():
|
||||
"""Раздел ADR-naming фиксирует формат ADR-NNN-<slug>.md с нумерацией с 001 и kebab-slug."""
|
||||
content = _read(MANIFEST)
|
||||
assert "ADR-NNN-" in content, "нет формата ADR-NNN-<slug>.md"
|
||||
assert "06-adr" in content, "нет пути 06-adr/"
|
||||
assert "001" in content, "не зафиксирована нумерация с 001"
|
||||
assert "kebab" in content.lower(), "нет правила kebab-case для slug"
|
||||
|
||||
|
||||
# --- TC-17 -----------------------------------------------------------------
|
||||
def test_tc17_adr_naming_matches_real_repo():
|
||||
"""ADR-naming совпадает с реальными ADR в репо (пример из манифеста существует)."""
|
||||
# Манифест приводит ORCH-088 как пример; реальный файл должен существовать.
|
||||
adr_dir = REPO_ROOT / "docs" / "work-items" / "ORCH-088" / "06-adr"
|
||||
real = list(adr_dir.glob("ADR-001-*.md"))
|
||||
assert real, f"реальный ADR-001-*.md не найден в {adr_dir}"
|
||||
# И глобальный реестр следует 4-значной конвенции.
|
||||
global_adr = REPO_ROOT / "docs" / "architecture" / "adr"
|
||||
assert list(global_adr.glob("adr-0019-*.md")), "глобальный adr-0019-*.md не найден"
|
||||
|
||||
|
||||
# --- TC-18 -----------------------------------------------------------------
|
||||
def test_tc18_claude_md_links_standard():
|
||||
"""CLAUDE.md содержит ссылку на docs/_standards/PIPELINE_DOCS.md."""
|
||||
content = _read(REPO_ROOT / "CLAUDE.md")
|
||||
assert "docs/_standards/PIPELINE_DOCS.md" in content
|
||||
|
||||
|
||||
# --- TC-19 -----------------------------------------------------------------
|
||||
def test_tc19_architecture_readme_links_standard():
|
||||
"""docs/architecture/README.md содержит ссылку на стандарт документов."""
|
||||
content = _read(REPO_ROOT / "docs" / "architecture" / "README.md")
|
||||
assert "PIPELINE_DOCS.md" in content
|
||||
|
||||
|
||||
# --- TC-20 -----------------------------------------------------------------
|
||||
def test_tc20_changelog_mentions_task():
|
||||
"""CHANGELOG.md содержит запись об ORCH-52b/ORCH-075 в разделе Unreleased."""
|
||||
content = _read(REPO_ROOT / "CHANGELOG.md")
|
||||
unreleased = content.split("## [Unreleased]", 1)
|
||||
assert len(unreleased) == 2, "нет раздела ## [Unreleased]"
|
||||
# Берём срез до следующего релизного заголовка, если он есть.
|
||||
body = unreleased[1].split("\n## ", 1)[0]
|
||||
assert "ORCH-075" in body or "ORCH-52b" in body, "в Unreleased нет записи ORCH-075/ORCH-52b"
|
||||
164
tests/test_plane_sync_labels.py
Normal file
164
tests/test_plane_sync_labels.py
Normal file
@@ -0,0 +1,164 @@
|
||||
"""ORCH-089 — plane_sync: label reading + Approved setter (offline, httpx mocked).
|
||||
|
||||
Covers (04-test-plan.yaml):
|
||||
TC-07 fetch_issue_labels parses the issue's `labels` field; get_project_labels
|
||||
resolves {normalized_name -> uuid}.
|
||||
TC-08 the project label-map is cached with a TTL (a repeat inside the TTL window
|
||||
makes no second GET).
|
||||
TC-09 set_issue_approved PATCHes the issue to the Approved UUID; never-raise.
|
||||
"""
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
os.environ.setdefault("ORCH_DB_PATH", os.path.join(tempfile.gettempdir(), "test_ps_labels.db"))
|
||||
os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token")
|
||||
os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token")
|
||||
|
||||
from unittest.mock import MagicMock # noqa: E402
|
||||
|
||||
from src import plane_sync as ps # noqa: E402
|
||||
|
||||
|
||||
def _resp(json_body):
|
||||
m = MagicMock()
|
||||
m.json.return_value = json_body
|
||||
m.raise_for_status.return_value = None
|
||||
return m
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def fresh_cache(monkeypatch):
|
||||
ps.reload_project_labels()
|
||||
monkeypatch.setattr(ps, "_resolve_project_id", lambda w=None, p=None: "proj-1")
|
||||
monkeypatch.setattr(ps.settings, "auto_label_states_ttl_s", 300, raising=False)
|
||||
yield
|
||||
ps.reload_project_labels()
|
||||
|
||||
|
||||
# --- TC-07: fetch_issue_labels + get_project_labels ------------------------
|
||||
def test_tc07_fetch_issue_labels(monkeypatch):
|
||||
monkeypatch.setattr(ps, "find_issue_id", lambda w, p=None: "issue-uuid")
|
||||
monkeypatch.setattr(
|
||||
ps.httpx, "get",
|
||||
lambda *a, **k: _resp({"labels": ["uuid-A", "uuid-B"]}),
|
||||
)
|
||||
assert ps.fetch_issue_labels("ORCH-1") == ["uuid-A", "uuid-B"]
|
||||
|
||||
|
||||
def test_tc07_fetch_issue_labels_not_found(monkeypatch):
|
||||
# Issue not resolvable -> None (distinct from [] = "no labels").
|
||||
monkeypatch.setattr(ps, "find_issue_id", lambda w, p=None: None)
|
||||
assert ps.fetch_issue_labels("ORCH-404") is None
|
||||
|
||||
|
||||
def test_tc07_fetch_issue_labels_api_error(monkeypatch):
|
||||
monkeypatch.setattr(ps, "find_issue_id", lambda w, p=None: "issue-uuid")
|
||||
monkeypatch.setattr(ps.httpx, "get", MagicMock(side_effect=Exception("boom")))
|
||||
assert ps.fetch_issue_labels("ORCH-1") is None # never-raise
|
||||
|
||||
|
||||
def test_tc07_get_project_labels_normalized(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
ps.httpx, "get",
|
||||
lambda *a, **k: _resp({"results": [
|
||||
{"id": "uuid-A", "name": "autoApprove"},
|
||||
{"id": "uuid-B", "name": "Auto Deploy"},
|
||||
]}),
|
||||
)
|
||||
m = ps.get_project_labels("proj-1")
|
||||
assert m["autoapprove"] == "uuid-A"
|
||||
assert m["auto deploy"] == "uuid-B"
|
||||
|
||||
|
||||
def test_tc07_get_project_labels_ambiguous(monkeypatch):
|
||||
# Two distinct labels collapse to the same normalized name -> sentinel.
|
||||
monkeypatch.setattr(
|
||||
ps.httpx, "get",
|
||||
lambda *a, **k: _resp([
|
||||
{"id": "uuid-A", "name": "autoApprove"},
|
||||
{"id": "uuid-B", "name": "AUTOAPPROVE"},
|
||||
]),
|
||||
)
|
||||
m = ps.get_project_labels("proj-1")
|
||||
assert m["autoapprove"] == "__AMBIGUOUS__"
|
||||
|
||||
|
||||
def test_tc07_get_project_labels_api_error_empty(monkeypatch):
|
||||
monkeypatch.setattr(ps.httpx, "get", MagicMock(side_effect=Exception("down")))
|
||||
assert ps.get_project_labels("proj-1") == {} # never-raise, no cache -> {}
|
||||
|
||||
|
||||
# --- TC-08: TTL cache ------------------------------------------------------
|
||||
def test_tc08_label_map_cached_within_ttl(monkeypatch):
|
||||
clock = {"t": 1000.0}
|
||||
monkeypatch.setattr(ps.time, "monotonic", lambda: clock["t"])
|
||||
mock_get = MagicMock(side_effect=lambda *a, **k: _resp(
|
||||
{"results": [{"id": "uuid-A", "name": "autoApprove"}]}
|
||||
))
|
||||
monkeypatch.setattr(ps.httpx, "get", mock_get)
|
||||
|
||||
ps.get_project_labels("proj-1")
|
||||
ps.get_project_labels("proj-1") # within TTL -> served from cache
|
||||
assert mock_get.call_count == 1
|
||||
|
||||
# Past the TTL -> refetch.
|
||||
clock["t"] += 301
|
||||
ps.get_project_labels("proj-1")
|
||||
assert mock_get.call_count == 2
|
||||
|
||||
|
||||
def test_tc08_ttl_zero_lifetime_cache(monkeypatch):
|
||||
monkeypatch.setattr(ps.settings, "auto_label_states_ttl_s", 0, raising=False)
|
||||
clock = {"t": 1000.0}
|
||||
monkeypatch.setattr(ps.time, "monotonic", lambda: clock["t"])
|
||||
mock_get = MagicMock(side_effect=lambda *a, **k: _resp(
|
||||
[{"id": "uuid-A", "name": "autoApprove"}]
|
||||
))
|
||||
monkeypatch.setattr(ps.httpx, "get", mock_get)
|
||||
ps.get_project_labels("proj-1")
|
||||
clock["t"] += 100000
|
||||
ps.get_project_labels("proj-1")
|
||||
assert mock_get.call_count == 1 # lifetime cache, never expires
|
||||
|
||||
|
||||
def test_tc08_stale_served_on_refresh_failure(monkeypatch):
|
||||
clock = {"t": 1000.0}
|
||||
monkeypatch.setattr(ps.time, "monotonic", lambda: clock["t"])
|
||||
responses = iter([
|
||||
_resp({"results": [{"id": "uuid-A", "name": "autoApprove"}]}),
|
||||
Exception("transient"),
|
||||
])
|
||||
|
||||
def flaky(*a, **k):
|
||||
r = next(responses)
|
||||
if isinstance(r, Exception):
|
||||
raise r
|
||||
return r
|
||||
monkeypatch.setattr(ps.httpx, "get", flaky)
|
||||
ps.get_project_labels("proj-1")
|
||||
clock["t"] += 301 # force a refresh that fails -> stale map served
|
||||
m = ps.get_project_labels("proj-1")
|
||||
assert m["autoapprove"] == "uuid-A"
|
||||
|
||||
|
||||
# --- TC-09: set_issue_approved ---------------------------------------------
|
||||
def test_tc09_set_issue_approved_patches_approved_uuid(monkeypatch):
|
||||
monkeypatch.setattr(ps, "get_project_states", lambda pid: {"approved": "approved-uuid"})
|
||||
monkeypatch.setattr(ps, "find_issue_id", lambda w, p=None: "issue-uuid")
|
||||
patch_spy = MagicMock(return_value=_resp({}))
|
||||
monkeypatch.setattr(ps.httpx, "patch", patch_spy)
|
||||
|
||||
ps.set_issue_approved("ORCH-1")
|
||||
|
||||
patch_spy.assert_called_once()
|
||||
assert patch_spy.call_args.kwargs["json"] == {"state": "approved-uuid"}
|
||||
|
||||
|
||||
def test_tc09_set_issue_approved_never_raises(monkeypatch):
|
||||
monkeypatch.setattr(ps, "get_project_states", lambda pid: {"approved": "approved-uuid"})
|
||||
monkeypatch.setattr(ps, "find_issue_id", lambda w, p=None: "issue-uuid")
|
||||
monkeypatch.setattr(ps.httpx, "patch", MagicMock(side_effect=Exception("boom")))
|
||||
# Must not raise.
|
||||
ps.set_issue_approved("ORCH-1")
|
||||
202
tests/test_qg_verdicts.py
Normal file
202
tests/test_qg_verdicts.py
Normal file
@@ -0,0 +1,202 @@
|
||||
"""ORCH-076 (ORCH-52c): anti-regression for the five verdict gates after the parse
|
||||
is delegated to the unified frontmatter API.
|
||||
|
||||
Covers TC-08..TC-15 of docs/work-items/ORCH-076/04-test-plan.yaml. The MECHANISM of
|
||||
YAML-frontmatter parsing is now centralised in ``src/frontmatter.parse_frontmatter``,
|
||||
but the verdict SEMANTICS (value -> transition) must be 1:1 with before the task:
|
||||
* check_reviewer_verdict (12-review.md, verdict:) — TC-08
|
||||
* _parse_tests_verdict (13-test-report.md, result/verdict/status, ORCH-047) — TC-09
|
||||
* _parse_deploy_status (14-deploy-log.md, deploy_status:) — TC-10
|
||||
* _parse_staging_status (15-staging-log.md, staging_status:) — TC-11
|
||||
* (parse_security_status is exercised in tests/test_security_gate.py — TC-12)
|
||||
* backward-compat of old docs (no new schema) + additive schema — TC-13 / TC-14
|
||||
* worktree -> origin/main fallback preserved — TC-15
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token")
|
||||
os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token")
|
||||
|
||||
from src.qg import checks as qg # noqa: E402
|
||||
from src.qg.checks import ( # noqa: E402
|
||||
_parse_deploy_status,
|
||||
_parse_staging_status,
|
||||
_parse_tests_verdict,
|
||||
check_deploy_status,
|
||||
check_reviewer_verdict,
|
||||
check_staging_status,
|
||||
)
|
||||
|
||||
|
||||
def _write(dirpath, work_item_id, name, content):
|
||||
d = os.path.join(dirpath, "docs", "work-items", work_item_id)
|
||||
os.makedirs(d, exist_ok=True)
|
||||
with open(os.path.join(d, name), "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# TC-08 — check_reviewer_verdict via the unified API.
|
||||
# --------------------------------------------------------------------------- #
|
||||
def test_tc08_reviewer_verdict_semantics(monkeypatch):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
monkeypatch.setattr(qg, "_repo_path", lambda repo, branch=None: d)
|
||||
_write(d, "ORCH-1", "12-review.md", "---\nverdict: APPROVED\n---\nbody")
|
||||
ok, reason = check_reviewer_verdict("orchestrator", "ORCH-1")
|
||||
assert ok is True and "APPROVED" in reason
|
||||
|
||||
_write(d, "ORCH-1", "12-review.md", "---\nverdict: REQUEST_CHANGES\n---\nbody")
|
||||
ok, reason = check_reviewer_verdict("orchestrator", "ORCH-1")
|
||||
assert ok is False and "REQUEST_CHANGES" in reason
|
||||
|
||||
# Missing verdict key -> (False).
|
||||
_write(d, "ORCH-1", "12-review.md", "---\nother: x\n---\nbody")
|
||||
ok, _ = check_reviewer_verdict("orchestrator", "ORCH-1")
|
||||
assert ok is False
|
||||
|
||||
# No frontmatter at all -> (False).
|
||||
_write(d, "ORCH-1", "12-review.md", "# review\nAPPROVED in prose\n")
|
||||
ok, _ = check_reviewer_verdict("orchestrator", "ORCH-1")
|
||||
assert ok is False
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# TC-09 — _parse_tests_verdict: ORCH-047 three equal-rank fields + negative-token
|
||||
# priority preserved.
|
||||
# --------------------------------------------------------------------------- #
|
||||
def test_tc09_tests_three_fields_each_pass():
|
||||
for field in ("result", "verdict", "status"):
|
||||
ok, reason = _parse_tests_verdict(f"---\n{field}: PASS\n---\nbody")
|
||||
assert ok is True, field
|
||||
assert "PASS" in reason
|
||||
|
||||
|
||||
def test_tc09_negative_token_is_authoritative():
|
||||
# BLOCKED in one field beats a positive token in another (ET-013 case).
|
||||
ok, reason = _parse_tests_verdict("---\nverdict: BLOCKED\nstatus: PASS\n---\n")
|
||||
assert ok is False
|
||||
assert "BLOCKED" in reason
|
||||
# FAIL likewise.
|
||||
ok, _ = _parse_tests_verdict("---\nresult: FAIL\n---\n")
|
||||
assert ok is False
|
||||
|
||||
|
||||
def test_tc09_tests_no_frontmatter_and_malformed():
|
||||
ok, reason = _parse_tests_verdict("no frontmatter, 23 passed\n")
|
||||
assert ok is False and "No YAML frontmatter" in reason
|
||||
ok, reason = _parse_tests_verdict("---\nresult: PASS\nunterminated")
|
||||
assert ok is False and "Malformed" in reason
|
||||
ok, reason = _parse_tests_verdict("---\nresult: PASS\nempty:\n---\n")
|
||||
assert ok is True # result PASS still wins; empty other field is fine
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# TC-10 — _parse_deploy_status semantics (БАГ-8) unchanged.
|
||||
# --------------------------------------------------------------------------- #
|
||||
def test_tc10_deploy_status_semantics():
|
||||
assert _parse_deploy_status("---\ndeploy_status: SUCCESS\n---\n")[0] is True
|
||||
assert _parse_deploy_status("---\ndeploy_status: FAILED\n---\n")[0] is False
|
||||
assert _parse_deploy_status("---\nother: SUCCESS\n---\n")[0] is False
|
||||
assert _parse_deploy_status("prose only SUCCESS")[0] is False
|
||||
# Bad YAML -> (False) with the preserved reason prefix.
|
||||
ok, reason = _parse_deploy_status("---\nx: : :\n---\n")
|
||||
assert ok is False and "Invalid YAML frontmatter in deploy log" in reason
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# TC-11 — _parse_staging_status + ORCH-35 conditionality (non-self -> N/A pass).
|
||||
# --------------------------------------------------------------------------- #
|
||||
def test_tc11_staging_status_semantics():
|
||||
assert _parse_staging_status("---\nstaging_status: SUCCESS\n---\n")[0] is True
|
||||
assert _parse_staging_status("---\nstaging_status: FAILED\n---\n")[0] is False
|
||||
assert _parse_staging_status("---\nother: SUCCESS\n---\n")[0] is False
|
||||
|
||||
|
||||
def test_tc11_staging_gate_na_for_non_self():
|
||||
ok, reason = check_staging_status("enduro-trails", "ET-1", "feature/x")
|
||||
assert ok is True
|
||||
assert "N/A" in reason
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# TC-13 — old verdict docs WITHOUT the new schema read exactly as before, for
|
||||
# every parser.
|
||||
# --------------------------------------------------------------------------- #
|
||||
def test_tc13_old_docs_without_schema_still_read():
|
||||
# None of these carry work_item/stage/author_agent/status/created_at/model_used.
|
||||
assert _parse_tests_verdict("---\nresult: PASS\n---\n")[0] is True
|
||||
assert _parse_tests_verdict("---\nverdict: FAIL\n---\n")[0] is False
|
||||
assert _parse_deploy_status("---\ndeploy_status: SUCCESS\n---\n")[0] is True
|
||||
assert _parse_staging_status("---\nstaging_status: SUCCESS\n---\n")[0] is True
|
||||
|
||||
|
||||
def test_tc13_reviewer_old_doc(monkeypatch):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
monkeypatch.setattr(qg, "_repo_path", lambda repo, branch=None: d)
|
||||
_write(d, "ORCH-1", "12-review.md", "---\nverdict: APPROVED\n---\nlegacy body")
|
||||
assert check_reviewer_verdict("orchestrator", "ORCH-1")[0] is True
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# TC-14 — a doc WITH the full additive schema + verdict key yields the SAME
|
||||
# verdict as without the schema (schema is additive, never changes it).
|
||||
# --------------------------------------------------------------------------- #
|
||||
_SCHEMA = (
|
||||
"work_item: ORCH-076\nstage: testing\nauthor_agent: tester\n"
|
||||
"status: PASS\ncreated_at: 2026-06-09\nmodel_used: claude-opus-4-8\n"
|
||||
)
|
||||
|
||||
|
||||
def test_tc14_full_schema_does_not_change_verdict():
|
||||
bare = _parse_tests_verdict("---\nresult: PASS\n---\n")
|
||||
full = _parse_tests_verdict(f"---\n{_SCHEMA}result: PASS\n---\n")
|
||||
assert bare[0] == full[0] is True
|
||||
|
||||
bare_d = _parse_deploy_status("---\ndeploy_status: FAILED\n---\n")
|
||||
full_d = _parse_deploy_status(
|
||||
"---\nwork_item: ORCH-076\nstage: deploy\nauthor_agent: deployer\n"
|
||||
"status: done\ncreated_at: 2026-06-09\nmodel_used: claude-opus-4-8\n"
|
||||
"deploy_status: FAILED\n---\n"
|
||||
)
|
||||
assert bare_d[0] == full_d[0] is False
|
||||
|
||||
|
||||
def test_tc14_reviewer_with_schema(monkeypatch):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
monkeypatch.setattr(qg, "_repo_path", lambda repo, branch=None: d)
|
||||
_write(
|
||||
d, "ORCH-1", "12-review.md",
|
||||
"---\nwork_item: ORCH-1\nstage: review\nauthor_agent: reviewer\n"
|
||||
"status: APPROVED\ncreated_at: 2026-06-09\nmodel_used: claude-opus-4-8\n"
|
||||
"verdict: APPROVED\n---\nbody",
|
||||
)
|
||||
assert check_reviewer_verdict("orchestrator", "ORCH-1")[0] is True
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# TC-15 — fallback worktree -> origin/main preserved (the gate still reads the
|
||||
# log recovered from main through the unified parser).
|
||||
# --------------------------------------------------------------------------- #
|
||||
def test_tc15_deploy_status_origin_main_fallback(monkeypatch):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
# No 14-deploy-log.md in the worktree -> the gate must consult origin/main.
|
||||
monkeypatch.setattr(qg, "_repo_path", lambda repo, branch=None: d)
|
||||
monkeypatch.setattr(
|
||||
qg, "_deploy_log_from_main",
|
||||
lambda repo, wi: "---\ndeploy_status: SUCCESS\n---\nfrom main",
|
||||
)
|
||||
ok, reason = check_deploy_status("orchestrator", "ORCH-1", "feature/x")
|
||||
assert ok is True and "SUCCESS" in reason
|
||||
|
||||
|
||||
def test_tc15_staging_status_origin_main_fallback(monkeypatch):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
monkeypatch.setattr(qg, "_repo_path", lambda repo, branch=None: d)
|
||||
monkeypatch.setattr(
|
||||
qg, "_staging_log_from_main",
|
||||
lambda repo, wi: "---\nstaging_status: FAILED\n---\nfrom main",
|
||||
)
|
||||
ok, reason = check_staging_status("orchestrator", "ORCH-1", "feature/x")
|
||||
assert ok is False and "FAILED" in reason
|
||||
@@ -202,6 +202,32 @@ def test_tc09_missing_or_broken_frontmatter_failclosed():
|
||||
assert ok is False
|
||||
|
||||
|
||||
def test_orch076_parse_security_status_via_unified_api():
|
||||
"""ORCH-076 TC-12: parse_security_status now reads through the unified
|
||||
frontmatter primitive; the PASS/FAIL semantics are 1:1 with before, and an
|
||||
old report WITHOUT the new schema fields still reads exactly the same."""
|
||||
from src import frontmatter as fm
|
||||
|
||||
# Delegates to the single parse primitive (no private duplicated parse).
|
||||
assert "parse_frontmatter" in sg.parse_security_status.__doc__
|
||||
|
||||
# PASS / FAIL semantics preserved.
|
||||
assert sg.parse_security_status("---\nsecurity_status: PASS\n---\n")[0] is True
|
||||
assert sg.parse_security_status("---\nsecurity_status: FAIL\n---\n")[0] is False
|
||||
|
||||
# An additive full schema does not change the verdict (FR-5 / AC-4).
|
||||
schema = (
|
||||
"work_item: ORCH-076\nstage: deploy-staging\nauthor_agent: deployer\n"
|
||||
"status: PASS\ncreated_at: 2026-06-09\nmodel_used: claude-opus-4-8\n"
|
||||
)
|
||||
with_schema = fm.render_frontmatter(
|
||||
{**{k.split(":")[0]: v for k, v in
|
||||
(line.split(": ", 1) for line in schema.strip().splitlines())},
|
||||
"security_status": "PASS"}
|
||||
)
|
||||
assert sg.parse_security_status(with_schema)[0] is True
|
||||
|
||||
|
||||
def test_tc10_artifact_has_valid_frontmatter_and_body(tmp_path, monkeypatch):
|
||||
"""TC-10: 17-security-report.md is written with valid frontmatter (all machine
|
||||
fields) and a body listing the findings; read-back == the written verdict."""
|
||||
|
||||
55
tests/test_stages_invariants.py
Normal file
55
tests/test_stages_invariants.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""ORCH-076 (ORCH-52c) TC-16 / AC-6: the pipeline invariants are untouched.
|
||||
|
||||
ORCH-52c only unifies the MECHANISM of YAML-frontmatter parsing behind a single
|
||||
API; it must NOT change the composition of the stage machine or the QG registry.
|
||||
This guard fails first if a future edit drifts either contract.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token")
|
||||
os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token")
|
||||
|
||||
from src.qg.checks import QG_CHECKS # noqa: E402
|
||||
from src.stages import STAGE_TRANSITIONS # noqa: E402
|
||||
|
||||
_EXPECTED_QGS = {
|
||||
"check_analysis_approved",
|
||||
"check_analysis_complete",
|
||||
"check_architecture_done",
|
||||
"check_ci_green",
|
||||
"check_review_approved",
|
||||
"check_tests_passed",
|
||||
"check_reviewer_verdict",
|
||||
"check_tests_local",
|
||||
"check_deploy_status",
|
||||
"check_staging_status",
|
||||
"check_branch_mergeable",
|
||||
"check_staging_image_fresh",
|
||||
"check_security_gate",
|
||||
}
|
||||
|
||||
_EXPECTED_TRANSITIONS = {
|
||||
"created": {"next": "analysis", "agent": "analyst", "qg": None},
|
||||
"analysis": {"next": "architecture", "agent": "architect", "qg": "check_analysis_approved"},
|
||||
"architecture": {"next": "development", "agent": "developer", "qg": "check_architecture_done"},
|
||||
"development": {"next": "review", "agent": "reviewer", "qg": "check_ci_green"},
|
||||
"review": {"next": "testing", "agent": "tester", "qg": "check_reviewer_verdict"},
|
||||
"testing": {"next": "deploy-staging", "agent": "deployer", "qg": "check_tests_passed"},
|
||||
"deploy-staging": {"next": "deploy", "agent": "deployer", "qg": "check_staging_status"},
|
||||
"deploy": {"next": "done", "agent": None, "qg": "check_deploy_status"},
|
||||
"done": {"next": None, "agent": None, "qg": None},
|
||||
}
|
||||
|
||||
|
||||
def test_tc16_qg_registry_unchanged():
|
||||
assert set(QG_CHECKS.keys()) == _EXPECTED_QGS
|
||||
|
||||
|
||||
def test_tc16_qg_callables():
|
||||
for name, fn in QG_CHECKS.items():
|
||||
assert callable(fn), f"QG {name} is not callable"
|
||||
|
||||
|
||||
def test_tc16_stage_transitions_unchanged():
|
||||
assert STAGE_TRANSITIONS == _EXPECTED_TRANSITIONS
|
||||
Reference in New Issue
Block a user