BorisovAI
All projects

Cascadev0.14.0

Intelligent trend analysis platform. Automatic signal collection from 5+ sources, cascading AI impact analysis, role-based recommendations, and ready-made reports — everything you need to make decisions ahead of the competition.

Cascade
Analytics PlatformsPythonTSXGoTypeScriptCSS

Screenshots

Documentation

Cascade Trend Analysis

An intelligent system for analyzing technology trends and forecasting their cascading impact. Automatically collects signals from multiple sources, groups them into trends, evaluates them using a three-dimensional model (significance, momentum, confidence), and generates analytical reports with actionable recommendations.

Version Python


What the system does

  • Signal collection from 5 sources: Hacker News, GitHub, arXiv, Semantic Scholar, SearXNG (246+ search engines)
  • Source-aware scoring — separate formulas for each source type with web citation enrichment
  • LLM classification — grouping signals into trends with automatic category detection
  • 3D trend profiling — evaluation across three axes: Significance / Momentum / Confidence
  • Recommendations — automatic calculation: ACT_NOW, MONITOR, RISKY_HYPE, EVERGREEN, IGNORE
  • Analytical reports — cascading impact graph, impact zones, validated sources
  • Role-based recommendations — specific actions for CTO, Developer, PM, Investor per impact zone
  • Multilingual support — context-aware batch translation (EN, RU) with automatic language detection
  • External API — REST API v1 with Personal Access Token authentication for agent integrations

Key features

Scoring and recommendations

Each trend is evaluated using a three-dimensional model:

Dimension What it measures Weight
Significance Evidence strength, source diversity, signal density 40%
Momentum Signal arrival rate, score trajectory, freshness 35%
Confidence Source coverage, sample size, data consistency 25%

Based on the combination of axes, the system automatically determines a recommendation and lifecycle phase (emerging → mature → fading).

Cascade analysis

An LLM agent (LangGraph + Claude) generates:

  • Full analytical report with trend overview
  • Cascading impact graph — how the trend propagates to adjacent domains
  • Impact zones with influence scores (1-10)
  • Role-based recommendations — specific actions for 4 roles (CTO, Developer, PM, Investor)
  • Sources validated through web citations

Translation

Context-aware batch translation using LLM:

  • Titles are translated together with descriptions and categories for accuracy
  • Automatic source language detection (Cyrillic / Latin)
  • Sentence case normalization
  • 2 LLM calls per analysis instead of 30-80 (batched short texts with deduplication)

External API

REST API v1 for external agents and integrations:

  • GET /api/v1/trends — paginated list with sorting (score, momentum, significance, confidence)
  • GET /api/v1/trends/top — Top 5 by 3 criteria (new, fast_growing, highest_scored)
  • GET /api/v1/trends/{id} — trend details with signals and latest analysis
  • GET /api/v1/signals — signal list with filters
  • Authentication: Personal Access Tokens (PAT)
  • Per-token rate limiting, feature toggle via settings

Architecture

Frontend (React + TanStack Router + shadcn/ui)
    ↕ REST API
FastAPI + SQLite
    ├── Crawler → 5 source adapters → signals table
    ├── Scoring → source-aware + web citation enrichment
    ├── Classifier → LLM trend grouping + categorization → trends table
    ├── Scorer → 3D model (Significance/Momentum/Confidence) → recommendations
    ├── Analyzer → LangGraph + Claude → reports + impact zones
    ├── Recommendations → role-based actions (CTO/Dev/PM/Investor)
    ├── Translator → context-aware batch translation → translations table
    └── External API v1 → PAT auth + rate limiting
External:
    ├── SearXNG (self-hosted meta-search, Docker)
    ├── Claude API (Anthropic) — analysis + classification + translation
    └── pgvector — vector similarity (HNSW cosine; numpy fallback on SQLite)

Quick start

# 1. Clone and dependencies
git clone git@gitlab.dev.borisovai.tech:soft/trend-analisis.git
cd trend-analisis
python -m venv venv
venv\Scripts\activate          # Windows
pip install -r requirements.txt
cd frontend-cascade/app && npm install && cd ../..

# 2. Configuration
cp .env.example .env
# Required: ANTHROPIC_API_KEY, JWT_SECRET

# 3. Run (API :8000 + Frontend :5173)
python dev.py

More details: docs/guides/QUICKSTART.md

API

Group Prefix Description
Signals /signals Individual signals from sources
Trends /trends Aggregated trends with 3D scoring
Analyses /analyses Launch and retrieve analyses
Zones /zones Impact zones
Recommendations /recommendations Role-based recommendations per zone
Query /query Free-form query "How does X affect Y?"
External API /api/v1 REST API for external agents (PAT auth)
Auth /auth JWT authentication + Personal Access Tokens
Admin /admin System and crawler settings

Full documentation: docs/api/ENDPOINTS.md

Tech stack

Component Technology
Backend Python 3.12+, FastAPI, uvicorn
Database SQLite (single file)
Frontend React, TypeScript, TanStack Router, Zustand, shadcn/ui
LLM Anthropic Claude (via LangGraph)
Search SearXNG (self-hosted, Docker)
Vector Store pgvector (HNSW cosine; numpy fallback on SQLite)
CI/CD GitLab CI, PM2

Documentation

Section Description
docs/INDEX.md Entry point
docs/architecture/ Architecture, data model, algorithms
docs/scoring/ Signal and trend scoring
docs/api/ API endpoints, auth, translations
docs/frontend-cascade/ Components, routing, state management
docs/guides/ Quick start, deploy, extending
docs/CHANGELOG.md Changelog
research/ Research and methodology

License

Proprietary. All rights reserved.


Version: 0.8.0 | February 2026

Changelog

Журнал изменений (Changelog)

Правило для агента: Перед исправлением любой ошибки — проверь этот журнал. Возможно, проблема уже решалась и есть готовое решение или известные подводные камни.


Формат записи

### [ДАТА] Краткое описание
**Тип:** bug | feature | refactor | fix
**Файлы:** список измененных файлов
**Проблема:** что было не так
**Решение:** что сделали
**Подводные камни:** на что обратить внимание в будущем

v0.25.81 — 2026-05-09

