Files
enduro-trails/docs/work-items/ET-005/12-review.md
claude-bot d32ad8f018
All checks were successful
CI / lint (push) Successful in 4s
CI / test (push) Successful in 5s
CI / lint (pull_request) Successful in 3s
CI / build (push) Successful in 2s
CI / test (pull_request) Successful in 4s
CI / build (pull_request) Successful in 3s
reviewer(ET): auto-commit from reviewer run_id=11
2026-05-21 19:59:23 +00:00

18 KiB
Raw Blame History

type, work_item_id, title, version, status, verdict, created_at, authors
type work_item_id title version status verdict created_at authors
review ET-005 Code Review: Переключение единиц измерения расстояний (км/мили) 1 approved APPROVED 2026-05-21
agent:reviewer

Code Review — ET-005

Вердикт

APPROVED (с комментариями).

P0/P1-findings нет. Зафиксировано 1×P2 и 2×P3 — все некритичные, не блокируют merge. P2 — побочный эффект оркестратора (сброс выбора варианта связки при переключении единиц), не нарушает требований ТЗ.

Объект ревью

  • Ветка: feature/ET-005-
  • Код-коммит: 2fe5cfe feat(web): переключатель единиц измерения расстояний (км/мили)
  • Изменённые файлы кода:
    • src/web/units.js (новый, 190 строк)
    • src/web/index.html (+11 строк)
    • src/web/app.js (+118 / 24 строк)
    • src/web/app.css (+20 строк)
    • tests/unit/units.test.js (новый, 219 строк)
    • tests/unit/test_unit_toggle.py (новый, 211 строк)
    • CHANGELOG.md (+5 строк)
  • Прочитано: 02-trz.md, 03-acceptance-criteria.md, 04-test-plan.yaml, 06-adr/adr-0001-unit-toggle-client-side.md, 10-tech-risks.md, CLAUDE.md.

1. Соответствие ТЗ

Функциональные требования

Требование Статус Комментарий
ФТ-1 Кнопка-toggle в панели настроек карты index.html:62-69 — сегментированный .seg-control в попапе #terrain-popup, после чекбокса POI, отделён <hr>
ФТ-2 Два состояния: km (default) / mi units.js DEFAULT_UNIT = 'km', VALID_UNITS = ['km','mi']
ФТ-3 Пересчёт всех видимых расстояний Единый оркестратор onUnitChange() — карточки маршрутов, мини-карточка, лист точек, линейка, scale-bar, связка, «красивый» маршрут. См. R-01 (P2) — побочный сброс выбора варианта связки
ФТ-4 Сохранение выбора в localStorage (distance_unit) units.js setUnit()writeStored(), ключ distance_unit
ФТ-5 При загрузке — чтение из localStorage getUnit() лениво читает ключ; syncUnitToggleUI() на DOMContentLoaded восстанавливает вид кнопок

Нефункциональные требования

Требование Статус Комментарий
Пересчёт < 100 мс Чисто клиентская конвертация, нет ни сети, ни перезапроса тайлов
Кнопка доступна на всех размерах экрана Переиспользован адаптивный .seg-control; статически подтверждено test_unit_toggle_reuses_seg_control_component. Браузерный e2e на 375px (TP-05) — на этап тестирования
Не блокирует другие UI элементы Аддитивный ряд .terrain-unit-row с разделителем <hr>; margin-bottom:0 для последнего элемента попапа

Технический дизайн ТЗ

Два пункта раздела «Технический дизайн» ТЗ скорректированы в ADR-0001 — оба отклонения обоснованы и легитимны, дефектом не являются:

  • путь src/web/static/js/units.jssrc/web/units.js (ADR-0001 п.2: каталога static/js/ нет, сборщика нет, app.js — классический скрипт; Вариант C отклонён);
  • «все компоненты слушают unitchange» → единый оркестратор onUnitChange() (ADR-0001 п.6; Вариант B отклонён ради низкой связности).

Согласно CLAUDE.md ADR обладает архитектурным мандатом; раздел «Технический дизайн» ТЗ носит рекомендательный характер. Реализация следует ADR — это корректно.

Все функциональные и нефункциональные требования ТЗ выполнены.

2. Соответствие ADR-0001

