docs(opus): раунд 2 отвечен + вводная пользователя — запись в журнал диалога

Дословно: сырой лог и дельта Opus (M1 сверка номеров строк, M2 факт из check_hardcoded_service_ids.sh,
M3 снятие S4 и переклассификация S2, M4 шкала R2>R3>R6>R5, M5 разбор Read/вечного diff,
U1 modifiers.yaml не существует, U2 тройной словарь жизненного цикла, U3 варианты, M6 запрос двух файлов).
Плюс указание пользователя: модификаторы — небольшая часть, главное — сам провайдер.
This commit is contained in:
Repinoid
2026-09-30 20:07:34 +03:00
parent ea75507fe4
commit e1e45423fa
@@ -544,12 +544,172 @@ apply-ами модификатора и вернуть поле назад (п
--- ---
## Ход 9 — Ответ Opus на раунд 2 (дословно, как получен от пользователя)
### Сырой лог сессии
```
Read
Раунд 2. Перепроверю номера строк и прочту новые разрешённые файлы.
Read , lines 193 to 195
Read
Read , lines 178 to 195
Searched for regex `ResolveRefSvcParamValue\(ctx, 19|noNeededIpSpace|func \(r \*|len\(items\) == 0|RunInstanceOperationUniversalByCode|liveItems, parseErr|if raw, ok := live\["ipSpaceName"\]|func parseVipConfigure|func formatVipConfigure|func vipAllocationsEqual` (`**/provider/internal/resources_core/org_ip_allocation_resource.go`), 21 results
Searched for regex `noNeededIpSpace|func \(r \*NsxtSnat|if raw, ok := live\["ipSpaceName"\]|func \(r \*NsxtSnatResource\) setSnat|RunInstanceOperationUniversalByCode|ImportState` (`**/provider/internal/resources_core/nsxt_snat_resource.go`), 18 results
Прочитаю новые разрешённые файлы: шаблоны генератора, helpers, params_compare, state_refresh.
Read
Read
Прочитаю helpers.go, state_refresh.go, params_compare.go для проверки поведения `ParamFormat`/refresh.
Read
Проверю точные номера строк в `ARCHITECTURE.md` для опоры S2/S4.
Searched for regex `must not contain service-specific|fully generated from YAML|No manual edits|no service-specific logic inside the core|modify всегда через WithDefaults|serviceSpecificModifiers|exactly two named registries` (`**/TOOLS/ARCHITECTURE.md`), 7 results
Searched for regex `for _, param := range cfsParams|func \(c \*UniversalClient\) RunInstanceOperationUniversalByCode|RunInstanceOperationUniversalByIdempotent|val, hasLive := lookupLiveParam` (`**/provider/internal/core/operation_run_bycode.go`), 5 results
Read , lines 174 to 193
Дельта по раунду 2.
```
### Дельта (дословно)
```markdown
## M1 — перепроверенные `файл:строка`
| Ссылка раунда 1 | Было | Факт (сверено) |
|---|---|---|
| Регистрация модификаторов | `provider.go:199-204` | `provider.go:182` — `func Resources()`; регистрация — `provider.go:186` и `provider.go:187` |
| Удалённый реестр в генераторе | `yaml-generator/main.go:70-90` | `main.go:80-90` (комментарий про снятый `serviceSpecificModifiers`) |
| hardcoded id 19 | `~300` | `org_ip_allocation_resource.go:308` |
| `noNeededIpSpace` | `43` | const — `nsxt_snat_resource.go:44`; `setSnat` — `nsxt_snat_resource.go:234`; inverse-modify — `nsxt_snat_resource.go:199-200` |
| Досылка дефолтов ByCode | `105-150` | `operation_run_bycode.go:112-150`; `ByCode` — `operation_run_bycode.go:11`, `ByIdempotent` — `operation_run_bycode.go:18` |
| `len(items)==0` → error | `328-331` | `org_ip_allocation_resource.go:333` |
| GenModifier-конвейер | `55-90` | `loader.go:57-86` (`op.Kind=="modifier"`) |
Прочее сверено: org_ip `ImportState` `org_ip_allocation_resource.go:290`; nsxt `ImportState` `nsxt_snat_resource.go:224`; nsxt `Read` overwrite `nsxt_snat_resource.go:152`; org_ip `Read` semantic-compare `org_ip_allocation_resource.go:177-188`.
## M2 — R2 переформулирован как ФАКТ (скрипт прочитан)
`check_hardcoded_service_ids.sh:15`: `grep -rnE '\.(ServiceID|ID)[[:space:]]*==[[:space:]]*[1-9][0-9]*' "$ROOT_DIR/TOOLS"`.
Два проверяемых факта:
1. **Область — только `TOOLS`.** Дерево `provider` не сканируется вовсе → ручные модификаторы в `resources_core` вне охвата стража по определению.
2. **Паттерн ловит только `.ServiceID==N` / `.ID==N`.** Литерал-аргумент `org_ip_allocation_resource.go:308` (`ResolveRefSvcParamValue(ctx, 19, …)`) под паттерн не подпадает даже теоретически.
Итог R2: hardcoded 19/22/`"no-needed"` не покрыты стражем по **двум** причинам (область + паттерн). Догадки убраны.
## M3 — S2/S4 пересмотрены
**S4 — СНИМАЮ.** Опора `ARCHITECTURE.md:134` находится в разделе «Generated Code Resilience» и относится к `Update` генерируемых инстанс-ресурсов, а не к ручному модификатору. К модификаторам правило неприменимо → расхождения нет.
**S2 — переклассифицирую в ПРОБЕЛ СПЕКИ (не «нарушение No manual edits»).**
Опоры: `ARCHITECTURE.md:13` и `ARCHITECTURE.md:213` говорят про *generated* Go — к ручным файлам не относятся (замечание верное). Но `ARCHITECTURE.md:12` («core … must not contain service-specific logic») и `ARCHITECTURE.md:110` («no service-specific logic inside the core») сформулированы про «core» без оговорок. Ручные модификаторы с зашитыми id 19/22 лежат в `resources_core` (`org_ip_allocation_resource.go:308`, `nsxt_snat_resource.go:44`). Спека **не содержит категории «ручной сервисный оверлей»** и не говорит, является ли `resources_core` частью «core». Поэтому S2 — пробел контракта (нет разрешённого места для такого кода), а по букве принципа 2 — пограничное противоречие. Не «нарушение No manual edits».
## M4 — шкала ранжирования и пересчёт
Шкала: **severity = вероятность × тяжесть_последствия × (1/обнаружимость)**. «Тихие» дефекты (низкая обнаружимость) поднимаются; то, что сразу видно в плане/диффе — опускается.
- **R2** — вер. высокая (любое добавление сервис-логики), тяжесть средняя (эрозия инварианта «ядро универсально»), обнаружимость низкая (страж молчит) → **верх**.
- **R3** — вер. средняя (забытый `depends_on`), тяжесть средняя (непонятная ошибка платформы), обнаружимость средняя → **середина**.
- **R6** — вер. средняя (импорт редок, но реален), тяжесть низкая, обнаружимость средняя → **ниже R3**.
- **R5** — «корректности не нарушает» (моё же слово), тяжесть минимальная, обнаружимость высокая → **низ**.
Новый порядок: **R2 > R3 > R6 > R5**. R5 понижен ниже R6 (замечание принято).
## M5 — устойчивость `Read` и вечный diff (главный разбор)
**org_ip, путь значения `vip_configure`:**
- Конфиг: `jsonencode([{name,count}])` → ключи по алфавиту (`count`,`name`); `count` — тип из `var.ip_count` (в `modifiers.tf:24` не квотирован → зависит от типа переменной).
- Канон провайдера `org_ip_allocation_resource.go:385`: `name` первым, `count` — всегда строка.
- Эти две формы **байт-различны** (порядок ключей; возможно число vs строка).
- Защита от вечного diff — `org_ip_allocation_resource.go:177-188`: сравнение **смысловое** (`org_ip_allocation_resource.go:398`, игнорирует порядок и формат), и при совпадении state **не перезаписывается** → в state остаётся байт-в-байт конфиг-форма → `plan` чист. `count`-число нормализуется в строку в `org_ip_allocation_resource.go:351` до сравнения, поэтому «3» (число) и «"3"» считаются равными. `null`/пустые/`[{}]` отбрасываются там же → не порождают фантомную аллокацию.
- Перезапись (реальный дрейф) даёт канон-форму (name-first) — она никогда не совпадёт байтово с `jsonencode`, но diff тогда **легитимен**; ближайший `Update` ставит `state=plan` (`org_ip_allocation_resource.go:119`) и вечного diff не создаёт.
**nsxt_snat, путь `ip_space_name`:** скаляр-строка. `nsxt_snat_resource.go:152` перезаписывает state только если live-значение непустое; иначе оставляет план (защита Required-атрибута от null). Форматных расхождений нет (простое имя) → вечного diff нет.
**`keep_on_destroy` в плане:** Optional+Computed, `Default=false` (`org_ip_allocation_resource.go:84`, `nsxt_snat_resource.go:78`). `Read` читает его из state в начале и не трогает (наружу его в API нет) → после первого apply стабилен, `plan` чист. В `modifiers.tf:30` задан `true` явно — diff отсутствует.
**Вывод M5-основной:** сами модификаторы вечного diff **не дают** — смысловое сравнение в `Read` его гасит.
**Временной сценарий R1 (инстанс-ресурс возвращает поле назад):**
Шаблон instance.go Update строит `params` из **всех** `ModifyParams` безусловно (не пропускает null для не-nested), и вызывает `UpdateResourceWithTimeout` → `RunInstanceOperationUniversalWithDefaults`. Гейт — `hasServiceParamChanges` (тот же шаблон): modify запускается, если изменился **любой** modify-параметр.
- `vc_org`: modify содержит **только** `vIPConfigure` (`19_vc_org.yaml`, op modify) → `nubes_vc_org` тронет поле, лишь если пользователь сам задал `vIPConfigure` на инстанс-ресурсе. Риск ниже.
- `vc_nsxt`: modify содержит `needEnableAVI`, `virtualServicesCount`, `ipSpaceName`, `qosProfile`, `routedNetConfiguration` (`22_vc_nsxt.yaml`). Сценарий: (t1) модификатор включил SNAT → (t2) пользователь на `nubes_vc_nsxt` меняет `needEnableAVI` → `hasServiceParamChanges=true` → `params[372]=ParamFormat(plan.IpSpaceName)` (запись присутствует всегда) → (t3) следующий `Read` модификатора видит дрейф и на очередном apply восстанавливает. Две сущности «пинают» поле по очереди.
**Не сверено** (нужны файлы вне §4): пошлёт ли `RunInstanceOperationUniversalWithDefaults` пустой `ipSpaceName` как `""` (затрёт SNAT) или дособерёт из live (как ByCode). Это решает, «затирание» или «no-op». Файлы: `core/operation_*` с реализацией `WithDefaults` и генератор-хелпер `ParamFormat` (funcs.go) для поведения null→"". См. M6.
## U1 — открытый вопрос №1 переформулирован
`modifiers.yaml` в репозитории **нет** — только упоминания-комментарии (`main.go:84-90`). Генерируемый слой модификаторов при этом **реален и готов**: шаблон `modifier.go` (полный CRUD + `reconcile` + `delete_strategy` + `idempotency`) и конвейер `loader.go:57-86`. То есть это **задокументированное-но-несозданное наложение**: механизм есть, данных для него нет. Вопрос: создавать `modifiers.yaml`-оверлей (данные) и перевести org_ip/nsxt_snat на генерацию — или узаконить ручные ресурсы отдельной категорией в спеке?
## U2 — рассинхронизация словаря жизненного цикла (архитектурный дефект)
Три несогласованных словаря «что делать при destroy», в разных ветках кода:
1. Генерируемый инстанс: `suspend_on_destroy` + `keep_on_destroy` (runtime-булевы, приоритет keep) — instance.go schema и `Delete` (ветки `suspend`/`state_only`).
2. Генерируемый модификатор: `delete_strategy` = `noop_warn`/`inverse`/`error` — **compile-time из YAML**, без `keep_on_destroy` — modifier.go Delete.
3. Ручной модификатор: снова `keep_on_destroy` (runtime) + ручной inverse — `org_ip_allocation_resource.go:84`, `org_ip_allocation_resource.go:195`; `nsxt_snat_resource.go:78`.
Дефект: один смысл («не трогать эффект при destroy / обратный откат») выражен тремя разными контрактами (runtime-флаг vs compile-time-стратегия), причём генерируемый и ручной модификаторы, решая одну задачу, используют **противоположные** механизмы. Нужен единый словарь жизненного цикла для всех трёх видов ресурсов.
## U3 — корневая причина «ручных» модификаторов (по вариантам, одной строкой)
- **Оверлей-данные (`modifiers.yaml` + второй проход):** доменное знание (сервис, операция, имя, inverse, idempotency) — декларативно; org_ip/nsxt_snat становятся генерируемыми; убирает S2/U2/R2 разом; цена — доделать загрузчик оверлея и миграцию.
- **Ручной Go-код (статус-кво):** максимум гибкости под нестандартную семантику (replace-массив, `no-needed`); цена — сервис-логика в `resources_core`, обход стража, тройной словарь.
- **Иное (гибрид):** генерировать каркас из оверлея, а нетиповые куски (канон vIPConfigure) выносить в маленькие data-хуки; цена — усложнение шаблона ради 1-2 сервисов.
## M6 — где нужна глубина (прошу доступ)
Для однозначного вердикта по временному R1 (затирание vs досбор) не хватает **двух** файлов вне §4:
1. Реализация `RunInstanceOperationUniversalWithDefaults` (ядро, `core/operation_run*.go` / `operation_cfs.go`) — шлёт ли пустой `ipSpaceName` как `""`.
2. Генератор-хелпер `ParamFormat` (`TOOLS/resource-generator/internal/.../funcs.go`) — во что разворачивается null-строка в `params`.
Разрешить чтение этих двух файлов? Без них пункт помечен «не сверено».
```
---
## Ход 10 — Инструкция и пояснение пользователя (дословно)
```
#attachment:Pasted text #1
докуменитируй сразу
пояснияю - модификаторы - модет и слодная, новая, но НЕБОЛЬШАЯ часть всего нашего сервиса
не нужно делать на них чрезмерный упор
главное - САМ провайдер, его устойчивость и правильность и тд
```
Разбор указания (для дальнейших раундов):
- **Документировать немедленно** — исполнено этим разделом.
- **Приоритет — САМ провайдер**: его устойчивость и корректность. Модификаторы — новая и, возможно,
сложная, но **НЕБОЛЬШАЯ** часть сервиса; чрезмерный упор на них делать не нужно.
- Следствие: темы `M5`/`M6`/`U2` (глубина по модификаторам) — второстепенны по отношению к вопросам
устойчивости/корректности ядра, генерации и всего провайдера в целом.
---
## Статус ## Статус
- Раунд 1 завершён (отчёт Opus получен), **раунд 2 отправлен**: замечания M1–M5, U1–U3, разрешение - Раунд 1: отчёт получен. Раунд 2: отправлен и **отвечен** (M1–M6, U1–U3).
копать глубже. Ответ Opus на раунд 2 ещё НЕ получен. - **Открытый вопрос Opus (M6)**: просит разрешить чтение двух файлов —
- Открытый вопрос Opus №1 (про `modifiers.yaml`) остаётся, но переформулируется в раунде 2 (см. U1). реализация `RunInstanceOperationUniversalWithDefaults` (ядро) и хелпер `ParamFormat` (`funcs.go`).
- Артефакты: коммит `752244f` — файл промпта раунда 1; Решение по доступу НЕ принято.
- **Новая вводная пользователя**: не делать чрезмерный упор на модификаторах; главное — сам провайдер
(устойчивость, корректность). Учитывать при формировании следующих раундов.
- Артефакты: коммит `752244f` — промпт раунда 1;
`NOTES/20_prompts/prompt_for_opus_provider_architecture_modifiers_round2.md` — замечания раунда 2. `NOTES/20_prompts/prompt_for_opus_provider_architecture_modifiers_round2.md` — замечания раунда 2.
- Настоящий документ ведётся по прямому указанию пользователя «документируй ВЕСЬ диалог, - Настоящий документ ведётся по прямому указанию пользователя «документируй ВЕСЬ диалог,
без сокращений». без сокращений».