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 @@ -135,6 +135,7 @@ Merge pairs: `uv run --no-sync python3 scripts/check-merge-pairs.py [--base <ref
Merge sequence: `uv run --no-sync python3 scripts/check-merge-sequence.py [--base <ref>] <PR>...` — 合并前问「按**这个顺序**合,每一步落地的树还过守卫吗」。健康是**步骤**属性:这一步的输入是上一步产出的树,所以任何「单 PR」事实都推不出它。存在理由(cyc20260912-174026 实测):#1167 与 #1166 都带 master、都写 `(1541)` ⇒ 各自合入都 CLEAN 且自洽,**依次**合入却仍是 CLEAN 而守卫在合并树上红(1541 文档 vs 1560 实收)——两个相等值写同一行,git 保留一份、冲突为零、陈旧数字进 master。危险与信号**反向**:计数**值不同**必冲突(安全,逼人看一眼),**值相同**才静默通过,而 `check-merge-order.py` 恰好按「弄脏别人最少」排序 ⇒ 「挑最省事顺序」读起来正是那句不安全的建议。逐 PR 健康检查在此队列会**退化**:所有 head 都含 master ⇒ `merge-tree master head` 的结果等于分支自己的树 ⇒ 守卫只在回答「这条分支自洽吗」,而待审分支永远自洽。退出码:0 = **每一步都测过**且落地健康,1 = 有「干净合并却落地不健康树」的步骤(本工具存在的那个发现),2 = 无法测量(响亮失败,绝不把「没测成」报成健康;**空计划也算无法测量**——`args.prs or _open_pr_numbers(...)` 决定「零步」只可能是**计划根本没取到**,而旧行为会印 `all 0 step(s) ... pass the guards` 并退出 0,即对一个树都没量过的运行**断言通过**,故改为 2;实测同一行为在父修订 `6456a98` 与本修订上逐字相同 ⇒ 既有缺口而非本次回归),3 = **计划在冲突步中断**(只测了前缀,其后各步**根本没测**——`cyc20260913-084752` 实测:默认调用(全部在飞 PR 升序)在第 1 步 `#1136` 冲突即停,**一个树都没判**却退出 0,与「全部验证通过」在退出码上不可区分;三种状态药方不同——解冲突重排 vs 重排危险步 vs 重试测量失败——故各自一码,同 `check-vote-count.py` 分 `BLOCKED`/`SHORT`。只想知道「有没有发现问题」的调用方可判 `rc in (0, 3)`)。
Merge tree health: `uv run --no-sync python3 scripts/check-merge-tree-health.py [PR...] [--base <ref>]` — 合并前问**别人都不问的那一句**:把这张 PR 合下去,**产出的那棵树**还能不能通过本仓库自己的守卫?现有闸门问的都是「关于这张 PR」的问题(票还算不算数、base 还能不能到 master、CI 绿灯是不是关于这棵树、合下去会弄脏谁),没有一个问合并**之后** master 是否健康。存在理由(cyc20260912-040220 实测,清空 11 张绿灯 PR 队列时):#1133 与 #1140 各自加测试、各自把 Agent.md 的 Python 计数行改写成**对自己成立**的值(两张自己的树都是 1500 收 1500,自洽),而**合起来**:两边都写 1500 ⇒ git **无冲突**保留一份 ⇒ 合并树收 **1506** 而文档仍写 1500 ⇒ 守卫 FAIL。**无冲突的那种最危险**:有冲突会逼人看一眼,同一行干净合并看起来什么都没发生;而 `check-merge-order.py` 恰恰把这对报成「弄脏别人最少」,即「最便宜先合」会主动推荐那个会落地不一致树的合并(#1137 也是同一形态,只是走的是「CI 绿灯过期」那条路)。为什么不建模:计数**无法**由两侧推出来,不是 max 也不是相加——它是合并树实际收集到的数(取决于 import、conftest、参数化),所以**把合并树建出来**(scratch 目录,绝不动工作区)并在里面跑**该树自己的** `scripts/check-doc-count.py`(与 CI 同一个守卫、同一个路径、读它自己的树,后者自 #1140 起会打印 tree:),建模只会把猜的位置往上挪一层。**同一个周期实测的第二个坑(本工具存在的另一半理由)**:守卫会打印 `Fix with: ... --write`,而 `--write` 改的是**该守卫解析到的树**里的文件;跑完只提交**合并**而不提交**那次修改**,推上去的 head 依旧写着旧计数,CI 自己的守卫随即在推上去的 commit 上红(本地跑一次全绿也没用)——本工具判定的是**你问的那个已提交 head**,因为推前要问的是「我要合的这个 commit 过不过」,不是「我的工作区过不过」。exit 0 全部干净且健康 / 1 有「干净但合并树自身守卫不过」的(真正的发现)/ 2 问不出来(gh/git/守卫失败,绝不报健康);**冲突单列为 CONFLICT 不算失败**——合不成就没有合并树可判,那是 `check-merge-order.py` 的问题。`check-vote-count.py` → `check-pr-base.py` → `check-merge-freshness.py` → `check-merge-order.py` → 本工具:最后这一环才是「合完 master 还健康吗」
Conflict triage: `python3 scripts/classify-conflict.py <file>... [--all]` — 合并冲突的**分类器(只报告,绝不改文件,exit 1 = 有需要人判的块)**,把「两边都动了同一个文件」拆成五种可判定的形状并给出证据支持的解法:`count-line`(两侧只差一个**带括号的计数**数字 ⇒ 先分清它是「存储的计数」还是「旧分支带进来的写死值」:GUI/Renderer 的分解是文档里的事实,必须在合并后的树上**测量**;Python 测试数自 2026-09-13 起不写进任何文档,故那一行一律**删掉数字**,永不选边,交给 `check-doc-count.py --resolve-conflict`)、`duplicate`(两侧**声明符号**互为真超集**且共有符号的正文全同** ⇒ 取超集那侧)、`disjoint`(声明符号不相交 ⇒ **两边都保留**)、`overlapping`(同名符号,或超集但共有符号正文不同,或单行无判据 ⇒ 必须人读)、`identical`。存在理由(cyc20260911-112155 实测):同一轮里同一形状需要**相反**的解法——#1136 的分支带着 #1134 的**未合并副本**(分支基于 #1134 而后者已被 squash,祖先关系不可见),master 是严格超集故「取 theirs」正确;#1140 两侧是**不相交新增**,取任何一侧都会静默丢掉另一侧 4 个探针,唯一正解「两边都留」。一条 `--theirs` 对前者对、对后者错。两条判别轴都是被实测打出来的:① 必须是**声明符号**而非文本行——初版按内容行比较,因两侧都含 ` """` 与 ` )` 这种样板行,把两个真实冲突全判成 `overlapping`;② 名字子集**不等于**内容包含——只比名字时「master 声明了分支所有名字」被读成「master 含分支全部内容」,而两侧同名 `test_alpha` 正文不同时取超集会**静默丢掉分支对它的修改**(正是本工具要防的数据丢失);③ `count-line` 必须限定为**带括号的计数**,否则 `x = compute(1)` vs `compute(2)` 这种纯代码差异也会被答「测量、永不选边」并 exit 0,把唯一该人看的情况关掉。三条均已钉为变异验证的回归测试(cyc20260911-120717)。**第四类:读不了的布局即拒绝**(2026-09-11 实测)——`merge.conflictStyle = diff3` 会在 ours 与分隔符之间插入 `||||||| <base>` 段,而 `CONFLICT_BLOCK` **照样匹配**(把 base 段吞进 ours),于是判出来的两侧是(ours+base)vs theirs:一段 `disjoint` 被判成 `KEEP BOTH` 并 exit 0,而「都保留」恰好会把**谁都不想要的 base 副本**拼回去;本仓库的真实形态(Agent.md 计数行)更隐蔽——判成 `duplicate - ours is a strict superset - take OURS`,因为 base 段(带着**陈旧**计数的第三份拷贝)被算成 ours 多出来的行。故先于解析检查该标记并报 `unparsed-layout` + exit 1,与 `check-doc-count.py` 对同一布局的具名拒绝一致(那条路是 read-only 无损失,只会不必要地拦住一个本可解的 PR;这条路的错误建议会让解冲突的人**删掉自己的改动**,代价不对称)。**第五类:跨行聚合与「同一段落的两个修订」**(cyc20260911-190629 实测,两个缺陷都在**本工具自己**里,且都靠「拿全部真实冲突块做差分」而非手写 fixture 才暴露出来——手写 fixture 只会覆盖我本来就相信的形状):① 一条 `count-line` 原先是「两侧各一行」,但 51 次改 Agent.md 的提交里有 **3 次一次动掉 2 个以上计数**,git 会把它们并成**一个**块,该块因此落到内容行兜底、被判「两侧无共同行 ⇒ KEEP BOTH(拼接)」并 exit 0——拼接恰好把每个计数行**各留两份**,正是本仓库 `_duplicated_count_line_kinds` 守卫要拒的状态;现改为**对齐的、逐对只差数字的**块一律 `count-line`(任意长度,且每对都要满足带括号计数)。② 「两侧无共同行」**不等于**「各自新增」——同一段落的新旧两个修订永远不相等。当前 #1140 的 Agent.md 真实块里,ours 的两行段落正是 master 两行的**严格前缀**(890 vs 539、601 vs 471 字符),兜底判 KEEP BOTH 会同时留下陈旧与当前两份段落;现改为见到严格前缀关系即报 `overlapping` 交人读(前缀证据弱于符号子集,误差不对称:升级只是多读一次,「取 theirs」会静默丢一行)。两个修复各配变异验证(还原旧行为即测试变红)。**第六类:计数行与正文修订混在一个块里**(cyc20260911-194733 实测,发生在刚合入的第五类修复**自身**上):第五类要求「两侧等长且**每一对**都只差数字」,而真实块常把**计数行**和**正文行**配在一起——cb651a4 的真实块正是 ours「Python 计数(1393) + Doc count sync 正文」对 theirs「同一计数行的另一个值 + 同一正文的另一次改写」,两侧等长却**有一对不是计数对**,于是仍落到内容行兜底、判 KEEP BOTH 并 exit 0,而拼接结果里有两个 ``Python: `uv run pytest` `` 行(即文档声称两个 pytest 数),正是 `_duplicated_count_line_kinds` 要拒的状态;且计数数字**在行内**,前面的严格前缀判据(`b.startswith(a)`)对它**结构性不可见**——它只认「短的是长的前缀」,而这里数字之后的内容被改写,两侧互不为前缀。改判据为**按下标配对**:只看对应位置上的行对,任一对「两边都带括号计数且遮罩数字后相等」即报 `overlapping` 交人读(不再要求等长);证据强度就是遮罩本身——遮罩后相等意味着**同一个事实被重新测量过**,而同一事实出现两个值正是仓库守卫要拒的,故 KEEP BOTH 对它必错。实测收益(最近 400 次改 Agent.md 的提交,逐 hunk 差分):7 个 hunk 在兜底里带上计数行且**全部**给出**重复内容**的建议——e46c160/0c8a212(对齐,已由第五类改判 count-line)、cb651a4(2v2)/5c039b4(3v3)(等长但混了正文修订,第五类看不见)、3335877(1v2)/444e1d5(1v2)/18fd0af(1v13)(不等长);本条修复把余下 5 个全部转为 `overlapping`,全仓 150 次提交 1039 个 hunk 差分仅 6 处类别变化(5 个 Agent.md + 1 个 `preload-api.test.js`,后者也是真阳性:两侧写着 54/53 与 55/54 两个不同成员数)。变异验证:退回「只查前缀」即新增测试变红;对面(真实互不相干的两个新增)仍判 disjoint。**第七类:同一计数行的「重新拆分」**(cyc20260912-002444 实测,仍是拿真实冲突块差分出来的):第六类要在「遮罩数字后**完全相等**」,而真实块里同一个计数**种类**还会被**重新拆分**——47af6bc2 的真实合并块里 ours 是 `GUI: … (92: 45 daemon_client + … + 3 preload-api + 3 boot-contract)`,master 是**同一行**的 `(89: … + 3 preload-api)`(少一个组件、总数也从 92 变 89),两侧**行数 1 vs 2**且遮罩后不等,于是两条规则都看不见,仍判 `disjoint - KEEP BOTH` 并 exit 0,而拼接结果里有两个 `GUI: ` 行——正是 `_duplicated_count_line_kinds` 要拒的状态(本测试直接把拼接结果喂给该守卫,不是靠眼看)。判据改为**按「计数种类」而非「整行相等」**:两侧「首个计数之前的文本完全相同」即同一行被重新测量过(该文本正是命令与种类名),报 `overlapping` 交人读。实测范围:用**真实合并提交**(对每个 merge 的三个真实 blob 跑 legacy `git merge-tree`)重建出 **185** 个冲突块,该规则**只改 1 个类**(就是这一块,`disjoint`→`overlapping`),其余不动;反向对照(`Python:` 对 `GUI:` 这种不同种类)不升级。变异验证:删掉该子句,新增测试即变红(`disjoint`)。
Plan final tree suite: `uv run --no-sync python3 scripts/check-merge-plan-suite.py [--base <ref>] <PR>...` — 合并前问**只有把整份计划建出来才能回答**的那一句:这几张 PR **一起**落地之后,那棵树过不过仓库**自己的测试套件**?存在的理由(cyc20260913-154837 实测):7 张 PR 各自 `MERGEABLE`、各自 CI 双绿、**21/21 有序对全干净**、逐步骤的计数守卫也全 OK,而七张合起来的树**是红的**(全量套件里唯一的失败点就是这一条),失败点是 #1136 带来的守卫判 #1172 新增的三处 `subprocess.run(..., text=True)` 没有 pin `encoding=`。**per-PR CI 在结构上看不见这一层**:`pull_request` 建的是 `Merge <head> into <merge-base>`,于是 #1136 的那次运行里没有 #1172 的代码、#1172 的运行里没有那个守卫,两边都绿而落地的那棵树是红的。故本工具收**多张** PR、按给定顺序把它们**折**成终局树(`merge-tree --write-tree` + `commit-tree` 造合成 commit,不碰工作区也不动分支),再在**真 worktree** 里跑全量套件 —— **不是 `git archive` 解包**:实测解包树没有 `.git`,而本仓的测试要经 git 解析路径,于是那个装置对**任何**输入都同样失败(连 master 自己的树也报 8 failed ⇒ **装置在量自己**)。解释器用 `sys.executable`(新 worktree 里 `.venv` 是空的,本仓反复实测,`uv run` 在其中只会造个空环境)。exit 0 终局树建出来且套件通过 / 1 建出来但套件失败(这就是发现)/ 2 问不出来(git/gh 失败,或套件根本跑不起来 —— **绝不把「没查」报成健康**)/ 3 该计划**没有**终局树(某一步冲突,这不是健康裁决,那个问题归 `check-merge-sequence.py`)。输出**点名所测的树**与计划顺序:这个工具族的缺陷历来是「读错了树还说一致」。

## Releasing

Expand Down
Loading
Loading