Bladeren bron

docs(wayfinder): record ticket 71 landing (backend a416f4ab/a13dcaf5) and its gate

票 71 收口:判定序改为「未转工单的报警 > 工单 > 预警」,新增 9 条契约测试与变异闸;
本提交只落票面、地图条目、变异闸与实测取证,不含前端业务代码。

- 票面:裁定(Q1/Q2/Q3=A)、实施记录、交付与两条留档
- 地图:任务清单打勾 + Decisions so far 条目
- 证据:verify/artifacts/t71/(10 台设备 aggregate 载荷、map-snapshot、部署前预演表、复验脚本)
- 闸门:verify/t71-mutations.mjs(4 处变异打红并字节还原)
lead 18 uur geleden
bovenliggende
commit
58e711bca4
19 gewijzigde bestanden met toevoegingen van 712 en 4 verwijderingen
  1. 241 0
      .scratch/cockpit-real-data-wayfinding/issues/71-vs-52.md
  2. 6 4
      .scratch/cockpit-real-data-wayfinding/map.md
  3. 21 0
      .scratch/cockpit-real-data-wayfinding/verify/artifacts/t71/README.md
  4. 0 0
      .scratch/cockpit-real-data-wayfinding/verify/artifacts/t71/agg-21119740.json
  5. 1 0
      .scratch/cockpit-real-data-wayfinding/verify/artifacts/t71/agg-41010500001320000001.json
  6. 0 0
      .scratch/cockpit-real-data-wayfinding/verify/artifacts/t71/agg-41010500001320000002.json
  7. 0 0
      .scratch/cockpit-real-data-wayfinding/verify/artifacts/t71/agg-860347065467627.json
  8. 1 0
      .scratch/cockpit-real-data-wayfinding/verify/artifacts/t71/agg-8652052334.json
  9. 0 0
      .scratch/cockpit-real-data-wayfinding/verify/artifacts/t71/agg-TEST-CBLL222222222.json
  10. 0 0
      .scratch/cockpit-real-data-wayfinding/verify/artifacts/t71/agg-TEST-CBYL5432168.json
  11. 1 0
      .scratch/cockpit-real-data-wayfinding/verify/artifacts/t71/agg-TEST-CNXHS0000001.json
  12. 1 0
      .scratch/cockpit-real-data-wayfinding/verify/artifacts/t71/agg-TEST-TCLLJ002215.json
  13. 0 0
      .scratch/cockpit-real-data-wayfinding/verify/artifacts/t71/agg-TEST_001.json
  14. 220 0
      .scratch/cockpit-real-data-wayfinding/verify/artifacts/t71/device-facts.json
  15. 33 0
      .scratch/cockpit-real-data-wayfinding/verify/artifacts/t71/expected-after-deploy.md
  16. 0 0
      .scratch/cockpit-real-data-wayfinding/verify/artifacts/t71/map-snapshot.json
  17. 11 0
      .scratch/cockpit-real-data-wayfinding/verify/artifacts/t71/probe-alarm-wo.mjs
  18. 35 0
      .scratch/cockpit-real-data-wayfinding/verify/artifacts/t71/probe.mjs
  19. 141 0
      .scratch/cockpit-real-data-wayfinding/verify/t71-mutations.mjs

+ 241 - 0
.scratch/cockpit-real-data-wayfinding/issues/71-vs-52.md

