# Финальный план рефакторинга GateCoordinator
## Гейтхосты-фасеты с транзакционным построением графа

---

## Контекст

Это неинвазивная трансформация — существующие файлы, классы и декораторы остаются на месте. Добавляются новые файлы и методы. Пользовательский код не меняется. Старый `MetadataBuilder` работает параллельно до полного перехода, после чего удаляется.

---

## 1. Архитектура

### 1.1. Центральная идея

Система строит **граф зависимостей** между объектами предметной области. Узлы графа — это классы (`Action`, `Entity`, `Role`, `Aspect`...). Рёбра — отношения между ними (`depends`, `has_aspect`, `belongs_to`...).

```
        CreateOrderAction
         /            \
    depends          has_aspect
       /                  \
 OrderEntity          ValidationAspect
       \
    belongs_to
          \
        OrderTable
```

Граф строится один раз при старте приложения. После построения — только читается.

---

### 1.2. Кто знает про классы?

Гейтхост — это маркерный миксин. Когда разработчик пишет:

```python
class CreateOrderAction(BaseAction, RoleGateHost):
    ...
```

Python автоматически регистрирует `CreateOrderAction` как наследника `RoleGateHost` через `__subclasses__()`. Никакой ручной регистрации классов не существует. Гейтхост сам знает всех своих наследников:

```
RoleGateHost.__subclasses_recursive__()
    → [CreateOrderAction, UpdateOrderAction, ...]

AspectGateHost.__subclasses_recursive__()
    → [ValidationAspect, LoggingAspect, ...]

EntityGateHost.__subclasses_recursive__()
    → [OrderEntity, PaymentEntity, ...]
```

---

### 1.3. Что делает гейтхост

Каждый гейтхост реализует три метода. Все три — `abstractmethod`. Явность обязательна:

```
┌─────────────────────────────────────────────────────────┐
│                    AbstractGateHost                      │
│                                                          │
│  inspect(cls) → FacetPayload | None      [abstract]     │
│    Подходит ли класс? Если да — вернуть payload.        │
│    Если нет — None.                                     │
│    Если класс обязан подойти, но сломан — TypeError.    │
│                                                          │
│  _build_payload(cls) → FacetPayload      [abstract]     │
│    Собрать узел и рёбра. Использует хелперы базового    │
│    класса: _make_node_name, _make_edge, _make_meta.     │
│                                                          │
│  _validate(cls, payload) → None          [abstract]     │
│    Оркестратор проверок. Вызывает отдельные методы      │
│    для каждого инварианта. Каждый инвариант —           │
│    отдельный метод, одна проверка, одно исключение.     │
│    Если проверок нет — pass явно. Это зона              │
│    ответственности каждого гейтхоста.                   │
└─────────────────────────────────────────────────────────┘
```

**Правило разделения ответственности:**
```
inspect()        → проверяет ТОЛЬКО наличие данных
_build_payload() → читает данные, формирует payload, не валидирует
_validate()      → проверяет КОРРЕКТНОСТЬ данных и инварианты
```

---

### 1.4. Хелперы базового класса

Базовый класс предоставляет хелперы, устраняющие дублирование в каждом гейтхосте:

```
┌─────────────────────────────────────────────────────────┐
│              AbstractGateHost — хелперы                  │
│                                                          │
│  _make_node_name(cls, suffix) → str                     │
│    "module.ClassName" — без префикса типа.              │
│    Ключ "тип:имя" собирает координатор.                 │
│                                                          │
│  _make_edge(target_type, target_cls,                    │
│             edge_type, is_structural,                   │
│             edge_meta) → EdgeInfo                       │
│    Собирает EdgeInfo без ручного заполнения полей.      │
│                                                          │
│  _make_meta(**kwargs) → tuple[tuple[str, Any], ...]     │
│    dict-синтаксис → иммутабельный tuple пар.            │
│                                                          │
│  __subclasses_recursive__() → list[type]                │
│    Рекурсивный обход всех наследников гейтхоста.        │
└─────────────────────────────────────────────────────────┘
```

---

### 1.5. FacetPayload — контракт между гейтхостом и координатором

```
┌─────────────────────────────────────────────────────────┐
│                     FacetPayload                        │
│                                                          │
│  node_type  : str                ← "action", "role"...  │
│  node_name  : str                ← "module.ClassName"   │
│  node_class : type               ← ссылка на класс      │
│  node_meta  : tuple[tuple]       ← специфика гейтхоста  │
│  edges      : tuple[EdgeInfo]    ← рёбра от этого узла  │
└─────────────────────────────────────────────────────────┘
```

`FacetPayload` — временный транспортный объект. Живёт только внутри `build()`. После коммита выбрасывается. В граф данные попадают как обычный `dict` — конвертация `tuple → dict` происходит один раз при коммите.

`tuple[tuple]` вместо `dict` — `frozen dataclass` должен быть хешируемым.

---

### 1.6. Два типа рёбер

```
┌─────────────────┬────────────────────────────────────────┐
│   is_structural │  Примеры        Проверка               │
├─────────────────┼────────────────────────────────────────┤
│   True          │  depends        Ацикличность обязательна│
│                 │  connection     Цикл → InvalidGraphError│
├─────────────────┼────────────────────────────────────────┤
│   False         │  has_aspect     Циклы допустимы         │
│                 │  belongs_to     Просто метаданные        │
│                 │  requires_ctx                           │
└─────────────────┴────────────────────────────────────────┘
```

---

### 1.7. GateCoordinator — сборщик графа

```python
coordinator = GateCoordinator(strict=True)\
    .register_gate_host(RoleGateHost)\
    .register_gate_host(AspectGateHost)\
    .register_gate_host(EntityGateHost)\
    .build()
```

- `build()` — ровно один раз. Повторный вызов — исключение.
- Регистрация гейтхоста после `build()` — исключение.
- Дубликат гейтхоста при регистрации — исключение.

---

### 1.8. Транзакционный build() — три фазы

Граф либо строится полностью и корректно, либо не строится вообще. Никакого частичного состояния.

```
╔══════════════════════════════════════════════════════════╗
║                    build()                               ║
╠══════════════════════════════════════════════════════════╣
║                                                          ║
║  ФАЗА 1 — СБОР (только чтение, граф не трогаем)        ║
║                                                          ║
║  для каждого gate_host:                                  ║
║    для каждого cls в __subclasses_recursive__:           ║
║      payload = gate_host.inspect(cls)                    ║
║      если payload → _validate(cls, payload)             ║
║                   → добавить в new_payloads             ║
║                                                          ║
╠══════════════════════════════════════════════════════════╣
║                                                          ║
║  ФАЗА 2 — ПРОВЕРКИ (граф не трогаем)                    ║
║                                                          ║
║  2a. Обязательные поля payload'ов не пустые             ║
║  2b. Уникальность ключей внутри new_payloads            ║
║      конфликт → DuplicateNodeError с именами            ║
║      обоих гейтхостов в сообщении                       ║
║  2c. Ссылочная целостность рёбер                        ║
║      цель ∈ new_payloads → OK                           ║
║      иначе → InvalidGraphError                          ║
║  2d. Ацикличность структурных рёбер                     ║
║      симуляция на новом пустом графе                    ║
║      цикл → InvalidGraphError                           ║
║                                                          ║
╠══════════════════════════════════════════════════════════╣
║                                                          ║
║  ФАЗА 3 — КОММИТ (только если фаза 2 прошла)           ║
║                                                          ║
║  добавить все узлы в граф                               ║
║  добавить все рёбра в граф                              ║
║  self._built = True                                      ║
║                                                          ║
╚══════════════════════════════════════════════════════════╝
```

---

### 1.9. Тестирование гейтхостов

Тестовая среда идентична продакшену. Тестовые классы с гейтхостами обязаны быть полными и валидными — они попадают в граф наравне с продакшен-классами.

Тестирование невалидных данных — изолированно через `inspect()`, без координатора:

```python
def test_role_gate_host_raises_on_empty_role_info():
    class BrokenAction(BaseAction, RoleGateHost):
        _role_info = {}

    with pytest.raises(TypeError):
        RoleGateHost.inspect(BrokenAction)
```

Маркировка тестовых узлов в графе — откладывается на этап визуализации.

---

## 2. Ключевые интерфейсы

### `metadata/payload.py`

```python
from __future__ import annotations
from dataclasses import dataclass
from typing import Any


@dataclass(frozen=True)
class EdgeInfo:
    target_node_type: str
    target_name: str
    edge_type: str
    is_structural: bool
    edge_meta: tuple[tuple[str, Any], ...] = ()


@dataclass(frozen=True)
class FacetPayload:
    node_type: str
    node_name: str
    node_class: type
    node_meta: tuple[tuple[str, Any], ...] = ()
    edges: tuple[EdgeInfo, ...] = ()
```

### `metadata/abstract_gate_host.py`

```python
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Any
from .payload import FacetPayload, EdgeInfo


class AbstractGateHost(ABC):

    # ── Обязательный контракт ──────────────────────────────────────────

    @classmethod
    @abstractmethod
    def inspect(cls, target_cls: type) -> FacetPayload | None:
        """
        None      → класс не субъект этого гейтхоста.
        payload   → класс подходит, данные собраны.
        TypeError → класс обязан подойти, но данные отсутствуют.
        """
        ...

    @classmethod
    @abstractmethod
    def _build_payload(cls, target_cls: type) -> FacetPayload:
        """
        Собирает FacetPayload.
        Использует хелперы: _make_node_name, _make_edge, _make_meta.
        Не валидирует данные — это задача _validate.
        """
        ...

    @classmethod
    @abstractmethod
    def _validate(cls, target_cls: type, payload: FacetPayload) -> None:
        """
        Оркестратор проверок инвариантов.
        Каждый инвариант — отдельный приватный метод.
        Если проверок нет — реализовать явно с pass.
        """
        ...

    # ── Хелперы для _build_payload ─────────────────────────────────────

    @classmethod
    def _make_node_name(cls, target_cls: type, suffix: str = "") -> str:
        """
        Возвращает "module.ClassName" — БЕЗ префикса типа.
        Ключ "тип:module.ClassName" собирает координатор.
        """
        name = f"{target_cls.__module__}.{target_cls.__qualname__}"
        return f"{name}.{suffix}" if suffix else name

    @classmethod
    def _make_edge(
        cls,
        target_node_type: str,
        target_cls: type,
        edge_type: str,
        is_structural: bool,
        edge_meta: tuple[tuple[str, Any], ...] = (),
    ) -> EdgeInfo:
        """Собирает EdgeInfo без ручного заполнения каждого поля."""
        return EdgeInfo(
            target_node_type=target_node_type,
            target_name=cls._make_node_name(target_cls),
            edge_type=edge_type,
            is_structural=is_structural,
            edge_meta=edge_meta,
        )

    @classmethod
    def _make_meta(cls, **kwargs: Any) -> tuple[tuple[str, Any], ...]:
        """dict-синтаксис → иммутабельный tuple пар для node_meta."""
        return tuple(kwargs.items())

    # ── Обход наследников ─────────────────────────────────────────────

    @classmethod
    def __subclasses_recursive__(cls) -> list[type]:
        """Рекурсивно собирает всех наследников гейтхоста."""
        result = []
        for subclass in cls.__subclasses__():
            result.append(subclass)
            result.extend(subclass.__subclasses_recursive__())
        return result
```

---

## 3. Требования к реализации каждого гейтхоста

### MetaGateHost

```
_build_payload:
  node_type : "meta"
  node_meta : title, description, version, tags
  edges     : ()

_validate:
  _validate_title_not_empty     — title обязан быть непустой строкой
  _validate_description_present — description обязан присутствовать
  _validate_version_format      — version соответствует semver если задан
```

### RoleGateHost

```
_build_payload:
  node_type : "role"
  node_meta : spec
  edges     : ()

_validate:
  pass  — инварианты проверены декоратором @role и в inspect()
```

### AspectGateHost

```
_build_payload:
  node_type : "aspect"
  node_meta : aspect_type ("regular" | "summary"), method_name
  edges     : ()

_validate:
  _validate_aspect_type_valid   — aspect_type ∈ {"regular", "summary"}
  _validate_method_name_present — method_name непустой
```

### CheckerGateHost

```
_build_payload:
  node_type : "checker"
  node_meta : checker_type, target_field
  edges     :
    belongs_to → аспект (is_structural=False)

_validate:
  _validate_checker_type_valid  — checker_type известный тип
  _validate_target_field_exists — target_field есть в Params/Result
```

### DependencyGateHost

```
_build_payload:
  node_type : "action"
  node_meta : ()
  edges     :
    depends → каждый класс из _depends_info (is_structural=True)

_validate:
  _validate_no_self_dependency — класс не зависит от самого себя
  _validate_deps_are_types     — каждая зависимость это type
```

### ConnectionGateHost

```
_build_payload:
  node_type : "action"
  node_meta : ()
  edges     :
    connection → каждый класс из _connection_info (is_structural=True)

_validate:
  _validate_no_self_connection — класс не соединён сам с собой
  _validate_targets_are_types  — каждая цель это type
```

### EntityGateHost

```
_build_payload:
  node_type : "entity"
  node_meta : table, pk, fields
  edges     : ()

_validate:
  _validate_table_name_present — table непустая строка
  _validate_pk_present         — pk непустая строка
  _validate_fields_are_typed   — каждое поле имеет аннотацию типа
```

### SubscriptionGateHost

```
_build_payload:
  node_type : "subscription"
  node_meta : event_type, handler_name
  edges     :
    subscribes_to → событие (is_structural=False)

_validate:
  _validate_handler_is_callable — handler_name соответствует методу класса
```

### OnErrorGateHost

```
_build_payload:
  node_type : "error_handler"
  node_meta : error_types, handler_name
  edges     : ()

_validate:
  _validate_error_types_not_empty — список типов ошибок непустой
  _validate_handler_is_callable   — handler_name соответствует методу
```

### ContextRequiresGateHost

```
_build_payload:
  node_type : "action"
  node_meta : ()
  edges     :
    requires_context → каждый ключ контекста (is_structural=False)

_validate:
  _validate_keys_are_strings — каждый ключ контекста это строка
  _validate_keys_not_empty   — список ключей непустой
```

### SensitiveGateHost

```
_build_payload:
  node_type : "action"
  node_meta : sensitive_fields, sensitivity_level
  edges     : ()

_validate:
  _validate_fields_not_empty  — список полей непустой
  _validate_sensitivity_level — уровень ∈ допустимых значений
```

---

## 4. Примеры реализации гейтхостов

### RoleGateHost — без рёбер, простой

```python
class RoleGateHost(AbstractGateHost):

    @classmethod
    def inspect(cls, target_cls: type) -> FacetPayload | None:
        if not hasattr(target_cls, "_role_info"):
            return None
        if not target_cls._role_info:
            raise TypeError(
                f"{target_cls.__qualname__} наследует RoleGateHost, "
                "но _role_info отсутствует или пустой."
            )
        return cls._build_payload(target_cls)

    @classmethod
    def _build_payload(cls, target_cls: type) -> FacetPayload:
        return FacetPayload(
            node_type="role",
            node_name=cls._make_node_name(target_cls),
            node_class=target_cls,
            node_meta=cls._make_meta(
                spec=target_cls._role_info["spec"]
            ),
            edges=(),
        )

    @classmethod
    def _validate(
        cls, target_cls: type, payload: FacetPayload
    ) -> None:
        pass  # инварианты проверены декоратором @role и в inspect()
```

### DependencyGateHost — со структурными рёбрами

```python
class DependencyGateHost(AbstractGateHost):

    @classmethod
    def inspect(cls, target_cls: type) -> FacetPayload | None:
        if not hasattr(target_cls, "_depends_info"):
            return None
        if not target_cls._depends_info:
            return None
        return cls._build_payload(target_cls)

    @classmethod
    def _build_payload(cls, target_cls: type) -> FacetPayload:
        edges = tuple(
            cls._make_edge(
                target_node_type="action",
                target_cls=dep,
                edge_type="depends",
                is_structural=True,
            )
            for dep in target_cls._depends_info
        )
        return FacetPayload(
            node_type="action",
            node_name=cls._make_node_name(target_cls),
            node_class=target_cls,
            node_meta=(),
            edges=edges,
        )

    @classmethod
    def _validate(
        cls, target_cls: type, payload: FacetPayload
    ) -> None:
        cls._validate_no_self_dependency(target_cls)
        cls._validate_deps_are_types(target_cls)

    @classmethod
    def _validate_no_self_dependency(cls, target_cls: type) -> None:
        if target_cls in target_cls._depends_info:
            raise ValueError(
                f"{target_cls.__qualname__} зависит от самого себя."
            )

    @classmethod
    def _validate_deps_are_types(cls, target_cls: type) -> None:
        for dep in target_cls._depends_info:
            if not isinstance(dep, type):
                raise TypeError(
                    f"{target_cls.__qualname__}: зависимость "
                    f"'{dep}' не является типом."
                )
```

---

## 5. GateCoordinator

```python
from __future__ import annotations
import rustworkx as rx
from .metadata.abstract_gate_host import AbstractGateHost
from .metadata.payload import FacetPayload
from .exceptions import DuplicateNodeError, InvalidGraphError


class GateCoordinator:

    def __init__(self, strict: bool = False) -> None:
        self._strict = strict
        self._gate_hosts: list[type[AbstractGateHost]] = []
        self._registered_gate_hosts: set[type[AbstractGateHost]] = set()
        self._graph = rx.PyDiGraph()
        self._node_index: dict[str, int] = {}
        self._built = False

    # ── Fluent-регистрация ─────────────────────────────────────────────

    def register_gate_host(
        self, gate_host_cls: type[AbstractGateHost]
    ) -> GateCoordinator:
        if self._built:
            raise RuntimeError(
                "Нельзя регистрировать гейтхосты после build()"
            )
        if gate_host_cls in self._registered_gate_hosts:
            raise ValueError(
                f"{gate_host_cls.__name__} уже зарегистрирован"
            )
        self._gate_hosts.append(gate_host_cls)
        self._registered_gate_hosts.add(gate_host_cls)
        return self

    # ── Публичные методы проверки ──────────────────────────────────────

    def has_node(self, node_type: str, name: str) -> bool:
        return self._make_key(node_type, name) in self._node_index

    def node_exists(self, key: str) -> bool:
        return key in self._node_index

    # ── Построение ────────────────────────────────────────────────────

    def build(self) -> GateCoordinator:
        if self._built:
            raise RuntimeError("build() уже был вызван")

        new_payloads = self._collect_payloads()
        self._check_payload_fields(new_payloads)
        self._check_key_uniqueness(new_payloads)
        self._check_referential_integrity(new_payloads)
        self._check_no_cycles(new_payloads)
        self._commit(new_payloads)

        self._built = True
        return self

    # ── Фаза 1: сбор ──────────────────────────────────────────────────

    def _collect_payloads(self) -> list[FacetPayload]:
        payloads = []
        key_to_gate_host: dict[str, str] = {}

        for gh in self._gate_hosts:
            for cls in gh.__subclasses_recursive__():
                payload = gh.inspect(cls)
                if payload is None:
                    continue
                gh._validate(cls, payload)
                key = self._make_key(payload.node_type, payload.node_name)
                if key in key_to_gate_host:
                    raise DuplicateNodeError(
                        f"Конфликт ключа '{key}':\n"
                        f"  создан:    {key_to_gate_host[key]}\n"
                        f"  конфликт:  {gh.__name__}"
                    )
                key_to_gate_host[key] = gh.__name__
                payloads.append(payload)

        return payloads

    # ── Фаза 2: проверки ──────────────────────────────────────────────

    def _check_payload_fields(
        self, payloads: list[FacetPayload]
    ) -> None:
        for p in payloads:
            if not p.node_type:
                raise ValueError(
                    f"node_type пустой в payload для {p.node_class}"
                )
            if not p.node_name:
                raise ValueError(
                    f"node_name пустой в payload для {p.node_class}"
                )
            if not isinstance(p.node_class, type):
                raise TypeError(
                    f"node_class должен быть типом, "
                    f"получен {type(p.node_class)}"
                )

    def _check_key_uniqueness(
        self, payloads: list[FacetPayload]
    ) -> None:
        seen: set[str] = set()
        for p in payloads:
            key = self._make_key(p.node_type, p.node_name)
            if key in seen:
                raise DuplicateNodeError(
                    f"Дубликат ключа '{key}'"
                )
            seen.add(key)

    def _check_referential_integrity(
        self, payloads: list[FacetPayload]
    ) -> None:
        all_keys = {
            self._make_key(p.node_type, p.node_name)
            for p in payloads
        }
        for p in payloads:
            for edge in p.edges:
                target_key = self._make_key(
                    edge.target_node_type, edge.target_name
                )
                if target_key not in all_keys:
                    raise InvalidGraphError(
                        f"Ребро '{edge.edge_type}' из "
                        f"'{p.node_type}:{p.node_name}' ссылается "
                        f"на несуществующий узел '{target_key}'"
                    )

    def _check_no_cycles(
        self, payloads: list[FacetPayload]
    ) -> None:
        test_graph = rx.PyDiGraph()
        test_index: dict[str, int] = {}

        for p in payloads:
            key = self._make_key(p.node_type, p.node_name)
            idx = test_graph.add_node(key)
            test_index[key] = idx

        for p in payloads:
            source_key = self._make_key(p.node_type, p.node_name)
            source_idx = test_index[source_key]
            for edge in p.edges:
                if not edge.is_structural:
                    continue
                target_key = self._make_key(
                    edge.target_node_type, edge.target_name
                )
                test_graph.add_edge(
                    source_idx,
                    test_index[target_key],
                    edge.edge_type,
                )

        if not rx.is_directed_acyclic_graph(test_graph):
            raise InvalidGraphError(
                "Структурные рёбра образуют цикл. "
                "Проверьте зависимости depends/connection."
            )

    # ── Фаза 3: коммит ────────────────────────────────────────────────

    def _commit(self, payloads: list[FacetPayload]) -> None:
        for p in payloads:
            key = self._make_key(p.node_type, p.node_name)
            idx = self._graph.add_node({
                "node_type": p.node_type,
                "name": p.node_name,
                "class_ref": p.node_class,
                "meta": dict(p.node_meta),
            })
            self._node_index[key] = idx

        for p in payloads:
            source_idx = self._node_index[
                self._make_key(p.node_type, p.node_name)
            ]
            for edge in p.edges:
                target_idx = self._node_index[
                    self._make_key(
                        edge.target_node_type, edge.target_name
                    )
                ]
                self._graph.add_edge(
                    source_idx, target_idx,
                    {
                        "edge_type": edge.edge_type,
                        "meta": dict(edge.edge_meta),
                    },
                )

    # ── Утилиты ───────────────────────────────────────────────────────

    @staticmethod
    def _make_key(node_type: str, name: str) -> str:
        return f"{node_type}:{name}"
```

---

## 6. Фабрика

```python
# core/action_machine_core.py
class ActionMachineCore:

    @staticmethod
    def create_coordinator(strict: bool = False) -> GateCoordinator:
        return (
            GateCoordinator(strict=strict)
            .register_gate_host(MetaGateHost)
            .register_gate_host(RoleGateHost)
            .register_gate_host(AspectGateHost)
            .register_gate_host(CheckerGateHost)
            .register_gate_host(DependencyGateHost)
            .register_gate_host(ConnectionGateHost)
            .register_gate_host(OnErrorGateHost)
            .register_gate_host(ContextRequiresGateHost)
            .register_gate_host(SubscriptionGateHost)
            .register_gate_host(SensitiveGateHost)
            .register_gate_host(EntityGateHost)
            .build()
        )
```

---

## 7. Что удаляется после миграции

| Файл | Причина |
|------|---------|
| `metadata/builder.py` | Заменён `GateCoordinator.build()` |
| `metadata/collectors.py` | Логика переехала в `_build_payload` гейтхостов |
| `metadata/validators.py` | Логика переехала в `_validate` гейтхостов |
| `core/class_metadata.py` | Заменён `FacetPayload` + `node_meta` |

---

## 8. План миграции

```
Шаг 1  Создать metadata/payload.py
       Создать metadata/abstract_gate_host.py
         — три abstractmethod: inspect, _build_payload, _validate
         — четыре хелпера: _make_node_name, _make_edge,
           _make_meta, __subclasses_recursive__
       Тесты: unit на каждый хелпер отдельно.

Шаг 2  Реализовать RoleGateHost
       Самый простой — нет рёбер, _validate → pass явно.
       Тесты: inspect для своего класса, чужого, сломанного.

Шаг 3  Добавить GateCoordinator:
       register_gate_host / build() и три фазы внутри.
       Старый get() продолжает использовать MetadataBuilder.
       Тесты: build с одним гейтхостом,
              дубликат гейтхоста → исключение,
              повторный build → исключение,
              регистрация после build → исключение,
              конфликт ключей → исключение с именами гейтхостов.

Шаг 4  Реализовать остальные гейтхосты
       Порядок: от простых (без рёбер) к сложным (с рёбрами):
         RoleGateHost → MetaGateHost → AspectGateHost
         → EntityGateHost → SensitiveGateHost
         → ContextRequiresGateHost → OnErrorGateHost
         → SubscriptionGateHost → CheckerGateHost
         → DependencyGateHost → ConnectionGateHost
       Для каждого гейтхоста:
         — _build_payload использует хелперы базового класса
         — _validate вызывает отдельные методы-инварианты
         — каждый метод-инвариант покрыт отдельным тестом
       После каждого — интеграционный тест с координатором.

Шаг 5  Переключить GateCoordinator.get()
       Теперь использует данные из построенного графа.
       Все существующие тесты на get() остаются зелёными.

Шаг 6  Обновить ActionProductMachine
       Использовать ActionMachineCore.create_coordinator().

Шаг 7  Удалить builder.py, collectors.py,
       validators.py, class_metadata.py.
       Финальный прогон всех тестов.
```

Пришли все изменные и созданные файлы целиком со всеми комментариями
По ходу актуализируй комментарии делай их полными, чтобы можно было собрать по ним документацию по применению и по архитектуре
Каждый файл присылай как отдельно сообщение
В начале каждого файл указывай полй путь к нему, вот пример: # src/action_machine/runtime/machines/action_product_machine.py
Пиши сколько файлов из скольки осталось 
Пиши по русски
КОММЕНТАРИИ НЕ ДОЛЖНЫ СОДЕРЖАТЬ ИСТОРИЮ ИЗМЕНЕНИЙ ТОЛЬКО ТЕКУЩИЙ ФАКТ КАК ВСЕ УСТРОЕНО
НИ КАКОЙ ОБРАТНОЙ СОВМЕСТИМОСТИ! ПИШЕМ ЧИСТО, КАК С НУЛЯ!
все импорты обязаны быть в начале файла - строго


Ты всегда можешь для уточнения запросить у меня загрузить актуальный файл

ПЕРВЫЙ файл ПОШЕЛ

и сначала скажи согласен с планом?
Да, я согласен с планом. Он логичен и последователен. Несколько уточнений перед стартом:

Что мне нравится:

