# Phase 0: Спека v1 + рантайм (формульные сцены)

**Status:** ✅ Implemented (не закоммичено — коммит за оркестратором)
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** frontend

## Objective
Заложить ядро: формат JSON-спеки v1, безопасный движок выражений, рантайм `SimEngine`,
адаптер регистрации в `LabRegistry`. После фазы рукописная спека «брошенное тело» играет в /lab.

## Tasks
- [x] Задокументировать формат спеки v1 в шапке нового файла + в CONTEXT.md (params, objects, viewport, controls). → шапка `_sim_engine.js` (полный JSON-формат) + CONTEXT.md.
- [x] `frontend/js/labs/_sim_expr.js` — безопасный движок выражений: токенайзер → AST → `evaluate(ast, env)`. Whitelist математики (см. CONTEXT.md). Парсер расширяет логику `y=f(x)` из `graph.js` (тот же подход к токенам/неявному умножению; добавлены сравнения, логика, тернарник, multi-var env, min/max/mod/log(b,x)). ⛔ без `eval`/`Function`. `compile(src) -> {ast, fn(env), error}`.
- [x] `frontend/js/labs/_sim_engine.js` — `window.SimEngine.mount(host, spec)`:
  - canvas с мир→экран (равные оси, вписан в viewport, Y вверх) + оверлей-слой `<div>` для подписей (KaTeX `renderToString`, как в graph.js);
  - объекты: point|segment|vector|circle|rect|polyline|path|label (числовые свойства = число или строка-выражение, компилируются один раз в mount);
  - rAF-цикл: `t += dt*speed`, loop/duration, перевычисление привязок, перерисовка, трассы (`trail`);
  - контролы: слайдеры из `params[]` + play/pause/reset; API `{ play, pause, reset, setParam, getParam, isRunning, destroy, el }`.
- [x] `frontend/js/labs/_sim_adapter.js` — `registerSpecSim(spec)` строит манифест LabRegistry (`open(ctx)` → ленивый собственный хост-div + `SimEngine.mount`; `stop` прячет хост+pause; `destroy` уничтожает инстанс; `preview` из спеки или авто-SVG) и регистрирует.
- [x] Фикстура-демо: рукописная спека «projectile» (слайдеры θ 0..90 / v 0..30, точка x=v·cosθ·t, y=max(0, v·sinθ·t−5t²), вектор v0, земля, подпись) — зарегистрирована как `customdemo` за флагом `?simdemo=1` / `?sim=customdemo` / `LAB_SHOW_SPEC_DEMO` / localStorage `lab-spec-demo=1`. Ученикам не светится (карточки в SIMS нет; добавляется только при включённом флаге).
- [x] Подключить новые файлы в /lab прямыми `<script>` (минимально-инвазивно): `_sim_expr/_sim_engine/_sim_adapter` — eager после `_graph_panel.js`; `_sim_demo` — после `_register-all.js`. Lazy-схема `_sim_deps` не тронута (каркасные модули должны быть до диспетчера). Старт /lab и ~40 симуляций не затронуты.

## Files to Modify/Create
- `frontend/js/labs/_sim_expr.js` — движок выражений (new)
- `frontend/js/labs/_sim_engine.js` — рантайм (new)
- `frontend/js/labs/_sim_adapter.js` — адаптер LabRegistry (new)
- `frontend/js/labs/_sim_demo.js` — демо-спека-фикстура (new, временная)
- `frontend/lab.html` или `_sim_deps.js` — подключение файлов (минимальная правка)

## Acceptance Criteria
- В /lab открывается демо-симуляция, слайдеры меняют движение, play/pause/reset работают.
- Движок выражений не использует eval/Function; некорректная формула не роняет рантайм (показывает ошибку/0).
- Существующие ~40 симуляций и старт /lab не сломаны.

## Notes
- Подписи с LaTeX — переиспользовать существующий рендер формул (KaTeX), не тянуть новый.
- Мир-координаты с осью Y вверх (математические), трансформация в экранные внутри движка.
- Производительность: компилировать выражения один раз при mount, в цикле только evaluate.

## Review Checklist
- [x] Все задачи выполнены
- [x] Нет eval/new Function в движке выражений (grep -nE "eval\(|new Function|Function\(" → только упоминания в комментариях, ни одного call-site)
- [x] Нет эмодзи (скан: только ASCII + кириллица + θ/∞/box-drawing — те же баннеры, что в graph.js)
- [x] Старт /lab и существующие симуляции не регрессировали (не тронуты SIMS/OPEN/манифесты; добавлены только новые `<script>` + register())
- [x] Код в стиле проекта (vanilla, IIFE с экспортом в window.*, как _registry/_simbase/_graph_panel)

## Handoff to Next Phase

