mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
committed by
Sergey Matyunin
parent
2c6d937d46
commit
bf40e26ed0
@@ -0,0 +1,250 @@
|
||||
# SPEC-REVIEW-405-r1
|
||||
|
||||
- Issue: https://github.com/Matysh/houseplan-card/issues/405
|
||||
- ТЗ: `docs/specs/405-dropped-promise-and-witness-floor.md`
|
||||
- SHA ТЗ: `cd0c85b9ad5096d4772f99edc0354dea6a62f241`
|
||||
- Заход: r1 · блокирующих циклов израсходовано 0 из 4 (до этого раунда)
|
||||
- Трек: полный (заявлен автором — две несвязанные поверхности, `src/**` и
|
||||
`scripts/**`; критерий `small` «одна поверхность» не выполняется — согласен)
|
||||
- Вердикт: **жёлтый**
|
||||
|
||||
## Скоуп ревью
|
||||
|
||||
Первый заход, дельты нет — разбор ТЗ целиком по §7.1: обязательные разделы,
|
||||
однозначность и доказуемость AC1…AC8, соответствие описанных технических фактов
|
||||
реальному коду (`src/houseplan-editor-runtime.ts`, `src/houseplan-card.ts`,
|
||||
`src/editor-secondary.ts`, `demo/smoke_free_walls.mjs`,
|
||||
`scripts/docs-acceptance.mjs`, `test/docs-acceptance.test.mjs`), а также
|
||||
продуктовая рамка по `docs/SCOPE.md` (задача инфраструктурная по духу — не про
|
||||
J1–J7, а про доказательность гейтов, что для класса B/тестовой инфраструктуры
|
||||
уместно и не требует отдельного продуктового обоснования; правка `src/**`
|
||||
делает задачу полным треком по §1, но не продуктовой в смысле пользовательского
|
||||
сценария — ТЗ прямо пишет «видимого поведения не меняет», что не противоречит
|
||||
скоупу, поскольку задача чинит доказательство существующего инварианта, а не
|
||||
добавляет функциональность).
|
||||
|
||||
## Как проверялось
|
||||
|
||||
Читкой кода, без исполнения (spec review, гейты не прогонялись — это код-ревью
|
||||
этап, здесь ему нечего проверять по §2.4). Для каждого технического утверждения
|
||||
ТЗ — сверка со строками файлов на HEAD (`cd0c85b9`):
|
||||
|
||||
- `src/houseplan-editor-runtime.ts:3050-3150, 5282-5300` — таблица «Место» для
|
||||
M2 (`_deletePhysicalSelection`, `_runEditorContext`, `_deleteDraftWhole`);
|
||||
- `src/editor-secondary.ts:200-224` — `runContext`;
|
||||
- `src/houseplan-card.ts:3015-3022, 7845-7874` — вызывающие и параллельные
|
||||
обёртки `_deletePhysicalSelection`/`_deleteDraftWhole`/`_deleteDraftSegment`;
|
||||
- `demo/smoke_free_walls.mjs:1-30, 175-208` — стаб `_confirmDanger` и вызовы
|
||||
`_deletePhysicalSelection`;
|
||||
- `scripts/docs-acceptance.mjs:1-128` — `docsWitnessFloor`,
|
||||
`docsAcceptancePlan`, вычисление `floor`;
|
||||
- `test/docs-acceptance.test.mjs` — существующие тесты AC7/AC8;
|
||||
- `scripts/golden-acceptance.mjs:1-200`, `demo/golden/accept.mjs:1-121`,
|
||||
`demo/golden/run.mjs:800-850` — сверка риска «изменение порога затрагивает
|
||||
golden»;
|
||||
- `scripts/mutation-gate.mjs` — конвенция именования мутантов (`id: 'kebab-case'`),
|
||||
сверена с предложенными `draft-delete-drops-the-promise` и
|
||||
`witness-floor-counts-survivors` — согласуется.
|
||||
|
||||
Один факт проверен эмпирически (не просто чтением): предположение, что
|
||||
TypeScript пропустит функцию, аннотированную `(): void`, но фактически
|
||||
возвращающую `Promise<void>` изнутри, — **проверено `tsc`** на изолированном
|
||||
файле (см. находку M2-a ниже), потому что от этого зависела оценка серьёзности
|
||||
находки, а не только её наличие.
|
||||
|
||||
Issue #406 (куда ТЗ отправляет один Low-пункт) прочитан целиком через
|
||||
`gh issue view 406` — сверка ссылки «#406 «д»».
|
||||
|
||||
## Находки
|
||||
|
||||
### M2-a (Medium, в скоупе) — таблица «Место» неполна: пропущена обёртка `houseplan-card.ts:7856`, и без неё правка не пройдёт `tsc --noEmit`
|
||||
|
||||
Файл: `docs/specs/405-dropped-promise-and-witness-floor.md`, раздел «(1) M2»,
|
||||
таблица «Место» и абзац «Практически».
|
||||
|
||||
**Что не так.** Смок вызывает не `houseplan-editor-runtime.ts`, а
|
||||
`window.__card` — экземпляр класса `houseplan-card.ts` (`demo/srv/demo.html:194`
|
||||
создаёт `<houseplan-card>`, `demo/serve.mjs` его же отдаёт в `window.__card`,
|
||||
`smoke_free_walls.mjs:194,199` зовёт `c._deletePhysicalSelection()`). Реальная
|
||||
точка входа — обёртка `src/houseplan-card.ts:7856`:
|
||||
|
||||
```ts
|
||||
private _deletePhysicalSelection = (): void => {
|
||||
return this._editorRuntimeOrThrow()._deletePhysicalSelection();
|
||||
}
|
||||
```
|
||||
|
||||
Таблица «Место» в ТЗ называет только `editor-secondary.ts:221`,
|
||||
`houseplan-editor-runtime.ts:5290` и `:3073` — эту строку не называет вовсе.
|
||||
«Практически» перечисляет три функции для правки (`_deletePhysicalSelection`
|
||||
в runtime, `_runEditorContext`, `runContext`) — тоже без неё.
|
||||
|
||||
**Воспроизведено**: изолированный `tsc --strict --noEmit` на паре функций,
|
||||
повторяющей форму `houseplan-card.ts:7856` после того, как внутренний вызов
|
||||
меняет тип на `Promise<void>`, а внешняя обёртка остаётся `(): void`:
|
||||
|
||||
```ts
|
||||
async function inner(): Promise<void> {}
|
||||
const wrapper = (): void => {
|
||||
return inner();
|
||||
};
|
||||
```
|
||||
|
||||
```
|
||||
error TS2322: Type 'Promise<void>' is not assignable to type 'void'.
|
||||
```
|
||||
|
||||
Это не эстетика типов — это реальный отказ гейта `npx tsc --noEmit`,
|
||||
единственного среди трёх дешёвых гейтов, который правка данной задачи
|
||||
действительно затрагивает. Если исполнитель проверит только смок (JS,
|
||||
типов не видит) или отправит правку без прогона `tsc`, регресс поймает CI,
|
||||
но раунд код-ревью на это тратиться не должен.
|
||||
|
||||
**Это не догадка о недостающем шаге, а видимый паттерн этого же файла.**
|
||||
Строками ниже, `houseplan-card.ts:7864` и `:7868`, у соседних методов
|
||||
(`_deleteDraftWhole`, `_deleteDraftSegment`) обёртки **уже** типизированы
|
||||
`Promise<void>`, зеркаля асинхронные методы runtime. Это подтверждает: в файле
|
||||
есть устоявшаяся конвенция «тип обёртки повторяет тип метода runtime», и
|
||||
`_deletePhysicalSelection`-обёртка на `:7856` — единственная, которую эта
|
||||
задача обязана привести к тому же виду, а ТЗ её не называет.
|
||||
|
||||
**Требуется**: добавить `src/houseplan-card.ts:7856` в таблицу «Место» и
|
||||
в перечень правок «Практически» — тип обёртки меняется на `Promise<void>` с
|
||||
`return`, как у `_deleteDraftWhole`/`_deleteDraftSegment` рядом.
|
||||
|
||||
Серьёзность — Medium, не High: контракт AC1–AC8 в остальном верен и
|
||||
достижим, дефект — не в контракте, а в полноте технической карты внутри уже
|
||||
названного в скоупе файла (`src/houseplan-card.ts` фигурирует в файлах задачи
|
||||
неявно — это тот же модуль, где объявлена обёртка `_deleteDraftWhole`,
|
||||
`_deleteDraftSegment`, уже упомянутых как соседний прецедент); дешёвый гейт
|
||||
`tsc` его поймает при обычной проверке, но ТЗ, которое ведёт прямиком к
|
||||
провалу первого же гейта, не «выполнимо без доработки» по мерке §2.4.
|
||||
|
||||
### M4-b (Low) — ссылка «#406 «д»» на несуществующий пункт
|
||||
|
||||
Файл: `docs/specs/405-dropped-promise-and-witness-floor.md`, раздел «Скоуп /
|
||||
не-скоуп»:
|
||||
|
||||
> затирание `acceptance.declared` при повторной приёмке (#406 «д») — соседняя
|
||||
> строка того же файла, но другой контракт
|
||||
|
||||
Прочитан issue #406 целиком (`gh issue view 406`): в нём ровно четыре пункта,
|
||||
(а)–(г) — мёртвые ключи `confirm.*`, роль диалога, непокрытая HA-ветка
|
||||
подтверждения, снапшоты area. Пункта «(д)» нет. Оригинальный аудит-комментарий
|
||||
в теле #405 упоминает это как отдельный Low-пункт («Отдельно, из того же
|
||||
разбора (Low, чинить попутно): повторная приёмка неизменённого набора
|
||||
затирает `acceptance.declared`… `scripts/docs-accept.mjs:142-150`»), но нигде
|
||||
не заводит его как пункт «(д)» и не кладёт в #406.
|
||||
|
||||
Итог: Low-находка из исходного аудита сейчас **не отслеживается ни одним
|
||||
issue** — ссылка ТЗ на несуществующий пункт создаёт впечатление, что она
|
||||
где-то учтена, хотя это не так.
|
||||
|
||||
**Решение ревьюера**: не правится сейчас (сам дефект `docs-accept.mjs:142-150`
|
||||
за пределами скоупа #405 — другая строка того же файла, другой контракт, как
|
||||
верно замечает автор) — но ссылка ложная и должна быть исправлена: либо
|
||||
пункт действительно добавляется в #406, либо ТЗ отправляет его в новый issue,
|
||||
либо ТЗ формулирует «Low, пока нигде не заведён» вместо несуществующей ссылки.
|
||||
Это правится в рамках текущего цикла возврата (Low, не отдельный лимит).
|
||||
|
||||
## Медиум вне скоупа — заведён отдельным issue
|
||||
|
||||
### Golden-эталоны подвержены тому же классу дефекта, что и M4 — #408
|
||||
|
||||
`scripts/golden-acceptance.mjs:182-183` считает порог свидетелей от
|
||||
`withBaseline.length` — числа сцен со статусом **не** `missing-baseline`, то
|
||||
есть от того, что уцелело на диске, а не от размера известной матрицы
|
||||
(`GOLDEN_SCENARIOS.length`). Проверено чтением всей цепочки
|
||||
(`demo/golden/run.mjs:836-839` ставит `status: 'missing-baseline'`, когда файла
|
||||
эталона нет; `goldenAcceptanceRefusal` в `scripts/golden-acceptance.mjs:59-93`
|
||||
**требует**, чтобы каждая такая сцена была объявлена через `--expect-new` —
|
||||
и не более; `demo/golden/accept.mjs:64-72` вызывает `goldenWitnessRefusal` уже
|
||||
после этой проверки). Модельный разбор: `git rm demo/golden/baselines/*.png`
|
||||
+ объявление всех сцен `--expect-new` → каждая сцена легально проходит
|
||||
`goldenAcceptanceRefusal` → `withBaseline.length === 0` →
|
||||
`goldenWitnessFloor(0) === 0` → `witnesses.length (0) < floor (0)` ложно →
|
||||
отказа `goldenWitnessRefusal` нет. `--reviewed` не является независимой
|
||||
проверкой (булев CLI-флаг), так что рассуждение ТЗ #405 о том, что M4
|
||||
недостаточно прикрыт `--reviewed` для `docs-accept.mjs`, симметрично относится
|
||||
и сюда.
|
||||
|
||||
ТЗ #405 обсуждает golden только в разделе «Риски» и только применительно к
|
||||
формуле (`goldenWitnessFloor` та же формула, что `docsWitnessFloor`, и должна
|
||||
остаться той же) — не к тому, что вызывающий код (`withBaseline.length`)
|
||||
подвержен той же уязвимости на входе, что чинит M4. Файл другой
|
||||
(`scripts/golden-acceptance.mjs`, не `scripts/docs-acceptance.mjs`), скоуп
|
||||
ТЗ его не называет — правка не входит в #405.
|
||||
|
||||
Issue заведён: **#408**, метки `bug`, `P2`, `S1-new`, `infra`, ссылка на #405.
|
||||
|
||||
## Проверено и корректно
|
||||
|
||||
- Обязательные разделы §7.1 присутствуют все: сценарий, что человек увидит,
|
||||
проблема, скоуп/не-скоуп, контракт, UX, модель данных/миграция, i18n, AC1–AC8
|
||||
с доказательством, план автотестов, риски, откат, release-артефакты.
|
||||
- Продуктовых вопросов владельцу нет, догадок, выданных за факт, в контракте
|
||||
M2/M4 не найдено (кроме описанного выше пробела в таблице «Место» — это не
|
||||
догадка, а неполнота карты).
|
||||
- Таблица «Место» для M2 (кроме отсутствующей строки `houseplan-card.ts:7856`)
|
||||
точно соответствует коду: `runContext` (`editor-secondary.ts:221`),
|
||||
`_runEditorContext` (`houseplan-editor-runtime.ts:5290`),
|
||||
`_deletePhysicalSelection` (`:3073`, ветка `sel.kind === 'draft'` действительно
|
||||
роняет промис через `return`), `_deleteDraftWhole` (`:3119`, действительно
|
||||
`async`, `await _confirmDanger`).
|
||||
- Смок `demo/smoke_free_walls.mjs:16` — стаб `_confirmDanger = async () => true`
|
||||
резолвится в том же микротаске, и `:194,199` действительно не проверяет
|
||||
ожидание по-настоящему; воспроизведение из тела issue (макрозадача → два
|
||||
`FAILED`) правдоподобно и согласуется с кодом.
|
||||
- `scripts/docs-acceptance.mjs:44-46,107-108` — формула и место вычисления
|
||||
`floor` от `withCommitted.length` подтверждены; контраст «PNG на месте» vs
|
||||
«PNG удалены» из тела issue воспроизводим по чтению кода без исполнения.
|
||||
- Предложенный контракт AC6 (`floor` от размера набора сценариев, не от
|
||||
наличия файлов) корректно сохраняет существующее поведение AC7 — проверено
|
||||
арифметически: `docsWitnessFloor(ids.length)` и текущее
|
||||
`docsWitnessFloor(withCommitted.length)` дают одно и то же значение во всех
|
||||
ветках существующих тестов `test/docs-acceptance.test.mjs`, включая
|
||||
«кадр без закоммиченной пары не может быть свидетелем» (`floor` остаётся 1
|
||||
в обоих случаях — 3 или 1 округляются к одному порогу на этом наборе).
|
||||
- «Не-скоуп» по `_deleteDraftSegment` — проверено: у него отдельный,
|
||||
корректно типизированный путь (`houseplan-card.ts:7868` уже `Promise<void>`,
|
||||
зовётся напрямую из своей кнопки, не через `_deletePhysicalSelection`) —
|
||||
исключение обосновано верно, не прячет тот же дефект.
|
||||
- Риск «изменение порога затрагивает golden» правильно определяет, что *формулу*
|
||||
трогать нельзя без зеркалирования — но не замечает независимый дефект на
|
||||
стороне вызова (см. #408).
|
||||
- AC2 (отрицательный прогон обязателен) сформулирован по правилу «тест должен
|
||||
уметь падать» и предлагает конкретный механизм (`scripts/mutation-gate.mjs`,
|
||||
который в проекте уже используется по этому шаблону) — не декларация.
|
||||
- Откат и release-артефакты соразмерны факту «видимого поведения нет»
|
||||
(`User-Visible: no` подтверждён отсутствием изменений в UX/i18n разделах).
|
||||
|
||||
## Чего не проверял
|
||||
|
||||
- Гейты (`tsc`, `npm test`, `npm run build`, смоки, `golden:verify`,
|
||||
`model-invariants`) не прогонял — этап spec review, кода задачи ещё нет,
|
||||
прогонять нечего. Эмпирическая проверка ограничена одним изолированным
|
||||
TS-файлом вне репозитория, подтверждающим находку M2-a (см. выше), — это не
|
||||
прогон проектных гейтов.
|
||||
- Не проверял `scripts/mutation-gate.mjs` на предмет того, заведутся ли
|
||||
предложенные мутанты технически без ошибок раннера — это вопрос реализации,
|
||||
а не ТЗ; на этапе спеки достаточно, что названный механизм существует и
|
||||
используется в проекте по тому же образцу.
|
||||
- Не проверял golden `test/golden-policy.test.mjs` целиком — находка по #408
|
||||
установлена чтением `scripts/golden-acceptance.mjs` и `demo/golden/accept.mjs`
|
||||
напрямую; тестовое покрытие этой ветки (принимает ли текущий набор тестов
|
||||
сценарий «все эталоны удалены») не сверял — это будет частью работы над #408,
|
||||
не #405.
|
||||
- Не оценивал вне двух Medium/одного Low другие возможные дефекты в
|
||||
`scripts/docs-accept.mjs` целиком (не-`docs-acceptance.mjs`) — за пределами
|
||||
того, что уже упомянуто как «не-скоуп» самим ТЗ.
|
||||
|
||||
## Итог
|
||||
|
||||
Один Medium **в скоупе** (M2-a) не даёт зелёный вердикт по §2.4 — ТЗ ведёт
|
||||
прямиком к отказу гейта `tsc --noEmit`, если исполнитель буквально следует
|
||||
таблице «Место». Плюс один Low (ложная ссылка на #406) для правки в этом же
|
||||
цикле. Medium вне скоупа заведён отдельным issue (#408), в задаче #405 не
|
||||
чинится.
|
||||
|
||||
**Вердикт: жёлтый.** Возврат автору: дополнить таблицу «Место» и раздел
|
||||
«Практически» строкой `src/houseplan-card.ts:7856`, поправить ссылку на #406.
|
||||
Reference in New Issue
Block a user