Files
WorkBuddy eee042c855 ③段第八轮收口:功能入口卡按参照物对齐 + 导航按 tiaoyue 源站重建 + 密度 A 方案
原型(执行会话/目标-…-d85687/ui/mcn-workbench.html)
- 功能入口卡改三段纵向(图标/标题/描述各一行),68 → 111.19px,对齐 mcn-work-shop
  参考卡(同视口 1249×1277 实测 133px),补 --sh-L1 投影;主因是排布不是字号
- 左侧导航重建:由「纯图标 + 悬停提示」改为 64px 药丸浮栏,图标 24 在上、名字常驻在下
  (名字一律 2 字,说明走 data-tip);真值取自 tiaoyue 源站实测
  (栏 64/内衬 3/圆角 100/项 56×78/字 12px-500-行高 16)
- 密度 A 方案:≤1000 卡最小宽 200→160、≤820 统计条最小宽 132→96
  (两处 auto-fit 只在真装不下时才缩)⇒ 768/700/560 三处转绿,其余 10 个视口未动
- 移动档隐藏导航条横向滚动轨(16px 轨道曾吃掉 52px 条高的三成,还白吃 16px 主区)

取证通道
- _tools/cap.py 改为「只连不起」:只连 9223 常驻实例,视口走 Emulation.setDeviceMetricsOverride,
  不再自起 headless Chrome(顺带解除 500px 视口下限,390×844 可测)
- _tools/check_js.py 新增第 5 道门禁(node --check + 模板内注释反引号扫描)

文档
- 复盘-为什么第一版做得很差.md(新建):13 轮时间轴/四类反作用/三重归因/已修与仍欠
- 3d-审查报告.md:更正 §九 那条假 ✓(把「§5.5 五条逐条过完」记成机核,实际零覆盖,
  正是「五道闸门全绿却难看」的开关),补 §十二 功能入口卡高差、§十三 tiaoyue 导航逐条对照
- 3b-实测记录.md/DESIGN.md/3c-GPT 参考版与会诊/布局排版参考 ×2

机制与记忆
- .workbuddy/collab/collabctl.py:off --ws 不再全机扫描(原先一次杀掉他区+看板共 5 个进程)
- .workbuddy/memory/:本轮纪律与实测真值
- .gitignore:补排运行时噪音(ui/_scratch/__pycache__/浏览器取证临时目录/*.bak-*/一次性脚本)

闸门:语法全过/骨架八页 39 板块/变异对照 15-15/逐视口 14 绿(500×844 唯一红,缺口待拍板)/命名 2060-0-27
2026-10-08 22:21:42 +08:00