@@ -0,0 +1,241 @@
+<!-- DSH-IDEMPOTENCY-KEY: deck-5c65af1d-5974654 -->
+<!-- parentKey: 00 -->
+<!-- created: 2026-10-19T07:50:06.050Z -->
+# 后端契约票 —— 单设备优先级「预警 vs 工单」与票 52 已关闭裁定的实测冲突
+
+Category: backend-contract
+
+**来源**:票 58 讨论期对真实后端(`192.168.110.96:8301`,快照 2026-10-19T15:19:25)的实测复核。
+
+## 实测事实(可直接复验)
+
+`GET /api/cockpit/v1/map-snapshot?page=1&pageSize=1000` 返回的未关联设备 `deviceStatus`,与其各自 `GET /api/cockpit/v1/equipments/{code}/detail-aggregate` 的四轴事实对照:
+
+| 设备 | 未处理报警 | 未解决预警 | 未结束工单 | 后端发布 |
+| --- | --- | --- | --- | --- |
+| `41010500001320000002` | 0 | **2**(PENDING/PROCESSING) | 1 | **WARNING** |
+| `TEST-CBYL5432168` | 0 | **1** | 1(stateCode=4 待验收) | **WARNING** |
+| `TEST-TCLLJ002215` | 0 | **1** | 1 | **WARNING** |
+| `860347065467627` | 0 | 0 | 1(stateCode=2 已接单) | **WORK_ORDER** |
+
+**推论**:三台设备同时存在「未解决预警 + 未结束工单」,后端判 `WARNING` → **预警高于工单**。`860347065467627` 无预警、有工单 → `WORK_ORDER`,说明工单只在无预警时才压过预警。即设备侧序为 **未转工单的报警 > 预警 > 工单**。
+
+## 冲突点
+
+- **票 52**(已关闭,2026-10-17 已推送 `3772220e`)实施的是「设备的**全部未处理报警**都已被未结束工单承接 → `WORK_ORDER`」,其余维持 `alarm → warning → workOrderInProgress` 原序。该票自身留档也写明:新规则在今天真实数据上不改变任何读数。
+- **维护者 2026-10-19 口述**:设备「未转工单的报警 > 工单 > 预警」,即在「报警全部转工单、预警仍存在」时应显示**工单**。
+- **实测边界**:今天的数据集里**找不到**「未处理报警全部已承接 + 仍有未解决预警」的样本(`alarm_data` 只有 90 行、仅涉及设备 `21119740`;「未处理且已承接」的行数为 0)。因此**无法用实测裁定这三者中哪一条是对的**,只能记录。
+
+## 需要裁定
+
+> **2026-10-22 `/triage` 诊断更正**:第 1 问的窄三路场景(报警标志为真 + 全部未处理报警已被未结束工单承接)**现行代码已判 `WORK_ORDER`**;真正未裁的是「无报警时,未解决预警 vs 未结束工单」的二路序(今天 2 台设备可观测)。详见 `## Comments` 本轮诊断。
+
+1. 「报警已全部转工单、预警仍未解决」时,设备 `deviceStatus` 应为 `WORK_ORDER` 还是 `WARNING`?
+2. 若结论为 `WORK_ORDER`:票 52 的 `deviceStatus()` 实现需要再改(当前实现把 warning 判在 workOrderInProgress 之前),且属**已完成票的返工**,需评估影响面与测试闸。
+3. 前端是否有耦合:**无**。前端 `DEVICE_STATUS_VISUALS.priority`(ALARM 1 / WARNING 2 / WORK_ORDER 3)与后端枚举一致,票 52 裁定 4 已确认「零改动、无耦合」;前端只渲染状态字符串(CONTEXT.md「点位状态」/ ADR 0002)。
+
+## 非目标
+
+- 不改前端代码(前端不推断状态)。
+- 不动监测点聚合 `resolve()` 语义(票 52 裁定 4 明确「不跟随」)。
+
+## Acceptance
+
+- 第 1 问有一份成文裁定。
+- 若需返工票 52,列出要改的方法、要补的契约测试与要复跑的闸门。
+- 明确记录「当前真实数据无法证伪任何一方」这一事实,不用推测填补。
+
+## Context
+
+- 发现于:[票 58](58-untitled.md) 讨论期(尺寸票,与本次结论零耦合)。
+- 相关:[票 52](52-untitled.md)(已关闭的后端实施)、[票 45](45-untitled.md)(前端状态轴)。
+- 真实数据端点:`/map-snapshot`、`/equipments/{deviceCode}/detail-aggregate`(`alarms` / `warnings` / `workOrders` / `alarmWarningRelations` 四轴)。
+
+## 进度
+
+- 2026-10-22 `/triage` 诊断完成(只读复核):真实后端复现 2 台设备「预警未解决 + 工单未结束、无报警」发 `WARNING`;票 52 窄三路规则现行已判 `WORK_ORDER`,真正未裁的是二路序。证据 `verify/artifacts/t71/`。
+- 2026-10-22 维护者裁定 Q1=A / Q2=A / Q3=A → 转 `ready-for-agent`,实施契约见 Agent Brief。
+- 2026-10-23 **实施完成并推送** `a416f4ab`:判定序改为「未转工单的报警 > 工单 > 预警」(只动非报警分支)、Q3 注释留档、新增 9 条契约测试;`Cockpit*Test,BusinessCockpit*Test` **566/566**、变异闸 **4/4**。真实后端未部署,部署后按 `verify/artifacts/t71/expected-after-deploy.md` 复验(归票 17/18)。
+
+Status: resolved
+Type: bug
+Blocked by:
+Labels: wayfinder:task, backend-contract, bug
+
+## Comments
+
+### local — 2026-10-22T09:02:24.463Z
+> *This was generated by AI during triage.*
+
+### local — 2026-10-22T09:30:57.449Z
+> *This was generated by AI during triage.*
+
+### local — 2026-10-23T01:32:45.711Z
+> *This was generated by AI during triage.*
+
+## 实施记录(2026-10-23,后端已推送 `a416f4ab`)
+
+**裁定依据**:维护者 2026-10-22 Q1=A / Q2=A / Q3=A。
+
+**改了什么**
+- 判定单源 `CockpitMonitoringPointStatusResolver.deviceStatus()`:非报警分支把 `workOrderInProgress` 提到 `warning` 之前;报警分支(票 52 的「全部未处理报警被未结束工单承接 → WORK_ORDER」)原样保留。最终业务序 = **未转工单的报警 > 工单处置 > 预警**。
+- `BusinessCockpitMonitoringSituationAdapter.metricStatus()` **只加注释**(Q3:按事实判、不跟随;同一设备「地图=工单处置 / 运行监测异常清单=预警」是裁定结果)。
+- 新增 `CockpitMonitoringPointStatusOrderTest` 9 条:二路压序、票 52 窄三路、查不到≠已承接、NORMAL/OFFLINE、Q2 枚举优先级与点位聚合 `min` 不变、单设备点位自动跟随。
+
+**验证**
+- `Cockpit*Test,BusinessCockpit*Test` **566/566** 全绿(rebase 到最新 origin 后复跑)。
+- 变异闸 [t71-mutations.mjs](../verify/t71-mutations.mjs) **4/4** 打红并按字节还原:退回原序 / 报警分支忽略承接 / 删掉「工单压预警」/ 偷改枚举优先级(Q2 pin);还原后复绿。
+- 真实后端:**未部署**。2026-10-23 09:31 实测 `41010500001320000002`、`TEST-CBYL5432168` 仍 `WARNING`、`21119740` 的 `ALARM` 不变 —— 代码已在 `origin/master`,生效需后端重启/部署。部署后按 [expected-after-deploy.md](../verify/artifacts/t71/expected-after-deploy.md) 对照(预期 2 台翻 `WORK_ORDER`;业务轴 预警 3→1、工单 3→5),复验归票 17/18 窗口。
+
+**提交**:`a416f4ab`(rebase 后;本地与 `origin/master` 0/0),3 个文件 +143/−9。只摘本票 hunks,未动同工作树内他人的 `overview/**` 在制品与 `ApplicationConfig.java`。
+
+**两条如实留档**
+1. rebase 拉入的 `3c734fe9` 让 `zksy-system` **测试源码**编译不过(`WaterSupplyDeviceServiceImplTest` 第 190/268 行多一个 `)`),与本票零交集。因此验证改走「先 `install zksy-system`(main only)→ 再单独跑 `zksy-admin` 测试」,变异闸同步改成两步走。**建议另开小票修该测试**,否则任何 `-am` 全量闸门都会在这里断。
+2. 真实复验未做(无部署权限/窗口),本票收口依据是**契约测试 + 变异闸 + 部署前预演表**,不冒充真实读数。
+
+## Agent Brief(维护者裁定 2026-10-22:Q1=A / Q2=A / Q3=A,据此转 `ready-for-agent`)
+
+**Category:** bug
+**Summary:** 单设备状态判定序改为「未转工单的报警 > 工单处置 > 预警」;只改单设备判定,枚举 `priority`、点位聚合语义与 `monitoring/situation` 一律不跟随。
+
+**Current behavior:**
+非报警设备同时存在「未解决预警」与「未结束工单」时,后端按 `warning → workOrderInProgress` 发布 `WARNING`(预警压过工单)。真实后端今天可复现 2 台:`41010500001320000002`(2 条预警 + 1 张未结束工单)、`TEST-CBYL5432168`(1 条预警 + 1 张未结束工单),均发 `WARNING`;而维护者口径是「未转工单的报警 > 工单 > 预警」,这两台应为 `WORK_ORDER`。
+同时注意:现行 `deviceStatus()` 的报警分支已实现「报警且全部未处理报警都被未结束工单承接 → `WORK_ORDER`」(票 52),该行为**正确、必须保留**,不是本次返工对象。
+
+**Desired behavior:**
+单设备 `deviceStatus` 判定按下列顺序(这是**业务处置序**,不是枚举优先级序):
+
+1. 设备报警标志为真:
+   - 存在至少一条未处理报警,且**每一条**都被未结束工单承接 → `WORK_ORDER`(票 52 现状保留;工单未结束 = `order_status ∈ {0,1,2,3,4,6}`,5 已结案 / 7 已取消为终态);
+   - 否则 → `ALARM`(含「查不到未处理报警行」「读失败」的保守降级——查不到 ≠ 已承接)。
+2. 否则,存在未结束工单 → `WORK_ORDER`(**本次新增**:压在预警之前)。
+3. 否则,存在未解决预警 → `WARNING`(未解决 = `status ∈ {PENDING, PROCESSING, HANDLING, RELEASED}`)。
+4. 否则 → 在线 `NORMAL` / 离线 `OFFLINE`(现状不变)。
+
+**Key interfaces:**
+- `CockpitMonitoringPointStatusResolver.deviceStatus()` —— 唯一判定单源。顺序调整只在这里做,调用点(`resolve()` 点位聚合、`cards()`、`statistics()`、`orderedDevices()`、`map-snapshot` 的 `getPublishedDeviceStatus()`)自动跟随,**不要在各处另写一份判定**。
+- `BusinessCockpitDeviceFactResolver` —— 已提供 `isAlarm()`、`isWarning()`(`hasOpenWarning`)、`isWorkOrderInProgress()`(`hasOpenWorkOrder`)、`isAlarmsCoveredByOpenWorkOrders()` 四轴事实,本次**不新增第二套事实口径**;只消费。
+- `CockpitMonitoringPointStatus` 枚举 —— `priority`(ALARM 1 / WARNING 2 / WORK_ORDER 3)**不动**(Q2=A)。
+- `BusinessCockpitMonitoringSituationAdapter.metricStatus()` —— **不动**(Q3=A),但需在方法注释写明这是裁定结果。
+
+**Acceptance criteria:**
+- [ ] 契约测试成对覆盖:无报警 + 预警 + 未结束工单 → `WORK_ORDER`;无报警 + 预警 + 仅已结案/已取消工单 → `WARNING`;无报警 + 无预警 + 未结束工单 → `WORK_ORDER`;报警 + 全部未处理报警被承接 + 预警仍在 → `WORK_ORDER`(票 52 行为不变);报警 + 有未处理报警无工单 → `ALARM`;报警但查不到未处理报警行 → `ALARM`。
+- [ ] 枚举 `priority` 与 `resolve()` 聚合语义零改动(可断言三个优先级数值不变、点位聚合仍取 `min(priority)`)。
+- [ ] 四轴事实的三处透传接缝不丢(快照、分布适配器 + `effectiveDevice()` 重建、点位详情归一化 —— 票 52 的 M6/M7 教训)。
+- [ ] 真实后端复验:`map-snapshot` 中 `41010500001320000002`、`TEST-CBYL5432168` 的 `deviceStatus` 由 `WARNING` 变 `WORK_ORDER`;`21119740` 仍 `ALARM`;业务轴计数相应变为 预警 3→1、工单 3→5(以复验当时真实数据为准,变了就按实际记录,不改样例)。
+- [ ] `Cockpit*Test,BusinessCockpit*Test` 全绿(基线以当时 HEAD 为准,票 52 时为 512/512)+ 变异闸打红并按字节还原,至少三处:把 `warning` 判回工单之前;把「无未处理报警行」当已承接;把已结案/已取消当未结束。
+- [ ] 后端提交并推 `origin/master`,票面记录提交号、测试与变异数字(参照票 52 的交付路径:后端仓库 `E:\pipe-ner-server`,只读库可直连核对)。
+- [ ] 在 `situation.metricStatus()` 注释与本票写明:改序后同一设备出现「地图/设备卡 = 工单处置、运行监测异常设备清单 = 预警」是**裁定结果**,不是缺陷。
+
+**Out of scope:**
+- 不改枚举 `CockpitMonitoringPointStatus.priority`(Q2=A);不改点位聚合语义与点位状态词表。
+- 不改前端任何代码(`POINT_STATUS_VISUALS`/`DEVICE_STATUS_VISUALS`/t09 闸门零改动)。
+- 不给 `monitoring/situation` 的 `metricStatus` 新增 `WORK_ORDER` 桶(Q3=A;那是另一张契约票)。
+- 不动报警/工单生命周期、`alarm_data` 写入与 `AlarmClearServiceImpl` 清警逻辑。
+- 不改「异常点」定义(仍按设备 WARNING/ALARM 事实判,可见范围不变)。
+
+**已知影响(不是缺陷,实施时按此记录):**
+前端无代码改动,但观感与计数会变——`planDeviceEffects()` 的「N 台异常」口径是 `ALARM/WARNING`(票 07/58),翻转为 `WORK_ORDER` 的设备会掉出异常计数、特效由 blink 改 spin(票 58:工单处置不计入降级计数);业务轴计数随之变化。
+
+**证据:**
+`verify/artifacts/t71/`(README 含复验命令、`probe.mjs`、`probe-alarm-wo.mjs`、10 台设备 `aggregate` 载荷与 `map-snapshot` 快照);票 52 的库内取证 `verify/artifacts/t52/real-data-consequence.md`。
+
+## 诊断(2026-10-22,只读复核;未改任何业务代码、未关单、未进实施)
+
+**已查阅范围**:本票正文(无评论、无 Answer)、票 52 全文、票 45(F1)、票 58(来源)、map.md、CONTEXT.md「点位状态/异常点」、前端口径表 `domain/deviceStatus.js`、`domain/pointStatus.js`、`domain/modeLayerPlan.js`、`domain/deviceEffects.js`,后端 `CockpitMonitoringPointStatusResolver`、`BusinessCockpitDeviceFactResolver`、`AlarmClearServiceImpl`(`zksy-system`)、`BusinessCockpitMonitoringSituationAdapter`,票 52 取证 `verify/artifacts/t52/real-data-consequence.md`,以及真实后端四个只读端点实测。
+**既有笔记是否够用**:够用(票 52 取证 + 票 45 F1 已把事实钉死),故**不另做 `/research`** —— 本问题是业务口径拍板,没有可查的外部一手来源;缺的不是外部资料,是维护者裁定。本次新取证落盘 `verify/artifacts/t71/`(`README.md` 含复验命令)。
+
+### 现象与复现
+
+1. `GET /api/cockpit/v1/map-snapshot?page=1&pageSize=1000` → `unassociatedEquipment[].deviceStatus`;
+2. `GET /api/cockpit/v1/equipments/{code}/detail-aggregate` → `warnings[].status`、`workOrders[].stateCode`、`alarms[].status`;
+3. 对照后,「预警未解决 + 工单未结束、无报警」的设备后端发 `WARNING`,与维护者 2026-10-19 口述「未转工单的报警 > 工单 > 预警」冲突。
+
+### 实测(2026-10-22T16:45,快照 `16:45:36`)
+
+| 设备 | 未处理报警 | 未解决预警 | 未结束工单(stateCode) | 后端发布 | 「工单 > 预警」新序下应为 |
+| --- | --- | --- | --- | --- | --- |
+| `41010500001320000002` | 0 | **2**(PENDING/PROCESSING) | 1(1 待接单) | **WARNING** | **WORK_ORDER ← 冲突** |
+| `TEST-CBYL5432168` | 0 | **1**(PENDING) | 1(4 待验收) | **WARNING** | **WORK_ORDER ← 冲突** |
+| `TEST-TCLLJ002215` | 0 | 1(PENDING) | **0**(票面记 1,今日已无) | WARNING | WARNING(数据漂移,不影响结论) |
+| `860347065467627` | 0 | 0 | 1(2 已接单) | WORK_ORDER | WORK_ORDER |
+| `21119740`(关联点位) | **2**(UNHANDLED) | 6 | 5 | ALARM | ALARM(两种序一致) |
+
+- `21119740` 的 2 条未处理报警逐条查 `/events/ALARM/{alarmId}/work-orders` → 均 `NO_WORK_ORDERS`,故票 52 的窄规则不命中、发 `ALARM` 是对的(与票 52 库内取证一致)。
+- 未关联设备今天 9 台(票面 8 台):NORMAL 1 / WARNING 3 / WORK_ORDER 3 / OFFLINE 2。
+
+### 一个关键纠正:票 52 那条窄规则**已经**能判 WORK_ORDER
+
+读码 `CockpitMonitoringPointStatusResolver.deviceStatus()` 现行分支:`alarm` → (`isAlarmsCoveredByOpenWorkOrders()` ? `WORK_ORDER` : `ALARM`) → `warning` → `workOrderInProgress` → `online ? NORMAL : OFFLINE`。
+所以第 1 问的窄三路场景(设备报警标志为真 + 全部未处理报警已被未结束工单承接 + 预警仍在)**现在返回 WORK_ORDER,不是 WARNING** —— 与维护者口述一致。
+真正还没裁的是**二路序**:报警标志已清(或本来无报警)时,未解决预警与未结束工单谁在前。今天 2 台设备正是这一形态。
+
+### 根因候选(标注置信度)
+
+| # | 候选 | 置信度 | 说明 |
+| --- | --- | --- | --- |
+| R1 | **规格缺口:口述的「工单 > 预警」从未成文** | 高(0.8) | 票 52 只落地了「报警全承接才抬工单」的窄例外;非报警分支仍是最初序 `warning → workOrderInProgress`。冲突是「未成文裁定」与「已部署实现」的差,不是实现违背已写规格。 |
+| R2 | **票面判据过窄,「无法实测裁定」不成立** | 高(0.8) | 二路样本今天就有 2 台、可判可复验;缺的只是三路样本,而三路在现行代码下已被判 WORK_ORDER。 |
+| R3 | **「报警已全部转工单」有两种事实表达** | 中(0.5) | (a) 设备 `alarm_status=1` 且未处理报警逐条被承接(票 52 口径;`AlarmClearServiceImpl` 把「有未办结工单」当报警原因的强阻塞项,该态可持续);(b) 报警全部处理完、标志清 0,只剩预警+工单。两读法结论相反((a)=WORK_ORDER、(b)=WARNING),不裁定无法实施。 |
+| R4 | 票面表格已过期(数据漂移) | 低(0.2) | `TEST-TCLLJ002215` 今天 0 张工单;未关联设备 8→9 台。不影响结论,但票面「三台」已变「两台」。 |
+| R5 | ~~部署滞后~~ | 已排除 | 观测到的 WARNING 与 52 的规则无关(52 只在报警分支抬工单);52 规则今天仍无样本可观测。 |
+
+### 影响面(若裁定改为「未转工单的报警 > 工单 > 预警」)
+
+- **后端一处**:`CockpitMonitoringPointStatusResolver.deviceStatus()` 非报警分支把 `workOrderInProgress` 提到 `warning` 之前;票 52 的保守判据(查不到 ≠ 已承接)保留。可写成:非报警时 `workOrderInProgress > warning > online?NORMAL:OFFLINE`。
+- **自动跟随**:同类 `resolve()`(点位聚合 min)、`cards()`、`statistics()`、`orderedDevices()`、map-snapshot 的 `getPublishedDeviceStatus()`。
+- **契约测试需同步**:`BusinessCockpitMapSnapshotAdapterTest`、`CockpitMapSnapshotControllerTest`、`BusinessCockpitMonitoringDistributionAdapter/ControllerTest`、`BusinessCockpitMonitoringPointDetailAdapter/Controller/SnapshotContractTest`、`CockpitPointStatusContractTest`、`CockpitPointStatusSnapshotSerializationTest`、`BusinessCockpitDeviceFactResolverTest`;闸门 `Cockpit*Test,BusinessCockpit*Test`(基线以当时 HEAD 为准,票 52 时为 512/512)+ `verify/t52-mutations.mjs`。
+- **前端零代码改动**(前提:枚举 `priority` 不改),但**观感/计数会变**:`DEVICE_STATUS_VISUALS` 不变,可 `planDeviceEffects()` 的「N 台异常」口径是 `ALARM/WARNING`(票 07/58),那 2 台会掉出异常计数、特效由 blink 改 spin;业务轴计数 预警 3→1、工单 3→5。
+- **后端内部不一致(需一并裁)**:`BusinessCockpitMonitoringSituationAdapter.metricStatus()` 按**事实**判(`isWarning()` 优先于工单),不看 `deviceStatus` —— 改序后同一台设备会出现「地图/设备卡=工单处置、运行监测异常设备清单=预警」。CONTEXT.md 的「异常点」定义按设备 WARNING/ALARM 事实判,因此**异常点可见范围不变**。
+- **真实复验判据**:上述 2 台 `map-snapshot.deviceStatus` 变 `WORK_ORDER`;`21119740` 仍 `ALARM`;分布读数随之变化。
+
+### 分流建议
+
+category 维持 `bug`(观测行为与业务口径不符);state 由 `needs-triage` → **`ready-for-human`**:三问都是业务口径/契约范围拍板,不是实现细节,猜不得;Q1 一旦选「改序」,即可转 `ready-for-agent` 返工(后端仓库 `E:\pipe-ner-server` 与只读库已具备,参照票 52 的交付路径)。
+
+### 待维护者拍板
+
+❓ **Q1(决定要不要动)**「未转工单的报警 > 工单 > 预警」是否作为正式口径落地?
+➡️ 建议 **A:落地**(维护者 2026-10-19 已两次口述;今天 2 台设备即翻转,代价是一处分支调整 + 测试同步)。
+选项:A 落地改序 → 转 `ready-for-agent` 返工;B 维持现状(承认「预警 > 工单」是正确口径)→ 转 `wontfix` 并把裁定写进 `## Answer`;C 只改文档措辞、代码不动 → `wontfix` + 票 52 留档更正。
+
+❓ **Q2(若 Q1=A)** 枚举 `priority` 与点位聚合是否跟着改(`WORK_ORDER` 提到 `WARNING` 之前)?
+➡️ 建议 **A:不改**。差距只在「单设备展示序」,聚合仍表达跨设备严重度;不改则前端两张 priority 表、图例排序、t09 闸门零改动。若选 B,则后端枚举 + 前端 `POINT_STATUS_VISUALS`/`DEVICE_STATUS_VISUALS` + t09 38/38 + 契约测试一并改。
+
+❓ **Q3(若 Q1=A)** `monitoring/situation` 的 `metricStatus`/异常设备清单是否跟随改序?
+➡️ 建议 **A:不跟随**(异常点定义与预警处置口径不动,代价是同一设备两处显示不同,需在票面与代码注释留档);B 跟随则要为 `metricStatus` 新增 `WORK_ORDER` 桶(改契约 + 前端六态词表),属另一张契约票。
+
+回了哪一项就执行对应标签动作:Q1=A → `ready-for-agent` + 出实施 brief;Q1=B/C → `wontfix`;Q2/Q3 的答案写进 brief,不单独流转。
+
+## Answer
+
+**维护者裁定(2026-10-22):Q1=A / Q2=A / Q3=A。** 本票由「待裁定」转为「可实施的后端返工」,实施契约见 `## Comments` 中的 **Agent Brief**(2026-10-22T09:30,含判定序、边界、验收与出界)。
+
+### 三条裁定
+
+| 问 | 裁定 | 含义 |
+| --- | --- | --- |
+| Q1 | **A 落地** | 单设备序正式定为「未转工单的报警 > 工单处置 > 预警」;票 52 的窄三路规则保留(它已判 `WORK_ORDER`),本次只补二路:非报警设备「有未结束工单」压在「有未解决预警」之前。 |
+| Q2 | **A 不改** | `CockpitMonitoringPointStatus.priority`(ALARM 1 / WARNING 2 / WORK_ORDER 3)与点位聚合 `min(priority)` 语义不动;前端两张词表、图例排序、t09 闸门零改动。 |
+| Q3 | **A 不跟随** | `monitoring/situation` 的 `metricStatus`/异常设备清单继续按设备事实判(`isWarning()` 优先于工单),**不加 `WORK_ORDER` 桶**;改序后「地图/设备卡 = 工单处置、运行监测异常清单 = 预警」是裁定结果,须在代码注释与票面留档。「异常点」可见范围不变。 |
+
+### 事实基线(2026-10-22T16:45 真实后端,证据 `verify/artifacts/t71/`)
+
+- 冲突可复现 2 台:`41010500001320000002`(2 预警 + 1 未结束工单)、`TEST-CBYL5432168`(1 预警 + 1 未结束工单),今日均发 `WARNING`;改序后应为 `WORK_ORDER`。
+- `TEST-TCLLJ002215` 今日已无工单(票面记 1 张),只剩预警 → 两种序都是 `WARNING`。
+- `21119740`(唯一有报警的设备)2 条未处理报警经 `/events/ALARM/{id}/work-orders` 核实均无工单承接 → `ALARM`,两序一致,票 52 规则正确不命中。
+- 票面「无法用实测裁定」一句作废:二路样本今天就有 2 台,可判别可复验。
+
+### 交付(2026-10-23,已推送)
+
+- **提交 `a416f4ab`**(rebase 后),已推 `origin/master`,本地与远端 0/0;3 文件 +143/−9。
+- **判定序落地**:非报警分支「未结束工单」压过「未解决预警」;报警分支(票 52 窄三路)原样保留。Q3 只在 `BusinessCockpitMonitoringSituationAdapter.metricStatus()` 注释留档,行为未动。
+- **契约测试**:新增 `CockpitMonitoringPointStatusOrderTest` 9 条;`Cockpit*Test,BusinessCockpit*Test` **566/566**。
+- **变异闸** `verify/t71-mutations.mjs` **4/4** 打红并字节还原(含「偷改枚举优先级」这条 Q2 pin)。
+- **真实复验:待部署**。2026-10-23 09:31 实测两台仍 `WARNING`、`21119740` 仍 `ALARM`;部署后按 `verify/artifacts/t71/expected-after-deploy.md` 对照,归票 17/18 窗口。
+- **既有阻碍已清理**:rebase 拉入的 `3c734fe9` 使 `zksy-system` 测试源码编译不过(`WaterSupplyDeviceServiceImplTest` 两处多一个 `)`)。按维护者 2026-10-23 指令**直接删除该测试类**(提交 `a13dcaf5`,395 行 / 13 个测试随之移除,内容可从 `c654d3f9` 取回),`-pl zksy-admin -am` 闸门恢复,清理后 `Cockpit*Test,BusinessCockpit*Test` **566/566**、变异闸 **4/4** 复跑通过。
+
+### 状态
+
+**resolved**(已实施、测试与变异闭环、已推送)。真实环境读数复验待后端部署,归票 17/18。
+

