/** * 票 38 变异闸:把本票要防的错法逐一注入,确认 t38 真的转红;每次变异后按字节还原并校验哈希。 * * 判据只看退出码(不用管道捕获子进程输出——那是本环境的已知边界,票 37 记过)。 * 用法:先单跑 t38 看绿,再跑本脚本看每条变异都被抓住。 * 运行:node .scratch/cockpit-real-data-wayfinding/verify/t38-mutations.mjs */ import { spawnSync } from 'node:child_process' import { readFileSync, writeFileSync } from 'node:fs' import { createHash } from 'node:crypto' import { fileURLToPath } from 'node:url' const ROOT = fileURLToPath(new URL('../../../', import.meta.url)) const GATE = '.scratch/cockpit-real-data-wayfinding/verify/t38-infrastructure-panel-adjustments.mjs' const MUTATIONS = [ { id: 'M1', why: '规模 KPI 回流到基础档案(第 2 条被撤销)→ A2/A6/D1 必须红', file: 'src/modules/cockpit/domain/infrastructurePanels.js', from: " { key: 'lengthKm', label: '管线里程', value: valueText(current.lengthKm), unit: 'km' },\n ]),", to: " { key: 'lengthKm', label: '管线里程', value: valueText(current.lengthKm), unit: 'km' },\n { key: 'scale', label: '规模', value: current.scale ? codeText(current.scale) : DETAIL_PLACEHOLDER },\n ]),", }, { id: 'M2', why: '规模 KPI 回流到总体概览基础设施面板 → A5 必须红', file: 'src/modules/cockpit/domain/overviewPanels.js', from: " { key: 'lengthKm', label: '管线里程', value: valueText(regionData && regionData.lengthKm), unit: 'km', wide: true },\n ]),", to: " { key: 'lengthKm', label: '管线里程', value: valueText(regionData && regionData.lengthKm), unit: 'km', wide: true },\n { key: 'scale', label: '规模', value: '中型' },\n ]),", }, { id: 'M3', why: 'lengthKm 也被顺手删掉(票面明说保留)→ A1b/A2b/A5/D3b 必须红', file: 'src/modules/cockpit/domain/infrastructurePanels.js', from: " { key: 'facilityCount', label: '设施总数', value: valueText(current.facilityCount), unit: '处' },\n { key: 'lengthKm', label: '管线里程', value: valueText(current.lengthKm), unit: 'km' },\n ]),", to: " { key: 'facilityCount', label: '设施总数', value: valueText(current.facilityCount), unit: '处' },\n ]),", }, { id: 'M4', why: '监测点定位退回只换中心的 setCameraFocus(「没反应」的相机根因复发)→ E9/F1 必须红', file: 'src/modules/cockpit/stores/infrastructureStore.js', from: ' const applied = plan.object.kind === MAP_OBJECT_KIND.MONITORING_POINT\n ? await flyPoint(plan.object)\n : focus(target)', to: ' const applied = focus(target)', }, { id: 'M5', why: '相机下发结果不核对(把没飞说成飞了)→ F4/F5 必须红', file: 'src/modules/cockpit/stores/infrastructureStore.js', from: ' if (applied !== true) {\n locate.value = { result: LOCATE_RESULT.UNAVAILABLE, pointCode: plan.pointCode, reasonText: LOCATE_TEXT.UNAVAILABLE }\n return locate.value\n }', to: ' if (false) {\n locate.value = { result: LOCATE_RESULT.UNAVAILABLE, pointCode: plan.pointCode, reasonText: LOCATE_TEXT.UNAVAILABLE }\n return locate.value\n }', }, { id: 'M6', why: '管理信息主图退回普通 bars(可切换维度的图表块被撤,第 6 条不成立)→ C1/C2 必须红', file: 'src/modules/cockpit/domain/infrastructurePanels.js', from: " dimensionBarsBlock('片区 × 行业', rows, {", to: " barsBlock('片区 × 行业', rows, {", }, { id: 'M7', why: '维度聚合退回逐行直画(城南两行不合并)→ C3/C4 必须红', file: 'src/modules/cockpit/domain/panelBlocks.js', from: ' entry.count += Number(countOf(row)) || 0\n for (const [name, source] of Object.entries(extras)) entry[name] += Number(row[source]) || 0\n }\n const sorted = sortByCountDesc([...byValue.values()], (row) => row.count)', to: ' entry.count = Number(countOf(row)) || 0\n for (const [name, source] of Object.entries(extras)) entry[name] = Number(row[source]) || 0\n byValue.set(key, { ...entry })\n }\n const sorted = [...byValue.values()]', }, { id: 'M8', why: 'lengthKm 整行独占**退回按 key 特判**(决定权应从 domain 的 wide 滑回渲染层)→ A3b 必须红', file: 'src/modules/cockpit/components/PanelBlocks.vue', from: " :class=\"{ 'panel-kpi--wide': item.wide === true }\"", to: " :class=\"{ 'panel-kpi--wide': item.key === 'lengthKm' }\"", }, { id: 'M9', why: '浮层被重新门控在折叠状态上(「点了没反应」换写法复发)→ E6c 必须红', file: 'src/modules/cockpit/CockpitShell.vue', from: '