(medium) ScenariosBlock/ScenarioCard: индекс дефицита берётся с самого длинного горизонта, но подписывается целевым — рассинхрон числа и подписи #1469

Closed
opened 2026-06-15 05:56:51 +00:00 by bot-backend · 1 comment
Collaborator

Severity: medium · Категория: report · Файл: frontend/src/components/site-finder/analysis/ScenariosBlock.tsx:47-69

ScenariosBlock.tsx:47-69, 118, 182. pickHorizonForecast при отсутствии точного совпадения (forecasts.find(f => f.horizon_months === targetHorizon)) возвращает forecast с САМЫМ ДЛИННЫМ горизонтом: [...forecasts].sort((a,b)=>b.horizon_months-a.horizon_months)[0] (стр.52-55). При этом ScenarioCard жёстко подписывает значение целевым горизонтом — «Индекс дефицита · {targetHorizon} мес.» (стр.182), а общий caption утверждает «Индекс дефицита приведён на целевом горизонте {targetHorizon} мес.» (стр.118). Если у сценария нет ряда на targetHorizon (выбран 6 мес, а forecasts содержат только 12/18/24), пользователю покажут дефицит за самый длинный горизонт под подписью «6 мес.». Импакт: число и подпись описывают разные горизонты в клиентском сценарном блоке (консервативный/базовый/агрессивный), где индекс дефицита — ключевая цифра решения. Для сравнения, в ForecastHorizonsBlock.tsx:109 подпись берётся из реального f.horizon_months. Срабатывает только в edge-case (нет ряда на targetHorizon), отсюда medium. Как чинить: либо возвращать null при отсутствии точного совпадения (показывать «—»), либо подписывать карточку реальным fc.horizon_months выбранного forecast, а не targetHorizon.

Почему баг: Число и его подпись описывают разные горизонты. В ForecastHorizonsBlock подпись берётся из реального f.horizon_months (стр.109), а здесь — из targetHorizon, при том что значение выбрано с другого горизонта. Это вводит в заблуждение в клиентском сценарном блоке (консервативный/базовый/агрессивный), где индекс дефицита — ключевая цифра принятия решения «строить/не строить».

Как чинить: Либо подписывать карточку фактическим горизонтом выбранного forecast (вернуть из pickHorizonForecast сам ряд и показывать fc.horizon_months), либо при отсутствии точного горизонта брать ТОЛЬКО scenariosSummary[key] (который по контракту = дефицит на целевом горизонте), не подменяя его самым длинным горизонтом.


deep-audit «армия ботов 2» · verify+harden, confidence 0.85 · unit U100

**Severity:** medium · **Категория:** report · **Файл:** `frontend/src/components/site-finder/analysis/ScenariosBlock.tsx:47-69` ScenariosBlock.tsx:47-69, 118, 182. pickHorizonForecast при отсутствии точного совпадения (forecasts.find(f => f.horizon_months === targetHorizon)) возвращает forecast с САМЫМ ДЛИННЫМ горизонтом: [...forecasts].sort((a,b)=>b.horizon_months-a.horizon_months)[0] (стр.52-55). При этом ScenarioCard жёстко подписывает значение целевым горизонтом — «Индекс дефицита · {targetHorizon} мес.» (стр.182), а общий caption утверждает «Индекс дефицита приведён на целевом горизонте {targetHorizon} мес.» (стр.118). Если у сценария нет ряда на targetHorizon (выбран 6 мес, а forecasts содержат только 12/18/24), пользователю покажут дефицит за самый длинный горизонт под подписью «6 мес.». Импакт: число и подпись описывают разные горизонты в клиентском сценарном блоке (консервативный/базовый/агрессивный), где индекс дефицита — ключевая цифра решения. Для сравнения, в ForecastHorizonsBlock.tsx:109 подпись берётся из реального f.horizon_months. Срабатывает только в edge-case (нет ряда на targetHorizon), отсюда medium. Как чинить: либо возвращать null при отсутствии точного совпадения (показывать «—»), либо подписывать карточку реальным fc.horizon_months выбранного forecast, а не targetHorizon. **Почему баг:** Число и его подпись описывают разные горизонты. В ForecastHorizonsBlock подпись берётся из реального f.horizon_months (стр.109), а здесь — из targetHorizon, при том что значение выбрано с другого горизонта. Это вводит в заблуждение в клиентском сценарном блоке (консервативный/базовый/агрессивный), где индекс дефицита — ключевая цифра принятия решения «строить/не строить». **Как чинить:** Либо подписывать карточку фактическим горизонтом выбранного forecast (вернуть из pickHorizonForecast сам ряд и показывать fc.horizon_months), либо при отсутствии точного горизонта брать ТОЛЬКО scenariosSummary[key] (который по контракту = дефицит на целевом горизонте), не подменяя его самым длинным горизонтом. --- <sub>deep-audit «армия ботов 2» · verify+harden, confidence 0.85 · unit U100</sub>
bot-backend added the
week ревью 1
label 2026-06-15 05:56:51 +00:00
bot-backend added the
scope/backend
label 2026-06-15 11:02:09 +00:00
Owner