Транзакционный build() с тремя фазами — это правильно, граф либо целый, либо никакой.
FacetPayload как транспортный объект — чисто и понятно.
Гейтхосты сами знают своих наследников через __subclasses__() — никакой ручной регистрации классов.
Разделение inspect / _build_payload / _validate — каждый метод делает одно.
Что я скорректирую по ходу:

__subclasses_recursive__ — двойное подчёркивание в начале и конце зарезервировано Python. Назову _subclasses_recursive (одно подчёркивание).
В плане DependencyGateHost уже существует как generic-миксин DependencyGateHost[T]. Новый гейтхост-фасет будет отдельным классом DependencyFacet(AbstractGateHost), чтобы не ломать существующий миксин. Аналогично для остальных — фасеты живут в metadata/facets/, а маркерные миксины остаются на месте. Или ты хочешь заменить старые миксины? Тогда мне нужно будет обновить BaseAction и все декораторы. Уточни.
class_metadata.py содержит ClassMetadata, AspectMeta, CheckerMeta, OnErrorMeta, CompensatorMeta и т.д. — их используют ActionProductMachine, ToolsBox, адаптеры. Удаление class_metadata.py потребует переписать машину, чтобы она читала данные из графа напрямую. Это большой объём. Подтверди, что машина тоже переписывается, или ClassMetadata остаётся как read-view поверх графа.
Мой вопрос перед первым файлом:

Маркерные миксины (RoleGateHost, AspectGateHost, ConnectionGateHost и т.д.) в BaseAction — они заменяются фасетами, или фасеты дополняют их? От ответа зависит, трогаю ли я base_action.py и декораторы.

Как только уточнишь — отправлю первый файл: src/action_machine/metadata/payload.py (файл 1 из N).

DependencyGateHost[T] должен быть наследником AbstractGateHost но при этом быть джереник, тк это влияет на проверку инвариантов - или найди другой способ но сначала со мной обсуди

а что они там читают и нужно ли это чтение?  - class_metadata.py содержит ClassMetadata, AspectMeta, CheckerMeta, OnErrorMeta, CompensatorMeta и т.д. — их используют ActionProductMachine, ToolsBox, адаптеры. Удаление class_metadata.py потребует переписать машину, чтобы она читала данные из графа напрямую. Это большой объём. Подтверди, что машина тоже переписывается, или ClassMetadata остаётся как read-view поверх графа.

Маркерные миксины и фасеты сейчас это синонимы и все остается как есть. фасеты это термин документации давай уберем этот термин пока
Отлично, давай разберём по порядку.

1. DependencyGateHost[T] + AbstractGateHost
Проблема: DependencyGateHost[T] сейчас — generic-миксин, где T задаёт bound для допустимых зависимостей. AbstractGateHost — абстрактный класс с inspect / _build_payload / _validate. Нужно совместить.

Варианты:

Вариант A: DependencyGateHost[T] наследует AbstractGateHost

Copyclass DependencyGateHost[T](AbstractGateHost):
    # T = bound для зависимостей
    # inspect/build/validate — classmethod'ы
Проблема: AbstractGateHost предполагает, что inspect(cls) вызывается координатором для каждого подкласса. Но DependencyGateHost[T] — миксин, который наследуется BaseAction. Значит BaseAction тоже станет наследником AbstractGateHost, и координатор увидит BaseAction как субъект всех гейтхостов, от которых наследует BaseAction. Это ломает логику — inspect должен отсеять неподходящие классы.

Впрочем, inspect возвращает None для неподходящих — так что это работает. BaseAction передаётся в DependencyGateHost.inspect(), тот проверяет _depends_info — нет → None. Конкретный CreateOrderAction с @depends → payload.

Но: тогда координатор при обходе DependencyGateHost._subclasses_recursive() найдёт BaseAction и всех его наследников. А RoleGateHost._subclasses_recursive() тоже найдёт тех же наследников BaseAction. Один класс обрабатывается несколькими гейтхостами — это и есть замысел (несколько payload от разных гейтхостов для одного класса). Конфликт ключей решается тем, что node_type разный: "role" vs "dependency_edge".

Вариант B: Разделить bound-проверку и граф-инспекцию

DependencyGateHost[T] остаётся маркерным миксином с generic bound. Отдельный класс DependencyInspector(AbstractGateHost) реализует inspect / _build_payload / _validate и использует данные из DependencyGateHost (читает _depends_info, _depends_bound). Координатор регистрирует DependencyInspector, а не DependencyGateHost.

Проблема: DependencyInspector._subclasses_recursive() не найдёт наследников DependencyGateHost, потому что DependencyInspector — это не миксин BaseAction. Нужен другой способ обхода — например, передать целевой миксин:

Copyclass DependencyInspector(AbstractGateHost):
    _target_mixin = DependencyGateHost
    
    @classmethod
    def _subclasses_recursive(cls):
        return cls._target_mixin._subclasses_recursive()
Мой выбор: Вариант A — проще, один класс, generic bound доступен через cls.get_depends_bound(). Координатор обходит подклассы, inspect отсеивает неподходящие.

Вопрос только в том, что T из generic нужен при декорировании (@depends проверяет bound), а при инспекции (inspect/build/validate) bound уже записан в cls._depends_bound. То есть generic T нужен только для декоратора, а гейтхост-логика работает через _depends_bound. Совмещение работает.

Что думаешь — Вариант A?

2. ClassMetadata и что читает машина
Я посмотрел код ActionProductMachine. Вот что она читает из ClassMetadata:

Метод машины	Что читает из ClassMetadata	Зачем
_check_action_roles	metadata.role.spec	Сравнить с ролями пользователя
_check_connections	metadata.get_connection_keys()	Проверить соответствие переданных connections
_get_regular_aspects	metadata.get_regular_aspects()	Список regular-аспектов для выполнения
_get_summary_aspect	metadata.get_summary_aspect()	Summary-аспект
_get_checkers_for_aspect	metadata.get_checkers_for_aspect(name)	Чекеры для валидации результата аспекта
_handle_aspect_error	metadata.get_error_handler_for(error)	Найти @on_error обработчик
_execute_regular_aspects	metadata.get_compensator_for_aspect(name)	Компенсатор для Saga-стека
_execute_regular_aspects	metadata.has_compensators()	Нужно ли строить Saga-стек
_run_internal	self._coordinator.get_factory(cls)	DependencyFactory
Также адаптеры (FastApiAdapter, McpAdapter) читают:

metadata.meta.description — для OpenAPI summary и MCP tool description.
extract_action_types(action_class) — для params_type и result_type.
Вопрос: это чтение нужно, потому что машина выполняет действия в рантайме и ей нужны метаданные каждого Action для каждого run(). Граф содержит узлы и рёбра, но не содержит method_ref (ссылку на async-метод аспекта), checker_class (класс чекера для создания экземпляра), exception_types (tuple типов для isinstance).

Мой вывод: ClassMetadata (или его аналог) нужен как read-view для рантайма. Граф — для структурных проверок и интроспекции. Рантайм — для выполнения конвейера.

Предложение: Гейтхосты строят граф. Но node_meta содержит всё, что нужно для рантайма: method_ref, checker_class, exception_types. Машина читает из графа через координатор. ClassMetadata как отдельный класс удаляется, а координатор предоставляет методы:

Copycoordinator.get_role_spec(cls) → str | list[str]
coordinator.get_regular_aspects(cls) → tuple[AspectMeta, ...]
coordinator.get_checkers_for_aspect(cls, name) → tuple[CheckerMeta, ...]
coordinator.get_error_handler_for(cls, error) → OnErrorMeta | None
Но AspectMeta, CheckerMeta, OnErrorMeta — это frozen dataclass'ы с конкретными полями. Они остаются как типизированные контейнеры рантайм-данных. Просто хранятся в node_meta графа, а не в отдельном ClassMetadata.

Или проще: ClassMetadata остаётся, но собирается из node_meta графа при первом обращении (lazy). Координатор кеширует. Машина продолжает читать ClassMetadata как раньше.

Что выбираешь?

3. Терминология
Понял, убираю термин «фасет». Везде: гейтхост — и маркерный миксин, и реализация inspect/build/validate. Одна сущность, один термин.

Жду ответы на два вопроса, и стартую первый файл.

 То есть generic T нужен только для декоратора а для всего остального уже нет - все верно 
Отлично. Значит Вариант A: DependencyGateHost[T] наследует AbstractGateHost. Generic T работает для декоратора @depends (проверка bound при определении класса), а inspect / _build_payload / _validate используют уже записанный _depends_bound через cls.get_depends_bound().

Остался второй вопрос: ClassMetadata.

Мой рекомендуемый путь:

ClassMetadata удаляется как класс. Мета-датаклассы (AspectMeta, CheckerMeta, OnErrorMeta, CompensatorMeta, RoleMeta и т.д.) остаются — это типизированные рантайм-контейнеры. Они хранятся в node_meta узлов графа.

Координатор предоставляет типизированные методы чтения из графа:

Copycoordinator.get_role_spec(cls) → str | list[str]
coordinator.get_regular_aspects(cls) → tuple[AspectMeta, ...]
coordinator.get_summary_aspect(cls) → AspectMeta | None
coordinator.get_checkers_for_aspect(cls, name) → tuple[CheckerMeta, ...]
coordinator.get_error_handler_for(cls, error) → OnErrorMeta | None
coordinator.get_compensator_for_aspect(cls, name) → CompensatorMeta | None
coordinator.has_compensators(cls) → bool
coordinator.get_connection_keys(cls) → tuple[str, ...]
coordinator.get_factory(cls) → DependencyFactory
Машина вместо metadata = self._get_metadata(action) + metadata.role.spec пишет self._coordinator.get_role_spec(action.__class__).

Это чуть больше вызовов координатора, зато:

Нет промежуточного God-объекта ClassMetadata с 30+ полями.
Каждый метод координатора — одна ответственность.
Граф — единый источник правды.
Согласен с таким подходом, или хочешь иначе?

шли первый класс
src/action_machine/metadata/payload.py
Copy# src/action_machine/metadata/payload.py
"""
Транспортные объекты для передачи данных между гейтхостами и координатором.

═══════════════════════════════════════════════════════════════════════════════
НАЗНАЧЕНИЕ
═══════════════════════════════════════════════════════════════════════════════

Модуль содержит два frozen-датакласса, образующих контракт между
гейтхостами (AbstractGateHost) и координатором (GateCoordinator):

1. EdgeInfo — описание одного ребра графа, исходящего из узла.
2. FacetPayload — полное описание одного узла графа с его рёбрами.

Оба объекта являются транспортными: они создаются гейтхостом в методе
_build_payload(), передаются координатору в build(), и после коммита
в граф выбрасываются. В граф данные попадают как обычные dict —
конвертация tuple → dict происходит один раз при коммите.

═══════════════════════════════════════════════════════════════════════════════
ИММУТАБЕЛЬНОСТЬ
═══════════════════════════════════════════════════════════════════════════════

Оба датакласса frozen=True. После создания ни одно поле изменить нельзя.
Это гарантирует, что данные, собранные гейтхостом, не будут случайно
модифицированы между фазами build() координатора (сбор → проверки → коммит).

Поля node_meta и edge_meta имеют тип tuple[tuple[str, Any], ...] вместо
dict[str, Any]. Причина: frozen dataclass должен быть хешируемым, а dict
не хешируем. tuple of tuples — иммутабельная и хешируемая альтернатива.
Конвертация в dict выполняется координатором один раз при коммите.

═══════════════════════════════════════════════════════════════════════════════
ДВА ТИПА РЁБЕР
═══════════════════════════════════════════════════════════════════════════════

Рёбра графа делятся на два типа по полю is_structural:

    is_structural=True  — структурные рёбра (depends, connection).
        Образуют скелет системы. Циклы запрещены. Координатор проверяет
        ацикличность на фазе 2 через симуляцию на временном графе.
        Цикл → InvalidGraphError.

    is_structural=False — информационные рёбра (has_aspect, belongs_to,
        requires_context, has_checker, subscribes и т.д.).
        Несут метаданные. Циклы допустимы (например, двусторонние связи
        сущностей). Координатор не проверяет ацикличность для них.

═══════════════════════════════════════════════════════════════════════════════
ФОРМАТ КЛЮЧЕЙ УЗЛОВ
═══════════════════════════════════════════════════════════════════════════════

Каждый узел графа идентифицируется строковым ключом формата "тип:имя".
Гейтхост формирует только имя (node_name) через хелпер _make_node_name().
Координатор собирает полный ключ "node_type:node_name" самостоятельно.

    node_type = "action",  node_name = "module.CreateOrderAction"
    → ключ в графе: "action:module.CreateOrderAction"

    node_type = "role",    node_name = "module.CreateOrderAction"
    → ключ в графе: "role:module.CreateOrderAction"

Один класс может порождать несколько узлов разных типов от разных
гейтхостов. Например, CreateOrderAction порождает узел "action:..." от
DependencyGateHost и узел "role:..." от RoleGateHost. Ключи уникальны
благодаря префиксу типа.

═══════════════════════════════════════════════════════════════════════════════
ПРИМЕР ЖИЗНЕННОГО ЦИКЛА
═══════════════════════════════════════════════════════════════════════════════

    # 1. Гейтхост создаёт payload в _build_payload():
    payload = FacetPayload(
        node_type="role",
        node_name="module.CreateOrderAction",
        node_class=CreateOrderAction,
        node_meta=(("spec", "admin"),),
        edges=(),
    )

    # 2. Координатор собирает все payload в фазе 1 (сбор).

    # 3. Координатор проверяет payload в фазе 2 (проверки):
    #    - node_type и node_name непустые
    #    - node_class — тип
    #    - ключи уникальны
    #    - цели рёбер существуют
    #    - структурные рёбра ацикличны

    # 4. Координатор коммитит в граф в фазе 3:
    #    graph.add_node({
    #        "node_type": "role",
    #        "name": "module.CreateOrderAction",
    #        "class_ref": CreateOrderAction,
    #        "meta": {"spec": "admin"},
    #    })

    # 5. payload выбрасывается. Граф — единственный источник правды.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any


@dataclass(frozen=True)
class EdgeInfo:
    """
    Описание одного ребра графа, исходящего из узла.

    Создаётся гейтхостом через хелпер AbstractGateHost._make_edge().
    Передаётся координатору внутри FacetPayload.edges. Координатор
    использует EdgeInfo для проверки ссылочной целостности (цель
    существует) и ацикличности (для структурных рёбер).

    Атрибуты:
        target_node_type : str
            Тип целевого узла ("action", "entity", "domain" и т.д.).
            Используется координатором для формирования полного ключа
            цели: "target_node_type:target_name".

        target_name : str
            Имя целевого узла (формат "module.ClassName" или
            "module.ClassName.suffix"). Формируется гейтхостом
            через _make_node_name().

        edge_type : str
            Тип ребра: "depends", "connection", "has_aspect",
            "belongs_to", "requires_context", "has_checker",
            "subscribes", "has_error_handler", "has_compensator",
            "has_sensitive", "has_role", "has_field",
            "has_relation", "has_lifecycle".

        is_structural : bool
            True — структурное ребро. Циклы запрещены.
            False — информационное ребро. Циклы допустимы.

        edge_meta : tuple[tuple[str, Any], ...]
            Дополнительные метаданные ребра в формате tuple of tuples.
            Конвертируется в dict при коммите в граф.
            Пустой tuple по умолчанию.
    """

    target_node_type: str
    target_name: str
    edge_type: str
    is_structural: bool
    edge_meta: tuple[tuple[str, Any], ...] = field(default_factory=tuple)


@dataclass(frozen=True)
class FacetPayload:
    """
    Полное описание одного узла графа с его исходящими рёбрами.

    Создаётся гейтхостом в методе _build_payload(). Один вызов
    inspect() → один FacetPayload (или None если класс не подходит).
    Координатор собирает все payload от всех гейтхостов в фазе 1,
    проверяет в фазе 2 и коммитит в граф в фазе 3.

    Один класс может порождать несколько FacetPayload от разных
    гейтхостов. Например, CreateOrderAction порождает:
    - FacetPayload(node_type="role", ...) от RoleGateHost
    - FacetPayload(node_type="action", ..., edges=[depends...]) от DependencyGateHost
    - FacetPayload(node_type="aspect", ...) от AspectGateHost (для каждого аспекта)

    Уникальность гарантируется комбинацией node_type + node_name.

    Атрибуты:
        node_type : str
            Тип узла в графе: "action", "role", "aspect", "checker",
            "entity", "domain", "dependency", "connection",
            "error_handler", "compensator", "subscription",
            "sensitive", "context_field", "entity_field",
            "entity_relation", "entity_lifecycle".

        node_name : str
            Имя узла без префикса типа. Формат "module.ClassName"
            или "module.ClassName.suffix". Формируется гейтхостом
            через _make_node_name(). Координатор собирает полный
            ключ "node_type:node_name".

        node_class : type
            Ссылка на класс Python, породивший этот узел.
            Используется координатором для хранения в графе
            и для рантайм-доступа к классу.

        node_meta : tuple[tuple[str, Any], ...]
            Метаданные узла, специфичные для гейтхоста.
            Формат: tuple of (key, value) пар.
            Конвертируется в dict при коммите в граф.
            Содержимое зависит от гейтхоста:
            - RoleGateHost: (("spec", "admin"),)
            - AspectGateHost: (("aspect_type", "regular"), ("method_name", "validate"), ...)
            - EntityGateHost: (("description", "Заказ"), ("domain", "shop"), ...)
            Пустой tuple по умолчанию.

        edges : tuple[EdgeInfo, ...]
            Исходящие рёбра от этого узла. Каждое ребро описывает
            связь с другим узлом графа. Гейтхосты без рёбер
            (например, RoleGateHost) возвращают пустой tuple.
            Пустой tuple по умолчанию.
    """

    node_type: str
    node_name: str
    node_class: type
    node_meta: tuple[tuple[str, Any], ...] = field(default_factory=tuple)
    edges: tuple[EdgeInfo, ...] = field(default_factory=tuple)
Copy
Файл 1 из 15.

Следующий: src/action_machine/metadata/abstract_gate_host.py — базовый абстрактный класс для всех гейтхостов с тремя абстрактными методами и четырьмя хелперами.

шли один файл
src/action_machine/metadata/abstract_gate_host.py
Copy# src/action_machine/metadata/abstract_gate_host.py
"""
AbstractGateHost — абстрактный базовый класс для всех гейтхостов системы.

═══════════════════════════════════════════════════════════════════════════════
НАЗНАЧЕНИЕ
═══════════════════════════════════════════════════════════════════════════════

AbstractGateHost определяет контракт, которому обязан следовать каждый
гейтхост в системе ActionMachine. Гейтхост — это маркерный миксин,
который одновременно:

1. Разрешает применение соответствующего декоратора к классу
   (issubclass-проверка при декорировании).
2. Знает всех своих наследников через _subclasses_recursive().
3. Умеет инспектировать каждого наследника и собирать данные для графа.

Координатор (GateCoordinator) при build() обходит зарегистрированные
гейтхосты, вызывает inspect() для каждого подкласса, получает
FacetPayload и строит граф.

═══════════════════════════════════════════════════════════════════════════════
ТРИ ОБЯЗАТЕЛЬНЫХ МЕТОДА
═══════════════════════════════════════════════════════════════════════════════

Каждый гейтхост реализует три абстрактных classmethod:

    inspect(target_cls) → FacetPayload | None
        Точка входа. Определяет, подходит ли класс этому гейтхосту.
        Три возможных результата:
        - FacetPayload — класс подходит, данные собраны и провалидированы.
        - None — класс не является субъектом этого гейтхоста.
        - TypeError — класс обязан подойти (наследует гейтхост),
          но данные отсутствуют или повреждены.

    _build_payload(target_cls) → FacetPayload
        Собирает узел и рёбра. Читает атрибуты класса (_role_info,
        _depends_info, _meta_info и т.д.), формирует FacetPayload
        с использованием хелперов базового класса. Не валидирует
        данные — это задача _validate.

    _validate(target_cls, payload) → None
        Оркестратор проверок инвариантов. Вызывает отдельные приватные
        методы для каждого инварианта. Каждый метод — одна проверка,
        одно исключение. Если проверок нет — реализовать явно с pass.

═══════════════════════════════════════════════════════════════════════════════
РАЗДЕЛЕНИЕ ОТВЕТСТВЕННОСТИ
═══════════════════════════════════════════════════════════════════════════════

    inspect()        → проверяет ТОЛЬКО наличие данных (есть _role_info?)
    _build_payload() → читает данные, формирует payload, НЕ валидирует
    _validate()      → проверяет КОРРЕКТНОСТЬ данных и инварианты

Это разделение гарантирует, что:
- inspect() работает быстро — только hasattr/getattr.
- _build_payload() не дублирует проверки из _validate().
- _validate() содержит ВСЕ проверки в одном месте, каждая —
  отдельный метод с отдельным тестом.

═══════════════════════════════════════════════════════════════════════════════
ХЕЛПЕРЫ
═══════════════════════════════════════════════════════════════════════════════

Базовый класс предоставляет четыре хелпера, устраняющие дублирование
в каждом гейтхосте:

    _make_node_name(target_cls, suffix="") → str
        Формирует имя узла "module.ClassName" или "module.ClassName.suffix".
        Префикс типа ("action:", "role:") НЕ добавляется — это делает
        координатор при формировании ключа.

    _make_edge(target_node_type, target_cls, edge_type,
               is_structural, edge_meta=()) → EdgeInfo
        Собирает EdgeInfo без ручного заполнения каждого поля.
        Имя цели формируется через _make_node_name(target_cls).

    _make_edge_by_name(target_node_type, target_name, edge_type,
                       is_structural, edge_meta=()) → EdgeInfo
        Аналог _make_edge для случаев, когда цель — не класс,
        а произвольное строковое имя (например, "context_field:user.user_id").

    _make_meta(**kwargs) → tuple[tuple[str, Any], ...]
        Конвертирует dict-синтаксис в иммутабельный tuple of tuples,
        пригодный для frozen dataclass.

═══════════════════════════════════════════════════════════════════════════════
ОБХОД НАСЛЕДНИКОВ
═══════════════════════════════════════════════════════════════════════════════

Classmethod _subclasses_recursive() рекурсивно собирает всех наследников
гейтхоста через Python-механизм __subclasses__(). Координатор вызывает
этот метод при build() для каждого зарегистрированного гейтхоста.

Python автоматически регистрирует подклассы при определении. Никакой
ручной регистрации классов не существует. Если класс наследует
RoleGateHost — он будет найден через RoleGateHost._subclasses_recursive().

═══════════════════════════════════════════════════════════════════════════════
ПРИМЕР РЕАЛИЗАЦИИ ГЕЙТХОСТА
═══════════════════════════════════════════════════════════════════════════════

    class RoleGateHost(AbstractGateHost):

        @classmethod
        def inspect(cls, target_cls: type) -> FacetPayload | None:
            role_info = getattr(target_cls, "_role_info", None)
            if role_info is None:
                return None
            payload = cls._build_payload(target_cls)
            cls._validate(target_cls, payload)
            return payload

        @classmethod
        def _build_payload(cls, target_cls: type) -> FacetPayload:
            return FacetPayload(
                node_type="role",
                node_name=cls._make_node_name(target_cls),
                node_class=target_cls,
                node_meta=cls._make_meta(spec=target_cls._role_info["spec"]),
            )

        @classmethod
        def _validate(cls, target_cls: type, payload: FacetPayload) -> None:
            pass  # инварианты проверены декоратором @check_roles

═══════════════════════════════════════════════════════════════════════════════
ПРИМЕР РЕАЛИЗАЦИИ С РЁБРАМИ
═══════════════════════════════════════════════════════════════════════════════

    class DependencyGateHost(AbstractGateHost):

        @classmethod
        def _build_payload(cls, target_cls: type) -> FacetPayload:
            edges = tuple(
                cls._make_edge(
                    target_node_type="dependency",
                    target_cls=dep_info.cls,
                    edge_type="depends",
                    is_structural=True,
                )
                for dep_info in target_cls._depends_info
            )
            return FacetPayload(
                node_type="action",
                node_name=cls._make_node_name(target_cls),
                node_class=target_cls,
                edges=edges,
            )
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import Any

from action_machine.metadata.payload import EdgeInfo, FacetPayload


