# 07: 坐标校验与无地图位置 **What to build:** 对后端发布坐标做防御性校验。非法、缺失、零值、超范围或未校验坐标的对象不上图,但保留在列表和详情中,并显示明确原因。 Blocked by: 08 Status: resolved Labels: ready-for-agent > 认领:2026-09-18 会话接手。前置 08(地图摘要分页与快照)已 resolved,依赖已解除。 > > 实施前按 grilling 澄清了 6 个悬而未决的取舍(票面语气是「不上图 / 保留在列表」,而地图上今天**一个点位都没画**、 > 列表也还没建),维护者裁定:Q1=只交**数据面准入判定**(渲染归 09/22);Q2=(c) 但以数据面为主体, > 另加一个地图上的可见清单作为本票的唯一可见证据;Q3=按推荐(只认后端 `renderable`);Q4=B(用后端**全量**口径); > Q5=逻辑闸门 + 真机走查两件套;Q6=依赖已解除。 ## 验收清单 - [x] 缺失、非法、零值、超范围和未校验坐标均不上图。 - [x] 对象仍出现在列表和详情中。—— 交的是**数据面** + 本票可见清单;真列表(票 10 两级详情 / 票 18 台账)接手同一份数据。 - [x] UI 显示“无地图位置”或具体坐标原因。—— 真机走查 B3 实测三条中文原因逐条渲染。 - [x] 前端不自动纠偏、不使用默认坐标。 - [x] 有坐标校验纯逻辑单测。—— `t07-coordinate.mjs` **39/39**;真机走查 **20/20**。 ## 进度:100% **已完成并关闭(2026-09-18)。** 逻辑闸门 **39/39**、真机走查 **20/20**; 回归 t05(179/179 + 走查 10/10)、t08(44/44 + 走查 16/16)、t01 走查 20/20、t28 7/7、t29 6/6、t30 7/7 全绿。 ### 交付 **领域层**(新增 `domain/coordinate.js`,纯函数、可脱浏览器断言) - 准入判据**唯一**:`isRenderable(item)` 只认后端 `coordinate.renderable === true`。 连 `state === 'VALID'` 都不作为备选判据——那是第二套判据,一旦与 `renderable` 分歧(后端将来加新 state), 两个答案必然有一个是错的,而错的会画到地图上。票 07 原句「前端不自动纠偏、不使用默认坐标」的代码化落点就在这里。 - 原因文案三层:后端**整段没给**校验段(票 05 的 `validated:false`)→「坐标未校验」(这是「缺失」,不是「非法」); reason 命中词表 → 具体原因;其余认不出 →「无地图位置」。词表**照抄后端源码**(本机 `E:\pipe-ner-server` 的 `CockpitCoordinateValidator`,六个取值:`UNVERIFIED_COORDINATE` / `MISSING_LONGITUDE` / `MISSING_LATITUDE` / `LONGITUDE_OUT_OF_RANGE` / `LATITUDE_OUT_OF_RANGE` / `ZERO_COORDINATE`),保证文案与后端结论一一对应而非前端猜一套。 - `splitByCoordinate()` 交**两堆**:不上图 ≠ 不存在。被拦下的对象带编码与名称进 `blockedEntries()`, 供列表渲染——静默丢掉正是 CONTEXT.md「无地图位置」词条点名禁止的(_Avoid_: 默认坐标点、静默隐藏)。 - `coordinateOverview(objects, backendSummary)`:界面唯一读的那个对象,把**两个口径并排**给出(见下)。 **两个口径,主语分开(维护者裁定的 Q4=B)** 这是本票最容易骗人的地方,所以分开说、不混成一个数: - `backend` —— 后端 `coordinateValidation` 汇总,**全量**口径。读后端源码确认:`CockpitMapSnapshotService.load()` 用整份 `points`/`equipment` 算完汇总**再**切片分页,所以它不随翻页跳动。 - `loaded` —— 前端按已加载对象算的自检口径。 - 二者不相等是**正常**的。原来的漏读:`toMapBatch()` 根本没读这段(票 05 遗留),现如实带出并单测钉住。 **修掉一处会骗人的措辞**:票 08 的摘要文案写「已上图 N 个」而 `N = loaded`(含不上图对象)。 票 07 起有对象因坐标被拦下,这两个数不再相等——照旧写法会把 5 个说成上图的 5 个(实际只有 2 个画得出来)。 现在按坐标准入分开报:「已上图 2 个 · 无地图位置 3 个」;调用方没给 `objects` 时(纯文案单测)保持原措辞。 **界面**(`components/MapCoordinateBlocked.vue`,新增) - 地图上的「无地图位置」清单:计数 + 两个口径各一行 + 可展开的逐对象清单(对象名 + 编码 + **中文具体原因**)。 - 定位是被左栏逼出来的:中央一张图是满幅背景,左右两栏 `z-index:2` 浮在其上(地图 `z-index:0` 且自成层叠上下文), 贴左下的浮块会被 300px 宽的左栏整块盖住 → 居中放在底部信息带之上(`bottom: 100u`,让开图例/标题/状态灯那一条)。 - `data-map-blocked-*` 供走查断言;原因码只上 `data-*`,界面上只有中文。 - `MapCanvas.vue` 追加宿主 dataset:`data-map-renderable` / `data-map-blocked` / `data-map-blocked-backend` / `data-map-coordinate-crs`。 - `stores/mapStore.js` 供货 `coordinate` 计算属性——组件零坐标判据(静态守卫 I3 钉住)。 **mock**(`mocks/contractMocks.js`) - 响应补 `coordinateValidation` 全量汇总,**照抄后端的三桶分法**(`renderable` 优先,其余里 `state === 'INVALID'` 记非法, 剩下的记未校验)——换一套算法就会让界面显示的数与后端对不上。 - `empty` / `degraded` 两个样例同样带上,供现场演示。 ### 验证证据 - **纯逻辑**:`node .scratch/cockpit-shell-panel-layout/verify/t07-coordinate.mjs` → **39/39**。八段: A 唯一准入判据(含两条对抗:`state=VALID` 但 `renderable=false` **必须**不上图;`state=INVALID` 但 `renderable=true` 照画, 以及「181° 原值不被纠正」);B 六种原因逐一命中 + `validated:false` 归「缺失」+ 认不出的不泄露原因码; C 切分不静默丢对象(两堆都交出去);D 两个口径分开 + 缺失不补 0;E 汇总进批次且**跨页不累加**; F mock 三桶分法与后端一致;G store 供货;H 「已上图」措辞不再把被拦下的算进去; I 静态守卫(领域层零范围重算、准入只有一处、界面零坐标判据、界面零原因码、后端裸字段不出领域层)。 - **真机走查**:`node .scratch/cockpit-shell-panel-layout/verify/t07-coordinate-walkthrough.mjs` → **20/20** (`yarn dev` @ localhost:8000,Chrome 152 headless;**先核对四个改动模块确实是最新的**再跑,避免拿旧模块假绿)。逐条: 翻到底后**可上图 2 · 无地图位置 3 · 已加载 5**;清单计数 3 且**对象没被静默丢掉**; 后端全量口径 `3`(非法 2 + 未校验 1)与已加载口径 `2/5` **是两个数、措辞分开**; 摘要文案不再虚报「已上图 5 个」;展开后三条原因**逐条中文**(坐标未经校验 / 经度超出合法范围 / 零坐标(经纬度均为 0)), 原因码只出现在 `data-*`;空样例 `未取到坐标校验汇总`(不编 0);字段缺失样例落「坐标未校验」且**仍被列出**; 清单不出地图宿主、零横向溢出、仍只有一个地图宿主、全程零页面级报错。 - **回归**:`t08-map-summary` **44/44**(逻辑)+ 走查 **16/16**、`t05-adapter` **179/179** + 走查 **10/10**、 `t01-browser-walkthrough` **20/20**、`t28-fill-container` **7/7**、`t29-basemap-walkthrough` **6/6**、 `t30-intro-walkthrough` **7/7**,另有 `t01-panel-catalog` / `t03-camera-contract` / `t04-config` / `t06-assets` / `t21-terrain` / `t21-view-modes` / `t29-basemap` / `t30-view-presets` 逻辑脚本全通过。 - **未运行**:`yarn build`(生产构建)与 git 提交——按 `AGENTS.md` 的验证规则,只在维护者明确要求时执行。 ### 对抗式自查抓到的问题(实证) 1. **守卫被自己的注释判红**:静态守卫 I2 第一版查「是否出现 `state === 'VALID'`」,结果命中的是我解释 *为什么不用它* 的那句注释。改成先剥注释再断言——拿注释当证据等于没守。 2. **空样例的汇总其实是空的,不是 0**:票 05 的 `sectionFromResult` 对 `EMPTY` 分区不带载荷, 全零汇总在适配层就被放下。我最初断言 `blockedBackend === '0'` 会假红,而措辞写成「后端未发布」 更是把责任推错地方(那个 0 后端确实发布过)→ 改为「未取到坐标校验汇总」,断言与文案同步修正。 3. **走查脚本第一版会假绿**:外壳挂载后只自动取**第 1 批**(票 08 的渐进加载),而 mock 五条按页大小 2 分三批—— 不翻页就只持有前 2 个对象、看不到后两个坏坐标对象,断言会以为「没有坏坐标」。 已补 `advanceUntilSettled()` 真翻页(票 08 踩过同型坑)。 4. **浮块会被左栏盖住**:见上「定位是被左栏逼出来的」。改为居中放底部信息带之上后,走查 D1 实测 `panelInsideHost: true`、零横向溢出,猜测被证据替代。 5. **走查第一版有一条假绿(跑起来才抓到)**:`data-map-blocked-backend` 这个名字**同时**挂在 地图宿主(dataset)与清单里的 `