🔍 week ревью 1 — верификация (ПОДТВЕРЖДЕНО)

Текущее состояние кода: frontend/src/components/site-finder/analysis/ScenariosBlock.tsx:47-56 (pickHorizonForecast), :58-69 (deficitForScenario), общий caption :117-120, подпись карточки :182.

Корень: pickHorizonForecast при отсутствии точного совпадения по targetHorizon (стр.52 forecasts.find(f => f.horizon_months === targetHorizon)) возвращает forecast с САМЫМ ДЛИННЫМ горизонтом (стр.55 [...forecasts].sort((a,b)=>b.horizon_months-a.horizon_months)[0]). При этом deficitForScenario берёт fc.deficit_index (стр.66), а ScenarioCard жёстко подписывает значение целевым горизонтом — «Индекс дефицита · {targetHorizon} мес.» (стр.182), и caption утверждает «приведён на целевом горизонте {targetHorizon} мес.» (стр.118). Если у сценария нет ряда на targetHorizon (выбран 6 мес, есть только 12/18/24), число — за самый длинный горизонт под подписью «6 мес.». Срабатывает только в edge-case → medium корректна.

Что править:

  • Вариант A: подписывать карточку фактическим горизонтом выбранного forecast — вернуть из pickHorizonForecast сам ряд и показывать fc.horizon_months, как ForecastHorizonsBlock.tsx (берёт реальный f.horizon_months).
  • Вариант B: при отсутствии точного горизонта в deficitForScenario не уходить в longest — использовать ТОЛЬКО scenariosSummary[key] (стр.67, по контракту = дефицит на целевом горизонте) либо вернуть null → карточка покажет «—» (ветка стр.196-197).

Что посмотреть:

  • Код: ScenariosBlock.tsx:47-69, :118, :182-197; сравнить с ForecastHorizonsBlock.tsx (подпись из реального f.horizon_months); тест с forecasts без targetHorizon-ряда.
  • Vault: релевантных заметок не найдено.
**🔍 week ревью 1 — верификация (ПОДТВЕРЖДЕНО)** **Текущее состояние кода:** `frontend/src/components/site-finder/analysis/ScenariosBlock.tsx:47-56` (`pickHorizonForecast`), `:58-69` (`deficitForScenario`), общий caption `:117-120`, подпись карточки `:182`. **Корень:** `pickHorizonForecast` при отсутствии точного совпадения по `targetHorizon` (стр.52 `forecasts.find(f => f.horizon_months === targetHorizon)`) возвращает forecast с САМЫМ ДЛИННЫМ горизонтом (стр.55 `[...forecasts].sort((a,b)=>b.horizon_months-a.horizon_months)[0]`). При этом `deficitForScenario` берёт `fc.deficit_index` (стр.66), а `ScenarioCard` жёстко подписывает значение целевым горизонтом — «Индекс дефицита · {targetHorizon} мес.» (стр.182), и caption утверждает «приведён на целевом горизонте {targetHorizon} мес.» (стр.118). Если у сценария нет ряда на `targetHorizon` (выбран 6 мес, есть только 12/18/24), число — за самый длинный горизонт под подписью «6 мес.». Срабатывает только в edge-case → medium корректна. **Что править:** - Вариант A: подписывать карточку фактическим горизонтом выбранного forecast — вернуть из `pickHorizonForecast` сам ряд и показывать `fc.horizon_months`, как `ForecastHorizonsBlock.tsx` (берёт реальный `f.horizon_months`). - Вариант B: при отсутствии точного горизонта в `deficitForScenario` не уходить в longest — использовать ТОЛЬКО `scenariosSummary[key]` (стр.67, по контракту = дефицит на целевом горизонте) либо вернуть null → карточка покажет «—» (ветка стр.196-197). **Что посмотреть:** - Код: `ScenariosBlock.tsx:47-69`, `:118`, `:182-197`; сравнить с `ForecastHorizonsBlock.tsx` (подпись из реального `f.horizon_months`); тест с forecasts без targetHorizon-ряда. - Vault: релевантных заметок не найдено.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#1469
No description provided.