16 KiB
SPEC-REVIEW — issue #225, цикл r2/2
- Issue: https://github.com/Matysh/houseplan-card/issues/225
- ТЗ: тело issue #225 (лёгкий трек
small, файл вdocs/specs/не создаётся, §5 PROCESS.md) - Трек:
small— лимит циклов ревью ТЗ = 2 (§4 PROCESS.md); это последний допустимый цикл - Ревьюер: Claude (роль «Ревьюер ТЗ», отдельная сессия от автора)
- Вердикт: зелёный · цикл r2/2 · High: 0 · Medium: 0
Скоуп проверки (по дельте, §2.10 PROCESS.md)
Раунд r1 закончился жёлтым вердиктом с одной Medium-находкой в скоупе (M1:
третий пример в AC4 требовал None, хотя по собственному контракту ТЗ («query
не участвует в резолвинге») корректная реализация обязана вернуть валидный
путь — критерий был недоказуем без нарушения контракта). Автор внёс правку
только в тело issue; ветка issue/225-* не создавалась, код не менялся —
подтверждено: git log --oneline после коммита be0277f (докладной документ
r1) новых коммитов нет.
SHA/версия r1 не названы в вердикте явно — это найдено при подготовке
этого раунда, как и предупреждает §2.10: комментарий-вердикт r1
(2026-08-20T18:14:47Z) не указывает, к какой версии тела issue он относится.
Восстановлено косвенно: r1 дословно цитирует AC4 в том виде, в каком он был
опубликован комментарием автора 2026-08-20T18:08:06Z («ТЗ написано в теле
issue... Отправляю на ревью ТЗ»), и это совпадает с текущей историей правок
issue (правки по r1 внесены следующим комментарием, 2026-08-20T18:18:05Z, уже
после вердикта). Коммит be0277f5f5ca7595b15f2c64d3b79bd031ed4130 — тот, что
добавил документ SPEC-REVIEW-225-r1.md, и остаётся HEAD dev на момент
этого раунда: код и остальные файлы репозитория с r1 не менялись, дельта
целиком в тексте issue.
Дельта r1 → r2 (сравнение цитат из документа r1 с текущим телом issue):
- Таблица AC: пример
…/files/m1/doc.pdf?x=/../../etcубран из AC4 и перенесён в новый AC4a с обратным ожиданием (резолвится в валидный путь), плюс добавлен второй пример с#fragment. - AC1 расширен веткой
plans/_/...сquery(/houseplan_files/plans/f1.svg?v=1→<root>/plans/f1.svg). - Блок «Принято предположительно» получил пункты 3 и 4, документирующие ревизию r2.
Контракт поведения, AC2/AC3/AC5/AC6/AC7, план автотестов, мутационный гейт, риски, откат, release-артефакты — текст не менялся. По правилу «дельта, а не задача целиком» они не разбираются заново; закрытие round r1 показано ниже, а неизменное — в разделе «Унаследовано».
Дельта локальна (правка двух строк таблицы + один пункт в предположениях, без смены контракта, без ребейза, без новой подсистемы) — полный разбор не требуется по критериям §2.10.
Как проверялось
- Получено тело issue #225 (текущая версия) и все 5 комментариев через
gh issue view 225 --json body,comments,labels. - Восстановлен текст AC4 на момент r1 из цитат в
docs/reviews/SPEC-REVIEW-225-r1.md(коммитbe0277f) и построчно сверен с текущим AC4/AC4a. - Перепроверена сама правка на непротиворечивость контракту:
- AC4a, пример 1 (
…/files/m1/doc.pdf?x=/../../etc): по контракту путь берётся изurlsplit(url).path;query— этоx=/../../etc, в.pathне попадает..path=.../files/m1/doc.pdf, без единой точки... Хвост после префикса —m1/doc.pdf, ровно 2 сегмента, оба проходятsanitize_*без изменений → ожидание «резолвится в валидный<root>/files/m1/doc.pdf» совпадает с тем, что контракт обязан выдать. Совпадает и с уже проверенным в r1 расчётом (там это был пример 3 AC4, только с противоположным, ошибочным ожиданиемNone). - AC4a, пример 2 (
…/files/m1/doc.pdf#/../..):urlsplitкладёт/../..воfragment,.pathне меняется. По контрактуfragmentтоже не участвует в резолвинге — то же ожидание, тот же вывод: валидный путь. Новый пример, ранее не разбирался; логика идентична предыдущему. - AC4 (оставшиеся 3 примера): не изменились, построчно уже проверены
в r1 без ошибок (все три сегментированы так, что дают
Noneуже сегодняшней проверкой числа сегментов/наличия/, независимо от query) — сверено, что текст этих трёх строк в issue не редактировался. - AC1, добавленная ветка
plans/_/...с query: веткаplansв_internal_path(по описанию в issue и r1:import_export.py:355) отклоняет только хвосты, содержащие/;f1.svg?v=1после отсечения query поurlsplitдаёт хвостf1.svg— один сегмент без/, сравнениеsanitize_filenameпроходит. Ожидание AC1 (резолвится в<root>/plans/f1.svg) согласуется с контрактом, новой неоднозначности не вносит. - Пункты 3–4 блока «Принято предположительно»: описывают ровно произошедшую правку (перенос примера, расширение AC1) и не подменяют продуктовое решение — технические примечания к диффу, а не новое решение за пользователя.
- AC4a, пример 1 (
- Проверено, что контракт поведения, AC2/AC3/AC5/AC6/AC7, план автотестов,
мутационный гейт, риски и «Не в скоупе» текстуально идентичны версии,
которую разобрал r1 (посимвольное сравнение с цитатами в
SPEC-REVIEW-225-r1.md) — правок нет, переразбор не требуется. - Мутационный гейт (3 id:
internal-path-ignores-query,internal-path-allows-traversal,roundtrip-import-with-attachment) остался без изменений; для AC4a отдельного id не заведено — не требуется: AC4a не защищает от регрессии существующего поведения, а описывает новое ожидание, которое доказывается собственным будущим тестом на AC4a напрямую (если реализация ошибочно отклонит валидный запрос, красным станет сам тест AC4a, отдельный мутант не нужен). - Гейты кода не запускались: этап — ревью ТЗ, реализации нет, ни один файл
репозитория кроме
docs/reviews/*.mdне менялся с r1.
Закрытие раунда r1
| Находка r1 | Чем закрыта | Где это видно |
|---|---|---|
M1 (Medium, в скоупе) — третий пример AC4 (…/files/m1/doc.pdf?x=/../../etc) требовал None, что противоречит контракту ТЗ («query не участвует в резолвинге») |
Пример вынесен из AC4 в новый AC4a с противоположным (корректным по контракту) ожиданием — «резолвятся в валидный <root>/files/m1/doc.pdf»; AC4 сохранил только три traversal-примера, ранее уже проверенных как корректные |
Тело issue #225, раздел «Критерии приёмки»: строки AC4 (3 примера) и AC4a (2 примера); авторский комментарий 2026-08-20T18:18:05Z: «M1 закрыт... пример перенесён в новый AC4a с обратным ожиданием» |
Дополнительно (не находка, инициатива автора, не требовала действия
ревьюера): AC1 расширен веткой plans/_/... с query — комментарий автора
сам называет это «дополнительно, не находка»; проверено выше и признано
корректным.
Унаследовано из r1
Документ: docs/reviews/SPEC-REVIEW-225-r1.md, SHA be0277f5f5ca7595b15f2c64d3b79bd031ed4130
(коммит, добавивший документ; версия текста issue на тот момент —
комментарий-публикация ТЗ от 2026-08-20T18:08:06Z, до правок по r1).
Принято без повторной проверки в этом раунде, так как дельта их не касается:
- Диагноз бага (
_internal_pathрежет URL по/без отделения query,_looks_internalсмотрит только на префикс,_SAFE_NAME_REломает сравнение на?/=) — проверен чтением кода в r1, текст issue не менялся. - Применимость лёгкого трека
small(сложность ≤3, одна поверхность, без миграции/compatibility/UX-контракта/перф/touch) — критерии не пересматривались. - AC2, AC3, AC5, AC6, AC7 — текст не менялся, однозначны и доказуемы по выводам r1.
- AC1 в части, не связанной с добавленной веткой
plans(резолвингfiles/...сquery,CONTENT_URL-префиксом,#fragment,?v=1#page=2) — не менялась, проверена в r1. - Риск смены классификации
content_manifest(external→internal) и сохранение совместимости черезidentity()(:1034-1039,storageне в ключе сравнения) — код не менялся, проверка r1 остаётся в силе. - Идемпотентность
plan_only-проверки (:704, AC7) — не затронута. - Решение не брать текст ошибки
invalid_contentв скоуп — продуктовое решение владельца на этапе аналитики, не пересматривается ревью ТЗ. - Откат и release-артефакты (
User-Visible: yes, оба changelog) — текст не менялся. - Мутационный гейт (3 id) — текст не менялся, соответствие AC не пересматривалось за исключением явной проверки, что деление AC4/AC4a не требует нового id (см. «Как проверялось»).
Находки
Нет находок в этом раунде (High: 0, Medium: 0, Low: 0). Единственная Medium-находка r1 (M1) закрыта корректно, новых противоречий правка не вносит.
Что проверено и корректно
- M1 закрыт по существу, а не переименован: новое AC4a требует ровно то
поведение, которое обязан выдать заявленный контракт («query и fragment
не участвуют в резолвинге»), и оба его примера (
?x=/../../etcи#/../..) построчно пересчитаны и дают валидный путь, а неNone. - AC4 после удаления спорного примера остался внутренне согласован: три
оставшихся примера — реальные traversal-случаи в самом пути, а не в
query/fragment, и ожидание
Noneдля них верно уже сегодняшней логикой подсчёта сегментов. - Расширение AC1 веткой
plans/_/...?v=...не создаёт скрытого нового контракта — это то же отделение query той же функцией, применённое ко второй ветке, которую r1 отдельно отметил как непроверенную (и намеренно не поднял до находки). - Дельта не расширяет скоуп и не меняет контракт поведения, поэтому ограниченный по объёму раунд корректен по критериям §2.10 (локальность правки, отсутствие ребейза, отсутствие новой подсистемы).
- Лимит циклов лёгкого трека (2) не превышен: это второй и последний цикл, вердикт зелёный — задача может двигаться в «Готово к разработке».
Чего не проверял
- Гейты кода (
npx tsc --noEmit,npm test,npm run build,python -m pytest tests_backend -q) — не запускались: реализации ещё нет, ни один продуктовый файл не менялся между r1 и r2, только текст issue. - Не пересматривался диагноз бага и код
_internal_path/_looks_internal/content_manifest/_content_state— не изменился с r1, где уже проверен чтением построчно. - Не проверялась ветка
plansв реальном коде на предмет иных, не описанных в ТЗ побочных эффектов — вне зоны этого ревью (ревью ТЗ судит текст, а не реализацию, которой ещё нет). - Percent-encoding в именах файлов и нормализация существующих конфигов — по-прежнему намеренно вне скоупа issue, не проверялись.