2 Commits

Author SHA1 Message Date
root
f2ce611330 fix: align CV rules and refresh final documentation
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-02 22:51:41 +02:00
root
d701339930 release: finalize clean submission repository
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-02 22:39:46 +02:00
11 changed files with 592 additions and 350 deletions

286
README.md
View File

@@ -1,110 +1,98 @@
# OZON Sorter Digital Twin # OWL PRIME — Ozon Tech Track 3
Complete Track 3 Ozon Tech solution in one repository: a web digital twin of the sorting line, plus a real RealSense/OpenCV CV prototype. ## О проекте
OWL PRIME — инженерный контур предварительной сортировки товаров: веб-цифровой двойник конвейерной линии, классификация B/C/D, физическая маршрутизация в симуляции, рабочий CV-прототип на RealSense D415 и экспериментальный физический стенд.
Это не сертифицированный промышленный ПАК, а воспроизводимое решение хакатона: цифровой twin на проде, CV-прототип в репозитории, CAD/ассеты и документация для проверки судьями.
## Состав решения
1. **Web digital twin** — React/Three.js симуляция конвейера, измерения, classifier, diverters.
2. **CV-прототип**`cv/`: RealSense D415 + OpenCV → габариты / круг → B/C/D.
3. **CAD и runtime 3D** — авторский FreeCAD и GLB/STL для деплоя.
4. **Физический стенд** — лента, рама, камера над полотном, электроника/SCADA.
5. **Документация**`/documentation`, `docs/ENGINEERING.md`.
6. **Презентация**`presentation/Owl_Prime_Ozon_Tech_Track_3_FINAL.pdf`.
## Демо
**Production:** https://arhipovdan.ru **Production:** https://arhipovdan.ru
## 1. Overview | Route | Назначение |
This repository delivers a continuous web simulation of an Ozon conveyor sorter: CAD conveyor, product models, camera/measurement stage, B/C/D classification, diverters, and Rapier physics. The public demo runs on https://arhipovdan.ru with routes `/` (simulation) and `/documentation` (engineering status).
Separately, `cv/` contains a **working hardware prototype** that reads Intel RealSense D415 depth, measures parcels with OpenCV, classifies B/C/D, and can publish results over MQTT. It is **not** wired into the live website.
Both paths share the same Track 3 classification domain (exclusive 10×10×10 … 450×320×320 mm, roundness K > 0.8). The web twin uses a digital sensor simulation; the CV folder uses real depth frames.
`main` is the canonical complete solution. Developers do not need other branches to run the web app or inspect/run the CV prototype.
Large submission artifacts (presentation, video, optional CAD/model mirrors) belong in team cloud storage; runtime assets required by deploy stay in Git.
## 2. Submission components
| Component | Location | Notes |
|---|---|---|
| Web digital twin | `src/`, `public/` | Production-integrated |
| Real CV prototype | `cv/` | WORKING_PROTOTYPE, not live-integrated |
| Author CAD | `3d_models/conveer.FCStd` | FreeCAD source |
| Official materials | `input_info/`, `official_sources/` | PDFs/ZIPs cited by docs/code |
| Production domain | https://arhipovdan.ru | Docker + nginx |
| Presentation / video | cloud (owner) | Links TBD — see §17 |
## 3. Production demo
- **URL:** https://arhipovdan.ru
- **`/`** — continuous digital-twin simulation
- **`/documentation`** — canonical engineering status
- Unknown routes redirect to `/`
Device behavior on current baseline:
- **Desktop:** interactive WebGL 3D
- **Mobile:** lightweight **2D** fallback (not full WebGL 3D)
Build identity: `/version.json`.
## 4. Web capabilities
- Three-module CAD conveyor (clean → camera → sorter)
- Camera / measurement simulation and Track 3 classifier
- B / C / D routing with CAD diverters (45° / +45°)
- Rapier product physics (contact sorting **not** fully validated)
- Continuous playback HUD + documentation page
## 5. Real CV prototype
| Field | Value |
|---|---| |---|---|
| Path | `cv/` | | `/` | Непрерывная симуляция |
| Origin | `drho1y-mvp_1` / `vision_classifier/` | | `/documentation` | Инженерный статус |
| Technology | RealSense D415 + OpenCV (depth segmentation + metrics) | | `*` | Редирект на `/` |
| Status | WORKING_PROTOTYPE |
| Integration | **Not** connected to production web runtime |
See **[cv/README.md](cv/README.md)** for install, demo, live camera, and MQTT. - **Desktop:** интерактивный WebGL 3D.
- **Mobile:** облегчённый 2D fallback (как в текущем baseline).
## 6. Architecture Идентичность сборки: `/version.json`.
## Основные возможности
- непрерывный CAD-конвейер и STL-товары;
- цифровой этап камеры / измерения;
- rule-based classifier B/C/D;
- CAD-дивертеры LEFT/RIGHT (45° / +45°);
- физика Rapier (лента 1 м/с);
- CV-прототип depth→B/C/D (не в live web);
- инженерная документация в приложении.
## Правила классификации
Официальные границы (`official_sources/doc-1783095831.pdf`), реализованы в web (`src/domain/classifier.ts`) и CV (`cv/classify.py`):
1. Габариты строго **> 10×10×10 мм** и **< 450×320×320 мм**, иначе → **C**.
2. Если габариты OK и **K > 0.8** (круг) → **D**.
3. Иначе → **B**.
4. **C-priority:** негабарит + круг → только **C**.
5. Граница: **K = 0.8 не круг** (→ B, не D). Одинаково в web и CV.
## Архитектура
**Web**
``` ```
Real device path: Product → measurement (digital) → classifier → route → twin → B/C/D receiver
RealSense D415 → depth preprocess → segmentation → measurement
→ B/C/D → optional MQTT / hardware
Web path:
Digital product → simulated sensor → classifier
→ physical digital twin → B/C/D receiver visualization
``` ```
Shared: B/C/D semantics and official dimension/roundness rules. **CV**
Not shared today: live camera frames into the website.
## 7. Repository structure
``` ```
.github/ CI (build, unit, e2e) RealSense D415 → depth → segmentation → L×W×H + K → B/C/D → optional MQTT
3d_models/ Author CAD (conveer.FCStd)
cv/ Real CV prototype (Python)
docs/ Engineering notes
e2e/ Playwright smoke/routes
input_info/ Official Ozon input packs
official_sources/ Classifier bounds PDF
public/ Runtime static assets (GLB/STL/draco)
src/ React/Three web twin
Dockerfile Web production image
docker-compose.server.yml
nginx.conf
package.json / lock
vite / vitest / playwright / tsconfig
README.md
``` ```
No other top-level product directories are required to run or understand the solution. CV **не подключён** к https://arhipovdan.ru. Общее — домен правил B/C/D, не live-канал кадров.
## 8. Requirements ## Технологии
**Web:** Node.js 20+, npm (`package-lock.json`). | Слой | Стек |
**CV:** Python 3.10+, ffmpeg, V4L2; RealSense D415 for live mode (`cv/requirements.txt`). |---|---|
**Hardware (CV live / MQTT):** D415 USB3; optional MQTT broker + servo/motor controllers on site network. | Web | React, TypeScript, Three.js / R3F, Rapier, Vite, Vitest, Playwright |
| CV | Python, OpenCV, RealSense D415 (V4L2), optional MQTT (`paho-mqtt`) |
## 9. Web quick start ## Структура репозитория
```
.github/ CI
3d_models/ Авторский CAD (conveer.FCStd)
cv/ CV-прототип RealSense + OpenCV
docs/ Engineering notes
e2e/ Playwright smoke/routes
input_info/ Официальные входные пакеты
official_sources/ PDF с границами classifier
presentation/ Финальная презентация (один PDF)
public/ Runtime GLB/STL/draco
src/ Web twin
```
Конфиги деплоя: `Dockerfile`, `docker-compose.server.yml`, `nginx.conf`, `package.json`.
## Запуск web
Node.js 20+.
```bash ```bash
npm ci npm ci
@@ -114,100 +102,78 @@ npm run build
npm run preview # http://127.0.0.1:3100 npm run preview # http://127.0.0.1:3100
``` ```
## 10. CV quick start ## Запуск CV
Python 3.10+. Live-режим требует Intel RealSense D415 и ffmpeg.
```bash ```bash
cd cv cd cv
./demo.sh # venv + deps; HUD on :8080 (needs D415 for live view) python3 -m venv .venv
# without camera: source .venv/bin/activate
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt pip install -r requirements.txt
.venv/bin/python test_classify.py cp config.example.yaml config.yaml # MQTT выключен по умолчанию
.venv/bin/python test_geometry.py
# live pipeline (hardware): # без камеры:
python test_classify.py
python test_geometry.py
# с камерой:
./demo.sh # HUD http://127.0.0.1:8080/
./run.sh --preview --no-mqtt --no-motor ./run.sh --preview --no-mqtt --no-motor
``` ```
`npm install` does **not** install CV dependencies. Подробности: [`cv/README.md`](cv/README.md). `npm` CV-зависимости не ставит.
## 11. Configuration ## Конфигурация
**Web (build-time, optional):** `VITE_BUILD_COMMIT`, `VITE_BUILD_BRANCH`, `VITE_BUILD_RELEASE``/version.json`. No runtime secrets. - `.env` **не** коммитится.
- Web: опционально `VITE_BUILD_COMMIT` / `VITE_BUILD_BRANCH` / `VITE_BUILD_RELEASE` для `/version.json`.
- CV: `cv/config.example.yaml` → локальный `cv/config.yaml` (gitignored). MQTT/motor/routing **disabled by default**. Секреты не хранить в Git.
**CV:** copy `cv/config.example.yaml``cv/config.yaml` (gitignored). MQTT/motor/routing **disabled by default**. Never commit real passwords or broker credentials. ## CAD и модели
## 12. Classification rules | Файл | Роль |
Verified in web (`src/domain/classifier.ts`) and CV (`cv/classify.py`):
- dimensions strictly **> 10×10×10 mm** and **< 450×320×320 mm**
- circular when **K > 0.8** (web) / `circle_ratio ≥ 0.8` (CV)
- order: dimensions fail → **C**; else circular → **D**; else **B**
Official citation: `official_sources/doc-1783095831.pdf` (present; not re-parsed on every doc pass). Missing extracted brief PDF is not claimed.
## 13. Physics (accepted `main`)
From current source (not superseded experimental branches):
- belt speed **1.0 m/s** (`CONVEYOR_SPEED_MPS`)
- fixed timestep **1/60 s** (`PHYSICS_TIMESTEP_SEC`)
- CCD for light/thin product bodies
- diverters LEFT **45°**, RIGHT **+45°**
- full contact-only junction sorting through CAD: **not fully validated**
## 14. CAD and assets
| Asset | Path |
|---|---| |---|---|
| Author CAD | `3d_models/conveer.FCStd` | | `3d_models/conveer.FCStd` | Авторский CAD |
| Runtime GLB | `public/models/sorter/conveyor-clean.glb` | | `public/models/sorter/conveyor-clean.glb` | Runtime конвейер |
| Products | `public/models/*.stl` | | `public/models/*.stl` | Модели товаров |
Keep runtime assets in Git for deploy. Mirror large CAD/models/presentation/video to cloud for submission. Крупные материалы для сдачи дополнительно зеркалируйте в облако; runtime-ассеты для деплоя остаются в Git.
SHA-256 (frozen): ## Физика (текущий main)
``` - скорость ленты **1.0 м/с**;
3d_models/conveer.FCStd - timestep **1/60 с**;
90c1844a4ca05e26def783d6130fc4b993430dde14307534ef8fbb21c9fac2e6 - CCD для лёгких/тонких тел;
- diverters 45° / +45°;
- полный contact-only junction sorting **не fully validated**.
public/models/sorter/conveyor-clean.glb ## Проверка
1dc7a8d7891bfe756e277ad5368df74cb73410156b2fe0f92845afb8a56f285a
```
## 15. Testing
```bash ```bash
npm test -- --run npm ci && npm test -- --run && npm run build
# current release result: 196/196 # E2E: preview :3101 + e2e/routes.spec.ts + e2e/smoke.spec.ts
npm run build
PLAYWRIGHT_BASE_URL=http://127.0.0.1:3101 npm run test:e2e -- e2e/routes.spec.ts e2e/smoke.spec.ts
cd cv && .venv/bin/python test_classify.py && .venv/bin/python test_geometry.py cd cv && python -m compileall . && python test_classify.py && python test_geometry.py
``` ```
## 16. Deployment Актуальный релиз: unit **196/196**, build PASS, focused E2E PASS, CV compile + unit без камеры PASS. Прод: `/` и `/documentation` → 200.
Nginx terminates TLS for `arhipovdan.ru` and proxies to Docker `owl-web-1` (`docker-compose.server.yml` + `Dockerfile`) on `127.0.0.1:3100`. Deploy from `main` with build-args for `/version.json`. CV is **not** part of the web container. ## Ограничения
## 17. Submission materials - инженерный прототип, не industrial-certified ПАК;
- CV не live-интегрирован в web;
- live CV требует D415; на CI — только unit/compile;
- параметры симуляции требуют калибровки на стенде;
- mobile — 2D lite fallback;
- облачная ссылка на доп. материалы — по решению владельца.
| Material | Status | ## Материалы
|---|---|
| Presentation URL | REQUIRED_FROM_OWNER |
| Video demo URL | REQUIRED_FROM_OWNER |
| Cloud folder URL | REQUIRED_FROM_OWNER |
Do not invent links. Runtime site assets remain in Git even when mirrored to cloud. - Презентация: [`presentation/Owl_Prime_Ozon_Tech_Track_3_FINAL.pdf`](presentation/Owl_Prime_Ozon_Tech_Track_3_FINAL.pdf)
- Production: https://arhipovdan.ru
- Cloud folder: `[ДОБАВИТЬ ССЫЛКУ]`
## 18. Known limitations ## Статус
- Mobile uses 2D lite fallback on current baseline **`main` — каноническое полное решение.** Другие ветки исторические и не нужны для запуска web или CV.
- CV is a prototype and is not live-integrated into arhipovdan.ru
- Simulation physics is engineering-derived; hardware calibration still required
- Contact routing through CAD diverters not fully validated
- Large presentation/video must be uploaded to cloud by owner
## 19. Branch history policy
**`main` is the canonical complete solution** (web + cleaned CV under `cv/`). Historical branches (`dan_branch`, `drho1y-mvp_1`, …) may remain for audit but are not required to run the product.