File diff suppressed because it is too large
+ 6 - 4
.scratch/cockpit-real-data-wayfinding/map.md


+ 21 - 0
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t71/README.md

@@ -0,0 +1,21 @@
+# 票 71 诊断取证(2026-10-22,只读)
+
+真实后端 `http://192.168.110.96:8301`,快照 `2026-10-22T16:45:36+08:00`。
+
+## 复验命令
+
+```bash
+curl -s 'http://192.168.110.96:8301/api/cockpit/v1/map-snapshot?page=1&pageSize=1000' -o map-snapshot.json
+for c in 21119740 41010500001320000001 41010500001320000002 860347065467627 8652052334 \
+         TEST-CBLL222222222 TEST-CBYL5432168 TEST-CNXHS0000001 TEST-TCLLJ002215 TEST_001; do
+  curl -s "http://192.168.110.96:8301/api/cockpit/v1/equipments/$c/detail-aggregate" -o "agg-$c.json"
+done
+node probe.mjs          # 四轴事实表:未处理报警 / 未解决预警 / 未结束工单 / 发布状态
+node probe-alarm-wo.mjs # 逐条未处理报警 → /events/ALARM/{id}/work-orders,核实是否被工单承接
+```
+
+## 一句话结论
+
+今天只有 **2 台**设备同时存在「未解决预警 + 未结束工单」(`41010500001320000002`、`TEST-CBYL5432168`),后端按 `warning > workOrderInProgress` 发 `WARNING`;
+唯一有报警的 `21119740` 其 2 条未处理报警经 `/events/ALARM/{id}/work-orders` 核实**均无工单**,故发 `ALARM`——票 52 的窄三路规则今天仍无样本,
+而票 71 真正冲突的二路场景(无报警、预警与工单并存)今天可观测。详见票 71 评论。

