AAYMA
RU
Войти

Граф денег

Рабочее место AML-аналитика: по графу внутрибанковских переводов от 81 seed-клиента присваивает каждому из 2 248 узлов роль, кластер и приоритет…

Доведённый продукт
Команда «TAMYR» 27💬 0 комментариевс 23 сентября 2026 г.

О проекте

Команда: TAMYR
Кейс HackAlem.ai: Финансы — Freedom
Стадия: Доведённый продукт

Задача кейса: «Граф денег»: восстановить структуру организованной группы по сети транзакций (AML)

Суть

Рабочее место AML-аналитика: по графу внутрибанковских переводов от 81 seed-клиента присваивает каждому из 2 248 узлов роль, кластер и приоритет проверки с объяснением на русском; есть визуализация сети и AI-ассистент.

Как устроено

Пайплайн на TypeScript читает parquet (узлы, рёбра, транзакции), считает метрики узлов (степени, суммы, коэффициент пропуска, PageRank, HITS, посредничество, быстрый транзит), по порогам присваивает одну из шести ролей (consolidator, transit, distributor, terminal, coordinator, peripheral), кластеризует Louvain и считает priority_score. На выходе nodes_roles.csv, clusters.csv, top_nodes.csv и analysis.json с evidence. Next.js-экран на sigma.js показывает граф и «путь денег» (обратный BFS до seed). Ассистент вызывает 7 read-only инструментов над анализом через OpenAI Responses API (gpt-6-luna) или сценарный mock без ключа.

Ключевые решения

  • Louvain
  • PageRank
  • HITS
  • betweenness centrality
  • rule-based role thresholds
  • reverse BFS money path
  • function calling
  • mock LLM provider

Особенности

Каждая роль — проверяемое формальное правило с порогом и evidence с числами; выводы подаются как гипотезы, а не обвинения. Режим LLM_PROVIDER=mock прогоняет настоящие инструменты через тот же диспетчер, так что продукт проверяется без ключа. Пайплайн сам валидирует выход (падает, если строк не 2 248) и пишет run_summary.json с SHA-256 входов.

Стек

TypeScript · Next.js · Node.js · pnpm · sigma.js · Parquet

Модели

gpt-6-luna · OpenAI Responses API


Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-d162b91e-tamyr.

README

README.md

Граф денег

AML-инструмент аналитика: по 4-хоповому графу переводов определяет, кого из 2 248 клиентов проверять первым и почему. HackAlem AI, трек 2 «Граф денег».

Экспертам и жюри: установка, запуск и проверка основного сценария — без API-ключа, без базы данных и без Docker. Достаточно Node.js и двух команд.

Содержание

Что это

«Граф денег» — рабочее место AML-аналитика. На входе — граф исходящих внутрибанковских переводов от 81 известного клиента (seed) на глубину 4 колена: 2 248 узлов, 3 119 рёбер, 4 840 транзакций, июль 2026, переводы от 5 000 KZT.

Пайплайн считает метрики каждого узла и присваивает ему роль (ровно одну из шести по ТЗ), кластер и приоритет проверки, а у каждой строки есть evidence — объяснение с числами. Экран показывает сеть с направлением денег и ролями, ищет любой gid и открывает карточку узла. AI-ассистент отвечает на вопросы вида «кого проверять первым и почему?», вызывая инструменты только для чтения над тем же анализом.

Выводы — это гипотезы для проверки, а не обвинения. Разметки «виновен / не виновен» в данных нет, поэтому каждая роль — формальное правило с порогом, которое любой может проверить руками. Тексты для пользователя (интерфейс, evidence, why, hypothesis, ответы ассистента) — на русском, код — на английском.

Схема решения: данные → метрики → роли, кластеры, приоритет → интерфейс

Быстрый старт

Нужно: Node.js ≥ 22 и pnpm 10. Python, база данных и Docker не нужны.

Если их нет:

  1. Node.js — установщик LTS-версии с https://nodejs.org (Windows, macOS, Linux; npm входит в комплект). Или менеджером пакетов: winget install OpenJS.NodeJS.LTS (Windows), brew install node (macOS).
  2. pnpm — одной командой в новом окне терминала: npm install -g pnpm@10.
  3. Проверка: node -v печатает v22… или новее, pnpm -v — 10….

Без глобальной установки pnpm можно вызывать через npx pnpm@10 вместо pnpm в командах ниже.