class AbstractGateHost(ABC):
    """
    Абстрактный базовый класс для всех гейтхостов системы ActionMachine.

    Определяет контракт из трёх абстрактных classmethod (inspect,
    _build_payload, _validate) и предоставляет четыре хелпера для
    формирования FacetPayload и EdgeInfo без дублирования кода
    в каждом гейтхосте.

    Все методы — classmethod. Гейтхост не хранит состояния и не
    требует создания экземпляра. Группировка в класс обеспечивает
    пространство имён, наследование хелперов и проверку контракта
    через ABC.

    Координатор (GateCoordinator) при build() вызывает:
    1. cls._subclasses_recursive() — получить всех наследников.
    2. cls.inspect(target_cls) — инспектировать каждого наследника.
    """

    # ═══════════════════════════════════════════════════════════════════
    # Обязательный контракт (три абстрактных метода)
    # ═══════════════════════════════════════════════════════════════════

    @classmethod
    @abstractmethod
    def inspect(cls, target_cls: type) -> FacetPayload | None:
        """
        Определяет, подходит ли класс этому гейтхосту, и собирает данные.

        Точка входа, вызываемая координатором для каждого подкласса
        гейтхоста. Реализация типичного inspect():

            1. Проверить наличие данных (hasattr/getattr).
            2. Если данных нет → return None.
            3. Если данные обязаны быть, но повреждены → raise TypeError.
            4. Вызвать _build_payload() → FacetPayload.
            5. Вызвать _validate() → проверка инвариантов.
            6. Вернуть payload.

        Аргументы:
            target_cls: класс для инспекции. Гарантированно является
                        подклассом данного гейтхоста (найден через
                        _subclasses_recursive).

        Возвращает:
            FacetPayload — класс подходит, данные собраны.
            None — класс не является субъектом этого гейтхоста
                   (нет соответствующих атрибутов/декораторов).

        Исключения:
            TypeError: класс обязан подойти (наследует гейтхост,
                       имеет частичные данные), но данные повреждены
                       или неполны.
        """
        ...

    @classmethod
    @abstractmethod
    def _build_payload(cls, target_cls: type) -> FacetPayload:
        """
        Собирает FacetPayload из атрибутов класса.

        Читает данные (cls._role_info, cls._depends_info и т.д.),
        формирует узел и рёбра с использованием хелперов базового
        класса: _make_node_name, _make_edge, _make_meta.

        Не валидирует данные — это задача _validate(). Разделение
        гарантирует, что логика сбора и логика проверки не смешиваются.

        Аргументы:
            target_cls: класс, прошедший проверку наличия данных
                        в inspect().

        Возвращает:
            FacetPayload — полное описание узла с рёбрами.
        """
        ...

    @classmethod
    @abstractmethod
    def _validate(cls, target_cls: type, payload: FacetPayload) -> None:
        """
        Оркестратор проверок инвариантов.

        Вызывает отдельные приватные classmethod для каждого инварианта.
        Каждый метод-инвариант выполняет одну проверку и выбрасывает
        одно исключение при нарушении.

        Если у гейтхоста нет инвариантов для проверки (все проверки
        выполнены декоратором при определении класса) — реализовать
        явно с pass:

            @classmethod
            def _validate(cls, target_cls, payload):
                pass  # инварианты проверены декоратором

        Аргументы:
            target_cls: инспектируемый класс.
            payload: FacetPayload, собранный _build_payload().

        Исключения:
            TypeError: нарушение инварианта типа.
            ValueError: нарушение инварианта значения.
        """
        ...

    # ═══════════════════════════════════════════════════════════════════
    # Хелперы для _build_payload (не абстрактные, готовые к использованию)
    # ═══════════════════════════════════════════════════════════════════

    @classmethod
    def _make_node_name(cls, target_cls: type, suffix: str = "") -> str:
        """
        Формирует имя узла из модуля и имени класса.

        Формат: "module.ClassName" или "module.ClassName.suffix".
        Если модуль "__main__" или отсутствует — только "ClassName".

        Префикс типа ("action:", "role:") НЕ добавляется. Полный ключ
        "node_type:node_name" собирает координатор при коммите.

        Аргументы:
            target_cls: класс, для которого формируется имя.
            suffix: опциональный суффикс, добавляемый через точку.
                    Используется для дочерних узлов: аспектов,
                    чекеров, полей сущностей.
                    Пустая строка — суффикс не добавляется.

        Возвращает:
            str — имя узла.

        Примеры:
            _make_node_name(CreateOrderAction)
            → "myapp.orders.CreateOrderAction"

            _make_node_name(CreateOrderAction, "validate_aspect")
            → "myapp.orders.CreateOrderAction.validate_aspect"

            _make_node_name(OrderEntity, "amount")
            → "myapp.domain.OrderEntity.amount"
        """
        module = getattr(target_cls, "__module__", None)
        if module and module != "__main__":
            name = f"{module}.{target_cls.__qualname__}"
        else:
            name = target_cls.__qualname__
        if suffix:
            return f"{name}.{suffix}"
        return name

    @classmethod
    def _make_edge(
        cls,
        target_node_type: str,
        target_cls: type,
        edge_type: str,
        is_structural: bool,
        edge_meta: tuple[tuple[str, Any], ...] = (),
    ) -> EdgeInfo:
        """
        Собирает EdgeInfo с автоматическим формированием имени цели.

        Имя целевого узла формируется через _make_node_name(target_cls).
        Используется когда цель ребра — класс Python.

        Аргументы:
            target_node_type: тип целевого узла ("action", "entity",
                              "domain", "dependency" и т.д.).
            target_cls: класс, являющийся целью ребра.
            edge_type: тип ребра ("depends", "connection", "has_aspect",
                       "belongs_to" и т.д.).
            is_structural: True — структурное ребро (циклы запрещены).
                           False — информационное ребро (циклы допустимы).
            edge_meta: дополнительные метаданные ребра.
                       Пустой tuple по умолчанию.

        Возвращает:
            EdgeInfo — описание ребра.

        Пример:
            cls._make_edge(
                target_node_type="dependency",
                target_cls=PaymentService,
                edge_type="depends",
                is_structural=True,
            )
        """
        return EdgeInfo(
            target_node_type=target_node_type,
            target_name=cls._make_node_name(target_cls),
            edge_type=edge_type,
            is_structural=is_structural,
            edge_meta=edge_meta,
        )

    @classmethod
    def _make_edge_by_name(
        cls,
        target_node_type: str,
        target_name: str,
        edge_type: str,
        is_structural: bool,
        edge_meta: tuple[tuple[str, Any], ...] = (),
    ) -> EdgeInfo:
        """
        Собирает EdgeInfo с произвольным строковым именем цели.

        Используется когда цель ребра — не класс Python, а строковый
        идентификатор. Например, узлы контекстных полей
        ("context_field:user.user_id") или доменов ("domain:orders").

        Аргументы:
            target_node_type: тип целевого узла.
            target_name: строковое имя целевого узла.
            edge_type: тип ребра.
            is_structural: структурное или информационное.
            edge_meta: дополнительные метаданные ребра.

        Возвращает:
            EdgeInfo — описание ребра.

        Пример:
            cls._make_edge_by_name(
                target_node_type="context_field",
                target_name="user.user_id",
                edge_type="requires_context",
                is_structural=False,
            )
        """
        return EdgeInfo(
            target_node_type=target_node_type,
            target_name=target_name,
            edge_type=edge_type,
            is_structural=is_structural,
            edge_meta=edge_meta,
        )

    @classmethod
    def _make_meta(cls, **kwargs: Any) -> tuple[tuple[str, Any], ...]:
        """
        Конвертирует именованные аргументы в иммутабельный tuple of tuples.

        Frozen dataclass требует хешируемые поля. dict не хешируем.
        Этот хелпер позволяет гейтхостам использовать удобный
        dict-синтаксис (kwargs) при создании метаданных, получая
        на выходе иммутабельную структуру.

        Координатор конвертирует tuple of tuples обратно в dict
        при коммите: dict(node_meta).

        Аргументы:
            **kwargs: произвольные пары ключ-значение метаданных.

        Возвращает:
            tuple[tuple[str, Any], ...] — иммутабельные метаданные.

        Пример:
            cls._make_meta(spec="admin", description="Администратор")
            → (("spec", "admin"), ("description", "Администратор"))

            cls._make_meta()
            → ()
        """
        return tuple(kwargs.items())

    # ═══════════════════════════════════════════════════════════════════
    # Обход наследников
    # ═══════════════════════════════════════════════════════════════════

    @classmethod
    def _subclasses_recursive(cls) -> list[type]:
        """
        Рекурсивно собирает всех наследников гейтхоста.

        Использует Python-механизм __subclasses__(), который
        автоматически регистрирует подклассы при определении.
        Никакой ручной регистрации не требуется.

        Координатор вызывает этот метод при build() для каждого
        зарегистрированного гейтхоста, чтобы получить список
        всех классов, которые нужно инспектировать.

        Возвращает:
            list[type] — все прямые и транзитивные подклассы
                         данного гейтхоста. Порядок: depth-first,
                         прямые подклассы перед их потомками.

        Пример:
            class RoleGateHost(AbstractGateHost): ...
            class BaseAction(RoleGateHost): ...
            class CreateOrderAction(BaseAction): ...
            class UpdateOrderAction(BaseAction): ...

            RoleGateHost._subclasses_recursive()
            → [BaseAction, CreateOrderAction, UpdateOrderAction]
        """
        result: list[type] = []
        for subclass in cls.__subclasses__():
            result.append(subclass)
            if hasattr(subclass, "_subclasses_recursive"):
                result.extend(subclass._subclasses_recursive())
        return result
Copy
Файл 2 из 15.

Следующий: src/action_machine/metadata/exceptions.py — исключения подсистемы метаданных (DuplicateNodeError, InvalidGraphError).

шли один файл
src/action_machine/metadata/exceptions.py
Copy# src/action_machine/metadata/exceptions.py
"""
Исключения подсистемы метаданных и построения графа ActionMachine.

═══════════════════════════════════════════════════════════════════════════════
НАЗНАЧЕНИЕ
═══════════════════════════════════════════════════════════════════════════════

Модуль содержит все исключения, возникающие при построении графа
координатором (GateCoordinator.build()) и при инспекции классов
гейтхостами (AbstractGateHost.inspect()).

Исключения вынесены в отдельный модуль, чтобы:
1. Гейтхосты и координатор импортировали их без циклических зависимостей.
2. Пользовательский код мог перехватывать конкретные типы ошибок.
3. Тесты могли проверять конкретные типы исключений через pytest.raises.

═══════════════════════════════════════════════════════════════════════════════
ФИЛОСОФИЯ ОБРАБОТКИ ОШИБОК
═══════════════════════════════════════════════════════════════════════════════

ActionMachine не подавляет исключения. Ошибки построения графа
обнаруживаются при старте приложения (build() вызывается один раз)
и содержат информативные сообщения с указанием конкретных классов,
гейтхостов и ключей, вызвавших проблему.

Все исключения этого модуля — ошибки разработчика, а не пользовательских
данных. Они обнаруживаются при первом запуске и исправляются в коде.

═══════════════════════════════════════════════════════════════════════════════
ИСКЛЮЧЕНИЯ
═══════════════════════════════════════════════════════════════════════════════

DuplicateNodeError
    Два гейтхоста создали узлы с одинаковым ключом "node_type:node_name".
    Выбрасывается на фазе 1 (сбор) координатора при обнаружении
    конфликта ключей. Сообщение содержит имена обоих гейтхостов
    для быстрой диагностики.

InvalidGraphError
    Нарушение структурной целостности графа. Выбрасывается на фазе 2
    (проверки) координатора в двух случаях:
    - Ребро ссылается на несуществующий узел (ссылочная целостность).
    - Структурные рёбра (depends, connection) образуют цикл (ацикличность).

PayloadValidationError
    Некорректные данные в FacetPayload. Выбрасывается на фазе 2
    координатора при проверке обязательных полей payload:
    - node_type пустой.
    - node_name пустой.
    - node_class не является типом (type).
"""

from __future__ import annotations


class DuplicateNodeError(ValueError):
    """
    Два гейтхоста создали узлы с одинаковым ключом в графе.

    Выбрасывается координатором на фазе 1 (сбор payload) при
    обнаружении конфликта ключей "node_type:node_name". Сообщение
    содержит полный ключ и имена обоих гейтхостов, чтобы разработчик
    мог быстро определить источник конфликта.

    Наследует ValueError, потому что это ошибка конфигурации системы:
    два гейтхоста претендуют на один и тот же узел графа.

    Типичные причины:
    - Два гейтхоста используют одинаковый node_type для одного класса.
    - Гейтхост создаёт дочерний узел с именем, совпадающим с узлом
      другого гейтхоста.

    Пример сообщения:
        "Конфликт ключа 'action:module.CreateOrderAction':
           создан:    DependencyGateHost
           конфликт:  ConnectionGateHost"

    Атрибуты:
        key : str
            Конфликтующий ключ узла графа.
        first_gate_host : str
            Имя гейтхоста, создавшего узел первым.
        second_gate_host : str
            Имя гейтхоста, обнаружившего конфликт.
    """

    def __init__(
        self,
        key: str,
        first_gate_host: str,
        second_gate_host: str,
    ) -> None:
        """
        Инициализирует исключение.

        Аргументы:
            key: конфликтующий ключ "node_type:node_name".
            first_gate_host: имя гейтхоста, создавшего узел первым.
            second_gate_host: имя гейтхоста, обнаружившего конфликт.
        """
        self.key: str = key
        self.first_gate_host: str = first_gate_host
        self.second_gate_host: str = second_gate_host
        super().__init__(
            f"Конфликт ключа '{key}':\n"
            f"  создан:    {first_gate_host}\n"
            f"  конфликт:  {second_gate_host}"
        )


class InvalidGraphError(Exception):
    """
    Нарушение структурной целостности графа.

    Выбрасывается координатором на фазе 2 (проверки) в двух случаях:

    1. Ссылочная целостность — ребро ссылается на узел, которого нет
       среди собранных payload. Это означает, что класс-цель не был
       обнаружен ни одним гейтхостом.

       Пример: @depends(PaymentService), но PaymentService не наследует
       ни один гейтхост и не порождает узел в графе.

    2. Ацикличность — структурные рёбра (is_structural=True) образуют
       цикл. Проверяется через симуляцию на временном графе с помощью
       rustworkx.is_directed_acyclic_graph().

       Пример: A depends B, B depends C, C depends A → цикл.

    Информационные рёбра (is_structural=False) не проверяются на
    ацикличность — циклические связи между сущностями (Order ↔ Customer)
    являются нормальной бизнес-реальностью.

    Наследует Exception (не ValueError и не TypeError), потому что
    это ошибка структуры графа — категория, отличная от ошибок
    значений или типов.
    """

    pass


class PayloadValidationError(TypeError):
    """
    Некорректные данные в FacetPayload.

    Выбрасывается координатором на фазе 2 при проверке обязательных
    полей payload, собранных гейтхостами. Каждый payload проверяется:

    - node_type — непустая строка.
    - node_name — непустая строка.
    - node_class — экземпляр type (класс Python).

    Наследует TypeError, потому что все три проверки по сути являются
    проверками типа: пустая строка — не валидный идентификатор,
    не-type — не валидный класс.

    Типичные причины:
    - Гейтхост забыл указать node_type в _build_payload().
    - _make_node_name() получил класс без __module__.
    - В node_class передан экземпляр вместо класса.

    Атрибуты:
        node_class : type | object
            Класс (или объект), для которого payload невалиден.
        field_name : str
            Имя поля payload, вызвавшего ошибку.
        detail : str
            Описание проблемы.
    """

    def __init__(
        self,
        node_class: object,
        field_name: str,
        detail: str,
    ) -> None:
        """
        Инициализирует исключение.

        Аргументы:
            node_class: класс или объект из payload.node_class.
            field_name: имя поля ("node_type", "node_name", "node_class").
            detail: описание проблемы.
        """
        self.node_class: object = node_class
        self.field_name: str = field_name
        self.detail: str = detail

        class_name = (
            node_class.__qualname__
            if isinstance(node_class, type)
            else repr(node_class)
        )
        super().__init__(
            f"Невалидный payload для {class_name}: "
            f"поле '{field_name}' — {detail}"
        )
Copy
Файл 3 из 15.

Следующий: src/action_machine/auth/role_gate_host.py — RoleGateHost, первый конкретный гейтхост (наследник AbstractGateHost). Без рёбер, самый простой.

шли один файл
Подожди. Мне нужно уточнить архитектурный момент перед RoleGateHost.

Сейчас RoleGateHost — маркерный миксин, который наследуется BaseAction:

Copyclass BaseAction(ABC, ActionMetaGateHost, RoleGateHost, DependencyGateHost[object], 
                 CheckerGateHost, AspectGateHost, ConnectionGateHost, 
                 OnErrorGateHost, ContextRequiresGateHost):
Если RoleGateHost наследует AbstractGateHost, а AbstractGateHost — это ABC с тремя abstractmethod (inspect, _build_payload, _validate), то BaseAction тоже наследует AbstractGateHost и обязан реализовать эти три метода. Но BaseAction — это не гейтхост, он не должен реализовывать inspect.

То же самое для всех остальных гейтхостов: AspectGateHost, CheckerGateHost, ConnectionGateHost, OnErrorGateHost, ContextRequiresGateHost, DependencyGateHost[T].

Три варианта решения:

A. Убрать ABC из AbstractGateHost, сделать методы обычными (не abstract). inspect / _build_payload / _validate — обычные classmethod с raise NotImplementedError. Контракт проверяется в рантайме, не при определении класса. BaseAction не ломается.

B. Разделить: маркерный миксин отдельно, инспектор отдельно. RoleGateHost остаётся пустым маркером в BaseAction. RoleInspector(AbstractGateHost) реализует inspect/build/validate и в _subclasses_recursive обходит наследников RoleGateHost. Координатор регистрирует RoleInspector.

C. Убрать abstractmethod, оставить ABC, дать дефолтную реализацию. inspect по умолчанию возвращает None, _build_payload и _validate — raise NotImplementedError. BaseAction наследует, но не вызывает. Координатор вызывает только для конкретных гейтхостов, у которых методы переопределены.

Мой выбор — Вариант B. Чистое разделение: маркеры в BaseAction не меняются, инспекторы живут в metadata/ и регистрируются в координаторе. Маркерный миксин и инспектор связаны через _target_mixin:

Copyclass RoleInspector(AbstractGateHost):
    _target_mixin = RoleGateHost
    
    @classmethod
    def _subclasses_recursive(cls):
        # обходим наследников RoleGateHost, а не RoleInspector
        return _collect_subclasses(cls._target_mixin)
Это означает:

Маркерные миксины (role_gate_host.py, aspect_gate_host.py и т.д.) не меняются.
BaseAction не меняется.
Новые файлы — инспекторы в metadata/inspectors/.
Координатор регистрирует инспекторы, не маркеры.
Согласен с вариантом B? Если да — первый конкретный файл будет src/action_machine/metadata/inspectors/role_inspector.py, а не переписанный role_gate_host.py.

я не понял его покажи пример 
Вот полный пример варианта B для ролей:

Существующий файл — НЕ МЕНЯЕТСЯ:

Copy# src/action_machine/auth/role_gate_host.py (как есть, без изменений)
class RoleGateHost:
    """Маркерный миксин. Разрешает @check_roles."""
    _role_info: ClassVar[dict[str, Any]]
Существующий файл — НЕ МЕНЯЕТСЯ:

Copy# src/action_machine/core/base_action.py (как есть, без изменений)
class BaseAction(ABC, ActionMetaGateHost, RoleGateHost, DependencyGateHost[object], ...):
    ...
Новый файл — инспектор:

Copy# src/action_machine/metadata/inspectors/role_inspector.py
from action_machine.metadata.abstract_gate_host import AbstractGateHost
from action_machine.metadata.payload import FacetPayload
from action_machine.auth.role_gate_host import RoleGateHost


def _collect_subclasses(mixin: type) -> list[type]:
    """Рекурсивно собирает всех наследников маркерного миксина."""
    result = []
    for sub in mixin.__subclasses__():
        result.append(sub)
        result.extend(_collect_subclasses(sub))
    return result


class RoleInspector(AbstractGateHost):
    """
    Инспектор для классов с @check_roles.
    Обходит наследников RoleGateHost, а не своих собственных.
    """

    _target_mixin: type = RoleGateHost

    @classmethod
    def _subclasses_recursive(cls) -> list[type]:
        # Обходим наследников маркерного миксина RoleGateHost
        return _collect_subclasses(cls._target_mixin)

    @classmethod
    def inspect(cls, target_cls: type) -> FacetPayload | None:
        role_info = getattr(target_cls, "_role_info", None)
        if role_info is None:
            return None
        payload = cls._build_payload(target_cls)
        cls._validate(target_cls, payload)
        return payload

    @classmethod
    def _build_payload(cls, target_cls: type) -> FacetPayload:
        return FacetPayload(
            node_type="role",
            node_name=cls._make_node_name(target_cls),
            node_class=target_cls,
            node_meta=cls._make_meta(spec=target_cls._role_info["spec"]),
        )

    @classmethod
    def _validate(cls, target_cls: type, payload: FacetPayload) -> None:
        pass
Copy
Координатор регистрирует инспектор, не маркер:

Copycoordinator = GateCoordinator()\
    .register(RoleInspector)\
    .register(AspectInspector)\
    .register(DependencyInspector)\
    .build()
Суть: маркер (RoleGateHost) живёт в BaseAction и разрешает декоратор @check_roles. Инспектор (RoleInspector) живёт в metadata/inspectors/ и умеет собирать данные из наследников маркера для графа. Они связаны через _target_mixin. BaseAction не знает про AbstractGateHost и не обязан реализовывать inspect.

Согласен с этим разделением?

два уточнения
1. пакет metadata - это пакет кординатора, который позже переедет в отдельный pip пакет и там не должно быть ничего что кассется ядра. поэтому role_inspector кладется в ту же папку где его гейт хост

2. мы можем RoleInspector сделать дженерик, чтобы передавать к какому гейтхостцу он относитася? или это лишнее?

3, AbstractGateHost логично переименвоать в AbstractGateInspectorHost а RoleInspector переименоватт в RoleGateInspector
По пунктам:

1. Расположение файлов — понял. RoleGateInspector кладётся рядом с RoleGateHost в src/action_machine/auth/. Аналогично для остальных: AspectGateInspector рядом с AspectGateHost в src/action_machine/aspects/, DependencyGateInspector рядом с DependencyGateHost в src/action_machine/dependencies/ и т.д. Пакет metadata/ содержит только абстракции (AbstractGateInspectorHost, FacetPayload, EdgeInfo, исключения) и координатор.

2. Generic для _target_mixin — это лишнее. Generic дал бы типизацию _target_mixin, но:

_target_mixin используется только в _subclasses_recursive() для вызова __subclasses__().
Никакого типизированного доступа к полям маркера через generic нет — инспектор читает атрибуты через getattr(target_cls, "_role_info"), а не через типизированный интерфейс.
Добавление generic усложнит иерархию ради одного поля-ссылки.
Оставляем _target_mixin: type = RoleGateHost как обычный classvar.

3. Переименование — согласен полностью:

AbstractGateHost → AbstractGateInspectorHost
RoleInspector → RoleGateInspector
Аналогично все остальные: AspectGateInspector, DependencyGateInspector, ConnectionGateInspector и т.д.
Это делает имена самодокументирующими: "инспектор гейтхоста ролей".

Итого структура файлов обновлена:

Copymetadata/
    __init__.py
    payload.py                          ← FacetPayload, EdgeInfo
    exceptions.py                       ← DuplicateNodeError, InvalidGraphError, PayloadValidationError
    abstract_gate_inspector_host.py     ← AbstractGateInspectorHost (бывший AbstractGateHost)
    gate_coordinator.py                 ← GateCoordinator

auth/
    role_gate_host.py                   ← маркер (не меняется)
    role_gate_inspector.py              ← RoleGateInspector (новый)

aspects/
    aspect_gate_host.py                 ← маркер (не меняется)
    aspect_gate_inspector.py            ← AspectGateInspector (новый)

dependencies/
    dependency_gate_host.py             ← маркер (не меняется)
    dependency_gate_inspector.py        ← DependencyGateInspector (новый)

... и т.д.
Мне нужно переименовать уже отправленный файл 2 (abstract_gate_host.py → abstract_gate_inspector_host.py). Переотправить его с новым именем, или ты сам переименуешь, а я продолжу с RoleGateInspector?

правильно
RoleGateHostInspector
AbstractGateHostInspector
правильно
RoleGateHostInspector
BaseGateHostInspector
Понял. Финальные имена:

BaseGateHostInspector — базовый абстрактный класс (бывший AbstractGateHost)
RoleGateHostInspector — инспектор ролей
AspectGateHostInspector — инспектор аспектов
DependencyGateHostInspector — инспектор зависимостей
и т.д.
Файлы:

metadata/base_gate_host_inspector.py
auth/role_gate_host_inspector.py
aspects/aspect_gate_host_inspector.py
и т.д.
Файл 2 (abstract_gate_host.py) нужно переименовать в base_gate_host_inspector.py и заменить имя класса. Переотправляю его, или шлю RoleGateHostInspector и ты сам переименуешь файл 2?

