/** * 票 44 离线闸门:类型/类别分布块降序的**完备性普查** + 统计条视觉密度守卫。 * * 票 37 的 t50 只钉住了「监测概览 · 设备类型分布」这一块。本票范围第 1 条要的是 * 「**所有**类型分布统计块真的降序」——只测一块等于没测完备性(票 20 的教训: * 测试与实现犯同一个错时闸门照样全绿)。做法: * * A. 普查(行为层,不是正则):把**故意打乱次序**的后端载荷灌进真实适配层 * (`toXxx` → `xxxPanel`),对每一块 bars/donut/stacked 断言精确次序。两类期望: * · RANK —— 排名类别(类型/行业/片区/区域):计数从多到少、并列保序; * · FIXED —— 固定语义序(状态/等级/生命周期/建成年限/工单 0–7/异常桶): * 与输入逐位相同(非目标第 2 条:不许强行改降序)。 * 期望表是**穷举**的:表外的分布块判红、表内没出现的期望(幽灵条目)也判红—— * 新增块必须显式归类,这就是「补齐遗漏块」的机制,不靠人记得去查。 * B. 视觉密度(静态守卫):行内收紧、轨道发丝边、填充端帽/高光、辉光删除, * 且 `EllipsisText` / `panel-action` / `data-bar-*` 三条接缝未被视觉改动带走。 * * 变异(守卫有牙): * M1 `sortByCountDesc` 改恒等 → 所有 RANK 块判红; * M2 把 FIXED 块也套上降序(如异常桶/等级)→ 对应 FIXED 判红; * M3 `.panel-bar` 的 gap 改回 `var(--space-2)` → B1 红; * M4 删 `.panel-bar__fill::after`(端帽/分割层)→ B2b 红; * M5 轨道内描边删除 → B2 红; * M6 行标签退回裸 `` → B4 红。 * * 运行:node .scratch/cockpit-real-data-wayfinding/verify/t44-distribution-ranking.mjs */ import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { barsBlock, sortByCountDesc } from '../../../src/modules/cockpit/domain/panelBlocks.js' import { archivePanel, emergencyPanel, managementPanel, operationPanel, toBaseArchives, toEmergencySupport, toOperatingInformation, } from '../../../src/modules/cockpit/domain/infrastructurePanels.js' import { monitoringDistributionPanel, monitoringOverviewPanel, operationOverviewPanel, toMonitoringDistribution, toMonitoringOverview, toMonitoringSituation, } from '../../../src/modules/cockpit/domain/monitoringPanels.js' import { alertSettingsPanel, infrastructureSummaryPanel, monitoringSummaryPanel, situationSummaryPanel, toOverviewInfrastructure, toOverviewMonitoring, toOverviewRegions, toOverviewWarningSettings, } from '../../../src/modules/cockpit/domain/overviewPanels.js' import { eventAnalysisPanel, historyAnalysisPanel, responseEfficiencyPanel, toEventAnalysis, toEventHistory, toHandlingEfficiency, } from '../../../src/modules/cockpit/domain/warningResponsePanels.js' import { SECTION_STATE, section } from '../../../src/modules/cockpit/domain/sectionState.js' const read = (rel) => readFileSync(new URL(`../../../src/modules/cockpit/${rel}`, import.meta.url), 'utf8') const sv = (data) => section(SECTION_STATE.SUCCESS, { data }) let passed = 0 let failed = 0 function check(name, condition, message) { if (condition) { passed += 1 console.log(`PASS ${name}`) } else { failed += 1 console.error(`FAIL ${name}: ${message}`) } } const isDesc = (counts) => counts.every((c, i) => i === 0 || counts[i - 1] >= c) /** * 普查一个面板视图: * · 每个分布块(bars/donut/stacked)的标题必须**恰好命中一条**期望(表外判红); * · 命中后比对精确次序(counts 与 keys 都断言); * · RANK 额外断言单调降序,FIXED 额外断言「不是碰巧也降序」(期望本身必须非单调, * 否则它证明不了没被重排——这是本组断言的牙)。 * @param {string} name 面板名 * @param {object} view 视图计划 * @param {Array<{label: string, mode: 'RANK'|'FIXED', counts: number[], keys?: string[]}>} expect 期望表 */ function audit(name, view, expect) { const blocks = ((view && view.blocks) || []).filter((b) => ['bars', 'donut', 'stacked'].includes(b.kind)) const seen = new Set() const orphans = [] for (const block of blocks) { const hit = expect.filter((e) => e.label === block.label) if (hit.length !== 1) { orphans.push(`${block.label}(命中 ${hit.length} 条期望)`) continue } const e = hit[0] seen.add(e.label) const counts = block.rows.map((r) => r.count) const keys = block.rows.map((r) => r.key) let detail = `「${block.label}」次序 ${counts.join('、')}` if (e.keys) detail += `;键 ${keys.join('、')}` check( `A-${name}-${e.label}`, JSON.stringify(counts) === JSON.stringify(e.counts) && (!e.keys || JSON.stringify(keys) === JSON.stringify(e.keys)), `${detail}(期望 ${e.counts.join('、')}${e.keys ? `;键 ${e.keys.join('、')}` : ''})`, ) if (e.mode === 'RANK') { check(`A-${name}-${e.label}-monotone`, isDesc(e.counts), `RANK 期望本身必须降序:${e.counts.join('、')}`) } else { check(`A-${name}-${e.label}-not-desc`, !isDesc(e.counts), `FIXED 期望必须**非单调**,否则证明不了没被强行降序:${e.counts.join('、')}`) } } check(`A-${name}-no-unclassified-block`, orphans.length === 0, `存在未归类的分布块:${orphans.join(';')} —— 新增块必须在期望表里显式声明 RANK/FIXED`) const ghosts = expect.filter((e) => !seen.has(e.label)).map((e) => e.label) check(`A-${name}-no-ghost-expectations`, ghosts.length === 0, `期望表里有面板不存在的块(块被改名/删除,表在骗人):${ghosts.join('、')}`) } // ================================================================ A. 真实适配层 + 打乱载荷 // ---- A1 运行监测 · 监测概览(设备类型分布:并列 5、5 必须保持传入次序) { const data = toMonitoringOverview({ counts: { monitoringPointCount: 6, equipmentCount: 11 }, status: { onlineCount: 8, offlineCount: 1, staleCount: 1, unknownOrMissingCount: 1, denominator: 11 }, onlineRate: { value: '0.7273', numerator: 8, denominator: 11, state: 'AVAILABLE' }, typeDistribution: [ { deviceType: { code: 'SMALL', name: '少' }, industry: 'GAS', deviceCount: 1 }, { deviceType: { code: 'TIE_A', name: '并列甲' }, industry: 'GAS', deviceCount: 5 }, { deviceType: { code: 'TIE_B', name: '并列乙' }, industry: 'WATER_SUPPLY', deviceCount: 5 }, { deviceType: { code: 'MID', name: '中' }, industry: 'DRAINAGE', deviceCount: 3 }, ], mapFilter: {}, excluded: {}, freshness: {}, }) const view = monitoringOverviewPanel(data, sv(data)) audit('monitoring-overview', view, [ { label: '设备类型分布', mode: 'RANK', counts: [5, 5, 3, 1], keys: ['TIE_A', 'TIE_B', 'MID', 'SMALL'] }, ]) } // ---- A2 运行监测 · 监测分布(片区/行业/设备类型降序;异常桶固定语义序) { const data = toMonitoringDistribution({ districtDistribution: [ { district: 0, pointCount: 1, deviceCount: 1 }, { district: 1, pointCount: 4, deviceCount: 4 }, { district: 2, pointCount: 2, deviceCount: 2 }, ], industryDistribution: [ { industry: 'GAS', pointCount: 3, deviceCount: 3 }, { industry: 'WATER_SUPPLY', pointCount: 6, deviceCount: 6 }, { industry: 'DRAINAGE', pointCount: 2, deviceCount: 2 }, ], deviceTypeDistribution: [ { deviceType: { code: 'A', name: '型A' }, industry: 'GAS', pointCount: 1, deviceCount: 1 }, { deviceType: { code: 'B', name: '型B' }, industry: 'GAS', pointCount: 5, deviceCount: 5 }, { deviceType: { code: 'C', name: '型C' }, industry: 'GAS', pointCount: 3, deviceCount: 3 }, ], pointStatusDistribution: [], monitoringPoints: [], unassociatedEquipment: [], pagination: { page: 1, pageSize: 20, total: 0, totalPages: 0, hasMore: false }, mapFilter: {}, freshness: {}, excluded: {}, }) const view = monitoringDistributionPanel(data, sv(data), { statusDistribution: { available: true, counts: { ALARM: 9, WARNING: 2, OFFLINE: 1, EMPTY: 4 } }, selectedStatuses: [], }) audit('monitoring-distribution', view, [ { label: '覆盖情况 · 行业', mode: 'RANK', counts: [6, 3, 2] }, { label: '覆盖情况 · 片区', mode: 'RANK', counts: [4, 2, 1] }, { label: '覆盖情况 · 设备类型', mode: 'RANK', counts: [5, 3, 1] }, // 异常桶=状态语义序(ALARM→WARNING→OFFLINE→EMPTY),不许被降序打乱。 { label: '监测点异常统计(全局未筛)', mode: 'FIXED', counts: [9, 2, 1, 4] }, ]) } // ---- A3 运行监测 · 运行概况(指标五桶与状态分布都是字典固定序) { const data = toMonitoringSituation({ // 故意乱序灌入:适配层按六态字典序重建,视图必须原样带出。 metricStatusDistribution: [ { status: 'OFFLINE', count: 8 }, { status: 'WARNING', count: 7 }, { status: 'NORMAL', count: 1 }, { status: 'ALARM', count: 3 }, { status: 'EXPIRED', count: 2 }, { status: 'UNKNOWN', count: 0 }, ], abnormalEquipment: [], warningTrend: [], alarmTrend: [], pagination: { total: 0 }, freshness: {}, excluded: {}, }) const view = operationOverviewPanel(data, sv(data)) audit('monitoring-situation', view, [ { label: '设备/指标异常统计', mode: 'FIXED', counts: [7, 3, 2, 8, 0] }, { label: '异常态势 · 指标状态分布', mode: 'FIXED', counts: [1, 7, 3, 2, 8, 0] }, ]) } // ---- A4 总体概览(运行监测类型分布 + 基础设施行业构成/片区分布 + 态势总览无分布块) { const monitoring = toOverviewMonitoring({ counts: { deviceCount: 11, monitoringPointCount: 6, monitoringFacilityCount: 3, infrastructureFacilityCount: 5 }, status: { online: 8, offline: 1, stale: 1, unknownOrMissing: 1, denominator: 11 }, onlineRate: { value: '0.7273', onlineCount: 8, denominator: 11, state: 'AVAILABLE' }, formalScope: { industries: ['GAS'] }, metricFreshness: {}, typeDistribution: [ { industry: { code: 'GAS', label: '燃气' }, deviceType: { code: 'A', name: '型A' }, deviceCount: 1 }, { industry: { code: 'GAS', label: '燃气' }, deviceType: { code: 'B', name: '型B' }, deviceCount: 6 }, { industry: { code: 'WATER_SUPPLY', label: '供水' }, deviceType: { code: 'C', name: '型C' }, deviceCount: 4 }, ], excluded: {}, }) const infra = toOverviewInfrastructure({ totals: { facilityCount: 9, smartDeviceCount: 11 }, industries: [ { code: 'DRAINAGE', label: '排水', key: 'drainage', facilityCount: 1, smartDeviceCount: 2 }, { code: 'GAS', label: '燃气', key: 'gas', facilityCount: 5, smartDeviceCount: 4 }, { code: 'WATER_SUPPLY', label: '供水', key: 'water', facilityCount: 3, smartDeviceCount: 9 }, ], excluded: {}, }) const regions = toOverviewRegions({ totals: { facilityCount: 9, lengthKm: 12.4, scale: 'medium' }, districtDistribution: [ { district: 'UNASSIGNED', count: 1 }, { district: 2, count: 4 }, { district: 0, count: 3 }, ], ageReference: {}, excluded: {}, }) audit('overview-monitoring', monitoringSummaryPanel(monitoring, sv(monitoring)), [ { label: '类型分布 · 智能设备', mode: 'RANK', counts: [6, 4, 1] }, ]) audit('overview-infrastructure', infrastructureSummaryPanel({ infrastructure: infra, regions }, sv(infra)), [ { label: '行业构成 · 设施', mode: 'RANK', counts: [5, 3, 1] }, { label: '行业构成 · 智能设备', mode: 'RANK', counts: [9, 4, 2] }, { label: '片区分布', mode: 'RANK', counts: [4, 3, 1] }, ]) // 态势总览没有分布块(KPI+趋势);空期望表 = 出现任何未声明的分布块即红。 audit('overview-situation', situationSummaryPanel({ infrastructure: infra, monitoring, warningSettings: null }, sv(null)), []) } // ---- A5 总体概览 · 预警设置(票 42 生命周期四块:状态/等级固定序,行业/片区降序) { const data = toOverviewWarningSettings({ warning: { eventFamily: 'WARNING', state: 'READY', total: 31, handled: null, pending: null, cancelledCount: null, unlinkedEventCount: null, workOrderStates: null, handlingEfficiency: { value: '0.6129', state: 'AVAILABLE', numerator: 19, denominator: 31 }, statusDistribution: [ { code: 'UNHANDLED', label: '待处置', count: 12 }, { code: 'HANDLED', label: '已解除', count: 19 }, ], levelDistribution: [ { code: '1', label: '1 级', count: 2 }, { code: '2', label: '2 级', count: 9 }, ], industryDistribution: [ { code: 'GAS', label: '燃气', count: 4 }, { code: 'DRAINAGE', label: '排水', count: 17 }, { code: 'WATER_SUPPLY', label: '供水', count: 10 }, ], districtDistribution: [ { code: '0', label: '城南', count: 1 }, { code: '1', label: '城北', count: 20 }, ], trend: [], }, alarm: { eventFamily: 'ALARM', state: 'READY', total: 66, handled: 40, pending: 20, cancelledCount: 3, unlinkedEventCount: 3, workOrderStates: { 0: 5, 1: 4, 2: 6, 3: 7, 4: 8, 5: 40, 6: 2, 7: 3 }, handlingEfficiency: { value: '0.6061', state: 'AVAILABLE', numerator: 40, denominator: 66 }, statusDistribution: [ { code: 'UNHANDLED', label: '未处理', count: 26 }, { code: 'HANDLED', label: '已处理', count: 40 }, ], levelDistribution: [ { code: '1', label: '1 级', count: 26 }, { code: '2', label: '2 级', count: 40 }, ], industryDistribution: [ { code: 'GAS', label: '燃气', count: 26 }, { code: 'DRAINAGE', label: '排水', count: 40 }, ], districtDistribution: [ { code: '0', label: '城南', count: 6 }, { code: '1', label: '城北', count: 60 }, ], trend: [], }, freshness: {}, range: { type: 'LAST_7_DAYS' }, }) const view = alertSettingsPanel(data, sv(data)) audit('alert-settings', view, [ { label: '预警 · 生命周期状态', mode: 'FIXED', counts: [12, 19] }, { label: '预警 · 等级分布', mode: 'FIXED', counts: [2, 9] }, { label: '预警 · 行业分布', mode: 'RANK', counts: [17, 10, 4] }, { label: '预警 · 片区分布', mode: 'RANK', counts: [20, 1] }, { label: '报警 · 工单状态分布', mode: 'FIXED', counts: [5, 4, 6, 7, 8, 40, 2, 3] }, { label: '报警 · 生命周期状态', mode: 'FIXED', counts: [26, 40] }, { label: '报警 · 等级分布', mode: 'FIXED', counts: [26, 40] }, { label: '报警 · 行业分布', mode: 'RANK', counts: [40, 26] }, { label: '报警 · 片区分布', mode: 'RANK', counts: [60, 6] }, ]) } // ---- A6 预警处置 · 预警分析(类型/行业/片区降序;等级是语义阶梯固定序) { const data = toEventAnalysis({ dimensions: [ { dimensionCode: 'EVENT_TYPE', buckets: [ { bucketCode: 'T1', bucketLabel: '类型甲', count: 2 }, { bucketCode: 'T2', bucketLabel: '类型乙', count: 8 }, { bucketCode: 'T3', bucketLabel: '类型丙', count: 5 }, ] }, { dimensionCode: 'LEVEL', buckets: [ { bucketCode: '1', bucketLabel: '1 级', count: 1 }, { bucketCode: '2', bucketLabel: '2 级', count: 9 }, ] }, { dimensionCode: 'INDUSTRY', buckets: [ { bucketCode: 'GAS', bucketLabel: '燃气', count: 3 }, { bucketCode: 'DRAINAGE', bucketLabel: '排水', count: 11 }, ] }, { dimensionCode: 'DISTRICT', buckets: [ { bucketCode: '0', bucketLabel: '城南', count: 7 }, { bucketCode: '1', bucketLabel: '城北', count: 4 }, ] }, ], highlights: [], }) audit('event-analysis', eventAnalysisPanel(data, sv(data)), [ { label: '按事件类型', mode: 'RANK', counts: [8, 5, 2] }, { label: '按行业分布', mode: 'RANK', counts: [11, 3] }, { label: '按事件等级', mode: 'FIXED', counts: [1, 9] }, { label: '按片区分布', mode: 'RANK', counts: [7, 4] }, ]) } // ---- A7 基础设施 · 基础档案(行业/片区降序;建成年限是时间轴分桶固定序) { const data = toBaseArchives({ totals: { facilityCount: 9, lengthKm: 12.4, scale: 'medium' }, industryDistribution: [ { code: 'MANHOLE', count: 1 }, { code: 'GAS', count: 6 }, { code: 'DRAINAGE', count: 3 }, ], districtDistribution: [ { district: 2, count: 2 }, { district: 0, count: 5 }, ], ageDistribution: [ { code: '20_YEARS_OR_OVER', count: 9 }, { code: 'UNDER_5_YEARS', count: 1 }, { code: '10_TO_20_YEARS', count: 4 }, { code: '5_TO_10_YEARS', count: 2 }, ], ageReference: {}, excluded: {}, }) audit('base-archives', archivePanel(data, sv(data)), [ { label: '行业分类', mode: 'RANK', counts: [6, 3, 1] }, { label: '片区分布', mode: 'RANK', counts: [5, 2] }, { label: '档案摘要 · 建成年限', mode: 'FIXED', counts: [9, 1, 4, 2] }, ]) } // ---- A8 基础设施 · 应急保障(区域分布降序) { const data = toEmergencySupport({ totals: { hazardSourceCount: 3, protectionTargetCount: 2, emergencyResourceCount: 1, totalObjectCount: 6 }, sections: { hazardSources: { state: 'EMPTY', items: [] }, protectionTargets: { state: 'EMPTY', items: [] }, emergencyResources: { state: 'EMPTY', items: [] }, }, industryDistribution: [{ industry: 'GAS', count: 1 }], areaDistribution: [ { area: '城东', count: 2 }, { area: '城南', count: 9 }, { area: '城北', count: 5 }, ], eventSummary: {}, }) audit('emergency', emergencyPanel(data, sv(data)), [ { label: '区域分布', mode: 'RANK', counts: [9, 5, 2] }, ]) } // ---- A9 预警处置 · 预警效率(工单 0–7 全铺字典序,计数最大不得置顶) { const data = toHandlingEfficiency({ handlingEfficiency: { value: '0.5', state: 'AVAILABLE', numerator: 3, denominator: 6 }, workOrderStateCounts: [ { stateCode: '1', count: 9 }, { stateCode: '0', count: 1 }, { stateCode: '5', count: 3 }, ], trend: [], }) audit('response-efficiency', responseEfficiencyPanel(data, sv(data)), [ { label: '工单状态分布', mode: 'FIXED', counts: [1, 9, 0, 0, 0, 3, 0, 0] }, ]) } // ---- A10 无分布块的面板也进普查:期望表为空,长出未声明的分布块即红(完备性反向锁定) { const history = historyAnalysisPanel(toEventHistory({ data: [], trend: [] }), sv(null)) audit('history-analysis', history, []) const management = managementPanel( { facilityCount: 9, equipmentCount: 11, districtIndustry: [{ district: 0, industry: 'GAS', facilityCount: 5, equipmentCount: 2 }] }, sv(null), ) audit('management', management, []) // 运行信息:KPI + 指标清单(票 38 才把它图表化);现在长出分布块就是未声明的改动。 const operation = operationPanel( toOperatingInformation({ industries: [{ code: 'GAS', label: '燃气', metrics: [] }] }), sv(null), ) audit('operation', operation, []) } // ================================================================ B. 并列稳定性 + 视觉密度守卫 { // B1 并列保序由代码结构保证(票 37 条文在本票复验):全并列原样带出、与码值无关。 const rows = [{ code: 'A', count: 5 }, { code: 'B', count: 5 }, { code: 'C', count: 5 }] assert.deepEqual(sortByCountDesc(rows, (r) => r.count).map((r) => r.code), ['A', 'B', 'C'], '全并列必须原样带出') const rev = [{ code: 'C', count: 5 }, { code: 'B', count: 5 }, { code: 'A', count: 5 }] assert.deepEqual(sortByCountDesc(rev, (r) => r.count).map((r) => r.code), ['C', 'B', 'A'], '并列次序跟传入,不跟码值') check('B1-tie-stability-by-structure', true, '并列保序') // B2 并列行条长相同(视觉可比较);全 0 不产生 NaN。 const block = barsBlock('x', [{ label: '甲', count: 5 }, { label: '乙', count: 5 }, { label: '丙', count: 1 }]) check('B2-tie-ratio-equal', block.rows[0].ratio === block.rows[1].ratio && block.rows[0].ratio === 1, '并列计数得到相同条长,最大项为 1') const zero = barsBlock('x', [{ label: '甲', count: 0 }]) check('B2b-all-zero-no-nan', Number.isFinite(zero.rows[0].ratio) && zero.rows[0].ratio === 0, '全 0 时条长 0,不得 NaN') } { const panelVue = read('components/PanelBlocks.vue') /** 取某选择器的声明块(简单括号配对;找不到回空串,断言会因此判红而不是漏判)。 */ const ruleOf = (selector) => { const at = panelVue.indexOf(`\n${selector} {`) if (at < 0) return '' const open = panelVue.indexOf('{', at) let depth = 0 for (let i = open; i < panelVue.length; i += 1) { if (panelVue[i] === '{') depth += 1 else if (panelVue[i] === '}') { depth -= 1 if (depth === 0) return panelVue.slice(open + 1, i) } } return '' } // B3 每行文字与统计条更紧密:列间距 8u(--space-2) → 4u,且不再引用宽间距 token。 const barRule = ruleOf('.panel-bar') check('B3-bar-row-tightened', /gap:\s*calc\(4 \* var\(--u/.test(barRule) && !/gap:\s*var\(--space-2/.test(barRule), `条形行的列间距必须收到 4u(文字与条贴合),当前:${barRule.trim() || '(找不到 .panel-bar 规则)'}`) // B4 条段之间有明确分割:轨道内发丝描边(起止容器)+ 填充右端头帽与顶部高光(装饰层不挡悬停)。 const trackRule = ruleOf('.panel-bar__track') const fillRule = ruleOf('.panel-bar__fill') const capRule = ruleOf('.panel-bar__fill::after') check('B4-track-hairline-boundary', /box-shadow:\s*inset 0 0 0 1px/.test(trackRule), '轨道必须有 1u 内描边,条段才看得出容器边界') check('B4b-fill-cap-and-layer', /inset -1px 0 0/.test(capRule) && /linear-gradient\(180deg/.test(capRule) && /pointer-events:\s*none/.test(capRule), '填充需要右端头帽 + 顶部高光装饰层(轻量层次),且不挡悬停') check('B4c-fill-is-positioning-parent', /position:\s*relative/.test(fillRule), '装饰层贴填充本体,填充需为定位父级') // B5 越界辉光删除:旧 `0 0 8px` 渗过 2u 行距糊进相邻条,正是「条段无分割」的直接成因。 check('B5-no-bleeding-glow', !/box-shadow:\s*0 0 8px/.test(fillRule), '条形填充不得再带 8px 外辉光(跨行渗色 = 分割不清)') // B6 读数密度与可读性不牺牲:读数列仍右对齐等宽;行距维持 2u(收紧的是行内)。 const countRule = ruleOf('.panel-bar__count') const barsRule = ruleOf('.panel-bars') check('B6-reading-density-kept', /text-align:\s*right/.test(countRule) && /font-family:\s*var\(--num/.test(countRule), '读数列保持右对齐等宽数字') check('B6b-row-pitch-unchanged', /gap:\s*calc\(2 \* var\(--u/.test(barsRule), '行间距维持 2u(不把行堆挤)') check('B6c-track-height-6u', /height:\s*calc\(6 \* var\(--u/.test(trackRule), '轨道 5u→6u 承载描边与端帽,但不越过读数行高') // B7 三条接缝原样(票 37 收口条件:EllipsisText / panel-action / data-bar-* 不变)。 check('B7-ellipsis-seam-intact', //.test(panelVue), '条形行标签必须仍走共用省略方法') check('B7b-panel-action-seam-intact', /emit\('panel-action', \{ kind: block\.action, key: row\.actionKey \}\)/.test(panelVue), 'panel-action 仍只转发 actionKey(视觉改动不碰语义接缝)') check('B7c-assertion-surface-intact', /data-bar="row\.key"/.test(panelVue) && /data-bar-count/.test(panelVue) && /data-bar-selected/.test(panelVue) && /data-bar-tone/.test(panelVue), '走查断言面 data-bar-* 原样保留') // B8 交互与既有横条图一致:悬停增亮保留;选中态不改条长口径。 check('B8-hover-feedback-kept', /\.panel-bar:hover \.panel-bar__fill\s*\{[^}]*filter:\s*brightness/.test(panelVue), '条形悬停增亮必须保留') const activeRule = ruleOf('.panel-bar--button.is-active') check('B8b-selected-does-not-change-bar', activeRule.includes('background') && !/width/.test(activeRule), '选中态只给底色/边线,不得改条宽或计数呈现') // B9 省略声明仍只有一份(票 37 的 A4 不变量,视觉改动不许顺手加第二份)。 check('B9-single-ellipsis-declaration', !/text-overflow/.test(panelVue), 'PanelBlocks.vue 不得重新长出第二份 text-overflow') } console.log('') console.log(`T44 distribution ranking & visual density: ${failed ? `${failed} FAILED` : `${passed} passed`}`) if (failed) process.exit(1)