EPIC: exhaustive line-by-line аудит backend ПТИЦЫ (2026-07-07) — 89 подтверждённых находок #2464

Open
opened 2026-07-07 11:46:44 +00:00 by bot-backend · 0 comments
Collaborator

Мульти-агентный исчерпывающий line-by-line ревью всего backend/app/** (263 файла, 90 639 строк, 45 чанков). Каждый файл прочитан целиком; каждая находка проверена 2 независимыми скептиками (adversarial verify).

89 подтверждённых (из 140 кандидатов, 51 отсеян). Сгруппировано по паттернам — фиксить волнами.

A. Session-poisoning (нет rollback/SAVEPOINT) (16)

  • [C] backend/app/services/site_finder/developer_attribution.py:153 — get_developer_attribution swallows OperationalError/ProgrammingError/DataError (and generic Exception) from db.execute() without calling db.rollback(), leaving the shared request-scoped Session's transaction in Postgres's aborted state.
  • [H] backend/app/services/forecasting/orchestrator.py:127 — _safe_call swallows any exception from a §9.x layer without db.rollback(), so a real DB-level error in one layer poisons the shared SQLAlchemy Session for every later layer that reuses the same db object.
  • [H] backend/app/services/forecasting/macro_series.py:401 — _query_mortgage_monthly loops over 5 mortgage fields on the same db Session and swallows exceptions per-field without db.rollback(), so one field's DB error cascades and empties out all later fields in the same call, contradicting its own 'graceful: сбой одного ряда не валит остальные' docstring.
  • [H] backend/app/services/forecasting/special_indices.py:1661_run() (the shared try/except wrapper around all six §25 index builders) swallows any exception from db.execute(...) without calling db.rollback(), even though all six builders share ONE SQLAlchemy Session for the whole report (per forecast_request_cache.py docstring: one Session per §22 report / Celery task).
  • [H] backend/app/services/forecasting/sales_series.py:571_query_source_a and _query_source_b catch any db.execute exception and return {} without calling db.rollback(), on a Session that is explicitly documented as shared/reused across many build_sales_series calls within one report (module docstring: 'db не в ключе (одна сессия на отчёт)').
  • [H] backend/app/services/job_settings.py:142 — get_all()/get_one() catch DB exceptions and return fallback data without calling db.rollback(), leaving the caller-supplied SQLAlchemy session in an aborted-transaction state.
  • [H] backend/app/services/site_finder/connection_capacity_lookup.py:355 — Gas/heat/gas-outlet query helpers catch DB exceptions with a bare except Exception and never call db.rollback() / use a SAVEPOINT, unlike the sibling _query_nearby_network_zones (which correctly wraps its query in db.begin_nested()). Once one of these statements fails, the SQLAlchemy session is left in a failed-transaction state, so every subsequent query on the same db in the same call poisons and fails too — and gets misattributed to the wrong cause.
  • [H] backend/app/services/site_finder/competitors.py:801 — The three sequential post-competitor DB lookups (avg_price, sold-count, objective price fallback) each wrap their db.execute in a bare except Exception with no rollback/SAVEPOINT, so a failure in an earlier one leaves the session's transaction aborted and silently poisons every later query on the same db, which then also fails and is misreported as 'query failed, continuing without X'.
  • [H] backend/app/services/site_finder/saturation.py:241 — compute_district_saturation() catches a bare Exception around db.execute() and returns None without rolling back, poisoning the shared request Session.
  • [H] backend/app/services/site_finder/supply_layers.py:576 — _safe_rows() catches Exception around db.execute() and returns [] without db.rollback(); compute_all_layers() calls it 3 times in a row (L1, L2, L3) on the same Session, so one failed layer silently zeroes out the other two as well.
  • [H] backend/app/services/site_finder/zone_regulation.py:468 — backfill_ekb_zone_regulations never rolls back the shared db session after a failed commit or other DB-level exception, unlike the sibling get_or_fetch_zone_regulation path which does write_db.rollback() in its except block.
  • [M] backend/app/services/cadastre/bulk_harvest.py:554 — backfill_parcel_geom's per-quarter except Exception (line 554) would itself swallow a NspdBulkWafError propagated from _grid_walk_category, contradicting its own adjacent comment (lines 556-557) that claims 'WAF 403 пробросится из client и прервёт прогон — это ожидаемо'.
  • [M] backend/app/services/exporters/full_report_pdf.py:217_get_connection_capacity's except block also omits db.rollback() after a DB-backed lookup failure, which can silently force _generate_concept_result's market-price lookup into the class_norm fallback even when a fresh session would have succeeded.
  • [M] backend/app/services/site_finder/pat_lookup.py:49 — Caught OperationalError/ProgrammingError is logged and swallowed without a db.rollback(), leaving the shared SQLAlchemy Session's transaction in an aborted state for any subsequent query on that same session within the request.
  • [M] backend/app/workers/tasks/scrape_objective.py:252 — _save_raw()'s INSERT (lines 89-146) is called outside of any except clause that catches general exceptions — the surrounding per-job try only catches ObjectiveAuthError/ObjectiveAPIError (line 370) — so a DB-level failure there propagates to the outer handler, where _finish_run's own DB call on the now-poisoned session is swallowed by a bare except Exception: pass (lines 403-409), silently leaving the run row stuck at status='running' forever.
  • [L] backend/app/services/scrapers/nspd_denorm.py:327 — denorm_dump()'s docstring states the caller is responsible for commit/close, but the function itself unconditionally calls db.commit() at line 373 (confirmed by the test suite asserting db.commit.assert_called_once()), directly contradicting the documented contract.

B. Silent caps (усечение как итог) (10)

  • [H] backend/app/api/v1/parcels.py:3687market_pulse.competitors_total is len(competitor_rows), but competitor_rows comes from a SQL query with LIMIT 20 (line 2251). The field name implies the total number of competitors within 3km, but it is really 'up to 20 nearest', and it is also used to compute coverage_pct = competitors_priced * 100 / competitors_total, with no truncation flag disclosing the cap.
  • [H] backend/app/services/cadastre/bulk_harvest.py:402 — _grid_walk_category's blanket except Exception (line 402-414) swallows NspdBulkWafError/NspdBulkRateLimitError instead of re-raising, contradicting harvest_quarter's documented 'Raises: NspdBulkWafError ... caller не retry' contract and inconsistent with the codebase's own corrected pattern in the sibling get_features_in_bbox_grid (nspd_bulk_client.py:519-529, explicitly labeled 'Issue #252-mirror') and in search_by_quarter (nspd_bulk_client.py:305, which explicitly re-raises NspdBulkWafError/RateLimitError/ServerError).
  • [H] backend/app/services/etl/objective_backfill.py:469 — Ambiguous-candidate resolution (core-pass and geo-pass) never excludes objective_complex_name values already taken in objective_complex_mapping, so a core that has one already-mapped candidate and one genuinely-available candidate is misclassified as ambiguous / can resolve to the wrong (doomed) candidate instead of the available one.
  • [H] backend/app/services/site_finder/eesk_reserve_loader.py:239 — load_ps_35_220 writes a numeric load-percentage string into power_supply_centers.load_index via COALESCE(load_index, CAST(:load_pct AS text)), but that column is a categorical enum ('open'|'limited'|'closed'|NULL per data/sql/180_connection_capacity.sql:35) populated elsewhere by rosseti_wfs_loader._map_load_index with exactly those three string values.
  • [M] backend/app/api/v1/parcels.py:965neighbors_summary.count_buildings_100m is len(neighbor_rows), where neighbor_rows comes from the neighbors CTE in _NEIGHBORS_SUMMARY_SQL which has LIMIT 30 (line 858) — same LIMIT-capped-count-presented-as-total pattern as the permits/competitors findings above, with no truncation flag.
  • [M] backend/app/services/exporters/full_report_html.py:1517 — _build_permits_nearby hard-slices items to the first 10 rows without checking/surfacing the upstream items_truncated flag or adding a "and N more" disclosure, unlike every other capped table in this same file.
  • [M] backend/app/services/scrapers/nspd_client.py:814 — search_by_quarter docstring says step 3 fetches 'каждого core layer' via get_features_in_bbox and prices the whole call at 6/11/22 requests (~3.6s/6.6s/13s), but 3 of the 5 core layers plus all zouit/risk layers actually go through get_features_in_bbox_grid at grid_n=7 (49 requests each per that function's own docstring at line ~544), making the true request count roughly 25-50x higher than documented
  • [M] backend/app/services/site_finder/poi_score.py:141 — compute_poi_weighted_top7 (and similarly compute_poi_routing_decay at line 328) selects candidate POIs by pure nearest-distance SQL LIMIT before applying the category-weighted ranking, so a genuinely higher-weighted-but-farther POI can be excluded from the candidate window entirely.
  • [M] backend/app/services/site_finder/velocity.py:213 — The competitor query is capped with LIMIT 200 (line 213) and n_comps = len(comp_rows) (line 326) is then reported as competitors_count in every VelocityResult, with no truncation flag — unlike the analogous fix this same codebase already applied elsewhere (utility_infrastructure_loader.py's features_truncated, explicitly citing Epic #2445 A2's exact anti-pattern of a LIMIT-capped count masquerading as the true total).
  • [L] backend/app/services/etl/objective_backfill.py:697 — GeoReject docstring enumerates reason values as 'no_address' | 'no_geocode' | 'too_far' | 'ambiguous_multi' | 'call_limit', but the code also emits reason='partial_geocode' (line 918), which is undocumented and omitted from the enumerated set.

C. Неэффективный timeout-guard (3)

  • [H] backend/app/api/v1/admin_scrape.py:194 — queue_status's ThreadPoolExecutor with block still blocks on shutdown(wait=True) even after the code 'gives up' via result(timeout=...), so the documented ~600ms worst-case latency guarantee is false.
  • [H] backend/app/api/v1/photos.py:110 — DB session (checked out from the connection pool via Depends(get_db)) is held open for the entire duration of the synchronous upstream HTTP fetch to DOM.РФ.
  • [H] backend/app/services/exporters/report_maps.py:149_add_basemap's timeout protection is ineffective: with ThreadPoolExecutor(...) as pool: calls shutdown(wait=True) on exit even after .result(timeout=...) times out, so the function can still block indefinitely on a hung tile fetch.

D. Price без sanity-фильтра (3)

  • [H] backend/app/api/v1/parcels.py:2209obj_pricing CTE computes avg_price_per_m2_rub as a raw AVG(oll.price_per_m2_rub) with no sanity/outlier filter, unlike the sibling district_price_block (line 2902, BETWEEN 30000 AND 600000) and market_trend (line 2983, BETWEEN 30000 AND 500000) queries in the same file that guard against bad scraped prices.
  • [M] backend/app/api/v1/parcels.py:3383geo_radius_price median query (percentile_cont(0.5) over objective_lots.price_per_m2_rub) has no price sanity filter, unlike the near-identical district_price_block query above it (line 2902) which bounds prices to 30000-600000.
  • [L] backend/app/services/site_finder/parcel_financial.py:152synthesize_teap_from_buildability never validates that max_building_pct is within a sane 0-100 range (or that max_far/max_floors are plausible) before using it to derive built area and GFA, so a bad upstream zoning value silently produces a physically impossible result presented as a normal financial figure.

E. snapshot-инфляция счётчиков (3)

  • [H] backend/app/services/analytics_queries.py:1799 — _active_competitors_count()'s _q() helper does SELECT COUNT(*) FROM domrf_kn_objects WHERE region_cd=:rc AND site_status='Строящиеся' ... with no snapshot_date filter and no DISTINCT ON obj_id, so it counts every retained historical snapshot row per object, not distinct objects.
  • [H] backend/app/services/analytics_queries.py:509 — developer_portfolio() selects from domrf_kn_objects filtered only by dev_id, with no snapshot_date filter or DISTINCT ON obj_id, so it returns every retained historical snapshot of each project as a separate row.
  • [H] backend/app/services/scrapers/domrf_kn.py:537 — UPSERT_OBJECT_SQL's ON CONFLICT (obj_id, snapshot_date) DO UPDATE SET omits problem_flag, green_house, floor_min, floor_max, hobj_id, short_addr, dev_inn and region_cd, even though all eight are populated on INSERT (lines 498-516) and freshly computed every call by _norm_object.

F. ON CONFLICT пропускает колонки (1)

  • [M] backend/app/services/scrapers/domrf_kn.py:923 — UPSERT_PHOTO_SQL's ON CONFLICT (obj_id, obj_file_id) DO UPDATE SET updates ord_num/photo_url/photo_dttm/period_dt/size_bytes/photo_name/ready_desc/hidden but omits build_type, which is part of the INSERT column list (line 916) and populated every call from p.get('objBuildTypeShortDesc').

G. Прочие корректность/данные (31)

  • [H] backend/app/api/v1/parcels.py:2381 — The noise-source query (step 7) has no WHERE source_type IN (...) filter, so it pulls every row from osm_noise_sources_ekb within 2km — including 'water' and 'utility' rows that the very same file queries separately (with explicit source_type = 'water' / = 'utility' filters) for the hydrology and utilities blocks a few dozen lines below.
  • [H] backend/app/services/exporters/full_report_html.py:460 — _build_zoning's legacy-zoning fallback validates data on the zoning dict but then builds the table by reading from the still-empty nspd_zoning dict, silently discarding real fallback zoning data.
  • [H] backend/app/services/scrapers/domrf_catalog.py:80 — Status keyword regex has no negation guard, so Russian negated forms ("нереализованных", "непроданных" — meaning UNSOLD/available) substring-match the sold keywords and get classified as SOLD.
  • [H] backend/app/services/scrapers/domrf_catalog_object.py:440 — The entire batch runs inside one long-lived, uncommitted outer transaction with no interim commits — a late failure (e.g. BrowserSession.aexit raising, or any unhandled exception after the loop) discards every already-succeeded per-object UPDATE.
  • [H] backend/app/services/scrapers/nspd_client.py:610 — get_features_in_bbox_grid swallows every per-cell HTTP exception (return_exceptions=True + logger.warning + continue) and never raises, so a layer-wide WAF ban / outage produces an empty feature list indistinguishable from a genuine 'no zones here' result
  • [H] backend/app/services/scrapers/page_reservation_parser.py:129 — _detect_kind classifies the whole document by unanchored substring search for 'резервир' before 'изъят' anywhere in the full text, not by which act type the document actually is.
  • [H] backend/app/services/site_finder/best_layouts.py:195 — avg_area_m2 in _INLINE_VELOCITY_SQL is COALESCE(...,0) when all contributing deals rows have NULL deals_total_avg_area_m2, and this 0 is then silently treated downstream as a genuine '<25 м²' apartment area instead of 'area unknown'.
  • [H] backend/app/services/site_finder/quarter_dump_lookup.py:767 — _get_risk_zones computes ST_Intersection with BOTH operands pre-cast to ::geography, reintroducing the exact PostGIS 3.4 'geography×geography ST_Intersection transform error' bug that the sibling _get_red_lines function in the SAME file explicitly documents and avoids (there, ST_Intersection runs in planar geometry and only the result is cast to ::geography, per the comment at lines 967-970).
  • [H] backend/app/services/site_finder/velocity.py:167 — class_filter references alias o. inside the latest_obj CTE, but that CTE's FROM clause (FROM domrf_kn_objects, line 189) has no alias o — the alias o is only introduced later by the outer query (FROM latest_obj o, line 206).
  • [M] backend/app/api/v1/admin_cadastre.py:83 — manual_list validation checks non-empty BEFORE stripping whitespace, so an all-whitespace quarters list silently bypasses the intended 400 error and creates a zero-target job.
  • [M] backend/app/api/v1/admin_leads.py:152 — revenue_total and deals_total in the /stats KPI response are actually scoped to the months window (via window_leads CTE), not all-time totals, despite being named identically in style to leads_total which genuinely is all-time — a consumer trusting the '_total' suffix will display a partial-window figure as the grand total.
  • [M] backend/app/api/v1/admin_scrape.py:1096 — cancel_geo_job always returns {"cancelled": true} even when the UPDATE matched zero rows (nonexistent job_id or job already in a terminal state), silently misreporting success.
  • [M] backend/app/services/cadastre/bulk_harvest.py:1464 — _save_territorial_zones falls back to a synthetic zone_id = md5(sorted properties) when NSPD's WMS feature carries no id/zone_id anywhere (properties or top-level feature id); two geometrically-distinct zones sharing identical properties collide on this hash and one polygon silently overwrites the other via ON CONFLICT (zone_id) DO UPDATE.
  • [M] backend/app/services/exporters/full_report_docx.py:288 — ЗОУИТ-reconciliation sets zouit_count = len(overlaps) (count of overlap/border records), but the KV row is labeled "Кол-во типов ЗОУИТ" (count of distinct ZOUIT TYPES) — the correct value is len(zouit_types).
  • [M] backend/app/services/scrapers/domrf_catalog_object.py:460 — No circuit breaker on repeated WafBlockedError: once the DOM.РФ WAF starts blocking the session (the exact scenario referenced in the anti-ban comment at lines 444-450, incident #2443), the loop keeps sending one live request per remaining obj_id to the already-banned session instead of aborting the batch.
  • [M] backend/app/services/scrapers/ekb_geoportal_client.py:217_get_feature treats an HTTP-200 response whose JSON body lacks a usable "features" list identically to a genuine "no features found at this location" — with no logging — so a GeoServer WFS error/degenerate response (common failure mode: CQL/typeName errors returned as HTTP 200 with an OWS ExceptionReport-shaped JSON) is silently reported as "this parcel has no PZZ zone/ЗОУИТ/КРТ here" instead of "the query failed".
  • [M] backend/app/services/scrapers/stealth.py:293 — download_binary has no retry/backoff on transient failures (429/5xx), unlike get_json which retries up to 5 times with exponential backoff under the same WAF.
  • [M] backend/app/services/site_finder/best_layouts.py:1113 — The objects_total_in_radius field means two different things across the two 'empty response' branches: line 1113 reports the pre-exclude/pre-filter group count, while line 1194 (a few dozen lines later, same function, same field name) reports the post-exclude/post-filter group count.
  • [M] backend/app/services/site_finder/best_layouts.py:402 — _SUPPLY_ONLY_LOTS_SQL's area_bin CASE bucket for on_sale lots maps area_pd IS NULL into the same '<25' bucket as genuinely tiny (<25 m²) apartments, rather than excluding/flagging lots with missing area data.
  • [M] backend/app/services/site_finder/competitors.py:553 — _SOLD_COUNT_SQL's mapped CTE only sources from objective_complex_mapping, omitting the nearest_cx spatial/name gap-fill branch that both the velocity CTE (in _COMPETITORS_SQL) and _OBJECTIVE_PRICE_FALLBACK_SQL include — so flats_sold is never computed for competitors whose velocity/price come from the spatial gap-fill match, silently collapsing their stage_at_horizon to the neutral default.
  • [M] backend/app/services/site_finder/eias_heat_loader.py:519 — load_heat_reserves opens a single Session before iterating all 8 organizations, each doing multiple 60s-timeout HTTP round-trips to a slow/geo-blocked external host, and commits only once at the very end — so a single Postgres transaction (opened implicitly by the first per-row SAVEPOINT) stays open across several minutes of external network I/O for the whole batch.
  • [M] backend/app/workers/beat_schedule.py:551 — newbuilding-crossload-nightly cron string fires 3 hours earlier than intended because the comment double-converts UTC→MSK when Celery's global timezone is already Europe/Moscow.
  • [M] backend/app/services/weather_cache.py:164 — precipitation_total_mm (and the seasonal total_precip_mm equivalent) silently default to 0 when precipitation data is entirely missing, while every sibling metric (uv_index_max, avg_precip_per_day_mm, etc.) correctly defaults to None for the same missing-data condition — silent data dishonesty.
  • [M] backend/app/services/site_finder/weight_profiles.py:99 — _SELECT_DEFAULT has no ORDER BY and returns an arbitrary row via LIMIT 1, while create_profile/update_profile's unset-then-set sequence for is_default is not atomic across concurrent requests, so two profiles can end up with is_default=TRUE and get_default_profile can non-deterministically flip between them.
  • [M] backend/app/workers/tasks/scrape_kn.py:42 — Redis singleton lock key for a kn-API sweep is built by joining the developers list in caller-supplied order, so the same developer set submitted in a different order produces a different lock key and the lock silently fails to prevent concurrent duplicate sweeps.
  • [L] backend/app/services/exporters/full_report_html.py:1127metrics.get("sell_through_pct") is rendered raw (via _fmt) instead of through _fmt_pct_raw, even though the underlying value is already on a 0-100 scale (sold/(sold+available)*100, see market_metrics.py:194-195) and the row label explicitly says '%'.
  • [L] backend/app/services/generative/placement.py:297 — When a program item overrides the catalog footprint dimensions (item.footprint_w_m/footprint_d_m both set), the "placed N of M" warning still logs the catalog's house.footprint_w_m/house.footprint_d_m instead of the actual fp_w/fp_d that were used for placement.
  • [L] backend/app/services/scrapers/rosstat_emiss.py:228 — _decode_csv tries cp1251 before plain utf-8 in its fallback chain; cp1251 almost never raises UnicodeDecodeError (it maps nearly all byte values), so it silently 'succeeds' on corrupted/truncated UTF-8 content instead of falling through to the intended utf-8/errors='replace' path.
  • [L] backend/app/services/site_finder/gate_verdict.py:441 — cad_utility_label (and thus the ZOUIT_NETWORK_OBREMENENIE vs ZOUIT_CAD_BLOCKER label choice) is taken from the first overlap with a resolved network_kind, but the aggregated coverage/threshold decision mixes in generic keyword-matched overlaps with no confirmed network_kind — so a blocker can be labeled as a specific network encumbrance even though most of the blocking coverage came from an unrelated/unclassified keyword match.
  • [L] backend/app/services/site_finder/vodokanal_reserve_loader.py:488load_water_reserves_from_docx builds result including "period": period and logs that full dict (line 489), but the actual return statement (line 490) filters result.items() to isinstance(v, int) only, which always drops period (a str or None) — so the value actually returned (and thus what load_water_reserves/the Celery task sync_water_reserves surfaces) silently diverges from what was just logged.
  • [L] backend/app/workers/tasks/cbr_macro_sync.py:116 — Task is declared with bind=True, max_retries=2 but never calls self.retry() and has no autoretry_for, so the retry configuration has zero effect — the task fails permanently on first error despite the parameter suggesting up to 2 retries.

H. Doc/comment drift (22)

  • [H] backend/app/api/v1/admin_scrape.py:212 — The Redis queue_depth probe (channel.client.llen("celery")) has no timeout at all, unlike the inspect() calls above it, so it can hang the request indefinitely if the broker connection stalls.
  • [H] backend/app/services/dadata_client.py:185_suggest_geocode crashes with AttributeError instead of returning None when DaData's data field is not a dict.
  • [H] backend/app/services/forecasting/report_assembler.py:205 — _domrf_coverage() fallback feeds the confidence engine's 'главный sparse-риск проекта domrf↔objective (~2.5%)' signal with an unrelated metric (analyze.market_data_coverage_pct = % of nearby competitors with a priced Objective listing), and this fallback is the only path ever exercised in production because the sole current producer of supply_layers (orchestrator.py _summarize_supply_layers, line ~154-157) never emits supply_layers.domrf_coverage.
  • [H] backend/app/services/scrapers/ekb_ppt_tep_parser.py:72_page_contains_table's docstring claims cross-reference/ToC false positives are detected and suppressed, but the implementation is a bare regex search with no such logic — so table-of-contents entries or narrative cross-references (e.g. "показатели приведены в таблице 12") are indistinguishable from the real table caption.
  • [H] backend/app/services/scrapers/nspd_client.py:842 — search_by_quarter's docstring claims the whole operation is atomic ('Partial-success НЕ возвращается... failure → exception'), but this only holds for parcels/buildings (legacy get_features_in_bbox path); grid-walked layers (territorial_zones, red_lines, engineering_structures, all zouit, all risks) never raise on failure per finding above
  • [M] backend/app/api/v1/admin_scrape.py:1121 — resume_geo_job has no status guard on its UPDATE (unlike cancel_geo_job) and unconditionally re-enqueues the worker task, contradicting its own docstring ('Re-enqueue paused/failed job') by allowing a currently-RUNNING job to be double-dispatched, and reports success even for a nonexistent job_id.
  • [M] backend/app/services/analytics_queries.py:1423 — Contradictory in-file documentation about the vocabulary of objective_corpus_room_month.district: _velocity_baseline()'s docstring (line 1423) claims it 'matches domrf_kn_objects.district_name' (admin vocab), while _elasticity_coef()'s docstring (lines 1959-1962) states the same column is MICRO-neighborhood vocab ('Втузгородок', 'ЖБИ', ...) and that passing an admin district name gives 0 rows (labeled bug #1211). recommend_mix() calls _velocity_baseline, _velocity_baseline_per_bucket, and _district_velocity_trend with district_row['district_name'] (admin vocab from ekb_districts, e.g. 'Кировский') and calls _elasticity_coef without the districts resolver param, taking exactly the legacy admin-vocab path that _elasticity_coef's own docstring calls out as 'отдельный bug class' for this file's callers.
  • [M] backend/app/services/forecasting/macro_series.py:305 — get_monthly_macro's docstring claims the empty-list return only happens when months_back < 0, but the implementation clamps months_back with max(0, months_back) before computing the grid start, so the grid can never actually be empty for a negative months_back -- the documented behavior is unreachable/wrong.
  • [M] backend/app/services/generative/exporters/pdf.py:182 — The exported concept PDF's methodology footnote hardcodes 'распродажа 30 мес' regardless of the actually-computed DCF sales window, so the disclosed assumption can be factually wrong for the very numbers on the same page.
  • [M] backend/app/services/site_finder/osrm_client_local.py:139 — Per-element float(d) conversion of OSRM distances happens outside the try/except block, contradicting the function's documented contract that ANY unexpected-format response raises OsrmLocalUnavailableError.
  • [M] backend/app/services/site_finder/ors_client.py:131 — Same pattern as osrm_client_local.py: float(sec) for each ORS matrix duration is computed outside the try/except, so a malformed element type raises a raw exception instead of the documented OrsUnavailableError.
  • [M] backend/app/workers/lifecycle.py:92 — Zombie-resume query for kn_scrape_runs only catches 'running' rows that already have objects_snapshot set, contradicting the function's own stated invariant that ANY running row at worker_ready is by definition a zombie.
  • [M] backend/app/workers/tasks/izyatie_ocr_ingest.py:101 — Docstring claims per-batch Python dedup + a two-step (cad_num, doc_url) upsert prevents duplicate land_reservation rows for act_number-less records, but the actual code implements neither — every weekly re-run reinserts brand-new duplicate rows.
  • [L] backend/app/services/forecasting/confidence_engine.py:100 — Comment claims _HISTORY_MONTHS_LOW mirrors §9.6's _MIN_OBS=8, but the actual constant is 12
  • [L] backend/app/services/forecasting/macro_coefficient.py:99 — Stale comment claims the backed-weight sum is 0.45, but since #946 promoted inflation to a backed channel with weight 0.08 (line 110), the actual current backed-weight sum is 0.53.
  • [L] backend/app/services/forecasting/sales_series.py:496 — Docstring/code contradiction: build_sales_series (and its docstring) claims the returned series is empty (months=[]) 'только если сетка пуста (months_back < 0)', but the code clamps negative months_back to 0 before computing the grid, so the grid is never empty for any input.
  • [L] backend/app/services/forecasting/special_indices.py:589_timing_overlap's docstring states the formula is exp(−|Δмесяцев| / half_life) with 'расхождение в half_life мес → 0.5', but that formula does not equal 0.5 at Δ=half_life (it equals e^-1≈0.368); the actual, correct implementation uses 0.5 ** (Δ/half_life) (line 601), which does hit exactly 0.5 at Δ=half_life, matching the 'inline comment fix' but contradicting the docstring's stated formula.
  • [L] backend/app/services/objective_etl.py:466 — get_sqlite_info() has a TOCTOU race: it checks Path.exists() and then calls Path.stat() unguarded, outside the try/except that only covers the sqlite3.connect block.
  • [L] backend/app/services/scrapers/domrf_catalog.py:375 — Comment claims a BFS traversal of the NEXT_DATA JSON tree, but the implementation uses stack.pop() (LIFO), which is depth-first in reverse-child order — contradicting the documented search-order guarantee for picking the winning plan_image_url.
  • [L] backend/app/services/scrapers/domrf_catalog_object.py:426 — stats["skipped"] is declared and returned but never incremented anywhere in the function — every distinct failure mode (WAF block, 404, parse error, DB row not found) is lumped into stats["failed"], so callers cannot use the documented skipped/failed split to tell a benign/temporary condition (e.g. WAF ban) apart from a genuine data/parsing regression.
  • [L] backend/app/services/scrapers/nspd_client.py:263 — QuarterDump class docstring states 'Default = только core, чтобы не сжигать rate-limit на 17 запросов', but search_by_quarter's actual default is include_zouit=True (line 805), so the default call already includes 5 ЗОУИТ layers (and, per the finding above, at grid-walk cost not the '1 request per layer' the surrounding cost table implies)
  • [L] backend/app/services/site_finder/cadastre_fetch.py:101 — The docstring of find_active_on_demand_job claims a 60-second grace window for FAILED on-demand jobs ('Если в БД есть FAILED on-demand за последние 60 секунд — тоже None'), but the SQL implementing the function contains no reference to 'failed' status or any created_at/time-based filter at all — it only matches status IN ('queued','running','paused').

Найдено exhaustive-прогоном 2026-07-07 (wf_7ef04945). Фаза 1 = backend. Далее Фаза 2 = frontend/src (287 файлов), Фаза 3 = data/sql (153).

Мульти-агентный **исчерпывающий line-by-line** ревью всего `backend/app/**` (263 файла, 90 639 строк, 45 чанков). Каждый файл прочитан целиком; каждая находка проверена 2 независимыми скептиками (adversarial verify). **89 подтверждённых** (из 140 кандидатов, 51 отсеян). Сгруппировано по паттернам — фиксить волнами. ## A. Session-poisoning (нет rollback/SAVEPOINT) (16) - [ ] **[C]** `backend/app/services/site_finder/developer_attribution.py:153` — get_developer_attribution swallows OperationalError/ProgrammingError/DataError (and generic Exception) from db.execute() without calling db.rollback(), leaving the shared request-scoped Session's transaction in Postgres's aborted state. - [ ] **[H]** `backend/app/services/forecasting/orchestrator.py:127` — _safe_call swallows any exception from a §9.x layer without db.rollback(), so a real DB-level error in one layer poisons the shared SQLAlchemy Session for every later layer that reuses the same `db` object. - [ ] **[H]** `backend/app/services/forecasting/macro_series.py:401` — _query_mortgage_monthly loops over 5 mortgage fields on the same `db` Session and swallows exceptions per-field without db.rollback(), so one field's DB error cascades and empties out all later fields in the same call, contradicting its own 'graceful: сбой одного ряда не валит остальные' docstring. - [ ] **[H]** `backend/app/services/forecasting/special_indices.py:1661` — `_run()` (the shared try/except wrapper around all six §25 index builders) swallows any exception from `db.execute(...)` without calling `db.rollback()`, even though all six builders share ONE SQLAlchemy Session for the whole report (per forecast_request_cache.py docstring: one Session per §22 report / Celery task). - [ ] **[H]** `backend/app/services/forecasting/sales_series.py:571` — `_query_source_a` and `_query_source_b` catch any `db.execute` exception and return `{}` without calling `db.rollback()`, on a Session that is explicitly documented as shared/reused across many `build_sales_series` calls within one report (module docstring: 'db не в ключе (одна сессия на отчёт)'). - [ ] **[H]** `backend/app/services/job_settings.py:142` — get_all()/get_one() catch DB exceptions and return fallback data without calling db.rollback(), leaving the caller-supplied SQLAlchemy session in an aborted-transaction state. - [ ] **[H]** `backend/app/services/site_finder/connection_capacity_lookup.py:355` — Gas/heat/gas-outlet query helpers catch DB exceptions with a bare `except Exception` and never call db.rollback() / use a SAVEPOINT, unlike the sibling `_query_nearby_network_zones` (which correctly wraps its query in `db.begin_nested()`). Once one of these statements fails, the SQLAlchemy session is left in a failed-transaction state, so every subsequent query on the same `db` in the same call poisons and fails too — and gets misattributed to the wrong cause. - [ ] **[H]** `backend/app/services/site_finder/competitors.py:801` — The three sequential post-competitor DB lookups (avg_price, sold-count, objective price fallback) each wrap their `db.execute` in a bare `except Exception` with no rollback/SAVEPOINT, so a failure in an earlier one leaves the session's transaction aborted and silently poisons every later query on the same `db`, which then also fails and is misreported as 'query failed, continuing without X'. - [ ] **[H]** `backend/app/services/site_finder/saturation.py:241` — compute_district_saturation() catches a bare Exception around db.execute() and returns None without rolling back, poisoning the shared request Session. - [ ] **[H]** `backend/app/services/site_finder/supply_layers.py:576` — _safe_rows() catches Exception around db.execute() and returns [] without db.rollback(); compute_all_layers() calls it 3 times in a row (L1, L2, L3) on the same Session, so one failed layer silently zeroes out the other two as well. - [ ] **[H]** `backend/app/services/site_finder/zone_regulation.py:468` — backfill_ekb_zone_regulations never rolls back the shared `db` session after a failed commit or other DB-level exception, unlike the sibling get_or_fetch_zone_regulation path which does `write_db.rollback()` in its except block. - [ ] **[M]** `backend/app/services/cadastre/bulk_harvest.py:554` — backfill_parcel_geom's per-quarter `except Exception` (line 554) would itself swallow a NspdBulkWafError propagated from _grid_walk_category, contradicting its own adjacent comment (lines 556-557) that claims 'WAF 403 пробросится из client и прервёт прогон — это ожидаемо'. - [ ] **[M]** `backend/app/services/exporters/full_report_pdf.py:217` — `_get_connection_capacity`'s except block also omits `db.rollback()` after a DB-backed lookup failure, which can silently force `_generate_concept_result`'s market-price lookup into the class_norm fallback even when a fresh session would have succeeded. - [ ] **[M]** `backend/app/services/site_finder/pat_lookup.py:49` — Caught OperationalError/ProgrammingError is logged and swallowed without a db.rollback(), leaving the shared SQLAlchemy Session's transaction in an aborted state for any subsequent query on that same session within the request. - [ ] **[M]** `backend/app/workers/tasks/scrape_objective.py:252` — _save_raw()'s INSERT (lines 89-146) is called outside of any except clause that catches general exceptions — the surrounding per-job try only catches ObjectiveAuthError/ObjectiveAPIError (line 370) — so a DB-level failure there propagates to the outer handler, where _finish_run's own DB call on the now-poisoned session is swallowed by a bare `except Exception: pass` (lines 403-409), silently leaving the run row stuck at status='running' forever. - [ ] **[L]** `backend/app/services/scrapers/nspd_denorm.py:327` — denorm_dump()'s docstring states the caller is responsible for commit/close, but the function itself unconditionally calls db.commit() at line 373 (confirmed by the test suite asserting `db.commit.assert_called_once()`), directly contradicting the documented contract. ## B. Silent caps (усечение как итог) (10) - [ ] **[H]** `backend/app/api/v1/parcels.py:3687` — `market_pulse.competitors_total` is `len(competitor_rows)`, but `competitor_rows` comes from a SQL query with `LIMIT 20` (line 2251). The field name implies the total number of competitors within 3km, but it is really 'up to 20 nearest', and it is also used to compute `coverage_pct = competitors_priced * 100 / competitors_total`, with no truncation flag disclosing the cap. - [ ] **[H]** `backend/app/services/cadastre/bulk_harvest.py:402` — _grid_walk_category's blanket `except Exception` (line 402-414) swallows NspdBulkWafError/NspdBulkRateLimitError instead of re-raising, contradicting harvest_quarter's documented 'Raises: NspdBulkWafError ... caller не retry' contract and inconsistent with the codebase's own corrected pattern in the sibling get_features_in_bbox_grid (nspd_bulk_client.py:519-529, explicitly labeled 'Issue #252-mirror') and in search_by_quarter (nspd_bulk_client.py:305, which explicitly re-raises NspdBulkWafError/RateLimitError/ServerError). - [ ] **[H]** `backend/app/services/etl/objective_backfill.py:469` — Ambiguous-candidate resolution (core-pass and geo-pass) never excludes objective_complex_name values already taken in objective_complex_mapping, so a core that has one already-mapped candidate and one genuinely-available candidate is misclassified as ambiguous / can resolve to the wrong (doomed) candidate instead of the available one. - [ ] **[H]** `backend/app/services/site_finder/eesk_reserve_loader.py:239` — load_ps_35_220 writes a numeric load-percentage string into power_supply_centers.load_index via COALESCE(load_index, CAST(:load_pct AS text)), but that column is a categorical enum ('open'|'limited'|'closed'|NULL per data/sql/180_connection_capacity.sql:35) populated elsewhere by rosseti_wfs_loader._map_load_index with exactly those three string values. - [ ] **[M]** `backend/app/api/v1/parcels.py:965` — `neighbors_summary.count_buildings_100m` is `len(neighbor_rows)`, where `neighbor_rows` comes from the `neighbors` CTE in `_NEIGHBORS_SUMMARY_SQL` which has `LIMIT 30` (line 858) — same LIMIT-capped-count-presented-as-total pattern as the permits/competitors findings above, with no truncation flag. - [ ] **[M]** `backend/app/services/exporters/full_report_html.py:1517` — _build_permits_nearby hard-slices `items` to the first 10 rows without checking/surfacing the upstream `items_truncated` flag or adding a "and N more" disclosure, unlike every other capped table in this same file. - [ ] **[M]** `backend/app/services/scrapers/nspd_client.py:814` — search_by_quarter docstring says step 3 fetches 'каждого core layer' via get_features_in_bbox and prices the whole call at 6/11/22 requests (~3.6s/6.6s/13s), but 3 of the 5 core layers plus all zouit/risk layers actually go through get_features_in_bbox_grid at grid_n=7 (49 requests each per that function's own docstring at line ~544), making the true request count roughly 25-50x higher than documented - [ ] **[M]** `backend/app/services/site_finder/poi_score.py:141` — compute_poi_weighted_top7 (and similarly compute_poi_routing_decay at line 328) selects candidate POIs by pure nearest-distance SQL LIMIT before applying the category-weighted ranking, so a genuinely higher-weighted-but-farther POI can be excluded from the candidate window entirely. - [ ] **[M]** `backend/app/services/site_finder/velocity.py:213` — The competitor query is capped with `LIMIT 200` (line 213) and `n_comps = len(comp_rows)` (line 326) is then reported as `competitors_count` in every VelocityResult, with no truncation flag — unlike the analogous fix this same codebase already applied elsewhere (utility_infrastructure_loader.py's `features_truncated`, explicitly citing Epic #2445 A2's exact anti-pattern of a LIMIT-capped count masquerading as the true total). - [ ] **[L]** `backend/app/services/etl/objective_backfill.py:697` — GeoReject docstring enumerates reason values as 'no_address' | 'no_geocode' | 'too_far' | 'ambiguous_multi' | 'call_limit', but the code also emits reason='partial_geocode' (line 918), which is undocumented and omitted from the enumerated set. ## C. Неэффективный timeout-guard (3) - [ ] **[H]** `backend/app/api/v1/admin_scrape.py:194` — queue_status's ThreadPoolExecutor `with` block still blocks on shutdown(wait=True) even after the code 'gives up' via result(timeout=...), so the documented ~600ms worst-case latency guarantee is false. - [ ] **[H]** `backend/app/api/v1/photos.py:110` — DB session (checked out from the connection pool via Depends(get_db)) is held open for the entire duration of the synchronous upstream HTTP fetch to DOM.РФ. - [ ] **[H]** `backend/app/services/exporters/report_maps.py:149` — `_add_basemap`'s timeout protection is ineffective: `with ThreadPoolExecutor(...) as pool:` calls `shutdown(wait=True)` on exit even after `.result(timeout=...)` times out, so the function can still block indefinitely on a hung tile fetch. ## D. Price без sanity-фильтра (3) - [ ] **[H]** `backend/app/api/v1/parcels.py:2209` — `obj_pricing` CTE computes `avg_price_per_m2_rub` as a raw `AVG(oll.price_per_m2_rub)` with no sanity/outlier filter, unlike the sibling `district_price_block` (line 2902, `BETWEEN 30000 AND 600000`) and `market_trend` (line 2983, `BETWEEN 30000 AND 500000`) queries in the same file that guard against bad scraped prices. - [ ] **[M]** `backend/app/api/v1/parcels.py:3383` — `geo_radius_price` median query (`percentile_cont(0.5)` over `objective_lots.price_per_m2_rub`) has no price sanity filter, unlike the near-identical `district_price_block` query above it (line 2902) which bounds prices to 30000-600000. - [ ] **[L]** `backend/app/services/site_finder/parcel_financial.py:152` — `synthesize_teap_from_buildability` never validates that `max_building_pct` is within a sane 0-100 range (or that `max_far`/`max_floors` are plausible) before using it to derive built area and GFA, so a bad upstream zoning value silently produces a physically impossible result presented as a normal financial figure. ## E. snapshot-инфляция счётчиков (3) - [ ] **[H]** `backend/app/services/analytics_queries.py:1799` — _active_competitors_count()'s _q() helper does `SELECT COUNT(*) FROM domrf_kn_objects WHERE region_cd=:rc AND site_status='Строящиеся' ...` with no snapshot_date filter and no DISTINCT ON obj_id, so it counts every retained historical snapshot row per object, not distinct objects. - [ ] **[H]** `backend/app/services/analytics_queries.py:509` — developer_portfolio() selects from domrf_kn_objects filtered only by dev_id, with no snapshot_date filter or DISTINCT ON obj_id, so it returns every retained historical snapshot of each project as a separate row. - [ ] **[H]** `backend/app/services/scrapers/domrf_kn.py:537` — UPSERT_OBJECT_SQL's ON CONFLICT (obj_id, snapshot_date) DO UPDATE SET omits problem_flag, green_house, floor_min, floor_max, hobj_id, short_addr, dev_inn and region_cd, even though all eight are populated on INSERT (lines 498-516) and freshly computed every call by _norm_object. ## F. ON CONFLICT пропускает колонки (1) - [ ] **[M]** `backend/app/services/scrapers/domrf_kn.py:923` — UPSERT_PHOTO_SQL's ON CONFLICT (obj_id, obj_file_id) DO UPDATE SET updates ord_num/photo_url/photo_dttm/period_dt/size_bytes/photo_name/ready_desc/hidden but omits build_type, which is part of the INSERT column list (line 916) and populated every call from p.get('objBuildTypeShortDesc'). ## G. Прочие корректность/данные (31) - [ ] **[H]** `backend/app/api/v1/parcels.py:2381` — The noise-source query (step 7) has no `WHERE source_type IN (...)` filter, so it pulls every row from `osm_noise_sources_ekb` within 2km — including 'water' and 'utility' rows that the very same file queries separately (with explicit `source_type = 'water'` / `= 'utility'` filters) for the hydrology and utilities blocks a few dozen lines below. - [ ] **[H]** `backend/app/services/exporters/full_report_html.py:460` — _build_zoning's legacy-zoning fallback validates data on the `zoning` dict but then builds the table by reading from the still-empty `nspd_zoning` dict, silently discarding real fallback zoning data. - [ ] **[H]** `backend/app/services/scrapers/domrf_catalog.py:80` — Status keyword regex has no negation guard, so Russian negated forms ("нереализованных", "непроданных" — meaning UNSOLD/available) substring-match the sold keywords and get classified as SOLD. - [ ] **[H]** `backend/app/services/scrapers/domrf_catalog_object.py:440` — The entire batch runs inside one long-lived, uncommitted outer transaction with no interim commits — a late failure (e.g. BrowserSession.__aexit__ raising, or any unhandled exception after the loop) discards every already-succeeded per-object UPDATE. - [ ] **[H]** `backend/app/services/scrapers/nspd_client.py:610` — get_features_in_bbox_grid swallows every per-cell HTTP exception (return_exceptions=True + logger.warning + continue) and never raises, so a layer-wide WAF ban / outage produces an empty feature list indistinguishable from a genuine 'no zones here' result - [ ] **[H]** `backend/app/services/scrapers/page_reservation_parser.py:129` — _detect_kind classifies the whole document by unanchored substring search for 'резервир' before 'изъят' anywhere in the full text, not by which act type the document actually is. - [ ] **[H]** `backend/app/services/site_finder/best_layouts.py:195` — avg_area_m2 in _INLINE_VELOCITY_SQL is COALESCE(...,0) when all contributing deals rows have NULL deals_total_avg_area_m2, and this 0 is then silently treated downstream as a genuine '<25 м²' apartment area instead of 'area unknown'. - [ ] **[H]** `backend/app/services/site_finder/quarter_dump_lookup.py:767` — _get_risk_zones computes ST_Intersection with BOTH operands pre-cast to ::geography, reintroducing the exact PostGIS 3.4 'geography×geography ST_Intersection transform error' bug that the sibling _get_red_lines function in the SAME file explicitly documents and avoids (there, ST_Intersection runs in planar geometry and only the result is cast to ::geography, per the comment at lines 967-970). - [ ] **[H]** `backend/app/services/site_finder/velocity.py:167` — class_filter references alias `o.` inside the `latest_obj` CTE, but that CTE's FROM clause (`FROM domrf_kn_objects`, line 189) has no alias `o` — the alias `o` is only introduced later by the outer query (`FROM latest_obj o`, line 206). - [ ] **[M]** `backend/app/api/v1/admin_cadastre.py:83` — manual_list validation checks non-empty BEFORE stripping whitespace, so an all-whitespace quarters list silently bypasses the intended 400 error and creates a zero-target job. - [ ] **[M]** `backend/app/api/v1/admin_leads.py:152` — revenue_total and deals_total in the /stats KPI response are actually scoped to the `months` window (via window_leads CTE), not all-time totals, despite being named identically in style to leads_total which genuinely is all-time — a consumer trusting the '_total' suffix will display a partial-window figure as the grand total. - [ ] **[M]** `backend/app/api/v1/admin_scrape.py:1096` — cancel_geo_job always returns `{"cancelled": true}` even when the UPDATE matched zero rows (nonexistent job_id or job already in a terminal state), silently misreporting success. - [ ] **[M]** `backend/app/services/cadastre/bulk_harvest.py:1464` — _save_territorial_zones falls back to a synthetic zone_id = md5(sorted properties) when NSPD's WMS feature carries no id/zone_id anywhere (properties or top-level feature id); two geometrically-distinct zones sharing identical properties collide on this hash and one polygon silently overwrites the other via ON CONFLICT (zone_id) DO UPDATE. - [ ] **[M]** `backend/app/services/exporters/full_report_docx.py:288` — ЗОУИТ-reconciliation sets zouit_count = len(overlaps) (count of overlap/border records), but the KV row is labeled "Кол-во типов ЗОУИТ" (count of distinct ZOUIT TYPES) — the correct value is len(zouit_types). - [ ] **[M]** `backend/app/services/scrapers/domrf_catalog_object.py:460` — No circuit breaker on repeated WafBlockedError: once the DOM.РФ WAF starts blocking the session (the exact scenario referenced in the anti-ban comment at lines 444-450, incident #2443), the loop keeps sending one live request per remaining obj_id to the already-banned session instead of aborting the batch. - [ ] **[M]** `backend/app/services/scrapers/ekb_geoportal_client.py:217` — `_get_feature` treats an HTTP-200 response whose JSON body lacks a usable "features" list identically to a genuine "no features found at this location" — with no logging — so a GeoServer WFS error/degenerate response (common failure mode: CQL/typeName errors returned as HTTP 200 with an OWS ExceptionReport-shaped JSON) is silently reported as "this parcel has no PZZ zone/ЗОУИТ/КРТ here" instead of "the query failed". - [ ] **[M]** `backend/app/services/scrapers/stealth.py:293` — download_binary has no retry/backoff on transient failures (429/5xx), unlike get_json which retries up to 5 times with exponential backoff under the same WAF. - [ ] **[M]** `backend/app/services/site_finder/best_layouts.py:1113` — The `objects_total_in_radius` field means two different things across the two 'empty response' branches: line 1113 reports the pre-exclude/pre-filter group count, while line 1194 (a few dozen lines later, same function, same field name) reports the post-exclude/post-filter group count. - [ ] **[M]** `backend/app/services/site_finder/best_layouts.py:402` — _SUPPLY_ONLY_LOTS_SQL's area_bin CASE bucket for on_sale lots maps area_pd IS NULL into the same '<25' bucket as genuinely tiny (<25 m²) apartments, rather than excluding/flagging lots with missing area data. - [ ] **[M]** `backend/app/services/site_finder/competitors.py:553` — _SOLD_COUNT_SQL's `mapped` CTE only sources from `objective_complex_mapping`, omitting the `nearest_cx` spatial/name gap-fill branch that both the velocity CTE (in _COMPETITORS_SQL) and _OBJECTIVE_PRICE_FALLBACK_SQL include — so flats_sold is never computed for competitors whose velocity/price come from the spatial gap-fill match, silently collapsing their stage_at_horizon to the neutral default. - [ ] **[M]** `backend/app/services/site_finder/eias_heat_loader.py:519` — load_heat_reserves opens a single Session before iterating all 8 organizations, each doing multiple 60s-timeout HTTP round-trips to a slow/geo-blocked external host, and commits only once at the very end — so a single Postgres transaction (opened implicitly by the first per-row SAVEPOINT) stays open across several minutes of external network I/O for the whole batch. - [ ] **[M]** `backend/app/workers/beat_schedule.py:551` — newbuilding-crossload-nightly cron string fires 3 hours earlier than intended because the comment double-converts UTC→MSK when Celery's global timezone is already Europe/Moscow. - [ ] **[M]** `backend/app/services/weather_cache.py:164` — precipitation_total_mm (and the seasonal total_precip_mm equivalent) silently default to 0 when precipitation data is entirely missing, while every sibling metric (uv_index_max, avg_precip_per_day_mm, etc.) correctly defaults to None for the same missing-data condition — silent data dishonesty. - [ ] **[M]** `backend/app/services/site_finder/weight_profiles.py:99` — _SELECT_DEFAULT has no ORDER BY and returns an arbitrary row via LIMIT 1, while create_profile/update_profile's unset-then-set sequence for is_default is not atomic across concurrent requests, so two profiles can end up with is_default=TRUE and get_default_profile can non-deterministically flip between them. - [ ] **[M]** `backend/app/workers/tasks/scrape_kn.py:42` — Redis singleton lock key for a kn-API sweep is built by joining the `developers` list in caller-supplied order, so the same developer set submitted in a different order produces a different lock key and the lock silently fails to prevent concurrent duplicate sweeps. - [ ] **[L]** `backend/app/services/exporters/full_report_html.py:1127` — `metrics.get("sell_through_pct")` is rendered raw (via `_fmt`) instead of through `_fmt_pct_raw`, even though the underlying value is already on a 0-100 scale (`sold/(sold+available)*100`, see market_metrics.py:194-195) and the row label explicitly says '%'. - [ ] **[L]** `backend/app/services/generative/placement.py:297` — When a program item overrides the catalog footprint dimensions (item.footprint_w_m/footprint_d_m both set), the "placed N of M" warning still logs the catalog's house.footprint_w_m/house.footprint_d_m instead of the actual fp_w/fp_d that were used for placement. - [ ] **[L]** `backend/app/services/scrapers/rosstat_emiss.py:228` — _decode_csv tries cp1251 before plain utf-8 in its fallback chain; cp1251 almost never raises UnicodeDecodeError (it maps nearly all byte values), so it silently 'succeeds' on corrupted/truncated UTF-8 content instead of falling through to the intended utf-8/errors='replace' path. - [ ] **[L]** `backend/app/services/site_finder/gate_verdict.py:441` — cad_utility_label (and thus the ZOUIT_NETWORK_OBREMENENIE vs ZOUIT_CAD_BLOCKER label choice) is taken from the first overlap with a resolved network_kind, but the aggregated coverage/threshold decision mixes in generic keyword-matched overlaps with no confirmed network_kind — so a blocker can be labeled as a specific network encumbrance even though most of the blocking coverage came from an unrelated/unclassified keyword match. - [ ] **[L]** `backend/app/services/site_finder/vodokanal_reserve_loader.py:488` — `load_water_reserves_from_docx` builds `result` including `"period": period` and logs that full dict (line 489), but the actual `return` statement (line 490) filters `result.items()` to `isinstance(v, int)` only, which always drops `period` (a str or None) — so the value actually returned (and thus what `load_water_reserves`/the Celery task `sync_water_reserves` surfaces) silently diverges from what was just logged. - [ ] **[L]** `backend/app/workers/tasks/cbr_macro_sync.py:116` — Task is declared with `bind=True, max_retries=2` but never calls `self.retry()` and has no `autoretry_for`, so the retry configuration has zero effect — the task fails permanently on first error despite the parameter suggesting up to 2 retries. ## H. Doc/comment drift (22) - [ ] **[H]** `backend/app/api/v1/admin_scrape.py:212` — The Redis queue_depth probe (`channel.client.llen("celery")`) has no timeout at all, unlike the inspect() calls above it, so it can hang the request indefinitely if the broker connection stalls. - [ ] **[H]** `backend/app/services/dadata_client.py:185` — `_suggest_geocode` crashes with AttributeError instead of returning None when DaData's `data` field is not a dict. - [ ] **[H]** `backend/app/services/forecasting/report_assembler.py:205` — _domrf_coverage() fallback feeds the confidence engine's 'главный sparse-риск проекта domrf↔objective (~2.5%)' signal with an unrelated metric (analyze.market_data_coverage_pct = % of nearby competitors with a priced Objective listing), and this fallback is the only path ever exercised in production because the sole current producer of supply_layers (orchestrator.py _summarize_supply_layers, line ~154-157) never emits supply_layers.domrf_coverage. - [ ] **[H]** `backend/app/services/scrapers/ekb_ppt_tep_parser.py:72` — `_page_contains_table`'s docstring claims cross-reference/ToC false positives are detected and suppressed, but the implementation is a bare regex search with no such logic — so table-of-contents entries or narrative cross-references (e.g. "показатели приведены в таблице 12") are indistinguishable from the real table caption. - [ ] **[H]** `backend/app/services/scrapers/nspd_client.py:842` — search_by_quarter's docstring claims the whole operation is atomic ('Partial-success НЕ возвращается... failure → exception'), but this only holds for parcels/buildings (legacy get_features_in_bbox path); grid-walked layers (territorial_zones, red_lines, engineering_structures, all zouit, all risks) never raise on failure per finding above - [ ] **[M]** `backend/app/api/v1/admin_scrape.py:1121` — resume_geo_job has no status guard on its UPDATE (unlike cancel_geo_job) and unconditionally re-enqueues the worker task, contradicting its own docstring ('Re-enqueue paused/failed job') by allowing a currently-RUNNING job to be double-dispatched, and reports success even for a nonexistent job_id. - [ ] **[M]** `backend/app/services/analytics_queries.py:1423` — Contradictory in-file documentation about the vocabulary of objective_corpus_room_month.district: _velocity_baseline()'s docstring (line 1423) claims it 'matches domrf_kn_objects.district_name' (admin vocab), while _elasticity_coef()'s docstring (lines 1959-1962) states the same column is MICRO-neighborhood vocab ('Втузгородок', 'ЖБИ', ...) and that passing an admin district name gives 0 rows (labeled bug #1211). recommend_mix() calls _velocity_baseline, _velocity_baseline_per_bucket, and _district_velocity_trend with district_row['district_name'] (admin vocab from ekb_districts, e.g. 'Кировский') and calls _elasticity_coef without the `districts` resolver param, taking exactly the legacy admin-vocab path that _elasticity_coef's own docstring calls out as 'отдельный bug class' for this file's callers. - [ ] **[M]** `backend/app/services/forecasting/macro_series.py:305` — get_monthly_macro's docstring claims the empty-list return only happens when months_back < 0, but the implementation clamps months_back with max(0, months_back) before computing the grid start, so the grid can never actually be empty for a negative months_back -- the documented behavior is unreachable/wrong. - [ ] **[M]** `backend/app/services/generative/exporters/pdf.py:182` — The exported concept PDF's methodology footnote hardcodes 'распродажа 30 мес' regardless of the actually-computed DCF sales window, so the disclosed assumption can be factually wrong for the very numbers on the same page. - [ ] **[M]** `backend/app/services/site_finder/osrm_client_local.py:139` — Per-element `float(d)` conversion of OSRM distances happens outside the try/except block, contradicting the function's documented contract that ANY unexpected-format response raises `OsrmLocalUnavailableError`. - [ ] **[M]** `backend/app/services/site_finder/ors_client.py:131` — Same pattern as osrm_client_local.py: `float(sec)` for each ORS matrix duration is computed outside the try/except, so a malformed element type raises a raw exception instead of the documented `OrsUnavailableError`. - [ ] **[M]** `backend/app/workers/lifecycle.py:92` — Zombie-resume query for kn_scrape_runs only catches 'running' rows that already have objects_snapshot set, contradicting the function's own stated invariant that ANY running row at worker_ready is by definition a zombie. - [ ] **[M]** `backend/app/workers/tasks/izyatie_ocr_ingest.py:101` — Docstring claims per-batch Python dedup + a two-step (cad_num, doc_url) upsert prevents duplicate land_reservation rows for act_number-less records, but the actual code implements neither — every weekly re-run reinserts brand-new duplicate rows. - [ ] **[L]** `backend/app/services/forecasting/confidence_engine.py:100` — Comment claims _HISTORY_MONTHS_LOW mirrors §9.6's _MIN_OBS=8, but the actual constant is 12 - [ ] **[L]** `backend/app/services/forecasting/macro_coefficient.py:99` — Stale comment claims the backed-weight sum is 0.45, but since #946 promoted inflation to a backed channel with weight 0.08 (line 110), the actual current backed-weight sum is 0.53. - [ ] **[L]** `backend/app/services/forecasting/sales_series.py:496` — Docstring/code contradiction: `build_sales_series` (and its docstring) claims the returned series is empty (`months=[]`) 'только если сетка пуста (months_back < 0)', but the code clamps negative months_back to 0 before computing the grid, so the grid is never empty for any input. - [ ] **[L]** `backend/app/services/forecasting/special_indices.py:589` — `_timing_overlap`'s docstring states the formula is `exp(−|Δмесяцев| / half_life)` with 'расхождение в half_life мес → 0.5', but that formula does not equal 0.5 at Δ=half_life (it equals e^-1≈0.368); the actual, correct implementation uses `0.5 ** (Δ/half_life)` (line 601), which does hit exactly 0.5 at Δ=half_life, matching the 'inline comment fix' but contradicting the docstring's stated formula. - [ ] **[L]** `backend/app/services/objective_etl.py:466` — get_sqlite_info() has a TOCTOU race: it checks Path.exists() and then calls Path.stat() unguarded, outside the try/except that only covers the sqlite3.connect block. - [ ] **[L]** `backend/app/services/scrapers/domrf_catalog.py:375` — Comment claims a BFS traversal of the __NEXT_DATA__ JSON tree, but the implementation uses stack.pop() (LIFO), which is depth-first in reverse-child order — contradicting the documented search-order guarantee for picking the winning plan_image_url. - [ ] **[L]** `backend/app/services/scrapers/domrf_catalog_object.py:426` — stats["skipped"] is declared and returned but never incremented anywhere in the function — every distinct failure mode (WAF block, 404, parse error, DB row not found) is lumped into stats["failed"], so callers cannot use the documented skipped/failed split to tell a benign/temporary condition (e.g. WAF ban) apart from a genuine data/parsing regression. - [ ] **[L]** `backend/app/services/scrapers/nspd_client.py:263` — QuarterDump class docstring states 'Default = только core, чтобы не сжигать rate-limit на 17 запросов', but search_by_quarter's actual default is include_zouit=True (line 805), so the default call already includes 5 ЗОУИТ layers (and, per the finding above, at grid-walk cost not the '1 request per layer' the surrounding cost table implies) - [ ] **[L]** `backend/app/services/site_finder/cadastre_fetch.py:101` — The docstring of find_active_on_demand_job claims a 60-second grace window for FAILED on-demand jobs ('Если в БД есть FAILED on-demand за последние 60 секунд — тоже None'), but the SQL implementing the function contains no reference to 'failed' status or any created_at/time-based filter at all — it only matches status IN ('queued','running','paused'). --- _Найдено exhaustive-прогоном 2026-07-07 (wf_7ef04945). Фаза 1 = backend. Далее Фаза 2 = frontend/src (287 файлов), Фаза 3 = data/sql (153)._
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#2464
No description provided.