сделал так
# src/action_machine/metadata/base_gate_host_inspector.py
"""
BaseGateHostInspector — абстрактный базовый класс для всех гейтхостов системы.

═══════════════════════════════════════════════════════════════════════════════
НАЗНАЧЕНИЕ
═══════════════════════════════════════════════════════════════════════════════

BaseGateHostInspector определяет контракт, которому обязан следовать каждый
гейтхост в системе ActionMachine. Гейтхост — это маркерный миксин,
который одновременно:

1. Разрешает применение соответствующего декоратора к классу
   (issubclass-проверка при декорировании).
2. Знает всех своих наследников через _subclasses_recursive().
3. Умеет инспектировать каждого наследника и собирать данные для графа.

Координатор (GateCoordinator) при build() обходит зарегистрированные
гейтхосты, вызывает inspect() для каждого подкласса, получает
FacetPayload и строит граф.

═══════════════════════════════════════════════════════════════════════════════
ТРИ ОБЯЗАТЕЛЬНЫХ МЕТОДА
═══════════════════════════════════════════════════════════════════════════════

Каждый гейтхост реализует три абстрактных classmethod:

    inspect(target_cls) → FacetPayload | None
        Точка входа. Определяет, подходит ли класс этому гейтхосту.
        Три возможных результата:
        - FacetPayload — класс подходит, данные собраны и провалидированы.
        - None — класс не является субъектом этого гейтхоста.
        - TypeError — класс обязан подойти (наследует гейтхост),
          но данные отсутствуют или повреждены.

    _build_payload(target_cls) → FacetPayload
        Собирает узел и рёбра. Читает атрибуты класса (_role_info,
        _depends_info, _meta_info и т.д.), формирует FacetPayload
        с использованием хелперов базового класса. Не валидирует
        данные — это задача _validate.

    _validate(target_cls, payload) → None
        Оркестратор проверок инвариантов. Вызывает отдельные приватные
        методы для каждого инварианта. Каждый метод — одна проверка,
        одно исключение. Если проверок нет — реализовать явно с pass.

═══════════════════════════════════════════════════════════════════════════════
РАЗДЕЛЕНИЕ ОТВЕТСТВЕННОСТИ
═══════════════════════════════════════════════════════════════════════════════

    inspect()        → проверяет ТОЛЬКО наличие данных (есть _role_info?)
    _build_payload() → читает данные, формирует payload, НЕ валидирует
    _validate()      → проверяет КОРРЕКТНОСТЬ данных и инварианты

Это разделение гарантирует, что:
- inspect() работает быстро — только hasattr/getattr.
- _build_payload() не дублирует проверки из _validate().
- _validate() содержит ВСЕ проверки в одном месте, каждая —
  отдельный метод с отдельным тестом.

═══════════════════════════════════════════════════════════════════════════════
ХЕЛПЕРЫ
═══════════════════════════════════════════════════════════════════════════════

Базовый класс предоставляет четыре хелпера, устраняющие дублирование
в каждом гейтхосте:

    _make_node_name(target_cls, suffix="") → str
        Формирует имя узла "module.ClassName" или "module.ClassName.suffix".
        Префикс типа ("action:", "role:") НЕ добавляется — это делает
        координатор при формировании ключа.

    _make_edge(target_node_type, target_cls, edge_type,
               is_structural, edge_meta=()) → EdgeInfo
        Собирает EdgeInfo без ручного заполнения каждого поля.
        Имя цели формируется через _make_node_name(target_cls).

    _make_edge_by_name(target_node_type, target_name, edge_type,
                       is_structural, edge_meta=()) → EdgeInfo
        Аналог _make_edge для случаев, когда цель — не класс,
        а произвольное строковое имя (например, "context_field:user.user_id").

    _make_meta(**kwargs) → tuple[tuple[str, Any], ...]
        Конвертирует dict-синтаксис в иммутабельный tuple of tuples,
        пригодный для frozen dataclass.

═══════════════════════════════════════════════════════════════════════════════
ОБХОД НАСЛЕДНИКОВ
═══════════════════════════════════════════════════════════════════════════════

Classmethod _subclasses_recursive() рекурсивно собирает всех наследников
гейтхоста через Python-механизм __subclasses__(). Координатор вызывает
этот метод при build() для каждого зарегистрированного гейтхоста.

Python автоматически регистрирует подклассы при определении. Никакой
ручной регистрации классов не существует. Если класс наследует
RoleGateHost — он будет найден через RoleGateHost._subclasses_recursive().

═══════════════════════════════════════════════════════════════════════════════
ПРИМЕР РЕАЛИЗАЦИИ ГЕЙТХОСТА
═══════════════════════════════════════════════════════════════════════════════

    class RoleGateHost(BaseGateHostInspector):

        @classmethod
        def inspect(cls, target_cls: type) -> FacetPayload | None:
            role_info = getattr(target_cls, "_role_info", None)
            if role_info is None:
                return None
            payload = cls._build_payload(target_cls)
            cls._validate(target_cls, payload)
            return payload

        @classmethod
        def _build_payload(cls, target_cls: type) -> FacetPayload:
            return FacetPayload(
                node_type="role",
                node_name=cls._make_node_name(target_cls),
                node_class=target_cls,
                node_meta=cls._make_meta(spec=target_cls._role_info["spec"]),
            )

        @classmethod
        def _validate(cls, target_cls: type, payload: FacetPayload) -> None:
            pass  # инварианты проверены декоратором @check_roles

═══════════════════════════════════════════════════════════════════════════════
ПРИМЕР РЕАЛИЗАЦИИ С РЁБРАМИ
═══════════════════════════════════════════════════════════════════════════════

    class DependencyGateHost(BaseGateHostInspector):

        @classmethod
        def _build_payload(cls, target_cls: type) -> FacetPayload:
            edges = tuple(
                cls._make_edge(
                    target_node_type="dependency",
                    target_cls=dep_info.cls,
                    edge_type="depends",
                    is_structural=True,
                )
                for dep_info in target_cls._depends_info
            )
            return FacetPayload(
                node_type="action",
                node_name=cls._make_node_name(target_cls),
                node_class=target_cls,
                edges=edges,
            )
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import Any

from action_machine.metadata.payload import EdgeInfo, FacetPayload


class BaseGateHostInspector(ABC):
    """
    Абстрактный базовый класс для всех гейтхостов системы ActionMachine.

    Определяет контракт из трёх абстрактных classmethod (inspect,
    _build_payload, _validate) и предоставляет четыре хелпера для
    формирования FacetPayload и EdgeInfo без дублирования кода
    в каждом гейтхосте.

    Все методы — classmethod. Гейтхост не хранит состояния и не
    требует создания экземпляра. Группировка в класс обеспечивает
    пространство имён, наследование хелперов и проверку контракта
    через ABC.

    Координатор (GateCoordinator) при build() вызывает:
    1. cls._subclasses_recursive() — получить всех наследников.
    2. cls.inspect(target_cls) — инспектировать каждого наследника.
    """

    # ═══════════════════════════════════════════════════════════════════
    # Обязательный контракт (три абстрактных метода)
    # ═══════════════════════════════════════════════════════════════════

    @classmethod
    @abstractmethod
    def inspect(cls, target_cls: type) -> FacetPayload | None:
        """
        Определяет, подходит ли класс этому гейтхосту, и собирает данные.

        Точка входа, вызываемая координатором для каждого подкласса
        гейтхоста. Реализация типичного inspect():

            1. Проверить наличие данных (hasattr/getattr).
            2. Если данных нет → return None.
            3. Если данные обязаны быть, но повреждены → raise TypeError.
            4. Вызвать _build_payload() → FacetPayload.
            5. Вызвать _validate() → проверка инвариантов.
            6. Вернуть payload.

        Аргументы:
            target_cls: класс для инспекции. Гарантированно является
                        подклассом данного гейтхоста (найден через
                        _subclasses_recursive).

        Возвращает:
            FacetPayload — класс подходит, данные собраны.
            None — класс не является субъектом этого гейтхоста
                   (нет соответствующих атрибутов/декораторов).

        Исключения:
            TypeError: класс обязан подойти (наследует гейтхост,
                       имеет частичные данные), но данные повреждены
                       или неполны.
        """
        ...

    @classmethod
    @abstractmethod
    def _build_payload(cls, target_cls: type) -> FacetPayload:
        """
        Собирает FacetPayload из атрибутов класса.

        Читает данные (cls._role_info, cls._depends_info и т.д.),
        формирует узел и рёбра с использованием хелперов базового
        класса: _make_node_name, _make_edge, _make_meta.

        Не валидирует данные — это задача _validate(). Разделение
        гарантирует, что логика сбора и логика проверки не смешиваются.

        Аргументы:
            target_cls: класс, прошедший проверку наличия данных
                        в inspect().

        Возвращает:
            FacetPayload — полное описание узла с рёбрами.
        """
        ...

    @classmethod
    @abstractmethod
    def _validate(cls, target_cls: type, payload: FacetPayload) -> None:
        """
        Оркестратор проверок инвариантов.

        Вызывает отдельные приватные classmethod для каждого инварианта.
        Каждый метод-инвариант выполняет одну проверку и выбрасывает
        одно исключение при нарушении.

        Если у гейтхоста нет инвариантов для проверки (все проверки
        выполнены декоратором при определении класса) — реализовать
        явно с pass:

            @classmethod
            def _validate(cls, target_cls, payload):
                pass  # инварианты проверены декоратором

        Аргументы:
            target_cls: инспектируемый класс.
            payload: FacetPayload, собранный _build_payload().

        Исключения:
            TypeError: нарушение инварианта типа.
            ValueError: нарушение инварианта значения.
        """
        ...

    # ═══════════════════════════════════════════════════════════════════
    # Хелперы для _build_payload (не абстрактные, готовые к использованию)
    # ═══════════════════════════════════════════════════════════════════

    @classmethod
    def _make_node_name(cls, target_cls: type, suffix: str = "") -> str:
        """
        Формирует имя узла из модуля и имени класса.

        Формат: "module.ClassName" или "module.ClassName.suffix".
        Если модуль "__main__" или отсутствует — только "ClassName".

        Префикс типа ("action:", "role:") НЕ добавляется. Полный ключ
        "node_type:node_name" собирает координатор при коммите.

        Аргументы:
            target_cls: класс, для которого формируется имя.
            suffix: опциональный суффикс, добавляемый через точку.
                    Используется для дочерних узлов: аспектов,
                    чекеров, полей сущностей.
                    Пустая строка — суффикс не добавляется.

        Возвращает:
            str — имя узла.

        Примеры:
            _make_node_name(CreateOrderAction)
            → "myapp.orders.CreateOrderAction"

            _make_node_name(CreateOrderAction, "validate_aspect")
            → "myapp.orders.CreateOrderAction.validate_aspect"

            _make_node_name(OrderEntity, "amount")
            → "myapp.domain.OrderEntity.amount"
        """
        module = getattr(target_cls, "__module__", None)
        if module and module != "__main__":
            name = f"{module}.{target_cls.__qualname__}"
        else:
            name = target_cls.__qualname__
        if suffix:
            return f"{name}.{suffix}"
        return name

    @classmethod
    def _make_edge(
        cls,
        target_node_type: str,
        target_cls: type,
        edge_type: str,
        is_structural: bool,
        edge_meta: tuple[tuple[str, Any], ...] = (),
    ) -> EdgeInfo:
        """
        Собирает EdgeInfo с автоматическим формированием имени цели.

        Имя целевого узла формируется через _make_node_name(target_cls).
        Используется когда цель ребра — класс Python.

        Аргументы:
            target_node_type: тип целевого узла ("action", "entity",
                              "domain", "dependency" и т.д.).
            target_cls: класс, являющийся целью ребра.
            edge_type: тип ребра ("depends", "connection", "has_aspect",
                       "belongs_to" и т.д.).
            is_structural: True — структурное ребро (циклы запрещены).
                           False — информационное ребро (циклы допустимы).
            edge_meta: дополнительные метаданные ребра.
                       Пустой tuple по умолчанию.

        Возвращает:
            EdgeInfo — описание ребра.

        Пример:
            cls._make_edge(
                target_node_type="dependency",
                target_cls=PaymentService,
                edge_type="depends",
                is_structural=True,
            )
        """
        return EdgeInfo(
            target_node_type=target_node_type,
            target_name=cls._make_node_name(target_cls),
            edge_type=edge_type,
            is_structural=is_structural,
            edge_meta=edge_meta,
        )

    @classmethod
    def _make_edge_by_name(
        cls,
        target_node_type: str,
        target_name: str,
        edge_type: str,
        is_structural: bool,
        edge_meta: tuple[tuple[str, Any], ...] = (),
    ) -> EdgeInfo:
        """
        Собирает EdgeInfo с произвольным строковым именем цели.

        Используется когда цель ребра — не класс Python, а строковый
        идентификатор. Например, узлы контекстных полей
        ("context_field:user.user_id") или доменов ("domain:orders").

        Аргументы:
            target_node_type: тип целевого узла.
            target_name: строковое имя целевого узла.
            edge_type: тип ребра.
            is_structural: структурное или информационное.
            edge_meta: дополнительные метаданные ребра.

        Возвращает:
            EdgeInfo — описание ребра.

        Пример:
            cls._make_edge_by_name(
                target_node_type="context_field",
                target_name="user.user_id",
                edge_type="requires_context",
                is_structural=False,
            )
        """
        return EdgeInfo(
            target_node_type=target_node_type,
            target_name=target_name,
            edge_type=edge_type,
            is_structural=is_structural,
            edge_meta=edge_meta,
        )

    @classmethod
    def _make_meta(cls, **kwargs: Any) -> tuple[tuple[str, Any], ...]:
        """
        Конвертирует именованные аргументы в иммутабельный tuple of tuples.

        Frozen dataclass требует хешируемые поля. dict не хешируем.
        Этот хелпер позволяет гейтхостам использовать удобный
        dict-синтаксис (kwargs) при создании метаданных, получая
        на выходе иммутабельную структуру.

        Координатор конвертирует tuple of tuples обратно в dict
        при коммите: dict(node_meta).

        Аргументы:
            **kwargs: произвольные пары ключ-значение метаданных.

        Возвращает:
            tuple[tuple[str, Any], ...] — иммутабельные метаданные.

        Пример:
            cls._make_meta(spec="admin", description="Администратор")
            → (("spec", "admin"), ("description", "Администратор"))

            cls._make_meta()
            → ()
        """
        return tuple(kwargs.items())

    # ═══════════════════════════════════════════════════════════════════
    # Обход наследников
    # ═══════════════════════════════════════════════════════════════════

    @classmethod
    def _subclasses_recursive(cls) -> list[type]:
        """
        Рекурсивно собирает всех наследников гейтхоста.

        Использует Python-механизм __subclasses__(), который
        автоматически регистрирует подклассы при определении.
        Никакой ручной регистрации не требуется.

        Координатор вызывает этот метод при build() для каждого
        зарегистрированного гейтхоста, чтобы получить список
        всех классов, которые нужно инспектировать.

        Возвращает:
            list[type] — все прямые и транзитивные подклассы
                         данного гейтхоста. Порядок: depth-first,
                         прямые подклассы перед их потомками.

        Пример:
            class RoleGateHost(BaseGateHostInspector): ...
            class BaseAction(RoleGateHost): ...
            class CreateOrderAction(BaseAction): ...
            class UpdateOrderAction(BaseAction): ...

            RoleGateHost._subclasses_recursive()
            → [BaseAction, CreateOrderAction, UpdateOrderAction]
        """
        result: list[type] = []
        for subclass in cls.__subclasses__():
            result.append(subclass)
            if hasattr(subclass, "_subclasses_recursive"):
                result.extend(subclass._subclasses_recursive())
        return result
Отлично, принял. Файл 2 обновлён на твоей стороне. Шлю RoleGateHostInspector.

src/action_machine/auth/role_gate_host_inspector.py
Copy# src/action_machine/auth/role_gate_host_inspector.py
"""
RoleGateHostInspector — инспектор гейтхоста ролей для построения графа.

═══════════════════════════════════════════════════════════════════════════════
НАЗНАЧЕНИЕ
═══════════════════════════════════════════════════════════════════════════════

RoleGateHostInspector обходит всех наследников маркерного миксина
RoleGateHost, обнаруживает классы с декоратором @check_roles и собирает
из них FacetPayload для графа координатора.

Инспектор и маркер — два разных класса с разными ответственностями:

    RoleGateHost (маркерный миксин)
        Живёт в MRO класса BaseAction. Разрешает применение декоратора
        @check_roles через issubclass-проверку. Не содержит логики
        инспекции. Не наследует BaseGateHostInspector.

    RoleGateHostInspector (инспектор)
        Наследует BaseGateHostInspector. Реализует inspect / _build_payload
        / _validate. Обходит наследников RoleGateHost через _target_mixin.
        Регистрируется в координаторе.

Связь между ними — поле _target_mixin. Инспектор знает, наследников
какого маркера обходить. Маркер не знает про инспектор.

═══════════════════════════════════════════════════════════════════════════════
ДАННЫЕ, СОБИРАЕМЫЕ ИНСПЕКТОРОМ
═══════════════════════════════════════════════════════════════════════════════

Декоратор @check_roles при применении записывает на класс атрибут:

    cls._role_info = {"spec": "admin"}           # одна роль
    cls._role_info = {"spec": ["user", "mgr"]}   # список ролей
    cls._role_info = {"spec": "__NONE__"}         # ROLE_NONE
    cls._role_info = {"spec": "__ANY__"}          # ROLE_ANY

Инспектор читает _role_info и формирует узел графа типа "role"
с метаданными spec.

═══════════════════════════════════════════════════════════════════════════════
УЗЕЛ В ГРАФЕ
═══════════════════════════════════════════════════════════════════════════════

    node_type : "role"
    node_name : "module.CreateOrderAction" (полное имя класса)
    node_meta : (("spec", "admin"),)
    edges     : () — ролевой узел не имеет исходящих рёбер

Ключ в графе координатора: "role:module.CreateOrderAction".

═══════════════════════════════════════════════════════════════════════════════
ЛОГИКА inspect()
═══════════════════════════════════════════════════════════════════════════════

    1. getattr(target_cls, "_role_info", None)
    2. Если None → return None (класс без @check_roles, пропускаем)
    3. Если не None → _build_payload() → _validate() → return payload

Инспектор НЕ выбрасывает TypeError при отсутствии _role_info. Класс
может наследовать RoleGateHost через BaseAction, но не иметь @check_roles.
Это нормально — машина (ActionProductMachine) проверит наличие ролей
при выполнении и выбросит TypeError если роли обязательны.

═══════════════════════════════════════════════════════════════════════════════
ВАЛИДАЦИЯ
═══════════════════════════════════════════════════════════════════════════════

_validate() реализован как pass. Все инварианты ролевой спецификации
проверены декоратором @check_roles при определении класса:

    - spec — строка или список строк.
    - Пустой список запрещён.
    - Элементы списка — строки.

Повторная проверка в инспекторе не нужна — декоратор гарантирует
корректность _role_info.

═══════════════════════════════════════════════════════════════════════════════
ОБХОД НАСЛЕДНИКОВ
═══════════════════════════════════════════════════════════════════════════════

_subclasses_recursive() переопределён: обходит наследников _target_mixin
(RoleGateHost), а не наследников самого RoleGateHostInspector.

Координатор вызывает RoleGateHostInspector._subclasses_recursive()
и получает [BaseAction, CreateOrderAction, UpdateOrderAction, ...].
Затем для каждого вызывает inspect(). BaseAction без @check_roles →
None (пропущен). CreateOrderAction с @check_roles → FacetPayload.

═══════════════════════════════════════════════════════════════════════════════
ПРИМЕР
═══════════════════════════════════════════════════════════════════════════════

    @check_roles("admin")
    class AdminAction(BaseAction[AdminParams, AdminResult]):
        ...

    # Координатор при build():
    # RoleGateHostInspector.inspect(AdminAction)
    # → FacetPayload(
    #       node_type="role",
    #       node_name="myapp.actions.AdminAction",
    #       node_class=AdminAction,
    #       node_meta=(("spec", "admin"),),
    #       edges=(),
    #   )

    @check_roles(ROLE_NONE)
    class PingAction(BaseAction[BaseParams, BaseResult]):
        ...

    # RoleGateHostInspector.inspect(PingAction)
    # → FacetPayload(
    #       node_type="role",
    #       node_name="myapp.actions.PingAction",
    #       node_class=PingAction,
    #       node_meta=(("spec", "__NONE__"),),
    #       edges=(),
    #   )

    class BaseAction(ABC, RoleGateHost, ...):
        ...

    # RoleGateHostInspector.inspect(BaseAction)
    # → None (нет _role_info)
