札记

一等公民:能连 ≠ 能深

跨全仓设计公理。上位 docs/truth-system.md(判断函数)。 结论先行:通用抽象让你「连得上」,一等公民才让你「做得深」。深 = 把领域知识升为系统认识的一等能力,不是把通用抽象做得更精致。

0. 苹果的 AirPods:公理的原型

老蓝牙配对是通用抽象——所有设备一个模型:设备名 + 配对 + 连接。AirPods 在这套里就是列表里一行字 + 一个连接按钮,跟杂牌音箱没区别。通用抽象连得上 AirPods,但给不了 AirPods 该有的体验。

苹果没有去把通用蓝牙列表做得更好。它为 AirPods 单独做了一张弹出卡:开盖靠近就弹,AirPods 的图、左耳/右耳/充电盒分开的实时电量、一个连接大按钮——根本不走那个通用设备列表。

关键不在卡好看。在于那张卡把 AirPods 的领域知识做成了系统认识的东西:左右耳分开耗电、入耳检测、自动切换设备、空间音频。这些在「通用蓝牙设备」抽象里不存在——通用抽象只认识「一个设备」,不认识「一对会分开耗电、会感知你戴没戴的耳机」。

一等公民不是换个 UI,是把业务的领域知识变成系统认识的能力。

1. 公理

通用抽象  →  能连(覆盖广,每个对象都是抽象的一个实例)
一等公民  →  能深(领域知识成为系统的一等能力,而非被压扁的参数)

深 ≠ 把通用抽象做得更精致
=  让系统「认识这是什么」,于是该业务的难点变成系统能力

通用化的代价是压扁:把世界塞进一个统一模型(设备f(x)=y资源),横得越宽,业务里真正难的东西在压扁中丢得越多。要把丢掉的捡回来,不是优化通用模型,是为该业务单独立一个认识它的一等公民

2. 本仓的实例

通用抽象(能连)

业务一等公民(能深)

压扁丢了什么

蓝牙(苹果)

设备名+连接

AirPods 弹出卡

左右耳分电量、入耳、切换

测试

teact value → string 快照

aihand 的 DOM 命中 测试层

命中消歧、命中理由、DOM 魔鬼值(同名/隐藏/aria 盖文本)

teact 是「纯函数测试」域的正确解,横扫全仓纯函数。但 aihand 的核心业务是「在 DOM 里做对决策」,被通用 teact 压成 expect(findElement(...)).toBe(submit)——退回裸 vitest、手搭 DOM、引用相等断言,「为什么选 Submit 而不是 Cancel」这条命中理由彻底丢失findElement 的真 bug 不藏在 NaN,藏在「两按钮同名选谁」「一可见一 display:none 选谁」——DOM 语义的边界,通用 typegen 的 0/''/NaN 一个都撞不到。详见 packages/teact/DESIGN-verifier-role.md §8

3. 决断标准:该单独做就单独做,别和稀泥

最容易犯的错是和稀泥:「通用框架做地基、业务做深井、两者不互斥」——这话拦住了"该为业务单独建层"的决断。苹果的答案干脆得多:该单独做就单独做,通用蓝牙列表它根本没碰。

判断「该不该为某业务立一等公民」:

  1. 通用抽象能连上它吗? 能 → 别急着满足,连上只是底线。

  2. 这业务的难点,在通用抽象里有名字吗? 没有(像「左右耳分电量」「DOM 命中消歧」在通用模型里根本不存在)→ 信号:该立一等公民。

  3. 立了之后,难点变成系统能力了吗? 是 → 立。只是 UI 更漂亮、抽象更精致 → 不是一等公民,别立。

通用抽象守住自己那个域做到极致(teact 守纯函数、CAS 守资源寻址);难的业务域单独长出认识它的一等公民层。两条线都做满,但别用"通用做地基"和稀泥拦住该单独做的决断。

4. 砍掉的(别加回来)

  • 为了"通用"硬把业务塞进统一模型:DOM/组件/store 塞进 value→string 只产假快照;这是压扁不是覆盖。

  • 「两者不互斥」式和稀泥:它听起来全面,实际是回避决断。该单独做就单独做。

  • 把"一等公民"误解成换 UI / 加抽象层:一等公民的判据是「领域知识是否升为系统能力」,不是界面或代码结构好不好看。

元真理检查:去掉这条公理会怎样?→ 团队会无限优化通用抽象,以为"做得更精致 = 做得更深",而真正难的业务域永远被压成通用模型的一个贫瘠实例。有影响,留——它把「该不该为业务单独立层」从品味降成可判断的标准。