t54-mutations.mjs 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. /**
  2. * 票 54 变异闸:把本票要防的错法逐一注入,确认 t54 真的一致红;
  3. * 每条变异后按字节还原并校验哈希。判据只看退出码(不管道捕获子进程输出,票 37 的沙箱教训)。
  4. *
  5. * node .scratch/cockpit-real-data-wayfinding/verify/t54-mutations.mjs
  6. */
  7. import { spawnSync } from 'node:child_process'
  8. import { readFileSync, writeFileSync } from 'node:fs'
  9. import { createHash } from 'node:crypto'
  10. import { fileURLToPath } from 'node:url'
  11. const ROOT = fileURLToPath(new URL('../../../', import.meta.url))
  12. const T54 = '.scratch/cockpit-real-data-wayfinding/verify/t54-monitoring-overview-grid.mjs'
  13. const PANELS = 'src/modules/cockpit/domain/monitoringPanels.js'
  14. const RENDERER = 'src/modules/cockpit/components/PanelBlocks.vue'
  15. const MUTATIONS = [
  16. {
  17. id: 'M1',
  18. file: PANELS,
  19. from: ' return { key: \'createdAt\', label: \'数据时间\', value: shortEventTime(value) || full, full }',
  20. to: ' return { key: \'createdAt\', label: \'数据时间\', value: full, full }',
  21. why: '屏上退回 ISO 全串(2×2 半格里必然省略成半截读数,验收第 1 条的原始诉求落空)→ G1a 必须红',
  22. },
  23. {
  24. id: 'M2',
  25. file: PANELS,
  26. from: ' return { key: \'createdAt\', label: \'数据时间\', value: shortEventTime(value) || full, full }',
  27. to: ' return { key: \'createdAt\', label: \'数据时间\', value: shortEventTime(value) || full }',
  28. why: '完整原文字段丢失(title 只剩短读法,「语义全部保持」被破坏)→ G1b/G1c 必须红',
  29. },
  30. {
  31. id: 'M3',
  32. file: PANELS,
  33. from: " if (full === DETAIL_PLACEHOLDER) return { key: 'createdAt', label: '数据时间', value: full, full }",
  34. to: " if (full === DETAIL_PLACEHOLDER) return { key: 'createdAt', label: '数据时间', value: full, full: '' }",
  35. why: '缺值分支的完整原文置空(占位符的 title 契约破坏)→ G1d 必须红',
  36. },
  37. {
  38. id: 'M4',
  39. file: PANELS,
  40. from: ' ], { layout: \'two-by-two\' }),',
  41. to: ' ]),',
  42. why: 'domain 不再声明 2×2(退回 auto-fit,宽面板又展开成一行)→ G2a 必须红',
  43. },
  44. {
  45. id: 'M5',
  46. file: PANELS,
  47. from: " createdAtKpi(current.createdAt),\n { key: 'offlineCount'",
  48. to: " { key: 'createdAt', label: '数据时间', value: valueText(current.createdAt) },\n { key: 'offlineCount'",
  49. why: '运行概况另起一套写法(两处数据时间早晚漂移)→ G1f 必须红',
  50. },
  51. {
  52. id: 'M6',
  53. file: RENDERER,
  54. from: ' return `${item.full || item.value}${unit}`',
  55. to: ' return `${item.value}${unit}`',
  56. why: 'title 退回只显示值(domain 的 full 没人消费,hover 读不到完整 ISO)→ G4c 必须红',
  57. },
  58. {
  59. id: 'M7',
  60. file: RENDERER,
  61. from: ":class=\"{ 'is-muted': isPlaceholder(item.value) }\"",
  62. to: ":class=\"{ 'is-muted': isPlaceholder(item.value), 'panel-kpi__value--time': item.key === 'createdAt' }\"",
  63. why: '渲染层重新按 item.key 特判(票 38 明令反对的第二份布局语义)→ G2d/G3c 必须红',
  64. },
  65. {
  66. id: 'M8',
  67. file: RENDERER,
  68. from: '.panel-kpi__value {\n min-width: 0;',
  69. to: '.panel-kpi__value--time {\n font-size: var(--fs-caption, 10.5px);\n}\n\n.panel-kpi__value {\n min-width: 0;',
  70. why: '时间降档规则被复活(数据时间又与四卡不同档)→ G3a/G3c 必须红',
  71. },
  72. {
  73. id: 'M9',
  74. file: RENDERER,
  75. from: ' grid-template-columns: repeat(2, minmax(0, 1fr));\n container-type: inline-size;',
  76. to: ' grid-template-columns: repeat(2, 1fr);\n container-type: inline-size;',
  77. why: '列宽去掉 minmax(0,·)(auto 最小尺寸把长读数顶出横向溢出,验收第 2 条)→ G2e 必须红',
  78. },
  79. {
  80. id: 'M10',
  81. file: RENDERER,
  82. from: ' grid-template-columns: repeat(2, minmax(0, 1fr));\n container-type: inline-size;',
  83. to: ' grid-template-columns: repeat(auto-fit, minmax(calc(78 * var(--u, 1px)), 1fr));\n container-type: inline-size;',
  84. why: '两列改回 auto-fit(宽面板不固定 2×2)→ G2e 必须红',
  85. },
  86. {
  87. id: 'M11',
  88. file: RENDERER,
  89. from: '@container panel-kpi-grid (max-width: 260px) {',
  90. to: '@media (max-width: 260px) {',
  91. why: '降级退回视口 media query(面板栏宽固定 280–460px,这条规则在大屏永不生效=假完成)→ G2f/G2g 必须红',
  92. },
  93. {
  94. id: 'M12',
  95. file: RENDERER,
  96. from: ' .panel-kpis--two-by-two .panel-kpi {\n grid-column: 1 / -1;\n }',
  97. to: ' .panel-kpis--two-by-two {\n grid-template-columns: minmax(0, 1fr);\n }',
  98. why: '把降级写在容器自身上(CSS containment 规定容器查不了自己的尺寸,规则永不命中)→ G2h 必须红',
  99. },
  100. {
  101. id: 'M13',
  102. file: RENDERER,
  103. from: '.panel-blocks {\n /* 图表 hover 详情浮层',
  104. to: '.panel-blocks {\n container-type: inline-size;\n /* 图表 hover 详情浮层',
  105. why: '给 .panel-blocks 根加 containment(contain:layout 让它成为绝对定位后代的包含块,.chart-tip 基准被换掉)→ G4f 必须红',
  106. },
  107. ]
  108. const hash = (path) => createHash('sha256').update(readFileSync(path)).digest('hex')
  109. /** 变异源文本按 CRLF 归一后再注入(票 37 的教训:锚点含 \n 而源文件是 CRLF 会假未命中)。 */
  110. const normalizeFor = (text, path) => (readFileSync(path, 'utf8').includes('\r\n') ? text.replace(/\n/g, '\r\n') : text)
  111. let failures = 0
  112. for (const m of MUTATIONS) {
  113. const path = ROOT + m.file.replaceAll('/', process.platform === 'win32' ? '\\' : '/')
  114. const before = hash(path)
  115. const source = readFileSync(path, 'utf8')
  116. const needle = normalizeFor(m.from, path)
  117. if (!source.includes(needle)) {
  118. console.log(`BAD ${m.id}: 变异锚点未命中(源码已变,需更新脚本)`)
  119. failures += 1
  120. continue
  121. }
  122. writeFileSync(path, source.replace(needle, normalizeFor(m.to, path)))
  123. const caught = spawnSync(process.execPath, [T54], { cwd: ROOT, stdio: 'ignore' }).status !== 0
  124. writeFileSync(path, source)
  125. const restored = hash(path) === before
  126. if (!caught) {
  127. console.log(`MISS ${m.id}: 注入后闸门仍全绿 → 守卫没有牙(${m.why})`)
  128. failures += 1
  129. } else if (!restored) {
  130. console.log(`BAD ${m.id}: 还原后哈希不一致,文件状态可疑,立刻人工核对`)
  131. failures += 1
  132. } else {
  133. console.log(`OK ${m.id} ${m.why} → 闸门转红;还原按字节一致`)
  134. }
  135. }
  136. console.log(`\nT54 MUTATIONS: ${MUTATIONS.length - failures}/${MUTATIONS.length} 全部被抓住`)
  137. process.exitCode = failures ? 1 : 0