File diff suppressed because it is too large
+ 0 - 0
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t71/agg-21119740.json


+ 1 - 0
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t71/agg-41010500001320000001.json

@@ -0,0 +1 @@
+{"msg":"操作成功","code":200,"data":{"requestId":"3789797b51ed411682ac7b443310d52e","apiVersion":"v1","readOnly":true,"deviceCode":"41010500001320000001","aggregateState":"READY","serverTime":"2026-10-22T16:48:36.4922059+08:00","archive":{"state":"SUCCESS","reason":"SUCCESS","message":null,"data":{"coordinateSystem":"WGS84","latestImage":null,"equipmentLocation":null,"remark":null,"deviceCode":"41010500001320000001","latitude":null,"longitude":null,"deviceName":"办公区1","manufacturer":"Dahua","deviceType":"20","installTime":null,"deviceModel":null,"deviceTypeName":"视频监控(排口+内涝点)","productionDate":null,"maintainer":null,"maintainerPhone":null,"ownershipUnit":null,"assetValue":null,"deviceSpec":null,"expectedServiceYears":null}},"metrics":{"state":"EMPTY","reason":"NO_METRICS","message":null,"data":null},"warnings":{"state":"EMPTY","reason":"NO_EVENTS","message":null,"data":null},"alarms":{"state":"EMPTY","reason":"NO_EVENTS","message":null,"data":null},"alarmWarningRelations":{"state":"EMPTY","reason":"NO_EVENTS","message":null,"data":null},"workOrders":{"state":"EMPTY","reason":"NO_WORK_ORDERS","message":null,"data":null},"timelines":{"state":"EMPTY","reason":"NO_WORK_ORDERS","message":null,"data":null},"latestImage":{"state":"EMPTY","reason":"NO_IMAGE","message":null,"data":null}}}

