t46-mode-layer-plan.mjs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. /**
  2. * T46 一级模式图层计划与选择态矩阵 纯逻辑检查(票 46;规格 18 / 决策票 13)。
  3. *
  4. * 零依赖 node 断言脚本(约定见 t01-panel-catalog.mjs)。
  5. * 运行:node .scratch/cockpit-shell-panel-layout/verify/t46-mode-layer-plan.mjs
  6. *
  7. * 主 seam 是 `domain/modeLayerPlan.js` 的纯函数:五种状态 × 各对象层的可见矩阵、
  8. * 异常点判定(只认设备状态事实)、图例与标记一致、引导门控、降级说明、回联目标与选择态。
  9. */
  10. import {
  11. MODE_LAYER,
  12. MODE_SELECTION,
  13. planModeLayers,
  14. planModeSelectionTransition,
  15. isAssociatedDeviceVisible,
  16. isAttentionDeviceStatus,
  17. } from '../../../src/modules/cockpit/domain/modeLayerPlan.js'
  18. import { legendMatchesMarkers } from '../../../src/modules/cockpit/domain/pointStatus.js'
  19. const checks = []
  20. function check(name, pass, detail = '') {
  21. checks.push({ name, pass })
  22. const rest = detail === '' ? '' : ` — ${detail}`
  23. console.log(`${pass ? 'PASS' : 'FAIL'} ${name}${rest}`)
  24. }
  25. /** 造一张摘要对象集:2 监测点(1 异常)+ 1 未关联设备。 */
  26. function fixtureObjects() {
  27. return [
  28. {
  29. kind: 'monitoring-point',
  30. code: 'MP-A',
  31. name: '异常点监测点',
  32. status: 'NORMAL',
  33. deviceCodes: ['EQ-A1', 'EQ-A2'],
  34. longitude: 110.1,
  35. latitude: 28.4,
  36. coordinate: { state: 'VALID', reason: '', renderable: true, validated: true },
  37. },
  38. {
  39. kind: 'monitoring-point',
  40. code: 'MP-B',
  41. name: '正常点监测点',
  42. status: 'NORMAL',
  43. deviceCodes: [],
  44. longitude: 110.2,
  45. latitude: 28.4,
  46. coordinate: { state: 'VALID', reason: '', renderable: true, validated: true },
  47. },
  48. {
  49. kind: 'unassociated-equipment',
  50. code: 'EQ-FREE-1',
  51. name: '异常未关联设备',
  52. status: '',
  53. deviceCodes: [],
  54. longitude: 110.3,
  55. latitude: 28.4,
  56. coordinate: { state: 'VALID', reason: '', renderable: true, validated: true },
  57. },
  58. ]
  59. }
  60. /** 异常设备事实(照 `/monitoring/situation` 的领域形状)。 */
  61. function fixtureAbnormal() {
  62. return [
  63. { code: 'EQ-A1', name: '关联报警设备', status: 'ALARM' },
  64. { code: 'EQ-FREE-1', name: '异常未关联设备', status: 'WARNING' },
  65. ]
  66. }
  67. /** 关联设备行(placement 已由 deviceLayer.js 判好;计划只做模式过滤)。 */
  68. function fixtureAssociated() {
  69. return [
  70. { code: 'EQ-A1', pointCode: 'MP-A', status: 'ALARM', placement: 'drawn', longitude: 110.11, latitude: 28.41 },
  71. { code: 'EQ-A2', pointCode: 'MP-A', status: 'NORMAL', placement: 'drawn', longitude: 110.12, latitude: 28.42 },
  72. ]
  73. }
  74. function planOf(mode, extra = {}) {
  75. return planModeLayers({
  76. mode,
  77. objects: fixtureObjects(),
  78. abnormalDevices: fixtureAbnormal(),
  79. associatedDevices: fixtureAssociated(),
  80. entered: true,
  81. ...extra,
  82. })
  83. }
  84. // ================================================================ A 完整可见矩阵
  85. {
  86. const overview = planOf(MODE_LAYER.OVERVIEW)
  87. const infra = planOf(MODE_LAYER.INFRASTRUCTURE)
  88. const monitoring = planOf(MODE_LAYER.MONITORING)
  89. const warning = planOf(MODE_LAYER.WARNING_RESPONSE)
  90. const codesOf = (plan) => plan.markers.map((m) => `${m.kind}|${m.code}`).sort()
  91. check('A1-overview-shows-everything',
  92. codesOf(overview).length === 3 &&
  93. codesOf(overview).includes('monitoring-point|MP-A') &&
  94. codesOf(overview).includes('monitoring-point|MP-B') &&
  95. codesOf(overview).includes('unassociated-equipment|EQ-FREE-1'),
  96. `总览 ${codesOf(overview).join(' ')}`)
  97. check('A2-infrastructure-hides-monitoring',
  98. infra.markers.length === 0,
  99. `基础设施 ${infra.markers.length} 个标记(监测点/未关联设备/关联设备全隐藏)`)
  100. check('A3-monitoring-shows-points',
  101. codesOf(monitoring).length === 3,
  102. `运行监测 ${codesOf(monitoring).join(' ')}`)
  103. check('A4-warning-only-abnormal-points',
  104. codesOf(warning).length === 2 &&
  105. codesOf(warning).includes('monitoring-point|MP-A') &&
  106. codesOf(warning).includes('unassociated-equipment|EQ-FREE-1'),
  107. `预警处置 ${codesOf(warning).join(' ')}(正常点 MP-B 不在、异常未关联设备在)`)
  108. // 关联设备图层按票 05 锁定的范围:概览/运行监测全部,基础设施隐藏,预警处置仅 ALARM/WARNING。
  109. check('A5-associated-device-matrix',
  110. overview.associatedDevices.length === 2 &&
  111. monitoring.associatedDevices.length === 2 &&
  112. infra.associatedDevices.length === 0 &&
  113. warning.associatedDevices.length === 1 && warning.associatedDevices[0].code === 'EQ-A1',
  114. `关联设备各模式 ${[overview, infra, monitoring, warning].map((p) => p.associatedDevices.length).join('/')}`)
  115. // 异常点判定只认 WARNING/ALARM:工单/离线不混入(规格 Q8)。
  116. const onlyWorkOrder = planModeLayers({
  117. mode: MODE_LAYER.WARNING_RESPONSE,
  118. objects: fixtureObjects(),
  119. abnormalDevices: [
  120. { code: 'EQ-A1', status: 'WORK_ORDER' },
  121. { code: 'EQ-FREE-1', status: 'OFFLINE' },
  122. ],
  123. entered: true,
  124. })
  125. check('A6-work-order-offline-not-abnormal',
  126. onlyWorkOrder.markers.length === 0,
  127. `工单/离线不构成异常点(标记 ${onlyWorkOrder.markers.length} 个)`)
  128. // 异常设备数据未就绪 → 不猜:预警处置不画任何点,且给降级说明(规格:不借用点位聚合状态)。
  129. const noAbnormalData = planOf(MODE_LAYER.WARNING_RESPONSE, { abnormalDevices: null })
  130. check('A7-abnormal-data-missing-degrades-honestly',
  131. noAbnormalData.markers.length === 0 &&
  132. noAbnormalData.degradedNotes.some((note) => note.key === 'abnormal-devices-unready'),
  133. `异常数据未就绪:标记 ${noAbnormalData.markers.length} 个,降级说明 ${
  134. noAbnormalData.degradedNotes.map((n) => n.key).join(',') || '无'
  135. }`)
  136. // 管线四模式常驻(进入后);设施只在概览/基础设施可见。
  137. const pipelinePlan = (mode) => planModeLayers({
  138. mode,
  139. objects: fixtureObjects(),
  140. abnormalDevices: fixtureAbnormal(),
  141. pipelines: [],
  142. entered: true,
  143. })
  144. check('A8-pipeline-resident-in-all-modes',
  145. [MODE_LAYER.OVERVIEW, MODE_LAYER.INFRASTRUCTURE, MODE_LAYER.MONITORING, MODE_LAYER.WARNING_RESPONSE]
  146. .every((mode) => pipelinePlan(mode).layerStates.pipelines === 'ready'),
  147. '管线在四模式均为可见层(契约就绪=ready;未就绪应是 degraded 而非 hidden)')
  148. // 未就绪也不许被模式隐藏(常驻 ≠ 数据就绪才常驻):
  149. const pipelineUnready = (mode) => planModeLayers({
  150. mode,
  151. objects: fixtureObjects(),
  152. abnormalDevices: fixtureAbnormal(),
  153. pipelines: null,
  154. entered: true,
  155. })
  156. check('A8b-pipeline-unready-still-visible-not-hidden',
  157. [MODE_LAYER.OVERVIEW, MODE_LAYER.INFRASTRUCTURE, MODE_LAYER.MONITORING, MODE_LAYER.WARNING_RESPONSE]
  158. .every((mode) => pipelineUnready(mode).layerStates.pipelines === 'degraded'),
  159. '管线数据未就绪四模式都是 degraded(degraded ≠ hidden:不把缺数据伪装成"该模式没有")')
  160. check('A9-facility-only-overview-infrastructure',
  161. pipelinePlan(MODE_LAYER.OVERVIEW).layerStates.facilities === 'degraded' &&
  162. pipelinePlan(MODE_LAYER.INFRASTRUCTURE).layerStates.facilities === 'degraded' &&
  163. pipelinePlan(MODE_LAYER.MONITORING).layerStates.facilities === 'hidden' &&
  164. pipelinePlan(MODE_LAYER.WARNING_RESPONSE).layerStates.facilities === 'hidden',
  165. '设施点只在概览/基础设施可见,运行监测/预警处置隐藏')
  166. }
  167. // ================================================================ B 引导态门控
  168. {
  169. const plan = planModeLayers({
  170. mode: MODE_LAYER.OVERVIEW,
  171. objects: fixtureObjects(),
  172. abnormalDevices: fixtureAbnormal(),
  173. associatedDevices: fixtureAssociated(),
  174. entered: false,
  175. })
  176. check('B1-intro-plan-empty',
  177. plan.markers.length === 0 && plan.legend.length === 0 && plan.locateTargets.length === 0 &&
  178. plan.associatedDevices.length === 0 && plan.facilities.length === 0 && plan.pipelines.length === 0 &&
  179. plan.selection.visible === false,
  180. `引导态全空(markers ${plan.markers.length} / legend ${plan.legend.length} / targets ${plan.locateTargets.length})`)
  181. }
  182. // ================================================================ C 图例与标记同源
  183. {
  184. for (const mode of Object.values(MODE_LAYER)) {
  185. const plan = planOf(mode)
  186. const verdict = legendMatchesMarkers(plan.legend, plan.markers)
  187. check(`C1-legend-matches-markers-${mode}`, verdict.ok, verdict.problems.join(';') || '图例与标记逐键一致')
  188. }
  189. }
  190. // ================================================================ D 回联目标
  191. {
  192. const warning = planOf(MODE_LAYER.WARNING_RESPONSE)
  193. check('D1-locate-targets-follow-mode',
  194. warning.locateTargets.length === 2 &&
  195. warning.locateTargets.every((t) => ['MP-A', 'EQ-FREE-1'].includes(t.code)),
  196. `预警处置回联目标 ${warning.locateTargets.map((t) => t.code).join('/')}`)
  197. const infra = planOf(MODE_LAYER.INFRASTRUCTURE)
  198. check('D2-infrastructure-has-no-monitoring-targets',
  199. infra.locateTargets.length === 0,
  200. '基础设施模式下没有监测点回联目标(不拿监测点坐标冒充设施)')
  201. }
  202. // ================================================================ E 降级说明(数据未就绪 ≠ 空数据)
  203. {
  204. const withArrays = planModeLayers({
  205. mode: MODE_LAYER.INFRASTRUCTURE,
  206. objects: [],
  207. facilities: [],
  208. pipelines: [],
  209. entered: true,
  210. })
  211. check('E1-ready-layers-no-note',
  212. withArrays.degradedNotes.length === 0 && withArrays.layerStates.facilities === 'ready',
  213. '设施/管线契约就绪(空数组也是就绪)时无降级说明')
  214. const withoutArrays = planModeLayers({
  215. mode: MODE_LAYER.INFRASTRUCTURE,
  216. objects: [],
  217. facilities: null,
  218. pipelines: null,
  219. entered: true,
  220. })
  221. check('E2-unready-layers-degraded-notes',
  222. withoutArrays.degradedNotes.some((n) => n.key === 'facilities-unready') &&
  223. withoutArrays.degradedNotes.some((n) => n.key === 'pipelines-unready') &&
  224. withoutArrays.layerStates.facilities === 'degraded',
  225. `未就绪说明 ${withoutArrays.degradedNotes.map((n) => n.key).join(',')}`)
  226. }
  227. // ================================================================ F 选择态语义
  228. {
  229. const pointSelection = { level: 'point', pointCode: 'MP-B', deviceCode: '' }
  230. // 总览↔分项:保留身份;目标模式显示不了就暂停(visible=false),切回自动恢复。
  231. const infra = planOf(MODE_LAYER.INFRASTRUCTURE, { selection: pointSelection })
  232. const back = planOf(MODE_LAYER.OVERVIEW, { selection: pointSelection })
  233. check('F1-preserve-suspends-in-infrastructure',
  234. infra.selection.visible === false && back.selection.visible === true,
  235. `选 MP-B:基础设施暂停(visible=${infra.selection.visible})、切回总览恢复(visible=${back.selection.visible})`)
  236. // 分项↔分项:清空。
  237. const t = planModeSelectionTransition({ fromMode: 'monitoring', toMode: 'warningResponse', selection: pointSelection })
  238. check('F2-submode-switch-clears-selection',
  239. t.action === MODE_SELECTION.CLEAR && t.selection.level === 'none',
  240. '分项↔分项清空选择')
  241. const t2 = planModeSelectionTransition({ fromMode: 'overview', toMode: 'monitoring', selection: pointSelection })
  242. check('F3-overview-submode-preserves',
  243. t2.action === MODE_SELECTION.PRESERVE && t2.selection.pointCode === 'MP-B',
  244. '总览↔分项保留选择身份')
  245. // 选中的设备在预警处置隐藏(NORMAL)→ 暂停;报警设备可见。
  246. const normalDeviceSelection = { level: 'equipment', pointCode: 'MP-A', deviceCode: 'EQ-A2' }
  247. const warningHidden = planOf(MODE_LAYER.WARNING_RESPONSE, { selection: normalDeviceSelection })
  248. const alarmDeviceSelection = { level: 'equipment', pointCode: 'MP-A', deviceCode: 'EQ-A1' }
  249. const warningShown = planOf(MODE_LAYER.WARNING_RESPONSE, { selection: alarmDeviceSelection })
  250. check('F4-device-selection-pauses-by-mode',
  251. warningHidden.selection.visible === false && warningShown.selection.visible === true,
  252. `NORMAL 设备选中在预警处置暂停(${warningHidden.selection.visible})、ALARM 设备可见(${warningShown.selection.visible})`)
  253. // 未关联设备选中:基础设施隐藏,预警处置只认异常的那台。
  254. const freeSelection = { level: 'equipment', pointCode: '', deviceCode: 'EQ-FREE-1' }
  255. const infraFree = planOf(MODE_LAYER.INFRASTRUCTURE, { selection: freeSelection })
  256. const warningFree = planOf(MODE_LAYER.WARNING_RESPONSE, { selection: freeSelection })
  257. check('F5-free-equipment-selection',
  258. infraFree.selection.visible === false && warningFree.selection.visible === true,
  259. `未关联设备在基础设施暂停(${infraFree.selection.visible})、预警处置可见(${warningFree.selection.visible})`)
  260. // 回归(真机人工走查抓到):设备级选择 + **不上图**(`no-location`,现网正式边界
  261. // `UNVERIFIED_COORDINATE/renderable=false`)的设备 → 详情仍应可见。
  262. // 详情面板与该设备能否上图无关 —— 一台无地图位置的设备仍要看档案/指标/预警分区。
  263. // 曾误判为不可见导致外壳 `<aside v-if>` 卸载、点击设备卡片后面板直接消失。
  264. const noLocationRows = [
  265. { code: 'EQ-NL-1', pointCode: 'MP-A', status: 'ALARM', placement: 'no-location', longitude: null, latitude: null },
  266. ]
  267. const nlPoint = planModeLayers({
  268. mode: MODE_LAYER.OVERVIEW,
  269. objects: fixtureObjects(),
  270. associatedDevices: noLocationRows,
  271. selection: { level: 'equipment', pointCode: 'MP-A', deviceCode: 'EQ-NL-1' },
  272. entered: true,
  273. })
  274. check('F6-no-location-device-detail-visible',
  275. nlPoint.selection.visible === true,
  276. `不上图设备(no-location)的设备级详情可见(visible=${nlPoint.selection.visible})—— 详情与能否上图无关`)
  277. }
  278. // ================================================================ G 模式判据唯一性(关联设备)
  279. {
  280. const STATUSES = ['ALARM', 'WARNING', 'WORK_ORDER', 'NORMAL', 'OFFLINE', 'UNKNOWN']
  281. check('G1-infrastructure-hides-all-associated',
  282. STATUSES.every((status) => !isAssociatedDeviceVisible('infrastructure', status)),
  283. '基础设施对任何设备状态都隐藏')
  284. check('G2-warning-only-attention-statuses',
  285. isAssociatedDeviceVisible('warningResponse', 'ALARM') &&
  286. isAssociatedDeviceVisible('warningResponse', 'WARNING') &&
  287. !['WORK_ORDER', 'NORMAL', 'OFFLINE', 'UNKNOWN'].some((status) => isAssociatedDeviceVisible('warningResponse', status)),
  288. '预警处置只认设备自身 ALARM/WARNING(未知不猜)')
  289. check('G3-overview-monitoring-show-all',
  290. STATUSES.every((status) =>
  291. isAssociatedDeviceVisible('overview', status) && isAssociatedDeviceVisible('monitoring', status)),
  292. '概览/运行监测显示全部(状态未知也显示,不凭状态隐藏)')
  293. check('G4-attention-predicate-consistent',
  294. isAttentionDeviceStatus('ALARM') && isAttentionDeviceStatus('WARNING') && !isAttentionDeviceStatus('NORMAL'),
  295. '「需要处置」两态判据一致')
  296. }
  297. const failed = checks.filter((item) => !item.pass)
  298. console.log('')
  299. console.log(`T46 模式图层计划检查:${checks.length - failed.length}/${checks.length} 项通过。`)
  300. if (failed.length) {
  301. console.log('未通过:')
  302. for (const item of failed) console.log(` · ${item.name}`)
  303. process.exit(1)
  304. }
  305. console.log('T46 模式图层计划全部通过。')