AAYMA
RU
Войти

Аким на 5 часов

Городской симулятор бюджета Астаны: пользователь делает 5 решений из 14 мер по 5 районам, детерминированный движок считает Quality of Life Score, а…

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

О проекте

Команда: lowtap
Кейс HackAlem.ai: Спецтрек — Astana Innovations
Стадия: Доведённый продукт

Задача кейса: «Аким на 5 часов»: городской симулятор, распределяющий бюджет по 5 направлениям и 5 районам

Суть

Городской симулятор бюджета Астаны: пользователь делает 5 решений из 14 мер по 5 районам, детерминированный движок считает Quality of Life Score, а LLM пишет аналитическую записку, где каждое число сверяется с движком.

Как устроено

Детерминированный движок на Python (валидатор с 12 кодами нарушений, Score с лагами и синергиями, вклады Шепли по 32 подмножествам, полный перебор 694 395 допустимых наборов для перцентиля и оптимума) продублирован на TypeScript в браузере, что даёт офлайн-режим и ghost-превью. LLM-агент на OpenAI Responses API вызывает строгие инструменты evaluate_scenario, best_neighbors и compare_scenarios и получает только таблицу фактов F1..Fn. guard_report перепроверяет каждое десятичное число (допуск ±0.005) и пересчитывает рекомендации. Провайдеры идут цепочкой cache → llm → rules, ответ всегда HTTP 200, стриминг через SSE.

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

  • Shapley attribution
  • exhaustive enumeration
  • leave-one-out
  • function calling
  • number-grounding guard
  • SSE streaming
  • cache/llm/rules fallback chain
  • TS/Python engine parity tests

Метрики

Score примера ТЗ 56.543 при базе 52.558, оптимум 57.237, перцентиль 99.918%; 298 backend- и 143 frontend-теста, 8 e2e

Особенности

Принцип «движок считает, LLM объясняет»: guard проверяет каждое число в тексте LLM и перевалидирует рекомендации. TS-зеркало движка с тестом паритета на golden.json позволяет работать без бэкенда и без ключа. Полный перебор всех планов даёт честный перцентиль и оптимум.

Стек

Python 3.14 · FastAPI · React 19 · Vite · TypeScript · zustand · Docker · Docker Compose · GitHub Actions · Playwright · pytest

Модели

OpenAI Responses API


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

README

README.md

Аким на 5 часов

AI-симулятор бюджетных решений по районам Астаны. Хакатон, спец-трек Astana Innovations, кейс «Аким на 5 часов», команда lowtap. Сдача: тег v1.0.5.

Пользователь получает 100 у.е., пять реальных районов Астаны на карте и каталог из 14 мер по пяти направлениям. Он принимает ровно пять решений, а приложение считает Astana Quality of Life Score по формуле ТЗ, показывает влияние на 50 показателей и пишет аналитическую записку с сильными сторонами, рисками, последствиями и рекомендациями.

Ключевая идея: движок считает, LLM объясняет, числа проверяются. Score, вклады мер и альтернативы вычисляет детерминированный движок; языковая модель получает только таблицу фактов, ссылается на них и советует; каждое десятичное число в её записке сверяется с движком, каждая рекомендация пересчитывается сервером. Без API-ключа и без сети приложение работает полностью.

Актуальный интерфейс: фон карты, выбор M7, пресет «Пример ТЗ», приближение карты, таймлайн Q4 и Q8, Вердикт, Сравнение

Карту можно перемещать мышью и плавно приближать колёсиком; кнопка центрирования возвращает исходный вид. Скриншоты и демо сняты в локальном режиме без бэкенда; записка Вердикта сформирована по шаблону на числах движка.

ПультВердиктСравнение
Пульт с фоном карты и управлением масштабомВердикт в локальном режимеСравнение сценариев

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

Ключ OpenAI не обязателен: без него для пяти пресетов сервер отдаёт сохранённые ответы агента, для остальных наборов строит записку по правилам на тех же числах.

Вариант 1. Docker

Нужен Docker с Compose. PowerShell:

if (-not (Test-Path .env)) { Copy-Item .env.example .env }
docker compose up --build -d --wait

bash:

test -f .env || cp .env.example .env
docker compose up --build -d --wait

Интерфейс: http://localhost:8000, Swagger: http://localhost:8000/docs. Образ собирает фронт на node:24-alpine и запускает FastAPI на python:3.14-slim. Остановка: docker compose down. Если порт 8000 занят, задайте другой перед запуском: $env:APP_PORT='8010' в PowerShell или APP_PORT=8010 в bash. HTTP-проверка собранного контейнера: py -3 scripts/container_smoke.py (bash: python3 scripts/container_smoke.py); при другом порте добавьте --base-url http://127.0.0.1:8010.