File diff suppressed because it is too large
+ 0 - 0
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t71/agg-41010500001320000002.json


File diff suppressed because it is too large
+ 0 - 0
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t71/agg-860347065467627.json


+ 1 - 0
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t71/agg-8652052334.json

@@ -0,0 +1 @@
+{"msg":"操作成功","code":200,"data":{"requestId":"8d9ae73a993f4cfb8aa7b589c73b8661","apiVersion":"v1","readOnly":true,"deviceCode":"8652052334","aggregateState":"READY","serverTime":"2026-10-22T16:48:36.656205+08:00","archive":{"state":"SUCCESS","reason":"SUCCESS","message":null,"data":{"coordinateSystem":"WGS84","latestImage":{"url":"/profile/upload/2026/09/24/RY-4GGW-X01管网漏损监测仪_20260924160721A001.png","capturedAt":null},"equipmentLocation":"湖南省怀化市沅陵县沅陵镇辰州中街118号","remark":null,"deviceCode":"8652052334","latitude":28.45954800,"longitude":110.39707500,"deviceName":"测试漏失监测设备01","manufacturer":null,"deviceType":"13","installTime":null,"deviceModel":null,"deviceTypeName":"漏失监测仪","productionDate":null,"maintainer":null,"maintainerPhone":null,"ownershipUnit":null,"assetValue":null,"deviceSpec":null,"expectedServiceYears":null}},"metrics":{"state":"EMPTY","reason":"NO_METRICS","message":null,"data":null},"warnings":{"state":"EMPTY","reason":"NO_EVENTS","message":null,"data":null},"alarms":{"state":"EMPTY","reason":"NO_EVENTS","message":null,"data":null},"alarmWarningRelations":{"state":"EMPTY","reason":"NO_EVENTS","message":null,"data":null},"workOrders":{"state":"EMPTY","reason":"NO_WORK_ORDERS","message":null,"data":null},"timelines":{"state":"EMPTY","reason":"NO_WORK_ORDERS","message":null,"data":null},"latestImage":{"state":"SUCCESS","reason":"SUCCESS","message":null,"data":{"url":"/profile/upload/2026/09/24/RY-4GGW-X01管网漏损监测仪_20260924160721A001.png","capturedAt":null}}}}

File diff suppressed because it is too large
+ 0 - 0
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t71/agg-TEST-CBLL222222222.json


File diff suppressed because it is too large
+ 0 - 0
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t71/agg-TEST-CBYL5432168.json


+ 1 - 0
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t71/agg-TEST-CNXHS0000001.json

@@ -0,0 +1 @@
+{"msg":"操作成功","code":200,"data":{"requestId":"66d07f1af129433e91e990971e267976","apiVersion":"v1","readOnly":true,"deviceCode":"TEST-CNXHS0000001","aggregateState":"READY","serverTime":"2026-10-22T16:48:36.83429+08:00","archive":{"state":"SUCCESS","reason":"SUCCESS","message":null,"data":{"coordinateSystem":"WGS84","latestImage":{"url":"/profile/upload/2026/09/24/SY-4GXFYL-X01智能消火栓压力监测终端_20260924160732A002.png","capturedAt":null},"equipmentLocation":"湖南省怀化市沅陵县沅陵镇龙兴路","remark":null,"deviceCode":"TEST-CNXHS0000001","latitude":28.43452400,"longitude":110.39182600,"deviceName":"测试消火栓","manufacturer":null,"deviceType":"14","installTime":null,"deviceModel":null,"deviceTypeName":"消火栓压力监测终端","productionDate":null,"maintainer":null,"maintainerPhone":null,"ownershipUnit":null,"assetValue":null,"deviceSpec":null,"expectedServiceYears":null}},"metrics":{"state":"EMPTY","reason":"NO_METRICS","message":null,"data":null},"warnings":{"state":"EMPTY","reason":"NO_EVENTS","message":null,"data":null},"alarms":{"state":"EMPTY","reason":"NO_EVENTS","message":null,"data":null},"alarmWarningRelations":{"state":"EMPTY","reason":"NO_EVENTS","message":null,"data":null},"workOrders":{"state":"EMPTY","reason":"NO_WORK_ORDERS","message":null,"data":null},"timelines":{"state":"EMPTY","reason":"NO_WORK_ORDERS","message":null,"data":null},"latestImage":{"state":"SUCCESS","reason":"SUCCESS","message":null,"data":{"url":"/profile/upload/2026/09/24/SY-4GXFYL-X01智能消火栓压力监测终端_20260924160732A002.png","capturedAt":null}}}}

+ 1 - 0
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t71/agg-TEST-TCLLJ002215.json

