fix: stabilize public demo and align sorter twins

Document arhipovdan.ru DNS/TLS blocker, add production smoke and GPU
perf tooling, unify twin layout, and expand visual regression coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Даня Архипов
2026-07-15 18:00:25 +00:00
parent b5985f33bb
commit 16e7930304
43 changed files with 1696 additions and 227 deletions

View File

@@ -1,8 +1,10 @@
# Demo Runbook — OZON Tech Sorter Simulation
**Назначение:** провести живую демонстрацию жюри/заказчику без сюрпризов.
**Дата актуализации:** 2026-07-15
**Публичный URL:** https://arhipovdan.ru/
**Назначение:** провести живую демонстрацию жюри/заказчику без сюрпризов.
**Дата актуализации:** 2026-07-15
**Рабочий публичный URL (временный):** Quick Tunnel — см. `PRODUCTION_DEPLOYMENT_REPORT.md`
**Ожидаемый постоянный домен:** https://arhipovdan.ru/ — **BLOCKED_EXTERNAL** (`PUBLIC_DOMAIN_DIAGNOSTIC.md`)
**Локальный production:** http://127.0.0.1:3100/
**Репозиторий:** `/home/coder/arhipovdan/app`
---
@@ -24,7 +26,7 @@ npm test
npm run build
```
Ожидание: Vitest **153** passed / **16** files; build OK.
Ожидание: Vitest **166** passed; build OK.
### 1.3. Health
@@ -105,16 +107,16 @@ npm run demo:stop
Порядок storytelling (см. `demoPlaylist.ts`):
1. Короб → **B**
2. ЛанчБокс → **B**
3. Негабарит → **C**
4. Мелкий item (ручка) → **C** (min dims)
5. Тарелка → **D** (K)
6. Бутылка → **D**
7. Oversized+round → **C** (priority)
8. (edge / доп. кейс по playlist)
9. **Jam**
10. **Emergency stop**
1. Короб → **B**
2. ЛанчБокс → **B**
3. Негабарит → **C**
4. Мелкий item (ручка) → **C** (min dims)
5. Тарелка → **D** (K)
6. Бутылка → **D**
7. Oversized+round → **C** (priority)
8. (edge / доп. кейс по playlist)
9. **Jam**
10. **Emergency stop**
Точные id/SKU — в `src/domain/demoPlaylist.ts`.
@@ -141,9 +143,9 @@ git checkout backup/pre-maximum-demo-realism-20260715
## 7. Что не обещать жюри
- «Настоящий CV с камеры» — нет, pseudo-CV.
- «Полный physics engine» — нет, кинематика.
- «Агент сам выкатывает в prod» — запрещено.
- «Настоящий CV с камеры» — нет, pseudo-CV.
- «Полный physics engine» — нет, кинематика.
- «Агент сам выкатывает в prod» — запрещено.
- «`/` и `/details` — идентичная 3D-сцена» — пока нет.
---

View File

@@ -0,0 +1,40 @@
# FINAL_DEMO_READINESS
Дата: 2026-07-15
Ветка: `feature/maximum-demo-realism`
Production release (не тронут этим коммитом): `20260715-1712` / `index-ncgt6PBL.js`
## Status board
| Область | Статус | Комментарий |
| ------- | ------ | ----------- |
| Git | PASS | Feature branch clean after commit; no push |
| Production container | PASS | `owl-web-1` на `:3100`, bundle `index-ncgt6PBL.js` |
| Permanent domain | BLOCKED_EXTERNAL | `arhipovdan.ru` → openresty 404 / TLS SNI fail |
| TLS | BLOCKED_EXTERNAL | Нет SNI-сертификата на destination IP |
| Main route | PASS | Local + Quick Tunnel |
| Details route | PASS | SPA fallback 200 |
| Unit tests | PASS | 166 |
| E2E | PASS | 15 (excl. production) |
| Visual regression | PASS | 10 snapshots |
| GPU performance | PARTIAL | SwiftShader/xvfb only; hardware baseline blocked in coder |
| Safety scenarios | PASS | Jam + E-stop e2e |
| Rollback | PASS | Tag `backup/pre-maximum-demo-realism-20260715` + docker rename |
| Agent safety | PASS | Worktree / dry-run / no auto DNS |
## Verdict
```text
READY WITH EXTERNAL DOMAIN BLOCKER
```
Live demo **можно** вести с:
- `http://127.0.0.1:3100/` (локально), или
- актуального Quick Tunnel `https://invitations-based-characters-accent.trycloudflare.com/`
Постоянный домен `https://arhipovdan.ru/` **не готов** до DNS/proxy фикса пользователем (см. `PUBLIC_DOMAIN_DIAGNOSTIC.md`).
## Next deploy note
Новый код (perf overlay, twin layout, visual snapshots) собран как `index-DEbogfOZ.js` в `dist/`, но **не** задеплоен в `owl-web-1`, чтобы не нарушить работающий production. Redeploy — отдельный шаг после доменного фикса или явного запроса.