Пункт решения ADR Статус Комментарий
п.1 Только клиент; backend/БД/инфра без изменений Затронут только src/web/ + тесты
п.2 Модуль src/web/units.js, подключён перед app.js index.html:415 units.js строго до app.js; статически подтверждено test_units_js_loaded_before_app_js (R7)
п.3 Неймспейс window.Units (getUnit/setUnit/formatDistance/KM_TO_MI) Публичный контракт собран; дополнительно module.exports для Node-тестов
п.4 Каноническая единица — метры; конвертация только при форматировании formatDistance(meters,...) принимает метры; внутреннее состояние (route.distance_m, rulerTotal) не трогается
п.5 localStorage ключ distance_unit, дефолт km при отсутствии/мусоре getUnit() через isValidUnit() деградирует к DEFAULT_UNIT
п.6 Единый оркестратор — ровно одна подписка на unitchange document.addEventListener('unitchange', onUnitChange) единственная; подтверждено test_app_js_single_unitchange_subscription
п.7 Все 13 мест форматирования → Units.formatDistance(); GPX остаётся метрическим grep-аудит: вне generateGPX() захардкоженного (m/1000).toFixed()+' км' не осталось; GPX-экспорт намеренно метрический (R6), подтверждено test_app_js_gpx_export_stays_metric
п.8 UI на готовом .seg-control в #terrain-popup Новый CSS-компонент не вводится; добавлены только обёрточные стили .terrain-unit-row
п.9 C4 без изменений Состав компонентов не меняется

Реализация полностью соответствует принятому ADR-0001. Риски R1R8 из 10-tech-risks.md отработаны: R1 (call-sites) — централизация + аудит; R2 (суб-км значения) — явная политика в formatDistance(); R3 (scale-bar) — отдельная unit-aware ветка + включение в оркестратор; R4 (разделитель) — единая запятая в units.js (по scale-bar см. R-02); R5 (точность) — параметр precision; R6 — GPX исключён; R7 — порядок скриптов; R8 — переиспользование .seg-control.

3. Acceptance Criteria

AC Покрытие
AC-1 Кнопка km/mi в панели, показывает выбор, клик переключает test_unit_toggle_present_in_html; syncUnitToggleUI() переключает класс .active
AC-2 Пересчёт всех расстояний, коэф. 0.621371, округление до 1 знака JS TP-02, AC-2: KM_TO_MI, точность по умолчанию — 1 знак. Карточки маршрутов — precision:0 (R5, осознанная трактовка), суб-км в милях — 2 знака (R2)
AC-3 Сохранение и восстановление из localStorage, дефолт km JS TP-01, TP-03, недоступный localStorage
AC-4 Кнопка не перекрывает элементы, mobile, переключение < 100мс (структурно) test_unit_toggle_has_styles; e2e mobile (TP-05) — этап тестирования

Поведенческое ядро (модуль units.js) покрыто полностью. Замечание по покрытию слоя app.js — см. R-03 (P3).

4. Качество кода

Сильные стороны:

  • Единый источник истины. KM_TO_MI объявлен ровно один раз; форматирование расстояний централизовано в units.js; 13 call-sites в app.js сведены к Units.formatDistance(). Дублирования нет.
  • Защитное программирование. readStored()/writeStored() обёрнуты в try/catch (private mode); dispatchEvent защищён от headless-среды; formatDistance() корректно отдаёт '-' для null/undefined/NaN; setUnit() валидирует вход и не шлёт событие при неизменной единице.
  • Каноническое состояние в метрах. Конвертация — только в момент форматирования; тест TP-04: многократное переключение подтверждает отсутствие дрейфа округления.
  • Консистентность с кодовой базой. Глобальный неймспейс и классический скрипт соответствуют стилю app.js; паттерн window._updateScaleZoom зеркалит существующий window._map; блок-маркеры >>> ET-005 ... >>> повторяют приём ET-002.
  • Документированность. JSDoc на всех публичных и приватных функциях со ссылками на ADR/риски; запись в CHANGELOG.md; commit — Conventional Commits (feat(web):).
  • Оркестратор аккуратен. renderLinkCards()/drawScenicRoutes() идемпотентны (remove-before-add), повторный вызов из onUnitChange() не вызывает «layer already exists»; перерисовка связки/«красивого» маршрута огорожена флагами linkMode/scenicMode.

Замечания — см. findings R-01 (P2), R-02/R-03 (P3).