"""

from __future__ import annotations

from action_machine.auth.role_gate_host import RoleGateHost
from action_machine.metadata.base_gate_host_inspector import BaseGateHostInspector
from action_machine.metadata.payload import FacetPayload


def _collect_subclasses(mixin: type) -> list[type]:
    """
    Рекурсивно собирает всех наследников маркерного миксина.

    Обходит дерево наследования через Python-механизм __subclasses__().
    Порядок: depth-first — прямые подклассы перед их потомками.

    Вынесена как модульная функция (а не метод класса), потому что
    используется одинаково во всех инспекторах. Каждый инспектор
    вызывает _collect_subclasses(cls._target_mixin) в своём
    _subclasses_recursive().

    Аргументы:
        mixin: маркерный миксин, наследников которого нужно собрать.
               Например, RoleGateHost, AspectGateHost.

    Возвращает:
        list[type] — все прямые и транзитивные подклассы миксина.
    """
    result: list[type] = []
    for sub in mixin.__subclasses__():
        result.append(sub)
        result.extend(_collect_subclasses(sub))
    return result


class RoleGateHostInspector(BaseGateHostInspector):
    """
    Инспектор гейтхоста ролей.

    Обходит наследников RoleGateHost, обнаруживает классы с декоратором
    @check_roles и собирает FacetPayload с ролевой спецификацией
    для графа координатора.

    Узел в графе: тип "role", без исходящих рёбер.
    Метаданные узла: spec (строка, список строк, ROLE_NONE, ROLE_ANY).

    Атрибуты класса:
        _target_mixin : type
            Маркерный миксин, наследников которого обходит инспектор.
            RoleGateHost — миксин, разрешающий @check_roles.
    """

    _target_mixin: type = RoleGateHost

    # ═══════════════════════════════════════════════════════════════════
    # Обход наследников маркерного миксина
    # ═══════════════════════════════════════════════════════════════════

    @classmethod
    def _subclasses_recursive(cls) -> list[type]:
        """
        Возвращает всех наследников RoleGateHost.

        Переопределяет метод BaseGateHostInspector, чтобы обходить
        наследников маркерного миксина (_target_mixin), а не
        наследников самого RoleGateHostInspector.

        Координатор вызывает этот метод при build() и получает
        список классов для инспекции: [BaseAction, CreateOrderAction, ...].
        Классы без @check_roles будут отфильтрованы в inspect() → None.

        Возвращает:
            list[type] — все наследники RoleGateHost.
        """
        return _collect_subclasses(cls._target_mixin)

    # ═══════════════════════════════════════════════════════════════════
    # Контракт BaseGateHostInspector
    # ═══════════════════════════════════════════════════════════════════

    @classmethod
    def inspect(cls, target_cls: type) -> FacetPayload | None:
        """
        Проверяет наличие @check_roles и собирает данные.

        Читает атрибут _role_info, записанный декоратором @check_roles.
        Если атрибут отсутствует — класс не имеет ролевых ограничений,
        возвращает None. Если присутствует — собирает payload и
        валидирует.

        Аргументы:
            target_cls: класс для инспекции (наследник RoleGateHost).

        Возвращает:
            FacetPayload — класс имеет @check_roles, данные собраны.
            None — класс не имеет @check_roles (нет _role_info).
        """
        role_info = getattr(target_cls, "_role_info", None)
        if role_info is None:
            return None
        payload = cls._build_payload(target_cls)
        cls._validate(target_cls, payload)
        return payload

    @classmethod
    def _build_payload(cls, target_cls: type) -> FacetPayload:
        """
        Собирает FacetPayload из _role_info класса.

        Формирует узел типа "role" с метаданными spec. Рёбра
        отсутствуют — ролевой узел не ссылается на другие узлы.

        Аргументы:
            target_cls: класс с атрибутом _role_info.

        Возвращает:
            FacetPayload с node_type="role" и spec в node_meta.
        """
        return FacetPayload(
            node_type="role",
            node_name=cls._make_node_name(target_cls),
            node_class=target_cls,
            node_meta=cls._make_meta(
                spec=target_cls._role_info["spec"],
            ),
            edges=(),
        )

    @classmethod
    def _validate(cls, target_cls: type, payload: FacetPayload) -> None:
        """
        Проверка инвариантов ролевой спецификации.

        Все инварианты проверены декоратором @check_roles при определении
        класса (spec — строка или список строк, пустой список запрещён,
        элементы списка — строки). Повторная проверка не требуется.

        Аргументы:
            target_cls: инспектируемый класс.
            payload: FacetPayload, собранный _build_payload().
        """
        pass
Copy
Файл 4 из 15.

Следующий: нужно решить — _collect_subclasses дублируется в каждом инспекторе. Вынести в metadata/utils.py? Или оставить в каждом файле инспектора? Если выносить — это будет файл 5, потом продолжу с инспекторами. Если оставить — следующий файл AspectGateHostInspector.

@classmethod
    def _validate(cls, target_cls: type, payload: FacetPayload) -> None:
        """
        Проверка инвариантов ролевой спецификации.

        Все инварианты проверены декоратором @check_roles при определении
        класса (spec — строка или список строк, пустой список запрещён,
        элементы списка — строки). Повторная проверка не требуется.

        Аргументы:
            target_cls: инспектируемый класс.
            payload: FacetPayload, собранный _build_payload().
        """
        pass

я с таким не согласен!

нужно или этот метод удалять или в него выносить всю логику но при этом проверка долдна щапускаттся при старте системы. как быть?
Правильный вопрос. Два варианта:

Вариант 1: Вся валидация в _validate() инспектора.

Убираем проверки из декоратора @check_roles. Декоратор только записывает _role_info. Все проверки (spec — строка или список, пустой список запрещён, элементы — строки) переезжают в _validate() инспектора. Проверка запускается при build() координатора при старте системы.

Плюс: единая точка валидации. Минус: ошибка обнаруживается при build(), а не при определении класса (import-time). Разработчик написал @check_roles(42) — узнаёт об ошибке не при импорте модуля, а при старте приложения.

Вариант 2: Декоратор проверяет базовые типы, инспектор проверяет структурные инварианты.

Декоратор оставляет проверки, которые возможны при определении класса: spec — строка или список строк, элементы — строки, пустой список. Это дешёвые проверки, обнаруживающие опечатки немедленно.

Инспектор в _validate() проверяет инварианты, которые требуют контекста графа или всей системы. Для ролей таких инвариантов сейчас нет — поэтому pass.

Плюс: ранние ошибки при import-time. Минус: дублирование — валидация в двух местах.

Мой выбор: Вариант 1. Причины:

Единая точка валидации — один из принципов рефакторинга. Если валидация размазана между декоратором и инспектором, непонятно кто за что отвечает.
build() вызывается при старте приложения — это секунды после import. Разница между "ошибка при импорте" и "ошибка при старте" — минимальна на практике.
Декоратор становится тонким: записал _role_info → вернул класс. Вся интеллектуальная работа — в инспекторе.
При этом декоратор оставляет одну проверку: spec — не None и передан (чтобы @check_roles() без аргументов не молча записал мусор). Всё остальное — инспектор.

Согласен с вариантом 1?

это не деградация?
build() вызывается при старте приложения — это секунды после import. Разница между "ошибка при импорте" и "ошибка при старте" — минимальна на практике.
Ты прав, это деградация. Ошибка при import-time лучше ошибки при старте. Чем раньше — тем лучше.

Тогда правильный подход:

Декоратор проверяет всё, что может проверить при определении класса (типы аргументов, пустоту, issubclass). Это не меняется.

_validate() в инспекторе — не дублирует проверки декоратора, а проверяет то, что декоратор проверить не может. Например, инварианты, требующие контекста всей системы или графа.

Для RoleGateHostInspector сейчас таких инвариантов нет — декоратор @check_roles проверяет всё при определении. Значит _validate() остаётся с pass, но это осознанный и документированный pass, а не лень.

Если в будущем появится инвариант, требующий контекста (например: "если strict-режим — каждый Action с ролями обязан иметь @meta с domain"), он добавляется в _validate().

Итого правило для всех инспекторов:

Декоратор проверяет аргументы и типы при import-time.
_validate() инспектора проверяет структурные инварианты, требующие контекста графа или всей системы.
Если структурных инвариантов нет — pass с комментарием почему.
Дублирования нет. Каждая проверка живёт в одном месте.
Согласен? Если да — файл 4 (RoleGateHostInspector) корректен как есть, шлю следующий.

можем сделать так?
миксины гейтхостов являются дженерик и через него привязываются к инспектору
в координатор мы регнистрируем гейтхотсы
и получается гейтхост при импорте находит дженерик и вызывает в нем validast для проверки
и когда коорднатор строит граф так же находит инспектор и берет в нем build
Красивая идея. Давай разберу механику.

Copy# Маркерный миксин — generic, привязан к инспектору
class RoleGateHost(Generic[I]):
    """I = RoleGateHostInspector"""
    _role_info: ClassVar[dict[str, Any]]

# Конкретный миксин в BaseAction
class BaseAction(RoleGateHost[RoleGateHostInspector], ...):
    ...
Проблема: циклический импорт.

RoleGateHost живёт в auth/role_gate_host.py. RoleGateHostInspector живёт в auth/role_gate_host_inspector.py. Инспектор импортирует RoleGateHost (для _target_mixin). Если RoleGateHost generic по инспектору — он тоже импортирует инспектор. Цикл.

Можно решить через TYPE_CHECKING:

Copyfrom __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from action_machine.auth.role_gate_host_inspector import RoleGateHostInspector
Но тогда generic параметр I доступен только для mypy, а в рантайме — нет. RoleGateHost не сможет в рантайме извлечь RoleGateHostInspector из generic и вызвать validate().

Вторая проблема: момент вызова validate.

Ты пишешь: "гейтхост при импорте находит generic и вызывает validate". Но при импорте класс ещё определяется — _role_info записывается декоратором @check_roles, который применяется ПОСЛЕ определения тела класса. В __init_subclass__ атрибута _role_info ещё нет.

Последовательность при import:

Python читает тело класса.
Вызывается __init_subclass__ — _role_info ещё нет.
Применяются декораторы снизу вверх: @check_roles записывает _role_info.
Значит validate из инспектора нельзя вызвать ни в generic, ни в __init_subclass__. Только после применения декоратора.

Третья проблема: кто вызывает validate после декоратора?

Единственный момент, когда _role_info гарантированно существует — сразу после @check_roles. Сам декоратор и вызывает проверки. Это текущее поведение, и оно правильное.

Вывод: привязка через generic технически не работает для валидации при import-time. Generic решает задачу "знать свой инспектор", но не решает задачу "вызвать validate при импорте".

Предлагаю оставить как есть:

Декоратор проверяет при import-time.
Инспектор проверяет структурные инварианты при build().
Связь инспектор → маркер через _target_mixin.
Координатор регистрирует инспекторы.
Шлю следующий файл?

Давай так 
Вариант 2: Декоратор проверяет базовые типы, инспектор проверяет структурные инварианты
_validate() получается не нужен, тк глобальные проверки уникальности колбчей и ациклиночти находятся в самом координаторе

не хочу размазывать логику провреки в разные места для одного гейтхоста

так?
Да, именно так. Чистое разделение:

Декоратор (@check_roles) — проверяет аргументы при import-time. Типы, пустоту, issubclass. Не меняется.

_build_payload() — читает данные, формирует FacetPayload. Без валидации.

Координатор (build()) — глобальные структурные проверки: уникальность ключей, ссылочная целостность, ацикличность. Одно место для всех инспекторов.

_validate() — удаляется из контракта BaseGateHostInspector.

Три метода становятся двумя:

inspect() — наличие данных + _build_payload()
_build_payload() — формирование payload
Координатор после сбора всех payload проверяет их целиком.

Обновляю BaseGateHostInspector: убираю _validate() из абстрактных методов. Убираю вызов _validate() из документации inspect(). Шлю обновлённый файл?

перепиши нагш план с учетом всех измененийц сначала и пришли его таким же полным или еще более полным
Финальный план рефакторинга GateCoordinator
Гейтхосты-инспекторы с транзакционным построением графа
Контекст
Неинвазивная трансформация. Существующие маркерные миксины, декораторы и классы остаются на месте без изменений. Добавляются новые файлы — инспекторы гейтхостов. Пользовательский код не меняется. Старый MetadataBuilder работает параллельно до полного перехода, после чего удаляется.

1. Архитектура
1.1. Центральная идея
Система строит граф зависимостей между объектами предметной области. Узлы графа — это классы (Action, Entity, Role, Aspect...). Рёбра — отношения между ними (depends, has_aspect, belongs_to...).

Copy        CreateOrderAction
         /            \
    depends          has_aspect
       /                  \
 PaymentService       validate_aspect
       \
    belongs_to
          \
        OrdersDomain
Граф строится один раз при старте приложения через coordinator.build(). После построения — только читается.

1.2. Два слоя: маркеры и инспекторы
Каждый гейтхост существует в двух ипостасях:

Маркерный миксин (RoleGateHost, AspectGateHost, DependencyGateHost[T]...)

Живёт в MRO класса BaseAction (или BaseEntity, BaseResourceManager).
Разрешает применение соответствующего декоратора через issubclass-проверку.
Не содержит логики инспекции.
Не наследует BaseGateHostInspector.
Не меняется при рефакторинге.
Инспектор (RoleGateHostInspector, AspectGateHostInspector, DependencyGateHostInspector...)

Наследует BaseGateHostInspector.
Реализует inspect() и _build_payload().
Обходит наследников маркера через _target_mixin.
Регистрируется в координаторе.
Новый файл, лежит рядом с маркером.
Связь между ними — поле _target_mixin инспектора. Инспектор знает, наследников какого маркера обходить. Маркер не знает про инспектор.

Copy┌─────────────────────────┐         ┌──────────────────────────────┐
│  RoleGateHost           │         │  RoleGateHostInspector       │
│  (маркерный миксин)     │◄────────│  (наследник BaseGateHost-    │
│                         │ _target │   Inspector)                 │
│  Живёт в BaseAction MRO │ _mixin  │                              │
│  Разрешает @check_roles │         │  inspect() → FacetPayload    │
│  НЕ МЕНЯЕТСЯ            │         │  _build_payload() → payload  │
└─────────────────────────┘         └──────────────────────────────┘
1.3. Кто знает про классы?
Python автоматически регистрирует подклассы при определении через __subclasses__(). Никакой ручной регистрации классов не существует.

Инспектор обходит наследников своего маркера:

Copyclass RoleGateHostInspector(BaseGateHostInspector):
    _target_mixin = RoleGateHost

    @classmethod
    def _subclasses_recursive(cls) -> list[type]:
        return _collect_subclasses(cls._target_mixin)
CopyRoleGateHost.__subclasses__() (рекурсивно)
    → [BaseAction, CreateOrderAction, UpdateOrderAction, ...]

AspectGateHost.__subclasses__() (рекурсивно)
    → [BaseAction, CreateOrderAction, ...]

EntityGateHost.__subclasses__() (рекурсивно)
    → [BaseEntity, OrderEntity, PaymentEntity, ...]
Один класс может быть обнаружен несколькими инспекторами. Например, CreateOrderAction найдётся и RoleGateHostInspector, и AspectGateHostInspector, и DependencyGateHostInspector. Каждый создаёт свой payload с уникальным node_type.

1.4. Разделение валидации
Валидация выполняется на двух уровнях, каждый — в одном месте:

Декораторы (@check_roles, @regular_aspect, @depends...)

Проверяют аргументы при import-time.
Типы, пустоту, issubclass, дубликаты.
Обнаруживают ошибки немедленно при определении класса.
Не меняются при рефакторинге.
Координатор (GateCoordinator.build())

Глобальные структурные проверки после сбора всех payload.
Уникальность ключей узлов.
Ссылочная целостность рёбер (цель существует).
Ацикличность структурных рёбер.
Обязательные поля payload непустые.
Одно место для всех проверок графа.
_validate() отсутствует в контракте инспектора. Логика проверки не размазывается между декоратором и инспектором. Декоратор отвечает за свои аргументы, координатор — за целостность графа.

1.5. Что делает инспектор
Каждый инспектор реализует два метода:

Copy┌─────────────────────────────────────────────────────────┐
│               BaseGateHostInspector                      │
│                                                          │
│  inspect(cls) → FacetPayload | None      [abstract]     │
│    Подходит ли класс? Если да — вернуть payload.        │
│    Если нет — None.                                     │
│                                                          │
│  _build_payload(cls) → FacetPayload      [abstract]     │
│    Собрать узел и рёбра. Использует хелперы базового    │
│    класса: _make_node_name, _make_edge, _make_meta.     │
│                                                          │
│  _subclasses_recursive() → list[type]    [override]     │
│    Обходит наследников _target_mixin, не своих.         │
└─────────────────────────────────────────────────────────┘
Правило разделения ответственности:

Copyinspect()        → проверяет ТОЛЬКО наличие данных
_build_payload() → читает данные, формирует payload
координатор      → проверяет КОРРЕКТНОСТЬ графа
1.6. Хелперы базового класса
Copy┌─────────────────────────────────────────────────────────┐
│         BaseGateHostInspector — хелперы                  │
│                                                          │
│  _make_node_name(cls, suffix) → str                     │
│    "module.ClassName" — без префикса типа.              │
│    Ключ "тип:имя" собирает координатор.                 │
│                                                          │
│  _make_edge(target_type, target_cls,                    │
│             edge_type, is_structural,                   │
│             edge_meta) → EdgeInfo                       │
│    Собирает EdgeInfo с автоформированием имени цели.    │
│                                                          │
│  _make_edge_by_name(target_type, target_name,           │
│                     edge_type, is_structural,           │
│                     edge_meta) → EdgeInfo               │
│    Для целей, не являющихся классами (строковые имена). │
│                                                          │
│  _make_meta(**kwargs) → tuple[tuple[str, Any], ...]     │
│    dict-синтаксис → иммутабельный tuple пар.            │
└─────────────────────────────────────────────────────────┘
1.7. FacetPayload — контракт между инспектором и координатором
Copy┌─────────────────────────────────────────────────────────┐
│                     FacetPayload                        │
│                                                          │
│  node_type  : str                ← "action", "role"...  │
│  node_name  : str                ← "module.ClassName"   │
│  node_class : type               ← ссылка на класс      │
│  node_meta  : tuple[tuple]       ← специфика инспектора │
│  edges      : tuple[EdgeInfo]    ← рёбра от этого узла  │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│                      EdgeInfo                           │
│                                                          │
│  target_node_type : str          ← тип целевого узла    │
│  target_name      : str          ← имя целевого узла    │
│  edge_type        : str          ← "depends", "has_*"   │
│  is_structural    : bool         ← циклы запрещены?      │
│  edge_meta        : tuple[tuple] ← доп. метаданные       │
└─────────────────────────────────────────────────────────┘
FacetPayload — транспортный объект. Живёт только внутри build(). После коммита в граф выбрасывается. В граф данные попадают как dict — конвертация tuple → dict происходит один раз при коммите.

tuple[tuple] вместо dict — frozen dataclass должен быть хешируемым.

1.8. Два типа рёбер
Copy┌─────────────────┬────────────────────────────────────────┐
│   is_structural │  Примеры        Проверка               │
├─────────────────┼────────────────────────────────────────┤
│   True          │  depends        Ацикличность обязательна│
│                 │  connection     Цикл → InvalidGraphError│
├─────────────────┼────────────────────────────────────────┤
│   False         │  has_aspect     Циклы допустимы         │
│                 │  belongs_to     Просто метаданные        │
│                 │  requires_ctx                           │
│                 │  has_checker                            │
│                 │  subscribes                             │
│                 │  has_error_handler                      │
│                 │  has_compensator                        │
│                 │  has_sensitive                          │
│                 │  has_role                               │
│                 │  has_field                              │
│                 │  has_relation                           │
│                 │  has_lifecycle                          │
└─────────────────┴────────────────────────────────────────┘
1.9. GateCoordinator — сборщик графа
Copycoordinator = GateCoordinator(strict=True)\
    .register(RoleGateHostInspector)\
    .register(AspectGateHostInspector)\
    .register(DependencyGateHostInspector)\
    .register(EntityGateHostInspector)\
    .build()
build() — ровно один раз. Повторный вызов — RuntimeError.
Регистрация инспектора после build() — RuntimeError.
Дубликат инспектора при регистрации — ValueError.
Координатор регистрирует инспекторы, не маркеры.
1.10. Транзакционный build() — три фазы
Граф либо строится полностью и корректно, либо не строится вообще.

Copy╔══════════════════════════════════════════════════════════╗
║                    build()                               ║
╠══════════════════════════════════════════════════════════╣
║                                                          ║
║  ФАЗА 1 — СБОР (только чтение, граф не трогаем)        ║
║                                                          ║
║  для каждого inspector:                                  ║
║    для каждого cls в _subclasses_recursive():            ║
║      payload = inspector.inspect(cls)                    ║
║      если payload → добавить в all_payloads             ║
║                                                          ║
╠══════════════════════════════════════════════════════════╣
║                                                          ║
║  ФАЗА 2 — ПРОВЕРКИ (граф не трогаем)                    ║
║                                                          ║
║  2a. Обязательные поля payload непустые                 ║
║      node_type, node_name — непустые строки             ║
║      node_class — тип (type)                            ║
║      → PayloadValidationError                           ║
║                                                          ║
║  2b. Уникальность ключей "node_type:node_name"         ║
║      конфликт → DuplicateNodeError с именами            ║
║      обоих инспекторов в сообщении                      ║
║                                                          ║
║  2c. Ссылочная целостность рёбер                        ║
║      цель ∈ all_payloads → OK                           ║
║      иначе → InvalidGraphError                          ║
║                                                          ║
║  2d. Ацикличность структурных рёбер                     ║
║      симуляция на временном пустом графе                 ║
║      только рёбра с is_structural=True                  ║
║      цикл → InvalidGraphError                           ║
║                                                          ║
╠══════════════════════════════════════════════════════════╣
║                                                          ║
║  ФАЗА 3 — КОММИТ (только если фаза 2 прошла)           ║
║                                                          ║
║  добавить все узлы в граф (dict из tuple)               ║
║  добавить все рёбра в граф                              ║
║  self._built = True                                      ║
║                                                          ║
╚══════════════════════════════════════════════════════════╝
1.11. Рантайм-доступ к данным графа
ClassMetadata как отдельный God-объект удаляется. Мета-датаклассы (AspectMeta, CheckerMeta, OnErrorMeta, CompensatorMeta, RoleMeta) остаются как типизированные рантайм-контейнеры. Они хранятся в node_meta узлов графа.

Координатор предоставляет типизированные методы чтения:

Copycoordinator.get_role_spec(cls) → str | list[str]
coordinator.get_regular_aspects(cls) → tuple[AspectMeta, ...]
coordinator.get_summary_aspect(cls) → AspectMeta | None
coordinator.get_checkers_for_aspect(cls, name) → tuple[CheckerMeta, ...]
coordinator.get_error_handler_for(cls, error) → OnErrorMeta | None
coordinator.get_compensator_for_aspect(cls, name) → CompensatorMeta | None
coordinator.has_compensators(cls) → bool
coordinator.get_connection_keys(cls) → tuple[str, ...]
coordinator.get_factory(cls) → DependencyFactory
coordinator.get_meta_description(cls) → str
Машина (ActionProductMachine) вместо:

Copymetadata = self._coordinator.get(action.__class__)
role_spec = metadata.role.spec
пишет:

Copyrole_spec = self._coordinator.get_role_spec(action.__class__)
1.12. Расположение файлов
Пакет metadata/ содержит только абстракции и координатор. Инспекторы лежат рядом со своими маркерами:

Copymetadata/
    __init__.py
    payload.py                          ← FacetPayload, EdgeInfo
    exceptions.py                       ← DuplicateNodeError, InvalidGraphError,
                                          PayloadValidationError
    base_gate_host_inspector.py         ← BaseGateHostInspector
    gate_coordinator.py                 ← GateCoordinator

auth/
    role_gate_host.py                   ← маркер (НЕ МЕНЯЕТСЯ)
    role_gate_host_inspector.py         ← RoleGateHostInspector
    check_roles.py                      ← декоратор (НЕ МЕНЯЕТСЯ)

aspects/
    aspect_gate_host.py                 ← маркер (НЕ МЕНЯЕТСЯ)
    aspect_gate_host_inspector.py       ← AspectGateHostInspector
    regular_aspect.py                   ← декоратор (НЕ МЕНЯЕТСЯ)
    summary_aspect.py                   ← декоратор (НЕ МЕНЯЕТСЯ)

checkers/
    checker_gate_host.py                ← маркер (НЕ МЕНЯЕТСЯ)
    checker_gate_host_inspector.py      ← CheckerGateHostInspector

dependencies/
    dependency_gate_host.py             ← маркер Generic[T] (НЕ МЕНЯЕТСЯ)
    dependency_gate_host_inspector.py   ← DependencyGateHostInspector

resource_managers/
    connection_gate_host.py             ← маркер (НЕ МЕНЯЕТСЯ)
    connection_gate_host_inspector.py   ← ConnectionGateHostInspector

on_error/
    on_error_gate_host.py               ← маркер (НЕ МЕНЯЕТСЯ)
    on_error_gate_host_inspector.py     ← OnErrorGateHostInspector

context/
    context_requires_gate_host.py       ← маркер (НЕ МЕНЯЕТСЯ)
    context_requires_gate_host_inspector.py ← ContextRequiresGateHostInspector

compensate/
    compensate_decorator.py             ← декоратор (НЕ МЕНЯЕТСЯ)
    compensate_gate_host_inspector.py   ← CompensateGateHostInspector

core/
    meta_gate_hosts.py                  ← маркеры Action/Resource (НЕ МЕНЯЮТСЯ)
    meta_gate_host_inspector.py         ← MetaGateHostInspector

domain/
    entity_gate_host.py                 ← маркер (НЕ МЕНЯЕТСЯ)
    entity_gate_host_inspector.py       ← EntityGateHostInspector

logging/
    sensitive_decorator.py              ← декоратор (НЕ МЕНЯЕТСЯ)
    sensitive_gate_host_inspector.py    ← SensitiveGateHostInspector

plugins/
    plugin.py                           ← базовый класс (НЕ МЕНЯЕТСЯ)
    subscription_gate_host_inspector.py ← SubscriptionGateHostInspector
1.13. Утилита _collect_subclasses
Функция _collect_subclasses(mixin) рекурсивно собирает всех наследников маркерного миксина. Используется каждым инспектором в _subclasses_recursive(). Вынесена в metadata/utils.py:

Copy# metadata/utils.py
def collect_subclasses(mixin: type) -> list[type]:
    result: list[type] = []
    for sub in mixin.__subclasses__():
        result.append(sub)
        result.extend(collect_subclasses(sub))
    return result
2. Ключевые интерфейсы
metadata/payload.py
Copy@dataclass(frozen=True)
class EdgeInfo:
    target_node_type: str
    target_name: str
    edge_type: str
    is_structural: bool
    edge_meta: tuple[tuple[str, Any], ...] = ()

@dataclass(frozen=True)
class FacetPayload:
    node_type: str
    node_name: str
    node_class: type
    node_meta: tuple[tuple[str, Any], ...] = ()
    edges: tuple[EdgeInfo, ...] = ()
metadata/base_gate_host_inspector.py
Copyclass BaseGateHostInspector(ABC):

    @classmethod
    @abstractmethod
    def inspect(cls, target_cls: type) -> FacetPayload | None: ...

    @classmethod
    @abstractmethod
    def _build_payload(cls, target_cls: type) -> FacetPayload: ...

    # Хелперы:
    @classmethod
    def _make_node_name(cls, target_cls, suffix="") -> str: ...

    @classmethod
    def _make_edge(cls, target_node_type, target_cls,
                   edge_type, is_structural, edge_meta=()) -> EdgeInfo: ...

    @classmethod
    def _make_edge_by_name(cls, target_node_type, target_name,
                           edge_type, is_structural, edge_meta=()) -> EdgeInfo: ...

    @classmethod
    def _make_meta(cls, **kwargs) -> tuple[tuple[str, Any], ...]: ...

    @classmethod
    def _subclasses_recursive(cls) -> list[type]: ...
3. Требования к каждому инспектору
RoleGateHostInspector
CopyМаркер:     RoleGateHost
Декоратор:  @check_roles (проверяет аргументы при import-time)
Файл:       auth/role_gate_host_inspector.py

_build_payload:
  node_type : "role"
  node_meta : spec
  edges     : ()
MetaGateHostInspector
CopyМаркер:     ActionMetaGateHost, ResourceMetaGateHost
Декоратор:  @meta (проверяет аргументы при import-time)
Файл:       core/meta_gate_host_inspector.py

_build_payload:
  node_type : "meta"
  node_meta : description, domain
  edges     :
    belongs_to → domain (is_structural=False) если domain указан
AspectGateHostInspector
CopyМаркер:     AspectGateHost
Декораторы: @regular_aspect, @summary_aspect
Файл:       aspects/aspect_gate_host_inspector.py

_build_payload (один payload на каждый аспект класса):
  node_type : "aspect"
  node_meta : aspect_type, method_name, description, method_ref, context_keys
  edges     : ()

Дополнительно создаёт payload для каждого аспекта с context_keys:
  edges: requires_context → context_field (is_structural=False)
CheckerGateHostInspector
CopyМаркер:     CheckerGateHost
Декораторы: @result_string, @result_int, @result_float, @result_bool,
            @result_date, @result_instance
Файл:       checkers/checker_gate_host_inspector.py

_build_payload (один payload на каждый чекер):
  node_type : "checker"
  node_meta : checker_class, field_name, required, extra_params, method_name
  edges     : ()
DependencyGateHostInspector
CopyМаркер:     DependencyGateHost[T]
Декоратор:  @depends (проверяет аргументы и bound при import-time)
Файл:       dependencies/dependency_gate_host_inspector.py

_build_payload:
  node_type : "action"
  node_meta : ()
  edges     :
    depends → каждый класс из _depends_info (is_structural=True)
ConnectionGateHostInspector
CopyМаркер:     ConnectionGateHost
Декоратор:  @connection (проверяет аргументы при import-time)
Файл:       resource_managers/connection_gate_host_inspector.py

_build_payload:
  node_type : "action"
  node_meta : ()
  edges     :
    connection → каждый класс из _connection_info (is_structural=True)
OnErrorGateHostInspector
CopyМаркер:     OnErrorGateHost
Декоратор:  @on_error (проверяет аргументы при import-time)
Файл:       on_error/on_error_gate_host_inspector.py

_build_payload (один payload на каждый обработчик):
  node_type : "error_handler"
  node_meta : method_name, exception_types, description, method_ref, context_keys
  edges     :
    requires_context → context_field (is_structural=False) для каждого ключа
ContextRequiresGateHostInspector
CopyМаркер:     ContextRequiresGateHost
Декоратор:  @context_requires (проверяет аргументы при import-time)
Файл:       context/context_requires_gate_host_inspector.py

Не создаёт собственные payload. Контекстные зависимости аспектов и
обработчиков ошибок обрабатываются AspectGateHostInspector и
OnErrorGateHostInspector через context_keys в node_meta.

Этот инспектор может быть исключён из списка, если context_keys
полностью обрабатываются другими инспекторами.
CompensateGateHostInspector
CopyМаркер:     нет отдельного маркера (декоратор @compensate на методе)
Декоратор:  @compensate (проверяет аргументы при import-time)
Файл:       compensate/compensate_gate_host_inspector.py

_build_payload (один payload на каждый компенсатор):
  node_type : "compensator"
  node_meta : method_name, target_aspect_name, description, method_ref, context_keys
  edges     :
    requires_context → context_field (is_structural=False) для каждого ключа
EntityGateHostInspector
CopyМаркер:     EntityGateHost
Декоратор:  @entity (проверяет аргументы при import-time)
Файл:       domain/entity_gate_host_inspector.py

_build_payload:
  node_type : "entity"
  node_meta : description, domain, field_count, relation_count, lifecycle_count
  edges     :
    belongs_to → domain (is_structural=False) если domain указан
    has_field → entity_field для каждого простого поля (is_structural=False)
    has_relation → entity_relation для каждой связи (is_structural=False)
    has_lifecycle → entity_lifecycle для каждого Lifecycle (is_structural=False)

Дочерние узлы (entity_field, entity_relation, entity_lifecycle) создаются
как отдельные FacetPayload:
  entity_field:     node_type="entity_field", node_meta: field_type, description, required, constraints
  entity_relation:  node_type="entity_relation", node_meta: relation_type, cardinality, target_entity, ...
  entity_lifecycle: node_type="entity_lifecycle", node_meta: lifecycle_class, state_count, ...
SensitiveGateHostInspector
CopyМаркер:     нет отдельного маркера (декоратор @sensitive на property)
Декоратор:  @sensitive (проверяет аргументы при import-time)
Файл:       logging/sensitive_gate_host_inspector.py

_build_payload (один payload на каждое @sensitive свойство):
  node_type : "sensitive"
  node_meta : property_name, config (enabled, max_chars, char, max_percent)
  edges     : ()
SubscriptionGateHostInspector
CopyМаркер:     Plugin (базовый класс плагинов)
Декоратор:  @on (проверяет аргументы при import-time)
Файл:       plugins/subscription_gate_host_inspector.py

_build_payload:
  node_type : "plugin"
  node_meta : description, domain, subscription_count
  edges     :
    subscribes → subscription для каждой подписки (is_structural=False)

Дочерние узлы подписок:
  node_type="subscription", node_meta: event_class, action_name_pattern, method_name
4. Примеры реализации инспекторов
RoleGateHostInspector — без рёбер
Copyclass RoleGateHostInspector(BaseGateHostInspector):
    _target_mixin: type = RoleGateHost

    @classmethod
    def _subclasses_recursive(cls) -> list[type]:
        return collect_subclasses(cls._target_mixin)

    @classmethod
    def inspect(cls, target_cls: type) -> FacetPayload | None:
        role_info = getattr(target_cls, "_role_info", None)
        if role_info is None:
            return None
        return cls._build_payload(target_cls)

    @classmethod
    def _build_payload(cls, target_cls: type) -> FacetPayload:
        return FacetPayload(
            node_type="role",
            node_name=cls._make_node_name(target_cls),
            node_class=target_cls,
            node_meta=cls._make_meta(
                spec=target_cls._role_info["spec"]
            ),
        )
DependencyGateHostInspector — со структурными рёбрами
Copyclass DependencyGateHostInspector(BaseGateHostInspector):
    _target_mixin: type = DependencyGateHost

    @classmethod
    def _subclasses_recursive(cls) -> list[type]:
        return collect_subclasses(cls._target_mixin)

    @classmethod
    def inspect(cls, target_cls: type) -> FacetPayload | None:
        depends_info = getattr(target_cls, "_depends_info", None)
        if not depends_info:
            return None
        return cls._build_payload(target_cls)

    @classmethod
    def _build_payload(cls, target_cls: type) -> FacetPayload:
        edges = tuple(
            cls._make_edge(
                target_node_type="dependency",
                target_cls=dep_info.cls,
                edge_type="depends",
                is_structural=True,
            )
            for dep_info in target_cls._depends_info
        )
        return FacetPayload(
            node_type="action",
            node_name=cls._make_node_name(target_cls),
            node_class=target_cls,
            edges=edges,
        )
Copy
5. GateCoordinator
Copyclass GateCoordinator:

    def __init__(self, strict: bool = False) -> None:
        self._strict = strict
        self._inspectors: list[type[BaseGateHostInspector]] = []
        self._registered: set[type[BaseGateHostInspector]] = set()
        self._graph = rx.PyDiGraph()
        self._node_index: dict[str, int] = {}
        self._built = False

    def register(self, inspector_cls) -> GateCoordinator:
        """Fluent-регистрация инспектора."""
        if self._built:
            raise RuntimeError("Регистрация после build() запрещена")
        if inspector_cls in self._registered:
            raise ValueError(f"{inspector_cls.__name__} уже зарегистрирован")
        self._inspectors.append(inspector_cls)
        self._registered.add(inspector_cls)
        return self

    def build(self) -> GateCoordinator:
        """Транзакционное построение графа."""
        if self._built:
            raise RuntimeError("build() уже вызван")
        all_payloads = self._phase1_collect()
        self._phase2_validate(all_payloads)
        self._phase3_commit(all_payloads)
        self._built = True
        return self

    def _phase1_collect(self) -> list[FacetPayload]: ...
    def _phase2_validate(self, payloads) -> None: ...
    def _phase3_commit(self, payloads) -> None: ...

    # Рантайм-доступ к данным графа:
    def get_role_spec(self, cls) -> str | list[str]: ...
    def get_regular_aspects(self, cls) -> tuple[AspectMeta, ...]: ...
    def get_summary_aspect(self, cls) -> AspectMeta | None: ...
    def get_checkers_for_aspect(self, cls, name) -> tuple[CheckerMeta, ...]: ...
    def get_error_handler_for(self, cls, error) -> OnErrorMeta | None: ...
    def get_compensator_for_aspect(self, cls, name) -> CompensatorMeta | None: ...
    def has_compensators(self, cls) -> bool: ...
    def get_connection_keys(self, cls) -> tuple[str, ...]: ...
    def get_factory(self, cls) -> DependencyFactory: ...
    def get_meta_description(self, cls) -> str: ...
Copy
6. Фабрика
Copy# core/action_machine_core.py
class ActionMachineCore:

    @staticmethod
    def create_coordinator(strict: bool = False) -> GateCoordinator:
        return (
            GateCoordinator(strict=strict)
            .register(RoleGateHostInspector)
            .register(MetaGateHostInspector)
            .register(AspectGateHostInspector)
            .register(CheckerGateHostInspector)
            .register(DependencyGateHostInspector)
            .register(ConnectionGateHostInspector)
            .register(OnErrorGateHostInspector)
            .register(CompensateGateHostInspector)
            .register(SubscriptionGateHostInspector)
            .register(SensitiveGateHostInspector)
            .register(EntityGateHostInspector)
            .build()
        )
7. Что удаляется после миграции
Файл	Причина
metadata/builder.py	Заменён GateCoordinator.build() + инспекторы
metadata/collectors.py	Логика переехала в _build_payload() инспекторов
metadata/validators.py	Логика переехала в декораторы (import-time) и координатор (build-time)
core/class_metadata.py	Заменён типизированными методами координатора. Мета-датаклассы (AspectMeta, CheckerMeta, OnErrorMeta, CompensatorMeta) остаются
8. Что НЕ меняется
Компонент	Почему не меняется
Маркерные миксины (RoleGateHost, AspectGateHost...)	Остаются в MRO классов, разрешают декораторы
Декораторы (@check_roles, @regular_aspect, @depends...)	Записывают атрибуты + проверяют аргументы при import-time
BaseAction, BaseEntity, BaseResourceManager	MRO не меняется
Мета-датаклассы (AspectMeta, CheckerMeta, OnErrorMeta...)	Используются как типизированные рантайм-контейнеры
Пользовательский код (Action, Entity, Plugin)	Декораторы и наследование остаются
9. План миграции
CopyШаг 1  metadata/payload.py               ← FacetPayload, EdgeInfo
       metadata/exceptions.py             ← DuplicateNodeError, InvalidGraphError,
                                             PayloadValidationError
       metadata/utils.py                  ← collect_subclasses()
       metadata/base_gate_host_inspector.py ← BaseGateHostInspector
                                             (два абстрактных метода:
                                              inspect, _build_payload;
                                              хелперы; _subclasses_recursive)
       Тесты: unit на каждый хелпер, на collect_subclasses.

Шаг 2  auth/role_gate_host_inspector.py   ← RoleGateHostInspector
       Самый простой — нет рёбер.
       Тесты: inspect для класса с @check_roles,
              inspect для класса без @check_roles → None,
              inspect для BaseAction без @check_roles → None.

Шаг 3  metadata/gate_coordinator.py       ← GateCoordinator
       register() / build() и три фазы внутри.
       Тесты: build с одним инспектором,
              дубликат инспектора → ValueError,
              повторный build → RuntimeError,
              регистрация после build → RuntimeError,
              конфликт ключей → DuplicateNodeError,
              невалидный payload → PayloadValidationError,
              битая ссылка ребра → InvalidGraphError,
              цикл структурных рёбер → InvalidGraphError.

Шаг 4  Остальные инспекторы.
       Порядок: от простых (без рёбер) к сложным (с рёбрами):
         RoleGateHostInspector (готов)
         → MetaGateHostInspector
         → AspectGateHostInspector
         → CheckerGateHostInspector
         → SensitiveGateHostInspector
         → OnErrorGateHostInspector
         → CompensateGateHostInspector
         → SubscriptionGateHostInspector
         → EntityGateHostInspector
         → DependencyGateHostInspector
         → ConnectionGateHostInspector
       Для каждого инспектора:
         — _build_payload использует хелперы базового класса
         — тесты inspect для разных случаев
       После каждого — интеграционный тест с координатором.

Шаг 5  Рантайм-методы координатора.
       get_role_spec, get_regular_aspects, get_summary_aspect,
       get_checkers_for_aspect, get_error_handler_for,
       get_compensator_for_aspect, has_compensators,
       get_connection_keys, get_factory, get_meta_description.
       Тесты: каждый метод с реальными Action.

Шаг 6  Обновить ActionProductMachine.
       Использовать рантайм-методы координатора вместо ClassMetadata.
       Использовать ActionMachineCore.create_coordinator().

Шаг 7  Обновить адаптеры (FastApiAdapter, McpAdapter).
       Использовать coordinator.get_meta_description(cls) вместо
       metadata.meta.description.

Шаг 8  Удалить builder.py, collectors.py, validators.py, class_metadata.py.
       Финальный прогон всех тестов.
10. Тестирование
Тестовая среда идентична продакшену. Тестовые классы с гейтхостами обязаны быть полными и валидными — они попадают в граф наравне с продакшен-классами.

Тестирование невалидных данных — изолированно через inspect() отдельного инспектора, без координатора:

Copydef test_role_inspector_returns_none_without_check_roles():
    class PlainAction(BaseAction[BaseParams, BaseResult]):
        pass
    assert RoleGateHostInspector.inspect(PlainAction) is None

def test_role_inspector_returns_payload_with_check_roles():
    @check_roles("admin")
    class AdminAction(BaseAction[BaseParams, BaseResult]):
        pass
    payload = RoleGateHostInspector.inspect(AdminAction)
    assert payload is not None
    assert payload.node_type == "role"
    assert dict(payload.node_meta)["spec"] == "admin"
Тестирование координатора — интеграционно:

Copydef test_coordinator_builds_graph():
    coordinator = GateCoordinator()\
        .register(RoleGateHostInspector)\
        .build()
    assert coordinator.graph_node_count > 0

def test_coordinator_detects_duplicate_keys():
    # Два инспектора создают узлы с одинаковым ключом
    with pytest.raises(DuplicateNodeError):
        GateCoordinator()\
            .register(ConflictingInspectorA)\
            .register(ConflictingInspectorB)\
            .build()
я вот так обновил класс
# src/action_machine/metadata/base_gate_host_inspector.py
"""
BaseGateHostInspector — абстрактный базовый класс для всех гейтхостов системы.

