Cascadev0.14.0
Платформа интеллектуального анализа трендов. Автоматический сбор сигналов из 5+ источников, каскадный AI-анализ влияния, ролевые рекомендации и готовые отчёты — всё что нужно чтобы принимать решения раньше конкурентов.
Скриншоты
Документация
Cascade Trend Analysis
Система сбора и анализа технологических/новостных сигналов. Извлекает факты, группирует в события, формирует тренды, генерирует аналитические отчёты.
Stack: Python 3.12+ · PostgreSQL 16 + pgvector · Go 1.21+ · React + TypeScript · Ollama LLM
Архитектура (v0.22+)
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Frontend │ ───► │ Go API │ ───► │ PostgreSQL │
│ React SPA │ │ :4010 RO │ │ + pgvector │
└─────────────┘ └──────┬──────┘ └─────────────┘
│ writes proxy ▲ ▲
▼ │ │
┌─────────────┐ │ │
│ Python API │──────────────┘ │
│ :4014 RW │ │
└─────────────┘ │
│
┌─────────────┐ │
│ Pipeline │─────────────────┘
│ 45+ src │ crawl + extract + link
└─────────────┘
│
▼
┌─────────────┐
│ Ollama │ tier1: hermes3 (:11436)
│ 3 tunnels │ tier2: gemma4 (:11435)
└─────────────┘
3 процесса на проде (PM2):
-
go-api— read-only endpoints (port 4010), отдаёт статику SPA -
python-api— write-only endpoints (port 4014), auth, proxy target -
pipeline— crawler + event_linker + summarizer + translation_watchdog
Быстрый старт (разработка)
Требования
- Python 3.12+
- Node 20+
- Go 1.21+ (только если будете менять Go API)
- Docker (для PostgreSQL + SearXNG)
- Ollama tunnels (см. раздел LLM ниже) — опционально, без них LLM-шаги не работают
1. PostgreSQL + pgvector
cd docker/postgres-trends
docker compose up -d
# проверь: docker exec postgres-trends pg_isready -U trends
Применить схему (один раз на пустую БД):
docker exec -i postgres-trends psql -U trends trends < db/pg_schema.sql
docker exec -i postgres-trends psql -U trends trends < db/pg_indexes.sql
2. Python backend
python3.12 -m venv venv
source venv/bin/activate # Linux/Mac
# venv\Scripts\activate # Windows
pip install -r requirements.txt
# Настройка .env (см. раздел "Переменные окружения" ниже)
cp .env.example .env
$EDITOR .env
3. Фронтенд
cd frontend-cascade/app
npm install
npm run dev # http://localhost:5173
4. Запуск dev-сервера
# вариант 1: всё сразу (API + фронт)
python dev.py
# вариант 2: только API
python dev.py --api-only --port 8000
# вариант 3: только фронт
python dev.py --web-only
5. Запуск pipeline (опционально)
Pipeline разделён на два процесса — запускайте оба в отдельных терминалах:
source venv/bin/activate
# терминал 1 — сбор сигналов
python -m src.workers.collector
# терминал 2 — обработка (extraction, events, trends, ai_insights, watchdog)
python -m src.workers.analytics
Переменные окружения
Скопируйте .env.example → .env и заполните:
Обязательные
DATABASE_URL=postgresql://trends:PASSWORD@localhost:5432/trends
# Ollama tunnels — 3 отдельных инстанса
OLLAMA_URL=http://localhost:11435 # tier2/3: gemma4:e2b (translation, reasoning)
OLLAMA_TIER1_URL=http://localhost:11436 # tier1: hermes3:8b (fact extraction)
OLLAMA_BACKUP_URL=http://localhost:11434 # backup (opportunistic)
LLM_PROVIDER=ollama
Auth (Casdoor)
CASDOOR_ENDPOINT=https://auth.trendominus.ru
CASDOOR_CLIENT_ID=...
CASDOOR_CLIENT_SECRET=...
CASDOOR_JWT_SECRET=... # public key of Casdoor JWT cert
Необязательные
SEARXNG_ENABLED=0 # fetch_news через SearXNG (default: off)
SEARXNG_ENRICHMENT=0 # citation enrichment (default: off — блокирует event loop)
Frontend (frontend-cascade/app/.env)
VITE_API_URL= # пусто = same-origin; иначе https://api.example.com
Production deploy
Деплой только через CI/CD (GitLab). SSH-деплой запрещён: CI билдит Go binary + Vite bundle, ручной rsync это пропускает.
Процесс
- Push в feature-ветку → создать MR в
main - Пройти CI (
lint+test+build:go-api+build:frontend) - Merge MR
- В GitLab UI нажать Deploy (manual stage)
- Проверить:
curl https://trendominus.ru/api/health
GitLab CI Variables (Settings → CI/CD → Variables)
| Ключ | Тип | Описание |
|---|---|---|
BACKEND_ENV |
File | Полный .env для прода (все переменные выше) |
DEPLOY_PATH |
Variable | /var/www/trend-analisis |
VITE_API_URL |
Variable | пусто для same-origin |
Пример содержимого BACKEND_ENV — см. .gitlab/ci/pipeline.yml (секция deploy).
Что делает deploy job
- rsync Python backend +
db/+i18n/+scripts/+ static bundle →$DEPLOY_PATH - Копирует Go binary из artifact
-
pip install -r requirements.txtвvenv - Копирует
BACKEND_ENV→$DEPLOY_PATH/.env pm2 restart go-api python-api pipeline --update-env
Первый запуск инфраструктуры
# Manual CI job:
install:postgres-trends # ставит pgvector/pgvector:pg16 в docker-compose
LLM infrastructure
3 Ollama инстанса (разные GPU, разные туннели):
| Port | GPU | Модель | Роль |
|---|---|---|---|
| 11436 | 3090-B | hermes3:8b | Tier 1 — fact extraction |
| 11435 | 3090-A | gemma4:e2b | Tier 2/3 — translation, reasoning, summarizer |
| 11434 | 4090 | any | Opportunistic backup (нестабильный) |
Туннели конфигурируются через FRP client. Подробнее: CLAUDE.md → Ollama Infrastructure.
Разработка
Перед push'ем
venv/Scripts/python.exe -m ruff check src/ api/ # lint (0 errors)
cd frontend-cascade/app && npx tsc --noEmit && npx vite build
Тесты
venv/Scripts/python.exe -m pytest tests/ -x -q # 740+ tests
Работа с БД
# Применить миграции (только SQLite — PG schema статическая)
venv/Scripts/python.exe -m db.migrator
# Консоль PG
docker exec -it postgres-trends psql -U trends trends
Структура репозитория
api/ Python API (FastAPI) — writes-only
src/ Pipeline + services + adapters
workers/ main.py (pipeline), crawler.py, translation_watchdog.py
services/ fact_extractor, event_linker, adapters/ (45+)
llm/ Ollama clients
go-api/ Go API (chi router, read-only)
db/ engine.py (DAL), migrations, pg_schema.sql
frontend-cascade/ React SPA
tests/ pytest
scripts/ CI/CD, ops helpers
docker/ postgres-trends, searxng, ollama-proxy
.gitlab/ci/ GitLab CI pipeline
Мониторинг
# Статус процессов
ssh prod "sudo -u gitlab-runner pm2 list"
# Логи
ssh prod "sudo -u gitlab-runner pm2 logs pipeline --lines 50"
# Здоровье API
curl https://trendominus.ru/api/health
# Состояние БД
ssh prod "docker exec postgres-trends psql -U trends trends -c \
'SELECT COUNT(*) FROM events; SELECT COUNT(*) FROM facts;'"
Правила проекта
- Не костыли. Если фикс затрагивает >3 файлов или >30 мин — согласуй подход с автором.
- Без SSH-деплоя. Только через CI.
-
Все зависимости в
requirements.txt(не вpyproject.toml— CI его игнорирует). -
Коммиты:
type(scope): description(feat / fix / perf / refactor / docs / test). -
Version bump при каждом коммите: patch-bump в
pyproject.toml+frontend-cascade/app/package.json.
Подробнее — см. CLAUDE.md.
История изменений
Журнал изменений (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`
**Проблема:**
Описание проблемы
**Решение:**
Что сделали (можно с кодом)
**Подводные камни:**
- На что обратить внимание