fix: align CV rules and refresh final documentation

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
root
2026-08-02 22:51:41 +02:00
parent d701339930
commit f2ce611330
10 changed files with 462 additions and 191 deletions

View File

@@ -48,6 +48,7 @@ OWL PRIME — инженерный контур предварительной
2. Если габариты OK и **K > 0.8** (круг) → **D**.
3. Иначе → **B**.
4. **C-priority:** негабарит + круг → только **C**.
5. Граница: **K = 0.8 не круг** (→ B, не D). Одинаково в web и CV.
## Архитектура

View File

@@ -36,8 +36,8 @@ Primary modules: `main.py` (live), `demo.py` (HUD), `classify.py` (rules), `meas
## Classification rules (Track 3)
1. Dimensions must be strictly **> 10×10×10 mm** and **< 450×320×320 mm** → else **C**
2. Else if circularity exceeds the official threshold **0.8** → **D**
(web twin uses strict `K > 0.8`; this prototype uses `circle_ratio >= 0.8`)
2. Else if `circle_ratio > 0.8` → **D**
(`K == 0.8` is **not** circular — same strict rule as web `classifier.ts`)
3. Else → **B**
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 (приоритет над кругом)
2) если r_in/r_out >= 0.8 в любом сечении → D
2) если r_in/r_out > 0.8 в любом сечении → D
(K == 0.8 НЕ круг — как в web classifier.ts)
3) иначе → B
"""
dims = _sorted_dims(measurement.length_mm, measurement.width_mm, measurement.height_mm)
passes = check_size(dims, min_mm, max_mm)
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))
if not passes or clipped:
@@ -112,7 +113,7 @@ def classify(
circle_ratio=ratio,
passes_size=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(
@@ -121,7 +122,7 @@ def classify(
circle_ratio=ratio,
passes_size=True,
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)
sec_m = _median(self._secs)
ratio_p75 = _percentile(self._ratios, 75)
# устойчивый круг: медиана >= 0.8 ИЛИ (медиана сечений >= 0.8 и ≥ половины окна сильные)
# устойчивый круг: медиана > 0.8 ИЛИ (медиана сечений > 0.8 и ≥ половины окна сильные)
# K == 0.8 официально НЕ круг (строгое > threshold)
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 (
sec_m >= self.enter_circle and sec_strong >= 0.55
circular = ratio > self.enter_circle or (
sec_m > self.enter_circle and sec_strong >= 0.55
)
ratio_show = max(ratio, sec_m) if circular else ratio
@@ -153,7 +154,7 @@ class DecisionStabilizer:
reason=(
f"круг: med={ratio:.3f} p75={ratio_p75:.3f} "
f"top={top_m:.3f} sec={sec_m:.3f} strong={sec_strong:.0%} "
f">= {self.enter_circle}"
f"> {self.enter_circle}"
),
)
else:
@@ -165,7 +166,7 @@ class DecisionStabilizer:
is_circular=False,
reason=(
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"
def test_border_circle():
r = classify_from_dims(100, 50, 50, circle_ratio=0.8)
def test_strict_circle_boundary():
"""Официально: круг только при 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
r2 = classify_from_dims(100, 50, 50, circle_ratio=0.799)
assert r2.category == Category.SUITABLE
assert r.category.zone == "D"
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():
@@ -80,7 +109,10 @@ if __name__ == "__main__":
test_too_small()
test_need_pack_cylinder()
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_dims_order_independent()
print("OK: all classification tests passed")

View File

@@ -20,7 +20,7 @@ Companion to `README.md` and `/documentation`. Not a stage changelog.
| `OPENING_SAFETY_MARGIN_SEC` | 0.15 | same |
| Contact / clear planes | ≈1.0538 / 1.6000 | `buildDiverterPlanes` + mount hinge |
| Classifier min/max | exclusive 10³ / 450×320×320 | `src/domain/classifier.ts` |
| Roundness | K > 0.8 (web); CV code uses ≥ 0.8 | `classifier.ts` / `cv/classify.py` |
| 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` |
@@ -73,7 +73,7 @@ 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) |
| 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 `/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 |
| Presentation | platform rules | `presentation/Owl_Prime_Ozon_Tech_Track_3_FINAL.pdf` | 10-page PDF in repo | PASS (file present) |

View File

@@ -10,7 +10,10 @@ test.describe('routes', () => {
await page.getByTestId('nav-documentation').first().click();
await expect(page).toHaveURL(/\/documentation\/?$/);
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 page.reload();

View File

@@ -4,16 +4,29 @@
* Unverified / planned items are labeled explicitly — never implied complete.
*/
/** Kept for e2e (`docs-production-status`) and acquisition-pack provenance. */
export const PRODUCTION_STATUS = {
acquisitionPackStatus: 'DATA_ACQUISITION_PACK_READY',
webTwinStatus: 'BASELINE_PRESERVED',
projectStatus: 'FINAL_ENGINEERING_PROTOTYPE_READY',
webTwinStatus: 'PRODUCTION_LIVE',
cvStatus: 'WORKING_PROTOTYPE',
cvLiveIntegrated: false,
unitTests: '196/196',
cvTests: 'PASS (no camera)',
productionBuild: 'PASS',
productionUrl: 'https://arhipovdan.ru',
contactPhysics: 'NOT_FULLY_VALIDATED',
officialCompliance: 'PARTIAL_SOURCES_PRESENT',
canonicalBranch: 'main',
} 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 = {
workspaceMm: { length: 10000, width: 6000 },
conveyorWidthMm: 500,
@@ -23,7 +36,7 @@ export const CONFIRMED_LAYOUT = {
} as const;
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',
officialSourceParsedThisPass: false,
minExclusiveMm: { width: 10, depth: 10, height: 10 },
@@ -33,8 +46,63 @@ export const CLASSIFIER_BOUNDS = {
min: '> 10×10×10 мм',
max: '< 450×320×320 мм',
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;
export const ROUTE_MAPPING = [
@@ -72,6 +140,7 @@ export const PHYSICS_STATUS = {
'Synchronized CAD diverter visual / kinematic targets',
'CCD enabled for light/thin SKUs in runtime and headless sim',
'Visual/physics spawn gating via product asset preload',
'Belt speed target 1.0 m/s; physics timestep 1/60 s',
],
notFullyValidated: [
'Complete contact-only routing through CAD diverters',
@@ -83,18 +152,26 @@ export const PHYSICS_STATUS = {
planned: [
'Visual full belt loop with surface-velocity coupling',
'Controlled tangential friction at 1 m/s',
'Dynamic rigid bodies for divert segment with fixed timestep',
'Per-SKU collider, damping, and friction profiles',
],
} 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 = [
{ item: 'Unit tests', status: '196/196 PASS' },
{ item: 'Web unit tests', status: '196/196 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: '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' },
] as const;
@@ -146,7 +223,14 @@ export const OFFICIAL_SOURCE_MATRIX = [
purpose: 'Classifier bounds authority cited by code',
present: 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',
@@ -169,15 +253,24 @@ export const RUNTIME_FLOW = [
'RECEIVER',
] as const;
export const CURRENT_LIMITATIONS = [
'Full physical contact sorting through CAD diverters is not fully validated.',
'Belt surface-velocity drive at exactly 1 m/s is planned, not complete.',
'Per-SKU physical parameters still require profiling/calibration.',
'Author CAD horn / transmission incomplete in active GLB.',
'Official compliance is partial: missing extracted task PDF; scoring PDF not re-parsed this pass.',
'Real CV prototype lives in cv/ (WORKING_PROTOTYPE) and is not connected to the live website.',
'Live CV validation requires Intel RealSense D415 hardware.',
export const REPOSITORY_LAYOUT = [
'src/ — web digital twin',
'cv/ — RealSense + OpenCV prototype',
'3d_models/ — author CAD',
'public/ — runtime GLB/STL/draco',
'docs/ — engineering notes',
'presentation/ — final PDF',
'e2e/ + Vitest — tests',
'Docker / nginx — deployment',
] as const;
/** @deprecated alias — historical Gate wording retained for acquisition-pack note */
export const DATA_ACQUISITION_SEQUENCE = ['MEASURE', 'VERIFY', 'FREEZE', 'DESIGN'] as const;
export const CURRENT_LIMITATIONS = [
'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 {
ARCHITECTURE,
CAD_PROVENANCE,
CLASSIFIER_BOUNDS,
CONFIRMED_LAYOUT,
CURRENT_LIMITATIONS,
CV_PROTOTYPE,
DIVERTER_KINEMATICS,
MOBILE_BEHAVIOR,
OFFICIAL_SOURCE_MATRIX,
PHYSICAL_STAND,
PHYSICS_STATUS,
PRODUCTION_STATUS,
REPOSITORY_LAYOUT,
ROUTE_MAPPING,
RUNTIME_FLOW,
SOLUTION_COMPONENTS,
VALIDATION_BOARD,
} from '../data/productionStatusSummary';
const SECTIONS = [
{ id: 'about', title: '1. Цель проекта' },
{ id: 'runtime-flow', title: '2. Runtime flow' },
{ id: 'layout', title: '3. Активная линия' },
{ id: 'classifier', title: '4. Классификация' },
{ id: 'routes', title: '5. Маршруты B/C/D' },
{ id: 'diverter', title: '6. Diverter state machine' },
{ id: 'cad', title: '7. CAD provenance' },
{ id: 'physics', title: '8. Физика' },
{ id: 'validation', title: '9. Validation status' },
{ id: 'official', title: '10. Official-source matrix' },
{ id: 'limits', title: '11. Known limitations' },
{ id: 'about', title: '1. Обзор проекта' },
{ id: 'production-web', title: '2. Production web' },
{ id: 'cv', title: '3. Real CV prototype' },
{ id: 'classifier', title: '4. Официальная классификация' },
{ id: 'stand', title: '5. Физический стенд' },
{ id: 'twin', title: '6. Digital twin' },
{ id: 'architecture', title: '7. Архитектура' },
{ id: 'repository', title: '8. Репозиторий' },
{ id: 'validation', title: '9. Validation' },
{ id: 'limits', title: '10. Ограничения' },
{ id: 'routes', title: '11. Маршруты B/C/D' },
{ id: 'official', title: '12. Official sources' },
] as const;
function TocList({ onNavigate }: { onNavigate?: () => void }) {
@@ -55,15 +62,15 @@ export default function DocumentationPage() {
O
</span>
<div className="product-brand-text">
<div className="product-brand-title">Ozon Tech Sorter</div>
<div className="product-brand-sub">Цифровой двойник линии сортировки</div>
<div className="product-brand-title">OWL PRIME</div>
<div className="product-brand-sub">Ozon Tech Track 3 — документация</div>
</div>
</div>
<AppNav variant="solid" />
<div className="product-telemetry docs-topbar-meta">
<div className="product-tele-item">
<span className="product-tele-label">Раздел</span>
<span className="product-tele-value">Документация</span>
<span className="product-tele-label">Ветка</span>
<span className="product-tele-value">{PRODUCTION_STATUS.canonicalBranch}</span>
</div>
</div>
</header>
@@ -75,7 +82,7 @@ export default function DocumentationPage() {
</span>
<div>
<h1 className="docs-title">Документация</h1>
<p className="docs-subtitle">Текущее состояние веб-симуляции и инженерный статус</p>
<p className="docs-subtitle">Финальный инженерный статус решения OWL PRIME</p>
</div>
</div>
<AppNav variant="solid" />
@@ -99,144 +106,158 @@ export default function DocumentationPage() {
<main className="docs-main">
<section className="docs-status-banner" data-testid="docs-production-status">
<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">
Web twin: {PRODUCTION_STATUS.webTwinStatus}. Unit tests {PRODUCTION_STATUS.unitTests},
build {PRODUCTION_STATUS.productionBuild}. Contact physics:{' '}
{PRODUCTION_STATUS.contactPhysics}. Official compliance:{' '}
{PRODUCTION_STATUS.officialCompliance}.
Web: {PRODUCTION_STATUS.webTwinStatus}. CV: {PRODUCTION_STATUS.cvStatus} (live
integrated: {PRODUCTION_STATUS.cvLiveIntegrated ? 'YES' : 'NO'}). Unit tests{' '}
{PRODUCTION_STATUS.unitTests}, CV tests {PRODUCTION_STATUS.cvTests}, build{' '}
{PRODUCTION_STATUS.productionBuild}. Domain: {PRODUCTION_STATUS.productionUrl}. Contact
physics: {PRODUCTION_STATUS.contactPhysics}.
</p>
</section>
<section id="about" className="docs-section">
<h2>{SECTIONS[0].title}</h2>
<p>
Цифровой двойник линии сортировки Ozon Tech: конвейер, камера/классификация, физическая
маршрутизация и три приёмные категории (B / C / D). Страница <code>/</code> — рабочая
веб-симуляция; эта страница — каноническая сводка текущего состояния. Реальный
CV-прототип (RealSense D415) лежит в <code>cv/</code> и не подключён к live-сайту.
OWL PRIME — инженерный прототип предварительной сортировки товаров для Ozon Tech Track
3: веб-цифровой двойник, реальный CV-прототип, физический экспериментальный стенд,
авторский CAD и единые правила классификации B/C/D.
</p>
<ul>
{SOLUTION_COMPONENTS.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</section>
<section id="runtime-flow" className="docs-section">
<section id="production-web" className="docs-section">
<h2>{SECTIONS[1].title}</h2>
<p>{RUNTIME_FLOW.join(' → ')}</p>
</section>
<section id="layout" className="docs-section">
<h2>{SECTIONS[2].title}</h2>
<p>
Production: <a href={PRODUCTION_STATUS.productionUrl}>{PRODUCTION_STATUS.productionUrl}</a>
</p>
<ul>
<li>
Рабочая зона (чертёж): {CONFIRMED_LAYOUT.workspaceMm.length} ×{' '}
{CONFIRMED_LAYOUT.workspaceMm.width} мм; лента {CONFIRMED_LAYOUT.conveyorWidthMm} мм /
высота верха {CONFIRMED_LAYOUT.conveyorHeightMm} мм.
<code>/</code> — непрерывная симуляция конвейера
</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>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>
</section>
<section id="classifier" className="docs-section">
<h2>{SECTIONS[3].title}</h2>
<p>
Status: <code>{CLASSIFIER_BOUNDS.status}</code>
Status: <code>{CLASSIFIER_BOUNDS.status}</code>. Source:{' '}
<code>{CLASSIFIER_BOUNDS.officialSourceReference}</code>.
</p>
<ul>
<li>Min: {CLASSIFIER_BOUNDS.display.min}</li>
<li>Max: {CLASSIFIER_BOUNDS.display.max}</li>
<li>Roundness: {CLASSIFIER_BOUNDS.display.roundness}</li>
<li>
Boundary: <strong>{CLASSIFIER_BOUNDS.display.exactBoundary}</strong>
</li>
<li>Order: {CLASSIFIER_BOUNDS.checkOrder}</li>
<li>Oversized circular product still goes to C (C priority).</li>
</ul>
<p>
OFFICIAL_SOURCE_REFERENCE: <code>{CLASSIFIER_BOUNDS.officialSourceReference}</code>
{CLASSIFIER_BOUNDS.officialSourceParsedThisPass
? ''
: ' — PDF not re-parsed in this documentation pass; bounds match current code/tests.'}
Web (<code>src/domain/classifier.ts</code>) and CV (<code>cv/classify.py</code>) both use
strict <code>K &gt; 0.8</code>.
</p>
</section>
<section id="routes" className="docs-section">
<section id="stand" className="docs-section">
<h2>{SECTIONS[4].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>
<p>
Status: <code>{PHYSICAL_STAND.status}</code>. {PHYSICAL_STAND.purpose}
</p>
<ul>
{PHYSICAL_STAND.elements.map((el) => (
<li key={el}>{el}</li>
))}
</ul>
</section>
<section id="diverter" className="docs-section">
<section id="twin" className="docs-section">
<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>
<li>
Author: <code>{CAD_PROVENANCE.authorFcstd}</code>
<br />
SHA-256: <code>{CAD_PROVENANCE.authorSha256}</code>
Author CAD: <code>{CAD_PROVENANCE.authorFcstd}</code>
</li>
<li>
Runtime: <code>{CAD_PROVENANCE.runtimeGlb}</code>
<br />
SHA-256: <code>{CAD_PROVENANCE.runtimeSha256}</code>
Runtime conveyor: <code>{CAD_PROVENANCE.runtimeGlb}</code>
</li>
<li>Author servo/holder nodes used in sorter module only.</li>
<li>Invented sorter drive mesh inactive; author CAD diverters only.</li>
<li>Horn / transmission in GLB: {CAD_PROVENANCE.hornTransmissionInGlb}.</li>
<li>Product STL models in <code>public/models/</code></li>
<li>Same B/C/D classifier rules as CV</li>
<li>Rapier physics + CAD diverters (−45° / +45°)</li>
<li>Synchronized digital routing into B/C/D receivers</li>
</ul>
</section>
<section id="physics" className="docs-section">
<h2>{SECTIONS[7].title}</h2>
<h3>IMPLEMENTED</h3>
<h3>Physics</h3>
<h4>IMPLEMENTED</h4>
<ul>
{PHYSICS_STATUS.implemented.map((x) => (
<li key={x}>{x}</li>
))}
</ul>
<h3>NOT FULLY VALIDATED</h3>
<h4>NOT FULLY VALIDATED</h4>
<ul>
{PHYSICS_STATUS.notFullyValidated.map((x) => (
<li key={x}>{x}</li>
))}
</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>
{PHYSICS_STATUS.planned.map((x) => (
<li key={x}>{x}</li>
{REPOSITORY_LAYOUT.map((line) => (
<li key={line}>{line}</li>
))}
</ul>
</section>
@@ -265,8 +286,45 @@ export default function DocumentationPage() {
</div>
</section>
<section id="official" className="docs-section">
<section id="limits" className="docs-section" data-testid="docs-blockers">
<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">
<table className="docs-table">
<thead>
@@ -294,15 +352,6 @@ export default function DocumentationPage() {
</table>
</div>
</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>
</div>
</div>

View File

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