[Fable 5][P3] §15 confidence_engine: advisory-cap сообщение дублируется в rationale при all-high (самый частый advisory-случай) #1596

Closed
opened 2026-06-16 17:14:48 +00:00 by bot-backend · 0 comments
Collaborator

Severity: low · Категория: ложь/мусор в отчёте (report quality) · Файл: backend/app/services/forecasting/confidence_engine.py:294-303, 326-341, 466-474

Файл: backend/app/services/forecasting/confidence_engine.py:331-336 (ветка advisory) совместно с 328-329 (_dragging_factors) и 467-474 (добавление _F_ADVISORY_CAP).

Симптом (воспроизведено запуском): при advisory=True (дефолт, «почти всегда» по docstring строка 407) и всех факторах качества 'high' raw_level='high', cap опускает до 'medium', advisory_capped=True. В этот момент в factors добавляется искусственный фактор _F_ADVISORY_CAP с level='medium' и note='прогноз советующий — уровень ограничен «medium»'. В _build_rationale ветка advisory_capped and level == _ADVISORY_CEILING (строка 331) формирует base='Medium: прогноз советующий (не провалидирован) — уровень ограничен «medium»', а затем notes=_dragging_factors(factors,'medium') (строки 328-329) возвращает ровно этот же advisory_cap-фактор (единственный medium в наборе), и строка 335 добавляет '; также '+ та же фраза. Итог: rationale='Medium: прогноз советующий (не провалидирован) — уровень ограничен «medium»; также прогноз советующий — уровень ограничен «medium».'

Почему всегда дубль: advisory_capped=True достижим (строка 464) только при raw_level='high', а _aggregate возвращает 'high' лишь когда ВСЕ data-факторы 'high'. Значит реальных medium-факторов в этот момент нет — «также …» (задуманное для ПЕРЕЧИСЛЕНИЯ ДРУГИХ тянущих-вниз факторов) всегда содержит лишь сам cap-фактор. Контрпример подтверждает: при одном реальном medium-факторе raw уже medium, advisory_capped=False, и cap-фактор вовсе не добавляется.

Импакт: rationale ложится в слот ReportConfidence отчёта #987 (клиентский §15-вывод, читается экспортёрами/чатом). Дефект текстовый — корявая фраза с повтором в самом частом (all-high data) благоприятном случае; уровень/числа не искажаются, P3.

Фикс: в advisory-ветке (строки 331-336) собирать «также»-ноты ТОЛЬКО из РЕАЛЬНЫХ тянущих-вниз факторов, исключив _F_ADVISORY_CAP. Варианты: (а) notes = [f.note for f in _dragging_factors(factors, level) if f.name != _F_ADVISORY_CAP]; либо (б) вычислять drag/notes до добавления advisory_cap-фактора (передавать в _build_rationale список без него), либо вообще не append'ить _F_ADVISORY_CAP, раз advisory_capped уже несёт эту информацию. Дополнить тест: проверить rationale.count('советующий')==1 для all-high advisory-кейса.

Почему баг: _dragging_factors(factors, level) включает любой фактор с level==итоговому, в т.ч. сам advisory_cap-фактор (level='medium'). «также …» в advisory-ветке задумано для ПЕРЕЧИСЛЕНИЯ ДРУГИХ тянущих-вниз факторов (реальных проблем с данными), а не для повтора самого cap-сообщения. Поскольку advisory_capped=True достижимо только при raw_level='high' (т.е. когда все данные-факторы 'high'), реальных medium-факторов там не бывает — «также» ВСЕГДА содержит ровно дубль. Это не косметика: rationale уходит в слот ReportConfidence отчёта #987 (клиентский §15-вывод) и читается экспортёрами/чатом. Тест test_advisory_cap_spoken_in_rationale вызывает _build_rationale вручную со списком БЕЗ _F_ADVISORY_CAP-фактора, а test_advisory_never_high проверяет лишь "советующий" in rationale — оба зелёные при дубле, поэтому дефект пережил аудит.

Как чинить: Исключить _F_ADVISORY_CAP из перечисления виновников. Либо в _dragging_factors добавить фильтр f.name != _F_ADVISORY_CAP, либо в _build_rationale внутри advisory-ветки строить notes только из факторов, чей name != _F_ADVISORY_CAP (drag = [f for f in _dragging_factors(...) if f.name != _F_ADVISORY_CAP]). После фикса all-high→medium rationale = «Medium: прогноз советующий (не провалидирован) — уровень ограничен «medium».» без хвоста «; также …». Добавить тест на полный путь через compute_report_confidence(all-high, advisory=True), проверяющий, что подстрока «также» отсутствует.


