/**
* 票 44 变异闸:把守卫要防的错法逐一注入,确认 t44 真的转红;每次变异后按字节还原并校验哈希。
*
* 判据只看退出码(不用管道捕获子进程输出——那是本环境的已知边界)。
* 用法:node .scratch/cockpit-real-data-wayfinding/verify/t44-mutations.mjs
* 先单独跑一次 t44 看绿,再跑本脚本看每条变异都被抓住。
*/
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/t44-distribution-ranking.mjs'
const MUTATIONS = [
{
id: 'M1',
file: 'src/modules/cockpit/domain/panelBlocks.js',
from: ' .sort((a, b) => b.count - a.count || a.index - b.index)',
to: ' .sort((a, b) => a.index - b.index)',
why: '排序改恒等(遗漏降序)→ RANK 组必须全红',
},
{
id: 'M2',
file: 'src/modules/cockpit/domain/abnormalCounts.js',
from: ' }))',
to: ' })).sort((a, b) => b.count - a.count)',
why: '把固定语义序的异常桶强行降序(非目标第 2 条)→ FIXED 组必须红',
},
{
id: 'M3',
file: 'src/modules/cockpit/components/PanelBlocks.vue',
from: ' gap: calc(4 * var(--u, 1px));\n}\n\n.panel-bar__label {',
to: ' gap: var(--space-2, 8px);\n}\n\n.panel-bar__label {',
why: '文字与统计条退回宽间距(收紧被撤销)→ B3 必须红',
},
{
id: 'M4',
file: 'src/modules/cockpit/components/PanelBlocks.vue',
from: ' box-shadow: inset -1px 0 0 rgba(255, 255, 255, 0.85);\n',
to: '',
why: '删掉条段端帽(分割消失)→ B4b 必须红',
},
{
id: 'M5',
file: 'src/modules/cockpit/components/PanelBlocks.vue',
from: ' box-shadow: inset 0 0 0 1px rgba(1, 171, 234, 0.12);\n',
to: '',
why: '删掉轨道发丝边(条段无容器边界)→ B4 必须红',
},
{
id: 'M6',
file: 'src/modules/cockpit/components/PanelBlocks.vue',
from: ' ',
to: ' {{ row.label }}',
why: '行标签退回裸 span(票 37 省略接缝被视觉改动带走)→ B7 必须红',
},
]
const hash = (path) => createHash('sha256').update(readFileSync(path)).digest('hex')
/** 变异源文本按 CRLF 归一后再注入(票 37 的教训:PowerShell 写盘是 CRLF,锚点含 \n 会假未命中)。 */
const normalizeFor = (text, path) => (readFileSync(path, 'utf8').includes('\r\n') ? text.replace(/\n/g, '\r\n') : text)
let failures = 0
for (const m of MUTATIONS) {
const path = ROOT + m.file.replaceAll('/', process.platform === 'win32' ? '\\' : '/')
const before = hash(path)
const source = readFileSync(path, 'utf8')
const needle = normalizeFor(m.from, path)
if (!source.includes(needle)) {
console.log(`BAD ${m.id}: 变异锚点未命中(源码已变,需更新脚本)`)
failures += 1
continue
}
writeFileSync(path, source.replace(needle, normalizeFor(m.to, path)))
const status = spawnSync(process.execPath, [GATE], { cwd: ROOT, stdio: 'ignore' }).status
writeFileSync(path, source)
const restored = hash(path) === before
const red = status !== 0
console.log(`${red && restored ? 'OK ' : 'BAD '} ${m.id} ${m.why} → 闸门${red ? '转红' : '未转红'};还原${restored ? '按字节一致' : '**不一致**'}`)
if (!red || !restored) failures += 1
}
console.log('')
console.log(failures ? `T44 MUTATIONS: ${failures} FAILED` : `T44 MUTATIONS: ${MUTATIONS.length}/${MUTATIONS.length} 全部被抓住`)
process.exit(failures ? 1 : 0)