fix(honesty): true competitors/neighbors totals + permits truncation disclosure (#2464 cluster B) #2471

Merged
bot-backend merged 1 commit from worktree-agent-a080ecae2420a5765 into main 2026-07-08 08:31:03 +00:00
Collaborator

Что

Волна 3 эпика #2464, кластер B (silent-caps) — счётчики, усечённые SQL-LIMIT'ом, выдавались за истинные итоги без раскрытия. Паттерн-референс: features_truncated (Epic #2445 A2).

Находка Было Стало
market_pulse.competitors_total (LIMIT 20) len(rows) ≤20 выдавался за «всего конкурентов в 3км»; coverage_pct считался от усечённого знаменателя добавлен unbounded COUNT(*) (та же latest_obj/ST_DWithin-популяция, без дорогих join'ов); coverage_pct от истинного итога; вынесено в _build_market_pulse (тестируемо)
neighbors_summary.count_buildings_100m (LIMIT 30) len(rows) neighbors_total CTE (без LIMIT) + neighbors_truncated
VelocityResult.competitors_count (LIMIT 200) нет флага competitors_truncated (True при len==200), проведён через все 4 конструктора вкл. rosreestr-fallback
permits-nearby (HTML и DOCX) молча [:10] «… и ещё N записей (полный список в веб-версии §6)» от честного unbounded total_count

Бонус-находка: neighbors дополнительно режется Python-срезом [:20] поверх SQL LIMIT 30 — neighbors_truncated сравнивается с реально показанной длиной (20), а не с промежуточными 30, чтобы флаг не был ложно-False на 21–30 соседях.

OpenAPI: схема НЕ меняется — market_pulse/neighbors_summary/velocity объявлены dict[str, Any] (loose); app.openapi() byte-identical до/после (проверено). Codegen не нужен.

Тесты

Новый test_market_pulse_and_neighbors_honesty.py (12) + velocity (4) + exporters HTML/DOCX (5) — истинный итог > cap → корректный total + флаг; boundary at/below cap; propagation через fallback; DOCX-disclosure. Починены 3 хрупких mock-теста с жёстким call_idx (новый COUNT сдвинул порядок; заодно схлопнут фантомный 2-в-1 mock-энтри _neighbors_summary — единый statement с #1130). tests/api/v1/ = 343 passed; exporters/velocity зелёные; ruff чисто.

Ревью

code-reviewer: APPROVE, ноль блокеров. Проверено: новый COUNT совпадает по популяции с усечённым запросом (coverage_pct корректен), cluster-A-безопасен (read-only, до записей, чистый 500 без poisoning), neighbors_total без row-multiplication, все 4 velocity-конструктора, honest-total в permits, корректность вставки mock-индекса. Два фолд-ина из ревью (DOCX-паритет + mock-cleanup) закрыты в этом PR.

Follow-up (не блокер, отдельно): competitors_count рендерится в UI (VelocityBlock.tsx:320) без раскрытия — frontend-задача показать competitors_truncated.

Refs #2464 (cluster B).

## Что Волна 3 эпика **#2464**, кластер **B (silent-caps)** — счётчики, усечённые SQL-LIMIT'ом, выдавались за истинные итоги без раскрытия. Паттерн-референс: `features_truncated` (Epic #2445 A2). | Находка | Было | Стало | |---|---|---| | `market_pulse.competitors_total` (LIMIT 20) | `len(rows)` ≤20 выдавался за «всего конкурентов в 3км»; `coverage_pct` считался от усечённого знаменателя | добавлен **unbounded `COUNT(*)`** (та же `latest_obj`/`ST_DWithin`-популяция, без дорогих join'ов); `coverage_pct` от истинного итога; вынесено в `_build_market_pulse` (тестируемо) | | `neighbors_summary.count_buildings_100m` (LIMIT 30) | `len(rows)` | `neighbors_total` CTE (без LIMIT) + `neighbors_truncated` | | `VelocityResult.competitors_count` (LIMIT 200) | нет флага | `competitors_truncated` (True при len==200), проведён через все 4 конструктора вкл. rosreestr-fallback | | permits-nearby (HTML **и DOCX**) | молча `[:10]` | «… и ещё N записей (полный список в веб-версии §6)» от честного unbounded `total_count` | **Бонус-находка:** `neighbors` дополнительно режется Python-срезом `[:20]` поверх SQL LIMIT 30 — `neighbors_truncated` сравнивается с **реально показанной** длиной (20), а не с промежуточными 30, чтобы флаг не был ложно-False на 21–30 соседях. **OpenAPI:** схема НЕ меняется — `market_pulse`/`neighbors_summary`/`velocity` объявлены `dict[str, Any]` (loose); `app.openapi()` byte-identical до/после (проверено). Codegen не нужен. ## Тесты Новый `test_market_pulse_and_neighbors_honesty.py` (12) + velocity (4) + exporters HTML/DOCX (5) — истинный итог > cap → корректный total + флаг; boundary at/below cap; propagation через fallback; DOCX-disclosure. Починены 3 хрупких mock-теста с жёстким `call_idx` (новый COUNT сдвинул порядок; заодно схлопнут фантомный 2-в-1 mock-энтри `_neighbors_summary` — единый statement с #1130). `tests/api/v1/` = **343 passed**; exporters/velocity зелёные; ruff чисто. ## Ревью code-reviewer: **✅ APPROVE**, ноль блокеров. Проверено: новый COUNT совпадает по популяции с усечённым запросом (coverage_pct корректен), **cluster-A-безопасен** (read-only, до записей, чистый 500 без poisoning), `neighbors_total` без row-multiplication, все 4 velocity-конструктора, honest-total в permits, корректность вставки mock-индекса. Два фолд-ина из ревью (DOCX-паритет + mock-cleanup) закрыты в этом PR. **Follow-up (не блокер, отдельно):** `competitors_count` рендерится в UI (`VelocityBlock.tsx:320`) без раскрытия — frontend-задача показать `competitors_truncated`. Refs #2464 (cluster B).
bot-backend added 1 commit 2026-07-08 08:04:19 +00:00
fix(honesty): true totals for competitors/neighbors + permits truncation disclosure (#2464 cluster B)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m5s
CI / backend-tests (pull_request) Successful in 15m17s
47b3fffc99
market_pulse.competitors_total and neighbors_summary.count_buildings_100m
were silently capped by their SQL LIMITs (20/30) and reported as true totals;
add unbounded COUNT(*) for both (coverage_pct now uses the true denominator),
matching the features_truncated pattern from epic #2445 A2. VelocityResult
gains competitors_truncated for its LIMIT-200 query. The permits-nearby table
in both the HTML and DOCX full-report exporters now discloses "… и ещё N
записей" instead of silently hard-slicing to 10. neighbors_truncated compares
against the actually-returned (20) list, catching a second Python-side [:20]
slice beyond the SQL LIMIT 30.

Refs #2464 (cluster B silent-caps).
bot-backend merged commit f987e819b2 into main 2026-07-08 08:31:03 +00:00
bot-backend deleted branch worktree-agent-a080ecae2420a5765 2026-07-08 08:31:04 +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#2471
No description provided.