Files
Learn_System/CLAUDE.md
T

108 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# BQ-System — правила для Claude
## Поиск по коду
**ast-index — дефолт.** ВСЕГДА первым для «найти символ по имени / usages / callers / outline».
Grep/Read — только если ast-index вернул пустой результат.
**vex** — для поиска **по смыслу**, AST-паттернов, дубликатов, компактного тела символа:
`vex search "..." --semantic`, `vex similar`, `vex pattern`, `vex duplicates`, `vex show`.
Что и когда — подробно в `.claude/rules/search-tools.md`. (usages/callers по JS — только ast-index.)
```bash
# Найти класс/функцию/символ
ast-index class "ClassName"
ast-index symbol "functionName"
# Найти использования
ast-index usages "symbolName"
# Поиск по содержимому файла
ast-index search "keyword" --in-file "filename"
# Структура файла
ast-index outline "path/to/file.js"
# Универсальный поиск
ast-index search "query"
```
Grep использовать только для:
- Поиска строковых литералов (`"some text"`)
- Regex-паттернов
- Если ast-index вернул пустой результат
## Git — обновление репо
**После любых изменений** — коммит и push:
```bash
git add <изменённые файлы>
git commit -m "тип: описание"
git push origin master
```
- Коммитить только изменённые файлы (не `git add -A`)
- Сообщение коммита: `feat:` / `fix:` / `refactor:` / `style:` + краткое описание на русском или английском
- Push выполнять сразу после коммита
## Стек
- Node.js/Express backend, SQLite (better-sqlite3, sync)
- Frontend: vanilla JS, без бандлера
- ast-index проиндексирован: `ast-index rebuild` при добавлении новых файлов
## Feature: Конструктор симуляций (SimForge)
Движок авторинга интерактивных 2D-симуляций из JSON-спеки (данные, НЕ код). План: `plans/sim-builder/`.
### Phase 0 — Learnings
- **Спека = данные.** Любое числовое свойство объекта = число ИЛИ строка-выражение. Выражения шарятся между людьми → движок безопасный, ⛔ без `eval`/`new Function`.
- **`window.SimExpr`** (`frontend/js/labs/_sim_expr.js`): токенайзер → AST → evaluate. `compile(src)->{ast,fn,error}`; `fn(env)` НИКОГДА не бросает (NaN/∞/деление на 0 → 0). Whitelist: `+ - * / ^ %`, унарный `- + !`, сравнения `< <= > >= == !=`, логика `&& ||`, тернарник `?:`, функции `sin cos tan tg ctg cot asin..arctg sqrt abs exp ln log log2 log10 floor ceil round sign min max mod atan2 pow hypot`, константы `pi e tau`. Идентификаторы (вкл. точечные `obj.x`) — только из `env`. Парсер — расширение `y=f(x)` из `graph.js`; `-2^2 == 4` (парити). Также `evalSafe`, `compileValue`, `parse`, `tokenize`, `FUNCTIONS`, `CONSTANTS`.
- **`window.SimEngine.mount(host, spec)`** (`_sim_engine.js`) → `{ play, pause, reset, setParam, getParam, isRunning, destroy, el }`. Canvas (мир→экран, равные оси, Y вверх) + KaTeX-оверлей подписей (`katex.renderToString`, как graph.js) + слайдеры из `params[]`. Выражения компилируются 1 раз в mount; в rAF — только evaluate. `env = { t, <params>, w, h, xmin..ymax, <objId>.x, <objId>.y }`. Объекты: `point segment vector circle rect polyline path label`. **Формат спеки v1 — в шапке `_sim_engine.js`.**
- **`window.registerSpecSim(spec)`** (`_sim_adapter.js`): спека → манифест LabRegistry (ленивый хост `#sim-spec-host-<id>` в `#lab-sim`; `stop` прячет, `destroy` уничтожает). Так спек-сим открывается тем же путём, что рукописные ~40 (через `openSim` → реестр).
- Демо `customdemo``_sim_demo.js`, за флагом `?simdemo=1` / `?sim=customdemo` / `LAB_SHOW_SPEC_DEMO` / localStorage `lab-spec-demo=1` (ученикам не светится).
- Подключение: 3 каркасных `<script>` eager после `_graph_panel.js` в `lab.html`, демо — после `_register-all.js`. `_sim_deps.js` не трогать (каркас грузится до диспетчера).
### Phase 1 — Learnings
- **Новые типы объектов** (в `_sim_engine.js`, формат — в шапке файла):
- `plot` — график `f(var)` на canvas движка в мир-координатах (НЕ через `GraphPanelUI` — тот stacked time-series в фикс. оверлее, не `y=f(x)`). Поля: `expr`, `var` (деф.`x`), `range:[a,b]` (числа/выражения, деф. xmin..xmax), `samples` (клампится 2..2000, деф.200), `trace` (точка var=t пишется в trail; при trace без range статич. кривая не рисуется), `color/width`. Свободная переменная подставляется во временную копию env-ключа (восстанавливается после).
- `readout` — живой бейдж на DOM-оверлее (`_labelLayer`, как label). Поля: `expr`, `label`, `unit`, `precision` (0..8, деф.2), `x/y` (мир-коорд.; без них — авто-столбик верх-право, счётчик `_readoutSlot` сбрасывается на кадр). Ошибка — мягко через `SimExpr.evalSafe` (AST компилируется 1 раз в prepare), показывает «—».
- `vector` — новая форма `origin:[ox,oy]+dx/dy` (конец = origin + (dx,dy)); старая `x1/y1/x2/y2` сохранена; стрелка из Ф0.
- **Drag** (`point`/`circle` с `drag:{param,axis,min,max,paramY}`): pointer events на canvas (мышь+тач, `touchAction:none`); хит-тест в экранных px (допуск 16px, ближайшая ручка), приоритет ручек. `axis:'xy'` требует `paramY`. Курсор → мир через `_toWorld` (инверсия `_toPx`) → `_setParamClamped` (clamp по `drag.min/max` И по диапазону параметра из `_paramRange` — не полагаться на DOM-clamp слайдера). Слушатели снимаются в `destroy`. Drag только point/circle (вершины polyline/конец вектора — не реализовано).
- Тестировать движок headless: `vm.createContext` + ручной DOM/canvas-стаб (canvas-ctx через `Proxy` с noop). `_renderFrame` рано выходит при `_cw/_ch==0` — выставить вручную. `setParam`/drag используют `new Event('input')` (браузерно безопасно, в стабе нужен `Event`).
-`lab.html`/`lab-glue.js` — зона параллельной сессии (Ф2 измерит. инструменты); Ф1 их НЕ трогала, работала только в `_sim_engine.js`/`_sim_demo.js`.
### Phase 2 — Learnings
- **Физический режим** (всё в `_sim_engine.js`, формат — в шапке файла): блок `physics:{ enabled, gravity:{x,y}, friction?, restitution?, dt?, walls?:[...], springs?:[...] }` + `body:{ mass, vx, vy, fixed }` на point/circle. gravity/friction/restitution/k/length/damping/mass/нач.позиция/vx/vy — число ИЛИ выражение от params (вычисляются на **reset**, не каждый кадр — для стабильности).
- **`window.SimPhysics`** — экспортированный интегратор (`step(state,dtFrame)`, `integrate`, `resolveCollisions`). Полу-неявный (симплектический) Эйлер `v+=a·dt; x+=v·dt` — та же математика, что `_fx_motion.spring`, обобщённая на N связанных тел. Фикс-шаг с накопителем (кламп dt 1/2000..1/30, кап подшагов 8, кламп скорости 1e4, вязкое трение `exp(-friction·dt)`) → энергия не «взрывается». Упругие столкновения круг-круг (импульс по нормали + позиционная коррекция по обратным массам) и круг-стена. Чистая функция над state, без DOM/eval — переиспользуемо headless. Отдельного файла `_sim_physics.js` НЕТ (нельзя подключить без правки lab.html — зона параллельной сессии); код внутри `_sim_engine.js`.
- **`_fx_motion` API не подходит** для спек-движка напрямую: `tween`/`springFactory` — rAF-замыкания, тянущие ОДНО значение к цели, не связанные тела с силами. Переиспользована только их интеграционная математика (формула спринга), а не сами функции.
- **env-поля тел**: `<id>.x/.y/.vx/.vy` берутся из СОСТОЯНИЯ интегратора и кладутся в `_buildEnv` ПЕРВЫМИ (до формульных центров) — это снимает forward-ref проблему однопроходного env для тел: формульный объект, ссылающийся на тело (`segment x2:'ball.x'`), видит актуальную позицию в том же кадре. point/circle с `body` рисуются из env-полей тела, а не из выражения x/y.
- **Drag тела**: тело (point/circle с `body`, не fixed) перетаскиваемо по умолчанию (без `drag`-конфига). Тащишь — `body.x/y = курсор`, тело временно `fixed` в `_stepPhysics`; отпускаешь — `body.vx/vy` из сглаженной оценки скорости курсора (кламп 40 м/с). Хит-тест тела — max(16px, экранный радиус). drag-ручки Ф1 и физ-тела сосуществуют.
- **Гочи**: (1) имя param **`e`** зарезервировано — это число Эйлера в SimExpr (parser проверяет CONSTANTS до env), выражение `e` даст 2.718, не значение param. Брать `el`/`elast`. (2) Радиус тела для коллизий: circle — мировой `r`; point — экранные px → мир через `_scale`, поэтому физика собирается в `reset()` ПОСЛЕ первого `_fit()`. (3) Слайдеры во время play меняют только env (readout/формулы), но НЕ силы/тела до reset (намеренно). На паузе при `t==0` — пересборка для предпросмотра старта.
- Headless-тест физики: виртуальные часы (`vclock`) синхронны с `performance.now()` и таймстампом rAF (иначе первый кадр получит огромный/отрицательный dt и ничего не сдвинется).
### Phase 3 — Learnings
- **Персистентность**: таблица `custom_sims` (миграция **071**), API `/api/custom-sims` (контроллер `customSimController.js`, роутер `customSims.js`, смонтировано в `server.js` после `/api/materials`), клиент `LS.customSimsList/Get/Create/Update/Delete`. Спека хранится как `spec_json` TEXT(JSON); парс — только на чтение/валидацию, на сервере НЕ исполняется. `version` ++ на каждом update со `spec`.
- **`validateSpec(spec)` — серверная защита БЕЗ исполнения** (спека шарится между людьми): размер ≤200KB, `specVersion`=1, лимиты (params≤50/objects≤200/walls≤20/springs≤50/expr≤500симв./глубина≤8/points≤1000), whitelist типов объектов (point|segment|vector|circle|rect|polyline|path|label|plot|readout), physics-границы (restitution 0..1, dt 1/2000..1/30, body.mass>0). Строки-выражения (x/y/expr/…) НЕ парсятся (это делает безопасный SimExpr на клиенте) — проверяется только длина. Текст-поля (text/label/unit/meta/param.label/drag.param/id) **обрезаются и экранируются** (`& < >` → entities). Возврат `{ ok, error?, clean? }` — в БД пишется `clean` (санитизированная).
- **Ownership-паттерн = studentMaterialsController**: read-роуты auth-only (видимость own+published решает контроллер), мутации — inline `requireRole('teacher','admin')` + per-row проверка (`owner_id === req.user.id || role==='admin'` → иначе 403; нет строки → 404). НЕ blanket `router.use(requireRole)` — иначе ученик не увидит published.
- **lint:routes (baseline 0)**: `:id`-роуты прикрыты router-level `authMiddleware` (линтер видит `router.use(<guard>)`); read `GET/PUT/DELETE /:id` дополнительно помечены `// @public-by-design:` с указанием на ownership-проверку в хендлере (как в materials.js).
- **Тесты**: setup.js строит СВОЙ Express-app и НЕ монтирует новые роуты — тест-файл должен сам `app.use('/api/custom-sims', require(...))` (как lab-links.test.js). `getToken(role)`/`inject(method,path,body,token)` — готовые хелперы. seedRow(PRAGMA table_info) — для прямого посева строк, устойчив к дрейфу схемы (здесь не понадобился: пользователи создаются через getToken, спеки — через API).
- **Окружение тестов**: 8 fail из 209 = 3 baseline (`auth.test.js` — bcrypt/JWT в тест-окружении) + 5 page-тестов (`chemistry7/8-*`, `math5/6-page`), падающих на `Cannot find module 'jsdom'` (devDep не установлен) — оба класса не связаны с бэкенд-фазой.
### Phase 4 — Learnings
- **Билдер = `frontend/sim-builder.html` + `frontend/js/sim-builder.js`** (логика модульна: html держит только разметку/стили/bootstrap). `window.SimBuilder.create({host,previewHost,panelHost,toolbarHost}) -> Builder`. Состояние `Builder.st`; `_uid` на объектах/стенах/пружинах — UI-метка, вырезается в `buildSpec()`. Доступ teacher/admin: `LS.initPage()``{isTeacher,isAdmin}` → редирект `/dashboard` (паттерн live-quiz.html).
- **Подключение движка тем же путём, что lab.html**: `<script src="/js/labs/_sim_expr.js">` + `_sim_engine.js`. Гочи маршрутизации: `/js` мапится на **корневой** `js/` (api.js/sidebar.js/mobile.js/notifications.js), а в нём НЕТ `labs/` → запрос `/js/labs/*` и `/js/sim-builder.js` проваливается на `express.static(frontendDir)` и отдаёт `frontend/js/...`. Это уже работающий механизм (lab.html), не трогать server.js.
- **Генерация спеки**: `buildSpec()` → JSON v1. `stripObj()` убирает `_uid`/пустые поля. **plot** хранит в UI `range_a/range_b` отдельно и материализуется `normalizePlotForSpec``range:[a,b]` (границы — число ИЛИ выражение). `stripObj` переопределён в конце IIFE на plot-aware версию — работает т.к. `buildSpec` вызывает её в рантайме (function-declaration binding мутабелен). Числовые поля хранятся «как введено» (число/строка) — `SimExpr.compileValue` ест оба, серверная `validateSpec` не парсит.
- **Выражения = только SimExpr** (без eval/Function): `SimExpr.compile(v).error` → inline-ошибка у поля; `FUNCTIONS`/`CONSTANTS`**обычные объекты** (ключи=имена, не Set) → палитра через `Object.keys`. `exprError()` пропускает чистые числа и пустые строки.
- **Запрет имени param**: не только `e` (число Эйлера), но и `pi/E/PI/tau/t/w/h` (служебные env-переменные движка) — иначе слайдер затрёт системное имя.
- **Drag-on-preview**: переиспользует геометрию движка — `inst.canvas` + `inst._toWorld(px,py)` (px относит. canvas getBoundingClientRect). Пишет x/y (point/circle/label/readout/rect) или x2/y2 (segment/vector). Только на паузе (`!inst.isRunning()`), чтобы не конфликтовать со встроенным drag/анимацией движка.
- **Клиентская валидация зеркалит серверную** (Ф3 лимиты) и показывает дружелюбную модалку-список ДО запроса — экономит round-trip и даёт понятные ошибки вместо сырого 400.
- **Сайдбар — аддитивно**: пункт `/sim-builder` в `js/sidebar.js` в группе `G('practice',...)` после `/lab`, паттерн `{ cls:'sb-teacher-only', hidden:!isTch }`. `isActive('/sim-builder')` подсвечивает на странице (clean URL без .html). НЕ ломает остальной сайдбар.
- **Верификация без jsdom**: headless-смоук — `vm.createContext` + ручной DOM/window/Blob-стаб (canvas getContext-заглушка, setTimeout no-op чтобы debounce не стрелял), грузим `_sim_expr.js`+`sim-builder.js`, дёргаем `buildSpec()`/`validate()`/`loadFromSim()` напрямую (рендер не нужен для логики). 23/23.