fix(core): idempotency pre-check без схемы — единый источник (live)

Код-ревью (раунд 5), п.1/6: pre-check брал схему из GET /instanceOperations/default/{opId},
а payload строился по живой схеме ?fields=cfsParams — два источника. default может
расходиться с живой => риск ложного пропуска modify.

Решение: pre-check вообще не запрашивает схему.
- modifierDesiredEqualsLive(ctx, uid, desired): сравнение по live-кодам
  (live[lower(code)]), единственный источник — state.params.
- Значения: похожи на JSON ({/[) — смысловое сравнение; иначе скалярное с
  нормализацией (null/"" -> ""; true/false без учёта регистра) — закрывает и
  регистр bool.
- Fail-safe сохранён: пусто/нет кода/ошибка live => modify выполняется.
- Удалены: fetchOperationSchemaByID, modifierValuesEqual, lookupLiveParam больше
  не участвует в pre-check (остаётся для досылки).
- Тесты переписаны (RawValuesEqual_Scalars/JSON, DesiredEqualsLive: 4 кейса).

Документация: TOOLS/ARCHITECTURE.md -> новый раздел «Modifier Idempotency»
(5 правил контракта); HISTORY — журнал раунда 5.

Проверено: go build ./... OK; go test ./internal/... -short PASS.
This commit is contained in:
Repinoid
2026-09-30 21:07:49 +03:00
parent 4197a76aba
commit 99f963484b
7 changed files with 131 additions and 87 deletions
@@ -106,4 +106,39 @@ Required с пустым `data_type` (все получают `""`; угадыв
**Осталось:** замер владельцем (`terraform plan` ×2 на `DEV_STAND/FullPipe`); B8 (`modifiers.yaml`).
---
## Раунд 5 — код-ревью (Opus) и его фиксы
Промпт: `NOTES/20_prompts/prompt_for_opus_code_review_round5.md`. Opus нашёл 6 пунктов; критичные — 2.
| # | Находка | Решение |
|---|---|---|
| 1 | **Регрессия B6:** pre-check стоял ПОСЛЕ `POST /instanceOperations` → при совпадении оставался «черновик» операции (pending). | Исправлено: pre-check до создания операции. |
| 2 | Partial state: `resp.State.Set(&data)` мог записать unknown/computed → «invalid new value … unknown». | Исправлено: пишется только `id` (`SetAttribute`). |
| 3 | 401 ретраится, но токен между попытками не обновляется. | Принято как есть; пояснено в комментарии `http.go`. |
| 4 | `ImportState` vs `Read` — конфликта нет. | ок. |
| 5 | `modifierDesiredEqualsCurrent` — мёртвый код (только тесты). | Удалено; тесты переведены на живые функции. |
| 6 | Сравнение по `default`-схеме рискует ложным пропуском (ревизия п.1). | Исправлено: pre-check **без схемы** — только live-коды. |
**Итоговый контракт pre-check (см. `TOOLS/ARCHITECTURE.md` → «Modifier Idempotency»):**
1. pre-check выполняется **до** `POST /instanceOperations`;
2. единственный источник — live (`state.params`), схема операции не запрашивается
(`default/{opId}` может расходиться с живой; живая доступна только после создания);
3. поиск значения — по самому коду (`live[lower(code)]`);
4. fail-safe: пусто/нет кода/ошибка live → modify выполняется;
5. сравнение: похоже на JSON — смысловое (порядок ключей не значим), иначе — скалярное
с нормализацией (`null`/`""` → `""`; `true`/`false` без учёта регистра).
Файлы: `core/modifier_compare.go` (новые `modifierRawValuesEqual`, `looksLikeJSON`,
`normalizeRawScalar`, `modifierDesiredEqualsLive` без схемы), `core/operation_run_bycode.go`,
`core/operation_cfs.go` (удалена `fetchOperationSchemaByID`), `core/client_test.go`,
`core/modifier_compare_test.go`, `TOOLS/ARCHITECTURE.md`.
Тесты: `TestRunInstanceOperationByIdempotent_SkipsWithoutCreatingOperation` (POST операции не
вызывается при совпадении), `TestModifierRawValuesEqual_Scalars/JSON`,
`TestModifierDesiredEqualsLive` (совпало / отличается / нет кода / live недоступен).
Проверено: `go build ./...` OK, `go test ./internal/... -short` PASS.