### Что готово (Phase 0)
- **Движок выражений** `frontend/js/labs/_sim_expr.js` → `window.SimExpr`:
  - `compile(src) -> { ast, fn, error }`. `fn(env) -> number`, НИКОГДА не бросает (NaN/∞/деление на 0 → 0). `error` — строка при синтаксической ошибке (не бросается).
  - `evaluate(ast, env) -> number` (то же, безопасно), `evalSafe(ast, env) -> { value, error }` (для билдера/отладки — отличает NaN от валидного 0).
  - `compileValue(value) -> { fn, error, constant, ast }` — число вернёт константу, строку скомпилирует (используется движком для свойств-привязок).
  - `parse(src) -> ast` (бросает при ошибке), `tokenize(src)`, `FUNCTIONS` / `CONSTANTS` (объекты-«множества» имён, для подсветки в билдере).
  - Whitelist: `+ - * / ^ %`, унарный `-`/`+`/`!`, скобки, сравнения `< <= > >= == !=`, логика `&& ||`, тернарник `?:`; функции `sin cos tan tg ctg cot asin acos atan arcsin arccos arctan arctg sqrt abs exp ln log(x|base,x) log2 log10 floor ceil round sign min max mod atan2 pow hypot`; константы `pi e tau`. Идентификаторы (вкл. точечные `obj.x`) берутся ТОЛЬКО из `env`.
  - ⚠️ Степень право-ассоциативна, базой служит unary → `-2^2 == 4` (паритет с graph.js, задокументировано). Для `-(2^2)` писать скобки.
  - Самопроверка: 29/30 логических кейсов PASS (единственный «FAIL» — `-2^2`, это и есть парити-поведение, не баг).
- **Рантайм** `frontend/js/labs/_sim_engine.js` → `window.SimEngine`:
  - `mount(host, spec) -> instance`. `instance`: `play() pause() reset() setParam(name,val) getParam(name) isRunning() destroy()` + поле `el` (корневой DOM для скрытия/показа).
  - Сцена: панель слайдеров+play/pause/reset слева, `<canvas>` + оверлей `<div>` подписей справа. Мир→экран: равные оси, центрирование, Y вверх. Выражения компилируются 1 раз в mount; в rAF — только evaluate. `env` = `{ t, <params>, w, h, xmin/xmax/ymin/ymax, <objId>.x, <objId>.y }`.
  - Объекты: `point segment vector circle rect polyline path label`. Подписи — KaTeX (фолбэк на текст). Трассы (`trail:true`).
- **Адаптер** `frontend/js/labs/_sim_adapter.js` → `window.registerSpecSim(spec)` / `window.SimAdapter`:
  - Строит манифест LabRegistry из спеки, кладёт `_spec`/`_instance`/`instance()` для будущих фаз. Каждая спек-симуляция получает ленивый хост `#sim-spec-host-<id>` внутри `#lab-sim`. `stop` прячет хост (важно при switch — openSim не знает наших хостов), `destroy` уничтожает инстанс.
- **Демо** `frontend/js/labs/_sim_demo.js` — `customdemo` за флагом, для приёмки.
- **Подключение** в `frontend/lab.html`: 3 каркасных `<script>` после `_graph_panel.js`, демо после `_register-all.js`.

### Формат спеки v1 (поддержан фактически)
`{ specVersion?, id, cat?, meta:{title,desc}, viewport:{xmin,xmax,ymin,ymax,grid?,axes?,bg?}, time:{autoplay?,loop?,duration?,speed?}, params:[{name,label,min,max,step,value,unit?}], objects:[{id?,type,...числа|строки-выражения, color?,fill?,width?,trail?,trailColor?,latex?,size?,text?,points?,closed?}], theory?, subject?,grade?,topics? }`
Полная спецификация полей по типам объектов — в шапке `_sim_engine.js`.

### Осталось на Фазу 1 / временные решения / риски
- **Drag-интеракции, графики, оси-подписи с числами** — Фаза 1 (сейчас оси без числовых меток, есть сетка). `GraphPanelUI` из `_graph_panel.js` готов к переиспользованию для plots.
- **Физика** (`body`/`physics`) — Фаза 2, объекты сейчас чисто кинематические (формульные).
- **Однопроходный env для `obj.x/obj.y`**: центры объектов вычисляются в порядке объявления за один проход — взаимные ссылки «вперёд» (объект ссылается на тот, что объявлен ниже) дадут значение предыдущего кадра (или 0 на первом). Достаточно для типовых сцен; при необходимости в Ф1 сделать топосортировку/итерации.
- **`r` точки** трактуется как экранный радиус в пикселях (не мир-единицы) — намеренно, чтобы точки не «таяли» при масштабе; для circle радиус мировой.
- **Демо `customdemo`** — временный раздел, удалить/спрятать после билдера (Фаза 4). Карточка добавляется в каталог только при включённом флаге.
- **Санитизация подписей/валидация спеки на сервере** — Фаза 3 (сейчас спеки только рукописные/локальные, в БД не идут).
