From 561f58abe040f39ec3a95cbdd2fdbcdcb240f940 Mon Sep 17 00:00:00 2001 From: Slava Date: Tue, 9 Jun 2026 13:36:53 +0300 Subject: [PATCH 1/7] docs: init ORCH-076 business request --- docs/work-items/ORCH-076/00-business-request.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 docs/work-items/ORCH-076/00-business-request.md diff --git a/docs/work-items/ORCH-076/00-business-request.md b/docs/work-items/ORCH-076/00-business-request.md new file mode 100644 index 0000000..03644ab --- /dev/null +++ b/docs/work-items/ORCH-076/00-business-request.md @@ -0,0 +1,7 @@ +# Business Request: ORCH-52c: протокол handoff + frontmatter-контракт (writer/валидатор/схема) + +Work Item ID: ORCH-076 + +## Description + +TBD -- 2.49.1 From 98c50a094befd9eb1e44f4c5269721cdfca50888 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Tue, 9 Jun 2026 13:40:55 +0300 Subject: [PATCH 2/7] analyst(ET): auto-commit from analyst run_id=453 --- docs/work-items/ORCH-076/01-brd.md | 151 ++++++++++++++++++ docs/work-items/ORCH-076/02-trz.md | 124 ++++++++++++++ .../ORCH-076/03-acceptance-criteria.md | 104 ++++++++++++ docs/work-items/ORCH-076/04-test-plan.yaml | 122 ++++++++++++++ 4 files changed, 501 insertions(+) create mode 100644 docs/work-items/ORCH-076/01-brd.md create mode 100644 docs/work-items/ORCH-076/02-trz.md create mode 100644 docs/work-items/ORCH-076/03-acceptance-criteria.md create mode 100644 docs/work-items/ORCH-076/04-test-plan.yaml diff --git a/docs/work-items/ORCH-076/01-brd.md b/docs/work-items/ORCH-076/01-brd.md new file mode 100644 index 0000000..729d6dd --- /dev/null +++ b/docs/work-items/ORCH-076/01-brd.md @@ -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`. diff --git a/docs/work-items/ORCH-076/02-trz.md b/docs/work-items/ORCH-076/02-trz.md new file mode 100644 index 0000000..bf2632f --- /dev/null +++ b/docs/work-items/ORCH-076/02-trz.md @@ -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`. Формат на 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). diff --git a/docs/work-items/ORCH-076/03-acceptance-criteria.md b/docs/work-items/ORCH-076/03-acceptance-criteria.md new file mode 100644 index 0000000..978b7f2 --- /dev/null +++ b/docs/work-items/ORCH-076/03-acceptance-criteria.md @@ -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 | diff --git a/docs/work-items/ORCH-076/04-test-plan.yaml b/docs/work-items/ORCH-076/04-test-plan.yaml new file mode 100644 index 0000000..d50ef2b --- /dev/null +++ b/docs/work-items/ORCH-076/04-test-plan.yaml @@ -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 -- 2.49.1 From 2030d1627ab2ac2212594c2f52d985589673b279 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Tue, 9 Jun 2026 13:49:25 +0300 Subject: [PATCH 3/7] architect(ET): auto-commit from architect run_id=454 --- docs/architecture/adr/README.md | 6 +- .../adr/adr-0020-frontmatter-contract.md | 63 +++++ .../06-adr/ADR-001-frontmatter-contract.md | 248 ++++++++++++++++++ .../ORCH-076/07-infra-requirements.md | 50 ++++ .../ORCH-076/08-data-requirements.md | 34 +++ docs/work-items/ORCH-076/10-tech-risks.md | 25 ++ 6 files changed, 425 insertions(+), 1 deletion(-) create mode 100644 docs/architecture/adr/adr-0020-frontmatter-contract.md create mode 100644 docs/work-items/ORCH-076/06-adr/ADR-001-frontmatter-contract.md create mode 100644 docs/work-items/ORCH-076/07-infra-requirements.md create mode 100644 docs/work-items/ORCH-076/08-data-requirements.md create mode 100644 docs/work-items/ORCH-076/10-tech-risks.md diff --git a/docs/architecture/adr/README.md b/docs/architecture/adr/README.md index 8efe640..9d79d09 100644 --- a/docs/architecture/adr/README.md +++ b/docs/architecture/adr/README.md @@ -23,13 +23,17 @@ Per-work-item решения живут в `docs/work-items//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. diff --git a/docs/architecture/adr/adr-0020-frontmatter-contract.md b/docs/architecture/adr/adr-0020-frontmatter-contract.md new file mode 100644 index 0000000..2c355bb --- /dev/null +++ b/docs/architecture/adr/adr-0020-frontmatter-contract.md @@ -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). diff --git a/docs/work-items/ORCH-076/06-adr/ADR-001-frontmatter-contract.md b/docs/work-items/ORCH-076/06-adr/ADR-001-frontmatter-contract.md new file mode 100644 index 0000000..a20042a --- /dev/null +++ b/docs/work-items/ORCH-076/06-adr/ADR-001-frontmatter-contract.md @@ -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\n---\n"; формат совместим со 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`. diff --git a/docs/work-items/ORCH-076/07-infra-requirements.md b/docs/work-items/ORCH-076/07-infra-requirements.md new file mode 100644 index 0000000..48cd92f --- /dev/null +++ b/docs/work-items/ORCH-076/07-infra-requirements.md @@ -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 — не вводятся. diff --git a/docs/work-items/ORCH-076/08-data-requirements.md b/docs/work-items/ORCH-076/08-data-requirements.md new file mode 100644 index 0000000..cdbb44d --- /dev/null +++ b/docs/work-items/ORCH-076/08-data-requirements.md @@ -0,0 +1,34 @@ +# 08 — Требования к данным / схеме БД: ORCH-076 (ORCH-52c) + +Work Item: **ORCH-076** · Repo: **orchestrator** · Стадия: architecture + +## Сводка + +**Изменений схемы БД нет.** Контракт frontmatter работает исключительно на **файлах** +(YAML-frontmatter номерных документов `docs/work-items//*.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` — старые и новые парсеры читают единообразно. diff --git a/docs/work-items/ORCH-076/10-tech-risks.md b/docs/work-items/ORCH-076/10-tech-risks.md new file mode 100644 index 0000000..40fcf66 --- /dev/null +++ b/docs/work-items/ORCH-076/10-tech-risks.md @@ -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` штатным путём (не ручной рестарт). -- 2.49.1 From 92961d1d323c93c36b2fbeb6d6c7fd86f1dc6dd0 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Tue, 9 Jun 2026 14:03:49 +0300 Subject: [PATCH 4/7] refactor(frontmatter): unified frontmatter contract + handoff spec (ORCH-52c) src/frontmatter.py grows from a single-key reader into the full machine contract: reader (read_frontmatter_value, unchanged), one parse primitive (parse_frontmatter), writer (render/write_frontmatter), schema validator (validate_schema/REQUIRED_FIELDS, warning-only by default) and a shared strip_frontmatter helper. The five verdict gates (check_reviewer_verdict, _parse_tests_verdict, _parse_deploy_status, _parse_staging_status, parse_security_status) now read through the single parse_frontmatter point instead of duplicated ad-hoc YAML logic; review_parse._strip_frontmatter and security_gate.extract_security_findings reuse the shared helper. Strictly backward compatible + never-raise: STAGE_TRANSITIONS, the QG_CHECKS composition, verdict semantics (incl. ORCH-047 three-field tester + negative token priority), reason-strings and worktree->origin/main fallback are 1:1. The schema validator never influences a gate verdict by default; hard-fail is reserved behind the frontmatter_validation_strict kill-switch (default False). New formal handoff spec docs/_standards/HANDOFF_PROTOCOL.md ("stage -> required output" + required frontmatter schema), aligned 1:1 with PIPELINE_DOCS.md. Tests: test_frontmatter.py (TC-01..07), test_qg_verdicts.py (TC-08..15), test_security_gate.py (TC-12), test_stages_invariants.py (TC-16). Full tests/ green (1212). Refs: ORCH-076 Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 6 + CLAUDE.md | 2 +- docs/_standards/HANDOFF_PROTOCOL.md | 118 ++++++++++ docs/_standards/PIPELINE_DOCS.md | 21 +- docs/architecture/README.md | 14 +- src/config.py | 11 + src/frontmatter.py | 319 ++++++++++++++++++++++++---- src/qg/checks.py | 89 ++++---- src/review_parse.py | 15 +- src/security_gate.py | 30 ++- tests/test_frontmatter.py | 244 +++++++++++++++++++++ tests/test_qg_verdicts.py | 202 ++++++++++++++++++ tests/test_security_gate.py | 26 +++ tests/test_stages_invariants.py | 55 +++++ 14 files changed, 1043 insertions(+), 109 deletions(-) create mode 100644 docs/_standards/HANDOFF_PROTOCOL.md create mode 100644 tests/test_frontmatter.py create mode 100644 tests/test_qg_verdicts.py create mode 100644 tests/test_stages_invariants.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c1ea53..1568b41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ Формат: [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//` → невидимы гейтам наличия файлов (`check_architecture_done`/`check_analysis_complete`). diff --git a/CLAUDE.md b/CLAUDE.md index a2bca7c..49b2b8a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -117,7 +117,7 @@ created → analysis → architecture → development → review → testing → - ADR per work-item: `docs/work-items//06-adr/ADR-NNN-slug.md` - Global ADR (сквозные решения): `docs/architecture/adr/adr-NNNN-slug.md` - Work items: `docs/work-items//` -- Машинные вердикты 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//`) `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). diff --git a/docs/_standards/HANDOFF_PROTOCOL.md b/docs/_standards/HANDOFF_PROTOCOL.md new file mode 100644 index 0000000..14570d1 --- /dev/null +++ b/docs/_standards/HANDOFF_PROTOCOL.md @@ -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-.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** (регистр чувствителен — иначе гейт упадёт ложно). diff --git a/docs/_standards/PIPELINE_DOCS.md b/docs/_standards/PIPELINE_DOCS.md index a5fa33f..ace257b 100644 --- a/docs/_standards/PIPELINE_DOCS.md +++ b/docs/_standards/PIPELINE_DOCS.md @@ -133,6 +133,21 @@ check_tests_passed → check_staging_status → check_deploy_status`. ключа** (регистр чувствителен — иначе гейт упадёт ложно). 3. Сверяйся с манифестом (§2–§3): какой агент, на какой стадии, какой гейт читает документ. -> Стандарт **описательный** (слой 1). Машинная проверка соответствия шаблонам/frontmatter — -> отдельная задача ORCH-52c; до неё соблюдение — на ответственности агента и reviewer'а -> (правило CLAUDE.md «обновлена ли документация»). +> Стандарт **описательный** (слой 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`. diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 6153351..ebc6432 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -39,7 +39,7 @@ 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`), @@ -49,9 +49,13 @@ created → analysis → architecture → development → review → testing → [`docs/_templates/`](../_templates/). Манифест **документирует** поведение гейтов (источник истины остаётся код: `src/stages.py`, `src/qg/checks.py`), честно различая machine-verdict доки (`12/13/14/15/17` — несут читаемый гейтом ключ) и информационные (`00/08/10/16` — гейтом не -парсятся). Это слой 1 (описательный); машинная проверка соответствия — задача ORCH-52c. ADR: -[adr-0019](adr/adr-0019-pipeline-docs-standard.md), детально — -`docs/work-items/ORCH-075/06-adr/ADR-001-pipeline-docs-standard.md`. +парсятся). Это слой 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_`/`ORCH_AGENT_EFFORT_` > `*_default` > CLI-дефолт (без флага)**. **Эффорт (ORCH-081):** ниже `*_default` добавлен непустой **per-role floor** — class-default поля `agent_effort_` из `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`. @@ -642,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) diff --git a/src/config.py b/src/config.py index 3045b8d..39c610d 100644 --- a/src/config.py +++ b/src/config.py @@ -553,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 diff --git a/src/frontmatter.py b/src/frontmatter.py index 284ebe9..f8bcc38 100644 --- a/src/frontmatter.py +++ b/src/frontmatter.py @@ -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=``, ``data={}``. + * parsed value is not a mapping -> ``data={}`` (``has_block=True``). + * valid mapping -> ``data=``. + """ + 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\\n---\\n"``. 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=[]) diff --git a/src/qg/checks.py b/src/qg/checks.py index 78db3c4..d4f77c2 100644 --- a/src/qg/checks.py +++ b/src/qg/checks.py @@ -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, ) + + 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, ) + + 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": diff --git a/src/review_parse.py b/src/review_parse.py index db57778..4f22853 100644 --- a/src/review_parse.py +++ b/src/review_parse.py @@ -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: diff --git a/src/security_gate.py b/src/security_gate.py index 2ac698f..d35b545 100644 --- a/src/security_gate.py +++ b/src/security_gate.py @@ -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, )`` (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 = [] diff --git a/tests/test_frontmatter.py b/tests/test_frontmatter.py new file mode 100644 index 0000000..a2a561f --- /dev/null +++ b/tests/test_frontmatter.py @@ -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] diff --git a/tests/test_qg_verdicts.py b/tests/test_qg_verdicts.py new file mode 100644 index 0000000..ce59a34 --- /dev/null +++ b/tests/test_qg_verdicts.py @@ -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 diff --git a/tests/test_security_gate.py b/tests/test_security_gate.py index 499ca3b..e23c291 100644 --- a/tests/test_security_gate.py +++ b/tests/test_security_gate.py @@ -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.""" diff --git a/tests/test_stages_invariants.py b/tests/test_stages_invariants.py new file mode 100644 index 0000000..a800cfb --- /dev/null +++ b/tests/test_stages_invariants.py @@ -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 -- 2.49.1 From d846910ca621fdc1043f7a2812b83ada2100c40c Mon Sep 17 00:00:00 2001 From: claude-bot Date: Tue, 9 Jun 2026 14:08:13 +0300 Subject: [PATCH 5/7] reviewer(ET): auto-commit from reviewer run_id=456 --- docs/work-items/ORCH-076/12-review.md | 93 +++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 docs/work-items/ORCH-076/12-review.md diff --git a/docs/work-items/ORCH-076/12-review.md b/docs/work-items/ORCH-076/12-review.md new file mode 100644 index 0000000..73c30ac --- /dev/null +++ b/docs/work-items/ORCH-076/12-review.md @@ -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). + +Документация полная и согласованная — претензий нет. -- 2.49.1 From cb9bfcff12ad20a2c75110f9800a088cf379194d Mon Sep 17 00:00:00 2001 From: claude-bot Date: Tue, 9 Jun 2026 14:10:09 +0300 Subject: [PATCH 6/7] tester(ET): auto-commit from tester run_id=457 --- docs/work-items/ORCH-076/13-test-report.md | 85 ++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 docs/work-items/ORCH-076/13-test-report.md diff --git a/docs/work-items/ORCH-076/13-test-report.md b/docs/work-items/ORCH-076/13-test-report.md new file mode 100644 index 0000000..534f4da --- /dev/null +++ b/docs/work-items/ORCH-076/13-test-report.md @@ -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`. -- 2.49.1 From 2e27f689581c3ea50949ae225fc8c6765588c99e Mon Sep 17 00:00:00 2001 From: deploy-finalizer Date: Tue, 9 Jun 2026 14:18:42 +0300 Subject: [PATCH 7/7] deploy(ORCH-036): finalize SUCCESS for ORCH-076 --- docs/work-items/ORCH-076/14-deploy-log.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 docs/work-items/ORCH-076/14-deploy-log.md diff --git a/docs/work-items/ORCH-076/14-deploy-log.md b/docs/work-items/ORCH-076/14-deploy-log.md new file mode 100644 index 0000000..8efba4c --- /dev/null +++ b/docs/work-items/ORCH-076/14-deploy-log.md @@ -0,0 +1,12 @@ +--- +deploy_status: SUCCESS +work_item: ORCH-076 +hook_exit_code: 0 +deployed_by: deploy-finalizer +--- + +# Deploy log — ORCH-036 executable self-deploy + +Прод-деплой завершён хост-хуком с exit-code `0` -> `deploy_status: SUCCESS`. + +Вердикт зафиксирован детерминированным finalizer'ом (Фаза C), не LLM. -- 2.49.1