View File

@@ -0,0 +1,53 @@
# HEADED_GPU_PERFORMANCE_REPORT
Measured: 2026-07-15T17:48:54.064Z
Commit: `b5985f3`
Base URL: http://127.0.0.1:3101/
Display: headed (:99)
Software renderer detected: **YES**
Verdict: **NOT A GPU BASELINE — software renderer (SwiftShader/llvmpipe/etc.)**
## Notes
- Headed Chromium with GPU-preferring flags.
- PerfCollector only with `?perf=1`; quality forced via `?quality=`.
- Prior headless ~9.6 FPS is labeled software/headless WebGL diagnostic baseline.
- **Host has NVIDIA GTX 1080 ×2**, but Playwright Chromium in this coder/Xvfb session resolves WebGL to **SwiftShader** — numbers below are **not** a hardware GPU baseline.
- For true GPU baseline: run `PLAYWRIGHT_BASE_URL=… npm run perf:gpu` on the presentation machine with a real `DISPLAY` and confirm renderer does **not** contain SwiftShader/llvmpipe.
## Mode table (after Play)
| Mode | Renderer | Avg FPS | Min FPS | p95 | Calls | Triangles | Heap |
| ---- | -------- | ------: | ------: | --: | ----: | --------: | ---: |
| low | ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero) (0x0000C0DE)), SwiftShader driver) | 10.58 | 4.44 | 151.9 | 167 | 3546 | 16.26 |
| medium | ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero) (0x0000C0DE)), SwiftShader driver) | 9.91 | 4.68 | 158.28 | 167 | 3546 | 16.38 |
| high | ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero) (0x0000C0DE)), SwiftShader driver) | 8.69 | 4.03 | 182.2 | 167 | 3546 | 17.45 |
| demo | ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero) (0x0000C0DE)), SwiftShader driver) | 8.88 | 4.17 | 181.81 | 167 | 3546 | 21.1 |
## Memory after 3 seek+replay (demo mode)
```
{
"heapDeltaMb": 2.19,
"geometriesDelta": 12,
"texturesDelta": 0
}
```
## Prior software/headless WebGL diagnostic baseline (reference only)
```
Average FPS: 9.6
Minimum FPS: 3.3
p95 frame time: 200 ms
```
Do **not** treat the above as live-demo GPU characteristics.
## Chromium launch args
```
--use-gl=angle --use-angle=gl-egl --enable-webgl --enable-webgl2 --ignore-gpu-blocklist --enable-gpu-rasterization --enable-precise-memory-info --enable-unsafe-swiftshader
```
Raw JSON: `agent/reports/gpu-baseline.json`

View File

