batch5-decor-check.mjs 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. /**
  2. * 票 18(批次 5:装饰/纹理替换)专项断言面。
  3. *
  4. * 立场同 batch2-token-check.mjs:零依赖 Node 静态断言,不运行构建/dev/浏览器。
  5. * 覆盖四件可断言事实:
  6. * [A] 资源化装饰:manifest 条目存在、文件可达、hash 一致、status=formal、有注册表与消费点
  7. * [B] 无死装饰资源:运行时图标目录不再有未登记的装饰候选文件
  8. * [C] 代码化装饰无散落主题色:四角切角 data-URI 不再内联色值、第二底色走 --bg-1
  9. * [D] 消费方式合法:样式层不出现物理资产路径(经语义 key 注入),装饰不碰结构属性
  10. */
  11. import assert from 'node:assert/strict'
  12. import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs'
  13. import { createHash } from 'node:crypto'
  14. import { fileURLToPath } from 'node:url'
  15. import { dirname, join, relative } from 'node:path'
  16. const here = dirname(fileURLToPath(import.meta.url))
  17. const root = join(here, '..', '..', '..')
  18. const cockpitDir = join(root, 'src', 'modules', 'cockpit')
  19. const read = (p) => readFileSync(join(root, p), 'utf8')
  20. const problems = []
  21. function check(name, fn) {
  22. try {
  23. fn()
  24. console.log(` PASS ${name}`)
  25. } catch (error) {
  26. problems.push(`${name}: ${error.message}`)
  27. console.log(` FAIL ${name}\n ${error.message}`)
  28. }
  29. }
  30. const manifest = JSON.parse(read('public/cockpit/assets/manifest.json'))
  31. const shell = read('src/modules/cockpit/CockpitShell.vue')
  32. const panel = read('src/modules/cockpit/components/CockpitPanel.vue')
  33. const mapCanvas = read('src/modules/cockpit/components/MapCanvas.vue')
  34. const icons = read('src/modules/cockpit/domain/icons.js')
  35. const tokens = read('src/modules/cockpit/styles/tokens.css')
  36. console.log('\n[A] 资源化装饰(光池)manifest 与消费链')
  37. check('A1 icon.brand.pool 条目存在且 formal', () => {
  38. const e = manifest.entries.find((x) => x.key === 'icon.brand.pool')
  39. assert.ok(e, 'manifest 缺 icon.brand.pool 条目')
  40. assert.equal(e.status, 'formal')
  41. assert.equal(e.format, 'png')
  42. assert.equal(e.renderMode, 'image')
  43. assert.equal(e.dimensions.width, 489)
  44. assert.equal(e.dimensions.height, 128)
  45. })
  46. check('A2 光池文件真实存在且 hash 一致(含 HTML 兜底拒绝)', () => {
  47. const e = manifest.entries.find((x) => x.key === 'icon.brand.pool')
  48. const abs = join(root, 'public/cockpit', e.path)
  49. assert.ok(existsSync(abs), `文件不存在: ${e.path}`)
  50. const buf = readFileSync(abs)
  51. assert.ok(!(buf[0] === 0x3c && buf.subarray(0, 15).toString().includes('!DOCTYPE')), '取到 HTML 兜底页')
  52. assert.equal(`sha256:${createHash('sha256').update(buf).digest('hex')}`, e.hash)
  53. })
  54. check('A3 注册表与语义 key 一致(icons.js brandPool -> icon.brand.pool)', () => {
  55. assert.match(icons, /brandPool:\s*'icon\.brand\.pool'/)
  56. })
  57. check('A4 消费点存在:Shell 经 --brand-pool-url 语义注入,样式无物理路径', () => {
  58. assert.match(shell, /iconFor\('brandPool'\)/, 'navDecorStyle 未解析 brandPool')
  59. assert.match(shell, /--brand-pool-url'/, '未注入 --brand-pool-url')
  60. assert.match(shell, /--brand-pool-url, none\)/, 'CSS 未消费 --brand-pool-url(缺失退化)')
  61. assert.doesNotMatch(shell, /assets\/icons\/brand-pool\.png/, '样式层出现物理路径(违反共识 §3)')
  62. })
  63. check('A5 光池按设计 frame 布局:489/128 比例、不参与拾取、缺失自动不画', () => {
  64. assert.match(shell, /aspect-ratio:\s*489\s*\/\s*128/)
  65. assert.match(shell, /pointer-events:\s*none/, '装饰必须 pointer-events:none')
  66. })
  67. console.log('\n[B] 无死装饰资源(共识 §1:没有消费点的图不进运行时)')
  68. check('B1 装饰候选不在运行时目录(已挪归档或改名)', () => {
  69. const iconDir = join(root, 'public/cockpit/assets/icons')
  70. const files = readdirSync(iconDir, { recursive: true }).map(String)
  71. const gone = [
  72. 'brand-glow-1.png', 'brand-glow-3.png', 'panel-subtitle-decoration.png', 'panel-subtitle-lead.png',
  73. 'select-bg.png', 'select-bg-base.png', 'topbar-line.png',
  74. 'panel-corner-tl.png', 'panel-corner-tr.png', 'panel-corner-bl.png', 'panel-corner-br.png',
  75. ]
  76. const left = gone.filter((f) => files.includes(f))
  77. assert.deepEqual(left, [], `装饰候选仍在运行时目录: ${left.join(', ')}`)
  78. assert.ok(files.includes('brand-pool.png'), 'brand-pool.png 应在运行时目录(icon.brand.pool 消费中)')
  79. })
  80. check('B2 归档留痕:unconsumed-decor/ 含 11 个候选 + LEDGER 索引', () => {
  81. const archDir = join(root, '.scratch/lanhu-cockpit-asset-replacement/design-archive-v2/unconsumed-decor')
  82. assert.ok(existsSync(archDir), '归档目录不存在')
  83. const files = readdirSync(archDir).filter((f) => f.endsWith('.png'))
  84. assert.equal(files.length, 11, `归档应为 11 个候选,实际 ${files.length}`)
  85. assert.ok(existsSync(join(archDir, 'LEDGER.md')), 'LEDGER.md 索引缺失')
  86. })
  87. console.log('\n[C] 代码化装饰无散落主题色(规格 03:色值单一来源)')
  88. check('C1 面板四角切角 data-URI 不再内联主题色(%2301abea 清零,几何白母版)', () => {
  89. assert.doesNotMatch(panel, /%2301abea/, '四角 data-URI 仍内联 #01abea')
  90. assert.match(panel, /%23fff/, '四角 data-URI 应为白色几何母版')
  91. assert.match(panel, /background-color:\s*var\(--panel-line-hi/, '四角应经 mask 染 --panel-line-hi')
  92. assert.match(panel, /-webkit-mask:[\s\S]*--corner-tl/, '应改用 mask 简写消费四角(mask-image 不收层语法)')
  93. })
  94. check('C2 第二底色 --bg-1 已建槽位且有消费点(氛围底 + 地图画布)', () => {
  95. assert.match(tokens, /--bg-1:\s*#02091b/, 'tokens.css 缺 --bg-1')
  96. assert.match(shell, /var\(--bg-1,\s*#02091b\)/, 'Shell 氛围底未消费 --bg-1')
  97. assert.match(mapCanvas, /var\(--bg-1,\s*#02091b\)/, 'MapCanvas 画布底未消费 --bg-1')
  98. assert.doesNotMatch(shell, /linear-gradient\(180deg,\s*#010514/, 'Shell 氛围底仍有裸 hex 渐变')
  99. assert.doesNotMatch(mapCanvas, /background:\s*#02091b/, 'MapCanvas 仍有裸 hex 画布底')
  100. })
  101. console.log('\n[D] 结构保护(差异判断表:装饰不碰结构)')
  102. check('D1 装饰改动未触及面板结构属性(panelId/收放/滚动边界选择器仍在)', () => {
  103. assert.match(panel, /data-panel-id="panelId"/)
  104. assert.match(panel, /is-collapsed/)
  105. assert.match(panel, /overflow-y:\s*auto/)
  106. assert.match(panel, /overflow-x:\s*hidden/)
  107. })
  108. check('D2 批次 2 既有 token 断言面未破坏(语义槽未被装饰批改名)', () => {
  109. for (const name of ['--cyan', '--cyan-hi', '--cyan-d', '--bg-0', '--panel-line-hi']) {
  110. assert.match(tokens, new RegExp(`${name}:`), `${name} 缺失`)
  111. }
  112. assert.doesNotMatch(tokens, /--fs-metric/)
  113. })
  114. if (problems.length) {
  115. console.log(`\n${problems.length} 项失败`)
  116. process.exit(1)
  117. }
  118. console.log('\n批次 5 装饰断言面全部通过。')