Вариант 2. Локально, одно приложение на порту 8000

FastAPI отдаёт собранный фронт. Нужны Python 3.14 (проверено на CPython 3.14.7, его же используют Docker и CI) и Node.js 20 или новее (проверено на 24). Команды выполняются из корня клона, активация venv не нужна.

PowerShell:

$env:PYTHONUTF8='1'
py -3 -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r backend\requirements.txt
cd web; npm ci; npm run build; cd ..
cd backend; ..\.venv\Scripts\python.exe -m uvicorn app.main:app --port 8000

bash:

export PYTHONUTF8=1
python3 -m venv .venv
.venv/bin/python -m pip install -r backend/requirements.txt
(cd web && npm ci && npm run build)
cd backend && ../.venv/bin/python -m uvicorn app.main:app --port 8000

Интерфейс: http://localhost:8000, Swagger: http://localhost:8000/docs. Сервер слушает 127.0.0.1; если localhost на Windows отвечает медленно из скриптов, обращайтесь к http://127.0.0.1:8000.

Вариант 3. Только фронт, без Python

cd web
npm ci
npm run dev

Откройте http://localhost:5173. Бэкенд не нужен: TypeScript-движок в браузере зеркалит Python-движок (паритет проверяется тестом на data/golden.json), записка строится по шаблону на числах движка, в шапке горит бейдж «офлайн». Если на порту 8000 запущен uvicorn, dev-сервер проксирует /api, и приложение само переключается на сервер.

Ключ OpenAI (необязательно)

Copy-Item .env.example .env
cp .env.example .env

Заполните OPENAI_API_KEY и OPENAI_MODEL (id модели из GET /v1/models вашего ключа). Цепочка провайдеров при значении по умолчанию AI_CACHE=first: cache, затем llm, затем rules; ответ всегда HTTP 200. Числа одинаковы с ключом и без: LLM их не считает. Все переменные окружения и режимы кэша описаны в docs/ARCHITECTURE.md.

Проверка за 5 минут

Команды PowerShell из корня клона; в bash замените .\.venv\Scripts\python.exe на .venv/bin/python.

КомандаОжидаемый результат
cd web; npm test143 passed: эталоны 52.558 / 56.543 / 55.667 / 54.009 / 52.041 / 57.237, 12 невалидных наборов дают ровно свой код нарушения, 71 кейс golden.json совпадает с Python до 1e-6
cd web; npx playwright install chromium; npm run e2e8 passed: пресет «Пример ТЗ» даёт 56.54 и 95/100, невалидный бюджет даёт причину вместо Score; проверены масштабирование, перетаскивание, центрирование и взаимодействие с картой. Порт задаётся переменной E2E_PORT (по умолчанию 5173)
$env:PYTHONUTF8='1'; .\.venv\Scripts\python.exe -m pytest -q backend/tests298 passed, 1 deselected (полный перебор запускается отдельно: -m slow)
cd backend; ..\.venv\Scripts\python.exe -m app.cli evaluate ../data/scenarios/example_tz.jsonScore 56.543, baseline 52.558, cost 95, n_crit 0, percentile 99.918%, scenario_id efb979f1c9c1
docker compose up --build -d --wait, затем py -3 scripts/container_smoke.pyhealth; 5 районов и 14 мер; распределение 694 395 / 20 003; example 56.543 / 95 / 0; HTTP 422; SSE; HTML; 5 из 5 demo-ответов из кэша
Swagger, POST /api/evaluate с телом data/scenarios/invalid_budget.jsonHTTP 422, код BUDGET_EXCEEDED, сообщение «превышение на 29 у.е.»
POST /api/analyze с телом example_tz.json без ключаprovider: "cache", в трассе шаг kind: "agent", verified_numbers 10 из 10; с ?provider=rules записка по правилам, 21 из 21
GET /api/config, поле data_hash1172683703cf; тот же хэш показан внизу Пульта

Основной сценарий