5. Качество тестов

  • Unit (units.test.js) — высокое качество: тесты исполняют реальный src/web/units.js (сброс require.cache + инъекция моков window/document/localStorage перед каждым тестом). Покрыты TP-01..TP-04, AC-2, AC-3, граница 1000 м, недоступный localStorage, валидация setUnit(), публикация неймспейса.
  • Python (test_unit_toggle.py) — статические проверки структуры units.js/index.html/app.js (ADR-0001, риски R1/R3/R6/R7) + запуск JS-раннера через node --test со skip при отсутствии node (по аналогии с test_poi_toggle.py).
  • e2e TP-05 (mobile responsive) не реализован — Playwright-инфраструктуры в репозитории нет; отклонение задокументировано в шапке test_unit_toggle.py. На merge не влияет, относится к этапу тестирования.

Findings

R-01 — Переключение единиц сбрасывает выбор варианта связки (P2)

onUnitChange() (app.js:2937-2938) при активном режиме связки вызывает renderLinkCards(linkRoutes). В отличие от drawScenicRoutes(routes, activeIdx), функция renderLinkCards() не принимает индекс активного варианта и всегда отрисовывает «Вариант 1» как активный: карточке i===0 присваивается класс active, линии link-line-0line-width:5. В коде нет переменной activeLinkIdx — выбор существует только как класс в DOM и ширина линии на карте.

Следствие: пользователь, выбравший вариант 2/3 связки через selectLinkRoute(), при каждом переключении км/мили теряет выбор — подсветка карточки и акцент линии откатываются на вариант 1. Требования ТЗ (ФТ-3 «пересчёт расстояний») формально выполнены — расстояния пересчитываются корректно; это побочный дефект UX, ограниченный режимом связки, поэтому P2, а не P1.

Рекомендация: ввести activeLinkIdx (по аналогии с activeScenicIdx), обновлять его в selectLinkRoute() и после повторного renderLinkCards() в onUnitChange() вызывать selectLinkRoute(activeLinkIdx) для восстановления подсветки. Можно поправить в этом же PR либо вынести в техдолг.

R-02 — Scale-bar в режиме «mi» использует точку и латиницу (P3)

Масштабная линейка (updateScaleZoom, app.js:1455) формирует подпись как distance + ' ' + unit, где unit — латинские 'mi'/'km'/'m', а суб-единичные значения выводятся стандартным Number.toString() с точкой ('0.5 mi'). Это расходится с units.js, который по R4 задаёт запятую и русские подписи ('0,5 ми'). Митигация R4 в 10-tech-risks.md предписывает «единый разделитель для всех поверхностей».

Замечание ограничено P3: латиница и точка в scale-bar — поведение, существовавшее до ET-005 (в режиме «km» линейка и раньше показывала '30 km'/'0.5 km'); ET-005 лишь расширил тот же стиль на мили и консистентен внутри компонента. Полная унификация требует отдельного форматирования подписи scale-bar и выходит за scope ET-005. Рекомендация: при желании привести подпись scale-bar к запятой и русским единицам — отдельной косметической правкой/техдолгом.

R-03 — Слой app.js (оркестратор, scale-bar) покрыт только статически (P3)

Поведенческими тестами покрыт изолированный модуль units.js. Логика app.jsonUnitChange() (перерисовка поверхностей), unit-aware ветка scale-bar — проверяется лишь строковыми ассертами наличия в test_unit_toggle.py (test_app_js_*). Поведение «после переключения каждая видимая поверхность пересчиталась» (TP-02) не верифицируется автоматически.

Это приемлемо: в репозитории нет DOM/MapLibre-харнесса для app.js (монолитный non-module скрипт), и подход повторяет принятый в ET-002. Сквозную проверку TP-02 закрывает этап тестирования. Фиксируется как технический долг — учесть при будущей модуляризации фронтенда.

Заключение

Реализация ET-005 корректна и полна: все функциональные и нефункциональные требования ТЗ выполнены, ADR-0001 (пп.19) соблюдён полностью, риски R1R8 отработаны. Архитектурные отклонения от раздела «Технический дизайн» ТЗ легитимны и зафиксированы в ADR. Дублирования нет, форматирование централизовано, защитное программирование на месте, модуль units.js тщательно покрыт unit-тестами на реальном коде.

Блокирующих замечаний нет.

Вердикт: APPROVED. R-01 (P2) рекомендуется поправить в этом же PR либо осознанно вынести в техдолг; R-02/R-03 (P3) — на усмотрение разработчика. Ни одно замечание merge не блокирует.