` 上,而宿主在 DOM 里更靠前 —— `querySelector` 命中宿主, 拿回的是**整页文本**,于是 `includes('全量')` 恒真。改成面板内选择器 + 给 `

` 换 `data-map-coordinate-backend/-loaded`,复跑后 A5 的证据才变成真正那一行 (`全量可上图 2/5 · 坐标非法 2 · 未校验 1 | 已加载 5 个 · 可上图 2 · 无地图位置 3`)。 教训同票 08:**走查绿不够,得知道绿的是什么**。 ### 边界与遗留 - **渲染归票 09/22**:本票只交准入判定 + 可见原因,地图标记/图例/管线都不画(维护者 Q1=A 裁定)。 - **真列表归票 10/18**:本票的清单是「保留在列表里」的早期可见面,等真列表建起来后消费同一份 `blockedEntries`,本清单自然退为地图上的辅助提示。 - **事件坐标**:事件读模型暂无 `coordinateValidation`(后端补契约见 `issues/25` 的 BT-4),票 14 的 「无有效坐标时显示原因」需等该契约;本票的判据已可复用(`blockReason()` 对缺校验段给「坐标未校验」)。 - **未提交**:本票改动留在工作区(工作区另有票 21/27/29/30 会话的在途改动,见 `map.md`)。