@@ -1,35 +1,38 @@
# PERFORMANCE_AFTER_MAXIMUM_DEMO
Measured: 2026-07-15T17:11:06.031Z
Commit: `985f7c3`
Measured: 2026-07-15
Commits referenced: `985f7c3` / tooling on current branch
## Bundle (measured)
## Bundle (production container, unchanged)
| Метрика | Значение |
| ------- | -------: |
| Total JS | 1300.9 KB |
| Total JS gzip | 358.1 KB |
| Main chunk | index-ncgt6PBL.js (96.4 KB gzip) |
| R3F chunk gzip | 226.1 KB |
| Main chunk | `index-ncgt6PBL.js` |
| Release | `20260715-1712` |
## Runtime (Playwright Chromium headless)
## software/headless WebGL diagnostic baseline
| Метрика | До | После | Цель | Статус |
| ------- | -: | ----: | ---: | ------ |
| Average FPS | n/a (не измерялось) | 9.6 | ≥30 | CHECK |
| Minimum FPS | n/a | 3.3 | ≥20 | CHECK |
| p95 frame time | n/a | 200.1 ms | ≤33 ms | CHECK |
| Load time | n/a | 1008 ms | ≤5000 | OK |
| JS heap start | n/a | 13400000 | — | measured |
| JS heap after interaction | n/a | 13400000 | no runaway | measured |
| Canvas present | — | 1 | ≥1 | OK |
| JS gzip (main+vendor) | ~335 KB main previously | 358.1 KB | — | measured |
**Не является характеристикой живой демонстрации.**
```text
Average FPS: 9.6
Minimum FPS: 3.3
p95 frame time: 200 ms
```
Повторено под xvfb/SwiftShader (~8.9 FPS demo) — см. `HEADED_GPU_PERFORMANCE_REPORT.md`.
## Headed / GPU tooling
| Item | Status |
| ---- | ------ |
| `?perf=1` overlay | Implemented |
| `?quality=low\|medium\|high\|demo` | Implemented |
| `npm run perf:gpu` | Writes report + JSON |
| Hardware NVIDIA WebGL in coder | **Not available** (Chromium → SwiftShader) |
| True GPU baseline | Run on presentation laptop with DISPLAY + real GPU |
## Notes
- **Headless Chromium WebGL is not a GPU FPS measurement.** Values ~9 FPS / p95 ~200 ms reflect software/SwiftShader-like rendering in CI/headless, **not** desktop Chrome with NVIDIA. Do not claim 30–60 FPS from this pass.
- For jury/demo FPS, measure in headed Chromium on the presentation machine (`npm run test:e2e:headed` + DevTools Performance) or Engineering Details FPS meter.
- Draw calls / triangles require WebGL inspector in headed mode; not available in this headless pass.
- Demo quality mode keeps `shadows=false` and `effectsEnabled=false` for stable real-device FPS (see `qualityMode.ts`). High mode may enable shadows.
- Raw JSON: `agent/reports/perf-metrics.json`
- Runtime error (if any): none
- Demo quality keeps `shadows=false` for stable FPS.
- Do not quote SwiftShader FPS to jury as demo capability.

View File

