Friend 品牌 UI 不变量 INV-UI-1 全解:禁止紫色与白/中性色强调体系(No-Purple 无增长棘轮)
2026/9/17 0:09:58 网站建设 项目流程

Friend 品牌 UI 不变量 INV-UI-1 全解:禁止紫色与白/中性色强调体系(No-Purple 无增长棘轮)

【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend

导读

本文围绕 Friend 开源仓库的product/invariants/brand-ui.md这一条锁定(locked)产品不变量INV-UI-1展开,系统讲解该项目的品牌视觉红线:界面中一律不使用紫色,主操作与强调色统一采用白色/中性色处理。你将掌握这条不变量在桌面端 SwiftUI、Flutter App 与 Web 端的具体落地范围,以及仓库如何用.github/scripts/check_brand_ui.py的**无增长棘轮(no-increase ratchet)**在 CI 中自动拦截新增紫色,包括其检测模式、HSV 色相判定算法、允许列表与 PR 协作规则。

一、不变量是什么:品牌红线的定义与状态

在 brand-ui.md 中,INV-UI-1 被定义为一条Status: locked(已锁定)的产品不变量:

Statement:Purple is off-brand. UI accents and primary actions use white/neutral treatments, not purple hues, glows, or gradients.

紫色是偏离品牌(off-brand)的。UI 强调色与主操作应使用白色/中性处理,而非紫色调、辉光或渐变。

"locked" 状态意味着它属于仓库product/invariants/目录下的产品不变量体系——这类规则一旦锁定,改动必须走专门流程(详见后文 PR 规则)。Friend 项目是一款"AI that sees your screen, listens to your conversations and tells you what to do"的 AI 助手,其视觉语言刻意保持克制的中性风格,紫色被明确定义为品牌禁区。

仓库根目录 AGENTS.md 的 Cross-Component Guidelines 中亦同步了这条红线:

Never use purpleanywhere in UI (icons, accents, glows, gradients) — off-brand; use white/neutral. Enforced as a no-increase ratchet (INV-UI-1); seeproduct/invariants/brand-ui.md.

即:界面中的图标、强调色、辉光、渐变等任何位置都不允许紫色;强制执行方式为INV-UI-1的无增长棘轮。

二、MUST NOT:被明确禁止的行为清单

文档以MUST NOT形式列出的红线非常具体,共两条:

  1. 禁止在 UI 中使用紫色——包括图标(icons)、强调色(accents)、辉光(glows)、悬停状态(hover states)、渐变(gradients)。
  2. 禁止引入新的紫色系统色或品牌 token——即不得新增purplePrimarypurpleGradient之类的语义色命名。

值得注意的是第二条与"允许存量债务存在"的棘轮机制配合使用:历史遗留的紫色引用可以在迁移期内存在,但新增紫色(无论是新文件还是已有文件中提高计数)都会导致检查失败。

三、适用范围:哪些界面受约束

INV-UI-1 覆盖三块端侧界面(Surfaces):