Экран с картой и каталогом мер далее называется Пультом (кнопки «Пульт» и «на Пульт» в интерфейсе).

  1. Откройте Пульт. База 52.56, две критические пары, у Нуры штриховка: школы 38 и поликлиники 35 ниже порога 40.
  2. Наведите курсор на карточку «M7 Школа + детсад»: карта и число в правой панели показывают предсказанный результат 54.01, хотя мера ещё не поставлена.
  3. Нажмите M11 «Безопасные переходы»: на всех пяти районах появятся предсказанные дельты. Алматы получает −0.87 (T1 падает с 40 до 38.25, новая критическая пара), Нура +0.32, лучший вариант. Esc отменяет выбор.
  4. Меню «Пресеты», пункт «Пример ТЗ»: Score 56.54 (+3.99), 95/100 у.е., критических пар 0, валидатор 6/6, набор лучше примерно 99.92 % допустимых сценариев.
  5. Пробел запускает проигрывание 8 кварталов. В Q4 у Нуры S1 достигает 40.0, слагаемое − N_crit 2 перечёркивается и становится 1; в Q6 критических пар 0; в Q8 Score равен тем же 56.54.
  6. Кнопка «AI-анализ» открывает Вердикт: трасса анализа, водопад вкладов мер по Шепли и записка. Наведите курсор на число «+1.45» в записке: подсветится столбик M7. У рекомендации «M5 на M3 Нура» наведите курсор на «Примерить», затем нажмите «Применить»: приложение возвращается на Пульт, гнездо M3 подсвечено, Score 57.21.
  7. Кнопка сравнения в шапке: база 52.56, пример ТЗ 56.54, самый дешёвый набор 55.67, оптимум 57.24 и ваш сценарий 57.21 на гистограмме всех 694 395 допустимых наборов.

Сценарий показа на 3 минуты с таймкодами: docs/DEMO.md. Тезисы и ответы на вопросы: docs/PITCH.md.

Как это устроено

