feat(lab-content-engine): phase 5 - курикулумная привязка симуляций

- Миграция 043_lab_sim_links.sql: таблица связей (sim_id, kind[textbook|topic|
  kmap|question], ref_id, label), UNIQUE(sim_id,kind,ref_id) + индексы. Применена.
- lab.js (расширение):
  - GET /api/lab/sims/:id/related (auth inline) — связи по типам; label из
    textbooks/topics; href для навигации
  - GET /api/lab/links?kind=&ref_id= (auth) — обратный поиск включённых
    привязанных симуляций (для кнопки «Открыть в лаборатории»)
  - POST /api/lab/sims/:id/links (admin), DELETE .../links/:linkId (admin)
  - graceful-degradation если таблица ещё не отмигрирована
- tests/lab-links.test.js: 18 тестов (auth/роли/related/reverse/валидация/дубль/
  enabled-фильтр/удаление); seedRow() устойчив к NOT NULL дрейфу схемы
- plans: Фаза 5 done + handoff

Все мои тесты: lab-sims 11/11, lab-links 18/18. route-auth: новый :id-роут
защищён inline authMiddleware. Миграция применена к живой БД.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-30 16:27:05 +03:00
parent 72bd3ff72c
commit dead984d8a
3 changed files with 331 additions and 5 deletions
@@ -0,0 +1,24 @@
-- 043_lab_sim_links.sql — Контент-движок лаборатории, Фаза 5 (курикулумная привязка).
-- Связи симуляции с учебной программой: § учебника, тема, узел knowledge-map,
-- задача банка вопросов. Двусторонняя навигация (sim ↔ контент).
--
-- kind:
-- 'textbook' — ref_id = textbooks.slug
-- 'topic' — ref_id = topics.id (как текст)
-- 'kmap' — ref_id = id узла графа знаний (свободная строка)
-- 'question' — ref_id = questions.id (как текст)
-- label — необязательная человекочитаемая подпись (если не резолвится из БД).
CREATE TABLE IF NOT EXISTS lab_sim_links (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sim_id TEXT NOT NULL,
kind TEXT NOT NULL, -- textbook | topic | kmap | question
ref_id TEXT NOT NULL,
label TEXT,
created_by INTEGER REFERENCES users(id),
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE (sim_id, kind, ref_id)
);
CREATE INDEX IF NOT EXISTS idx_lab_sim_links_sim ON lab_sim_links (sim_id);
CREATE INDEX IF NOT EXISTS idx_lab_sim_links_ref ON lab_sim_links (kind, ref_id);