Files
orchestrator/scripts/build_presentation.py
claude-bot 6d798c01ef docs(overview): витрина системы docs/overview/ — бизнес+тех, 3 аудитории, презентация (ORCH-011)
Единая точка входа в документацию платформы (ADR-001 D1–D9):
- docs/overview/ — 10 файлов: индекс (маршруты «Я заказчик / Я менеджер /
  Я разработчик» + норматив «изменил функциональность → обнови витрину в том же
  PR»), business.md (без жаргона, 6 сценариев), 7 тех-блоков (link-first),
  presentation.md (16 слайдов + процедура сборки «команда + Проверка:»).
- scripts/build_presentation.py — генератор .pptx в тёмном дизайне (python-pptx;
  чистый stdlib-парсер parse_slides + ленивый import pptx; бинарь не коммитится,
  build/ в .gitignore; зависимость НЕ в прод-образе — машинный гард TC-09).
- tests/test_system_docs.py — структурный анти-дрейф: derive-сверки стадий/
  гейтов/агентов импортом STAGE_TRANSITIONS/QG_CHECKS/glob промптов/config,
  валидность ссылок, FORBIDDEN-скан + секрет-эвристика, слайды каноническим
  парсером, NFR-2, указатели.
- reviewer.md — ось обзорных доков ORCH-079 расширена на витрину (D7; канон 52d
  байт-в-байт, только текст внутри секций) + анти-регресс ассерт в
  test_agent_prompts_canon.py.
- Указатели: README.md, CLAUDE.md (правила №2/№6, «Структура»),
  PRODUCT_VISION.md (врезка-ссылка), CHANGELOG.md.

