Files
Learn_System/CLAUDE.md
T

22 KiB
Raw Blame History

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.)

# Найти класс/функцию/символ
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:

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 отдельно и материализуется normalizePlotForSpecrange:[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.

Phase 5 — Learnings

  • id-неймспейс custom: гочи LabRegistry. LabRegistry.get/has обрезают часть после : (_baseId), т.к. встроенные используют base:arg (emfield:E, stereo:cube). Поэтому регистрировать custom:42 НЕЛЬЗЯ — has('custom:42') искал бы _byId['custom']. Решение: в реестре id без двоеточия customsim_<dbid>, а наружу (deep-link/клик/data-open) — custom:<dbid>. Конвертация одной функцией LabCustom.resolveId через хук в начале openSim (lab-init.js, +7 строк).
  • Ленивый манифест-заглушка вместо ранней загрузки spec. На старте /lab грузим только мету (customSimsList, без spec) и регистрируем заглушку с асинхронным open(). При первом открытии: ensureSpec(dbid) (customSimGet, кэш+дедуп) → registerSpecSim(spec) (Ф0-адаптер) заменяет заглушку на месте (LabRegistry.register сохраняет позицию по тому же id) → setActive(real) + real.open(ctx). Дисп. в openSim уже умеет Promise-возврат open() (Ф3). Повторное открытие — синхронно (реальный манифест в реестре). Движок _sim_* уже eager (Ф0) → ленивый файл не нужен, _sim_deps.js не трогаем.
  • Аддитивность в чужих файлах: вся логика — в новом IIFE window.LabCustom в КОНЦЕ lab-glue.js; в существующий код добавлены только хуки: renderSims() merge +&& !m._custom (1 терм) + вызов renderSection; init зовёт init(). Секция «Мои симуляции» (#custom-sim-section) создаётся динамически в #lab-home — без правок lab.html/CSS (тот же приём, что _loadRelated в Ф-каталоге). Карточки переиспользуют .sim-card/.sim-cat/.sim-preview; бейджи/кнопки — inline-стиль + SVG .ic (без эмодзи).
  • Owner-only действия: owner_id === user.id (user из LS.initPage(), поле id — канон всего фронта, ср. t.createdBy === user.id в theory.html). Edit → location.href='/sim-builder?id='+dbid; Delete → LS.customSimDelete + убрать карточку. Делегированный клик по контейнеру секции: data-act (edit/del, stopPropagation) vs data-open (открыть). Видимость draft/published обеспечивает сервер Ф3 (список = свои+чужие published).
  • Embed/Ф7 заметка: для ?sim=custom:* открытие отложено до LabCustom.init() (и в обычном, и в embed-режиме). _loadRelated('customsim_<id>') дергает /api/lab/sims/.../related (404, тихо). LabRegistry не имеет unregister → удалённая custom остаётся заглушкой в реестре (карточки нет, ensureSpec вернёт 404). Источник spec для доски (Ф7): LabCustom.ensureSpec(dbid).
  • Смоук на РЕАЛЬНОМ registry/adapter: harness грузит настоящие _registry.js+_sim_adapter.js в vm-контекст, стабит только SimEngine/LS/DOM, извлекает IIFE LabCustom из lab-glue по маркеру и прогоняет init→open→del. Гочи стаба: реальный код проверяет window.LS (api.js ставит и window.LS, и глобал LS) — в стабе надо ставить ОБА; document.getElementById стаба должен находить и динамически appendChild-нутые элементы (регистрировать по id в appendChild). 22/22.