20 KiB
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:
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:
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.