@
chore(quantik-game): полировка по финальному ревью + security-review
Финальное ревью: READY TO MERGE (0 блокеров). Security: SECURE (0 critical).
Применены дешёвые фиксы из ревью:
- validateSpec: блок game{} санитизируется ПОИМЁННО (chapter/subject →
sanitizeText, order/par_ms/unlockStars → проверка типа, неизвестные ключи
отбрасываются) — закрыт латентный хранимый XSS (раньше clean.game=spec.game).
- quantik.html: @media (prefers-reduced-motion) делает анимации мгновенными
(не выключает — иначе forwards-появление узлов оставило бы их скрытыми).
- progress-logic.js: фикс комментария isUnlocked (сумма звёзд по ВСЕМ уровням
с меньшим глобальным order, а не «той же главы»).
План: Ф6 (лидерборд/гонка) удалена (Amendment 1, решение пользователя);
финальные гейты отмечены; deferred-бэклог зафиксирован.
Затронутые тесты 45/45; lint:routes 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
This commit is contained in:
@@ -275,10 +275,18 @@ function validateSpec(spec) {
|
||||
clean.goal = cg;
|
||||
}
|
||||
|
||||
// game{} — зарезервированный блок мета-слоя (Фаза 1/5). Пропускаем как есть
|
||||
// (проверен общими лимитами: размер/глубина). Не исполняем.
|
||||
// game{} — мета-слой игрового уровня (Фаза 1/5). Санитизируем ПОИМЁННО (как goal):
|
||||
// строки → sanitizeText (escape), числа → проверка типа, неизвестные ключи отбрасываем.
|
||||
// Иначе произвольная строка в game.* стала бы хранимым XSS у любого, кому раздали уровень.
|
||||
if (spec.game && typeof spec.game === 'object' && !Array.isArray(spec.game)) {
|
||||
clean.game = spec.game;
|
||||
const gm = spec.game;
|
||||
const cgm = {};
|
||||
if (gm.chapter !== undefined) cgm.chapter = sanitizeText(gm.chapter, 60);
|
||||
if (gm.subject !== undefined) cgm.subject = sanitizeText(gm.subject, 60);
|
||||
if (typeof gm.order === 'number') cgm.order = gm.order;
|
||||
if (typeof gm.par_ms === 'number') cgm.par_ms = gm.par_ms;
|
||||
if (typeof gm.unlockStars === 'number') cgm.unlockStars = gm.unlockStars;
|
||||
clean.game = cgm;
|
||||
}
|
||||
|
||||
if (errs.length) return { ok: false, error: errs.slice(0, 8).join('; ') };
|
||||
|
||||
@@ -46,11 +46,10 @@
|
||||
}
|
||||
|
||||
/* ── Разблокировка ────────────────────────────────────────────────────────
|
||||
Уровень открыт, если СУММА звёзд во ВСЕХ предыдущих уровнях той же главы
|
||||
(по полю order) ≥ level.unlockStars. Первый уровень главы (минимальный order
|
||||
или unlockStars==0) открыт всегда. Глава открывается, если открыт её первый
|
||||
уровень — он гейтится суммой звёзд предыдущих глав через unlockStars==0
|
||||
первого уровня (по умолчанию) ИЛИ явным порогом.
|
||||
Уровень открыт, если СУММА звёзд во ВСЕХ уровнях с меньшим ГЛОБАЛЬНЫМ order
|
||||
(по всем главам, не только текущей) ≥ level.unlockStars. Уровень с
|
||||
unlockStars==0 (или без него) открыт всегда. Так первый уровень главы
|
||||
гейтится суммой звёзд всех предыдущих глав через свой порог unlockStars.
|
||||
|
||||
Чистая функция: вход — уровень + карта прогресса + ВЕСЬ список (для подсчёта
|
||||
«предыдущих» по order). Возвращает bool. */
|
||||
|
||||
@@ -220,6 +220,18 @@
|
||||
.qg-star-svg { filter: drop-shadow(0 2px 6px rgba(251,191,36,0.4)); }
|
||||
.qg-star-on { animation: qgStarPop .45s cubic-bezier(.22,1.3,.4,1) backwards; animation-delay: calc(.12s * var(--si, 0) + .15s); }
|
||||
@keyframes qgStarPop { 0% { transform: scale(0) rotate(-30deg); opacity: 0; } 70% { transform: scale(1.25) rotate(6deg); } 100% { transform: scale(1) rotate(0); opacity: 1; } }
|
||||
/* Доступность: уважаем prefers-reduced-motion. Делаем анимации мгновенными
|
||||
(а не выключаем) — иначе forwards-анимация появления узлов (.qm-in/qmNodeIn)
|
||||
не применит конечное состояние и узлы останутся скрытыми. Циклы (пульс/мерцание/
|
||||
покачивание) при этом фактически останавливаются (1 итерация мгновенно). */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: .01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: .01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
.qg-stats { display: flex; justify-content: center; gap: 22px; margin-bottom: 20px; }
|
||||
.qg-stat { display: flex; flex-direction: column; gap: 3px; }
|
||||
.qg-stat-lbl { font-size: .68rem; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; color: #8B9AAE; }
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
- [x] Phase 3: Граф-уровни (движение по f(x)) + зоны-препятствия [domain: fullstack] → [subplan](./phase-3-graph-levels.md)
|
||||
- [x] Phase 4: Квантовые способности + SR-комнаты [domain: fullstack] → [subplan](./phase-4-quantum-abilities-sr.md)
|
||||
- [x] Phase 5: Авторинг уровней в sim-builder + раздача классу [domain: fullstack] → [subplan](./phase-5-authoring-sharing.md)
|
||||
- [ ] Phase 6: Класс-лидерборд / живая гонка (classroom SSE) [domain: fullstack] → [subplan](./phase-6-leaderboard-live.md)
|
||||
- ~~Phase 6: Класс-лидерборд / живая гонка (classroom SSE)~~ — **REMOVED** (см. Amendment 1) → [subplan](./phase-6-leaderboard-live.md)
|
||||
|
||||
## Phase Progress Log
|
||||
|
||||
@@ -76,16 +76,32 @@
|
||||
| Phase 3: Граф-уровни + зоны | fullstack | ✅ Done | ✅ | ✅ | ✅ |
|
||||
| Phase 4: Квантовые способности + SR | fullstack | ✅ Done | ✅ | ✅ | ✅ |
|
||||
| Phase 5: Авторинг + раздача | fullstack | ✅ Done | ✅ | ✅ | ✅ |
|
||||
| Phase 6: Лидерборд / живая гонка | fullstack | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
|
||||
| ~~Phase 6: Лидерборд / живая гонка~~ | fullstack | ❌ Removed (Amendment 1) | — | — | — |
|
||||
|
||||
## MVP boundary
|
||||
После **Phase 2** игра играбельна и отгружаема: один полный мир физ-уровней с картой,
|
||||
прогрессом, XP и скинами. Фазы 3–6 — расширение (новые типы уровней, способности,
|
||||
авторинг, мультиплеер).
|
||||
|
||||
## Amendment Log
|
||||
|
||||
### Amendment 1 — 2026-06-14
|
||||
**Type:** Removed phase
|
||||
**What changed:** Phase 6 (Класс-лидерборд / живая гонка через classroom SSE) убрана из объёма по решению пользователя.
|
||||
**Why:** Пользователь решил не реализовывать соревновательный слой; переходим к полировке и финальному ревью после Ф5.
|
||||
**Impact on existing phases:** Нет. Фазы 0–5 самодостаточны и отгружаемы. `game_progress.level_id` (TEXT) уже готов под будущий лидерборд, если фичу вернут. Subplan `phase-6-leaderboard-live.md` сохранён как архив с пометкой REMOVED.
|
||||
|
||||
## Final Review
|
||||
- [ ] Comprehensive code review (final-reviewer)
|
||||
- [ ] Security review (новые API: прогресс/лидерборд, user-input)
|
||||
- [ ] `npm test` без новых регрессий (поверх baseline)
|
||||
- [ ] `npm run lint:routes` baseline 0
|
||||
- [ ] Merged to `feature/sim-builder`
|
||||
- [x] Comprehensive code review (final-reviewer) — ✅ READY TO MERGE, 0 блокеров (2026-06-14)
|
||||
- [x] Security review (новые API/ввод) — ✅ SECURE, 0 critical (2026-06-14)
|
||||
- [x] Polish-фиксы по ревью применены: game-блок санитизируется (был латентный XSS), prefers-reduced-motion guard, фикс комментария isUnlocked. Тесты 45/45 затронутых, lint 0.
|
||||
- [x] `npm test` без новых регрессий (8 = baseline: 3 auth + 5 jsdom)
|
||||
- [x] `npm run lint:routes` baseline 0
|
||||
- [ ] Merged to `feature/sim-builder` (ожидает одобрения пользователя)
|
||||
|
||||
### Deferred / Backlog (не блокеры — из финального ревью)
|
||||
- `QuantikLevels.ensureCustom` — N+1 `customSimGet` на загрузку /quantik; при росте числа авторённых уровней заменить на bulk-эндпоинт «список game-спек».
|
||||
- Уровни 3/5/6 (отскок/орбита/манёвр): `fail` — только таймаут, «честная» механика не форсится. Точечно ужесточить `fail`-предикаты (контент-тюнинг).
|
||||
- `graph-exp-11` капстоун узковат (~36–42/625) — при жалобах на сложность чуть расширить gate.
|
||||
- Три похожих `starSvg`/`_starIcon` в разных модулях — консолидация не стоит связывания движка с игрой (оставлено).
|
||||
- Клиентские очки прогресса фальсифицируемы (ожидаемо для single-player). ⚠️ Если когда-нибудь вернут Ф6-лидерборд — валидировать на сервере (replay/подписанные токены).
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# Phase 6: Класс-лидерборд / живая гонка (classroom SSE)
|
||||
|
||||
**Status:** ⬜ Not Started
|
||||
> **REMOVED (Amendment 1, 2026-06-14)** — фаза не реализуется по решению пользователя.
|
||||
> Архивный subplan. `game_progress.level_id` (TEXT) уже готов под лидерборд, если фичу вернут.
|
||||
|
||||
**Status:** ❌ Removed
|
||||
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||
**Domain:** fullstack
|
||||
|
||||
|
||||
Reference in New Issue
Block a user