202 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""变异对照:证明自检的判据不是「恒绿」。
做法:把原型复制一份,往里注入一处**已知的假病**,再跑一次自检,
要求它必须报红,并且红在**预期的那一条**上。测完即删临时件。
为什么要这个:一个永远 PASS 的自检等于没有自检。只有「注病 → 报红 → 复原 → 回绿」
这条链走通了,前面那串 PASS 才算证据。
用法:python _tools/mutate_test.py
"""
import os, re, subprocess, sys, tempfile, html
HERE = os.path.dirname(os.path.abspath(__file__))
UI = os.path.abspath(os.path.join(HERE, ".."))
PROTO = os.path.join(UI, "mcn-workbench.html")
PY = sys.executable
# (名称, 说明, 查找串, 替换串, 期望出现的报红片段, 视口[可选,默认 1440x900])
MUTANTS = [
("M1 死控件",
"把 P2-01「低粉爆款」的实现掏空 —— 点它不再有任何反馈",
"'P2-01':()=>{ S.benchFilter='低粉爆款'; paint(); },",
"'P2-01':()=>{},",
"P2-01"),
("M2 未登记的可点元素",
"在页面上塞一个没写进交互清单的 data-act。"
"⚠️ 2026-10-08 改:P2 的「对标内容列表」已由行式列表改成满宽表格,"
"原来那个注入点(.action-cell 里的 P2-03)**在文件里已经不存在了** —— 脚本本来会直接"
"判「找不到注入点、跳过」,那等于这条变异悄悄失效 ⇒ 注入点跟着形态换到表格那一行。",
"<td class=\"cell-act\"><button class=\"btn-micro\" data-act=\"P2-03\"",
"<td class=\"cell-act\"><button class=\"btn-micro\" data-act=\"ZZ-99\">野按钮</button><button class=\"btn-micro\" data-act=\"P2-03\"",
"未登记的 data-act"),
("M3 悬空声明",
"清单里声明了一个页面上根本不存在的 ID",
"'CHECKOUT-OK':()=>{",
"'ZZ-98':()=>{},\n 'CHECKOUT-OK':()=>{",
"声明了但页面里没有"),
("M4 骨架漂移",
"给 P3 多塞一个板块 —— ②段那份骨架是冻结的,这属于越界",
"${head('分区切换',",
"${head('多出来的板块', '')}<div class=\"panel\"><p class=\"meta\">越界加的块</p></div>${head('分区切换',",
"骨架与②段不一致"),
("M5 对比度越界",
"把 --ink-faint 换成偏白的灰,压到契约线以下",
"--t-ink-faint:#8a887f;",
"--t-ink-faint:#c9c7c0;",
"对比度 --ink-faint"),
("M6 点击无反馈(跳页不同步)",
"把 go() 退回「只改 hash」的写法 —— 点完状态不动,自检应判它没反应",
" S.route=page; S.pageState=st||'ok'; S.layer=null;\n const want='#/p/'+page+'/'+(st||'ok');\n if(location.hash!==want){ try{ location.hash=want; }catch(e){} }\n paint();",
" const want='#/p/'+page+'/'+(st||'ok');\n if(location.hash!==want){ try{ location.hash=want; }catch(e){} }",
"驱动了没反应"),
("M7 行高不齐(把某一行撑高)",
"只把表里**第一行**的单元格内衬撑大 —— 模拟「某一行被状态标或长文字撑高」这件事:"
"一旦行高不齐,扫表时节奏就乱了。"
"⚠️ 2026-10-08 改:P1 两块已由行式列表改成表格,原来那条「放开换行」的注入对表格无效"
"(表格列宽自适应、根本不折行,是假对照)⇒ 换成直接撑高一行。",
"table.ds-table.tbl-dense .cell-lead{ font-weight:600; }",
"table.ds-table.tbl-dense tbody tr:first-child td{ padding:20px var(--mg-8); }\ntable.ds-table.tbl-dense .cell-lead{ font-weight:600; }",
"行高不齐", "1440x900"),
("M8 密度退化(内衬变肥)",
"把表格行的内衬从 4px 加到 36px —— 行变肥,一屏就装不下 6 行了。"
"⚠️ 2026-10-08 改:密度判据量的已是 P1 那张满宽表格(.tbl-dense 的 td 内衬),"
"原来注在 .list-row 内衬上、对表格无效(假对照)⇒ 注入点换到表格的 td 内衬。",
"table.ds-table.tbl-dense td{ padding:var(--mg-4) var(--mg-8); vertical-align:middle; white-space:nowrap; }",
"table.ds-table.tbl-dense td{ padding:36px var(--mg-8); vertical-align:middle; white-space:nowrap; }",
"低于契约 6 行"),
("M9 量尺寸时页面不在零点",
"量 P1 之前先把主区滚下去 —— rect 取到的是相对视口的坐标,滚了之后 top 全变负,"
"「一屏装得下几行」的减法就会凭空变大(这条守卫盯的就是它)。"
"⚠️ 2026-10-08 改:注入点从 window.scrollTo 换成 .view.scrollTop —— 外壳修过之后"
"整页的滚动容器就是 .view,window 根本滚不动,拿 window 注入等于没注入(假对照)。",
" guardScroll('P1');",
" document.querySelector('.view').scrollTop=300;\n guardScroll('P1');",
"没回零点", "768x900"),
("M10 行分隔线被关掉",
"把表格行的分隔线(td 的 border-top)关掉 —— 一格一格的表读起来就没有行界了。"
"⚠️ 2026-10-08 改:判据现在量的是 P1 那张表的 td 边框,注入点也跟着从 `.list-row` 换过来"
"(原来那条注在 `.list-row:first-child` 上,对表格无效,是假对照)。",
"table.ds-table td{ padding:var(--mg-8); border-top:1px solid var(--line-soft); vertical-align:top; }",
"table.ds-table td{ padding:var(--mg-8); vertical-align:top; }",
"行分隔线根本没画出来", "1440x900"),
("M11 浮层没有遮罩",
"去掉遮罩层 —— 「点层外空白关掉浮层」这条通道就没了",
"const lh=layerHTML();\n $('#layer-root').innerHTML = lh ? `<div class=\"layer-mask\" data-act=\"X-mask\" aria-hidden=\"true\"></div>`+lh : '';",
"const lh=layerHTML();\n $('#layer-root').innerHTML = lh;",
"没有遮罩"),
("M12 关掉浮层后焦点不还回去",
"把 closeLayer 退回「只清状态」的写法 —— 焦点会掉回 body,键盘用户就断了",
" const back=layerTrigger;\n S.layer=null; paint();\n if(back){ const el=document.querySelector('[data-act=\"'+back+'\"]'); if(el) el.focus(); }",
" S.layer=null; paint();",
"关掉后焦点没回到"),
("M13 浮层多出一个中性关闭出口",
"给「随手记」再加一颗 ×(多一个 X-close)—— 一个层两个中性出口会让用户找不着北,"
"这条判据原先只数 .layer-close 类名,多出来的是同类,应当报红",
" const close=`<button class=\"layer-close\" data-act=\"X-close\" aria-label=\"关闭\">×</button>`;",
" const close=`<button class=\"layer-close\" data-act=\"X-close\" aria-label=\"关闭\">×</button><button class=\"layer-close\" data-act=\"X-close\" aria-label=\"再关一次\">×</button>`;",
"中性关闭出口 = 2"),
("M14 外壳高度链没约束住(整页滚不动、折叠线以下拿不到)",
"把 .app 的 height:100vh 退回 min-height:100vh —— 就是 2026-10-08 修掉的那个真缺陷的原样:"
"主区跟着内容长高、自己不滚,滚动落回 <body>,而 body 是 overflow:hidden,"
"于是内容掉到折叠线以下就永远拿不到",
"grid-template-rows:minmax(0,1fr); height:100vh; }",
"grid-template-rows:minmax(0,1fr); min-height:100vh; }",
"高度链没约束住"),
("M15 用内联 style 表达选中态(组件口径被破坏)",
"把 P2 选题库分组那个分段控件改回「`.btn btn-sm` + 内联 border-color:var(--ink)」的写法 ——"
"这正是 2026-10-08 统一掉的那套旧画法:选中态一旦能被内联样式随便盖,"
"全站就会出现第三套、第四套画法,而「选中态只有一套」这件事就没人看着了。"
"⚠️ 这条变异是给新增判据「组件口径」(自检第 84 项)配的对照 —— "
"没有它,那条判据是不是恒绿就无从证明。",
"<button class=\"seg-item ${S.topicGroup===g?'is-on':''}\" data-act=\"P2-08\"",
"<button class=\"seg-item ${S.topicGroup===g?'is-on':''}\" style=\"border-color:var(--ink)\" data-act=\"P2-08\"",
"内联 style 越权"),
]
def read_out(dom_path):
s = open(dom_path, encoding="utf-8", errors="replace").read()
m = re.search(r'<div class="st-out" id="selftest-out">(.*?)</div>', s, re.S)
if not m:
return None
return html.unescape(re.sub(r"<[^>]+>", "", m.group(1)))
def run(proto, vp="1440x900"):
out = os.path.join(tempfile.gettempdir(), "mut-selftest.html")
r = subprocess.run([PY, os.path.join(HERE, "cap.py"), "--mode", "dom", "--route", "selftest",
"--viewport", vp, "--proto", proto, "--out", out],
capture_output=True, text=True)
if not os.path.exists(out):
return None
return read_out(out)
def headline(text):
m = re.search(r"自检结果:PASS (\d+) / FAIL (\d+)", text or "")
return (int(m.group(1)), int(m.group(2))) if m else None
def main():
src = open(PROTO, encoding="utf-8").read()
base = run(PROTO)
hb = headline(base)
print("基线(未注入):PASS %s / FAIL %s" % hb if hb else "基线读不到自检输出")
if not hb or hb[1] != 0:
print("⛔ 基线就不是全绿 ⇒ 先修原型,别急着做变异对照。")
return 2
bad = 0
for row in MUTANTS:
name, why, find, repl, expect = row[:5]
vp = row[5] if len(row) > 5 else "1440x900"
if find not in src:
print("⚠️ %s:找不到注入点,跳过(注入点失效本身也要修)" % name)
bad += 1
continue
tmp = os.path.join(tempfile.gettempdir(), "mut-proto.html")
with open(tmp, "w", encoding="utf-8") as f:
f.write(src.replace(find, repl, 1))
text = run(tmp, vp)
h = headline(text)
os.remove(tmp)
if not h:
print("❌ %s:注入后自检跑不出来(连输出都没有)" % name)
bad += 1
continue
caught = h[1] > 0 and expect in (text or "")
print("%s %s:注入(%s) ⇒ PASS %d / FAIL %d | %s" %
("✅" if caught else "❌", name, why, h[0], h[1],
"报红且红在预期处(%s,视口 %s)" % (expect, vp) if caught
else "**没在预期处报红(期望含「%s」,视口 %s)**" % (expect, vp)))
if not caught:
bad += 1
print()
print("变异对照:%d / %d 通过" % (len(MUTANTS) - bad, len(MUTANTS)))
if bad == 0:
print("⇒ %d 处假病全部被自检抓住,且基线回绿:这套判据不是恒绿的。" % len(MUTANTS))
return 0 if bad == 0 else 1
if __name__ == "__main__":
sys.exit(main())