═══════════════════════════════════════════════════════════════════════════════
НАЗНАЧЕНИЕ
═══════════════════════════════════════════════════════════════════════════════

BaseGateHostInspector определяет контракт, которому обязан следовать каждый
гейтхост в системе ActionMachine. Гейтхост — это маркерный миксин,
который одновременно:

1. Разрешает применение соответствующего декоратора к классу
   (issubclass-проверка при декорировании).
2. Знает всех своих наследников через _subclasses_recursive().
3. Умеет инспектировать каждого наследника и собирать данные для графа.

Координатор (GateCoordinator) при build() обходит зарегистрированные
гейтхосты, вызывает inspect() для каждого подкласса, получает
FacetPayload и строит граф.

═══════════════════════════════════════════════════════════════════════════════
ТРИ ОБЯЗАТЕЛЬНЫХ МЕТОДА
═══════════════════════════════════════════════════════════════════════════════

Каждый гейтхост реализует три абстрактных classmethod:

    inspect(target_cls) → FacetPayload | None
        Точка входа. Определяет, подходит ли класс этому гейтхосту.
        Три возможных результата:
        - FacetPayload — класс подходит, данные собраны и провалидированы.
        - None — класс не является субъектом этого гейтхоста.
        - TypeError — класс обязан подойти (наследует гейтхост),
          но данные отсутствуют или повреждены.

    _build_payload(target_cls) → FacetPayload
        Собирает узел и рёбра. Читает атрибуты класса (_role_info,
        _depends_info, _meta_info и т.д.), формирует FacetPayload
        с использованием хелперов базового класса. Не валидирует
        данные — это задача _validate.

═══════════════════════════════════════════════════════════════════════════════
РАЗДЕЛЕНИЕ ОТВЕТСТВЕННОСТИ
═══════════════════════════════════════════════════════════════════════════════

    inspect()        → проверяет ТОЛЬКО наличие данных (есть _role_info?)
    _build_payload() → читает данные, формирует payload, НЕ валидирует

Это разделение гарантирует, что:
- inspect() работает быстро — только hasattr/getattr.
- _build_payload() не дублирует проверки из _validate().

═══════════════════════════════════════════════════════════════════════════════
ХЕЛПЕРЫ
═══════════════════════════════════════════════════════════════════════════════

Базовый класс предоставляет четыре хелпера, устраняющие дублирование
в каждом гейтхосте:

    _make_node_name(target_cls, suffix="") → str
        Формирует имя узла "module.ClassName" или "module.ClassName.suffix".
        Префикс типа ("action:", "role:") НЕ добавляется — это делает
        координатор при формировании ключа.

    _make_edge(target_node_type, target_cls, edge_type,
               is_structural, edge_meta=()) → EdgeInfo
        Собирает EdgeInfo без ручного заполнения каждого поля.
        Имя цели формируется через _make_node_name(target_cls).

    _make_edge_by_name(target_node_type, target_name, edge_type,
                       is_structural, edge_meta=()) → EdgeInfo
        Аналог _make_edge для случаев, когда цель — не класс,
        а произвольное строковое имя (например, "context_field:user.user_id").

    _make_meta(**kwargs) → tuple[tuple[str, Any], ...]
        Конвертирует dict-синтаксис в иммутабельный tuple of tuples,
        пригодный для frozen dataclass.

═══════════════════════════════════════════════════════════════════════════════
ОБХОД НАСЛЕДНИКОВ
═══════════════════════════════════════════════════════════════════════════════

Classmethod _subclasses_recursive() рекурсивно собирает всех наследников
гейтхоста через Python-механизм __subclasses__(). Координатор вызывает
этот метод при build() для каждого зарегистрированного гейтхоста.

Python автоматически регистрирует подклассы при определении. Никакой
ручной регистрации классов не существует. Если класс наследует
RoleGateHost — он будет найден через RoleGateHost._subclasses_recursive().

═══════════════════════════════════════════════════════════════════════════════
ПРИМЕР РЕАЛИЗАЦИИ ГЕЙТХОСТА
═══════════════════════════════════════════════════════════════════════════════

    class RoleGateHost(BaseGateHostInspector):

        @classmethod
        def inspect(cls, target_cls: type) -> FacetPayload | None:
            role_info = getattr(target_cls, "_role_info", None)
            if role_info is None:
                return None
            payload = cls._build_payload(target_cls)
            cls._validate(target_cls, payload)
            return payload

        @classmethod
        def _build_payload(cls, target_cls: type) -> FacetPayload:
            return FacetPayload(
                node_type="role",
                node_name=cls._make_node_name(target_cls),
                node_class=target_cls,
                node_meta=cls._make_meta(spec=target_cls._role_info["spec"]),
            )

═══════════════════════════════════════════════════════════════════════════════
ПРИМЕР РЕАЛИЗАЦИИ С РЁБРАМИ
═══════════════════════════════════════════════════════════════════════════════

    class DependencyGateHost(BaseGateHostInspector):

        @classmethod
        def _build_payload(cls, target_cls: type) -> FacetPayload:
            edges = tuple(
                cls._make_edge(
                    target_node_type="dependency",
                    target_cls=dep_info.cls,
                    edge_type="depends",
                    is_structural=True,
                )
                for dep_info in target_cls._depends_info
            )
            return FacetPayload(
                node_type="action",
                node_name=cls._make_node_name(target_cls),
                node_class=target_cls,
                edges=edges,
            )
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import Any

from action_machine.metadata.payload import EdgeInfo, FacetPayload


class BaseGateHostInspector(ABC):
    """
    Абстрактный базовый класс для всех гейтхостов системы ActionMachine.

    Определяет контракт из трёх абстрактных classmethod (inspect,
    _build_payload, _validate) и предоставляет четыре хелпера для
    формирования FacetPayload и EdgeInfo без дублирования кода
    в каждом гейтхосте.

    Все методы — classmethod. Гейтхост не хранит состояния и не
    требует создания экземпляра. Группировка в класс обеспечивает
    пространство имён, наследование хелперов и проверку контракта
    через ABC.

    Координатор (GateCoordinator) при build() вызывает:
    1. cls._subclasses_recursive() — получить всех наследников.
    2. cls.inspect(target_cls) — инспектировать каждого наследника.
    """

    # ═══════════════════════════════════════════════════════════════════
    # Обязательный контракт (три абстрактных метода)
    # ═══════════════════════════════════════════════════════════════════

    @classmethod
    @abstractmethod
    def inspect(cls, target_cls: type) -> FacetPayload | None:
        """
        Определяет, подходит ли класс этому гейтхосту, и собирает данные.

        Точка входа, вызываемая координатором для каждого подкласса
        гейтхоста. Реализация типичного inspect():

            1. Проверить наличие данных (hasattr/getattr).
            2. Если данных нет → return None.
            3. Если данные обязаны быть, но повреждены → raise TypeError.
            4. Вызвать _build_payload() → FacetPayload.
            5. Вызвать _validate() → проверка инвариантов.
            6. Вернуть payload.

        Аргументы:
            target_cls: класс для инспекции. Гарантированно является
                        подклассом данного гейтхоста (найден через
                        _subclasses_recursive).

        Возвращает:
            FacetPayload — класс подходит, данные собраны.
            None — класс не является субъектом этого гейтхоста
                   (нет соответствующих атрибутов/декораторов).

        Исключения:
            TypeError: класс обязан подойти (наследует гейтхост,
                       имеет частичные данные), но данные повреждены
                       или неполны.
        """
        ...

    @classmethod
    @abstractmethod
    def _build_payload(cls, target_cls: type) -> FacetPayload:
        """
        Собирает FacetPayload из атрибутов класса.

        Читает данные (cls._role_info, cls._depends_info и т.д.),
        формирует узел и рёбра с использованием хелперов базового
        класса: _make_node_name, _make_edge, _make_meta.

        Не валидирует данные — это задача _validate(). Разделение
        гарантирует, что логика сбора и логика проверки не смешиваются.

        Аргументы:
            target_cls: класс, прошедший проверку наличия данных
                        в inspect().

        Возвращает:
            FacetPayload — полное описание узла с рёбрами.
        """
        ...

    # ═══════════════════════════════════════════════════════════════════
    # Хелперы для _build_payload (не абстрактные, готовые к использованию)
    # ═══════════════════════════════════════════════════════════════════

    @classmethod
    def _make_node_name(cls, target_cls: type, suffix: str = "") -> str:
        """
        Формирует имя узла из модуля и имени класса.

        Формат: "module.ClassName" или "module.ClassName.suffix".
        Если модуль "__main__" или отсутствует — только "ClassName".

        Префикс типа ("action:", "role:") НЕ добавляется. Полный ключ
        "node_type:node_name" собирает координатор при коммите.

        Аргументы:
            target_cls: класс, для которого формируется имя.
            suffix: опциональный суффикс, добавляемый через точку.
                    Используется для дочерних узлов: аспектов,
                    чекеров, полей сущностей.
                    Пустая строка — суффикс не добавляется.

        Возвращает:
            str — имя узла.

        Примеры:
            _make_node_name(CreateOrderAction)
            → "myapp.orders.CreateOrderAction"

            _make_node_name(CreateOrderAction, "validate_aspect")
            → "myapp.orders.CreateOrderAction.validate_aspect"

            _make_node_name(OrderEntity, "amount")
            → "myapp.domain.OrderEntity.amount"
        """
        module = getattr(target_cls, "__module__", None)
        if module and module != "__main__":
            name = f"{module}.{target_cls.__qualname__}"
        else:
            name = target_cls.__qualname__
        if suffix:
            return f"{name}.{suffix}"
        return name

    @classmethod
    def _make_edge(
        cls,
        target_node_type: str,
        target_cls: type,
        edge_type: str,
        is_structural: bool,
        edge_meta: tuple[tuple[str, Any], ...] = (),
    ) -> EdgeInfo:
        """
        Собирает EdgeInfo с автоматическим формированием имени цели.

        Имя целевого узла формируется через _make_node_name(target_cls).
        Используется когда цель ребра — класс Python.

        Аргументы:
            target_node_type: тип целевого узла ("action", "entity",
                              "domain", "dependency" и т.д.).
            target_cls: класс, являющийся целью ребра.
            edge_type: тип ребра ("depends", "connection", "has_aspect",
                       "belongs_to" и т.д.).
            is_structural: True — структурное ребро (циклы запрещены).
                           False — информационное ребро (циклы допустимы).
            edge_meta: дополнительные метаданные ребра.
                       Пустой tuple по умолчанию.

        Возвращает:
            EdgeInfo — описание ребра.

        Пример:
            cls._make_edge(
                target_node_type="dependency",
                target_cls=PaymentService,
                edge_type="depends",
                is_structural=True,
            )
        """
        return EdgeInfo(
            target_node_type=target_node_type,
            target_name=cls._make_node_name(target_cls),
            edge_type=edge_type,
            is_structural=is_structural,
            edge_meta=edge_meta,
        )

    @classmethod
    def _make_edge_by_name(
        cls,
        target_node_type: str,
        target_name: str,
        edge_type: str,
        is_structural: bool,
        edge_meta: tuple[tuple[str, Any], ...] = (),
    ) -> EdgeInfo:
        """
        Собирает EdgeInfo с произвольным строковым именем цели.

        Используется когда цель ребра — не класс Python, а строковый
        идентификатор. Например, узлы контекстных полей
        ("context_field:user.user_id") или доменов ("domain:orders").

        Аргументы:
            target_node_type: тип целевого узла.
            target_name: строковое имя целевого узла.
            edge_type: тип ребра.
            is_structural: структурное или информационное.
            edge_meta: дополнительные метаданные ребра.

        Возвращает:
            EdgeInfo — описание ребра.

        Пример:
            cls._make_edge_by_name(
                target_node_type="context_field",
                target_name="user.user_id",
                edge_type="requires_context",
                is_structural=False,
            )
        """
        return EdgeInfo(
            target_node_type=target_node_type,
            target_name=target_name,
            edge_type=edge_type,
            is_structural=is_structural,
            edge_meta=edge_meta,
        )

    @classmethod
    def _make_meta(cls, **kwargs: Any) -> tuple[tuple[str, Any], ...]:
        """
        Конвертирует именованные аргументы в иммутабельный tuple of tuples.

        Frozen dataclass требует хешируемые поля. dict не хешируем.
        Этот хелпер позволяет гейтхостам использовать удобный
        dict-синтаксис (kwargs) при создании метаданных, получая
        на выходе иммутабельную структуру.

        Координатор конвертирует tuple of tuples обратно в dict
        при коммите: dict(node_meta).

        Аргументы:
            **kwargs: произвольные пары ключ-значение метаданных.

        Возвращает:
            tuple[tuple[str, Any], ...] — иммутабельные метаданные.

        Пример:
            cls._make_meta(spec="admin", description="Администратор")
            → (("spec", "admin"), ("description", "Администратор"))

            cls._make_meta()
            → ()
        """
        return tuple(kwargs.items())

    # ═══════════════════════════════════════════════════════════════════
    # Обход наследников
    # ═══════════════════════════════════════════════════════════════════

    @classmethod
    def _subclasses_recursive(cls) -> list[type]:
        """
        Рекурсивно собирает всех наследников гейтхоста.

        Использует Python-механизм __subclasses__(), который
        автоматически регистрирует подклассы при определении.
        Никакой ручной регистрации не требуется.

        Координатор вызывает этот метод при build() для каждого
        зарегистрированного гейтхоста, чтобы получить список
        всех классов, которые нужно инспектировать.

        Возвращает:
            list[type] — все прямые и транзитивные подклассы
                         данного гейтхоста. Порядок: depth-first,
                         прямые подклассы перед их потомками.

        Пример:
            class RoleGateHost(BaseGateHostInspector): ...
            class BaseAction(RoleGateHost): ...
            class CreateOrderAction(BaseAction): ...
            class UpdateOrderAction(BaseAction): ...

            RoleGateHost._subclasses_recursive()
            → [BaseAction, CreateOrderAction, UpdateOrderAction]
        """
        result: list[type] = []
        for subclass in cls.__subclasses__():
            result.append(subclass)
            if hasattr(subclass, "_subclasses_recursive"):
                result.extend(subclass._subclasses_recursive())
        return result
Принял обновлённый BaseGateHostInspector. Вижу, что в docstring модуля осталось упоминание _validate в двух местах:

В описании inspect(): "5. Вызвать _validate() → проверка инвариантов."
В заголовке: "ТРИ ОБЯЗАТЕЛЬНЫХ МЕТОДА" (а методов теперь два).
В docstring класса: "Определяет контракт из трёх абстрактных classmethod (inspect, _build_payload, _validate)"
Хочешь, чтобы я прислал исправленную версию, или ты сам почистишь? Если сам — шлю следующий файл (metadata/utils.py с collect_subclasses).

пригли
src/action_machine/metadata/utils.py
Copy# src/action_machine/metadata/utils.py
"""
Утилиты пакета метаданных ActionMachine.

═══════════════════════════════════════════════════════════════════════════════
НАЗНАЧЕНИЕ
═══════════════════════════════════════════════════════════════════════════════

Модуль содержит вспомогательные функции, используемые инспекторами
гейтхостов при построении графа координатора.

═══════════════════════════════════════════════════════════════════════════════
ФУНКЦИИ
═══════════════════════════════════════════════════════════════════════════════

collect_subclasses(mixin) → list[type]
    Рекурсивно собирает всех наследников маркерного миксина через
    Python-механизм __subclasses__(). Используется каждым инспектором
    в переопределённом _subclasses_recursive() для обхода наследников
    своего маркера, а не своих собственных.

    Инспектор (RoleGateHostInspector) наследует BaseGateHostInspector,
    но обходить должен наследников маркера (RoleGateHost). Без этой
    функции каждый инспектор дублировал бы один и тот же рекурсивный
    обход.

═══════════════════════════════════════════════════════════════════════════════
ПРИМЕР ИСПОЛЬЗОВАНИЯ
═══════════════════════════════════════════════════════════════════════════════

    from action_machine.metadata.utils import collect_subclasses
    from action_machine.auth.role_gate_host import RoleGateHost

    class RoleGateHostInspector(BaseGateHostInspector):
        _target_mixin: type = RoleGateHost

        @classmethod
        def _subclasses_recursive(cls) -> list[type]:
            return collect_subclasses(cls._target_mixin)

    # Результат: [BaseAction, CreateOrderAction, UpdateOrderAction, ...]
"""

from __future__ import annotations


def collect_subclasses(mixin: type) -> list[type]:
    """
    Рекурсивно собирает всех наследников маркерного миксина.

    Обходит дерево наследования через Python-механизм __subclasses__(),
    который автоматически регистрирует подклассы при определении.
    Никакой ручной регистрации не требуется.

    Порядок обхода: depth-first. Прямые подклассы добавляются перед
    их потомками. Это гарантирует, что BaseAction окажется в списке
    раньше CreateOrderAction.

    Аргументы:
        mixin: маркерный миксин, наследников которого нужно собрать.
               Например: RoleGateHost, AspectGateHost, EntityGateHost.

    Возвращает:
        list[type] — все прямые и транзитивные подклассы миксина.
                     Пустой список если у миксина нет наследников.

    Примеры:
        class RoleGateHost: ...
        class BaseAction(RoleGateHost): ...
        class CreateOrderAction(BaseAction): ...
        class UpdateOrderAction(BaseAction): ...

        collect_subclasses(RoleGateHost)
        → [BaseAction, CreateOrderAction, UpdateOrderAction]

        class EmptyMixin: ...

        collect_subclasses(EmptyMixin)
        → []
    """
    result: list[type] = []
    for sub in mixin.__subclasses__():
        result.append(sub)
        result.extend(collect_subclasses(sub))
    return result
Copy
Файл 5 из 15.

Следующий: обновлённый src/action_machine/auth/role_gate_host_inspector.py — без _validate(), с использованием collect_subclasses из utils.py. Шлю?

шли один файл 
src/action_machine/auth/role_gate_host_inspector.py
Copy# src/action_machine/auth/role_gate_host_inspector.py
"""
RoleGateHostInspector — инспектор гейтхоста ролей для построения графа.

═══════════════════════════════════════════════════════════════════════════════
НАЗНАЧЕНИЕ
═══════════════════════════════════════════════════════════════════════════════

RoleGateHostInspector обходит всех наследников маркерного миксина
RoleGateHost, обнаруживает классы с декоратором @check_roles и собирает
из них FacetPayload для графа координатора.

═══════════════════════════════════════════════════════════════════════════════
ДВА КЛАССА — ДВЕ ОТВЕТСТВЕННОСТИ
═══════════════════════════════════════════════════════════════════════════════

    RoleGateHost (маркерный миксин, auth/role_gate_host.py)
        Живёт в MRO класса BaseAction. Разрешает применение декоратора
        @check_roles через issubclass-проверку. Не содержит логики
        инспекции. Не наследует BaseGateHostInspector. Не меняется.

    RoleGateHostInspector (инспектор, этот файл)
        Наследует BaseGateHostInspector. Реализует inspect() и
        _build_payload(). Обходит наследников RoleGateHost через
        _target_mixin. Регистрируется в координаторе.

Связь между ними — поле _target_mixin. Инспектор знает, наследников
какого маркера обходить. Маркер не знает про инспектор.

═══════════════════════════════════════════════════════════════════════════════
ДАННЫЕ, СОБИРАЕМЫЕ ИНСПЕКТОРОМ
═══════════════════════════════════════════════════════════════════════════════

Декоратор @check_roles при применении записывает на класс атрибут:

    cls._role_info = {"spec": "admin"}           # одна роль
    cls._role_info = {"spec": ["user", "mgr"]}   # список ролей
    cls._role_info = {"spec": "__NONE__"}         # ROLE_NONE
    cls._role_info = {"spec": "__ANY__"}          # ROLE_ANY

Инспектор читает _role_info и формирует узел графа типа "role"
с метаданными spec.

═══════════════════════════════════════════════════════════════════════════════
УЗЕЛ В ГРАФЕ
═══════════════════════════════════════════════════════════════════════════════

    node_type : "role"
    node_name : "module.CreateOrderAction" (полное имя класса)
    node_meta : (("spec", "admin"),)
    edges     : () — ролевой узел не имеет исходящих рёбер

Ключ в графе координатора: "role:module.CreateOrderAction".

═══════════════════════════════════════════════════════════════════════════════
ЛОГИКА inspect()
═══════════════════════════════════════════════════════════════════════════════

    1. getattr(target_cls, "_role_info", None)
    2. Если None → return None (класс без @check_roles, пропускаем)
    3. Если не None → _build_payload() → return payload

Инспектор НЕ выбрасывает TypeError при отсутствии _role_info. Класс
может наследовать RoleGateHost через BaseAction, но не иметь @check_roles.
Это допустимо — машина (ActionProductMachine) проверит наличие ролей
при выполнении и выбросит TypeError если роли обязательны.

═══════════════════════════════════════════════════════════════════════════════
ВАЛИДАЦИЯ
═══════════════════════════════════════════════════════════════════════════════

Инспектор не содержит метода _validate(). Валидация разделена между
двумя уровнями:

    Декоратор @check_roles (import-time):
        Проверяет аргументы при определении класса — spec является
        строкой или списком строк, пустой список запрещён, элементы
        списка — строки. Ошибки обнаруживаются немедленно при импорте.

    Координатор GateCoordinator.build() (build-time):
        Глобальные структурные проверки — уникальность ключей узлов,
        ссылочная целостность рёбер, ацикличность структурных рёбер.

Логика проверки не размазывается. Декоратор отвечает за свои аргументы,
координатор — за целостность графа.

═══════════════════════════════════════════════════════════════════════════════
ОБХОД НАСЛЕДНИКОВ
═══════════════════════════════════════════════════════════════════════════════

_subclasses_recursive() переопределён: обходит наследников _target_mixin
(RoleGateHost), а не наследников самого RoleGateHostInspector.

Координатор вызывает RoleGateHostInspector._subclasses_recursive()
и получает [BaseAction, CreateOrderAction, UpdateOrderAction, ...].
Затем для каждого вызывает inspect(). BaseAction без @check_roles →
None (пропущен). CreateOrderAction с @check_roles → FacetPayload.

═══════════════════════════════════════════════════════════════════════════════
ПРИМЕР
═══════════════════════════════════════════════════════════════════════════════

    @check_roles("admin")
    class AdminAction(BaseAction[AdminParams, AdminResult]):
        ...

    # Координатор при build():
    # RoleGateHostInspector.inspect(AdminAction)
    # → FacetPayload(
    #       node_type="role",
    #       node_name="myapp.actions.AdminAction",
    #       node_class=AdminAction,
    #       node_meta=(("spec", "admin"),),
    #       edges=(),
    #   )

    @check_roles(ROLE_NONE)
    class PingAction(BaseAction[BaseParams, BaseResult]):
        ...

    # RoleGateHostInspector.inspect(PingAction)
    # → FacetPayload(
    #       node_type="role",
    #       node_name="myapp.actions.PingAction",
    #       node_class=PingAction,
    #       node_meta=(("spec", "__NONE__"),),
    #       edges=(),
    #   )

    class BaseAction(ABC, RoleGateHost, ...):
        ...

    # RoleGateHostInspector.inspect(BaseAction)
    # → None (нет _role_info)
"""