[2026-05-09] feat(admin): /admin/actions/* enqueue endpoints в Go (Phase 3c4d)

Тип: feature (ADR-009 phase 3 progress) Файлы: go-api/internal/store/{admin_jobs,admin_counts}.go (new), go-api/internal/handler/{admin_actions,admin_actions_test}.go (new), go-api/cmd/server/main.go Проблема: /admin/actions/* проксились в Python — каждый клик "Run admin action" в UI делал 2 hop'а (frontend → go-api proxy → python-api). Сами handler'ы admin actions уже на queue (Phase 2c), просто триггеры остались в Python. Решение: 5 enqueue handler'ов мигрировано в Go:

  • POST /api/admin/actions/generate-descriptions — counts empty descriptions + total trends, enqueue → {status: "queued"}.
  • POST /api/admin/actions/regenerate-recommendations — counts completed analyses w/ impact_zones, skip if 0, enqueue.
  • POST /api/admin/actions/cluster-objects — counts active objects, skip if <2, enqueue.
  • POST /api/admin/actions/backfill-objects — counts empty object_name, skip if 0, enqueue (no-op since v0.17 — handler в Python оставлен для совместимости).
  • POST /api/admin/actions/regenerate-change-types — counts non-merged trends w/ change_type, skip if 0, enqueue.

Что НЕ мигрировано:

  • POST /admin/actions/regenerate-predictions — sync, fast (seconds), DELETE+SELECT+extract.
  • POST /admin/actions/cleanup-objects — sync, sklearn-heavy (auto_merge_duplicates uses cosine similarity на embedding vectors).
  • POST /admin/actions/retry-entity-normalization
  • /admin/llm-usage — cost dashboard (read-only)
  • /admin/memprof/* — debug

Все эти остаются проксированными через специфичные routes + /admin/* wildcard.

Артефакты:

  • store/admin_jobs.goEnqueueAdminAction(action, extra) — INSERT в jobs(kind=admin_action), external_ref admin:{action}:{ms}-{uuid8} mirrors Python.
  • store/admin_counts.go — 5 cheap COUNT(*) helpers; gracefully handle missing tables (return 0+nil for "no such table"/"does not exist").
  • handler/admin_actions.go — 5 endpoints, share requireAdmin через embed *AdminSettingsHandler.
  • main.go — 5 chi routes + 2 specific proxy routes (regenerate-predictions, cleanup-objects) перед HandleFunc("/admin/*", pythonProxy) wildcard.

Тесты: 6 новых: nil-verifier fail-closed для всех 5 endpoints + joinComma helper.

Подводные камни:

  1. Response shape идентичен Python ({"status": "queued"|"skipped", "message": "..."}). Frontend не требует изменений.
  2. external_ref format admin:{action}:{ms}-{uuid8} совместим с Python writer — observability через SELECT * FROM jobs WHERE kind='admin_action' работает одинаково.
  3. Сами handler'ы admin actions остаются в src/workers/admin_action_handlers.py — Python владеет Ollama/sklearn/SearXNG. Go только enqueue'ит.

v0.25.80 — 2026-05-09

[2026-05-09] feat(admin): /admin/settings + sources в Go (Phase 3c4c)

Тип: feature (ADR-009 phase 3 progress) Файлы: go-api/internal/store/{admin_settings,admin_sources}.go (new), go-api/internal/handler/{admin_settings,admin_settings_test}.go (new), go-api/cmd/server/main.go Проблема: /admin/settings (4 endpoint'а) проксились в Python — простой key-value over settings table. Обычный admin page-load делал N proxy hops. Решение:

Endpoint Что делает
GET /api/admin/settings Read 8 admin toggles (translations/collection/auto_analysis/external_api/recommendations) с дефолтами
PUT /api/admin/settings Partial update с clamping (max_per_day 1-50, depth 1-7, rate_limit 1-300)
GET /api/admin/settings/sources List 70+ sources с enabled-state + last_fetch_at status (active/warning/dead/unknown)
`PUT /api/admin/settings/sources/{name}?enabled=true false`

Артефакты:

  • store/admin_settings.go — Get/SetSettingString/Bool/Int over settings(key, value) table. Bool encoded как "1"/"0" (compatible с Python writer).
  • store/admin_sources.goSourceCatalog map (70 sources hardcoded — duplicates src/config.py::SOURCE_TYPE_MAP). ListAdminSources мерджит catalog + DB enabled state + source_last_fetch:{name} для status. IsValidSourceName guard.
  • handler/admin_settings.go — все на requireAdmin (re-Verify bearer + check IsAdmin claim → 403 если не admin). clamp reused из notifications handler.
  • main.go — 4 chi routes до r.HandleFunc("/admin/*", pythonProxy) так что специфичные сматчатся первыми.

Что осталось проксироваться (Phase 3c4d):

  • /admin/actions/* — 7 endpoints (5 enqueue queue jobs, 2 sync — regenerate-predictions + cleanup-objects). Просто port'ятся — _enqueue_admin_action уже в Go форме (admin-worker MR522).
  • /admin/llm-usage — cost dashboard (read-only)
  • /admin/memprof/{start,stop} — debug

Подводные камни:

  1. SourceCatalog дублирует SOURCE_TYPE_MAP в src/config.py. Новый адаптер требует update в обоих местах. Минорный maintenance overhead, но не блокер.
  2. Bool encoding "1"/"0" совместим с Python writer (api/settings_store.update_setting). Round-trip Go ↔️ Python работает.
  3. ?enabled=true|false — query param (не body). Mirrors Python.

Тесты: 3 новых — fail-closed gates (nil verifier → 503 для всех 3 endpoints). Все Go-тесты passed.


v0.25.79 — 2026-05-09

[2026-05-09] feat(routes): PAT CRUD + audit-log в Go (Phase 3c4a)

Тип: feature (ADR-009 phase 3 progress) Файлы: go-api/internal/store/{pat_admin,audit_log}.go (new), go-api/internal/handler/{pat_admin,audit_log,pat_admin_test}.go (new), go-api/cmd/server/main.go Проблема: После Phase 3c1a остались proxied 4 admin endpoint'а: PAT CRUD + audit-log GET. Все — single-table operations без Casdoor management API. Простой port. Решение: 4 endpoint'а мигрированы:

Endpoint Что делает
POST /api/auth/pat Mint новый PAT, return raw token (shown once)
GET /api/auth/pat List PATs пользователя (active + revoked, is_active computed against now)
DELETE /api/auth/pat/{id} Soft-revoke (sets revoked_at)
GET /api/auth/audit-log Admin-only, fetches login_audit_log с username filter + pagination

Артефакты:

  • store/pat_admin.goCreatePAT/ListPATs/RevokePAT. Token format pat_{base64url(32)} совместим с MR7 LookupPAT (sha256 hash check). Nullable expires_at через *string.
  • store/audit_log.goListAuditLog с условным WHERE username = ? + LIMIT/OFFSET. Success int materialised в bool для JSON-output (мирорим Python AuditLogEntry).
  • handler/pat_admin.go — все 3 endpoint'а через authedUserID(r) (verified JWT). Validation: name 1-100 chars, expires_days 0-3650.
  • handler/audit_log.go — re-Verify bearer для IsAdmin claim → 403 если не admin. Nil verifier → 503 (fail-closed).
  • main.go — 4 chi routes зарегистрированы, прокси-строки удалены.

Что осталось проксироваться (Phase 3c4b):

  • POST /auth/login — local password (dev-only path)
  • PUT /auth/me — Casdoor management API (update profile)
  • GET/PUT/DELETE /auth/users/* — Casdoor management API (admin user mgmt)

Тесты (4 новых): auth gate rejection для всех 4 endpoint'ов. Все Go-тесты passed. Local smoke ✓ — /auth/pat и /auth/audit-log без auth → 401.

Подводные камни:

  1. audit_log.go использует ту же login_audit_log таблицу что Python пишет (см. api/auth/adapters/sqlite_audit.py). Мы только читаем, Python остаётся owner записи. Без race conditions.
  2. IsAdmin claim из Casdoor JWT — проверяем через re-Verify (не передаём context object). Cost: один decode + RSA verify per admin call. Admin endpoints редкие, acceptable.
  3. PAT raw token shown ONCE на create — frontend admin UI должен это handle'ить.

v0.25.78 — 2026-05-09

[2026-05-09] feat(routes): /saved + /notifications в Go (Phase 3c1)

Тип: feature (ADR-009 phase 3 — drop python-api progress) Файлы: go-api/internal/store/{saved,notifications}.go (new), go-api/internal/handler/{saved,notifications,saved_test,notifications_test}.go (new), go-api/cmd/server/main.go Проблема: После Phase 3b (auth routes) python-api всё ещё owns пользовательские write endpoints /saved, /notifications/*. Каждое POST /save или mark-read шло через httputil reverse-proxy hop. Phase 3c1 убирает 8 endpoint'ов из proxy. Решение: Мигрированы 8 endpoint'ов:

  • GET /api/saved — list saved items
  • POST /api/saved — idempotent save (INSERT OR IGNORE / ON CONFLICT DO NOTHING)
  • DELETE /api/saved?item_type=…&item_id=… — delete (query params для gh:owner/repo slashes)
  • GET /api/notifications — list w/ limit/offset/unread_only filter
  • GET /api/notifications/unread-count — bell-icon count (anon → 0, не 401 — bell polls часто)
  • POST /api/notifications/{id}/read — mark single read
  • POST /api/notifications/read-all — bulk mark all read
  • DELETE /api/notifications/{id} — delete one

Подводные камни:

  1. Все endpoint'ы (кроме unread-count) требуют IsVerifiedUser=trueCASDOOR_CERTIFICATE env обязателен на проде.
  2. system-wide notifications (user_id IS NULL) surface'ятся каждому verified user'у через predicate (user_id IS NULL OR user_id = $1).
  3. Phase 3c1 НЕ закрывает /watchlist/* (сложный JOIN), /tags/* (custom interest tags), /profile/* (topic prefs). Остаются проксироваться.

Тесты: 12 новых (auth gates + helpers). Все Go-тесты passed. Local smoke ✓.


v0.25.77 — 2026-05-09

[2026-05-09] feat(auth): port user-facing OIDC routes to Go (Phase 3b)

Тип: feature (ADR-009 phase 1 — auth in Go) Файлы: go-api/internal/auth/{origin,casdoor,origin_test,casdoor_test}.go (new), go-api/internal/handler/auth.go (new), go-api/internal/store/{credits,user_profile}.go (new), go-api/cmd/server/main.go Проблема: После Phase 3a (JWT verify в Go) auth routes всё ещё проксились в python-api через httputil.NewSingleHostReverseProxy. Лишний hop, double JWT decode, дублирование redirect_uri логики между Python и Go. ADR-009 phase 1 явно требует "Auth in Go". Решение: 8 user-facing OIDC endpoint'ов мигрированы:

Endpoint Что делает
GET /api/auth/provider-info Возвращает provider type + casdoor_endpoint
GET /api/auth/login-url?origin=…&provider=… Builds Casdoor /login/oauth/authorize URL с состоянием
GET /api/auth/signup-url?origin=… Builds Casdoor signup page URL
POST /api/auth/callback Code → tokens (Casdoor /api/login/oauth/access_token); fire-and-forget user_profiles upsert
POST /api/auth/refresh Rotate refresh_token (Casdoor grant_type=refresh_token)
POST /api/auth/logout Возвращает logout_url для SPA (JWT stateless, серверный invalidate не нужен)
GET /api/auth/me Verified JWT claims → user info
GET /api/auth/credits Daily credits (verified user only)

Артефакты:

  • auth/origin.go — port _resolve_frontend_base из Python: explicit origin → Origin header → Referer → frontend_url fallback. Same-host fallback через X-Forwarded-Host для two-frontend deploy (cascade-trend.ru + trendominus.ru). HTTP downgrade rejected кроме localhost dev.
  • auth/casdoor.go — pure stdlib net/http HTTP client (отказ от casdoor-go-sdk: heavy json-iterator deps, unstable API). LoginURL/SignupURL/LogoutURL builders + ExchangeCode/RefreshToken через standard form-encoded POST. 15s timeout — fail-fast clean 401 вместо minute-long hang.
  • handler/auth.go — все 8 handler'ов через AuthDeps struct. /me требует verified JWT (Phase 3a verifier). /credits требует verified user (anon → 401).
  • store/credits.goEnsureTodayCredits с INSERT ... ON CONFLICT DO NOTHING через write pool. Mirrors api/credits_store.check_credits API (remaining/limit/used).
  • store/user_profile.goEnsureUserProfile для post-login hook. Cluster recompute остаётся в Python analytics process (sklearn-heavy).

Что НЕ мигрировано (остаётся проксироваться в python-api):

  • POST /auth/login (local password) — dev-only path
  • PUT /auth/me — Casdoor management API
  • GET/PUT/DELETE /auth/users*, POST /auth/users/{id}/{enable,disable} — admin user management
  • GET /auth/audit-log, POST /auth/pat, DELETE /auth/pat/{id}, GET /auth/pat — admin/PAT

Тесты: 21 новых:

  • origin_test.go (10) — explicit allowlist match, rejection, Origin header, Referer, fallback, same-host fwd, HTTP downgrade reject, localhost dev allowed, trailing slash strip.
  • casdoor_test.go (11) — LoginURL params, optional provider, SignupURL encoding, LogoutURL, ExchangeCode happy path + OAuth error surfacing + network error, RefreshToken grant verification, NewCasdoorClientFromEnv (nil when unset, builds when set, trailing slash strip).
  • Тесты против httptest.Server — без сетевых зависимостей.

Local smoke:

  • GET /api/auth/provider-info{"provider":"casdoor","supports_oidc":true,"casdoor_endpoint":"http://localhost:8100"}
  • GET /api/auth/login-url{state, url} с правильным client_id, redirect_uri, scope ✓
  • Все Go-тесты passed ✓

Подводные камни:

  1. Casdoor cert на проде должен быть в BACKEND_ENV (CI File var) для verify. Без него /auth/me → 503, /auth/credits → 401 (legacy unverified path не используется в этих handler'ах).
  2. Concurrency: _post_login идёт в go func() — fire-and-forget upsert после возврата tokens клиенту. Если упадёт — пользователь получит токены, user_profiles row создастся при первом write через watchdog. Acceptable.
  3. Phase 3c остаётся: ~2000 LoC миграция оставшихся writes (/saved, /watchlist, /notifications, /tags, /profile/*, /lab non-queue, /search, /topics, /onboarding, /v1/*, /query) + admin auth endpoints. После этого можно убрать python-api процесс.

v0.25.76 — 2026-05-08

[2026-05-08] feat(auth): JWT signature verification in Go (RS256 via Casdoor cert)

Тип: security + Phase 3 prep Файлы: go-api/internal/auth/jwt.go (new), go-api/internal/auth/jwt_test.go (new), go-api/internal/middleware/auth.go (rewritten), go-api/cmd/server/main.go (verifier init), go-api/go.mod (+golang-jwt/jwt/v5) Проблема: Go middleware декодировал Casdoor JWT БЕЗ verify подписи (extractSubFromJWT) — safe для read-only personalization, но реальный security gap для write endpoints (interactions в MR8a, ingest в MR7). Атакующий мог сгенерировать токен с любым sub claim'ом и писать interactions/dismiss от чужого имени. Решение:

  • go-api/internal/auth/jwt.go — RS256 signature verifier на golang-jwt/jwt/v5. Принимает PEM-encoded RSA public key OR X.509 certificate. Поддерживает inline PEM string и file path в CASDOOR_CERTIFICATE env. Fail-closed: empty cert → verifier rejects everything.
  • go-api/internal/middleware/auth.go переписан с двумя путями:
    • verifier set + valid signatureuser_id = sub, verified=true
    • verifier set + bad signature → дропаем bearer, fall back на anon cookie
    • verifier not set → legacy decode-without-verify (для миграционного периода)
  • Новый IsVerifiedUser(r) helper — mutating endpoints могут проверять что user_id пришёл из verified JWT.
  • main.go инициализирует verifier из CASDOOR_CERTIFICATE env при старте, логирует "JWT signature verification enabled" / "unset — accepted unverified (legacy)".

Тесты (12 новых): generate fresh RSA key per test, sign + verify happy path, key mismatch rejection, expired token (separate ErrExpired), alg=none rejection, garbage input, SubjectFromUnverified legacy fallback paths, LoadCertFromEnv (empty / inline PEM).

Безопасность:

  • Атакующий с forged JWT (правильный sub, неправильная подпись) больше НЕ может писать через Go endpoints — verifier дропает токен, request падает в anon cookie path. Anon cookie sandboxed (per-browser), не позволяет действовать от лица другого пользователя.
  • Existing extractSubFromJWT legacy path удалён.
  • Read-only personalization работает по-прежнему даже без cert — graceful degradation.

Подводные камни:

  1. На проде должен быть установлен CASDOOR_CERTIFICATE env (BACKEND_ENV file). Иначе go-api логирует warning и работает в legacy режиме (без verify). Безопасно для read-side, но MUST be fixed для production.
  2. Существующие endpoint'ы (/interactions, /dismiss/*) сейчас не проверяют IsVerifiedUser — не блокируют unverified user_id. Это легко добавить когда пройдёт verify в проде.
  3. Phase 3 продолжается: следующий шаг — port /auth/login-url, /callback, /refresh, /me, /logout в Go. Затем drop /auth/* из proxy.

v0.25.75 — 2026-05-08

[2026-05-08] refactor(workers): admin actions on queue (Phase 2c — Phase 2 complete)

Тип: refactor (Phase 2c — closes Phase 2) Файлы: src/workers/admin_action_handlers.py (new), src/workers/admin_worker.py (new), api/settings_routes.py, ecosystem.config.js, tests/unit/test_admin_worker.py (new) Проблема: 5 admin endpoint'ов спавнили threading.Thread для долгих операций (generate-descriptions, regenerate-recommendations, cluster-objects, backfill-objects, regenerate-change-types). Каждый со своим in-line _run closure'ом — ~150 LoC дублированной обвязки. uvicorn рестарт = потеря in-flight admin job. Видимости статуса не было. Решение:

  • src/workers/admin_action_handlers.py — registry ACTIONS: dict[str, Callable] с 5 handler'ами, извлечёнными из inline _run closures. Каждый handler синхронный, открывает свой get_conn(), возвращает dict для записи в jobs.result.
  • src/workers/admin_worker.pykind="admin_action" worker. Discriminator — payload["action"] (имя функции в registry). Эмитит progress events runningcomplete/failed. Один процесс на все admin actions, потому что они serial-by-nature (cluster + descriptions против одних таблиц = race) и низкочастотные.
  • api/settings_routes.py: 5 endpoint'ов сократились с ~80 LoC каждый до ~10 LoC — сразу _enqueue_admin_action(action_name) + return. Helper строит external_ref="admin:{action}:{ts}-{uuid8}" для observability.
  • ecosystem.config.js: admin-worker PM2 entry (1500M cap, 30s kill).
  • Synchronous endpoints (regenerate-predictions, cleanup-objects) оставлены как есть — они завершаются за секунды.
  • HTTP responses: status="started"status="queued" чтобы UI знал что это асинхронная очередь.

Тесты: 9 новых (test_admin_worker.py):

  • registry exposes 5 known actions
  • get_handler raises with helpful message on unknown
  • handler dispatch + result wrap (non-dict → {"result": str})
  • running/complete/failed progress events
  • empty payload + unknown action both raise (queue marks failed)

Все 30+ Python worker tests passed (admin + lab + analysis + dispatcher + jobs + worker_loop).

Подводные камни:

  1. admin-worker процесс должен быть запущен — иначе admin actions копятся в очереди. PM2 startOrReload поднимет автоматически.
  2. Существующие endpoint'ы возвращают status="queued" вместо "started" — frontend должен обрабатывать оба.
  3. UI пока не показывает progress admin jobs (нет SSE endpoint для kind=admin_action). Можно добавить generic /admin/jobs/{ref}/stream — отдельная задача.
  4. Phase 2 (унификация очереди) закрыта. Все долгие задачи (analysis + lab + admin) идут через единый jobs queue + per-kind workers поверх generic dispatcher.

Phase 3 (next session): Casdoor auth port в Go + удаление python-api


v0.25.74 — 2026-05-08

[2026-05-08] refactor(workers): generic queue dispatcher + lab pipeline on queue

Тип: refactor (Phase 2 из плана "system-wide cleanup") Файлы: src/workers/_dispatcher.py (new), src/workers/analysis_worker.py (rewritten on dispatcher), src/workers/lab_worker.py (new), api/lab/services/pipeline_engine.py, api/lab/routes/needs.py, ecosystem.config.js, tests/unit/test_dispatcher.py (new), tests/unit/test_analysis_worker.py (rewritten), tests/unit/test_lab_worker.py (new) Проблема: После v0.25.73 (анализы на queue) у нас был один queue-driven процесс — analysis-worker. Lab pipeline всё ещё спавнил threading.Thread из POST /lab/needs, держал прогресс в in-memory lab_state (та же проблема рассинхрона что мы убрали для анализов в MR9). Каждый новый kind воркера копировал бы 60 строк boilerplate из analysis_worker (claim/finalise/error handling). Решение:

  • src/workers/_dispatcher.py: вынес общий scaffold — run_kind_worker(kind, handler, fallback_interval). Boilerplate (bootstrap, WorkerLoop, claim_next_pending, mark_completed/failed, progress emitter, worker_id) живёт здесь. Новый kind = ~30 строк handler'а вместо 150 строк копипасты.
  • src/workers/analysis_worker.py: переписан как тонкий handler (~30 LoC). Поведение идентично — payload mapping, progress adapter, asyncio.to_thread.
  • src/workers/lab_worker.py (new): kind="lab_pipeline", handler делегирует в run_lab_pipeline через asyncio.to_thread.
  • run_lab_pipeline теперь принимает progress_fn параметр (default → legacy lab_state.append_progress для обратной совместимости с in-process тестами). Все internal _progress(need_id, ...) вызовы заменены на emit(...) closure.
  • POST /lab/needs: enqueue вместо thread.spawn. external_ref="lab:{need_id}" (префикс избегает коллизий с UUID анализов в той же колонке).
  • GET /lab/needs/{id}/progress: SSE читает из analysis_progress через replay_progress(after_id=cursor). Watch queue terminal status для случая worker crash до первого progress event'а. Fallback на in-memory lab_state сохранён для legacy in-process runs.
  • ecosystem.config.js: добавлен lab-worker PM2 entry (1200M cap, 30s kill).
  • In-process semaphore удалён — concurrency control теперь worker pool size.

Тесты: 30 passed (было 25). Новые:

  • test_dispatcher.py (7) — claim filter, payload dispatch, mark_completed/failed, cancellation safety, progress resilience to DB hiccup, kind isolation, worker_id format.
  • test_lab_worker.py (5) — payload mapping, progress adapter shape (need_id+step→dict), missing IDs, blank context, KIND/STEP_KEYS sanity.
  • test_analysis_worker.py переписан под новую структуру (5 тестов).

Подводные камни:

  1. analysis_progress table используется для всех kind'ов (analysis + lab). Имя таблицы (analysis_progress) теперь немного misleading — это generic event log. Переименование = миграция, отложено как technical-debt note.
  2. lab_state остаётся как fallback path для тестов и legacy in-process runs. После v0.25.74 ВСЕ продакшн-вызовы идут через worker → queue, lab_state остаётся пустым на проде. Удаление безопасно в follow-up MR.
  3. lab-worker процесс должен быть запущен на проде — иначе POST /lab/needs будет копить jobs без обработки.
  4. Phase 2 продолжается: admin actions (generate-descriptions, cluster-and-merge, etc) — следующий шаг.

v0.25.73 — 2026-05-08

[2026-05-08] feat(analyses): durable queue + DB-backed SSE — analysis_state.py removed

Тип: refactor (ADR-009 phase 3) Файлы: api/routes/analyses.py, src/workers/auto_analyzer.py, api/main.py, api/routes/__init__.py, api/analysis_state.py (deleted), tests/conftest.py, tests/unit/test_analysis_pipeline.py, tests/unit/test_auto_analyzer.py Проблема: POST /analyses спавнил threading.Thread внутри Python API процесса. State (_jobs, _results, _progress) жил в module-level dict в api/analysis_state.py. Crash uvicorn = потеря всех in-flight анализов. Невозможно горизонтально масштабировать. Race conditions между in-memory state и DB. Решение:

  • POST /analyses теперь enqueue'ит job в jobs queue (kind='analysis', external_ref=UUID), возвращает {job_id, status: "pending"} сразу. analysis-worker (MR5b) подхватывает.
  • GET /analyses/running читает jobs.list_running('analysis') + tail последнего progress event'а из analysis_progress.
  • GET /analyses/{id}/status использует find_by_external_ref(uuid) → queue row. Fallback на persisted analyses table для завершённых.
  • GET /analyses/{id}/stream — polling 300ms replay_progress(after_id=cursor) + watch queue status для terminal states. Cursor по analysis_progress.id гарантирует exactly-once delivery событий.
  • GET /analyses/{id}/report читает только из analyses table (in-memory cache удалён).
  • auto_analyzer.py: 3 точки spawn'а потоков (_start_auto_analysis, retry_pending_analyses, fire_pending_reanalyses) → enqueue_job. _run_incremental_update остался in-process (rare, low-risk path).
  • api/analysis_state.py удалён полностью. wait_for_active_analyses из uvicorn shutdown тоже убран.
  • api/routes/__init__.py: убраны re-exports AnalysisStateManager и _state.

Тесты: 18 unit-тестов помечены skip — они тестировали удалённый routes._state API (5 классов: TestRunAnalysis, TestStatusEndpoint, TestReportEndpoint, TestRunningAnalysesEndpoint, TestSSEStream). Новое поведение покрыто test_jobs_queue.py (13), test_analysis_worker.py (5), test_worker_loop.py (14). tests/conftest.py теперь создаёт jobs + analysis_progress таблицы. Итого 100 passed / 18 skipped, все остальные тесты MR1-9 зелёные.

Подводные камни:

  1. POST /analyses возвращает status: "pending" вместо "running" — фронт треатит и то и другое как "in progress", визуально разницы нет. Если что-то ломается в UI — это место.
  2. Когда analysis-worker процесс не запущен (локально), enqueue'енные jobs накапливаются в queue без обработки. Запускать через dev-microservices.ps1 -Only "analysis-worker".
  3. Skipped-тесты должны быть переписаны под queue-driven flow. Это технический долг, но не блокер.
  4. incremental_update_analysis остался in-process — для будущей конверсии нужен kind='analysis_incremental' worker.

v0.25.72 — 2026-05-08

[2026-05-08] feat(go): user behavioural writes — interactions + dismiss

Тип: feature (ADR-009 phase 2 — simple writes in Go) Файлы: go-api/internal/store/interactions.go (new), go-api/internal/handler/interactions.go (new), go-api/internal/handler/interactions_test.go (new), go-api/cmd/server/main.go Проблема: POST /api/interactions и POST /api/dismiss/{id} шли через httputil reverse-proxy в python-api: лишний hop, двойной auth-decode, накладные расходы. Это hottest-path writes (по событию на каждый view карточки). Решение:

  • store.SaveInteraction — INSERT в user_interactions с rate-limit (600 событий/час/user) + truncation метаданных >16K + RETURNING id.
  • store.DismissObject / UndismissObject / GetDismissedObjectIDs — обвязка вокруг user_interactions с event_type='dismiss'.
  • handler.LogInteraction — POST /interactions: валидирует event_type/entity_type, выдаёт server-managed cascade_anon_id cookie для анонимов (90 дней, HttpOnly, SameSite=Lax).
  • handler.DismissObject / UndismissObject / ListDismissed — POST/DELETE /dismiss/{object_id}, GET /dismissed.
  • main.go: 4 маршрута зарегистрированы в Go, удалены 2 строки прокси на Python.

Совместимость: cascade_anon_id cookie name + 90-day expiry идентичны Python-варианту, существующие анонимные пользователи сохраняют персонализацию через cutover.

Тесты: 4 unit (test_handler/interactions_test.go) — invalid JSON, unknown event_type, unknown entity_type, anon-id shape. Все Go-тесты зелёные.

Smoke-результаты:

  • POST /interactions без cookie → 200 + Set-Cookie: cascade_anon_id=anon_…, запись is_anonymous=1 в user_interactions.
  • POST /interactions с invalid event_type → 400.
  • POST /dismiss/123 с anon-cookie → 200, запись event_type=dismiss, entity_type=object, entity_id='123'.
  • GET /dismissed{"object_ids":[123]}.

Подводные камни:

  1. Auth (Casdoor JWT) тоже декодируется в Go (authmw.ExtractUserID). На текущий момент Go доверяет JWT без verify подписи — это safe для read-only personalization, и для interactions это тоже acceptable (rate-limit + anonymous fallback). MR8b (auth port) добавит signature verify.
  2. SaveInteraction открывает db.Write() pool (отдельный от read-only). На SQLite WAL — concurrent с analytics process работает.
  3. Не мигрированы: /saved, /watchlist, /notifications, /tags, /profile/* — остаются на python-api (низкий трафик, ниже приоритет; MR8c в следующей сессии).

v0.25.71 — 2026-05-08

[2026-05-08] fix(ingest): label sql.ErrNoRows from ON CONFLICT as "duplicate" + dev launcher

Тип: fix + tooling Файлы: go-api/internal/store/ingest.go, scripts/dev-microservices.ps1 (new) Проблема: Локальный smoke выявил, что при попытке загрузить дубль сигнала (тот же id) ответ возвращал rejection_reasons: {"smoke:1": "sql: no rows in result set"} вместо понятного "duplicate". Причина: INSERT ... ON CONFLICT DO NOTHING RETURNING 1 при конфликте даёт пустой rowset, и tx.GetContext возвращает sql.ErrNoRows — мой код помечал это generic ошибкой. Решение: Отдельная ветка errors.Is(err, sql.ErrNoRows)Reasons[id] = "duplicate". Else-ветка if inserted == 1 ... else "duplicate" стала недостижимой и удалена. Локальный dev: Добавил scripts/dev-microservices.ps1 — стартует все компоненты микросервисной топологии в отдельных PowerShell-окнах. Поддерживает -Only, -Skip, -Status, -Stop. По умолчанию: embedding-svc, python-api, go-api, collector, analytics, analysis-worker, frontend. Использование:

powershell -ExecutionPolicy Bypass -File scripts/dev-microservices.ps1 -Only "python-api,go-api,collector,analytics"
powershell -ExecutionPolicy Bypass -File scripts/dev-microservices.ps1 -Status
powershell -ExecutionPolicy Bypass -File scripts/dev-microservices.ps1 -Stop

Smoke-результаты (все зелёные):

  • POST /api/ingest/signals без auth → 401 missing bearer token.
  • С PAT + новым id → items_accepted=1. signals row создан с source_kind=telegram, collector_id=smoke-test-1, ingested_via=external_api.
  • Дубль того же id → items_accepted=0, rejected=1, reason="duplicate".
  • POST /api/ingest/heartbeat204. collector_heartbeat строка обновлена с last_batch_id.
  • ingest_log логирует все 4 batch'а (2 accepted, 2 duplicate-rejected). Подводные камни:
  1. PAT для smoke-теста создан через api.auth.pat_store.create_pat(user_id='dev-user', name='dev-collector'). Raw token виден один раз — не сохраняется. На проде PAT создаются через UI.
  2. PowerShell 5.1 (Windows) читает скрипты как Windows-1252; em-dash и стрелки в UTF-8 ломают парсер. Скрипт нормализован под чистый ASCII.

v0.25.70 — 2026-05-08

[2026-05-08] feat(go): ingest API + collector SDK

Тип: feature Файлы: go-api/internal/{handler,store,middleware}/{ingest,pat}.go (new), go-api/internal/db/write_pool.go (new), go-api/internal/model/ingest.go (new), go-api/internal/store/pat_test.go (new), go-api/cmd/server/main.go (route wired), src/collector_sdk/{__init__,client}.py (new), tests/unit/test_collector_sdk.py (new) Проблема: Внешним коллекторам (Telegram-скрейпер, OSINT-feed, internal field reporter) некуда было пушить данные. Не было ни server endpoint'а с PAT-аутентификацией, ни клиент-SDK. Решение:

Server side (Go):

  • Новый write-capable pool в db.Write() — отдельно от read-only db.Get(). Read-side остаётся в read-only режиме (default_transaction_read_only=on), write-pool открывается лениво при первом обращении.
  • store.LookupPAT(ctx, raw) — sha256(token) lookup в personal_access_tokens, проверка revoked_at/expires_at, best-effort обновление last_used_at. Хеш-схема совместима с существующим api/auth/pat_store.py (sha256, hex digest).
  • middleware.RequirePAT — Bearer token validation, контекст обогащается pat_id, pat_user_id, collector_id (из X-Collector-ID header или fallback pat.name).
  • store.IngestSignals(ctx, collectorID, batchID, items) — одна транзакция: per-item INSERT signals ... ON CONFLICT (id) DO NOTHING, pg_notify('ingest_signals', signal_id) после каждой успешной вставки, один INSERT ingest_log (audit), UPSERT collector_heartbeat (last_seen).
  • handler.IngestSignals POST /api/ingest/signals — JSON body с batch_id + items[], body cap 8MiB, max 500 items per batch, per-item rejection reasons.
  • handler.IngestHeartbeat POST /api/ingest/heartbeat — лёгкий liveness-ping, 204 No Content.
  • Маршруты подключены в main.go под /api/ingest/* через chi.Route + middleware.RequirePAT.

Client side (Python src/collector_sdk/):

  • Collector(base_url, pat, collector_id, timeout, max_retries) класс.
  • c.push(items, batch_id=None) — POST batch, возвращает IngestResponse dict.
  • c.heartbeat(status, metadata) — POST heartbeat.
  • Retry policy: 5xx + network errors → exponential backoff (1s → 2s → 4s); 4xx → не ретраим, сразу IngestError.
  • Реализован на чистом urllib без зависимостей — внешние коллекторы могут просто скопировать модуль.

Тесты:

  • go-api/internal/store/pat_test.go — sha256 совместимость с Python схемой (sha256("hello") == 2cf24...).
  • tests/unit/test_collector_sdk.py (8): real urllib path, 4xx no-retry, 5xx retry-then-succeed, 5xx exhausts, heartbeat 204, batch_id auto-gen, constructor validation.
  • go build ./... — clean.

Подводные камни:

  1. В Go-side handler используется новый отдельный write-pool. Если read-only flag в production'е применён через GUC на роли, write-pool не сможет писать. На контабо роль trends имеет write-доступ — проверено в pg_schema.sql baseline.
  2. SQLite write-pool открывается лениво и держит свой connection — на dev одновременная работа read pool + write pool + analytics process + collector приводит к потенциальной WAL contention. WAL-журнал и _busy_timeout=10000 это покрывают, но не идеально. PG production не страдает.
  3. PAT не привязан к конкретному collector_id — любой PAT может пушить под любым X-Collector-ID. Будущее усиление: добавить pat_collector_binding(pat_id, collector_id) таблицу.
  4. Идемпотентность через ON CONFLICT (id) DO NOTHING — один и тот же signal_id от двух коллекторов попадёт только от первого. Дубль помечается в rejection_reasons как "duplicate".
  5. NOTIFY доставляется только подписчикам LISTEN ingest_signals. Никаких listener'ов пока нет — это материал MR9 (когда конвертируем event_linker_loop / fact_extractor на WorkerLoop pattern).

v0.25.69 — 2026-05-08

[2026-05-08] feat(embedding-svc): gRPC service for shared fastembed model

Тип: feature Файлы: proto/embedding.proto (new), src/services/embedding_svc/{__init__,server,client,__main__}.py (new), src/services/embedding_svc/_generated/{embedding_pb2,embedding_pb2_grpc,__init__}.py (new, generated), tests/unit/test_embedding_svc.py (new), requirements.txt, ecosystem.config.js, pyproject.toml (ruff exclude) Проблема: fastembed (paraphrase-multilingual-mpnet-base-v2) грузит ~1.1GB в RAM, прогрев ~30 секунд. Каждый pipeline процесс (collector, analytics, analysis-worker, lab-worker) грузил свою копию — суммарно ~4.4GB только на одну и ту же модель, плюс stretched startup. Решение: Отдельный PM2 процесс embedding-svc хостит одну тёплую модель за gRPC. Прото embedding.v1.Embedding с RPC: Embed, EmbedBatch, MatchZone (UNIMPLEMENTED — на следующий MR), Health. Клиент src/services/embedding_svc/client.py — sync facade (embed_text, embed_batch, health) с lazy-инициализированным channel'ом, кэшируемым на модуль. Сервер сам прогревает модель в serve() ДО приёма соединений — клиенты никогда не видят неполное состояние. Транспорт: TCP localhost:50051 по умолчанию (работает Win/Mac/Linux). На Linux production можно переключить на Unix domain socket через EMBEDDING_SVC_ADDR=unix:/var/run/embedding.sock для sub-ms latency без TCP overhead. Тесты: 12 (test_embedding_svc.py) — async server + sync server, dim/L2-normalisation, INVALID_ARGUMENT на пустой text, batch round-trip, MatchZone UNIMPLEMENTED, Health metrics, sync client wrappers. Используют fake fastembed чтобы не качать веса в CI. Подводные камни:

  1. MatchZone пока возвращает UNIMPLEMENTED. Контракт зарезервирован в proto чтобы Go-сторона могла планировать против него. Реализация — следующий MR (потребует достать impact_zones_dictionary.embedding через SQL, заменить ZoneMatcher монки-патчинг).
  2. Существующий код (src/services/zone_matcher.py, api/services/event_search.py, api/routes/{tags,profile_topics}.py) ВСЁ ЕЩЁ грузит свой fastembed. Конверсия callers'ов на embedding_svc.client — отдельный коммит на этой ветке (или MR9 когда переписываем search-path в Go).
  3. proto-стабы committed в _generated/. После редактирования embedding.proto запустить: python -m grpc_tools.protoc -I proto --python_out=src/services/embedding_svc/_generated --grpc_python_out=src/services/embedding_svc/_generated proto/embedding.proto, потом руками заменить import embedding_pb2 на from . import embedding_pb2 в embedding_pb2_grpc.py.
  4. ruff extend-exclude добавлен в pyproject.toml для _generated/ — авто-сгенерированный код не подчиняется style-rules.

v0.25.68 — 2026-05-08

[2026-05-08] feat(workers): analysis-worker process (durable queue consumer)

Тип: feature Файлы: src/workers/analysis_worker.py (new), tests/unit/test_analysis_worker.py (new), ecosystem.config.js Проблема: POST /analyses спавнит analysis pipeline в потоке внутри Python API процесса. Crash uvicorn = потеря всех in-flight анализов. State лежит в in-memory analysis_state.{_jobs,_results,_progress} — рассинхрон с DB периодически проявляется как «отчёт уже готов в DB, но Python отдаёт running из памяти». Решение: Новый PM2 процесс analysis-workerpython -m src.workers.analysis_worker. Использует:

  • WorkerLoop (MR3) — LISTEN jobs_pending + fallback poll каждые 30s + reconnect.
  • src/db/queries/jobs.py::claim_next_pending (MR5a, ADR-009 phase 0b) — SELECT FOR UPDATE SKIP LOCKED + промоушн в running.
  • run_analysis в asyncio.to_thread — runner синхронный и тяжёлый, отдельный поток освобождает event loop под NOTIFY.
  • progress_callback замыкается на engine + job.id → пишет каждое событие в analysis_progress + pg_notify('job_progress', '<job.id>:<progress.id>').
  • mark_completed / mark_failed — финальное состояние queue row + financial NOTIFY.

job.external_ref хранит публичный UUID (тот что фронт получает из POST /analyses); SSE обработчик мапит UUID → queue row через find_by_external_ref (готовится в следующем коммите MR5).

Multi-instance — клонировать запись в ecosystem.config.js с разными именами; SKIP LOCKED не пропустит дубли. Mem cap 1800M (peak deep analysis ~1.4G).

Тесты: 5 (test_analysis_worker.py): pickup payload + dispatch kwargs, mark done, mark failed on runner exception, kind filter, worker_id format. Используют StaticPool + check_same_thread=False SQLite чтобы worker thread видел тот же DB что claim transaction. Подводные камни:

  1. POST /analyses пока не перенаправлен на enqueue — это следующий коммит MR5 (риск route conversion отделён от риска worker'а).
  2. На SQLite с тысячей строк в analyses _run_startup_backfills блокирует bootstrap ~30s. На PG — мгновенно. Локально терпимо.
  3. Если worker crashes между claim и mark_done, queue row застрянет в running. Watchdog (reset_stale_running_analyses) возвращает analyses строку в pending через 120 минут; queue row нужно вычищать вручную (или добавить аналогичный watchdog в analytics).

v0.25.67 — 2026-05-08

[2026-05-08] feat(jobs): migration 109 — jobs.external_ref + queue API extensions

Тип: schema + lib Файлы: db/migrations/109_jobs_external_ref.py (new), db/pg_schema.sql, src/db/tables.py, src/db/queries/jobs.py, tests/unit/test_jobs_queue.py Проблема: Существующая queue library (src/db/queries/jobs.py, ADR-009 phase 0b) использует BIGSERIAL jobs.id как PK. Аналитика и lab-pipeline уже имеют свои UUID-идентификаторы (analyses.job_id), и SSE-обработчикам надо мапить «public UUID → queue row». Без явной FK-колонки приходится сканировать payload JSONB. Решение: Migration 109 добавляет jobs.external_ref TEXT с UNIQUE-индексом (на PG — partial WHERE NOT NULL, на SQLite — полный). enqueue_job принимает external_ref параметром. Новые функции: find_by_external_ref(engine, ref) -> Job | None для SSE/route lookup, list_running(engine, kind) -> list[Job] для /running endpoint. Job dataclass и все SELECT'ы расширены полем external_ref. Тесты: +4 (всего 13): enqueue с external_ref + find, отсутствие entry, UNIQUE constraint, list_running по kind+status. Подводные камни:

  1. Это фундамент под analysis-worker (следующий коммит в MR5). Сама worker-обвязка, route conversion, auto_analyzer и удаление analysis_state.py будут отдельными коммитами в этой же ветке.
  2. Существующих writers external_ref ещё нет — все enqueue вызовы пройдут с external_ref=None. UNIQUE-index допускает множественные NULL на обоих диалектах.

v0.25.66 — 2026-05-08

[2026-05-08] feat(schema): migration 108 — ingest provenance + non-URL sources

Тип: schema Файлы: db/migrations/108_ingest_signals_columns.py (new), db/pg_schema.sql, src/db/tables.py, tests/unit/test_migration_108_ingest_schema.py (new) Проблема: Прежняя схема предполагает что у каждого сигнала есть url (FK для dedup, scoring, citation enrichment). Telegram-каналы, OSINT-feeds, internal field reports часто URL не имеют. Также нет провенанса — кто и когда залил сигнал в систему через будущий external ingest API. Решение: Migration 108 добавляет в signals:

  • source_kind TEXT NOT NULL DEFAULT 'url' — url | telegram | social | private | field | rss. Drives downstream: scoring branch, UI render, citation enrichment skip.
  • collector_id TEXT NULL — opaque ID коллектора (telegram-osint-1, internal).
  • source_ref JSONB NULL — атрибуция non-URL: channel, message_id, capture_at, visibility.
  • ingested_via TEXT NOT NULL DEFAULT 'internal' — internal | external_api.

И две новые таблицы:

  • ingest_log — append-only audit каждого batch'а через ingest API. Indexed by (collector_id, ts DESC) и ts. GC-friendly.
  • collector_heartbeat — one row per collector. Watchdog в analytics будет alert'ить на silent collectors (now() - last_seen > expected_interval).

url остался NOT NULL DEFAULT '' (не NULL) — non-URL источники пишут пустую строку, рендеры/scoring проверяют source_kind. Избегает SQLite table-rewrite для значения которое уже sentinel'ится пустой строкой. Тесты: 6 (test_migration_108_ingest_schema.py): добавление колонок, создание таблиц, идемпотентность, дефолтные значения, round-trip insert. Локальный smoke: Migration применилась на data/trending_cache.db (SQLite), все 4 колонки + 2 таблицы появились. Подводные камни:

  1. Никакого CODE пока не пишет в новые поля — это сделают MR7 (Go ingest API) и MR4-конверсии loops в pipeline. До тех пор все рядки имеют source_kind='url', ingested_via='internal', остальные NULL.
  2. На SQLite CHECK constraints не задаются (миграция гейтит is_pg). Если кто-то пишет non-conforming source_kind локально — это пройдёт. На PG CHECK сработает.
  3. pg_schema.sql обновлён зеркально — fresh PG install получит колонки сразу через baseline; миграция 108 при последующем apply_pending будет no-op (column_exists check).

v0.25.65 — 2026-05-08

[2026-05-08] feat(workers): WorkerLoop helper + pg_notify emission helper

Тип: feature Файлы: src/workers/_worker_loop.py (new), src/workers/_pg_notify.py (new), tests/unit/test_worker_loop.py (new) Проблема: Существующие loops в _main_loops.py опрашивают БД фиксированными таймерами (event_linker — 30s, summarizer — 10s, ai_insights — 90s). Каждый цикл делает SELECT-запрос даже когда работы нет; новые сигналы ждут до полного периода прежде чем их подберут. Нет horisontal scaling — нельзя запустить второй экземпляр воркера, оба будут конкурировать за одни и те же строки. Решение: Добавил две библиотеки. WorkerLoop (src/workers/_worker_loop.py) — стандартный паттерн "LISTEN + fallback poll + SKIP LOCKED": работа триггерится через pg_notify, при пропуске notify (сеть, рестарт PG) fallback-таймер всё равно дёргает poll, БД остаётся источником истины. Listener-соединение отдельное от пула, реконнектится с экспоненциальным backoff (1s → 2s → … → 60s). На SQLite mode listener task не стартует — graceful degradation в чистый polling. pg_notify (src/workers/_pg_notify.py) — emission helper: отправляет pg_notify(channel, payload) через переданный SA/psycopg connection в той же транзакции что и INSERT/UPDATE. На SQLite — no-op. Защита: payload >7900 байт truncated, подозрительные channel names отвергаются. Использование (для будущих MR):

async def process_batch() -> int:
    # SELECT FOR UPDATE SKIP LOCKED, обработать, COMMIT, вернуть кол-во
    ...

loop = WorkerLoop(name="event_linker", channel="facts_extracted",
                   process_batch=process_batch, fallback_interval=60.0)
await loop.run()

И на стороне записи:

with engine.begin() as conn:
    conn.execute(text("INSERT INTO facts ..."))
    notify(conn, "facts_extracted", signal_id)

Подводные камни:

  1. NOTIFY доставляется только подписанным в момент publish. Listener-коннект ОБЯЗАН быть долгоживущим, не из пула.
  2. NOTIFY в той же транзакции что и write — иначе можно нотифицировать о несуществующих rows (Postgres откатывает NOTIFY на abort транзакции).
  3. На SQLite оба helper'а — no-op. Это сознательно: production = PG, локальная разработка на SQLite — просто polling без NOTIFY оптимизации.
  4. Существующие loops в _main_loops.py ещё НЕ используют WorkerLoop. Конверсия — следующие MR4-5.

v0.25.64 — 2026-05-08

[2026-05-08] feat(adapters): deployment groups + COLLECTOR_GROUP env selector

Тип: feature Файлы: src/services/adapters/__init__.py, src/workers/collector.py, tests/unit/test_adapter_groups.py (new), ecosystem.config.js Проблема: После расщепления pipeline на collector + analytics (v0.25.63) collector грузит все 70+ адаптеров в одном процессе. Нет способа выделить тяжёлые адаптеры (Telegram, headless browser, paid API) в отдельный процесс на отдельной машине без дублирования всего кодa. Источники не имеют свойства "группа развёртывания". Решение: Добавил DataSourceAdapter.group property с дефолтным маппингом из source_type (academic→science, tech→tech, …). Регистр: list_enabled_in_groups(groups) для фильтра по подмножеству групп. src/workers/collector.py читает COLLECTOR_GROUP env: пусто/"all" = все группы, "news,science" = подмножество. Если группа не существует — collector логирует список доступных групп и завершается с кодом 1. Использование: COLLECTOR_GROUP=news python -m src.workers.collector запустит только новостные адаптеры. PM2 — клонировать запись collector в ecosystem.config.js с разным env.COLLECTOR_GROUP (примеры в комментариях файла). Подводные камни:

  1. Регистр заполняется через side-effect импорты в src/workers/crawler/_orchestrator.py. Запрос source_registry.list_enabled_in_groups() ДО импорта TrendingCrawler вернёт пустой список. Порядок импортов в collector.py::main критичен.
  2. Дефолтный маппинг (DATA→data) предполагает что все DATA-адаптеры — финансовые. Если появится не-финансовый DATA-адаптер, нужно явно переопределить group свойство.

v0.25.63 — 2026-05-08

[2026-05-08] refactor(workers): split monolithic pipeline into collector + analytics

Тип: refactor Файлы: src/workers/{collector.py,analytics.py,_bootstrap.py} (new), src/workers/main.py (deleted), src/workers/_main_loops.py, tests/unit/test_workers_split.py (new), tests/unit/test_workers_main.py (deleted), ecosystem.config.js, api/main.py, CLAUDE.md, README.md, src/workers/translation_watchdog/__init__.py Проблема: Один процесс pipeline (src/workers/main.py) запускал в одном event loop крауль и всю downstream-обработку (extraction, event linker, summarizer, ai_insights, watchdog). Один зависший RSS-источник тормозил весь pipeline. Невозможно масштабировать сбор отдельно от обработки. Решение: Расщепил на два независимых PM2-процесса. collector крутит только crawl_loop. analytics запускает все остальные loops + translation watchdog. Связь — через Postgres (signals table). Старый in-memory mutex watchdog._crawler._facts_extracting удалён — он был SQLite-эры guard, на PG транзакционная изоляция справляется. Общий код init вынесен в src/workers/_bootstrap.py. PM2 конфиг обновлён. Обратная совместимость с python -m src.workers.main НЕ сохранена сознательно (см. user request). Подводные камни:

  1. Если деплой обновляет код до того, как PM2 перезапустит процесс с новой конфигурацией — pm2 попытается стартовать pipeline процесс и упадёт (src/workers/main.py нет). Деплой-скрипт должен делать pm2 delete pipeline перед pm2 start ecosystem.config.js.
  2. На SQLite оба процесса вызывают Migrator().apply_pending() — race condition в теории возможен на самом первом запуске. На практике _migrations PK-table защищает (первый коммит выигрывает, второй видит уже применённое).

v0.24.41 — 2026-04-30

[2026-04-30] feat(personalization): Topic Registry + semantic search shipped

Тип: feature Файлы: db/migrations/098_topics_registry.py, api/services/{topic_registry,topic_ancestry,profile_topics_store,event_search}.py, api/routes/{search,profile_topics,onboarding}.py, go-api/internal/{model,store,handler}/, frontend-news/src/routes/{feed,search,profile}.tsx, frontend-news/src/components/feed/{event-actions,onboarding-strip}.tsx, docs/architecture/ADR/008-topic-registry-personalization-v2.md Проблема: HDBSCAN cluster_id ephemeral — после weekly rebuild номера тем меняются, привязывать пользовательские предпочтения нельзя. Кроме того, не было семантического поиска и UI персонализации в news SPA. Решение: Topic Registry — стабильный реестр тем с трёхуровневой иерархией (root/mid/leaf), closure table topic_ancestry, atomic swap через staging-таблицу, drift guard + EMA blend на эмбеддингах. Семантический поиск через pgvector hnsw (без отдельного FAISS-индекса). News SPA: /profile, /search, OnboardingStrip с mid-level chips, EventLikeButton, filter-empty state, 3px brand-coloured border на cards с matching темой. Полная i18n RU/EN. См. ADR-008. MR: !422 … !433 (9 MR'ов, фазы 0–3 по плану). Подводные камни:

  • cluster_engine.py:107 строит centroids на PCA-50d, но topics.embedding = vector(768) → bind молча падает в catch. Follow-up P0.
  • Go API всё ещё читает legacy matching_clusters JSON (personalization.go:36). Резолв matching_topics → topic_ancestry → topic_clusters в Go не реализован — лента в /api/events пока игнорирует выбор тем, фича работает end-to-end только через Python /api/search. Follow-up P0.
  • Phase 4 (cascade SPA paritет) отложен — реализован только forward-compat type field.
  • Calibration thresholds (BIND=0.70, DRIFT=0.60, LEAF_TO_MID=0.55) — TODO в topic_registry.py:51, на проде не калибровались.
  • ADR-007 (multi-axis: semantic + domain + geo) переведён в Superseded — из трёх осей реализована только semantic; domain/geo ждут backfill базовых сигналов.

v0.20.5 — 2026-04-21

[2026-04-21] feat(pipeline): translate non-EN signals before fact extraction

Тип: feature Файлы: src/workers/crawler.py, src/workers/translation_watchdog.py, api/fact_store.py, db/migrations/093_signal_translation_columns.py, go-api/internal/store/health.go Проблема: Факты из русских источников (interfax, rbc) извлекались на русском → эмбеддинги в русском семантическом пространстве → event linker не мог связать с EN-событиями (cosine 0.67 < порог 0.82). Перевод через watchdog приходил после линковки и не обновлял эмбеддинги. Решение:

  • Новый pipeline: pending → translating → translated → processing → done
  • _translate_signals_pipeline(): переводит title+content на EN через gemma4:e2b перед extraction
  • EN-сигналы проходят без LLM-вызова (title_en=title, content_en=content)
  • _extract_facts_pipeline() теперь берёт только WHERE extraction_status = 'translated'
  • Двойное кеширование: при RU→EN сохраняет оригинал как RU-перевод EN-текста
  • Watchdog: _translate_stuck_signals() подхватывает сигналы застрявшие >30 мин
  • Watchdog: _reembed_untranslated_facts() пересчитывает эмбеддинги старых RU-фактов на EN
  • insert_fact(): новый параметр embedding_lang для tracking
  • Migration 093: колонки content_lang, title_en, content_en в signals; claim_en, embedding_lang в facts Подводные камни: fetch_full_text перенесён из extraction в translate pipeline. Перевод добавляет ~1 LLM-вызов на non-EN сигнал.

v0.19.14 — 2026-04-17

[2026-04-17] fix(events): normalize event_time to UTC, fix sort order

Тип: fix Файлы: api/event_store.py, src/services/event_linker.py, src/config.py Проблема: Events сортировались неправильно по дате — event_time хранился с оригинальным timezone offset (+03:00, +09:00, +05:30), SQLite сортировал как строку, игнорируя TZ. Японские NHK-события (05:35+09:00 = 20:35 UTC) отображались выше московских (01:47+03:00 = 22:47 UTC).

Решение:

  • event_store.py: refresh_event_stats нормализует новые event_time в UTC через strftime('%Y-%m-%dT%H:%M:%S+00:00', ...)
  • Backfill 7511 событий нормализован через SQL на сервере
  • event_linker.py: добавлен commit check log (max_id, total) для диагностики
  • src/config.py: tier2 model qwen3.5:9bgemma4:e2b (убирает model swap timeout)

Подводные камни: SQLite strftime конвертирует TZ-aware ISO строки в UTC автоматически. Новые events будут записываться в UTC. Старые нормализованы backfill-ом.


v0.19.13 — 2026-04-17

[2026-04-17] fix(pipeline): serialize crawler/watchdog writes, prevent WAL bloat

Тип: fix Файлы: src/workers/main.py, src/workers/crawler.py, src/workers/translation_watchdog.py, db/connection.py Проблема: pipeline не записывал сигналы в БД 4+ часов. Root cause — deadlock между crawler и watchdog: оба писали в SQLite через разные connections, _db_retry использовал blocking time.sleep() что блокировал asyncio event loop, watchdog не мог завершить транзакцию → взаимная блокировка. WAL вырос до 171MB (нормально ~1MB), усугубляя ситуацию. Citation enrichment обрабатывал все ~1646 items перед INSERT (~40 мин), pipeline не доживал до записи.

Решение:

  • Сериализация: _crawl_active threading.Event — watchdog пропускает write-цикл если crawl_once() активен. Флаг set() перед crawl_once(), clear() в finally
  • Crawl интервал: 900с → 600с (10 мин) через _crawl_start_with_flag() wrapper в main.py
  • Citation enrichment: ограничен items[:50] (было без лимита = все items)
  • busy_timeout: 15с → 60с (connection.py + watchdog), time.sleep() убран из _db_retry/_db_commit_retry
  • WAL checkpoint loop: каждые 5 мин PASSIVE checkpoint, предотвращает рост WAL от go-api read connections

Подводные камни:

  • _crawl_active — threading.Event (не asyncio.Event) потому что проверяется из sync-контекста watchdog
  • Citation enrichment limit 50 — может пропускать новые сигналы, но они обогатятся в следующем цикле
  • _crawl_start_with_flag заменяет crawler.start() — оригинальный метод больше не вызывается из main.py

v0.19.12 — 2026-04-17

[2026-04-17] fix(pipeline): prevent database-locked from killing Phase 2-3

Тип: fix Файлы: src/workers/crawler.py Проблема: _extract_facts_pipeline падал на sqlite3.OperationalError: database is locked каждые 10 минут (22:23, 22:34, 22:44, 22:54 — все 4 последних цикла). Причина: update_signal_extraction_status(sid, "failed") внутри except-handler'а тоже бросал "database is locked" → второе необработанное исключение убивало весь pipeline → Phase 2 (event/trend linking), Phase 3 (trend formation, event dedup) и Phase 4 (prediction matching) не запускались вообще. Отсюда: нет новых событий и нет новых трендов. Решение:

  • update_signal_extraction_status(sid, "processing") (строка 771) — вынесен в собственный try/except. При DB locked — skip сигнал (остаётся pending, retry next cycle)
  • update_signal_extraction_status(sid, "failed") (строка 984-986) — обёрнут в try/except. При DB locked — логируем debug, pipeline продолжает
  • record_metric — обёрнут в try/except (cosmetic, не критичен)

Backfill (одноразово): 220 сигналов застряли в processing от предыдущих крашей:

ssh root@144.91.108.139 'sudo -u gitlab-runner sqlite3 /var/www/trend-analisis/data/trending_cache.db "UPDATE signals SET extraction_status=\"pending\" WHERE extraction_status=\"processing\""'

Подводные камни: Основная причина DB lock — конкурентные записи между pipeline (port N/A, background) и Python API (port 4014). WAL mode + busy_timeout=15s не спасают при длительных транзакциях. Нужно расследование: какой процесс держит write lock 15+ секунд.


v0.19.11 — 2026-04-16

[2026-04-16] fix(trends): wire ADR v4.1 enrichment, add diagnostics + retry

Тип: fix + feature Файлы:

  • src/services/trend_formation.py — diagnostic counters в detect_trends_from_facts и enrich_pending_trends; bounded retry для failed-трендов; materialize_detected_trends и run_trend_formation возвращают touched-set
  • src/services/trend_linker.pyauto_link_facts возвращает touched-set; новый update_trend_evidence_batch
  • src/workers/crawler.py — после auto_link_facts и run_trend_formation обновляется evidence_fact_count для затронутых трендов
  • src/workers/translation_watchdog.py — новые _generate_trend_insights (5-секционный insight) и _generate_event_analytics (4-секционный summary); materialize_detected_trends обновляет evidence
  • db/migrations/091_trend_enrichment_retry.py (new) — enrichment_retry_count + enrichment_last_attempt
  • go-api/internal/store/pulse.go, health.go — счётчик трендов учитывает enrichment_status='ready' (раньше показывал все, включая failed/pending)
  • scripts/backfill_facts.py — обновлён под новую сигнатуру auto_link_facts

Проблема: на проде только 8 трендов из 41K фактов, 6 из них застряли в enrichment_status='failed' со stub-именами "ai", "linux", "meta", "openai" — coherence LLM сказал NO один раз и наказал навсегда. evidence_fact_count всегда 0. analytics_generator.generate_trend_insight() и update_trend_evidence() — orphan-функции (0 вызовов в коде), хотя они в ADR-001 v4.1. Не было видно, на каком гейте отваливаются кандидаты.

Решение:

  • Diagnostic logs: TrendFormation summary: facts=N entities_seen=N candidates=N rej_too_few_efacts=N rej_generic=N rej_dup_cluster=N rej_no_recent=N rej_low_sources=N rej_low_facts=N + TrendFormation pipeline: rej_coherence_emb=N + TrendEnrichment summary: pending=N enriched=N failed_coherence=N failed_llm_unavailable=N name_missing=N. Теперь видно, где именно режет.
  • Retry: enrichment_status='failed' теперь не приговор — после 12h cooldown watchdog повторяет до 3 раз. Транзиентный Ollama outage больше не убивает тренд навсегда. enrichment_last_attempt штампуется до LLM-запроса, enrichment_retry_count инкрементируется только когда LLM реально вынес вердикт (не на outage).
  • Wire-up orphan: update_trend_evidence_batch вызывается после auto_link_facts и после run_trend_formationevidence_fact_count теперь живой счётчик. _generate_trend_insights и _generate_event_analytics подключены в watchdog (max 5 на цикл, fail-safe).
  • API hygiene: pulse и health счётчики трендов теперь возвращают только enrichment_status='ready' (раньше pulse.TrendCount=8 при 2 видимых = misleading).

Backfill (one-shot, после деплоя):

ssh root@144.91.108.139 'sudo -u gitlab-runner sqlite3 /var/www/trend-analisis/data/trending_cache.db "UPDATE trends SET evidence_fact_count = (SELECT COUNT(*) FROM fact_trends WHERE trend_id = trends.id) WHERE merged_into IS NULL"'

Подводные камни:

  • Migration 091 идемпотентна (ALTER TABLE с try/except в pattern существующих миграций).
  • auto_link_facts теперь возвращает tuple[int, set[int]] вместо int — обновлён scripts/backfill_facts.py (другие места не используют).
  • run_trend_formation теперь возвращает tuple[int, set[int]] — обновлён crawler.py (других callers нет).
  • materialize_detected_trends теперь возвращает tuple[int, set[int]] — обновлён вызов в translation_watchdog.py для cluster trends.
  • Pending-трендам retry_count бампится только на финальном UPDATE (ready) или на coherence=NO. На LLM-outage счётчик не растёт — это даёт неограниченные мягкие попытки, но bounded жёсткие.

v0.19.0 — 2026-04-15

[2026-04-15] feat(entity): canonicalization via term_glossary + normalization_status

Тип: feature Файлы:

  • db/migrations/087_term_glossary_multi_translation.py (new)
  • db/migrations/088_fact_entities_normalization_status.py (new)
  • api/glossary_store.py — multi-translation, reverse lookup, retry sentinel
  • src/services/entity_normalizer.py — glossary-driven cascade + non-Latin detection + status
  • api/fact_store.py, src/workers/crawler.py — persist normalization_status
  • src/workers/translation_watchdog.py — retry queue in daily maintenance
  • scripts/seed_glossary.py — ISO-3166 countries + international organizations
  • scripts/backfill_entity_canonicalization.py (new)
  • api/settings_routes.py — admin endpoints for error entities + manual retry
  • frontend-cascade/app/src/routes/admin/system.tsx — entity normalization card

Проблема: canonical_name в fact_entities часто оставался на языке оригинала (кириллица), Go API выбирал related events через entity overlap с ENG-only genericSet, и RU-entities («Россия», «РФ») проходили как specific — например, event 7642 (Мемориал) получал в «связанные» совершенно случайные RU-источники.

Решение: основной язык обработки English. term_glossary теперь хранит множество синонимов для одного термина: UNIQUE(term, lang, translation). Entity normalizer делает обратный lookup (translation → term). Колонка fact_entities.normalization_status отмечает 'error' для сущностей с не-латиницей без перевода, ошибки агрегируются в notifications по часам. Глоссарий изменяется через admin UI и триггерит async-retry через watchdog. Seed добавляет топ-60 стран + международные организации с RU-синонимами и склонениями.

Подводные камни: partial index на fact_entities(normalization_status) WHERE != 'normalized' — быстрый фильтр для admin. _NON_LATIN_RE пропускает Latin-1 Supplement + Extended (café, Zürich), но режет кириллицу/CJK/арабский.

[2026-04-15] fix(fact_extractor): strict temporal_date validation

Тип: fix Файлы: src/services/fact_extractor.py, scripts/backfill_temporal_dates.py (new) Проблема: LLM иногда возвращал hedge-строки типа "2025-10-01 or null" или даты с неправдоподобным годом — они прошли валидацию и попадали в хронологию (event 6207). Решение: _sanitize_temporal_date требует строгий формат YYYY-MM-DD + отсекает даты, год которых отличается от published_at более чем на 5 лет. Backfill-скрипт чистит существующие broken записи (два прохода: формат + year-divergence).

[2026-04-15] docs(plans): save event relations redesign for future work

Тип: docs Файлы: docs/plans/event-relations-redesign.md (new) Описание: сохранён план редизайна related events (persistent hard-links через event_related + soft через HDBSCAN cluster + property gates) — на будущую итерацию.


v0.18.1 — 2026-04-14

[2026-04-14] fix(event_linker): entity overlap gate prevents false merges

Тип: fix Файлы: src/services/event_linker.py Проблема: Event linker объединял несвязанные события (напр. Vedomosti про авиацию + Al Jazeera про военные преступления) — cosine similarity embeddings была выше порога, а существующие gates (location, numeric, title) не срабатывали при отсутствии specific location entities. Решение: Добавлен entity overlap gate — 4-й слой защиты. Собирает ВСЕ named entities (не только location) для signal и event. Если обе стороны имеют ≥2 specific entities, но нет ни exact overlap, ни token overlap (fuzzy: ≥2 общих значимых токена) → hard veto. Token overlap обрабатывает плохую нормализацию ("russian air defense forces" vs "russian defense industry" → shared "defense"). Подводные камни: Gate активируется только при ≥2 entities на каждой стороне (ENTITY_MIN_PER_SIDE). Факты без entities не блокируются — для них работают остальные gates.


v0.17.9 — 2026-04-10

[2026-04-10] feat(i18n): multilingual term glossary for domain translations

Тип: feature Файлы: api/glossary_store.py (NEW), db/migrations/083_term_glossary.py (NEW), scripts/seed_glossary.py (NEW), api/translation_service.py, api/settings_routes.py Проблема:

  • LLM переводит доменные термины буквально: "Fast Breeder reactor" → "Быстро размножающийся реактор" вместо "реактор на быстрых нейтронах"
  • "critical" (ядерный термин) → "критический" вместо "критичность"
  • Проблема воспроизводится на qwen3.5 и aya-expanse:8b

Решение:

  • Мультиязычная таблица term_glossary(term, lang, translation, domain) — любое количество языков
  • ~110 seed-терминов: nuclear, AI/ML, finance, medicine, space, cybersecurity, crypto, geopolitics, climate, semiconductors
  • Автоматический glossary hint в промпте перевода: "fast breeder reactor" → "реактор на быстрых нейтронах"
  • Английский: regex с inflection (plural, irregular: analysis→analyses, phenomenon→phenomena)
  • Русский: pymorphy3 лемматизация — матчит любую словоформу ("реактора", "реактором", "санкциями")
  • Бидирекциональный поиск: EN→RU и RU→EN через лемматизированные формы
  • Admin API: GET/POST/DELETE /admin/glossary

Подводные камни:

  • Glossary hint увеличивает prompt на ~50-200 tokens (только matching terms)
  • pymorphy3 уже в requirements.txt, 0.17ms на фразу
  • LLM сам сопрягает термины в контексте — glossary даёт словарную форму

v0.17.7 — 2026-04-10

[2026-04-10] fix(i18n): universal source language detection for translations

Тип: fix Файлы: api/translation_service.py, src/workers/translation_watchdog.py, api/routes/events.py, api/routes/trends.py, api/routes/signals.py, api/routes/helpers.py, src/workers/crawler.py Проблема:

  • Система переводов предполагала, что весь контент на английском (hardcoded source_lang="en")
  • 8+ русскоязычных RSS-источников (RBC, Kommersant, Habr, vc.ru и др.) генерируют контент на русском
  • Watchdog: _fill_event_gaps и _fill_fact_gaps передавали source_lang="en" → русский текст "переводился из английского"
  • Watchdog: events/facts/signals/trends переводились только в non_en_langs → русский контент не переводился на английский
  • API: все if lang != "en": guards пропускали перевод → русский текст показывался без перевода для EN-пользователей

Решение:

  • _detect_text_language() — определение языка по Unicode-скриптам (Cyrillic→ru, CJK/Arabic→other, Latin→en)
  • source_lang="auto" в _batch_translate_short_texts — LLM сам определяет исходный язык
  • Watchdog: группировка текстов по детектированному языку + skip при source==target
  • Watchdog: events/facts/signals/trends теперь итерируют SUPPORTED_LANGS (не только non-en)
  • API: убраны if lang != "en": guards в events, trends, signals, helpers — кеш-lookup работает для всех языков
  • Crawler: _pre_translate_items итерирует SUPPORTED_LANGS

Подводные камни:

  • get_cached_translations_batch() — instant SQL lookup, не LLM. Убрание guards безопасно для производительности
  • Recommendations/predictions/lab/zones НЕ затронуты — LLM генерирует их на английском
  • Для языков не из SUPPORTED_LANGS (fr, zh, ar) — source_lang="auto" позволяет LLM определить язык

v0.17.6 — 2026-04-10

[2026-04-10] feat(events): tier + freshness в формуле significance

Тип: feature Файлы: api/event_store.py Проблема:

  • compute_event_significance() не учитывала качество источников (tier) и свежесть события (freshness), хотя docstring заявлял freshness.
  • Событие из 6 wire agencies (tier 1) и событие из 1 tech-блога (tier 4) при одинаковых fact_count/source_count получали одинаковый score.

Решение:

  • Формула: significance = clamp(base_score × tier_mult × freshness_mult, 0, 100)
  • tier_mult [0.85..1.15]: доля tier 1-2 источников среди media_group в coverage_sources.
  • freshness_mult [0.75..1.00]: exponential decay, half-life 7 дней. Сегодня=1.0, 7d=0.875, 30d=0.76.
  • Обратная совместимость: старый формат coverage_sources (["type"]) → fallback tier=3 → tier_mult=0.85.

Подводные камни:

  • Значения significance пересчитываются при каждом вызове compute_event_significance() (после линковки фактов). Массовый пересчёт произойдёт при следующем crawl-цикле.
  • get_related_events фильтрует significance >= 40 — события с tier 3-4 через 30+ дней могут выпасть (base=91 × 0.85 × 0.76 = 59 — ОК, но base=46 × 0.85 × 0.76 = 30 — выпадет). Это ожидаемое поведение.

[2026-04-10] fix(events): verification через media_group (ADR-001-v3)

Тип: fix Файлы: src/services/event_dedup.py, db/migrations/081_source_media_groups.py Проблема:

  • update_event_verification() считал DISTINCT source_type по захардкоженному _SOURCE_TYPE_MAP (6 префиксов). Все 12+ RSS-адаптеров попадали в ELSE 'other'coverage_count=1 → всегда unverified.
  • Событие 5838 (запуск ракет КНДР, 6 независимых СМИ) показывалось как unverified.

Решение (гибридный подход из ADR-001-v3):

  • Таблица source_media_groups(adapter, media_group, tier, region) — справочник аффилированности и доверия. 36 адаптеров, seed в миграции 081.
  • Корпоративная аффилиация: адаптеры в одном media_group считаются за 1 независимый источник. Примеры: wired + arstechnica = conde_nast (1), techcrunch + engadget = yahoo_verizon (1).
  • Тиры (1-4): 1=wire/academic (tass, interfax, arxiv), 2=quality media (bbc, nhk, kommersant), 3=general (cnews, pandaily, yfinance), 4=social/aggregator (hn, gh, searxng).
  • independent_source_count = COUNT(DISTINCT media_group) → verification_level по прежним порогам (1=unverified, 2=developing, 3+=confirmed, 5+=established).
  • Неизвестные адаптеры = каждый сам себе группа (консервативно).
  • coverage_sources теперь хранит [[adapter, media_group, tier], ...] для прозрачности.

Очистка прод-БД: таблица создана, данные засеяны, все 5743 событий пересчитаны. Результат: 458 developing, 143 confirmed, 14 established.

Подводные камни:

  • _SOURCE_TYPE_MAP в trend_formation.py и trend_probability.py НЕ тронут — там он для категоризации трендов, не для event verification.
  • Tier пока не используется в scoring формуле (event_score = freshness × coverage × tier). Это следующий шаг.
  • Для новых RSS-адаптеров нужно добавлять строку в source_media_groups (миграция или admin endpoint).

[2026-04-10] fix(facts): анкер дат на published_at + запрет галлюцинаций

Тип: fix Файлы: src/services/fact_extractor.py, src/workers/crawler.py, scripts/backfill_facts.py Проблема:

  • Промпт extract_facts() не передавал в LLM дату публикации статьи. Модель массово ставила temporal_date из своих training-знаний — например, для свежей новости от 2026-04-08 со словом «先月» («в прошлом месяце») извлекала 2022-09-09. Самый частый паттерн: дата ровно на 1 год раньше публикации (knowledge cutoff модели). На проде на момент фикса: ~868 core-фактов с разрывом >90 дней между temporal_date и published_at источника.
  • В промпте не было запрета использовать даты из собственных знаний и не было правил разрешения относительных выражений («last month», «вчера», «先月»).

Решение:

  • Добавлен параметр published_at: str | None в extract_facts(). Нормализуется в YYYY-MM-DD и подставляется в промпт.
  • В промпт добавлен блок === TEMPORAL RULES (CRITICAL) ===:
    1. Запрет использовать даты из «знаний» модели — только то, что явно в тексте.
    2. Если дату нельзя извлечь из текста — null (всегда лучше null, чем галлюцинация).
    3. Правила разрешения относительных выражений на русском/английском/японском с привязкой к published_at.
    4. Различение «новость о новом событии, ссылающаяся на старое» — старая дата только если явно указана.
  • crawler.py и backfill_facts.py фетчат published_at из signals и пробрасывают в extract_facts(...).

Очистка БД (прод):

  • Обнулены temporal_date у фактов с fact_role='core' и разрывом >90 дней между temporal_date и минимальным published_at источника. Background-факты не тронуты (там много валидных исторических отсылок типа «since 2019»).
  • После обнуления events.earliest_date/latest_date пересчитаны через backfill_event_dates() (они и так берутся из signals.published_at, но на всякий случай).

Подводные камни:

  • Каскад отображения даты в get_event(): temporal_date → resolved_date → MIN(signals.published_at) → created_at. Когда temporal_date обнулён — fallback на signal date, что соответствует дате публикации новости. Это норм, потому что event.earliest_date/latest_date уже идут из signal published_at.
  • Если LLM продолжит галлюцинировать даже с новым промптом — нужно добавить пост-валидацию в _validate_facts(): при gap > 30 days от published_at форсить null или хотя бы precision='unspecified'. Пока не добавляли, чтобы не маскировать другие проблемы.

v0.17.5 — 2026-04-07

[2026-04-07] feat(events): полная хронология — merge вместо drop, двухуровневая линковка

Тип: feature Файлы: src/services/event_linker.py, src/services/event_summarizer.py (NEW), src/workers/crawler.py, src/llm/ollama_client.py Изменения:

  • _dedup_against_event(): вместо silent drop при cosine ≥ 0.90 — вызов merge_fact() (source_count++, claim_variant сохранён)
  • Двухуровневая линковка: core (≥0.82) + background с entity gate (≥0.55 + ≥1 общая entity, relevance=0.5)
  • _has_entity_overlap() — предотвращает слияние разных тем через entity check
  • _link_all_facts(relevance=) — параметризация relevance
  • event_summarizer.py — простая батч-функция генерации описаний событий из фактов (tier 3, хронологический порядок)
  • Ollama tier 3 модель: qwen3.5:4bqwen3.5:9b (избежать model swap задержек) Подводные камни:
  • Background facts (relevance=0.5) не должны доминировать в summary — промпт помечает их [background context]
  • Entity overlap проверяет canonical_name из fact_entities — если entity normalizer не отработал, overlap может быть 0

[2026-04-07] feat(events): claim_variant в API + background facts в UI

Тип: feature Файлы: api/event_store.py, api/schemas.py, frontend-cascade/app/src/types/fact.ts, frontend-cascade/app/src/components/facts/fact-card.tsx, frontend-cascade/app/src/routes/_dashboard/event.$eventId.tsx, frontend-cascade/app/src/i18n/{en,ru}.json Изменения:

  • FactSourceSignal.claim_variant — альтернативные формулировки фактов из разных источников
  • Background facts (relevance < 1.0) — приглушённый стиль + метка "Context" в timeline
  • claim_variant показывается курсивом под source link если отличается от основного claim

v0.17.4 — 2026-04-07

[2026-04-07] feat(trends): Enrichment pipeline — полная переработка формирования трендов

Тип: architecture Файлы: src/services/trend_formation.py, tests/unit/test_trend_formation.py

Entity blacklist:

  • Generic entities (Russia, AI, China, Trump, etc.) не могут быть primary ключом кластера тренда
  • Предотвращает ложные тренды типа "ai" из 3 несвязанных фактов

Coherence check (Ollama LLM):

  • При создании тренда: отправить 5 core claims → спросить "same specific topic? YES/NO"
  • Если NO → тренд отклоняется. Fail-open: если Ollama недоступна → пропустить

5-компонентный scoring (замена Bayesian):

  • source_diversity (25%): log2(types+1)/log2(5)
  • temporal_persistence (25%): unique_days/7
  • independence (20%): unique_sources/5
  • signal_type (15%): academic=1.5, tech=1.2, social=0.8, news=0.5
  • convergence (15%): 2+ types = 1.0
  • Результат → trend_probability, trend_composite, trend_significance, trend_momentum, trend_confidence

LLM naming:

  • Ollama генерирует "[Subject]: [What is happening]" из топ-5 фактов
  • Fallback: primary_entity если LLM timeout

Metric extraction:

  • Числа из metric-type фактов → trend_insight поле
  • Ollama: "Extract key numbers" → short list

Scoring для существующих трендов:

  • При update (новые факты) — пересчёт scores по той же формуле
  • Убран update_all_trend_probabilities() (старый Bayesian перезаписывал scores)

v0.17.3 — 2026-04-07

[2026-04-07] feat: 12 новых RSS-источников

Тип: feature Файлы: src/config.py, src/workers/crawler.py, src/services/adapters/{reuters,france24,dw,nhk,arstechnica,mittr,ieeespectrum,nature,sciencedaily,physorg,nikkeiasia,thehindu}.py Изменения:

  • 12 новых RSS-адаптеров по шаблону bbc.py: Reuters, France24, DW, NHK World, Ars Technica, MIT Technology Review, IEEE Spectrum, Nature News, Science Daily, Phys.org, Nikkei Asia, The Hindu
  • Все freshness-based scoring (без метрик engagement), source_type = NEWS/TECH/ACADEMIC
  • Config-классы + SOURCE_TYPE_MAP + crawler imports Подводные камни: Некоторые фиды могут быть нестабильны (NHK, Nikkei Asia — geo-блокировка возможна)

[2026-04-07] feat: персонализация событий (cluster filter/boost)

Тип: feature Файлы: api/event_store.py, api/routes/events.py, api/routes/interactions.py, api/services/interaction_store.py, db/migrations/079_event_topic_cluster_index.py Изменения:

  • list_events() поддерживает cluster_ids + p_mode (filter/boost) + dismissed_ids
  • Events endpoint получил auth (Depends get_current_user_optional) и resolve_personalization()
  • VALID_ENTITY_TYPES += "event", VALID_EVENT_TYPES += "interest"
  • rebuild_behavioral_centroid() рефакторинг: поддержка event embeddings наравне с object embeddings
  • get_dismissed_event_ids() — скрытие отклонённых событий из ленты
  • EVENT_WEIGHTS: interest=0.8, dismiss=-0.3
  • Миграция 079: индекс idx_events_topic_cluster Подводные камни: topic_cluster в events — TEXT, cluster_engine пишет integer → используется CAST(e.topic_cluster AS INTEGER) в SQL

[2026-04-07] feat: interest/skip кнопки на EventCard

Тип: feature Файлы: frontend-cascade/app/src/components/facts/event-card.tsx, frontend-cascade/app/src/stores/event-feedback-store.ts, frontend-cascade/app/src/lib/api-client.ts, frontend-cascade/app/src/i18n/{en,ru}.json Изменения:

  • EventCard: ThumbsUp (interest) + X (dismiss) — 44×44px touch targets
  • Zustand store event-feedback-store.ts с persist (localStorage)
  • InteractionEvent типы расширены: 'dismiss' | 'interest' + 'event'
  • 7 i18n-ключей для event feedback (en + ru) Подводные камни: Dismiss скрывает карточку локально (useState) + серверно (dismissed_ids). При очистке localStorage dismissed вернутся на клиенте, но серверная фильтрация останется.

[2026-04-07] fix(i18n): hardcoded строки в UI персонализации

Тип: fix Файлы: frontend-cascade/app/src/routes/_dashboard/profile.tsx, frontend-cascade/app/src/components/personalization/{tag-bar,tag-chip,suggested-tag-chip}.tsx, frontend-cascade/app/src/i18n/{en,ru}.json Изменения:

  • profile.tsx: Connected/Disconnected/Checking → t(), savedItemsCount → t() с интерполяцией
  • tag-bar.tsx: error fallback → t('tags.addError')
  • tag-chip.tsx, suggested-tag-chip.tsx: aria-labels → t() с интерполяцией
  • 11 новых i18n-ключей (profile.saved, displayName, emailLabel, statusConnected/Disconnected/Checking, savedItemsCount, tags.addError/removeAriaLabel/confirmAriaLabel/dismissAriaLabel)

v0.16.17 — 2026-04-05

[2026-04-05] feat(pipeline): Phase 1 — entity normalizer type-gate + trend creation from facts

Тип: feature Файлы: src/services/entity_normalizer.py, src/services/trend_formation.py, tests/unit/test_entity_normalizer.py, tests/unit/test_trend_formation.py Изменения:

  • entity_normalizer.py: type-gate теперь работает в L2 (fuzzy) и L3 (embedding) — матчит только в рамках одного entity_type group (org, tech, geo, person, event). Предотвращает ложные слияния "Moscow City" (geo) ↔️ "Moscow Exchange" (tech)
  • trend_formation.py: _create_trend_for_entity() — создание новых трендов для entity-кластеров, обнаруженных по конвергенции фактов. Ранее было отключено ("deferred"). Создаёт object + trend с change_type='convergence'
  • Тесты: +8 новых тестов (type-gate blocking/allowing, null category, find/create/reuse trend) Подводные камни: objects.category может быть NULL у старых объектов — type-gate пропускает их (не фильтрует)

[2026-04-05] fix(prod): Phase 0 — data cleanup

Тип: fix Изменения на prod (SQL):

  • trends.fact_count синхронизирован с fact_trends (7,987 трендов обновлены)
  • 27 zero-composite трендов удалены
  • 65 NONE change_type трендов смёржены в sibling-тренды
  • 4,507 orphan-объектов удалены (0 сигналов, 0 активных трендов)
  • 3,867 orphan-алиасов удалены
  • objects.signal_count синхронизирован (12,850 объектов)

v0.16.7 — 2026-03-30

[2026-03-30] feat(monitoring): Phase 4 — pipeline metrics + enriched health + crawler integration

Тип: feature Файлы: src/services/fact_extractor.py, src/workers/crawler.py, api/main.py, tests/unit/test_health_endpoint.py (NEW) Изменения:

  • fact_extractor.py: _record_llm_metrics() записывает llm_call_duration_ms и llm_error_rate при каждом LLM-вызове
  • crawler.py: после extraction запускается run_trend_formation() + deduplicate_events() + update_all_event_verification(). Записываются extraction_queue_size и cross_source_convergence_rate
  • api/main.py: /api/health возвращает pipeline stats (facts/events/trends counts, extraction_queue, last_crawl, db_size_mb, memory_rss_mb)
  • Тесты: 2 теста health endpoint, 2 теста LLM metrics recording Подводные камни: psutil нужен для RSS на Windows (graceful fallback). Все метрики записываются через pipeline_metrics_store.record_metric() — best-effort, ошибки не ломают pipeline

v0.16.6 — 2026-03-30

[2026-03-30] feat(frontend): Phase 3 — RecommendationCallout, VerificationBadge, answer-first layout

Тип: feature Файлы: components/trends/recommendation-callout.tsx (NEW), components/facts/verification-badge.tsx (NEW), routes/_dashboard/trend.$trendId.tsx, routes/_dashboard/events.tsx, routes/_dashboard/events.$eventId.tsx, types/trend.ts, types/fact.ts, i18n/en.json, i18n/ru.json Решение:

  • RecommendationCallout: 5 вариантов (ACT_NOW, RISKY_HYPE, MONITOR, EVERGREEN, IGNORE) с MiniBar для momentum/significance/confidence + probability display
  • Trend detail: answer-first — RecommendationCallout первым после breadcrumb (перед score panel)
  • VerificationBadge: 4 уровня (unverified/developing/confirmed/established) с tooltip
  • Events list + detail: VerificationBadge в карточках и шапке
  • Types: TrendDetail +trend_probability/probability_level, EventSummary +verification_level/coverage_count/coverage_sources
  • i18n: en + ru для callout, verification, probability, scoring dimensions

v0.16.5 — 2026-03-30

[2026-03-30] feat(facts): Phase 2 — trend formation, probability scoring, event dedup

Тип: feature Файлы: src/services/trend_formation.py (NEW), src/services/trend_probability.py (NEW), src/services/event_dedup.py (NEW), src/services/trend_linker.py, db/migrations/074_trend_probability.py, db/migrations/075_event_verification.py Проблема: Тренды формировались через objects, не через конвергенцию фактов. Events не имели верификации и дедупликации. Решение:

  • trend_formation: detect trends by fact convergence (2+ source types, 30d window, 3+ facts OR high citations)
  • trend_probability: Bayesian P(trend) scoring per fact with source LR, cross-source bonus, diminishing returns
  • event_dedup: FAISS embedding similarity (≥0.70) + 18h temporal window, merge duplicate events
  • Event verification levels: unverified → developing → confirmed → established (by source diversity)
  • trend_linker: 3-strategy cascade (entity→object→trend, signal→object→trend, entity_name→entity_cluster)
  • Migrations 074 (trend_probability, probability_level, entity_cluster, fact_count) + 075 (event embedding, verification) Подводные камни: trend_formation пока не создаёт НОВЫЕ тренды (только обогащает существующие через object matching). Создание новых трендов из фактов — Phase 5 (удаление objects).

v0.16.4 — 2026-03-30

[2026-03-30] fix(facts): Phase 1 — extraction fixes, entity normalizer 3-level, dedup calibration

Тип: fix + feature Файлы: src/services/fact_extractor.py, src/services/entity_normalizer.py, src/services/fact_deduplicator.py, db/migrations/073_facts_object_extraction.py, requirements.txt Проблема: 8% ошибок extraction (LLM возвращает list вместо dict), 0% entity resolution (только exact match), жёсткий entity gate в дедупликации отбрасывает хорошие кандидаты, некорректный числовой порог для процентов Решение:

  • Extraction: обёртка list → {"facts": list} при парсинге LLM ответа
  • Entity normalizer: 3-уровневый каскад (L0 exact → L1 alias → L2 fuzzy rapidfuzz 0.75 → L3 embedding cosine 0.80). Защита: short name gate (≤3 символов = только exact), авто-алиас при L2/L3 совпадении
  • Дедупликация: cosine 0.88→0.85, жёсткий entity cutoff → мягкий бонус (effective_sim = cosine + 0.05 × entity_overlap), абсолютный порог для процентов (0.3пп)
  • Migration 073: facts.object_id FK + backfill extraction_status='pending'
  • Зависимость: rapidfuzz>=3.0.0 Подводные камни: L3 embedding matching загружает все object embeddings в память (~5400 × 768d). Кэшируется per-process. Если objects сильно растут — нужен FAISS индекс вместо brute-force.

v0.16.3 — 2026-03-30

[2026-03-30] feat(facts): analytics generator, translations, fact GC, object recent facts

Тип: feature Файлы: src/services/analytics_generator.py, src/workers/translation_watchdog.py, api/routes/objects.py, api/schemas.py, frontend-cascade/.../objects_.$objectId.tsx, frontend-cascade/.../types/trend.ts Изменения:

  1. Analytics Generator (src/services/analytics_generator.py): AI-генерация аналитики для событий (What happened / Why matters / Key numbers / What to watch) и трендов (Direction / Evidence / Counterevidence / Recommendation). Предсказания из "What to Watch" автоматически сохраняются в predictions
  2. Translation watchdog: _fill_fact_gaps() — перевод fact claims, _fill_event_gaps() — перевод event titles/summaries
  3. Fact GC (_fact_gc()): еженедельная очистка orphaned фактов (source_count=1, confidence<0.5, age>30d, no links)
  4. Object recent facts: endpoint GET /objects/{id} теперь возвращает recent_facts[] (top 5 фактов по объекту)
  5. Frontend: секция Recent Facts на странице объекта с type badges и confidence

v0.16.2 — 2026-03-30

[2026-03-30] fix(facts): ADR-001 v4.1 schema gap fixes

Тип: fix Файлы: db/migrations/072_facts_adr_gaps.py, src/services/trend_linker.py, src/services/prediction_matcher.py, api/fact_store.py, api/routes/objects.py, api/routes/signals.py, api/routes/trends.py Проблема: Ревью ADR-001 v4.1 выявило пробелы: нет extraction_confidence в fact_sources, predictions не имеет matched_fact_id/event_id/matched_at; contribution_type расходится со схемой; отсутствуют эндпоинты /objects/{id}/facts, /signals/{id}/extracted-facts Решение:

  1. Migration 072: fact_sources.extraction_confidence, predictions.matched_fact_id/event_id/matched_at/timeframe_days
  2. contribution_type: 'supporting''evidence' + добавлен 'direct_projection' для прогнозных фактов
  3. prediction_matcher: при resolve записывает matched_fact_id + matched_at
  4. insert_fact(): принимает extraction_confidence параметр
  5. Новые эндпоинты: GET /objects/{id}/facts, GET /signals/{id}/extracted-facts Подводные камни: Старые записи fact_trends имеют contribution_type='supporting' — при необходимости можно обновить через UPDATE

v0.15.95 — 2026-03-30

[2026-03-30] feat(seo): phase 4 — public object pages, Yandex optimization, bot pre-rendering

Тип: feature Файлы: api/seo_middleware.py, api/main.py, frontend-cascade/.../routes/t.$slug.tsx, frontend-cascade/.../lib/utils.ts Изменения:

  1. Публичные страницы объектов /t/[slug]-[id] — 2370 объектов доступны поисковикам. Frontend: полная страница с трендами, сигналами, related. Backend: SEO middleware + sitemap (top 500) + robots.txt
  2. Пререндер контента для ботов — middleware генерит полный HTML с объектом, сигналами, трендами и related links для Yandex/Google
  3. Yandex оптимизацияHost: директива в robots.txt, WebSite + SearchAction JSON-LD, FAQPage schema на use-cases
  4. slugify() бэкенд — Cyrillic транслитерация для URL в sitemap Подводные камни: ID извлекается из конца slug regex (\d+)$. Sitemap лимит 500 объектов

v0.15.94 — 2026-03-30

[2026-03-30] feat(seo): phase 3 — hreflang HTML, Cache-Control, SEO-friendly share URLs

Тип: feature Файлы: api/seo_middleware.py, api/main.py, api/routes/shared.py, frontend-cascade/.../hooks/use-page-meta.ts, frontend-cascade/.../routes/shared.$token.tsx, frontend-cascade/.../routes/_dashboard/analyze.$jobId.report.tsx, frontend-cascade/.../lib/utils.ts Изменения:

  1. hreflang <link> теги в HTML — middleware инжектит en/ru/x-default для ботов, usePageMeta создаёт для клиентов (с cleanup)
  2. Cache-Control headers_SPAStaticFiles добавляет: immutable для hashed assets, max-age=86400 для images/fonts, no-cache для index.html
  3. SEO-friendly slug URL для shared reports — /shared/ai-trend-analysis--abc123 вместо /shared/abc123. Backend парсит -- разделитель (backward-compatible). Новая утилита slugify() с Cyrillic транслитерацией Подводные камни: -- разделитель в URL, token может содержать - но не --. slugify обрезает до 60 символов

v0.15.90 — 2026-03-29

[2026-03-29] refactor: удаление мёртвого кода и артефактов

Тип: refactor Удалено:

  1. api/routes/helpers.py — дубликат функции _translate_zone_names() (строки 131-182, идентичная копия 77-128)
  2. frontend-cascade/.../trends/content-plan-modal.tsx — неиспользуемый компонент (0 импортов)
  3. frontend-cascade/.../analysis/trend-to-zones-flow.tsx — неиспользуемый компонент (0 импортов)
  4. frontend-cascade/.../hooks/use-predictions.ts — неиспользуемый хук (0 вызовов)
  5. frontend-cascade/.../hooks/use-latest-analysis.ts — неиспользуемый хук (0 импортов)
  6. frontend-cascade/.../components/ui/tabs.tsx — shadcn Tabs wrapper (0 импортов из ui/tabs)
  7. api/_gen_middleware.py, api/_write_middleware.py — локальные артефакты кодогенерации (не в git)
  8. .claude/worktrees/ — 2 stale worktrees (17MB)

[2026-03-29] Анализ техдолга

Тип: doc Выявленные проблемы (средний приоритет, backlog):

  • God files: analysis_store.py (1599 строк), crawler.py (1351 строка) — разбить на модули
  • Legacy dual-write: таблицы signal_mappings + trend_aliases — 77 файлов ссылаются, планировать удаление
  • Deprecated колонки urgency_score/quality_score — nullable, удалить в v1.0
  • 71 миграция — консолидировать backfill-цепочки
  • sys.path.insert hack в crawler.py — перейти на пакетный import

v0.15.86 — 2026-03-25

[2026-03-25] feat(trend-map): карта трендов — сворачиваемый блок, описание, фильтр сигналов в URL

Тип: feature Файлы: frontend-cascade/app/src/components/trends/trend-cluster-graph.tsx, frontend-cascade/app/src/routes/_dashboard/trend.$trendId.tsx, api/routes/objects.py, api/services/tag_engine.py Изменения:

  1. Trend Map — сворачиваемый блок (collapsed по умолчанию, состояние в localStorage)
  2. Убран gap между заголовком и контентом карты (Card py-0 gap-0)
  3. Тултип показывает описание тренда (из trends.description, с переводом)
  4. showSignals передаётся через URL при переходе между трендами
  5. Масштаб и размер узлов уменьшаются при малом количестве (≤3 → 0.6x, ≤5 → 0.8x)
  6. count_related_trends_batch() — батч-подсчёт связанных трендов через FAISS
  7. Все страницы с sidebar: убрана дублирующая полоса под navbar Подводные камни: graphData должен быть объявлен до useEffect который его использует (TDZ)

v0.15.85 — 2026-03-25

[2026-03-25] feat(dedup): периодический мерж семантически похожих change_type

Тип: feature Файлы: src/services/trend_change_dedup.py (new), src/workers/translation_watchdog.py, frontend-cascade/app/src/hooks/use-tags.ts Проблема: LLM генерирует чуть разные change_type ("diplomatic tension" ≈ "diplomatic talks"). Объект 7244 имел 21 тренд, 19 из них — дубликаты. Решение:

  1. merge_similar_change_types() — embedding + cosine ≥ 0.85 + union-find + merge в canonical
  2. Запуск в watchdog каждые 5 мин
  3. Fix TS: mode: stringmode: 'boost' | 'filter' | 'off'

v0.15.84 — 2026-03-25

[2026-03-25] fix(personalization): отключить для анонимных + react-query вместо Zustand для тегов

Тип: fix + refactor Файлы: api/routes/helpers.py, frontend-cascade/app/src/hooks/use-tags.ts, frontend-cascade/app/src/components/personalization/tag-bar.tsx, frontend-cascade/app/src/routes/_dashboard/profile.tsx, frontend-cascade/app/src/stores/tag-store.ts (удалён) Проблема: 1) resolve_personalization() обрабатывал анонимных пользователей (anon_*), бесполезно искал кластеры без tag_centroid. 2) Zustand tag-store дублировал серверное состояние тегов — optimistic updates рассинхронивались с сервером при ошибках сети. Решение: 1) Добавлен guard anon_ → off в resolve_personalization(). 2) Заменён Zustand tag-store на 6 react-query хуков (useUserTags, useAddTag, useRemoveTag, useConfirmSuggestion, useDismissSuggestion, useSetFeedMode). Сервер = единственный источник правды. Подводные камни: TagBar и Profile page теперь зависят от use-tags.ts хуков. При добавлении/удалении тега инвалидируются ВСЕ персонализированные запросы.

[2026-03-25] fix(descriptions): ускорение backfill описаний трендов

Тип: fix Файлы: src/workers/crawler.py, src/workers/translation_watchdog.py Проблема: 2235 трендов без описания. Генератор запускался только в crawler с batch_size=10 (30 за цикл) — backfill занял бы ~19 часов. Решение:

  1. Увеличен batch_size до 30 (90 за цикл) в crawler
  2. Добавлен вызов generate_trend_descriptions() в translation_watchdog (каждые 5 мин)
  3. Итого: ~6 часов вместо ~19 для полного заполнения Подводные камни: Watchdog создаёт отдельное соединение для генерации описаний (не переиспользует основное)

v0.15.83 — 2026-03-25

[2026-03-25] fix(convergence): LLM кеширование + non-blocking execution

Тип: fix Файлы: src/services/convergence_analyzer.py, api/main.py Проблема: convergence_analyzer запускал LLM-синтез для всех 238 зон каждые 10 мин, блокируя uvicorn event loop → сервер переставал отвечать. Решение:

  1. Content-hash кеширование: _content_hash() хеширует contributing trends, _load_previous_results() сравнивает с предыдущими — LLM вызывается только для изменённых зон
  2. _save_convergence_results() сохраняет content_hash колонку (idempotent ALTER TABLE)
  3. run_in_executor в api/main.py — convergence loop больше не блокирует async event loop Подводные камни: При первом запуске после обновления все зоны будут без хеша → один полный LLM-проход, далее только изменения

v0.15.81 — 2026-03-25

Тип: refactor Файлы: api/routes/pulse.py (new), api/routes/trends.py, api/main.py, api/routes/__init__.py, frontend-cascade/app/src/lib/api-client.ts, frontend-cascade/app/src/components/layout/sidebar.tsx, frontend-cascade/app/src/hooks/use-pulse-meta.ts, dev.py Проблема: /{class_id} catch-all на trends_router затенял /pulse/meta → 404. Sidebar переполнен (14 пунктов → скролл). Решение:

  1. Pulse endpoints вынесены в отдельный pulse_routerGET /api/pulse, GET /api/pulse/meta
  2. Trends prefix переименован /api/trends/api/trend (единственное число)
  3. Frontend api-client обновлён под новые пути
  4. Sidebar сокращён с 14 до 9 пунктов (Explore слит в Discover, убраны Convergence/Accuracy/Watchlist/Query)
  5. Polling backoff: вместо остановки при ошибках — прогрессивное замедление (60с → 120с → 300с)
  6. dev.py: _kill_port() убивает предыдущий процесс перед очисткой __pycache__ (фиксит file lock) Подводные камни:
  • Старые клиенты с /api/trends/ перестанут работать (breaking change, только dev)
  • pulse_router регистрируется отдельно в main.py (prefix="/pulse"), НЕ через trends

v0.15.80 — 2026-03-24

[2026-03-24] fix(classify): pipeline crash on orphan trend cleanup

Тип: fix Файлы: src/services/trend_classifier.py, src/llm/cli_client.py Проблема: Classify pipeline падает с UNIQUE constraint failed: trends.object_id, trends.change_type при каждом цикле. Причина: UPDATE trends SET merged_into = NULL un-merge'ит тренды — они попадают в partial UNIQUE index (object_id, change_type) WHERE merged_into IS NULL и конфликтуют с существующими трендами. Дополнительно: ~50% LLM batch экстракций fail из-за Extra data (LLM возвращает невалидный JSON, fallback парсер не обрабатывал массивы). Решение:

  1. Тренды, ссылающиеся на orphan через merged_into, теперь удаляются (вместо un-merge)
  2. query_json fallback теперь ищет JSON массивы [...] перед объектами {...} Подводные камни: Partial UNIQUE index на trends — любой UPDATE merged_into→NULL может нарушить constraint

v0.15.79 — 2026-03-24

[2026-03-24] feat(pulse): live feed panel + auto-refresh + heatmap improvements

Тип: feature Файлы: api/routes/trends.py, api/schemas.py, frontend-cascade/app/src/hooks/use-pulse.ts, frontend-cascade/app/src/hooks/use-pulse-meta.ts, frontend-cascade/app/src/routes/_dashboard/pulse.tsx, src/services/convergence_analyzer.py Решение:

  1. Two-tier polling: GET /api/trends/pulse/meta (1ms, каждые 60с) → invalidate при изменении last_updated
  2. Live Feed Panel справа от heatmap (lg: side-by-side, mobile: 1 колонка) с ResizeObserver для выравнивания высоты
  3. FeedRow: 3 строки (score+name+time, description, badges NEW/UPD + phase + momentum)
  4. Server-side фильтрация live feed при клике на heatmap (зоны и категории)
  5. DigestCard теперь показывается для всех периодов включая 24ч
  6. Heatmap: все 5 категорий видны даже при 0 значениях (backend заполняет пустые дни/категории)
  7. fix(convergence): send_messageclient.query() (async через thread pool) Подводные камни: asyncio.run() нельзя вызывать из async контекста — используется ThreadPoolExecutor

v0.15.71 — 2026-03-24

[2026-03-24] fix(objects): merge duplicate objects + prevent future dupes

Тип: fix Файлы: db/migrations/066_merge_duplicate_objects.py, src/services/object_extractor.py Проблема: Параллельные потоки краулера создают дубли объектов — L0 exact match не видит uncommitted rows из других потоков. 30 групп exact дублей (same object_name) + 21 semantic near-duplicate (cosine >= 0.90). Решение:

  1. Migration 066: merge 37 exact + 21 semantic дублей (dual-threshold: name cosine >= 0.90 AND enriched text cosine >= 0.85)
  2. UNIQUE partial index на objects.object_name (WHERE merged_into IS NULL) — предотвращает будущие exact дубли
  3. _upsert_object() fallback: если INSERT blocked UNIQUE index → ищет по object_name Подводные камни:
  • Semantic merge использует union-find (transitive) — при слишком низком threshold может сливать несвязанные объекты
  • Dual threshold (name + enriched text) фильтрует false positives ("age verification systems" ≠ "age verification regulation")
  • UNIQUE index блокирует concurrent INSERT с тем же object_name — INSERT OR IGNORE + fallback SELECT

v0.15.59 — 2026-03-23

[2026-03-23] feat(personalization): tag suggestion + confirmation flow

Тип: feature Файлы: api/services/interaction_store.py, api/routes/tags.py, db/migrations/055_dismissed_tag_suggestions.py (NEW), frontend-cascade/app/src/stores/tag-store.ts, frontend-cascade/app/src/components/personalization/tag-bar.tsx, frontend-cascade/app/src/components/personalization/suggested-tag-chip.tsx (NEW), frontend-cascade/app/src/lib/api-client.ts Проблема: Теги персонализации добавлялись только вручную. Пользователь не видел, какие предпочтения система определила по его поведению. suggest_tags_for_user() использовал только сильные сигналы (save, watchlist) — views не учитывались. Решение:

  • suggest_tags_for_user() теперь учитывает ВСЕ взаимодействия (view, source_click, search) — слабые сигналы накапливаются
  • Минимальный порог MIN_SUGGEST_SCORE = 1.0 — предложения появляются только при накоплении достаточного веса
  • Object name имеет приоритет (×0.7) над change_types (×0.5) и categories (×0.2)
  • dismissed_tag_suggestions таблица — отклонённые предложения не показываются повторно
  • POST /tags/suggest/dismiss — endpoint для отклонения
  • Frontend TagBar показывает предложения (dashed border) с кнопками confirm (✓) / dismiss (✗)
  • Confirm = addTag(text, source="suggested") → вес 0.7 в кластерном matching
  • Suggestions не персистятся в localStorage (загружаются с сервера при каждом визите) Подводные камни:
  • MIN_SUGGEST_SCORE = 1.0 — при весе view=0.3 нужно ~5 просмотров объекта чтобы его name набрал 1.0 (0.3×0.7×5=1.05)
  • Dismissed suggestions привязаны к lowercase tag_text — если объект переименован, старый dismiss не блокирует новое имя

v0.15.58 — 2026-03-23

[2026-03-23] feat(personalization): lazy epoch-based cluster recomputation

Тип: feature Файлы: db/migrations/054_cluster_epoch.py (NEW), api/services/cluster_engine.py, api/routes/helpers.py, src/workers/translation_watchdog.py Проблема: Cluster IDs нестабильны — каждый rebuild_clusters() назначает новые. Между rebuild и recompute_users matching_clusters в профилях указывали на чужие кластеры. Eagerly recompute для всех юзеров — лишние вычисления для неактивных. Решение:

  • cluster_epoch в cluster_meta — глобальный счётчик, инкрементируется при rebuild_clusters()
  • cluster_epoch в user_profiles — фиксирует epoch последнего пересчёта
  • get_user_cluster_ids() сравнивает epoch: если stale → compute_user_clusters() inline (<10ms)
  • Watchdog больше не итерирует по всем юзерам — только rebuild_clusters() + rebuild_relevant_roles()
  • Неактивные юзеры не пересчитываются, активные — при первом запросе после rebuild Подводные камни:
  • При первом запросе после rebuild latency +~10ms (PCA transform + cosine sim)
  • Если cluster_meta таблица пуста, epoch=0 — все юзеры пересчитаются при первом запросе

v0.15.57 — 2026-03-23

[2026-03-23] fix(personalization): use pure tag centroid for cluster matching

Тип: fix Файлы: api/services/cluster_engine.py Проблема: compute_user_clusters() использовал blended centroid из user_profiles.tag_centroid (60% tags + 40% behavioral). Behavioral centroid строился из истории взаимодействий (11 объектов военной тематики), что перетягивало кластер-матчинг на нерелевантные кластеры. Пользователь с тегом "Игровые автоматы" получал военный контент вместо gaming. Решение:

  • compute_user_clusters() теперь вычисляет чистый tag centroid из user_tags (active tags only) через _compute_pure_tag_centroid()
  • Используется тот же алгоритм что и _update_tag_centroid() в interaction_store: source weights × temporal decay
  • Fallback на blended centroid из profile только если нет активных тегов
  • Результат: admin с тегом "Игровые автоматы" → кластеры [video games, AI gaming] вместо [military, Iran] Подводные камни:
  • Behavioral centroid всё ещё хранится в user_profiles.behavioral_centroid и используется в _update_tag_centroid() blend — это влияет на objects.py FAISS search, но НЕ на кластерное матчинг
  • Cluster IDs нестабильны — каждый rebuild_clusters() присваивает новые ID. Watchdog автоматически пересчитывает matching_clusters после rebuild

v0.15.56 — 2026-03-22

[2026-03-22] fix(objects): migrate objects endpoint to cluster-based personalization

Тип: fix Файлы: api/routes/objects.py Проблема: objects.py использовал FAISS re-ranking (Python-side), в то время как все остальные endpoints (trends, signals, predictions, recommendations) уже мигрировали на SQL-level кластерную фильтрацию. FAISS вычислял total на основе покрытия индекса, а не SQL WHERE → пагинация показывала больше записей, чем фактически фильтровалось. category_counts вычислялись без учёта кластерного фильтра. Решение:

  • Заменён FAISS re-ranking на SQL-level cluster-based filter/boost (единый подход со всеми endpoints)
  • Filter mode: WHERE cluster_id IN (...) AND (relevant_roles IS NULL OR relevant_roles LIKE ?) — total считается корректно
  • Boost mode: ORDER BY CASE WHEN cluster_id IN (...) THEN 0 ELSE 1 END, ...
  • category_counts теперь включает кластерный фильтр в filter mode
  • Удалён numpy import и ~65 строк FAISS re-ranking кода Подводные камни:
  • tag_engine.get_personalized_object_ids() остаётся доступной для других использований, но больше не вызывается из objects route

v0.15.55 — 2026-03-22

[2026-03-22] feat(personalization): SQL-level cluster-based personalization (HDBSCAN)

Тип: feature Файлы: api/services/cluster_engine.py (NEW), db/migrations/053_object_clusters.py (NEW), api/routes/helpers.py, api/routes/trends.py, api/routes/signals.py, api/routes/predictions.py, api/routes/recommendations.py, src/services/trend_classifier.py, src/workers/crawler.py, api/prediction_store.py, api/recommendation_store.py, src/workers/translation_watchdog.py Проблема: Персонализация (FAISS filter/boost) применялась ПОСЛЕ SQL-пагинации → неправильный total, сломанная пагинация, хаки с limit=5000. Не масштабируется для десятков тысяч объектов. Решение: HDBSCAN кластеризация объектов (PCA 768→50d + density-based). 2D фильтрация: area (cluster_id) × role (relevant_roles). SQL WHERE для filter mode, ORDER BY CASE для boost mode. Все 5 endpoint stores принимают cluster_ids + preferred_role + personalization_mode. Старый FAISS re-ranking удалён из всех routes. Watchdog пересчитывает кластеры + user matching. Подводные камни:

  • relevant_roles заполняется из zone_recommendations — если анализы не генерируют рекомендации с ролями, поле пусто (fallback: relevant_roles IS NULL не исключает объект)
  • objects.py по-прежнему использует FAISS через get_personalized_object_ids() — другой паттерн
  • numpy int → Python int при записи в SQLite (int(cid))
  • Тесты: mock-функции crawler должны принимать **kwargs для новых params

v0.15.53 — 2026-03-22

[2026-03-22] fix(personalization): FAISS index empty — dimension mismatch 384 vs 768

Тип: bug Файлы: db/migrations/051_reembed_objects_768.py Проблема: Модель эмбеддингов сменилась с BAAI/bge-small-en-v1.5 (384-dim) на paraphrase-multilingual-mpnet-base-v2 (768-dim), но 1689 объектов не были пере-эмбеддены. FAISS индекс в tag_engine.py фильтровал все объекты по vec.shape[0] == _EMBED_DIM (768) → индекс пуст → rank_objects_by_tags() возвращал {} → персонализация не работала. Решение: Миграция 051 — пере-эмбеддинг всех объектов с текущей 768-dim моделью (8.8 сек). Также пересчитан behavioral centroid. Подводные камни: При смене embedding модели нужно пере-эмбеддить ВСЕ таблицы с эмбеддингами. ZoneMatcher делает это автоматически (_build_index перегенерирует stale зоны), но objects и user_tags — нет.


v0.15.52 — 2026-03-21

[2026-03-21] feat(personalization): dismiss, transparency labels, behavioral centroid

Тип: feature Файлы: api/routes/interactions.py, api/routes/objects.py, api/services/interaction_store.py, api/services/tag_engine.py, api/schemas.py, src/workers/translation_watchdog.py, db/migrations/050_behavioral_centroid.py, frontend-cascade/app/src/components/trends/object-card.tsx, objects.tsx, i18n Что сделано:

  • 2.4 "Not Interested" / Dismiss (YouTube/TikTok): dismiss_object() / undismiss_object() / get_dismissed_object_ids(). Endpoints POST/DELETE /dismiss/{id}, GET /dismissed. Фильтрация dismissed из objects list. Кнопка X на ObjectCard
  • 2.5 "Why This?" labels (Habr/YouTube): get_match_reasons() в tag_engine — находит ближайший user tag для каждого объекта. match_reasons dict в ObjectListResponse. Italic label "Matches: " под заголовком карточки
  • 2.6 Behavioral Centroid (YouTube/TikTok): rebuild_behavioral_centroid() — weighted mean embeddings объектов из interactions (EVENT_WEIGHTS × recency decay 14d half-life). Blend: final = 0.6×tag_centroid + 0.4×behavioral. Migration 050. Вызов из watchdog каждые 5 мин
  • Документация: docs/features/PERSONALIZATION.md — полное описание архитектуры персонализации. Обновлены INDEX.md, ALGORITHMS.md

v0.15.51 — 2026-03-21

[2026-03-21] feat(personalization): поведенческие рекомендации по образцу топовых платформ

Тип: feature Файлы: api/routes/objects.py, api/routes/signals.py, api/routes/trends.py, api/routes/tags.py, api/routes/recommendations.py, api/services/tag_engine.py, api/services/interaction_store.py, src/services/trend_classifier.py, src/workers/translation_watchdog.py, фронтенд (6 файлов), i18n (en/ru) Что сделано:

  • 1.1 Momentum в ранжировании (Reddit): base_scores = trend_composite вместо obj_significance — свежие тренды с высоким momentum получают приоритет
  • 1.2 Temporal decay (TikTok): exp(-ln2 × age_days / 30) в _update_tag_centroid() — старые теги затухают с half-life 30 дней
  • 1.3 Расширенный tracking (TikTok/Google): trackEvent() на страницах signals, trends, objects, recommendations (было только trend detail + object detail)
  • 2.1 Auto-tag из взаимодействий (Habr): auto_generate_tags() — change_type с ≥3 strong interactions автоматически становится тегом. Вызывается из watchdog каждые 5 мин
  • 2.2 Hot sort (Reddit): hot_score = momentum × log10(signal_count+1) × exp(-age_hours/168). Новый sort_by=hot на objects и trends endpoints
  • 2.3 Related Technologies (Google Discover/YouTube): find_related_objects() через FAISS kNN. Endpoint GET /objects/{id}/related. Секция "Related Technologies" на странице тренда

v0.15.50 — 2026-03-21

[2026-03-21] fix(auto-analyzer): trends.id передавался как object_id

Тип: bug Файлы: src/workers/auto_analyzer.py, db/migrations/052_fix_auto_analysis_object_id.py Проблема: check_and_queue_auto_analyses() выбирал trends.id и передавал его как object_id в _start_auto_analysis(). Поскольку objects.id ≠ trends.id (после миграции 018), анализы загружали сигналы из неправильных объектов. Результат: ~39 анализов с неправильным контекстом, 8 из них с полностью пустыми отчётами (0 impact zones). Решение:

  • SQL-запрос теперь выбирает t.object_id вместо t.id
  • Миграция 052: исправляет analyses.object_id, analyses.trend_id, trends.latest_analysis_id Подводные камни: objects.id ≠ trends.id — НИКОГДА не использовать trends.id как object_id. Всегда trends.object_id.

[2026-03-21] feat(predictions): фальсифицируемые прогнозы вместо наблюдений

Тип: feature Файлы: src/prompts/schemas.py, src/prompts/templates.py, api/services/analysis_runner.py, api/prediction_store.py, api/settings_routes.py Проблема: Predictions содержали наблюдения/мнения ("asyncio важен для скрапинга"), которые невозможно проверить как TRUE/FALSE. Причина: LLM-промпт не просил генерировать falsifiable claims, extraction брал mechanism (= rationale) как claim. Решение:

  • Добавлены поля prediction_claim и verification_metric в ZoneInfluence pydantic-схему
  • В оба P1-шаблона добавлен блок "ФАЛЬСИФИЦИРУЕМОЕ ПРЕДСКАЗАНИЕ" с требованиями: конкретный субъект + измеримый результат + горизонт + источник проверки
  • Extraction теперь берёт prediction_claim как claim, зоны без prediction_claim пропускаются (вместо генерации generic fallback)
  • verification_metric прокидывается в create_prediction() (колонка в DB уже существовала)
  • Dedup в extraction теперь проверяет только status = 'open' (не блокирует resolved)
  • Новый admin action POST /api/admin/actions/regenerate-predictions — удаляет open predictions, перевыгружает из analyses Подводные камни: Старые analyses (без prediction_claim в impact_zones JSON) дадут 0 predictions при regenerate — это корректно. Нужно заново запускать анализы для генерации falsifiable claims.

v0.15.52 — 2026-03-21

[2026-03-21] feat(personalization): FAISS boost для signals/trends + tag suggestions + preferred_role

Тип: feature Файлы: api/routes/signals.py, api/routes/trends.py, api/routes/tags.py, api/routes/recommendations.py, api/services/interaction_store.py, frontend-cascade/app/src/lib/api-client.ts, frontend-cascade/app/src/hooks/use-tags.ts

Изменения:

  • /signals и /trends endpoints: FAISS boost через object_id сигнала/тренда → relevance из tag centroid
  • Персонализация активируется только при дефолтном sort (score для signals, default/composite для trends)
  • GET /tags/suggest — behavioral tag suggestions на основе saved/watchlist/analysis interactions (EVENT_WEIGHTS)
  • /recommendations/aggregated — auto-inject preferred_role из user_profiles если role фильтр не задан
  • useSuggestedTags() hook + getSuggestedTags() API метод + SuggestedTag тип
  • useInvalidatePersonalized() теперь инвалидирует и signals

v0.15.51 — 2026-03-21

[2026-03-21] fix(personalization): Expert panel — 26 issues fixed (P0/P1/P2)

Тип: fix Файлы: api/services/tag_engine.py, api/services/interaction_store.py, api/routes/tags.py, api/routes/profile.py, api/routes/interactions.py, api/routes/objects.py, frontend-cascade/app/src/stores/tag-store.ts, frontend-cascade/app/src/hooks/use-tags.ts, frontend-cascade/app/src/lib/interaction-tracker.ts, frontend-cascade/app/src/components/personalization/tag-bar.tsx, frontend-cascade/app/src/components/personalization/tag-chip.tsx, frontend-cascade/app/src/components/personalization/tag-input.tsx, frontend-cascade/app/src/routes/_dashboard/profile.tsx, frontend-cascade/app/src/lib/api-client.ts

P0 (Critical):

  • FAISS re-ranking теперь ПЕРЕД пагинацией: get_personalized_object_ids() ранжирует все объекты глобально, затем пагинация
  • Race condition: _rebuild_lock (non-blocking) предотвращает конкурентный rebuild; _ensure_index() с double-check locking
  • Sync SQLite из async: все endpoint'ы конвертированы в def (не async def), FastAPI запускает их в thread pool
  • IDOR через anon_id: сервер генерирует anonymous ID через httpOnly cookie (cascade_anon_id), client-side anon_id убран
  • Dual source of truth: убран useUserTags() react-query hook, Zustand store — единственный источник

P1 (High):

  • Лимит тегов: MAX_TAGS_PER_USER=50 (backend), MAX_TAGS=30 (frontend), ошибка с toast
  • Rate limiting: MAX_INTERACTIONS_PER_USER_PER_HOUR=200, SQL COUNT проверка
  • Metadata size: MAX_METADATA_SIZE=4096, truncation к {}; Pydantic validator на 20 ключей
  • EVENT_WEIGHTS → centroid: SOURCE_WEIGHTS по источнику тега (manual=1.0, auto=0.4), weighted average
  • Centroid dilution: MAX_TAGS_FOR_CENTROID=10, ограничение при вычислении
  • Threshold: DEFAULT_FILTER_THRESHOLD снижен с 0.35 до 0.25
  • Scale normalization: min-max нормализация relevance scores к [0,1] для балансировки с composite
  • Optimistic delete с rollback: при ошибке сервера теги восстанавливаются + toast
  • Debounce: useDeferredValue для поиска popular tags + placeholderData в react-query
  • TagBar scope: показывается только на explore/radar/objects/signals/trends страницах
  • Batching: interaction-tracker с очередью (flush каждые 5с или 20 событий) + flush при visibilitychange
  • Error feedback: addTag показывает toast при ошибке вместо silent fail

P2 (Medium):

  • LIKE escape: % и _ экранируются в search_popular_tags()
  • Transaction: delete_user_data() обёрнут в BEGIN/COMMIT/ROLLBACK
  • GDPR log: логируется hash(user_id), не сам user_id
  • Popular tags limit cap: min(limit, 200) на get, min(limit, 100) на search
  • N+1 fix: rebuild_popular_tags() change_type counts в одном GROUP BY запросе
  • A11y: role="radiogroup" + role="radio" + aria-checked на mode toggle (TagBar + Profile)
  • A11y: role="listbox" + role="option" на TagInput dropdown, aria-label на input
  • Touch target: TagChip remove button min-h-[44px] min-w-[44px], mode toggle min-h-[32px]
  • Logout reset: useAuthStore.subscribe() вызывает reset() при logout
  • Variable shadowing: (t) => заменён на (tag) => в profile.tsx
  • Tag route order: /tags/popular перед /tags/{tag_id}
  • Source validation: VALID_TAG_SOURCES whitelist, entity_type validation
  • onOpenAutoFocus вместо setTimeout для Popover focus

Подводные камни:

  • get_personalized_object_ids() делает доп. запрос SELECT id, obj_significance FROM objects для base_scores — при >10K объектов может быть медленно
  • Exploration slots работают глобально: 80% matched + 20% exploration interleaved по страницам
  • Anonymous cookie cascade_anon_id — httpOnly, 90 дней TTL

v0.15.50 — 2026-03-20

[2026-03-20] feat(personalization): Tag-based FAISS personalization + behavioral tracking

Тип: feature Файлы:

  • db/migrations/048_user_personalization.py (NEW) — 4 таблицы: user_tags, user_profiles, user_interactions, popular_tags
  • api/services/tag_engine.py (NEW) — FAISS ranking engine: compute_tag_centroid, rank_objects_by_tags, apply_personalized_sort
  • api/services/interaction_store.py (NEW) — CRUD для тегов, профилей, взаимодействий, popular_tags rebuild
  • api/routes/tags.py (NEW) — GET/POST/DELETE /api/tags, GET /api/tags/popular
  • api/routes/profile.py (NEW) — GET/PUT /api/profile/preferences, DELETE /api/profile (GDPR)
  • api/routes/interactions.py (NEW) — POST /api/interactions (fire-and-forget)
  • api/main.py — подключение 3 новых роутеров
  • api/routes/objects.py — FAISS re-ranking при наличии тегов пользователя
  • src/workers/translation_watchdog.py — ежедневный rebuild popular_tags + 90-day TTL cleanup
  • frontend-cascade/app/src/components/personalization/ (NEW) — TagBar, TagInput, TagChip
  • frontend-cascade/app/src/stores/tag-store.ts (NEW) — Zustand + persist
  • frontend-cascade/app/src/hooks/use-tags.ts (NEW) — react-query hooks
  • frontend-cascade/app/src/lib/interaction-tracker.ts (NEW) — fire-and-forget tracking
  • frontend-cascade/app/src/lib/api-client.ts — новые API методы + типы (UserTag, PopularTag, UserProfile, InteractionEvent)
  • frontend-cascade/app/src/routes/_dashboard.tsx — TagBar в layout
  • frontend-cascade/app/src/routes/_dashboard/profile.tsx — секция "Мои теги" + GDPR delete
  • frontend-cascade/app/src/routes/_dashboard/trend.$trendId.tsx — dwell tracking + save/watchlist tracking
  • frontend-cascade/app/src/routes/_dashboard/objects_.$objectId.tsx — dwell tracking
  • frontend-cascade/app/src/i18n/en.json, ru.json — ключи tags.*

Описание: Персонализация на основе свободных семантических тегов + FAISS cosine similarity.

  • Пользователь добавляет теги ("AI", "quantum computing") → embed_texts() → 768-dim вектор
  • Tag centroid (mean) → cosine similarity с objects.embedding → ранжирование
  • Два режима: Boost (все объекты, matching бустятся, λ=0.30) и Filter (только cosine ≥ 0.35)
  • 20% exploration slots (prevent filter bubble)
  • Behavioral tracking: view (dwell>3s), save, unsave, watchlist, analysis, share, search, source_click
  • Popular tags: извлекаются из signals.tags_json (GitHub topics, arXiv codes, SO tags)
  • 90-day TTL на interactions + GDPR DELETE /api/profile
  • Ranking formula: (1-λ)*composite/100 + λ*relevance

Подводные камни:

  • FAISS index rebuilds every 5 min (in-memory, ~1ms for 1833 objects)
  • Personalization only active for authenticated users with ≥1 active tag
  • apply_personalized_sort() only applies when sort_by="significance" (default)
  • Popular tags need initial crawl data in signals.tags_json

9b7a228 (feat(predictions): falsifiable predictions instead of observations)


v0.15.49 — 2026-03-21

Тип: feature Файлы: src/services/adapters/google_trends.py (NEW), src/config.py, src/workers/crawler.py, api/services/signal_mapper.py Описание: Адаптер для Google Trends Daily Search Trends по RSS. Без API-ключа.

  • Fetches trending searches из 2 гео (US, RU) — дедупликация по title hash
  • Scoring: 60% traffic (sigmoid до 5000) + 40% freshness (48h decay)
  • ID формат: gtrends:{md5(title)[:12]}
  • Метаданные: approx_traffic, geo, news_urls, news_sources
  • Каждый item содержит linked news URLs из Google для source enrichment

v0.15.47 — 2026-03-21

[2026-03-21] fix(db): Database concurrency + scoring fixes

Тип: fix Файлы: src/workers/crawler.py, src/workers/translation_watchdog.py, src/services/alias_resolver.py, src/services/trend_classifier.py, src/services/trend_scoring.py, api/analysis_store.py, src/services/title_generator.py Описание: Серия исправлений для стабильности SQLite при высоком параллелизме:

  • DB retry helpers: _db_retry() и _db_commit_retry() с 5 попытками и экспоненциальным backoff (5-25с) для INSERT/commit в crawler
  • Batch commits: INSERT сигналов коммитится каждые 50 записей (вместо одной транзакции на весь crawl)
  • WAL + busy_timeout: Все 12+ точек sqlite3.connect() переведены на PRAGMA journal_mode=WAL + busy_timeout=15000
  • _init_db split: Разделён на _init_db_schema() (DDL, 5 retries) + _init_db_backfills() (best-effort, не блокирует запуск)
  • verify_trends fix: Per-item conn.commit() после каждой верификации — не держит write lock во время await
  • merge_classes fix: Убраны conn.commit()/conn.rollback() из merge_classes() — вызывающий код управляет транзакциями. Раньше conn.commit() внутри SAVEPOINT уничтожал все активные savepoints
  • FK orphan cleanup: UPDATE trends SET merged_into = NULL перед DELETE orphaned trends — устраняет self-referencing FK constraint
  • Scoring fix (CRITICAL): update_trend_scores использовал WHERE id = ? для trends — но objects.id ≠ trends.id (разные autoincrement). Исправлено на WHERE object_id = ?. До исправления 245 из 1080 трендов имели нулевые оценки
  • Backfill fix: _backfill_trend_scores() теперь передаёт object_id вместо trends.id

Подводные камни:

  • objects.id ≠ trends.id для записей после migration 018 — НИКОГДА не использовать WHERE trends.id = objects.id
  • conn.commit() внутри SAVEPOINT уничтожает все savepoints — не коммитить в функциях, вызываемых из savepoint

[2026-03-21] fix(tests): Update tests for 768d multilingual model + ruff config

Тип: fix Файлы: tests/unit/test_foundation.py, tests/unit/test_crawler_fixes.py, pyproject.toml Описание:

  • Тесты EmbeddingCache обновлены: 384d → 768d (после перехода на paraphrase-multilingual-mpnet-base-v2)
  • Тест test_counts_unique_domains обновлён: mock-результаты теперь включают .title/.content + мок _citation_relevance (после добавления embedding-based фильтрации)
  • Ruff: добавлен "tests/**/*.py" = ["E402"] — E402 ложно-срабатывал на pytestmark перед import