@@ -0,0 +1 @@
+{"msg":"操作成功","code":200,"data":{"requestId":"651365f6730b4195902b78cf499f1db6","apiVersion":"v1","readOnly":true,"deviceCode":"TEST-TCLLJ002215","aggregateState":"READY","serverTime":"2026-10-22T16:48:36.8832896+08:00","archive":{"state":"SUCCESS","reason":"SUCCESS","message":null,"data":{"coordinateSystem":"WGS84","latestImage":{"url":"/profile/upload/2026/09/24/SY-4GLLJ-X01雷达超声一体化流量计_20260924160748A003.png","capturedAt":null},"equipmentLocation":"湖南省怀化市沅陵县沅陵镇","remark":null,"deviceCode":"TEST-TCLLJ002215","latitude":28.45229500,"longitude":110.36364900,"deviceName":"测试流量计","manufacturer":null,"deviceType":null,"installTime":null,"deviceModel":null,"deviceTypeName":null,"productionDate":null,"maintainer":null,"maintainerPhone":null,"ownershipUnit":null,"assetValue":null,"deviceSpec":null,"expectedServiceYears":null}},"metrics":{"state":"EMPTY","reason":"NO_METRICS","message":null,"data":null},"warnings":{"state":"SUCCESS","reason":"SUCCESS","message":null,"data":[{"warningNo":"YJ20261009140551331F9517A","warningName":"YJ20261009140551331F9517A","warningType":"1","level":1,"levelColor":"RED","status":"PENDING","eventTime":"2026-10-09T14:06:08+08:00","value":null,"unit":null,"deviceCode":"TEST-TCLLJ002215","deviceName":"测试流量计","location":"长沙"}]},"alarms":{"state":"SUCCESS","reason":"SUCCESS","message":null,"data":[]},"alarmWarningRelations":{"state":"SUCCESS","reason":"SUCCESS","message":null,"data":[]},"workOrders":{"state":"EMPTY","reason":"NO_WORK_ORDERS","message":null,"data":null},"timelines":{"state":"EMPTY","reason":"NO_WORK_ORDERS","message":null,"data":null},"latestImage":{"state":"SUCCESS","reason":"SUCCESS","message":null,"data":{"url":"/profile/upload/2026/09/24/SY-4GLLJ-X01雷达超声一体化流量计_20260924160748A003.png","capturedAt":null}}}}

File diff suppressed because it is too large
+ 0 - 0
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t71/agg-TEST_001.json


+ 220 - 0
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t71/device-facts.json

@@ -0,0 +1,220 @@
+[
+ {
+  "code": "21119740",
+  "aggState": "READY",
+  "alarmsAll": 90,
+  "alarmsUnhandled": 0,
+  "alarmStatusSample": [
+   {},
+   {},
+   {}
+  ],
+  "warningsAll": 6,
+  "warningsOpen": 6,
+  "warningStatuses": [
+   "PENDING",
+   "PROCESSING"
+  ],
+  "woInAggregate": 9,
+  "woOpen": 5,
+  "woStatuses": [
+   {
+    "s": 0
+   },
+   {
+    "s": 0
+   },
+   {
+    "s": 0
+   },
+   {
+    "s": 5
+   },
+   {
+    "s": 1
+   },
+   {
+    "s": 3
+   },
+   {
+    "s": 7
+   },
+   {
+    "s": 5
+   },
+   {
+    "s": 5
+   }
+  ],
+  "woEndpointCount": null,
+  "woEndpointData": "{\"requestId\":\"42d4c487c1204dd1adfb0003928f8c26\",\"apiVersion\":\"v1\",\"readOnly\":true,\"deviceCode\":\"21119740\",\"eventSource\":null,\"eventCode\":null,\"state\":\"READY\",\"stateReason\":\"READY\",\"stateDictionary\":[{"
+ },
+ {
+  "code": "41010500001320000001",
+  "aggState": "READY",
+  "alarmsAll": 0,
+  "alarmsUnhandled": 0,
+  "alarmStatusSample": [],
+  "warningsAll": 0,
+  "warningsOpen": 0,
+  "warningStatuses": [],
+  "woInAggregate": 0,
+  "woOpen": 0,
+  "woStatuses": [],
+  "woEndpointCount": null,
+  "woEndpointData": "{\"requestId\":\"58d3f4bd924045c1b20e5c3a32ace111\",\"apiVersion\":\"v1\",\"readOnly\":true,\"deviceCode\":\"41010500001320000001\",\"eventSource\":null,\"eventCode\":null,\"state\":\"EMPTY\",\"stateReason\":\"NO_WORK_ORDERS\""
+ },
+ {
+  "code": "41010500001320000002",
+  "aggState": "READY",
+  "alarmsAll": 0,
+  "alarmsUnhandled": 0,
+  "alarmStatusSample": [],
+  "warningsAll": 2,
+  "warningsOpen": 2,
+  "warningStatuses": [
+   "PROCESSING",
+   "PENDING"
+  ],
+  "woInAggregate": 1,
+  "woOpen": 1,
+  "woStatuses": [
+   {
+    "s": 1
+   }
+  ],
+  "woEndpointCount": null,
+  "woEndpointData": "{\"requestId\":\"61433f86b01548879903a760739f9efc\",\"apiVersion\":\"v1\",\"readOnly\":true,\"deviceCode\":\"41010500001320000002\",\"eventSource\":null,\"eventCode\":null,\"state\":\"READY\",\"stateReason\":\"READY\",\"stateDi"
+ },
+ {
+  "code": "860347065467627",
+  "aggState": "READY",
+  "alarmsAll": 0,
+  "alarmsUnhandled": 0,
+  "alarmStatusSample": [],
+  "warningsAll": 0,
+  "warningsOpen": 0,
+  "warningStatuses": [],
+  "woInAggregate": 1,
+  "woOpen": 1,
+  "woStatuses": [
+   {
+    "s": 2
+   }
+  ],
+  "woEndpointCount": null,
+  "woEndpointData": "{\"requestId\":\"8418f6a5209840749aea02d3be1c03d5\",\"apiVersion\":\"v1\",\"readOnly\":true,\"deviceCode\":\"860347065467627\",\"eventSource\":null,\"eventCode\":null,\"state\":\"READY\",\"stateReason\":\"READY\",\"stateDiction"
+ },
+ {
+  "code": "8652052334",
+  "aggState": "READY",
+  "alarmsAll": 0,
+  "alarmsUnhandled": 0,
+  "alarmStatusSample": [],
+  "warningsAll": 0,
+  "warningsOpen": 0,
+  "warningStatuses": [],
+  "woInAggregate": 0,
+  "woOpen": 0,
+  "woStatuses": [],
+  "woEndpointCount": null,
+  "woEndpointData": "{\"requestId\":\"ca6a26f1d1ff43ebab8419b43176402a\",\"apiVersion\":\"v1\",\"readOnly\":true,\"deviceCode\":\"8652052334\",\"eventSource\":null,\"eventCode\":null,\"state\":\"EMPTY\",\"stateReason\":\"NO_WORK_ORDERS\",\"stateDic"
+ },
+ {
+  "code": "TEST-CBLL222222222",
+  "aggState": "READY",
+  "alarmsAll": 0,
+  "alarmsUnhandled": 0,
+  "alarmStatusSample": [],
+  "warningsAll": 0,
+  "warningsOpen": 0,
+  "warningStatuses": [],
+  "woInAggregate": 1,
+  "woOpen": 1,
+  "woStatuses": [
+   {
+    "s": 0
+   }
+  ],
+  "woEndpointCount": null,
+  "woEndpointData": "{\"requestId\":\"babe41aee9c9497f99450d83a539eaf0\",\"apiVersion\":\"v1\",\"readOnly\":true,\"deviceCode\":\"TEST-CBLL222222222\",\"eventSource\":null,\"eventCode\":null,\"state\":\"READY\",\"stateReason\":\"READY\",\"stateDict"
+ },
+ {
+  "code": "TEST-CBYL5432168",
+  "aggState": "READY",
+  "alarmsAll": 0,
+  "alarmsUnhandled": 0,
+  "alarmStatusSample": [],
+  "warningsAll": 1,
+  "warningsOpen": 1,
+  "warningStatuses": [
+   "PENDING"
+  ],
+  "woInAggregate": 2,
+  "woOpen": 1,
+  "woStatuses": [
+   {
+    "s": 4
+   },
+   {
+    "s": 5
+   }
+  ],
+  "woEndpointCount": null,
+  "woEndpointData": "{\"requestId\":\"933ce55751034d7da450c327f9315ac4\",\"apiVersion\":\"v1\",\"readOnly\":true,\"deviceCode\":\"TEST-CBYL5432168\",\"eventSource\":null,\"eventCode\":null,\"state\":\"READY\",\"stateReason\":\"READY\",\"stateDictio"
+ },
+ {
+  "code": "TEST-CNXHS0000001",
+  "aggState": "READY",
+  "alarmsAll": 0,
+  "alarmsUnhandled": 0,
+  "alarmStatusSample": [],
+  "warningsAll": 0,
+  "warningsOpen": 0,
+  "warningStatuses": [],
+  "woInAggregate": 0,
+  "woOpen": 0,
+  "woStatuses": [],
+  "woEndpointCount": null,
+  "woEndpointData": "{\"requestId\":\"09a2bf4a976e4a8e852654d82228dd90\",\"apiVersion\":\"v1\",\"readOnly\":true,\"deviceCode\":\"TEST-CNXHS0000001\",\"eventSource\":null,\"eventCode\":null,\"state\":\"EMPTY\",\"stateReason\":\"NO_WORK_ORDERS\",\"s"
+ },
+ {
+  "code": "TEST-TCLLJ002215",
+  "aggState": "READY",
+  "alarmsAll": 0,
+  "alarmsUnhandled": 0,
+  "alarmStatusSample": [],
+  "warningsAll": 1,
+  "warningsOpen": 1,
+  "warningStatuses": [
+   "PENDING"
+  ],
+  "woInAggregate": 0,
+  "woOpen": 0,
+  "woStatuses": [],
+  "woEndpointCount": null,
+  "woEndpointData": "{\"requestId\":\"cd50db1bc1da4b80b80aaa5ee2c210e5\",\"apiVersion\":\"v1\",\"readOnly\":true,\"deviceCode\":\"TEST-TCLLJ002215\",\"eventSource\":null,\"eventCode\":null,\"state\":\"EMPTY\",\"stateReason\":\"NO_WORK_ORDERS\",\"st"
+ },
+ {
+  "code": "TEST_001",
+  "aggState": "READY",
+  "alarmsAll": 0,
+  "alarmsUnhandled": 0,
+  "alarmStatusSample": [],
+  "warningsAll": 0,
+  "warningsOpen": 0,
+  "warningStatuses": [],
+  "woInAggregate": 2,
+  "woOpen": 2,
+  "woStatuses": [
+   {
+    "s": 1
+   },
+   {
+    "s": 2
+   }
+  ],
+  "woEndpointCount": null,
+  "woEndpointData": "{\"requestId\":\"47269bdfcb8a4d298e938e3a4d83ec84\",\"apiVersion\":\"v1\",\"readOnly\":true,\"deviceCode\":\"TEST_001\",\"eventSource\":null,\"eventCode\":null,\"state\":\"READY\",\"stateReason\":\"READY\",\"stateDictionary\":[{"
+ }
+]