from __future__ import annotations

from action_machine.auth.role_gate_host import RoleGateHost
from action_machine.metadata.base_gate_host_inspector import BaseGateHostInspector
from action_machine.metadata.payload import FacetPayload
from action_machine.metadata.utils import collect_subclasses


class RoleGateHostInspector(BaseGateHostInspector):
    """
    Инспектор гейтхоста ролей.

    Обходит наследников RoleGateHost, обнаруживает классы с декоратором
    @check_roles и собирает FacetPayload с ролевой спецификацией
    для графа координатора.

    Узел в графе: тип "role", без исходящих рёбер.
    Метаданные узла: spec (строка, список строк, ROLE_NONE, ROLE_ANY).

    Атрибуты класса:
        _target_mixin : type
            Маркерный миксин, наследников которого обходит инспектор.
            RoleGateHost — миксин, разрешающий @check_roles.
    """

    _target_mixin: type = RoleGateHost

    # ═══════════════════════════════════════════════════════════════════
    # Обход наследников маркерного миксина
    # ═══════════════════════════════════════════════════════════════════

    @classmethod
    def _subclasses_recursive(cls) -> list[type]:
        """
        Возвращает всех наследников RoleGateHost.

        Переопределяет метод BaseGateHostInspector, чтобы обходить
        наследников маркерного миксина (_target_mixin), а не
        наследников самого RoleGateHostInspector.

        Координатор вызывает этот метод при build() и получает
        список классов для инспекции: [BaseAction, CreateOrderAction, ...].
        Классы без @check_roles будут отфильтрованы в inspect() → None.

        Возвращает:
            list[type] — все наследники RoleGateHost.
        """
        return collect_subclasses(cls._target_mixin)

    # ═══════════════════════════════════════════════════════════════════
    # Контракт BaseGateHostInspector
    # ═══════════════════════════════════════════════════════════════════

    @classmethod
    def inspect(cls, target_cls: type) -> FacetPayload | None:
        """
        Проверяет наличие @check_roles и собирает данные.

        Читает атрибут _role_info, записанный декоратором @check_roles.
        Если атрибут отсутствует — класс не имеет ролевых ограничений,
        возвращает None. Если присутствует — собирает payload.

        Аргументы:
            target_cls: класс для инспекции (наследник RoleGateHost).

        Возвращает:
            FacetPayload — класс имеет @check_roles, данные собраны.
            None — класс не имеет @check_roles (нет _role_info).
        """
        role_info = getattr(target_cls, "_role_info", None)
        if role_info is None:
            return None
        return cls._build_payload(target_cls)

    @classmethod
    def _build_payload(cls, target_cls: type) -> FacetPayload:
        """
        Собирает FacetPayload из _role_info класса.

        Формирует узел типа "role" с метаданными spec. Рёбра
        отсутствуют — ролевой узел не ссылается на другие узлы.

        Аргументы:
            target_cls: класс с атрибутом _role_info.

        Возвращает:
            FacetPayload с node_type="role" и spec в node_meta.
        """
        return FacetPayload(
            node_type="role",
            node_name=cls._make_node_name(target_cls),
            node_class=target_cls,
            node_meta=cls._make_meta(
                spec=target_cls._role_info["spec"],
            ),
            edges=(),
        )
Copy
Файл 6 из 15.

Следующий: src/action_machine/metadata/gate_coordinator.py — координатор с register(), транзакционным build() (три фазы) и рантайм-методами чтения графа. Шлю?

1. я просио прислать целиком base_gate_host_inspector
2. не понимаб зачем отдельный utils.py почему это не частт base_gate_host_inspector?
По пунктам:

1. Понял, сейчас пришлю обновлённый base_gate_host_inspector.py целиком с вычищенными упоминаниями _validate.

2. Ты прав, collect_subclasses — это деталь реализации инспекторов, а не общая утилита системы. Она используется только наследниками BaseGateHostInspector. Логично сделать её методом базового класса:

Copyclass BaseGateHostInspector(ABC):
    
    @staticmethod
    def _collect_subclasses(mixin: type) -> list[type]:
        """Рекурсивно собирает наследников маркерного миксина."""
        result = []
        for sub in mixin.__subclasses__():
            result.append(sub)
            result.extend(BaseGateHostInspector._collect_subclasses(sub))
        return result
Инспектор использует:

Copyclass RoleGateHostInspector(BaseGateHostInspector):
    @classmethod
    def _subclasses_recursive(cls):
        return cls._collect_subclasses(cls._target_mixin)
utils.py удаляется. Всё в одном классе.

Шлю обновлённый base_gate_host_inspector.py целиком с _collect_subclasses внутри и без _validate:

src/action_machine/metadata/base_gate_host_inspector.py
Copy# src/action_machine/metadata/base_gate_host_inspector.py
"""
BaseGateHostInspector — абстрактный базовый класс для всех инспекторов
гейтхостов системы ActionMachine.

═══════════════════════════════════════════════════════════════════════════════
НАЗНАЧЕНИЕ
═══════════════════════════════════════════════════════════════════════════════

BaseGateHostInspector определяет контракт, которому обязан следовать каждый
инспектор гейтхоста в системе ActionMachine.

Инспектор — это класс, который:
1. Знает, наследников какого маркерного миксина обходить (_target_mixin).
2. Умеет инспектировать каждого наследника и собирать данные для графа.
3. Регистрируется в координаторе (GateCoordinator).

Координатор при build() обходит зарегистрированные инспекторы, вызывает
inspect() для каждого подкласса маркера и строит граф.

═══════════════════════════════════════════════════════════════════════════════
РАЗДЕЛЕНИЕ: МАРКЕР И ИНСПЕКТОР
═══════════════════════════════════════════════════════════════════════════════

Каждый гейтхост существует в двух ипостасях:

    Маркерный миксин (RoleGateHost, AspectGateHost, DependencyGateHost[T]...)
        Живёт в MRO класса BaseAction (или BaseEntity, BaseResourceManager).
        Разрешает применение соответствующего декоратора через issubclass.
        Не содержит логики инспекции. Не наследует BaseGateHostInspector.
        Не меняется при рефакторинге.

    Инспектор (RoleGateHostInspector, AspectGateHostInspector...)
        Наследует BaseGateHostInspector. Реализует inspect() и
        _build_payload(). Обходит наследников маркера через _target_mixin.
        Регистрируется в координаторе.

Связь между ними — поле _target_mixin инспектора. Инспектор знает,
наследников какого маркера обходить. Маркер не знает про инспектор.

═══════════════════════════════════════════════════════════════════════════════
ДВА ОБЯЗАТЕЛЬНЫХ МЕТОДА
═══════════════════════════════════════════════════════════════════════════════

Каждый инспектор реализует два абстрактных classmethod:

    inspect(target_cls) → FacetPayload | None
        Точка входа. Определяет, подходит ли класс этому инспектору.
        Два возможных результата:
        - FacetPayload — класс подходит, данные собраны.
        - None — класс не является субъектом этого инспектора.

    _build_payload(target_cls) → FacetPayload
        Собирает узел и рёбра. Читает атрибуты класса (_role_info,
        _depends_info, _meta_info и т.д.), формирует FacetPayload
        с использованием хелперов базового класса.

═══════════════════════════════════════════════════════════════════════════════
РАЗДЕЛЕНИЕ ВАЛИДАЦИИ
═══════════════════════════════════════════════════════════════════════════════

Валидация выполняется на двух уровнях, каждый — в одном месте:

    Декораторы (@check_roles, @regular_aspect, @depends...)
        Проверяют аргументы при import-time: типы, пустоту, issubclass,
        дубликаты. Обнаруживают ошибки немедленно при определении класса.

    Координатор (GateCoordinator.build())
        Глобальные структурные проверки после сбора всех payload:
        уникальность ключей, ссылочная целостность рёбер, ацикличность
        структурных рёбер.

Инспектор НЕ содержит метода _validate(). Логика проверки не
размазывается между декоратором и инспектором.

═══════════════════════════════════════════════════════════════════════════════
РАЗДЕЛЕНИЕ ОТВЕТСТВЕННОСТИ
═══════════════════════════════════════════════════════════════════════════════

    inspect()        → проверяет ТОЛЬКО наличие данных (есть _role_info?)
    _build_payload() → читает данные, формирует payload

Это разделение гарантирует, что:
- inspect() работает быстро — только hasattr/getattr.
- _build_payload() формирует payload без побочных эффектов.

═══════════════════════════════════════════════════════════════════════════════
ХЕЛПЕРЫ
═══════════════════════════════════════════════════════════════════════════════

Базовый класс предоставляет пять хелперов, устраняющие дублирование
в каждом инспекторе:

    _make_node_name(target_cls, suffix="") → str
        Формирует имя узла "module.ClassName" или "module.ClassName.suffix".
        Префикс типа ("action:", "role:") НЕ добавляется — это делает
        координатор при формировании ключа.

    _make_edge(target_node_type, target_cls, edge_type,
               is_structural, edge_meta=()) → EdgeInfo
        Собирает EdgeInfo без ручного заполнения каждого поля.
        Имя цели формируется через _make_node_name(target_cls).

    _make_edge_by_name(target_node_type, target_name, edge_type,
                       is_structural, edge_meta=()) → EdgeInfo
        Аналог _make_edge для случаев, когда цель — не класс,
        а произвольное строковое имя (например, "context_field:user.user_id").

    _make_meta(**kwargs) → tuple[tuple[str, Any], ...]
        Конвертирует dict-синтаксис в иммутабельный tuple of tuples,
        пригодный для frozen dataclass.

    _collect_subclasses(mixin) → list[type]
        Рекурсивно собирает всех наследников маркерного миксина.
        Используется инспекторами в переопределённом
        _subclasses_recursive() для обхода наследников своего маркера.

═══════════════════════════════════════════════════════════════════════════════
ОБХОД НАСЛЕДНИКОВ
═══════════════════════════════════════════════════════════════════════════════

Метод _subclasses_recursive() определён в базовом классе и рекурсивно
собирает подклассы через __subclasses__(). Инспекторы переопределяют его,
чтобы обходить наследников _target_mixin, а не своих собственных:

    class RoleGateHostInspector(BaseGateHostInspector):
        _target_mixin = RoleGateHost

        @classmethod
        def _subclasses_recursive(cls) -> list[type]:
            return cls._collect_subclasses(cls._target_mixin)

Координатор вызывает _subclasses_recursive() при build() для каждого
зарегистрированного инспектора.

═══════════════════════════════════════════════════════════════════════════════
ПРИМЕР РЕАЛИЗАЦИИ ИНСПЕКТОРА БЕЗ РЁБЕР
═══════════════════════════════════════════════════════════════════════════════

    class RoleGateHostInspector(BaseGateHostInspector):
        _target_mixin = RoleGateHost

        @classmethod
        def _subclasses_recursive(cls) -> list[type]:
            return cls._collect_subclasses(cls._target_mixin)

        @classmethod
        def inspect(cls, target_cls: type) -> FacetPayload | None:
            role_info = getattr(target_cls, "_role_info", None)
            if role_info is None:
                return None
            return cls._build_payload(target_cls)

        @classmethod
        def _build_payload(cls, target_cls: type) -> FacetPayload:
            return FacetPayload(
                node_type="role",
                node_name=cls._make_node_name(target_cls),
                node_class=target_cls,
                node_meta=cls._make_meta(spec=target_cls._role_info["spec"]),
            )

═══════════════════════════════════════════════════════════════════════════════
ПРИМЕР РЕАЛИЗАЦИИ ИНСПЕКТОРА С РЁБРАМИ
═══════════════════════════════════════════════════════════════════════════════

    class DependencyGateHostInspector(BaseGateHostInspector):
        _target_mixin = DependencyGateHost

        @classmethod
        def _subclasses_recursive(cls) -> list[type]:
            return cls._collect_subclasses(cls._target_mixin)

        @classmethod
        def inspect(cls, target_cls: type) -> FacetPayload | None:
            depends_info = getattr(target_cls, "_depends_info", None)
            if not depends_info:
                return None
            return cls._build_payload(target_cls)

        @classmethod
        def _build_payload(cls, target_cls: type) -> FacetPayload:
            edges = tuple(
                cls._make_edge(
                    target_node_type="dependency",
                    target_cls=dep_info.cls,
                    edge_type="depends",
                    is_structural=True,
                )
                for dep_info in target_cls._depends_info
            )
            return FacetPayload(
                node_type="action",
                node_name=cls._make_node_name(target_cls),
                node_class=target_cls,
                edges=edges,
            )
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import Any

from action_machine.metadata.payload import EdgeInfo, FacetPayload


class BaseGateHostInspector(ABC):
    """
    Абстрактный базовый класс для всех инспекторов гейтхостов.

    Определяет контракт из двух абстрактных classmethod (inspect,
    _build_payload) и предоставляет пять хелперов для формирования
    FacetPayload и EdgeInfo без дублирования кода в каждом инспекторе.

    Все методы — classmethod или staticmethod. Инспектор не хранит
    состояния и не требует создания экземпляра. Группировка в класс
    обеспечивает пространство имён, наследование хелперов и проверку
    контракта через ABC.

    Координатор (GateCoordinator) при build() вызывает:
    1. inspector._subclasses_recursive() — получить всех наследников маркера.
    2. inspector.inspect(target_cls) — инспектировать каждого наследника.
    """

    # ═══════════════════════════════════════════════════════════════════
    # Обязательный контракт (два абстрактных метода)
    # ═══════════════════════════════════════════════════════════════════

    @classmethod
    @abstractmethod
    def inspect(cls, target_cls: type) -> FacetPayload | None:
        """
        Определяет, подходит ли класс этому инспектору, и собирает данные.

        Точка входа, вызываемая координатором для каждого подкласса
        маркерного миксина. Реализация типичного inspect():

            1. Проверить наличие данных (hasattr/getattr).
            2. Если данных нет → return None.
            3. Вызвать _build_payload() → FacetPayload.
            4. Вернуть payload.

        Аргументы:
            target_cls: класс для инспекции. Найден через
                        _subclasses_recursive() — является подклассом
                        маркерного миксина (_target_mixin).

        Возвращает:
            FacetPayload — класс подходит, данные собраны.
            None — класс не является субъектом этого инспектора
                   (нет соответствующих атрибутов/декораторов).
        """
        ...

    @classmethod
    @abstractmethod
    def _build_payload(cls, target_cls: type) -> FacetPayload:
        """
        Собирает FacetPayload из атрибутов класса.

        Читает данные (_role_info, _depends_info и т.д.), формирует
        узел и рёбра с использованием хелперов базового класса:
        _make_node_name, _make_edge, _make_edge_by_name, _make_meta.

        Аргументы:
            target_cls: класс, прошедший проверку наличия данных
                        в inspect().

        Возвращает:
            FacetPayload — полное описание узла с рёбрами.
        """
        ...

    # ═══════════════════════════════════════════════════════════════════
    # Хелперы для _build_payload
    # ═══════════════════════════════════════════════════════════════════

    @classmethod
    def _make_node_name(cls, target_cls: type, suffix: str = "") -> str:
        """
        Формирует имя узла из модуля и имени класса.

        Формат: "module.ClassName" или "module.ClassName.suffix".
        Если модуль "__main__" или отсутствует — только "ClassName".

        Префикс типа ("action:", "role:") НЕ добавляется. Полный ключ
        "node_type:node_name" собирает координатор при коммите.

        Аргументы:
            target_cls: класс, для которого формируется имя.
            suffix: опциональный суффикс, добавляемый через точку.
                    Используется для дочерних узлов: аспектов,
                    чекеров, полей сущностей.
                    Пустая строка — суффикс не добавляется.

        Возвращает:
            str — имя узла.

        Примеры:
            _make_node_name(CreateOrderAction)
            → "myapp.orders.CreateOrderAction"

            _make_node_name(CreateOrderAction, "validate_aspect")
            → "myapp.orders.CreateOrderAction.validate_aspect"

            _make_node_name(OrderEntity, "amount")
            → "myapp.domain.OrderEntity.amount"
        """
        module = getattr(target_cls, "__module__", None)
        if module and module != "__main__":
            name = f"{module}.{target_cls.__qualname__}"
        else:
            name = target_cls.__qualname__
        if suffix:
            return f"{name}.{suffix}"
        return name

    @classmethod
    def _make_edge(
        cls,
        target_node_type: str,
        target_cls: type,
        edge_type: str,
        is_structural: bool,
        edge_meta: tuple[tuple[str, Any], ...] = (),
    ) -> EdgeInfo:
        """
        Собирает EdgeInfo с автоматическим формированием имени цели.

        Имя целевого узла формируется через _make_node_name(target_cls).
        Используется когда цель ребра — класс Python.

        Аргументы:
            target_node_type: тип целевого узла ("action", "entity",
                              "domain", "dependency" и т.д.).
            target_cls: класс, являющийся целью ребра.
            edge_type: тип ребра ("depends", "connection", "has_aspect",
                       "belongs_to" и т.д.).
            is_structural: True — структурное ребро (циклы запрещены).
                           False — информационное ребро (циклы допустимы).
            edge_meta: дополнительные метаданные ребра.
                       Пустой tuple по умолчанию.

        Возвращает:
            EdgeInfo — описание ребра.

        Пример:
            cls._make_edge(
                target_node_type="dependency",
                target_cls=PaymentService,
                edge_type="depends",
                is_structural=True,
            )
        """
        return EdgeInfo(
            target_node_type=target_node_type,
            target_name=cls._make_node_name(target_cls),
            edge_type=edge_type,
            is_structural=is_structural,
            edge_meta=edge_meta,
        )

    @classmethod
    def _make_edge_by_name(
        cls,
        target_node_type: str,
        target_name: str,
        edge_type: str,
        is_structural: bool,
        edge_meta: tuple[tuple[str, Any], ...] = (),
    ) -> EdgeInfo:
        """
        Собирает EdgeInfo с произвольным строковым именем цели.

        Используется когда цель ребра — не класс Python, а строковый
        идентификатор. Например, узлы контекстных полей
        ("context_field:user.user_id") или доменов ("domain:orders").

        Аргументы:
            target_node_type: тип целевого узла.
            target_name: строковое имя целевого узла.
            edge_type: тип ребра.
            is_structural: структурное или информационное.
            edge_meta: дополнительные метаданные ребра.

        Возвращает:
            EdgeInfo — описание ребра.

        Пример:
            cls._make_edge_by_name(
                target_node_type="context_field",
                target_name="user.user_id",
                edge_type="requires_context",
                is_structural=False,
            )
        """
        return EdgeInfo(
            target_node_type=target_node_type,
            target_name=target_name,
            edge_type=edge_type,
            is_structural=is_structural,
            edge_meta=edge_meta,
        )

    @classmethod
    def _make_meta(cls, **kwargs: Any) -> tuple[tuple[str, Any], ...]:
        """
        Конвертирует именованные аргументы в иммутабельный tuple of tuples.

        Frozen dataclass требует хешируемые поля. dict не хешируем.
        Этот хелпер позволяет инспекторам использовать удобный
        dict-синтаксис (kwargs) при создании метаданных, получая
        на выходе иммутабельную структуру.

        Координатор конвертирует tuple of tuples обратно в dict
        при коммите: dict(node_meta).

        Аргументы:
            **kwargs: произвольные пары ключ-значение метаданных.

        Возвращает:
            tuple[tuple[str, Any], ...] — иммутабельные метаданные.

        Пример:
            cls._make_meta(spec="admin", description="Администратор")
            → (("spec", "admin"), ("description", "Администратор"))

            cls._make_meta()
            → ()
        """
        return tuple(kwargs.items())

    # ═══════════════════════════════════════════════════════════════════
    # Обход наследников
    # ═══════════════════════════════════════════════════════════════════

    @staticmethod
    def _collect_subclasses(mixin: type) -> list[type]:
        """
        Рекурсивно собирает всех наследников маркерного миксина.

        Обходит дерево наследования через Python-механизм __subclasses__(),
        который автоматически регистрирует подклассы при определении.
        Никакой ручной регистрации не требуется.

        Используется инспекторами в переопределённом _subclasses_recursive()
        для обхода наследников своего маркера (_target_mixin), а не
        наследников самого инспектора.

        Порядок обхода: depth-first. Прямые подклассы добавляются
        перед их потомками.

        Аргументы:
            mixin: маркерный миксин, наследников которого нужно собрать.
                   Например: RoleGateHost, AspectGateHost, EntityGateHost.

        Возвращает:
            list[type] — все прямые и транзитивные подклассы миксина.
                         Пустой список если у миксина нет наследников.

        Пример:
            class RoleGateHost: ...
            class BaseAction(RoleGateHost): ...
            class CreateOrderAction(BaseAction): ...

            BaseGateHostInspector._collect_subclasses(RoleGateHost)
            → [BaseAction, CreateOrderAction]
        """
        result: list[type] = []
        for sub in mixin.__subclasses__():
            result.append(sub)
            result.extend(BaseGateHostInspector._collect_subclasses(sub))
        return result

    @classmethod
    def _subclasses_recursive(cls) -> list[type]:
        """
        Рекурсивно собирает всех наследников для инспекции.

        Базовая реализация обходит наследников самого инспектора.
        Конкретные инспекторы переопределяют этот метод, чтобы
        обходить наследников _target_mixin:

            @classmethod
            def _subclasses_recursive(cls) -> list[type]:
                return cls._collect_subclasses(cls._target_mixin)

        Координатор вызывает этот метод при build() для каждого
        зарегистрированного инспектора.

        Возвращает:
            list[type] — все классы для инспекции.
        """
        result: list[type] = []
        for subclass in cls.__subclasses__():
            result.append(subclass)
            if hasattr(subclass, "_subclasses_recursive"):
                result.extend(subclass._subclasses_recursive())
        return result
Copy
Файл 2 из 14 (обновлённый, utils.py удалён из плана).

Следующий: src/action_machine/metadata/gate_coordinator.py — координатор. Шлю?

перепиши # src/action_machine/auth/role_gate_host_inspector.py тк utils переехал
src/action_machine/auth/role_gate_host_inspector.py
Copy# src/action_machine/auth/role_gate_host_inspector.py
"""
RoleGateHostInspector — инспектор гейтхоста ролей для построения графа.

═══════════════════════════════════════════════════════════════════════════════
НАЗНАЧЕНИЕ
═══════════════════════════════════════════════════════════════════════════════

RoleGateHostInspector обходит всех наследников маркерного миксина
RoleGateHost, обнаруживает классы с декоратором @check_roles и собирает
из них FacetPayload для графа координатора.

═══════════════════════════════════════════════════════════════════════════════
ДВА КЛАССА — ДВЕ ОТВЕТСТВЕННОСТИ
═══════════════════════════════════════════════════════════════════════════════

    RoleGateHost (маркерный миксин, auth/role_gate_host.py)
        Живёт в MRO класса BaseAction. Разрешает применение декоратора
        @check_roles через issubclass-проверку. Не содержит логики
        инспекции. Не наследует BaseGateHostInspector. Не меняется.

    RoleGateHostInspector (инспектор, этот файл)
        Наследует BaseGateHostInspector. Реализует inspect() и
        _build_payload(). Обходит наследников RoleGateHost через
        _target_mixin. Регистрируется в координаторе.

Связь между ними — поле _target_mixin. Инспектор знает, наследников
какого маркера обходить. Маркер не знает про инспектор.

═══════════════════════════════════════════════════════════════════════════════
ДАННЫЕ, СОБИРАЕМЫЕ ИНСПЕКТОРОМ
═══════════════════════════════════════════════════════════════════════════════

Декоратор @check_roles при применении записывает на класс атрибут:

    cls._role_info = {"spec": "admin"}           # одна роль
    cls._role_info = {"spec": ["user", "mgr"]}   # список ролей
    cls._role_info = {"spec": "__NONE__"}         # ROLE_NONE
    cls._role_info = {"spec": "__ANY__"}          # ROLE_ANY

Инспектор читает _role_info и формирует узел графа типа "role"
с метаданными spec.

═══════════════════════════════════════════════════════════════════════════════
УЗЕЛ В ГРАФЕ
═══════════════════════════════════════════════════════════════════════════════

    node_type : "role"
    node_name : "module.CreateOrderAction" (полное имя класса)
    node_meta : (("spec", "admin"),)
    edges     : () — ролевой узел не имеет исходящих рёбер

Ключ в графе координатора: "role:module.CreateOrderAction".

═══════════════════════════════════════════════════════════════════════════════
ЛОГИКА inspect()
═══════════════════════════════════════════════════════════════════════════════

    1. getattr(target_cls, "_role_info", None)
    2. Если None → return None (класс без @check_roles, пропускаем)
    3. Если не None → _build_payload() → return payload

Инспектор НЕ выбрасывает TypeError при отсутствии _role_info. Класс
может наследовать RoleGateHost через BaseAction, но не иметь @check_roles.
Это допустимо — машина (ActionProductMachine) проверит наличие ролей
при выполнении и выбросит TypeError если роли обязательны.

═══════════════════════════════════════════════════════════════════════════════
ВАЛИДАЦИЯ
═══════════════════════════════════════════════════════════════════════════════

Валидация разделена между двумя уровнями:

    Декоратор @check_roles (import-time):
        Проверяет аргументы при определении класса — spec является
        строкой или списком строк, пустой список запрещён, элементы
        списка — строки. Ошибки обнаруживаются немедленно при импорте.

    Координатор GateCoordinator.build() (build-time):
        Глобальные структурные проверки — уникальность ключей узлов,
        ссылочная целостность рёбер, ацикличность структурных рёбер.

Логика проверки не размазывается. Декоратор отвечает за свои аргументы,
координатор — за целостность графа.

═══════════════════════════════════════════════════════════════════════════════
ОБХОД НАСЛЕДНИКОВ
═══════════════════════════════════════════════════════════════════════════════