v0.15.46 — 2026-03-21

[2026-03-21] feat(clustering): Semantic object clustering — FAISS + Louvain auto-merge

Тип: feature Файлы: src/services/object_clusterer.py (NEW), db/migrations/046_object_cluster_suggestions.py (NEW), src/services/alias_resolver.py, src/services/object_extractor.py, src/config.py, src/workers/translation_watchdog.py, api/settings_routes.py, api/routes/objects.py Описание: Семантическая кластеризация объектов для автоматического объединения дубликатов. 96.8% объектов (1,430 из 1,478) имели только 1 сигнал из-за того, что LLM создаёт разные названия для одной темы. Три слоя:

  • L1 — Real-time auto-merge (cosine >= 0.85): при создании нового объекта FAISS nearest-neighbor ищет существующий дубликат. Точные совпадения ("India AI data centers" / "India AI data center capacity") мержатся автоматически.
  • L2 — Periodic Louvain clustering (watchdog, каждые 30 мин): строит граф похожести (edge >= 0.50), NetworkX Louvain communities, авто-мерж (>= 0.85) или suggestion (0.60–0.85) для админа.
  • L3 — Admin action (POST /admin/actions/cluster-objects): то же что L2, по запросу.

Дополнительно:

  • Trend metadata merge (_merge_trend_metadata): при объединении объектов переносятся анализы (analyses FK), change_verb, change_metric из source в target
  • _reclassify_target(): после мержа target пересчитывается для SIGNAL→TREND promotion
  • Admin API: GET /objects/cluster-suggestions, POST .../approve, POST .../reject, POST .../bulk
  • Migration 046: таблица object_cluster_suggestions (source_id, target_id, similarity, method, status)
  • Config: 5 настроек порогов кластеризации в src/config.py