View File

@@ -36,7 +36,8 @@ Primary modules: `main.py` (live), `demo.py` (HUD), `classify.py` (rules), `meas
## Classification rules (Track 3) ## Classification rules (Track 3)
1. Dimensions must be strictly **> 10×10×10 mm** and **< 450×320×320 mm** → else **C** 1. Dimensions must be strictly **> 10×10×10 mm** and **< 450×320×320 mm** → else **C**
2. Else if `circle_ratio 0.8`**D** 2. Else if `circle_ratio > 0.8`**D**
(`K == 0.8` is **not** circular — same strict rule as web `classifier.ts`)
3. Else → **B** 3. Else → **B**
Stabilization: median window + vote → **LOCK**. Uncertain cases fall back to zone **C** after N frames. Stabilization: median window + vote → **LOCK**. Uncertain cases fall back to zone **C** after N frames.

View File

@@ -79,13 +79,14 @@ def classify(
""" """
Порядок ТЗ: Порядок ТЗ:
1) габариты → иначе C (приоритет над кругом) 1) габариты → иначе C (приоритет над кругом)
2) если r_in/r_out >= 0.8 в любом сечении → D 2) если r_in/r_out > 0.8 в любом сечении → D
(K == 0.8 НЕ круг — как в web classifier.ts)
3) иначе → B 3) иначе → B
""" """
dims = _sorted_dims(measurement.length_mm, measurement.width_mm, measurement.height_mm) dims = _sorted_dims(measurement.length_mm, measurement.width_mm, measurement.height_mm)
passes = check_size(dims, min_mm, max_mm) passes = check_size(dims, min_mm, max_mm)
ratio = float(measurement.circle_ratio) ratio = float(measurement.circle_ratio)
circular = ratio >= float(circle_ratio_threshold) circular = ratio > float(circle_ratio_threshold)
clipped = bool(getattr(measurement, "clipped_by_frame", False)) clipped = bool(getattr(measurement, "clipped_by_frame", False))
if not passes or clipped: if not passes or clipped:
@@ -112,7 +113,7 @@ def classify(
circle_ratio=ratio, circle_ratio=ratio,
passes_size=True, passes_size=True,
is_circular=True, is_circular=True,
reason=f"круг в сечении: r_in/r_out={ratio:.3f} >= {circle_ratio_threshold}", reason=f"круг в сечении: r_in/r_out={ratio:.3f} > {circle_ratio_threshold}",
) )
return ClassificationResult( return ClassificationResult(
@@ -121,7 +122,7 @@ def classify(
circle_ratio=ratio, circle_ratio=ratio,
passes_size=True, passes_size=True,
is_circular=False, is_circular=False,
reason=f"габариты OK, круга нет: r_in/r_out={ratio:.3f} < {circle_ratio_threshold}", reason=f"габариты OK, круга нет: r_in/r_out={ratio:.3f} <= {circle_ratio_threshold}",
) )

View File

@@ -113,12 +113,13 @@ class DecisionStabilizer:
top_m = _median(self._tops) top_m = _median(self._tops)
sec_m = _median(self._secs) sec_m = _median(self._secs)
ratio_p75 = _percentile(self._ratios, 75) ratio_p75 = _percentile(self._ratios, 75)
# устойчивый круг: медиана >= 0.8 ИЛИ (медиана сечений >= 0.8 и ≥ половины окна сильные) # устойчивый круг: медиана > 0.8 ИЛИ (медиана сечений > 0.8 и ≥ половины окна сильные)
# K == 0.8 официально НЕ круг (строгое > threshold)
sec_strong = ( sec_strong = (
sum(1 for x in self._secs if x >= self.enter_circle) / max(1, len(self._secs)) sum(1 for x in self._secs if x > self.enter_circle) / max(1, len(self._secs))
) )
circular = ratio >= self.enter_circle or ( circular = ratio > self.enter_circle or (
sec_m >= self.enter_circle and sec_strong >= 0.55 sec_m > self.enter_circle and sec_strong >= 0.55
) )
ratio_show = max(ratio, sec_m) if circular else ratio ratio_show = max(ratio, sec_m) if circular else ratio
@@ -153,7 +154,7 @@ class DecisionStabilizer:
reason=( reason=(
f"круг: med={ratio:.3f} p75={ratio_p75:.3f} " f"круг: med={ratio:.3f} p75={ratio_p75:.3f} "
f"top={top_m:.3f} sec={sec_m:.3f} strong={sec_strong:.0%} " f"top={top_m:.3f} sec={sec_m:.3f} strong={sec_strong:.0%} "
f">= {self.enter_circle}" f"> {self.enter_circle}"
), ),
) )
else: else:
@@ -165,7 +166,7 @@ class DecisionStabilizer:
is_circular=False, is_circular=False,
reason=( reason=(
f"не круг: med={ratio:.3f} sec={sec_m:.3f} " f"не круг: med={ratio:.3f} sec={sec_m:.3f} "
f"strong={sec_strong:.0%} < {self.enter_circle}" f"strong={sec_strong:.0%} <= {self.enter_circle}"
), ),
) )

View File

@@ -43,11 +43,40 @@ def test_lying_bottle_must_be_D_not_B():
assert r.category.zone == "D" assert r.category.zone == "D"
def test_border_circle(): def test_strict_circle_boundary():
r = classify_from_dims(100, 50, 50, circle_ratio=0.8) """Официально: круг только при K > 0.8. K == 0.8 → B (не D)."""
below = classify_from_dims(100, 50, 50, circle_ratio=0.799999)
assert below.category == Category.SUITABLE
assert below.category.zone == "B"
assert below.is_circular is False
exact = classify_from_dims(100, 50, 50, circle_ratio=0.8)
assert exact.category == Category.SUITABLE
assert exact.category.zone == "B"
assert exact.is_circular is False
above = classify_from_dims(100, 50, 50, circle_ratio=0.800001)
assert above.category == Category.NEED_PACK
assert above.category.zone == "D"
assert above.is_circular is True
def test_normal_non_circular_B():
r = classify_from_dims(120, 80, 40, circle_ratio=0.5)
assert r.category == Category.SUITABLE
assert r.category.zone == "B"
def test_normal_circular_above_threshold_D():
r = classify_from_dims(100, 50, 50, circle_ratio=0.81)
assert r.category == Category.NEED_PACK assert r.category == Category.NEED_PACK
r2 = classify_from_dims(100, 50, 50, circle_ratio=0.799) assert r.category.zone == "D"
assert r2.category == Category.SUITABLE
def test_oversized_circular_still_C():
r = classify_from_dims(500, 100, 100, circle_ratio=0.95)
assert r.category == Category.OVERSIZE
assert r.category.zone == "C"
def test_border_dims_strict(): def test_border_dims_strict():
@@ -80,7 +109,10 @@ if __name__ == "__main__":
test_too_small() test_too_small()
test_need_pack_cylinder() test_need_pack_cylinder()
test_lying_bottle_must_be_D_not_B() test_lying_bottle_must_be_D_not_B()
test_border_circle() test_strict_circle_boundary()
test_normal_non_circular_B()
test_normal_circular_above_threshold_D()
test_oversized_circular_still_C()
test_border_dims_strict() test_border_dims_strict()
test_dims_order_independent() test_dims_order_independent()
print("OK: all classification tests passed") print("OK: all classification tests passed")

View File

@@ -20,8 +20,11 @@ Companion to `README.md` and `/documentation`. Not a stage changelog.
| `OPENING_SAFETY_MARGIN_SEC` | 0.15 | same | | `OPENING_SAFETY_MARGIN_SEC` | 0.15 | same |
| Contact / clear planes | ≈1.0538 / 1.6000 | `buildDiverterPlanes` + mount hinge | | Contact / clear planes | ≈1.0538 / 1.6000 | `buildDiverterPlanes` + mount hinge |
| Classifier min/max | exclusive 10³ / 450×320×320 | `src/domain/classifier.ts` | | Classifier min/max | exclusive 10³ / 450×320×320 | `src/domain/classifier.ts` |
| Roundness | K > 0.8 | same | | Roundness | K > 0.8 (K = 0.8 not circular); web + CV aligned | `classifier.ts` / `cv/classify.py` |
| Belt speed (sim) | 1.0 m/s | runtime / physics helpers |
| Physics timestep | 1/60 s | Rapier world step |
| `CONVEYOR_CAD_URL` | `/models/sorter/conveyor-clean.glb` | `ConveyorCadModel.tsx` | | `CONVEYOR_CAD_URL` | `/models/sorter/conveyor-clean.glb` | `ConveyorCadModel.tsx` |
| Presentation | `presentation/Owl_Prime_Ozon_Tech_Track_3_FINAL.pdf` | 10 slides |
## Active source tree (runtime) ## Active source tree (runtime)
@@ -69,10 +72,10 @@ Grounded only in present sources + current `main` code/tests (not a full officia
|---|---|---|---|---| |---|---|---|---|---|
| B/C/D bounds & roundness | `official_sources/doc-1783095831.pdf` | web `classifier.ts`, CV `cv/classify.py` | unit tests | PARTIAL (PDF not re-parsed each pass) | | B/C/D bounds & roundness | `official_sources/doc-1783095831.pdf` | web `classifier.ts`, CV `cv/classify.py` | unit tests | PARTIAL (PDF not re-parsed each pass) |
| Digital twin demo | workspace PDFs in `input_info/` | `/` + `/documentation` | e2e smoke/routes, production | PASS (demo present) | | Digital twin demo | workspace PDFs in `input_info/` | `/` + `/documentation` | e2e smoke/routes, production | PASS (demo present) |
| CAD conveyor | author pack / STL references | `3d_models/`, `conveyor-clean.glb` | checksums in README | PASS (assets present) | | CAD conveyor | author pack / STL references | `3d_models/`, `conveyor-clean.glb` | checksums in `/documentation` | PASS (assets present) |
| Real measurement CV | Track 3 camera intent in briefs | `cv/` RealSense+OpenCV | `cv/README.md`, `test_classify.py` | PARTIAL (prototype, not live web) | | Real measurement CV | Track 3 camera intent in briefs | `cv/` RealSense+OpenCV on `main` | `cv/README.md`, `test_classify.py` | PARTIAL (WORKING_PROTOTYPE, not live web) |
| Physical industrial line | scoring/workspace PDFs | web physics + optional MQTT CV | code; contact not fully validated | PARTIAL | | Physical industrial line | scoring/workspace PDFs | web physics + optional MQTT CV | code; contact not fully validated | PARTIAL |
| Presentation / video | platform rules | cloud links | README §17 | NOT_VERIFIED (links missing) | | Presentation | platform rules | `presentation/Owl_Prime_Ozon_Tech_Track_3_FINAL.pdf` | 10-page PDF in repo | PASS (file present) |
## Real CV prototype ## Real CV prototype

View File

@@ -10,7 +10,10 @@ test.describe('routes', () => {
await page.getByTestId('nav-documentation').first().click(); await page.getByTestId('nav-documentation').first().click();
await expect(page).toHaveURL(/\/documentation\/?$/); await expect(page).toHaveURL(/\/documentation\/?$/);
await expect(page.getByTestId('documentation-page')).toBeVisible(); await expect(page.getByTestId('documentation-page')).toBeVisible();
await expect(page.getByTestId('docs-production-status')).toContainText('DATA_ACQUISITION_PACK_READY'); await expect(page.getByTestId('docs-production-status')).toContainText(
'FINAL_ENGINEERING_PROTOTYPE_READY',
);
await expect(page.getByTestId('docs-production-status')).toContainText('WORKING_PROTOTYPE');
await expect(page.getByTestId('nav-documentation').first()).toHaveClass(/active/); await expect(page.getByTestId('nav-documentation').first()).toHaveClass(/active/);
await page.reload(); await page.reload();

Binary file not shown.

View File

@@ -4,16 +4,29 @@
* Unverified / planned items are labeled explicitly — never implied complete. * Unverified / planned items are labeled explicitly — never implied complete.
*/ */
/** Kept for e2e (`docs-production-status`) and acquisition-pack provenance. */
export const PRODUCTION_STATUS = { export const PRODUCTION_STATUS = {
acquisitionPackStatus: 'DATA_ACQUISITION_PACK_READY', projectStatus: 'FINAL_ENGINEERING_PROTOTYPE_READY',
webTwinStatus: 'BASELINE_PRESERVED', webTwinStatus: 'PRODUCTION_LIVE',
cvStatus: 'WORKING_PROTOTYPE',
cvLiveIntegrated: false,
unitTests: '196/196', unitTests: '196/196',
cvTests: 'PASS (no camera)',
productionBuild: 'PASS', productionBuild: 'PASS',
productionUrl: 'https://arhipovdan.ru',
contactPhysics: 'NOT_FULLY_VALIDATED', contactPhysics: 'NOT_FULLY_VALIDATED',
officialCompliance: 'PARTIAL_SOURCES_PRESENT', officialCompliance: 'PARTIAL_SOURCES_PRESENT',
canonicalBranch: 'main',
} as const; } as const;
export const SOLUTION_COMPONENTS = [
'Web digital twin (https://arhipovdan.ru)',
'Real CV working prototype (cv/, RealSense D415 + OpenCV)',
'Physical experimental conveyor stand',
'Author CAD (3d_models/conveer.FCStd)',
'Official B/C/D classifier (web + CV, K > 0.8)',
'Engineering documentation (/documentation)',
] as const;
export const CONFIRMED_LAYOUT = { export const CONFIRMED_LAYOUT = {
workspaceMm: { length: 10000, width: 6000 }, workspaceMm: { length: 10000, width: 6000 },
conveyorWidthMm: 500, conveyorWidthMm: 500,
@@ -23,7 +36,7 @@ export const CONFIRMED_LAYOUT = {
} as const; } as const;
export const CLASSIFIER_BOUNDS = { export const CLASSIFIER_BOUNDS = {
status: 'CURRENT_IMPLEMENTATION_VERIFIED_IN_CODE' as const, status: 'ALIGNED_WEB_AND_CV' as const,
officialSourceReference: 'official_sources/doc-1783095831.pdf', officialSourceReference: 'official_sources/doc-1783095831.pdf',
officialSourceParsedThisPass: false, officialSourceParsedThisPass: false,
minExclusiveMm: { width: 10, depth: 10, height: 10 }, minExclusiveMm: { width: 10, depth: 10, height: 10 },
@@ -33,8 +46,63 @@ export const CLASSIFIER_BOUNDS = {
min: '> 10×10×10 мм', min: '> 10×10×10 мм',
max: '< 450×320×320 мм', max: '< 450×320×320 мм',
roundness: 'K > 0.8', roundness: 'K > 0.8',
exactBoundary: 'K = 0.8 → B (not circular), not D',
}, },
checkOrder: 'dimensions→C, else circular→D, else B' as const, checkOrder: 'dimensions→C, else circular (K>0.8)→D, else B' as const,
} as const;
export const CV_PROTOTYPE = {
path: 'cv/',
status: 'WORKING_PROTOTYPE',
hardware: 'Intel RealSense D415',
software: 'Python + OpenCV (+ optional MQTT)',
liveIntegrated: false,
pipeline: [
'depth frame',
'segmentation',
'object contour',
'L×W×H',
'roundness K',
'B/C/D',
'optional MQTT',
] as const,
notes: [
'CV works as a separate prototype under cv/ on main.',
'CV is not connected directly to the live public website.',
'Hardware live validation requires RealSense D415.',
'No-camera unit tests and compileall run in CI/local verification.',
],
} as const;
export const PHYSICAL_STAND = {
status: 'EXPERIMENTAL_PROTOTYPE',
elements: [
'Physical belt conveyor',
'Camera mounting structure above the belt',
'Intel RealSense D415',
'Electronics / control nodes',
'Experimental actuators and printed components',
] as const,
purpose: 'Measurements, calibration, and hardware validation — not claimed as industrial end-to-end certified sorting.',
} as const;
export const ARCHITECTURE = {
realPath: [
'physical product',
'RealSense D415',
'OpenCV measurement',
'classifier (K > 0.8)',
'B/C/D result',
'optional MQTT/controller',
] as const,
digitalPath: [
'digital product',
'simulated measurement',
'same classifier rules',
'route command',
'digital twin',
'B/C/D receiver',
] as const,
} as const; } as const;
export const ROUTE_MAPPING = [ export const ROUTE_MAPPING = [
@@ -72,6 +140,7 @@ export const PHYSICS_STATUS = {
'Synchronized CAD diverter visual / kinematic targets', 'Synchronized CAD diverter visual / kinematic targets',
'CCD enabled for light/thin SKUs in runtime and headless sim', 'CCD enabled for light/thin SKUs in runtime and headless sim',
'Visual/physics spawn gating via product asset preload', 'Visual/physics spawn gating via product asset preload',
'Belt speed target 1.0 m/s; physics timestep 1/60 s',
], ],
notFullyValidated: [ notFullyValidated: [
'Complete contact-only routing through CAD diverters', 'Complete contact-only routing through CAD diverters',
@@ -83,18 +152,26 @@ export const PHYSICS_STATUS = {
planned: [ planned: [
'Visual full belt loop with surface-velocity coupling', 'Visual full belt loop with surface-velocity coupling',
'Controlled tangential friction at 1 m/s', 'Controlled tangential friction at 1 m/s',
'Dynamic rigid bodies for divert segment with fixed timestep',
'Per-SKU collider, damping, and friction profiles', 'Per-SKU collider, damping, and friction profiles',
], ],
} as const; } as const;
export const MOBILE_BEHAVIOR = {
desktop: 'Interactive WebGL 3D digital twin',
mobile:
'Capability-based tier: WebGL when viable; SVG/2D lite fallback on low FPS / missing WebGL (not claimed as full 3D)',
} as const;
export const VALIDATION_BOARD = [ export const VALIDATION_BOARD = [
{ item: 'Unit tests', status: '196/196 PASS' }, { item: 'Web unit tests', status: '196/196 PASS' },
{ item: 'Production build', status: 'PASS' }, { item: 'Production build', status: 'PASS' },
{ item: 'Active routes / + /documentation', status: 'PASS' }, { item: 'Focused E2E (/ + /documentation)', status: 'PASS' },
{ item: 'CV compileall', status: 'PASS' },
{ item: 'CV classify/geometry tests', status: 'PASS (no camera)' },
{ item: 'Production / and /documentation', status: 'PASS' },
{ item: 'conveyor-clean.glb checksum', status: 'PASS' }, { item: 'conveyor-clean.glb checksum', status: 'PASS' },
{ item: 'Author FCStd checksum', status: 'PASS' }, { item: 'Author FCStd checksum', status: 'PASS' },
{ item: 'Frozen diverter angles / 0.50 s', status: 'PASS' }, { item: 'Classifier K > 0.8 (web + CV)', status: 'ALIGNED' },
{ item: 'Full contact physics', status: 'NOT_FULLY_VALIDATED' }, { item: 'Full contact physics', status: 'NOT_FULLY_VALIDATED' },
] as const; ] as const;
@@ -146,7 +223,14 @@ export const OFFICIAL_SOURCE_MATRIX = [
purpose: 'Classifier bounds authority cited by code', purpose: 'Classifier bounds authority cited by code',
present: true, present: true,
canonical: true, canonical: true,
usage: 'Referenced by classifier.ts; not re-parsed this pass', usage: 'Referenced by classifier.ts and cv/classify.py',
},
{
source: 'presentation/Owl_Prime_Ozon_Tech_Track_3_FINAL.pdf',
purpose: 'Final presentation (10 slides)',
present: true,
canonical: true,
usage: 'Single presentation PDF in repository',
}, },
{ {
source: 'input_info/extracted/Постановка_Задача_3_сжато_2.pdf', source: 'input_info/extracted/Постановка_Задача_3_сжато_2.pdf',
@@ -169,14 +253,24 @@ export const RUNTIME_FLOW = [
'RECEIVER', 'RECEIVER',
] as const; ] as const;
export const CURRENT_LIMITATIONS = [ export const REPOSITORY_LAYOUT = [
'Full physical contact sorting through CAD diverters is not fully validated.', 'src/ — web digital twin',
'Belt surface-velocity drive at exactly 1 m/s is planned, not complete.', 'cv/ — RealSense + OpenCV prototype',
'Per-SKU physical parameters still require profiling/calibration.', '3d_models/ — author CAD',
'Author CAD horn / transmission incomplete in active GLB.', 'public/ — runtime GLB/STL/draco',
'Official compliance is partial: missing extracted task PDF; scoring PDF not re-parsed this pass.', 'docs/ — engineering notes',
'Repository retains only the web twin, author CAD, official sources, and active tests.', 'presentation/ — final PDF',
'e2e/ + Vitest — tests',
'Docker / nginx — deployment',
] as const; ] as const;
/** @deprecated alias — historical Gate wording retained for acquisition-pack note */ export const CURRENT_LIMITATIONS = [
export const DATA_ACQUISITION_SEQUENCE = ['MEASURE', 'VERIFY', 'FREEZE', 'DESIGN'] as const; 'Engineering prototype, not an industrial-certified PAK.',
'Real CV prototype is not live-integrated into the public website.',
'Live camera mode requires Intel RealSense D415 hardware.',
'Physical stand parameters still require calibration against the digital twin.',
'Full physical contact sorting through CAD diverters is not fully validated.',
'Belt surface-velocity drive at exactly 1 m/s is not fully validated.',
'Author CAD horn / transmission incomplete in active GLB.',
'Cloud presentation/video links for the platform form are provided separately by the team.',
] as const;

View File

@@ -1,30 +1,37 @@
import AppNav from '../components/AppNav'; import AppNav from '../components/AppNav';
import { import {
ARCHITECTURE,
CAD_PROVENANCE, CAD_PROVENANCE,
CLASSIFIER_BOUNDS, CLASSIFIER_BOUNDS,
CONFIRMED_LAYOUT, CONFIRMED_LAYOUT,
CURRENT_LIMITATIONS, CURRENT_LIMITATIONS,
CV_PROTOTYPE,
DIVERTER_KINEMATICS, DIVERTER_KINEMATICS,
MOBILE_BEHAVIOR,
OFFICIAL_SOURCE_MATRIX, OFFICIAL_SOURCE_MATRIX,
PHYSICAL_STAND,
PHYSICS_STATUS, PHYSICS_STATUS,
PRODUCTION_STATUS, PRODUCTION_STATUS,
REPOSITORY_LAYOUT,
ROUTE_MAPPING, ROUTE_MAPPING,
RUNTIME_FLOW, RUNTIME_FLOW,
SOLUTION_COMPONENTS,
VALIDATION_BOARD, VALIDATION_BOARD,
} from '../data/productionStatusSummary'; } from '../data/productionStatusSummary';
const SECTIONS = [ const SECTIONS = [
{ id: 'about', title: '1. Цель проекта' }, { id: 'about', title: '1. Обзор проекта' },
{ id: 'runtime-flow', title: '2. Runtime flow' }, { id: 'production-web', title: '2. Production web' },
{ id: 'layout', title: '3. Активная линия' }, { id: 'cv', title: '3. Real CV prototype' },
{ id: 'classifier', title: '4. Классификация' }, { id: 'classifier', title: '4. Официальная классификация' },
{ id: 'routes', title: '5. Маршруты B/C/D' }, { id: 'stand', title: '5. Физический стенд' },
{ id: 'diverter', title: '6. Diverter state machine' }, { id: 'twin', title: '6. Digital twin' },
{ id: 'cad', title: '7. CAD provenance' }, { id: 'architecture', title: '7. Архитектура' },
{ id: 'physics', title: '8. Физика' }, { id: 'repository', title: '8. Репозиторий' },
{ id: 'validation', title: '9. Validation status' }, { id: 'validation', title: '9. Validation' },
{ id: 'official', title: '10. Official-source matrix' }, { id: 'limits', title: '10. Ограничения' },
{ id: 'limits', title: '11. Known limitations' }, { id: 'routes', title: '11. Маршруты B/C/D' },
{ id: 'official', title: '12. Official sources' },
] as const; ] as const;
function TocList({ onNavigate }: { onNavigate?: () => void }) { function TocList({ onNavigate }: { onNavigate?: () => void }) {
@@ -55,15 +62,15 @@ export default function DocumentationPage() {
O O
</span> </span>
<div className="product-brand-text"> <div className="product-brand-text">
<div className="product-brand-title">Ozon Tech Sorter</div> <div className="product-brand-title">OWL PRIME</div>
<div className="product-brand-sub">Цифровой двойник линии сортировки</div> <div className="product-brand-sub">Ozon Tech Track 3 документация</div>
</div> </div>
</div> </div>
<AppNav variant="solid" /> <AppNav variant="solid" />
<div className="product-telemetry docs-topbar-meta"> <div className="product-telemetry docs-topbar-meta">
<div className="product-tele-item"> <div className="product-tele-item">
<span className="product-tele-label">Раздел</span> <span className="product-tele-label">Ветка</span>
<span className="product-tele-value">Документация</span> <span className="product-tele-value">{PRODUCTION_STATUS.canonicalBranch}</span>
</div> </div>
</div> </div>
</header> </header>
@@ -75,7 +82,7 @@ export default function DocumentationPage() {
</span> </span>
<div> <div>
<h1 className="docs-title">Документация</h1> <h1 className="docs-title">Документация</h1>
<p className="docs-subtitle">Текущее состояние веб-симуляции и инженерный статус</p> <p className="docs-subtitle">Финальный инженерный статус решения OWL PRIME</p>
</div> </div>
</div> </div>
<AppNav variant="solid" /> <AppNav variant="solid" />
@@ -99,143 +106,158 @@ export default function DocumentationPage() {
<main className="docs-main"> <main className="docs-main">
<section className="docs-status-banner" data-testid="docs-production-status"> <section className="docs-status-banner" data-testid="docs-production-status">
<div className="docs-status-label">Текущий статус</div> <div className="docs-status-label">Текущий статус</div>
<div className="docs-status-value">{PRODUCTION_STATUS.acquisitionPackStatus}</div> <div className="docs-status-value">{PRODUCTION_STATUS.projectStatus}</div>
<p className="docs-status-note"> <p className="docs-status-note">
Web twin: {PRODUCTION_STATUS.webTwinStatus}. Unit tests {PRODUCTION_STATUS.unitTests}, Web: {PRODUCTION_STATUS.webTwinStatus}. CV: {PRODUCTION_STATUS.cvStatus} (live
build {PRODUCTION_STATUS.productionBuild}. Contact physics:{' '} integrated: {PRODUCTION_STATUS.cvLiveIntegrated ? 'YES' : 'NO'}). Unit tests{' '}
{PRODUCTION_STATUS.contactPhysics}. Official compliance:{' '} {PRODUCTION_STATUS.unitTests}, CV tests {PRODUCTION_STATUS.cvTests}, build{' '}
{PRODUCTION_STATUS.officialCompliance}. {PRODUCTION_STATUS.productionBuild}. Domain: {PRODUCTION_STATUS.productionUrl}. Contact
physics: {PRODUCTION_STATUS.contactPhysics}.
</p> </p>
</section> </section>
<section id="about" className="docs-section"> <section id="about" className="docs-section">
<h2>{SECTIONS[0].title}</h2> <h2>{SECTIONS[0].title}</h2>
<p> <p>
Цифровой двойник линии сортировки Ozon Tech: конвейер, камера/классификация, физическая OWL PRIME инженерный прототип предварительной сортировки товаров для Ozon Tech Track
маршрутизация и три приёмные категории (B / C / D). Страница <code>/</code> рабочая 3: веб-цифровой двойник, реальный CV-прототип, физический экспериментальный стенд,
веб-симуляция; эта страница каноническая сводка текущего состояния. авторский CAD и единые правила классификации B/C/D.
</p> </p>
<ul>
{SOLUTION_COMPONENTS.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</section> </section>
<section id="runtime-flow" className="docs-section"> <section id="production-web" className="docs-section">
<h2>{SECTIONS[1].title}</h2> <h2>{SECTIONS[1].title}</h2>
<p>{RUNTIME_FLOW.join(' → ')}</p> <p>
</section> Production: <a href={PRODUCTION_STATUS.productionUrl}>{PRODUCTION_STATUS.productionUrl}</a>
</p>
<section id="layout" className="docs-section">
<h2>{SECTIONS[2].title}</h2>
<ul> <ul>
<li> <li>
Рабочая зона (чертёж): {CONFIRMED_LAYOUT.workspaceMm.length} ×{' '} <code>/</code> непрерывная симуляция конвейера
{CONFIRMED_LAYOUT.workspaceMm.width} мм; лента {CONFIRMED_LAYOUT.conveyorWidthMm} мм /
высота верха {CONFIRMED_LAYOUT.conveyorHeightMm} мм.
</li> </li>
<li>Модули CAD: {CONFIRMED_LAYOUT.modules.join(', ')}.</li> <li>
<code>/documentation</code> эта инженерная документация
</li>
</ul>
<p>
На desktop интерактивный WebGL 3D twin: CAD-конвейер, цифровое измерение,
классификация B/C/D, CAD-дивертеры и физика Rapier. Mobile: {MOBILE_BEHAVIOR.mobile}.
</p>
<ul>
<li>
Рабочая зона: {CONFIRMED_LAYOUT.workspaceMm.length} × {CONFIRMED_LAYOUT.workspaceMm.width}{' '}
мм; лента {CONFIRMED_LAYOUT.conveyorWidthMm} мм / высота{' '}
{CONFIRMED_LAYOUT.conveyorHeightMm} мм.
</li>
<li>Модули: {CONFIRMED_LAYOUT.modules.join(', ')}.</li>
<li>Приёмники: {CONFIRMED_LAYOUT.receivers.join(', ')}.</li> <li>Приёмники: {CONFIRMED_LAYOUT.receivers.join(', ')}.</li>
<li>Runtime flow: {RUNTIME_FLOW.join(' → ')}</li>
</ul>
</section>
<section id="cv" className="docs-section">
<h2>{SECTIONS[2].title}</h2>
<p>
Path: <code>{CV_PROTOTYPE.path}</code>. Status:{' '}
<code>{CV_PROTOTYPE.status}</code>. Hardware: {CV_PROTOTYPE.hardware}. Software:{' '}
{CV_PROTOTYPE.software}.
</p>
<p>Pipeline: {CV_PROTOTYPE.pipeline.join(' → ')}</p>
<ul>
{CV_PROTOTYPE.notes.map((n) => (
<li key={n}>{n}</li>
))}
</ul> </ul>
</section> </section>
<section id="classifier" className="docs-section"> <section id="classifier" className="docs-section">
<h2>{SECTIONS[3].title}</h2> <h2>{SECTIONS[3].title}</h2>
<p> <p>
Status: <code>{CLASSIFIER_BOUNDS.status}</code> Status: <code>{CLASSIFIER_BOUNDS.status}</code>. Source:{' '}
<code>{CLASSIFIER_BOUNDS.officialSourceReference}</code>.
</p> </p>
<ul> <ul>
<li>Min: {CLASSIFIER_BOUNDS.display.min}</li> <li>Min: {CLASSIFIER_BOUNDS.display.min}</li>
<li>Max: {CLASSIFIER_BOUNDS.display.max}</li> <li>Max: {CLASSIFIER_BOUNDS.display.max}</li>
<li>Roundness: {CLASSIFIER_BOUNDS.display.roundness}</li> <li>Roundness: {CLASSIFIER_BOUNDS.display.roundness}</li>
<li>
Boundary: <strong>{CLASSIFIER_BOUNDS.display.exactBoundary}</strong>
</li>
<li>Order: {CLASSIFIER_BOUNDS.checkOrder}</li> <li>Order: {CLASSIFIER_BOUNDS.checkOrder}</li>
<li>Oversized circular product still goes to C (C priority).</li>
</ul> </ul>
<p> <p>
OFFICIAL_SOURCE_REFERENCE: <code>{CLASSIFIER_BOUNDS.officialSourceReference}</code> Web (<code>src/domain/classifier.ts</code>) and CV (<code>cv/classify.py</code>) both use
{CLASSIFIER_BOUNDS.officialSourceParsedThisPass strict <code>K &gt; 0.8</code>.
? ''
: ' — PDF not re-parsed in this documentation pass; bounds match current code/tests.'}
</p> </p>
</section> </section>
<section id="routes" className="docs-section"> <section id="stand" className="docs-section">
<h2>{SECTIONS[4].title}</h2> <h2>{SECTIONS[4].title}</h2>
<div className="docs-table-scroll"> <p>
<table className="docs-table"> Status: <code>{PHYSICAL_STAND.status}</code>. {PHYSICAL_STAND.purpose}
<thead> </p>
<tr> <ul>
<th>Category</th> {PHYSICAL_STAND.elements.map((el) => (
<th>Physical route</th> <li key={el}>{el}</li>
<th>CAD diverter</th> ))}
<th>Angle</th> </ul>
</tr>
</thead>
<tbody>
{ROUTE_MAPPING.map((row) => (
<tr key={row.category}>
<td>{row.category}</td>
<td>
<code>{row.physicalRoute}</code>
</td>
<td>{row.activeDiverter}</td>
<td>{row.signedAngleDeg}°</td>
</tr>
))}
</tbody>
</table>
</div>
</section> </section>
<section id="diverter" className="docs-section"> <section id="twin" className="docs-section">
<h2>{SECTIONS[5].title}</h2> <h2>{SECTIONS[5].title}</h2>
<p>{DIVERTER_KINEMATICS.phases.join(' → ')}</p>
<ul>
<li>Rotation duration: {DIVERTER_KINEMATICS.rotationDurationSec.toFixed(2)} s</li>
<li>Safety margin: {DIVERTER_KINEMATICS.openingSafetyMarginSec.toFixed(2)} s</li>
<li>Contact plane S: {DIVERTER_KINEMATICS.contactPlaneS}</li>
<li>Clear plane S: {DIVERTER_KINEMATICS.clearPlaneS.toFixed(4)}</li>
<li>Route command bound to productId; one active product at a time</li>
<li>Close after rear-clear of clear plane</li>
<li>
Invented sorter drive mesh active:{' '}
{DIVERTER_KINEMATICS.generatedMechanismActive ? 'YES' : 'NO'}
</li>
</ul>
</section>
<section id="cad" className="docs-section">
<h2>{SECTIONS[6].title}</h2>
<ul> <ul>
<li> <li>
Author: <code>{CAD_PROVENANCE.authorFcstd}</code> Author CAD: <code>{CAD_PROVENANCE.authorFcstd}</code>
<br />
SHA-256: <code>{CAD_PROVENANCE.authorSha256}</code>
</li> </li>
<li> <li>
Runtime: <code>{CAD_PROVENANCE.runtimeGlb}</code> Runtime conveyor: <code>{CAD_PROVENANCE.runtimeGlb}</code>
<br />
SHA-256: <code>{CAD_PROVENANCE.runtimeSha256}</code>
</li> </li>
<li>Author servo/holder nodes used in sorter module only.</li> <li>Product STL models in <code>public/models/</code></li>
<li>Invented sorter drive mesh inactive; author CAD diverters only.</li> <li>Same B/C/D classifier rules as CV</li>
<li>Horn / transmission in GLB: {CAD_PROVENANCE.hornTransmissionInGlb}.</li> <li>Rapier physics + CAD diverters (45° / +45°)</li>
<li>Synchronized digital routing into B/C/D receivers</li>
</ul> </ul>
</section> <h3>Physics</h3>
<h4>IMPLEMENTED</h4>
<section id="physics" className="docs-section">
<h2>{SECTIONS[7].title}</h2>
<h3>IMPLEMENTED</h3>
<ul> <ul>
{PHYSICS_STATUS.implemented.map((x) => ( {PHYSICS_STATUS.implemented.map((x) => (
<li key={x}>{x}</li> <li key={x}>{x}</li>
))} ))}
</ul> </ul>
<h3>NOT FULLY VALIDATED</h3> <h4>NOT FULLY VALIDATED</h4>
<ul> <ul>
{PHYSICS_STATUS.notFullyValidated.map((x) => ( {PHYSICS_STATUS.notFullyValidated.map((x) => (
<li key={x}>{x}</li> <li key={x}>{x}</li>
))} ))}
</ul> </ul>
<h3>PLANNED</h3> <p>
Diverter phases: {DIVERTER_KINEMATICS.phases.join(' → ')} (
{DIVERTER_KINEMATICS.rotationDurationSec.toFixed(2)} s rotation).
</p>
</section>
<section id="architecture" className="docs-section">
<h2>{SECTIONS[6].title}</h2>
<h3>REAL PATH</h3>
<p>{ARCHITECTURE.realPath.join(' → ')}</p>
<h3>DIGITAL PATH</h3>
<p>{ARCHITECTURE.digitalPath.join(' → ')}</p>
</section>
<section id="repository" className="docs-section">
<h2>{SECTIONS[7].title}</h2>
<p>
Canonical branch: <code>{PRODUCTION_STATUS.canonicalBranch}</code>. Other branches are
historical and not required to run the solution.
</p>
<ul> <ul>
{PHYSICS_STATUS.planned.map((x) => ( {REPOSITORY_LAYOUT.map((line) => (
<li key={x}>{x}</li> <li key={line}>{line}</li>
))} ))}
</ul> </ul>
</section> </section>
@@ -264,8 +286,45 @@ export default function DocumentationPage() {
</div> </div>
</section> </section>
<section id="official" className="docs-section"> <section id="limits" className="docs-section" data-testid="docs-blockers">
<h2>{SECTIONS[9].title}</h2> <h2>{SECTIONS[9].title}</h2>
<ul className="docs-blocker-list">
{CURRENT_LIMITATIONS.map((b) => (
<li key={b}>{b}</li>
))}
</ul>
</section>
<section id="routes" className="docs-section">
<h2>{SECTIONS[10].title}</h2>
<div className="docs-table-scroll">
<table className="docs-table">
<thead>
<tr>
<th>Category</th>
<th>Physical route</th>
<th>CAD diverter</th>
<th>Angle</th>
</tr>
</thead>
<tbody>
{ROUTE_MAPPING.map((row) => (
<tr key={row.category}>
<td>{row.category}</td>
<td>
<code>{row.physicalRoute}</code>
</td>
<td>{row.activeDiverter}</td>
<td>{row.signedAngleDeg}°</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
<section id="official" className="docs-section">
<h2>{SECTIONS[11].title}</h2>
<div className="docs-table-scroll"> <div className="docs-table-scroll">
<table className="docs-table"> <table className="docs-table">
<thead> <thead>
@@ -293,15 +352,6 @@ export default function DocumentationPage() {
</table> </table>
</div> </div>
</section> </section>
<section id="limits" className="docs-section" data-testid="docs-blockers">
<h2>{SECTIONS[10].title}</h2>
<ul className="docs-blocker-list">
{CURRENT_LIMITATIONS.map((b) => (
<li key={b}>{b}</li>
))}
</ul>
</section>
</main> </main>
</div> </div>
</div> </div>

View File

@@ -3312,17 +3312,24 @@ button:disabled {
} }
/* ========================================================= /* =========================================================
Documentation page Documentation page — light readable surface (Ozon blue)
========================================================= */ ========================================================= */
.docs-page { .docs-page {
--docs-bg: #f5f8fc;
--docs-panel: #ffffff;
--docs-text: #101828;
--docs-muted: #344054;
--docs-heading: #0b1f3a;
--docs-blue: #005bff;
--docs-magenta: #f91155;
--docs-line: #d0d5dd;
--docs-status-bg: #e8f1ff;
min-height: 100dvh; min-height: 100dvh;
max-width: 100vw; max-width: 100vw;
overflow-x: hidden; overflow-x: hidden;
background: background: var(--docs-bg);
radial-gradient(circle at 12% 0%, rgba(56, 189, 248, 0.12), transparent 32%), color: var(--docs-text);
linear-gradient(180deg, #07111d 0%, #050910 100%);
color: var(--text);
} }
.docs-header { .docs-header {
@@ -3336,9 +3343,8 @@ button:disabled {
gap: 12px 20px; gap: 12px 20px;
min-height: 56px; min-height: 56px;
padding: 14px 20px; padding: 14px 20px;
border-bottom: 1px solid var(--line); border-bottom: 1px solid var(--docs-line);
background: rgba(5, 9, 16, 0.92); background: #ffffff;
backdrop-filter: blur(12px);
} }
.docs-brand { .docs-brand {
@@ -3354,9 +3360,9 @@ button:disabled {
width: 36px; width: 36px;
height: 36px; height: 36px;
border-radius: 10px; border-radius: 10px;
background: rgba(56, 189, 248, 0.15); background: rgba(0, 91, 255, 0.1);
border: 1px solid rgba(56, 189, 248, 0.35); border: 1px solid rgba(0, 91, 255, 0.35);
color: var(--cyan); color: var(--docs-blue);
font-weight: 800; font-weight: 800;
flex: 0 0 auto; flex: 0 0 auto;
} }
@@ -3364,11 +3370,13 @@ button:disabled {
.docs-title { .docs-title {
font-size: 1.25rem; font-size: 1.25rem;
line-height: 1.2; line-height: 1.2;
color: var(--docs-heading);
} }
.docs-subtitle { .docs-subtitle {
color: var(--muted); color: var(--docs-muted);
font-size: 0.85rem; font-size: 0.85rem;
opacity: 1;
} }
.docs-layout { .docs-layout {
@@ -3385,9 +3393,9 @@ button:disabled {
top: 84px; top: 84px;
align-self: start; align-self: start;
padding: 12px; padding: 12px;
border: 1px solid var(--line); border: 1px solid var(--docs-line);
border-radius: 12px; border-radius: 12px;
background: rgba(10, 19, 31, 0.7); background: var(--docs-panel);
font-size: 0.85rem; font-size: 0.85rem;
} }
@@ -3405,16 +3413,17 @@ button:disabled {
.docs-toc-desktop a, .docs-toc-desktop a,
.docs-toc-mobile a { .docs-toc-mobile a {
color: var(--muted); color: var(--docs-muted);
text-decoration: none; text-decoration: none;
display: inline-flex; display: inline-flex;
min-height: 36px; min-height: 36px;
align-items: center; align-items: center;
opacity: 1;
} }
.docs-toc-desktop a:hover, .docs-toc-desktop a:hover,
.docs-toc-mobile a:hover { .docs-toc-mobile a:hover {
color: var(--cyan); color: var(--docs-blue);
} }
.docs-main { .docs-main {
@@ -3432,31 +3441,39 @@ button:disabled {
} }
.docs-status-banner { .docs-status-banner {
border: 1px solid rgba(74, 222, 128, 0.35); border: 1px solid rgba(0, 91, 255, 0.28);
background: rgba(20, 48, 32, 0.45); background: var(--docs-status-bg);
} }
.docs-status-label { .docs-status-label {
font-size: 0.75rem; font-size: 0.75rem;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.08em; letter-spacing: 0.08em;
color: var(--muted); color: var(--docs-blue);
opacity: 1;
} }
.docs-status-value { .docs-status-value {
margin-top: 6px; margin-top: 6px;
font-size: 1.15rem; font-size: 1.15rem;
font-weight: 700; font-weight: 700;
color: var(--green); color: var(--docs-heading);
overflow-wrap: anywhere; overflow-wrap: anywhere;
word-break: break-word; word-break: break-word;
} }
.docs-status-note { .docs-status-note {
margin-top: 10px; margin-top: 10px;
color: var(--muted); color: var(--docs-muted);
font-size: 0.92rem; font-size: 0.92rem;
line-height: 1.5; line-height: 1.5;
opacity: 1;
}
.docs-section h2,
.docs-section h3,
.docs-section h4 {
color: var(--docs-heading);
} }
.docs-section h2 { .docs-section h2 {
@@ -3464,9 +3481,19 @@ button:disabled {
margin-bottom: 10px; margin-bottom: 10px;
} }
.docs-section a {
color: var(--docs-blue);
}
.docs-section strong {
color: var(--docs-magenta);
font-weight: 700;
}
.docs-section p, .docs-section p,
.docs-section li { .docs-section li {
color: #c9d8e8; color: var(--docs-text);
opacity: 1;
font-size: 0.98rem; font-size: 0.98rem;
line-height: 1.5; line-height: 1.5;
} }
@@ -3480,19 +3507,22 @@ button:disabled {
.docs-section code { .docs-section code {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.88em; font-size: 0.88em;
color: var(--cyan-hover); color: var(--docs-blue);
background: rgba(0, 91, 255, 0.08);
padding: 0.1em 0.35em;
border-radius: 4px;
overflow-wrap: anywhere; overflow-wrap: anywhere;
word-break: break-word; word-break: break-word;
} }
.docs-conflict { .docs-conflict {
margin-top: 12px; margin-top: 12px;
border: 1px solid rgba(245, 158, 11, 0.4); border: 1px solid rgba(249, 17, 85, 0.35);
background: rgba(48, 32, 10, 0.4); background: #fff5f8;
} }
.docs-blocker-list li { .docs-blocker-list li {
color: #ffd0d5; color: var(--docs-text);
} }
.docs-table-scroll { .docs-table-scroll {
@@ -3507,20 +3537,24 @@ button:disabled {
min-width: 420px; min-width: 420px;
border-collapse: collapse; border-collapse: collapse;
font-size: 0.92rem; font-size: 0.92rem;
background: var(--docs-panel);
} }
.docs-table th, .docs-table th,
.docs-table td { .docs-table td {
padding: 8px 10px; padding: 8px 10px;
border: 1px solid var(--line); border: 1px solid var(--docs-line);
text-align: left; text-align: left;
vertical-align: top; vertical-align: top;
white-space: nowrap; white-space: normal;
color: var(--docs-text);
overflow-wrap: anywhere;
word-break: break-word;
} }
.docs-table th { .docs-table th {
background: rgba(12, 23, 37, 0.9); background: #eef4ff;
color: var(--muted); color: var(--docs-heading);
font-weight: 600; font-weight: 600;
} }
@@ -3689,8 +3723,7 @@ button:disabled {
background: #070d16; background: #070d16;
} }
.main-page--product .product-topbar, .main-page--product .product-topbar {
.docs-page .product-topbar {
position: relative; position: relative;
z-index: 30; z-index: 30;
flex: 0 0 auto; flex: 0 0 auto;
@@ -3706,6 +3739,41 @@ button:disabled {
backdrop-filter: blur(14px); backdrop-filter: blur(14px);
} }
.docs-page .product-topbar {
position: relative;
z-index: 30;
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
height: 68px;
max-height: 72px;
padding: 0 24px;
border-bottom: 1px solid var(--docs-line);
background: #ffffff;
backdrop-filter: none;
}
.docs-page .product-brand-mark {
border-color: rgba(0, 91, 255, 0.35);
background: rgba(0, 91, 255, 0.1);
color: var(--docs-blue);
}
.docs-page .product-brand-title {
color: var(--docs-heading);
}
.docs-page .product-brand-sub,
.docs-page .product-tele-label {
color: var(--docs-muted);
}
.docs-page .product-tele-value {
color: var(--docs-text);
}
.product-brand { .product-brand {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -3742,8 +3810,7 @@ button:disabled {
color: #8aa0b8; color: #8aa0b8;
} }
.main-page--product .app-nav-solid, .main-page--product .app-nav-solid {
.docs-page .product-topbar .app-nav-solid {
flex: 0 0 auto; flex: 0 0 auto;
display: inline-flex; display: inline-flex;
gap: 4px; gap: 4px;
@@ -3753,14 +3820,38 @@ button:disabled {
background: rgba(8, 16, 26, 0.7); background: rgba(8, 16, 26, 0.7);
} }
.main-page--product .app-nav-link, .docs-page .product-topbar .app-nav-solid,
.docs-page .product-topbar .app-nav-link { .docs-page .docs-header .app-nav-solid {
flex: 0 0 auto;
display: inline-flex;
gap: 4px;
padding: 3px;
border-radius: 10px;
border: 1px solid var(--docs-line);
background: #f2f6ff;
}
.main-page--product .app-nav-link {
min-height: 34px; min-height: 34px;
padding: 0 16px; padding: 0 16px;
font-size: 13px; font-size: 13px;
border-radius: 8px; border-radius: 8px;
} }
.docs-page .product-topbar .app-nav-link,
.docs-page .docs-header .app-nav-link {
min-height: 34px;
padding: 0 16px;
font-size: 13px;
border-radius: 8px;
color: var(--docs-heading);
}
.docs-page .app-nav-link.active {
background: var(--docs-blue);
color: #ffffff;
}
.product-telemetry { .product-telemetry {
display: flex; display: flex;
align-items: stretch; align-items: stretch;
@@ -3940,11 +4031,9 @@ button:disabled {
white-space: normal; white-space: normal;
} }
/* Documentation desktop grid aligned to product chrome */ /* Documentation desktop grid — keep light readable surface */
.docs-page { .docs-page {
background: background: var(--docs-bg);
radial-gradient(circle at 10% 0%, rgba(56, 189, 248, 0.1), transparent 34%),
linear-gradient(180deg, #07111d 0%, #050910 100%);
} }
.docs-layout { .docs-layout {
@@ -3960,6 +4049,8 @@ button:disabled {
top: 88px; top: 88px;
padding: 14px 12px; padding: 14px 12px;
font-size: 0.84rem; font-size: 0.84rem;
background: var(--docs-panel);
border-color: var(--docs-line);
} }
.docs-main { .docs-main {