pnpm install
cp .env.example .env          # PowerShell: Copy-Item .env.example .env
pnpm pipeline                 # data/*.parquet → output/*.csv + output/analysis.json, секунды
pnpm dev                      # http://localhost:3000
  • pnpm pipeline читает сырые data/*.parquet (лежат в репозитории, 87 КБ) и пишет output/nodes_roles.csv, output/clusters.csv, output/top_nodes.csv, output/analysis.json и output/run_summary.json. Работает ~3–6 секунд (по ТЗ допустимо до 5 минут), печатает время, число строк и заявленные особенности данных и падает с ошибкой, если в nodes_roles.csv не 2 248 строк или в top_nodes.csv меньше 20.
  • pnpm dev и pnpm build сами запускают пайплайн (predev / prebuild), так что pnpm install && pnpm dev — достаточно. Отдельный pnpm pipeline нужен, чтобы посмотреть выгрузки без запуска сервера.
  • CSV — UTF-8 без BOM: так их читает pandas.read_csv без параметров, а первая колонка называется ровно gid. В Excel открывать через «Данные → Из текста/CSV → UTF-8», иначе кириллица в evidence отобразится кракозябрами.
  • База данных и Docker не нужны. .env.example уже содержит LLM_PROVIDER=mock — ассистент работает без ключа и без сети.

Продакшен-запуск и деплой

На любой машине или сервере с Node.js ≥ 22 и pnpm 10 (установка — в разделе выше):

pnpm install --frozen-lockfile
cp .env.example .env          # для живой модели: LLM_PROVIDER=responses и LLM_API_KEY
pnpm build                    # сам запускает пайплайн, затем собирает Next.js
pnpm start                    # http://localhost:3000; порт меняется переменной PORT

Приложение — один Node-процесс, без базы и внешних сервисов. Переменные задаются в .env или в окружении хостинга.

Переменные окружения

.env.example полный и содержит LLM_PROVIDER=mock, так что его копия сразу даёт рабочий продукт.

ПеременнаяДля проверкиДля живой моделиСмысл
LLM_PROVIDERmockresponsesmock, responses (OpenAI Responses API) или chat
LLM_MODELне используетсяgpt-6-lunaИдентификатор модели
LLM_API_KEYпустоключ OpenAI APIНастоящий ключ никогда не коммитится
LLM_BASE_URLпустопустоТолько для OpenAI-совместимого эндпоинта с chat

LLM_PROVIDER=mock — способ проверить основной сценарий без ключа: сценарный агент вызывает настоящие инструменты через тот же диспетчер, что и живая модель, без ключа и без сети. Это продукт без модели, а не заглушка: инструмент, который должен отказать, отказывает и здесь.

Остальные переменные в .env.example (встроенные инструменты OpenAI) унаследованы от шаблона и продуктом не используются.

Как проверить основной сценарий

  1. Запустить пайплайн: pnpm pipeline. В консоли — время выполнения и счётчики строк.

  2. Проверить выгрузки:

    • в output/nodes_roles.csv ровно 2 248 строк данных плюс заголовок, шесть обязательных колонок (gid, role, role_score, cluster_id, priority_score, evidence) заполнены в каждой строке;
    • в output/top_nodes.csv — не менее 20 строк, по убыванию приоритета;
    • в output/clusters.csv — строка на кластер, у каждого гипотеза.

    Подсчёт строк на любой ОС (печатает 2248):

    node -e "console.log(require('fs').readFileSync('output/nodes_roles.csv','utf8').trim().split('\n').length - 1)"
    
  3. Открыть экран: pnpm dev, затем http://localhost:3000 (по умолчанию тёмная тема, переключатель вверху справа). Сеть раскрашена по ролям (можно переключить на раскраску по кластерам), стрелки показывают направление денег, размер — приоритет. Чипы ролей над графом называют каждый цвет и включают/выключают роль; периферия (80% узлов) скрыта по умолчанию — чип «Периферийный» включает её. Справа открыта вкладка «Топ» — список приоритетов.

  4. Найти gid: вставить любой gid из nodes_roles.csv в поиск. Узел фокусируется, соседи подсвечиваются (даже скрытые периферийные), открывается карточка: роль и сила правила, evidence, метрики, флаги аномалий, крупнейшие отправители и получатели.

  5. Показать путь денег: кнопка «Показать путь денег» в карточке прослеживает самые крупные входящие рёбра назад до seed и по коленам подсвечивает маршрут на графе, внизу — итог в виде гипотезы («деньги могли дойти за N колен от K seed»). Вкладка «Кластеры»: клик по кластеру подсвечивает его на графе.

  6. Спросить ассистента «кого проверять первым и почему?», затем «кто собирает деньги с этих пятерых?» и, по желанию, «что если убрать первых пятерых?». В панели активности видно каждый вызов инструмента — имя, аргументы, результат и время, — а ответ собран из этих результатов. Любой gid в ответе кликабелен.

Выгрузки

Все три CSV строго следуют схеме ТЗ: обязательные колонки идут первыми в порядке ТЗ, дополнительные — после них.

output/nodes_roles.csv — строка на каждый узел, 2 248 строк.

КолонкаСмысл
gidИдентификатор клиента, хранится строкой (значения ~1e17 больше безопасного целого JS)
roleОдна из consolidator, transit, distributor, terminal, coordinator, peripheral
role_score0–1: насколько узел превышает порог своей роли
cluster_idСообщество Louvain
priority_score0–1: приоритет проверки
evidenceНа русском, ≤ 200 символов, с числами
далеедополнительные колонки: метрики ниже (колено, seed, степени, суммы, коэффициент пропуска, PageRank, HITS, посредничество, seed выше по потоку, быстрый транзит, обрыв) и flags

output/clusters.csv — строка на кластер: cluster_id, n_nodes, n_seed, sum_kzt_internal, top_gids, hypothesis. sum_kzt_internal — сумма рёбер, оба конца которых внутри кластера; hypothesis — на русском, сформулирована как предмет проверки.

output/top_nodes.csv — 50 строк (ТЗ требует не менее 20): rank, gid, role, priority_score, why. why называет два-три слагаемых, которые сильнее всего дали вклад в приоритет, с их значениями.

Во всех CSV gid — просто цифры, булевы значения — true/false, отсутствующее значение (например, pass_through у узла, который ничего не получил) — пустая ячейка, списки (top_gids, flags) соединены через |.

output/run_summary.json — журнал одного запуска: SHA-256 каждого входного файла, число строк, число узлов по ролям, время каждого этапа и заявленные особенности данных как предупреждения (изолированные seed, узлы, которые отдают больше, чем по данным получили, узлы 4-го колена без исходящих).

output/analysis.json — всё, что читают экран и ассистент: статистика, пороги ролей (thresholds), каждый узел с метриками, вердиктом и заранее посчитанной раскладкой, каждое ребро с датами первой и последней транзакции, кластеры и топ-лист. Его форма — zod-схема в src/server/graph/model/graph.schema.ts.

Метод

Метрики

Считаются на направленном взвешенном графе для каждого узла:

МетрикаОпределение
in_deg, out_degЧисло различных отправителей / получателей
in_kzt, out_kztKZT получено / отправлено внутри графа
in_tx, out_txЧисло переводов — суммы и количества дают разные сигналы
pass_throughout_kzt / in_kzt, пусто, если ничего не пришло. Не используется для seed
pagerankPageRank со взвешиванием по sum_kzt, α = 0.85, «висячая» масса распределяется равномерно
hub, authorityHITS: хабы шлют хорошим сборщикам, авторитеты собирают от хороших хабов
betweennessПосредничество по Брандесу, направленное, нормировано на (n−1)(n−2)
seeds_upstreamСколько различных seed достигают узла по направленным рёбрам
fast_transit_shareДоля исходящих KZT, отправленных в течение 2 дней после входящего перевода (по датам)
truncated4-е колено и нет исходящих рёбер: остановился обход, а не деньги

Наши PageRank, HITS и betweenness сверены с networkx на всех 2 248 узлах и совпадают с точностью до 1e-14.

Роли

Каждая роль — формальное правило. Правила применяются в этом порядке, выигрывает первое совпавшее. role_score — насколько узел превысил порог: для значения v и порога t это (v − t) / (v + t), так что узел ровно на пороге получает 0, а далеко за ним — почти 1.

#Роль / случайПравилоПороги (ROLE_THRESHOLDS)Узлов
1truncated (флаг)4-е колено и out_deg = 0: остановился обход, а не деньги. Никогда не terminal; consolidator — только если входы проходят правило 4, иначе peripheralколено ≥ 4; role_score ≤ 0.5444 с флагом
2coordinatorНачало цепочки с ветвлением в слой сбора/распределения: seed или узел колена ≤ 1, который шлёт нескольким получателям и через который за 2 шага достигается много консолидаторов/распределителей по нескольким отдельным веткамколено ≤ 1; out_deg ≥ 3; ≥ 15 таких узлов в пределах 2 шагов; ≥ 3 прямые ветки16
3distributorВеер: много получателей, мало отправителейout_deg ≥ 10 и out_deg ≥ 2.5 × in_deg53
4consolidatorСобирает от многих и мало отдаёт дальше(in_deg ≥ 5, или in_deg ≥ 3 при ≥ 2 seed выше по потоку) и pass_through < 0.594
5transitПришло ≈ ушло, небольшие степени; пересылка в течение 2 дней повышает скор. Никогда для seed (его приход занижен)0.8 ≤ pass_through ≤ 1.2; in_deg, out_deg ≤ 8; флаг fast_transit при ≥ 50% пересланного за 2 дня68
6terminalДеньги приходят и остаются — и не потому, что обход остановилсяout_deg = 0, колено < 4 и (in_deg ≥ 2 или in_kzt ≥ 200 000 ₸)219
7peripheralВсё остальное; evidence говорит, какой порог не пройден—1 798

Применённые числа выгружаются при каждом запуске как thresholds в output/analysis.json; если эта таблица и файл расходятся, верен файл.

Для масштаба, на этих данных: у 51 узла in_deg ≥ 5; у 64 — out_deg ≥ 10; у 70 узлов, не seed, pass_through от 0.8 до 1.2; 444 узла с обрывом; у 1 091 нет исходящих при колене < 4, из них 219 проходят порог terminal. Число узлов по ролям для каждого запуска — в output/run_summary.json.

Флаги аномалий

Флаги стоят рядом с ролью и никогда не меняют ни её, ни скор, ни приоритет. Они видны бейджами в карточке узла и в колонке flags в nodes_roles.csv (src/server/graph/model/anomalies.ts, пороги также выгружаются в analysis.json):

ФлагПравилоУзлов
split (дробление сумм)один отправитель сделал ≥ 3 перевода узлу в один день, все в пределах ±10% от медианы6
sync_inflow (синхронный приход)в какой-то день узел получил от ≥ 3 различных плательщиков38
cycle (возвратный поток)узел лежит на направленном цикле длиной ≤ 6 — деньги возвращаются303
repeat_route (повторяющийся маршрут)средний узел B цепочки A→B→C с ≥ 2 переводами по обоим звеньям, и в ≥ 2 датах B переслал C в течение 2 дней после получения от A54
burst (всплеск активности)в какой-то день у узла ≥ 3 транзакций и ≥ 3× его среднего дневного числа (нужно ≥ 2 активных дня)10
depth_outlier (аномальный профиль для колена)оборот log1p(in+out) не ниже 95-го перцентиля узлов того же колена (слои ≥ 20 узлов)120
fast_transit (быстрый транзит)≥ 50% исходящих KZT ушли в течение 2 дней после входящего перевода275
truncated (обрыв обхода)4-е колено без исходящих переводов (см. «Роли»)444

Дробление ниже порога выгрузки 5 000 KZT в этих данных невидимо, поэтому split может только недосчитывать.

Кластеры

Выделение сообществ Louvain (graphology-communities-louvain) на ненаправленной проекции, отдельно в каждой слабосвязной компоненте; встречные рёбра объединяются, вес — log1p(sum_kzt) + 0.25 · log1p(n_tx). Направление отбрасывается только для кластеризации — все метрики и роли выше считаются на направленном графе. Генератор случайных чисел зафиксирован (42), а номера кластеров упорядочены по внутреннему обороту, так что на тех же данных кластеры одинаковы при каждом запуске. Каждый кластер получает гипотезу на русском по составу ролей и числу seed. На этих данных: 84 кластера (изолированные seed — кластеры из одного узла).

Приоритет

priority_score отвечает на вопрос «что проверять следующим», а не «кто виновен». Это взвешенная сумма слагаемых, каждое нормировано к 0–1 (src/server/graph/model/priority.ts):

СлагаемоеВесНормировка
Сила роли0.25вес роли × role_score (coordinator 1.0, consolidator 0.95, distributor 0.9, transit 0.8, terminal 0.55, peripheral 0)
Объём потока0.20log1p(in_kzt + out_kzt) ÷ максимум
Охват seed0.15seeds_upstream ÷ максимум
Посредничество0.15÷ максимум
PageRank0.15÷ максимум
Плотность seed в кластере0.10seed ÷ узлов в кластере узла

Затем × 0.75 для seed (аналитику он уже известен) и × 0.85 для узла с обрывом (исходящее поведение не наблюдается). why называет два-три слагаемых с наибольшим вкладом и их значения, плюс штраф, если он есть. Ни один seed не попадает в топ-50.

Как учтены ловушки данных

  • Обрыв на 4-м колене. 444 узла стоят на 4-м колене без исходящих рёбер, потому что обход закончился. Они никогда не terminal: у них флаг truncated, role_score ограничен 0.5, приоритет снижен, а evidence говорит «обход остановлен на 4-м колене».
  • Приход seed занижен. Граф собран от seed по исходящим переводам, поэтому деньги, полученные seed извне выборки, отсутствуют, и pass_through 20+ — артефакт выгрузки. pass_through никогда не используется для роли seed.
  • Суммы и количества различаются. Один перевод на 4 млн и сорок по 100 тыс. дают одинаковый in_kzt; степени, число переводов и суммы сохраняются и показываются все.
  • Порог 5 000 KZT. Меньших переводов нет, поэтому степень узла — оценка снизу, а «маленький» поток может быть раздробленным.
  • Фрагменты. В графе 35 слабосвязных компонент: 16 компонент плюс 19 изолированных seed. Изолированные seed сохраняются, получают роль, как любой узел, и отмечаются как белое пятно.
  • Нет разметки. Меток нет, поэтому ничего не обучается. Объяснимые правила с порогами позволяют аналитику — и жюри — проверить любой вердикт по числам самого узла за минуту.

Архитектура

Одно приложение Next.js, TypeScript от начала до конца: пайплайн, который пишет CSV, и сервер, который отвечает ассистенту, вызывают одни и те же функции.

data/*.parquet
   │  scripts/pipeline.ts            CLI: --data ./data --out ./output; время и число строк
   ▼
src/server/graph/repo/               только файловый ввод-вывод
   parquet.ts                        readRawGraph: hyparquet + ZSTD; gid → строка, date → 'YYYY-MM-DD'
   outputs.ts                        writeOutputs: 3 CSV + analysis.json
   analysis.ts                       readAnalysis: синхронно, с кэшем, валидация zod-схемой
   ▼
src/server/graph/model/              чистый алгоритм: синхронный, без ввода-вывода, покрыт тестами
   graph.schema.ts                   контракт (zod): все структуры ниже
   metrics.ts                        степени, суммы, коэффициент пропуска, PageRank, HITS, посредничество…
   roles.ts                          ROLE_THRESHOLDS, assignRoles
   anomalies.ts                      флаги аномалий
   clusters.ts                       Louvain, сводки кластеров и гипотезы
   priority.ts                       scorePriority, rankTop
   queries.ts                        карточка узла, сборщики, трассировка потока, изъятие, белые пятна
   ▼
src/server/graph/usecase/
   analyze.ts                        метрики → роли → кластеры → раскладка → приоритет → топ
   layout.ts                         ForceAtlas2 с фиксированным зерном: картинка не «прыгает»
   getAnalysis.ts                    единственная точка входа страницы
   tools.ts                          семь инструментов ассистента
   ▼
src/app/ + src/views/graph           экран сети на Sigma.js (WebGL), карточка, топ, кластеры
src/widgets/assistant                чат рядом с графом, панель активности
src/server/agent/usecase             цикл агента, реестр инструментов, промпт, mock-адаптер
app/api/chat/route.ts                POST /api/chat — единственный сетевой вызов интерфейса

Страница читает output/analysis.json и никогда не разбирает parquet во время запроса, поэтому обработчики инструментов синхронные, а экран открывается мгновенно.

Почему TypeScript, а не Python. Одна среда выполнения — одна установка для экспертов, а проект, который эксперты не смогли запустить, выбывает. Инструменты ассистента вызывают ровно те функции, которые построили CSV, поэтому чат и файлы не могут разойтись. Прежде чем выбрать этот путь, мы сверили графовые алгоритмы с networkx (1e-14); весь расчёт занимает около трети секунды. Python- стартер организаторов использован только как справка.

Структура кода. Интерфейс — Feature-Sliced Design (app → views → widgets → features → entities → shared, импорты только вниз). Сервер — src/server/ по уровням (kernel → <domain>/model → <domain>/repo → <domain>/usecase), тоже только вниз. Оба правила проверяет eslint-plugin-boundaries, нарушение ломает сборку. docs/architecture.md — спецификация; docs/plan.md и docs/decisions.md — как распределялась работа и почему.

Стек

Next.js 16 (App Router) · React 19 · TypeScript · Tailwind CSS 4 · Radix UI · Zod · hyparquet · graphology (Louvain, ForceAtlas2) · Sigma.js 3 с @react-sigma/core · OpenAI SDK (Responses API) · Vitest · ESLint.

LLM-провайдеры

LLM_PROVIDER выбирает, как ассистент общается с моделью. Инструменты и цикл одинаковы во всех трёх вариантах; меняется только протокол.

ЗначениеПротоколДля чего
responsesOpenAI Responses APIЖивая модель gpt-6-luna
chatChat CompletionsЛюбой OpenAI-совместимый эндпоинт (задать LLM_BASE_URL)
mock—Без ключа и без сети. Проверка и офлайн-демо

Инструменты ассистента

Все инструменты только читают, поэтому подтверждения не требуют. Каждый проверяет аргументы через zod (аргументы — свободный текст модели), перепроверяет, что каждый gid существует, и иначе возвращает отказ. Диспетчер никогда не бросает исключение: упавший инструмент — это строка «ошибка».

ИнструментАргументыВозвращает
get_top_nodeslimit? (1–50), role?, clusterId?Строки топа по приоритету с why
get_nodegidКарточку узла: метрики, роль, evidence, топ-5 входящих и исходящих
get_clusterclusterIdСтроку кластера и его главных участников
find_collectorsgids (2–20), maxHops? (1–4)Узлы, до которых доходят деньги ≥ 2 из gid, с полученными KZT
trace_flowgid, direction, maxHops?Подграф рёбер ниже или выше по потоку от gid
simulate_removalgidsКомпоненты и охват seed до и после изъятия gid
coverage_gaps—Каких данных не хватает и какой запрос сделать в банк следующим

Промпт требует, чтобы каждое число в ответе бралось из результата инструмента, каждое утверждение ссылалось на gid, выводы формулировались как гипотезы, а ассистент говорил, чего данные показать не могут (обрыв, приход seed).

Ограничения

  • Выборка односторонняя и обрезанная. Только исходящие переводы от seed, четыре колена, один месяц. Приход в seed и всё дальше 4-го колена не видно; узел с обрывом нельзя считать конечным получателем.
  • Переводов меньше 5 000 KZT нет, поэтому дробление на мелкие суммы не видно.
  • Только внутрибанковские переводы. Наличные, другие банки и выход в крипту выглядят как конечные получатели или отсутствуют.
  • Правила, а не модель. Пороги откалиброваны на одном этом датасете и на другом потребуют перенастройки; без разметки точность и полноту измерить нельзя.
  • Роли взаимоисключающие. Узел, который и собирает, и распределяет, получает первую совпавшую роль; остальное видно по метрикам в его строке.
  • Louvain в общем случае недетерминирован; фиксированное зерно делает результат воспроизводимым, но не единственным. Ненаправленная кластеризация не учитывает, куда текут деньги внутри кластера.
  • Ассистент прав ровно настолько, насколько правы его инструменты; он объясняет правила и не добавляет знаний сверх данных.

Масштабирование до ~1 млн узлов

Сейчас всё считается в памяти за треть секунды на 2 248 узлах. При миллионе узлов и десятках миллионов переводов изменится следующее:

  • Чтение. Читать parquet потоково по группам строк и колонкам, а не целиком; строить смежность сразу в сжатые разреженные массивы (CSR), а не в JS-объекты.
  • Метрики. PageRank и HITS — итерации разреженного умножения матрицы на вектор (линейно по рёбрам за итерацию). Точное посредничество по Брандесу — O(V·E) и становится неподъёмным, поэтому — приближённое посредничество по выборке из k опорных вершин. Тяжёлые расчёты — в графовый движок: igraph или graph-tool на одной машине, GraphFrames/Spark или графовая БД для масштаба банка.
  • Кластеризация. Leiden вместо Louvain: быстрее на больших графах и гарантирует связные сообщества.
  • Инкрементальный пересчёт. Новые транзакции приходят ежедневно; пересчитывать только затронутые окрестности и роли, а полный пересчёт оставить ночным заданием.
  • Интерфейс. Sigma.js тянет порядка 100 тыс. узлов в браузере. Дальше экран показывает сводки кластеров и эго-сети вокруг найденного gid с фильтрацией на сервере, заранее посчитанными тайлами и уровнями детализации вместо всего графа.
  • Инструменты ассистента. Запросы — через индекс или графовую БД (окрестность, k шагов, топ по приоритету) вместо перебора массивов в памяти; контракты инструментов не меняются.

Тесты и проверки

pnpm check

Одна команда: проверка типов, линтер с --max-warnings 0 и юнит-тесты, параллельно. Предупреждения считаются ошибками. Ничего не нужно запускать заранее — ни базы, ни API-ключа, ни сети. Алгоритм в src/server/graph/model покрыт юнит-тестами на небольших графах, собранных вручную: по тесту на каждую роль и флаг, плюс ловушки обрыва и seed.

Структурные проверки шаблона тоже действуют: src/app/globals.spec.ts (стили собираются и следуют правилам токенов), src/shared/ui/rawControls.spec.ts (нет «сырых» элементов форм на экранах) и src/server/agent/usecase/tools.spec.ts (диспетчер инструментов никогда не бросает исключение).

Команды

КомандаЧто делает
pnpm pipelinedata/*.parquet → output/*.csv + output/analysis.json
pnpm devDev-сервер Next на http://localhost:3000
pnpm build / pnpm startПродакшен-сборка и запуск
pnpm checkТипы, линтер и юнит-тесты параллельно
pnpm testТолько юнит-тесты
pnpm verifyТипы и линтер
pnpm formatPrettier

База данных продуктом не используется: необязательный слой PostgreSQL/Prisma из шаблона удалён.

Команда

УчастникЗона ответственности
Ораз ИсабековКонтракт данных, интерфейс, README
БекжанПайплайн, инструменты ассистента, промпт
Саян КареевАлгоритм: метрики, роли, кластеры, приоритет, запросы

Сторонние компоненты и предварительные наработки

Обязательное раскрытие (§5.4.4).

Заранее подготовленный шаблон. Проект начат с универсального стартового шаблона, подготовленного до соревнования и закоммиченного как 8d67410 chore: pre-existing hackathon starter template: оболочка приложения Next.js, провайдер-независимый цикл LLM-агента с реестром инструментов и инструментом часов без предметной логики, чат-интерфейс с панелью активности, библиотека UI-компонентов, система дизайн-токенов и архитектура, проверяемая линтером. Функциональности по этой задаче в нём не было. Всё, что решает задачу, — контракт данных, пайплайн, метрики, правила ролей, кластеризация и приоритет, флаги аномалий, графовые инструменты, экран сети и этот README — сделано во время соревновательной части и видно в истории коммитов после этого коммита.

Материалы организаторов. Датасет (data/edges.parquet, data/nodes.parquet, data/transactions.parquet, 4-хоповый граф внутрибанковских переводов, июль 2026) и ТЗ предоставлены организаторами для использования в рамках хакатона. Python-стартер организаторов (starter.py) прочитан как справка по схеме выгрузок и ловушкам данных; его код не используется — пайплайн наш, на TypeScript.

Модель. OpenAI gpt-6-luna через OpenAI Responses API (LLM_PROVIDER=responses), проприетарная, используется на условиях OpenAI API. Никакая модель не обучалась и не дообучалась. LLM_PROVIDER=mock модель не использует вовсе.

Зависимости времени выполнения (полный список и версии — в package.json):

ПакетДля чегоЛицензия
hyparquet, hyparquet-compressorsЧтение parquet со сжатием ZSTDMIT
graphologyСтруктура данных графаMIT
graphology-communities-louvainКластеризация LouvainMIT
graphology-layout-forceatlas2Заранее посчитанная раскладкаMIT
sigma, @react-sigma/coreWebGL-отрисовка сетиMIT
@sigma/node-borderОреол узлов (свечение)MIT
next, react, react-domФреймворк приложенияMIT
radix-ui, @heroicons/react, clsxUI-примитивы, иконки, классыMIT
tailwindcss (dev)СтилиMIT
zodСхемы и валидацияMIT
openaiКлиент OpenAI APIApache-2.0
dotenvЗагрузка .envBSD-2-Clause
server-onlyЗащита границы сервер/клиентMIT

Инструменты разработки — TypeScript, ESLint и его плагины, Prettier, Vitest, tsx — каждый под своей лицензией, перечислены в package.json.

Графовые алгоритмы не из библиотек — PageRank, HITS, посредничество по Брандесу, seeds_upstream, быстрый транзит, правила ролей и приоритет — наш код в src/server/graph/model, сверенный с networkx.

Звёзды
⭐ 0
Форки
0
Язык
TypeScript
Последний push
в прошлом месяце
Коммиты
60
Авторы
3
Файлы
177
Ветки
1

Авторы коммитов

Oraz Issabekov 47Бекжан Қайратов 9Саян Кареев 4

История коммитов

  • 2026-09-23 18:14docs(readme): how to install Node.js and pnpmOraz Issabekov
  • 2026-09-23 18:01docs(readme): whole README in Russian, as the task isOraz Issabekov
  • 2026-09-23 17:56docs(readme): production run and deploy stepsOraz Issabekov
  • 2026-09-23 17:51chore: remove the starter's unused Prisma/PostgreSQL layerOraz Issabekov
  • 2026-09-23 17:46chore: drop unused packages, tidy docs, outputs with all anomaly flagsOraz Issabekov
  • 2026-09-23 17:45Merge burst, depth_outlier and repeat_route flagsOraz Issabekov
  • 2026-09-23 17:44feat(model): burst, depth_outlier and repeat_route anomaly flagsOraz Issabekov
  • 2026-09-23 17:42feat(graph): labels for burst, depth_outlier and repeat_route flagsOraz Issabekov
Все коммиты (60)
  • 2026-09-23 17:38chore: outputs with anomaly flags; README documents split, sync_inflow and cycleOraz Issabekov
  • 2026-09-23 17:37Merge anomaly flags: split, sync_inflow, cycleOraz Issabekov
  • 2026-09-23 17:37feat(model): split, sync_inflow and cycle anomaly flags on role verdictsOraz Issabekov
  • 2026-09-23 17:37Merge mock off-topic fixOraz Issabekov
  • 2026-09-23 17:36fix(agent): scripted top-list intent needs a graph cue, so off-topic questions get the fallbackOraz Issabekov
  • 2026-09-23 17:34feat(graph): Russian labels for the anomaly flags on the node cardOraz Issabekov
  • 2026-09-23 17:31feat(ui): dark theme by default when no choice is storedOraz Issabekov
  • 2026-09-23 17:24fix(ui): light graph theme, readable clusters, roomier assistant, Russian labelsOraz Issabekov
  • 2026-09-23 17:01chore(output): regenerate after acc40e2 (clearer priority why); role counts unchangedOraz Issabekov
  • 2026-09-23 17:00feat(graph): graph-first layout, periphery hidden by default, role-colour neon on straight edgesOraz Issabekov
  • 2026-09-23 17:00docs(readme): the scenario matches the redesigned screen; disclose sigma pluginsOraz Issabekov
  • 2026-09-23 17:00docs(plan): clean-clone check of the README passed (install, pipeline, gate, build)Oraz Issabekov
  • 2026-09-23 17:00chore: commit the block next dev appends to AGENTS.md, as CLAUDE.md advisesOraz Issabekov
  • 2026-09-23 16:50fix(graph): clarify priority evidence for defenceСаян Кареев
  • 2026-09-23 16:39docs(readme): sync role thresholds, counts, clustering and priority weights with the codeOraz Issabekov
  • 2026-09-23 16:38feat(graph): hub-and-spoke layout; outputs regeneratedOraz Issabekov
  • 2026-09-23 16:34chore: add @sigma/node-border and @sigma/edge-curve for the graph redesignOraz Issabekov
  • 2026-09-23 16:34feat(graph): money path — reverse BFS from a node to the seeds, animated hop by hopOraz Issabekov
  • 2026-09-23 16:34feat(graph): neon canvas — dark ground in both themes, glow, curved role-coloured streams, hover focus, money-path and hidden-roles propsOraz Issabekov
  • 2026-09-23 16:34feat(graph): analytics dashboard around the graphOraz Issabekov
  • 2026-09-23 16:34feat(graph): wire role filter and money path into the canvas and the cardOraz Issabekov
  • 2026-09-23 16:27feat(graph): outputs on the calibrated rules; the mock reads a role in «кто собирает»Бекжан Қайратов
  • 2026-09-23 16:17fix(api): Russian error messages from /api/chatБекжан Қайратов
  • 2026-09-23 16:12fix(graph): calibrate explainable role rulesСаян Кареев
  • 2026-09-23 16:10fix(agent): review of c1ea0c8 — Russian names, rounded results, one card for «кого первым»Бекжан Қайратов
  • 2026-09-23 16:05Merge graph screen visual fixes (Ораз)Oraz Issabekov
  • 2026-09-23 16:05fix(graph): readable canvas — no label pile on focus, receding dim nodes, laptop fitOraz Issabekov
  • 2026-09-23 16:02feat(assistant): render the model's **bold** and headings; review notesOraz Issabekov
  • 2026-09-23 15:58docs(plan): three-gid check of the roles, request to Саян on peripheral evidenceБекжан Қайратов
  • 2026-09-23 15:58docs(decisions): live run on gpt-6-luna answers the three demo questionsБекжан Қайратов
  • 2026-09-23 15:50docs(plan): review of Саян's analytics model — three blockers for the defenceOraz Issabekov
  • 2026-09-23 15:47feat(agent): seven graph tools, the scripted demo, and the analyst promptБекжан Қайратов
  • 2026-09-23 15:39feat(graph): implement analytics modelСаян Кареев
  • 2026-09-23 15:36docs: demo script; README matches the working pipelineOraz Issabekov
  • 2026-09-23 15:31feat(graph): the screen reads the real analysis; pipeline runs before dev/buildOraz Issabekov
  • 2026-09-23 15:27feat(graph): read the pipeline's analysis.json, cached and validatedБекжан Қайратов
  • 2026-09-23 15:27feat(graph): pipeline end to end — parquet in, three CSVs and analysis.json outБекжан Қайратов
  • 2026-09-23 15:27feat(graph): audit the input, hash it, and write run_summary.jsonБекжан Қайратов
  • 2026-09-23 15:25feat(graph): dock the assistant beside the graphOraz Issabekov
  • 2026-09-23 15:17Merge assistant widget (Ораз, Chunk 3)Oraz Issabekov
  • 2026-09-23 15:17feat(assistant): dockable assistant widget with clickable gidsOraz Issabekov
  • 2026-09-23 15:12Merge graph screen (Ораз, Chunk 2)Oraz Issabekov
  • 2026-09-23 15:11feat(graph): graph screen on the sample analysisOraz Issabekov
  • 2026-09-23 15:03docs(readme): add Саян's surname from his git identityOraz Issabekov
  • 2026-09-23 15:02Merge README and solution diagram (Ораз, Chunk 4)Oraz Issabekov
  • 2026-09-23 15:02docs: product README and solution diagramOraz Issabekov
  • 2026-09-23 15:00docs(plan): answer Саян's contract request and review ANALYTICS.mdOraz Issabekov
  • 2026-09-23 14:56docs: define AML network analytics methodologyСаян Кареев
  • 2026-09-23 14:49feat(graph): freeze the contract, add graph deps and the case dataOraz Issabekov
  • 2026-09-23 14:44chore: pick gpt-6-luna as the live modelOraz Issabekov
  • 2026-09-23 14:40docs: plan and decisions for Track 2 "Graph of money"Oraz Issabekov
  • 2026-09-23 13:45chore: pre-existing hackathon starter templateOraz Issabekov
Структура репозитория (177 файлов)
.claude/settings.json
.env.example
.env.test
.gitattributes
.gitignore
.prettierignore
.prettierrc
AGENTS.md
CLAUDE.md
README.md
data/edges.parquet
data/nodes.parquet
data/transactions.parquet
docs/architecture.md
docs/decisions.md
docs/demo.md
docs/design-system.md
docs/kickoff.md
docs/plan.md
docs/solution.svg
docs/ui-patterns.md
eslint.config.js
next.config.mjs
output/analysis.json
output/clusters.csv
output/nodes_roles.csv
output/run_summary.json
output/top_nodes.csv
package.json
pnpm-lock.yaml
postcss.config.mjs
scripts/check.mjs
scripts/pipeline.ts
scripts/upload-files.ts
src/app/api/chat/route.ts
src/app/design/page.tsx
src/app/globals.css
src/app/globals.spec.ts
src/app/layout.tsx
src/app/page.tsx
src/entities/.gitkeep
src/instrumentation.ts
src/server/agent/data/.gitkeep
src/server/agent/model/agent.schema.ts
src/server/agent/repo/.gitkeep
src/server/agent/usecase/agent.spec.ts
src/server/agent/usecase/agent.ts
src/server/agent/usecase/client.ts
src/server/agent/usecase/defineTool.ts
src/server/agent/usecase/mock.ts
src/server/agent/usecase/prompt.ts
src/server/agent/usecase/structured.ts
src/server/agent/usecase/tools.spec.ts
src/server/agent/usecase/tools.ts
src/server/graph/data/ANALYTICS.md
src/server/graph/data/DEFENCE.md
src/server/graph/model/anomalies.spec.ts
src/server/graph/model/anomalies.ts
src/server/graph/model/clusters.spec.ts
src/server/graph/model/clusters.ts
src/server/graph/model/graph.schema.ts
src/server/graph/model/metrics.spec.ts
src/server/graph/model/metrics.ts
src/server/graph/model/priority.spec.ts
src/server/graph/model/priority.ts
src/server/graph/model/queries.spec.ts
src/server/graph/model/queries.ts
src/server/graph/model/roles.spec.ts
src/server/graph/model/roles.ts
src/server/graph/repo/analysis.spec.ts
src/server/graph/repo/analysis.ts
src/server/graph/repo/outputs.spec.ts
src/server/graph/repo/outputs.ts
src/server/graph/repo/parquet.spec.ts
src/server/graph/repo/parquet.ts
src/server/graph/usecase/analyze.spec.ts
src/server/graph/usecase/analyze.ts
src/server/graph/usecase/getAnalysis.ts
src/server/graph/usecase/layout.ts
src/server/graph/usecase/tools.spec.ts
src/server/graph/usecase/tools.ts
src/server/kernel/ctx.ts
src/server/kernel/env.spec.ts
src/server/kernel/env.ts
src/server/kernel/errors.ts
src/shared/config/.gitkeep
src/shared/lib/form.spec.ts
src/shared/lib/form.ts
src/shared/lib/index.ts
src/shared/lib/theme.ts
src/shared/ui/Avatar/Avatar.tsx
src/shared/ui/Avatar/identity.spec.ts
src/shared/ui/Avatar/identity.ts
src/shared/ui/Avatar/index.ts
src/shared/ui/Badge/Badge.tsx
src/shared/ui/Badge/index.ts
src/shared/ui/Button/Button.tsx
src/shared/ui/Button/index.ts
src/shared/ui/Callout/Callout.tsx
src/shared/ui/Callout/index.ts
src/shared/ui/Card/Card.tsx
src/shared/ui/Card/index.ts
src/shared/ui/Checkbox/Checkbox.tsx
src/shared/ui/Checkbox/index.ts
src/shared/ui/ConfirmButton/ConfirmButton.tsx
src/shared/ui/ConfirmButton/index.ts
src/shared/ui/ConfirmDialog/ConfirmDialog.tsx
src/shared/ui/ConfirmDialog/index.ts
src/shared/ui/Field/Field.tsx
src/shared/ui/Field/index.ts
src/shared/ui/Input/Input.tsx
src/shared/ui/Input/index.ts
src/shared/ui/NumberInput/NumberInput.tsx
src/shared/ui/NumberInput/digits.spec.ts
src/shared/ui/NumberInput/digits.ts
src/shared/ui/NumberInput/index.ts
src/shared/ui/SegmentedControl/SegmentedControl.tsx
src/shared/ui/SegmentedControl/index.ts
src/shared/ui/Select/Select.tsx
src/shared/ui/Select/index.ts
src/shared/ui/Table/Table.tsx
src/shared/ui/Table/index.ts
src/shared/ui/Textarea/Textarea.tsx
src/shared/ui/Textarea/index.ts
src/shared/ui/ThemeToggle/ThemeToggle.tsx
src/shared/ui/ThemeToggle/index.ts
src/shared/ui/Toast/Toast.tsx
src/shared/ui/Toast/index.ts
src/shared/ui/Toast/useToast.ts
src/shared/ui/control.ts
src/shared/ui/rawControls.spec.ts
src/views/chat/index.ts
src/views/chat/ui/ChatScreen.tsx
src/views/graph/index.ts
src/views/graph/model/buildGraph.ts
src/views/graph/model/canvasStyle.ts
src/views/graph/model/canvasTypes.ts
src/views/graph/model/focus.ts
src/views/graph/model/format.ts
src/views/graph/model/graphIndex.spec.ts
src/views/graph/model/graphIndex.ts
src/views/graph/model/moneyPath.spec.ts
src/views/graph/model/moneyPath.ts
src/views/graph/model/roles.ts
src/views/graph/model/sampleAnalysis.spec.ts
src/views/graph/model/sampleAnalysis.ts
src/views/graph/model/useMoneyPath.ts
src/views/graph/model/usePalette.ts
src/views/graph/ui/CanvasErrorBoundary.tsx
src/views/graph/ui/GraphCanvas.tsx
src/views/graph/ui/GraphScreen.tsx
src/views/graph/ui/GraphSkeleton.tsx
src/views/graph/ui/Legend.tsx
src/views/graph/ui/MoneyPathControl.tsx
src/views/graph/ui/NodeCard.tsx
src/views/graph/ui/RankTables.tsx
src/views/graph/ui/RoleFilter.tsx
src/views/graph/ui/RoleTag.tsx
src/views/graph/ui/StatsBar.tsx
src/widgets/appShell/index.ts
src/widgets/appShell/ui/AppShell.tsx
src/widgets/appShell/ui/NavLink.tsx
src/widgets/assistant/index.ts
src/widgets/assistant/lib/gids.spec.ts
src/widgets/assistant/lib/gids.ts
src/widgets/assistant/lib/inlineMarkdown.spec.ts
src/widgets/assistant/lib/inlineMarkdown.ts
src/widgets/assistant/model/suggestions.ts
src/widgets/assistant/model/useChat.ts
src/widgets/assistant/ui/AgentActivity.tsx
src/widgets/assistant/ui/Assistant.tsx
src/widgets/assistant/ui/Composer.tsx
src/widgets/assistant/ui/GidText.tsx
src/widgets/assistant/ui/MessageList.tsx
tsconfig.eslint.json
tsconfig.json
vitest.config.ts

Обсуждение 0 комментариев

Войдите, чтобы комментировать

Пока нет комментариев — будьте первым.