docs: #397 spec revision 2 per SPEC-REVIEW-397-r1

User-Visible: no
Issue: #397
This commit is contained in:
Codex
2026-08-31 02:21:20 +03:00
parent 38b48ee0aa
commit 6edcde012a
+23 -7
View File
@@ -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`):