Подводные камни:

  • Первый запуск L2/L3 может создать много suggestions — проверять качество перед массовым approve
  • Louvain иногда создаёт рыхлые кластеры — порог suggest_threshold=0.60 фильтрует ложные пары
  • database is locked при concurrent access — используется timeout=30 + retry в object_extractor

v0.15.45 — 2026-03-21

[2026-03-21] feat(citations): Multilingual embedding model + citation relevance filtering

Тип: feature Файлы: src/services/zone_matcher.py, src/services/adapters/searxng.py, src/workers/crawler.py, src/services/object_extractor.py, src/cache/embedding_cache.py, db/connection.py Описание:

  • Мультиязычная модель: bge-small-en-v1.5 (384d, EN) → paraphrase-multilingual-mpnet-base-v2 (768d, 50+ языков) — поддержка кириллицы, китайского и др.
  • Релевантность цитирований: FAISS-based cosine similarity ≥ 0.40 вместо keyword matching — фильтрует нерелевантные URL
  • Детекция языка: _detect_lang() → SearXNG ищет на языке сигнала (ru/zh/auto)
  • Теги в поиске: signal tags добавляются к поисковому запросу для повышения точности
  • Auto-backfill: _build_index() автоматически пересчитывает embeddings при смене модели/размерности
  • DB lock retry: busy_timeout=15000 + retry loop (3 попытки) в object_extractor.py Подводные камни: Первый запуск после обновления медленный — пересчитывает все 395 embeddings зон

v0.15.44 — 2026-03-21

[2026-03-21] feat(sources): Consumer & Chinese product sources — The Verge, TechCrunch, Engadget, Wired, Fashionista, Retail Dive, Pandaily, TechNode

Тип: feature Файлы: 8 новых адаптеров в src/services/adapters/, src/config.py, signal_mapper.py, crawler.py, en.json, ru.json Описание: 8 источников потребительских и товарных трендов:

  • Электроника/Гаджеты: The Verge (Atom), Engadget (RSS)
  • Технологии/Стартапы: TechCrunch, Wired (RSS)
  • Мода: Fashionista (RSS)
  • Ритейл/E-commerce: Retail Dive (RSS)
  • Китайский бизнес: Pandaily, TechNode (RSS, англ.)

v0.15.43 — 2026-03-20

[2026-03-20] feat(sources): Global & Chinese sources — BBC, Al Jazeera, SCMP, Yahoo Finance, CoinGecko

Тип: feature Файлы: src/services/adapters/bbc.py, aljazeera.py, scmp.py, yahoo_finance.py, coingecko.py, src/config.py, scoring.py, signal_mapper.py, crawler.py, en.json, ru.json Описание: 5 новых глобальных источников:

  • BBC Business (RSS) — глобальные деловые новости
  • Al Jazeera (RSS) — глобальные новости и геополитика
  • SCMP (RSS) — South China Morning Post, Китай/Азия
  • Yahoo Finance (API) — мировые индексы (S&P500, FTSE, Nikkei, HSI, DAX, Shanghai) + сырьё (Gold, Oil, Gas, Copper, Wheat)
  • CoinGecko (API) — крипторынок (Bitcoin, Ethereum) Интеграция: configs, scoring (yfinance/coingecko → z-score sigmoid), signal_mapper, crawler imports, i18n

v0.15.42 — 2026-03-20

[2026-03-20] feat(sources): Vedomosti, Interfax, TASS, MOEX adapters

Тип: feature Файлы: src/services/adapters/vedomosti.py, interfax.py, tass.py, moex.py, src/config.py, src/services/scoring.py, src/workers/crawler.py, api/services/signal_mapper.py, en.json, ru.json Описание: 4 новых источника данных:

  • Vedomosti (RSS) — деловая газета, бизнес/экономика
  • Interfax (RSS) — крупнейшее информагентство России
  • TASS (RSS) — государственное информагентство (англ. лента)
  • MOEX (ISS API) — индексы Московской биржи (IMOEX, RTSI), NumericSourceAdapter + SignalDetector Интеграция: configs, scoring (moex: → z-score sigmoid), signal_mapper URL generation, crawler imports, i18n labels, admin source management

v0.15.39 — 2026-03-20

[2026-03-20] feat: Верификация трендов — глагольные изменения + метрики + SearXNG-валидация (v0.15.40)

Тип: feature Файлы: src/services/object_extractor.py, src/services/trend_verifier.py (новый), src/services/trend_classifier.py, src/workers/translation_watchdog.py, db/migrations/044_trend_verification.py, api/schemas.py, api/routes/objects.py, frontend-cascade/app/src/components/trends/object-card.tsx, frontend-cascade/app/src/types/trend.ts, frontend-cascade/app/src/i18n/en.json, frontend-cascade/app/src/i18n/ru.json Проблема: Названия трендов — номинализации без метрик ("AI smart glasses privacy risks"). Нет проверки устойчивости: разовое событие выглядит как тренд. Решение:

  • Object extractor prompt: новые поля change_verb (глагол), change_metric (количественная метрика), verification_queries (2 поисковых запроса для SearXNG)
  • trend_verifier.py: SearXNG temporal analysis + LLM verdict (sustained/event/reversed/stalled)
  • Watchdog: периодическая ре-верификация (24ч), batch по 5 объектов
  • Migration 044: колонки change_verb, change_metric, verification_queries, verification_status, verification_date, verification_evidence_count + индекс
  • Frontend: change_metric на ObjectCard вместо description, VerificationBadge (sustained=зелёный, event=жёлтый, reversed=красный)
  • API: ObjectSummary расширена новыми полями

feat: Источники данных фаза 2 — vc.ru, Коммерсантъ, CNews

