Files
UI/ITERATION.md
T
2026-10-27 02:30:18 +08:00

210 lines
17 KiB
Markdown
Raw 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.
## 无人值守自我迭代协议(ITERATION.md)
> 本协议由自动化 Agent 执行,也可供人工参考。任何迭代必须遵守 `CODING.md`、`AGENTS.md`、`ARCHITECTURE.md` 三份规范,门禁 `npm run check` 不通过的一切产出作废。
## 运行模式:连续迭代 + 心跳监护
调度器(每小时唤起)**不是**迭代器,只做两件事:健康检查 + 中断重启。真正的迭代是单次唤起内的**连续循环**——反复执行《每轮迭代流程》,直到 backlog 清空或时间预算耗尽,中间不停歇。
- **并发锁**:`.ua/loop.lock`(含时间戳,不入库)。唤起时若锁存在且心跳在 15 分钟内 → 已有实例在跑,直接退出;否则写锁开始循环。每轮开始前刷新锁内时间戳作心跳。
- **时间预算**:单次唤起最多连续迭代约 50 分钟,到点在完成当前轮后停止(防止与下次唤起重叠)。**预算判断必须基于客观时钟**:点火时用 `date +%s` 记录起点并写入 `.ua/loop.lock`,每轮开始时重新取 `date +%s` 与起点求差,差值 ≥ 2700 秒(45 分钟)即到点;**禁止凭感觉估算时长**——LLM 无内部时钟,自估时长系统性偏高(实测自报 40+ 分钟而实际约 30 分钟)。
- **熔断**:同一 backlog 条目连续 2 轮失败 → 停止循环,锁照常释放,等待下次唤起。
- **重启语义**:锁心跳超过 15 分钟视为循环中断(崩溃/超时),调度器删旧锁、重新点火。
- **诊断入口**:拿不准该不该抢锁、上一条做完没有,先跑 `ua loop`(锁心跳 / 上次留痕 / backlog 待办与视觉欠账 / 下一条选题建议与下一个可用编号一次给全);它只读不写,`--json` 给脚本用,判据与本文的 15 分钟口径同源,不要另写一套。
- **调度器约束(踩过的坑,勿重复)**:本地调度器只支持 `HOURLY` 及以上粒度(`DAILY/WEEKLY/MONTHLY/YEARLY`),**`FREQ=MINUTELY` / `FREQ=SECONDLY` 不受支持**——写入后 `next_run_at` 会变成 `NULL`,首跑之后永久静默(实测 10/23–10/26 停摆三天)。因此「高频监护」不能靠加密 rrule 实现,只能靠**单次唤起内的连续循环**;心跳间隔 = 1 小时,循环预算 45–50 分钟,两者共同保证覆盖面。任何对自动化的改动(改 prompt、改 rrule、暂停恢复)都必须回查 `next_run_at` 非空,改 prompt 会把状态重置为 `PAUSED`,需再单独置 `ACTIVE`。
- **有效性自检**:每轮迭代首步除同步仓库外,检查 `.ua/loop.lock` 与 `automation_runtime_state.last_run_at`;若发现上次运行距今超过 2 小时(说明调度中断),在留痕 `summary` 里注明「调度中断后恢复」,便于周报统计停摆时长。
## 多实例错位接力
单条自动化的占空比受平台语义限制:**`next_run_at` = 本次运行结束时刻 + `INTERVAL`,不是整点对齐**(实测 12:46:50 结束 → 下次 13:46:50)。`INTERVAL` 下限 1 小时,45 分钟预算下单条仅 `45/(45+60) ≈ 43%` 覆盖面,即「干 45 分、歇 60 分」。因此用**两条同构自动化错位 30 分钟**补齐空档:
| 实例 | 锚点(错位) | 角色 |
|---|---|---|
| A `无人值守迭代循环` | 整点链(当前 13:46:50 → 逐轮顺延) | 主循环 |
| B `…B(错位接力)` | A 之后 30 分钟(当前 14:16:50) | 补位循环 |
- **接力靠 `missed` 队列自动完成**:到点时若另一实例仍持锁,该次被记为 `missed` 并保留 `missedScheduledAt`,待当前实例释放锁后**立即补跑**(实测间隔 9 秒)。无需人工干预,也不占用额外配置。
- **抢锁失败是正常行为**:看到 15 分钟内新鲜锁的实例直接回复「循环存活,未干预」退出——不计失败、不写留痕、不改 backlog。
- **同一把锁、同一份 backlog**:多实例共享 `.ua/loop.lock` 与 `qa/*` 分支流水线,任何时刻只有一个实例在写,冲突由锁而非分支解决。
- **调整排期的方法**:改 `validFrom`(`next = max(now, validFrom) + INTERVAL`),改完必须回查 `next_run_at` 非空且落在期望时刻;改 prompt 会把状态重置为 `PAUSED`,须再单独置 `ACTIVE`。
## 分支流水线
```
qa/probe ──门禁通过──▶ qa/stable ──人工审阅──▶ main(受保护,禁直接推送)
```
| 分支 | 用途 | 谁能写 |
|---|---|---|
| `main` | 稳定发布线,受 Gitea 分支保护 | 仅人工审阅后合并 |
| `qa/stable` | 集成测试线,所有通过门禁的迭代落在这里 | 自动化(仅 fast-forward 合并) |
| `qa/probe` | 试验分支,每轮迭代的工作现场,可被强制重置 | 自动化 |
## 每轮迭代流程(自动化 Agent 执行)
1. **同步**:`git fetch --all --prune`,切到 `qa/stable` 并 `git pull`;从其最新提交强制重建 `qa/probe` 并切换过去。
2. **选题**:读 `.ua/backlog.json`,先取 `status === "open"` 且 `domain === "visual"` 的条目(有多条取编号最小的一条);没有视觉欠账时再按数组顺序取第一条 `status === "open"` 的条目;若全部关闭,按《选题规则》自行补充一条新点子后继续。
3. **实现**:按 `CODING.md` 编码规范实现,保持低侵入,单文件 < 200 行,涉及组件需同步 manifest/docs/llms-full.txt。
4. **门禁**:跑 `npm run check`(build → demo → style → typecheck → smoke → index → verify → lint-iterations → audit:visual → audit:contrast → audit:motion)。
- **通过**:提交到 `qa/probe`,提交信息按 `COMMITTING.md` 规范(`iter(<条目id>): <中文祈使句>`,元数据变更用 `chore(backlog): 记录 <id> 完成`;commit-msg 钩子自动校验),推送 `qa/probe`,fast-forward 合并到 `qa/stable` 并推送;将 backlog 条目 `status` 置为 `done` 并随迭代提交。
- **失败**:尝试修复一次;仍失败则放弃本轮——`qa/probe` 直接重置回 `qa/stable`,backlog 条目在 `notes` 里追加失败原因、`status` 保持 `open`,提交并推送该元数据变更(仅改 `.ua/backlog.json`,不碰代码)。
5. **留痕**:在 `.ua/iterations.log` 追加一行 JSON,**字段统一**:`{"ts":"本地ISO时间","id":"条目id","result":"done|aborted","commit":"短哈希","summary":"一句话"}`。硬性规范:`ts` 必须来自 `date` 命令实测(本地时间,带 +08:00 偏移,**禁止手写或用 UTC 换算**);时长结论必须来自两次时间查询之差,禁止使用「大约/接近」等估算词;并发实例格式不得自创变体。随本轮提交。`ua lint-iterations` 会把每行 `commit` 拿到 git 历史里回查:哈希必须真实存在、主题必须指向同一条目(`iter(<id>)`,早期无 scope 的 `iter: <id>` 也认),伪造或抄错哈希直接判违规。
6. **边界**:不改 `main` 分支保护、不删分支、不动 `.workbuddy/`、不重写历史、不 force push 到 `qa/stable` 与 `main`。
## 选题规则(自主补点子时)
- 只从以下方向选:组件补充(皮肤包缺的常规组件)、既有组件的可用性细节、文档站与 Agent 文档完善、构建/门禁工具链改进、演示页完善。
- 单轮改动范围可控:新增文件 + 少量既有文件修改,避免跨包大规模重构。
- 每条点子必须写清 `title / motivation / acceptance`(验收标准,可被 `npm run check` 与人工检查验证)。
### 域轮换与视觉欠账优先
`backlog.json` 的条目带可选字段 `domain`:`new`(新增组件)/ `tune`(既有组件打磨)/ `visual`(视觉地基与观感)/ `tooling`(门禁、CLI、文档、配方)。选题按下面三条排队,保证视觉欠账会被持续消化,而不是永远排在「加新组件」后面:
1. **域轮换**:连续 3 轮迭代中,至少 1 轮的条目 `domain` 为 `visual`。若已连续 3 轮都不是 visual,下一轮必须从 open 的 visual 条目里选。
2. **视觉欠账优先**:只要存在 `status === "open"` 且 `domain === "visual"` 的条目,它的优先级高于 `domain === "new"` 的条目——先清视觉欠账,再加新组件(视觉地基不牢,新组件只是把漂移再复制一份)。
3. **新增组件的入库门槛**:`domain === "new"` 的条目,验收标准里必须写明「样式只消费 `--ua-*` 令牌,禁止新增硬编码阴影/时长/字号」,否则该条不得入库。视觉类改进的验收标准要能落到 `npm run audit:visual` 的计数上(涨了就是没做完),涉及颜色令牌的还要落到 `npm run audit:contrast`(对比度跌破阈值即没做完)。
## 视觉回归抽查清单(人工,视觉类条目收尾时逐条过)
视觉改动只能靠人眼验收,凭记忆看一遍必然漏项。本节把十三个代表组件(六个高频 + 容器 / 数据展示 / 图表类各取代表)的观感要点固化成可勾选项,视觉类条目(`domain === "visual"`)提交前照单过一遍,勾不上的就是没做完。
- **格式约定**:三条横线以下是清单本体,每个组件一个 `### <组件id>` 段,每条以 `- [ ] 【维度】` 开头;维度取值固定五类:`边框` / `阴影` / `过渡` / `焦点环` / `暗色`。
- **门禁口径**:`ua verify` 校验本节存在、六个组件齐全、每个组件 ≥3 条、合计 ≥18 条、五个维度全覆盖;条目改写或删漏直接报错。
- **与自动门禁的分工**:`npm run audit:visual` 管「字面量有没有回流成硬编码」,本节管「令牌用对了但观感对不对」——两者都过才算一轮视觉改动完成。
---
### button
- [ ] 【边框】次按钮与描边按钮的 1px 描边在页面底与卡片底上亮度一致,不出现某个按钮偏黑
- [ ] 【阴影】hover 只上浮 1–2px 且走 elevation 令牌,没有「影子 + 边框」双层叠加
- [ ] 【过渡】hover 上浮与按下回缩能看出先后,时长不超过 `--ua-duration-fast`
- [ ] 【焦点环】键盘 Tab 到按钮时环完整贴边不被容器裁切,与描边按钮的描边不重影
- [ ] 【暗色】暗色下主按钮文字与底色对比仍够,描边按钮的边不消失
### textField
- [ ] 【边框】默认 / hover / 聚焦三态只变色不变粗细(1px 始终 1px)
- [ ] 【焦点环】聚焦是光环外扩而非描边变粗;带前后缀时环包住整组而不只是输入框
- [ ] 【过渡】聚焦与失焦的边框色过渡在 `--ua-duration-fast` 内完成,无闪烁感
- [ ] 【暗色】暗色下占位文字(placeholder)与输入文字都可读,边框仍可见
### select
- [ ] 【边框】触发器与相邻 textField 同高同粗细,并排时基线不歪
- [ ] 【阴影】下拉弹层用 `--ua-shadow-2` 档,出现在滚动容器内时边缘不被裁成硬边
- [ ] 【过渡】弹层进出各一次(位移 + 淡入),关闭后不残留一帧
- [ ] 【焦点环】键盘 ↑↓ 唤出弹层时焦点环留在触发器上,不跳到 body
### form
- [ ] 【边框】校验失败只把边框描红、不常驻光环:未聚焦的失败字段与正常字段粗细一致,只有颜色不同
- [ ] 【焦点环】失败字段获得焦点时才亮危险色光环,且不与已描红的边框重影成双线
- [ ] 【过渡】改好一项后错误文案退场、汇总条数字跟着减,字段高度不跳(错误区为空时不留白)
- [ ] 【暗色】暗色下错误文案与红边在表面底上都读得清,不被 danger 淡底吃掉
- [ ] 【阴影】提交按钮转圈期间投影不叠加,loading 与 hover 同时成立也不出现双层阴影
### modal
- [ ] 【阴影】弹窗用 `--ua-shadow-4`,与遮罩形成明确层级,不糊成一片
- [ ] 【边框】页头 / 页脚分割线用 `--ua-border-subtle`,观感与卡片页脚一致
- [ ] 【过渡】弹入有轻微缩放位移,Esc 关闭后无残影、无布局跳动
- [ ] 【焦点环】打开后焦点落在弹窗内首个可聚焦元素,Tab 不逃出弹窗
- [ ] 【暗色】暗色下弹窗表面与遮罩可区分,标题与正文层级仍清楚
### dataTable
- [ ] 【边框】行分割线用 `--ua-border-subtle`,连续 20 行不显重,表头下沿不发黑
- [ ] 【阴影】粘性表头滚动透底无效(背景实色 + 底部 1px 细线贴合)
- [ ] 【过渡】行 hover 与选中底色过渡柔和,选中行不抢表头与首列的视线
- [ ] 【暗色】暗色下斑马纹可辨但不突兀,选中行与 hover 行仍能区分
### toast
- [ ] 【阴影】通知用最高档投影,堆叠三条时前后层级清楚
- [ ] 【边框】通知卡片描边不抢语义色图标,警示色不溢出到边框
- [ ] 【过渡】进场 / 出场各一次,堆叠时其余条目平滑让位,不出现跳位
- [ ] 【暗色】四种语义色(info / success / warning / error)在暗底上图标与文字都不糊
### layout
- [ ] 【边框】页头细线与侧栏描边各只一道,两者交汇处不出现双线加深
- [ ] 【过渡】侧栏折叠展开展收干净,内容区不出现横向抖动或文字重排闪烁
- [ ] 【暗色】暗色下页头、侧栏、内容区三层表面能分出层次,不糊成同一块灰
### jsonViewer
- [ ] 【边框】嵌套层级的左侧竖线与容器外框同色同粗细,深三层不显重
- [ ] 【过渡】展开 / 收起即时切换,箭头旋转不与行高变化打架(不推动下方内容跳动)
- [ ] 【暗色】四种叶子类型(字符串 / 数字 / 布尔 / null)在凹陷底色上都能分辨,不互相撞色
- [ ] 【焦点环】Tab 到折叠开关时环贴住整行左缘,不被容器 overflow 裁掉
### diffView
- [ ] 【边框】统计条下沿与差异区之间只一条细线,与代码块头部观感一致
- [ ] 【过渡】滚动长差异时行底色不渗出双行号列,行号列保持静止
- [ ] 【暗色】增行为绿、删行为红的底色在暗底上仍可辨,文字对比不被底色吃掉
### codeBlock
- [ ] 【边框】头部与代码区之间一条细线,悬停整行提亮不出界、不盖住行号
- [ ] 【过渡】悬停提亮瞬时完成,连续快速移动不留下残影行
- [ ] 【暗色】行号(faint)与代码正文在凹陷底上对比都够,行号不糊到底色里
### signaturePad
- [ ] 【边框】画布与底部工具条之间一条细线,四角圆角裁切笔迹不溢出
- [ ] 【过渡】「清空」在未落笔时变为不可点,禁用态与可用态能一眼分清
- [ ] 【暗色】暗色下笔迹(走 text 令牌)在画布表面上清晰可见,不发灰
### heatmap
- [ ] 【边框】五个色阶块与网格格子同尺寸同圆角,图例看起来像「网格的一小段」而不是另做的一排
- [ ] 【过渡】空窗周(零档)与有数据的列并排时,凹陷底与页面底能区分,不出现「缺了一块」的错觉
- [ ] 【暗色】暗色下四档浓度仍能依次分辨(相邻两档不糊成一档),最高档不刺眼
### sparkline
- [ ] 【边框】折线不被 viewBox 边缘裁掉,末点圆点完整可见(内缩留白够)
- [ ] 【过渡】数值全等时折线落在中线而不是贴顶或贴底(除零保护生效)
- [ ] 【暗色】暗色下面积填充比折线淡一档,不压过主数值文字
### commandPalette
- [ ] 【阴影】命令面板用最高档投影 + 遮罩,与底下的内容明确分层,不糊成一块
- [ ] 【过渡】打开时面板轻缩放位移入场,输入过滤结果切换不跳行高
- [ ] 【焦点环】输入框始终持有焦点,↑↓ 移动只改选中行、光环留在输入框上
- [ ] 【暗色】暗色下选中行淡色底与未选中行可辨,快捷键角标(kbd)仍读得清
### noticeBar
- [ ] 【边框】左侧语义色只出现在图标与细竖条上,卡片描边不被语义色染透
- [ ] 【过渡】关闭走淡出退场,收起后下方内容平滑上移、不砸下来
- [ ] 【暗色】四种语义色(info / success / warning / danger)在暗底上图标与正文都不糊
### progressRing
- [ ] 【边框】轨道环与进度环同宽同圆角端点,交接处不出现缺口或重叠
- [ ] 【过渡】进度变化时环长平滑过渡,不确定态转圈期间中心文字不抖
- [ ] 【暗色】暗色下轨道环仍可见(不被页面底吃掉),完成态语义色不刺眼
- [ ] 【焦点环】可聚焦(带 aria 进度语义)时焦点环贴住整个圆环外缘,不被容器裁掉
### sortableList
- [ ] 【边框】普通行 1px 描边与卡片描边同色同粗细;拖拽行换成虚线后视觉粗细不变
- [ ] 【阴影】拖拽行用 2 档投影抬升,且压过后面的行——投影不被相邻行的底色切掉半截
- [ ] 【过渡】悬停底色与把手转色的时长一致,放下后没有残留过渡在跑
- [ ] 【焦点环】Tab 进来时环贴住整行、不被容器裁切;禁用态下既不显示环也不给悬停反馈
- [ ] 【暗色】暗色下虚线 accent 边与 tint 底都能辨认,行内正文没有被淡底吃掉
### masonry
- [ ] 【边框】卡片不被列边界拆成两截,同一列上下两张的圆角都完整
- [ ] 【过渡】窗口变宽加列时卡片按序重排,没有「半张卡片」跳到隔壁列
- [ ] 【暗色】暗色下列间留白与卡片底能区分,横向不糊成一片
## 人工介入点
- 审阅 `qa/stable` 的提交,确认质量后合入 `main`(PR 或本地 merge 后推送)。
- 发现自动化产出质量下滑时:暂停自动化、在 backlog 中把质量要求写进条目、恢复运行。