feat(generator): каркас проекта и контракт схемы события

- Зачем:
  - этап 2 начинается с формы: контракт схемы — источник истины и для
    генерации событий, и для DDL хранилища, а имена пакета и модулей
    задают границы всем следующим тикетам этапа.
- Что:
  - заведён uv-проект generator/ (pyproject.toml и uv.lock в git; numpy,
    pytest и ruff), пакет clickstream_generator.
  - schema.py — контракт: чистые данные о 47 колонках выгрузки (имя
    Метрики, тип ClickHouse, тип numpy, имя для DDS, группа); порядок
    несёт сам кортеж COLUMNS, отдельного поля с номером нет намеренно.
  - schema_doc.py собирает из контракта описание выгрузки
    docs/formats/clickstream-event.md — по нему пишется сторона
    хранилища; документ руками не правится.
  - тесты: инварианты контракта (состав, уникальность, заполненность,
    согласие типов и порядок групп) и свежесть описания выгрузки.
  - цели make lint, make test и make docs; README, AGENTS.md и
    CONTEXT.md дополнены генератором, форматами и словарной статьёй.
- Проверка:
  - make test (248 тестов), make lint, make config-test;
  - make docs, затем git diff --exit-code docs/ — пусто.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-01 22:22:14 +03:00
co-authored by Claude Opus 5
parent 9e04eae72a
commit 2890c7f9fb
13 changed files with 1001 additions and 4 deletions
+57
View File
@@ -0,0 +1,57 @@
"""Проверки «описания выгрузки»: свежесть документа и полнота таблицы.
Документ собирается из контракта, значит расходиться они могут только одним
способом — контракт правили, документ не пересобрали. Ровно это здесь и
сторожится.
"""
import re
from pathlib import Path
import pytest
from clickstream_generator.schema import COLUMNS, Column, ColumnGroup
from clickstream_generator.schema_doc import render
REPO_ROOT = Path(__file__).resolve().parents[2]
DOC_PATH = REPO_ROOT / "docs" / "formats" / "clickstream-event.md"
TABLE_ROW = re.compile(r"^\| \d+ \|", re.MULTILINE)
@pytest.fixture(scope="module")
def rendered() -> str:
return render()
def test_doc_is_up_to_date(rendered: str):
assert DOC_PATH.exists(), f"описание выгрузки не найдено: {DOC_PATH}"
assert DOC_PATH.read_text(encoding="utf-8") == rendered, (
"описание выгрузки отстало от контракта — пересоберите: make docs"
)
def test_every_column_has_a_row(rendered: str):
assert len(TABLE_ROW.findall(rendered)) == len(COLUMNS)
def test_rows_are_numbered_in_contract_order(rendered: str):
numbers = [int(row.strip("| ")) for row in TABLE_ROW.findall(rendered)]
assert numbers == list(range(1, len(COLUMNS) + 1))
@pytest.mark.parametrize("column", COLUMNS, ids=lambda column: column.name)
def test_column_is_described_in_full(column: Column, rendered: str):
cells = (
f"`{column.name}`",
f"`{column.clickhouse_type}`",
f"`{column.numpy_dtype}`",
f"`{column.dds_name}`",
column.comment,
)
assert "| " + " | ".join(cells) + " |" in rendered
@pytest.mark.parametrize("group", list(ColumnGroup), ids=lambda group: group.name)
def test_group_is_a_heading(group: ColumnGroup, rendered: str):
assert f"\n## {group.value}\n" in rendered