22 KiB
22 KiB
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/ localStoragelab-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_motionAPI не подходит для спек-движка напрямую: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_jsonTEXT(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). НЕ blanketrouter.use(requireRole)— иначе ученик не увидит published. - lint:routes (baseline 0):
:id-роуты прикрыты router-levelauthMiddleware(линтер видитrouter.use(<guard>)); readGET/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 хранит в UIrange_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.
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) vsdata-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, извлекает IIFELabCustomиз lab-glue по маркеру и прогоняет init→open→del. Гочи стаба: реальный код проверяетwindow.LS(api.js ставит иwindow.LS, и глобалLS) — в стабе надо ставить ОБА;document.getElementByIdстаба должен находить и динамическиappendChild-нутые элементы (регистрировать по id в appendChild). 22/22.