@@ -1,95 +1,62 @@
# Playwright E2E + Visual Regression
Lightweight end-to-end and visual regression coverage for the continuous demo on `/` and the SPA route `/details`.
## Prerequisites
- Node.js with project deps installed (`npm install`)
- Chromium for Playwright: `npx playwright install chromium`
- App reachable at the base URL (default preview: `http://127.0.0.1:3101`)
By default tests **do not** start a server. Start preview yourself, for example:
```bash
npm run build && npx vite preview --host 127.0.0.1 --port 3101
```
Or point at an already-running preview/prod instance via `PLAYWRIGHT_BASE_URL`.
## Scripts
| Script | Command | Purpose |
|--------|---------|---------|
| `test:e2e` | `playwright test` | Run all e2e + visual tests |
| `test:e2e:headed` | `playwright test --headed` | Same, headed browser |
| `test:e2e:update` | `playwright test --update-snapshots` | Refresh visual baselines |
| Script | Purpose |
|--------|---------|
| `npm run test:e2e` | Local e2e + visual (`--grep-invert @production`) |
| `npm run test:e2e:headed` | Same, headed |
| `npm run test:e2e:update` | Refresh visual baselines (manual review) |
| `npm run test:e2e:production` | Manual production smoke via `PLAYWRIGHT_BASE_URL` |
## Environment
| Variable | Default | Meaning |
|----------|---------|---------|
| `PLAYWRIGHT_BASE_URL` | `http://127.0.0.1:3101` | Target app origin |
| `PLAYWRIGHT_START_SERVER` | unset | Set to `1` to let Playwright run `vite preview` on port 3101 |
| `PLAYWRIGHT_BASE_URL` | `http://127.0.0.1:3101` | Target origin |
| `PLAYWRIGHT_START_SERVER` | unset | `1` → auto `vite preview` :3101 |
Examples:
## Config
```bash
# Against local preview (already running)
PLAYWRIGHT_BASE_URL=http://127.0.0.1:3101 npm run test:e2e
# Against prod-style port
PLAYWRIGHT_BASE_URL=http://127.0.0.1:3100 npm run test:e2e
# Auto-start preview if nothing is listening
PLAYWRIGHT_START_SERVER=1 npm run test:e2e
```
## Config highlights
- **workers: 1** — keep e2e light on shared servers
- **retries: 1**
- **chromium only**
- Screenshot on failure; trace on first retry
- Soft visual thresholds (`threshold: 0.3`, `maxDiffPixelRatio: 0.05`)
- workers: **1**
- retries: **1** (trace on first retry)
- screenshot on failure
- visual: `threshold: 0.35`, `maxDiffPixelRatio: 0.08`
- CI: `npm ci` → `npm test` → `npm run build` → `npm run test:e2e` (no production smoke)
## Test map
| Spec | Coverage |
|------|----------|
| `e2e/smoke.spec.ts` | `/` loads, canvas (or loading→canvas), no `pageerror`, play works, replay if finished |
| `e2e/controls.spec.ts` | next/prev seek, jump case 0 & 8, speed buttons, presentation toggle |
| `e2e/safety.spec.ts` | jam (index 8) → FAULT; emergency (index 9) → EMERGENCY |
| `e2e/routes.spec.ts` | `/details`, refresh stays on details, navigate back to `/` |
| `e2e/visual.spec.ts` | Idle HUD panel snapshot only (not full WebGL canvas) |
| `smoke` | canvas, play |
| `controls` | seek, speed, presentation |
| `safety` | jam / emergency |
| `routes` | `/details` SPA |
| `visual` | 10 snapshots (HUD B/C/D/jam/E-stop/recovery, presentation, details, perf overlay) |
| `production.smoke` | `@production` — manual only |
Stable selectors live on `MainPage` as `data-testid` values (`demo-play`, `demo-hud`, `demo-case-{n}`, etc.).
## Snapshot list
## Updating visual snapshots
1. Ensure the app at `PLAYWRIGHT_BASE_URL` matches the UI you want to lock.
2. Run:
```bash
npm run test:e2e:update
```text
01-home-idle-hud
02-class-b-hud
03-class-c-hud
04-class-d-hud
05-jam-fault-hud
06-emergency-hud
07-presentation-mode
08-details-page
09-recovery-hud
10-perf-overlay
```
3. Review diffs under `e2e/visual.spec.ts-snapshots/` (and commit those PNGs when intentional).
4. Re-run `npm run test:e2e` to confirm green.
HUD snapshots preferred over full WebGL pixel-perfect. Golden files are not auto-updated in CI.
Snapshots intentionally target **`[data-testid="demo-hud"]`** so WebGL canvas noise does not fail CI.
## Latest local run
## System deps
On a fresh Linux host, Chromium may need OS libraries:
```bash
npx playwright install chromium
npx playwright install-deps chromium
```text
15 passed (e2e, excl. production)
Production smoke @ :3100 — PASS (index-ncgt6PBL.js)
Production smoke @ Quick Tunnel — PASS
Production smoke @ arhipovdan.ru — FAIL (TLS/DNS blocker)
```
## Artifacts (gitignored)
- `test-results/` — failure screenshots / traces
- `playwright-report/` — HTML report
- `blob-report/` — blob reporter output
`}

View File

@@ -1,15 +1,15 @@
# PRODUCTION_DEPLOYMENT_REPORT
Дата: 2026-07-15
Ветка: `feature/maximum-demo-realism`
Базовый commit этапа 1: `985f7c3`
Дата: 2026-07-15
Ветка: `feature/maximum-demo-realism`
Базовый commit этапа 1: `985f7c3`
Production bundle после этапа 2: `index-ncgt6PBL.js`
## Способ деплоя
Docker-контейнер `owl-web-1` (image `owl-web:<release>`), порт `127.0.0.1:3100→80`.
Docker API: `DOCKER_HOST=tcp://127.0.0.1:2375` (из coder-контейнера).
Compose-файл: `docker-compose.server.yml` (исторически `-p owl`).
Docker-контейнер `owl-web-1` (image `owl-web:<release>`), порт `127.0.0.1:3100→80`.
Docker API: `DOCKER_HOST=tcp://127.0.0.1:2375` (из coder-контейнера).
Compose-файл: `docker-compose.server.yml` (исторически `-p owl`).
Скрипт: `scripts/deploy-production.sh`.
Публичный доступ в этой среде:
@@ -47,6 +47,14 @@ curl -s http://127.0.0.1:3100/ | grep -Eo 'index-[A-Za-z0-9_-]+\.js'
| `GET /details` (SPA) | 200 |
| Bundle | `index-ncgt6PBL.js` |
| Tunnel HTML | same bundle |
| `https://arhipovdan.ru/` | **BLOCKED_EXTERNAL** — `PUBLIC_DOMAIN_DIAGNOSTIC.md` |
## Public smoke
```bash
PLAYWRIGHT_BASE_URL=http://127.0.0.1:3100 npm run test:e2e:production
PLAYWRIGHT_BASE_URL=https://invitations-based-characters-accent.trycloudflare.com/ npm run test:e2e:production
```
## Rollback
@@ -61,4 +69,4 @@ docker start owl-web-1
## Smoke (tunnel)
Подтверждено: CASE 1/10, Jump to case 1–10, Previous/Next, Present, Play Demo.
Подтверждено: CASE 1/10, Jump to case 1–10, Previous/Next, Present, Play Demo, `/details`.

