fix(forecasting): honest scenarios collapse when β rate-sensitivity gate fails (audit #1871 P1) #1873

Merged
bot-backend merged 1 commit from audit/1871-p1-scenarios-collapse into main 2026-06-22 14:17:33 +00:00
Collaborator

Summary

Audit-issue #1871 пункт P1.1 — §22 сценарии идентичны.

При sensitivity.confidence=='low' (ЕКБ-регрессия не проходит gate n≥30/R²≥0.1/slope<0) demand_normalization возвращает coefficient=1.0 для всех трёх сценариев. По дизайну. Все 393 прод-отчёта в этом состоянии — фронт рисует 3 разноцветные карточки/линии одинаковых чисел без caveat.

Variant A (минимальный честный фикс): backend детектирует collapse → пробрасывает флаг → 3 UI-поверхности + PDF/Excel схлопываются в одну карточку «Базовый» + caveat с причиной полным предложением.

Изменения

  • Backend (8 файлов, +584): compute_scenarios сигнатура → tuple; _detect_collapsed использует math.isclose на обеих метриках (demand+deficit) по всем горизонтам; ReportScenarios расширен 2 полями; PDF/Excel экспортёры схлопываются.
  • Frontend (7 файлов, +234/-48): тип ReportScenarios? расширен; collapse-guard в ScenariosBlock / ScenarioCards / ScenarioCompareTable (null) / ForecastChart (одна линия) / Section6Forecast (caveat выше графика).
  • demand_normalization.py НЕ ТРОНУТ — поведение корректно по контракту.
  • Caveat читается исключительно из scenarios.scenarios_collapse_reason — backend единственный источник истины.

Тесты

  • 18 новых тестов (TestMetricsEqual×6, TestDetectCollapsed×7, TestComputeScenariosCollapseDetection×4+).
  • pytest forecasting: 102 passed.
  • pytest cross-module (test_forecast_request_cache, test_parcels_forecast, test_forecast worker, test_orchestrator): 88 passed, 1 skipped (WeasyPrint env).
  • ruff , mypy (наш scope) , tsc , lint .
  • code-reviewer APPROVE.

Test plan

  • tsc + lint clean
  • pytest зелёный
  • code-review APPROVE
  • прод-QA: открыть analysis-страницу с реальным §22-отчётом → одна карточка + caveat (light), одна dark-карточка + collapse-note + scrollbar (dark)
  • прод-QA: ForecastChart показывает одну линию vs три

Follow-ups (вне scope этого PR)

  • report_md.py / report_docx.py / report_pptx.py — collapse-aware рендеринг не сделан (не было в скоупе P1; добавить отдельным PR чтобы chat-export/DOCX/PPTX не нарушали честность контракта).
  • Pre-existing mypy Unused type: ignore в scenarios.py:384 — сместился со старой строки, не введён этим PR.

Refs #1871
🤖 Generated with Claude Code

## Summary Audit-issue #1871 пункт **P1.1 — §22 сценарии идентичны**. При `sensitivity.confidence=='low'` (ЕКБ-регрессия не проходит gate n≥30/R²≥0.1/slope<0) `demand_normalization` возвращает `coefficient=1.0` для всех трёх сценариев. По дизайну. Все 393 прод-отчёта в этом состоянии — фронт рисует 3 разноцветные карточки/линии одинаковых чисел без caveat. Variant A (минимальный честный фикс): backend детектирует collapse → пробрасывает флаг → 3 UI-поверхности + PDF/Excel схлопываются в **одну карточку «Базовый» + caveat** с причиной полным предложением. ## Изменения - **Backend** (8 файлов, +584): `compute_scenarios` сигнатура → tuple; `_detect_collapsed` использует `math.isclose` на обеих метриках (demand+deficit) по всем горизонтам; `ReportScenarios` расширен 2 полями; PDF/Excel экспортёры схлопываются. - **Frontend** (7 файлов, +234/-48): тип `ReportScenarios?` расширен; collapse-guard в `ScenariosBlock` / `ScenarioCards` / `ScenarioCompareTable` (null) / `ForecastChart` (одна линия) / `Section6Forecast` (caveat выше графика). - `demand_normalization.py` НЕ ТРОНУТ — поведение корректно по контракту. - Caveat читается **исключительно** из `scenarios.scenarios_collapse_reason` — backend единственный источник истины. ## Тесты - 18 новых тестов (TestMetricsEqual×6, TestDetectCollapsed×7, TestComputeScenariosCollapseDetection×4+). - pytest forecasting: **102 passed**. - pytest cross-module (test_forecast_request_cache, test_parcels_forecast, test_forecast worker, test_orchestrator): **88 passed, 1 skipped** (WeasyPrint env). - ruff ✅, mypy (наш scope) ✅, tsc ✅, lint ✅. - code-reviewer ✅ APPROVE. ## Test plan - [x] tsc + lint clean - [x] pytest зелёный - [x] code-review APPROVE - [ ] прод-QA: открыть analysis-страницу с реальным §22-отчётом → одна карточка + caveat (light), одна dark-карточка + collapse-note + scrollbar (dark) - [ ] прод-QA: ForecastChart показывает одну линию vs три ## Follow-ups (вне scope этого PR) - `report_md.py` / `report_docx.py` / `report_pptx.py` — collapse-aware рендеринг не сделан (не было в скоупе P1; добавить отдельным PR чтобы chat-export/DOCX/PPTX не нарушали честность контракта). - Pre-existing mypy `Unused type: ignore` в `scenarios.py:384` — сместился со старой строки, не введён этим PR. Refs #1871 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bot-backend added 1 commit 2026-06-22 14:06:27 +00:00
fix(forecasting): honest scenarios collapse when β rate-sensitivity gate fails (audit #1871 P1)
All checks were successful
CI / changes (pull_request) Successful in 7s
CI / frontend-tests (pull_request) Successful in 1m17s
CI / openapi-codegen-check (pull_request) Successful in 2m11s
CI / backend-tests (pull_request) Successful in 10m19s
dcccebf2c2
Когда demand_normalization honesty-gate отбивает β rate-sensitivity
(`sensitivity.confidence=='low'`, ЕКБ-регрессия не проходит n≥30/R²≥0.1/slope<0),
все 393 прод-отчёта получают coefficient=1.0 для всех трёх сценариев
conservative/base/aggressive. По дизайну backend честно деградирует, но фронт
рисует 3 разноцветные карточки/линии одинаковых чисел без caveat.

Backend (scenarios.py, report.py, report_assembler.py, orchestrator.py):
- compute_scenarios теперь возвращает (list, collapsed: bool, reason: str|None)
- _detect_collapsed(): math.isclose(rel_tol=1e-9, abs_tol=1e-6) сравнивает
  projected_demand_units И deficit_index на всех горизонтах между
  conservative и aggressive — расхождение в любой метрике на любом h → False
- _COLLAPSE_REASON_LOW_BETA — единственный источник истины каноничного
  русского предложения
- ReportScenarios получает поля scenarios_collapsed + scenarios_collapse_reason
- demand_normalization.py НЕ ТРОНУТ — поведение корректно по контракту

Backend экспортёры (report_pdf.py, excel.py):
- PDF: при collapse — тёмная плашка-параграф вместо таблицы трёх столбцов
- Excel: ОДНА строка «base» + caveat-ячейка вместо трёх идентичных столбцов

Frontend (forecast.ts, ScenariosBlock, ScenarioCards, ScenarioCompareTable,
ForecastChart, Section6Forecast, ptica.module.css):
- Тип ReportScenarios расширен двумя optional-полями
- ScenariosBlock (light): headline-bar + одна grid-карточка base + caveat
- ScenarioCards (ptica dark): sub-component ScenarioBaseCard, одна карточка
  + collapse-note, ScenarioCompareTable return null
- ForecastChart: effectiveScenarios filter — одна линия viz-1 «Базовый»
  вместо трёх перекрывающихся
- Section6Forecast: caveat выше графика, читается ТОЛЬКО из
  scenarios_collapse_reason (источник истины — backend constant)

Тесты: +18 новых (TestMetricsEqual×6, TestDetectCollapsed×7,
TestComputeScenariosCollapseDetection×4 + 1 проверка graceful + corner).
102 forecasting passed, 88 cross-module passed.

Refs #1871
bot-backend merged commit fd107aeadb into main 2026-06-22 14:17:33 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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#1873
No description provided.