# 40: 全程质量收口(内部换级 / 降级 / 性能 / 证据) **What to build:** 把前三票的「方案通过」收成「生产可用」:确认矩形内部换级与各降级路径的真实表现,拿到性能与走查证据,回写偏差,并把可丢弃原型归档。 Blocked by: 39 Status: claimed Labels: ready-for-agent ## 契约来源 - 规格:`cockpit-primary-area-guardrails/spec.md`、`spec-fog.md`。 - 决策:`cockpit-primary-area-guardrails/issues/07-fog-implementation-spec.md`、`08-coarse-to-fine-transition-solution.md`(均已 resolved)。 - 迷雾清单:`cockpit-primary-area-guardrails/map.md` 的「Not yet specified」两条。 ## 验收 - [ ] **矩形内部 LOD 换级**:真机确认是否肉眼可辨;若可辨,在「不接管 Cesium 场景、不引第二渲染器」的边界内评估局部过渡或 shader 羽化;预烘焙中精度影像层仅作**更高成本兜底**。 - [ ] **异常降级四条路径**各有可复现证据且提示如实(不宣称已遮缝):① 无深度能力;② 自建雾运行失败;③ 目标瓦片失败;④ 主场景区未配置(关闭限位与雾)。 - [ ] **管线视图**:近处管线可读;若因透明深度错位减弱/暂关雾,记录实际处置与提示文案。 - [ ] **性能**:同机 / 同分辨率 / 同视角 / 相同加载条件下,取**无原型基线**与开启雾后的重复采样 P95 帧耗时,合计增幅 **≤15%**;首次启用与进出切换的卡顿单独记录。读数口径写清(渲染事件间隔 ≠ GPU 时间)。 - [ ] **走查证据**:进入 → 锁定 → 拉远被夹 → 位于边界回弹 → 返回全球 → 闲置回引导;并覆盖浅俯角与多个旋转方向。 - [ ] **规格同步**:实现与 `spec.md` / `spec-fog.md` 的偏差逐条回写票面;改口径需维护者确认,不静默漂移。 - [ ] **原型归档**:可丢弃原型与 `/cockpit` 入口的 DEV 接线单独保存到 throwaway 分支,并在本票留分支指针;`/cockpit` 的 `pages` 入口在生产路径下不再挂原型。 - [ ] **运维自检**:`docs/cockpit-deployment.md` 增一节「限位与雾气是否生效」(或明确记为遗留,写明原因)。 - [ ] 收口时区分三类结论:**已验证**(有证据)、**已知代价**(明确接受,例如降级时远景保护减弱)、**遗留**(列明下一步),不得把原型观感认可写成生产达标。 ## 进度:本轮已修「回引导态」缺陷、定夺闲置时长、归档原型;验收 9 条里 4 条需真机(故不 close) ### 一、维护者新报缺陷(2026-09-30):闲置回引导态"看不出来" 原话: > 退出到地球视图后,在重新回到引导状态时,摄像机角度没有回到引导状态,且地球也没有重新开始旋转, > 除了中心出现「点击进入系统」外用户不一定能意识到进入引导状态了。 **这不是新需求,是一次实现漂移** —— ADR 0009 第 27 行早已写死: 「全球态闲置 5 秒未操作 → 回到程序初始态:**地球自转恢复** + 屏幕中央闪烁『点击进入系统』」。 所以它同时踩了两条既有契约:票 37 验收第 3 条、以及上面这句 ADR。 **两个根因**(都在代码里坐实,不是猜): 1. **相机没回初始全景。** `CockpitShell.vue` 的 `returnToIntro()` 只做了两件事 —— `introVisible.value = true` 与 `setGlobeSpin(...)`,**没有把相机送回去**。 于是画面停在用户最后停留的视角上,"回初始态"就只剩中央那行字。 2. **自转起不来,而且失败被伪装成成功。** `mapService` 用了粘滞开关 `instance.spinPaused`: `goHome()` 与 `returnGlobal` 把它置 `true`,而 `setGlobeSpin(>0)` 命中它时 `return true` —— **代码里没有任何地方会把它清回 `false`**。于是回引导态那次起转被静默吞掉, 且调用方(外壳挂载时的 600 ms 重试)以为已经成功、不再重试。 **修法**: - **相机侧**:新增 `mapService.resetToIntroView()`,把相机送回 `INITIAL_PANORAMA`(`0/−60/2×10⁷`), 中心沿用当前位置(与「返回全球」同一条口径);`returnToIntro()` 改成 「显示引导层 + 回初始全景 + 恢复自转」三件事一起做。 **已经在初始全景上就不排飞行** —— 否则自转要白等一个飞行时长才起得来。 - **自转侧**:**删掉粘滞开关**,把"意图"与"正在转的循环"分开 —— 意图 `spinWanted`(想要几秒一圈)、循环 `instance.spin`。 `setGlobeSpin()` 先记意图;"现在能不能转"按**相机是否真的停稳**(`state.settled`)判, 不能转就如实 `return false`(不再假装成功);等相机落定后由 `resumeSpinIfWanted()` 自动接上 (挂在 `notifyCameraStateChange`,飞行最后一帧必到)。 `stopSpin()` 负责**放弃意图**(进入系统/归位/返回全球都不该再自己转起来), `startSpin()` 只停循环、不抹掉意图。 - 读数加 `spinWanted`:只报 `spin` 分不清「意图立着但相机还没落定」与「压根没想转」。 **证据**: - 新闸门 `verify/t40-intro-return.mjs`(静态守卫 + 一处纯行为断言):禁止 `spinPaused` 复活、 断言"记下意图之后不得 `return true`"、断言 `stopSpin` 放弃意图而 `startSpin` 只停循环、 断言落定后兑现意图、断言 `returnToIntro` 三件事齐备、断言 `resetToIntroView` 套用初始全景 且已在其中时不排飞行、断言阈值复用 `SETTLE_EPSILON`、断言读数带 `spinWanted`。 - **定向变异 10/10 全部打红** —— 把两个原缺陷的形状各自复现一遍都抓得住: `return true` 假装成功、`spinPaused` 复活、`startSpin` 误用 `stopSpin`、`stopSpin` 不放弃意图、 落定后不兑现、回引导态不送相机、不套用初始全景、去掉"已在其中"短路、改用魔数、读数去掉 `spinWanted`。 - 部署手册第 5 节补一行「闲置回引导」自检(含读哪两个读数);顺手修掉该表一处**属性名写错** (`data-fog-phase` → `data-map-fog-phase`;实际写的是 `host.dataset.mapFogPhase`)。 - 全量闸门 **33 通过 / 1 失败**,唯一失败 `t04-config` 与本票无关(并发会话的 `cockpitApi.js` 硬编码后端地址,见票 07 方向);t37 已随「契约改 10 秒」转绿。 ### 二、诊断中发现的**规格漂移**(2026-09-30 已由维护者定夺:**改为 10 秒并同步契约**) `domain/areaLimits.js` 的实现值原先是 10000;本轮并发写入期间一度被改成 2000,导致 t37 立即失败。维护者已经明确选择「我要 10 秒:把契约一起改」,因此我把实现**恢复为 10000**,并同步更新了五处权威: ADR 0009 / 票 37 验收 / 部署手册 / 常量注释 / 闸门 t37(含 walkthrough 的等待时长)。 现在实现与契约重新一致,t37 已转绿。 ### 三、逐条验收状态(本票**不能 close**,原因在表里) | # | 验收项 | 状态 | | --- | --- | --- | | 1 | 矩形内部 LOD 换级真机确认;可辨则在不接管场景/不引第二渲染器边界内评估 | **遗留** —— 观感判据,需真机(本会话工具清单里没有浏览器工具) | | 2 | 异常降级四条路径各有可复现证据且提示如实 | **部分** —— 逻辑面已覆盖(① 无深度能力 / ② 自建雾失败 → `t38-fog`;③ 目标瓦片失败 → `t39-handover`;④ 主场景区未配置 → `t36`/`t37`);**真机逐条复现**仍缺 | | 3 | 管线视图:近处管线可读;若因透明深度错位减弱/暂关雾,记录处置与文案 | **遗留** —— 需真机 | | 4 | 性能:无原型基线与开雾后 P95 帧耗时,合计增幅 ≤15%;首启与进出切换卡顿单独记录 | **遗留** —— 需真机同机/同分辨率/同视角采样。(票 38 曾测得开/关雾 P95 增幅 0%,但那是 CPU 侧口径、且**不是**本票要求的"无原型基线",不能顶替) | | 5 | 走查证据:进入 → 锁定 → 拉远被夹 → 边界回弹 → 返回全球 → 闲置回引导;覆盖浅俯角与多旋转方向 | **部分** —— 末段「闲置回引导」的**功能缺陷本轮已修**并有闸门+变异;链路其余段由 `t37-area-limits-walkthrough` 脚本覆盖。**真机逐帧观感与"多方向"仍待维护者** | | 6 | 规格同步:实现与 spec 的偏差逐条回写;改口径需确认 | **部分(已回写两处,均经维护者确认或记录)** —— ① 票 39 的部署值 vs 代码默认(有意分离,已记录);② `GLOBAL_IDLE_MS` 5秒→10秒(**维护者 2026-09-30 确认**,契约五处已同步,见上) | | 7 | 原型归档到 throwaway 分支 + 留分支指针 + 生产路径不挂原型 | **已完成(2026-09-30,维护者确认"现在就做")** —— 归档分支 `throwaway/cockpit-prototypes` @ `81932a0`;生产树已删 `src/modules/cockpit/prototypes/` 并摘除 `/cockpit` 入口接线(见下) | | 8 | 运维自检:手册增一节「限位与雾气是否生效」 | **已完成** —— 手册第 5 节「部署后自检」已含主场景区/进区限位/区内视距雾/雾的高度收口/雾不"一瞬间"散掉/粗→精交接/先收后撤全表;本轮补「闲置回引导」一行并修一处属性名错误 | | 9 | 收口时区分**已验证 / 已知代价 / 遗留** | **进行中** —— 见上表与下方"已知代价" | ### 四、已知代价(明确接受,不当作达标) - 交接 `requireTilesLoaded: false` 牺牲的正是决策 08 设它要防的"先空白再跳方块"(票 39 记录)。 - 退出时「相机已高于雾的全遮蔽端点而雾仍可见」的窗口 ≈ 387 ms(票 38/39 记录)。 ### 五、验收第 7 条(原型归档)— **已完成**(2026-09-30,维护者确认"现在就做") 归档用 git plumbing(临时 index)完成,**没有动工作树里的并发在途改动**: - **归档分支**:`throwaway/cockpit-prototypes` @ `81932a0` `(git log -1 该分支查看 commit:'归档:可丢弃原型(Transition/Fog/Anchor)与 /cockpit DEV 接线(票 40 收口)')` - 归档内容(7 个原型文件 + 带 DEV 接线的 `src/pages/cockpit/index.vue`): `anchor-prototype-effects.js`、`anchor-prototype-layout.js`、`AnchorPrototype.vue`、 `fog-prototype-stage.js`、`FogPrototype.vue`、`transition-prototype-scene.js`、`TransitionPrototype.vue` - **生产路径**:已删除 `src/modules/cockpit/prototypes/`;`src/pages/cockpit/index.vue` 已摘除 三组 `defineAsyncComponent` / `show*Prototype` 接线(现在只挂 `CockpitShell`,全仓再无 `cockpit/prototypes` 引用)。 - **波及面已收**:`t39-handover.mjs` 原先读 `TransitionPrototype.vue` 的两条断言改为 断言"生产树已无 `prototypes/` 目录";服务层的 alpha 互斥开关(`setHandoverPrototypeActive` / `applyHandoverAlpha` 内 `if (handoverPrototypeActive) return`)**保留** —— 一旦日后从归档分支还原原型, 互斥仍成立。`cesiumEngine.js` 两处提到 `prototypes/fog-prototype-stage.js` 的注释已标注"已归档分支"。 - 还原方法:`git checkout throwaway/cockpit-prototypes -- src/modules/cockpit/prototypes src/pages/cockpit/index.vue` ### 六、下一步 1. 真机四项(LOD 换级 / 管线视图 / 性能 P95 / 走查逐帧)需要可用的浏览器环境; 本轮本会话没有浏览器工具,只能给出逻辑与结构证据 —— 这四项是本票仍不能 close 的原因。 2. 真机确认「闲置回引导」观感(镜头回 `0/−60/2×10⁷` + 星球重新自转);按手册第 5 节「闲置回引导」自检行 读 `__cockpitMap.getLifecycleStats()` 的 `spin`/`spinWanted` 与 `getCameraState()` 即可。