View File

@@ -0,0 +1,122 @@
# PUBLIC_DOMAIN_DIAGNOSTIC
Дата: 2026-07-15
Окружение: coder container `coder-arhipov-owlprime` на хосте OwlPrime
## Verdict
```text
BLOCKED_EXTERNAL
```
Постоянный домен `https://arhipovdan.ru/` **не обслуживается** текущим production-контейнером `owl-web-1`. Приложение на `127.0.0.1:3100` здорово и отдаёт актуальный bundle; проблема на уровне DNS → чужой openresty на публичном IP.
## DNS
| Поле | Значение |
| ---- | -------- |
| Nameservers | `ns1.reg.ru.`, `ns2.reg.ru.` (REG.RU, **не** Cloudflare) |
| A | `185.160.137.162` (TTL ~86400 у registrar) |
| AAAA | **нет** |
| CNAME | нет на apex; `www` тоже → `185.160.137.162` |
| Cloudflare Proxy | **нет** (NS не Cloudflare) |
Команды:
```bash
dig A arhipovdan.ru +short # 185.160.137.162
dig AAAA arhipovdan.ru +short # (empty)
dig NS arhipovdan.ru +short # ns1.reg.ru. ns2.reg.ru.
```
## Сетевая топология (факт)
| Проверка | Результат |
| -------- | --------- |
| Egress IP хоста (ipify) | `185.160.137.162` (совпадает с A) |
| `127.0.0.1:80` / `:443` на хосте | **closed** |
| `185.160.137.162:80` / `:443` | **open**, отвечает **openresty** |
| `127.0.0.1:3100` | **open**, `owl-web-1` nginx → `index-ncgt6PBL.js` |
Вывод: порт-форвард / другой хост за тем же публичным IP принимает 80/443. Это **не** namespace текущего Docker host listener set (на host net видны лишь `:3100` и `:8082`).
## TLS / HTTP к домену
| Проверка | Результат |
| -------- | --------- |
| `curl -4 http://arhipovdan.ru/` | **404** `Server: openresty` |
| `curl -4 https://arhipovdan.ru/` | **TLS fail**: `tlsv1 unrecognized name` (нет сертификата/vhost SNI для имени) |
| `curl -6` | нет AAAA → resolve fail |
| Certificate / SAN | получить нельзя (handshake abort) |
| HTML / bundle | недоступны через постоянный домен |
## Cloudflare Tunnel
| Поле | Значение |
| ---- | -------- |
| Type | **Quick Tunnel** only |
| Process | `/usr/local/bin/cloudflared tunnel --url http://127.0.0.1:3100` (pid в coder) |
| Named Tunnel | **не найден** (нет systemd unit, нет config.yml credentials) |
| Permanent ingress for arhipovdan.ru | **отсутствует** |
| Working public URL | `https://invitations-based-characters-accent.trycloudflare.com/` → bundle `index-ncgt6PBL.js` |
Quick Tunnel **не** является завершённым production DNS-решением.
## Точная причина 404 / TLS fail
1. DNS A указывает на `185.160.137.162`.
2. На этом IP:80 отвечает **чужой openresty** без vhost `arhipovdan.ru` → HTTP 404.
3. На IP:443 нет SNI-сертификата для `arhipovdan.ru` → `unrecognized_name`.
4. Актуальный sorter (`owl-web-1`) слушает только `127.0.0.1:3100` и **не** связан с этим openresty.
## Что было исправлено в коде/инфре приложения
- Production container уже отдаёт `index-ncgt6PBL.js` на `:3100`.
- Добавлены диагностика, production smoke script, документация (этот файл).
- DNS/openresty **не** изменялись из этого окружения (нет доступа к REG.RU и к openresty vhost).
## Действия пользователя (точные)
### Вариант 1 — предпочтительно: Cloudflare Named Tunnel + смена NS/DNS
1. Открыть Cloudflare → Add site `arhipovdan.ru` **или** создать Named Tunnel в существующем CF-аккаунте.
2. В REG.RU → DNS управления доменом: либо делегировать NS на Cloudflare, либо оставить REG.RU и создать запись, которую выдаст `cloudflared tunnel route dns`.
3. Ingress tunnel:
- hostname: `arhipovdan.ru` (+ `www`)
- service: `http://127.0.0.1:3100` (если cloudflared на том же host net, что и publish `3100`)
4. Выключить/не использовать Quick Tunnel как постоянный URL.
5. Проверить:
```bash
dig A arhipovdan.ru +short
curl -I https://arhipovdan.ru/
curl -s https://arhipovdan.ru/ | grep -Eo 'index-[A-Za-z0-9_-]+\.js'
# ожидается: index-ncgt6PBL.js
```
### Вариант 2 — починить openresty на том, кто слушает 185.160.137.162:80/443
1. На сервере с openresty добавить `server_name arhipovdan.ru www.arhipovdan.ru`.
2. `proxy_pass http://<IP_этого_хоста_в_LAN>:3100;` (нужен LAN-доступ к OwlPrime:3100; сейчас publish только `127.0.0.1:3100` — потребуется сменить publish на `0.0.0.0:3100` или LAN IP **осознанно**).
3. Выпустить TLS (certbot/acme) для SNI `arhipovdan.ru`.
4. Проверить теми же curl-командами.
### Вариант 3 — сменить A-запись в REG.RU
```text
Тип: A
Имя: @ (и www)
Старое значение: 185.160.137.162
Новое значение: <IP, где реально слушает ваш reverse proxy / CF>
Proxy status: DNS only (если не Cloudflare) или Proxied (если Cloudflare)
```
## Проверочные команды после фикса
```bash
curl -I https://arhipovdan.ru/
curl -I https://arhipovdan.ru/details
curl -I https://arhipovdan.ru/assets/index-ncgt6PBL.js
curl -s https://arhipovdan.ru/ | grep -Eo 'index-[A-Za-z0-9_-]+\.js'
PLAYWRIGHT_BASE_URL=https://arhipovdan.ru/ npm run test:e2e:production
```