_subclasses_recursive() переопределён: обходит наследников _target_mixin
(RoleGateHost) через хелпер _collect_subclasses() базового класса,
а не наследников самого RoleGateHostInspector.

Координатор вызывает RoleGateHostInspector._subclasses_recursive()
и получает [BaseAction, CreateOrderAction, UpdateOrderAction, ...].
Затем для каждого вызывает inspect(). BaseAction без @check_roles →
None (пропущен). CreateOrderAction с @check_roles → FacetPayload.

═══════════════════════════════════════════════════════════════════════════════
ПРИМЕР
═══════════════════════════════════════════════════════════════════════════════

    @check_roles("admin")
    class AdminAction(BaseAction[AdminParams, AdminResult]):
        ...

    # Координатор при build():
    # RoleGateHostInspector.inspect(AdminAction)
    # → FacetPayload(
    #       node_type="role",
    #       node_name="myapp.actions.AdminAction",
    #       node_class=AdminAction,
    #       node_meta=(("spec", "admin"),),
    #       edges=(),
    #   )

    @check_roles(ROLE_NONE)
    class PingAction(BaseAction[BaseParams, BaseResult]):
        ...

    # RoleGateHostInspector.inspect(PingAction)
    # → FacetPayload(
    #       node_type="role",
    #       node_name="myapp.actions.PingAction",
    #       node_class=PingAction,
    #       node_meta=(("spec", "__NONE__"),),
    #       edges=(),
    #   )

    class BaseAction(ABC, RoleGateHost, ...):
        ...

    # RoleGateHostInspector.inspect(BaseAction)
    # → None (нет _role_info)
"""

from __future__ import annotations

from action_machine.auth.role_gate_host import RoleGateHost
from action_machine.metadata.base_gate_host_inspector import BaseGateHostInspector
from action_machine.metadata.payload import FacetPayload


class RoleGateHostInspector(BaseGateHostInspector):
    """
    Инспектор гейтхоста ролей.

    Обходит наследников RoleGateHost, обнаруживает классы с декоратором
    @check_roles и собирает FacetPayload с ролевой спецификацией
    для графа координатора.

    Узел в графе: тип "role", без исходящих рёбер.
    Метаданные узла: spec (строка, список строк, ROLE_NONE, ROLE_ANY).

    Атрибуты класса:
        _target_mixin : type
            Маркерный миксин, наследников которого обходит инспектор.
            RoleGateHost — миксин, разрешающий @check_roles.
    """

    _target_mixin: type = RoleGateHost

    # ═══════════════════════════════════════════════════════════════════
    # Обход наследников маркерного миксина
    # ═══════════════════════════════════════════════════════════════════

    @classmethod
    def _subclasses_recursive(cls) -> list[type]:
        """
        Возвращает всех наследников RoleGateHost.

        Переопределяет метод BaseGateHostInspector, чтобы обходить
        наследников маркерного миксина (_target_mixin), а не
        наследников самого RoleGateHostInspector.

        Использует хелпер _collect_subclasses() из базового класса
        BaseGateHostInspector.

        Координатор вызывает этот метод при build() и получает
        список классов для инспекции: [BaseAction, CreateOrderAction, ...].
        Классы без @check_roles будут отфильтрованы в inspect() → None.

        Возвращает:
            list[type] — все наследники RoleGateHost.
        """
        return cls._collect_subclasses(cls._target_mixin)

    # ═══════════════════════════════════════════════════════════════════
    # Контракт BaseGateHostInspector
    # ═══════════════════════════════════════════════════════════════════

    @classmethod
    def inspect(cls, target_cls: type) -> FacetPayload | None:
        """
        Проверяет наличие @check_roles и собирает данные.

        Читает атрибут _role_info, записанный декоратором @check_roles.
        Если атрибут отсутствует — класс не имеет ролевых ограничений,
        возвращает None. Если присутствует — собирает payload.

        Аргументы:
            target_cls: класс для инспекции (наследник RoleGateHost).

        Возвращает:
            FacetPayload — класс имеет @check_roles, данные собраны.
            None — класс не имеет @check_roles (нет _role_info).
        """
        role_info = getattr(target_cls, "_role_info", None)
        if role_info is None:
            return None
        return cls._build_payload(target_cls)

    @classmethod
    def _build_payload(cls, target_cls: type) -> FacetPayload:
        """
        Собирает FacetPayload из _role_info класса.

        Формирует узел типа "role" с метаданными spec. Рёбра
        отсутствуют — ролевой узел не ссылается на другие узлы.

        Аргументы:
            target_cls: класс с атрибутом _role_info.

        Возвращает:
            FacetPayload с node_type="role" и spec в node_meta.
        """
        return FacetPayload(
            node_type="role",
            node_name=cls._make_node_name(target_cls),
            node_class=target_cls,
            node_meta=cls._make_meta(
                spec=target_cls._role_info["spec"],
            ),
            edges=(),
        )
Copy
Файл 4 из 14 (обновлённый, без utils.py).

Следующий: src/action_machine/metadata/gate_coordinator.py. Шлю?

дальше один файл
src/action_machine/metadata/gate_coordinator.py
Copy# src/action_machine/metadata/gate_coordinator.py
"""
GateCoordinator — центральный реестр и сборщик графа зависимостей системы.

═══════════════════════════════════════════════════════════════════════════════
НАЗНАЧЕНИЕ
═══════════════════════════════════════════════════════════════════════════════

GateCoordinator — единственная точка входа для построения и чтения графа
всех сущностей системы ActionMachine. Координатор:

1. Принимает регистрацию инспекторов гейтхостов через fluent-метод register().
2. Строит граф один раз при вызове build() через транзакционный трёхфазный
   процесс.
3. Предоставляет типизированные методы чтения данных из графа для рантайма
   (машина, адаптеры, плагины).

После build() граф становится единственным источником правды. Координатор
только читает граф, никогда не модифицирует его после коммита.

═══════════════════════════════════════════════════════════════════════════════
ТРАНЗАКЦИОННЫЙ build() — ТРИ ФАЗЫ
═══════════════════════════════════════════════════════════════════════════════

Граф либо строится полностью и корректно, либо не строится вообще.
Никакого частичного состояния.

    ФАЗА 1 — СБОР
        Для каждого зарегистрированного инспектора обходятся наследники
        его маркерного миксина (_subclasses_recursive). Для каждого
        наследника вызывается inspect(). Результат (FacetPayload или None)
        накапливается в список. Граф не трогается.

    ФАЗА 2 — ПРОВЕРКИ
        Все собранные payload проверяются на:
        2a. Обязательные поля непустые (PayloadValidationError).
        2b. Уникальность ключей "node_type:node_name" (DuplicateNodeError).
        2c. Ссылочная целостность рёбер — цель существует (InvalidGraphError).
        2d. Ацикличность структурных рёбер через симуляцию на временном
            графе (InvalidGraphError).
        Граф не трогается.

    ФАЗА 3 — КОММИТ
        Только если фаза 2 прошла. Все узлы и рёбра добавляются в граф.
        tuple of tuples конвертируется в dict. Флаг _built = True.

═══════════════════════════════════════════════════════════════════════════════
РАЗДЕЛЕНИЕ ВАЛИДАЦИИ
═══════════════════════════════════════════════════════════════════════════════

    Декораторы (@check_roles, @regular_aspect, @depends...)
        Проверяют аргументы при import-time. Типы, пустоту, issubclass.
        Ошибки обнаруживаются немедленно при определении класса.

    Координатор (build(), фаза 2)
        Глобальные структурные проверки: уникальность ключей, ссылочная
        целостность, ацикличность. Одно место для всех проверок графа.

Логика проверки не размазывается. Декоратор отвечает за свои аргументы,
координатор — за целостность графа.

═══════════════════════════════════════════════════════════════════════════════
ОГРАНИЧЕНИЯ
═══════════════════════════════════════════════════════════════════════════════

- build() вызывается ровно один раз. Повторный вызов → RuntimeError.
- register() после build() → RuntimeError.
- Дубликат инспектора при register() → ValueError.

═══════════════════════════════════════════════════════════════════════════════
ФОРМАТ КЛЮЧЕЙ УЗЛОВ
═══════════════════════════════════════════════════════════════════════════════

Каждый узел графа идентифицируется строковым ключом "node_type:node_name".
Инспектор формирует node_type и node_name. Координатор собирает ключ:

    node_type="role", node_name="module.CreateOrderAction"
    → ключ: "role:module.CreateOrderAction"

Один класс может порождать несколько узлов от разных инспекторов.
Уникальность гарантируется комбинацией node_type + node_name.

═══════════════════════════════════════════════════════════════════════════════
ГРАФ
═══════════════════════════════════════════════════════════════════════════════

Граф построен на библиотеке rustworkx (rx.PyDiGraph). Узлы хранят dict
с полями: node_type, name, class_ref, meta. Рёбра хранят dict с полями:
edge_type, meta.

═══════════════════════════════════════════════════════════════════════════════
РАНТАЙМ-ДОСТУП
═══════════════════════════════════════════════════════════════════════════════

Координатор предоставляет типизированные методы чтения данных из графа.
Машина (ActionProductMachine) и адаптеры используют эти методы вместо
прямого доступа к ClassMetadata.

═══════════════════════════════════════════════════════════════════════════════
ПРИМЕР ИСПОЛЬЗОВАНИЯ
═══════════════════════════════════════════════════════════════════════════════

    from action_machine.metadata.gate_coordinator import GateCoordinator
    from action_machine.auth.role_gate_host_inspector import RoleGateHostInspector
    from action_machine.aspects.aspect_gate_host_inspector import AspectGateHostInspector

    coordinator = (
        GateCoordinator(strict=True)
        .register(RoleGateHostInspector)
        .register(AspectGateHostInspector)
        .build()
    )

    # Рантайм-доступ:
    spec = coordinator.get_role_spec(CreateOrderAction)
    aspects = coordinator.get_regular_aspects(CreateOrderAction)
"""

from __future__ import annotations

from typing import Any

import rustworkx as rx

from action_machine.metadata.base_gate_host_inspector import BaseGateHostInspector
from action_machine.metadata.exceptions import (
    DuplicateNodeError,
    InvalidGraphError,
    PayloadValidationError,
)
from action_machine.metadata.payload import FacetPayload


class GateCoordinator:
    """
    Центральный реестр и сборщик графа зависимостей системы ActionMachine.

    Принимает регистрацию инспекторов, строит граф через транзакционный
    build() и предоставляет типизированные методы чтения.

    Атрибуты:
        _strict : bool
            Если True — дополнительные проверки при build()
            (например, обязательность domain в @meta для Action).

        _inspectors : list[type[BaseGateHostInspector]]
            Зарегистрированные инспекторы в порядке регистрации.

        _registered : set[type[BaseGateHostInspector]]
            Множество зарегистрированных инспекторов для проверки
            дубликатов.

        _graph : rx.PyDiGraph
            Направленный граф сущностей системы. Заполняется
            при коммите (фаза 3). После build() — только чтение.

        _node_index : dict[str, int]
            Карта ключ_узла → индекс_в_графе. Заполняется при коммите.

        _class_index : dict[type, list[str]]
            Карта класс → список ключей узлов, порождённых этим классом.
            Заполняется при коммите. Используется рантайм-методами
            для поиска узлов по классу.

        _built : bool
            Флаг завершения build(). После True — register() запрещён,
            повторный build() запрещён.
    """

    def __init__(self, strict: bool = False) -> None:
        """
        Создаёт координатор с пустым графом.

        Аргументы:
            strict: если True — дополнительные проверки при build().
        """
        self._strict: bool = strict
        self._inspectors: list[type[BaseGateHostInspector]] = []
        self._registered: set[type[BaseGateHostInspector]] = set()
        self._graph: rx.PyDiGraph = rx.PyDiGraph()
        self._node_index: dict[str, int] = {}
        self._class_index: dict[type, list[str]] = {}
        self._built: bool = False

    # ═══════════════════════════════════════════════════════════════════
    # Fluent-регистрация инспекторов
    # ═══════════════════════════════════════════════════════════════════

    def register(
        self, inspector_cls: type[BaseGateHostInspector],
    ) -> GateCoordinator:
        """
        Регистрирует инспектор гейтхоста.

        Вызывается до build(). Поддерживает fluent-цепочку:

            coordinator = GateCoordinator()\\
                .register(RoleGateHostInspector)\\
                .register(AspectGateHostInspector)\\
                .build()

        Аргументы:
            inspector_cls: класс инспектора (наследник BaseGateHostInspector).

        Возвращает:
            self — для fluent-цепочки.

        Исключения:
            RuntimeError: если build() уже вызван.
            ValueError: если инспектор уже зарегистрирован.
        """
        if self._built:
            raise RuntimeError(
                f"Регистрация {inspector_cls.__name__} после build() запрещена. "
                f"Все инспекторы регистрируются до вызова build()."
            )
        if inspector_cls in self._registered:
            raise ValueError(
                f"Инспектор {inspector_cls.__name__} уже зарегистрирован."
            )
        self._inspectors.append(inspector_cls)
        self._registered.add(inspector_cls)
        return self

    # ═══════════════════════════════════════════════════════════════════
    # Построение графа
    # ═══════════════════════════════════════════════════════════════════

    def build(self) -> GateCoordinator:
        """
        Транзакционное построение графа.

        Вызывается ровно один раз после регистрации всех инспекторов.
        Три фазы: сбор → проверки → коммит. Если любая проверка
        не прошла — граф не изменяется, исключение пробрасывается.

        Возвращает:
            self — для fluent-цепочки (coordinator = GateCoordinator()...build()).

        Исключения:
            RuntimeError: если build() уже вызван.
            PayloadValidationError: невалидные поля payload (фаза 2a).
            DuplicateNodeError: конфликт ключей узлов (фаза 2b).
            InvalidGraphError: битая ссылка ребра или цикл (фаза 2c, 2d).
        """
        if self._built:
            raise RuntimeError(
                "build() уже вызван. Координатор строит граф один раз."
            )

        all_payloads, payload_sources = self._phase1_collect()
        self._phase2_check_payloads(all_payloads)
        self._phase2_check_key_uniqueness(all_payloads, payload_sources)
        self._phase2_check_referential_integrity(all_payloads)
        self._phase2_check_acyclicity(all_payloads)
        self._phase3_commit(all_payloads)

        self._built = True
        return self

    # ═══════════════════════════════════════════════════════════════════
    # Фаза 1 — Сбор
    # ═══════════════════════════════════════════════════════════════════

    def _phase1_collect(
        self,
    ) -> tuple[list[FacetPayload], dict[str, str]]:
        """
        Обходит всех инспекторов и собирает FacetPayload.

        Для каждого инспектора вызывает _subclasses_recursive(), затем
        inspect() для каждого найденного класса. payload с None
        отфильтровываются.

        Дополнительно отслеживает, какой инспектор создал каждый payload,
        для информативных сообщений об ошибках в фазе 2.

        Возвращает:
            Кортеж из двух элементов:
            - list[FacetPayload] — все собранные payload.
            - dict[str, str] — карта ключ_узла → имя_инспектора
              (для сообщений об ошибках DuplicateNodeError).
        """
        all_payloads: list[FacetPayload] = []
        payload_sources: dict[str, str] = {}

        for inspector_cls in self._inspectors:
            inspector_name = inspector_cls.__name__
            subclasses = inspector_cls._subclasses_recursive()

            for target_cls in subclasses:
                payload = inspector_cls.inspect(target_cls)
                if payload is None:
                    continue

                key = self._make_key(payload.node_type, payload.node_name)

                if key in payload_sources:
                    raise DuplicateNodeError(
                        key=key,
                        first_gate_host=payload_sources[key],
                        second_gate_host=inspector_name,
                    )

                payload_sources[key] = inspector_name
                all_payloads.append(payload)

        return all_payloads, payload_sources

    # ═══════════════════════════════════════════════════════════════════
    # Фаза 2 — Проверки
    # ═══════════════════════════════════════════════════════════════════

    def _phase2_check_payloads(
        self, payloads: list[FacetPayload],
    ) -> None:
        """
        Проверка 2a: обязательные поля payload непустые.

        Каждый payload должен иметь:
        - node_type — непустая строка.
        - node_name — непустая строка.
        - node_class — экземпляр type.

        Аргументы:
            payloads: список payload для проверки.

        Исключения:
            PayloadValidationError: если любое обязательное поле невалидно.
        """
        for p in payloads:
            if not p.node_type:
                raise PayloadValidationError(
                    node_class=p.node_class,
                    field_name="node_type",
                    detail="пустая строка",
                )
            if not p.node_name:
                raise PayloadValidationError(
                    node_class=p.node_class,
                    field_name="node_name",
                    detail="пустая строка",
                )
            if not isinstance(p.node_class, type):
                raise PayloadValidationError(
                    node_class=p.node_class,
                    field_name="node_class",
                    detail=f"ожидался type, получен {type(p.node_class).__name__}",
                )

    def _phase2_check_key_uniqueness(
        self,
        payloads: list[FacetPayload],
        payload_sources: dict[str, str],
    ) -> None:
        """
        Проверка 2b: уникальность ключей узлов.

        Дубликаты уже обнаруживаются в _phase1_collect при заполнении
        payload_sources. Этот метод — дополнительная защита на случай
        если payload_sources не используется (прямой вызов фазы 2
        в тестах).

        Аргументы:
            payloads: список payload для проверки.
            payload_sources: карта ключ → имя инспектора.

        Исключения:
            DuplicateNodeError: если обнаружен дубликат ключа.
        """
        seen: set[str] = set()
        for p in payloads:
            key = self._make_key(p.node_type, p.node_name)
            if key in seen:
                raise DuplicateNodeError(
                    key=key,
                    first_gate_host=payload_sources.get(key, "unknown"),
                    second_gate_host="unknown",
                )
            seen.add(key)

    def _phase2_check_referential_integrity(
        self, payloads: list[FacetPayload],
    ) -> None:
        """
        Проверка 2c: ссылочная целостность рёбер.

        Каждое ребро ссылается на целевой узел через
        "target_node_type:target_name". Этот ключ обязан существовать
        среди собранных payload. Если цель не найдена — ребро битое.

        Аргументы:
            payloads: список payload для проверки.

        Исключения:
            InvalidGraphError: если ребро ссылается на несуществующий узел.
        """
        all_keys: set[str] = {
            self._make_key(p.node_type, p.node_name)
            for p in payloads
        }

        for p in payloads:
            source_key = self._make_key(p.node_type, p.node_name)
            for edge in p.edges:
                target_key = self._make_key(
                    edge.target_node_type, edge.target_name,
                )
                if target_key not in all_keys:
                    raise InvalidGraphError(
                        f"Ребро '{edge.edge_type}' из '{source_key}' "
                        f"ссылается на несуществующий узел '{target_key}'. "
                        f"Класс-цель не обнаружен ни одним инспектором."
                    )

    def _phase2_check_acyclicity(
        self, payloads: list[FacetPayload],
    ) -> None:
        """
        Проверка 2d: ацикличность структурных рёбер.

        Создаёт временный граф, добавляет только структурные рёбра
        (is_structural=True) и проверяет ацикличность через
        rustworkx.is_directed_acyclic_graph().

        Информационные рёбра (is_structural=False) не проверяются —
        циклические связи между сущностями допустимы.

        Аргументы:
            payloads: список payload для проверки.

        Исключения:
            InvalidGraphError: если структурные рёбра образуют цикл.
        """
        test_graph: rx.PyDiGraph = rx.PyDiGraph()
        test_index: dict[str, int] = {}

        for p in payloads:
            key = self._make_key(p.node_type, p.node_name)
            idx = test_graph.add_node(key)
            test_index[key] = idx

        has_structural_edges = False
        for p in payloads:
            source_key = self._make_key(p.node_type, p.node_name)
            source_idx = test_index[source_key]
            for edge in p.edges:
                if not edge.is_structural:
                    continue
                target_key = self._make_key(
                    edge.target_node_type, edge.target_name,
                )
                target_idx = test_index[target_key]
                test_graph.add_edge(source_idx, target_idx, edge.edge_type)
                has_structural_edges = True

        if has_structural_edges and not rx.is_directed_acyclic_graph(test_graph):
            raise InvalidGraphError(
                "Структурные рёбра (depends, connection) образуют цикл. "
                "Проверьте зависимости между классами."
            )

    # ═══════════════════════════════════════════════════════════════════
    # Фаза 3 — Коммит
    # ═══════════════════════════════════════════════════════════════════

    def _phase3_commit(self, payloads: list[FacetPayload]) -> None:
        """
        Коммит всех payload в граф.

        Выполняется только если фаза 2 прошла полностью.
        Конвертирует tuple of tuples в dict для хранения в узлах графа.
        Заполняет _node_index и _class_index.

        Аргументы:
            payloads: список провалидированных payload.
        """
        # Добавляем все узлы
        for p in payloads:
            key = self._make_key(p.node_type, p.node_name)
            idx = self._graph.add_node({
                "node_type": p.node_type,
                "name": p.node_name,
                "class_ref": p.node_class,
                "meta": dict(p.node_meta),
            })
            self._node_index[key] = idx

            # Заполняем class_index
            if p.node_class not in self._class_index:
                self._class_index[p.node_class] = []
            self._class_index[p.node_class].append(key)

        # Добавляем все рёбра
        for p in payloads:
            source_key = self._make_key(p.node_type, p.node_name)
            source_idx = self._node_index[source_key]
            for edge in p.edges:
                target_key = self._make_key(
                    edge.target_node_type, edge.target_name,
                )
                target_idx = self._node_index[target_key]
                self._graph.add_edge(source_idx, target_idx, {
                    "edge_type": edge.edge_type,
                    "meta": dict(edge.edge_meta),
                })

    # ═══════════════════════════════════════════════════════════════════
    # Утилиты
    # ═══════════════════════════════════════════════════════════════════

    @staticmethod
    def _make_key(node_type: str, name: str) -> str:
        """
        Формирует уникальный ключ узла: "node_type:name".

        Аргументы:
            node_type: тип узла ("role", "action", "entity" и т.д.).
            name: имя узла ("module.ClassName").

        Возвращает:
            str — ключ вида "role:module.CreateOrderAction".
        """
        return f"{node_type}:{name}"

    # ═══════════════════════════════════════════════════════════════════
    # Публичные свойства
    # ═══════════════════════════════════════════════════════════════════

    @property
    def strict(self) -> bool:
        """Возвращает strict-режим координатора."""
        return self._strict

    @property
    def is_built(self) -> bool:
        """True если build() уже вызван."""
        return self._built

    @property
    def graph_node_count(self) -> int:
        """Количество узлов в графе."""
        return self._graph.num_nodes()

    @property
    def graph_edge_count(self) -> int:
        """Количество рёбер в графе."""
        return self._graph.num_edges()

    # ═══════════════════════════════════════════════════════════════════
    # Чтение графа — низкоуровневые методы
    # ═══════════════════════════════════════════════════════════════════

    def has_node(self, node_type: str, name: str) -> bool:
        """
        Проверяет существование узла в графе.

        Аргументы:
            node_type: тип узла.
            name: имя узла.

        Возвращает:
            True если узел существует.
        """
        return self._make_key(node_type, name) in self._node_index

    def get_node(self, node_type: str, name: str) -> dict[str, Any] | None:
        """
        Возвращает payload узла по типу и имени.

        Аргументы:
            node_type: тип узла.
            name: имя узла.

        Возвращает:
            dict с полями node_type, name, class_ref, meta.
            None если узел не найден.
        """
        key = self._make_key(node_type, name)
        idx = self._node_index.get(key)
        if idx is None:
            return None
        return dict(self._graph[idx])

    def get_nodes_by_type(self, node_type: str) -> list[dict[str, Any]]:
        """
        Возвращает все узлы указанного типа.

        Аргументы:
            node_type: тип узлов для поиска.

        Возвращает:
            Список dict с данными узлов.
        """
        return [
            dict(self._graph[idx])
            for idx in self._graph.node_indices()
            if self._graph[idx].get("node_type") == node_type
        ]

    def get_nodes_for_class(self, cls: type) -> list[dict[str, Any]]:
        """
        Возвращает все узлы графа, порождённые указанным классом.

        Один класс может порождать несколько узлов от разных инспекторов
        (например, "role:...", "action:...", "aspect:...").

        Аргументы:
            cls: класс Python.

        Возвращает:
            Список dict с данными узлов. Пустой список если класс
            не порождал узлов.
        """
        keys = self._class_index.get(cls, [])
        result: list[dict[str, Any]] = []
        for key in keys:
            idx = self._node_index.get(key)
            if idx is not None:
                result.append(dict(self._graph[idx]))
        return result

    def get_node_meta(
        self, cls: type, node_type: str,
    ) -> dict[str, Any] | None:
        """
        Возвращает meta узла указанного типа для класса.

        Ищет среди узлов, порождённых cls, узел с node_type.
        Возвращает его meta как dict.

        Аргументы:
            cls: класс Python.
            node_type: тип искомого узла.

        Возвращает:
            dict с метаданными узла. None если узел не найден.
        """
        name = BaseGateHostInspector._make_node_name(cls)
        node = self.get_node(node_type, name)
        if node is None:
            return None
        return node.get("meta")

    def get_graph(self) -> rx.PyDiGraph:
        """
        Возвращает копию графа.

        Копия — чтобы внешний код не мог модифицировать граф.

        Возвращает:
            rx.PyDiGraph — копия направленного графа.
        """
        return self._graph.copy()

    # ═══════════════════════════════════════════════════════════════════
    # Рантайм-доступ — типизированные методы
    # ═══════════════════════════════════════════════════════════════════

    def get_role_spec(self, cls: type) -> Any:
        """
        Возвращает ролевую спецификацию класса.

        Читает spec из node_meta узла типа "role" для указанного класса.

        Аргументы:
            cls: класс Action.

        Возвращает:
            str | list[str] — спецификация ролей.
            None — если класс не имеет @check_roles.
        """
        meta = self.get_node_meta(cls, "role")
        if meta is None:
            return None
        return meta.get("spec")

    # ═══════════════════════════════════════════════════════════════════
    # Строковое представление
    # ═══════════════════════════════════════════════════════════════════

    def __repr__(self) -> str:
        """Компактное строковое представление для отладки."""
        state = "built" if self._built else "not built"
        inspector_names = ", ".join(i.__name__ for i in self._inspectors)
        return (
            f"GateCoordinator("
            f"state={state}, "
            f"strict={self._strict}, "
            f"inspectors=[{inspector_names}], "
            f"nodes={self.graph_node_count}, "
            f"edges={self.graph_edge_count})"
        )
Copy
Файл 5 из 14.

Пока добавил один рантайм-метод get_role_spec() — остальные (get_regular_aspects, get_summary_aspect, get_checkers_for_aspect и т.д.) добавятся по мере написания соответствующих инспекторов, потому что формат node_meta зависит от инспектора.

Следующий по плану: AspectGateHostInspector или ты хочешь сначала что-то другое?

дальше один файл и переврдисм все на английский
и наверно попутно можно удалять лишние комменатрии при описании методов, особенно приватных. но так, чтобы мы не потерядли документацию