Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ Renderer: `cd emrg/gui/renderer && npm run typecheck && npm test` (514: 5 snapsh
CI: `uv run pytest` (ubuntu + **windows-2025 matrix** — Windows pytest 回归在 PR CI 即失败,v0.2.29 教训 #725) + GUI tests + **actionlint workflow lint** (`rhysd/actionlint@v1.7.12` gate, #444 — workflow 解析错误在 PR CI 即失败,如 `if:` secrets 上下文)
Re-trigger: `scripts/re-trigger-ci.sh [branch]` (workflow_dispatch, #527 — 替代空 commit 重触发:Actions outage 会整段丢弃 push 事件,dispatch 走 API 路径不受影响)
Merge freshness: `uv run --no-sync python3 scripts/check-merge-freshness.py <PR>...` — 合并前问一句「这条绿色 CI 说的还是**将要合并的那棵树**吗」。`pull_request` 事件下 GitHub 构建的是 `Merge <head> into <merge-base>`(合到**分叉点**,不是当前 master):分叉点就是 master 时两者同一棵树,master 一移动就不是了,而 master 移动**不触发** `synchronize`(只有 push 分支才触发),于是绿灯永远保持绿灯却已经过期。#1137 实测:`MERGEABLE/CLEAN` + 双 job 全绿,两侧计数行都写成同一个 1397 ⇒ git **无冲突**自动合并、保留 1397,而合并树实收 1401,两个守卫在 master 上才变红——**干净合并才是危险的那一种**(冲突时人被迫看一眼,反而安全)。判定不用时间戳比较(时钟/秒级竞态会骗人)而用**图结构**:master 的 tip 是否是 head 的祖先(`compare/master...<head>` 的 `identical`/`ahead`),是则 merge base 就是 master 本身、判决可平移。**祖先性只是一半**:head 含 master 但**根本没有 CI 运行**(push 事件丢失 ⇒ `no checks reported`)同样不算新鲜,故第二个条件是「该 SHA 上存在一个**通过**的运行」。按 SHA 而非分支取运行(分支推两次会有两个运行)。exit 0 全部新鲜 / 1 至少一条过期 / 2 问不出来(坏 PR、gh 失败、状态不认识)——不认识的状态一律 fail-loud,绝不默认新鲜
PR base check: `uv run --no-sync python3 scripts/check-pr-base.py [--repo owner/name] [PR...]` — 合并前查每张 open PR 的 **base 分支是否还能到达 master**。`gh pr merge` 合的是 PR 的 **base 分支**,而 stacked PR(base 是另一条 feature 分支)的 base 在本仓库被 squash 合并后**永远不是 master 的祖先**:合进去只会落到那条死分支上,master 一无所获,而 PR 状态显示 MERGED、CI 双绿、票数也照常累计——三个现有闸门全都放行。实测(cyc20260911-210746):#1148 的 base 是 #1147 的分支,而 #1147 已 squash 合入 master,若照原样批准,这个「让 master 的分类器不再重复内容」的修复就会合进死分支,队列里留一张 MERGED 却什么都没改变的 PR。判据不是「base 是不是 master」(stacked PR 在父分支未合并时是正常且有用的工作流,正是 #1149 要服务的情形),而是**「合进这个 base 还能不能到 master」**:base 已在 master 上 ⇒ OK;base 是**某张 open PR 的 head 分支** ⇒ LIVE(父分支仍在飞行中,合进去会落到活着的父分支上、随父 PR 落地;不是死路,但也不是保证——父 PR 仍可能被**未合并关闭**,故不判失败、只提示优先 retarget 到 master);base 既不在 master 上、也没有任何 open PR 以它为 head ⇒ DEAD(须先 retarget)。**LIVE 与 DEAD 无法由 compare status 区分**(cyc20260911-215935 实测修正:飞行中的父分支**必然**有 master 没有的 commit,status 就是 `ahead`;master 因任何无关原因前进后它又变成 `diverged`,与 squash 合并后的死分支**逐字节相同**——五个 open PR 分支当时全部是 `ahead`,只因 master 尚未前进)。可判定的输入是「有没有 open PR 的 head 等于这条分支」,而 `headRefName` 本就在抓取的字段里;旧版直接用 status 判 OK/DEAD,于是**每个飞行中的 stacked 父分支都被报成 DEAD**(5/5 真实父分支),而它自己文档里的「live stacked base 返回 OK」在旧谓词下**不可达**(能到 OK 的非 master base 只有「内容已在 master 上」的已落地分支,那不是 stacked 工作流)。LIVE/DEAD 两侧各有变异验证测试(删掉 LIVE 分支、或把 `open_heads` 挪到 `prs` 过滤之后,测试即变红——后者是过滤顺序陷阱:父 PR 通常不在被问的 PR 列表里)。status 读法用 compare API(`identical`/`behind` = 在 master 上,`ahead`/`diverged` = 不在),四个状态都有测试钉住(把判据放宽成「不是 ahead 就算 OK」会让 `diverged` 蒙混过关)。exit 0 无死路 / 1 有 PR 停在死路 / 2 问不出来(gh 失败、响应不可解析)——问不出来绝不报 OK。retarget 命令:`gh api -X PATCH repos/<owner>/<repo>/pulls/<N> -f base=master`(`gh pr edit --base` 会因 Projects-classic 弃用报错)。
Release bump: `python3 scripts/bump-version.py <x.y.z>` — 一次改齐 8 处版本声明(`emrg/__init__.py`、`pyproject.toml`、`emrg/gui/package.json`、`emrg/gui/package-lock.json` 根 + `packages[""]`、`uv.lock`、`packaging/{build-runtime,make-installer,make-run-installer}.sh`);`--check` 只报告漂移(宿主侧自检,与 CI 的 test_version_sync 对称),`--dry-run` 预览不落盘。锚点缺失/数量不符即 fail-loud,绝不猜测;`uv.lock` 只改 `name = "emrg"` 那一行(v0.2.94 教训:直接 `uv run` 会把 lock 里所有 registry URL 重写成镜像,556 行环境噪声)。bump 后用 `uv run --no-sync pytest` 避免 uv 重生成 lock。**测的是「你站着的那个 checkout」**(与 doc/node count 两工具同一处修复:root 原先取自 `__file__` ⇒ 在 worktree 里跑主 checkout 的那份脚本会去**锁主树**——worktree 处于 9.9.9/7 处漂移时它报 `OK: all 8 version sources agree`,而 `bump` 会改写**另一个** checkout 的 8 处版本声明,含决定构建产物的那个)。现 root 从 cwd 解析,输出首行报 `tree: <path>`。详见 Agent.md「Releasing」
Doc count sync: `uv run --no-sync python3 scripts/check-doc-count.py [--measure|--resolve-conflict]` — **「Python 测试数不写进任何 tracked 文件」的守卫**(宿主侧自检,与 CI 的 `tests/test_doc_counts.py::test_no_tracked_file_states_the_python_test_count` 同一份规则,两处读的是本工具里的同一个正则)。默认扫描全仓 tracked 文件(`tests/` 与 `scripts/` 除外——那两个树是规则与它的探针住的地方,必须能拼出「写死的样子」;该排除有实测背书:466 个 tracked 文件里这两处之外的命中只有下面那两条真实声明)并**指名文件+行号+写法**,exit 1。`--measure` 现场测量当前树收集的测试数(`pytest --collect-only`,缺 pytest/解析不出即 fail-loud,绝不报假数)。`--resolve-conflict` 专治计数行的合并冲突:**解法不是选边而是去掉那个数字**(两侧各自去掉计数后文本必须完全相同才动手;措辞也不同即拒绝——那是内容冲突,得人读)。**存在理由(2026-09-13 实测,宿主的决定)**:写死一行派生数字既是冲突磁铁又是静默错误源——14 张在飞 PR 里 11 张冲突,**11/11 冲突的都是这一行**(每张加测试的 PR 都得改同一个数字),而两个分支写**同一个值**时 git 干净合并、悄悄留下陈旧值(实测 #1179+#1180 都写 1601 而合并树实收 1603,守卫只在合并后的树上才红)。故数字不写、现场测量:`README.md`/`README.cn.md` 早就这么做了(改挂 Tests badge)。**测的是「你站着的那个 checkout」**(2026-09-11 实测的坑:解冲突要在 git worktree 里干活,而 root 原先取自 `__file__` ⇒ 跑主 checkout 的脚本、量的却是**主树**,worktree 自己的 Agent.md 写 1401 而工具报 `OK: ... documents 1420`——**读错了树还说一致**)。现在 root 从 cwd 解析(cwd 同时有 `Agent.md` 与 `scripts/` 才算 checkout),拿不准就退回脚本自身 root,并在输出首行**报出所测的树**
Node count sync: `uv run --no-sync python3 scripts/check-node-test-count.py [--write|--dry-run]` — 直接问**真实运行器**(vitest / node --test)并校验 Agent.md 的 Renderer 与 GUI 两个总数:`tests/test_doc_counts.py` 只能**静态**数 `it(`/`test(` 定义(pytest 作业没有 node_modules),而静态计数只是运行器的**模型**——R2254(445→448)、#1120(同 stem 文件整份被吞)、#1125(`it.each`/`test.skip` 正则看不见)三次都是模型与实践脱节。GUI 侧按 CI 环境(EMRG_SKIP_INTEGRATION=1)运行后减去 1 个模块级 `skip()` 原因条目(该条目数会先断言为 1,形状变了就停手而不是报个看着像对的数)。缺 node_modules 即报该原因,绝不报假数。**同样测「你站着的那个 checkout」**(与 doc count 同一处修复:root 原先取自 `__file__`,在 worktree 里跑主 checkout 的脚本会量到主树,`--write` 会改错树;现从 cwd 解析并报出所测的树)
Expand Down
Loading
Loading