+ 33 - 0
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t71/expected-after-deploy.md

@@ -0,0 +1,33 @@
+# 票 71 改序后预期读数(部署前预演,2026-10-22 真实后端)
+
+- 取证:`verify/artifacts/t71/`(`map-snapshot.json`、`agg-*.json`、`probe.mjs`、`probe-alarm-wo.mjs`)
+- 快照:`2026-10-22T16:45:36+08:00`
+- 判定规则(实施后):`alarm → (全部未处理报警被未结束工单承接 ? WORK_ORDER : ALARM) → workOrderInProgress ? WORK_ORDER → warning ? WARNING → online ? NORMAL : OFFLINE`
+
+## 逐设备
+
+| 设备 | 未处理报警 | 未解决预警 | 未结束工单 | 当前发布 | 部署后预期 |
+| --- | --- | --- | --- | --- | --- |
+| `41010500001320000002` | 0 | 2 | 1(stateCode=1 待接单) | WARNING | **WORK_ORDER** |
+| `TEST-CBYL5432168` | 0 | 1 | 1(stateCode=4 待验收) | WARNING | **WORK_ORDER** |
+| `TEST-TCLLJ002215` | 0 | 1 | 0 | WARNING | WARNING(不变) |
+| `860347065467627` | 0 | 0 | 1(stateCode=2 已接单) | WORK_ORDER | WORK_ORDER |
+| `TEST-CBLL222222222` / `TEST_001` | 0 | 0 | 1 / 2 | WORK_ORDER | WORK_ORDER |
+| `21119740`(关联点位 CBWS202610150001) | 2(UNHANDLED,逐条 `/events/ALARM/{id}/work-orders` = NO_WORK_ORDERS) | 6 | 5 | ALARM | ALARM(不变) |
+| `41010500001320000001` / `8652052334` / `TEST-CNXHS0000001` | 0 | 0 | 0 | NORMAL / OFFLINE | 不变 |
+
+## 业务轴计数(未关联设备 9 台)
+
+| 状态 | 当前 | 部署后 |
+| --- | --- | --- |
+| 报警 | 1(关联设备 `21119740` 的点位) | 1 |
+| 预警 | 3 | **1** |
+| 工单处置 | 3 | **5** |
+| 正常 | 1 | 1 |
+| 离线 | 2 | 2 |
+
+## 口径与边界(不猜)
+
+- 设备级 `equipment_status.alarm_status` **不在只读 API 暴露**,因此「报警标志」由发布状态反推:发布 `WARNING`/`WORK_ORDER` 的设备必然 `alarm_status≠1`(否则会先落 ALARM 分支)。这条反推只用于本预演,不写进代码。
+- 「未处理报警是否被逐条承接」通过 `/events/ALARM/{alarmId}/work-orders` 逐条核实,实测两条均无工单。
+- 预演**不是**验收证据:部署后必须重跑 `probe.mjs` 对照本表,才能算真实复验(归票 17/18 窗口)。

File diff suppressed because it is too large
+ 0 - 0
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t71/map-snapshot.json


+ 11 - 0
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t71/probe-alarm-wo.mjs

@@ -0,0 +1,11 @@
+import fs from 'node:fs';
+const BASE='http://192.168.110.96:8301/api/cockpit/v1';
+const d=JSON.parse(fs.readFileSync('agg-21119740.json','utf8')).data;
+const un=(d.alarms.data||[]).filter(a=>a.status==='UNHANDLED');
+for (const a of un) {
+  for (const fam of ['ALARM','alarm']) {
+    const url=BASE+'/events/'+fam+'/'+encodeURIComponent(a.alarmId)+'/work-orders';
+    try { const r=await fetch(url); const t=await r.text(); console.log(fam, a.alarmId, r.status, t.slice(0,300)); } catch(e){ console.log(fam,'ERR',String(e).slice(0,120)); }
+  }
+  console.log('alarm:', JSON.stringify({id:a.alarmId, code:a.warningCode, name:a.warningName, status:a.status, eventTime:a.eventTime}));
+}

+ 35 - 0
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t71/probe.mjs

@@ -0,0 +1,35 @@
+import fs from 'node:fs';
+const BASE = 'http://192.168.110.96:8301/api/cockpit/v1';
+const DEVICES = ['21119740','41010500001320000001','41010500001320000002','860347065467627','8652052334','TEST-CBLL222222222','TEST-CBYL5432168','TEST-CNXHS0000001','TEST-TCLLJ002215','TEST_001'];
+const OPEN_WO = new Set([0,1,2,3,4,6]);
+const OPEN_WARN = new Set(['PENDING','PROCESSING','HANDLING','RELEASED']);
+async function get(path, file) {
+  const res = await fetch(BASE + path);
+  const text = await res.text();
+  if (file) fs.writeFileSync(file, text);
+  return { status: res.status, json: JSON.parse(text) };
+}
+const rows = [];
+for (const c of DEVICES) {
+  let agg, wo;
+  try { agg = await get('/equipments/' + encodeURIComponent(c) + '/detail-aggregate', 'agg-' + c + '.json'); } catch (e) { agg = null; }
+  try { wo = await get('/equipments/' + encodeURIComponent(c) + '/work-orders', 'wo-' + c + '.json'); } catch (e) { wo = null; }
+  const d = agg && agg.json && agg.json.data ? agg.json.data : {};
+  const alarms = (d.alarms && d.alarms.data) || [];
+  const warnings = (d.warnings && d.warnings.data) || [];
+  const workOrders = (d.workOrders && d.workOrders.data) || [];
+  const unhandled = alarms.filter(a => a.alarmStatus === 0 || a.alarmStatus === '0' || a.status === 0);
+  const openWarn = warnings.filter(w => OPEN_WARN.has(String(w.status||'').toUpperCase()));
+  const openWo = workOrders.filter(o => OPEN_WO.has(Number(o.orderStatus ?? o.stateCode ?? o.status)));
+  rows.push({ code: c, aggState: d.aggregateState,
+    alarmsAll: alarms.length, alarmsUnhandled: unhandled.length,
+    alarmStatusSample: alarms.slice(0,3).map(a=>({alarmStatus:a.alarmStatus, workOrderId:a.workOrderId, code:a.alarmNo||a.alarmCode})),
+    warningsAll: warnings.length, warningsOpen: openWarn.length, warningStatuses: [...new Set(warnings.map(w=>w.status))],
+    woInAggregate: workOrders.length, woOpen: openWo.length,
+    woStatuses: workOrders.map(o=>({id:o.workOrderId||o.orderId, s:o.orderStatus??o.stateCode??o.status})),
+    woEndpointCount: wo && wo.json && wo.json.data ? ((wo.json.data.list||wo.json.data.records||wo.json.data).length ?? null) : null,
+    woEndpointData: wo && wo.json ? JSON.stringify(wo.json.data).slice(0,200) : null,
+  });
+}
+fs.writeFileSync('device-facts.json', JSON.stringify(rows,null,1));
+for (const r of rows) console.log(JSON.stringify(r));

