tz-design — Фаза 2: Декомпозиция данных
Часть skill'а tz-design. Условия запуска и условие пропуска Фазы 2 — в SKILL.md. Этот файл читается целиком перед запуском Фазы 2.
Алгоритм
-
Перечислить все участвующие сущности — существующие (затрагиваемые), новые (создаваемые), модифицируемые. Включая на первый взгляд нелогичные (audit_log, news, system_flags, observer-точки).
-
Декомпозировать каждую до атомарной модели.
Атомарная = все поля являются одним из:
- примитивы (int, string, decimal, bool, datetime, json со схемой);
- коллекции примитивов (массив строк, JSON-список с описанной структурой);
- foreign key с явной семантикой связи.
Если поле — составной объект, который сам по себе имеет жизненный цикл/инварианты → выделить в отдельную сущность, продолжить декомпозицию.
Стоп-условие: все поля атомарны.
-
Re-check самодостаточности. Для каждой атомарной модели — вопрос:
«Достаточно ли этой модели одной для полного описания доменного смысла, или нужны данные извне?»
Если самодостаточна → отметить явно «доменный смысл полный».
Если нужны данные извне → обязательно указать достраивающую зависимость одним из:
- Поглощение/денормализация: «копируем поле X из сущности Y в момент Z, потому что …»
- Явная FK с семантикой: «
field_id→OtherEntity. Семантика: <зачем эта связь, не просто "связан с">» - Документированный путь резолвции: «доступ только через
Service::method()/Model::scope()— обходные пути запрещены» - Контракт со-существования: «сущности X, Y, Z создаются/удаляются вместе, инвариант проверяется в
<место>(validateFond / observer / DB::transaction)»
Без явного выбора — модель помечена «архитектурно недозавершена», переход в Фазу 3 невозможен.
Decision helper для самодостаточности. Если пользователь подтверждает несамодостаточность, но не понимает какой из 4 типов выбрать — активируй вопросник:
Помогу выбрать тип достраивающей зависимости. Ответь на 1–2 вопроса: Q1: Нужна ли историческая фиксация значения (snapshot, актуального на момент создания связи)? — Да → Поглощение/денормализация. Копия живёт независимо от источника, защищена от будущих изменений (пример: commission_percent в заявке на вывод хранится как копия из настроек, чтобы редактирование настроек не меняло задним числом сумму комиссии). — Нет → переход к Q2. Q2: В каком отношении сущности с зависимостью? — Зависимость существует независимо, может быть удалена/изменена → Явная FK с семантикой (типовой случай user → resource). — Сущности создаются всегда вместе в одной транзакции, по отдельности невалидны → Контракт со-существования (пример: 3 fond_situation при processWithdraw). — Зависимость не хранится напрямую, резолвляется через бизнес-логику (scope / service / computed property) → Документированный путь резолвции (пример: hedge.balance через ledger SUM).Триггеры активации helper'а: явная просьба пользователя («не понимаю», «помоги выбрать», «какой подходит») ИЛИ повторное «несамодостаточна, не знаю» подряд.
Auto-выбор зависимости запрещён даже после helper'а — пользователь обязан явно ответить Q1/Q2.
-
Re-check реестровой модели. Для каждой сущности с коллекцией экземпляров — вопрос:
«Несёт ли коллекция собственный доменный смысл помимо хранилища?»
Реестр выделяется в отдельную сущность (свой контроллер, scope'ы, бизнес-методы), если выполнено хотя бы одно:
- собственное состояние коллекции (текущий активный, версия, счётчик, метаданные коллекции);
- доменные операции над коллекцией, не сводимые к стандартному Eloquent API («синхронизировать все», «выбрать тот, что подходит по правилу»);
- несколько потребителей с единообразным API/контрактом.
Если нет — это просто
hasManyот владельца, без отдельной модели управления. -
Cross-cutting инварианты. Формулы и правила, связывающие несколько атомарных моделей. Например:
fond_situation.summ + hedge_ledger.balance + open_options = real_Bybit_total- «Любая
WithdrawalRequestимеет ровно 3 связанныхfond_situationс конкретными source-значениями».
Для каждого инварианта: где проверяется (
validateFond/ observer / тест / DB constraint). -
Анализ пробелов. По каждому полю/связи:
- Кто отвечает за заполнение? (если никто — пробел)
- Что произойдёт при NULL?
- Что произойдёт при удалении связанной сущности (CASCADE / RESTRICT / SET NULL)?
- Как мигрируются существующие записи при добавлении нового поля?
Формат карточки атомарной модели
### <Имя сущности>
- **Назначение:** одна строка.
- **Поля:** список с типами и обоснованием (зачем нужно, кто пишет, кто читает).
- **Жизненный цикл:** создание, переходы, удаление/архивация.
- **Самодостаточность:** ✅ полный доменный смысл ИЛИ
⚠️ Достраивающая зависимость — <тип>: <конкретное описание>
- **Реестровая обёртка:** не нужна ИЛИ
Выделена как `<RegistryModel>` с обоснованием: <какой критерий сработал>
- **Источники мутации:** контроллер/сервис/observer/миграция/admin-UI.
- **Источники чтения:** UI/API/отчёт/другой сервис.
- **Участие в инвариантах:** список cross-cutting инвариантов (раздел 5).
- **Поведение при NULL ключевых полей:** конкретно.
- **CASCADE при удалении связанных:** RESTRICT / SET NULL / CASCADE и почему.
Формат output
Вывести карточки атомарных моделей блоками. Между блоками — вопросы по каждому:
- «Декомпозиция до атомарного уровня — корректна? Есть ли поля, которые я не разбил, хотя надо было?»
- «Самодостаточность блока X — выбранный способ выражения зависимости (поглощение/FK/путь/контракт) — правильный?»
- «Реестровая обёртка для коллекции Y — нужна или избыточна?»
Не вываливать все блоки разом без вопросов. Получать подтверждение по каждому.
Условие выхода из Фазы 2
Пользователь подтвердил состав, самодостаточность, реестровый статус, инварианты и lifecycle по всем блокам. Ни одной модели в статусе «архитектурно недозавершена».