View File

@@ -13,27 +13,27 @@
| ID | Риск | Вероятность | Влияние | Статус | Митигация |
| -- | ---- | ----------- | ------- | ------ | --------- |
| R01 | Production :3100 / https://arhipovdan.ru/ отдаёт старый dist | High | Critical | Open | `demo:health`; ручной redeploy; показывать :3101 при сомнении |
| R02 | Визуальный разрыв `/` vs `/details` путает жюри | High | High | Open | Вести показ на `/`; unify twins в плане |
| R01 | Production :3100 / https://arhipovdan.ru/ отдаёт старый dist / домен 404 | High | Critical | Open | Local+tunnel PASS; domain BLOCKED_EXTERNAL — `PUBLIC_DOMAIN_DIAGNOSTIC.md` |
| R02 | Визуальный разрыв `/` vs `/details` путает жюри | Medium | High | Mitigated | `sharedLayout` + routeConsistency tests |
| R03 | Жюри воспринимает кинематику как «фейк» | Medium | High | Mitigated | Proof HUD + честные формулировки; jam/e-stop |
| R04 | Pseudo-CV раскрыт как «обман» | Medium | Medium | Accepted | RULE overlay + confidence; не обещать ML |
| R05 | Просадка FPS на ноутбуке жюри | Medium | High | Mitigated | quality modes; shadows/effects off |
| R06 | Регрессия classifier/min dims | Low | Critical | Mitigated | tests 153; measurement+DIMENSION_LIMITS |
| R05 | Просадка FPS на ноутбуке жюри | Medium | High | Mitigated | quality modes; `?perf=1`; SwiftShader ≠ GPU |
| R06 | Регрессия classifier/min dims | Low | Critical | Mitigated | tests 166; measurement+DIMENSION_LIMITS |
| R07 | Fault freeze без recover ломает показ | Low | High | Mitigated | recover path + hotkeys R/N |
| R08 | Agent/фоновые job портят ресурсы во время демо | Medium | High | Mitigated | kill switch; runbook stop |
| R09 | Будущий auto-patch агента ломает main | Low (сейчас) | Critical | Mitigated | forbid merge/deploy; MVP verify-only |
| R10 | Утечка секретов `.env` в отчёты/логи | Low | Critical | Mitigated | policy forbidSecretAccess; не документировать values |
| R11 | Нет e2e в CI — UI регрессия незамечена | Medium | Medium | Open | ручные scripts; план CI 1 worker |
| R11 | Нет e2e в CI — UI регрессия незамечена | Low | Medium | Mitigated | CI + 10 visual snapshots; production smoke manual |
| R12 | OOM при локальной LLM + Playwright | Medium | High | Accepted (avoid) | Variant C; не совмещать |
| R13 | Disk fill отчётами/скриншотами | Low | Medium | Open | ротация reports |
| R14 | Нет docker CLI в operator env | High (факт) | Medium | Accepted | внешний deploy path документирован |
| R14 | Нет docker CLI в operator env | High (факт) | Medium | Accepted | `DOCKER_HOST=tcp://127.0.0.1:2375` |
| R15 | Swap thrash под нагрузкой | Low | High | Mitigated | лимиты 1 worker; demo mode без фона |
| R16 | Расхождение playlist expectedCategory и classifyItem | Low | High | Mitigated | wire classifyItem; tests |
| R17 | STL/fallback выглядят «игрушечно» | Medium | Low | Accepted | modelAssets notes; backlog textures |
| R18 | Tunnel/cloudflared outage | Low | Critical | Open | fallback loopback :3100 / LAN preview |
| R18 | Tunnel/cloudflared outage / permanent DNS | High | Critical | Open | Quick Tunnel temp; Named Tunnel + REG.RU DNS required |
| R19 | Несогласованность документации и кода | Medium | Low | Mitigated | этот пакет docs = snapshot 2026-07-15 |
| R20 | Попытка «добавить physics» перед показом → регрессия | Medium | High | Open | запрет P0-physics перед demo day |
| R21 | Hardware GPU baseline недоступен в coder | High | Medium | Accepted | NVIDIA есть, Chromium→SwiftShader; измерять на demo laptop |
---
## 2. Детали по критическим рискам

