# Opus: архитектура модификаторов (project, полный) — 2026-09-22 Источник: ответ Opus на `prompt_for_opus_modifier_architecture_full.md`. ## Ключевая модель Модификатор — **декларативная проекция подмножества полей родителя**, а не «действие». Отсюда: - один **reconcile** (Create ≡ Update), не два разных пути; - payload всегда **полный по своим полям** (не дельта); - источник истины — родитель; модификатор в state хранит только read-back. --- ## 1. Сравнить и применить — полный payload, не дельта Дельта запрещена: бэкенд трактует отсутствующий параметр как reset-to-default (класс A). Reconcile: 1. взять все `SchemaParams`; 2. заданные пользователем → значение из плана; 3. незаданные → live → default (уже в `operation_run_bycode.go`); 4. drift в Read — сравнение модели с `state_params` родителя (`state_refresh.go`). ## 2. Досылка незаданных (заливы A и B) — канон `CompactParams` в шаблоне + досылка в клиенте — два конца одного бага. Правило по приоритету (уже в `operation_run_bycode.go:98-118`): | Ситуация | Что слать | |---|---| | задан пользователем | значение из плана | | не задан, есть live ParamValue | live | | не задан, нет live, есть DefaultValue | дефолт | | не задан, ничего нет | **пропустить** (не синтезировать) | **Дыра:** `CompactParams` в `modifier.go:92` выкидывает пустые ДО клиента (теряется «задал пусто» vs «не задал»). → Убрать `CompactParams` из шаблона модификатора, передавать map напрямую. Единственная точка решения — клиент. `CompactParams` оставить только для instance-ресурсов. ## 3. Delete / rollback No-op Delete = скрытый drift (класс D). Пока обратный payload не подтверждён — допустимы 3 стратегии через флаг YAML `delete_strategy`: 1. `noop_warn` — удалить из state + `AddWarning` (дефолт для необратимых: `ip_space`); 2. `inverse` — если есть «выключающие» значения в modify (напр. `needEnableAVI:false`); 3. `error` — запретить destroy (`AddError`), если откат критичен. Обратный payload — та же modify с выключающими значениями. Для `ip_space` его нет → только `noop_warn`. ## 4. Idempotency + ID - ID = **идентичность** (родитель + имя модификатора) = `instanceUID:modifierName`. Это правильно и не должен меняться per-apply. opUid в ID **не класть** (иначе replace). opUid — только в лог/приватный state. - **Двойная аллокация (класс E)** защищается не ID, а **идемпотентностью modify**: pre-check «desired == current» → пропустить run. Для `ip_space` перед modify читать `state_params`; если целевое достигнуто — skip. ## 5. Связь с родителем - `_id` — ссылка на родителя (Required, уже так). `depends_on` не нужен — пользователь передаёт UUID. - Borrow state не нужен: Read тянет `state_params` родителя по UUID. - Родителя нет (`ShouldRemoveFromState`) → модификатор удаляется из state (уже есть). ## 6. Create vs Update Единый `reconcile(ctx, plan)`; Create и Update вызывают его (устраняет дубль веток). ## 7. Полный перечень кейсов (13 шт) | # | Кейс | Поведение | |---|---|---| | 1 | create родителя → create модификатора | reconcile, полный payload | | 2 | изменение одного поля | полный payload, соседние не сбрасываются (A) | | 3 | partial params | досылка live→default→skip (B) | | 4 | `integer > 0` без значения/дефолта | пропустить (не слать `"0"`) | | 5 | `is_modifiable:true` (`needEnableAVI`) | не CreateOnly, менять без replace (C) | | 6 | destroy модификатора | по `delete_strategy` (D) | | 7 | replace/taint | reconcile + idempotency pre-check (E) | | 8 | повторный apply без изменений | desired==current → skip | | 9 | родитель удалён | remove из state | | 10 | API не вернул код в state_params | unknown→null (уже) | | 11 | два модификатора разных типов | разные ID | | 12 | operation in progress | waitForInstanceIdle (уже) | | 13 | drift на платформе | Read → план показывает изменение | ## Сводка мест правки | Место | Правка | |---|---| | `modifier.go:92` | убрать `CompactParams` → прямой map (п.2) | | `modifier.go:77` | единый `reconcile()` (п.6) | | `modifier.go:156` | `delete_strategy` (п.3) | | `modifier.go:100` | ID = `instanceUID:modifierName` (п.4) | | `RunOperationByCodeWithTimeout` / reconcile | idempotency pre-check (п.4,7) | | `params.go:116` | учитывать modifier-канал/`is_modifiable` (класс C, кейс 5) | | loader модификаторов | YAML-поля `delete_strategy`, `idempotency` | | `operation_run_bycode.go` | оставить как есть (guard корректен) | ## Открытые вопросы к Opus (не закрыты ответом) 1. **Где брать значения для `inverse`-стратегии Delete?** Для `network` «выключающие» значения — это хардкод per-modifier? Как их задать декларативно в YAML, без хардкода в генераторе? 2. **Формат YAML новых полей.** Точная схема `delete_strategy` и `idempotency`: enum-значения, дефолты, валидация (fail-fast на неизвестных). 3. **Pre-check «desired == current» — где читать current?** Через `RefreshResourceState`/`state_params` или отдельный GET? Как сериализовать сравнение для map-fixed/array-map-fixed (порядок ключей)? 4. **Как пометить модификатор «idempotency: check_before_run» на уровне YAML** (а не хардкодом в коде reconcile)? 5. **Что если желаемое == текущее, но была «частичная» ошибка ранее** — пропускать run безопасно всегда, или есть исключения?