技术栈覆盖范围
桌面端SwiftUIOmiTheme及桌面视图(desktop/macos/Desktop/Sources/
移动 AppFlutterapp/lib/下的全部 UI 代码
Web营销/管理后台web/下以 Omi 品牌名义上线的界面

对应的Path globs(路径匹配)被写进文档,同时也被检查脚本作为UI_ROOTS常量原样复用(见.github/scripts/check_brand_ui.py第 20–24 行):

desktop/macos/Desktop/Sources/** app/lib/** web/**

这些路径与检查脚本中的UI_ROOTS一一对应,说明文档与代码守卫是"契约—实现"的强绑定关系,而非松散的约定。

四、Guard 测试:check_brand_ui.py 的"无增长棘轮"机制

文档明确指出守卫脚本为.github/scripts/check_brand_ui.py。这是本不变量的核心工程实现,值得深入拆解。

4.1 棘轮(ratchet)的核心语义

脚本 docstring 开宗明义(第 1–8 行):

Compares purple-hit counts in changed UI sources against the merge base. Existing debt may remain; introducing new purple (raising a file's count, or adding purple in a new file) fails.

即:只比较本次变更文件在 PR 基线上的紫色命中数变化。存量紫色债务可以继续存在;但某个文件中的紫色命中数一旦上升,或新文件引入紫色,检查即失败。这正是"无增长棘轮"的含义——它不追求一步清零,而是保证紫色使用量只减不增。

4.2 文件过滤:is_ui_source

在统计紫色之前,脚本先过滤"哪些文件需要被监管"(is_ui_source,第 81–91 行),同时满足四个条件才算 UI 源文件:

  1. 不在ALLOWLIST_FILES允许列表中;
  2. 路径以UI_ROOTS任一前缀开头(desktop/macos/Desktop/Sources/app/lib/web/);
  3. 扩展名属于UI_SUFFIXES = {".swift", ".dart", ".ts", ".tsx", ".js", ".jsx", ".css"}
  4. 路径中不含.gitnode_modulesbuilddist.next__pycache__等跳过目录。

4.3 检测模式:从关键字到色相判定的三层防线

count_purple函数(第 115–121 行)采用穷举模式 + 色相判定的双层计数,每一层都对应一个真实发生过的漏检事故。

第一层:显式模式匹配(PURPLE_PATTERNS,第 39–66 行)

  • 平台 API:Color.purple.purple(SwiftUI 简写,如.foregroundStyle(.purple))、Colors.purple(Flutter);
  • Flutter 特有拼写:\bdeepPurple(?:Accent)?\b——注释说明\bpurple\b因 camelCase 无词边界无法命中deepPurple,曾导致漏检;
  • 十六进制字面量:#7C3AED#8B5CF6等 9 个枚举紫色色值(PURPLE_HEX_HUES,第 37 行),以及 Dart 风格的0x(?:[0-9A-F]{2})?(?:7C3AED|...)0xFF8B5CF6形态,lookbehind 要求0x必须作为 token 起点,防止命中foo0x8B5CF6这类无关常量);
  • 语义 token:purplePrimary|purpleSecondary|purpleAccent|purpleLight|purpleGradient|purpleLightGradient
  • Tailwind/CSS:purple-500类名、--purple-CSS 变量、color: purple、字符串'Purple'
  • 邻近色族\b(?:violet|indigo|fuchsia)-\d{2,4}\b——注释还原了事故经过:violet/indigo 拼写与 purple 完全不同,之前的模式全部漏过,导致 Web 市场(marketplace)曾上线 violet 促销卡片与两个 violet 分类主题而检查仍报 OK。

第二层:HSV 色相判定(is_purple_hex,第 106–112 行)

由于"枚举色值"永远追不上真实世界,脚本引入了第二个更智能的判定:对任何 6 位十六进制字面量(HEX_LITERAL,第 96 行)做 HSV 转换,判断其是否落在"人眼感知为紫色"的区间:

  • PURPLE_HUE_RANGE = (235.0, 320.0):HSV 色相在蓝色到品红之间视为紫色;
  • MIN_SATURATION = 0.25MIN_VALUE = 0.20:低于该饱和度和明度的颜色(如#1A1A1A)即使色相计算值落在区间内,也会被视为中性色而非紫色。

注释里记录了一个经典的漏检案例:#6C2BD9与枚举值#6D28D9只差一个数字,App Store 开发者横幅的紫色渐变就曾携带这种色值绕过"成员判定"通过检查——色相判定正是为此补上的兜底防线。

4.4 计数与比较:棘轮如何"证明不增长"

主流程main()(第 131–166 行)逻辑清晰:

  1. 解析参数--changed-files(必填,列出变更文件)、--base(必填,合并基线 git ref)、--root(仓库根,默认.);
  2. 读取变更文件列表并过滤出 UI 源文件;若无任何 UI 源文件变更则直接OK
  3. 对每个变更文件:读取当前工作区内容统计head_count,用git show <base>:<path>取出基线内容统计base_count
  4. head_count > base_count,记录回归(path: purple hits base → head);
  5. 存在回归则输出FAIL: INV-UI-1 — purple usage increased in changed UI files.并列出明细,退出码 1;否则输出OK: INV-UI-1 — no purple increase across N changed UI file(s).,退出码 0。

命令行调用形态为:

python3 .github/scripts/check_brand_ui.py \ --changed-files /tmp/changed.txt \ --base origin/main \ --root .

其中--base缺失时会直接FAIL,因为棘轮必须依赖基线内容才能判断"是否增长"。

4.5 允许列表(Allowlist):债务的受控出口

脚本ALLOWLIST_FILES(第 30–33 行)目前仅含一个路径:

ALLOWLIST_FILES: set[str] = { # Theme token definitions still expose purple* names during migration. "desktop/macos/Desktop/Sources/Theme/OmiColors.swift", }

被允许的原因注释得很清楚:这是迁移期间仍暴露purple*命名的旧主题 token 定义文件。同时脚本明确要求:允许列表应优先收缩而非扩张——如需新增条目,必须在 PR 中引用 INV-UI-1 说明理由。

有趣的是,仓库中该文件当前实际内容(desktop/macos/Desktop/Sources/Theme/OmiColors.swift)显示旧的OmiColors深色寄存器已经被删除:原硬编码近黑背景(backgroundPrimary#0F0F0F、textPrimary#FFFFFF)在改为 light-pinned 玻璃面板(InkGlass)后全部失效,项目选择删除而非重新着色或别名("a silent invisible surface is the one failure a build cannot report"),视图改用Ink(颜色)、InkType(字体)以及PageGlass/GlassShell/NotchGlass(表面词汇)表达。文件仅保留通用的Color(hex:)便利初始化器。这从侧面印证了不变量生态的运作方式:文档声明红线,脚本执行棘轮,而存量债务通过 allowlist 与迁移逐步清除。

五、测试佐证:test_check_brand_ui.py

仓库为count_purpleis_purple_hexis_ui_source提供了单元测试(.github/scripts/test_check_brand_ui.py),其中每个测试都对应一次真实事故或边界设计:

测试用例验证点
test_counts_color_purple_and_hexColor.purple0x8B5CF6#8B5CF6purplePrimary均计数
test_counts_flutter_deep_purpleColors.deepPurple/deepPurpleAccent/.shade300命中
test_counts_dart_hex_literalColor(0xFF8B5CF6)Color(0xff7c3aed)命中
test_ignores_colours_that_merely_contain_purple_hex_digits0xFF1A2B3C0xFF00FF00不计数(防止误伤)
test_dart_hex_literal_must_start_a_tokenfoo0x8B5CF6SOME_CONST0x8B5CF6不计数,value = 0x8B5CF6;计数
test_is_ui_source桌面 Sources 为 UI 源;backend/main.py不是;allowlist 文件不算
test_counts_swiftui_dot_purple.foregroundStyle(.purple)命中
test_counts_tailwind_bg_purple_500bg-purple-500 text-purple-700计 2
test_counts_tailwind_ramps_that_are_purple_by_sight_not_by_namebg-violet-500 text-indigo-300计 2、from-fuchsia-600计 1
test_hex_is_judged_by_hue_not_by_membership#6C2BD9#2D1B69等未枚举色值按色相判定命中
test_counts_css_swift_and_dart_indigo_literals#6366F1(indigo 色值)计 1

这些测试既锁定了检测器的行为契约,也把"为什么需要这层检测"的历史事故固化为回归防线,属于典型的"不变量 + 守卫测试 + 事故驱动补丁"闭环。

六、与其他守卫的联动:guardrail_pulse.py 的品牌指标

INV-UI-1 的守卫并非孤立存在。.github/scripts/guardrail_pulse.py导入了check_brand_ui模块(第 25 行),在其_brand_ui_metric(第 102–119 行)中直接复用UI_ROOTSis_ui_sourcecount_purple,遍历仓库根下全部 UI 源文件并累加紫色命中数,产出名为brand_ui_purple的度量指标(第 119 行)。

这意味着仓库存在两条互补的监控视角:

  • PR 级的棘轮(check_brand_ui.py):阻止单个 PR 使紫色使用量上升;
  • 全局脉冲度量(guardrail_pulse.py):持续观测整个仓库的紫色总量,量化"债务消减"的长期趋势。

同一套判定函数被两处复用,也保证了"局部拦截"与"全局度量"的判定口径完全一致。

七、PR 规则:何时需要在 PR 中显式点名 INV-UI-1

文档的PR rule刻意区分了两种情况:

  1. 常规 UI PR 不需要在标题或描述中点名INV-UI-1——因为品牌棘轮已经作为 CI 守卫自动兜底,"品牌地板由 ratchet 强制保证"。
  2. 只有两类场景需要显式点名:一是故意变更品牌配色政策(例如调整品牌色方向);二是修改允许列表ALLOWLIST_FILES)。这两种操作本质上是在"修改不变量本身",因此必须走显式引用流程。

这与仓库根 AGENTS.md 的跨组件规范一致:"如果你的 diff 触及某条锁定不变量的 path globs,必须在 PR body 中点名每一个命中的不变量 ID(基于路径而非意图),并在行为变化时更新该不变量的守卫测试。"可借助scripts/pr-preflight --suggest自动发现命中项。

八、实践建议:在 Friend 仓库中如何遵守与自查

结合文档与源码,给读者一份可执行的自查清单:

  1. 改动前:先确认改动文件是否落入desktop/macos/Desktop/Sources/**app/lib/**web/**三个 glob 之一;若命中,本不变量即刻生效。
  2. 编码时:强调色与主操作使用白色/中性色(如InkPageGlass等桌面端表面词汇,或 Flutter/Web 的中性 token);不引入任何purple*语义命名,不使用#7C3AED#8B5CF6#A855F7#9333EA#6D28D9#AF52DE#D946EF#A78BFA#C4B5FD等紫色系色值,也避免 Tailwind 的violet-*indigo-*fuchsia-*色阶。
  3. 本地验证:运行单元测试确认检测器行为符合预期——python3 .github/scripts/test_check_brand_ui.py(位于.github/scripts/目录下);需要完整走一遍棘轮时可准备变更文件清单并指定基线执行check_brand_ui.py
  4. 存量债务:若某文件已存在紫色且本次改动会自然触碰它,优先顺带消减;严禁在同一 PR 中让该文件计数上升。
  5. 允许列表:除非确有迁移期理由,否则不要向ALLOWLIST_FILES新增路径;必须新增时在脚本注释中说明原因,并在 PR 描述中显式点名 INV-UI-1。
  6. 策略变更:任何品牌色政策的调整都属于不变量变更,须显式引用 INV-UI-1 并同步更新守卫脚本与测试。

九、小结:一条不变量的完整工程闭环

从 brand-ui.md 我们可以看到一个"产品不变量"在 Friend 仓库中的完整生命周期:

  • 语义层:一句话锁定品牌方向——"No purple; neutral accents",状态 locked;
  • 约束层:MUST NOT 清单 + Surfaces + Path globs,明确"禁什么、管哪里";
  • 执行层.github/scripts/check_brand_ui.py以"无增长棘轮"方式在 CI 中拦截新增紫色,模式匹配 + HSV 色相判定双层防漏,allowlist 提供受控债务出口;
  • 验证层.github/scripts/test_check_brand_ui.py把历次漏检事故固化为回归用例;
  • 度量层.github/scripts/guardrail_pulse.py持续统计brand_ui_purple全局总量;
  • 协作层:PR 规则区分"常规改动(自动守卫)"与"策略变更(显式点名)",与根目录 AGENTS.md 的 invariants 流程衔接。

对任何要在 Friend 仓库中提交 UI 改动的开发者而言,理解 INV-UI-1 及其棘轮实现,等于同时理解了品牌红线本身、CI 拦截机制,以及"如何在债务与红线之间做受控的渐进式迁移"——这正是仓库把一条视觉规则工程化为可强制执行、可度量、可持续消减的系统性做法。

【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询