Рантайм байт-в-байт: src/**, docker-compose.yml, Dockerfile, requirements* —
ноль изменений (docs+tests+dev-скрипт, паттерн ORCH-102/103). pytest: 1873 passed.

Refs: ORCH-011

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 09:36:40 +03:00

193 lines
8.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""ORCH-011 (ADR-001 D4): сборка `.pptx` из слайдо-источника витрины.
Источник истины — `docs/overview/presentation.md` (машинно-парсимая
слайдо-структура: `## Слайд N: Заголовок` + тезисы `- ...` + опциональная
строка `> Визуал: ...`). Скрипт собирает редактируемую PowerPoint-презентацию
в тёмном дизайне (D-1 Владельца): тёмный фон, светлый текст, один акцентный
цвет, системные шрифты с полной кириллицей.
Канон (D4/D5):
- запуск ТОЛЬКО вне рантайма конвейера (host/dev venv, явный запуск человеком —
паттерн ORCH-009); `python-pptx` НЕ входит в requirements*/Dockerfile (NFR-2);
- `parse_slides` — чистая stdlib-функция БЕЗ импорта pptx: её импортирует
`tests/test_system_docs.py` (один парсер = один источник истины о формате);
- рендерер импортирует pptx ЛЕНИВО внутри `build_pptx`;
- дефолтный выход — `build/orchestrator-overview.pptx` (в `.gitignore`;
собранный бинарь в git НЕ коммитится — D5).
Процедура запуска (канон «команда + Проверка:») — `docs/overview/presentation.md`,
раздел «Как собрать .pptx».
"""
from __future__ import annotations
import argparse
import re
from dataclasses import dataclass, field
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
DEFAULT_SOURCE = REPO_ROOT / "docs" / "overview" / "presentation.md"
DEFAULT_OUTPUT = REPO_ROOT / "build" / "orchestrator-overview.pptx"
# Тёмная тема (D4): фон ~#1F1F2E, светлый текст, один акцент, приглушённый серый.
DARK_BG = "1F1F2E"
TEXT_MAIN = "F2F2F7"
ACCENT = "8AB4F8"
TEXT_MUTED = "9A9AAD"
FONT_NAME = "Calibri" # системный шрифт с полной кириллицей (D4)
_SLIDE_RE = re.compile(r"^##\s+Слайд\s+(\d+)\s*:\s*(.+?)\s*$")
_BULLET_RE = re.compile(r"^-\s+(.+?)\s*$")
_VISUAL_RE = re.compile(r"^>\s*Визуал\s*:\s*(.+?)\s*$")
_ANY_HEADING_RE = re.compile(r"^#{1,6}\s+")
@dataclass
class Slide:
"""Один слайд источника: номер, заголовок, тезисы, подпись визуала."""
number: int
title: str
bullets: list[str] = field(default_factory=list)
visual: str | None = None
def parse_slides(text: str) -> list[Slide]:
"""Разобрать слайдо-источник в список :class:`Slide` (чистая, stdlib-only).
Формат (D4): слайд открывается строкой ``## Слайд N: Заголовок``; его тезисы —
строки ``- ...``; опциональная подпись визуала — ``> Визуал: ...``. Любой
другой markdown-заголовок (например, раздел «Как собрать .pptx») завершает
текущий слайд — служебные разделы источника в слайды не попадают.
"""
slides: list[Slide] = []
current: Slide | None = None
for line in text.splitlines():
m = _SLIDE_RE.match(line)
if m:
current = Slide(number=int(m.group(1)), title=m.group(2))
slides.append(current)
continue
if _ANY_HEADING_RE.match(line):
current = None # служебный раздел — не слайд
continue
if current is None:
continue
bullet = _BULLET_RE.match(line)
if bullet:
current.bullets.append(bullet.group(1))
continue
visual = _VISUAL_RE.match(line)
if visual:
current.visual = visual.group(1)
return slides
def build_pptx(slides: list[Slide], output: Path) -> None:
"""Собрать `.pptx` в тёмном дизайне из распарсенных слайдов.
Импорт `pptx` — ленивый (D4): без установленного `python-pptx` модуль
остаётся импортируемым (нужно тестам), а сборка честно подсказывает
`pip install python-pptx`. Текст пишется настоящими редактируемыми
run'ами — кириллица не растрируется, слайды правятся руками.
"""
from pptx import Presentation
from pptx.dml.color import RGBColor
from pptx.util import Inches, Pt
prs = Presentation()
prs.slide_width = Inches(13.333) # 16:9
prs.slide_height = Inches(7.5)
blank_layout = prs.slide_layouts[6]
for slide_def in slides:
slide = prs.slides.add_slide(blank_layout)
slide.background.fill.solid()
slide.background.fill.fore_color.rgb = RGBColor.from_string(DARK_BG)
title_box = slide.shapes.add_textbox(
Inches(0.6), Inches(0.45), prs.slide_width - Inches(1.2), Inches(1.2)
)
title_tf = title_box.text_frame
title_tf.word_wrap = True
run = title_tf.paragraphs[0].add_run()
run.text = slide_def.title
run.font.size = Pt(34)
run.font.bold = True
run.font.name = FONT_NAME
run.font.color.rgb = RGBColor.from_string(ACCENT)
body_box = slide.shapes.add_textbox(
Inches(0.8), Inches(1.85), prs.slide_width - Inches(1.6), Inches(4.4)
)
body_tf = body_box.text_frame
body_tf.word_wrap = True
for i, bullet in enumerate(slide_def.bullets):
para = body_tf.paragraphs[0] if i == 0 else body_tf.add_paragraph()
run = para.add_run()
run.text = f"{bullet}"
run.font.size = Pt(20)
run.font.name = FONT_NAME
run.font.color.rgb = RGBColor.from_string(TEXT_MAIN)
para.space_after = Pt(10)
if slide_def.visual:
cap_box = slide.shapes.add_textbox(
Inches(0.8), Inches(6.55), prs.slide_width - Inches(1.6), Inches(0.6)
)
cap_tf = cap_box.text_frame
cap_tf.word_wrap = True
run = cap_tf.paragraphs[0].add_run()
run.text = f"Визуал: {slide_def.visual}"
run.font.size = Pt(13)
run.font.italic = True
run.font.name = FONT_NAME
run.font.color.rgb = RGBColor.from_string(TEXT_MUTED)
output.parent.mkdir(parents=True, exist_ok=True)
prs.save(str(output))
def main(argv: list[str] | None = None) -> int:
"""CLI: распарсить источник, собрать `.pptx`, напечатать число слайдов."""
parser = argparse.ArgumentParser(
description="Сборка docs/overview/presentation.md -> .pptx (тёмный дизайн, ORCH-011 D4)."
)
parser.add_argument(
"--source",
type=Path,
default=DEFAULT_SOURCE,
help=f"слайдо-источник (default: {DEFAULT_SOURCE.relative_to(REPO_ROOT)})",
)
parser.add_argument(
"--out",
type=Path,
default=DEFAULT_OUTPUT,
help=f"выходной .pptx (default: {DEFAULT_OUTPUT.relative_to(REPO_ROOT)})",
)
args = parser.parse_args(argv)
if not args.source.is_file():
print(f"ОШИБКА: источник не найден: {args.source}")
return 1
slides = parse_slides(args.source.read_text(encoding="utf-8"))
if not slides:
print(f"ОШИБКА: в {args.source} не найдено ни одного слайда (формат: '## Слайд N: ...')")
return 1
try:
build_pptx(slides, args.out)
except ImportError:
print(
"ОШИБКА: python-pptx не установлен. Сборка выполняется в одноразовом "
"dev-venv ВНЕ прод-образа (NFR-2): pip install python-pptx"
)
return 1
print(f"Собрано слайдов: {len(slides)}{args.out}")
return 0
if __name__ == "__main__":
raise SystemExit(main())