EPIC: exhaustive line-by-line аудит backend ПТИЦЫ (2026-07-07) — 89 подтверждённых находок #2464
Open
opened 2026-07-07 11:46:44 +00:00 by bot-backend
·
16 comments
No Branch/Tag specified
main
fix/3469-showcase-schedule
fix/3443-caddy-selfdowntime
fix/3463-db-statement-timeout
fix/3448-caddy-only-detection
fix/metrics-deploy-reloads-prometheus
feat/landing-showcase-band
fix/3452-corridor-advisory-zone
fix/alert-value-is-not-the-ratio
fix/3449-geocoder-cancel-orphan
fix/3451-tvf-deals-rooms
fix/msk-collector-net-retry
feat/msk-yandex-import
fix/3051-fetch-deals-rooms-dropped
feat/msk-collector-cian
fix/3256-asking-to-sold-buckets
fix/3083-estimate-throughput
fix/3274-part2b-gate
fix/3274-part2a-caddy-retry
fix/perimeter-smoke-retry-on-no-response
fix/3274-zero-downtime-frontend
fix/3274-part1-deploy-swap
fix/3412-cian-instance-relaunch
fix/3415-bucket-done-only-if-phases-ok
chore/backend-pool-wiring-tails
fix/3398-pool-ops-off-event-loop
fix/3402-followups-ban-kinds-curl
feat/3404-proxy-run-attribution
fix/cian-captcha-is-platform-refusal
refactor/3390-single-runs-module
fix/3398-estimator-valuations-proxy-pool
fix/3386-estimator-imv-proxy-pool
fix/3394-cian-sweep-houses-proxy-pool
fix/3393-interrupted-runs-outside-streaks
fix/drain-marks-inflight-app-tasks
fix/3384-no-proxy-at-batch-start
fix/3197-yandex-newbuilding-proxy-pool
fix/3376-price-history-decimal-slips
fix/backend-proxy-pool-flags
fix/3197-cian-history-backfill-proxy-pool
fix/3288-sidecar-ban-status-code
fix/3375-yandex-fullload-on-bucket-contract
fix/3380-deploy-metrics-alloy-recreate
fix/3354-alloy-query-secret-scrub
fix/3360-admin-prefix-anon-404
fix/3368-yandex-leaf-complete-flag
fix/3369-domclick-cancel-checkpoint
fix/3225-domklik-diff-percent
fix/smoke-history-path
fix/3288-breaker-block-kind
fix/3355-drain-mark-fullloads
fix/3191-yandex-detail-completeness
fix/3359-containment-cian-yandex
fix/3323-payment-init-fail-409
fix/3172-freshness-by-data
fix/3288-avito-ban-per-source
fix/3253-domclick-house-fields
fix/3312-defer-interval-minutes
fix/3324-rbac-401-oracle
fix/3274-deploy-window-error-page
fix/3343-auth-latency-flaky
fix/3315-exhaustive-containment-skip
fix/3338-backfill-save-false
fix/3339-thin-market-badge
fix/3154-glitchtip-secret-log-leak
fix/3333-drain-mark-all-sweeps
fix/3323-imv-thin-market-gate
fix/3324-mera-perimeter
fix/3323-coverage-copy-honesty
fix/3332-domclick-counter-identity
fix/3324-ptica-deploy-gate
fix/3316-role-single-source
fix/3319-citysweep-checkpoint
fix/3322-rosseti-stable-key
fix/3320-v2-honest-zeros
fix/3321-draft-fields-survive
fix/mera-mobile-nav-and-sticky
fix/audit-form-ux
fix/audit-coverage-500
fix/audit-dkp-price
fix/audit-accuracy-window
fix/audit-faq-contradiction
fix/perimeter-smoke-after-payments
feat/b2c-landing-honest-copy
feat/b2c-landing-live-numbers
feat/b2c-v3-becomes-root
feat/b2c-v3-port-v1-blocks
feat/b2c-report-redesign
feat/b2c-landing-v3-full
feat/3192b-yandex-phones
feat/3168-backfill-cursor-resume
fix/3118-domclick-no-proxy
fix/3170-yandex-combo-checkpoint
feat/3074-newbuilding-sweep-resume
fix/3163-infra-topic-default
fix/3156-tgbot-error-type
feat/3163-infra-topic-split
fix/3161-alertmanager-route-prefix
fix/3155-prometheus-no-alertmanager
feat/2997-drop-listings-geom-idx
fix/3122-boot-reap-observable
feat/3122-boot-reap
feat/3051-houses-region-code
feat/3074-cian-anchor-checkpoint
feat/3051-region-registry
fix/2656-anchor-ratio-freshness
fix/3114-scrub-credentials-in-logs
fix/3078-grafana-single-auth
fix/3081-honest-progress
fix/3063-pro-seller-derive
fix/3074-checkpoint-survives-claim
fix/3082-estimate-concurrency
fix/3044-enriched-total-seen
fix/telegram-pin-selectel-override
chore/compose-host-parameterised-resources
fix/revisit-floor-lateral-lookup
fix/avito-detail-fields
fix/3034-avito-fingerprint
fix/avito-offers-per-page
feat/3033-avito-secondary-filter
fix/2203-restore-drill-readiness
feat/2203-backup-staleness-and-forgejo
fix/2990-clean-start-initdb
fix/2924-yandex-jk-resolve-tried-at
fix/2992-upsert-distinct-gate
fix/2998-rosreestr-q2-partitions
feat/2936-null-penalty-flag
fix/2996-mislocated-houses-watchdog
fix/1781-secondary-only-param
fix/2934-risks-coverage
fix/2464-three-small
fix/2464-two-docstrings
fix/2464-zouit-label
fix/2464-building-pct-guard
fix/2464-download-binary-retry
fix/2986-permits-key
fix/2464-tep-docstring
fix/2464-act-date-backfill
fix/2464-area-bin-null
fix/2464-act-date-citation
fix/2464-detect-kind
fix/2464-status-negation
fix/2464-objective-zombie-sweep
fix/2464-inert-retry-config
fix/2464-default-profile-unique
fix/2464-zombie-any-running
fix/2464-catalog-skipped-counter
fix/2464-heat-loader-tx
fix/2464-objective-run-stuck
fix/2464-catalog-waf-breaker
fix/2464-lock-key-order
fix/2464-backfill-waf-abort
fix/2464-docs-vs-code
fix/2464-land-reservation-dedup
fix/2464-manual-list-validation
fix/2464-capacity-savepoints
fix/2464-leads-window-naming
fix/2464-zouit-count-label
fix/2464-catalog-batch-commits
fix/2464-html-zoning-fallback
fix/2950-lock-wait-observable
fix/2956-freshness-counts-failed-harvest
fix/2950-host-lock
fix/2953-nominatim-throttle
fix/2950-deploy-docker-lock
fix/2464-ird-lookups-savepoint
fix/2464-parcels-savepoint-pair
fix/2674-deal-floor-display
docs/2854-stale-justifications
chore/2857-drop-placeholder-columns
fix/2855-probe-protected-path
fix/2674-has-panorama
fix/house-imv-anchor-era
fix/2674-quality-cadastre-column
fix/sber-freshness-guard
fix/dkp-corridor-as-of
fix/scheduler-resume-checkpoint
fix/quarter-target-from-neighbour
fix/2687-domclick-ban-kind-platform
fix/2814-address-backfill
fix/2677-house-anchor-renovation
feat/tradein-paid-retention
chore/ci-config-cleanup
feat/mera-b2c-privacy
feat/tradein-payments-schema
chore/2674-dead-code-sweep
fix/2674-matching-tiers-honesty
fix/2674-avito-full-load-coverage
fix/2674-admin-metrics-honesty
fix/2674-writers-honor-schema
fix/2674-alerts-actually-fire
fix/2679-scraper-deploy-parity
fix/2674-house-imv-params
fix/2666-discount-plausibility-gate
feat/2435-cian-house-enrichment
fix/2657-domclick-block-not-done
feat/2571-login-throttle
fix/2660-display-freshness-segment
fix/2658-loud-skip-status
fix/2603-geocode-city-hint-tails
feat/2600-per-source-proxy-health
feat/tradein-v2-exposure-in-result-card
fix/tradein-avito-browser-proxy-pool
chore/tradein-cut-wasteful-scraping
fix/tradein-v2-deals-fallback-visible
fix/tradein-city-stamp-geo-guard
fix/tradein-cian-oblast-secondary
fix/tradein-implausible-build-year
fix/tradein-snapshot-query-perf
fix/tradein-asking-sold-city-scope
chore/tradein-purge-dead-mobileproxy
feat/tradein-asocks-rotation
fix/tradein-region-code-foreign-cities
fix/tradein-proxy-pool-self-healing
fix/tradein-backfill-listing-city-from-url
fix/tradein-geocode-queue-active-only
fix/tradein-geocode-city-hint
chore/tradein-remove-yandex-scripts
fix/tradein-listing-city-from-sweep
chore/tradein-remove-yandex-geocoder
chore/tradein-remove-n1
fix/tradein-geocoder-fail-closed
fix/tradein-ui-city-label-clip
feat/tradein-ui-city-hint
fix/tradein-coords-backfill-city-gate
fix/tradein-geocoder-city-default
fix/tradein-anchor-geo-scope
fix/tradein-avito-detail-oblast
feat/tradein-price-bands-refresh
feat/tradein-caddy-cutover
fix/tradein-team-nav-and-401
feat/tradein-team-dashboard
fix/tradein-team-list-order
feat/tradein-team-api
feat/tradein-login-ui
feat/tradein-auth-core
feat/tradein-auth-password-foundation
feat/tradein-auth-users-schema
fix/tradein-rbac-test-user2-expired
fix/tradein-auth-revoke-user2
fix/2013-avito-cohort-null
fix/rbac-test-user2-expired
fix/expire-brusnika-trial-access
feat/tradein-domclick-house-sku-schema
chore/design-sync-tradein
fix/tradein-deploy-scraper-paths-filter
fix/tradein-no-numberless-house-insert
fix/tradein-dedup-fias-pass-no-geoguard
feat/tradein-fias-estimate-frontend
fix/tradein-dedup-by-fias
feat/tradein-fias-tier-matching
fix/tradein-v2-uiux-p1
fix/objective-lots-dedup-1964
fix/audit-followups-1953
feat/concept-house-picker-1965-stage3b
feat/concept-house-catalog-1965-stage3a
feat/concept-live-massing-1965-stage2b
feat/concept-recompute-1965-stage2a
feat/concept-section-1965-stage1
fix/best-layouts-per-object-snapshot-1956
fix/deficit-segmentation-1959
feat/tradein-hedonic-ground-floor
fix/report-map-points-1961
fix/report-microcopy-1963
fix/report-nspd-zoning-1962
fix/report-frontend-display-1953
perf/tradein-estimate-audit
perf/best-layouts-objects-first
fix/171-migration-path
perf/171-objective-crm-report-month-idx
fix/1926-app-log-stdout
fix/estimator-tiebreaker-nanalogs
fix/1917-fetch-json-network-retry
fix/1926-scrape-runs-total-seen
fix/1781-house-fk-source-identity
feat/39-osrm-per-category
feat/1746-utility-infra-map
fix/test-849-savepoint-count
fix/parking-economics-by-class
fix/zouit-engineering-area-gate
fix/zoning-resolution-decouple
feat/dcf-velocity-schedule
feat/zone-regulation-backfill
chore/mock-financial-estimate
fix/finmodel-split-disclosure-threshold
feat/finmodel-financing
feat/finmodel-sitefinder-bridge
feat/finmodel-dcf
feat/finmodel-price-calibration
feat/finmodel-cost-cascade
audit/1871-p2-estimator-dedup-split
audit/1871-p2-avito-coord
audit/1871-p3-uiux
audit/1871-p2-invest-moi
audit/1871-p2-velocity-market
audit/1871-p1-3-isreal-stubs
audit/1871-p1-2-ghost-anchor-guard
audit/1871-p1-scenarios-collapse
revert/site-finder-v2
fix/v2-remove-resource-legend
fix/v2-map-legend-position
feat/site-finder-v2-map-light-polish
rollback/site-finder-home-light
feat/site-finder-v2-cockpit
rollback/sf-old-analysis-design
feat/ptica-tabs-6-6-reports-params
fix/ptica-vri-row
feat/ptica-map-glyphs-sitefinder-dark
fix/ptica-3d-preview-product-verdict
fix/ptica-button-fontsize
fix/ptica-parity-data-mapping
chore/ptica-iso-preview-spin
fix/ptica-inter-map-product
fix/ptica-visual-parity
fix/ptica-left-rail
feat/ptica-cockpit-complete-bottom
feat/ptica-default-and-rich-map
feat/ptica-real-ksit-frontend
feat/tradein-ekb-geoportal-buildings
fix/sf-feedback-batch
feat/avito-newbuilding-sweep
feat/browser-concurrent-pool
fix/backfill-kadastr-column
fix/avito-proactive-ip-rotate
feat/activate-domclick-sweep
feat/avito-newbuilding-capture
fix/house-id-fk-realtime-link
fix/migration-162-table-if-exists
feat/riasurt-sverdl-layers-108
fix/sber-dataset-paths-902
fix/conflict-resolution-freshness-1539
feat/objective-lots-history-writer-307-obj6
feat/velocity-mv-61
fix/objective-mapping-is-reviewed-gate-307-obj2
fix/cian-detail-rich-fields
fix/avito-novostroyki-segment
feat/yandex-novostroyki-newflat
fix/ci-dedup-runner-storm-1709
fix/emiss-pk-granularity-1687
feat/listings-segment-param-1188
fix/inn-regex-word-boundary-1682
fix/excel-deficit-horizon-1590
fix/admin-scrape-validation
fix/admin-leads-interval-cast-1694
fix/dkp-corridor-doc-p10p90-1695
fix/weather-wind-d-allnone
test/residential-zone-rawprops-1681
fix/domrf-badge-regex-narrow-1686
feat/yandex-camoufox-transport
fix/score-sat-factor-1509
fix/base-pace-room-bucket-1593
fix/gate-verdict-no-blocker-1617
fix/confidence-deal-count-window-1637
fix/match-economics-1511-1512
fix/listings-partial-indexes-1398
fix/smart-suggestions-threshold-1503
fix/dkp-corridor-percentile-1520
fix/enrich-economics-window-1513
fix/leads-stats-window-1383
fix/poi-score-scale-1486
fix/geo-weight-floor-1633
fix/inn-regex-context-1640
fix/domrf-status-classify-1609
fix/emiss-period-dedup-1606
fix/cian-paginate-tail-1393
fix/normalize-zero-fill-guard-1638
fix/supply-view-devgroup-key-1650
fix/residential-zone-detect-1353
fix/score-label-threshold-1357
fix/trend-ratio-vprior-zero-1508
fix/district-median-dedup-1352
fix/supply-layers-overlap-1355
fix/pdf-expires-at-1530
fix/quartirography-area-bucket-1406
fix/cad-zones-multipolygon-1344
fix/sold-pct-market-verdict-1407
fix/week-review-finish
fix/week-review-backend-v2
feat/plan-vectorization-spike-299
fix/1668-analyze-confidence
fix/1667-metro-block
fix/1666-layout-velocity-beat
chore/remove-n1-scraper
fix/cian-kitchen-area
feat/yandex-detail-backfill
feat/avito-detail-backfill
fix/best-layouts-bucket-normalization
fix/gknspecial-incremental-commit-per-prefix
fix/okn-objects-full-registry-load
fix/parcel-ird-overlap-except-expand
fix/report-assembler-confounded-keys
fix/job-settings-json-persist-v2
fix/nspd-geo-soft-time-limit-passthrough
fix/nspd-soft-time-limit-v2
fix/pilot-request-modal-validation
fix/recommend-mix-saturation-latest-snapshot
fix/scrape-cadastre-fail-counter-v2
fix/izyatie-ocr-cad-normalize
fix/job-settings-panel-json-state-persist
fix/noise-loader-savepoint-per-row
fix/objective-sync-config-bare-except
fix/parcel-snapshot-mdash-double-escape
fix/poi-loader-leap-year-date-replace
fix/scrape-cadastre-permanent-fail-counter
fix/usermenu-analyst-role-label
feat/location-948b-r
feat/location-entity-948b
feat/insight-entity-948a
feat/analyst-role-audit-962
feat/cbr-inflation-macro-946
feat/cian-zhk-resolve-972
perf/forecast-parallel-horizons-1129
feat/newbuilding-enrich-backfill-972
feat/emiss-macro-indicators-946
feat/rosstat-emiss-macro-946
ci/frontend-vitest-job
chore/frontend-vitest-wkt-tests
feat/forecast-meta-footer
feat/forecast-future-supply-competitors
feat/forecast-docx-pptx-buttons
feat/forecast-scoring-panel
feat/forecast-product-tz-block
feat/market-map-layers-999
feat/forecast-chart-section6
feat/forecast-export-pptx-959
feat/forecast-export-docx-959
feat/forecast-export-ui-959
feat/forecast-export-telegram-959
feat/forecast-export-md-959
feat/analyze-response-contract-961
feat/competitor-velocity-spatial-fallback-968
fix/frontend-healthcheck-hostname
feat/analyze-market-coords-999
fix/site-finder-entry-crash-1001
feat/horizon-selector-996
feat/forecast-section-998
feat/forecast-endpoint-horizon-enqueue
feat/forecast-celery-task-persist
feat/forecast-orchestrator-service
feat/demand-supply-months-of-inventory
feat/wire-district-resolver
feat/district-vocab-reconciliation
fix/affordability-market-spread
fix/backtest-sig-gate
feat/wire-978-almon-rate-regime
feat/backtest-oos-almon-seasonal
ci/forgejo-pytest-gate
fix/tradein-praktika-logo-form-scroll
feat/yandex-exhaustive-price-room
feat/794-sber-time-adjust
chore/bot-harness-tuning
docs/agent-usage-throttle-qa-terminal
fix/avito-sweep-single-run-guard
chore/analyst-inbound-queue-tradein-db
feat/801-preview-full-wt
chore/bot-safety-followups
feat/tradein-avito-imv-fix
feat/tradein-repair-coef-calibration
feat/tradein-avito-houses-fix
feat/tradein-address-precision
feat/tradein-backfill-prioritize-listings
feat/tradein-geocode-deals-nominatim
feat/tradein-geocode-deals-from-houses
feat/tradein-same-building-match
feat/claude-envvars-and-stale-cron
No results found.
Labels
Clear labels
Находки полного мульти-агентного ревью (Claude Fable 5, 2026-06-09)
Новое ТЗ от Ген Геныча (июнь 2026): Site Finder v2 — ИИ-прогноз продуктовой стратегии девелопера. Актуальный приоритет.
Admin UI / internal tools / pipeline tracker
Аналитика данных / matching
Authentication / authorization / multi-tenancy
Автоматизация / cron / scheduler
Something isn't working
Коммерческая воронка / пилоты / pipeline / реестр ПО
Operational housekeeping
Forgejo Actions / build pipeline
152-ФЗ / реестр ПО / legal / TOS
Data sources, parsers, ETL
Уникальные данные — sales tracker, CRM aggregation
README / runbooks / onboarding
This issue or pull request already exists
Developer experience / build speed
New feature or request
Feedback from Max (Senior partner)
Generative Design — concept generation engine
Pending team sync / decision
Human review/decision required
Monitoring / metrics / logs / Sentry / Uptime
Kill-switch — all autonomous bots pause
Latency / throughput / DB query tuning
Critical / production-blocker
High priority
Normal priority
Низкий приоритет (follow-up, tech-debt)
Backend Python — FastAPI, Celery, scrapers
DB migrations, indexes, SQL
docker-compose, Caddy, workflows
Frontend — Next.js, React, TypeScript
Tests, smoke, Playwright
Скрапперы (Avito/Cian/Yandex/Rosreestr)
Security audit / secrets / vulnerabilities
Site Finder scoring quality & data layer
Stage 1: Generative MVP (нед 2-7)
Stage 2: Site Finder MVP (нед 8-12)
Blocked — needs human intervention
Closed, completed
Raw/fuzzy task filed by human — auto-analyst подхватывает: code-archeology + декомпозиция в actionable scope/* + status/ready (или split на под-issues)
Reviewer/QA вернул worker на доработку — worker сам чинит свой PR (fixup), НЕ требует human (в отличие от status/blocked)
PR merged, awaiting smoke test
Ready for pickup by worker bot
PR open, awaiting code review
Work in progress (worker claimed)
Технический долг
Trade-In MVP проект
User experience / responsive / conversion
Week review 1 — находки multi-agent аудита проекта (2026-06-14)
This will not be worked on
Исходно-разрешительная документация — слой ограничений участка для analyze-таблицы
Trade-In Estimator — проект оценки вторичного жилья
Fable 5 ревью
Находки полного мульти-агентного ревью (Claude Fable 5, 2026-06-09)
GG-форсайт
Новое ТЗ от Ген Геныча (июнь 2026): Site Finder v2 — ИИ-прогноз продуктовой стратегии девелопера. Актуальный приоритет.
admin
Admin UI / internal tools / pipeline tracker
analytics
Аналитика данных / matching
auth
Authentication / authorization / multi-tenancy
automation
Автоматизация / cron / scheduler
bug
Something isn't working
business
Коммерческая воронка / пилоты / pipeline / реестр ПО
chore
Operational housekeeping
ci
Forgejo Actions / build pipeline
compliance
152-ФЗ / реестр ПО / legal / TOS
data
Data sources, parsers, ETL
data-moat
Уникальные данные — sales tracker, CRM aggregation
docs
README / runbooks / onboarding
duplicate
This issue or pull request already exists
dx
Developer experience / build speed
enhancement
New feature or request
feedback/max
Feedback from Max (Senior partner)
generative
Generative Design — concept generation engine
needs-discussion
Pending team sync / decision
needs-human
Human review/decision required
observability
Monitoring / metrics / logs / Sentry / Uptime
pause-bots
Kill-switch — all autonomous bots pause
performance
Latency / throughput / DB query tuning
priority/p0
Critical / production-blocker
priority/p1
High priority
priority/p2
Normal priority
priority/p3
Низкий приоритет (follow-up, tech-debt)
scope/backend
Backend Python — FastAPI, Celery, scrapers
scope/db
DB migrations, indexes, SQL
scope/devops
docker-compose, Caddy, workflows
scope/frontend
Frontend — Next.js, React, TypeScript
scope/qa
Tests, smoke, Playwright
scrapers
Скрапперы (Avito/Cian/Yandex/Rosreestr)
security
Security audit / secrets / vulnerabilities
site-finder
Site Finder scoring quality & data layer
stage/1
Stage 1: Generative MVP (нед 2-7)
stage/2
Stage 2: Site Finder MVP (нед 8-12)
status/blocked
Blocked — needs human intervention
status/done
Closed, completed
status/needs-analysis
Raw/fuzzy task filed by human — auto-analyst подхватывает: code-archeology + декомпозиция в actionable scope/* + status/ready (или split на под-issues)
status/needs-fix
Reviewer/QA вернул worker на доработку — worker сам чинит свой PR (fixup), НЕ требует human (в отличие от status/blocked)
status/qa
PR merged, awaiting smoke test
status/ready
Ready for pickup by worker bot
status/review
PR open, awaiting code review
status/wip
Work in progress (worker claimed)
tech-debt
Технический долг
tradein
Trade-In MVP проект
ux
User experience / responsive / conversion
week ревью 1
Week review 1 — находки multi-agent аудита проекта (2026-06-14)
wontfix
This will not be worked on
ИРД
Исходно-разрешительная документация — слой ограничений участка для analyze-таблицы
вторичка
Trade-In Estimator — проект оценки вторичного жилья
No labels
Fable 5 ревью
GG-форсайт
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
feedback/max
generative
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
ИРД
вторичка
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
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
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Мульти-агентный исчерпывающий line-by-line ревью всего
backend/app/**(263 файла, 90 639 строк, 45 чанков). Каждый файл прочитан целиком; каждая находка проверена 2 независимыми скептиками (adversarial verify).89 подтверждённых (из 140 кандидатов, 51 отсеян). Сгруппировано по паттернам — фиксить волнами.
A. Session-poisoning (нет rollback/SAVEPOINT) (16)
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.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 samedbobject.backend/app/services/forecasting/macro_series.py:401— _query_mortgage_monthly loops over 5 mortgage fields on the samedbSession 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.backend/app/services/forecasting/special_indices.py:1661—_run()(the shared try/except wrapper around all six §25 index builders) swallows any exception fromdb.execute(...)without callingdb.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).backend/app/services/forecasting/sales_series.py:571—_query_source_aand_query_source_bcatch anydb.executeexception and return{}without callingdb.rollback(), on a Session that is explicitly documented as shared/reused across manybuild_sales_seriescalls within one report (module docstring: 'db не в ключе (одна сессия на отчёт)').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.backend/app/services/site_finder/connection_capacity_lookup.py:355— Gas/heat/gas-outlet query helpers catch DB exceptions with a bareexcept Exceptionand never call db.rollback() / use a SAVEPOINT, unlike the sibling_query_nearby_network_zones(which correctly wraps its query indb.begin_nested()). Once one of these statements fails, the SQLAlchemy session is left in a failed-transaction state, so every subsequent query on the samedbin the same call poisons and fails too — and gets misattributed to the wrong cause.backend/app/services/site_finder/competitors.py:801— The three sequential post-competitor DB lookups (avg_price, sold-count, objective price fallback) each wrap theirdb.executein a bareexcept Exceptionwith no rollback/SAVEPOINT, so a failure in an earlier one leaves the session's transaction aborted and silently poisons every later query on the samedb, which then also fails and is misreported as 'query failed, continuing without X'.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.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.backend/app/services/site_finder/zone_regulation.py:468— backfill_ekb_zone_regulations never rolls back the shareddbsession after a failed commit or other DB-level exception, unlike the sibling get_or_fetch_zone_regulation path which doeswrite_db.rollback()in its except block.backend/app/services/cadastre/bulk_harvest.py:554— backfill_parcel_geom's per-quarterexcept 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 и прервёт прогон — это ожидаемо'. ← #2969backend/app/services/exporters/full_report_pdf.py:217—_get_connection_capacity's except block also omitsdb.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. ← #2964backend/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.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 bareexcept Exception: pass(lines 403-409), silently leaving the run row stuck at status='running' forever. ← #2972backend/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 assertingdb.commit.assert_called_once()), directly contradicting the documented contract. ← #2968B. Silent caps (усечение как итог) (10)
backend/app/api/v1/parcels.py:3687—market_pulse.competitors_totalislen(competitor_rows), butcompetitor_rowscomes from a SQL query withLIMIT 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 computecoverage_pct = competitors_priced * 100 / competitors_total, with no truncation flag disclosing the cap.backend/app/services/cadastre/bulk_harvest.py:402— _grid_walk_category's blanketexcept 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).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. ← ОТКЛОНЁН по замеру 20.08: гео-половина закрыта #2929, core-pass оставлен сознательно (имя+застройщик не различают «Старт»/«СТАРТ» — разные ЖК). Остаточная мелочь:_OBJECTIVE_PROJECTS_SQLбез ORDER BY →candidates[0]у ambiguous недетерминирован, но наружу уходят только счётчики, а сам кандидат доживает лишь до метки в журнале отказов гео-прохода.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.backend/app/api/v1/parcels.py:965—neighbors_summary.count_buildings_100mislen(neighbor_rows), whereneighbor_rowscomes from theneighborsCTE in_NEIGHBORS_SUMMARY_SQLwhich hasLIMIT 30(line 858) — same LIMIT-capped-count-presented-as-total pattern as the permits/competitors findings above, with no truncation flag.backend/app/services/exporters/full_report_html.py:1517— _build_permits_nearby hard-slicesitemsto the first 10 rows without checking/surfacing the upstreamitems_truncatedflag or adding a "and N more" disclosure, unlike every other capped table in this same file.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 ← #2968backend/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. ← ОТКЛОНЁН по замеру 20.08 — 683 усечённых центра, 0 расхождений топ-7backend/app/services/site_finder/velocity.py:213— The competitor query is capped withLIMIT 200(line 213) andn_comps = len(comp_rows)(line 326) is then reported ascompetitors_countin every VelocityResult, with no truncation flag — unlike the analogous fix this same codebase already applied elsewhere (utility_infrastructure_loader.py'sfeatures_truncated, explicitly citing Epic #2445 A2's exact anti-pattern of a LIMIT-capped count masquerading as the true total).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)
backend/app/api/v1/admin_scrape.py:194— queue_status's ThreadPoolExecutorwithblock 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.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.РФ.backend/app/services/exporters/report_maps.py:149—_add_basemap's timeout protection is ineffective:with ThreadPoolExecutor(...) as pool:callsshutdown(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)
backend/app/api/v1/parcels.py:2209—obj_pricingCTE computesavg_price_per_m2_rubas a rawAVG(oll.price_per_m2_rub)with no sanity/outlier filter, unlike the siblingdistrict_price_block(line 2902,BETWEEN 30000 AND 600000) andmarket_trend(line 2983,BETWEEN 30000 AND 500000) queries in the same file that guard against bad scraped prices.backend/app/api/v1/parcels.py:3383— ПРОВЕРЕНО, правка не нужна (медиана устойчива: 5 комплексов из 310 с выбросами, сдвиг ≤456 ₽/м², <1%; см. комментарий) —geo_radius_pricemedian query (percentile_cont(0.5)overobjective_lots.price_per_m2_rub) has no price sanity filter, unlike the near-identicaldistrict_price_blockquery above it (line 2902) which bounds prices to 30000-600000.backend/app/services/site_finder/parcel_financial.py:152—synthesize_teap_from_buildabilitynever validates thatmax_building_pctis within a sane 0-100 range (or thatmax_far/max_floorsare 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. — ✅ PR #3000 (смержен 20.08; + второй дефект той же функции: пятно застройки выходило больше участка)E. snapshot-инфляция счётчиков (3)
backend/app/services/analytics_queries.py:1799— _active_competitors_count()'s _q() helper doesSELECT 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.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.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)
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)
backend/app/api/v1/parcels.py:2381— The noise-source query (step 7) has noWHERE source_type IN (...)filter, so it pulls every row fromosm_noise_sources_ekbwithin 2km — including 'water' and 'utility' rows that the very same file queries separately (with explicitsource_type = 'water'/= 'utility'filters) for the hydrology and utilities blocks a few dozen lines below.backend/app/services/exporters/full_report_html.py:460— _build_zoning's legacy-zoning fallback validates data on thezoningdict but then builds the table by reading from the still-emptynspd_zoningdict, silently discarding real fallback zoning data. ← #2959backend/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. — ✅ PR #2979 (смержен 20.08; эффект сегодня нулевой — путь отключён, измерено)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. ← #2960backend/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' resultbackend/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. — ✅ PR #2980 (смержен 20.08; парсер на прод строк не писал, измерено)backend/app/services/site_finder/best_layouts.py:195— ЧАСТИЧНО: цена починена (PR #2868), площадь требует правки контракта → #2867. Механизм оказался не тот: не «все слагаемые NULL» (таких строк 0), а пустое окно продаж. 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'.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). ← ОТКЛОНЁН по замеру 20.08 — не воспроизводится на PostGIS 3.4.3, слои пустыbackend/app/services/site_finder/velocity.py:167— class_filter references aliaso.inside thelatest_objCTE, but that CTE's FROM clause (FROM domrf_kn_objects, line 189) has no aliaso— the aliasois only introduced later by the outer query (FROM latest_obj o, line 206).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. ← #2965backend/app/api/v1/admin_leads.py:152— revenue_total and deals_total in the /stats KPI response are actually scoped to themonthswindow (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. ← #2963backend/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.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. ← ОТКЛОНЁН по замеру 20.08: синтетических zone_id на проде 0 из 1 строки, а таблицуcad_territorial_zonesНЕ ЧИТАЕТ никто (ПЗЗ-зона в отчёте идёт изnspd_quarter_dumps.features_jsonчерез_get_zoning). Судьба самой таблицы вынесена в #2985 — чинить хеш до её решения бессмысленно.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). ← #2961backend/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. ← #2971backend/app/services/scrapers/ekb_geoportal_client.py:217—_get_featuretreats 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". ← ОТКЛОНЁН по замеру 20.08 — ошибки WFS не притворяются пустым результатом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. — ✅ PR #2999 (смержен 20.08; ретраи по образцу get_json + контроль на паузы)backend/app/services/site_finder/best_layouts.py:1113— Theobjects_total_in_radiusfield 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. ← ТРЕБУЕТ РЕШЕНИЯ — какая семантика поля верна, см. коммент 20.08 ← ОТКЛОНЁН по замеру 20.08:objects_total_in_radius— свойство РАДИУСА, а не пользовательского фильтра, и существующий тест это закрепляет (исключение конкурента оставляет счётчик = 1). Разбор в комментарии к эпику от 20.08.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. — ✅ PR #2983 (смержен 20.08; 11 557 квартир без площади сидели в корзине «<25 м²» при 7 013 настоящих)backend/app/services/site_finder/competitors.py:553— _SOLD_COUNT_SQL'smappedCTE only sources fromobjective_complex_mapping, omitting thenearest_cxspatial/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. ← ЗАБЛОКИРОВАН #2962 — мост gap-fill сломан, копировать нельзя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. — ✅ PR #2973 (смержен)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.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.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. — ✅ PR #2976 (смержен 20.08, индекс на проде фальсифицирован)backend/app/workers/tasks/scrape_kn.py:42— Redis singleton lock key for a kn-API sweep is built by joining thedeveloperslist 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. ← #2970backend/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 '%'. ← НАХОДКА НЕВЕРНА, не «починено»:_fmtшкалу не меняет, значение округлено вmarket_metrics.py:136, единицу несёт подпись строки (сверка 19.08)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. — ✅ PR #3003 (смержен 21.08)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. ← ОТКЛОНЁН по замеру 20.08. Порчи нет: 0 следов мохибейка («Ð», «Ñ», «â€», «Ã») в 2862 строках macro_indicator. Перестановка сделала бы ХУЖЕ: utf-8-sig уже ловит валидный utf-8 первым, а настоящие CP1251-файлы после неё ушли бы в utf-8/replace. Побочно проверено и подтверждено эмпирически: третий элемент цепочкиutf-8НЕДОСТИЖИМ (200k случайных байтовых строк — ни одного случая, где utf-8-sig падает, а utf-8 проходит), а cp1251 декодирует и обрезанный utf-8, и случайные байты, поэтому веткаerrors="replace"со своим logger.warning не может сработать НИКОГДА — отлаживающий будет искать это предупреждение в логах впустую. Эвристику «похоже ли на мохибейк» не добавляю: калибровать её не на чем (0 случаев), а правило без образцов ловит ровно те случаи, которые придумали вместе с ним.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. — ✅ PR #3001 (смержен 20.08; довод пункта опровергнут замером — смешиваются РАЗНЫЕ ВИДЫ СЕТЕЙ, а не сети с keyword; вывод верен)backend/app/services/site_finder/vodokanal_reserve_loader.py:488—load_water_reserves_from_docxbuildsresultincluding"period": periodand logs that full dict (line 489), but the actualreturnstatement (line 490) filtersresult.items()toisinstance(v, int)only, which always dropsperiod(a str or None) — so the value actually returned (and thus whatload_water_reserves/the Celery tasksync_water_reservessurfaces) silently diverges from what was just logged. — ✅ PR #3003 (смержен 21.08; фильтр стоял ради аннотации dict[str, int], она тоже исправлена)backend/app/workers/tasks/cbr_macro_sync.py:116— Task is declared withbind=True, max_retries=2but never callsself.retry()and has noautoretry_for, so the retry configuration has zero effect — the task fails permanently on first error despite the parameter suggesting up to 2 retries. — ✅ PR #2977 (смержен 20.08; заодно ещё 10 тасок + AST-гейт)H. Doc/comment drift (22)
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.backend/app/services/dadata_client.py:185—_suggest_geocodecrashes with AttributeError instead of returning None when DaData'sdatafield is not a dict.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.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. — ✅ PR #2988 (смержен 20.08; докстрока приведена к коду + замер уточнил довод пункта: косвенные падежи регекс НЕ ловит, опасно только оглавление)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 abovebackend/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.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 thedistrictsresolver param, taking exactly the legacy admin-vocab path that _elasticity_coef's own docstring calls out as 'отдельный bug class' for this file's callers. ← #2968backend/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. ← #2968backend/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.backend/app/services/site_finder/osrm_client_local.py:139— Per-elementfloat(d)conversion of OSRM distances happens outside the try/except block, contradicting the function's documented contract that ANY unexpected-format response raisesOsrmLocalUnavailableError.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 documentedOrsUnavailableError.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. — ✅ PR #2975 (смержен 20.08, код проверен в контейнере)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. ← #2966backend/app/services/forecasting/confidence_engine.py:100— Comment claims _HISTORY_MONTHS_LOW mirrors §9.6's _MIN_OBS=8, but the actual constant is 12backend/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. ← #2968backend/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. ← #2968backend/app/services/forecasting/special_indices.py:589—_timing_overlap's docstring states the formula isexp(−|Δмесяцев| / 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 uses0.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.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. — ✅ PR #3003 (смержен 21.08)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.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. — ✅ PR #2974 (смержен)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) — ✅ PR #3002 (смержен 21.08; заодно поправлено число «17 запросов» — grid-walk даёт по 49 на слой)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'). — ✅ PR #3002 (смержен 21.08; окна «60 секунд» в SQL нет вовсе, failed не возвращается никогда)Найдено exhaustive-прогоном 2026-07-07 (wf_7ef04945). Фаза 1 = backend. Далее Фаза 2 = frontend/src (287 файлов), Фаза 3 = data/sql (153).
Сверка всех 89 находок с кодом — 13.08.2026, origin/main @
9e83eb4aЭпик от 07.07 стоял с нулём отмеченных пунктов. Проверил каждый против сегодняшнего кода.
Остаток по серьёзности: высокая 22, средняя 30, низкая 18.
Почему это стоило отдельной работы
Все 19 закрыты семью правками (
39dd6333,82fdabcc,22f3c44d,01360e9c,d3f3370b,9a6b5601,f987e819), каждая ссылается на #2464 — и ни одна отметка не проставлена. Учёт отставал от кода на все девятнадцать.Цена такого расхождения не теоретическая: сегодня в соседнем продукте агент построил дубль уже существующей правки, потому что не посмотрел смежное. Эпик с нулём отметок гарантирует повторение — следующий исполнитель возьмёт из списка сделанное.
Отметки в теле задачи расставлены.
Аудит не ошибся ни разу
Отдельно проверял исход «пункт неверен» — его нет ни одного. У пяти пунктов уточнена формулировка (не тот номер строки после правок, механизм описан общее, чем есть), но вывод в каждом случае устоял.
Это довод в пользу метода: аудит собирался с двумя независимыми скептиками на находку, 51 кандидат из 140 был отсеян на входе. Месяц спустя оставшиеся 89 подтвердились.
Что осталось — высокая серьёзность
Полный список в отчёте сверки; здесь то, что стоит взять первым.
Соединение из пула удерживается на время внешнего запроса —
api/v1/photos.py:110(8 с, больше при перенаправлениях) иexporters/report_maps.py:149(подложка карты, срок 10 с не срабатывает; путь наследуют обе точки выгрузки).Сторож по времени не возвращает управление —
api/v1/admin_scrape.py:194: выход из блока пула потоков ждёт зависшие потоки, при том что описание на :89-95 обещает 600 мс.Числовой процент пишется в категориальный столбец —
site_finder/eesk_reserve_loader.py:239: ломает распределение вconnection_capacity_lookup.py:186-188.Цена без границ правдоподобия —
api/v1/parcels.py:2322, при том что соседние запросы по той же таблице ограничивают 30 000–600 000 (:3036,:3117). Контроль рядом, а здесь его нет.Отказ источника неотличим от «данных нет» —
scrapers/nspd_client.py:610: ошибки ячеек сетки только пишутся в журнал. Тот же вид, что весь день ловился в МЕРЕ.«Нереализованных» и «непроданных» распознаются как «продано» —
scrapers/domrf_catalog.py:80.Оговорка
Сверка — по коду, не по поведению. «Механизм на месте» не означает «проверено на проде»: для 19 закрытых я убедился, что дефект в
origin/mainне воспроизводится, но прод-подтверждения эффекта не делал. Для раздела A это приемлемо (наличие отката проверяется чтением), для разделов про данные — нет, и там при работе стоит мерить.Кластер D разобран целиком — с замерами, и один пункт закрываю БЕЗ правки
D1 —
parcels.pyobj_pricing.avg_price_per_m2_rubбез границ → PR #2863, смерженГраницы
BETWEEN 30000 AND 600000(те же, что в двух соседних запросах поobjective_lots)теперь стоят через
FILTER, а неWHERE, чтобы не поехалиunits_sold/units_available.Замер через тот же путь, что и продукт (physflat-дедуп + маппинг на
domrf_obj_id):Поправка к себе: раньше в ветке я насчитал «26 из 890, худший в 24 раза» — это был замер
по неверной популяции (все
project_name, без дедупа и маппинга, включая 573 проекта,которые до экрана не доходят). Правильные цифры выше.
Чинить всё равно надо: ограничения сверху нет по построению, максимум в таблице —
19 198 429 ₽/м² (ЖК «Дебют»), и он вне экрана только потому, что проект пока не
замаплен (308 имён из 881, список пополняется).
D2 —
geo_radius_priceмедиана без границ → правка не нужна, и вот почемуНаходка формально верна:
percentile_cont(0.5)поobjective_lots.price_per_m2_rubникаких границ не имеет. Но прежде чем ставить фильтр, замерил, что он изменит.
Медиана устойчива к хвостам по построению — это и подтвердилось:
Считал по одиночным комплексам — это самая жёсткая нарезка: реальный запрос требует
≥10 лотов и ≥2 ЖК в радиусе, объединение популяций только размывает выброс дальше.
Вывод: фильтр здесь дал бы изменение ниже шума и ещё одну ветку в запросе. Оставляю как есть,
пункт закрываю как проверенный. Если данные поедут (например, появится ЖК, где выбросы —
заметная доля лотов), сигналом станет
with_outliers; сейчас это 5 комплексов из 310.Отмечаю в чеклисте оба пункта.
D3 —
parcel_financial.pymax_building_pctбез валидацииЕщё не смотрел, остаётся открытым.
Заодно из соседних кластеров, тоже с замерами:
velocity.pyфильтр класса ссылался на алиасo, которого нет в CTE →прод-EXPLAIN даёт
missing FROM-clause entry for table "o". Ветка мёртвая (никто непередаёт
obj_class), но упала бы молча — исключение глотаетexcept, и блок темпапродаж просто исчез бы из отчёта. PR #2865.
beat_schedule.pyсчитали сдвиг МСК дважды. По логам beatзадача уходит в 21:30 UTC = 00:30 МСК, а комментарий обещал 03:30 МСК. Расписание НЕ
трогаю (03:30 МСК завело бы ETL в окно
newbuilding_enrich, с которым он делитисточник) — правлю подпись под факт. PR #2866.
best_layouts.pyподставлял0 ₽/м²там, где сделок за окно не было, хотя схемауже объявляла
float | Noneи Python уже умел None —COALESCEделал честную веткунедостижимой. PR #2868. Площадь в том же месте требует правки контракта → вынес в #2867.
Перепроверка всех оставшихся [H]-находок — 19 штук, каждая с замером и разбором скептика
Находки датированы 07.07, прошло пять недель. Прогнал их заново против
main@53bb769eи живого прода: 7 проверяющих + 7 скептиков + сверка. Скептику ставилась задача
опровергнуть, и он возвращал ДВА раздельных бита — устоял ли ВЫВОД и годно ли
ОБОСНОВАНИЕ (вывод бывает верным при негодном доводе, склеивать нельзя).
Ни одна находка не оказалась выдумкой. Но у семи обоснование пришлось переписать —
чаще всего потому, что замер делался не по той популяции или «ноль» объявлялся
результатом без названной причины.
Чинить в первую очередь (по видимости × размеру)
nspd_client.py:610-613territorial_zones_count=0у 124 из 669 дампов, из них у 50 legacy-слой сработал, а grid — нет. Близнецnspd_bulk_client.py:519-579уже починен — отзеркалитьnspd_client.py:844report_assembler.py:216-223bulk_harvest.py:402except ExceptionглушитNspdBulkWafErrorвопреки контрактуharvest_quarter. Тот же дефект ещё на:185и:282eesk_reserve_loader.py:239-242load_index(open/limited/closed). Прод: 3416 строк,open 2741 / limited 346 / closed 329— числовых пока нет, но лоадер их запишетphotos.py:74-124/app/dataв контейнере нетНе чинить — с причиной и числом
quarter_dump_lookup.py:772risks_count=0у 669/669 дампов. Унификация под соседнюю функцию ухудшила бы точностьfull_report_html.py:462pzz_zones_ekb— 0 строк с 15.06. Ленивый ход — удалить мёртвую legacy-ветку в обоих экспортёрахdomrf_catalog.py:80catalog_updated_at— 0 из 983 088 квартир. Правка на один\b, когда снимут WAF-паузуekb_ppt_tep_parser.py:72ekb_ppt_tep— 0 строк, PDF-образцов нетpage_reservation_parser.py:129land_reservation— 270 строк, всеизъятие, «резервирование» — 0 за всю историю. Правку нельзя отличить от no-opОтдельно: находка, где правка «по тексту аудита» навредила бы
A-session-3
objective_backfill.py:473-479. Аудит предлагал авто-резолвитьнеоднозначного кандидата в «единственное свободное имя». Проверяющий прогнал реальную
функцию на проде: такой авто-резолв сгенерировал бы три заведомо неверных маппинга
(ЛСР↔Голос, Формула Строительства↔Брусника, TEN↔Nova). Дефект в коде есть, но
рекомендация аудита — вредная.
Чего проверка не смогла
admin_scrape.py:194,:214) — ретенциялогов равна времени жизни контейнера, а контейнеры пересозданы сегодня в 12:31.
Инструмент слеп по построению, это не «ноль».
собрал сам из результатов 14 отработавших. Данные все на месте.
Отмечу в чеклисте только то, что реально закрыто правками; остальное остаётся открытым
с уточнёнными формулировками и приоритетом.
Находка кластера G: отрицания в регексе статуса — правку НЕ делаю, ноль заблокирован выше
domrf_catalog.py:80_STATUS_KW_REне различает утверждение и отрицание. Паттернреализован[аоы]?не имеет границы слева, поэтому:soldsoldfreeОпаснее всего на «Уровне 3» (
parse_catalog_flat, строки 563-579) — там перебираются все блоки страницы, и первое же совпадениеsoldделаетbreak. Достаточно одного счётчика «нереализованных» в сайдбаре, чтобы вся квартира стала проданной.Почему правки нет
Замер на проде (19.08):
Парсер каталога ни разу не отработал: его SELECT (
WHERE catalog_url_hash IS NOT NULL) возвращает ноль строк, а вbeat_schedule.py:436он закомментирован после WAF hard-ban ДОМ.РФ на IP сервера (#2443). Статусы в таблице пришли из JSON-свипа, где регекса нет вообще.То есть дефект реальный, но популяция затронутых строк — ноль, и останется нулём, пока не выполнятся два условия. Чинить сейчас — менять код, который нельзя ни запустить, ни проверить: тест был бы зелёным по построению, а не потому что механизм работает.
Критерий, когда это станет работой
Включать правку в тот же заход, что и расписание — не раньше и не позже:
Фикс на будущее — отрицание перед ключевым словом и граница слева:
(?<![а-яё])плюс отбрасывание совпадений, которым предшествуетне/нетв пределах пары слов. Проверять на реальном сохранённом HTML, не на синтетике.Сплошная сверка реестра с кодом (19.08): устарели 4 отметки из 59
Прогнал все 59 открытых пунктов через проверку «воспроизводится ли дефект в коде сегодня», каждый вердикт «уже починено» отдельно пытался опровергнуть скептик, читавший код заново.
Учёт оказался точнее, чем я ожидал: расхождение всего на 4 позиции, и все четыре — в безопасную сторону (числились открытыми, фактически закрыты). Реестр ни разу не соврал в опасную: ни одна отметка не была бы закрыта ошибочно.
Проставлено 3 галочки
site_finder/eesk_reserve_loader.py:239→fe019f26(14.08). Проверил сам: записи вload_indexне осталось, только комментарии, объясняющие снятие.etl/objective_backfill.py:697→ PR #2929. Половина исходной находки; core-pass остаётся открытым отдельным пунктом (обоснование замером — в комментарии к PR).exporters/full_report_html.py:1127→ закрыт как невалидный, а не как починенный._fmtшкалу не меняет, значение уже округлено вmarket_metrics.py:136, единицу несёт подпись строки; предложенная в находке правка дала бы дубль «%». Это первый пункт эпика, оказавшийся неверным по существу — шапка эпика утверждала, что таких нет.Одну галочку НЕ ставлю
api/v1/parcels.py:2381проверка тоже показала закрытым — но потому, что читала моё рабочее дерево. Правка живёт в PR #2931, который ещё не смержен. Поставить галочку сейчас значило бы записать в реестр несуществующее состояние. Отмечу после мержа.Побочные утверждения проверки, которые не подтвердились
Проверка сообщила, что
objective_backfill.py:704документирует причину отказаall_candidates_taken, которая «нигде не выставляется». Это неверно — эмиттер стоит вorigin/main:910. Проверил лично, потому что речь шла о только что смерженном коде.Второе побочное:
full_report_html.py:979/987якобы даёт дубль единицы «%». Фактически заголовок столбца «Свободно, %» и ячейка «18.4%» — избыточно, но не ошибка. Заводить не стал.Пять самых серьёзных из оставшихся 55
site_finder/quarter_dump_lookup.py:767—ST_Intersectionс обоими операндами в::geography, ровно тот баг PostGIS, который соседний_get_red_linesв этом же файле документирует и обходит. Цена: риск-зоны участка падают в ошибку или пустоту, §6 отчёта молча без пересечений ЗОУИТ.scrapers/domrf_catalog.py:80— статусный регекс без отрицаний. Цена: инверсия статуса квартир. Отдельно отмечу: правку сюда делать рано, замер выше в этой задаче показал ноль строк на проде и блокировку WAF-баном.exporters/full_report_html.py:460— fallback валидируетzoning, а таблицу строит из пустогоnspd_zoning. Цена: реальные данные ПЗЗ выбрасываются молча.etl/objective_backfill.py:469— core-pass решает «неоднозначно» до сверки с занятыми. Цена: недосбор связок Объектива; оставлен сознательно, обоснование в #2929.services/job_settings.py:142—except Exception→ fallback безdb.rollback(). Цена: тема самого эпика, чужая сессия остаётся в aborted-tx.Чего сверке не хватило
git log -S.best_layouts.py:195одной галочкой не закрывается: цена починена PR #2868, площадь вынесена в #2867. Отметку нужно разбивать.Мутационная проверка кластера A: 4 из 11 защит не сторожатся тестами
Пункты кластера A отмечены закрытыми. Проверил не «есть ли
begin_nestedв коде», а упадёт ли хоть один тест, если его снять. Метод: временно удалитьwith db.begin_nested():с де-индентацией тела, прогнать тесты, упоминающие модуль, вернуть исходник.developer_attribution.pycompetitors.pysupply_layers.pyforecasting/orchestrator.pyforecasting/macro_series.pyforecasting/special_indices.pyforecasting/sales_series.pysaturation.pyzone_regulation.pypat_lookup.pyconnection_capacity_lookup.pyЧто это значит и чего НЕ значит
Код везде на месте — я снимал защиту временно и возвращал, все исходники восстановлены (
git statusчист). Отметки о закрытии верны: дефект исправлен во всех одиннадцати.Не сторожатся именно регрессии. У четырёх мест любой будущий рефакторинг может снять SAVEPOINT, и CI промолчит.
Почему так вышло
Причина видна в
tests/test_saturation.py:143-162: мокbegin_nested— пустой контекст-менеджер, у сессии нет aborted-состояния. Поэтому второйexecuteв тесте проходит независимо от того, обёрнут первый в SAVEPOINT или нет. Тест написан аккуратно и читается убедительно — комментарий в нём даже объясняет, что на реальном Postgres было бы иначе, — но проверить он может только то, что исключение не проглотилось.Это не упрёк автору: без aborted-состояния в моке такую проверку и не сделать, а поднимать Postgres в юнит-тесте дорого.
Рабочий приём
В PR #2937 (
job_settings, последний открытый пункт кластера) мок воспроизводит семантику: упавший запрос переводит сессию вaborted, дальнейшие падают, откат SAVEPOINT восстанавливает. Плюс отдельный тест на сам мок — он требует, чтобы без SAVEPOINT мок обязательно отравлялся. Без этой проверки основные тесты были бы зелёными по построению.Тот же мок можно перенести в четыре беззубых места — правка тестовая, кода не касается.
Предложение по учёту
Отметка
[x]сейчас означает «код написан». Для кластера A это недостаточно: смысл пункта — чтобы защита работала и продолжала работать. Предлагаю считать пункт закрытым только когда снятие защиты роняет тест, и завести это отдельной задачей на четыре места.Поправка к предыдущему комментарию: метод был неточен
В таблице выше я снимал первый
with db.begin_nested():в файле, а не тот, о котором говорит находка. Для файлов с несколькими SAVEPOINT это делает вывод недействительным: тест видаassert db.begin_nested.call_count >= 1переживёт снятие одного из нескольких и промолчит.Так и оказалось у
connection_capacity_lookup.py: у него есть специальный тестtests/services/site_finder/test_connection_capacity_savepoint.py::test_uses_begin_nested_savepoint, который проверяет ровно этот вызов. Моя пометка «не сторожится» там почти наверняка неверна — я снял не тот SAVEPOINT.Перемеряю строго: снимаю все
begin_nestedв файле. Что уже подтверждено на строгом методе:saturation.py— вывод устоял. Снятие ЕДИНСТВЕННОГО SAVEPOINT оставляет 465 тестов зелёными (прогон по всем шести файлам, упоминающим модуль).pat_lookup.py— ни один тест-файл вообще не проверяет вызовbegin_nested; строгий прогон идёт.zone_regulation.py,connection_capacity_lookup.py— считаются, числа допишу.Отдельная находка, независимая от пересчёта:
call_count >= 1— слабое утверждение. Оно говорит «хоть один SAVEPOINT был», но не «нужный запрос обёрнут». В файле с несколькими защищёнными местами такой тест не заметит, что защиту сняли ровно с того, ради которого он написан. Точнее было бы проверять, что конкретныйexecuteидёт внутри savepoint'а, либо считать точное число.Вывод, который НЕ меняется: код везде на месте, речь только о защите от регрессий. Но «4 из 11» — число, которое я обязан пересчитать, и до пересчёта на него опираться нельзя.
Строгий пересчёт: не 4, а 3. И одна из моих пометок была неверной
Снимал все
with db.begin_nested():в файле (не первый попавшийся), прогонял все тест-файлы, упоминающие модуль, возвращал исходник. Дерево после прогона чистое.saturation.pyzone_regulation.pypat_lookup.pyconnection_capacity_lookup.pyconnection_capacity_lookupя пометил ошибочно. У него пять SAVEPOINT и специальный тестtest_connection_capacity_savepoint.py; мой первый прогон снял один из пяти,call_count >= 1это пережил, и я записал файл в беззащитные. Строгий прогон это опроверг: снятие всех пяти роняет 4 теста.Итог по кластеру A: 7 сторожатся, 3 нет, 1 (мой) исправлен и сторожится — то есть из 11 закрытых пунктов регрессии не отслеживаются у трёх.
Что при этом всё равно верно про
connection_capacity_lookupСнятие одного SAVEPOINT из пяти проходит незамеченным.
assert db.begin_nested.call_count >= 1отвечает на вопрос «был ли хоть один», а не «обёрнут ли нужный запрос». Файл защищён от полного отката правки и не защищён от точечного — а точечный как раз и вероятен при рефакторинге одной функции.Это не ошибка автора теста: с
MagicMockбез aborted-состояния сильнее и не сделать.Что делаю дальше
Переношу в три беззащитных места мок из PR #2937 — тот, что воспроизводит семантику Postgres (упавший запрос →
aborted; дальнейшие падают; выход из SAVEPOINT с исключением снимаетaborted), вместе с контролем на сам мок. Правка тестовая, кода не касается.Для
connection_capacity_lookupотдельно заменюcall_count >= 1на проверку, что конкретныйexecuteидёт внутри savepoint'а.Замер по
page_reservation_parser.py:129— дефект реален, неверных строк не породил_detect_kindищет «резервир» и «изъят» несвязанным поиском по всему тексту, и первое слово побеждает независимо от того, о чём документ. То есть акт об изъятии ранее зарезервированного участка получил бы тип «резервирование».Что в данных (
land_reservation, 19.08):Строк с типом «резервирование» — ноль. Если бы дефект срабатывал, они бы там были: ошибка направлена именно в эту сторону (резерв перебивает изъятие).
Проверка на неоднозначные акты по
basis_act: 0 из 297 содержат оба слова. Оговорка:basis_act— это ссылка на акт, а не полный текст PDF, по которому работает_detect_kind; полный текст не сохраняется, поэтому прямо измерить долю неоднозначных документов нельзя.Почему не чиню сейчас
Правка означала бы смену эвристики (по первому вхождению, по заголовку, по преамбуле) на выборке, где текущая эвристика не ошиблась ни разу из 297. Риск изменить верные строки есть, выигрыш не измерим.
Разумнее сначала сделать неоднозначность видимой: логировать случаи, когда в тексте есть оба слова. Тогда через какое-то время станет известно, встречается ли этот класс вообще, — и правка (если понадобится) будет опираться на числа, а не на предположение. Это отдельная маленькая задача, кодом парсера не рискующая.
Пункт оставляю открытым: дефект в коде есть, просто его цена сегодня равна нулю и это измерено, а не предположено.
Сводка за 20.08.2026
Закрыто правками
parcels.py— два блокаanalyze_parcelsite_finderppt_tep_lookupSAVEPOINT имелfull_report_html.py:460domrf_catalog_object.py:440Прод-проверка по коду в контейнере:
begin_nestedвparcels.py18 → 20, все шесть lookup'ов по 1 (уzone_regulation3).Отклонено — с замером, а не по впечатлению
quarter_dump_lookup.py:767— «_get_risk_zonesвоспроизводит PostGIS 3.4 geography × geography transform error».На боевом PostGIS 3.4.3 не воспроизводится:
Ловушка в комментарии соседнего
_get_red_linesназвана точнее, чем в пункте: она про LINESTRING, а риск-слои полигональные. И главное — функция ни разу не получала данных: риск-слои дали ноль объектов по всем 669 дампам, при 4935 ЗОУИТ и 1337 инженерных. Подробный разбор в #2934.Форма запроса действительно расходится с соседями по файлу (те делают пересечение в planar и кастуют результат). Привести к общему виду стоит — но как единообразие, а не как починку наблюдаемого дефекта, и не сейчас: пока слои пусты, правка непроверяема.
ekb_geoportal_client.py:217— «WFS-ошибка приходит под HTTP 200 с ExceptionReport-подобным JSON и молча читается как „объектов нет“».Проба с прода по четырём случаям:
Ошибки не притворяются пустым результатом — они бросают исключение, которое выше ловится и логируется (
parcels.py:logger.warning("zone regulation resolve failed …")). Описанный режим на этом геопортале не наступает.Заодно проверил смежную гипотезу — тихую усечённость выдачи:
Тоже нет.
Побочно — две тихие остановки, найденные по дороге
Пункт
competitors.py:553— остановлен на замере, вынесен в #2962Начал делать:
_SOLD_COUNT_SQLдействительно не имеет gap-fill, который есть у velocity и у цены. Скопировал мост, написал двусторонний тест на живой БД — зелено, наorigin/mainкрасно по нужной причине.Померил на боевых данных перед выпуском:
Восемьдесят восемь прежних не должны были меняться. Разбор дал две вещи.
Первая — моя. При общем
DISTINCT ONповерх обеих веток физлот с одинаковым ключом достаётся одной ветке, а вторая его теряет. Развёл дедуп по веткам — стало +185 / 0 изменившихся, как и должно.Вторая — не моя, и она серьёзнее. Сам мост сломан:
objective_lots.complex_idне связывает ЖК. На проде 28.7project_nameна одинcomplex_id(максимум 156), под «ЖК Мичуринский» лежат лоты Малахита, Амундсена, Атлас Ривер — 119 чужих ЖК. Для 185 gap-fill конкурентов подтягивается в среднем 1470 лотов, своих из них 432 — раздутие 4×.На этом мосту уже стоят живые скорость продаж и медианная цена. Мой PR стал бы третьим копированием (после #1615, где мост так же «закрыли пробел покрытия» в цене).
Ветку не выпускаю. Замеры и направление починки — в #2962. Пункт эпика остаётся открытым и блокируется тем issue: чинить надо мост, а не тиражировать его.
Пункт
poi_score.py:141— отклоняю по замеруУтверждение: кандидаты отбираются
ORDER BY distance ASC LIMIT top_n*10, поэтому POI с бо́льшим весом категории, но подальше, может не попасть в окно и выпасть из топ-7.Теоретически верно:
weight = cat_weight / (d + 100), отношение весов 6:1 (метро 6.0 против default 1.0), значит вшестеро более далёкий POI может законно обойти ближний.На проде не срабатывает.
Замер 1 — меняется ли топ-7
Взял 800 центров в самых плотных местах (сами POI как точки отсчёта — там плотность максимальна), сравнил топ-7 с окном 70 и без окна:
Замер 2 — насколько близко к грани
Считал отношение «вес лучшего POI ЗА окном» / «вес 7-го в окне». Единица означала бы ровно грань:
Даже в худшем из 683 случаев лучший исключённый POI набирает три четверти порога. Запас четверть и больше — на грани не стоим.
Механизм понятен: чтобы обойти 7-го, внешнему POI нужно
cat_weight/(d+100) > w7. В плотной застройке среди 70 ближайших почти всегда есть остановка или школа в сотне метров, и порогw7оказывается выше, чем может дать метро с окраины окна.Про вторую функцию из пункта
compute_poi_routing_decay:328ограничивает кандидатов не десятикратнымtop_n, аors_client.MAX_MATRIX_DESTINATIONS = 1000. Это важно, потому что там затухание мягче (у школ полный вес до ⅓ радиуса, у парков — ступенька на весь радиус), и к окну она чувствительнее.Но окно там не срезает вообще никогда:
Итог
Правку не делаю: она изменила бы живое ранжирование ради случая, которого нет ни в одном из 683 срезающих центров, при запасе минимум 25 %. Если плотность POI заметно вырастет или веса категорий разъедутся сильнее чем 6:1, замер стоит повторить — критерий записан здесь.
Пункт
best_layouts.py:1113— останавливаюсь, нужно решение о смысле поляРасхождение реально, и оно шире, чем в пункте: веток три, а не две.
Я довёл правку до конца — привёл все три к пост-фильтровой величине, добавил гейт, прогнал. И откатил, потому что доказать, какая семантика верна, из кода нельзя, а число видит пользователь.
Что говорит за пост-фильтровое значение
Комментарий в самом коде:
complex_groups— «знаменатель coverage», а покрытие считается по отфильтрованному набору. Две ветки из трёх так и делают.Что говорит за до-фильтровое
Имя поля.
objects_total_in_radius— «всего объектов в радиусе»: это свойство радиуса, а не пользовательского фильтра. И существующий тестtest_best_layouts.py::test_exclude_competitor_obj_idsзакрепляет именно его:Прочитать это как «снимок поведения» или как «намеренный контракт» по докстрингу нельзя — там сказано только «→ пустой ответ».
Насколько это сегодня наблюдаемо
Никак. Замер прода 20.08: 4094 разбора за 120 суток, ни одного с
exclude_competitor_obj_idsилиfilter_competitor_obj_idsв параметрах. При пустом фильтре_keepпропускает всё, и обе величины тождественно равны. Расхождение проявится при первом же использовании фильтра.Что нужно решить
Одно из двух, и оба варианта дёшевы:
raw_objects_total, так что прозрачность не теряется. Существующий тест переписывается с явным обоснованием.Первый вариант меняет число только в ветке «всё отфильтровано», второй — в штатной. Поэтому решать стоит до того, как фильтром начнут пользоваться.
Оставляю пункт открытым и помечаю как требующий решения, а не забытым.
Обновил чек-лист по факту сделанного
Счётчик расходился с реальностью: показывал 46/43, хотя за 20.08 семнадцать пунктов закрыты правками. Стало 63 закрыто / 26 открыто.
Проставлены галочки (каждая со ссылкой на PR):
Помечены БЕЗ галочки — они не сделаны, и выдавать их за сделанные нельзя:
quarter_dump_lookup.py:767ekb_geoportal_client.py:217poi_score.py:141competitors.py:553best_layouts.py:1113Разница между «отклонён» и «сделан» здесь существенная: у отклонённых есть замер, показывающий, что чинить нечего или нечем проверить. У заблокированного — причина в другом issue. Ни один из пяти не закрыт молча.
Три из них я довёл до готовой правки и откатил, когда замер или существующий тест показали, что премиса неверна.
Пункт
bulk_harvest.py:1464— отклоняю по замеру, но по дороге всплыло другоеУтверждение: при отсутствии id у WMS-фичи
zone_idсинтезируется какmd5(отсортированные properties), и две геометрически разные зоны с одинаковыми свойствами схлопываются черезON CONFLICT (zone_id) DO UPDATE— один полигон молча затирает другой.Механизм в коде именно такой (ключ —
{quarter_cad}_{md5}, то есть коллизия возможна только внутри квартала). Но на проде он ни разу не сработал:Единственная строка —
66:41:0614005, zone_id1614620304(настоящий id НСПД, не хеш), записана 24.05.2026.Что всплыло
Таблица практически пуста при том, что данные есть:
И её никто не читает. Проверил со всех сторон:
SELECTв коде — единственные упоминания это сама_save_territorial_zones(пишет) и миграция 102 (создаёт);pg_rewrite/pg_depend);pg_constraint).То есть это write-only хранилище, в которое за три месяца попала одна строка. Зонирование в отчёте берётся не отсюда, а из
nspd_dump_data["nspd_zoning"]и геопортала ЕКБ.Вывод по пункту
Чинить хеш-коллизию в ветке, которая ни разу не исполнялась, в таблице, которую никто не читает, — работа без адресата. Пункт закрываю отклонением.
Отдельный вопрос — нужна ли таблица вообще: удалять её или её наполнение я не предлагаю, это решение владельца. Но если она задумывалась как источник для чего-то, то это «оборванная проводка», а не пустой кэш: 2079 зон лежат в дампах и не доезжают.
Итог прохода 20–21.08: 80 закрыто, 9 с записанным решением, «просто не сделанных» — ноль
Реестр эпика приведён в состояние, где у каждого пункта есть либо правка, либо решение с числом. Ни одна строка больше не читается как «до этого не дошли руки».
Девять незакрытых — и почему
backend/app/services/etl/objective_backfill.py:469backend/app/services/site_finder/poi_score.py:141backend/app/services/site_finder/best_layouts.py:195backend/app/services/site_finder/quarter_dump_lookup.py:767backend/app/services/cadastre/bulk_harvest.py:1464backend/app/services/scrapers/ekb_geoportal_client.py:217backend/app/services/site_finder/best_layouts.py:1113backend/app/services/site_finder/competitors.py:553backend/app/services/scrapers/rosstat_emiss.py:228Каждое «отклонён» опирается на замер прода, а не на суждение. Самые показательные:
quarter_dump_lookup.py:767— заявленный баг PostGIS не воспроизводится на 3.4.3: 500 пар полигонов, обе формы совпали до 1e-6.bulk_harvest.py:1464— синтетическихzone_idна проде 0 из 1 строки, а таблицуcad_territorial_zonesне читает никто (вынесено в #2985).rosstat_emiss.py:228— 0 следов мохибейка в 2862 строках; перестановка кодировок сделала бы хуже. Побочно доказано, что третий элемент цепочки недостижим, а еёlogger.warningне сработает никогда.objective_backfill.py:469— гео-половина закрыта #2929, core-pass оставлен сознательно: имя и застройщик не различают «Старт» и «СТАРТ» — это разные ЖК в разных концах города.Три случая, где замер опроверг ДОВОД пункта, а вывод устоял
Это стоит отметить отдельно — если бы я чинил по формулировке, чинил бы не то:
gate_verdict.py:441— пункт говорит о смешении сетевых зон с keyword-совпадениями безnetwork_kind. Таких на проде ноль из 3493. Но вывод («конкретная причина приписывается чужой площади») верен по другой причине: смешиваются разные виды сетей — 316 пересечений, 155 зон. Починено #3001.ekb_ppt_tep_parser.py:72— пример «приведены в таблице 12» регекс не ловит: он требует именительное «Таблица N». Опасно ровно оглавление. Зафиксировано характеризующим тестом #2988.parcel_financial.py:152— пункт про валидацию входа; тесты попутно вскрыли второй дефект той же функции, существовавший независимо: пятно застройки выходило больше участка при КСИТ > 1 (#3000).Что нашлось по дороге и в эпик не входило
gisogd_permitsтерял 23.9 % реестра (2243 документа схлопывались, потому что разрешение и изменения к нему носят один номер). Починено #2987, данные вернутся прогоном 25.08.cad_territorial_zones: запрос к НСПД на каждый квартал, 1 строка в таблице, ни одного читателя.act_numberне извлекается ни у одной строкиland_reservation: регекс ждёт суффикс областных актов.ON CONFLICT DO NOTHING), понадобилась миграция-backfill.