+ 141 - 0
.scratch/cockpit-real-data-wayfinding/verify/t71-mutations.mjs

@@ -0,0 +1,141 @@
+/**
+ * 票 71 变异闸:证明契约测试对「未转工单的报警 > 工单处置 > 预警」这条序**有牙**。
+ *
+ * 每条变异:按字节把源码改成规则失效的形态 → 跑 CockpitMonitoringPointStatusOrderTest →
+ * 必须转红 → 再按字节还原。全部转红、还原后复绿,闸门才算通过。
+ *
+ * 沿用票 52 变异闸的两条教训:
+ *  1. 必须 '-pl zksy-admin -am' 走 reactor 重新编译,否则 zksy-system 从本地仓库 jar 解析,源码变异进不了测试;
+ *  2. Windows 上 mvn 是 mvn.cmd,显式给路径;「构建没跑到测试」不能算打红。
+ *
+ * 用法:node .scratch/cockpit-real-data-wayfinding/verify/t71-mutations.mjs
+ */
+import { execSync } from 'node:child_process'
+import { readFileSync, writeFileSync } from 'node:fs'
+
+const BACKEND = 'E:/pipe-ner-server'
+const MODULE = 'pipe-network-service'
+const TEST = 'CockpitMonitoringPointStatusOrderTest'
+const MVN = 'D:/Maven/apache-maven-3.9.15/bin/mvn.cmd'
+
+const RESOLVER = `${BACKEND}/${MODULE}/zksy-system/src/main/java/com/zksy/cockpit/point/CockpitMonitoringPointStatusResolver.java`
+const ENUM = `${BACKEND}/${MODULE}/zksy-system/src/main/java/com/zksy/cockpit/point/CockpitMonitoringPointStatus.java`
+
+const read = (p) => readFileSync(p, 'utf8')
+const write = (p, s) => writeFileSync(p, s)
+const toLf = (s) => s.replace(/\r\n/g, '\n')
+
+const ORDER_BLOCK = `        // 「未转工单的报警」已在上面拦截;剩下的「有未结束工单」压过「有未解决预警」。
+        if (device.isWorkOrderInProgress()) {
+            return CockpitMonitoringPointStatus.WORK_ORDER;
+        }
+        if (device.isWarning()) {
+            return CockpitMonitoringPointStatus.WARNING;
+        }`
+
+const mutations = [
+    {
+        id: 'M1',
+        name: '退回原序:预警重新压过工单(两个分支换回)',
+        file: RESOLVER,
+        from: ORDER_BLOCK,
+        to: `        if (device.isWarning()) {
+            return CockpitMonitoringPointStatus.WARNING;
+        }
+        if (device.isWorkOrderInProgress()) {
+            return CockpitMonitoringPointStatus.WORK_ORDER;
+        }`,
+    },
+    {
+        id: 'M2',
+        name: '报警分支忽略承接事实(全部报警恒判 ALARM)',
+        file: RESOLVER,
+        from: `            return device.isAlarmsCoveredByOpenWorkOrders()
+                    ? CockpitMonitoringPointStatus.WORK_ORDER
+                    : CockpitMonitoringPointStatus.ALARM;`,
+        to: `            return CockpitMonitoringPointStatus.ALARM;`,
+    },
+    {
+        id: 'M3',
+        name: '删掉非报警分支的工单判断(工单不再压预警)',
+        file: RESOLVER,
+        from: `        if (device.isWorkOrderInProgress()) {
+            return CockpitMonitoringPointStatus.WORK_ORDER;
+        }
+        if (device.isWarning()) {`,
+        to: `        if (device.isWarning()) {`,
+    },
+    {
+        id: 'M4',
+        name: '偷偷改枚举优先级(Q2 被破坏:WORK_ORDER 提到 WARNING 之前)',
+        file: ENUM,
+        from: `    WARNING(2, "预警"),
+    WORK_ORDER(3, "工单处置"),`,
+        to: `    WARNING(3, "预警"),
+    WORK_ORDER(2, "工单处置"),`,
+    },
+]
+
+/** 跑一次测试;返回 { ran, green } —— ran=false 表示构建没跑到测试(不能算证据)。 */
+const runTest = () => {
+    let out = ''
+    try {
+        out = execSync(
+            `"${MVN}" -o -pl zksy-admin -am test -Dtest=${TEST} -DfailIfNoTests=false`,
+            { cwd: `${BACKEND}/${MODULE}`, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })
+    } catch (error) {
+        out = `${error.stdout ?? ''}${error.stderr ?? ''}`
+    }
+    const summary = out.match(/Tests run: (\d+), Failures: (\d+), Errors: (\d+)/g)
+    if (!summary || summary.length === 0) {
+        return { ran: false, green: false, out }
+    }
+    const last = summary[summary.length - 1]
+    const [, total, failures, errors] = last.match(/Tests run: (\d+), Failures: (\d+), Errors: (\d+)/)
+    return { ran: true, green: Number(failures) === 0 && Number(errors) === 0 && Number(total) > 0, out }
+}
+
+const baseline = runTest()
+if (!baseline.ran || !baseline.green) {
+    console.log('基线不绿,闸门无法判定:先修好源码/测试再跑变异。')
+    process.exit(1)
+}
+console.log('基线绿(未变异时测试全过),开始变异…\n')
+
+let red = 0
+let broken = 0
+const originals = new Map()
+
+for (const m of mutations) {
+    if (!originals.has(m.file)) originals.set(m.file, read(m.file))
+    const original = originals.get(m.file)
+    const crlf = original.includes('\r\n')
+    const norm = toLf(original)
+    if (!norm.includes(m.from)) {
+        console.log(`SKIP ${m.id} — anchor 未命中(源码已变,需同步变异脚本):${m.name}`)
+        broken += 1
+        continue
+    }
+    write(m.file, crlf ? norm.replace(m.from, m.to).replace(/\n/g, '\r\n') : norm.replace(m.from, m.to))
+
+    const result = runTest()
+    if (!result.ran) {
+        console.log(`FAIL ${m.id} — 构建没跑到测试,不能算打红:${m.name}`)
+        broken += 1
+    } else if (!result.green) {
+        red += 1
+        console.log(`PASS ${m.id} — 变异被打红:${m.name}`)
+    } else {
+        console.log(`FAIL ${m.id} — 变异未被抓住(测试无牙):${m.name}`)
+    }
+
+    write(m.file, original)
+    if (read(m.file) !== original) {
+        console.log(`FAIL ${m.id} — 还原后字节不一致`)
+        broken += 1
+    }
+}
+
+const final = runTest()
+console.log(`\n变异打红 ${red}/${mutations.length};还原后复绿:${final.green ? '是' : '否'};异常 ${broken}`)
+process.exit(red === mutations.length && final.green && broken === 0 ? 0 : 1)

Some files were not shown because too many files changed in this diff