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.
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.
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.go—EnqueueAdminAction(action, extra)— INSERT в jobs(kind=admin_action), external_refadmin:{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, sharerequireAdminчерез 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.
Подводные камни:
- Response shape идентичен Python (
{"status": "queued"|"skipped", "message": "..."}). Frontend не требует изменений. - external_ref format
admin:{action}:{ms}-{uuid8}совместим с Python writer — observability черезSELECT * FROM jobs WHERE kind='admin_action'работает одинаково. - Сами 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 oversettings(key, value)table. Bool encoded как "1"/"0" (compatible с Python writer). -
store/admin_sources.go—SourceCatalogmap (70 sources hardcoded — duplicatessrc/config.py::SOURCE_TYPE_MAP).ListAdminSourcesмерджит catalog + DB enabled state +source_last_fetch:{name}для status.IsValidSourceNameguard. -
handler/admin_settings.go— все наrequireAdmin(re-Verify bearer + check IsAdmin claim → 403 если не admin).clampreused из 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
Подводные камни:
-
SourceCatalogдублируетSOURCE_TYPE_MAPвsrc/config.py. Новый адаптер требует update в обоих местах. Минорный maintenance overhead, но не блокер. - Bool encoding "1"/"0" совместим с Python writer (
api/settings_store.update_setting). Round-trip Go↔️ Python работает. -
?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.go—CreatePAT/ListPATs/RevokePAT. Token formatpat_{base64url(32)}совместим с MR7 LookupPAT (sha256 hash check). Nullableexpires_atчерез*string. -
store/audit_log.go—ListAuditLogс условнымWHERE username = ?+ LIMIT/OFFSET.Successint 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 дляIsAdminclaim → 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.
Подводные камни:
-
audit_log.goиспользует ту жеlogin_audit_logтаблицу что Python пишет (см.api/auth/adapters/sqlite_audit.py). Мы только читаем, Python остаётся owner записи. Без race conditions. -
IsAdminclaim из Casdoor JWT — проверяем через re-Verify (не передаём context object). Cost: один decode + RSA verify per admin call. Admin endpoints редкие, acceptable. - 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/reposlashes) -
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
Подводные камни:
- Все endpoint'ы (кроме
unread-count) требуютIsVerifiedUser=true—CASDOOR_CERTIFICATEenv обязателен на проде. - system-wide notifications (
user_id IS NULL) surface'ятся каждому verified user'у через predicate(user_id IS NULL OR user_id = $1). - 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 stdlibnet/httpHTTP 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'ов черезAuthDepsstruct. /me требует verified JWT (Phase 3a verifier). /credits требует verified user (anon → 401). -
store/credits.go—EnsureTodayCreditsсINSERT ... ON CONFLICT DO NOTHINGчерез write pool. Mirrorsapi/credits_store.check_creditsAPI (remaining/limit/used). -
store/user_profile.go—EnsureUserProfileдля 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 ✓
Подводные камни:
-
Casdoor cert на проде должен быть в
BACKEND_ENV(CI File var) для verify. Без него/auth/me→ 503,/auth/credits→ 401 (legacy unverified path не используется в этих handler'ах). -
Concurrency:
_post_loginидёт вgo func()— fire-and-forget upsert после возврата tokens клиенту. Если упадёт — пользователь получит токены, user_profiles row создастся при первом write через watchdog. Acceptable. -
Phase 3c остаётся: ~2000 LoC миграция оставшихся writes (
/saved,/watchlist,/notifications,/tags,/profile/*,/labnon-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_CERTIFICATEenv. Fail-closed: empty cert → verifier rejects everything. -
go-api/internal/middleware/auth.goпереписан с двумя путями:-
verifier set + valid signature→user_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_CERTIFICATEenv при старте, логирует "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
extractSubFromJWTlegacy path удалён. - Read-only personalization работает по-прежнему даже без cert — graceful degradation.
Подводные камни:
- На проде должен быть установлен
CASDOOR_CERTIFICATEenv (BACKEND_ENV file). Иначе go-api логирует warning и работает в legacy режиме (без verify). Безопасно для read-side, но MUST be fixed для production. - Существующие endpoint'ы (
/interactions,/dismiss/*) сейчас не проверяютIsVerifiedUser— не блокируют unverified user_id. Это легко добавить когда пройдёт verify в проде. - 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— registryACTIONS: dict[str, Callable]с 5 handler'ами, извлечёнными из inline_runclosures. Каждый handler синхронный, открывает свойget_conn(), возвращает dict для записи вjobs.result. -
src/workers/admin_worker.py—kind="admin_action"worker. Discriminator —payload["action"](имя функции в registry). Эмитит progress eventsrunning→complete/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-workerPM2 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_handlerraises with helpful message on unknown - handler dispatch + result wrap (non-dict →
{"result": str}) -
running/complete/failedprogress events - empty payload + unknown action both raise (queue marks failed)
Все 30+ Python worker tests passed (admin + lab + analysis + dispatcher + jobs + worker_loop).
Подводные камни:
-
admin-workerпроцесс должен быть запущен — иначе admin actions копятся в очереди. PM2startOrReloadподнимет автоматически. - Существующие endpoint'ы возвращают
status="queued"вместо"started"— frontend должен обрабатывать оба. - UI пока не показывает progress admin jobs (нет SSE endpoint для
kind=admin_action). Можно добавить generic/admin/jobs/{ref}/stream— отдельная задача. - Phase 2 (унификация очереди) закрыта. Все долгие задачи (analysis + lab + admin) идут через единый
jobsqueue + 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 → legacylab_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-memorylab_stateсохранён для legacy in-process runs. -
ecosystem.config.js: добавленlab-workerPM2 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 тестов).
Подводные камни:
-
analysis_progresstable используется для всех kind'ов (analysis + lab). Имя таблицы (analysis_progress) теперь немного misleading — это generic event log. Переименование = миграция, отложено как technical-debt note. -
lab_stateостаётся как fallback path для тестов и legacy in-process runs. После v0.25.74 ВСЕ продакшн-вызовы идут через worker → queue, lab_state остаётся пустым на проде. Удаление безопасно в follow-up MR. -
lab-workerпроцесс должен быть запущен на проде — иначеPOST /lab/needsбудет копить jobs без обработки. - 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 вjobsqueue (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 на persistedanalysestable для завершённых. -
GET /analyses/{id}/stream— polling 300msreplay_progress(after_id=cursor)+ watch queue status для terminal states. Cursor поanalysis_progress.idгарантирует exactly-once delivery событий. -
GET /analyses/{id}/reportчитает только изanalysestable (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-exportsAnalysisStateManagerи_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 зелёные.
Подводные камни:
-
POST /analysesвозвращаетstatus: "pending"вместо"running"— фронт треатит и то и другое как "in progress", визуально разницы нет. Если что-то ломается в UI — это место. - Когда
analysis-workerпроцесс не запущен (локально), enqueue'енные jobs накапливаются в queue без обработки. Запускать черезdev-microservices.ps1 -Only "analysis-worker". - Skipped-тесты должны быть переписаны под queue-driven flow. Это технический долг, но не блокер.
-
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-managedcascade_anon_idcookie для анонимов (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]}.
Подводные камни:
- Auth (Casdoor JWT) тоже декодируется в Go (
authmw.ExtractUserID). На текущий момент Go доверяет JWT без verify подписи — это safe для read-only personalization, и для interactions это тоже acceptable (rate-limit + anonymous fallback). MR8b (auth port) добавит signature verify. - SaveInteraction открывает
db.Write()pool (отдельный от read-only). На SQLite WAL — concurrent с analytics process работает. - Не мигрированы: /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/heartbeat→204.collector_heartbeatстрока обновлена сlast_batch_id. -
ingest_logлогирует все 4 batch'а (2 accepted, 2 duplicate-rejected). Подводные камни:
- PAT для smoke-теста создан через
api.auth.pat_store.create_pat(user_id='dev-user', name='dev-collector'). Raw token виден один раз — не сохраняется. На проде PAT создаются через UI. - 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-onlydb.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 или fallbackpat.name). -
store.IngestSignals(ctx, collectorID, batchID, items)— одна транзакция: per-itemINSERT signals ... ON CONFLICT (id) DO NOTHING,pg_notify('ingest_signals', signal_id)после каждой успешной вставки, одинINSERT ingest_log(audit),UPSERT collector_heartbeat(last_seen). -
handler.IngestSignalsPOST/api/ingest/signals— JSON body сbatch_id+items[], body cap 8MiB, max 500 items per batch, per-item rejection reasons. -
handler.IngestHeartbeatPOST/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, возвращаетIngestResponsedict. -
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.
Подводные камни:
- В Go-side handler используется новый отдельный write-pool. Если read-only flag в production'е применён через GUC на роли, write-pool не сможет писать. На контабо роль
trendsимеет write-доступ — проверено в pg_schema.sql baseline. - SQLite write-pool открывается лениво и держит свой connection — на dev одновременная работа
read pool + write pool + analytics process + collectorприводит к потенциальной WAL contention. WAL-журнал и_busy_timeout=10000это покрывают, но не идеально. PG production не страдает. - PAT не привязан к конкретному
collector_id— любой PAT может пушить под любымX-Collector-ID. Будущее усиление: добавитьpat_collector_binding(pat_id, collector_id)таблицу. - Идемпотентность через
ON CONFLICT (id) DO NOTHING— один и тот жеsignal_idот двух коллекторов попадёт только от первого. Дубль помечается вrejection_reasonsкак"duplicate". - 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.
Подводные камни:
-
MatchZoneпока возвращает UNIMPLEMENTED. Контракт зарезервирован в proto чтобы Go-сторона могла планировать против него. Реализация — следующий MR (потребует достатьimpact_zones_dictionary.embeddingчерез SQL, заменить ZoneMatcher монки-патчинг). - Существующий код (
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). - 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. - 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-worker — python -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.
Подводные камни:
-
POST /analysesпока не перенаправлен на enqueue — это следующий коммит MR5 (риск route conversion отделён от риска worker'а). - На SQLite с тысячей строк в
analyses_run_startup_backfillsблокирует bootstrap ~30s. На PG — мгновенно. Локально терпимо. - Если 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.
Подводные камни:
- Это фундамент под analysis-worker (следующий коммит в MR5). Сама worker-обвязка, route conversion, auto_analyzer и удаление analysis_state.py будут отдельными коммитами в этой же ветке.
- Существующих 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_kindTEXT NOT NULL DEFAULT 'url' —url | telegram | social | private | field | rss. Drives downstream: scoring branch, UI render, citation enrichment skip. -
collector_idTEXT NULL — opaque ID коллектора (telegram-osint-1,internal). -
source_refJSONB NULL — атрибуция non-URL: channel, message_id, capture_at, visibility. -
ingested_viaTEXT 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 таблицы появились.
Подводные камни:
- Никакого CODE пока не пишет в новые поля — это сделают MR7 (Go ingest API) и MR4-конверсии loops в pipeline. До тех пор все рядки имеют
source_kind='url',ingested_via='internal', остальные NULL. - На SQLite CHECK constraints не задаются (миграция гейтит
is_pg). Если кто-то пишет non-conformingsource_kindлокально — это пройдёт. На PG CHECK сработает. -
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)
Подводные камни:
- NOTIFY доставляется только подписанным в момент publish. Listener-коннект ОБЯЗАН быть долгоживущим, не из пула.
- NOTIFY в той же транзакции что и write — иначе можно нотифицировать о несуществующих rows (Postgres откатывает NOTIFY на abort транзакции).
- На SQLite оба helper'а — no-op. Это сознательно: production = PG, локальная разработка на SQLite — просто polling без NOTIFY оптимизации.
- Существующие 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 (примеры в комментариях файла).
Подводные камни:
- Регистр заполняется через side-effect импорты в
src/workers/crawler/_orchestrator.py. Запросsource_registry.list_enabled_in_groups()ДО импортаTrendingCrawlerвернёт пустой список. Порядок импортов вcollector.py::mainкритичен. - Дефолтный маппинг (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).
Подводные камни:
- Если деплой обновляет код до того, как PM2 перезапустит процесс с новой конфигурацией —
pm2попытается стартоватьpipelineпроцесс и упадёт (src/workers/main.pyнет). Деплой-скрипт должен делатьpm2 delete pipelineпередpm2 start ecosystem.config.js. - На SQLite оба процесса вызывают
Migrator().apply_pending()— race condition в теории возможен на самом первом запуске. На практике_migrationsPK-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_clustersJSON (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 modelqwen3.5:9b→gemma4: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_activethreading.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.py—auto_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_formation→evidence_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) ===:- Запрет использовать даты из «знаний» модели — только то, что явно в тексте.
- Если дату нельзя извлечь из текста —
null(всегда лучшеnull, чем галлюцинация). - Правила разрешения относительных выражений на русском/английском/японском с привязкой к
published_at. - Различение «новость о новом событии, ссылающаяся на старое» — старая дата только если явно указана.
-
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:4b→qwen3.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_idFK + backfillextraction_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
Изменения:
-
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 -
Translation watchdog:
_fill_fact_gaps()— перевод fact claims,_fill_event_gaps()— перевод event titles/summaries -
Fact GC (
_fact_gc()): еженедельная очистка orphaned фактов (source_count=1, confidence<0.5, age>30d, no links) -
Object recent facts: endpoint
GET /objects/{id}теперь возвращаетrecent_facts[](top 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
Решение:
- Migration 072:
fact_sources.extraction_confidence,predictions.matched_fact_id/event_id/matched_at/timeframe_days -
contribution_type:'supporting'→'evidence'+ добавлен'direct_projection'для прогнозных фактов -
prediction_matcher: при resolve записываетmatched_fact_id+matched_at -
insert_fact(): принимаетextraction_confidenceпараметр - Новые эндпоинты:
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
Изменения:
-
Публичные страницы объектов
/t/[slug]-[id]— 2370 объектов доступны поисковикам. Frontend: полная страница с трендами, сигналами, related. Backend: SEO middleware + sitemap (top 500) + robots.txt - Пререндер контента для ботов — middleware генерит полный HTML с объектом, сигналами, трендами и related links для Yandex/Google
-
Yandex оптимизация —
Host:директива в robots.txt, WebSite + SearchAction JSON-LD, FAQPage schema на use-cases -
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
Изменения:
-
hreflang
<link>теги в HTML — middleware инжектитen/ru/x-defaultдля ботов,usePageMetaсоздаёт для клиентов (с cleanup) -
Cache-Control headers —
_SPAStaticFilesдобавляет:immutableдля hashed assets,max-age=86400для images/fonts,no-cacheдля index.html -
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 Удалено:
-
api/routes/helpers.py— дубликат функции_translate_zone_names()(строки 131-182, идентичная копия 77-128) -
frontend-cascade/.../trends/content-plan-modal.tsx— неиспользуемый компонент (0 импортов) -
frontend-cascade/.../analysis/trend-to-zones-flow.tsx— неиспользуемый компонент (0 импортов) -
frontend-cascade/.../hooks/use-predictions.ts— неиспользуемый хук (0 вызовов) -
frontend-cascade/.../hooks/use-latest-analysis.ts— неиспользуемый хук (0 импортов) -
frontend-cascade/.../components/ui/tabs.tsx— shadcn Tabs wrapper (0 импортов изui/tabs) -
api/_gen_middleware.py,api/_write_middleware.py— локальные артефакты кодогенерации (не в git) -
.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.inserthack в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
Изменения:
- Trend Map — сворачиваемый блок (collapsed по умолчанию, состояние в localStorage)
- Убран gap между заголовком и контентом карты (Card
py-0 gap-0) - Тултип показывает описание тренда (из
trends.description, с переводом) -
showSignalsпередаётся через URL при переходе между трендами - Масштаб и размер узлов уменьшаются при малом количестве (≤3 → 0.6x, ≤5 → 0.8x)
-
count_related_trends_batch()— батч-подсчёт связанных трендов через FAISS - Все страницы с 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 из них — дубликаты.
Решение:
-
merge_similar_change_types()— embedding + cosine ≥ 0.85 + union-find + merge в canonical - Запуск в watchdog каждые 5 мин
- Fix TS:
mode: string→mode: '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 часов.
Решение:
- Увеличен batch_size до 30 (90 за цикл) в crawler
- Добавлен вызов
generate_trend_descriptions()в translation_watchdog (каждые 5 мин) - Итого: ~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 → сервер переставал отвечать.
Решение:
- Content-hash кеширование:
_content_hash()хеширует contributing trends,_load_previous_results()сравнивает с предыдущими — LLM вызывается только для изменённых зон -
_save_convergence_results()сохраняетcontent_hashколонку (idempotent ALTER TABLE) -
run_in_executorвapi/main.py— convergence loop больше не блокирует async event loop Подводные камни: При первом запуске после обновления все зоны будут без хеша → один полный LLM-проход, далее только изменения
v0.15.81 — 2026-03-25
[2026-03-25] refactor(routes): pulse → отдельный роутер, trends → trend, sidebar cleanup
Тип: 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 пунктов → скролл).
Решение:
- Pulse endpoints вынесены в отдельный
pulse_router→GET /api/pulse,GET /api/pulse/meta - Trends prefix переименован
/api/trends→/api/trend(единственное число) - Frontend api-client обновлён под новые пути
- Sidebar сокращён с 14 до 9 пунктов (Explore слит в Discover, убраны Convergence/Accuracy/Watchlist/Query)
- Polling backoff: вместо остановки при ошибках — прогрессивное замедление (60с → 120с → 300с)
- 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 парсер не обрабатывал массивы).
Решение:
- Тренды, ссылающиеся на orphan через
merged_into, теперь удаляются (вместо un-merge) -
query_jsonfallback теперь ищет 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
Решение:
- Two-tier polling:
GET /api/trends/pulse/meta(1ms, каждые 60с) → invalidate при измененииlast_updated - Live Feed Panel справа от heatmap (lg: side-by-side, mobile: 1 колонка) с ResizeObserver для выравнивания высоты
- FeedRow: 3 строки (score+name+time, description, badges NEW/UPD + phase + momentum)
- Server-side фильтрация live feed при клике на heatmap (зоны и категории)
- DigestCard теперь показывается для всех периодов включая 24ч
- Heatmap: все 5 категорий видны даже при 0 значениях (backend заполняет пустые дни/категории)
- fix(convergence):
send_message→client.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).
Решение:
- Migration 066: merge 37 exact + 21 semantic дублей (dual-threshold: name cosine >= 0.90 AND enriched text cosine >= 0.85)
- UNIQUE partial index на
objects.object_name(WHERE merged_into IS NULL) — предотвращает будущие exact дубли -
_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.pyFAISS 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 - Удалён
numpyimport и ~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(). EndpointsPOST/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_reasonsdict в 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. EndpointGET /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вZoneInfluencepydantic-схему - В оба 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и/trendsendpoints: 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-injectpreferred_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-sideanon_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 togglemin-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_SOURCESwhitelist, 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
[2026-03-21] feat(sources): Google Trends Daily Search Trends (RSS)
Тип: 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)
- Temporal weighting:
- 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.tsmethods (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
Новые фичи:
- Zone ID filter — фильтрация по zone_id (integer) вместо строкового имени зоны. Language-independent. Работает на /trends, /signals, /pulse.
-
Reports zone filter —
/reportsподдерживает фильтр по зоне влияния (через zone_recommendations). -
Zone resolution в анализах —
_resolve_analysis_zones()автоматически заполняет zone_id вanalyses.impact_zonesпосле завершения анализа (только когда trend_id установлен). -
API routes modularization —
api/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_id→object_idINTEGER FK → objects.id (переименование в v0.15.3) -
trend_idINTEGER FK → trends.id (добавлен как отдельная колонка, параллельно legacy TEXT trend_id) - Уникальный индекс
idx_analyses_trend_id_versionON (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.py—zone: str | None = Query(None) -
api/routes/signals.py—zone_id: int | None = Query(None) -
src/workers/crawler.py—get_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) ≠ legacytrend_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
Новые фичи:
-
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. -
Lab: Saved Products — Zustand store (
lab-saved-store→ localStorage), страница/saved, кнопка-закладка на карточках и странице продукта, навигация в sidebar/bottom-nav. -
Lab: trend_name перевод —
trend_nameдобавлен в_NEED_TRANSLATABLEдля needs endpoint.
Исправления:
- Reports сортировка по дате — ungrouped analyses дописывались в конец без пересортировки. Теперь оба режима (date/score) пересортируют объединённый список.
-
React hooks order —
crawler.tsx: useState/useEffect после early return;login.tsx: navigate() во время render → useEffect.products_.$productId.tsx: useSavedStore после early return. - Product card пустые заголовки — убран gradient placeholder при отсутствии image.
-
Product dimension scores —
score_market != null(0.0 тоже true) → проверка суммы > 0.
rebuild_analyses_search_text() — watchdog вызывает после перевода анализов для обновления search_text.
Подводные камни:
-
search_textbackfill при миграции = только 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
Исправления:
-
phase='new' → 'emerging' —
create_signals_from_sources()писал невалидную фазу,TrendPhase('new')бросал ValueError, 18/19 сигналов молча терялись. Добавлен fallback в crawler.py и миграция для исправления существующих записей. -
JOIN bug —
recommendation_store.pyиспользовалtr.id = t.idвместоtr.object_id = t.id. После migration 018 objects.id ≠ trends.id → все trend_momentum/composite были NULL. -
Shared report signal links — скрыты ссылки на сигналы на публичных отчётах (phantom searxng IDs → 404). Добавлен
showSignalLinkprop. -
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.
Новые фичи:
- Admin sources status — карточки источников показывают last_fetch_at (relative time), цветовой индикатор: зелёный (<2ч), красный (>2ч), чёрный (>24ч). Данные персистятся в DB.
-
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
feat: public report sharing (share links without auth)
Тип: 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
Проблема: Пользователь не мог поделиться отчётом анализа — все страницы требовали авторизации.
Решение:
-
Migration 019 — таблица
shared_reports(share_token PK, job_id UNIQUE, created_at, views_count). -
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}. -
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 для дедупликации кода отчётов.
feat: materialize signals & trends from analysis results
Тип: 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 пропускался, объект/тренд не создавался для новых тем.
Решение:
-
reclassify_single_object()вtrend_classifier.py— целевая реклассификация одного объекта (~8 SQL запросов вместо O(N*5) от полногоclassify_trends()). Обновляет агрегаты, upsert trends (dual-write), пересчёт scores. -
create_signals_from_sources()— score 0.1→0.3, confidence 0.3→0.4, добавленobject_class, добавленsignal_mappingsINSERT (dual-write gap fix). -
_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
Проблема: Пользователь не мог добавлять сигналы к тренду для отслеживания развития.
Решение:
-
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()после добавления. -
API:
POST /objects/{object_id}/signals(auth required). Schemas:CreateUserSignalRequest,CreateUserSignalResponse(signal_id, title, created). -
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.
Решение:
-
Migration 018 — создаёт
objects(backfill изtrends),object_signals(изsignal_mappingsпоtrend_idFK),object_aliases(изtrend_aliases). Добавляетobject_id,change_type,discovery_methodвsignalsиobject_id,change_direction,change_type,confirmed_atвtrends. 7 новых индексов. -
Object extractor — 3-field extraction:
object_name(что) +change_description(как) +trend_name(combined). Dual-write вobjects/object_signals+signal_mappings(legacy). -
Trend classifier —
_classify_via_objects()(new) /_classify_via_legacy()dispatch. TREND promotion ужесточён:signal_count ≥ 2 AND source_type_count ≥ 2. -
Все сервисы (scoring, descriptions, aliases, recommendations) — dual-read/write:
objectsfirst,trends/signal_mappingsfallback. Подводные камни:objects.id≠trends.id(независимые autoincrement). Всегда использоватьtrends.object_idFK для связи. 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 показывал устаревшие данные.
Решение:
-
Backend: удалены
calculate_urgency()иcalculate_quality()изscoring.py(~90 строк). Убраны из SQL INSERT/UPDATE вcrawler.py, изSignalSchema,AnalysisReportResponse. -
Frontend: удалён
UrgencyQualityIconsизsignal-view.tsx. Signal detail показывает Confidence вместо Quality. Sort optionqualityудалён. -
LLM prompts:
Срочность/Качество→Моментум/Значимостьв templates.agg_urgency/agg_quality→trend_momentum/trend_significanceв agent nodes. Подводные камни: Колонкиurgency_score/quality_scoreостаются в DB (nullable, deprecated). v1 API sortqualityудалён — 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).
Решение:
-
Shared components:
FilterChip(pill с count/disabled),FilterRow(ряд чипов с опциональным поиском),Popover(Radix UI wrapper). -
FilterRow searchable — для зон влияния:
<input>+ фильтрация чипов +max-h-[200px] overflow-y-auto. -
Все 4 страницы: inline-фильтры → кнопка
SlidersHorizontal+ badge с count активных + Popoversm:w-[560px] lg:w-[640px]. Search и View toggle остаются вне Popover. -
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-источники.
Решение:
-
SearXNG:
count_citations()возвращаетtuple[int, list[str]]— число + список URL. -
Crawler: сохраняет
web_citation_urlsвmetadata_jsonсигнала. -
API:
TrendDetail.web_citation_urls— агрегированные URL из всех сигналов тренда. -
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 был частично реализован но не интегрирован.
Решение:
- Lab API client — typed fetch wrapper для всех Lab endpoints.
-
10 react-query hooks —
useLabStats,useLabNeeds,useLabNeed,useLabProducts,useLabProduct,useCascadeTrends,useLabTrendStatus,useCreateNeed,useLabSSE,useDebounce. -
SSE pipeline progress —
GET /lab/needs/{id}/progressстримит прогресс пайплайна.PipelineProgressкомпонент с 4-step индикатором. -
Trends proxy —
GET /lab/trendsпроксируетget_trend_classes()без PAT auth, маппинг в Lab-формат. -
Need creation —
POST /lab/needsс dedup guard, 5-min cooldown, semaphore (max 5 concurrent). -
Translations —
langparam на всех Lab endpoints с кэшированными переводами. - Pagination — полноценная пагинация с sliding window (±2 pages).
-
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-цитированиях. Бэйджи фазы/рекомендации не имели тултипов.
Решение:
-
Object-Change model: отображение
object_name,change_description,change_typesна странице тренда. - Web citations: счётчик + кликабельные URL в секции Sources & Citations.
- Collapsible signals: список сигналов сворачивается/разворачивается.
-
Tooltips:
TrendScorePanel— тултипы на бэйджах phase и recommendation с описанием из i18n. -
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 (объект). Не было модалки деталей.
Решение:
-
Backend:
GET /objects(список) +GET /objects/{id}(детали с сигналами и трендами) —objects_routerчитает изobjects+object_signals+trends. -
Frontend:
useObjects()/useObject()хуки,ObjectDetailModal— модалка по клику на карточку (вместо навигации). - Карточка: заголовок =
object_name, убраноchange_description(атрибут тренда, не объекта). Подводные камни:signalsтаблица НЕ имеетupdated_at— использоватьfetched_at.objects.id≠trends.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 блокирует взаимодействие. Не было анимации разворачивания.
Решение:
- Установлен
vaul(drawer library) — bottom sheet с drag-handle и swipe-to-close. -
drawer.tsx— shadcn-style Drawer компонент (vaul primitive). -
useIsMobile()— хук определения мобильного экрана (< 640px, sm breakpoint). -
ObjectDetailModalиRecDetailModal— responsive:Drawerна мобиле,Dialogна десктопе. Общий контент вынесен в shared-функцию. - 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 и т.д.) непонятно что они означают.
Решение:
-
Migration 017 —
ALTER TABLE zone_recommendations ADD COLUMN zone_id INTEGER+ backfill изimpact_zones_dictionary(case-insensitive match). FK формализует связь zone_recommendations → impact_zones_dictionary. -
save_recommendations() — при INSERT теперь резолвит
zone_idиз словаря (lookup cache для производительности). -
Hierarchical filter counts — новый Level 2.5
zones(между categories и priorities) в_compute_filter_counts(). Zone filter пробрасывается в Level 3 (priorities) и Level 4 (timeframes). -
Zone translation —
zone_nameдобавлен в_translate_recommendations()и_fill_recommendation_gaps()(watchdog). Endpoint возвращаетzone_labelsdict (original_en → translated) вfiltersдля фильтр-чипов. -
Zone FilterRow — иерархический фильтр category → zone на странице
/recommendations. При смене category/role зона сбрасывается. Переведённые лейблы черезzone_labels. -
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_nameTEXT (не 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) не имели пояснений.
Решение:
- CSS-only тултипы через
.badge-tipclass (data-tip+::afterpseudo-element). - Все 8 badge-компонентов обновлены с
data-tip+ i18n. -
ScoreBadgeполучилtipKeyprop (trend score vs viability score). -
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 или инвестору.
Решение:
-
LLM-агент
recommendations_agent()(Haiku) — генерирует рекомендации по 4 ролям (CTO, Developer, PM, Investor) для каждой зоны влияния. ПромптP_REC_ZONE_ROLESна английском. -
Pipeline-интеграция — Step 5.5 в
analysis_runner.py, после генерации отчёта. Контролируется настройкойrecommendations_enabled. -
Scoring —
rec_score(0-100) = trend_composite×0.50 + zone_impact×10×0.25 + priority_w×0.15 + timeframe_w×0.10. Вычисляется на лету, не хранится. -
Standalone страница
/recommendations— role tabs, score groups (Act Now ≥70 / Plan For 40-69 / Monitor <40), карточки с trend scores (S/M/C bars), модальное окно деталей. -
Inline-таб в отчёте анализа —
zone-recommendations-view.tsxвнутри report tabs. -
Hierarchical filters — иерархия role → category → priority → timeframe, каждый уровень считает с применением фильтров сверху.
totalsдля "All" badge. -
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 не отображались в админке. Все настройки были на одной странице.
Решение:
- Разделение admin/system на две страницы: System (auto-analysis, recommendations, translations, descriptions, eval) и Crawler (sources, collection, external API).
-
Recommendations settings — toggle
recommendations_enabled+ кнопка "Regenerate All" (POST /admin/actions/regenerate-recommendations). - Auto-analysis settings — toggle + max_per_day (1-50) + depth (1-7).
-
Backend —
recommendations_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)
Проблема: Нет инструмента для извлечения пользовательских болей из трендов и генерации бизнес-идей.
Решение:
-
Backend —
api/lab/: needs extraction (P5), product ideation (P6), pipeline engine, SQLite store (needs_repo, products_repo), routes. -
Frontend —
frontend-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
feat: extend API sort options + exclude IGNORE from top trends
Тип: 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
feat: lower recommendation thresholds + add sort_by to trends API
Тип: 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 строк) Что сделано:
-
DB-1 (Migration 012): Пересоздание
trendsсclass_name UNIQUE COLLATE NOCASE— исправлена корневая причина case-дубликатов. Убраны 5 workaround'овCOLLATE NOCASEизtrend_classifier.py. -
BE-2:
_row_to_item()переведён с позиционных индексовrow[23]на именованный доступrow["object_class"]черезsqlite3.Row. Любое изменениеSIGNAL_COLSтеперь безопасно. -
DB-3 (Migration 013): Удалены мёртвые
agg_*колонки (agg_score/urgency/quality/velocity/recommendation) из: DB, scoring, classifier, routes, schemas, frontend types. LLM-промпты получают значения через вычисление изtrend_*. -
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):
-
Перевод отчётов на английский (v0.6.2): отчёты генерируются на русском, но система считала их английскими. Теперь Phase 6 + watchdog определяют язык отчёта и переводят на ВСЕ другие языки.
-
No LLM on READ path (v0.6.3): убран on-demand LLM вызов из
get_report— только cache lookup. Переводы заполняются Phase 6 и watchdog. -
Перевод карточек трендов (v0.6.7): watchdog не переводил
trends.class_nameиdescription— только signal titles. Добавлен_fill_trend_class_gaps()для автоматического перевода трендов. -
Chunking для CLI (v0.6.8): 1808 трендов одним батчем → промпт 332K символов →
[Errno 7] Argument list too long. Разбито на батчи по 30 штук. -
Analytics (v0.6.7): интеграция с Umami в
index.html. -
Test fixtures (v0.6.4): добавлена таблица
signal_mappingsв тестовые фикстуры.
Подводные камни:
-
translate_trend_items()через CLI: промпт > 100K символов →Argument list too long. Всегда чанкить по 30 items. - Правило: No LLM calls on READ path —
get_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) Что добавлено:
-
Personal Access Tokens (PAT) — создание, отзыв, просмотр через JWT-защищённые эндпоинты
/auth/pat/ -
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— пагинированный список сигналов
-
- Rate limiting per PAT (sliding window, настраиваемый через settings)
-
Feature toggle —
external_api_enabledв settings (по умолчанию выключен) - Frontend PatManager — компонент для управления токенами на странице профиля
-
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 Проблема:
- Фильтры на Explore (Status, Category, Sort) не помещались на экран 375px — лейблы
min-w-[5rem]отнимали 80px, сегменты не скроллились - Anthropic API 500 (
api_error,internal server error) не ретраился — клиент падал сразу - Русский поиск возвращал 0 результатов из-за зомби-процессов на порту 8000
Решение:
-
Mobile filters: лейблы
hidden sm:block(скрыты на мобильных), строкиflex-col→sm:flex-row, сегменты/sortoverflow-x-auto scrollbar-none -
Retry: добавлены
"internal server error","api_error","server error"вRETRYABLE_ERRORS— до 3 попыток с exponential backoff -
Search: причина — зомби-процессы. Решение:
taskkill //F //PIDдля всех процессов на порту + очистка__pycache__
Подводные камни:
- На Windows при рестарте сервера ВСЕГДА убивать ВСЕ процессы на порту (
netstat -ano | grep :8000→taskkillкаждого) - 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 Изменения:
-
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()для пере-категоризации существующих сигналов. -
Radar list view: реализовано табличное представление (
TrendRowкомпонент) — grid/table toggle теперь работает. Поиск на всю ширину, фильтры категорий на отдельной строке с count badge, все 5 категорий всегда видны. -
Analysis navigation: back-кнопки в report/progress ведут на
/reports(список анализов). Completion redirect всегда на report page. - Auth: token refresh hook, улучшенные error messages в api-client.
- Admin: settings routes, system admin page additions.
- 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 Изменения:
- Radar: TrendCard горизонтальный layout (scores слева, текст справа), фиксированная высота h-[168px], пагинация PAGE_SIZE=12
- Explore: SignalView lg фиксированная высота h-[280px], linked trend block side-by-side с метриками
-
Signals/Radar: formatClassName() для camelCase→Human, backend
langparam на /trends и /trends/weak, кэшированный перевод class_name/description - Trend detail: выравнивание высоты колонок (h-full на Stagger, TrendScorePanel, ObjectInfoPanel)
- Analysis redirect: navigate на /analyze/jobId после запуска, возврат на /trend/trendId после завершения
- Saved page: 3 секции (Тренды + Сигналы + Анализы), savedTrendClasses в store
- 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
#f59e0b→var(--impact-high),#6366f1→var(--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()для генерации отчётов на выбранном языке; удалён debugconsole.logиз api-client -
P1 Дедупликация:
formatRelativeTime()→lib/utils.ts(из 2 файлов);RECOMMENDATION_COLORS→lib/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,AnalysisJobinterface,generate_source_urls()(backend) -
UI: Исправлен постоянный скроллбар (
overflow-y: scroll→auto) -
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).
Решение:
- Добавлена оценка стоимости по прайсу модели (
_estimate_cost()) — sonnet: $3/$15 per 1M tokens, haiku: $0.80/$4, opus: $15/$75. Используется как fallback если CLI не вернулcost_usd. -
_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 табами.
Решение:
- Извлечены 2 shared компонента:
ReportTabs(5 табов) иReportActionBar(deepen/expand/re-analyze + слоты) - Report page и Trend page используют одни компоненты — нет дублирования кода
- Slot pattern:
versionNav(pills на trend, chevrons на report) иcompareButton(только report) -
startInlineAnalysisпринимает{ depth, timeHorizon, parentJobId }для deepen/expand inline
Подводные камни:
- ReportActionBar использует
DEPTH_STEPSиHORIZON_STEPS— изменять только там - При добавлении нового таба — обновить
ReportTabIdtype и массивtabsвreport-tabs.tsx
v0.4.2 — 2026-02-17
Fix: Analyses not linked to trends (trend_class_id missing)
Тип: 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:
-
trend.$trendId.tsx:328— кнопка "Analyze Best" передавалаtrend_class_id: undefinedвместоString(data.id) -
auto_analyzer.py:100—reserve_analysis()вызывался безtrend_class_id, хотя значение было доступно
Решение:
- Передаём
trend_class_id: String(data.id)при навигации на/analyzeсо страницы тренда - Передаём
trend_class_id=trend_class_idвreserve_analysis()в auto_analyzer - 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 каждое):
-
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)
-
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 по категориям)
-
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.py—TrendSummary+ 7 новых полей -
api/routes.py—_dict_to_trend_summary()helper, дедупликация 3 конструкторов -
frontend-cascade/app/src/types/trend.ts—TrendPhase,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 фаз:
-
SearXNG адаптер (
src/services/adapters/searxng.py): добавленыfetch_news(),count_citations(),_is_aggregator(),_clean_url() -
Config (
src/config.py):SearXNGConfig.enabled=True,min_citation_threshold=5,WEB_CITATION_BASELINES(alias для TAVILY_CITATION_BASELINES),TavilyConfig.enabled=False -
Scoring (
src/services/scoring.py):tavily_citations→web_citationsв CATEGORY_WEIGHTS (все 5 категорий), dual-read для обратной совместимости -
Crawler (
src/workers/crawler.py):tavily→searxngв fetch_news/enrichment, импорт searxng вместо search/metasearch -
API routes (
api/routes.py):searxng:иtavily:→"web"ключ в sources dict -
Frontend:
tavily→web/searxngв types, i18n, компонентах, report page -
Удалены:
search.py,tavily.py,metasearch.py,tavily-pythonиз requirements.txt -
Тесты:
test_tavily_citations_e2e.py→test_web_citations_e2e.py, все assertions обновлены -
Доп. файлы:
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_adapterkwarg оставлен как 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.
Фазы:
-
DB миграция (
db/migrations/005_signal_trend_rename.py):trending_items→signals,trend_classes→trends,trend_objects→signal_mappings,class_aliases→trend_aliases,trend_snapshots→signal_snapshots -
Backend скоринг (
src/services/trend_scoring.py):TrendAggregateScorer— агрегированный скоринг трендов. Авто-анализ при переходе SIGNAL→TREND -
API эндпоинты:
/signals,/trends,/analyses(бывшие/trends,/trends/objects,/trends/analyze) -
Frontend типы:
TrendItem→Signal,TrendClass→Trend, API client + hooks переименованы -
Frontend компоненты:
TrendView→SignalView,TrendCard→SignalCard,TrendClassCard→TrendCard,TrendTable→SignalTable -
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.$trendId→signal.$signalId, потомobjects.$classId→trend.$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()
Проблема:
-
CATEGORY_WEIGHTSдублировались в scoring.py и не синхронизировались сTAVILY_CITATION_BASELINESиз config.py - Extraction engagement метрик дублировался в 3 местах (calculate_weighted_engagement, calculate_velocity_from_snapshots, calculate_quality)
- Нет валидации score ranges и консистентности категорий
Решение:
- Перенесён
CATEGORY_WEIGHTSвsrc/config.py(single source of truth рядом сTAVILY_CITATION_BASELINES) - Добавлена валидация
_validate_scoring_config()на уровне модуля:- Проверка идентичности ключей категорий (CATEGORY_WEIGHTS
↔️ TAVILY_CITATION_BASELINES) - Проверка суммы весов (должна быть 1.0 ± 0.01)
- Проверка идентичности ключей категорий (CATEGORY_WEIGHTS
- Извлечена общая функция
_extract_engagement(item: TrendItem) -> dict[str, float]:- Возвращает
{"saves", "shares", "comments", "likes", "total_weighted"} - Использует
ENGAGEMENT_WEIGHTSдля взвешенного подсчёта - Заменяет 3 дублирующих блока кода
- Возвращает
- Добавлена функция
_clamp_score(value, min_val=0.0, max_val=1.0)для нормализации финальных scores - Обновлены методы:
-
calculate_weighted_engagement()— теперь вызывает_extract_engagement() -
calculate_velocity_from_snapshots()— inner functionget_engagement()использует_extract_engagement() -
calculate_quality()— использует_extract_engagement()вместо inline extraction -
calculate_urgency(),calculate_time_decay()— используют_clamp_score()вместо inlinemax(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, инфраструктура).
Решение:
- Проведён полный аудит 4 командами: backend API, pipeline (crawler/agents/services), frontend (React), инфраструктура (deps/tests/security)
- Составлен план из 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 гипотезы (подтверждена)
Проблема:
- Scoring-система работает только для HN tech-трендов (3.2/10): Tavily score passthrough, arXiv пустые метаданные, категория "society"/"social" mismatch
- Алгоритм наименования v1 генерировал красивые названия для не-трендов (25% элементов — не тренды)
- v2 с поштучным Step 0 фильтром отбрасывал 55% элементов, включая валидные части трендов (продуктовые релизы Claude/GPT/Voxtral — каждый по отдельности не тренд, но вместе = "специализация frontier LLM")
Решение (Object-as-Trend подход):
- Object extraction — для каждого элемента извлекаются конкретные сущности (продукты, технологии, компании)
- Group by class — объекты группируются в классы (LLM-роутинг, code review, frontier LLM)
- Class-level is_trend — тренд определяется на уровне класса (count >= 2), а не отдельного элемента
- 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
Проблема:
- Нет способа измерить качество LLM-генерации заголовков
- При изменении промптов невозможно обнаружить регрессию
- Нет административного интерфейса для системных операций
Решение:
- Eval Pipeline: 15 fixtures → генерация → LLM-as-Judge (4 критерия × 3 варианта) → JSON результат
- Regression Detection: сравнение с baseline, порог -0.2 для критической регрессии
-
Admin UI (
/admin/system): запуск eval, прогресс-бар, карточки метрик, история прогонов -
CLI:
scripts/eval_titles.pyдля CI и ручного запуска
Подводные камни:
- Eval jobs хранятся в памяти (
_eval_jobs) — не переживают перезапуск сервера - Judge может давать нестабильные оценки — рекомендуется запускать несколько прогонов
-
call_async→queryбаг исправлен в 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_async → query в обоих местах.
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
Проблема:
- Заголовки трендов идут raw от адаптеров без обработки ("Show HN: My Cool Project " → с мусором)
- Один тренд из разных источников (HN, GitHub, Tavily) создаёт дубликаты — нет связывания
- Нет хранилища для нескольких источников одного тренда (sources реконструировались на лету из item.id)
- Экспертная панель (6/6) назвала формулировки трендов главным UX-барьером
Решение:
-
Title Normalization (
normalize_title()): HTML unescape, Unicode NFKC, удаление HN-префиксов (Show HN/Ask HN/...), коллапс пробелов -
Cross-Source Dedup (
find_similar_trend()): fuzzy match по заголовку черезdifflib.SequenceMatcher(threshold 0.75). Точное совпадение → быстрый путь, иначе сравнение со всеми трендами - trend_sources таблица: хранит множественные источники для одного тренда с UNIQUE(trend_id, source_item_id). Миграция из существующих данных
- Dual-Layer Naming: 3 колонки в trending_items (title_technical, title_accessible, title_benefit). LLM генерирует batch по 10 трендов через ClaudeCLIClient
-
Skip-логика:
WHERE title_accessible IS NULL— повторная генерация не запускается -
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— добавлены русские переводы
Проблема:
- Zones таб показывал карточки в сетке — мелкий текст, плохая читаемость
- Нет навигации по зонам при большом количестве (>10)
- Нет иерархии: зоны → подзоны → эффекты
- На мобильных экранах карточки слишком компактные
Решение:
-
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
-
Типография (крупнее для читаемости):
- 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
- Zone header:
-
Иерархия:
- Zone header (название, тип, временной горизонт, impact/confidence)
- Description (параграф)
- Mechanism (отдельная секция)
- Sub-zones (сетка 1-2 колонки)
- Affected Groups (теги)
- Evidence (список с Quote icons)
- Cascade Effects (collapsible tree, рекурсивный CascadeTreeItem)
-
Адаптивность:
- Desktop: TOC слева + контент справа
- Mobile: TOC dropdown + контент на всю ширину
- Карточки зон с border-radius, padding, shadows
Подводные камни:
- IntersectionObserver может неправильно работать при быстром скролле — использован rootMargin для компенсации
- TOC sticky может конфликтовать с fixed header — добавлен
top-4offset - На очень длинных списках зон (>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— добавлены русские переводы
Проблема:
- Summary таб показывал только плоский markdown без структуры
- Нет визуальной связи между трендом и зонами влияния
- Отсутствует явный обзор тренда перед детальным анализом
- Пользователю сложно понять, как тренд влияет на различные области
Решение:
-
SummaryView компонент — трёхсекционная структура:
- Trend Overview: Карточка с описанием тренда, категорией, статусом
- Impact Mapping: Визуализация TrendToZonesFlow — как тренд влияет на зоны
- Detailed Analysis: Markdown отчёт с источниками (прежний ReportViewer)
-
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
-
Интеграция:
- Заменён старый простой markdown в summary tab на SummaryView
- Поддержка streaming через
isStreamingprop - Сохранена анимация StreamingText для первого просмотра
- 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
Проблема:
- Старый граф (react-force-graph-2d) перемещался под мышкой
- Нет весов на рёбрах
- Нет направленности (стрелок)
- Сложная навигация
Решение:
-
React Flow интеграция:
- Установлен пакет
reactflow(37 deps, 0 vulnerabilities) - Создан компонент
CascadeGraphс TypeScript типизацией
- Установлен пакет
-
Функционал:
- Направленные рёбра (MarkerType.ArrowClosed)
- Веса на рёбрах (% strength labels)
- Анимация для сильных связей (strength > 0.7)
- Цветовая кодировка узлов (positive=green, negative=red, mixed=amber)
- Impact display на каждом узле
- MiniMap для навигации
- Controls (zoom, fit view)
- Background grid
- Layout: Simple grid layout (5 columns), можно улучшить через dagre
- Интеграция: Заменён 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
Проблема:
- Зоны не канонизировались — каждый анализ создавал свои названия
- Нет поддержки локали — все отчёты генерировались на EN
- Зоны и каскадные эффекты не сохранялись в БД для обратного поиска
Решение:
-
Zone canonicalization (Issue #5, Phase 2):
-
_canonicalize_zones()вimpact_researcher_agent - Автоматический маппинг raw zone names → canonical names через ZoneMatcher
- Сохранение
original_nameдля reference
-
-
Locale parameter (Issue #6-7):
- Добавлено поле
localeв AnalyzeRequest (default="en") - Передача locale через state в LangGraph pipeline
-
report_generator_agentдобавляет language instruction в промпт - Колонка
localeв таблицеanalyses(migration)
- Добавлено поле
-
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— инициализация таблицы при старте
Проблема:
- Зоны влияния переписывались при каждом анализе (нет канонических названий)
- Похожие зоны (synonyms) создавались как отдельные записи
- Нет справочника для consistency across analyses
- Невозможен поиск/фильтрация по зонам
Решение:
-
Таблица БД:
impact_zones_dictionaryс полями:- canonical_name (уникальное каноническое название)
- synonyms (JSON array синонимов)
- embedding (BLOB для vector matching)
- category, description, usage_count
-
ZoneMatcher class:
-
match_or_create_zone()— главный метод (поиск или создание) -
_embed_zone_name()— генерация embeddings (пока TF-IDF style) -
_find_similar_zones()— cosine similarity с threshold=0.85 -
_increment_usage()— счётчик использований + синонимы
-
-
API endpoints:
-
GET /zones/dictionary— список зон -
GET /zones/dictionary/{id}— детали зоны -
GET /zones/search?q=...&category=...— поиск
-
- 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
Проблема:
- Источники не ранжировались по релевантности/авторитетности
- Все источники показывались в порядке сбора (не оптимально)
- Нет приоритета для более авторитетных источников (GitHub, HN vs Tavily)
- Нет учёта упоминаний источника в тексте отчёта
Решение:
-
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 в тексте
- Интеграция: После validation + deduplication в _run_analysis()
- Metadata propagation: data_collector_agent передаёт metadata (citations)
- 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()
Проблема:
- Некорректные URL (с localhost, опасными расширениями) могли попасть в sources
- Дубликаты URL (с разными utm params, trailing slash) не фильтровались
- Один URL мог быть представлен несколькими вариантами
Решение:
-
SourceExtractor.validate_url():
- Проверка схемы (только http/https)
- Блокировка localhost/internal IPs (127.0.0.1, 192.168., 10., etc.)
- Блокировка опасных расширений (.exe, .msi, .bat, .zip, etc.)
-
SourceExtractor.deduplicate_sources():
- Нормализация URL: lowercase domain, strip trailing slash
- Удаление utm_* query params
- Удаление fragment (#)
- Сохранение первого вхождения
-
Интеграция: После сборки 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— увеличен лимит источников
Проблема:
- Citation threshold был одинаковым (5) для всех категорий трендов
- Источники ограничивались первыми 10 items из collected_items
- Science тренды требуют более строгой валидации, social — менее строгой
Решение:
-
Гибкие пороги: Добавлены category-specific thresholds:
- science: 8 (академические тренды)
- technology: 5 (средний)
- business: 4 (ниже)
- economy: 6 (средне-высокий)
- society: 3 (вирусный контент)
-
Auto-detection: Метод
_detect_category_from_query()определяет категорию по ключевым словам -
Больше источников:
-
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). Это добавляло лишний шаг к основному сценарию — открыть последний отчёт.
Решение:
- Заменили клик по всей карточке на Link к
/analyze/${latest_job_id}/report - Добавили отдельную кнопку (иконка History) для раскрытия истории версий
- Кнопка использует
stopPropagation()чтобы не тригерить навигацию - Добавлены 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.tsxfrontend-cascade/app/src/routes/_dashboard/analyze.$jobId.report.tsxapi/routes.py
Проблема:
- Кнопки Save/Share/Export исчезали за правой границей на узких экранах (страница тренда)
- График Sparkline мешал восприятию на странице тренда
- Оценка анализа показывала 1 вместо ожидаемых 7-8
- Confidence display был отдельным блоком вместо части блока оценки
- Дублирование кнопок действий на странице анализа (ReportHeader + ReportActions)
- 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", хотя перевод уже был закэширован в БД.
Решение:
- Перемещен batch-перевод items ПЕРЕД поиском (вместо ПОСЛЕ пагинации)
- Создан кэш переводов
{item.id → translated_dict}для переиспользования - Поиск проверяет ОБЕИХ версии:
- Оригинал (EN):
title,description - Перевод (RU):
translated.title,translated.description
- Оригинал (EN):
- Если query найден в любой версии — item попадает в результаты
- Финальный ответ использует уже готовый кэш переводов (без повторного вызова)
Производительность:
- 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 постоянно перестраивался и "прыгал". Пользователю сложно было печатать, так как результаты обновлялись на каждый символ.
Решение:
- Создан универсальный хук
useDebounce<T>(value, delay)с default delay 1000ms - В Radar page:
const debouncedSearch = useDebounce(searchInput, 500)+useEffectдля синхронизации в filters - В Command Palette:
const debouncedSearch = useDebounce(searchValue, 500)+ передача в react-query - 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 через
useEffectcleanup - Задержка подобрана экспериментально: 500ms — баланс между отзывчивостью и снижением нагрузки
- Переменная
target_langтеперь объявлена в начале функции (до search блока), не дублировать перед финальным ответом
ReportHeader: description, score breakdown tooltip, source links fallback
Тип: feature Файлы:
-
api/analysis_store.py—get_trend_metadata()иget_trend_metadata_by_id()теперь SELECT-ятcontentизtrending_itemsи возвращаютtrend_description -
api/schemas.py— добавлено полеtrend_description: str = ""вAnalysisReportResponse -
api/routes.py—get_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.tsx—ReportHeader: описание тренда, 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
Проблема:
- Описание тренда (
contentизtrending_items) не отображалось на странице отчета - Оценка (score) показывалась как число без пояснения, из чего она складывается
- При отсутствии
trend_sourcesно наличииsource_urlне было fallback-ссылки на источник
Решение:
- Backend: добавлен SELECT
contentв обоихget_trend_metadata*(), передается какtrend_description(обрезан до 500 символов) - Frontend: score обернут в Tooltip с разбивкой (trend_score, urgency, quality, confidence)
- Frontend: trend_score показывается с tooltip (velocity, source diversity, cross-domain impact)
- Frontend: fallback-ссылка
<a href={source_url}>когдаtrend_sourcesпуст
Подводные камни:
-
contentвtrending_items— это не всегда чистое описание; для HN это может быть текст поста, для GitHub — README excerpt -
trend_scoreв API приходит 0-1, analysisscore— 0-10, нормализация в tooltip:trend_score * 10
Comprehensive Scrollbar Fix — Single Scroll Container
Тип: fix Файлы:
-
frontend-cascade/app/src/globals.css—html,body,#root { height:100%; overflow:hidden }+.dashboard-main-scrollclass -
frontend-cascade/app/src/routes/__root.tsx—h-fullвместоmin-h-screen -
frontend-cascade/app/src/routes/_dashboard.tsx—dashboard-main-scrollна<main> -
frontend-cascade/app/src/routes/admin.tsx— тот же паттерн -
frontend-cascade/app/src/routes/login.tsx—h-fullвместоmin-h-screen
Проблема: Двойной скроллбар и дёрганье контента (layout shift) при загрузке карточек на радаре. Предыдущие фиксы (scrollbarGutter:stable на main, h-screen overflow-hidden на wrapper) не помогли — скролл появлялся на html/body.
Решение:
- Lock
html, body, #root { height: 100%; overflow: hidden }— страница никогда не скроллится -
<main>— единственный scroll-контейнер сoverflow-y: scroll(всегда видимый трек) -
scrollbar-gutter: stable+ thin custom scrollbar (6px) через::-webkit-scrollbar - Все 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 (все тренды, а не отфильтрованные по категории).
Решение:
-
Split queries:
statsQueryучитывает category + search, но НЕ status.trendsQuery— все фильтры - Backend reorder: category/search фильтрация ДО подсчёта stats, status фильтрация ПОСЛЕ
- AnimatedCounter: анимация от предыдущего значения (не от 0), skip при одинаковых значениях
- 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:
-
_batch_translate_textsиспользовал маркеры[N], которые конфликтовали с содержимым типа[10],[2024]в описаниях → regex ломал парсинг - Crawler
_pre_translate_itemsотправлял ВСЕ описания в одном LLM-запросе → output truncation
Решение:
- Заменили маркеры на
<<<N>>>и regex<<<(\d+)>>>— не конфликтуют с контентом - Добавили BATCH_SIZE=30 для chunked LLM calls
- Используем
get_cached_translations_batch()для проверки уже переведённых текстов перед вызовом LLM
Подводные камни:
- Маркеры
<<<N>>>должны быть уникальны и НЕ встречаться в обычном тексте - При batch_size слишком маленьком — больше LLM вызовов; слишком большом — truncation
TrendView: Unified Component with Variants
Тип: refactor Файлы:
-
frontend-cascade/app/src/components/trends/trend-view.tsx— NEW: 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.py— NEW: 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.py— NEW: фоновый 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.py— NEW: SQLite tabledaily_credits, функцииcheck_credits(),use_credit() -
api/main.py— инициализация таблицы при старте -
api/routes.py— auth guard + credit deduction наPOST /trends/analyze -
api/auth/routes.py—GET /auth/creditsendpoint -
frontend-cascade/app/src/lib/api-client.ts—getCredits()метод -
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.tsx— NEW: страница сравнения двух версий анализа -
frontend-cascade/app/src/components/analysis/diff-report-viewer.tsx— NEW: Unified/SideBySide отображение diff -
frontend-cascade/app/src/components/analysis/zones-diff.tsx— NEW: сравнение impact zones с fuzzy matching -
frontend-cascade/app/src/lib/diff-engine.ts— NEW: paragraph-level diff + word-level highlights (Jaccard similarity) -
frontend-cascade/app/src/i18n/en.json,ru.json— ключиcomparison.*
Проблема: После добавления версионности анализов не было возможности увидеть, что именно изменилось между версиями.
Решение:
- Страница
/analyze/$jobId/compare?with=$otherIdзагружает оба отчёта -
diffMarkdown()— paragraph-level diff с word-level подсветкой изменений -
diffImpactZones()— fuzzy matching зон по названию (Jaccard threshold) - Два режима: Unified View и Side-by-Side View
- 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 успевал прочитать, фронтенд закрывал страницу не дождавшись перевода.
Решение:
- Заменили
_progress: dictна_progress_events: dict[str, list[dict]](append-only list) - SSE generator использует cursor — никогда не пропускает события
-
_update_progress()добавляет событие в список, не перезаписывает -
/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— удалён неиспользуемый importAnalysisVersionSummary -
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.py—reserve_analysis(),complete_analysis(),fail_analysis(),find_analyses_by_trend(),list_analyses_grouped(), ALTER TABLE миграции -
api/schemas.py—AnalysisVersionSummary,TrendAnalysesResponse,GroupedAnalysisSummary,GroupedAnalysisListResponse, расширеныAnalysisReportResponseиAnalysisSummary -
api/routes.py— атомарное резервирование версий, валидацияparent_job_id,depthclamping [1-7], обновлёнGET /analyses/by-trend/,GET /analyses?grouped=true -
frontend-cascade/app/src/types/analysis.ts—AnalysisVersionSummary,TrendAnalysesResponse,GroupedAnalysisSummary,getVersionType() -
frontend-cascade/app/src/lib/api-client.ts—getAnalysesForTrend(),getGroupedAnalyses(),parent_job_idвanalyzeTrend() -
frontend-cascade/app/src/components/analysis/analysis-timeline.tsx— NEW: Timeline на странице тренда -
frontend-cascade/app/src/components/analysis/version-type-badge.tsx— NEW: INITIAL / RE_ANALYZED / DEEPENED badge -
frontend-cascade/app/src/components/analysis/version-nav-bar.tsx— NEW: Prev/Next навигация на report page -
frontend-cascade/app/src/components/analysis/score-delta-strip.tsx— NEW: Дельты Score/Conf/Zones/Depth -
frontend-cascade/app/src/components/analysis/report-group-card.tsx— NEW: 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
Проблема: Каждый анализ тренда — одноразовый. Нет истории, нет версионности, нет возможности углубить анализ.
Решение:
-
Atomic version reservation:
reserve_analysis()сBEGIN IMMEDIATEгарантирует уникальность(trend_id, version)при параллельных запросах - 3-phase lifecycle: reserve → complete/fail (вместо save_analysis)
-
UNIQUE constraint на
(trend_id, version) WHERE trend_id IS NOT NULL - Version type: INITIAL (v1), RE_ANALYZED (v>1, no parent), DEEPENED (has parent_job_id)
- Frontend: Timeline на тренде, VersionNavBar + ScoreDeltaStrip на отчёте, grouped reports page
-
API:
GET /analyses/by-trend/{id}→ все версии,GET /analyses?grouped=true→ сгруппированный список
Подводные камни:
-
reserve_analysis()ловитIntegrityErrorи делает retry +1 (race condition safety net) -
getAnalysisForTrend()deprecated → используйgetAnalysesForTrend()(list) -
depthclamped 1-7 на API уровне -
parent_job_idвалидируется: должен существовать в БД и принадлежать тому жеtrend_id - Старые анализы без
trend_id→ version=1, не группируются
Trend-Analysis Linking by ID
Тип: feature Файлы:
-
api/analysis_store.py—get_trend_metadata_by_id(),save_analysis()с trend_id -
api/routes.py—AnalyzeRequest.trend_id, обновлёнget_report() -
frontend-cascade/app/src/lib/api-client.ts—trend_idвanalyzeTrend() -
frontend-cascade/app/src/routes/_dashboard/analyze.tsx—validateSearchс trend_id -
frontend-cascade/app/src/routes/_dashboard/trend.$trendId.tsx— передача trend_id при навигации
Проблема:
Связь тренда с анализом была через trend_title, что ломалось при:
- Разных языках (русский title в анализе, английский в trending_items)
- Изменении title после анализа
- Дублях названий
Решение:
- При запуске анализа с карточки тренда передаём
trend_idчерез URL params -
save_analysis()сохраняетtrend_idнапрямую в БД -
get_report()получает метаданные поtrend_id(primary), fallback на title - 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.ts—descriptionв 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 сохранял его как rationale → description, но frontend не передавал поле.
Решение:
- Добавили
description?: stringв типAnalysisReport.graph.nodes - Добавили
description: n.descriptionв mapping graphNodes - 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.tsx—getSourceUrl() -
frontend-cascade/app/src/components/trends/trend-table.tsx— подсчёт источников -
frontend-cascade/app/src/routes/_dashboard/trend.$trendId.tsx—totalSources(), отображение ссылок
Проблема:
Поле sources содержало синтетические числа ({github: 1, hn: 2}), а не реальные ссылки.
Пользователь ожидал кликабельные ссылки на каждый источник.
Решение:
- Изменили тип
sourcesсdict[str, int]наdict[str, list[str]] - Каждый ключ теперь содержит массив реальных URL
- Удалили поле
source_urls— теперь всё вsources - Frontend использует
sources?.[type]?.[0]для получения первой ссылки - На странице деталей —
.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 "насколько качественный тренд".
Решение:
-
Urgency (0-100): Как срочно нужно реагировать
- Формула: velocity × time_decay × engagement
- Высокий = быстро растёт, нужно действовать сейчас
-
Quality (0-100): Насколько надёжный/качественный тренд
- Формула: source_coverage × confidence × data_completeness
- Высокий = много источников, проверенные данные
-
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()) - Нет валидации "трендовости" — любой результат считался трендом
Решение:
-
API Improvements:
topic="news",search_depth="advanced",days=3 -
Aggregator Filter:
_is_aggregator(url)фильтрует категории и главные страницы -
Date Parsing:
_parse_date()извлекает реальную дату публикации -
Citation Validation:
count_citations(title)считает уникальные домены - Threshold: Только items с >= 5 уникальных доменов проходят валидацию
-
Scoring: Новый сигнал
tavily_citationsс весом 0.15 во всех категориях - 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`
**Проблема:**
Описание проблемы
**Решение:**
Что сделали (можно с кодом)
**Подводные камни:**
- На что обратить внимание