View File

@@ -0,0 +1,63 @@
# Twin Geometry Alignment
## Goal
Keep `/` (continuous twin) and `/details` (state-machine twin) as projections of one sorter layout — same belt height, zone X/Z, sensors, gate, and category colors — without rewriting both scenes or enabling a physics engine.
## What was unified
| Concern | Source of truth | Consumers |
| --- | --- | --- |
| Conveyor dims, `BELT_TOP_Y` (0.7 m), zones A/B/C/D, gate, roll cages | `src/domain/physicalLayout.ts` | Continuous twin, `conveyorNetwork`, cameras |
| Shared bridge + details-derived layout | `src/domain/layout/sharedLayout.ts` | Details `TWIN_LAYOUT`, route/color tokens |
| Category / route colors | `src/domain/industrialTheme.ts` → `CATEGORY_COLORS` | Both twins, details `ROUTE_COLORS` |
| Class route directions (B→+X, C→+Z, D→−Z) | `CLASS_ROUTE_DIRECTIONS` in `sharedLayout` | Documentation / consistency |
### Details twin derivation
`itemMotion.ts` no longer hardcodes a compact layout (`beltY: 0.35`, divergent X). It exports:
```ts
TWIN_LAYOUT = DETAILS_TWIN_LAYOUT // from sharedLayout
ROUTE_COLORS = CATEGORY_COLORS
PHYSICS_ENGINE_ENABLED = false
```
Aligned meters (vs previous details values):
- `beltY` → `BELT_TOP_Y` (0.7)
- `startX` / `zoneBX` / `zoneCZ` / `zoneDZ` → `ZONES.A/B/C/D`
- `cameraX` / `laserX` / `gateX` → `ZONES.CAMERA/LASER/GATE`
- `rollCageSize` → `ROLL_CAGE`
- Progress keyframes (`progressForState`) unchanged — only absolute meters moved
Details scene helpers (conveyor materials, zone pads, sensor poles, label Y) use `TWIN_LAYOUT.beltY` or `INDUSTRIAL_PALETTE` so the presentation layer tracks the shared height.
### Consistency test
`src/domain/routeConsistency.test.ts` asserts for each non-fault `DEMO_PLAYLIST` case:
1. `classifyItem` category === `expectedCategory`
2. Continuous playback `targetCategory` matches after seek
3. `CATEGORY_COLORS` has an entry for that category
4. `getPhysicalItemPose` at mid-routing has `activeRoute` === category and `phase === 'routing'`
## Intentional differences (keep)
| Continuous (`/`) | Details (`/details`) |
| --- | --- |
| Engineering HUD: measurement, events, physical surfaces from `conveyorNetwork` | Presentation / teaching twin: state-machine progress keyframes |
| Deterministic `getPhysicalItemPose` along network surfaces | `itemPosition3D` progress along a simplified X (and Z for C/D) |
| Light industrial floor / grid (`INDUSTRIAL_PALETTE` light) | Dark engineering projection of the same palette |
| Full B receiver spur + chutes + cage floors | Simplified zone boxes + roll-cage visuals |
| Ultrasonic not a separate continuous station | Details-only `ultrasonicX` between laser and gate |
| Cinematic camera / playlist director | OrbitControls + machine-state HUD chips |
Motion remains **state-machine / kinematic** only (`PHYSICS_ENGINE_ENABLED === false`). Do not enable a physics engine for alignment.
## How to extend
1. Change geometry in `physicalLayout.ts` (or zone map there).
2. Re-export / map in `layout/sharedLayout.ts` if details needs a new derived field.
3. Keep details keyframe ratios in `itemMotion.ts` unless timing intentionally changes.
4. Prefer `INDUSTRIAL_PALETTE` / `CATEGORY_COLORS` for new materials.