Commit graph

264 commits

Author SHA1 Message Date
6bf1042171 Merge pull request 'fix(llm): word-boundary INN match, tighten edge cases (#1640 follow-up)' (#1703) from fix/inn-regex-word-boundary-1682 into main
Some checks are pending
Deploy / changes (push) Waiting to run
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
2026-06-17 18:34:52 +00:00
3d2d6d5da2 Merge pull request 'fix(exporters): annotate scenario deficit horizon in excel too (#1590 follow-up)' (#1702) from fix/excel-deficit-horizon-1590 into main
Some checks are pending
Deploy / changes (push) Waiting to run
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
2026-06-17 18:34:47 +00:00
9e6b0b1786 Merge pull request 'fix(weather): wind_d returns None when no valid samples, not fabricated 0.0°' (#1700) from fix/weather-wind-d-allnone into main
Some checks are pending
Deploy / changes (push) Waiting to run
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
2026-06-17 18:34:45 +00:00
be33faf73e Merge pull request 'fix(scrapers): narrow domrf Level-1 status badge-class regex (#1609 follow-up)' (#1704) from fix/domrf-badge-regex-narrow-1686 into main
Some checks are pending
Deploy / changes (push) Waiting to run
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
2026-06-17 18:34:32 +00:00
4309a12c67 fix(llm): word-boundary INN match, tighten edge cases (#1640 follow-up)
Some checks failed
CI / backend-tests (push) Has been cancelled
CI / frontend-tests (push) Has been cancelled
CI / openapi-codegen-check (push) Has been cancelled
CI / changes (push) Has been cancelled
CI / changes (pull_request) Has been cancelled
CI / backend-tests (pull_request) Has been cancelled
CI / frontend-tests (pull_request) Has been cancelled
CI / openapi-codegen-check (pull_request) Has been cancelled
Replace (?<!\d)/(?!\d) lookarounds on the digit block in _INN_RE with
(?<!\w)/(?!\w). The old (?<!\d) did not block alpha-prefixed tokens
(e.g. «ИНН ref7707083893»), and (?!\d) did not block alpha-suffixed
tokens (e.g. «ИНН 7707083893more»). \b is unsuitable here because
Python \w covers both letters and digits, so there is no \b boundary
between an alpha char and a digit char. The (?<!\w)/(?!\w) pair
correctly anchors the 10/12-digit INN block to non-word boundaries on
both sides. Context anchor and checksum gate from #1640 are unchanged.

Adds 6 regression tests covering: alpha-prefix, alpha-suffix,
embedded mid-token, 12-digit alpha-prefix, punctuation separator
(should match), and end-of-string (should match).
2026-06-17 21:24:59 +03:00
c54502ebf8 fix(exporters): annotate scenario deficit horizon in excel too (#1590 follow-up)
Some checks failed
CI / backend-tests (push) Has been cancelled
CI / frontend-tests (push) Has been cancelled
CI / openapi-codegen-check (push) Has been cancelled
CI / changes (push) Has been cancelled
CI / changes (pull_request) Has been cancelled
CI / backend-tests (pull_request) Has been cancelled
CI / frontend-tests (pull_request) Has been cancelled
CI / openapi-codegen-check (pull_request) Has been cancelled
excel.py рендерил _scenario_deficit_index (скаляр) в таблицу сценариев под
шапкой «(12 мес)» — при fallback на другой горизонт подпись лгала (то же что
docx/pptx до 764db617). Добавлены _scenario_deficit_horizon и
_scenario_deficit_cell (зеркало report_pdf.py/764db617): при fallback ячейка
несёт «(гор. N мес)». _build_scenarios_sheet переключён на _scenario_deficit_cell.
Тесты: два новых кейса — fallback (6 мес → аннотация) и primary (12 мес → скаляр).
2026-06-17 21:23:57 +03:00
6cf65eb26b fix(weather): wind_d returns None when no valid samples, not fabricated 0.0° (#weather-cache)
Some checks failed
CI / changes (push) Has been cancelled
CI / backend-tests (push) Has been cancelled
CI / frontend-tests (push) Has been cancelled
CI / openapi-codegen-check (push) Has been cancelled
CI / backend-tests (pull_request) Has been cancelled
CI / frontend-tests (pull_request) Has been cancelled
CI / openapi-codegen-check (pull_request) Has been cancelled
CI / changes (pull_request) Has been cancelled
When all winddirection_10m_dominant samples were None (or the key was
absent), atan2(0, 0) produced 0.0° (due north) as a fabricated value.
Root causes:
  1. [None, None, ...] is truthy → entered circular-mean branch, but
     filtered sums x=y=0 → atan2(0,0)=0 → 0.0°
  2. Empty list → else 0.0 branch → same fabrication

Fix: filter None from wind_d before aggregation (consistent with t_max /
t_min / uv). Only compute circular mean when at least one valid sample
exists; otherwise dominant_direction_deg and dominant_direction_label are
None. Adds TestWindDirectionAllNone covering all-None, missing key, valid
samples sanity, and mixed None+valid cases.
2026-06-17 21:22:31 +03:00
827459e72a test(site-finder): cover is_residential_zone raw_props/subcategory branch (#1353 follow-up)
Some checks failed
CI / changes (push) Has been cancelled
CI / backend-tests (push) Has been cancelled
CI / frontend-tests (push) Has been cancelled
CI / openapi-codegen-check (push) Has been cancelled
CI / changes (pull_request) Successful in 5s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Failing after 1m46s
CI / backend-tests (pull_request) Failing after 8m53s
2026-06-17 21:22:04 +03:00
c3c89646ce fix(scrapers): narrow domrf Level-1 status badge-class regex (#1609 follow-up)
Some checks failed
CI / changes (push) Successful in 8s
CI / changes (pull_request) Has been cancelled
CI / backend-tests (pull_request) Has been cancelled
CI / frontend-tests (pull_request) Has been cancelled
CI / openapi-codegen-check (pull_request) Has been cancelled
CI / backend-tests (push) Has been cancelled
CI / frontend-tests (push) Has been cancelled
CI / openapi-codegen-check (push) Has been cancelled
_STATUS_BADGE_CLS_RE was too broad: `status|badge|tag|chip|label` matched
generic UI elements (e.g. <span class="label">, <span class="chip">) that
are unrelated to the sale-status badge, risking picking the wrong block.

Narrowed to `(?<![a-z])status(?![a-z])` — requires the literal token
"status" as a hyphen-delimited component of the CSS class (matches
`status-badge`, `flat-status-tag`, `object-status` but not bare `label`,
`tag`, `chip`, `badge`).

Adds regression test: page with generic label/tag/chip/badge elements
containing "В продаже" must NOT activate Level-1; only the real
`status-badge` block ("Продана") should be returned → status=sold.
2026-06-17 21:21:56 +03:00
f55e83a150 Merge pull request 'fix(site-finder): NO_ENGINEERING_NEARBY is a warning, not a verdict blocker (#1617)' (#1697) from fix/gate-verdict-no-blocker-1617 into main
Some checks are pending
Deploy / changes (push) Waiting to run
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
2026-06-17 18:11:44 +00:00
96f42cd033 Merge pull request 'fix(forecasting): thread room_bucket velocity into format ranking (#1593)' (#1696) from fix/base-pace-room-bucket-1593 into main
Some checks are pending
Deploy / changes (push) Waiting to run
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
2026-06-17 18:08:26 +00:00
ac19ef4823 fix(site-finder): NO_ENGINEERING_NEARBY is a warning, not a verdict blocker (#1617)
Some checks failed
CI / changes (push) Successful in 9s
CI / changes (pull_request) Successful in 7s
CI / frontend-tests (push) Has been skipped
CI / backend-tests (pull_request) Has been cancelled
CI / openapi-codegen-check (push) Failing after 1m40s
CI / frontend-tests (pull_request) Has been cancelled
CI / openapi-codegen-check (pull_request) Has been cancelled
CI / backend-tests (push) Failing after 8m47s
Introduce INFORMATIONAL_WARNING_CODES frozenset; verdict assembly now only
downgrades to «С ограничениями» when verdict-relevant warnings exist.
NO_ENGINEERING_NEARBY still appears in warnings[] for display but a clean
residential parcel (Ж-zone, no ЗОУИТ) correctly receives verdict_label=«Можно».
2026-06-17 20:59:32 +03:00
86d0475f45 Merge pull request 'fix(forecasting): deal_count confidence note carries «за N мес» window (#1637)' (#1684) from fix/confidence-deal-count-window-1637 into main
Some checks are pending
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / changes (push) Successful in 8s
2026-06-17 17:57:41 +00:00
9a0ca9336f Merge pull request 'fix(scrapers): scope+stem domrf status classification (#1609)' (#1686) from fix/domrf-status-classify-1609 into main
Some checks are pending
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Waiting to run
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
2026-06-17 17:55:42 +00:00
cc6ef80d07 fix(forecasting): thread room_bucket into base_pace/compute_market_metrics for real format ranking (#1593)
Some checks failed
CI / backend-tests (push) Blocked by required conditions
CI / frontend-tests (push) Blocked by required conditions
CI / openapi-codegen-check (push) Blocked by required conditions
CI / changes (push) Successful in 8s
CI / changes (pull_request) Successful in 8s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m42s
CI / backend-tests (pull_request) Has been cancelled
Add `velocity_by_room: dict[str, float] | None` to `MarketMetrics` — per-bucket
unit velocity (ед./мес) derived from the existing `sold_by_room` ROLLUP data that
`_query_sales_window` already returns. No new SQL required.

Thread per-bucket velocity through `_demand_only_overlay` via the new
`_FORECAST_TO_METRIC_BUCKETS` constant that maps each forecast bucket to its
market_metrics room-bucket keys. "80+ м²" sums "4" + "5+" keys. Fallback to
aggregate `unit_velocity` when `velocity_by_room` is None (thin-data path).

Previously `base_pace` was identical for all 5 room-buckets, so §9.4 norm and §9.2
base_pace cancelled out in pace/max_pace and ranking was driven purely by §9.5
macro_coef (segment steepness proxy). Now §9.2 reflects real per-bucket observed
demand from objective_lots.contract_date data.

Callers of `compute_market_metrics` that don't use `velocity_by_room` are unaffected
(the new field is additive to the frozen dataclass). All existing callers verified —
none construct `MarketMetrics` directly except the one production site.
2026-06-17 20:55:34 +03:00
435f7249d0 Merge pull request 'fix(scrapers): disambiguate EMISS yearly vs Q1 in dedup key (#1606)' (#1687) from fix/emiss-period-dedup-1606 into main
Some checks are pending
Deploy / changes (push) Waiting to run
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
2026-06-17 17:55:27 +00:00
b92d3aeb65 Merge pull request 'fix(forecasting): correct _geo_weight unknown floor so far projects rank low (#1633)' (#1685) from fix/geo-weight-floor-1633 into main
Some checks are pending
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Waiting to run
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
2026-06-17 17:55:17 +00:00
a1adf2fd19 Merge pull request 'fix(forecasting): count non-zero observations in normalize guard (#1638)' (#1683) from fix/normalize-zero-fill-guard-1638 into main
Some checks are pending
Deploy / changes (push) Waiting to run
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
2026-06-17 17:55:12 +00:00
c0454d8905 Merge pull request 'fix(llm): INN redaction requires context anchor, stop eating large numbers (#1640)' (#1682) from fix/inn-regex-context-1640 into main
Some checks are pending
Deploy / changes (push) Waiting to run
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
2026-06-17 17:55:06 +00:00
7d5962e7b4 Merge pull request 'fix(site-finder): normalize POI weighted score to 0..100 on backend (#1486)' (#1674) from fix/poi-score-scale-1486 into main
Some checks are pending
Deploy / changes (push) Waiting to run
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
2026-06-17 17:54:52 +00:00
b534579e1b fix(site-finder): correct POI-score mock shape+values, routing empty assert (#1486 review)
Some checks failed
CI / changes (pull_request) Successful in 8s
CI / changes (push) Successful in 9s
CI / backend-tests (push) Has been skipped
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) Has been skipped
CI / frontend-tests (pull_request) Successful in 49s
CI / openapi-codegen-check (pull_request) Failing after 1m35s
CI / backend-tests (pull_request) Failing after 8m47s
- Rename top_poi → items in poi-score.json to match PoiScoreResponse TS type
  (mock was cast as PoiScoreResponse but had wrong field name → items undefined
  at runtime in MOCK_POI_SCORE mode → PoiList2Gis crashed at [...items].sort)
- Recompute all score_contribution values using backend formula
  (weight / _MAX_STRAIGHT_SCORE * 100, _MAX_STRAIGHT_SCORE=0.315) and
  poi_weighted_score=19.9 (was 72, which was inconsistent with the new normalization)
- Add assert result.poi_weighted_score == 0.0 to test_routing_decay_empty_db
  to match the straight-line empty-db assertion
- Remove stale comment in PoiList2Gis.tsx saying normalization needs fixing in
  site-finder-api.ts (already done backend-side in this PR)
2026-06-17 20:47:01 +03:00
e4975c486f fix(forecasting): correct _geo_weight decay/floor so far projects get low weight (#1633)
Some checks failed
CI / changes (push) Successful in 7s
CI / changes (pull_request) Successful in 11s
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) Has been skipped
CI / backend-tests (push) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m40s
CI / backend-tests (pull_request) Failing after 8m48s
_GEO_WEIGHT_UNKNOWN was 0.1, which equals exp(−6.9/3)≈0.10 (weight of a
confirmed-far project at ~6.9 km). Projects beyond that distance got a
weight *below* 0.1, meaning unknown-coordinate projects outweighed
confirmed-far ones — an inversion of the documented intent.

Lowered to 0.05 (≈ exp(−3) = exp(−9 km / scale)), restoring the correct
hierarchy: confirmed-close > confirmed-far > unknown. Updated TestGeoWeight
(hardcoded 0.1 expectation) and TestCannibalizationTrueMode (overlap ×
floor comment/value) accordingly. Added two new assertions in TestGeoWeight
that enforce the hierarchy monotonically and verify unknown < exp(−6.9/3).
2026-06-17 20:44:00 +03:00
5c5be890ab fix(llm): INN redaction requires context/checksum, stop eating large numbers (#1640)
Some checks failed
CI / changes (push) Successful in 8s
CI / changes (pull_request) Successful in 7s
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) Has been skipped
CI / backend-tests (push) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m42s
CI / backend-tests (pull_request) Failing after 8m59s
_INN_RE now requires an explicit ИНН/inn keyword anchor (case-insensitive) within
~20 chars before a 10/12-digit block. Bare digit sequences without the keyword are
no longer candidates — eliminates false-positives on large monetary amounts such as
1 200 000 000 (which coincidentally passes the ФНС checksum). Checksum validation
is kept as a second gate to avoid redacting e.g. «ИНН 1234567890» with bad digits.

_inn_repl updated to use match.group(1) (digit-only capture group) instead of the
full match that now also includes the keyword prefix.

7 new regression tests in test_redaction.py: bare large numbers not redacted,
keyword-cued real INNs (10/12 digit) still redacted, bad-checksum + keyword left
intact, latin «inn:» accepted.
2026-06-17 20:41:39 +03:00
4f0cff92ec fix(scrapers): scope+stem domrf status classification, fix продано/продана mismatch (#1609)
Some checks failed
CI / changes (push) Successful in 8s
CI / changes (pull_request) Successful in 7s
CI / backend-tests (push) Has been skipped
CI / openapi-codegen-check (push) Has been skipped
CI / frontend-tests (push) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m39s
CI / backend-tests (pull_request) Failing after 8m48s
Replace whole-HTML re.search for status with a 3-level block-scoped strategy:
1. CSS badge classes (status/badge/tag/chip/label) — highest precision.
2. Proximity to block labelled «Статус» via _find_text_near.
3. Full blocks scan where sold/reserved keywords always beat free —
   preventing «в продаже» from nav/similar-flats sections from
   misclassifying sold flats as free.

Add _STATUS_KW_RE and _classify_status_kw at module level with full
morphological coverage: продан/продана/продано, реализован[аоы]?,
забронирован[аоы]?, свободн[аоы]?.

Add 31 tests in test_domrf_catalog_parse.py covering all three
extraction levels plus regression for fem. word-form «Квартира продана».
2026-06-17 20:40:49 +03:00
9dbfb03a3d fix(scrapers): disambiguate EMISS yearly vs Q1 in dedup key (#1606)
Some checks failed
CI / changes (push) Successful in 7s
CI / changes (pull_request) Successful in 7s
CI / backend-tests (push) Has been skipped
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m38s
CI / backend-tests (pull_request) Failing after 8m41s
_YEAR_PERIODS ('год') → month=1 collided with Q1 ('I квартал') → month=1
for the same region/year: both produced obs_date=YYYY-01-01, so the second
series in the XML silently overwrote the first in the in-memory by_key dict.

Fix: add _emiss_period_granularity() and extend the dedup key from
(region, obs_date) to (region, obs_date, granularity). 'year' and 'quarter'
are now distinct slots, so both rows survive the parser and reach the upsert.

Test: test_yearly_and_q1_both_survive_dedup verifies len==2 with both
values present; test_period_granularity covers all classification branches.
2026-06-17 20:39:52 +03:00
494155654e fix(forecasting): count non-zero observations in normalize guard, not zero-filled buckets (#1638)
Some checks failed
CI / changes (push) Successful in 8s
CI / changes (pull_request) Successful in 6s
CI / backend-tests (push) Has been skipped
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m59s
CI / backend-tests (pull_request) Failing after 8m52s
_count_full_years treated units=0 as a valid observation, so a series
where fill_month_grid zero-filled every month still accumulated 3 full
years and passed the _MIN_FULL_YEARS guard.  Zero-filled months carry no
seasonal signal, so they must be skipped in the year counter — the same
way None values already were.

Fix: skip v==0 alongside v is None in _count_full_years.
Add four tests: zero-filled 36-month series → n_full_years=0/applied=False;
partial-coverage years (only 6 non-zero months/year) → not counted as full;
real non-zero series still passes guard; normalize_demand on zero-filled
SalesSeries returns series unchanged.
2026-06-17 20:39:00 +03:00
afa7d80ecf fix(forecasting): deal_count confidence note carries «за N мес» window (#1637)
Some checks failed
CI / changes (push) Waiting to run
CI / backend-tests (push) Blocked by required conditions
CI / frontend-tests (push) Blocked by required conditions
CI / openapi-codegen-check (push) Blocked by required conditions
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m53s
CI / backend-tests (pull_request) Failing after 8m54s
Add `deal_count_months: int | None = None` to `compute_report_confidence`.
When provided, threads it as suffix into `_factor_from_count` so the
deal_count ConfidenceFactor note reads «7 сделок за 6 мес — мало» instead
of the windowless «7 сделок — мало». Existing callers unaffected (default None).

Tests: two new cases in TestComputeReportConfidence — with/without window.
2026-06-17 20:38:00 +03:00
68eb6ded89 fix(site-finder): normalize POI weighted score to 0..100 on backend (#1486)
Some checks failed
CI / changes (push) Successful in 10s
CI / changes (pull_request) Successful in 12s
CI / frontend-tests (push) Successful in 49s
CI / openapi-codegen-check (push) Failing after 1m45s
CI / backend-tests (push) Failing after 8m57s
CI / frontend-tests (pull_request) Successful in 45s
CI / openapi-codegen-check (pull_request) Failing after 1m37s
CI / backend-tests (pull_request) Failing after 8m53s
Backend now computes poi_weighted_score and per-POI score_contribution in
0..100 range. Normalization denominator = sum of top-7 category weights / 100
(straight-line mode) or sum of top-7 category weights (routing-decay mode).
Frontend stops reconstructing score from raw weight × 100.
2026-06-17 20:26:35 +03:00
20efc3bf64 merge: resolve conflicts with main (#1660) into week-review-finish
Some checks failed
CI / backend-tests (pull_request) Failing after 9m11s
CI / backend-tests (push) Failing after 9m22s
CI / openapi-codegen-check (push) Successful in 2m14s
CI / changes (pull_request) Successful in 6s
CI / changes (push) Successful in 8s
CI / frontend-tests (pull_request) Successful in 1m13s
CI / frontend-tests (push) Successful in 1m2s
CI / openapi-codegen-check (pull_request) Successful in 1m47s
5 conflicts resolved:
- redaction.py: merged INN checksum + _PHONE_LOCAL_RE + _Repl alias from main
- recommendation.py: kept commercial_sell_through_pct rename (#1635)
- report_pdf.py: kept _scenario_deficit_cell + int|None return type (#1590)
- parcel.py: kept main's detailed docstring (functionally identical)
- alembic/env.py: kept HEAD (app.models import in __init__ already covers all)
2026-06-17 20:21:06 +03:00
14f3ef2019 fix(week-review): backend-аудит v2 — 82 фиксов (#1660)
All checks were successful
Deploy / build-worker (push) Successful in 2m47s
Deploy / deploy (push) Successful in 1m20s
Deploy / changes (push) Successful in 9s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m52s
Co-authored-by: bot-backend <bot-backend@gendsgn.local>
Co-committed-by: bot-backend <bot-backend@gendsgn.local>
2026-06-17 17:13:38 +00:00
c9ba595abe refactor(forecasting): rename commercial_share_pct → commercial_sell_through_pct (#1635)
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (push) Successful in 1m40s
CI / openapi-codegen-check (push) Successful in 1m38s
CI / backend-tests (pull_request) Successful in 9m51s
CI / changes (push) Successful in 7s
CI / frontend-tests (pull_request) Successful in 53s
CI / openapi-codegen-check (pull_request) Successful in 3m10s
CI / backend-tests (push) Successful in 9m52s
Внутренний recommendation→product_scoring контракт-ключ был мислейблом: величина —
темп распродажи нежилого (sell-through, прокси ликвидности/спроса), а НЕ доля нежилого
в объёме застройки. Переименован ключ + исправлены reason/docstring/комментарии у
потребителя _score_commercial. Числовая логика не изменена. Ключ внутренний (нет
frontend/schema/openapi-потребителей) → rename контракт-безопасен. pytest 171 passed.

Closes #1635
2026-06-17 13:32:03 +05:00
be70cacf18 test: выровнять weather_cache/nspd_geo тесты под #1377/#1336 (CI red→green)
All checks were successful
CI / changes (push) Successful in 7s
CI / changes (pull_request) Successful in 9s
CI / frontend-tests (push) Successful in 1m4s
CI / frontend-tests (pull_request) Successful in 57s
CI / openapi-codegen-check (push) Successful in 2m7s
CI / openapi-codegen-check (pull_request) Successful in 1m41s
CI / backend-tests (push) Successful in 8m59s
CI / backend-tests (pull_request) Successful in 8m58s
backend-tests CI упал на 4 стейл-тестах — код автофиксов обновили, тесты нет:
- test_weather_cache: фикстура _make_air_response отдавала hourly-форму, а код
  (#1377) теперь читает current-bucket → current.time пуст → None. Фикстура → current.
- test_nspd_geo: 2 теста стерегли ошибочный ST_Transform(3857→4326), который #1336
  убрал (геометрия уже WGS84, иначе Null Island). Ассерты через _strip_sql_comments
  (как сиблинг parcel-тест) → ST_SetSRID(...,4326) без ST_Transform. Переименованы.

Локально: 38 passed (оба файла).
2026-06-16 08:35:14 +03:00
cab19020a9 perf(site-finder): pushdown obj_pricing CTE to nearby competitors (#70/#307) (#1335)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m40s
Deploy / build-worker (push) Successful in 2m38s
Deploy / deploy (push) Successful in 1m12s
2026-06-14 16:58:43 +00:00
b405ae291d feat(site-finder): additive objective_lots price fallback for competitors (#307 OBJ-3) (#1332)
All checks were successful
Deploy / changes (push) Successful in 7s
Deploy / build-backend (push) Successful in 1m57s
Deploy / build-worker (push) Successful in 2m51s
Deploy / build-frontend (push) Successful in 3m6s
Deploy / deploy (push) Successful in 1m12s
2026-06-14 16:10:02 +00:00
72e0b050f9 feat(site-finder): POI saturation per capita по районам ЕКБ (#42) (#1329)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m38s
Deploy / build-worker (push) Successful in 2m52s
Deploy / deploy (push) Successful in 1m12s
2026-06-14 15:05:22 +00:00
e5e0ccb283 feat(site-finder): сетевые обременения из ЗОУИТ (#1070, Refs #103) (#1328)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m36s
Deploy / build-worker (push) Successful in 2m40s
Deploy / deploy (push) Successful in 1m12s
2026-06-14 14:49:32 +00:00
5507bbc1a4 feat(site-finder): domrf→cad geom-match + parking_ratio конкурентов (#96) (#1327)
Some checks failed
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / changes (push) Has been cancelled
2026-06-14 14:49:28 +00:00
2c22c3f7ea feat(site-finder): per-building помещения/машино-места + parking_ratio (on-demand MVP, #96)
All checks were successful
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
CI / changes (push) Successful in 8s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (push) Successful in 1m59s
CI / openapi-codegen-check (pull_request) Successful in 1m58s
CI / backend-tests (push) Successful in 8m48s
CI / backend-tests (pull_request) Successful in 8m48s
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 4m58s
Deploy / build-worker (push) Successful in 5m46s
Deploy / deploy (push) Successful in 1m30s
Оживляет мёртвый foundation list_objects_in_building (#168 Q3-deferred). Fix реального
блокера: NSPDOptions не парсил objdocId (camelCase из NSPD search) → objdoc_id всегда None,
метод нельзя было вызвать. AliasChoices(objdoc_id, objdocId) + registers.

premises_lookup.get_building_premises(cad_num) — резолв objdoc → list_objects_in_building,
graceful (WAF/сеть/not-found → None). parking_ratio = машино-места/помещения (None при 0
помещений, 0.0 при реальном отсутствии паркинга). Verified live 66:41:0106036:183.

MVP on-demand (без bulk/схемы/prod-записей). parking_ratio готов, но НЕ wired в analyze:
конкуренты приходят из ДОМ.РФ без cad_num → нужен domrf↔cad_buildings geom-match (отдельная
задача). Сервис примет cad_num как только matching появится. 11 тестов, ruff clean.

Refs #96
2026-06-14 18:48:16 +05:00
d38417d8af feat(admin): freshness endpoint (#73) + README Forgejo refresh (#83) + migrations doc (#72)
All checks were successful
Deploy / build-frontend (push) Has been skipped
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Successful in 2m39s
Deploy / build-worker (push) Successful in 7m20s
Deploy / deploy (push) Successful in 2m48s
#73: GET /admin/scrape/freshness — last_success/age/freshness/status для 5 источников
(kn/objective/nspd/nspd_geo/cadastre) + overall_status (worst по critical). Prod-verified
имена таблиц, FILTER/COALESCE корректны, CAST(:d AS interval). +4 теста.
#83: README — GitHub→Forgejo CI/CD, секция 6 subagents, ссылки перемаплены.
#72: docs/Schema_Migrations_vs_Alembic.md — _schema_migrations паттерн (не Alembic),
рекомендация снести пустой alembic-скелет (отдельный PR).

Closes #73
Closes #83
Closes #72
2026-06-13 18:25:32 +00:00
50555821fb feat(site_finder): routing POI decay (#41) + OSM utility distances (#44) + ЗОУИТ geom (#255bk)
Some checks failed
CI / changes (push) Successful in 7s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 7s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 7m42s
CI / backend-tests (pull_request) Successful in 7m40s
Deploy / changes (push) Has been cancelled
Deploy / build-backend (push) Has been cancelled
Deploy / build-worker (push) Has been cancelled
Deploy / build-frontend (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
#41: per-category routing-radius decay через reusable ORS /matrix client (foot-walk
время + per-category пороги + decay-curves), opt-in ?decay=routing, graceful straight-line
fallback (analyze не 500 при ORS down) + length-guard durations==destinations.
#44: +5 OSM-категорий (transformer/gas/water/sewerage/heat) via Overpass nwr;
nearest_{water_main,substation,gas,heat}_m в analyze.utilities.
#255bk: ST_AsGeoJSON(CAST(geom AS geometry)) в _get_cad_zouit_overlaps → geom_geojson.

Closes #41
Closes #44
2026-06-13 22:31:56 +05:00
658d724075 feat(analytics): velocity-anomaly alerts (#17) + ARN ДДУ price indicator (#99)
Some checks failed
CI / changes (push) Successful in 8s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 8s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 7m37s
CI / backend-tests (pull_request) Successful in 7m42s
Deploy / deploy (push) Blocked by required conditions
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / changes (push) Has been cancelled
#17: detect_velocity_anomalies + GET /analytics/velocity-alerts — z-score drop-detection
на domrf_kn_sale_graph (double-gate z≤-2 AND drop≤-30%, starvation-guards). Snapshot=richest
(не MAX — prod новейшие частичные), lookback anchored на latest report_month (scrape лаг ~4мес).
Prod: ЖК Центральный Парк -69%, ~14ms.

#99: mv_ddu_price_indicator (миграция 152) + POST /market/ddu-indicator — ARN-mirror
ценовой индекс per quarter×area_bucket из rosreestr_deals (ДДУ регион 66). MVP: subject-level,
period Q, window 2025-Q2+, methods 1/2 (basis/previous, prev_period_value honesty). Q1-2026
headline 1.0185 vs ARN 1.03 (±5%). Method 3 blocked (нет pre-2025-Q2 данных) — задокументировано.

Closes #17
Closes #99
2026-06-13 22:04:49 +05:00
8242e51b61 feat(generative): exporters (dxf/pdf) + generative test suite (#54 #55 #56)
Some checks failed
CI / frontend-tests (push) Has been skipped
CI / changes (push) Successful in 10s
CI / changes (pull_request) Successful in 7s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 7m39s
CI / backend-tests (pull_request) Successful in 7m39s
Deploy / deploy (push) Has been cancelled
Deploy / build-frontend (push) Has been cancelled
Deploy / build-backend (push) Has been cancelled
Deploy / build-worker (push) Has been cancelled
Deploy / changes (push) Successful in 6s
Дополняет движок: exporters/{dxf,pdf}.py (ezdxf + WeasyPrint lazy) + 5 тест-модулей
(geometry/placement/teap_financial/exporters/api). Не вошли в предыдущий commit
(untracked-директории).
2026-06-13 21:33:20 +05:00
3b79533f9f feat(generative): движок концепций Stage 1a/1b/1c (#54 #55 #56)
Some checks failed
CI / changes (push) Successful in 7s
CI / frontend-tests (push) Has been skipped
CI / backend-tests (push) Has been cancelled
Заменяет generative stubs детерминированным end-to-end пайплайном:
- 1a geometry: WGS84-parse, метрическая AEQD-репроекция, setback-буфер, placement grid
- 1b placement: greedy section-fill + STRtree коллизии (full-gap), 3 стратегии, coverage cap
- 1c teap/financial: площади/квартиры/FAR/паркинг; выручка/затраты/маржа + IRR-proxy
- exporters: ezdxf DXF (геометрия) + WeasyPrint ТЭП/фин PDF (lazy import)
generate() заменил generate_stub в route (422 на degenerate). mypy-strict + ruff clean,
31 generative-тест + full suite 3078 passed. ConceptVariant заполняется реальными числами.

Closes #54
Closes #55
Closes #56
2026-06-13 21:32:32 +05:00
1883271908 fix(cadastre): NSPD 500-retry + per-layer skip (#252) + disable dead pzz-beat (#259) + geom backfill (#200)
Some checks failed
CI / changes (push) Successful in 7s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 6m43s
CI / backend-tests (pull_request) Successful in 6m41s
Deploy / build-backend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / changes (push) Has been cancelled
#252: NspdBulkServerError на HTTP 5xx + WMS <ServiceException>-body → quarter-level
autoretry; _grid_walk_category флагует layer_failed только если ВСЕ cells 5xx (none ok),
network-errors исключены — пустой/флапающий слой не ложно-скипается. harvest_meta.layer_X_failed.

#259: pzz-sync-monthly beat отключён (PKK6 deprecated, pzz_zones_ekb=0, заменён
zone_regulation_cache+NSPD dumps). logger.exception→warning (стоп GlitchTip BACKEND-1B),
raise сохранён для admin-endpoint.

#200: geom_unavailable флаг (миграция 151) + backfill_parcel_geom task — grid-walk
layer 36368 per quarter лечит 930/964 NULL-geom участков (NSPD search вернул центроиды),
остаток marks unavailable (не ретраить вечно). SAVEPOINT per-cell, idempotent.

Closes #252
Closes #259
Closes #200
2026-06-13 20:45:05 +05:00
84d08940c6 fix(analytics): свежая ипотечная ставка + recommend last_updated + drop dead route (#236 #237 #251)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Successful in 3m9s
Deploy / build-backend (push) Successful in 4m12s
Deploy / build-worker (push) Successful in 5m31s
Deploy / deploy (push) Successful in 1m19s
#236: _current_mortgage_rate сортировал cbr_mortgage_series по TEXT-полю period
лексикографически ('Январь'>'Февраль') → возвращал stale-точку. Сортировка по
parsed (year, month) + regex-guard на malformed period-строки. Остаётся на
subsidized weighted series (НЕ domrf market — иная метрика). Verified prod:
Февраль 2026 (было Январь 2026).

#237: scope.last_updated = MAX(snapshot_date) domrf_kn_objects в recommend_mix +
подпись «данные на <дата>» в recommend page.

#251: удалён never-implemented GET /parcels/{parcel_id} (501) + ParcelDetail схема
(frontend bare 1-сегментный route не зовёт).

Closes #236
Closes #237
Closes #251
2026-06-13 15:22:22 +00:00
3cf9fad683 fix(backend): экранировать Excel formula-injection (#1244) + увести chat-чтение с event loop (#1245)
Some checks failed
CI / changes (push) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 7s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 6m38s
CI / backend-tests (pull_request) Successful in 6m37s
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Has been cancelled
#1244 (security): внешние/скрейпинг-строки (comm_name из DOM.РФ, headline/usp_text)
с ведущим = + - @ \t \r писались как есть → openpyxl сохранял как формулы
(data_type='f'), исполнялись при открытии в Excel/LibreOffice. _sanitize_formula
префиксует такие строки апострофом (OWASP CSV-injection escape); числа/даты/bool
не трогаются. _write_kv labels тоже санитизируются. Подтверждено на openpyxl 3.1.5.

#1245 (concurrency): async ask() вызывал sync get_report_for_chat() (sync SQLAlchemy
тянет крупный JSONB §22-отчёт) напрямую — блокировал event loop, в отличие от
LLM-ветки (run_in_threadpool). Обёрнуто в run_in_threadpool.

Closes #1244
Closes #1245
2026-06-13 18:10:21 +05:00
99c9a130bc feat(forecast): расширить горизонт прогноза до 24 мес (ТЗ §12.1)
All checks were successful
CI / changes (push) Successful in 6s
CI / frontend-tests (push) Successful in 46s
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Successful in 46s
CI / backend-tests (push) Successful in 6m35s
CI / backend-tests (pull_request) Successful in 6m30s
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Successful in 2m14s
Deploy / build-frontend (push) Successful in 2m55s
Deploy / build-worker (push) Successful in 4m6s
Deploy / deploy (push) Successful in 1m40s
API отвергал ?horizon=24 (422), хотя ТЗ §12.1 называет 6/12/18/24, а движок
УЖЕ считает 24 на каждом ране: _DEFAULT_HORIZONS=(6,12,18,24) во всех 6 точках
стека (orchestrator/forecast-task/demand_supply_forecast/scenarios/
special_indices/report_assembler), PIPELINE_HORIZON_MONTHS=24.
_hidden_release_fraction клампит h/18→1.0 на 24 (без переполнения),
future_supply._horizon_weight расширяет окно чисто — скрытых ≤18 потолков нет.
Чистое расширение валидатора-enum, не новая математика.

Backend: _ALLOWED_FORECAST_HORIZONS → {6,12,18,24}, Query/docstring/error-msg.
Frontend: HorizonSelector HORIZONS=[6,12,18,24] (тип horizon=number, union не нужен;
прочие потребители data-driven через meta.horizons/forecasts_by_horizon).
Тесты: API принимает 24/отвергает 30; движок-тесты доказывают h=24 осмыслен
(поля посчитаны, demand(24)>demand(18), hidden созрел, индексы в диапазонах).

Closes #944 (Q1 горизонт 24)
2026-06-13 17:27:35 +05:00
b211183940 feat(site-finder): детерминированная атрибуция застройщика в analyze (#1088)
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / changes (push) Successful in 8s
CI / frontend-tests (push) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 6m33s
CI / backend-tests (pull_request) Successful in 6m33s
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m44s
Deploy / build-worker (push) Successful in 5m24s
Deploy / deploy (push) Successful in 1m30s
Сервис get_developer_attribution поверх fn_developer_for_parcel (миграция 149):
топ-1 застройщик участка + track-record (РНС/РВЭ + домрф) + nearby_developers.
Дедуп по норм-ИНН (лучший match_method первым). Wire в analyze additive без флага
(чистый DB-резолвер по индексам, graceful → None). Beat-refresh developer_registry
ежемесячно 05:30 МСК после ekburg-permits. Pydantic-схемы + 18 тестов.

Источники: ekburg_construction_permits (РНС, ИНН) ⋈ domrf_kn_objects по
нормализованному ИНН + spatial/quarter fallback. 68% domrf-застройщиков имеют РНС.
EXPLAIN: point-lookup 0.06ms, резолвер ~30ms (functional/GIST индексы в миграции).

Closes #1088
2026-06-13 16:13:45 +05:00
c3c6e2cef1 fix(okn): expand registry coverage + load detail-card name/address (#1159)
okn_objects_sync собирал 428 ОКН из ~798 ЕГРОКН и сохранял raw_props={id}
без названия/адреса — analyze показывал 'ОКН рядом' без указания КАКОГО.

Coverage: расширил _ADDRESS_PASSES с 2 до 11 подстрок (Екатеринбург,
г. Екатеринбург, 7 районов ЕКБ, 2 формы Свердловской обл.). _PASSES =
декартово (address × category). first-wins дедуп сохраняет высшую категорию.

Detail: fetch_okn_detail(source_id) — GET Show/Show?id=, BeautifulSoup
парсит HTML (dl/dt/dd и tr/td), извлекает name, address, protection_category,
object_kind, dating. _enrich_with_detail сливает в raw_props с graceful
failover (cap=1500, exception per-object не валит прогон).

Lookup: parcel_okn_objects возвращает name/address из raw_props->>'name'.
Existing ird_analyze тесты не сломаны.

49 okn-related тестов passed, ruff clean.

Closes #1159
2026-06-13 15:02:50 +05:00
2e428405d2 fix(site-finder): расширить except в parcel_ird_overlaps до DataError (#1095)
parcel_ird_overlaps ловил только (OperationalError, ProgrammingError).
При D9b-wiring в analyze malformed WKT в ST_GeomFromText давал PostGIS
ERROR → SQLAlchemy DataError, который пробивал try/except → analyze
падал вместо graceful-degrade.

- ird_overlay_lookup: + DataError в imports и в except tuple.
- quarter_dump_lookup: симметричное расширение в _get_engineering_*.
- test_ird_overlay_lookup: parametrized test_graceful_when_db_fails
  покрывает 3 класса (Operational/Programming + DataError для WKT).

Closes #1095
2026-06-13 15:02:50 +05:00