@
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;
|
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)) {
|
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('; ') };
|
if (errs.length) return { ok: false, error: errs.slice(0, 8).join('; ') };
|
||||||
|
|||||||
@@ -46,11 +46,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── Разблокировка ────────────────────────────────────────────────────────
|
/* ── Разблокировка ────────────────────────────────────────────────────────
|
||||||
Уровень открыт, если СУММА звёзд во ВСЕХ предыдущих уровнях той же главы
|
Уровень открыт, если СУММА звёзд во ВСЕХ уровнях с меньшим ГЛОБАЛЬНЫМ order
|
||||||
(по полю order) ≥ level.unlockStars. Первый уровень главы (минимальный order
|
(по всем главам, не только текущей) ≥ level.unlockStars. Уровень с
|
||||||
или unlockStars==0) открыт всегда. Глава открывается, если открыт её первый
|
unlockStars==0 (или без него) открыт всегда. Так первый уровень главы
|
||||||
уровень — он гейтится суммой звёзд предыдущих глав через unlockStars==0
|
гейтится суммой звёзд всех предыдущих глав через свой порог unlockStars.
|
||||||
первого уровня (по умолчанию) ИЛИ явным порогом.
|
|
||||||
|
|
||||||
Чистая функция: вход — уровень + карта прогресса + ВЕСЬ список (для подсчёта
|
Чистая функция: вход — уровень + карта прогресса + ВЕСЬ список (для подсчёта
|
||||||
«предыдущих» по order). Возвращает bool. */
|
«предыдущих» по order). Возвращает bool. */
|
||||||
|
|||||||
@@ -220,6 +220,18 @@
|
|||||||
.qg-star-svg { filter: drop-shadow(0 2px 6px rgba(251,191,36,0.4)); }
|
.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); }
|
.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; } }
|
@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-stats { display: flex; justify-content: center; gap: 22px; margin-bottom: 20px; }
|
||||||
.qg-stat { display: flex; flex-direction: column; gap: 3px; }
|
.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; }
|
.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 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 4: Квантовые способности + SR-комнаты [domain: fullstack] → [subplan](./phase-4-quantum-abilities-sr.md)
|
||||||
- [x] Phase 5: Авторинг уровней в sim-builder + раздача классу [domain: fullstack] → [subplan](./phase-5-authoring-sharing.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
|
## Phase Progress Log
|
||||||
|
|
||||||
@@ -76,16 +76,32 @@
|
|||||||
| Phase 3: Граф-уровни + зоны | fullstack | ✅ Done | ✅ | ✅ | ✅ |
|
| Phase 3: Граф-уровни + зоны | fullstack | ✅ Done | ✅ | ✅ | ✅ |
|
||||||
| Phase 4: Квантовые способности + SR | fullstack | ✅ Done | ✅ | ✅ | ✅ |
|
| Phase 4: Квантовые способности + SR | fullstack | ✅ Done | ✅ | ✅ | ✅ |
|
||||||
| Phase 5: Авторинг + раздача | fullstack | ✅ Done | ✅ | ✅ | ✅ |
|
| Phase 5: Авторинг + раздача | fullstack | ✅ Done | ✅ | ✅ | ✅ |
|
||||||
| Phase 6: Лидерборд / живая гонка | fullstack | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
|
| ~~Phase 6: Лидерборд / живая гонка~~ | fullstack | ❌ Removed (Amendment 1) | — | — | — |
|
||||||
|
|
||||||
## MVP boundary
|
## MVP boundary
|
||||||
После **Phase 2** игра играбельна и отгружаема: один полный мир физ-уровней с картой,
|
После **Phase 2** игра играбельна и отгружаема: один полный мир физ-уровней с картой,
|
||||||
прогрессом, XP и скинами. Фазы 3–6 — расширение (новые типы уровней, способности,
|
прогрессом, 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
|
## Final Review
|
||||||
- [ ] Comprehensive code review (final-reviewer)
|
- [x] Comprehensive code review (final-reviewer) — ✅ READY TO MERGE, 0 блокеров (2026-06-14)
|
||||||
- [ ] Security review (новые API: прогресс/лидерборд, user-input)
|
- [x] Security review (новые API/ввод) — ✅ SECURE, 0 critical (2026-06-14)
|
||||||
- [ ] `npm test` без новых регрессий (поверх baseline)
|
- [x] Polish-фиксы по ревью применены: game-блок санитизируется (был латентный XSS), prefers-reduced-motion guard, фикс комментария isUnlocked. Тесты 45/45 затронутых, lint 0.
|
||||||
- [ ] `npm run lint:routes` baseline 0
|
- [x] `npm test` без новых регрессий (8 = baseline: 3 auth + 5 jsdom)
|
||||||
- [ ] Merged to `feature/sim-builder`
|
- [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)
|
# 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)
|
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||||
**Domain:** fullstack
|
**Domain:** fullstack
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user