docs(studio-3d): 阶段 6 交付说明(正确性与失效状态)+ 接口骨架同步

- What:
  1. `docs/architecture/studio-3d-stages.md` 新增「阶段 6:运行正确性与失效状态(批次 A)—— 已完成」:
     做(epoch/字段级来源/设备几何身份/加载请求号/工具与重编包)、验收表格(含两份探针的
     before/after 与全量计数)、"这一阶段抓到的" 7 条、兼容说明 6 条;文首进度改成阶段 6 已完成、
     7/8/9 未开始;依赖关系图扩展为 6→7/8→9。
  2. `docs/architecture/studio-3d-contracts.pyi` 与本阶段的契约改动同步:`SceneState` 加 `'stale'`;
     `SurfaceFieldFrame`/`RenderFrame` 加 `computed_frame_id`;`ExpectedGeometry` 加 `source_geometry_id`
     并注明米制 id 何时为空;`SceneAssets` 加 `source_geometry_id`/`compatible_sensor_model`/
     `compatible_hardware_revisions`;`ThreeDCoordinator.__init__` 加 `clock`/`stale_grace_s`,新增
     `note_field_failure()`/`show_rest()`/`epoch`/`last_presented()`。
- Why: 规格 §13 要求每阶段同时交出实现、测试、输入样例、失败样例、兼容说明与平台诊断报告;阶段 6 的
  契约改动(`SceneState`、两个 `computed_frame_id`、`ExpectedGeometry`/`SceneAssets` 的新身份字段、
  协调器新参数)必须先落到接口骨架里,否则下一阶段会照着过期文档实现。审查报告本身不修改
  (`_review_scene3d_20260914/` 保持原样),结论与反号证据写进本仓文档。
- Verification:
  · 阶段末全量复核(与本提交同一工作树):apps/studio 234 项 0 失败(14 跳过 = offscreen 下的真 GL 用例,
    用 `QT_QPA_PLATFORM=windows` 单独跑为 14 项 0 失败 0 跳过);shared 316 项 0 失败 1 跳过;
    `ui_check` 587 项 0 失败;`locale_check` 0 缺失(zh/en 各 398 键,仅有键集合相同);
    `validate_scene.py` 对重编发布包 `source geometry recorded 46a03a5fbac1eace` 等全项 ok。
  · 复现证据:`.repro/probe_stage6_r1.py`(R1/R7-b 反号)、`.repro/probe_stage6_device.py`
    (R3:同尺寸不同映射被拒、表面不同被拒、设备不声明 → unverified),输出 JSON 与扫描件留在 `.repro/`。
- Not in this change: 不改代码(本提交只动文档与接口骨架);阶段 7/8/9 的计划条目(二维后端统一、
  回放入口收口、能力控件、限速与真实统计、保护包密钥注入、ARM 与 2 小时门禁)尚未开始;
  真机上"算法链失败时 `VRAW` 是否也过期"这一条仍待实测(风险项已写进阶段 6 小节)。
This commit is contained in:
ykcheungab
2026-09-14 21:13:57 +08:00
parent 3c0424b90c
commit bf643de93d
2 changed files with 136 additions and 10 deletions
+16 -4
View File
@@ -8,7 +8,7 @@ by NDArray. See studio-3d-design.md for threading and validation requirements.
"""
from dataclasses import dataclass
from pathlib import Path
from typing import Generic, Literal, Mapping, Protocol, TypeAlias, TypeVar
from typing import Callable, Generic, Literal, Mapping, Protocol, TypeAlias, TypeVar
from concurrent.futures import Executor
import numpy as np
@@ -28,7 +28,7 @@ Role: TypeAlias = Literal['sensing', 'housing', 'reference']
Quality: TypeAlias = Literal['calibrated', 'approximate', 'display_only']
Profile: TypeAlias = Literal['desktop33', 'gles30']
OfferResult: TypeAlias = Literal['accepted', 'replaced', 'stale', 'disabled', 'invalid']
SceneState: TypeAlias = Literal['disabled', 'loading', 'ready', 'live', 'no_data', 'suspended', 'unavailable']
SceneState: TypeAlias = Literal['disabled', 'loading', 'ready', 'live', 'no_data', 'suspended', 'unavailable', 'stale']
@dataclass(frozen=True)
@@ -75,6 +75,7 @@ class SurfaceFieldFrame:
displacement_sensor_m: F32 | None # [H,W,3]; base displacement BEFORE display gain.
displacement_quality: Quality | None
diagnostic: Diagnostic | None
computed_frame_id: int | None # Frame the array was computed for; None = unstated.
@dataclass(frozen=True)
@@ -118,9 +119,10 @@ class CameraPreset:
class ExpectedGeometry:
sensor_model: str
hardware_revision: str
geometry_id: str
geometry_id: str # Metre-scaled shape identity; '' when the device cannot state it.
field_domain_id: str
shape_hw: tuple[int, int]
source_geometry_id: str # Unit-free surface identity a device CAN reproduce.
@dataclass(frozen=True)
@@ -133,6 +135,9 @@ class SceneAssets:
binding: SurfaceBinding # v1 supports exactly one sensing part.
camera_presets: tuple[CameraPreset, ...]
bounds_sensor_m: F32 # [2,3]: min, max.
source_geometry_id: str # Unit-free source geometry this asset was built from.
compatible_sensor_model: str
compatible_hardware_revisions: tuple[str, ...] # '*' means any revision.
@dataclass(frozen=True)
@@ -172,6 +177,7 @@ class RenderFrame:
valid: Mask # [N].
scalar_descriptor: FieldDescriptor
diagnostics: tuple[Diagnostic, ...]
computed_frame_id: int | None # Carried through so the display side re-checks it.
@dataclass(frozen=True)
@@ -345,17 +351,23 @@ class Sensor3DWidget(QOpenGLWidget):
class ThreeDCoordinator:
def __init__(self, widget: Sensor3DWidget, composer: SceneComposer,
executor: Executor, max_fps: int = 30) -> None: ...
executor: Executor, max_fps: int = 30, *,
clock: Callable[[], float] = ..., stale_grace_s: float = 1.0) -> None: ...
def activate(self, assets: SceneAssets, source_id: str) -> int: ...
def submit(self, frame: SurfaceFieldFrame) -> OfferResult: ...
def note_field_failure(self, item: Diagnostic | None = None) -> None: ...
def tick(self) -> None: ...
def set_visible(self, visible: bool) -> None: ...
def set_settings(self, settings: DisplaySettings) -> None: ...
def show_rest(self) -> None: ...
def close(self) -> None: ...
@property
def state(self) -> SceneState: ...
@property
def generation(self) -> int: ...
@property
def epoch(self) -> int: ...
def last_presented(self) -> tuple[FrameIdentity, float] | None: ...
class ThreeDPanel(QWidget):