/** * 票 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)