diff --git a/HISTORY/OPUS/2026-09-22_modifier_architecture_project.md b/HISTORY/OPUS/2026-09-22_modifier_architecture_project.md index 1cd6d4e..eec0e02 100644 --- a/HISTORY/OPUS/2026-09-22_modifier_architecture_project.md +++ b/HISTORY/OPUS/2026-09-22_modifier_architecture_project.md @@ -113,3 +113,81 @@ No-op Delete = скрытый drift (класс D). Пока обратный pa (а не хардкодом в коде reconcile)? 5. **Что если желаемое == текущее, но была «частичная» ошибка ранее** — пропускать run безопасно всегда, или есть исключения? + +--- + +# Ответы Opus №2 (уточнения по 5 вопросам) + +## 1. inverse-Delete — только декларативно в YAML, хардкод запрещён + +Обратный payload зависит от параметров: `needEnableAVI:false` валиден, а +`virtualServicesCount` (`integer > 0`) обнулить нечем → `0` невозможен. +Значит inverse-значения задаются **явным блоком в YAML**. Если хоть один параметр +не имеет валидного inverse — стратегия `inverse` недопустима (fail-fast в загрузчике). +Для `ip_space` inverse нет вообще → только `noop_warn`. + +## 2. Точная схема YAML новых полей + +```yaml +operations: + - kind: modifier + modifier: network + action: modify + delete_strategy: noop_warn # enum: noop_warn | inverse | error + idempotency: check_before_run # enum: none | check_before_run + delete_params: # обязателен ТОЛЬКО при delete_strategy: inverse + - code: needEnableAVI + value: "false" + params: [...] +``` + +Go-контракт (`OperationSpec`): +```go +DeleteStrategy string `yaml:"delete_strategy,omitempty"` // "" → noop_warn +Idempotency string `yaml:"idempotency,omitempty"` // "" → none +DeleteParams []ParamSpec `yaml:"delete_params,omitempty"` +``` + +Дефолты: `delete_strategy` → `noop_warn`; `idempotency` → `none`. +Fail-fast в `ValidateSpec`: значение вне enum → ошибка; `inverse` с пустым +`delete_params` → ошибка; `delete_params.code` нет в `params` → ошибка; inverse-значение +нарушает constraint параметра → ошибка на этапе генерации. +`GenModifier` получает `DeleteStrategy`, `Idempotency`, `DeleteParams`. + +## 3. Откуда читать current + как сравнивать + +**Читать из `state_params`, отдельный GET не делать** (это уже источник истины для Read; +второй источник = риск рассогласования). + +Сравнение по типу: +| Тип | Как сравнивать | +|---|---| +| bool/int/string | равенство после `normalizeUniversalValueV6` | +| map-fixed | `JSONStringsEquivalent` (игнор порядка ключей) | +| array-map-fixed | deep-equal с сохранением **порядка элементов** (порядок значим) | + +Порядок ключей map-fixed — нормализовать (не значим). Порядок элементов +array-map-fixed — НЕ нормализовать (значим). + +## 4. idempotency декларативно + +Поле `idempotency` на modify-операции в YAML → `ValidateSpec` → `GenModifier.Idempotency` +→ шаблон `modifier.go` в `reconcile()` эмитит pre-check `{{- if eq .Idempotency "check_before_run" }}`. +Для `ip_space` — в YAML; для остальных — дефолт `none`. + +## 5. Когда безопасно skip run при desired == current (НЕ всегда) + +Три условия безопасного skip: +1. **Инстанс idle** — если pending/in-progress, сначала `waitForInstanceIdle`, потом + перечитать `state_params` (иначе mid-flight аллокация даст ложное «уже равно»). +2. **current из живого state_params, НЕ из TF-state** — после частичной ошибки TF-state + может врать, а state_params отражает реальную платформу. +3. **Сравнение по всем полям, не по одному** — skip только при совпадении ВСЕХ полей. + +Итог: +``` +idle? нет → wait, re-read +всё-live == всё-desired? да → skip run +иначе → reconcile (полный payload) +``` +