diff --git a/docs/specs/397-device-position-echo.md b/docs/specs/397-device-position-echo.md index 7949aa63..7e229446 100755 --- a/docs/specs/397-device-position-echo.md +++ b/docs/specs/397-device-position-echo.md @@ -3,7 +3,7 @@ - Issue: https://github.com/Matysh/houseplan-card/issues/397 - Приоритет: P1, bug; полный трек — класс A (продуктовый код) плюс правка подстроенного доказательства AC10 ТЗ #74 -- Ревизия: 1 (2026-08-31) +- Ревизия: 2 (2026-08-31) — по SPEC-REVIEW-397-r1 (Medium: доказательства AC5/AC7) ## Сценарий @@ -25,7 +25,7 @@ Undo/Redo молча гаснут — история очищена, хотя м ### (1) B3 — канонический ответ не возвращается в локальный layout -`_persistDevicePlacement` (`src/houseplan-card.ts:5226-5258`) отправляет на +`_persistDevicePlacement` (`src/houseplan-card.ts:5226-5261`) отправляет на сервер `canonicalizePosition(this._layout[deviceId])`, но сам `_layout` оставляет как есть, а следом (`:5253`) фиксирует `_layoutContentFingerprint = contentFingerprint(this._layout)` — по @@ -112,18 +112,30 @@ out.sameContentReloadKeepsHistory = c._devicePositionHistory.canUndo; получил) `canUndo` остаётся `true`. Доказательство: смок с честной фикстурой. - **AC3**. Тот же инвариант для `_adoptStructuralResponses` (полная - перезагрузка конфига и layout, reconnect-путь). + перезагрузка конфига и layout, reconnect-путь). Доказательство: смок, + пункт 5 плана — перемещение, затем полная перезагрузка с честным серверным + слепком, `canUndo` остаётся `true`. - **AC4**. Чужое изменение по-прежнему чистит историю: сервер отвечает позицией, отличной от отправленной, → `canUndo === false`, `_layout` заменён. Доказательство: тот же смок, обратная ветка. -- **AC5**. Удаление позиции (`placement === null`) подчиняется AC1–AC4: - локальный ключ удалён до фиксации отпечатка, эхо удаления историю не чистит. +- **AC5**. Удаление позиции (`placement === null`) подчиняется AC1–AC4 в обеих + половинах. (а) Локальный ключ удалён до фиксации отпечатка — доказательство: + юнит, пункт 2 плана. (б) Эхо удаления не чистит историю — доказательство: + смок, пункт 7 плана: удаление позиции, затем честный reload, `canUndo` + остаётся `true`. Вторая половина отдельная не для симметрии: удаление идёт + другой веткой (`houseplan/layout/delete`, `pending = null`), и запись в + `_layout` там снимает ключ, а не заменяет значение — доказательство первой + половины про вторую ничего не говорит. - **AC6**. Смок AC10 краснеет на коде до фикса (1). Доказательство: прогон переписанного смока против `git stash` реализации, зафиксирован в документе ревью. - **AC7**. Позиции, отправленные и ещё не подтверждённые (`_sentPos`), - продолжают побеждать ответ сервера при слиянии — поведение `_reloadLayoutOnly` - в этой части не меняется. + продолжают побеждать ответ сервера при слиянии — поведение + `_reloadLayoutOnly` в этой части не меняется. Доказательство: смок, пункт 8 + плана — пока запись «в полёте», сервер отвечает СТАРОЙ позицией, и после + слияния в `_layout` остаётся отправленная, а не серверная. Инвариант назван + явно, потому что фикс правит соседний код (порядок записи в `_layout` и + регистрации в `_sentPos`): без своего теста он ломается молча. ## План автотестов @@ -142,6 +154,10 @@ out.sameContentReloadKeepsHistory = c._devicePositionHistory.canUndo; 5. Перемещение → полный `_adoptStructuralResponses` (reconnect) → `canUndo === true` (AC3). 6. Сервер отвечает другой позицией → история очищена, `_layout` заменён (AC4). +7. Удаление позиции → честный reload → `canUndo` остаётся `true`, ключ не + возвращается (AC5б). +8. Запись «в полёте» (`_sentPos` не пуст), сервер отвечает старой позицией → + после слияния в `_layout` отправленная позиция, история цела (AC7). **Мутант** (`scripts/mutation-gate.mjs`):