验证流程与质量关卡
Ark UI skill 通过一套分层的验证流程(validation pipeline)与质量关卡(quality gates)来约束交付物:从项目自身的测试/构建,到内置的启发式审计脚本 scripts/audit-ark-ui.mjs,再到浏览器中的交互与双视口(桌面/竖屏)人工检查,最后以 references/depth-levels.md 中的六轴评分卡(validation scorecard)判定深度契约是否达成。本页是这套机制的唯一权威参考。
Purpose and Scope
本页覆盖 Ark UI skill 中"实现之后如何验证"的完整机制:
- SKILL.md 中
Validate与Iterate with an evidence lock两节定义的验证步骤与证据锁(evidence lock)规则 scripts/audit-ark-ui.mjs启发式审计脚本的检查项、严重级别与退出码语义references/depth-levels.md中的 Validation scorecard(六轴评估)与 Escalation/reduction(升降级)规则scripts/capture-showcases.mjs的截屏回归关卡(水平溢出即失败)- README 中的 skill 级验证入口
quick_validate.py
明确不在本页范围内、由兄弟页面承接的内容:
- 审计脚本要检查的设计规则本身(语义令牌、reduced-motion、focus-visible 等)→ 见"实现契约与深度系统"
- CSS 证据采集与来源台账(
analyze-css-evidence.py、source ledger)→ 见"证据研究与来源管理" - 家族(family)选择与各家族视觉语言 → 见"产品家族体系"
Overview
这套验证体系的设计意图可以概括为一句话:证据先于判断,关卡先于迭代。
Ark UI 的工作循环是"研究证据 → 锁定契约 → 实现 → 验证 → 迭代"。验证流程不是实现后的收尾动作,而是迭代循环的前置条件——SKILL.md 明确规定"compile, interaction, contrast, and screenshot checks pass"(编译、交互、对比度与截图检查全部通过)之后才允许进入下一轮迭代。这样设计的动机:
- 防止基于猜测或陈旧状态做优化:"Never optimize from a guessed or stale state"。所有视觉判断必须来自真实渲染的真实状态。
- 防止深度失控:深度(depth)评估必须基于"shell 转换、stage 分层、组件覆盖"等结构维度,而不是元素数量或装饰密度,否则会被诱导成堆装饰。
- 防止法律与来源风险:官方 CDN 资源与受保护 logo 的嵌入是最高严重级别(error)的关卡。
- 防止可用性退化:可达性(reduced-motion、focus-visible、landmark、ID 引用)在脚本中被强制为 error 级。
验证关卡共分四道,按执行顺序:
| 关卡 | 工具/依据 | 通过标准 |
|---|---|---|
| G1 项目门 | 目标项目自身 test / lint / build | 全部通过 |
| G2 脚本审计门 | audit-ark-ui.mjs | errors.length === 0(退出码 0) |
| G3 浏览器门 | 桌面 + 竖屏双视口人工检查 | 无裁切、无碰撞、焦点顺序正确、reduced-motion 正常 |
| G4 契约门 | depth-levels 评分卡 + 截图对比 | 与所选深度匹配,逐轴判断 |
Architecture
图中体现的关键设计:
- 关卡串联而非并联:G2(脚本)失败直接回到实现阶段,不会进入昂贵的人工浏览器检查。便宜的自动检查永远先跑。
- error/warning 分级:
audit-ark-ui.mjs内部把检查分为决定退出码的 error 与只提示的 warning,让"必须修"与"建议修"分离。 - QA 入口服务截图证据:当难以触达某个代表性状态时,用确定性的非持久 QA 入口暴露该状态,而不是放弃交互证据——这是"证据锁"的核心配套机制。
主流程:从实现到放行
第一步:项目门(G1)
SKILL.md Validate 第 1 条要求先跑目标项目自身的 tests / lint / build。这一关不依赖 skill 提供的任何脚本,纯粹是宿主项目的回归底线。
第二步:脚本审计门(G2)
运行方式(出自 SKILL.md Validate 第 2 条):
node "$CODEX_HOME/skills/ark-ui/scripts/audit-ark-ui.mjs" <html-or-css-path>Source: SKILL.md
脚本会对目标路径做递归收集,然后输出 JSON 报告并以 errors.length ? 1 : 0 决定退出码。文件收集逻辑是理解其边界的第一步:
1const root = resolve(process.argv[2] ?? '.');
2const supported = new Set(['.html', '.css', '.js', '.mjs', '.jsx', '.ts', '.tsx', '.vue', '.svelte']);
3
4async function collect(path) {
5 const info = await stat(path);
6 if (info.isFile()) return supported.has(extname(path)) ? [path] : [];
7 const entries = await readdir(path, { withFileTypes: true });
8 const nested = await Promise.all(entries
9 .filter((entry) => !['node_modules', '.next', 'dist', 'build'].includes(entry.name))
10 .map((entry) => collect(resolve(path, entry.name))));
11 return nested.flat();
12}
13
14const files = await collect(root);
15if (!files.length) {
16 console.error(`No supported frontend files found at ${root}`);
17 process.exit(2);
18}Source: scripts/audit-ark-ui.mjs
要点:
- 只处理 9 种前端扩展名;传入二进制或非前端目录会得到退出码 2。
- 显式跳过
node_modules / .next / dist / build,避免把依赖与产物当作交付物审计。 - 所有检查基于把全部文件文本拼接成
all、HTML/CSS/JS 分别拼接为html/css/js四个大字符串后做正则匹配——这是一种刻意的**启发式(heuristic)**设计:不解析语法树、不依赖框架,保证对任意技术栈(Vue/Svelte/React/原生)都能跑。
审计项分级表
audit-ark-ui.mjs 通过统一的 check() 收集结果:
1const errors = [];
2const warnings = [];
3const passes = [];
4
5function check(condition, pass, fail, severity = 'warning') {
6 if (condition) passes.push(pass);
7 else (severity === 'error' ? errors : warnings).push(fail);
8}| 检查 | 严重级别 | 通过条件(正则语义) | 设计意图 |
|---|---|---|---|
| 官方 CDN 资产 | error | 不匹配 web(-ipv6)?.hycdn.cn 或 hypergryph.com/*.png/jpg/svg/woff… | 法律红线:不得内嵌受保护资产 |
| 品牌/logo 引用 | error | 不出现 (rhodes island|arknights|endfield|monster siren)…(logo|wordmark|copyright) | 同上,防伪造授权 |
| reduced-motion | error | CSS 中存在 prefers-reduced-motion 规则 | 可达性不可妥协 |
| 响应式/方向 | error | CSS 中存在 @media … (max-width|orientation) | 不允许只缩放桌面布局 |
| focus-visible | error | CSS 中存在 :focus-visible | 键盘焦点可见 |
| viewport meta | error | HTML 存在 name="viewport" | 移动正确渲染前提 |
| lang 属性 | error | <html … lang= 存在 | 屏幕阅读器语言 |
| 语义 landmark | error | 存在 <main/nav/header/footer> | 结构语义 |
| ID 引用解析 | error | aria-* / for 引用的 id 全部声明 | 防 ARIA 悬空引用 |
| 语义颜色令牌 | warning | CSS 中存在 --ark-(ink|paper|signal) | 鼓励语义令牌而非散落字面色 |
| JS 选择器钩子 | warning | JS 中 querySelector(All) 的字面选择器在 HTML 中可命中 | 防"改了类名断 wiring" |
| 科幻陈词滥调 | warning | 不出现 scanline / glitch / random-hex / matrix-rain | 防风格化退化为俗套噪声 |
| 字面颜色数量 | warning | #hex 出现次数 < 180 | 推动收敛进令牌系统 |
其中"JS 选择器钩子"检查尤其值得展开——它是 SKILL.md 实现守则第 3 条(重命名 starter 类名/ID 时必须同步更新所有 JS 选择器)的机械化执行:
const queriedSelectors = [...js.matchAll(/\bquerySelector(?:All)?\(\s*(["'])([^"']+)\1\s*\)/g)]
.map((match) => match[2]);
const missingSelectors = [...new Set(queriedSelectors.filter((selector) => !htmlHasSelector(selector)))];Source: scripts/audit-ark-ui.mjs
htmlHasSelector() 依次尝试 #id、.class、[attr] 三种形式,把选择器翻译成对 HTML 文本的匹配;ID 引用检查则从 aria-controls/aria-labelledby/aria-describedby/for 抽取被引用 id,与声明的 id 集合求差集。
第三步:浏览器门(G3)
SKILL.md Validate 第 3、4 条要求在桌面宽度与竖屏宽度各渲染一次,检查裁切、文本碰撞、焦点顺序、激活态与 reduced-motion 行为;并在浏览器中实际操作主要控件、检查运行时错误。配套守则是"do not infer behavior from static markup alone"——静态标记不能替代运行时证据。
对 skill 自带的 showcase 样本,这一关由脚本机械化:
node "$CODEX_HOME/skills/ark-ui/scripts/capture-showcases.mjs" --mobileSource: README.md
capture-showcases.mjs 在精确的桌面与(可选)移动 CDP 视口下渲染五个样本,遇到水平溢出(horizontal overflow)直接失败——把"不许用横向滚动解决布局碰撞"变成可执行断言(脚本职责出自 SKILL.md Bundled code 清单)。
第四步:契约门(G4)与评分卡
references/depth-levels.md 定义了在代表性视口上评审的六个轴:
- Shell transformation(外壳转换) — 原生结构保留 / 重组 / 全系统重构 / 按模式定制
- Stage layering(舞台分层) — 无或一层 / 一到两层 / 两到四层 / 四到六层
- Component coverage(组件覆盖) — 仅关键控件 / 共享主件 / 完整共享集 / 状态特定变体
- State instrumentation(状态仪表化) — 选择提示 / 分组状态 / 全系统真实状态 / 改变构图的实时状态
- Motion coordination(动效协调) — 仅直接反馈 / 揭示+直接 / 多族协调 / 章节级编排
- Responsive recomposition(响应式重构) — 安全堆叠 / 外壳适配 / 完整重美术方向 / 模式特定构图
评分卡的元规则(防止机械凑分):
"The level is a holistic judgment. Do not average scores mechanically or add decorative elements merely to increase a score. A level fails if its extra depth reduces task clarity, truthful state visibility, accessibility, or performance beyond the product's budget." Source: references/depth-levels.md
此外还有两条边界校验(SKILL.md Validate 第 10 条):
- 最密的屏幕不得超过所选深度一个局部级别;
- 主屏幕不得低于所选深度;
- 有意为之的局部例外必须记录。
Core Flow:证据锁迭代中的验证回路
这条回路里的"证据锁"(evidence lock)规则是验证流程能闭环的原因:
- 改代码前必须先命名主家族、深度与台账中的确切公开模式——把审美争议锚定在可核查证据上。
- 每轮只改"一个共享令牌族 + 一个深度行为 + 一个代表性屏幕",把变更控制在可归因范围内。
- 截图必须在相同视口下做 before/after;若结果没有"visibly closer to the cited evidence"、降低了可读性、引入碰撞或让真实机制更难找到,则回退或修订。
- "compile, interaction, contrast, and screenshot checks pass"是进入下一轮的硬前置。
Source: SKILL.md
QA 入口的严格定义
SKILL.md 对"非持久 QA 入口"给出了操作性定义,避免它变成永久后门:
- 非持久 = 测试挂载显式抑制存档写入与存档删除,而不仅是创建内存对象;
- 坐标自动化不可靠时,通过 QA 入口直接暴露确切的模态/确认状态,而不是放弃交互证据;
- 对自动隐藏的过渡,QA 入口可以以"禁用自动隐藏"方式重启正常动画,但不得改变生产时序;
- 必须在瞬态 UI 可见时完成截屏与验证。
Source: SKILL.md
Usage Examples
运行启发式审计(HTML 或 CSS 路径)
node "$CODEX_HOME/skills/ark-ui/scripts/audit-ark-ui.mjs" <html-or-css-path>Source: SKILL.md
审计报告输出格式
脚本最终以 JSON 打印报告并按错误数决定退出码,便于 CI 直接消费:
const report = { root, files: files.length, errors, warnings, passes };
console.log(JSON.stringify(report, null, 2));
process.exit(errors.length ? 1 : 0);Source: scripts/audit-ark-ui.mjs
深度契约的稳定键表示(契约门校验对象)
审计与人工评审最终对照的是用稳定键表达的契约,HTML 用根属性、React 用组件 props,两轴(family/depth)可独立变化:
<html data-ark-theme="endfield" data-ark-depth="complex">Source: references/depth-levels.md
<ArkShell theme="endfield" depth="complex" />Source: references/depth-levels.md
skill 级验证入口(维护本 skill 时)
python3 "$CODEX_HOME/skills/.system/skill-creator/scripts/quick_validate.py" "$CODEX_HOME/skills/ark-ui"Source: README.md
API Reference
audit-ark-ui.mjs(CLI)
node audit-ark-ui.mjs [root]参数:
root(可选,默认.):要审计的 HTML/CSS 文件或目录;目录会递归收集支持的扩展名并跳过node_modules/.next/dist/build。
输出: JSON 报告 { root, files, errors[], warnings[], passes[] }。
退出码:
0:无 error 级失败(warning 不影响)1:存在至少一个 error2:路径下没有受支持的前端文件
capture-showcases.mjs(CLI)
node capture-showcases.mjs [--mobile]参数:
--mobile:附加移动端 CDP 视口截屏。
行为: 在精确桌面视口(及可选移动视口)渲染五个 showcase 样本,遇到水平溢出即失败。
说明:
capture-showcases.mjs与quick_validate.py、analyze-css-evidence.py的完整内部实现未在本次源码阅读范围内,以上职责描述取自 SKILL.md 的 Bundled code 清单与 README 中的调用示例;实现细节未在源码中核实之处如实标注。
Failure Modes, Edge Cases & Concurrency
已知边界与降级路径
| 情形 | 行为 | 依据 |
|---|---|---|
| 传入路径无受支持前端文件 | 退出码 2,stderr 提示 | collect() + 显式 process.exit(2) |
| 出现 error 级失败 | 退出码 1,CI 阻断 | process.exit(errors.length ? 1 : 0) |
| 只有 warning | 退出码 0,仅提示 | 同上 |
| 无法解析的选择器形式 | htmlHasSelector 返回 true(默认放行) | 只覆盖 #id / .class / [attr] 三种字面形式 |
| 模板字符串拼接的动态选择器 | 不被 querySelector 正则捕获 | 启发式设计的已知盲区 |
| 深度超标或不足 | 契约门不通过,需记录或修正 | Validate 第 10 条 |
| 状态难以触达 | 必须建非持久 QA 入口,不得放弃证据 | Evidence lock 第 6 条 |
语义边界(为什么是启发式而非解析器)
审计脚本刻意选择拼接文本 + 正则而非 AST:
- 要兼容
.vue/.svelte/.jsx/.tsx等所有技术栈,任何单一解析器都会失配; - 检查项本身是"是否存在某类规则/资产"的存在性判断,正则足够;
- 代价是精度有限(例如颜色计数
< 180是经验阈值、陈词滥调靠关键词命中),因此这些项是 warning 而非 error。
Performance & Operational Notes
- 关卡成本排序:G1(项目构建)与 G2(脚本审计,纯本地文件读取 + 正则)远便宜于 G3(人工浏览器双视口)与 G4(对照评分卡)。架构上把便宜的自动关卡放在前面,避免在必然返工的状态上消耗昂贵检查。
- 审计脚本 I/O 并行:所有文件用
Promise.all并发读取,目录遍历也并发展开,对大仓库友好。 - 截屏回归可机械化:对 skill 自带 showcase,
capture-showcases.mjs把"同视口 before/after"要求固化为脚本断言(水平溢出即失败),可作为第三方项目自建截屏回归的模板。 - 升降级同样以验证为前置:depth-levels 的 Escalation/reduction 规则要求"每补上一个缺失系统就先 Validate 再继续加";降级时先移除信息价值最低的持久层,且绝不靠隐藏必要信息或把字号压到不可读来"假装极简"。
Extension Points
- 新增审计项:在
audit-ark-ui.mjs中追加一条check(condition, passText, failText, severity)即可,severity 决定它是 error(阻断)还是 warning(提示)。 - 调整阻断策略:把某条 warning 升为 error 只需改
check()的第四个参数,例如把"语义颜色令牌"升级为硬门。 - 接入 CI:直接消费退出码(0/1/2)与 JSON 报告即可,无需额外封装。
- 评分卡扩展:六轴之外新增轴时,应保持"holistic judgment"原则——不允许机械平均,也不允许为提分加装饰。
- 家族/深度双轴解耦:契约用
data-ark-theme+data-ark-depth两个根属性表达,验证时可独立替换任一轴而不动语义内容与可访问名称。