deep-audit backend v2 (после PR #1543) · verify+harden, confidence 0.95 · unit B25

**Severity:** low · **Категория:** ложь/мусор в отчёте (report quality) · **Файл:** `backend/app/services/forecasting/confidence_engine.py:294-303, 326-341, 466-474` Файл: backend/app/services/forecasting/confidence_engine.py:331-336 (ветка advisory) совместно с 328-329 (_dragging_factors) и 467-474 (добавление _F_ADVISORY_CAP). Симптом (воспроизведено запуском): при advisory=True (дефолт, «почти всегда» по docstring строка 407) и всех факторах качества 'high' raw_level='high', cap опускает до 'medium', advisory_capped=True. В этот момент в factors добавляется искусственный фактор _F_ADVISORY_CAP с level='medium' и note='прогноз советующий — уровень ограничен «medium»'. В _build_rationale ветка `advisory_capped and level == _ADVISORY_CEILING` (строка 331) формирует base='Medium: прогноз советующий (не провалидирован) — уровень ограничен «medium»', а затем notes=_dragging_factors(factors,'medium') (строки 328-329) возвращает ровно этот же advisory_cap-фактор (единственный medium в наборе), и строка 335 добавляет '; также '+ та же фраза. Итог: rationale='Medium: прогноз советующий (не провалидирован) — уровень ограничен «medium»; также прогноз советующий — уровень ограничен «medium».' Почему всегда дубль: advisory_capped=True достижим (строка 464) только при raw_level='high', а _aggregate возвращает 'high' лишь когда ВСЕ data-факторы 'high'. Значит реальных medium-факторов в этот момент нет — «также …» (задуманное для ПЕРЕЧИСЛЕНИЯ ДРУГИХ тянущих-вниз факторов) всегда содержит лишь сам cap-фактор. Контрпример подтверждает: при одном реальном medium-факторе raw уже medium, advisory_capped=False, и cap-фактор вовсе не добавляется. Импакт: rationale ложится в слот ReportConfidence отчёта #987 (клиентский §15-вывод, читается экспортёрами/чатом). Дефект текстовый — корявая фраза с повтором в самом частом (all-high data) благоприятном случае; уровень/числа не искажаются, P3. Фикс: в advisory-ветке (строки 331-336) собирать «также»-ноты ТОЛЬКО из РЕАЛЬНЫХ тянущих-вниз факторов, исключив _F_ADVISORY_CAP. Варианты: (а) notes = [f.note for f in _dragging_factors(factors, level) if f.name != _F_ADVISORY_CAP]; либо (б) вычислять drag/notes до добавления advisory_cap-фактора (передавать в _build_rationale список без него), либо вообще не append'ить _F_ADVISORY_CAP, раз advisory_capped уже несёт эту информацию. Дополнить тест: проверить rationale.count('советующий')==1 для all-high advisory-кейса. **Почему баг:** _dragging_factors(factors, level) включает любой фактор с level==итоговому, в т.ч. сам advisory_cap-фактор (level='medium'). «также …» в advisory-ветке задумано для ПЕРЕЧИСЛЕНИЯ ДРУГИХ тянущих-вниз факторов (реальных проблем с данными), а не для повтора самого cap-сообщения. Поскольку advisory_capped=True достижимо только при raw_level='high' (т.е. когда все данные-факторы 'high'), реальных medium-факторов там не бывает — «также» ВСЕГДА содержит ровно дубль. Это не косметика: rationale уходит в слот ReportConfidence отчёта #987 (клиентский §15-вывод) и читается экспортёрами/чатом. Тест test_advisory_cap_spoken_in_rationale вызывает _build_rationale вручную со списком БЕЗ _F_ADVISORY_CAP-фактора, а test_advisory_never_high проверяет лишь `"советующий" in rationale` — оба зелёные при дубле, поэтому дефект пережил аудит. **Как чинить:** Исключить _F_ADVISORY_CAP из перечисления виновников. Либо в _dragging_factors добавить фильтр `f.name != _F_ADVISORY_CAP`, либо в _build_rationale внутри advisory-ветки строить notes только из факторов, чей name != _F_ADVISORY_CAP (drag = [f for f in _dragging_factors(...) if f.name != _F_ADVISORY_CAP]). После фикса all-high→medium rationale = «Medium: прогноз советующий (не провалидирован) — уровень ограничен «medium».» без хвоста «; также …». Добавить тест на полный путь через compute_report_confidence(all-high, advisory=True), проверяющий, что подстрока «также» отсутствует. --- <sub>deep-audit backend v2 (после PR #1543) · verify+harden, confidence 0.95 · unit B25</sub>
bot-backend added the
week ревью 1
label 2026-06-16 17:14:48 +00:00
Sign in to join this conversation.
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#1596
No description provided.