24 KiB
24 KiB
Feature Context: Конструктор симуляций (SimForge)
Current State
- Фаза 6 РЕАЛИЗОВАНА (в рабочем дереве, не закоммичено — коммит за оркестратором). Файлы:
backend/src/controllers/customSimController.js(+share/clone/related/addLink/removeLink, импортpushNotif),backend/src/routes/customSims.js(+POST/:id/share, POST/:id/clone, GET/:id/related, POST/:id/links, DELETE/:id/links/:linkId),js/api.js(+customSimShare/ Clone/Related/AddLink/DelLink),frontend/js/labs/lab-glue.js(аддитивно в IIFE LabCustom: кнопки share/clone/publish-toggle на карточках + делегат +shareToClass/clone/setStatus, ICON-блок),frontend/js/sim-builder.js(тулбар: «Шаблон»/«Раздать»/publish-toggle; методыsetStatus/ openShareModal/openTemplateModal; данныеTEMPLATES×4; ICON.template/unpublish),backend/tests/custom-sims-share.test.js(new, 15 it, все зелёные).- РЕШЕНИЕ копия-vs-доступ (зафиксировано): published custom-sim видна ВСЕМ в каталоге /lab
(
list/getотдают published любому; custom-sim НЕ гейтится allowlist'ом content_access 'sim' — тот гейтит только legacylab_sims). Поэтому «раздать классу» = (1) авто-публикация (status→published), (2) ДОЛГОВЕЧНОЕ адресное уведомление ученикам класса черезpushNotif(notifications-таблица + SSE) со ссылкой/lab?sim=custom:<id>. БЕЗ копии (в отличие от «Моих материалов», где оригинал приватный и копия обязательна) и БЕЗ записи content_access. - Привязка к программе: переиспользован
lab_sim_linksсsim_id='custom:<id>'(sim_id TEXT — отдельная таблица не нужна). Связями СВОЕЙ симуляции управляет владелец/admin (не только admin как у lab_sims). Backend + GET/relatedготовы; UI-редактор связей + чипы в каталоге — остаток (handoff). - Клон: копия spec вызвавшему как draft (title += ' (копия)', version=1). Источник: своя любая ИЛИ чужая published (чужой draft → 403).
- Верификация:
node --checkвсех 6 изм. файлов OK; эмодзи нет (скан — только→/∑в комментариях, как в существующем коде); eval/Function нет;npm run lint:routes0 unprotected (baseline 0);npm test216/224 pass (8 fail = тот же baseline: 3 auth.test + 5 page-тестов без jsdom — не моя фаза; обе custom-sims-сьюты зелёные). git status: только мои файлы; classroom.html/lab.html не тронуты.
- РЕШЕНИЕ копия-vs-доступ (зафиксировано): published custom-sim видна ВСЕМ в каталоге /lab
(
- Фаза 5 РЕАЛИЗОВАНА (в рабочем дереве, не закоммичено — коммит за оркестратором). Только
аддитивные правки двух файлов параллельной сессии (без рефактора их кода): рабочее дерево
по ним было ЧИСТЫМ до начала. classroom.html / backend /
_sim_deps.jsНЕ тронуты.frontend/js/labs/lab-init.js(+7 строк): в началоopenSim(id)добавлен хукif (window.LabCustom && LabCustom.resolveId) id = LabCustom.resolveId(id) || id;— переводит deep-link/кликcustom:<dbid>в реестровый idcustomsim_<dbid>(LabRegistry.get/has обрезают часть после:, поэтому в реестре двоеточие недопустимо). Для встроенных id — no-op.frontend/js/labs/lab-glue.js: (а)renderSims()merge +&& !m._custom(custom не в основной сетке) и вызовLabCustom.renderSection(_catFilter); (б) init-блок (non-embed и embed) зовётLabCustom.init(), отложенное открытие?sim=custom:*до загрузки списка; (в) новый IIFEwindow.LabCustomв конце файла.- Поток:
LS.customSimsList()(мета без spec) →_registerLazyкладёт в LabRegistry манифест-заглушкуcustomsim_<dbid>(_custom:true) с ленивымopen(). Секция «Мои симуляции»#custom-sim-section(создаётся динамически в#lab-home, без правок lab.html/CSS) рендерит карточки из_meta. Открытие:resolveId→ дисп. реестра →open()заглушки →ensureSpec(dbid)(LS.customSimGet, кэш+дедуп) →spec.id=regId→registerSpecSim(spec)(Ф0-адаптер, заменяет заглушку на месте) →setActive(real)+real.open(ctx)(монтирует SimEngine). spec лениво — на старте /lab не грузится. Движок (_sim_*) уже eager (Ф0), ленивый файл не нужен. - Карточка: preview-SVG + cat-бейдж + бейджи «Моя»(owner)/«Опубликована»(status)/«Черновик»
- кнопки «Редактировать»→
/sim-builder?id=<dbid>/ «Удалить»→LS.customSimDelete(владельцу,owner_id===user.id). Делегированный клик по#custom-sim-section. Иконки — inline SVG.ic.
- кнопки «Редактировать»→
- Верификация:
node --checkобоих изменённых файлов OK; эмодзи нет (скан кодпойнтов — только math/box-drawing глифы ∑/═/─/→, как в существующем коде); eval/Function нет; headless-смоук (vm + DOM/SimEngine/LS-стабы, РЕАЛЬНЫЕ_registry.js+_sim_adapter.js) 22/22: resolveId, регистрация ленивых манифестов+флаг_custom, секция/карточки, бейджи, owner-only edit/del, deep-linkdata-open, lazy spec→registerSpecSim→mount, reopen синхронно, delete, встроенные не сломаны. git status: изменены только lab-init.js/lab-glue.js (+ плановые .md); classroom.html/backend чисты.
- Фаза 4 РЕАЛИЗОВАНА (в рабочем дереве, не закоммичено — коммит за оркестратором). Только
новые файлы
frontend/sim-builder.html+frontend/js/sim-builder.js+ аддитивная правкаjs/sidebar.js(lab.html/lab-glue.js НЕ тронуты — зона параллельной сессии).- Учительский редактор
/sim-builder(гейт teacher/admin черезLS.initPage()): панели- аккордеоны (Мета+сцена / Параметры / Объекты / Графики / Физика) слева + живое превью (SimEngine.mount, перемонтаж с debounce 280мс) справа + тулбар (Тест/Сброс/Сохранить/ Опубликовать).window.SimBuilder.create({host,previewHost,panelHost,toolbarHost}). - Генерация спеки
buildSpec()→ JSON v1 (specVersion:1, meta, viewport, time, params[], objects[]+merged plots, physics?)._uid— UI-метка, вырезается; plot материализуется (range_a/range_b → range[a,b]); числовые поля — число ИЛИ строка-выражение (движок ест оба). - Выражения: каждое поле проверяется
SimExpr.compile→ inline-ошибка у поля; палитра функций/констант/параметров/id.xчерез модалку. Запрет имени parame(и pi/t/w/h/...). - Drag-on-preview: кнопка-«прицел» у объекта → клик/перетаскивание по
inst.canvas(px→мир черезinst._toWorld()) пишет x/y (или конец segment/vector) в свойства. Только на паузе. - Save/Load:
customSimCreate/customSimUpdate(?id= → update + replaceState), публикацияstatus:'published';?id=<id>→customSimGet→loadFromSimраскладывает по панелям. - Клиентская валидация зеркалит серверную (params≤50/objects≤200/walls≤20/springs≤50/ expr≤500/restitution 0..1/JSON≤200КБ) с дружелюбной модалкой-списком ошибок ДО запроса.
- Сайдбар: пункт
/sim-builder«Конструктор симуляций» (teacher-only, icon pencil-ruler) в группе «Практика и игры» после «Лаборатория» — минимальная правкаjs/sidebar.js. - Верификация:
node --checkобоих новых .js + извлечённого инлайна html OK; эмодзи нет (скан кодпойнтов, включая no-entry sign — заменён на текст); eval/Function нет (вычисления — SimExpr); headless-смоук (vm + DOM/Blob-стаб) 23/23: buildSpec форма, merge plot+range, strip _uid, physics-блок, валидация valid/reserved-e/syntax-error, loadFromSim round-trip стабилен. lab.html/lab-glue.js/_sim_engine.js/_sim_expr.js НЕ тронуты (git status).
- Учительский редактор
- Фаза 3 РЕАЛИЗОВАНА (в рабочем дереве, не закоммичено — коммит за оркестратором). Только backend + клиент
js/api.js(lab.html/lab-glue.js НЕ тронуты — зона параллельной сессии).- Миграция 071
backend/src/db/migrations/071_custom_sims.sql— таблицаcustom_sims(применена к живой БД черезnpm run migrate, без ошибок). - API
/api/custom-sims(роутерbackend/src/routes/customSims.js, контроллерbackend/src/controllers/customSimController.js, смонтировано вserver.js): GET/(свои+published), GET/:id(own ИЛИ published), POST/(teacher/admin), PUT/:id(owner/admin), DELETE/:id(owner/admin). Read — router-level authMiddleware; мутации — inline requireRole + per-row ownership. validateSpec(spec)в контроллере — серверная валидация БЕЗ исполнения: ≤200KB, specVersion=1, лимиты (params≤50/objects≤200/walls≤20/springs≤50/expr≤500/глубина≤8/points≤1000), whitelist типов объектов, physics (restitution 0..1, dt 1/2000..1/30, mass>0), санитизация текст-полей (escape &<>). Возврат{ ok, error?, clean? }.- Клиент
js/api.js:customSimsList/Get/Create/Update/Delete→req(...), добавлены вwindow.LS. - Верификация:
node --checkвсех новых/изменённых .js OK;npm run migrateOK;npm run lint:routesчисто (0 unprotected, baseline 0);backend/tests/custom-sims.test.js24/24 pass; общий suite 201/209 (8 fail = 3 baseline auth.test.js + 5 page-тестов без devDepjsdom— окружение, не моя фаза). Эмодзи нет; БД через node:sqlite.
- Миграция 071
- Фаза 2 РЕАЛИЗОВАНА (в рабочем дереве, не закоммичено — коммит за оркестратором). Только
_sim_engine.js+_sim_demo.js(lab.html/lab-glue.js НЕ тронуты — зона параллельной сессии).- Физический режим: блок
physics:{ enabled, gravity:{x,y}, friction?, restitution?, dt?, walls?:[...], springs?:[...] }+body:{ mass, vx, vy, fixed }на point/circle. Фикс-шаговый полу-неявный Эйлер (накопитель dt, кламп шага/скорости), опора на математику_fx_motion.spring. Упругие столкновения круг-круг и круг-стена (restitution), пружины (Гук+демпф) между телами/якорями. Drag тел (тащишь — позиция, отпускаешь — бросок со скоростью). Тела сосуществуют с формульными объектами Ф0/Ф1. - env-поля тел:
<id>.x/.y/.vx/.vyберутся из СОСТОЯНИЯ интегратора и кладутся в env первыми — снимает forward-ref проблему однопроходного env для тел. - Интегратор экспортирован как
window.SimPhysics(для билдера/доски/headless). Отдельного файла_sim_physics.jsНЕТ (нельзя подключить без правки lab.html — зона параллельной сессии); код внутри_sim_engine.js. - Демо за флагом: +
customphys(пружинный маятник), +customballs(упругие шары). Гочи: имя parameзарезервировано (число Эйлера) — в демо «шары» упругость названаel. - Верификация:
node --checkобоих файлов OK; eval/Function — только в комментарии; эмодзи нет (скан кодпойнтов); headless (vm+DOM/canvas-стаб) 28/28: падение под гравитацией (парабола, без NaN), упругие шары (скорости меняются, тела в коробке, ограничены), пружинный маятник (колебания, без взрыва), drag тела (позиция+бросок), смешанная сцена (формульный point + segment на ball.x/y + readout ball.y живут вместе),SimPhysics.stepraw.
- Физический режим: блок
- Фаза 1 РЕАЛИЗОВАНА (в рабочем дереве, не закоммичено — коммит за оркестратором). Только
_sim_engine.js+_sim_demo.js(lab.html/lab-glue.js НЕ тронуты — зона параллельной сессии).- Новые типы объектов спеки: plot (график
f(var)на canvas движка,trace— след поt), readout (живой бейдж, мягкая ошибка черезevalSafe), vector с формойorigin+dx/dy. drag на point/circle (drag:{param,axis,min,max,paramY}) — pointer events (мышь+тач), хит-тест в px (16px), двойной clamp (drag.min/max + диапазон параметра). Точные поля — в шапке_sim_engine.jsи handoff phase-1. - Демо
customdemoрасширено: +слайдеры x0/y0, draggable-старт (axis xy), plot траектории, 2 readout (R, H). По-прежнему за флагом. - Верификация:
node --checkобоих файлов OK; eval/Function — только в комментарии, ни одного call-site; эмодзи нет (скан кодпойнтов); headless-тесты (vm + DOM-стаб): подготовка типов, vector end=origin+(dx,dy), plot evaluate, readout evalSafe, drag clamp+slider-sync, рендер всех 8 типов демо ×6 кадров без ошибок, trail/readout-слоты накапливаются корректно.
- Новые типы объектов спеки: plot (график
- Фаза 0 РЕАЛИЗОВАНА (в рабочем дереве, не закоммичено — коммит за оркестратором). Ветка
feature/sim-builderотmaster.frontend/js/labs/_sim_expr.js→window.SimExpr(безопасный движок выражений, без eval/Function;compile/evaluate/evalSafe/compileValue/parse/tokenize, whitelist + сравнения/логика/тернарник/multi-var env).frontend/js/labs/_sim_engine.js→window.SimEngine.mount(host, spec) -> { play, pause, reset, setParam, getParam, isRunning, destroy, el }. Canvas (мир→экран, Y вверх) + KaTeX-оверлей подписей + слайдеры/play/pause/reset. Формат спеки v1 задокументирован в шапке файла.frontend/js/labs/_sim_adapter.js→window.registerSpecSim(spec)/window.SimAdapter— строит манифест LabRegistry (ленивый хост#sim-spec-host-<id>в#lab-sim).frontend/js/labs/_sim_demo.js— демоcustomdemo(бросок тела) за флагом?simdemo=1/?sim=customdemo/LAB_SHOW_SPEC_DEMO/ localStoragelab-spec-demo=1. Ученикам не светится.- Подключение в
frontend/lab.html: 3 каркасных модуля eager после_graph_panel.js, демо после_register-all.js._sim_deps.jsНЕ тронут. - Верификация:
node --checkвсе 4 файла OK; eval/Function отсутствуют (только в комментариях); эмодзи нет; SimExpr self-test 29/30 (единственный «FAIL»-2^2=4— это парити с graph.js).
- Лаборатория уже декларативна на уровне регистрации:
frontend/js/labs/_registry.js(LabRegistry.register/get/all/setActive/stop/destroy/resolvePreview), манифест сopen(ctx)/mount(host)/stop/destroy. ~40 симуляций — рукописные JS-модули вfrontend/js/labs/. - Каталог в БД: миграция
042_lab_sims.sql(lab_sims), роутыbackend/src/routes/lab.js(GET /api/lab/sims, PATCH/:id, POST /reorder, links). Привязка к программе:043_lab_sim_links.sql.
Архитектурные решения (зафиксированы при планировании)
- Спека = JSON-данные. Версия
specVersion. Корень:{ specVersion, meta, viewport, params[], objects[], physics?, plots[], controls }. - Движок выражений безопасный — собственный парсер (расширение
y=f(x)из graph.js): токенайзер → AST → eval по окружению{ params, t, объекты, whitelisted Math fns }. ⛔ Безeval/Function. Whitelist: + - * / ^ %, sin cos tan asin acos atan sqrt abs exp ln log min max floor ceil round sign pi e, сравнения, ?:. - Рантайм
window.SimEngine.mount(host, spec) -> instance{ play, pause, reset, setParam, destroy }. Рендер: canvas для геометрии/трасс + SVG/absolute-div оверлей для подписей (KaTeX). Регистрируется в LabRegistry адаптером (одна функция строит манифест из спеки). - Объект:
{ id, type, ...props-with-bindings }. type ∈ point|segment|vector|circle|rect|polyline|path|label|image. Любое числовое свойство может быть числом ИЛИ строкой-выражением. - Физический режим (Фаза 2): объект с
body:{ mass, vx, vy, fixed }интегрируется_fx_motion; силыphysics:{ gravity, springs[], collisions, friction, walls }. Формульный и физический режимы сосуществуют (формульные объекты — кинематические). - Безопасность шаринга: published-спека валидируется на сервере (размер, схема, глубина AST, число объектов/параметров); подписи-строки санитизируются как svg/текст.
Temporary Workarounds
- (нет)
Cross-Phase Dependencies
- Ф1 (графики/drag) зависит от рантайма Ф0.
- Ф2 (физика) зависит от Ф0 (модель объектов/цикл).
- Ф4 (билдер) зависит от Ф0–Ф2 (что строить) + Ф3 (куда сохранять).
- Ф5 (каталог) зависит от Ф3 (БД) + Ф0 (адаптер LabRegistry).
- Ф6 (раздача) зависит от Ф3+Ф5.
- Ф7 (доска) зависит от Ф0 (рантайм) + Ф5 (источник sim) + существующего
simOpen/simState.
Implementation Notes
- Каждая фаза должна оставлять /lab рабочим (Incremental).
- Тестировать рантайм Ф0–Ф2 рукописными спеками-фикстурами (без билдера).
- Reuse > переписывание: сначала смотреть
_fx_motion,_graph_panel,graph.js.
RESUME STATE
- Последний коммит фичи: — (Ф0..Ф6 реализованы, ещё не закоммичены — ждут оркестратора)
- Текущая фаза: Phase 6 — Раздача / шаблоны / клон / программа (✅ Implemented, pending commit) → дальше Phase 7 — Доска онлайн-урока (последняя)
- Эндпоинты Ф6: share/clone/related/links на
/api/custom-sims/:id/*; клиентLS.customSimShare/ Clone/Related/AddLink/DelLink. Раздача = авто-publish + pushNotif (НЕ копия). Связи — lab_sim_linkssim_id='custom:<id>'. Остаток Ф6: UI-редактор связей в билдере + чипы в каталоге (backend готов). - Файлы Ф5 (аддитивные правки зоны параллельной сессии — БЕЗ рефактора):
frontend/js/labs/lab-init.js(+7 строк: хукLabCustom.resolveIdвopenSim),frontend/js/labs/lab-glue.js(renderSims +!m._customи вызов renderSection; init зовётLabCustom.init(); новый IIFEwindow.LabCustom)._sim_deps.js, classroom.html, backend — НЕ тронуты. Публичное API:window.LabCustom.{init,resolveId,renderSection,ensureSpec,del}. - id-неймспейс custom: deep-link/клик/
data-open=custom:<dbid>; LabRegistry/host =customsim_<dbid>. - Режим: Automated / Orchestrator / Incremental
- Файлы Ф4 (несведённые с параллельной сессией):
frontend/sim-builder.html(new),frontend/js/sim-builder.js(new),js/sidebar.js(modify, аддитивный пункт/sim-builder). lab.html/lab-glue.js НЕ тронуты. Публичное API билдера:window.SimBuilder.create(...). - Номер миграции Ф3: 071 (
071_custom_sims.sql); следующая свободная — 072. - Новые публичные API для следующих фаз:
window.SimExpr,window.SimEngine.mount,window.SimPhysics(step/integrate/resolveCollisions),window.registerSpecSim/window.SimAdapter. Формат спеки v1 + типы plot/readout/drag/vector + блокphysics/body/springs/walls— в шапке_sim_engine.jsи в handoff phase-0/1/2. - API персистентности (Ф3):
/api/custom-sims(GET/, GET/PUT/DELETE/:id, POST/) + клиентLS.customSimsList/Get/Create/Update/Delete. Контракт спеки на вход/санитизация — в handoff phase-3. - Файлы Ф2 (несведённые с параллельной сессией):
frontend/js/labs/_sim_engine.js,frontend/js/labs/_sim_demo.js. - Файлы Ф3:
backend/src/db/migrations/071_custom_sims.sql,backend/src/controllers/customSimController.js,backend/src/routes/customSims.js,backend/tests/custom-sims.test.js(new);backend/src/server.js,js/api.js(точечные добавления). lab.html/lab-glue.js НЕ тронуты. - Для Ф4 (билдер): слать/получать спеку через
LS.customSimCreate/Update/Get; сервер вернёт спеку санитизированной (escaped-текст). Лимиты/коды 400 — см. handoff phase-3.