Браузер: web/ (React 19 + Vite + TypeScript)
  Пульт -> Вердикт -> Сравнение (экран в zustand + location.hash, пермалинк #/pult?d=M7:nura,M12,...)
  web/src/engine: TS-зеркало движка (ghost-превью, what-if, таймлайн, офлайн-режим)
        | HTTP JSON и SSE (trace -> report -> done)
FastAPI: backend/app/api/routes.py
  /api/health  /api/config  /api/validate  /api/evaluate  /api/analyze
  engine/ (валидатор, Score, Шепли, соседи, факты, перебор)  <-  ai/ (провайдеры llm / cache / rules, guard)
  StaticFiles(web/dist) на /

Движок и LLM

Движок (Python backend/app/engine, зеркало web/src/engine)LLM (backend/app/ai)
Валидатор: 12 кодов нарушений, все нарушения возвращаются сразуПолучает только таблицу фактов F1..Fn из движка; произвольный текст пользователя в промпт не попадает
I' = clip(I + эффект × (8 − L)/8 + синергии), D, D_avg, min D, N_crit, ScoreПишет разделы записки со ссылками evidence на F-id
Вклады Шепли по 32 подмножествам, LOO, водопадПредлагает улучшения; сервер перевалидирует и пересчитывает каждое (verified)
Соседние наборы, перцентиль по полному перебору 694 395 наборовЧисла не считает: guard сверяет каждое десятичное число с фактами (допуск ±0.005)
Таймлайн Q0..Q8, what-if, ghost-превью (TS, меньше 1 мс)Нет ключа, таймаут или ошибка: ответ из кэша или по правилам, HTTP 200

Guard

Каждый отчёт, от LLM, из кэша или по правилам, проходит guard_report (backend/app/ai/guard.py). Рекомендации заново валидируются и оцениваются движком: Score, дельта и стоимость перезаписываются, невалидная рекомендация получает verified: false и причину. Неизвестные F-id удаляются из evidence. Десятичные числа в тексте сверяются с фактами и результатами инструментов; счётчик verified_numbers показывается в интерфейсе как бейдж «N/N чисел подтверждены движком». Guard проверяет происхождение чисел, а не смысл каждого утверждения.

Агент и провайдеры

Живой режим (backend/app/ai/agent.py) — ручной цикл OpenAI Responses API со строгими схемами инструментов evaluate_scenario, best_neighbors, compare_scenarios. Результаты инструментов считает Python-движок, шаги с kind: "agent" в трассе фиксирует сервер, модель журнал не заполняет. Лимит раундов задаёт MAX_TOOL_ROUNDS.

Порядок провайдеров зависит от AI_CACHE: first (по умолчанию) — cache, llm, rules; fallback — llm, cache, rules; 0 — llm, rules. ?provider=rules или AI_PROVIDER=rules включает объяснение по правилам без кэша. Ошибки SDK, таймауты и занятый семафор не превращаются в HTTP 5xx: сервер переходит к следующему звену. stream=1 отдаёт SSE: события trace по одному, затем report после guard и done.

Реальный ответ агента (OpenAI gpt-4.1-mini, цикл инструментов) на пресет «Пример ТЗ» сохранён в data/cache/demo/efb979f1c9c1.json; без ключа сервер отдаёт его из кэша. Фрагмент:

{
  "summary": "Сценарий улучшил Score до 56.54, закрыл 2 критических показателя Нуры.",
  "strengths": [{ "text": "Score вырос на 3.99 до 56.54, что лучше базового 52.56.", "evidence": ["F1", "F2", "F3"] }],
  "risks": [{ "text": "Лаг мер M7, M8 по 3 квартала, M10 — 1 квартал, эффект реализуется не сразу.", "evidence": ["F94", "F100", "F106"] }],
  "recommendations": [
    { "change": "Заменить M5 (Сарыарка) на M3 (Нура) для транспорта.", "rationale": "Повышение Score до 57.21, полный бюджет 100 у.е.",
      "score": 57.20556, "delta": 4.64788, "cost": 100, "verified": true }
  ],
  "provider": "llm",
  "model": "gpt-4.1-mini",
  "trace": [
    { "n": 1, "kind": "server", "tool": "evaluate_scenario", "output_summary": "Score 56.543; критических пар 0" },
    { "n": 2, "kind": "server", "tool": "build_facts", "output_summary": "Сформировано фактов: 120" },
    { "n": 3, "kind": "agent", "tool": "best_neighbors", "output_summary": "Проверено альтернатив: 3." },
    { "n": 4, "kind": "server", "tool": "agent_loop", "output_summary": "Получен отчёт агента; модель gpt-4.1-mini-2025-04-14; токены: вход 28923, выход 1688." },
    { "n": 5, "kind": "server", "tool": "guard_report", "output_summary": "Подтверждено чисел: 10 из 10." }
  ],
  "verified_numbers": { "total": 10, "confirmed": 10, "unverified": [] }
}

Шаг с kind: "agent" — инструмент, который вызвала сама модель: она проверила альтернативы движком, прежде чем советовать. Score и дельту рекомендации сервер перезаписал своим пересчётом.

Паритет двух движков

Python-движок — источник истины; TypeScript-движок нужен для мгновенного предпросмотра и офлайн-режима. Паритет проверяется механически: python -m app.cli golden (из backend/) пишет data/golden.json с 71 кейсом (59 валидных с полной оценкой и 12 невалидных с кодами нарушений), а web/src/engine/engine.parity.test.ts сверяет с ним TS-движок по всем полям результата с допуском 1e-6.

Подробное описание бэкенда по этапам, настройки и Docker: docs/ARCHITECTURE.md.

Соответствие ТЗ

ТребованиеРеализацияФайлыКак проверить
Единый виртуальный бюджет и данныеОдин датасет data/*.json, data_hash в /api/config и внизу Пульта, пользовательских настроек нетdata/, web/src/engine/hash.ts, backend/app/engine/catalog.py/api/config, тест data_hash в engine.parity.test.ts
Решения по 5 направлениямКаталог 14 мер по направлениям ТЗ, не более 2 на направление, 5 гнёзд решенийweb/src/components/catalog/, web/src/engine/validate.ts, backend/app/engine/validator.pyUI, data/scenarios/invalid_direction.json
Автоматический контроль превышения бюджетаШкала бюджета из 100 тиков с красной зоной перерасхода, блокировка карточки с причиной, HTTP 422 на сервереweb/src/components/score/BudgetTicks.tsx, validate.ts, backend/app/api/routes.pyinvalid_budget.json даёт 422, e2e smoke
AI-анализ принятых решений/api/analyze: факты движка, затем LLM или правила, затем guard; офлайн — шаблон на тех же числахbackend/app/ai/, web/src/store/analysis.ts, web/src/components/verdict/MemoTemplate.tsВердикт, backend/tests/test_ai_guardrail.py, memo.test.ts
Расчёт Astana Quality of Life ScoreФормула ТЗ в двух движках с паритетом 1e-6backend/app/engine/scoring.py, web/src/engine/score.tspytest, vitest, CLI
Сильные стороны, риски, последствияРазделы записки: итог, сильные стороны, риски, последствия, компромиссы, влияние на город, рекомендацииbackend/app/ai/rules.py, MemoTemplate.ts, MemoDocument.tsxВердикт, поля ответа /api/analyze
Опционально: сравнение результатовЭкран Сравнения: пять сценариев в одной шкале, перцентиль по полному переборуweb/src/screens/Compare.tsx, backend/app/engine/distribution.pyUI
Опционально: визуализация изменений районовКарта Q0 и Q8, матрица 5 районов на 10 показателей, изменение D по районам, таймлайн 8 кварталовweb/src/components/map/, bottom/IndicatorMatrix.tsx, verdict/DistrictSlope.tsxUI (клавиша M открывает матрицу)
Опционально: AI-рекомендацииСоседние наборы на один шаг, каждый пересчитан движком, кнопки «Примерить» и «Применить»backend/app/engine/search.py, web/src/engine/neighbors.ts, MemoDocument.tsxВердикт
Опционально: городские события, автогенерация презентацииНе реализовано
Критерий проверкиКак закрытЧем подтверждается
Все начинают с одинакового бюджета и данныхОдин датасет, один data_hash/api/config, футер Пульта
Система не позволяет превысить бюджетБлокировка в UI и BUDGET_EXCEEDED на сервереe2e smoke, HTTP 422
Решения влияют на показатели50 показателей до и после, карта, матрица; набор M11 в Алматы хуже бездействияТесты движка, пресет «Худший» 52.04
AI объясняет результат и компромиссыЗаписка с разделами и evidence, проверка чисел guardБейдж «N/N подтверждены», verified_numbers
Изменение набора меняет ScoreGhost-превью, what-if на пяти районах, пресеты 56.54 / 55.67 / 57.24UI, vitest

Модель и данные

Датасет ТЗ лежит в data/: 5 районов с долями населения и 10 показателями (шкала 0–100, больше значит лучше), 14 мер с направлением, типом «Район» или «Город», стоимостью, лагом и эффектами, правила и веса в data/rules.json, границы районов в data/astana_districts.geojson.

Формула: Score = 0.7 × D_avg + 0.3 × min D − N_crit, где D — взвешенный индекс района, D_avg — среднее по долям населения, N_crit — число пар район и показатель строго ниже 40 после clip. Доля эффекта меры с лагом L равна (8 − L)/8. Синергии M1+M2, M10+M12, M5+M6 дают фиксированный бонус без лага в районе районной меры пары. Несовместимости: M1 и M3 в любых районах; M4 и M7, а также M5 и M13 в одном районе.

Эталоны (округление для показа):

СценарийScoreСтоимостьN_crit
База без решений52.55802
Пример ТЗ (example_tz.json)56.543950
Самый дешёвый допустимый (cheapest.json)55.667611
Всё в Есиль (naive_esil.json)54.0091002
Худший (worst_of_all.json)52.041803
Оптимум полного перебора (optimum.json): M2, M3 Нура, M8 Нура, M9 Нура, M1457.237980

Полный перебор (scripts/enumerate_plans.py, результат в data/plan_distribution.json) даёт 694 395 допустимых наборов, из них 20 003 хуже бездействия. Перцентиль сценария считается по этому распределению.

Округление: движок хранит 10 знаков, интерфейс показывает 2. 56.543 отображается как 56.54, дельта считается от неокруглённых значений: +3.985 показывается как +3.99.

Предпросмотр и Score по ТЗ различаются. Пока набор не собран по правилам (ровно 5 решений, без нарушений), число на Пульте подписано «предварительно», показано серым с пунктиром и без перцентиля. Astana Quality of Life Score появляется только при 6/6 валидатора; на невалидный набор сервер отвечает 422 без числа.

Таймлайн по кварталам — визуализация лага сверх ТЗ: к кварталу q эффект равен эффект × max(0, q − L)/8, синергия входит целиком с квартала max(L_a, L_b) + 1. В Q8 это ровно формула ТЗ.

Структура репозитория

backend/            FastAPI: app/engine (движок), app/ai (провайдеры, guard, агент), app/api, tests
data/               датасет ТЗ, границы районов (GeoJSON), сценарии-эталоны, golden.json,
                    plan_distribution.json, cache/demo (сохранённые ответы агента)
docs/               ARCHITECTURE.md, DEMO.md, PITCH.md, TASK_SPEC.md, VISUAL_SPEC.md, PLAN.md, screenshots/, demo.gif
scripts/            enumerate_plans.py (полный перебор), ai_eval.py (mini-eval), container_smoke.py
web/
  src/engine/       TS-движок: score, validate, timeline, contributions, whatif, neighbors, evaluate, presets
  src/screens/      Pult, Verdict, Compare
  src/components/   map, catalog, score, header, bottom, verdict
  src/store/        scenario (набор и undo), ui (экран, режим, квартал), analysis
  src/lib/          api (офлайн-фолбэк), sse, geo, format, permalink, verify
  e2e/              Playwright: smoke и навигация карты
Dockerfile, docker-compose.yml, .github/workflows/ci.yml

Стек

СлойТехнологии
Движок и APIPython 3.14, FastAPI, pydantic v2, uvicorn, pytest, Ruff
LLMОфициальный OpenAI Python SDK (Responses API, structured outputs, function tools), цепочка llm / cache / rules
ФронтReact 19, Vite 8, TypeScript 5.9, Tailwind 4 (токены), motion, zustand, d3-geo / d3-scale / d3-shape / d3-interpolate, lucide-react
Шрифты (в бандле, офлайн)Unbounded, Golos Text, JetBrains Mono
Тестыvitest (движок, паритет, валидатор, записка, SSE), Playwright (smoke и навигация карты), pytest
Сборка и CIDocker (multi-stage), Docker Compose, GitHub Actions

Тесты

Бэкенд: 298 тестов pytest без сети (движок, валидатор, Шепли, факты, поиск, API, SSE, кэш, guard, цикл агента, таймауты, mini-eval); один медленный тест полного перебора запускается явно: python -m pytest -q backend/tests -m slow. Фронт: 143 теста vitest, из них 71 кейс паритета с Python, и 8 e2e-тестов Playwright. Скрипт scripts/ai_eval.py независимо проверяет пять пресетов: валидность рекомендаций, наличие evidence, подтверждение чисел, объём текста; результаты лежат в data/cache/eval/.

CI (.github/workflows/ci.yml) прогоняет pytest, Ruff, npm ci, npm test, npm run build, затем собирает Compose и выполняет scripts/container_smoke.py; ключей в CI нет. В организации хакатона GitHub Actions отключены на уровне биллинга, поэтому этот workflow на GitHub не запускался; все проверки из таблицы выше выполнены локально и на чистых клонах, их и стоит использовать для оценки.

Ограничения

  • Модель линейная и условная, датасет ТЗ синтетический; лаг упрощён до доли эффекта за 8 кварталов.
  • Таймлайн по кварталам — визуализация сверх ТЗ; итоговый Score всегда считается только по формуле ТЗ.
  • Guard проверяет происхождение чисел, а не смысл каждого утверждения; стиль текста LLM не гарантирован.
  • Без ключа записка строится из сохранённых ответов агента или по правилам; числа при этом те же.
  • Перцентиль офлайн считается по сжатой таблице (расхождение с сервером меньше 0.005 п.п.), на сервере точно.
  • Моделирование городских событий и автогенерация презентации из опциональных пунктов ТЗ не реализованы.

Развитие

Реальные показатели акимата и Smart Astana вместо синтетики; калибровка весов; многопериодная симуляция с переносом бюджета; городские события (шоки бюджета и показателей); лидерборд команд; интеграция с eGov; полный интерфейс на казахском (названия районов RU/KZ уже переключаются в шапке Пульта). Датасет — JSON с хэшем, формула и веса вынесены в data/rules.json, поэтому замена данных не требует изменений кода движка.

Команда

Команда lowtap. Бэкенд, движок и AI-слой: backend/, scripts/. Фронтенд, визуал, документация и демо: web/, docs/. Этапы велись в отдельных ветках и попадали в main после зелёных тестов.

Лицензия и атрибуции

  • Код: MIT, см. LICENSE.
  • Границы районов: © OpenStreetMap contributors, ODbL 1.0.
  • Шрифты Unbounded, Golos Text, JetBrains Mono: SIL Open Font License 1.1; иконки lucide: ISC.
Звёзды
⭐ 0
Форки
0
Язык
TypeScript
Последний push
в прошлом месяце
Коммиты
37
Авторы
2
Файлы
188
Ветки
1

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

coolbay 32Different Type 5

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

  • 2026-09-23 17:54chore: remove unused filescoolbay
  • 2026-09-23 17:52fix(web): smooth map zoom — rasterized landscape, constant-size labels and pinscoolbay
  • 2026-09-23 17:50docs: refresh README screenshots and map demoDifferent Type
  • 2026-09-23 17:41Merge remote-tracking branch 'origin/main' into codex/map-landscape-navigationDifferent Type
  • 2026-09-23 17:38style: improve bottom panel typographyDifferent Type
  • 2026-09-23 17:37fix(web): header wordmark "Аким на 5 часов"; refresh screenshots and GIFcoolbay
  • 2026-09-23 17:36Merge remote-tracking branch 'origin/main' into codex/map-landscape-navigationDifferent Type
  • 2026-09-23 17:34fix(web): page title "Аким на 5 часов — AI-симулятор бюджета Астаны"coolbay
Все коммиты (37)
  • 2026-09-23 17:30docs: refresh README screenshots and demo GIF from the final buildcoolbay
  • 2026-09-23 17:26feat: add landscape background and smooth map navigationDifferent Type
  • 2026-09-23 17:25fix: configurable Compose host port, README notes on busy port, localhost and smoke scriptcoolbay
  • 2026-09-23 17:16docs: professional README rewrite, documentation tone sweep, v1.0.1coolbay
  • 2026-09-23 17:01fix: acceptance punch-list before v1.0coolbay
  • 2026-09-23 16:45docs: Docker quick start in README, backend test count after B5coolbay
  • 2026-09-23 16:44feat(web): own colour for every district on the Pult map, no landmarkcoolbay
  • 2026-09-23 16:38feat(deploy): reproducible Docker app, CI and bounded analysis (stage B5)coolbay
  • 2026-09-23 16:33feat(web): big district plates and Baiterek landmark on the mapcoolbay
  • 2026-09-23 16:25docs: backend test count after stage B4 (291 passed)coolbay
  • 2026-09-23 16:24docs: README shows the real agent answer from data/cache/demo; provider badge with modelcoolbay
  • 2026-09-23 16:22feat(web): RU/KZ district names toggle (option after F5)coolbay
  • 2026-09-23 16:21feat(agent): verified tools, live SSE and five offline demos (stage B4)coolbay
  • 2026-09-23 16:20docs: PITCH.md — theses, numbers and jury Q&Acoolbay
  • 2026-09-23 16:17docs: 3-minute demo script, README GIF and screenshots (stage F5, part 2)coolbay
  • 2026-09-23 16:15docs: README — quick start, 5-minute check, criteria tables (stage F5, part 1)coolbay
  • 2026-09-23 16:13feat(web): compare screen, polish, Playwright smoke (stage F4)coolbay
  • 2026-09-23 16:07feat(web): 8-quarter timeline playback and 5x10 matrix (stage F3)coolbay
  • 2026-09-23 16:03feat(web): pins, what-if, ghost preview, verdict screen, /api/analyze SSE (stage F2)coolbay
  • 2026-09-23 15:55feat(ai): structured reports with guarded fallback and cache (stage B3)coolbay
  • 2026-09-23 15:52feat(web): Pult without pins, full TS engine, template memo (stage F1)coolbay
  • 2026-09-23 15:47feat(api): expose engine and grounded rules reports (stage B2)coolbay
  • 2026-09-23 15:42feat(engine): deterministic scoring, validation and exhaustive search (stage B1)coolbay
  • 2026-09-23 15:36feat(web): Vite + React scaffold, tokens, stores and minimal engine (stage F0)coolbay
  • 2026-09-23 15:34docs(tasks): agents squash-merge their own stages, no human in the loopcoolbay
  • 2026-09-23 15:34feat(backend): bootstrap Python 3.14 app and health endpoint (#1)coolbay
  • 2026-09-23 15:15chore: root scaffold, dataset JSON, presets and two-agent task briefscoolbay
  • 2026-09-23 15:04docs: final project plan, visual spec and Astana district boundariescoolbay
  • 2026-09-23 14:32docs: add project plan and task speccoolbay
Структура репозитория (188 файлов)
.dockerignore
.env.example
.gitattributes
.github/workflows/ci.yml
.gitignore
Dockerfile
LICENSE
README.md
backend/app/__init__.py
backend/app/ai/__init__.py
backend/app/ai/agent.py
backend/app/ai/base.py
backend/app/ai/cache.py
backend/app/ai/evaluation.py
backend/app/ai/guard.py
backend/app/ai/llm.py
backend/app/ai/probe.py
backend/app/ai/prompts.py
backend/app/ai/rules.py
backend/app/ai/service.py
backend/app/ai/tools.py
backend/app/api/__init__.py
backend/app/api/routes.py
backend/app/cli.py
backend/app/config.py
backend/app/engine/__init__.py
backend/app/engine/attribution.py
backend/app/engine/catalog.py
backend/app/engine/distribution.py
backend/app/engine/facts.py
backend/app/engine/golden.py
backend/app/engine/models.py
backend/app/engine/scoring.py
backend/app/engine/search.py
backend/app/engine/validator.py
backend/app/main.py
backend/pyproject.toml
backend/requirements.txt
backend/tests/test_ai_cache.py
backend/tests/test_ai_eval.py
backend/tests/test_ai_guardrail.py
backend/tests/test_ai_loop.py
backend/tests/test_ai_pipeline.py
backend/tests/test_ai_timeout.py
backend/tests/test_ai_tools.py
backend/tests/test_api.py
backend/tests/test_bootstrap.py
backend/tests/test_distribution.py
backend/tests/test_engine.py
backend/tests/test_enumerate_slow.py
backend/tests/test_facts_cli.py
backend/tests/test_golden.py
backend/tests/test_llm.py
backend/tests/test_rules.py
backend/tests/test_search.py
backend/tests/test_sse.py
data/astana_districts.geojson
data/cache/demo/1a19e91138e6.json
data/cache/demo/7017dea1b09b.json
data/cache/demo/9562c4fabba0.json
data/cache/demo/efb979f1c9c1.json
data/cache/demo/f4017a13ec5f.json
data/cache/eval/live.jsonl
data/cache/eval/offline.jsonl
data/cache/eval/rules.jsonl
data/districts.json
data/golden.json
data/measures.json
data/plan_distribution.json
data/rules.json
data/scenarios/cheapest.json
data/scenarios/example_tz.json
data/scenarios/invalid_budget.json
data/scenarios/invalid_city_with_district.json
data/scenarios/invalid_conflict_m4m7.json
data/scenarios/invalid_conflict_m5m13.json
data/scenarios/invalid_direction.json
data/scenarios/invalid_district_required.json
data/scenarios/invalid_dup.json
data/scenarios/invalid_four.json
data/scenarios/invalid_incompat_m1m3.json
data/scenarios/invalid_six.json
data/scenarios/naive_esil.json
data/scenarios/optimum.json
data/scenarios/unknown_district.json
data/scenarios/unknown_measure.json
data/scenarios/worst_of_all.json
docker-compose.yml
docs/ARCHITECTURE.md
docs/DEMO.md
docs/PITCH.md
docs/PLAN.md
docs/TASK_SPEC.md
docs/VISUAL_SPEC.md
docs/demo.gif
docs/screenshots/f2-pult-pins.png
docs/screenshots/f4-compare-1280.png
docs/screenshots/verdict-offline.png
scripts/ai_eval.py
scripts/container_smoke.py
scripts/enumerate_plans.py
web/.gitignore
web/.oxlintrc.json
web/e2e/map-navigation.spec.ts
web/e2e/smoke.spec.ts
web/index.html
web/package-lock.json
web/package.json
web/playwright.config.ts
web/public/favicon.svg
web/scripts/screenshot.mjs
web/src/App.tsx
web/src/components/bottom/BottomPanel.tsx
web/src/components/bottom/IndicatorMatrix.tsx
web/src/components/bottom/Timeline.tsx
web/src/components/catalog/Catalog.tsx
web/src/components/catalog/EffectChips.tsx
web/src/components/catalog/LagStrip.tsx
web/src/components/catalog/MeasureCard.tsx
web/src/components/header/DecisionSockets.tsx
web/src/components/header/Header.tsx
web/src/components/header/Presets.tsx
web/src/components/map/MapAstana.tsx
web/src/components/map/MapLandscape.tsx
web/src/components/map/MapLegend.tsx
web/src/components/map/Pins.tsx
web/src/components/map/PultMap.tsx
web/src/components/map/WhatIfLabels.tsx
web/src/components/score/BudgetTicks.tsx
web/src/components/score/DistrictRows.tsx
web/src/components/score/FormulaRows.tsx
web/src/components/score/Percentile.tsx
web/src/components/score/ScoreHero.tsx
web/src/components/score/ScoreRail.tsx
web/src/components/score/ValidationStrip.tsx
web/src/components/verdict/AgentTrace.tsx
web/src/components/verdict/ContributionWaterfall.tsx
web/src/components/verdict/DistrictSlope.tsx
web/src/components/verdict/MemoDocument.tsx
web/src/components/verdict/MemoTemplate.ts
web/src/components/verdict/QuarterPair.tsx
web/src/components/verdict/VerifiedBadge.tsx
web/src/components/verdict/memo.test.ts
web/src/engine/catalog.ts
web/src/engine/contributions.ts
web/src/engine/distribution.ts
web/src/engine/engine.parity.test.ts
web/src/engine/engine.test.ts
web/src/engine/evaluate.ts
web/src/engine/hash.ts
web/src/engine/neighbors.ts
web/src/engine/presets.ts
web/src/engine/score.ts
web/src/engine/timeline.ts
web/src/engine/validate.ts
web/src/engine/whatif.ts
web/src/hooks/useClosedPairs.ts
web/src/hooks/useDisplayState.ts
web/src/hooks/useEvaluation.ts
web/src/hooks/useGhost.ts
web/src/hooks/useMapNavigation.ts
web/src/hooks/useSize.ts
web/src/lib/api.ts
web/src/lib/colors.ts
web/src/lib/format.ts
web/src/lib/geo.ts
web/src/lib/highlight.ts
web/src/lib/hotkeys.ts
web/src/lib/lib.test.ts
web/src/lib/permalink.ts
web/src/lib/sse.ts
web/src/lib/verify.ts
web/src/main.tsx
web/src/screens/Compare.tsx
web/src/screens/Pult.tsx
web/src/screens/Verdict.tsx
web/src/store/analysis.ts
web/src/store/scenario.ts
web/src/store/store.test.ts
web/src/store/ui.ts
web/src/styles/tokens.css
web/src/types/api.ts
web/src/types/data.ts
web/src/types/virtual.d.ts
web/tsconfig.app.json
web/tsconfig.json
web/tsconfig.node.json
web/vite.config.ts

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

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

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