Создан golden source структуры номерных документов work item (ORCH-52b, слой 1 эпика ORCH-52). Docs-only: STAGE_TRANSITIONS / QG_CHECKS / check_* / схема БД не трогаются (AC-6). - docs/_standards/PIPELINE_DOCS.md — манифест «стадия→агент→документ→категория→ гейт→frontmatter machine-key» (сверен с src/stages.py и src/qg/checks.py) + раздел ADR-naming. Манифест документирует поведение гейтов, источник истины остаётся код (ADR-001 §D2); честно различает machine-verdict (12/13/14/15/17) и информационные (00/08/10/16) доки; под-гейты ребра deploy-staging→deploy отмечены как врезки в advance_stage. - docs/_templates/* — 15 копируемых скелетов; машинные доки несут точный frontmatter-ключ из _parse_* (verdict/result/deploy_status/staging_status/ security_status/post_deploy_status). - Точки-ссылки: CLAUDE.md, docs/architecture/README.md; запись CHANGELOG. - tests/test_orch_52b_docs_standard.py — TC-01..TC-20 структурные проверки; полный pytest tests/ зелёный (1177 passed). Refs: ORCH-075 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
215 lines
11 KiB
Python
215 lines
11 KiB
Python
"""ORCH-075 (ORCH-52b) — структурные проверки стандарта документов конвейера.
|
||
|
||
Docs-only задача: проверяется НАЛИЧИЕ и СТРУКТУРА новых файлов-стандартов/шаблонов
|
||
(`docs/_standards/PIPELINE_DOCS.md`, `docs/_templates/*`) и обновление точек-ссылок
|
||
(`CLAUDE.md`, `docs/architecture/README.md`, `CHANGELOG.md`). Тесты НЕ меняют
|
||
`QG_CHECKS`/`STAGE_TRANSITIONS` и не вводят новый гейт (это ORCH-52c).
|
||
|
||
Покрытие тест-плана 04-test-plan.yaml: TC-01…TC-20 (TC-21 — полный регресс `pytest tests/`).
|
||
"""
|
||
|
||
from pathlib import Path
|
||
|
||
import yaml
|
||
|
||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||
STANDARDS = REPO_ROOT / "docs" / "_standards"
|
||
TEMPLATES = REPO_ROOT / "docs" / "_templates"
|
||
MANIFEST = STANDARDS / "PIPELINE_DOCS.md"
|
||
|
||
# Все номерные доки реального набора (TRZ §FR-1, AC-1).
|
||
NUMBERED_DOCS = ["00", "01", "02", "03", "04", "06", "07", "08", "10",
|
||
"12", "13", "14", "15", "16", "17"]
|
||
|
||
# Шаблоны required/when-applicable доков (TRZ §2, AC-2).
|
||
TEMPLATE_FILES = [
|
||
"00-business-request.md",
|
||
"01-brd.md",
|
||
"02-trz.md",
|
||
"03-acceptance-criteria.md",
|
||
"04-test-plan.yaml",
|
||
"06-adr-ADR-NNN-slug.md",
|
||
"07-infra-requirements.md",
|
||
"08-data-requirements.md",
|
||
"10-tech-risks.md",
|
||
"12-review.md",
|
||
"13-test-report.md",
|
||
"14-deploy-log.md",
|
||
"15-staging-log.md",
|
||
"16-post-deploy-log.md",
|
||
"17-security-report.md",
|
||
]
|
||
|
||
|
||
def _read(path: Path) -> str:
|
||
return path.read_text(encoding="utf-8")
|
||
|
||
|
||
# --- TC-01 -----------------------------------------------------------------
|
||
def test_tc01_manifest_exists_and_nonempty():
|
||
"""docs/_standards/PIPELINE_DOCS.md существует и непустой."""
|
||
assert MANIFEST.is_file(), f"манифест не найден: {MANIFEST}"
|
||
assert len(_read(MANIFEST).strip()) > 0, "манифест пустой"
|
||
|
||
|
||
# --- TC-02 -----------------------------------------------------------------
|
||
def test_tc02_manifest_mentions_all_numbered_docs():
|
||
"""Манифест упоминает все номерные доки набора."""
|
||
content = _read(MANIFEST)
|
||
missing = []
|
||
for num in NUMBERED_DOCS:
|
||
# Документ упоминается как `NN-...` или `NN-adr` — ищем по префиксу `NN-`.
|
||
if f"{num}-" not in content:
|
||
missing.append(num)
|
||
assert not missing, f"в манифесте не упомянуты доки: {missing}"
|
||
|
||
|
||
# --- TC-03 -----------------------------------------------------------------
|
||
def test_tc03_manifest_lists_owner_agents():
|
||
"""Манифест указывает владельцев-агентов конвейера (TC-03: analyst/architect/
|
||
reviewer/tester/deployer/система). `developer` не владеет номерным доком — пишет код+PR."""
|
||
content = _read(MANIFEST).lower()
|
||
for agent in ["analyst", "architect", "reviewer", "tester", "deployer"]:
|
||
assert agent in content, f"в манифесте нет владельца-агента: {agent}"
|
||
assert "систем" in content, "в манифесте нет системного владельца (Plane webhook)"
|
||
|
||
|
||
# --- TC-04 -----------------------------------------------------------------
|
||
def test_tc04_manifest_has_categories():
|
||
"""Манифест содержит категории required / when-applicable / optional."""
|
||
content = _read(MANIFEST)
|
||
for category in ["required", "when-applicable", "optional"]:
|
||
assert category in content, f"в манифесте нет категории: {category}"
|
||
|
||
|
||
# --- TC-05 -----------------------------------------------------------------
|
||
def test_tc05_templates_dir_has_all_templates():
|
||
"""docs/_templates/ существует и содержит шаблоны для всех required/when-applicable доков."""
|
||
assert TEMPLATES.is_dir(), f"каталог шаблонов не найден: {TEMPLATES}"
|
||
missing = [name for name in TEMPLATE_FILES if not (TEMPLATES / name).is_file()]
|
||
assert not missing, f"отсутствуют шаблоны: {missing}"
|
||
|
||
|
||
# --- TC-06..TC-11 — frontmatter machine-keys -------------------------------
|
||
def _assert_frontmatter_key(template_name: str, key: str):
|
||
content = _read(TEMPLATES / template_name)
|
||
assert content.lstrip().startswith("---"), f"{template_name}: нет YAML-frontmatter"
|
||
head = content.split("---", 2)
|
||
assert len(head) >= 3, f"{template_name}: frontmatter не закрыт"
|
||
assert f"{key}:" in head[1], f"{template_name}: нет machine-key `{key}:` во frontmatter"
|
||
|
||
|
||
def test_tc06_review_template_has_verdict():
|
||
"""Шаблон 12-review содержит frontmatter-ключ verdict:."""
|
||
_assert_frontmatter_key("12-review.md", "verdict")
|
||
|
||
|
||
def test_tc07_test_report_template_has_result():
|
||
"""Шаблон 13-test-report содержит frontmatter-ключ result:."""
|
||
_assert_frontmatter_key("13-test-report.md", "result")
|
||
|
||
|
||
def test_tc08_deploy_log_template_has_deploy_status():
|
||
"""Шаблон 14-deploy-log содержит frontmatter-ключ deploy_status:."""
|
||
_assert_frontmatter_key("14-deploy-log.md", "deploy_status")
|
||
|
||
|
||
def test_tc09_staging_log_template_has_staging_status():
|
||
"""Шаблон 15-staging-log содержит frontmatter-ключ staging_status:."""
|
||
_assert_frontmatter_key("15-staging-log.md", "staging_status")
|
||
|
||
|
||
def test_tc10_security_report_template_has_security_status():
|
||
"""Шаблон 17-security-report содержит frontmatter-ключ security_status:."""
|
||
_assert_frontmatter_key("17-security-report.md", "security_status")
|
||
|
||
|
||
def test_tc11_post_deploy_template_has_post_deploy_status():
|
||
"""Шаблон 16-post-deploy-log содержит frontmatter-ключ post_deploy_status:."""
|
||
_assert_frontmatter_key("16-post-deploy-log.md", "post_deploy_status")
|
||
|
||
|
||
# --- TC-12 -----------------------------------------------------------------
|
||
def test_tc12_brd_template_has_mandatory_sections():
|
||
"""Шаблон 01-brd содержит обязательные секции (TRZ §FR-2.1)."""
|
||
content = _read(TEMPLATES / "01-brd.md")
|
||
for section in ["Бизнес-контекст", "Объём", "Бизнес-требования", "Нефункциональные требования"]:
|
||
assert section in content, f"01-brd: нет секции `{section}`"
|
||
|
||
|
||
# --- TC-13 -----------------------------------------------------------------
|
||
def test_tc13_trz_template_has_mandatory_sections():
|
||
"""Шаблон 02-trz содержит обязательные секции (TRZ §FR-2.1)."""
|
||
content = _read(TEMPLATES / "02-trz.md")
|
||
for section in ["Задействованные модули", "Изменения API", "Изменения схемы БД",
|
||
"QG checks"]:
|
||
assert section in content, f"02-trz: нет секции `{section}`"
|
||
|
||
|
||
# --- TC-14 -----------------------------------------------------------------
|
||
def test_tc14_ac_template_has_pass_fail_block():
|
||
"""Шаблон 03-acceptance-criteria содержит блок AC-N с метками PASS и FAIL."""
|
||
content = _read(TEMPLATES / "03-acceptance-criteria.md")
|
||
assert "AC-1" in content, "03-ac: нет блока AC-N"
|
||
assert "**PASS:**" in content, "03-ac: нет метки PASS"
|
||
assert "**FAIL:**" in content, "03-ac: нет метки FAIL"
|
||
|
||
|
||
# --- TC-15 -----------------------------------------------------------------
|
||
def test_tc15_test_plan_template_is_valid_yaml():
|
||
"""Шаблон 04-test-plan.yaml — валидный YAML с work_item и списком tests."""
|
||
data = yaml.safe_load(_read(TEMPLATES / "04-test-plan.yaml"))
|
||
assert isinstance(data, dict), "04-test-plan: корень не словарь"
|
||
assert "work_item" in data, "04-test-plan: нет ключа work_item"
|
||
assert isinstance(data.get("tests"), list) and data["tests"], "04-test-plan: tests не список"
|
||
first = data["tests"][0]
|
||
for key in ["id", "type", "description", "module", "expected"]:
|
||
assert key in first, f"04-test-plan: в элементе tests нет ключа `{key}`"
|
||
|
||
|
||
# --- TC-16 -----------------------------------------------------------------
|
||
def test_tc16_adr_naming_section_present():
|
||
"""Раздел ADR-naming фиксирует формат ADR-NNN-<slug>.md с нумерацией с 001 и kebab-slug."""
|
||
content = _read(MANIFEST)
|
||
assert "ADR-NNN-" in content, "нет формата ADR-NNN-<slug>.md"
|
||
assert "06-adr" in content, "нет пути 06-adr/"
|
||
assert "001" in content, "не зафиксирована нумерация с 001"
|
||
assert "kebab" in content.lower(), "нет правила kebab-case для slug"
|
||
|
||
|
||
# --- TC-17 -----------------------------------------------------------------
|
||
def test_tc17_adr_naming_matches_real_repo():
|
||
"""ADR-naming совпадает с реальными ADR в репо (пример из манифеста существует)."""
|
||
# Манифест приводит ORCH-088 как пример; реальный файл должен существовать.
|
||
adr_dir = REPO_ROOT / "docs" / "work-items" / "ORCH-088" / "06-adr"
|
||
real = list(adr_dir.glob("ADR-001-*.md"))
|
||
assert real, f"реальный ADR-001-*.md не найден в {adr_dir}"
|
||
# И глобальный реестр следует 4-значной конвенции.
|
||
global_adr = REPO_ROOT / "docs" / "architecture" / "adr"
|
||
assert list(global_adr.glob("adr-0019-*.md")), "глобальный adr-0019-*.md не найден"
|
||
|
||
|
||
# --- TC-18 -----------------------------------------------------------------
|
||
def test_tc18_claude_md_links_standard():
|
||
"""CLAUDE.md содержит ссылку на docs/_standards/PIPELINE_DOCS.md."""
|
||
content = _read(REPO_ROOT / "CLAUDE.md")
|
||
assert "docs/_standards/PIPELINE_DOCS.md" in content
|
||
|
||
|
||
# --- TC-19 -----------------------------------------------------------------
|
||
def test_tc19_architecture_readme_links_standard():
|
||
"""docs/architecture/README.md содержит ссылку на стандарт документов."""
|
||
content = _read(REPO_ROOT / "docs" / "architecture" / "README.md")
|
||
assert "PIPELINE_DOCS.md" in content
|
||
|
||
|
||
# --- TC-20 -----------------------------------------------------------------
|
||
def test_tc20_changelog_mentions_task():
|
||
"""CHANGELOG.md содержит запись об ORCH-52b/ORCH-075 в разделе Unreleased."""
|
||
content = _read(REPO_ROOT / "CHANGELOG.md")
|
||
unreleased = content.split("## [Unreleased]", 1)
|
||
assert len(unreleased) == 2, "нет раздела ## [Unreleased]"
|
||
# Берём срез до следующего релизного заголовка, если он есть.
|
||
body = unreleased[1].split("\n## ", 1)[0]
|
||
assert "ORCH-075" in body or "ORCH-52b" in body, "в Unreleased нет записи ORCH-075/ORCH-52b"
|