Skip to content
Merged
2 changes: 1 addition & 1 deletion Agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ Merge freshness: `uv run --no-sync python3 scripts/check-merge-freshness.py <PR>
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 解析并报出所测的树)
Vote count: `uv run --no-sync python3 scripts/check-vote-count.py <PR>...` — 合并前数**仍然算数**的 LGTM 票,而不是评论里看得见的 ✅ 行数。三条规则让手工计数每次都错:① **票早于 head push 即作废**(rebase/解冲突推新 head 后,旧 ✅ 说的是已经不存在的 commit,而评论历史照样显示五六行 ✅,PR 看着能合实则零票——2026-09-11 实测 #1133/#1134/#1136/#1137 各显示 4-6 张 LGTM,解锁后**有效票全为 0**);② **❌ 重置计数**(三次 ✅ 后一个 needs-fix,再一个 ✅ 只算一票);③ **同一周期只算一票**(否则一个周期独自把 PR 送过门槛)。判票读正文首行**首个内容字符**(`gh pr review --comment` 让 GitHub 把 ✅ 和 ❌ 都记成 `COMMENTED`,state 字段在这里毫无用处);该字符**透过 markdown 装饰**读取(`**❌`/`- ❌`/`> ❌`/`## ❌`/`1. ❌` 都算,装饰字符本身不可能是判决),并**由它决定整行**——✅ 开头即使后文提到 ❌ 仍是批准(那些提及是在说「没有 ❌」,说法是开放集合),整行扫描只作为无开头标记时的兜底(散文式「Not LGTM」为否决、「Result: ❌ needs fix」为否决);**首行完全不含判决时**(散文开头),继续在其后各行找**成句的**否决——标记必须**位于该行开头**,因此本仓库批准票里那种**描述**已解决否决的句子(「The earlier ❌ was resolved by pushing the fix myself」)仍算批准(2026-09-11 #1144 合并后由探针发现:此前只读首行 ⇒ 散文开头 + 下文否决被判为 `comment`,而 comment 被跳过 ⇒ run 不重置、陈旧批准照印 `READY 3/3`;381 条真实 body 分类零变化,该形态真实但当前未被使用)。周期号从正文里取(`cyc20260911-091230`)。push 时间取**该 SHA 上最早一次 workflow run 的创建时间**(就是 GitHub 收到 push 事件的时刻);没有 run 时退回 commit 日期并**在输出里注明**(commit 日期可能早于 push,是乐观方向,不许默默采信)。exit 0 每张 PR 都够票 / 1 有 PR 缺票 / 2 问不出来(gh 失败、响应不可解析)——问不出来的问题绝不报数。评论**逐页取全**(reviews 端点默认只给 30 条且**按时间正序**,超限时丢掉的正是**最新的**票,也就是唯一算数的那几张;#1134 解锁一次就有 7 条评论),并在本地按时间排序(「❌ 重置计数」和「同周期只算一票」都是**位置相关**的规则,依赖服务端返回顺序等于把判决权外包)。两个自暴露的坑:① gh 的 `--jq` **后者覆盖前者**,helper 若自己追加 `--jq` 会吃掉调用方投影,字段名退回原名、`at` 读成空串,而 `"" <= push_time` 恒真 ⇒ **全部票作废**、有 2 票的 PR 显示 0/3(计数方向偏「安全」,没人会去查);故 helper 只管 `--paginate`,投影归调用方,并对载荷形状**运行时断言**(缺 `at` 即 exit 2,不报数)
Vote count: `uv run --no-sync python3 scripts/check-vote-count.py <PR>...` — 合并前数**仍然算数**的 LGTM 票,而不是评论里看得见的 ✅ 行数。三条规则让手工计数每次都错:① **票早于 head push 即作废**(rebase/解冲突推新 head 后,旧 ✅ 说的是已经不存在的 commit,而评论历史照样显示五六行 ✅,PR 看着能合实则零票——2026-09-11 实测 #1133/#1134/#1136/#1137 各显示 4-6 张 LGTM,解锁后**有效票全为 0**);② **❌ 重置计数**(三次 ✅ 后一个 needs-fix,再一个 ✅ 只算一票);③ **同一周期只算一票**(否则一个周期独自把 PR 送过门槛)。判票读正文首行**首个内容字符**(`gh pr review --comment` 让 GitHub 把 ✅ 和 ❌ 都记成 `COMMENTED`,state 字段在这里毫无用处);该字符**透过 markdown 装饰**读取(`**❌`/`- ❌`/`> ❌`/`## ❌`/`1. ❌` 都算,装饰字符本身不可能是判决),并**由它决定整行**——✅ 开头即使后文提到 ❌ 仍是批准(那些提及是在说「没有 ❌」,说法是开放集合),整行扫描只作为无开头标记时的兜底(散文式「Not LGTM」为否决、「Result: ❌ needs fix」为否决);**首行完全不含判决时**(散文开头),继续在其后各行找**成句的**否决——标记必须**位于该行开头**,因此本仓库批准票里那种**描述**已解决否决的句子(「The earlier ❌ was resolved by pushing the fix myself」)仍算批准(2026-09-11 #1144 合并后由探针发现:此前只读首行 ⇒ 散文开头 + 下文否决被判为 `comment`,而 comment 被跳过 ⇒ run 不重置、陈旧批准照印 `READY 3/3`;381 条真实 body 分类零变化,该形态真实但当前未被使用)。周期号从正文里取(`cyc20260911-091230`)。**票够 ≠ 能合**:合并规则是三段合取(3 ✅ + `MERGEABLE`/`CLEAN` + CI 绿),本工具以前只答第一段却印 `READY`——2026-09-12 实测 **6 张** PR(#1152/#1151/#1145/#1142/#1141/#1136)各印 `READY 3/3` 而 `mergeable=CONFLICTING`/`mergeStateStatus=DIRTY`,全是 `gh pr merge` 会拒的,队列看着像六张在等手续实则是死锁;故现读 `gh pr view --json mergeable,mergeStateStatus`,**只降不升**:`CONFLICTING` ⇒ 印 `BLOCKED`(与 `SHORT` 分开,因为药方相反:缺票要回来看,冲突要先把分支追上 master 解掉,而解冲突必然推新 head、把上面所有票作废 ⇒ 先投票是白投;两者都中时 `BLOCKED` 压过 `SHORT`,否则又是在指错药方);`UNKNOWN`(GitHub 尚未算完,刚 push 常见)与任何不认识的值 ⇒ exit 2 绝不猜;`MERGEABLE` 只用于「不冲突」这一个方向,**不当作安全证据**(GitHub 答的是「文本能否合」,而两个同值计数行干净自动合并正是危险那种——见 `check-merge-freshness.py`)。push 时间取**该 SHA 上最早一次 workflow run 的创建时间**(就是 GitHub 收到 push 事件的时刻);没有 run 时退回 commit 日期并**在输出里注明**(commit 日期可能早于 push,是乐观方向,不许默默采信)。exit 0 够票且可合 / 1 有 PR `SHORT`(缺票)或 `BLOCKED`(合不了)/ 2 问不出来(gh 失败、响应不可解析、mergeability 尚未算出)——问不出来的问题绝不报数。评论**逐页取全**(reviews 端点默认只给 30 条且**按时间正序**,超限时丢掉的正是**最新的**票,也就是唯一算数的那几张;#1134 解锁一次就有 7 条评论),并在本地按时间排序(「❌ 重置计数」和「同周期只算一票」都是**位置相关**的规则,依赖服务端返回顺序等于把判决权外包)。两个自暴露的坑:① gh 的 `--jq` **后者覆盖前者**,helper 若自己追加 `--jq` 会吃掉调用方投影,字段名退回原名、`at` 读成空串,而 `"" <= push_time` 恒真 ⇒ **全部票作废**、有 2 票的 PR 显示 0/3(计数方向偏「安全」,没人会去查);故 helper 只管 `--paginate`,投影归调用方,并对载荷形状**运行时断言**(缺 `at` 即 exit 2,不报数)
Git-over-https 兜底: `python scripts/sync-master-from-api.py [--repo owner/name] [--ref master]` — 受限网络下 github.com:443 不可达而 api.github.com 可达时,用 Git Data API 的 verification payload + signature 字节级重建上游 commit(含 web-flow GPG 签名 squash merge,reconstruct_commit 经 hermetic 测试验证 sha 一致)并推进本地 refs;内容对象缺失时 fail-loud 提示改用 git fetch(10+ 周期实证的恢复路径)
Git-over-https push 兜底: `python scripts/push-branch-from-api.py --branch feature/x [--ref HEAD] [--force]` — 同一宕机场景下的 push 方向(#988 配对):从本地 ref 沿一父链找到远端基点(已有分支头或首个远端已知祖先),自底向上上传 blobs(原始字节)/trees(`git mktree` 语义复算)/commits(结构化创建,author/committer 携带原始 +0800 偏移、消息去尾随换行——GitHub 规范化行为),更新远端 ref 后把本地分支 ref 重写为远端 sha 并 `git diff` 验证内容一致;失败即止不触碰 refs(hermetic 测试经忠实假 API 验证字节级 sha 一致)
Merge pairs: `uv run --no-sync python3 scripts/check-merge-pairs.py [--base <ref>] <PR>...` — 合并前问「**任意两张** PR 能不能一起落地」:m 张 PR 有 m*(m-1) 个有序对,每对实测 master -> A -> B 并让守卫判 A+B 合并后的树。存在理由(cyc20260913-091152 实测 6 张可合并 PR 全 15 对、两个方向共 30 次):14 个有序对**冲突**(安全,git 拦下),**唯 1173↔1174 静默合并且落地坏树**(都写 `1564` ⇒ git 保留一份、零冲突,而合并树实收 `1566`)——两张各自 MERGEABLE/CLEAN、各自双绿、各自单测都 OK,**只有这一对不行**,而该工具族的默认计划(全部在飞 PR 升序)在第 1 步冲突即停、根本走不到这一对。顺序敏感:先落一张会推新 head 重测另一张,故每次落地前重跑。成本二次方(m 张 ≈ m*(m-1) 次守卫运行,master+A 每张只合一次),所以要传你真正在权衡的那几张。退出码:0 = 每个有序对都有答案且无「干净合并却落地坏树」的对(被冲突挡住的对**已作答**——它落不了地,也就不会落地落坏;这与 check-merge-sequence 的冲突=3 相反,那里链上后续步骤没测),1 = 存在这样的对,2 = 测不出来(响亮失败)。
Expand Down
Loading
Loading