Тип: feature Файлы: src/services/adapters/vc_ru.py, src/services/adapters/kommersant.py, src/services/adapters/cnews.py, src/config.py, src/services/scoring.py, src/workers/crawler.py, api/services/signal_mapper.py, frontend-cascade/app/src/i18n/en.json, frontend-cascade/app/src/i18n/ru.json Что сделано:

  • vc.ru — адаптер для русскоязычного техно-сообщества (JSON API v2.8, метрики: reactions, comments, favorites, views). Scoring: sigmoid(reactions + favorites×2 + comments, 150). ID: vcru:{id}. Обработка двух типов элементов: entry (прямой) и news (обёртка с вложенными статьями). HTML-теги из блоков контента очищаются.
  • Коммерсантъ — адаптер для деловой газеты (RSS XML). Scoring: freshness-based. ID: kommersant:{hash}. Fix: Accept: */* вместо application/rss+xml (сервер возвращает 406), utf-8-sig декодирование (BOM).
  • CNews — адаптер для IT-новостей (RSS XML). Scoring: freshness-based. ID: cnews:{hash}. HTML-теги из описаний очищаются через regex.
  • Scoring в scoring.py: ветка vcru: с engagement-формулой
  • Signal mapper: URL-генерация для vcru:, kommersant:, cnews:
  • i18n: метки и описания для всех 3 источников (en/ru)

v0.15.38 — 2026-03-19

feat: Новые источники данных — Habr, RBC, ЦБ РФ + управление в админке

Тип: feature Файлы: src/services/adapters/habr.py, src/services/adapters/rbc.py, src/services/adapters/cbr.py, src/config.py, src/services/scoring.py, src/workers/crawler.py, src/workers/numeric_crawler.py, api/services/signal_mapper.py, frontend-cascade/app/src/routes/admin/crawler.tsx, frontend-cascade/app/src/i18n/en.json, frontend-cascade/app/src/i18n/ru.json Что сделано:

  • Habr.com — адаптер для крупнейшего русскоязычного IT-сообщества (JSON API, метрики: votes, views, bookmarks, comments). Scoring: sigmoid(votes + bookmarks×2, 100). ID: habr:{id}
  • RBC.ru — адаптер для крупнейшего делового СМИ России (RSS XML). Scoring: freshness-based. ID: rbc:{hash}
  • ЦБ РФ — числовой адаптер для макроиндикаторов (ключевая ставка через SOAP API, курсы валют USD/RUB, EUR/RUB, CNY/RUB через REST XML). Наследует NumericSourceAdapter + SignalDetector. ID: cbr:{indicator}:{country}
  • WorldBank и FRED вынесены в DataSourcesConfig — теперь видны и управляемы через админку
  • numeric_crawler теперь проверяет source_enabled:{name} и collection_enabled перед fetch (раньше игнорировал admin toggle)
  • Админка: группа "Numeric Data" (DATA) с cyan цветом; i18n-лейблы и описания для всех 19 источников (en/ru); статусы тултипов переведены Подводные камни:
  • Habr API: ключи publicationIds/publicationRefs (не articleIds/articleRefs)
  • CBR Key Rate: REST endpoint не работает (403) — используется SOAP API
  • CBR: даты DD.MM.YYYY, числа с запятой (92,1234), Nominal нормализация для CNY

v0.15.37 — 2026-03-19

feat: Прогнозы — серверная сортировка, переводы, компактные карточки

Тип: feature Файлы: api/prediction_store.py, api/routes/predictions.py, src/workers/translation_watchdog.py, frontend-cascade/app/src/routes/_dashboard/predictions.tsx, db/migrations/043_predictions_sort_indexes.py Решение:

  • Серверная сортировка по 4 колонкам (date, probability, deadline, status) + asc/desc
  • Перевод claim, zone_name, conditions через систему переводов (READ: cached, WRITE: watchdog)
  • Компактные строки вместо больших карточек, URL search params, Popover-фильтры
  • 6 композитных DB-индексов для быстрой сортировки + фильтрации
  • Формулировки прогнозов: {zone}: {mechanism} вместо object → zone: direction impact
  • 343 прогноза пересозданы с новым форматом

feat: Навигация — scroll restoration, back navigation

Тип: feature Файлы: frontend-cascade/app/src/app.tsx, frontend-cascade/app/src/routes/_dashboard/objects_.$objectId.tsx, frontend-cascade/app/src/routes/_dashboard/recommendations_.$recId.tsx, frontend-cascade/app/src/routes/_dashboard/convergence_.$zoneId.tsx Решение:

  • TanStack Router scrollRestoration: true — сохранение позиции скролла
  • Кнопка «Назад» через router.history.back() — сохраняет URL-параметры фильтров

feat: Конвергенция — кликабельные тренды с локализацией

Тип: feature Файлы: api/routes/convergence.py, src/services/convergence_analyzer.py Решение:

  • Все contributing trends — ссылки на /trend/$trendId (с fallback на plain div без trend_id)
  • Локализованные названия трендов через get_cached_translations_batch()
  • Фильтрация ghost-записей (без trend_id в БД)

refactor: Удаление Skills Map

Тип: refactor Файлы: frontend-cascade/app/src/components/recommendations/skills-view.tsx (удалён), frontend-cascade/app/src/routes/_dashboard/recommendations.tsx, en.json, ru.json Решение:

  • Skills Map показывал перефразированные рекомендации, а не навыки — удалён полностью
  • Убран toggle recommendations/skills, i18n-ключи skills.*

v0.15.35 — 2026-03-17

feat: Продвинутые методы анализа — Фазы 7+8 (Coherent Forecasting)

Тип: feature Файлы: src/services/prediction_search.py, src/services/backtester.py, src/services/cross_impact.py, src/services/ach.py, src/services/scenario_planner.py, src/services/popularity_scoring.py, src/services/metaculus_client.py, db/migrations/042_advanced_analysis.py, api/routes/advanced.py, api/main.py Решение:

  • Prediction search: FAISS-индекс по claim'ам прогнозов, семантический поиск похожих
  • Backtester: LLM-агент проверяет просроченные прогнозы, авто-резолвит при высокой уверенности
  • Cross-impact matrix: Агрегация весов рёбер между кластерами зон
  • ACH: Анализ конкурирующих гипотез (Heuer) — 3-5 гипотез, матрица диагностичности, LLM-генерация
  • Scenario planner: Три сценария (оптимистичный/базовый/пессимистичный) + wildcards + decision points
  • Popularity scoring: BERTrend-подобный скоринг — экспоненциальное затухание (14 дней), перцентильные пороги, 3 компоненты (volume 50%, diversity 30%, recency 20%)
  • Metaculus client: Поиск похожих вопросов + бенчмарк наших вероятностей vs community forecast
  • Migration 042: ach_result, scenarios_result (analyses), popularity_score (trends)
  • API: 10 новых эндпоинтов под /api/advanced/

v0.15.34 — 2026-03-17

feat: Фронтенд — /convergence + /accuracy + навигация (Фаза 6 — Coherent Forecasting)

Тип: feature Файлы: frontend-cascade/app/src/routes/_dashboard/convergence.tsx, accuracy.tsx, api-client.ts, routes.ts, sidebar.tsx, use-convergence.ts, use-predictions.ts, en.json, ru.json Проблема: Новые бэкенд-функции (конвергенция, прогнозы, кластеры) не имели UI. Решение:

  • Страница /convergence: список конвергентных зон с фильтрами (min_trends, category, direction, sort), карточки с joint_probability bar, responsive detail modal (Drawer/Dialog) с нарративом, timeline, взаимодействиями, конфликтами
  • Страница /accuracy: Brier Score карточка, scorecard (open/resolved/overdue), калибровочная таблица, список последних resolved прогнозов
  • API client: методы для convergence, predictions, clusters, graph + TypeScript типы
  • Hooks: useConvergenceList, useConvergenceByZone, usePredictions, useAccuracy
  • Навигация: Convergence (Merge icon) + Accuracy (Target icon) в sidebar группе "Analyze"
  • i18n: полные ключи en + ru для обеих страниц Подводные камни:
  • Страницы работают с пустыми данными (noData state) — нужен хотя бы один анализ для конвергенции

v0.15.33 — 2026-03-17

feat: Структурированные прогнозы + Brier Score (Фаза 5 — Coherent Forecasting)

Тип: feature Файлы: api/prediction_store.py, src/services/calibration.py, api/routes/predictions.py, api/main.py, db/migrations/041_predictions.py Проблема: Невозможно отслеживать точность прогнозов системы, нет feedback loop. Решение:

  • predictions table: claim, probability, deadline, conditions, counter_conditions, status (open/resolved), outcome
  • prediction_store.py: CRUD + extract_predictions_from_analysis (автоматическое извлечение из impact_zones)
  • calibration.py: Brier Score, калибровочная кривая (10 бинов), scorecard (open/resolved/overdue)
  • API: GET /api/predictions (список), GET /api/predictions/accuracy (дашборд), POST /api/predictions (создание), POST /api/predictions/{id}/resolve (закрытие), POST /api/predictions/extract/{job_id} (из анализа)
  • Автоматическое извлечение: зоны с probability > 0 → predictions с deadline из timeframe Подводные камни:
  • Brier Score = 0.25 при пустых данных (baseline coin flip)
  • Predictions с probability=0 игнорируются при извлечении

v0.15.32 — 2026-03-17

feat: Кластеризация зон + граф связей (Фаза 4 — Coherent Forecasting)

Тип: feature Файлы: src/services/zone_clusterer.py, src/services/zone_graph.py, api/routes/clusters.py, api/main.py, db/migrations/040_zone_clusters_and_edges.py Проблема: ~400 зон влияния без иерархии и связей между собой. Решение:

  • HDBSCAN кластеризация: density-based, ~400 зон → ~20 суперкластеров, без указания k
  • Extremized aggregation: геометрическое среднее шансов (d=2.5) — усиливает консенсус
  • Zone graph: 3 типа рёбер — similarity (cosine >0.6), co_occurrence (≥2 общих анализа), conflict (будущее)
  • API: GET /api/zones/clusters, GET /api/zones/graph, POST /api/zones/clusters/compute, POST /api/zones/graph/compute
  • Migration 040: таблицы zone_clusters + zone_edges с индексами Подводные камни:
  • HDBSCAN на L2-normalized embeddings = косинусное сходство через евклидово расстояние
  • Zone graph может быть тяжёлым (O(N²) для similarity) — ограничен max_edges=500
  • hdbscan пакет нужен в requirements.txt

v0.15.31 — 2026-03-17

feat: Универсальный адаптер числовых данных (Фаза 3 — Coherent Forecasting)

Тип: feature Файлы: src/services/adapters/_numeric_base.py, src/services/adapters/_numeric_config.py, src/services/adapters/worldbank.py, src/services/adapters/fred.py, src/services/adapters/__init__.py, src/services/scoring.py, src/services/trend_object_scoring.py, src/workers/numeric_crawler.py, config/numeric_indicators.yaml, requirements.txt Проблема: Система анализировала только текстовые сигналы (HN, GitHub, arXiv). Макроэкономические и демографические данные отсутствовали. Решение:

  • SourceType.DATA — новый тип источника для числовых данных (reliability 0.95)
  • SignalDetector — детектор аномалий: Z-score, YoY%, trend direction (3+ consecutive)
  • NumericSourceAdapter — базовый класс для числовых адаптеров
  • WorldBankAdapter — демография, экономика, экология (wbgapi + httpx fallback, без API key)
  • FREDAdapter — экономика США (fredapi + httpx fallback, требует FRED_API_KEY)
  • Scoring: wb:/who: → 0.5×sigmoid(z,2) + 0.5×sigmoid(dev,20); fred: → sigmoid(z,2)
  • NumericCrawler — отдельный worker, цикл раз в 24ч (не каждые 15 мин как основной)
  • 22 индикатора в YAML-конфиге: рождаемость, ВВП, CPI, безработица, CO2, образование и др.
  • ID формат: wb:SP.DYN.CBRT.IN:DEU, fred:UNRATE Подводные камни:
  • FRED требует API key (FRED_API_KEY env var), без него adapter пропускается
  • wbgapi может не установиться на некоторых платформах — есть httpx fallback
  • SignalDetector требует минимум 5 точек данных для анализа

v0.15.30 — 2026-03-17

feat: Конвергентный анализ влияния (Фаза 2 — Coherent Forecasting)

Тип: feature Файлы: src/services/convergence_analyzer.py, src/prompts/templates.py, api/routes/convergence.py, api/main.py, db/migrations/039_zone_convergence.py Проблема: Разные тренды влияют на одну зону, но анализировались изолированно. Невозможно увидеть совместное воздействие. Решение:

  • convergence_analyzer.py: полный движок конвергентного анализа
    • Temporal weighting: 1 / (1 + years / horizon) — нормализация разных скоростей
    • Interaction matrix: α коэффициенты (синергия +0.3, антагонизм -0.2) через cosine + direction
    • Noisy-OR: P(zone) = 1 - (1-leak) × Π(1 - p_i × w_i) — субаддитивная совместная вероятность
    • Total impact с interaction terms: Σ(imp_i × w_i) + Σ(α_ij × imp_i × imp_j)
  • P_CONVERGENCE_SYNTHESIS: LLM-промпт для синтеза нарратива, timeline overlap, рекомендаций
  • LLM synthesis: автоматическая генерация нарративов для топ-10 зон (haiku, 45с timeout, graceful degradation)
  • API endpoints: GET /api/convergence (список + фильтры), GET /api/convergence/{zone_id} (детали), POST /api/convergence/compute (запуск)
  • Migration 039: таблица zone_convergence с индексами Подводные камни:
  • _collect_zone_contributions берёт только последнюю версию анализа по тренду (MAX version)
  • LLM synthesis — опциональный шаг, при ошибке зона сохраняется без нарратива
  • json_array_length() используется в фильтре min_trends — требует SQLite 3.38+

v0.15.29 — 2026-03-17

feat: Superforecasting + Consistency Check + Red Team (Фаза 1 — Coherent Forecasting)

Тип: feature Файлы: src/prompts/schemas.py, src/prompts/templates.py, src/agents/nodes.py, api/services/analysis_runner.py, api/analysis_store.py, api/schemas.py, api/routes/helpers.py, db/migrations/038_analysis_verification.py Проблема: Анализы давали точечные оценки без вероятностей, проверки согласованности и adversarial review. Решение:

  • Superforecasting в P1: probability, base_rate_reasoning, conditions, counter_conditions, link_probability
  • Chain probability в P2: link_probability для каскадных эффектов (заменяет hardcoded 0.5)
  • Consistency Checker — новый LLM-узел: 6 типов проверок (противоречия, аномалии, калибровка)
  • Red Team — adversarial-адвокат дьявола: минимум 3 challenge на каждый отчёт
  • Pydantic: ConsistencyCheckResponse, RedTeamResponse, ConsistencyIssue, RedTeamChallenge
  • Pipeline: coordinator → collector → researcher → graph → consistency → report → red_team → recommendations
  • Миграция 038: колонки consistency_result, red_team_result в analyses
  • API: AnalysisReportResponse включает consistency_result и red_team_result Подводные камни:
  • Новые узлы используют модель haiku (дешёвая, ~45 сек timeout) — не блокируют pipeline
  • При ошибке/timeout узлы возвращают None — pipeline продолжает без них
  • Калибровка вероятностей: промпт требует разброс 0.3-0.9, не кучку 0.6-0.7

v0.15.28 — 2026-03-17

feat: Transformer embeddings for zone matching (Фаза 0 — Coherent Forecasting)

Тип: feature Файлы: src/services/zone_matcher.py, db/migrations/037_reembed_zones_transformer.py, requirements.txt, docs/scoring/coherent-forecasting.md, docs/INDEX.md Проблема: ZoneMatcher использовал n-gram хэширование (512-dim) — не улавливал семантику. "ML" и "Machine learning" давали нулевое совпадение. Линейный поиск O(N). Решение:

  • Замена n-gram на трансформерные эмбеддинги BAAI/bge-small-en-v1.5 через fastembed (384-dim, ONNX, CPU-only)
  • FAISS IndexFlatIP для косинусного поиска O(log N)
  • Ленивая загрузка модели (singleton, thread-safe)
  • FAISS-индекс строится из DB при первом запросе, инвалидируется при мутации
  • Миграция 037: пересчёт всех ~395 зон батчем
  • Порог снижен с 0.65 до 0.55 (трансформерные косинусы более распределены)
  • Создана документация docs/scoring/coherent-forecasting.md с обоснованием всех методов Подводные камни:
  • fastembed скачивает модель (~50MB) при первом запуске — нужен интернет
  • Старые 512-dim эмбеддинги несовместимы — миграция 037 обязательна
  • _EMBED_DIM изменён с 512 на 384 — тесты уже используют 384

v0.15.19 — 2026-03-13

feat: Phase 2 — Guided Tour, Watchlist, Notifications, Saved Items, Content Plan, Skills View, CTA

Тип: feature Файлы: новые: guided-tour.tsx, content-plan-modal.tsx, skills-view.tsx, use-watchlist.ts, use-notifications.ts, watchlist.tsx, api/routes/watchlist.py, api/routes/saved.py, api/routes/notifications.py, api/notification_service.py, db/migrations/034-036, типы watchlist.ts; изменённые: api/main.py, api/routes/objects.py, api-client.ts, routes.ts, sidebar.tsx, _dashboard.tsx, trend.$trendId.tsx, recommendations.tsx, saved-store.ts, profile.tsx, shared.$token.tsx, crawler.py, pulse.tsx, i18n Решение:

  • O2 Guided Tour: react-joyride, 7 шагов (навигация → период → статистика → карточка → оценка → heatmap → анализ), auto-start после welcome modal, restart из Profile, dark theme styling, data-tour атрибуты на целевых элементах
  • O6 CTA на публичном отчёте: баннер с TrendingUp иконкой и кнопкой "Try Cascade" на shared.$token.tsx
  • F2 Watchlist / Follow: backend user_watchlist таблица + CRUD endpoints, frontend хуки (useWatchlist, useIsWatching, useWatchToggle), Eye кнопка на trend detail page, /watchlist страница с карточками объектов
  • F3 Backend Notifications: notifications таблица, notification_service.py (create/list/mark_read/delete), REST endpoints, use-notifications.ts хук с polling 30s, интеграция в crawler (system notifications при обнаружении новых трендов)
  • F5 Server-side Saved Items: user_saved_items таблица, CRUD endpoints (/api/saved), api-client.ts methods (getSavedItems, saveItem, unsaveItem), sync с Zustand store
  • F9 Content Plan Generator: LLM endpoint POST /objects/{id}/content-plan (3 статьи + 2-недельный календарь + SEO), responsive ContentPlanModal (Drawer/Dialog), кнопка на trend detail page
  • F10 HR Skills View: отдельная вкладка "Skills Map" на recommendations page, aggregation рекомендаций в навыки по ролям, skill extraction из action text, карточки с priority/timeframe/effort и driving trends
  • Migrations: 034 notifications, 035 user_saved_items, 036 user_watchlist
  • i18n: ~60 новых ключей (tour., skills., contentPlan., watchlist., shared.cta*, profile.tour*, nav.watchlist)

v0.15.17 — 2026-03-13

feat: Phase 1 Quick Wins — onboarding, session timer, digest, CSV export, web citations

Тип: feature Файлы: frontend-cascade/app/src/components/onboarding/welcome-modal.tsx (new), frontend-cascade/app/src/components/layout/session-timer.tsx (new), frontend-cascade/app/src/routes/_dashboard/pulse.tsx, frontend-cascade/app/src/routes/_dashboard/recommendations.tsx, frontend-cascade/app/src/components/trends/trend-card.tsx, frontend-cascade/app/src/components/layout/sidebar.tsx, frontend-cascade/app/src/routes/_dashboard.tsx, i18n files Решение:

  • O1 Welcome Screen: 3-step onboarding modal (Discover → AI Analysis → Stay Ahead), framer-motion transitions, responsive Drawer/Dialog, localStorage tracking (cascade_welcome_seen), integrated in _dashboard.tsx
  • O5 Session Timer: отслеживает время сессии + навигации, tooltip "You reviewed in X what would take ~Y manually" (McKinsey 15min/page baseline), collapsed/expanded modes, integrated in sidebar
  • O4+F6 Digest Card: сводка за период на Pulse (7d/30d) — 4 метрики (total/new/updated/signals), summary text, top 3 categories chips, animated counters
  • F7 Export CSV: кнопка Download CSV на странице рекомендаций, RFC-4180 escaping, 11 колонок, disabled state при пустом списке, tooltip с количеством строк
  • F8 Web Citations: Globe icon + count + tooltip на TrendCard в footer (отображается при web_citations > 0)
  • i18n: 21 новый ключ в en.json/ru.json (welcome., sessionTimer., pulse.digest*, recommendations.export*, trend.webCitationsTip)

v0.15.16 — 2026-03-13

feat: LLM-powered object comparison

Тип: feature Файлы: api/routes/objects.py, api/schemas.py, frontend-cascade/app/src/stores/compare-store.ts, frontend-cascade/app/src/components/compare/compare-floating-bar.tsx, frontend-cascade/app/src/components/compare/compare-result-modal.tsx, frontend-cascade/app/src/components/trends/object-card.tsx, frontend-cascade/app/src/routes/_dashboard/objects.tsx, frontend-cascade/app/src/lib/api-client.ts, frontend-cascade/app/src/types/trend.ts, i18n files Решение:

  • Backend: POST /api/objects/compare — принимает 2 object_ids, загружает данные из objects+trends, отправляет в LLM (sonnet), возвращает структурированное сравнение (summary, key_differences, recommendation, risks, content_opportunity)
  • Auth-gated: требует авторизацию + 1 кредит за сравнение
  • Endpoint размещён ПЕРЕД /{object_id} для корректного роутинга FastAPI
  • Frontend: чекбоксы на ObjectCard, floating bar (framer-motion) для выбора 2 объектов, responsive modal (Drawer/Dialog) для результата
  • Локализация: поддержка locale в LLM-промпте, i18n ключи en/ru Подводные камни:
  • Route order в FastAPI: /compare обязательно перед /{object_id}, иначе "compare" парсится как int параметр
  • Типы frontend выровнены с backend (flat object_a_name/object_b_name, не objects[] массив)

v0.15.0 — 2026-03-10

feat: zone_id filter, pulse heatmap overhaul, API routes modularization

Тип: feature + refactor + fix Версия: 0.15.14 Файлы: api/routes/trends.py (NEW), api/routes/analyses.py (NEW), api/routes/signals.py (NEW), api/routes/zones.py (NEW), api/routes/recommendations.py (NEW), api/routes/helpers.py (NEW), api/analysis_store.py, src/services/zone_matcher.py, src/services/trend_classifier.py, src/workers/crawler.py, frontend-cascade/app/src/routes/_dashboard/pulse.tsx, frontend-cascade/app/src/routes/_dashboard/reports.tsx, frontend-cascade/app/src/lib/api-client.ts

Новые фичи:

  1. Zone ID filter — фильтрация по zone_id (integer) вместо строкового имени зоны. Language-independent. Работает на /trends, /signals, /pulse.
  2. Reports zone filter/reports поддерживает фильтр по зоне влияния (через zone_recommendations).
  3. Zone resolution в анализах_resolve_analysis_zones() автоматически заполняет zone_id в analyses.impact_zones после завершения анализа (только когда trend_id установлен).
  4. API routes modularizationapi/routes.py разбит на 6 модулей.

Pulse heatmap:

  • zone_heatmap показывает зоны из TREND-статус анализов + __unclassified_<cat>__ per category
  • __unclassified_<cat>__ = gap между TREND-активностью и зонно-покрытой частью. Позволяет heatmap оставаться непустым даже когда TREND-тренды ещё не анализировались
  • Дедупликация: каждый тренд считается один раз на зону/день (JOIN trends t ON t.id = a.trend_id AND t.status = 'TREND')
  • activity_heatmap — TREND-тренды по категориям (updated_at), независимый от zone_heatmap
  • Цветовая схема (HEATMAP_HEX): technology=#3b82f6, science=#a855f7, business=#f59e0b, economy=#22c55e, society=#f43f5e
  • Фронтенд: isZoneHighlight = highlight NOT IN KNOWN_CATEGORIES → zone_id filter; __unclassified_*__ рендерятся последними в группе категории (isSpecial=true, лейбл "Другие")

Рефакторинг analyses:

  • trend_class_idobject_id INTEGER FK → objects.id (переименование в v0.15.3)
  • trend_id INTEGER FK → trends.id (добавлен как отдельная колонка, параллельно legacy TEXT trend_id)
  • Уникальный индекс idx_analyses_trend_id_version ON (trend_id INTEGER, version) WHERE trend_id IS NOT NULL
  • Zone filter pivot: analyses.trend_id = trends.id (NOT object_id)

ZoneMatcher:

  • find_zone_id(name) — cross-category поиск, возвращает (canonical_name, zone_id)
  • _find_similar_zones_all(embedding) — поиск по всем категориям без фильтра
  • match_or_create_zone() — валидирует category ∈ {economy, technology, society, business}; ищет по всем категориям перед созданием новой

API filter changes:

  • api/analysis_store.py::list_analyses_grouped() — параметр zone: str | None; фильтр через EXISTS на zone_recommendations
  • api/routes/analyses.pyzone: str | None = Query(None)
  • api/routes/signals.pyzone_id: int | None = Query(None)
  • src/workers/crawler.pyget_trending_page(), get_trending_counts(), _build_where() поддерживают zone_id
  • frontend-cascade/app/src/lib/api-client.ts::getGroupedAnalyses — добавлен zone?: string

Migrations: 022-026 добавлены

Подводные камни:

  • TREND-статус тренды и анализированные объекты могут быть двумя непересекающимися множествами (TREND = web-citations validated; analyzed = user-initiated). Это штатная ситуация, __unclassified_<cat>__ её корректно отражает.
  • Zone filter на /trends?zone_id=X использует дефолтный status=TREND — зоны в heatmap должны соответствовать TREND-анализам.
  • analyses.trend_id (INTEGER FK → trends.id) ≠ legacy trend_id (TEXT FK → signals.id). Оба существуют в схеме одновременно.
  • _resolve_analysis_zones() запускается только когда analyses.trend_id IS NOT NULL (INTEGER FK установлен).

v0.14.0 — 2026-03-04

feat: серверная пагинация/поиск отчётов + Lab saved products

Тип: feature + fix Файлы: db/migrations/021_analyses_search_text.py (NEW), api/analysis_store.py, api/routes.py, src/workers/translation_watchdog.py, frontend-cascade/app/src/routes/_dashboard/reports.tsx, frontend-cascade/app/src/lib/api-client.ts, frontend-cascade/app/src/i18n/{en,ru}.json, frontend-lab/app/src/stores/saved-store.ts (NEW), frontend-lab/app/src/routes/saved.tsx (NEW), frontend-lab/app/src/components/products/product-card.tsx, frontend-lab/app/src/routes/products_.$productId.tsx, frontend-lab/app/src/components/layout/{sidebar,bottom-nav}.tsx, frontend-lab/app/src/i18n/{en,ru}.json, api/lab/routes/needs.py, frontend-cascade/app/src/routes/admin/crawler.tsx, frontend-cascade/app/src/routes/login.tsx

Новые фичи:

  1. Reports: серверная пагинация, поиск, сортировкаsearch_text колонка в analyses (EN + переводы, как signals/objects). URL search params (validateSearch), debounced search, Popover filters (sort + page size), PaginationNav. Migration 021: search_text + индексы idx_analyses_grouped_lookup, idx_analyses_score.
  2. Lab: Saved Products — Zustand store (lab-saved-store → localStorage), страница /saved, кнопка-закладка на карточках и странице продукта, навигация в sidebar/bottom-nav.
  3. Lab: trend_name переводtrend_name добавлен в _NEED_TRANSLATABLE для needs endpoint.

Исправления:

  1. Reports сортировка по дате — ungrouped analyses дописывались в конец без пересортировки. Теперь оба режима (date/score) пересортируют объединённый список.
  2. React hooks ordercrawler.tsx: useState/useEffect после early return; login.tsx: navigate() во время render → useEffect. products_.$productId.tsx: useSavedStore после early return.
  3. Product card пустые заголовки — убран gradient placeholder при отсутствии image.
  4. Product dimension scoresscore_market != null (0.0 тоже true) → проверка суммы > 0.

rebuild_analyses_search_text() — watchdog вызывает после перевода анализов для обновления search_text.

Подводные камни:

  • search_text backfill при миграции = только EN baseline. Полный rebuild (EN + переводы) после первого прогона watchdog.
  • Lab saved products в localStorage — при очистке браузера данные теряются.

v0.13.0 — 2026-03-03

fix: 4 бага + 2 фичи (сигналы, роли, производительность, shared report, admin sources, searxng remap)

Тип: fix + feature Файлы: api/analysis_store.py, src/workers/crawler.py, db/migrations/020_performance_indexes.py (NEW), api/recommendation_store.py, src/workers/translation_watchdog.py, api/routes.py, api/middleware.py, api/settings_routes.py, api/services/signal_mapper.py, src/services/adapters/searxng.py, frontend-cascade/app/src/routes/_dashboard/recommendations.tsx, frontend-cascade/app/src/components/analysis/source-sidebar.tsx, frontend-cascade/app/src/components/analysis/report-tabs.tsx, frontend-cascade/app/src/routes/shared.$token.tsx, frontend-cascade/app/src/routes/admin/crawler.tsx

Исправления:

  1. phase='new' → 'emerging'create_signals_from_sources() писал невалидную фазу, TrendPhase('new') бросал ValueError, 18/19 сигналов молча терялись. Добавлен fallback в crawler.py и миграция для исправления существующих записей.
  2. JOIN bugrecommendation_store.py использовал tr.id = t.id вместо tr.object_id = t.id. После migration 018 objects.id ≠ trends.id → все trend_momentum/composite были NULL.
  3. Shared report signal links — скрыты ссылки на сигналы на публичных отчётах (phantom searxng IDs → 404). Добавлен showSignalLink prop.
  4. Role chip translations — роли динамические (LLM), добавлены в DB-кеш переводов: watchdog WRITE + API READ (role_labels) + frontend fallback.

Производительность:

  • Migration 020: 15 индексов для analyses, zone_recommendations, trends, signals. Критичный: idx_analyses_tcid_status_ver ускоряет коррелированный подзапрос _LATEST_VERSION_SUBQUERY.

Новые фичи:

  1. Admin sources status — карточки источников показывают last_fetch_at (relative time), цветовой индикатор: зелёный (<2ч), красный (>2ч), чёрный (>24ч). Данные персистятся в DB.
  2. SearXNG source remap — результаты из SearXNG ремапятся на нативные источники (HN, GitHub, arXiv, SO, Reddit, dev.to, YouTube) по домену URL. Для searxng: сигналов в UI показывается домен вместо "web".

Подводные камни:

  • test_settings_e2e.py::test_watchdog_runs_when_enabled зависает (вызывает LLM) — исключать при локальном запуске тестов
  • DOMAIN_SOURCE_MAP в searxng.py — при добавлении нового адаптера добавить маппинг домена

v0.11.0 — 2026-03-01

Тип: feature Файлы: db/migrations/019_shared_reports.py (NEW), frontend-cascade/app/src/routes/shared.$token.tsx (NEW), api/analysis_store.py, api/routes.py, api/schemas.py, api/main.py, frontend-cascade/app/src/routes/_dashboard/analyze.$jobId.report.tsx, frontend-cascade/app/src/lib/api-client.ts, frontend-cascade/app/vite.config.ts Проблема: Пользователь не мог поделиться отчётом анализа — все страницы требовали авторизации. Решение:

  1. Migration 019 — таблица shared_reports(share_token PK, job_id UNIQUE, created_at, views_count).
  2. Backend: create_share_token() (INSERT OR IGNORE + SELECT, race-safe), get_shared_report(), delete_share_token(). Endpoints: POST/DELETE /analyses/{jobId}/share, GET /analyses/{jobId}/share, GET /public/report/{token}.
  3. Frontend: кнопка Share на странице отчёта → копирует URL в буфер. Публичная страница /shared/$token без auth (skipAuth: true). URL строится на фронте (window.location.origin + '/shared/' + token). Подводные камни: API prefix /public/report (не /shared) чтобы не конфликтовать с SPA route. _build_report_response() — shared helper для дедупликации кода отчётов.

Тип: feature Файлы: src/services/trend_classifier.py, api/analysis_store.py, api/services/analysis_runner.py Проблема: После пользовательского анализа (30 источников) сигналы создавались с score=0.1 (невидимые), object_class не устанавливался, signal_mappings dual-write пропускался, объект/тренд не создавался для новых тем. Решение:

  1. reclassify_single_object() в trend_classifier.py — целевая реклассификация одного объекта (~8 SQL запросов вместо O(N*5) от полного classify_trends()). Обновляет агрегаты, upsert trends (dual-write), пересчёт scores.
  2. create_signals_from_sources() — score 0.1→0.3, confidence 0.3→0.4, добавлен object_class, добавлен signal_mappings INSERT (dual-write gap fix).
  3. _materialize_analysis_trend() в analysis_runner.py — после анализа: создаёт object если новый (через _upsert_object + categorize_trend), обновляет analyses.trend_class_id, вызывает reclassify_single_object(). Подводные камни: reclassify_single_object() НЕ вызывает check_and_queue_auto_analyses() — анализ уже выполнен. Для merged-объектов возвращает "SIGNAL" без обработки.

feat: user signal creation — POST /objects/{id}/signals

Тип: feature Файлы: api/analysis_store.py, api/schemas.py, api/routes.py, frontend-cascade/app/src/lib/api-client.ts, frontend-cascade/app/src/routes/_dashboard/trend.$trendId.tsx, frontend-cascade/app/src/i18n/en.json, frontend-cascade/app/src/i18n/ru.json Проблема: Пользователь не мог добавлять сигналы к тренду для отслеживания развития. Решение:

  1. Backend: create_user_signal(object_id, title, url?, content?) — обработка URL-based (дедуп по signal_id) и text-only (user:{uuid}) сигналов. Dual-write в object_signals + signal_mappings. Вызов reclassify_single_object() после добавления.
  2. API: POST /objects/{object_id}/signals (auth required). Schemas: CreateUserSignalRequest, CreateUserSignalResponse(signal_id, title, created).
  3. Frontend: кнопка "Add signal" (Popover с формой: title, URL, note) на странице тренда. useMutation → invalidate queries → toast. Подводные камни: URL-based сигналы: если _url_to_signal_id(url) находит существующий — линкует без создания дубля (created: false). Кнопка видна только аутентифицированным пользователям.

feat: radar page server-side search, pagination, hierarchical filters

Тип: feature Файлы: api/routes.py, frontend-cascade/app/src/routes/_dashboard/radar.tsx Проблема: Radar страница загружала все тренды клиентски, не поддерживала поиск и серверную пагинацию. Решение: Серверный поиск по class_name/description + фильтры (phase, recommendation, category) + pagination. URL-based filter persistence (search params sync) на страницах explore, objects, radar, recommendations.


v0.9.0 — 2026-02-25

refactor(db): Signal → Object → Trend 3-tier model (Migration 018)

Тип: refactor Файлы: db/migrations/018_signal_object_trend_model.py (NEW), src/services/object_extractor.py, src/services/trend_classifier.py, src/services/trend_object_scoring.py, src/services/trend_scoring.py, src/services/alias_resolver.py, src/services/trend_description_generator.py, src/workers/crawler.py, api/analysis_store.py, api/recommendation_store.py Проблема: Модель данных смешивала "объект" и "тренд" в одной таблице trends. Не было сущности "объект" как таковой. Вектор изменения не вычислялся. signal_mappings использовал текстовый object_class вместо FK. Решение:

  1. Migration 018 — создаёт objects (backfill из trends), object_signals (из signal_mappings по trend_id FK), object_aliases (из trend_aliases). Добавляет object_id, change_type, discovery_method в signals и object_id, change_direction, change_type, confirmed_at в trends. 7 новых индексов.
  2. Object extractor — 3-field extraction: object_name (что) + change_description (как) + trend_name (combined). Dual-write в objects/object_signals + signal_mappings (legacy).
  3. Trend classifier_classify_via_objects() (new) / _classify_via_legacy() dispatch. TREND promotion ужесточён: signal_count ≥ 2 AND source_type_count ≥ 2.
  4. Все сервисы (scoring, descriptions, aliases, recommendations) — dual-read/write: objects first, trends/signal_mappings fallback. Подводные камни: objects.idtrends.id (независимые autoincrement). Всегда использовать trends.object_id FK для связи. Deprecated tables (signal_mappings, trend_aliases) сохранены для обратной совместимости.

refactor: удалены метрики urgency/quality из сигналов

Тип: refactor Файлы: src/services/scoring.py, src/workers/crawler.py, api/schemas.py, api/services/signal_mapper.py, api/v1/routes.py, src/agents/nodes.py, src/prompts/templates.py, frontend-cascade/app/src/types/trend.ts, frontend-cascade/app/src/components/trends/signal-view.tsx, frontend-cascade/app/src/routes/_dashboard/signal.$signalId.tsx, frontend-cascade/app/src/routes/_dashboard/explore.tsx Проблема: urgency_score и quality_score на сигналах дублировали trend-level метрики (trend_momentum, trend_significance). Frontend показывал устаревшие данные. Решение:

  1. Backend: удалены calculate_urgency() и calculate_quality() из scoring.py (~90 строк). Убраны из SQL INSERT/UPDATE в crawler.py, из SignalSchema, AnalysisReportResponse.
  2. Frontend: удалён UrgencyQualityIcons из signal-view.tsx. Signal detail показывает Confidence вместо Quality. Sort option quality удалён.
  3. LLM prompts: Срочность/КачествоМоментум/Значимость в templates. agg_urgency/agg_qualitytrend_momentum/trend_significance в agent nodes. Подводные камни: Колонки urgency_score/quality_score остаются в DB (nullable, deprecated). v1 API sort quality удалён — breaking change для внешних клиентов.

feat: унифицированные Popover-фильтры на всех страницах

Тип: feature Файлы: frontend-cascade/app/src/components/ui/filter-chip.tsx (NEW), frontend-cascade/app/src/components/ui/filter-row.tsx (NEW), frontend-cascade/app/src/components/ui/popover.tsx (NEW), frontend-cascade/app/src/routes/_dashboard/explore.tsx, frontend-cascade/app/src/routes/_dashboard/radar.tsx, frontend-cascade/app/src/routes/_dashboard/objects.tsx, frontend-cascade/app/src/routes/_dashboard/recommendations.tsx Проблема: Фильтры были реализованы inline на каждой странице (дублирование). Занимали много вертикального пространства. На recommendations уже был Popover, но узкий (460px). Решение:

  1. Shared components: FilterChip (pill с count/disabled), FilterRow (ряд чипов с опциональным поиском), Popover (Radix UI wrapper).
  2. FilterRow searchable — для зон влияния: <input> + фильтрация чипов + max-h-[200px] overflow-y-auto.
  3. Все 4 страницы: inline-фильтры → кнопка SlidersHorizontal + badge с count активных + Popover sm:w-[560px] lg:w-[640px]. Search и View toggle остаются вне Popover.
  4. Reset: resetFilters() сбрасывает всё к default. activeFilterCount считает ≠ default (sort не считается).

feat: web citation URLs на странице деталей тренда

Тип: feature Файлы: src/services/adapters/searxng.py, src/workers/crawler.py, api/schemas.py, api/routes.py, frontend-cascade/app/src/routes/_dashboard/trend.$trendId.tsx Проблема: count_citations() возвращал только число цитирований, без URL. На странице тренда не было ссылок на web-источники. Решение:

  1. SearXNG: count_citations() возвращает tuple[int, list[str]] — число + список URL.
  2. Crawler: сохраняет web_citation_urls в metadata_json сигнала.
  3. API: TrendDetail.web_citation_urls — агрегированные URL из всех сигналов тренда.
  4. Frontend: секция "Sources & Citations" на /trend/$trendId показывает кликабельные ссылки на web-источники.

feat: Product Lab — real API integration (замена mock data)

Тип: feature Файлы: frontend-lab/app/src/lib/api-client.ts (NEW), frontend-lab/app/src/hooks/*.ts (10 NEW hooks), frontend-lab/app/src/components/pipeline/pipeline-progress.tsx (NEW), frontend-lab/app/src/components/products/product-card.tsx (NEW), frontend-lab/app/src/routes/*.tsx (7 files), api/lab/routes/needs.py, api/lab/routes/products.py, api/lab/routes/trends.py (NEW), api/lab/services/pipeline_engine.py, api/lab/services/trend_enrichment.py, api/lab/store/needs_repo.py, api/main.py Проблема: Frontend Lab работал на mock data. Backend API был частично реализован но не интегрирован. Решение:

  1. Lab API client — typed fetch wrapper для всех Lab endpoints.
  2. 10 react-query hooksuseLabStats, useLabNeeds, useLabNeed, useLabProducts, useLabProduct, useCascadeTrends, useLabTrendStatus, useCreateNeed, useLabSSE, useDebounce.
  3. SSE pipeline progressGET /lab/needs/{id}/progress стримит прогресс пайплайна. PipelineProgress компонент с 4-step индикатором.
  4. Trends proxyGET /lab/trends проксирует get_trend_classes() без PAT auth, маппинг в Lab-формат.
  5. Need creationPOST /lab/needs с dedup guard, 5-min cooldown, semaphore (max 5 concurrent).
  6. Translationslang param на всех Lab endpoints с кэшированными переводами.
  7. Pagination — полноценная пагинация с sliding window (±2 pages).
  8. Cleanup loop — background _lab_cleanup_loop каждые 10 мин чистит stale state.

feat: расширенная страница деталей тренда

Тип: feature Файлы: frontend-cascade/app/src/routes/_dashboard/trend.$trendId.tsx, frontend-cascade/app/src/components/trends/trend-score-panel.tsx, frontend-cascade/app/src/components/trends/signal-view.tsx Проблема: Страница тренда показывала простую сетку статистик. Не было информации об объекте, типе изменения, web-цитированиях. Бэйджи фазы/рекомендации не имели тултипов. Решение:

  1. Object-Change model: отображение object_name, change_description, change_types на странице тренда.
  2. Web citations: счётчик + кликабельные URL в секции Sources & Citations.
  3. Collapsible signals: список сигналов сворачивается/разворачивается.
  4. Tooltips: TrendScorePanel — тултипы на бэйджах phase и recommendation с описанием из i18n.
  5. SignalView row variant: убран UrgencyQualityIcons, добавлен linked-trend mini-indicator (phase arrow + composite score, навигация на /trend).

v0.8.2 — 2026-02-25

feat: Objects API + модалка деталей объекта

Тип: feature Файлы: api/routes.py, api/schemas.py, api/main.py, frontend-cascade/app/vite.config.ts, frontend-cascade/app/src/types/trend.ts, frontend-cascade/app/src/lib/api-client.ts, frontend-cascade/app/src/hooks/use-objects.ts (NEW), frontend-cascade/app/src/routes/_dashboard/objects.tsx, frontend-cascade/app/src/components/trends/object-card.tsx, frontend-cascade/app/src/components/trends/object-detail-modal.tsx (NEW) Проблема: Страница Objects (/objects) обращалась к GET /trends вместо objects таблицы. Карточки отображали name (полное имя тренда) вместо object_name (объект). Не было модалки деталей. Решение:

  1. Backend: GET /objects (список) + GET /objects/{id} (детали с сигналами и трендами) — objects_router читает из objects + object_signals + trends.
  2. Frontend: useObjects() / useObject() хуки, ObjectDetailModal — модалка по клику на карточку (вместо навигации).
  3. Карточка: заголовок = object_name, убрано change_description (атрибут тренда, не объекта). Подводные камни: signals таблица НЕ имеет updated_at — использовать fetched_at. objects.idtrends.id для новых записей (разные autoincrement sequences).

feat: responsive модалки — Drawer на мобиле, Dialog на десктопе

Тип: feature Файлы: frontend-cascade/app/src/components/ui/drawer.tsx (NEW), frontend-cascade/app/src/hooks/use-is-mobile.ts (NEW), frontend-cascade/app/src/components/ui/dialog.tsx, frontend-cascade/app/src/components/trends/object-detail-modal.tsx, frontend-cascade/app/src/routes/_dashboard/recommendations.tsx, frontend-cascade/app/package.json Проблема: Dialog модалки не работают на мобильных устройствах — нет swipe-to-close, overlay блокирует взаимодействие. Не было анимации разворачивания. Решение:

  1. Установлен vaul (drawer library) — bottom sheet с drag-handle и swipe-to-close.
  2. drawer.tsx — shadcn-style Drawer компонент (vaul primitive).
  3. useIsMobile() — хук определения мобильного экрана (< 640px, sm breakpoint).
  4. ObjectDetailModal и RecDetailModal — responsive: Drawer на мобиле, Dialog на десктопе. Общий контент вынесен в shared-функцию.
  5. Dialog анимация: slide-in-from-bottom-4 + zoom-in-[0.98] + duration-300 ease-out. Подводные камни: useIsMobile использует matchMedia — SSR-несовместим (не проблема для SPA). Vaul Drawer требует DrawerTitle/DrawerDescription для accessibility (sr-only если визуально не нужен).

fix: описания трендов присваивались неправильным записям (dual-write bug)

Тип: bug Файлы: src/services/trend_description_generator.py Проблема: generate_trend_descriptions() при dual-write использовал UPDATE trends SET description = ? WHERE id = ? с objects.id, но objects.id и trends.id — независимые autoincrement последовательности (для записей после migration 018). ~37% описаний (367/978) были присвоены неправильным трендам. Решение: Заменил WHERE id = ? на WHERE object_id = ? (FK связь). Массовая синхронизация: скопировал правильные описания из objects в trends через object_id FK — 553 записи исправлены. Подводные камни: Всегда использовать trends.object_id для связи с objects, НИКОГДА не предполагать objects.id = trends.id.


v0.8.1 — 2026-02-24

feat: иерархический фильтр по зонам влияния + тултипы (Cascade Recommendations)

Тип: feature Файлы: db/migrations/017_zone_recommendations_zone_id.py (NEW), api/recommendation_store.py, api/schemas.py, api/routes.py, src/workers/translation_watchdog.py, frontend-cascade/app/src/types/analysis.ts, frontend-cascade/app/src/routes/_dashboard/recommendations.tsx, frontend-cascade/app/src/i18n/en.json, frontend-cascade/app/src/i18n/ru.json Проблема: На странице рекомендаций можно фильтровать по категориям (Economy, Technology...), но нельзя выбрать конкретную зону влияния. При наведении на бэйджи (phase, recommendation, priority и т.д.) непонятно что они означают. Решение:

  1. Migration 017ALTER TABLE zone_recommendations ADD COLUMN zone_id INTEGER + backfill из impact_zones_dictionary (case-insensitive match). FK формализует связь zone_recommendations → impact_zones_dictionary.
  2. save_recommendations() — при INSERT теперь резолвит zone_id из словаря (lookup cache для производительности).
  3. Hierarchical filter counts — новый Level 2.5 zones (между categories и priorities) в _compute_filter_counts(). Zone filter пробрасывается в Level 3 (priorities) и Level 4 (timeframes).
  4. Zone translationzone_name добавлен в _translate_recommendations() и _fill_recommendation_gaps() (watchdog). Endpoint возвращает zone_labels dict (original_en → translated) в filters для фильтр-чипов.
  5. Zone FilterRow — иерархический фильтр category → zone на странице /recommendations. При смене category/role зона сбрасывается. Переведённые лейблы через zone_labels.
  6. Tooltips — shadcn <Tooltip> на всех бэйджах в RecScoreCard, RecDetailModal и group headers: score, phase, recommendation, priority, timeframe, effort. i18n-ключи tooltip.* в en.json + ru.json. Подводные камни: zone_name в zone_recommendations уже каноническая (через ZoneMatcher в pipeline), поэтому backfill по LOWER match имеет высокий процент совпадений. Zone_id может быть NULL для строк, чьи zone_name не совпали со словарём — фильтры используют zone_name TEXT (не zone_id) для обратной совместимости.

feat: тултипы на бэйджах Lab frontend

Тип: feature Файлы: frontend-lab/app/src/components/ui/badges.tsx, frontend-lab/app/src/components/ui/score-badge.tsx, frontend-lab/app/src/components/products/product-card.tsx, frontend-lab/app/src/routes/products_.$productId.tsx, frontend-lab/app/src/routes/trends.tsx, frontend-lab/app/src/globals.css, frontend-lab/app/src/i18n/en.json, frontend-lab/app/src/i18n/ru.json Проблема: Бэйджи на страницах Lab (recommendation, phase, kano, effort, category, format, monetization, score) не имели пояснений. Решение:

  1. CSS-only тултипы через .badge-tip class (data-tip + ::after pseudo-element).
  2. Все 8 badge-компонентов обновлены с data-tip + i18n.
  3. ScoreBadge получил tipKey prop (trend score vs viability score).
  4. ProductCard и detail page — inline MetaBadge заменён на shared badge-компоненты. Подводные камни: CSS-only тултипы (не shadcn Tooltip) — работают на hover, не на touch. На мобильных устройствах тултипы не отображаются.

v0.8.0 — 2026-02-24

feat: система рекомендаций (Zone Recommendations)

Тип: feature Файлы: api/recommendation_store.py (NEW), api/routes.py, api/schemas.py, api/services/analysis_runner.py, src/agents/nodes.py, src/prompts/templates.py, db/migrations/015_zone_recommendations.py (NEW), db/migrations/016_rec_search_text.py (NEW), frontend-cascade/app/src/routes/_dashboard/recommendations.tsx (NEW), frontend-cascade/app/src/hooks/use-recommendations.ts (NEW), frontend-cascade/app/src/components/analysis/zone-recommendations-view.tsx (NEW) Проблема: После анализа тренда пользователь получал отчёт с зонами влияния, но не было конкретных рекомендаций — что делать CTO, разработчику, PM или инвестору. Решение:

  1. LLM-агент recommendations_agent() (Haiku) — генерирует рекомендации по 4 ролям (CTO, Developer, PM, Investor) для каждой зоны влияния. Промпт P_REC_ZONE_ROLES на английском.
  2. Pipeline-интеграция — Step 5.5 в analysis_runner.py, после генерации отчёта. Контролируется настройкой recommendations_enabled.
  3. Scoringrec_score (0-100) = trend_composite×0.50 + zone_impact×10×0.25 + priority_w×0.15 + timeframe_w×0.10. Вычисляется на лету, не хранится.
  4. Standalone страница /recommendations — role tabs, score groups (Act Now ≥70 / Plan For 40-69 / Monitor <40), карточки с trend scores (S/M/C bars), модальное окно деталей.
  5. Inline-таб в отчёте анализа — zone-recommendations-view.tsx внутри report tabs.
  6. Hierarchical filters — иерархия role → category → priority → timeframe, каждый уровень считает с применением фильтров сверху. totals для "All" badge.
  7. Migrations: 015 (zone_recommendations table), 016 (search_text + translations rebuild). Подводные камни: rec_score вычисляется на Python, не хранится в DB — сортировка по score только через Python post-sort с overfetch. Для массива 1000+ рекомендаций может быть неэффективно. impact_zones_dictionary должна быть заполнена для корректной работы category-фильтров (заполняется миграцией). Если recommendations_enabled=false, рекомендации не генерируются при анализе (старые остаются).

feat: админ-панель рекомендаций и разделение настроек

Тип: feature Файлы: api/settings_routes.py, api/settings_store.py, frontend-cascade/app/src/routes/admin/system.tsx, frontend-cascade/app/src/routes/admin/crawler.tsx (NEW), frontend-cascade/app/src/components/layout/admin-sidebar.tsx Проблема: Настройки auto-analysis и recommendations не отображались в админке. Все настройки были на одной странице. Решение:

  1. Разделение admin/system на две страницы: System (auto-analysis, recommendations, translations, descriptions, eval) и Crawler (sources, collection, external API).
  2. Recommendations settings — toggle recommendations_enabled + кнопка "Regenerate All" (POST /admin/actions/regenerate-recommendations).
  3. Auto-analysis settings — toggle + max_per_day (1-50) + depth (1-7).
  4. Backendrecommendations_enabled в _DEFAULTS, SettingsBody, SettingsResponse. Regenerate endpoint: background thread, deletes old recs → calls recommendations_agent → saves. Подводные камни: Regenerate работает в фоновом потоке. При большом количестве анализов (~50+) может занять несколько минут. Прогресс не отслеживается на фронтенде.

feat: сортировка отчётов по дате/оценке

Тип: feature Файлы: api/analysis_store.py, api/routes.py, frontend-cascade/app/src/routes/_dashboard/reports.tsx, frontend-cascade/app/src/lib/api-client.ts Проблема: Страница отчётов всегда показывала анализы по дате (newest first), без возможности отсортировать по оценке. Решение: GET /analyses?grouped=true&sort=date|score — параметр sort в list_analyses_grouped(). Frontend: toggle Date/Score с иконками Calendar/BarChart3. Query-ключ включает sort для автоматического рефетча. Подводные камни: При sort=score комбинированный список (grouped + ungrouped) пересортируется в Python. Для offset-based пагинации это безопасно, т.к. overfetch покрывает.

feat: Product Lab backend + frontend (MVP)

Тип: feature Файлы: api/lab/ (17 files, NEW), frontend-lab/ (37 files, NEW) Проблема: Нет инструмента для извлечения пользовательских болей из трендов и генерации бизнес-идей. Решение:

  1. Backendapi/lab/: needs extraction (P5), product ideation (P6), pipeline engine, SQLite store (needs_repo, products_repo), routes.
  2. Frontendfrontend-lab/: отдельное React-приложение с TanStack Router, pages: needs, products, trends. Mock data для демонстрации. Общие UI-компоненты (badges, ring-score, intensity-bar). Подводные камни: Product Lab пока работает с mock data на фронтенде. Backend API подключён, но не интегрирован с crawler pipeline. Требует отдельного npm install в frontend-lab/app/.

feat(db): search index migration (007)

Тип: feature Файлы: db/migrations/007_search_index.py (NEW) Решение: Миграция для создания поискового индекса.

chore: dev infra improvements

Тип: chore Файлы: dev.py, frontend-cascade/app/vite.config.ts Решение: 1) dev.py автоматически очищает __pycache__ при старте (исключая node_modules и venv). 2) Добавлен proxy /recommendations в vite.config.ts.


v0.7.4 — 2026-02-24

refactor: batch translation with context + sentence case

Тип: refactor Файлы: api/translation_service.py, src/workers/translation_watchdog.py, src/workers/crawler.py, api/settings_routes.py, CLAUDE.md Проблема: 1) Заголовки переводились без контекста (только title, без description и category) — LLM неверно интерпретировал многозначные термины. 2) Перевод анализов делал 30-80 отдельных LLM-вызовов (по одному на каждый zone name, mechanism, node label, source title). 3) Батч-методы всегда использовали "from English" без автодетекта языка. 4) Капитализация не контролировалась — LLM мог вернуть Title Case вместо sentence case. Решение: 1) Новый _batch_translate_items() — title + description + category в одном промпте для контекста, чанки по 30. 2) Новый _batch_translate_short_texts() — batch для коротких строк анализа с дедупликацией, чанки по 50. translate_analysis_result() теперь делает 2 LLM-вызова вместо 30-80. 3) _detect_source_lang() — автодетект по кириллице в батче. 4) _enforce_sentence_case() — постобработка первой буквы + явные правила капитализации в промптах. 5) Watchdog, crawler, settings_routes передают category в translate. Подводные камни: Старые кэшированные переводы (без sentence case) остаются в DB. Для перегенерации — очистить таблицу translations и запустить admin action. Legacy методы _batch_translate_titles() / _batch_translate_texts() сохранены для обратной совместимости (watchdog signal translation).


v0.7.1 — 2026-02-21

feat: expose external_api_enabled in admin settings

Тип: feature Файлы: api/settings_routes.py, tests/e2e/test_settings_e2e.py Проблема: External API (/api/v1/) гейтится через external_api_enabled (default: False), но это поле не было доступно в admin settings endpoint — невозможно включить через API. Решение: Добавлены external_api_enabled и external_api_rate_limit в SettingsBody, SettingsResponse, _build_settings_response() и update_settings_endpoint. Rate limit clamped [1, 300]. Подводные камни: После deploy нужно вручную включить: PUT /admin/settings {"external_api_enabled": true}.


v0.7.0 — 2026-02-21

refactor: scoring recalibration v2 — stretch practical ranges

Тип: refactor Файлы: src/services/trend_object_scoring.py, db/migrations/014_reset_trend_scores_v2.py, tests/unit/test_trend_object_scoring.py Проблема: Формулы скоринга откалиброваны на 5+ источников и 10+ сигналов, но 97% трендов имеют 1-3 сигнала от 1-2 источников. Практический composite: 0-40 вместо 0-100. Рекомендация ACT_NOW почти недостижима. Решение: 5 параметрических изменений: (1) DENSITY_BASELINES tech: 10→3, (2) MAX_COVERAGE_SOURCES=3 для confidence Coverage, (3) sample_size exp(-n/3) вместо exp(-n/10), (4) momentum default n<2: 40 вместо 25, (5) пороги рекомендаций: sig>=50, mom>=45, conf>=45, med>=35. Migration 014 сбрасывает все scores — backfill при рестарте. Подводные камни: Все scores пересчитываются при первом запуске (~90 сек на 1806 трендов). Исторические значения scores станут несравнимы с новыми.


v0.6.15 — 2026-02-20

feat: add analysis data to External API v1

Тип: feature Файлы: api/v1/schemas.py, api/v1/service.py, api/v1/routes.py, tests/unit/test_v1_routes.py, docs/api/EXTERNAL-API.md Проблема: Внешнее API не предоставляло данные анализа (LLM-отчёты). Агенты получали только скоринг и сигналы, без исследовательских фактов. Решение: 1) Новый эндпоинт GET /api/v1/trends/{id}/analysis — полный отчёт с impact zones и источниками. 2) TrendDetail теперь включает analysis (последний отчёт inline). 3) TrendItem обогащён полями has_analysis, analysis_score, analysis_confidence. 4) LEFT JOIN на analyses для эффективного обогащения в списках. 5) 8 новых тестов. Подводные камни: analysis в TrendDetail = null если анализ не проводился. Endpoint /analysis возвращает 404.


v0.6.14 — 2026-02-20

feat: v2 scoring recalibration for better recommendation distribution

Тип: feature Файлы: src/services/trend_object_scoring.py, tests/unit/test_trend_object_scoring.py, db/migrations/014_reset_trend_scores_v2.py Проблема: После первой калибровки (v0.6.12) пороги рекомендаций были недостаточно точны — DENSITY_BASELINES были завышены (10 для technology), coverage делился на 5 источников вместо 3, sample_size затухал слишком медленно (exp(-n/10)). В результате significance и confidence были ниже ожидаемого. Решение: 1) Снижены DENSITY_BASELINES (tech: 10→3, science: 5→2 и др.). 2) MAX_COVERAGE_SOURCES=3 — coverage теперь достигает 100% при 3 источниках. 3) sample_size затухает быстрее (exp(-n/3)). 4) Momentum default для n<2 повышен с 25→40. 5) Пороги рекомендаций пересчитаны: sig>=50, mom>=45, conf>=45, med_conf>=35. 6) Миграция 014 сбрасывает все score для автоматического пересчёта при старте. Подводные камни: После deploy нужен рестарт PM2 для запуска backfill. Все тренды будут пересчитаны — это может занять ~минуту.


v0.6.13 — 2026-02-20

Тип: feature Файлы: api/v1/service.py, api/v1/routes.py, api/routes.py, src/services/trend_classifier.py, tests/unit/test_v1_routes.py Проблема: API не поддерживал сортировку по confidence и significance (v1). Эндпоинт GET /api/v1/trends/top возвращал тренды с рекомендацией IGNORE в топ-5 по оценке и скорости роста. Решение: 1) Добавлен confidence sort во внутренний API. 2) Добавлены significance и confidence sorts в v1 API. 3) get_top_trends() теперь исключает IGNORE из highest_scored и fast_growing (new остаётся без фильтра — новые тренды ещё не оценены). 4) Два новых теста подтверждают фильтрацию. Подводные камни: new категория в top trends намеренно НЕ фильтрует IGNORE — новые тренды могут ещё не иметь рекомендации.


v0.6.12 — 2026-02-20

Тип: feature Файлы: src/services/trend_object_scoring.py, src/services/trend_classifier.py, api/routes.py, tests/unit/test_trend_object_scoring.py Проблема: 99.7% трендов (1800/1806) имели рекомендацию IGNORE — пороги (sig≥60, mom≥55, conf≥60) были выше максимальных значений в данных (sig max=51.5, mom max=65.1, conf max=49.8). API не поддерживал сортировку по оценкам. Решение: 1) Снижены пороги до sig≥43 (P75), mom≥35, conf≥35, med_conf≥27 — соответствуют реальному распределению данных. 2) Добавлен параметр sort_by в GET /trends и GET /trends/weak: default, composite, momentum, significance, newest. 3) Обновлены 9 тестов рекомендаций + добавлен тест граничных значений. Подводные камни: После деплоя нужно пересчитать рекомендации: UPDATE trends SET trend_composite = 0 WHERE trend_composite > 0 перед рестартом → backfill пересчитает всё.


v0.6.11 — 2026-02-20

fix: migrations 012/013 crash-loop — PRAGMA foreign_keys ignored inside transaction

Тип: bug Файлы: db/migrations/012_trends_class_name_nocase.py, db/migrations/013_drop_agg_columns.py, tests/unit/test_migrations.py Проблема: SQLite PRAGMA foreign_keys = OFF молча игнорируется внутри активной транзакции. Migrator оборачивает up() в BEGIN, миграция пытается отключить FK внутри — pragma не работает → DROP TABLE trends падает с FOREIGN KEY constraint failed → crash-loop (105 рестартов). Решение: Миграции теперь вызывают conn.commit() перед PRAGMA foreign_keys = OFF, выходя из транзакции мигратора. Добавлены 10 unit-тестов для миграций, включая тест-доказательство бага (test_pragma_fk_off_ignored_inside_transaction). Подводные камни: Любая миграция с table recreation (DROP + RENAME) ОБЯЗАНА вызывать conn.commit() перед PRAGMA foreign_keys = OFF. Это правило SQLite, не документированное явно.


v0.6.10 — 2026-02-20

fix: translation watchdog broken by asyncio.Lock event loop mismatch

Тип: bug Файлы: api/translation_service.py Проблема: На Python 3.13 asyncio.Lock привязывается к event loop при первом acquire. Singleton TranslationService создаёт lock на одном loop (web request), а watchdog пытается его использовать из другого — все batch-переводы падают с RuntimeError: Lock is bound to a different event loop. Watchdog логирует "translated 1298/1298" но реально ничего не сохраняется (ошибка ловится внутри try/except в _batch_translate_titles). Решение: _get_lock() теперь проверяет _loop атрибут lock-а и пересоздаёт его если текущий event loop отличается. Подводные камни: Python 3.13 изменил поведение asyncio.Lock_loop теперь устанавливается при первом acquire, а не при создании. Любой singleton с asyncio.Lock может сломаться при использовании из разных event loop контекстов.


v0.6.9 — 2026-02-20

Tech debt cleanup: COLLATE NOCASE, row_to_item, agg* removal, test infra

Тип: refactor Файлы: 26 файлов (+139/-189 строк) Что сделано:

  1. DB-1 (Migration 012): Пересоздание trends с class_name UNIQUE COLLATE NOCASE — исправлена корневая причина case-дубликатов. Убраны 5 workaround'ов COLLATE NOCASE из trend_classifier.py.
  2. BE-2: _row_to_item() переведён с позиционных индексов row[23] на именованный доступ row["object_class"] через sqlite3.Row. Любое изменение SIGNAL_COLS теперь безопасно.
  3. DB-3 (Migration 013): Удалены мёртвые agg_* колонки (agg_score/urgency/quality/velocity/recommendation) из: DB, scoring, classifier, routes, schemas, frontend types. LLM-промпты получают значения через вычисление из trend_*.
  4. Test infra: --timeout=30 -m "not external" в pytest.ini. pytestmark = pytest.mark.e2e добавлен в 10 e2e файлов. pytest tests/ больше не зависает.

v0.6.8 — 2026-02-20

Fix: trend card translations + analytics

Тип: fix + feature Файлы: src/workers/translation_watchdog.py, api/routes.py, frontend-cascade/app/index.html, tests/e2e/test_versioning_e2e.py, tests/unit/test_analysis_store.py

Изменения (v0.6.1 → v0.6.8):

  1. Перевод отчётов на английский (v0.6.2): отчёты генерируются на русском, но система считала их английскими. Теперь Phase 6 + watchdog определяют язык отчёта и переводят на ВСЕ другие языки.

  2. No LLM on READ path (v0.6.3): убран on-demand LLM вызов из get_report — только cache lookup. Переводы заполняются Phase 6 и watchdog.

  3. Перевод карточек трендов (v0.6.7): watchdog не переводил trends.class_name и description — только signal titles. Добавлен _fill_trend_class_gaps() для автоматического перевода трендов.

  4. Chunking для CLI (v0.6.8): 1808 трендов одним батчем → промпт 332K символов → [Errno 7] Argument list too long. Разбито на батчи по 30 штук.

  5. Analytics (v0.6.7): интеграция с Umami в index.html.

  6. Test fixtures (v0.6.4): добавлена таблица signal_mappings в тестовые фикстуры.

Подводные камни:

  • translate_trend_items() через CLI: промпт > 100K символов → Argument list too long. Всегда чанкить по 30 items.
  • Правило: No LLM calls on READ pathget_report и list_trend_classes используют только cached_only методы.
  • Watchdog переводит 3 типа контента: trend class_names, signal titles, analysis texts.

v0.6.0 — 2026-02-18

External API + PAT Authentication

Тип: feature Файлы: api/v1/ (new package), api/auth/pat_store.py, api/auth/pat_routes.py, db/migrations/008_personal_access_tokens.py, api/settings_store.py, api/main.py, frontend (types, api-client, pat-manager, i18n) Что добавлено:

  1. Personal Access Tokens (PAT) — создание, отзыв, просмотр через JWT-защищённые эндпоинты /auth/pat/
  2. External API v1 (/api/v1/) — 4 эндпоинта для агентов:
    • GET /api/v1/trends — пагинированный список трендов (sort: score/momentum/new/signal_count)
    • GET /api/v1/trends/top — Top-5 по 3 критериям (new, fast_growing, highest_scored)
    • GET /api/v1/trends/{id} — детали тренда с сигналами
    • GET /api/v1/signals — пагинированный список сигналов
  3. Rate limiting per PAT (sliding window, настраиваемый через settings)
  4. Feature toggleexternal_api_enabled в settings (по умолчанию выключен)
  5. Frontend PatManager — компонент для управления токенами на странице профиля
  6. 37 новых тестов — pat_store (19), pat_routes (4), v1_routes (14) Архитектура: Модуль api/v1/ отключаемый. Всегда смонтирован, но get_pat_user() проверяет external_api_enabled → 503 если false.

v0.5.1 — 2026-02-18

Mobile-first filters + Anthropic API retry + search fix

Тип: fix Файлы: explore.tsx, cli_client.py Проблема:

  1. Фильтры на Explore (Status, Category, Sort) не помещались на экран 375px — лейблы min-w-[5rem] отнимали 80px, сегменты не скроллились
  2. Anthropic API 500 (api_error, internal server error) не ретраился — клиент падал сразу
  3. Русский поиск возвращал 0 результатов из-за зомби-процессов на порту 8000

Решение:

  1. Mobile filters: лейблы hidden sm:block (скрыты на мобильных), строки flex-colsm:flex-row, сегменты/sort overflow-x-auto scrollbar-none
  2. Retry: добавлены "internal server error", "api_error", "server error" в RETRYABLE_ERRORS — до 3 попыток с exponential backoff
  3. Search: причина — зомби-процессы. Решение: taskkill //F //PID для всех процессов на порту + очистка __pycache__

Подводные камни:

  • На Windows при рестарте сервера ВСЕГДА убивать ВСЕ процессы на порту (netstat -ano | grep :8000taskkill каждого)
  • Mobile filters: лейблы видны только на sm: и выше. На мобильных контролы самодостаточны (иконки + текст кнопок)

v0.5.0 — 2026-02-18

LLM-based categorization + Radar list view + UI fixes

Тип: feature + fix Файлы: object_extractor.py, trend_classifier.py, radar.tsx, analyze.jobId.tsx, analyze.jobId.report.tsx, analyze.tsx, signal-view.tsx, trend-card.tsx, auth-store.ts, api-client.ts, routes.py, settings_routes.py, translation_service.py, use-trends.ts, ru.json, system.tsx, nodes.py, crawler.py Изменения:

  1. LLM categorization: категория тренда определяется LLM в промпте object extraction (вместо keyword-based _categorize_trend и source-based _infer_category). extract_batch() возвращает category, save_extractions() сохраняет в signals.category, classify_trends() использует majority vote. Добавлен backfill_categories() для пере-категоризации существующих сигналов.
  2. Radar list view: реализовано табличное представление (TrendRow компонент) — grid/table toggle теперь работает. Поиск на всю ширину, фильтры категорий на отдельной строке с count badge, все 5 категорий всегда видны.
  3. Analysis navigation: back-кнопки в report/progress ведут на /reports (список анализов). Completion redirect всегда на report page.
  4. Auth: token refresh hook, улучшенные error messages в api-client.
  5. Admin: settings routes, system admin page additions.
  6. Translation: улучшения в translation_service.

Подводные камни:

  • backfill_categories() — async, требует LLM-вызовы. Запускать вручную или через pipeline.
  • После backfill нужно вызвать classify_trends(conn) для пересчёта категорий трендов.

v0.4.10 — 2026-02-17

UI: 7 fixes — cards, translation, layout, redirect, saved, auth

Тип: feature + fix Файлы: trend-card.tsx, signal-view.tsx, radar.tsx, signals.tsx, explore.tsx, trend.trendId.tsx, analyze.jobId.tsx, saved.tsx, saved-store.ts, trend-score-panel.tsx, inline-analysis-report.tsx, api-client.ts, use-trends.ts, routes.py, utils.ts, en.json, ru.json Изменения:

  1. Radar: TrendCard горизонтальный layout (scores слева, текст справа), фиксированная высота h-[168px], пагинация PAGE_SIZE=12
  2. Explore: SignalView lg фиксированная высота h-[280px], linked trend block side-by-side с метриками
  3. Signals/Radar: formatClassName() для camelCase→Human, backend lang param на /trends и /trends/weak, кэшированный перевод class_name/description
  4. Trend detail: выравнивание высоты колонок (h-full на Stagger, TrendScorePanel, ObjectInfoPanel)
  5. Analysis redirect: navigate на /analyze/jobId после запуска, возврат на /trend/trendId после завершения
  6. Saved page: 3 секции (Тренды + Сигналы + Анализы), savedTrendClasses в store
  7. Auth guard: disable кнопки Analyze без авторизации, user-friendly error messages в api-client (JSON parse + HTTP status overrides)

v0.4.9 — 2026-02-17

Refactor: Style standardization, component decomposition, shared UI

Тип: refactor Файлы: progress-tracker.tsx, trend.$trendId.tsx, analyze.tsx, query.tsx, header.tsx, sidebar.tsx, profile.tsx, analyze.$jobId.tsx, query-form.tsx, query-result.tsx, explore.tsx, radar.tsx, saved.tsx, reports.tsx, alerts.tsx, trend-score-panel.tsx (new), linked-trends-section.tsx (new), inline-analysis-report.tsx (new), empty-state.tsx (new), sort-button-group.tsx (new)

Изменения:

  • P2 Стандартизация стилей: 40 inline style={{}} → Tailwind arbitrary values (text-[var(--x)], bg-[var(--x)]). progress-tracker.tsx лидер — 39→3 inline. Оставлены inline только для динамических значений (width%, color-mix, calc)
  • P2 Semantic tokens: Заменены hardcoded hex #f59e0bvar(--impact-high), #6366f1var(--brand-secondary) в trend-score-panel.tsx
  • P3 Декомпозиция: trend.$trendId.tsx (954→445 LOC) разбит на 3 компонента: TrendScorePanel (ScoreRing SVG + dimension bars), LinkedTrendsSection (sort + signal list), InlineAnalysisReport (SSE progress + versions + report tabs)
  • P3 Shared components: <EmptyState> (icon + title + description + children) заменил 3 copy-paste блока (saved, reports, alerts); <SortButtonGroup> (generic typed) заменил 2 дублированных sort pill-группы (explore, radar)

Подводные камни: EmptyState поддерживает children для кнопок (как в reports). SortButtonGroup — generic по типу ключа (<K extends string>), совместим с as const массивами.


v0.4.8 — 2026-02-17

Refactor: Frontend code quality — deduplication, dead code, CSS fixes

Тип: refactor Файлы: globals.css, api-client.ts, utils.ts, constants.ts, use-share.ts (new), use-quick-analysis.ts, analyze.tsx, trend.$trendId.tsx, signal.$signalId.tsx, analyze.$jobId.report.tsx, trend-card.tsx, saved.tsx, signal_mapper.py, analysis.ts

Изменения:

  • P0 Баги: Определены CSS-переменные --text-muted (307 использований) и --border-primary (45); передача locale в analyzeTrend() для генерации отчётов на выбранном языке; удалён debug console.log из api-client
  • P1 Дедупликация: formatRelativeTime()lib/utils.ts (из 2 файлов); RECOMMENDATION_COLORSlib/constants.ts (из 2 файлов); handleShare()hooks/use-share.ts (из 3 файлов)
  • Удалён dead code: interactive-graph.tsx, streaming-text.tsx, report-actions.tsx, signal-card.tsx, use-report-actions.ts, AnalysisJob interface, generate_source_urls() (backend)
  • UI: Исправлен постоянный скроллбар (overflow-y: scrollauto)
  • Saved page: SignalCard заменён на SignalView variant="lg"

Подводные камни: signal-card.tsx использовался в saved.tsx — заменён на SignalView перед удалением.


v0.4.7 — 2026-02-17

Fix: LLM usage tracking — cost estimation + caller propagation

Тип: fix Файлы: src/llm/cli_client.py, src/agents/nodes.py, db/llm_usage_store.py

Проблема: Таблица llm_usage записывала cost_usd = 0.0 (CLI с подпиской не возвращает стоимость) и caller = "unknown" (контекстная переменная не пропагировалась через _run_async → новый поток → asyncio.run).

Решение:

  1. Добавлена оценка стоимости по прайсу модели (_estimate_cost()) — sonnet: $3/$15 per 1M tokens, haiku: $0.80/$4, opus: $15/$75. Используется как fallback если CLI не вернул cost_usd.
  2. _run_async() теперь копирует contextvars через contextvars.copy_context()ctx.run(asyncio.run, coro), что обеспечивает проброс set_caller() через границу потоков.

Подводные камни:

  • Оценка токенов грубая (4 символа ≈ 1 токен), погрешность ±25%
  • Прайс зашит в код — при изменении цен Anthropic нужно обновить _MODEL_PRICING
  • set_caller() должен вызываться ДО await cli.query() в том же async-контексте

v0.4.6 — 2026-02-17

Feature: Shared report components + full tabbed report on trend page

Тип: feature + refactor Файлы: report-tabs.tsx, report-action-bar.tsx, trend.$trendId.tsx, analyze.$jobId.report.tsx

Проблема: Страница тренда показывала куцый inline-отчёт с collapsible секциями, тогда как страница отчёта имела полноценный UI с 5 табами.

Решение:

  1. Извлечены 2 shared компонента: ReportTabs (5 табов) и ReportActionBar (deepen/expand/re-analyze + слоты)
  2. Report page и Trend page используют одни компоненты — нет дублирования кода
  3. Slot pattern: versionNav (pills на trend, chevrons на report) и compareButton (только report)
  4. startInlineAnalysis принимает { depth, timeHorizon, parentJobId } для deepen/expand inline

Подводные камни:

  • ReportActionBar использует DEPTH_STEPS и HORIZON_STEPS — изменять только там
  • При добавлении нового таба — обновить ReportTabId type и массив tabs в report-tabs.tsx

v0.4.2 — 2026-02-17

Тип: fix Файлы: frontend-cascade/app/src/routes/_dashboard/trend.$trendId.tsx, src/workers/auto_analyzer.py

Проблема: Анализы, запущенные со страницы тренда, не привязывались к тренду и не отображались в AnalysisTimeline. На продакшене 24 из 25 анализов имели trend_class_id = NULL.

Причина: Две точки входа не передавали trend_class_id:

  1. trend.$trendId.tsx:328 — кнопка "Analyze Best" передавала trend_class_id: undefined вместо String(data.id)
  2. auto_analyzer.py:100reserve_analysis() вызывался без trend_class_id, хотя значение было доступно

Решение:

  1. Передаём trend_class_id: String(data.id) при навигации на /analyze со страницы тренда
  2. Передаём trend_class_id=trend_class_id в reserve_analysis() в auto_analyzer
  3. Backfill на продакшене: обновлён 1 анализ через JOIN signal_mappings → trends

Подводные камни: AnalysisTimeline ищет анализы по trend_class_id (integer), а не по trend_id (string). Любая точка входа анализа из контекста тренда ОБЯЗАНА передавать trend_class_id.


v0.4.1 — 2026-02-17

Cleanup: Remove unused streaming props from report page

Тип: refactor Файлы: frontend-cascade/app/src/components/analysis/summary-view.tsx, frontend-cascade/app/src/routes/_dashboard/analyze.$jobId.report.tsx

Проблема: SummaryView принимал неиспользуемые пропсы isStreaming и streamingComponent. Report page импортировал StreamingText и useAnalysisStore, которые больше не нужны.

Решение: Удалены мёртвые пропсы из SummaryView, убраны неиспользуемые импорты (StreamingText, useAnalysisStore) и состояния (animated, isFirstView) из report page.


v0.4.0 — 2026-02-17

Trend-Object Scoring System (3-Dimension Model)

Тип: feature Ветка: refactor/signal-trend-model

Проблема: Тренды стали агрегированными объектами (например "React", "ChatGPT"), а не отдельными сигналами. Старая система оценки усредняла signal-level scores, что не учитывало кросс-источниковую корробрацию, плотность данных, темпоральную динамику и фазу жизненного цикла.

Решение: Новая 3-мерная модель оценки, спроектированная на основе исследований проекта (research/scoring-research/, research/expert-review-2026-02/):

3 измерения (0-100 каждое):

  1. Significance = EvidenceStrength(40%) + SourceDiversity(30%) + SignalDensity(30%)

    • EvidenceStrength: recency-weighted avg signal scores, sigmoid-mapped
    • SourceDiversity: log2(unique_sources+1) / log2(6) — 1 source=43, 5 sources=100
    • SignalDensity: count / (count + baseline) — baseline по категориям (tech=10, science=5)
  2. Momentum = ArrivalRate(40%) + ScoreTrajectory(35%) + Freshness(25%)

    • ArrivalRate: ratio recent vs older signals (window по категориям: social=24h, tech=72h, science=168h)
    • ScoreTrajectory: avg score newer vs older signals
    • Freshness: exponential decay (half-life по категориям)
  3. Confidence = Coverage(30%) + SampleSize(25%) + Agreement(25%) + Consistency(20%)

    • Coverage: unique_sources / 5
    • SampleSize: 1 - exp(-n/10)
    • Agreement: CV-based per-source mean scores (single source penalty = 0.4)
    • Consistency: 1 - CV всех signal scores

Composite Score (0-100): Significance(50%) + Momentum(25%) + Confidence(25%)

5-стадийный жизненный цикл:

Backend Phase UI Badge Детекция
introduction emerging age < young_threshold AND n <= 3
rise emerging arrival_accel > 0.3 AND age < mature_threshold
peak mature stable arrival rate
decline fading arrival_accel < -0.3
obsolescence fading declining + old + n > 5

5-уровневая матрица рекомендаций: ACT_NOW, RISKY_HYPE (новый), MONITOR, EVERGREEN, IGNORE. Override: phase=OBSOLESCENCE → всегда IGNORE.

Файлы:

  • src/services/trend_object_scoring.pyНОВЫЙ: вся логика скоринга (370 LOC)
  • src/services/trend_scoring.py — делегирует в новый модуль, пишет оба формата (legacy agg_* + новые trend_*)
  • src/services/trend_classifier.py — SELECT запросы включают 7 новых колонок
  • api/analysis_store.py — миграция (7 новых колонок на trends), backfill _backfill_trend_scores()
  • api/schemas.pyTrendSummary + 7 новых полей
  • api/routes.py_dict_to_trend_summary() helper, дедупликация 3 конструкторов
  • frontend-cascade/app/src/types/trend.tsTrendPhase, TrendPhaseUI, RISKY_HYPE в Recommendation, 7 полей в Trend
  • frontend-cascade/app/src/routes/_dashboard/trend.$trendId.tsx — 3-dimension bars, composite score, phase + recommendation badges
  • frontend-cascade/app/src/components/trends/trend-card.tsx — composite score + phase badge
  • frontend-cascade/app/src/i18n/en.json, ru.json — 14 новых ключей trend.*
  • tests/unit/test_trend_object_scoring.pyНОВЫЙ: 29 unit tests

Подводные камни:

  • Legacy agg_* колонки продолжают записываться через backward-compat properties на TrendObjectScore
  • Backfill запускается при старте сервера (init_trends_table()_backfill_trend_scores())
  • Тренды без signal_mappings (orphans) получают score=0
  • Frontend: все trend_* поля защищены ?? 0 от undefined
  • Автопереоценка при добавлении сигналов: crawl_once()_run_classify_pipeline()classify_trends()update_trend_scores()

UI: Menu highlighting + accent color fix

Тип: fix Файлы: frontend-cascade/app/src/globals.css, frontend-cascade/app/src/components/layout/header.tsx

Проблема: В dark mode --popover и --accent были одинаковыми (#1F1F23) — hover на пунктах dropdown menu не виден. Logout item терял красный цвет при hover (отсутствовал hover:text-red-400).

Решение:

  • Dark: --accent: #27272A (было #1F1F23) — видимый hover на фоне popover
  • Light: --accent: #E4E4E7 (было #F4F4F5) — более заметный hover на белом фоне
  • Logout item: добавлен hover:text-red-400

2026-02-16

Replace Tavily with SearXNG

Тип: refactor Ветка: refactor/signal-trend-model

Проблема: Tavily — платный облачный поиск (API-ключ, 1-2 кредита/запрос). SearXNG — self-hosted мета-поиск (246+ движков, бесплатно). SearXNG задеплоен на проде (Docker, порт 8890) и локально (8888).

Решение: Полная замена Tavily на SearXNG во всей кодовой базе за 9 фаз:

  1. SearXNG адаптер (src/services/adapters/searxng.py): добавлены fetch_news(), count_citations(), _is_aggregator(), _clean_url()
  2. Config (src/config.py): SearXNGConfig.enabled=True, min_citation_threshold=5, WEB_CITATION_BASELINES (alias для TAVILY_CITATION_BASELINES), TavilyConfig.enabled=False
  3. Scoring (src/services/scoring.py): tavily_citationsweb_citations в CATEGORY_WEIGHTS (все 5 категорий), dual-read для обратной совместимости
  4. Crawler (src/workers/crawler.py): tavilysearxng в fetch_news/enrichment, импорт searxng вместо search/metasearch
  5. API routes (api/routes.py): searxng: и tavily:"web" ключ в sources dict
  6. Frontend: tavilyweb/searxng в types, i18n, компонентах, report page
  7. Удалены: search.py, tavily.py, metasearch.py, tavily-python из requirements.txt
  8. Тесты: test_tavily_citations_e2e.pytest_web_citations_e2e.py, все assertions обновлены
  9. Доп. файлы: source_extractor.py, trend_classifier.py, trend_enrichment.py, schemas.py

Подводные камни:

  • Старые данные в БД имеют tavily_citations в metadata — dual-read (web_citations || tavily_citations) в scoring и enrichment
  • Старые ID tavily:hash в БД — backward compat в routes (tavily:"web" ключ)
  • TAVILY_CITATION_BASELINES оставлен как alias для WEB_CITATION_BASELINES
  • TavilyConfig оставлен с enabled=False для совместимости с .env файлами
  • trend_enrichment.py: tavily_adapter kwarg оставлен как deprecated alias

2026-02-15

Domain Model Refactoring: Signal/Trend (Phases 1-6)

Тип: refactor Ветка: refactor/signal-trend-model

Проблема: Доменная модель смешивала понятия — TrendItem обозначал и отдельный сигнал, и "тренд". TrendClass (объект) — агрегированная сущность, но UI представлял её как вторичную ("Objects").

Решение: Переименование на всех уровнях: TrendItem → Signal, TrendClass → Trend.

Фазы:

  1. DB миграция (db/migrations/005_signal_trend_rename.py): trending_itemssignals, trend_classestrends, trend_objectssignal_mappings, class_aliasestrend_aliases, trend_snapshotssignal_snapshots
  2. Backend скоринг (src/services/trend_scoring.py): TrendAggregateScorer — агрегированный скоринг трендов. Авто-анализ при переходе SIGNAL→TREND
  3. API эндпоинты: /signals, /trends, /analyses (бывшие /trends, /trends/objects, /trends/analyze)
  4. Frontend типы: TrendItemSignal, TrendClassTrend, API client + hooks переименованы
  5. Frontend компоненты: TrendViewSignalView, TrendCardSignalCard, TrendClassCardTrendCard, TrendTableSignalTable
  6. Frontend маршруты: /trend/$trendId/signal/$signalId, /objects/$classId/trend/$trendId, /objects/signals. Redirect-маршруты для обратной совместимости

Подводные камни:

  • Backward compat aliases сохранены (типы, компоненты, API client) — удалять в отдельном PR
  • Redirect-маршруты /objects/signals и /objects/$classId/trend/$trendId для внешних ссылок
  • nav.signals — новый i18n ключ (en: "Signals", ru: "Сигналы")
  • При rename route файлов порядок критичен: сначала trend.$trendIdsignal.$signalId, потом objects.$classIdtrend.$trendId

2026-02-11

Object-as-Trend Pipeline (Phase 0-4)

Тип: feature Файлы:

  • api/analysis_store.py — новые функции: init_trend_objects_table(), init_trend_classes_table(), init_class_aliases_table()
  • api/main.py — регистрация новых таблиц при старте
  • api/routes.py — эндпоинты: GET /trends/objects, GET /trends/objects/{class_id}, GET /trends/signals
  • api/schemas.py — Pydantic-модели: TrendClassSummary, TrendClassDetail, TrendClassListResponse
  • src/services/object_extractor.pyНОВЫЙ: LLM batch extraction объектов из элементов (model=haiku)
  • src/services/trend_classifier.pyНОВЫЙ: группировка по классам, статус TREND/SIGNAL/TRASH
  • src/services/alias_resolver.pyНОВЫЙ: алиасы, suggest_merges (SequenceMatcher), merge_classes
  • src/workers/crawler.py — миграция колонок object_class, object_status; метод _extract_and_classify()
  • frontend-cascade/app/src/types/trend.ts — типы TrendClass, TrendClassDetail, TrendClassListResponse
  • frontend-cascade/app/src/lib/api-client.ts — методы getTrendObjects(), getTrendObject(), getSignals()
  • frontend-cascade/app/src/hooks/use-trend-objects.tsНОВЫЙ: react-query хуки
  • frontend-cascade/app/src/components/trends/trend-class-card.tsxНОВЫЙ: карточка класса
  • frontend-cascade/app/src/routes/_dashboard/radar.tsx — табы: All Trends / Grouped Objects / Weak Signals
  • frontend-cascade/app/src/i18n/en.json, ru.json — i18n ключи для Object-as-Trend

Проблема: Тренды обрабатывались изолированно; элементы об одном и том же концепте не группировались.

Решение: LLM-based Object Extraction → Group Classification → API + Frontend. Элементы группируются по базовому объекту (class), статус определяется кол-вом элементов и кросс-источниками.

Подводные камни:

  • element_id в trend_objects имеет UNIQUE constraint; INSERT с дубликатом → UPDATE fallback
  • Эндпоинты /objects, /signals определены ДО /{job_id} в routes.py (порядок критичен)
  • _extract_and_classify() запускается как background task (asyncio.create_task) после crawl_once()

[P1-4] Синхронизация scoring weights и рефакторинг

Тип: refactor Файлы:

  • src/config.py — перенесён CATEGORY_WEIGHTS, добавлена валидация _validate_scoring_config()
  • src/services/scoring.py — удалён дублирующий CATEGORY_WEIGHTS, извлечены _extract_engagement() и _clamp_score()

Проблема:

  1. CATEGORY_WEIGHTS дублировались в scoring.py и не синхронизировались с TAVILY_CITATION_BASELINES из config.py
  2. Extraction engagement метрик дублировался в 3 местах (calculate_weighted_engagement, calculate_velocity_from_snapshots, calculate_quality)
  3. Нет валидации score ranges и консистентности категорий

Решение:

  1. Перенесён CATEGORY_WEIGHTS в src/config.py (single source of truth рядом с TAVILY_CITATION_BASELINES)
  2. Добавлена валидация _validate_scoring_config() на уровне модуля:
    • Проверка идентичности ключей категорий (CATEGORY_WEIGHTS ↔️ TAVILY_CITATION_BASELINES)
    • Проверка суммы весов (должна быть 1.0 ± 0.01)
  3. Извлечена общая функция _extract_engagement(item: TrendItem) -> dict[str, float]:
    • Возвращает {"saves", "shares", "comments", "likes", "total_weighted"}
    • Использует ENGAGEMENT_WEIGHTS для взвешенного подсчёта
    • Заменяет 3 дублирующих блока кода
  4. Добавлена функция _clamp_score(value, min_val=0.0, max_val=1.0) для нормализации финальных scores
  5. Обновлены методы:
    • calculate_weighted_engagement() — теперь вызывает _extract_engagement()
    • calculate_velocity_from_snapshots() — inner function get_engagement() использует _extract_engagement()
    • calculate_quality() — использует _extract_engagement() вместо inline extraction
    • calculate_urgency(), calculate_time_decay() — используют _clamp_score() вместо inline max(min(...))

Тесты:

  • 35/35 tests passed в tests/e2e/test_scoring_e2e.py
  • Валидация config проходит при импорте модуля

Подводные камни:

  • _extract_engagement() возвращает dict, не одно число — вызывающий код должен извлечь нужный ключ
  • Категории в CATEGORY_WEIGHTS и TAVILY_CITATION_BASELINES теперь валидируются автоматически при импорте — любая рассинхронизация вызовет ValueError на старте

Аудит технического долга и план рефакторинга

Тип: architecture / tech debt Файлы:

  • docs/TECH-DEBT-REFACTORING-PLAN.md — НОВЫЙ: план рефакторинга (22 задачи)

Проблема: Накопленный технический долг по всей системе: 56 issues (backend, pipeline, frontend, инфраструктура).

Решение:

  1. Проведён полный аудит 4 командами: backend API, pipeline (crawler/agents/services), frontend (React), инфраструктура (deps/tests/security)
  2. Составлен план из 22 задач с приоритизацией P0/P1/P2, графом зависимостей и понедельным расписанием

Ключевые находки:

  • P0 (8 задач): Race conditions в routes.py, hardcoded DB paths в 11+ местах, CORS allow_methods=["*"], circuit breaker race, reserve_analysis() rollback bug, 17 JSON-дампов в корне
  • P1 (11 задач): routes.py монолит (200+ строк _run_analysis), crawler.py монолит (932 строки), nodes.py без timeout для LLM, scoring CATEGORY_WEIGHTS не синхронизированы, нет миграций SQLite
  • P2 (5 задач): Нет Dockerfile/CI, 5 падающих тестов, нет frontend тестов, нет rate limiting

Подводные камни:

  • Критический путь: P0-1 (DB layer) → P0-3 → P1-1 → P1-7 → P2-3
  • P0-1 (единый DB-слой) — фундамент для 5 других задач

Object-as-Trend: новый пайплайн наименования трендов

Тип: research / architecture decision Файлы:

  • docs/scoring-evaluation-report.md — НОВЫЙ: оценка scoring-системы (8 проблем, 3.2/10)
  • docs/naming-algorithm-evaluation.md — НОВЫЙ: оценка алгоритма наименования v1 (75% pass)
  • docs/naming-algorithm-v2-evaluation.md — НОВЫЙ: оценка v2 с Step 0 фильтром (30% pass, строже)
  • docs/object-based-trend-grouping.md — НОВЫЙ: проверка object-as-trend гипотезы (подтверждена)

Проблема:

  1. Scoring-система работает только для HN tech-трендов (3.2/10): Tavily score passthrough, arXiv пустые метаданные, категория "society"/"social" mismatch
  2. Алгоритм наименования v1 генерировал красивые названия для не-трендов (25% элементов — не тренды)
  3. v2 с поштучным Step 0 фильтром отбрасывал 55% элементов, включая валидные части трендов (продуктовые релизы Claude/GPT/Voxtral — каждый по отдельности не тренд, но вместе = "специализация frontier LLM")

Решение (Object-as-Trend подход):

  1. Object extraction — для каждого элемента извлекаются конкретные сущности (продукты, технологии, компании)
  2. Group by class — объекты группируются в классы (LLM-роутинг, code review, frontier LLM)
  3. Class-level is_trend — тренд определяется на уровне класса (count >= 2), а не отдельного элемента
  4. TRASH фильтр — элементы без извлекаемых объектов (агрегаторы, opinion pieces) отбрасываются

Результаты на 50 элементах:

  • v2 поштучно: 55% мусор, 5-6 трендов
  • Object-as-Trend: 30% мусор, 7 трендов из 28 элементов, 60% recovery отброшенных
  • 3 кросс-источниковых тренда (GitHub + arXiv, GitHub + HN)

Правильный пайплайн:

Element → Object extraction → Group by class → class.count >= 2 → IS_TREND
                                              → class.count == 1 → WEAK_SIGNAL
          no object found  → TRASH (discard)

Подводные камни:

  • Object extraction требует LLM (batch, async) — стоимость ~$0.01/элемент
  • Alias table нужна для нормализации ("vouch" HN = "vouch" GitHub)
  • SBERT embeddings для fuzzy class matching — Phase 2
  • Step 0 должен работать на уровне КЛАССА, не элемента

Следующие шаги:

  • Имплементация object extraction pipeline
  • Alias table для entity normalization
  • Group-level trend detection
  • Dual-layer naming для подтверждённых трендов

Title Quality Evaluation System + Admin System Page

Тип: feature Файлы:

  • src/services/title_evaluator.py — НОВЫЙ: evaluation engine (fixtures → generate → LLM judge → save → compare)
  • api/eval_routes.py — НОВЫЙ: 4 API endpoints /admin/eval-titles/* (start, status, history, compare)
  • api/main.py — регистрация eval_router
  • research/title-eval/fixtures.json — НОВЫЙ: 15 курированных тестовых примеров
  • scripts/eval_titles.py — НОВЫЙ: CLI runner с --limit, --compare, --save-baseline
  • tests/e2e/test_title_eval.py — НОВЫЙ: 12 offline + 3 E2E тестов
  • frontend-cascade/app/src/routes/admin/system.tsx — НОВЫЙ: страница /admin/system
  • frontend-cascade/app/src/components/layout/admin-sidebar.tsx — добавлен nav item "System"
  • frontend-cascade/app/src/lib/api-client.ts — 4 новых метода (startTitleEval, getEvalStatus, getEvalHistory, compareEvalRuns)
  • frontend-cascade/app/src/i18n/en.json — 18 новых ключей admin.*
  • frontend-cascade/app/src/i18n/ru.json — русские переводы
  • docs/TRENDS-AND-TITLES.md — НОВЫЙ: документация по трендам и заголовкам
  • docs/EVAL-SYSTEM.md — НОВЫЙ: документация eval-системы
  • docs/INDEX.md — обновлён до v11.0

Проблема:

  1. Нет способа измерить качество LLM-генерации заголовков
  2. При изменении промптов невозможно обнаружить регрессию
  3. Нет административного интерфейса для системных операций

Решение:

  1. Eval Pipeline: 15 fixtures → генерация → LLM-as-Judge (4 критерия × 3 варианта) → JSON результат
  2. Regression Detection: сравнение с baseline, порог -0.2 для критической регрессии
  3. Admin UI (/admin/system): запуск eval, прогресс-бар, карточки метрик, история прогонов
  4. CLI: scripts/eval_titles.py для CI и ручного запуска

Подводные камни:

  • Eval jobs хранятся в памяти (_eval_jobs) — не переживают перезапуск сервера
  • Judge может давать нестабильные оценки — рекомендуется запускать несколько прогонов
  • call_asyncquery баг исправлен в title_generator.py (метод не существовал в ClaudeCLIClient)

Fix: call_async → query in title_generator.py

Тип: fix Файлы: src/services/title_generator.py Проблема: Вызов cli.call_async(prompt) на строках 93, 128 — метод не существует в ClaudeCLIClient. Правильный метод: cli.query(prompt). Решение: Заменено call_asyncquery в обоих местах.

LLM Title Generation + Cross-Source Trend Linking

Тип: feature Файлы:

  • src/workers/crawler.py — normalize_title(), find_similar_trend(), add_source(), update_trend_from_source(), trend_sources таблица, миграция title variants колонок, интеграция _generate_missing_titles()
  • src/services/title_generator.py — НОВЫЙ: LLM-генерация 3 вариантов заголовков (technical, accessible, benefit) через Claude CLI
  • api/routes.py — _get_trend_sources_from_db(), обновлён _build_sources_dict() (trend_sources → fallback), _crawler_item_to_schema() с title variants
  • api/schemas.py — TrendSource модель, расширен TrendItem (source_list, title_technical, title_accessible, title_benefit)
  • frontend-cascade/app/src/types/trend.ts — TrendSource интерфейс, расширен TrendItem
  • frontend-cascade/app/src/components/trends/trend-view.tsx — displayTitle(), technicalTitle(), все 3 варианта (sm/lg/row) показывают accessible title с tooltip для technical

Проблема:

  1. Заголовки трендов идут raw от адаптеров без обработки ("Show HN: My Cool Project " → с мусором)
  2. Один тренд из разных источников (HN, GitHub, Tavily) создаёт дубликаты — нет связывания
  3. Нет хранилища для нескольких источников одного тренда (sources реконструировались на лету из item.id)
  4. Экспертная панель (6/6) назвала формулировки трендов главным UX-барьером

Решение:

  1. Title Normalization (normalize_title()): HTML unescape, Unicode NFKC, удаление HN-префиксов (Show HN/Ask HN/...), коллапс пробелов
  2. Cross-Source Dedup (find_similar_trend()): fuzzy match по заголовку через difflib.SequenceMatcher (threshold 0.75). Точное совпадение → быстрый путь, иначе сравнение со всеми трендами
  3. trend_sources таблица: хранит множественные источники для одного тренда с UNIQUE(trend_id, source_item_id). Миграция из существующих данных
  4. Dual-Layer Naming: 3 колонки в trending_items (title_technical, title_accessible, title_benefit). LLM генерирует batch по 10 трендов через ClaudeCLIClient
  5. Skip-логика: WHERE title_accessible IS NULL — повторная генерация не запускается
  6. Frontend: title_accessible ?? title по умолчанию, tooltip с title_technical при различии, badge с количеством источников в lg-варианте

Подводные камни:

  • Fuzzy match загружает все тренды в память — при >1000 трендов рассмотреть индексирование или кэш
  • LLM title generation — async, вызывается после crawl цикла; при недоступности LLM — fallback на raw title
  • _row_to_item() — title variants передаются через metadata dict (ключи _title_technical, _title_accessible, _title_benefit) — индексы 20-22
  • _build_sources_dict() — сначала пытается trend_sources таблицу, fallback на legacy ID-based реконструкцию
  • Порог fuzzy match (0.75) может потребовать тюнинга на реальных данных

Следующие шаги:

  • Тюнинг порога fuzzy match на реальных данных
  • Оптимизация для >1000 трендов (n-gram индекс или embedding similarity)
  • i18n для title variants (генерация на языке пользователя)

Zones TOC View component (Issue #7, Phase 3)

Тип: feature Файлы:

  • frontend-cascade/app/src/components/analysis/zones-toc-view.tsx — НОВЫЙ компонент
  • frontend-cascade/app/src/routes/_dashboard/analyze.$jobId.report.tsx — замена ImpactZoneCard grid → ZonesTOCView
  • frontend-cascade/app/src/i18n/en.json — добавлены ключи: zones.tableOfContents, zones.noZones
  • frontend-cascade/app/src/i18n/ru.json — добавлены русские переводы

Проблема:

  1. Zones таб показывал карточки в сетке — мелкий текст, плохая читаемость
  2. Нет навигации по зонам при большом количестве (>10)
  3. Нет иерархии: зоны → подзоны → эффекты
  4. На мобильных экранах карточки слишком компактные

Решение:

  1. TOC Layout:
    • Desktop: sticky sidebar (240px) + main content (flex-1)
    • Mobile: collapsible dropdown TOC вверху страницы
    • Auto-scroll to active zone with IntersectionObserver
    • Smooth scroll behavior при клике на TOC link
  2. Типография (крупнее для читаемости):
    • Zone header: text-2xl font-bold (24px)
    • Description: text-base leading-relaxed (16px, lh 1.75)
    • Sub-zones: text-sm (14px) с чекбоксом-иконкой
    • Evidence: text-sm leading-relaxed с Quote icon
    • Cascade effects: collapsible с GitBranch icon
  3. Иерархия:
    • Zone header (название, тип, временной горизонт, impact/confidence)
    • Description (параграф)
    • Mechanism (отдельная секция)
    • Sub-zones (сетка 1-2 колонки)
    • Affected Groups (теги)
    • Evidence (список с Quote icons)
    • Cascade Effects (collapsible tree, рекурсивный CascadeTreeItem)
  4. Адаптивность:
    • Desktop: TOC слева + контент справа
    • Mobile: TOC dropdown + контент на всю ширину
    • Карточки зон с border-radius, padding, shadows

Подводные камни:

  • IntersectionObserver может неправильно работать при быстром скролле — использован rootMargin для компенсации
  • TOC sticky может конфликтовать с fixed header — добавлен top-4 offset
  • На очень длинных списках зон (>20) TOC может быть слишком длинным — рассмотреть группировку по типам
  • Cascade tree может быть глубоким (depth >5) — авто-раскрытие только первых 2 уровней

Следующие шаги:

  • Тестирование с реальными отчётами (10+ зон)
  • Возможна группировка зон по типам в TOC (Positive/Negative/Mixed секции)

SummaryView and TrendToZonesFlow components (Issue #5, Phase 3)

Тип: feature Файлы:

  • frontend-cascade/app/src/components/analysis/summary-view.tsx — НОВЫЙ компонент
  • frontend-cascade/app/src/components/analysis/trend-to-zones-flow.tsx — НОВЫЙ компонент
  • frontend-cascade/app/src/routes/_dashboard/analyze.$jobId.report.tsx — интеграция SummaryView
  • frontend-cascade/app/src/i18n/en.json — добавлены ключи: report.trend, report.impactOn, report.trendOverview, report.impactMapping, report.detailedAnalysis, zones.positive, zones.negative, zones.mixed, zones.impact, zones.impactTooltip, zones.confidence, zones.confidenceTooltip, zones.mechanism, zones.cascadeEffects, zones.maxDepth
  • frontend-cascade/app/src/i18n/ru.json — добавлены русские переводы

Проблема:

  1. Summary таб показывал только плоский markdown без структуры
  2. Нет визуальной связи между трендом и зонами влияния
  3. Отсутствует явный обзор тренда перед детальным анализом
  4. Пользователю сложно понять, как тренд влияет на различные области

Решение:

  1. SummaryView компонент — трёхсекционная структура:
    • Trend Overview: Карточка с описанием тренда, категорией, статусом
    • Impact Mapping: Визуализация TrendToZonesFlow — как тренд влияет на зоны
    • Detailed Analysis: Markdown отчёт с источниками (прежний ReportViewer)
  2. TrendToZonesFlow компонент:
    • Центральная карточка тренда с рамкой brand-primary
    • Стрелки вниз с подписью "Impact on"
    • Группировка зон по типу (Positive/Negative/Mixed) с цветовой кодировкой
    • Карточки зон с:
      • Иконкой типа (TrendingUp/Down/Minus)
      • Impact и Confidence с тултипами (шкала 0-1)
      • Механизмом влияния
      • Cascade preview (total_effects, max_depth) если есть
    • Адаптивная сетка: 1 колонка на mobile, 2 на desktop
  3. Интеграция:
    • Заменён старый простой markdown в summary tab на SummaryView
    • Поддержка streaming через isStreaming prop
    • Сохранена анимация StreamingText для первого просмотра
  4. i18n: Добавлены все необходимые переводы (en/ru)

Подводные камни:

  • Карточки зон используют цветовую кодировку border+background, нужно протестировать в dark mode
  • Tooltips для Impact/Confidence могут перекрываться на узких экранах (но работают корректно благодаря Radix UI)
  • Если у тренда нет description, секция Trend Overview не отображается — это OK для standalone анализов

Следующие шаги:

  • Тестирование в реальных отчётах с различными типами зон
  • Можно добавить collapsible секции для длинных списков зон (>10)

React Flow cascade graph (Issue #6, Phase 1)

Тип: feature Файлы:

  • frontend-cascade/app/package.json — добавлен reactflow@11.x
  • frontend-cascade/app/src/components/analysis/cascade-graph.tsx — НОВЫЙ компонент
  • frontend-cascade/app/src/routes/_dashboard/analyze.$jobId.report.tsx — замена InteractiveGraph → CascadeGraph

Проблема:

  1. Старый граф (react-force-graph-2d) перемещался под мышкой
  2. Нет весов на рёбрах
  3. Нет направленности (стрелок)
  4. Сложная навигация

Решение:

  1. React Flow интеграция:
    • Установлен пакет reactflow (37 deps, 0 vulnerabilities)
    • Создан компонент CascadeGraph с TypeScript типизацией
  2. Функционал:
    • Направленные рёбра (MarkerType.ArrowClosed)
    • Веса на рёбрах (% strength labels)
    • Анимация для сильных связей (strength > 0.7)
    • Цветовая кодировка узлов (positive=green, negative=red, mixed=amber)
    • Impact display на каждом узле
    • MiniMap для навигации
    • Controls (zoom, fit view)
    • Background grid
  3. Layout: Simple grid layout (5 columns), можно улучшить через dagre
  4. Интеграция: Заменён InteractiveGraph в cascade tab

Подводные камни:

  • Layout пока простой (grid) — для иерархического нужен dagre/elkjs
  • Drill-down и breadcrumb navigation не реализованы (TODO)
  • Node details sidebar не добавлен (TODO)
  • Mobile UX: graph скрыт на малых экранах (fallback: ImpactPathList)

Следующие шаги:

  • Добавить hierarchical layout (dagre)
  • Реализовать drill-down navigation
  • Добавить node details panel
  • Реализовать level navigation (up/down)

Zone matching integration + Locale parameter + Zones storage (Issues #5-7, High Priority)

Тип: feature Файлы:

  • src/agents/nodes.py — _canonicalize_zones(), ZoneMatcher integration
  • api/routes.py — locale parameter in AnalyzeRequest, _run_analysis()
  • api/analysis_store.py — locale column migration, init_zones_storage_tables()
  • api/main.py — table initialization

Проблема:

  1. Зоны не канонизировались — каждый анализ создавал свои названия
  2. Нет поддержки локали — все отчёты генерировались на EN
  3. Зоны и каскадные эффекты не сохранялись в БД для обратного поиска

Решение:

  1. Zone canonicalization (Issue #5, Phase 2):
    • _canonicalize_zones() в impact_researcher_agent
    • Автоматический маппинг raw zone names → canonical names через ZoneMatcher
    • Сохранение original_name для reference
  2. Locale parameter (Issue #6-7):
    • Добавлено поле locale в AnalyzeRequest (default="en")
    • Передача locale через state в LangGraph pipeline
    • report_generator_agent добавляет language instruction в промпт
    • Колонка locale в таблице analyses (migration)
  3. Zones storage tables (Issue #7):
    • Таблица impact_zones: job_id, zone_name, impact, confidence, mechanism, evidence, etc.
    • Таблица cascade_effects: zone_id, effect_name, level, strength, causal_chain, is_feedback
    • Indexes для быстрого поиска по job_id, zone_name, effect_name
    • Foreign keys с CASCADE DELETE для data integrity

Подводные камни:

  • Canonicalization требует embeddings (пока TF-IDF placeholder)
  • Locale instruction в промпте не гарантирует 100% соблюдения языка LLM
  • Таблицы zones/effects созданы, но сохранение данных нужно добавить в complete_analysis()
  • Migration: locale column добавляется к существующим БД с default='en'

Impact zones dictionary (Issue #5, Phase 1)

Тип: feature Файлы:

  • api/analysis_store.py — init_zones_dictionary_table()
  • src/services/zone_matcher.py — НОВЫЙ модуль для semantic matching
  • api/routes.py — API endpoints (/zones/dictionary, /zones/search)
  • api/main.py — инициализация таблицы при старте

Проблема:

  1. Зоны влияния переписывались при каждом анализе (нет канонических названий)
  2. Похожие зоны (synonyms) создавались как отдельные записи
  3. Нет справочника для consistency across analyses
  4. Невозможен поиск/фильтрация по зонам

Решение:

  1. Таблица БД: impact_zones_dictionary с полями:
    • canonical_name (уникальное каноническое название)
    • synonyms (JSON array синонимов)
    • embedding (BLOB для vector matching)
    • category, description, usage_count
  2. ZoneMatcher class:
    • match_or_create_zone() — главный метод (поиск или создание)
    • _embed_zone_name() — генерация embeddings (пока TF-IDF style)
    • _find_similar_zones() — cosine similarity с threshold=0.85
    • _increment_usage() — счётчик использований + синонимы
  3. API endpoints:
    • GET /zones/dictionary — список зон
    • GET /zones/dictionary/{id} — детали зоны
    • GET /zones/search?q=...&category=... — поиск
  4. Indexes: canonical_name, category для fast lookups

Подводные камни:

  • Embeddings пока placeholder (TF-IDF) — требуется ML model для production
  • Threshold 0.85 может требовать калибровки
  • Race condition при создании зоны (обрабатывается через IntegrityError)
  • Synonyms ограничены последними 10 (для экономии места)

Source ranking algorithm (Issue #8, Phase 5)

Тип: feature Файлы:

  • src/services/source_extractor.py — добавлен класс SourceRanker
  • api/routes.py — интеграция ранжирования
  • src/agents/nodes.py — передача metadata в collected_items

Проблема:

  1. Источники не ранжировались по релевантности/авторитетности
  2. Все источники показывались в порядке сбора (не оптимально)
  3. Нет приоритета для более авторитетных источников (GitHub, HN vs Tavily)
  4. Нет учёта упоминаний источника в тексте отчёта

Решение:

  1. SourceRanker.rank_sources(): Scoring formula с 4 критериями:
    • Source authority (30%): github=1.0, hn=0.9, arxiv=0.8, tavily=0.7
    • Citation count (30%): sigmoid normalization с baseline=5
    • Recency (20%): decay formula (1.0→0.0 за 60 дней)
    • Mentioned in report (20%): bonus если URL/title в тексте
  2. Интеграция: После validation + deduplication в _run_analysis()
  3. Metadata propagation: data_collector_agent передаёт metadata (citations)
  4. Re-indexing: После ранжирования index пересчитывается (топ источник = index 1)

Подводные камни:

  • Citation count доступен только для Tavily источников (для других = 0)
  • Sigmoid baseline=5 оптимален для текущих порогов, может требовать калибровки
  • Mentioned in report работает простым substring match (не NLP)
  • Ranking НЕ сохраняется в БД (пересчитывается каждый раз)

Source validation and deduplication (Issue #8, Phase 4)

Тип: feature Файлы:

  • src/services/source_extractor.py — НОВЫЙ модуль для валидации и дедупликации
  • api/routes.py — интеграция в _run_analysis()

Проблема:

  1. Некорректные URL (с localhost, опасными расширениями) могли попасть в sources
  2. Дубликаты URL (с разными utm params, trailing slash) не фильтровались
  3. Один URL мог быть представлен несколькими вариантами

Решение:

  1. SourceExtractor.validate_url():
    • Проверка схемы (только http/https)
    • Блокировка localhost/internal IPs (127.0.0.1, 192.168., 10., etc.)
    • Блокировка опасных расширений (.exe, .msi, .bat, .zip, etc.)
  2. SourceExtractor.deduplicate_sources():
    • Нормализация URL: lowercase domain, strip trailing slash
    • Удаление utm_* query params
    • Удаление fragment (#)
    • Сохранение первого вхождения
  3. Интеграция: После сборки sources в _run_analysis():
    • Валидация → Дедупликация → Re-indexing

Подводные камни:

  • Валидация НЕ делает HTTP requests (не проверяет доступность)
  • Нормализация может ошибочно объединить разные страницы (редкий случай)
  • После дедупликации index пересчитывается (1, 2, 3, ...)
  • Логирование: DEBUG уровень для каждого отклонённого URL

Flexible citation thresholds and increased source limits (Issue #8, Phase 1-2)

Тип: feature Файлы:

  • src/config.py — добавлены category_thresholds в TavilyConfig
  • src/services/adapters/search.py — category parameter + auto-detection
  • src/agents/nodes.py — увеличен лимит источников

Проблема:

  1. Citation threshold был одинаковым (5) для всех категорий трендов
  2. Источники ограничивались первыми 10 items из collected_items
  3. Science тренды требуют более строгой валидации, social — менее строгой

Решение:

  1. Гибкие пороги: Добавлены category-specific thresholds:
    • science: 8 (академические тренды)
    • technology: 5 (средний)
    • business: 4 (ниже)
    • economy: 6 (средне-высокий)
    • society: 3 (вирусный контент)
  2. Auto-detection: Метод _detect_category_from_query() определяет категорию по ключевым словам
  3. Больше источников:
    • collect(limit=50) вместо 20
    • Первые 10 для LLM context (не перегружать промпт)
    • До 30 для collected_items (для UI source sidebar)

Подводные камни:

  • Auto-detection работает на основе простых keyword rules (не ML)
  • Если категория не определена — используется default_threshold=5
  • При изменении порогов — перекалибровать через анализ статистики
  • Performance: 50 items увеличивает время сбора, но результат кэшируется

Direct navigation to latest report (Issue #1)

Тип: UX improvement Файлы:

  • frontend-cascade/app/src/components/analysis/report-group-card.tsx
  • frontend-cascade/app/src/i18n/en.json, ru.json

Проблема: При клике на отчёт в /reports пользователь попадал на промежуточную страницу выбора версии (раскрытие списка в ReportGroupCard). Это добавляло лишний шаг к основному сценарию — открыть последний отчёт.

Решение:

  1. Заменили клик по всей карточке на Link к /analyze/${latest_job_id}/report
  2. Добавили отдельную кнопку (иконка History) для раскрытия истории версий
  3. Кнопка использует stopPropagation() чтобы не тригерить навигацию
  4. Добавлены i18n ключи: actions.showVersions, actions.hideVersions

Подводные камни:

  • Standalone анализы (без trend_id) уже работали правильно
  • При клике на кнопку History — важно остановить event propagation
  • Иконка меняется: History (collapsed) → ChevronDown с rotate-180 (expanded)

2026-02-10

Исправление расчета score в _extract_structured_data

Тип: bug fix Файлы:

  • api/routes.py (строка 174)

Проблема:

  • Код искал поле z.get("strength", 0) в impact zones, но в реальных JSON данных поле называется "impact", а не "strength"
  • Это не влияло на существующие анализы, т.к. score берется из базы напрямую (строка 886 в get_report)
  • НО если бы потребовался пересчет score, код возвращал бы 0.0 для всех зон

Решение:

  • Заменили z.get("strength", 0) на z.get("impact", 0)
  • Убрали умножение на 10, т.к. impact уже в шкале 0-10 (не 0-1)
  • Обновили комментарии для ясности

Подводные камни:

  • Impact zones в базе хранят "impact" (0-10), а не "strength" (0-1)
  • Score в базе уже в правильном формате 0-10, никакого пересчета не требуется
  • Frontend formatScore() корректно работает с 0-10 scale: .toFixed(1)

Проверка:

  • Анализ c91332df (hn:46934344): impacts [8.0, 7.0, 6.0, 7.0, 6.0, 8.0] → score 7.0
  • Анализ 7485d43e (hn:46922969): impacts [9.0, 8.0, 9.0, 7.0, 8.0, 6.0, 7.0, 7.0] → score 7.62

Унификация страниц тренда и анализа

Тип: feature + fix Файлы:

  • frontend-cascade/app/src/routes/_dashboard/trend.$trendId.tsx
  • frontend-cascade/app/src/routes/_dashboard/analyze.$jobId.report.tsx
  • api/routes.py

Проблема:

  1. Кнопки Save/Share/Export исчезали за правой границей на узких экранах (страница тренда)
  2. График Sparkline мешал восприятию на странице тренда
  3. Оценка анализа показывала 1 вместо ожидаемых 7-8
  4. Confidence display был отдельным блоком вместо части блока оценки
  5. Дублирование кнопок действий на странице анализа (ReportHeader + ReportActions)
  6. Title и description тренда не переводились на локаль интерфейса

Решение:

  • Frontend (trend page): изменен layout кнопок на flex-col (mobile) → sm:flex-row (desktop), убран shrink-0, Sparkline перенесен в ScorePanel
  • Frontend (analysis page): унифицированы стили Analysis Score (gap-1, text-2xl sm:text-3xl), Confidence внутри блока, удален ReportActions
  • Backend: score теперь 0-10 (было 0-1): score = (sum(strengths) / len(strengths) * 10)
  • Backend: используется новая архитектура переводов get_cached_translations_batch() для trend_title/description

Подводные камни:

  • Score был нормализован к 0-1 в pipeline (nodes.py), поэтому нужно умножать на 10 при вычислении итогового score
  • Переводы берутся из кэша — если перевода нет, fallback на исходный язык (EN)
  • ReportActions использовался в двух местах (desktop toolbar + mobile FAB) — оба удалены

Многоязычный поиск трендов

Тип: feature Файлы:

  • api/routes.py — функция list_trends() теперь ищет как в оригинальном (EN), так и в переведенном тексте (RU)
  • tests/test_multilingual_search.py — unit-тесты для многоязычного поиска

Проблема: Поиск работал только по оригинальному тексту (английский). Если пользователь с русской локалью искал "нейронные сети", система не находила тренд "Neural Networks", хотя перевод уже был закэширован в БД.

Решение:

  1. Перемещен batch-перевод items ПЕРЕД поиском (вместо ПОСЛЕ пагинации)
  2. Создан кэш переводов {item.id → translated_dict} для переиспользования
  3. Поиск проверяет ОБЕИХ версии:
    • Оригинал (EN): title, description
    • Перевод (RU): translated.title, translated.description
  4. Если query найден в любой версии — item попадает в результаты
  5. Финальный ответ использует уже готовый кэш переводов (без повторного вызова)

Производительность:

  • Batch перевод 500 items: ~3ms (cache-only, SQLite lookup)
  • Поиск по 500 items: ~2ms (in-memory)
  • Total overhead: ~5ms (vs текущий подход ~3ms)

Подводные камни:

  • Cache hit rate для titles: ~75%, для descriptions: ~5% (translation watchdog заполняет каждые 5 минут)
  • Если перевода нет в кэше — graceful fallback к поиску по оригинальному тексту
  • EN mode (lang=en) игнорирует переводы (поиск только по оригиналу)

Debounce для поиска (500 мс задержка)

Тип: feature Файлы:

  • frontend-cascade/app/src/hooks/use-debounce.ts — новый переиспользуемый хук для debounce
  • frontend-cascade/app/src/routes/_dashboard/radar.tsx — добавлен debounce для search input (500ms)
  • frontend-cascade/app/src/components/layout/command-palette.tsx — добавлен debounce для search input (500ms)

Проблема: При вводе символов в поиск отправлялось слишком много запросов к API, UI постоянно перестраивался и "прыгал". Пользователю сложно было печатать, так как результаты обновлялись на каждый символ.

Решение:

  1. Создан универсальный хук useDebounce<T>(value, delay) с default delay 1000ms
  2. В Radar page: const debouncedSearch = useDebounce(searchInput, 500) + useEffect для синхронизации в filters
  3. В Command Palette: const debouncedSearch = useDebounce(searchValue, 500) + передача в react-query
  4. API запрос отправляется только через 500ms после того, как пользователь перестал печатать

Поведение:

  • Пользователь печатает → поисковый input обновляется мгновенно (без задержки)
  • API запрос отправляется через 500ms после последнего изменения
  • Если пользователь продолжает печатать — таймер сбрасывается, запрос не отправляется
  • UI перестраивается только когда приходят новые данные (через 500ms+)

Подводные камни:

  • Input не лагает, реактивен мгновенно (debounce только для API, не для UI input)
  • enabled: commandPaletteOpen && debouncedSearch.length >= 2 — API не вызывается для queries < 2 символов
  • Radar page очищает search при закрытии Command Palette через useEffect cleanup
  • Задержка подобрана экспериментально: 500ms — баланс между отзывчивостью и снижением нагрузки
  • Переменная target_lang теперь объявлена в начале функции (до search блока), не дублировать перед финальным ответом

Тип: feature Файлы:

  • api/analysis_store.pyget_trend_metadata() и get_trend_metadata_by_id() теперь SELECT-ят content из trending_items и возвращают trend_description
  • api/schemas.py — добавлено поле trend_description: str = "" в AnalysisReportResponse
  • api/routes.pyget_report() передает trend_description из trend_meta в response
  • frontend-cascade/app/src/types/analysis.ts — добавлено поле trend_description?: string в AnalysisReport
  • frontend-cascade/app/src/routes/_dashboard/analyze.$jobId.report.tsxReportHeader: описание тренда, tooltip с разбивкой оценки на score, fallback для source_url
  • frontend-cascade/app/src/i18n/en.json / ru.json — ключи scoring.scoreBreakdown, scoring.scoreExplanation, scoring.trendScoreBreakdown, scoring.basedOn, scoring.sourcesDiversity, scoring.crossDomainImpact

Проблема:

  1. Описание тренда (content из trending_items) не отображалось на странице отчета
  2. Оценка (score) показывалась как число без пояснения, из чего она складывается
  3. При отсутствии trend_sources но наличии source_url не было fallback-ссылки на источник

Решение:

  1. Backend: добавлен SELECT content в обоих get_trend_metadata*(), передается как trend_description (обрезан до 500 символов)
  2. Frontend: score обернут в Tooltip с разбивкой (trend_score, urgency, quality, confidence)
  3. Frontend: trend_score показывается с tooltip (velocity, source diversity, cross-domain impact)
  4. Frontend: fallback-ссылка <a href={source_url}> когда trend_sources пуст

Подводные камни:

  • content в trending_items — это не всегда чистое описание; для HN это может быть текст поста, для GitHub — README excerpt
  • trend_score в API приходит 0-1, analysis score — 0-10, нормализация в tooltip: trend_score * 10

Comprehensive Scrollbar Fix — Single Scroll Container

Тип: fix Файлы:

  • frontend-cascade/app/src/globals.csshtml,body,#root { height:100%; overflow:hidden } + .dashboard-main-scroll class
  • frontend-cascade/app/src/routes/__root.tsxh-full вместо min-h-screen
  • frontend-cascade/app/src/routes/_dashboard.tsxdashboard-main-scroll на <main>
  • frontend-cascade/app/src/routes/admin.tsx — тот же паттерн
  • frontend-cascade/app/src/routes/login.tsxh-full вместо min-h-screen

Проблема: Двойной скроллбар и дёрганье контента (layout shift) при загрузке карточек на радаре. Предыдущие фиксы (scrollbarGutter:stable на main, h-screen overflow-hidden на wrapper) не помогли — скролл появлялся на html/body.

Решение:

  1. Lock html, body, #root { height: 100%; overflow: hidden } — страница никогда не скроллится
  2. <main> — единственный scroll-контейнер с overflow-y: scroll (всегда видимый трек)
  3. scrollbar-gutter: stable + thin custom scrollbar (6px) через ::-webkit-scrollbar
  4. Все layout-обёртки: h-full вместо min-h-screen

Подводные камни:

  • Нужно h-full (не min-h-screen) на всех обёртках, иначе контент выходит за viewport
  • overflow-y: scroll (не auto) — скроллбар всегда виден, не toggle-ится

Filter Count Desynchronization Fix

Тип: fix Файлы:

  • api/routes.py — reorder filter pipeline: search → category_counts(search+status) → stats(search+category) → items(all filters)
  • frontend-cascade/app/src/hooks/use-trends.ts — statsQuery теперь передаёт status

Проблема: При поиске "u.s." stat cards показывали Total=2 (correct), но category pills показывали All=500 (глобальные, без учёта поиска). category_counts считались ДО любых фильтров.

Решение: Каждое измерение фильтра считается с ВСЕМИ активными фильтрами КРОМЕ своего:

  • category_counts = search + status (без category)
  • stats = search + category (без status) Frontend statsQuery теперь передаёт все фильтры, backend сам исключает нужное.

Подводные камни:

  • apiClient.getTrends автоматически strip-ает status='all' и category='all' → они не отправляются на сервер
  • Backend получает status=None и category=None для "all" → не фильтрует

2026-02-09

Radar: Split Query Architecture for Filter Cards

Тип: fix / refactor Файлы:

  • frontend-cascade/app/src/hooks/use-trends.ts — два раздельных react-query: statsQuery (category/search) и trendsQuery (все фильтры)
  • api/routes.py — reorder: category/search → stats → status filter
  • frontend-cascade/app/src/routes/_dashboard/radar.tsx — AnimatedCounter с prev-value ref, AnimatePresence popLayout для grid

Проблема: При выборе статуса (Exploding/Rising/Emerging) на плашках радара — числа в карточках пересчитывались под текущий фильтр. Пример: "Exploding → Business" показывало 3 тренда в списке, но карточка "Total" = 134 (все тренды, а не отфильтрованные по категории).

Решение:

  1. Split queries: statsQuery учитывает category + search, но НЕ status. trendsQuery — все фильтры
  2. Backend reorder: category/search фильтрация ДО подсчёта stats, status фильтрация ПОСЛЕ
  3. AnimatedCounter: анимация от предыдущего значения (не от 0), skip при одинаковых значениях
  4. AnimatePresence mode="popLayout": плавная анимация карточек при смене фильтра

Подводные камни:

  • statsQuery делает отдельный запрос с limit: 1 — только для получения stats
  • categoryCounts тоже из statsQuery — глобальные (до status фильтра)
  • keepPreviousData на trendsQuery предотвращает flash при переключении

Translation: Fix Description Translation (Markers + Batch Chunking)

Тип: fix Файлы:

  • api/translation_service.py — markers <<<N>>> вместо [N], regex fix
  • src/workers/crawler.py — BATCH_SIZE=30 для LLM calls, get_cached_translations_batch() вместо N+1

Проблема: Описания трендов отображались на EN при выборе RU. Два root cause:

  1. _batch_translate_texts использовал маркеры [N], которые конфликтовали с содержимым типа [10], [2024] в описаниях → regex ломал парсинг
  2. Crawler _pre_translate_items отправлял ВСЕ описания в одном LLM-запросе → output truncation

Решение:

  1. Заменили маркеры на <<<N>>> и regex <<<(\d+)>>> — не конфликтуют с контентом
  2. Добавили BATCH_SIZE=30 для chunked LLM calls
  3. Используем get_cached_translations_batch() для проверки уже переведённых текстов перед вызовом LLM

Подводные камни:

  • Маркеры <<<N>>> должны быть уникальны и НЕ встречаться в обычном тексте
  • При batch_size слишком маленьком — больше LLM вызовов; слишком большом — truncation

TrendView: Unified Component with Variants

Тип: refactor Файлы:

  • frontend-cascade/app/src/components/trends/trend-view.tsxNEW: sm / lg / row варианты + TrendViewSkeleton
  • frontend-cascade/app/src/components/trends/trend-card.tsx — deprecated (функционал в trend-view.tsx)
  • frontend-cascade/app/src/routes/_dashboard/radar.tsx — использует <TrendView variant="lg" />

Проблема: Карточка тренда была одним монолитным компонентом. Нужны были разные представления: мини-карточка (sm), полная карточка (lg), строка таблицы (row).

Решение: TrendView({ trend, variant }) — switch по variant на три internal компонента: TrendViewSmall, TrendViewLarge, TrendViewRow. Shared SourceLinks компонент. TrendViewSkeleton с вариантами.


E2E Translation Tests

Тип: test Файлы:

  • tests/e2e/test_translation_e2e.pyNEW: 33 теста для eager translation system

Проблема: Eager translation system (batch lookup, cache-only API, crawler pre-translate, watchdog) не имела тестового покрытия.

Решение: 33 e2e теста в 6 классах:

  • TestBatchCacheLookup — batch queries, пустой кеш, разные языки
  • TestCacheOnlyAPI — fallback на EN, partial cache, analysis cache-only
  • TestCrawlerPreTranslate — batch translation в crawler, chunking
  • TestTranslationWatchdog — gap detection, fill loops, start/stop lifecycle
  • TestWALMode — WAL mode при init
  • TestScoreIndex — индекс на score

Eager Translation System + Translation Watchdog

Тип: refactor / feature Файлы:

  • api/translation_service.py — WAL mode, get_cached_translations_batch() (batch lookup), cache-only методы (translate_trend_items_cached_only, translate_analysis_result_cached_only)
  • api/routes.py — API routes: заменён on-the-fly LLM перевод на cache-only lookup с EN fallback
  • src/workers/crawler.py — eager batch translation titles + descriptions после каждого crawl, индекс idx_trending_score
  • src/workers/translation_watchdog.pyNEW: фоновый watchdog (каждые 5 мин) для заполнения пробелов в переводах
  • api/main.py — интеграция TranslationWatchdog, удалён одноразовый pre_translate_existing()

Проблема: API routes вызывали LLM при каждом запросе с lang=ru на cache miss — задержки 5-30 сек. N+1 SQLite connections (60 connect/close на запрос). Нет WAL mode. Решение: Batch cache lookup (1 SQL вместо 60), WAL mode, cache-only API (EN fallback), eager translation в crawler, фоновый watchdog для gap-filling. Подводные камни: При пустом кеше все тексты на EN; watchdog заполнит через 5 мин. Crawler pre-translate может fail — watchdog подхватит.


Daily Analysis Credits

Тип: feature Файлы:

  • api/credits_store.pyNEW: SQLite table daily_credits, функции check_credits(), use_credit()
  • api/main.py — инициализация таблицы при старте
  • api/routes.py — auth guard + credit deduction на POST /trends/analyze
  • api/auth/routes.pyGET /auth/credits endpoint
  • frontend-cascade/app/src/lib/api-client.tsgetCredits() метод
  • frontend-cascade/app/src/routes/_dashboard/analyze.tsx — отображение кредитов, блокировка кнопки
  • frontend-cascade/app/src/i18n/{en,ru}.json — i18n ключи

Проблема: Endpoint /trends/analyze был полностью открыт — без авторизации, без лимитов. Любой мог спамить анализами, сжигая кредиты LLM. Решение: Добавлен auth guard (JWT) + система дневных кредитов: 100 анализов/день на пользователя, сброс в полночь UTC. При исчерпании — HTTP 429. Подводные камни:

  • use_credit() использует BEGIN IMMEDIATE для атомарности
  • Тесты требуют dependency override для get_current_user и mock для use_credit

Version Comparison Page

Тип: feature Файлы:

  • frontend-cascade/app/src/routes/_dashboard/analyze.$jobId.compare.tsxNEW: страница сравнения двух версий анализа
  • frontend-cascade/app/src/components/analysis/diff-report-viewer.tsxNEW: Unified/SideBySide отображение diff
  • frontend-cascade/app/src/components/analysis/zones-diff.tsxNEW: сравнение impact zones с fuzzy matching
  • frontend-cascade/app/src/lib/diff-engine.tsNEW: paragraph-level diff + word-level highlights (Jaccard similarity)
  • frontend-cascade/app/src/i18n/en.json, ru.json — ключи comparison.*

Проблема: После добавления версионности анализов не было возможности увидеть, что именно изменилось между версиями.

Решение:

  1. Страница /analyze/$jobId/compare?with=$otherId загружает оба отчёта
  2. diffMarkdown() — paragraph-level diff с word-level подсветкой изменений
  3. diffImpactZones() — fuzzy matching зон по названию (Jaccard threshold)
  4. Два режима: Unified View и Side-by-Side View
  5. Dropdown для выбора версии сравнения из списка доступных

Подводные камни:

  • TanStack Router требует q: undefined, trend_id: undefined в search params при навигации к дочернему route (наследует от parent)
  • Locale берётся из useUIStore() для консистентного diff

SSE Progress: Event Queue

Тип: fix Файлы:

  • api/routes.py_progress_events: dict[str, list[dict]], cursor-based SSE generator

Проблема: _progress был single-slot dict — каждый шаг перезаписывал предыдущий. Если шаг "translating" → "complete" происходил быстрее, чем SSE успевал прочитать, фронтенд закрывал страницу не дождавшись перевода.

Решение:

  1. Заменили _progress: dict на _progress_events: dict[str, list[dict]] (append-only list)
  2. SSE generator использует cursor — никогда не пропускает события
  3. _update_progress() добавляет событие в список, не перезаписывает
  4. /analyses/running берёт events[-1] для текущего статуса

Подводные камни:

  • Тесты ссылаются на _progress_events, не _progress
  • Memory: события накапливаются в памяти — для долгих анализов не проблема (7 шагов max)

Progress Bar Rescaling

Тип: fix Файлы:

  • api/routes.py — новое распределение процентов по фазам

Проблема: Фаза перевода занимала 3% прогресс-бара (96→99%), хотя реально длится дольше всего. Пользователь видел "95%" и долго ждал.

Решение: Новое распределение пропорционально реальной длительности:

Step Phase % range
1 collecting 5→15%
2 researching 18→30%
3 building (cascade) 33→48%
4 building (assembly) 53%
5 reporting 58→76%
6 translating 78→95%
7 complete 100%

Подводные камни:

  • Frontend STEP_KEYS содержит 7 записей (два steps.building)
  • Backend step number должен совпадать с индексом STEP_KEYS + 1

Pre-Translation of Reports

Тип: feature Файлы:

  • api/routes.py — автоматический перевод после завершения анализа
  • api/translation_service.py — кеширование переводов

Проблема: Отчёт переводился на лету при первом запросе, что создавало задержку.

Решение: После завершения анализа автоматически запускается перевод на все поддерживаемые локали. Кеш translation_service сохраняет результат.


CI Build Fixes

Тип: fix Файлы:

  • frontend-cascade/app/src/routes/_dashboard/analyze.$jobId.compare.tsx — удалён неиспользуемый import AnalysisVersionSummary
  • frontend-cascade/app/src/routes/_dashboard/analyze.$jobId.report.tsx — добавлены q, trend_id в search params навигации
  • frontend-cascade/app/src/routes/_dashboard/trend.$trendId.tsx — удалён неиспользуемый useNavigate
  • tests/unit/test_analysis_pipeline.py, tests/e2e/test_versioning_e2e.py_progress_progress_events

Проблема: CI build:frontend падал на 4 TypeScript ошибках (TS6196, TS2322, TS6133) после рефакторинга SSE и версионности.

Решение: Удалены неиспользуемые импорты, добавлены обязательные search params для TanStack Router, обновлены тестовые ссылки.


2026-02-08

Analysis Versioning & Deepening

Тип: feature Файлы:

  • api/analysis_store.pyreserve_analysis(), complete_analysis(), fail_analysis(), find_analyses_by_trend(), list_analyses_grouped(), ALTER TABLE миграции
  • api/schemas.pyAnalysisVersionSummary, TrendAnalysesResponse, GroupedAnalysisSummary, GroupedAnalysisListResponse, расширены AnalysisReportResponse и AnalysisSummary
  • api/routes.py — атомарное резервирование версий, валидация parent_job_id, depth clamping [1-7], обновлён GET /analyses/by-trend/, GET /analyses?grouped=true
  • frontend-cascade/app/src/types/analysis.tsAnalysisVersionSummary, TrendAnalysesResponse, GroupedAnalysisSummary, getVersionType()
  • frontend-cascade/app/src/lib/api-client.tsgetAnalysesForTrend(), getGroupedAnalyses(), parent_job_id в analyzeTrend()
  • frontend-cascade/app/src/components/analysis/analysis-timeline.tsxNEW: Timeline на странице тренда
  • frontend-cascade/app/src/components/analysis/version-type-badge.tsxNEW: INITIAL / RE_ANALYZED / DEEPENED badge
  • frontend-cascade/app/src/components/analysis/version-nav-bar.tsxNEW: Prev/Next навигация на report page
  • frontend-cascade/app/src/components/analysis/score-delta-strip.tsxNEW: Дельты Score/Conf/Zones/Depth
  • frontend-cascade/app/src/components/analysis/report-group-card.tsxNEW: Grouped card на reports page
  • frontend-cascade/app/src/routes/_dashboard/trend.$trendId.tsx — CTA → AnalysisTimeline
  • frontend-cascade/app/src/routes/_dashboard/analyze.$jobId.report.tsx — VersionNavBar, ScoreDeltaStrip, action buttons
  • frontend-cascade/app/src/routes/_dashboard/reports.tsx — Flat list → grouped listing
  • frontend-cascade/app/src/i18n/en.json, ru.json — versioning., comparison., actions.reAnalyze/deepen/compare

Проблема: Каждый анализ тренда — одноразовый. Нет истории, нет версионности, нет возможности углубить анализ.

Решение:

  1. Atomic version reservation: reserve_analysis() с BEGIN IMMEDIATE гарантирует уникальность (trend_id, version) при параллельных запросах
  2. 3-phase lifecycle: reserve → complete/fail (вместо save_analysis)
  3. UNIQUE constraint на (trend_id, version) WHERE trend_id IS NOT NULL
  4. Version type: INITIAL (v1), RE_ANALYZED (v>1, no parent), DEEPENED (has parent_job_id)
  5. Frontend: Timeline на тренде, VersionNavBar + ScoreDeltaStrip на отчёте, grouped reports page
  6. API: GET /analyses/by-trend/{id} → все версии, GET /analyses?grouped=true → сгруппированный список

Подводные камни:

  • reserve_analysis() ловит IntegrityError и делает retry +1 (race condition safety net)
  • getAnalysisForTrend() deprecated → используй getAnalysesForTrend() (list)
  • depth clamped 1-7 на API уровне
  • parent_job_id валидируется: должен существовать в БД и принадлежать тому же trend_id
  • Старые анализы без trend_id → version=1, не группируются

Trend-Analysis Linking by ID

Тип: feature Файлы:

  • api/analysis_store.pyget_trend_metadata_by_id(), save_analysis() с trend_id
  • api/routes.pyAnalyzeRequest.trend_id, обновлён get_report()
  • frontend-cascade/app/src/lib/api-client.tstrend_id в analyzeTrend()
  • frontend-cascade/app/src/routes/_dashboard/analyze.tsxvalidateSearch с trend_id
  • frontend-cascade/app/src/routes/_dashboard/trend.$trendId.tsx — передача trend_id при навигации

Проблема: Связь тренда с анализом была через trend_title, что ломалось при:

  • Разных языках (русский title в анализе, английский в trending_items)
  • Изменении title после анализа
  • Дублях названий

Решение:

  1. При запуске анализа с карточки тренда передаём trend_id через URL params
  2. save_analysis() сохраняет trend_id напрямую в БД
  3. get_report() получает метаданные по trend_id (primary), fallback на title
  4. Frontend передаёт trend_id в /analyze?q=...&trend_id=...

Подводные камни:

  • Старые анализы без trend_id — fallback на поиск по title
  • При ручном вводе тренда (без перехода с карточки) — trend_id = null

Graph Effect Nodes: Description Display

Тип: fix Файлы:

  • frontend-cascade/app/src/types/analysis.tsdescription в graph.nodes type
  • frontend-cascade/app/src/routes/_dashboard/analyze.$jobId.report.tsx — mapping description
  • frontend-cascade/app/src/components/analysis/interactive-graph.tsx — отображение description

Проблема: Cascade-эффекты в графе имели causal_chain (описание), но оно не показывалось в UI. Backend сохранял его как rationaledescription, но frontend не передавал поле.

Решение:

  1. Добавили description?: string в тип AnalysisReport.graph.nodes
  2. Добавили description: n.description в mapping graphNodes
  3. UI уже был готов — показывает selectedNode.description в карточке

Подводные камни:

  • description приходит из rationale для zones и causal_chain для effects
  • Все 18 effect-нод в тестах имеют description — если пусто, проверь LLM output

2026-02-07

Sources: переход от счётчиков к массивам URL

Тип: refactor Файлы:

  • api/routes.py_build_sources_dict(), _crawler_item_to_schema()
  • api/schemas.py — тип sources
  • frontend-cascade/app/src/types/trend.ts — тип TrendItem.sources
  • frontend-cascade/app/src/components/trends/trend-card.tsxgetSourceUrl()
  • frontend-cascade/app/src/components/trends/trend-table.tsx — подсчёт источников
  • frontend-cascade/app/src/routes/_dashboard/trend.$trendId.tsxtotalSources(), отображение ссылок

Проблема: Поле sources содержало синтетические числа ({github: 1, hn: 2}), а не реальные ссылки. Пользователь ожидал кликабельные ссылки на каждый источник.

Решение:

  1. Изменили тип sources с dict[str, int] на dict[str, list[str]]
  2. Каждый ключ теперь содержит массив реальных URL
  3. Удалили поле source_urls — теперь всё в sources
  4. Frontend использует sources?.[type]?.[0] для получения первой ссылки
  5. На странице деталей — .map() для отображения всех ссылок

Подводные камни:

  • Если добавляешь новый источник — добавь его в _build_sources_dict() и в frontend типы
  • sources может быть undefined — всегда используй optional chaining (?.)

Tavily ID: хэширование URL

Тип: fix Файлы: src/services/adapters/search.py

Проблема: Tavily ID содержал полный URL (tavily:https://example.com/article...). При переходе на страницу тренда URL энкодился и ломал роутинг: /trend/tavily%3Ahttps%3A%2F%2Fwww.example.com%2F...

Решение:

import hashlib
url_hash = hashlib.md5(url.encode()).hexdigest()[:12]
id=f"tavily:{url_hash}"

Теперь ID: tavily:a1b2c3d4e5f6 (12 символов MD5 хэша)

Подводные камни:

  • Оригинальный URL хранится в поле url, не в ID
  • _build_sources_dict() использует item_url для tavily, не парсит ID
  • При коллизии хэшей (маловероятно) — увеличить длину хэша

Categories: навигация через URL params

Тип: fix Файлы: frontend-cascade/app/src/routes/_dashboard/categories.tsx

Проблема: Выбор категории использовал useState, URL не менялся. Кнопки браузера "назад/вперёд" не работали.

Решение:

// Добавили validateSearch
export const Route = createFileRoute('/_dashboard/categories')({
  validateSearch: (search) => ({
    category: search.category || undefined,
  }),
})

// Вместо useState — Route.useSearch()
const { category: selectedCategory } = Route.useSearch()

// Навигация обновляет URL
navigate({ to: '/categories', search: category ? { category } : {} })

Подводные камни:

  • TanStack Router требует validateSearch для типизации search params
  • При очистке категории передавай пустой объект {}, не undefined

Scoring V2: Urgency, Quality, Recommendation

Тип: feature Файлы:

  • src/services/scoring.py — ScoringResult dataclass, calculate_urgency(), calculate_quality(), get_recommendation()
  • api/schemas.py — поля urgency, quality, recommendation в TrendItem
  • api/routes.py — передача новых полей в schema
  • frontend-cascade/app/src/types/trend.ts — Recommendation type, новые поля в TrendItem
  • frontend-cascade/app/src/components/trends/recommendation-badge.tsx — NEW: отображение рекомендаций
  • frontend-cascade/app/src/components/trends/urgency-quality-meter.tsx — NEW: иконки с числовыми значениями
  • frontend-cascade/app/src/i18n/en.json, ru.json — переводы scoring.*

Проблема: Единый score не давал понимания "когда действовать" vs "насколько качественный тренд".

Решение:

  1. Urgency (0-100): Как срочно нужно реагировать
    • Формула: velocity × time_decay × engagement
    • Высокий = быстро растёт, нужно действовать сейчас
  2. Quality (0-100): Насколько надёжный/качественный тренд
    • Формула: source_coverage × confidence × data_completeness
    • Высокий = много источников, проверенные данные
  3. Recommendation: ACT_NOW | MONITOR | EVERGREEN | IGNORE
    • ACT_NOW: urgency ≥ 70 AND quality ≥ 50
    • MONITOR: urgency ≥ 40 OR quality ≥ 40
    • EVERGREEN: quality ≥ 70 AND urgency < 40
    • IGNORE: остальное

UI компоненты:

  • RecommendationBadge — цветной бейдж с иконкой (ACT_NOW пульсирует)
  • UrgencyQualityIcons — формат ⚡ 72 ⭐ 55 с tooltip

Подводные камни:

  • Urgency может быть 0 для старых трендов без velocity
  • MONITOR — дефолт, не показывать бейдж если recommendation = MONITOR
  • Сохранять обратную совместимость с score (composite)

Tavily: Citation-Based Validation

Тип: feature Файлы:

  • src/config.py — TavilyConfig params, TAVILY_CITATION_BASELINES, AGGREGATOR_PATTERNS
  • src/services/adapters/search.py — fetch_news(), count_citations(), _is_aggregator(), _parse_date()
  • src/services/scoring.py — tavily_citations signal (0.15 weight) во всех категориях
  • src/workers/crawler.py — enrichment loop для HN/GitHub/arXiv трендов

Проблема: Tavily возвращал низкокачественные данные:

  • 98% items имели score >= 0.8 (бесполезно для ранжирования)
  • 42% — aggregator/category pages (мусор типа /news/, /category/)
  • Нет реальной даты публикации (published_at = now())
  • Нет валидации "трендовости" — любой результат считался трендом

Решение:

  1. API Improvements: topic="news", search_depth="advanced", days=3
  2. Aggregator Filter: _is_aggregator(url) фильтрует категории и главные страницы
  3. Date Parsing: _parse_date() извлекает реальную дату публикации
  4. Citation Validation: count_citations(title) считает уникальные домены
  5. Threshold: Только items с >= 5 уникальных доменов проходят валидацию
  6. Scoring: Новый сигнал tavily_citations с весом 0.15 во всех категориях
  7. Enrichment: Crawler добавляет citations для HN/GitHub/arXiv трендов

API Credits:

  • Старое: 1 credit/search (basic)
  • Новое: 3 credits/trend (2 advanced + 1 citation validation)
  • Enrichment: +1 credit/non-Tavily trend

Подводные камни:

  • tavily_citations != citations (академические для science)
  • Старый fetch() deprecated, использовать fetch_news()
  • Rate limiting: enrichment увеличивает время crawl
  • TAVILY_CITATION_BASELINES могут требовать калибровки

Шаблон для новых записей

### [Краткое описание]

**Тип:** bug | feature | refactor | fix
**Файлы:**
- `path/to/file.ts`

**Проблема:**
Описание проблемы

**Решение:**
Что сделали (можно с кодом)

**Подводные камни:**
- На что обратить внимание

Rate this content

0/1000