> ⛔ **ЛОЖНЫЙ ПУТЬ — ОТМЕНЕНО (пометка 2026-09-24). ТАК ДЕЛАТЬ НЕЛЬЗЯ.** > Ревью плана отменённого захода (`kind: modifier` в YAML + реестр в генераторе). История. > Актуально: `NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md`. # Opus: ревью плана редизайна модификаторов — 2026-09-22 Источник: ответ на `prompt_for_opus_modifier_plan_review.md` (план `PLAN_modifier_redesign.md`). ## 1. Порядок шагов — скрытые зависимости - Шаг 4 (шаблон) ссылается на API из шагов 6–7 → **сначала 5→6→7, потом 4**. - Шаг 8 (yaml-generator) должен идти ДО регенерации `dev` и до сборки. Скорректированный порядок: 1 → 2 → 3 → 5 → 6 → 7 → 4 → 8 → регенерация → 9 → 10. ## 2. Шаг 5 (вынос JSON-эквивалентности) Путь верен. **Оставить реэкспорт-обёртку `JSONStringsEquivalent` в `json_normalize.go`**, не заменять вызовы по всему resources_core (иначе диф на инстансы, вопрос 7). Переносятся самодостаточные 5 функций: `JSONStringsEquivalent`, `normalizeJSONIfPossible`, `encodeCanonicalJSON`, `writeCanonicalJSON`, `normalizeJSONScalarsToStrings`. Вариант «готовые строки в core» — отклонить (размазывает нормализацию, не снимает потребность в JSONStringsEquivalent в core). ## 3. Шаг 6 — сигнатура и сравнение - Маппинг code→param по **двум** алиасам: `p.Code` И `p.SvcOperationCfsParam` (как в operation_run_bycode.go:50-58). Один `Code` даст пропуски. - Имя `modifierDesiredEqualsCurrent` — unexported, вызов внутри core. Слово «экспортированный» убрать. - bool/int/string — `normalizeUniversalValueV6` + сравнение. map-fixed — `JSONStringsEquivalent`. - **array-map-fixed — дыра:** `normalizeUniversalValueV6` строит дефолт только для `map-fixed`/`HasPrefix "map"` (params.go:33); `array-map-fixed` туда не попадает → сравнивать сырые значения через `jsonutil.JSONStringsEquivalent`, не через normalize. - desired = только явно заданные коды (до досылки live/default), иначе pre-check всегда «равно». ## 4. Шаг 4.4 Delete=inverse — подводный камень - Delete не имеет `plan` (только `req.State`). `reconcile(ctx, plan *Model)` не подходит. → `reconcile(ctx, model *Model, override map[string]string)`; для inverse override = delete_params. - `deleteParams` — финальные **wire-строки** (`"false"`, готовый JSON), БЕЗ прогонки через `ParamFormat`/тип. В реестре `deleteParam{Code, Value}` несёт готовую строку. ## 5. Шаг 8 — расширение реестра Верно. Держать в `serviceSpecificModifiers` (main.go:34), не отдельным реестром. Структура `modifierException` корректна. При переходе со `map[string]string` на структуру: `ModifierName` берётся из структуры (сейчас `modName, ok := serviceSpecificModifiers[name]` — строка 95). ## 6. Пропущенные кейсы - **taint/replace + `delete_strategy=error`** — конфликт: replace = Delete→Create, Delete=error блокирует → пользователь не сможет заменить error-модификатор. Решить явно: запретить replace у error (документировать) или отличить «чистый destroy» от replace. - **unknown в pre-check** — при unknown (computed ref) сравнение невозможно; шаг 6 должен skip-ить pre-check при unknown (иначе пустая строка даст ложный diff/панику). - **partial apply** — досылка live для незаданных + pre-check; проверить кейс «часть задана, часть live». ## 7. Риск сломать инстансы Низкий при условиях: - **НЕ удалять `CompactParams`** (helpers.go:68) — убирается только из modifier-шаблона; функция нужна инстанс/action. - **Шаг 7 — новый метод `RunOperationByCodeIdempotent`, НЕ менять сигнатуру** `RunOperationByCodeWithTimeout`/`RunInstanceOperationUniversalByCode` (зовут инстансы). - Шаг 1 (поля OperationSpec) аддитивен — безопасно. ## Дополнительно (не в вопросах) - **Шаг 4.3 (ID=identity) — ломающая миграция state.** Смена формата ID изменит ID уже задеплоенных модификаторов → Terraform форснёт replace. Нужно: либо сохранить старый формат ID, либо явный state-migration plan. В плане не отмечено. - **Шаг 3 — `normalizeDeleteStrategy`/`normalizeIdempotency`** — где живут (в loader.go, рядом с веткой modifier). Не указано. - **ValidateSpec** — проверка `delete_params.code ∈ op.Params` по lower-code; сверить поле `Code`.