Repository navigation
fix: merge-completeness gate no longer flags squash-merged branches whose lines were rewritten — 3.5.1 (#399) - #418
Conversation
…net change as landed (#399) The per-commit line-presence check flagged every branch commit whose added lines a later branch commit rewrote; a squash keeps only the final content, so those lines exist in no final tree. PR #408 drew 39 false orphans. Before the per-commit check, walk the first-parent baseline commits after the merge-base (oldest first, at most 200): if merging the branch head into some commit C is clean and leaves C's tree unchanged, every branch change is already in C and the gate exits 0. Checking every C, not only the tip, covers a later follow-up on the same file; the 3-way merge covers a squash of a branch that was never rebased. Anything undecided falls through to the existing check, which can only flag more. Fixtures 8-12: rewrite + squash, + a follow-up on the same file, a non-rebased squash, a genuine orphan next to the rewrite shape (still flagged), and the 200-commit cap. Refs #399
) Helper header Method step 0 and its hand-resolved-squash limit; idd-close Step 1.55 rationale and fixture list (the list still said 6 fixtures). Refs #399
plugin.json, marketplace.json, CHANGELOG, and both READMEs. Refs #399
Verify Report — PR #418(第 1 輪)Enginepai-ensemble 2.25.1 (canonical #207) — 4 IDD lenses + DA + Codex (gpt-6.1-sol, effort medium), model: opus AggregateFAIL — 3 blocking, 6 in-scope fix, 3 follow-up / documented(25 則原始 finding 去重後 13 項) Scope coveragePR refs: #399(PR body 另提及 #402、#408,兩者是 PR,不在驗證範圍) #399 — idd-close Step 1.55 flags squash-merged commits whose lines a later branch commit rewroteRequirements coverage: 原 issue 的 repro、#408 實例(39 → 0)、fixture 8–12 皆如宣稱(requirements 實跑確認;RED-first 屬實)。但包含檢查引入了兩個新的漏報方向,見 #1、#2。
Scope Check9 個檔案都在 #399 範圍內; 修正方向(第 2 輪)
Proposed Corrections
本輪在 |
…de-branch commits, or merge drivers (#399) Round-1 verify found that the containment step could report clean when nothing had landed. Fixes: - a squash that main reverted wholesale is not trusted: when every path the branch changed is back to its merge-base state at the tip, fall through - only baseline commits that descend from the merge-base are tried (--ancestry-path); a parallel first-parent commit measured against an older base made a never-merged branch look contained - repo merge drivers are neutralised (empty --attr-source, no core.attributesFile) so merge=ours cannot make a conflict look contained - the walk tries the oldest 200 commits instead of giving up past 200 - the per-commit check skips lines the branch head itself no longer has, so a genuine orphan is no longer listed next to rewritten-line noise - exit 3 prints what the containment step concluded Accepted and documented: a later commit that overwrites only part of a landed change is not reported (indistinguishable from a follow-up edit). Fixtures 9b and 11-16; containment fixtures expect the per-commit fallback on git without merge-tree --write-tree / --attr-source. Refs #399
Verify Report — PR #418(第 2 輪)Enginepai-ensemble 2.25.1 (canonical #207) — 4 IDD lenses + DA + Codex (gpt-6.1-sol, effort medium), model: opus AggregateFAIL — 4 blocking, 7 in-scope fix, 7 docs / test / follow-up(37 則原始 finding 去重後 18 項) Scope coveragePR refs: #399(PR body 另提及 #402、#408,兩者是 PR,不在驗證範圍) #399 — idd-close Step 1.55 flags squash-merged commits whose lines a later branch commit rewroteRequirements coverage:issue 的 repro rc=0、PR #408 實例 rc=0、第 1 輪 #2/#7/#9 確認已處理。但第 2 輪加的 safeguard 有四類被推翻,每一類都是「舊 helper 會報、新 helper 回 clean」。
Scope Check所有改動仍在 #399 範圍內。 停在這裡的原因
兩輪下來的共同根因是語意沒有定案:#408 的形狀(squash 後 main 又改了分支的行)要不報,就必須接受「落地過就算」;而每加一道 revert 偵測,都只蓋住一部分形狀(#1、#11),又帶來新的失效路徑。需要決定的是 gate 回答哪一個問題:
不論選哪一個,#4、#6、#7、#8、#12、#13 與文件項都要修。 Proposed Corrections
第 1 輪報告附的這兩份替換文字,本輪 DA 指出其中又含有不成立的宣稱,已改寫為只陳述實測結果。本輪在 |
…anded (#399) Verify round 2 showed that "contained somewhere, then exit 0" kept opening false negatives the old helper did not have, and that each revert detector covered only part of the shapes. The maintainer chose one question for the gate: did the branch's content land. - Find the newest first-parent baseline commit after the merge-base that holds the branch's net change (merge-tree in a scratch bare repo with no config, so no merge driver is defined or run and no objects are written), then check each candidate's added lines against it instead of exiting 0. A dropped commit that restores a merge-base value is reported again. - Skip a line only when the branch head's version of the touched files lacks it (cat-file), replacing the net-diff filter that rename detection, external diff drivers and color settings could empty. - Read patches and contents with plumbing and NUL-separated paths; read each touched path once (PR 408: 3 s). - Walk newest-first with an is-ancestor stop instead of --ancestry-path, and drop the revert check: reverts after landing are out of scope by design. - Fixtures 11-27 cover every round-2 shape; docs, CHANGELOG and manifests describe the landing semantics. Refs #399
Verify Report — PR #418(第 3 輪)Enginepai-ensemble 2.25.1 (canonical #207) — 4 IDD lenses + DA + Codex (gpt-6.1-sol, effort medium), model: opus AggregateFAIL — 0 HIGH;4 blocking(MEDIUM,其中兩項是本 PR 帶進來的回歸),10 in-scope fix,8 docs / follow-up(38 則原始 finding 去重後 22 項) Scope coveragePR refs: #399(PR body 另提及 #402、#408,兩者是 PR,不在驗證範圍) #399 — idd-close Step 1.55 flags squash-merged commits whose lines a later branch commit rewroteRequirements coverage:A 語意(「分支的內容有沒有落地」)的主幹成立。issue repro rc=0、PR #408 rc=0(約 3 秒,舊版約 6 秒);本 repo 99 個已合併 PR 以各自的 merge commit 當 baseline 全部 rc=0(舊版誤報 43 個);未合併的 #402 rc=3。第 2 輪的四類漏報都已修正並有 fixture。剩下的問題在落地 commit 的選法與幾條讀取路徑。
Scope Check所有改動仍在 #399 範圍內;#402 的修改是 maintainer 指示。 修正方向(第 4 輪)
Proposed Corrections
替換文字在第 4 輪定案後依最終實作改寫;本輪只列不做。 |
…mmit (#399) Verify round 3 (no HIGH, four blocking MEDIUM) showed the newest commit that still holds the branch's net change is not where it landed, that one genuine orphan sent every line back to the tip, that submodule bumps and binary-marked files hid lines, and that path dedup was quadratic. - Landing commit: the oldest commit of the first run (walking back from the tip) that holds the net change; only first-parent commits touching the branch's paths are tried, then the merge-base; 200 merge-tree calls in all. - Partial merges: when the whole branch never landed, its earlier commits are tried newest first; commits up to the one that landed are checked there. - Reading: one cat-file --batch-check per side, each distinct blob read once; gitlinks compare as "Subproject commit <oid>"; diff-tree --text and hunk-aware awk extraction; the scratch repo gets the shallow file and object format, and no attributes either. - Errors exit 4 with the reason, which idd-close prints; subjects are printed without signature checks or control bytes and marked as repository text. - Fixtures 27f-j, 28-40 (78 asserts), docs, CHANGELOG, manifests updated. Refs #399
Verify Report — PR #418(第 4 輪)Enginepai-ensemble 2.25.1 (canonical #207) — 4 IDD lenses + DA + Codex (gpt-6.1-sol, effort medium), model: opus AggregateFAIL — 0 HIGH;3 blocking(MEDIUM,其中兩項是較舊 git 或特殊路徑上的回歸),14 in-scope fix,6 docs / accepted(36 則原始 finding 去重後 23 項) Scope coveragePR refs: #399 · freshness gate: frozen #399 — idd-close Step 1.55 flags squash-merged commits whose lines a later branch commit rewroteRequirements coverage:issue repro、PR #408(2.6 秒,舊版約 5 秒)、78/78(bash 5 與 bash 3.2)、CI 皆通過;本 repo 99 個已合併 PR 中新版只標出 1 個(PR #36,手動解衝突的 squash,已列為限制),舊版標出的 48 個裡 47 個經 patch-id 獨立核對確實已落地。第 3 輪的 #2–#18 大多已到位,以下是剩下的問題。
Scope Check所有改動仍在 #399 範圍內。 修正方向(第 5 輪)
Proposed Corrections
替換文字在第 5 輪定案後依最終實作改寫;本輪只列不做。 |
…399) Verify round 4 (no HIGH, three blocking MEDIUM): on git 2.38-2.50 a gitlink was classified absent because cat-file --batch-check reported it as missing; a path ending in CR was looked up as its CR-less sibling; and the oldest commit of the newest holding run was not always where the branch landed. - Landing: walk the baseline's first-parent history forward from the merge-base (merge-base, first descendant, then path-touching descendants, descendants taken from rev-list --ancestry-path) and take the earliest commit that holds the net change. Fixes stacked branches squashed before their base was merged, and a hold broken and restored after landing. - Partial merges: try at most the newest 20 earlier commits. - Each side's tree comes from one ls-tree -r -z matched by exact path; a listed blob that cannot be read exits 4; CR or LF paths exit 4; diff-tree runs with --ignore-submodules=none. - awk read errors exit 4; subjects are quoted and stripped of C1 controls and bidi overrides; idd-close keeps only the helper's own reason and the ref on rc=4; core.fsmonitor is off for the helper's reads. - Fixtures 12 (reworked), 41-47b; docs, CHANGELOG, test header updated. Refs #399
Verify Report — PR #418(第 5 輪)Enginepai-ensemble 2.25.1 (canonical #207) — 4 IDD lenses + DA + Codex (gpt-6.1-sol, effort medium), model: opus AggregateFAIL — 1 HIGH(兩個 reviewer 各自回報)、5 MEDIUM blocking、12 in-scope fix、若干 docs/INFO(30 則原始 finding 去重後 22 項) Scope coveragePR refs: #399 · freshness gate: frozen #399 — idd-close Step 1.55 flags squash-merged commits whose lines a later branch commit rewroteRequirements coverage:issue repro、PR #408(約 2.6 秒,舊版約 5 秒,39 個誤報消失)、91/91(bash 5 與 bash 3.2)、舊 git 模擬、CI 皆通過;第 4 輪的程式面修正(ls-tree、CR 路徑、往前走的落地 commit、前綴、讀不到的 blob、
Scope Check所有改動仍在 #399 範圍內。 修正方向(第 6 輪)
Proposed Corrections
Diagnosis 的 Strategy 一併在第 6 輪結束後列入。本輪只列不做。 |
…ise merges (#399) Verify round 5 (one HIGH): in a UTF-8 locale macOS tr stopped at the first non-UTF-8 byte of a tree listing, so every later path looked absent from the branch head and a real orphan passed; the pipeline status was awk's. - export LC_ALL=C and set -o pipefail; run from the repository's top level (rev-list --stdin reads pathspecs relative to the current directory). - Refuse only LF in paths; CR stays inside awk's line and matches exactly, so a macOS Icon<CR> no longer disables the gate. - When git cherry finds a branch commit's patch in the baseline after the landing commit, the branch arrived piece by piece: check against the tip. - A prefix that undid itself is not a landed prefix; use_landing errors and awk/cherry parse failures exit 4; history lists reused per merge-base. - clean() replaces control, invisible and bidi characters with "?" and escapes quotes; --no-pager; --submodule=short; braced ${CLAUDE_PLUGIN_ROOT} in idd-close Step 1.55. - Fixtures 41 (now flagged), 48-54; docs, CHANGELOG, README row, test header. Refs #399
…ts own text (#399) Verify round 6 on PR #418 found no HIGH findings; this round handles its MEDIUM ones. - Deletions: when no commit of the target holds the branch's net change, every line the branch adds is there, and a file or non-blank line the net change deletes is still at the tip, the tip-side commits that delete something are listed. A squash whose conflicts were resolved by hand also holds no net change, but its deletions are made, so nothing is listed (fixtures 55, 55b, 56, 61; the merged-PR scan is unchanged). - The copy rule now applies only when the whole branch has no net change, so a squash equal to one branch commit no longer trips it (fixture 57). - Output: refs are printed in double quotes, the helper's own sentences are not escaped, and apostrophes in repository text are left as they are (fixtures 1, 62). - Reading: the tree listing tolerates a newline in a path elsewhere in the tree, and a failing filter in a pipeline now exits 4 (fixtures 58, 59). - Prefix search: the 20-commit limit is pinned on both sides (fixtures 60a, 60b). Suite 121/121 under bash 5 and bash 3.2; 105/105 with git 2.38; run-all-tests 63 suites, 0 failed; 99 merged PRs give the same result as round 6. Refs #399
Verify Report — PR #418(第 6 輪)Enginepai-ensemble 2.25.1 (canonical #207) — 4 IDD lenses + DA + Codex (gpt-6.1-sol, effort medium), model: opus AggregateFAIL — 0 HIGH;4 項 MEDIUM 被判 blocking,其中一項由 DA 反駁為決策 A 的設計結果;另有 16 項 LOW、6 項 INFO。原始 35 則 finding 去重後共 26 項。 Scope coveragePR refs: #399 · freshness gate: frozen #399 — idd-close Step 1.55 flags squash-merged commits whose lines a later branch commit rewroteRequirements coverage(requirements 與 regression 兩個 reviewer 各自實測):
Scope Check所有改動仍在 #399 範圍內。 第 7 輪的處理(
|
| # | Target | Section | 錯在哪 | Command |
|---|---|---|---|---|
| 1 | comment:6283603692 | ### Details | 建議「找到包含就 exit 0」 | /idd-edit comment:6283603692 --correct --section '### Details' --body-file '$HOME/.cache/idd/corrections/correction-399-1.md' |
| 2 | comment:6315089709 | ### Risks | 宣稱包含檢查不會比現在更容易漏報 | /idd-edit comment:6315089709 --correct --section '### Risks' --body-file '$HOME/.cache/idd/corrections/correction-399-2.md' |
| 3 | comment:6315687653 | ### Verification | 說拿掉 || continue 觀察不到 |
/idd-edit comment:6315687653 --correct --section '### Verification' --body-file '$HOME/.cache/idd/corrections/correction-399-3.md' |
| 4 | comment:6315089709 | ### Strategy | 「任一 C 包含就 exit 0」 | /idd-edit comment:6315089709 --correct --section '### Strategy' --body-file '$HOME/.cache/idd/corrections/correction-399-4.md' |
| 5 | comment:6315114750 | (整則) | Implementation Plan 描述的是舊設計 | /idd-edit comment:6315114750 --correct --body-file '$HOME/.cache/idd/corrections/correction-399-5.md' |
本輪只列出,沒有套用。
Verify round 7 on PR #418 found that the round-7 deletion rule, which looked for evidence in the branch's net change, did not run when the landing step fell back to the tip (git < 2.38, the 200-call budget, a merge-tree error), and that its count test listed hand-resolved squashes that had made every deletion (a moved import, a closing brace main also had). - Deletions are now checked like added lines, for every candidate at its own side: a deleted non-blank line the branch head no longer has must be gone from where the branch landed (the tip when it did not), and a deleted path the head no longer has must be gone too. Deleted lines the head still has in those files (moves, shared lines) are skipped. The net-change rule, its listing of every deleting commit and its note are removed (fixtures 63-68, 71-73, 75; 55, 55b, 56 now assert on every git). - A missing blob in a partial clone is not fetched into the repository: GIT_NO_LAZY_FETCH=1, exit 4 on git >= 2.44 (fixture 74). - has_lf and has_crlf report a read failure (fixture 69); the final sed pipeline is gone. - clean() replaces more invisible and format characters and escapes $ and backticks (fixtures 70, 70b); idd-close filters the default-branch name in its rc=3 header. - Docs: the blind-spot list is a closed enumeration (moved or duplicated lines, mode-only and blank-line deletions, files main renamed, merge commit content, any-form restore); stacked branches merged after their base are on the fall-back list; README's prefix bound and CHANGELOG's exit-4 wording are exact; test.sh lists every unpinned defense. Suite 141/141 under bash 5 and bash 3.2; 131/131 with git 2.38; run-all-tests 63 suites, 0 failed; PR #408 rc=0 in 3.4 s; 99 merged PRs give the same result as round 7 against their merge commits and against the current main. Refs #399
Verify Report — PR #418(第 7 輪)Enginepai-ensemble 2.25.1 (canonical #207) — 4 IDD lenses + DA + Codex (gpt-6.1-sol, effort medium), model: opus AggregateFAIL — 0 HIGH,6 項 MEDIUM:5 項來自第 7 輪新加的刪除規則,1 項是文件。另有 18 項 LOW、14 項 INFO。原始 38 則 finding,下表合併同一件事後共 26 項。 Scope coveragePR refs: #399 · freshness gate: frozen #399 — idd-close Step 1.55 flags squash-merged commits whose lines a later branch commit rewroteRequirements coverage:以下各項由 reviewer 各自實測:
Codex 盲審沒有找到必須修的缺陷。
另外,issue body 與 dashboard 仍是做決策前的狀態(requirements,INFO)。這兩處等 verify 通過後再同步。 Scope Check所有改動仍在 #399 範圍內。 第 8 輪的處理
第 8 輪的檢查(本機 commit
Proposed Corrections
本輪只列出,沒有套用。 |
Verify Report — PR #418(第 8 輪,本機 commit
|
| # | Severity | Finding | Source | Action |
|---|---|---|---|---|
| 1 | MEDIUM | **第 8 輪造成的回歸:在落地 commit 上檢查刪除時,main 在落地前自己加的同一行或同一個路徑,被當成「刪除沒落地」。**最常見的情況是平行發版:x 從 1.2.0 升到 1.3.0,同時另一個 PR 把 y 升到 1.2.0。也包括共用的 fi、return 0、main 自己建的同名檔。第 7 輪與舊 helper 在這些情況都回 0。輸出的說明句「the lines above are not in it」也不對 |
requirements+logic+regression+DA | Blocking |
| 2 | MEDIUM | 封閉的盲點清單又漏了幾類:只新增空檔、只加空白行、只改檔尾換行;main 改掉或搬走被刪的那一行 | logic+requirements+DA | Blocking |
| 3 | LOW | 前綴落地時,前綴內的 commit 仍對整個 branch head 判斷是否被取代,所以已落地的 commit 會被列出 | logic | In-scope |
| 4 | LOW | main 在落地前把分支改的檔案改名,落地處用原路徑找不到,仍會列出(第 8 輪以前就有);兩份清單都沒寫 | requirements+logic | Docs |
| 5 | LOW | GIT_NO_LAZY_FETCH 要 git 2.45 才支援,文件寫成 2.44 |
logic+security+DA | Docs |
| 6 | LOW | 第 8 輪新防護有 7 個 mutant 存活,第 7 輪報告寫「存活 2 個」,算錯了。刪除路徑清單的 ` | skip` 和 awk 的讀取檢查互為理由,兩個同時拿掉時會安靜地回 0 | |
| 7 | LOW | PR 本文、更正稿 1、2、第 7 輪報告「#1–#3 已處理」與程式不符 | requirements+DA | In-scope |
| 8 | INFO | DA 以 438 個真實已合併 PR 實測(本 repo、Akashic-Library、safari-browser、psychquant-claude-plugins):第 8 輪沒有比第 7 輪多報任何一處,#1 的形狀在實際歷史裡還沒出現過 | DA | — |
| 9 | INFO | Codex 沒找到必須修的缺陷;第 8 輪還沒在 Linux CI 上跑過 | codex+regression | — |
處理方式
第 7、8 輪的 blocking 都是前一輪修法造成的,所以不再逐輪修補(maintainer 2026-10-25 決定)。改成:
- 先把「每個 commit 什麼時候算落地」寫成判定表。
- 用有界窮舉把 helper 和判定表逐一比對,對不上的修好,或歸進由程式判定的封閉偏差清單。
- 最後跑一輪 verify。
#1 的修法已經在本機(第 9 輪,未推送):落地處的刪除改成同時對照落地前 main 的版本與 merge-base。另外新增了「新增路徑」的規則。fixture 76–80 在第 8 輪上 RED、在第 9 輪上通過。
#399) Rounds 9 and 10. SPEC.md now defines the right answer and the only allowed deviations; bounded.py runs the helper on every history of a small world and checks each answer against that definition. - Round 9: deletions at the landing commit are counted, paths a commit adds or deletes are checked, the tip compares presence only. - Round 10: a 6000-case sample of three-commit branches found 19 misses (exit 0 instead of 3) in round 9's count rule (p + h - m with t<h / t>h guards), which is wrong when the landing commit is the merge-base, when its first parent already holds the branch, and when only a prefix landed. The landing commit is now compared with the clean merge of the landed part onto main just before it (merge-tree in the scratch repository); when that merge conflicts, the tip is used and the note says why. - bounded.py: the tip rule is computed on the model (D1+D2), over-listing at the landing commit with the right verdict is D3, the listed blind spot is B1; --dump and --reclassify. Length <= 2: 0 unexplained of 7058; two length-3 samples of 6000: 0 unexplained. - Fixtures 76-88; mutation: 16 mutants of the new rules, all caught. - CHANGELOG, README, idd-close Step 1.55 and the helper header follow. - Round-8 LOWs: GIT_NO_LAZY_FETCH needs git 2.45 (git's own docs), not 2.44; blank-line-only and final-newline-only commits added to the blind spots (both confirmed: a never-landed one exits 0). Refs #399
Verify Report — PR #418(第 10 輪,本機 commit
|
| # | Severity | Finding | Source | Action |
|---|---|---|---|---|
| 1 | HIGH | **落地 commit 本身帶有分支以外的改動時,計數規則誤報,而且結論錯。**落地處比對的是「落地前的 main 加上已落地部分的乾淨 merge」,這只在落地 commit 剛好等於兩者時成立。release merge(PR 先 squash 進 develop,main 再 merge develop)、git pull 產生的 merge、stacked PR 的 base 又多了 commit、手改過的 squash,都會讓 }、return 0; 這類共用行的份數偏離期望值,把已落地的 commit 列出來。三種形狀都重現:舊 helper(origin/main)回 0,第 10 輪回 3。bounded.py 的 squash 永遠剛好等於「main 落地前 + S」,main 也沒有 merge,所以 0 個無法解釋對這一類沒有證明力 |
logic+DA | Blocking |
| 2 | MEDIUM | **B1 是相對 3.5.0 的回歸,不是原本就有的限制。**branch 沒有淨改動、多個 commit 被 squash 進 main、之後的 commit 又改回去:3.5.0 回 3 並列出沒落地的 commit,第 10 輪回 0(無輸出)。原因是整條 branch「落地」在 merge-base,不再找前綴;git cherry 對多 commit squash 不留 -,所以 CHERRY_DASH 的保護不會觸發。建議:落地在 merge-base 時照樣做前綴搜尋,只有沒有前綴落在更後面時才接受 merge-base |
requirements+regression+DA | Blocking |
| 3 | MEDIUM | **bounded.py 的 D1 取自 helper 自己的 trace,沒有從模型判定。**把整個落地步驟停用,bounded.py --max-len 2 --sample 2500 照樣回報 0 個無法解釋:落地步驟修好的 126 例退回去之後,全部被當成已知偏差吸收。SPEC 說「每一類都從模型判定」不成立,第 10 輪的「0 unexplained」也就不能證明落地步驟是對的;目前只有 fixtures 在證明它 |
DA | Blocking |
| 4 | LOW | CHANGELOG 說舊的 Step 1.55 呼叫「一定失敗」,與 #399 自己 2026-10-15 的觀察矛盾(那次 gate 回了 rc=3);未加大括號的 $CLAUDE_PLUGIN_ROOT 屬於 #401,但沒有連過去;同一個 idd-close/SKILL.md 還有 6 處(371、389、749、790、1224、1260);Implementation Complete 的「Sister Bugs Filed: (none surfaced)」因此不正確 |
requirements+regression+DA | In-scope |
| 5 | LOW | holds() 讀不到 tree 時被當成「不含」,不是錯誤,走訪會繼續、選到更後面的 commit |
logic | In-scope |
| 6 | LOW | classify() 沒檢查 helper 的結束碼是 0 或 3:exit 4 會被當成 tip 偏差吸收;minus 扣光預期清單後,預期結束碼沒有重算 |
logic | In-scope |
| 7 | LOW | 給 bounded.py 用的 trace hook 留在正式 helper 裡,會覆寫環境變數指定的任何路徑(跟隨 symlink),寫不進去時回 4,而 exit-4 清單沒列這一項 | security+regression | In-scope |
| 8 | LOW | 分支自帶 .gitattributes 的 * merge=union 目前被隔離擋住(已實測),但沒有 fixture 釘住 |
security | In-scope |
| 9 | LOW | 刪除檢查是新功能,卻寫在 3.5.1 的 ### Fixed;在 tip(git < 2.38 時是全部)會產生 3.5.0 沒有的誤報 |
regression | In-scope |
| 10 | LOW | idd-close Step 1.55 的說明從 959 位元組變成 7,979,成為限制清單與 fixture 索引的第四份手抄本,沒有 drift guard |
regression | In-scope |
| 11 | LOW | --after 那一列沒有測到任何東西:長度 1 的 530 例全部在找落地 commit 之前就 exit 0;預設模式的長度 1 也完全沒碰到落地那一側 |
DA | In-scope |
| 12 | LOW | Results 表與 CHANGELOG 沒交代結論錯的數量:seed 399 的樣本裡仍有 581 例該報 3 卻回 0(tip 565、B1 16),都歸進已知類別;「19 例漏報 → 0」讀起來像漏報清零 | DA | In-scope |
| 13 | LOW | test.sh 開頭「沒有 fixture 釘住的防線(only these)」不封閉:同時拿掉 --no-ext-diff --no-textconv --no-color、--no-pager、--no-renames 與 ISOGIT 的 -u GIT_WORK_TREE/GIT_INDEX_FILE/GIT_CONFIG,166 個 fixture 全過 |
DA | In-scope |
| 14 | INFO | plugin.json 的描述沒寫 #408 形狀需要 git 2.38+ |
requirements | In-scope |
| 15 | INFO | 決策 A 仍不在 #399 正文裡,Current Status 停在「等 A/B 決定」 | requirements | In-scope |
| 16 | INFO | 第 8–10 輪未推送,CI 沒跑過;PR 本文、3.5.1 日期都是舊的 | requirements+regression | — |
| 17 | INFO | clean() 的註解說輸出是「安全的 shell 字」,但 ! 原樣通過(互動 shell 會做 history expansion) |
security | In-scope |
| 18 | INFO | Codex 盲審沒找到必須修的缺陷;它只讀 diff、沒跑測試 | codex | — |
| 19 | INFO | requirements reviewer 獨立重跑 test.sh(166/0)與 bounded.py --max-len 1(412/384/28/0),數字都對得上;DA 重跑 SPEC 的各列數字也對得上 |
requirements+DA | — |
處理方式
第 10 輪的 blocking 有兩項(#1、#3)都在小世界之外,正好是判定表方法本身的盲區:小世界造不出「落地 commit 帶有別的改動」,而分類器又借用了 helper 自己的 trace。所以照第 8 輪定下的做法,先改規格與檢查器,再改 helper:
- 擴大小世界:main 可以在同一個 commit 裡落地 S 並帶自己的修改,也可以用 merge commit 落地(模擬 release merge 與
git pull)。 - D1 改由模型判定:commit 在 tip 檢查、但模型不符合 D1 任一前提時,算無法解釋;
classify()只接受結束碼 0 或 3(idd-all SLUG derivation: BSD/GNU sed alternation (\| not portable) #6)。 - 修 B1 回歸(idd-all: direct-commit mode 加 passive warning (dirty tree / on default branch) #2):落地在 merge-base 時仍做前綴搜尋。
- 修 idd-all: 增加 HITL(attended)模式 — direct-commit + 允許 sub-skill AskUserQuestion #1:一個候選做法是落地處只對「已落地部分沒有淨改動的文字」計數(分支內改回原值、加了又刪的那些),其餘文字已由三方合併的落地判定涵蓋,不受落地 commit 裡其他改動影響。先在擴大後的小世界驗證,不直接採用。
- LOW 與 INFO 一併處理;Results 表加「結論錯」一欄,CHANGELOG 改寫。
Proposed Corrections
| # | Target | Section | 錯在哪 | Command |
|---|---|---|---|---|
| 1 | comment:6315687653 | ### Sister Bugs Filed (mid-impl, v2.44.0+ #526) | 不是 none surfaced:Step 1.55 修的未加大括號 $CLAUDE_PLUGIN_ROOT 屬於 #401,同檔還有 6 處,應記錄並連到 #401 |
/idd-edit comment:6315687653 --correct --section '### Sister Bugs Filed (mid-impl, v2.44.0+ #526)' --body-file '$HOME/.cache/idd/corrections/correction-399-6.md' |
(#399 正文的 Current Status 由這次的 /idd-update 重建;決策 A 寫回正文另列在下一步。)
…mmit (#399) Round 10 verify found that a landing commit carrying changes of its own (a release merge, a git pull merge, a squash edited by hand) made the count rule list commits that landed, and that bounded.py could neither build that shape nor tell a broken landing step apart, since it took the sides from the helper's own trace. - At the landing commit a commit is reported when undoing it (merge-tree --merge-base, with the commit as the base) changes the branch head and changes nothing on the landing commit: its change is still in the head and not in place where the branch landed. Main's own edits sit elsewhere and can neither list nor hide it. Git 2.38 and 2.39 lack --merge-base and keep counting lines against the clean merge (SPEC.md D4, fixture 94). - Two intermediate rules did not hold up in bounded.py: exempting every text in the landed part's net change missed dropped commits whose text the branch also changed elsewhere (39 cases in two length-3 samples); confirming count listings in place still listed pairs of dropped commits that cancel out and missed dropped restores when main's own edit evened the count (131 cases in own/merge length-3 samples). - A branch with no net change lands at the merge-base; the prefix search now runs there too, so a dropped restore after a squash of the first commits is reported again, as in 3.5.0 (B1 regression). - holds() reports an unreadable tree as an error; the test-only trace never overwrites a file and never changes the answer. - bounded.py: --landing own|merge (main's own edit in the squash, a merge of a side branch carrying another PR); where each commit is checked, and the in-place answer at the landing commit, come from a model of the landing step; exit codes other than 0 and 3 are bugs; the expected exit code is recomputed after git cherry; a coverage line. - Fixtures 89-94, and a merge-completeness-count suite that runs every fixture under a shim refusing merge-tree --merge-base, so the git 2.38/2.39 count stays pinned on newer git; run-all-tests reads a per-suite timeout file (both merge-completeness suites ask for 300 s); SPEC rows 24-26, D3 and D4 redefined, B1, results with wrong verdicts; CHANGELOG (deletions marked as a new check, #401 wording), plugin.json and marketplace (git 2.38+, 2.40), README; idd-close Step 1.55 rationale shortened to pointers; the test.sh list of unpinned defenses is closed. Refs #399
Verify Report — PR #418(第 11 輪,本機 commit
|
| # | Severity | Finding | Source | Action |
|---|---|---|---|---|
| 1 | HIGH | 衝突被當成「已落地」,漏掉沒落地的還原 commit;3.5.0 與第 10 輪都抓得到。 undo_noop 把 merge-tree 衝突回成「撤銷會改變內容」,land_orphan_p 就判為已落地。git 的三方合併對相鄰行的改動也判衝突,所以只要 main(落地前,或落地 commit 本身)在被丟掉那一行的上下一行有改動,例如並行 PR 在設定檔加一個 key,沒落地的還原就被當成落地,exit 0。同一行被改成第三個值、或 rename 偵測把路徑配到相近的檔案,也一樣。重現:fixture 19 的形狀,加上另一個 PR 在 LOG 正下方插入 TIMEOUT = 5 → 第 11 輪 rc=0,第 10 輪與 3.5.0 rc=3。helper 開頭「main 自己的改動不會列出或藏住」、CHANGELOG 的同一句、SPEC 第 16、26 列都因此不成立。單純把衝突改成退回計數,會讓 fixture 90 的誤報回來,需要更細的判斷 |
logic+DA | Blocking |
| 2 | MEDIUM | 一個 commit 動了好幾處時,有一處落地就整個算落地。 撤銷 c 會改變落地 commit,只表示它至少一個效果在那裡,不表示全部都在。重現(Codex):c1 改 x、y;c2 把 x 改回、新增 z;squash 收了 y、z 但 x 還是 c1 的值 → 撤銷 c2 會刪掉 z,判為落地,x 的還原沒被列出 | codex | Blocking |
| 3 | MEDIUM | 原位檢查用不到 EXPECT,卻仍在 EXPECT 合併衝突時整批退回 tip。 手動解衝突、tree 與 head 完全相同的 squash,再加一個落地後的修改 → rc=3(正是 #408 的形狀)。把這個退回只留給計數路徑(git 2.38/2.39)就回 0 | requirements | Blocking |
| 4 | MEDIUM | 小世界造不出衝突,0 unexplained 對上面三項沒有證明力。 slot 之間有固定分隔行(永遠不相鄰)、每個 commit 只改一個 unit、main 不碰分支動過的 unit。SPEC 的「At the landing commit every answer agrees」、D3「只多列、結論正確」、CHANGELOG 的「neither hides nor flags」都建立在這個世界上,卻沒寫明。是第 10 輪 #3 的同一個模式 |
logic | Blocking |
| 5 | MEDIUM | B1 回歸只修了一半。 前綴搜尋靠 holds(),同樣怕相鄰行衝突:fixture 92 的歷史改成 release merge、另一個 PR 改了下一行 → 找不到前綴,落在 merge-base,rc=0(3.5.0 是 3) |
logic | Blocking |
| 6 | MEDIUM | main 落地前自己做了分支中間某個 commit 的同一個改動時,誤報被改回的 commit(0 → 3),而且照 idd-close 的補救 cherry-pick 會悄悄改回 main。 例:分支 c1 把 version 改 1.1、c2 改回 1.0;main 自己先 release 成 1.1 再 squash → 列出 c2。只看 tree 分不出「c2 落地了」與「c2 沒落地」,這是內在的歧義,卻不在封閉偏差清單裡(3.5.0 也會報;第 10 輪不會) | DA | Blocking |
| 7 | MEDIUM | git 2.38/2.39(Debian 12 是 2.39)沒有 --merge-base,退回第 10 輪的計數,第 10 輪 HIGH 原樣保留,fixture 94 與 count suite 把錯的答案釘成契約。建議改退回 tip(3.5.0 的 presence 對這些形狀回 0),或在 CHANGELOG 明寫這是相對 3.5.0 的回歸 |
logic | 待決定 |
| 8 | MEDIUM | 第 8 輪加的 GIT_NO_LAZY_FETCH=1 讓 blobless partial clone 的 git cherry 直接失敗,整個 gate 變成 rc=4(3.5.0 照樣抓得到 orphan);rc=4 的訊息也沒說原因是 partial clone。這是政策改變,#399 沒有紀錄決定 |
regression | 待決定 |
| 9 | MEDIUM | 第 8–11 輪都沒跑過 CI;第 11 輪改了共用的 test runner、新增一整套 count suite。高負載下(verify 同時在跑)count suite 跑了 324 秒,超過自己的 300 秒上限 | regression | In-scope |
| 10 | LOW | B1 的殘留仍是相對 3.5.0 的回歸(3 → 0),文件卻寫成固有盲點;「什麼都沒落地時結論 exit 3 正確」對淨改動為空的分支不成立(它落在 merge-base,不是 tip) | requirements | In-scope |
| 11 | LOW | 描述過時:idd-close SKILL.md:329 的 TaskCreate 仍寫 line-presence;test.sh 對 fixture 89–90 與 IN_PLACE_RC 的註解描述的是被淘汰的中間規則;bounded.py classify 的 docstring 還寫 D4 與「共用文字」;plugin.json/marketplace 寫「checks each commit's lines」;README 寫「都不會誤報或藏住」,與 2.38/2.39 的說明矛盾;PR 本文與 3.5.1 日期仍是第 7 輪 |
requirements+logic+regression | In-scope |
| 12 | LOW | CHANGELOG 沒提 D3(落地處仍可能多列),還寫「a commit undone later in the branch is not reported」 | requirements | In-scope |
| 13 | LOW | 第 10 輪 #8 仍缺 fixture:tree 裡 commit 的 * merge=union 被隔離擋住(已實測),但沒有測試釘住,也不在「沒有 fixture 釘住的防線」清單裡 |
requirements+security | In-scope |
| 14 | LOW | 刪除檢查是新功能、落地後 revert 不再回報是行為改變,都還放在 3.5.1 的 ### Fixed(第 10 輪 #9 只處理一半) |
requirements+regression | In-scope |
| 15 | LOW | 落地處現在看得到只改空白行、只改 file mode 的 commit(定義忽略這兩者),於是會列出 3.5.0 與第 10 輪不列的 commit;「Known blind spots(only these)」與 CHANGELOG「Still not seen」也因此不對 | logic | In-scope |
| 16 | LOW | 第 10 輪 Proposed Correction #1 還沒套用:Implementation Complete 的 ### Sister Bugs Filed 仍寫 none surfaced,與 CHANGELOG「其餘未加大括號的用法留給 #401」不一致 |
logic+requirements+regression | 待你執行 |
| 17 | LOW | clean() 只跳脫 ASCII 引號;全形 "、彎引號 “ ” 原樣通過,PR 標題可以在同一行偽造「引號已結束、後面是 helper 的判讀」 |
security | In-scope |
| 18 | LOW | 「最多 200 次 merge-tree」不成立:undo_noop、EXPECT、探測都沒扣預算,實際是 200 + 2N(N 由 PR 作者控制);文件的資源上限寫錯 |
security | In-scope |
| 19 | LOW | 一個候選動到含換行(或 \001)的路徑,整次檢查 rc=4,同分支其他正常 orphan 也不再列出(3.5.0 會列) |
regression | In-scope |
| 20 | LOW | 2026-10-24 回報的 Spectra archive 形狀(squash 後另一個 PR 把檔案搬進 archive/ 並修改)已修好,但沒有 fixture |
regression | In-scope |
| 21 | INFO | 第 10 輪的 #3、#5、#6、#7、#10–#15、#17 已解決(requirements、logic、regression 三個 lens 分別確認);#1 只在 git ≥ 2.40 解決,#2 部分解決(見上);兩個 suite 179/0、bounded.py --max-len 2 數字與 SPEC 一致;issue 的最小重現 rc=0 |
requirements+logic+regression | — |
| 22 | INFO | rc=3 的輸出格式變了(subject 加引號並跳脫、多一行 Landing check);只有 idd-close 消費,不影響別處 | regression | — |
| 23 | INFO | 未落地分支變慢(舊 2.4 s、新 5.8 s,高負載下),與 CHANGELOG 的計時說明一致 | regression | — |
| 24 | INFO | 未發現 prompt injection;安全面整體是淨改善(plumbing、隔離的 merge-tree、ref 先解析成 SHA) | security | — |
處理方式
第 11 輪的 blocking 有三項(#1、#2、#5)都來自同一件事:小世界造不出 git 的衝突,所以 merge-tree 的衝突判斷沒被測過。這和第 10 輪的教訓同一型,照第 8 輪定下的做法,先改檢查器再改 helper:
- 擴大小世界:相鄰的 slot(中間沒有分隔行)、一個 commit 動兩個 unit、main 在落地前或落地 commit 裡改分支動過的 unit(相鄰或同一行)。先用它重現 idd-all: 增加 HITL(attended)模式 — direct-commit + 允許 sub-skill AskUserQuestion #1、idd-all: direct-commit mode 加 passive warning (dirty tree / on default branch) #2、idd-all Phase 0.5: fork detection 改 fail-closed (gh repo view 失敗時預設 PR mode) #5、idd-all SLUG derivation: BSD/GNU sed alternation (\| not portable) #6。
- 換判斷方式:不再用「撤銷整個 commit 的三方合併有沒有衝突」,改成逐行判斷位置與身分。候選做法是用 head 與落地 commit 的雙向 diff 對齊位置,再用 blame 認出哪一行是誰寫的,這樣 main 自己的同文字副本與相鄰改動都不會混進來。先在擴大後的小世界驗證,不直接採用。
- idd-all SLUG derivation: BSD/GNU sed alternation (\| not portable) #6 是內在歧義(只看 tree 分不出),要寫進封閉偏差清單;idd-close 的「abort + land orphans」選項要提醒 cherry-pick 前先比對 main 目前的值。
- 需要 maintainer 決定的三件事:idd-all Phase 5: PR body heredoc 改 quoted (預防未來 RCE) #7(git 2.38/2.39 退回計數還是 tip)、idd-all Phase 0.2: GITHUB_REPO post-derive shape assertion #8(partial clone 要不要保留 lazy fetch)、[refactor] idd-list PR body parsing: ignore #N inside fenced code blocks (sister concern from #13) #14(3.5.1 還是 3.6.0)。
- LOW 一併處理;push 之後跑 CI。
Proposed Corrections
| # | Target | Section | 錯在哪 | Command |
|---|---|---|---|---|
| 1 | comment:6315687653 | ### Sister Bugs Filed (mid-impl, v2.44.0+ #526) | 不是 none surfaced:Step 1.55 修的未加大括號 $CLAUDE_PLUGIN_ROOT 屬於 #401,同檔還有其他處,應記錄並連到 #401 |
/idd-edit comment:6315687653 --correct --section '### Sister Bugs Filed (mid-impl, v2.44.0+ #526)' --body-file '$HOME/.cache/idd/corrections/correction-399-6.md' |
(與第 10 輪報告的同一項,尚未執行。#399 正文的 Current Status 由這次的 /idd-update 重建。)
Verify Report — PR #418(第 14 輪,本機 commit
|
| 重現 | 第 14 輪 | 第 13 輪 | 3.5.0 | 正確答案 |
|---|---|---|---|---|
| 修正之後分支搬了檔案,main 只拿到搬移(logic exp6、Codex 的歷史) | 0 | 3 | 3 | 3 |
| 手動解衝突的 squash 保留搬移、丟了那一行(logic exp5) | 0 | 3 | 3 | 3 |
| 複製函式兩次,main 改原函式,乾淨 squash(DA) | 3 | 0 | 0 | 0 |
| 唯一的一行夾在重複區塊之間,乾淨 squash(DA) | 3 | 3 | 0 | 0 |
| 兩個 commit 都碰同一段重複文字,乾淨 squash 加 follow-up(requirements) | 3 | 3 | 3 | 0 |
#399
| # | Severity | Finding | Source | Action |
|---|---|---|---|---|
| 1 | HIGH | 假的 clean(退步):分支後來把檔案搬走,前面 commit 的修正沒落地也看不見。 c1 在 y.txt 加 X = fix,c2 把 y.txt 搬到 lib/y.txt,main 只拿到 c2(或手動解衝突時保留搬移、丟了這一行)。git diff feat main 有 -X = fix,第 14 輪回 0,第 13 輪與 3.5.0 都是 3。原因:tip 檢查用 c1 自己的檔名讀 head 與 tip,兩邊都沒有 y.txt;第 14 輪的 -M 又讓 c2 的 patch 只剩它自己的改動,沒有 commit 帶著 X。P0 也一樣用 c1 的舊檔名比對(Codex:A 加一行、B 純 rename、main 只 rename,P0 直接回 0) |
logic+codex | Blocking |
| 2 | HIGH | 退步:複製函式再多複製一次,乾淨 squash(沒有 follow-up)就被列出。 fixture 124 加一個 c3 再複製一份。main 自己的 .strip() 讓 L^1 的副本數和 merge-base 不同,other_copies 看到 c3 也改了副本數就保留 finding,分不出差額是 main 造成的。DA 的 fuzz:82 個乾淨 squash,第 14 輪列出 51 個、第 13 輪 30 個、3.5.0 0 個 |
DA | Blocking |
| 3 | HIGH | 唯一的一行因為旁邊的重複區塊被配錯位置,乾淨 squash 被列出。 c2 把 load 改名 load_text(唯一的一行),c3 在中間插入一份同樣函式體的 load_lines;diff(c2→squash) 把 def load_text 配到 def load_lines。confirm() 對唯一文字一律相信位置,finding 直接成立。第 13、14 輪都是 3,3.5.0 是 0;兩份偏差清單都沒有這一類。這正是 #399 的症狀,而且在複製函式這種常見形狀上比 3.5.0 差 |
DA | Blocking |
| 4 | HIGH | 兩個 commit 都碰同一段重複文字時,乾淨的 squash 找不到落地 commit,follow-up 讓 commit 被列出。 複本規則只接受「整個分支只有一個 commit 碰這段文字」,c1 刪一行 pass、c2 再加一行 pass 就被拒,退回 tip。fuzzdel3(fuzzdel2 再加一個 commit 複製被刪的文字):64 個乾淨 squash 錯 7 個,第 13 輪相同,3.5.0 全錯。SPEC 第 56 列、D1 的封閉清單、CHANGELOG 與 header 的 fall-back 清單、manifest 的描述都沒有這個限制 |
requirements | Blocking |
| 5 | MEDIUM | part_landed 是全域的:分支任何較早的部分到過 L^1(cherry-pick 一個無關的 hotfix,或分兩個 PR 落地),R1 對所有 finding 都停用。fixture 124 加一個早先被 cherry-pick 到 main 的 hot.txt commit:第 14 輪 rc=3,第 13 輪與 3.5.0 是 0(regression);fixture 142 的形狀先落地前兩個 commit:第 13、14 輪都是 3(logic)。SPEC 還寫「計數只會拿掉 finding,不會加」 |
logic+regression | Blocking |
| 6 | MEDIUM | part_landed 只看最新 20 個較早的 prefix。分支的第一個 PR 是多個 commit 的 squash、而且早於這 20 個時,偵測不到,R1 清掉一個從沒落地的 commit(rc=0)。不是退步(第 13 輪與 3.5.0 也是 0),但這個上限在 SPEC、CHANGELOG、header 都沒寫 |
requirements | Blocking |
| 7 | MEDIUM | 分多步落地時複本規則不適用:Rebase and merge(一串 cherry-pick)與 release merge(先 squash 進 develop,再 --no-ff merge)。複本規則只數 L 自己那一步,又一律拒絕 merge commit,有 follow-up 就誤報(rebase fuzz 85 例錯 5、release fuzz 69 例錯 8,都與第 13 輪相同)。SPEC 與 CHANGELOG 寫的涵蓋範圍比實際大 |
DA | Blocking |
| 8 | MEDIUM | 整棵樹模式(路徑超過 64 KB)會漏掉 update-index 拒收的路徑(.git 變形、NTFS/HFS 形式等),訊息被丟棄、exit 0,結果是假的 clean:同一個歷史在 pathspec 模式是 3,整棵樹模式是 0 |
security | Blocking |
| 9 | MEDIUM | 整棵樹模式會寫進 repo 的 .git:core.splitIndex=true 時 update-index 把 shared index 寫進 $GIT_DIR(實測從 1 個變 5 個 sharedindex.*),write-tree 也會更新既有 object 的 mtime。header 寫「the repository is never written」不成立。DA 另行重現,確認不是誤報 |
security+DA | Blocking |
| 10 | MEDIUM | awk -v 會解讀路徑裡的反斜線(a\nb.txt 變成含換行),old_of 與 holds_by_copies 的三個 awk 比對不到,複本規則的限制形同沒有。實測只有誤報與錯的落地 commit,沒有假的 clean;也違反本檔「路徑經檔案進 awk,不經 argv」的規則 |
security | Blocking |
| 11 | MEDIUM | 效能退步:single_changer 在拒絕唯一的失敗行之前就跑,而且每個 prefix 的淨改動都重走一次整個分支。從沒落地、檔案有重複的 ### Fixed 標題時:150 個 commit 從 17 s 變 245 s CPU,40 個 commit 從 7.4 s 變 70 s;idd-close 的 Bash 呼叫預設 120 s 逾時。答案不變 |
regression | Blocking |
| 12 | MEDIUM | 效能退步:路徑超過 64 KB 時,每個落地檢查列四次整棵樹並建 index;10 萬個檔案的 repo、3000 個檔案的分支、60 個 main commit:33 s 變 202 s CPU。答案不變 | regression | Blocking |
| 13 | MEDIUM | bounded.py 對 tip 路由是循環驗證:D1+D2 類別不檢查送到 tip 的原因是否在 D1 列舉的五種裡,路由模型照抄 helper 的落地規則。加上小世界造不出重複區塊、Results 的 fuzz 只跑直接 squash,#2、#3、#7 都在證據之外 | DA | In-scope |
| 14 | LOW | 複本規則會接受沒帶分支改動的 commit:淨改動只是搬一行重複文字時要求的副本變化是 0,任何不碰這段文字的 commit 都符合;「其他行在原位」也會被較早落地的行滿足。實測只造成錯的落地訊息,答案沒錯 | logic | In-scope |
| 15 | LOW | part_landed 用的是落地搜尋共用的 200 次預算,用完時 R1 默默停用,finding 成立、輸出沒說明(需要約 190 個 main commit 碰同一檔) |
logic+regression | In-scope |
| 16 | LOW | 整棵樹模式的另外三處:repo 路徑含 : 時 alternates 被切錯;partial clone 下 write-tree 需要每個列出的 blob(git ≥ 2.45 失敗、較舊的會 fetch);TMPDIR 是相對路徑時 shell 與 git 解析的位置不同。三者都是往 tip 退,不會假的 clean |
security | In-scope |
| 17 | LOW | 文件:SPEC Results 前後對「跑的是哪個 helper」說法不一;CHANGELOG 與 SKILL.md 說 P0 比對「分支 commit 碰過的路徑」,實際是 candidate 的路徑;run-all-tests 寫 suite 要兩三分鐘,負載高時實測 4–10 分鐘,貼近 600 s 上限;3.5.1 的日期還是 10/21 | requirements+regression | In-scope |
| 18 | LOW | #399 留言的 ### Sister Bugs Filed 仍寫 none surfaced(第 13 輪 #11,草稿 6 已含 printf 那一條) |
requirements+DA | 推送後 |
| 19 | INFO | 第 8–14 輪都還沒推送,CI 沒跑過。reviewer 在 Docker(Ubuntu 24.04、git 2.43、mawk)跑 suite 289/0(fixture 74 在 2.45 以下跳過);Debian、git 2.30.2 跑 288/0,2.30 的下限成立 | requirements+regression | 推送後 |
| 20 | INFO | 確認過的:本 repo 99 個已合併 PR(含 #408)、Akashic-Library 235 個已合併 PR,第 13、14 輪答案完全相同,都是 0;issue 的最小重現 rc=0;PR #408 約 8–10 s;輸出清理、pparse 解碼、trace 檔、命令列上的 ref 與路徑都沒有注入或執行外部程式的途徑;輸入裡沒有 prompt injection | requirements+security+regression+DA | — |
處理方式
第 15 輪的順序:
- rename 跟到 head(idd-all: 增加 HITL(attended)模式 — direct-commit + 允許 sub-skill AskUserQuestion #1):P0 與 tip 檢查讀一個 commit 的檔案時,先沿著分支的 rename 找到它在 head 的名字,再用那個名字讀 head 與 tip。exp5、exp6、Codex 的歷史都做成 fixture,要求 exit 3。
- L^1 的副本逐段文字判斷(idd-all: direct-commit mode 加 passive warning (dirty tree / on default branch) #2、idd-all Phase 0.5: fork detection 改 fail-closed (gh repo view 失敗時預設 PR mode) #5、idd-all SLUG derivation: BSD/GNU sed alternation (\| not portable) #6、[enhancement] idd-list cluster_leader config (lowest | primary) (sister concern from #13) #15):拿掉全域的
part_landed與other_copies。改成只扣掉「確定已在 L^1、而且改過這段文字副本數」的分支部分(成立的 prefix、git cherry配到的 commit);跟這段文字無關的部分不影響 R1。搜尋沒做完(20 個 prefix 的上限、預算用完)時,只有另有分支 commit 改過這段文字的副本數才保留 finding,並在 exit 3 的輸出說明。這一條有自己的預算。 - 唯一的行也用計數確認(idd-all SKILL.md: pseudocode quality (goto bash / branch-exists no-op / stale Failure Modes table) #3):位置找到的 finding,不論文字是否重複都要副本數同意才成立;commit 自己搬移的那一行(同一段文字它既刪又加)仍以位置為準。先用 DA 的 fuzz 與 bounded 確認不會多出假的 clean;會的話改成在偏差清單列出這一類。
- 複本規則(idd-all Phase 4: ${MAX_ROUND} substitution missing in abort message #4、idd-all Phase 5: PR body heredoc 改 quoted (預防未來 RCE) #7、[refactor] idd-list PR body parsing: ignore #N inside fenced code blocks (sister concern from #13) #14):要求的副本變化是 0 的文字不用複本規則;「其他行在原位」只算 L 自己那一步帶進來的行。idd-all Phase 4: ${MAX_ROUND} substitution missing in abort message #4 與 idd-all Phase 5: PR body heredoc 改 quoted (預防未來 RCE) #7 先試能不能安全放寬;不行就列進 D1,並改正 SPEC 第 56 列、CHANGELOG、header 與 manifest 的說法。
- 效能([SOP] umbrella split flow should mandate commit log scan before filing sub-issues #11、Complexity routing: vagueness 應該是一階信號,但目前 4 層 gate 都沒衡量需求清晰度 #12):唯一的失敗行先拒絕,再跑
single_changer,每個 commit 碰過的文字快取起來;整棵樹模式每個 revision 的 tree 只建一次。 - 整棵樹模式(idd-all Phase 0.2: GITHUB_REPO post-derive shape assertion #8、idd-issue Step 2: 強制 sanitize incoming title (length cap / strip control chars) #9、idd-all Examples: 跨 repo invocation 段重複描述,合併 #10、[refactor] idd-list Step 5: needs-fix × PR state matrix 應展開,不該摺疊成單行 (verify follow-up from #13) #16):
core.splitIndex=false並清掉GIT_TEST_SPLIT_INDEX;寫入 index 的筆數和輸入不符就 fail closed;awk 一律經ENVIRON拿路徑;alternates 用 C-quoted;write-tree --missing-ok;SCRATCH先轉成絕對路徑;header 改成「不寫入內容,可能更新既有 object 的時間戳」。 - bounded.py(idd-list 應該顯示引用 issue 的 open PR 資訊 #13):D1+D2 類別要求模型找得到 D1 列舉的原因,否則算 unexplained;DA 的 fuzzcopies、fuzzrebase、fuzzrelease 與 fuzzdel3 加進 Results。
- [enhancement] idd-list Step 5: planning + has PR 應勸 user 處理 stale/zombie PR (verify follow-up from #13) #17 一併處理。需要 maintainer 決定:版本號(3.5.1 或 3.6.0)、partial clone 是否保留 exit 4。[test] idd-list cluster algorithm + footer rule + state matrix 沒被任何 production data 跑過 (verify follow-up from #13) #18 的更正與 CI([refactor] idd-list regex #(\d+) 應加 \d{1,7} 上限 + 過濾 #0 (verify follow-up from #13, related #14) #19)在推送後處理。
Proposed Corrections
| # | Target | Section | 錯在哪 | Command |
|---|---|---|---|---|
| 1 | comment:6315687653 | ### Sister Bugs Filed (mid-impl, v2.44.0+ #526) | 不是 none surfaced:Step 1.55 修的未加大括號 $CLAUDE_PLUGIN_ROOT 屬於 #401(同檔還有 6 處),echo "$OUT" 改成 printf 也是本範圍修掉的 idd-close 缺陷 |
/idd-edit comment:6315687653 --correct --section '### Sister Bugs Filed (mid-impl, v2.44.0+ #526)' --body-file '$HOME/.cache/idd/corrections/correction-399-6.md' |
(第 10–13 輪報告列過的同一項,尚未執行。#399 正文的 Current Status 由這次的 /idd-update 重建。)
…ies per text (#399) Round 14 verify found four HIGH problems: with renames detected at the tip, a commit that later moved a file no longer carried an earlier commit's lines, and the earlier commit was read under a name neither the head nor the tip has, so a fix that never landed read as landed; the check of other commits' copies listed a clean squash of a function copied twice; a unique line next to repeated function bodies was paired with another function's and listed; and a repeated text two commits touch has no landing commit. - The tip reads each candidate's files under their names in the head too (followed by a rename-detecting diff from the commit to the head). - Every finding at the landing commit needs the copies to agree, unique lines included; a unique line must also be in the landing commit as often as in what landed before main's own copies are counted, and one the commit moved is judged by position alone. - L^1's copies are main's unless another commit that changes them reached L^1 (its own change all in place there). This replaces round 14's global check of earlier parts and its check of other commits' copies; subtracting the copies of the newest prefix L^1 holds was tried and dropped (a replaced deletion makes L^1 hold a prefix whose state it does not have). - The copy rule needs a count to compare (no move) and a line of the net change its own step brought. - Whole-tree mode: every listed path must reach the scratch index (fail closed), no split index, the object directory C-quoted in the alternates, write-tree --missing-ok, trees cached per tree and list, SCRATCH absolute. - awk gets repository paths through the environment, never -v. - single_changer reads each commit's texts once per file; a unique failing line is refused before it runs. - bounded.py: the model follows the new rules; its tip class needs a cause SPEC's D1 names, read from how the case was built (three causes the class had taken for granted are now named in D1). - Fixtures 158-173 (each red on round 14 or on its mutant). Suite 322/322 with bash 5, /bin/bash 3.2 and the path threshold at 0; bounded.py: 18 modes, 0 unexplained (plain length 2 identical to rounds 13 and 14); fuzz: 0 false listings in 82 copied-function and 258 deleted-line clean squashes (round 14: 51 and 0); mutants: survivors in the test.sh header with the reason. - SPEC (D1, R1, rows 56-73, results, history), CHANGELOG (timing), manifests, idd-close's P0 wording, run-all-tests timing; the suite's timeout is 1200 s. Refs #399
Verify Report — PR #418(第 15 輪,本機 commit
|
| 重現 | 第 15 輪 | 第 14 輪 | 3.5.0 | 正確答案 |
|---|---|---|---|---|
| fixture 153 加上分支在 b2 之後搬檔(logic rep153r) | 0 | 3 | 0 | 3 |
| fixture 146 改成插入行、乾淨 squash 加 follow-up(logic rep146j) | 3 | 0 | 3 | 0 |
| fixture 153 加上分支最後搬檔(logic rep153m) | 0 | 0 | 3 | 3 |
| 改一行、改回來、搬檔,squash 丟掉改回來那個(Codex) | 未重跑(與上一列同類) | — | — | 3 |
#399
| # | Severity | Finding | Source | Action |
|---|---|---|---|---|
| 1 | HIGH | 假的 clean:落地處沒有沿著分支後來的 rename 讀檔。 land_orphan_p 的五個 diff 都只限 candidate 自己的路徑;分支後來把檔案搬走時,c→head 把整個檔案讀成刪除,c 的每一行都像被後來的 commit 取代,所以從沒落地的 commit 不會被列出。第 15 輪只在 tip 加了 head 的名字。重現:fixture 153 最後加一個搬檔 commit,第 14、15 輪都回 0,3.5.0 回 3(列出 b4)。Codex 給的歷史(改一行、改回來、純 rename,squash 丟掉改回來那個)是同一個機制 |
logic+codex | Blocking |
| 2 | HIGH | 假的 clean(退步):in_l1 用兩個不同的檔名 join。 b 自己的紀錄掛在 b 的檔名,b→L^1、b^→L^1 兩個 diff 的紀錄掛在 L^1 的檔名;分支在 b 之後搬檔、而且這次搬檔和 b 一起進了 L^1 時,b 的刪除永遠找不到,讀成「沒到過 L^1」,L^1 的副本被當成 main 的,從沒落地的 restore 被放行。fixture 153 在 b2 之後加搬檔:第 15 輪 0,第 14 輪 3 |
logic | Blocking |
| 3 | HIGH | 誤報(退步,#399 的症狀):「這一步帶進來的行」檢查在分支搬過檔時一律不成立。 比對 L^1 的兩個 diff 掛在 L^1 的舊檔名,淨改動掛在 head 的新檔名;搬檔那一步的新增行永遠算不上「帶進來」,乾淨的 squash 被拒,退回 tip,follow-up 讓 commit 被列出。fixture 146 把 pad 5 的取代改成插入:第 15 輪 3,第 14 輪 0。fixture 146 本身會過,是因為它的刪除紀錄靠同一個錯配往另一個方向通過 |
logic | Blocking |
| 4 | HIGH | DA 未完成(API 連線錯誤),對抗檢查沒有發生 | — | 第 16 輪重跑 |
| 5 | MEDIUM | bounded 的 D1 原因檢查對 own、merge、two 三種模式一律放行,所以「tip 類別需要 D1 列出的原因」在這三種模式裡仍是循環驗證。把那一條拿掉後,--touch --landing own 有 38 例、--touch --landing merge 有 24 例 unexplained,其中一例是 header 列了、SPEC D1 沒列的「git pull merge 的另一邊在 squash 之後改了分支的行」 |
requirements | Blocking |
| 6 | MEDIUM | 整棵樹模式會執行 repo 的 post-index-change hook:update-index、write-tree 寫 scratch index 時照樣觸發,core.hooksPath 指向工作樹時,跑什麼由 repo 內容決定;hook 卡住也會卡住 idd-close。第 14 輪就有。修法已驗證:-c core.hooksPath=/dev/null |
security | Blocking |
| 7 | LOW | SPEC D1 是封閉清單,卻少了 header 與 CHANGELOG 列出的四個 tip 退路:超過 200 次檢查、prefix 之後超過 20 個沒落地的 commit、git pull merge、stacked branch |
requirements | In-scope |
| 8 | LOW | 文件:plugin README 的 v3.5.1 列日期仍是 10/21、follow-up 的說法沒加限定、副本數仍寫「只有重複文字」;SPEC 的第 15 輪歷史仍描述已拿掉的 prefix 扣除,第 68 列仍寫「20 個 commit 的搜尋」,第 56b 列應寫「找不到整個分支的落地 commit」;idd-close 的 TaskCreate 說只有「什麼都沒落地」才用 tip | requirements+regression | In-scope |
| 9 | LOW | 第 56b 列(兩個 commit 碰同一段重複文字、Rebase and merge)只有 fuzz 佐證,沒有釘住路由的 fixture;而這兩種是 #399 第二種形狀,仍會列出 commit,要先由 maintainer 確認接受,再寫進 Key Decisions | requirements | 待決定 |
| 10 | LOW | 淨改動是純 rename(相似度 100%)時,pparse 讀不到 ---/+++,沒有 N/O 紀錄,confirm() 找不到 merge-base 的檔名,main 自己加的副本不會被扣掉 |
logic | In-scope |
| 11 | LOW | head_names 與 tip 的 patch 都用 -l1000 並丟掉 stderr;一次搬動並修改超過約 1000 個檔案時,rename 偵測被跳過,第 15 輪的對應安靜地失效 |
logic | In-scope |
| 12 | LOW | TMPDIR 是相對路徑且有 export CDPATH 時,新的 cd 把兩行輸出當成 SCRATCH,每次都 exit 4、暫存目錄留下來;TMPDIR 含反斜線時,scratch 路徑經 awk -v 被解讀錯,也每次 exit 4。都是 fail closed |
security | In-scope |
| 13 | LOW | other_copies 對每個 finding、每個分支 commit 重跑 in_l1,沒有快取:150 個 commit、8 行的複製函式 squash 要 136 s CPU(第 13 輪 25 s,第 14 輪更慢而且答錯) |
regression | In-scope |
| 14 | LOW | 在本 repo 自己的部分 merge 上,fc4842e(已落地)仍被列出:commit 新建的檔案裡,唯一的行被兩個 diff 配錯,而 confirm() 對新建的檔案一律保留 finding、不計數。verdict 仍是對的 |
regression | In-scope |
| 15 | INFO | 帶到下一輪的事:#399 的 Sister Bugs 更正、CI、版本號、partial clone | requirements | 推送後/待決定 |
| 16 | INFO | 確認過的:#399 的最小重現、PR #408(約 8.5 s CPU)、第 14 輪 HIGH 的重現;本 repo 33 個 PR 與 Akashic-Library 235 個 PR 的答案與第 14 輪逐字相同;Linux Docker(git 2.30.2、mawk)319/0;clean()、pparse 解碼、ENVIRON 路徑、single_changer 快取、want=0 的拒絕都沒有問題;輸入裡沒有 prompt injection |
全部 | — |
| 17 | INFO | 第 14 → 15 輪唯一不是修正的答案變化:logic exp3 從 3 變 0,屬於 SPEC 第 69 列記載的 tip 限制(3.5.0 也是 0) | regression | — |
處理方式
第 16 輪的順序:
- 一個檔名空間(idd-all: 增加 HITL(attended)模式 — direct-commit + 允許 sub-skill AskUserQuestion #1、idd-all: direct-commit mode 加 passive warning (dirty tree / on default branch) #2、idd-all SKILL.md: pseudocode quality (goto bash / branch-exists no-op / stale Failure Modes table) #3、idd-all Examples: 跨 repo invocation 段重複描述,合併 #10):pparse 改成依行號所屬的那一側取檔名:A 紀錄用新那一側的檔名,C、D 紀錄用舊那一側的檔名;
rename from/rename to也產生 N/O 紀錄。讀檔的地方改用紀錄所在那一側的名字。落地處與in_l1的 diff 把 head 的檔名一起放進路徑清單,讓 rename 配得起來。三個重現與 Codex 的歷史都做成 fixture。 - bounded 的 D1 原因(idd-all Phase 0.5: fork detection 改 fail-closed (gh repo view 失敗時預設 PR mode) #5、idd-all Phase 5: PR body heredoc 改 quoted (預防未來 RCE) #7):
own/merge只在 main 自己的修改碰到分支改過的單元時算原因,two只在有重複文字時算;跑出來的 unexplained 逐一處理;SPEC D1 補上四個 tip 退路。 - hook(idd-all SLUG derivation: BSD/GNU sed alternation (\| not portable) #6):GIT 一律加
-c core.hooksPath=/dev/null,加 fixture。 - 其餘 LOW(idd-all Phase 0.2: GITHUB_REPO post-derive shape assertion #8、[SOP] umbrella split flow should mandate commit log scan before filing sub-issues #11–[refactor] idd-list PR body parsing: ignore #N inside fenced code blocks (sister concern from #13) #14):文件修正;rename 偵測被跳過時當成錯誤;
CDPATH=、SCRATCH 含反斜線時直接跳過並說明;in_l1的結果快取;新建檔案的 A 紀錄也計數。 - 需要 maintainer 決定:idd-issue Step 2: 強制 sanitize incoming title (length cap / strip control chars) #9(兩個 commit 碰同一段重複文字、Rebase and merge 這兩種仍退回 tip,是否接受)、版本號、partial clone。
Proposed Corrections
| # | Target | Section | 錯在哪 | Command |
|---|---|---|---|---|
| 1 | comment:6315687653 | ### Sister Bugs Filed (mid-impl, v2.44.0+ #526) | 不是 none surfaced:Step 1.55 修的未加大括號 $CLAUDE_PLUGIN_ROOT 屬於 #401(同檔還有 6 處),echo "$OUT" 改成 printf 也是本範圍修掉的 idd-close 缺陷 |
/idd-edit comment:6315687653 --correct --section '### Sister Bugs Filed (mid-impl, v2.44.0+ #526)' --body-file '$HOME/.cache/idd/corrections/correction-399-6.md' |
(第 10–14 輪報告列過的同一項,尚未執行。#399 正文的 Current Status 由這次的 /idd-update 重建。)
…name (#399) Round 15 verify found three HIGH problems of one kind: every diff record carried the new side's name of its file, also for the old side's lines, so once the branch had moved a file, records of two diffs about the same line never met. The check at the landing commit read a candidate under its own names and missed a commit whose file a later commit moved; in_l1 missed a commit that reached L^1 before the move, clearing a restore that never landed; and the copy rule never found a line the squash's own step brought in a moved file, sending a clean squash to the tip. - pparse: A records carry the new side's name, C and D records the old side's; a move with no edit gets its N/O records from "rename from"/ "rename to"; a file is kept when either name is listed. - holds_by_copies, single_changer and confirm read each revision's file under the name it has there (pair_of, new_of, head_name_of); the diffs at the landing commit list the head's names of a candidate's files too. - confirm counts lines of a file the commit created as well (only the side the line comes from must be a file). - Every git call runs with core.hooksPath=/dev/null (writing the scratch index ran post-index-change); skipped rename detection is an error (exit 4); no CDPATH when resolving the temp dir, and a temp dir with a backslash is refused; in_l1's answer is kept per commit and names. - bounded.py: a landing mode is no longer a tip cause by itself (a hand- edited squash or a merge counts when main's own edit in that step touches a branch unit, a merge or two squashes when some text repeats). - Maintainer decision (2026-10-31): a repeated text two commits touch and a several-step landing failing on repeated text stay at the tip, accepted as known limits; fixtures 179 and 180 pin that route. Version stays 3.5.1; partial clones keep exit 4. - Fixtures 174-180 (174-178 red on round 15). Suite 335/335 with bash 5, /bin/bash 3.2 and the path threshold at 0; bounded.py: 18 modes, 0 unexplained under the narrower tip causes (plain length 2 identical to rounds 13-15); fuzz: identical to round 15 in every run; this repository's own partial merge now lists only the commit that did not land; round-16 mutants: survivors in the test.sh header with the reason. - SPEC (D1 with the four missing fall-backs, rows 56b, 68, 74-78, results, history), CHANGELOG, plugin README row, idd-close TaskCreate wording. Refs #399
Verify Report — PR #418(第 16 輪,本機 commit
|
| 重現 | 第 16 輪 | 第 15 輪 | 3.5.0 | 正確答案 |
|---|---|---|---|---|
| fixture 153 的形狀,檔案搬兩次(f → mid/f → lib/f),只改中間名的 b2 先被 pick 到 main,b4 沒落地(logic probeD) | 0 | 3 | 3 | 3 |
| A 加一行;B 搬檔;C 在原名放一個 shim;main 只 pick B、C(DA R1) | 0 | 0 | 3 | 3 |
| A 加一行;B 搬檔;C 把新檔大部分改寫;main pick B,C 解衝突後沒有 A 的行(DA R2) | 0 | 0 | 3 | 3 |
| R1 的刪除版:A 刪一行,main 的新檔仍有那一行(DA R3,落地處) | 0 | 0 | 0 | 3 |
#399
| # | Severity | Finding | Source | Action |
|---|---|---|---|---|
| 1 | HIGH | 假的 clean(相對第 15 輪與 3.5.0 的退步):檔案搬兩次時,只改過中間名的 commit 看不見。 other_copies 只用 head、commit、parent、merge-base 四個名字列分支的 commit;f → mid/f → lib/f 時,只改 mid/f 的 commit 不在清單裡,它被 pick 到 main 之後,L^1 的副本被當成 main 自己的,從沒落地的 b4 被放行。single_changer 和 prefix 模式的計數有同樣的限制。第 15 輪答對只是因為另一個錯配剛好抵銷 |
logic, DA | Blocking |
| 2 | HIGH | 假的 clean:檔案的身分只靠一次兩棵樹的 rename 偵測。 兩棵樹的 rename 偵測要求來源路徑消失、相似度至少 50%;搬走後在原名放 shim、或搬走後大幅改寫,commit → head 就配不起來,commit 的行用舊名去讀,讀到的是另一個檔。tip(R1、R2:3.5.0 為 3,第 15、16 輪為 0)和落地處(R3)都受影響 | DA | Blocking |
| 3 | MEDIUM | count_of_line() 用 awk 的 == 比較兩行,兩邊都像數字時比的是數值:1、01、1.0 被算成同一行的副本,計數可能把沒落地的 commit 放行。已在 macOS awk 確認,改成字串比較即可 |
codex | Blocking |
| 4 | MEDIUM | partial clone 的決定(exit 4、不 lazy fetch)只在 git ≥ 2.45 成立;支援的最低版本是 2.30,2.30–2.44 會從網路抓 blob 並寫進 repo。CHANGELOG 的寫法沒有加版本限定 | requirements | Blocking |
| 5 | MEDIUM | 「rename 偵測被跳過就是錯誤」讓沒有搬任何檔案的分支 exit 4:git 只要新增 × 刪除的檔案數超過 1000×1000 就跳過,與有沒有搬檔無關;第 16 輪又對每個 candidate 做整棵樹的 commit → head diff。分支後來重產上千個檔案(vendored、generated)、或 merge 過做了這件事的 main,之前每個 candidate 都 exit 4(3→4、0→4,git 2.55 與 2.30 都重現) | logic, regression, DA | Blocking |
| 6 | MEDIUM | 設定檔定義的 hook 仍會執行:core.hooksPath=/dev/null 擋不到 hook.<name>.event=post-index-change,整棵樹模式寫 scratch index 時照樣觸發(git 2.55 實測 6 次)。要另加 -c hook.post-index-change.enabled=false |
security, DA | Blocking |
| 7 | MEDIUM | regression 的「每個答案變化都是修正」被推翻(見 #1):sweep 用的 suite、real PR、fuzz、bounded 都不建兩次搬移 | DA | Blocking(證據缺口) |
| 8 | LOW | SPEC D1 少列 bounded.py 最常用的 tip 原因:「已落地的 prefix 之後的 commit」 | requirements | In-scope |
| 9 | LOW | bounded.py 的 d1_cause 仍比 SPEC D1 寬:mop 是 landing 之前的 main commit,不是 landing 那一步的修改;「沒有淨改動」一律放行;「多步落地」與「重複文字」沒有要求 landing commit 真的在那段文字上失敗。拿掉前兩條後仍是 0 unexplained |
requirements, DA | In-scope |
| 10 | LOW | idd-close Step 1.55 與 plugin README 仍寫成「找不到落地處時」才用 tip;部分 merge 時 prefix 之後的 commit 也在 tip 檢查 | requirements | In-scope |
| 11 | LOW | exit 4 的清單只寫暫存目錄含反斜線,程式也拒絕換行;CHANGELOG、header、test.sh 對「rename 偵測被跳過」的觸發條件寫錯,印出的理由也沒有提到它 | requirements, regression | In-scope |
| 12 | LOW | no_rename_skip 在 git 的整段 stderr 裡找一個片語,repository 的文字(例如目錄名出現在 .gitattributes 的錯誤訊息)可以造出它,乾淨的 squash 變成 exit 4 |
logic, security | In-scope |
| 13 | LOW | pparse 對沒列在清單那一側的檔名不檢查換行:一個含 LF 的檔名能偽造 C/A/D/N/O 紀錄。要先過 has_lf 的檢查,實際可達的路徑很窄,屬於 defense-in-depth |
security | In-scope |
| 14 | LOW | 每個 candidate 都跑 head_names,150 個 commit 的分支 CPU 時間多約 44%;CHANGELOG 的時間數據是更早幾輪的 |
regression | In-scope |
| 15 | LOW | #399 的 Sister Bugs 更正(comment 6315687653)從第 10 輪延到現在還沒套用 | requirements | 推送後 |
| 16 | INFO | 第 15 輪 #10、#11、#12 的處理沒有 fixture 釘住 | requirements | In-scope |
| 17 | INFO | 確認過的:#399 的最小重現、PR #408(約 9.7 s CPU)、第 15 輪 HIGH 的重現、fixture 179/180 的路由、版本號一致、外部寫入與 diff 一致、輸入裡沒有 prompt injection | 全部 | — |
處理方式
第 17 輪的順序:
- 檔案身分改成逐 commit 追蹤(idd-all: 增加 HITL(attended)模式 — direct-commit + 允許 sub-skill AskUserQuestion #1、idd-all: direct-commit mode 加 passive warning (dirty tree / on default branch) #2、idd-all Phase 0.5: fork detection 改 fail-closed (gh repo view 失敗時預設 PR mode) #5、idd-all Phase 5: PR body heredoc 改 quoted (預防未來 RCE) #7):分支每個 commit、以及 main 從 merge-base 到 landing 的每一步,各自記錄自己的 rename;任何一個 revision 裡的檔名,都從 candidate 的檔名沿這些步驟一步步推過去(像
git log --follow),不再靠一次兩棵樹的偵測、也不再從四個名字裡挑第一個存在的。比對兩個 revision 時,同一個檔案的兩個名字直接配對,不交給 git 猜。rename 偵測只在單一 commit 內做;那個 commit 的偵測被跳過時,只有我們追的檔名正好在它沒配對的檔案裡才算錯誤。bounded.py 加入搬兩次、原名放 shim、搬走後改寫的形狀,讓這一輪的主改動有證據。 - 計數改成字串比較(idd-all SKILL.md: pseudocode quality (goto bash / branch-exists no-op / stale Failure Modes table) #3)、設定檔 hook 一併關掉(idd-all SLUG derivation: BSD/GNU sed alternation (\| not portable) #6)、git < 2.45 的 partial clone 開頭就 exit 4(idd-all Phase 4: ${MAX_ROUND} substitution missing in abort message #4),各加 fixture。
- pparse 拒絕含換行的檔名(idd-list 應該顯示引用 issue 的 open PR 資訊 #13);rename 偵測是否被跳過改成從檔案數判斷,不再搜 stderr(Complexity routing: vagueness 應該是一階信號,但目前 4 層 gate 都沒衡量需求清晰度 #12)。
- bounded 的 D1 原因與 SPEC 對齊(idd-all Phase 0.2: GITHUB_REPO post-derive shape assertion #8、idd-issue Step 2: 強制 sanitize incoming title (length cap / strip control chars) #9),文件修正(idd-all Examples: 跨 repo invocation 段重複描述,合併 #10、[SOP] umbrella split flow should mandate commit log scan before filing sub-issues #11、[refactor] idd-list PR body parsing: ignore #N inside fenced code blocks (sister concern from #13) #14)。
- [enhancement] idd-list cluster_leader config (lowest | primary) (sister concern from #13) #15 推送之後套用。
Round 16 verify found two HIGH problems of one kind: a file's identity came from one rename detection between two trees, or from the first of four fixed names a revision had. A file moved twice (a commit that edited it only under the middle name was never listed), a file moved away with a new file put at its old name, and a file moved and then mostly rewritten were paired wrongly, and commits that never landed read as landed. - Every file is followed commit by commit, as git log --follow does: one git log -M over the non-merge commits since the fork point gives each commit's own moves; a file's name in a revision is what those moves make of it, and every file is known by its name in the head. A move over two commits of the branch comes from one rename detection between the merge-base and the head, among the files added and deleted there. On main, a file the branch moved is the one main created at a name it had on the branch (a squash that moves a file and puts a new one at its old name records no move). - Every diff compares trees that hold each file under its key (ctree in the scratch store when a moved file is involved), with no rename detection between two trees; pparse, confirm, other_copies, in_l1, single_changer, holds_by_copies, the tip check and the head-equals-tip shortcut read each file by its key. The commits that edited a file are listed by every name it had. head_names, pair_of and the per-side record names are gone; same_paths still compares the names themselves. - A skipped rename detection (more than 1000 files added and deleted in one commit) is an error only where it deletes a file an earlier candidate touched, read after it (exit 4 with the reason); a branch that replaces a thousand generated files no longer exits 4. - Lines compare as text in the copy count (awk compared "1", "01" and "1.0" as numbers); hook.post-index-change.enabled=false turns off a hook defined in the configuration; a partial clone with git older than 2.45 stops before reading anything (exit 4). - bounded.py: the tip causes follow SPEC D1's wording (main's commit before the squash is no cause, nor a branch with no net change by itself; each repeated-text cause needs the text at a unit the net change changes). SPEC D1 lists the commits after a landed prefix. - Fixtures 181-195 (red on round 16 or on round 17's own drafts; the test.sh header says which). Suite 371/371 with bash 5, /bin/bash 3.2 and the path threshold at 0; Linux (git 2.30.2, mawk) 368/368; bounded.py: 18 modes, 0 unexplained under the narrower tip causes, every number as in round 16; old fuzzers identical to rounds 15-16; new rename fuzzer (fuzzrename.py): 251 cases, 0 false clean, 0 false listing, 8 D1+D2 (round 16: 19 false clean, 13 false listing); round 16's and 15's repro scripts: same answers as round 16 (21 scripts), round 16's HIGH repros fixed. - SPEC (D1, rows 79-92, history, results), CHANGELOG, plugin README row, idd-close wording of the tip route, test.sh header. Refs #399
Verify Report — PR #418(第 17 輪,本機 commit
|
| 重現 | 第 17 輪 | 第 16 輪 | 3.5.0 | 正確答案 |
|---|---|---|---|---|
| 分支先加 b(a 的複本)、再刪 a,squash 後 head 與 tip 的 tree 完全相同(DA copydel) | 3 | 0 | 0 | 0 |
| 同上再加 main 的 follow-up(#399 的第二種形狀) | 3 | 0 | 3 | 0 |
| 兩檔分三個 commit 互換名字後 squash,之後一個刪行的 commit 沒落地(DA swap2 del) | 0 | 3 | — | 3 |
| 分支 merge 過 main(main 已 squash 過分支的一部分),之後一個刪行 commit 沒落地(logic mk_fc2) | 0 | 3 | — | 3 |
| 寬分支(上千個路徑)裡有搬過的檔、名字排在前面,squash 落地(regression mkwide) | 3 或 4 | 0 | — | 0 |
#399
| # | Severity | Finding | Source | Action |
|---|---|---|---|---|
| 1 | HIGH | 假的 clean(退步):分支 merge 過 main 時,merge-base 的檔案對不到 key。 同一路徑從兩條歷史線各來一個檔,重放時誰佔住路徑取決於 commit 日期;merge 進分支的 main commit 被當成分支的 revision,main 那一側的規則都不套用。淨改動因此少了刪除,沒落地的 commit 被判為 clean | logic | Blocking |
| 2 | HIGH | 誤報(退步):squash 自己的 rename 偵測與分支逐 commit 的歷史配對不同時(先複製再刪、搬移後再複製、搬到中間名再刪再加),main 依歷史把名字給了另一個檔,head 的檔在 main 上找不到;P0 也因此把 head 與 tip 完全相同的分支判成有 orphan,違反 #399 的 Expected | logic, DA | Blocking |
| 3 | HIGH | 假的 clean(退步):兩檔互換名字(或三個以上輪轉)後 squash,git 只記成兩次修改,main 依歷史把 key 對到錯的檔,沒落地的刪行被判為已落地 | DA | Blocking |
| 4 | HIGH | SIGPIPE:has_tracked、diff_by_key 的 awk 一找到就提早結束,set -o pipefail 下整條 pipeline 回 141。路徑清單超過 pipe buffer 且被追蹤的名字排在前面時,比對失敗,退回 tip 誤報或 exit 4;P0 則把 141 當成「沒有被追蹤的名字」 |
regression, DA | Blocking |
| 5 | HIGH | partial clone 只認 extensions.partialClone:git clone --filter 只寫 remote.<name>.promisor=true,所以 git 2.30–2.44 的真 partial clone 仍會從網路抓 blob、寫進 repo |
codex, requirements, security, DA | Blocking |
| 6 | MEDIUM | 路徑含位元組 \034(awk 的 SUBSEP)時,entries_of、ctree 把它當分隔符拆開,tip 檢查可能把沒落地的修改判為 clean |
codex | Blocking |
| 7 | MEDIUM | hook.post-index-change.enabled=false 這個 event 開關是 git 2.55 才有;2.54 已有設定檔 hook,卻只能逐一以名字關掉,寫 scratch index 時照樣執行 |
security | Blocking |
| 8 | MEDIUM | 沒有 merge-base(淺 clone、不相關的歷史)時,identity 層在 P0 之前就 exit 4;第 15、16 輪會照常回答 | regression | Blocking |
| 9 | MEDIUM | 只要 fork point 之後有任何 rename(包括 main 自己不相關的),每個 diff 都重放一次 fork 以來的歷史:main 很長時 CPU 2–4.6 倍 | regression | Blocking |
| 10 | MEDIUM | 「rename fuzz 0 false clean」的涵蓋範圍說得太大:產生器不會移回原名、互換、輪轉、跨 commit 搬移、main 自己搬檔、分支 merge;bounded 也不建 move | DA | Blocking(證據缺口) |
| 11 | LOW | blob:none 的 partial clone 在 main 的 git log -M 需要 blob,exit 4 的理由沒有說是 partial clone |
requirements | In-scope |
| 12 | LOW | 文件:rename 偵測被跳過的條件應寫「新增 × 刪除 > 1,000,000」;「main 搬走的檔裡的刪除」已不再是盲點(現在回 3);test.sh 開頭的 exit 4 清單沒補新原因;SPEC/CHANGELOG 對 main 第 1 步的描述比程式窄;SPEC 說「不再有兩棵樹的 rename 偵測」但 links 就是一次;CHANGELOG 把未發布的內部輪次當比較基準 | requirements, regression | In-scope |
| 13 | LOW | unc_check 的 awk 讀檔失敗被當成「沒有」;P0 的 key 檢查在 has_tracked/cand_keys 出錯時落到 exit 0 |
logic | In-scope |
| 14 | LOW | \001 同時是名字裡 LF 的代用字元與檔名歷史的分隔符 |
security | In-scope |
| 15 | LOW | 第 16 輪說要在 bounded.py 加 rename 形狀,實際改用 repo 外的 fuzzer | requirements | In-scope |
| 16 | INFO | 確認過的:#399 最小重現與 follow-up、PR #408、第 16 輪 HIGH 的重現、433 個已 merge PR、bash 3.2 與 Linux 的 suite、外部寫入與 diff 一致、輸入沒有 prompt injection | 全部 | — |
處理方式
第 18 輪:
- main 那一側改成「落地處之後看名字、之前看歷史」(idd-all: 增加 HITL(attended)模式 — direct-commit + 允許 sub-skill AskUserQuestion #1–idd-all SKILL.md: pseudocode quality (goto bash / branch-exists no-op / stale Failure Modes table) #3):被當成落地處的 commit 及其後(含 tip)以名字配對,用分支在落地那一點的檔名;落地處之前(L^1 等)才依 main 的歷史。分支這一側的歷史改成同一路徑從兩條歷史線來時視為同一個檔,與重放順序無關;merge 進分支的 main commit 也走 main 那一側的規則。P0 只比名字。互換、輪轉、先複製再刪、分支 merge main 都做成 fixture,fuzzer 也加這些形狀。
- SIGPIPE(idd-all Phase 4: ${MAX_ROUND} substitution missing in abort message #4):判斷用的 awk 直接讀檔,不經 pipe;檢查其他提早結束的 pipeline。
- partial clone(idd-all Phase 0.5: fork detection 改 fail-closed (gh repo view 失敗時預設 PR mode) #5、[SOP] umbrella split flow should mandate commit log scan before filing sub-issues #11):同時認
remote.*.promisor,理由寫明是 partial clone。 \034、設定檔 hook、沒有 merge-base、效能(idd-all SLUG derivation: BSD/GNU sed alternation (\| not portable) #6–idd-issue Step 2: 強制 sanitize incoming title (length cap / strip control chars) #9):不用 SUBSEP 拼接路徑;逐一以名字關掉綁在 post-index-change 的 hook;沒有 merge-base 時不追蹤檔名、照舊回答;沒有被追蹤的名字時不重放歷史,ops 只留有動作的 commit。- LOW 與文件(Complexity routing: vagueness 應該是一階信號,但目前 4 層 gate 都沒衡量需求清晰度 #12–[enhancement] idd-list cluster_leader config (lowest | primary) (sister concern from #13) #15)。
Round 17 verify found six HIGH problems. Three of one kind: main's files were read by main's history, and a squash records the branch's moves as one rename detection, which pairs files otherwise. A copy and then the original deleted read as a move (a branch whose head and tip were the same tree was listed); two files that swap names read as two edits (a deletion that never landed read as landed); a branch that merged main's squash had one path reached by two lines of history, kept by whichever the log listed first (a deletion that never landed read as landed). Two checks stopped reading a pipe at the first match (SIGPIPE under pipefail on a wide branch), and a partial clone made by git clone --filter was not recognized. - Readings of a revision of main: "<rev>@<ref>" holds the branch's files at the names they had in <ref> where it has a file there (the landing commit and the tip after a landing); "<rev>@@<ref>" does the same except where main's history puts another of the branch's files at that name: history's when main never changed the file there, else the head file it shares more lines with (each commit the landing search tries; the tip when nothing landed); a revision in its reference's history is read by history. The landing search also asks that every file the net change moved be at the reference's name (same_files). - At the tip and at the landing commit a path a commit added or deleted is compared by file, not by whatever is at the name. - One file per path two lines of history bring a file to, whatever the order; a link's old name paired with the merge-base's file; a copy and the original deleted later are one file, link by link (each branch deletion paired with the copies made before it); of a copy and its original in one revision the copy stands for the file, and the original's name reads back to it. - P0 compares names. No check stops reading a pipe early; awk never joins two paths in one string; promisor remotes count as a partial clone; configured hooks are also turned off by name (git 2.54); with no merge-base no file is followed; a revision replays only the commits that move, add or delete a followed name. - Fixtures 196-212 (each red on round 17 or on round 18's own drafts; the test.sh header says which). Suite 408/408 with bash 5, /bin/bash 3.2 and the path threshold at 0; Linux (git 2.30.2, mawk) 405/405; bounded.py: 18 modes, every number as in round 17, 0 unexplained; the older fuzzers identical to round 17; fuzzrename2 (seeds 1-8, 353 cases): false clean/false listing 0/1 (D3) where the landed commits are a prefix and 2/10 where they are not (round 17: 6/44 and 11/15; round 16: 6/17 and 14/14); fuzzrename (251 cases): 0 false clean, 2 false listings, both where the landed commits are not a prefix; round 16's and 17's repro scripts: the same 30 answers as round 16; mutants M800-M823: 19 caught, M805's full variant caught, four unpinned backstops (test.sh header). CPU time on PR #408 about 20 s. - SPEC (D1, rows 93-109, history, results), CHANGELOG, plugin README row, helper header, test.sh header. Refs #399
Verify Report — PR #418(第 18 輪,本機 commit
|
| # | 重現 | 第 18 輪 | 第 17 輪 | 第 16 輪 | 3.5.0 | 正確答案 |
|---|---|---|---|---|---|---|
| 1 | 分支 merge 過 main(main 已 squash 並把檔搬走),之後分支改舊路徑上的檔,沒落地 | 0 | 3(但列錯 commit) | 3 | 3 | 3 |
| 2 | 兩檔互換、再複製並刪原檔;main 只 squash 了一部分(不是前段),互換的最後一步沒落地 | 0 | 3 | — | — | 3 |
| 3 | 單次「複製後刪原檔」、搬走後原名放新檔,main 只拿到一部分:第 18 輪多列已落地的 commit | 多列 | 對 | — | — | — |
| 4 | 分支「搬走再放回原檔」(或複製、刪除、再 revert),整個 squash 落地,之後 main 改了它 | 3 | 0 | 0 | 0 | 0 |
#399
| # | Severity | Finding | Source | Action |
|---|---|---|---|---|
| 1 | CRITICAL | 假的 clean(退步):同一路徑從兩條歷史線各來一個檔時,第 18 輪把兩者併成同一個檔;之後其中一條線把檔搬走,另一條線的檔也跟著失去身分,分支之後在舊路徑上的修改讀成空的。結果取決於 commit 的日期順序 | logic | Blocking |
| 2 | HIGH | 假的 clean(退步):落地的 commit 不是前段時,整個分支被當成在 squash 落地;分支中途建立又移除的名字(main 還留著檔)不在淨改動裡,於是在落地處漏掉。不在任何已記錄的偏差裡 | requirements | Blocking |
| 3 | HIGH | 新的 D1 限制寫得比實測窄:單次複製後刪除、搬走後原名放新檔(決策表第 80、82、88、92 列宣稱處理的形狀)也會出錯,且是相對第 17 輪的退步;Results 只給總數,看不出這些是新的 | requirements | Blocking |
| 4 | HIGH | 假的 listing(退步,#399 本身的形狀):搬走再放回原檔、或複製刪除再 revert,整個 squash 落地後 main 又改了它:弱讀法把原名保留給已經搬走的檔,放回來的檔在 main 上找不到 | DA | Blocking |
| 5 | MEDIUM | 弱讀法:搬走後在原名放一份一模一樣的檔,main 沒動過那個路徑,就把名字留給歷史,真的落地處被拒絕(退步,同屬 #399 的形狀) | logic | Blocking |
| 6 | MEDIUM | 純搬移落在 main 自己已有檔的名字上(另一個 PR 先加了同名檔),各版本都回 0;不在已知盲點清單 | DA | Blocking(文件或修) |
| 7 | MEDIUM | 複製配對:pathspec 不限長度(寬分支從 3 變成 exit 4)、O(刪除 × 新增)、依 awk 陣列順序(macOS 與 Linux 答案不同) | security, regression, logic | Blocking |
| 8 | MEDIUM | 環境裡的 GIT_CONFIG 會讓兩個讀設定的防線失效(2.54 的 hook、2.45 以前的 lazy fetch) |
security | Blocking |
| 9 | MEDIUM | 新的已知限制沒有經過你同意就進了封閉的偏差清單 | requirements | 需要你決定 |
| 10 | MEDIUM | 兩個 rename fuzzer 不在 repo 裡,無法重現 | codex | In-scope |
| 11 | LOW | nm_decide 平手時給名字,兩個幾乎相同的小檔互換可以讓 main 自己的 commit 被當成落地處(各版本都回 0) | DA | In-scope |
| 12 | LOW | CPU 時間比第 16、17 輪多 40–70%,落地處搜尋加上搬檔時是第 16 輪的 3.7 倍;suite 在 macOS 接近 1200 秒上限 | regression | In-scope |
| 13 | LOW | 文件:holds 實際用弱讀法;決策表第 101、106 列與 helper 不一致;remote.<name>.partialclonefilter;GIT_CONFIG_COUNT 前導零與 GIT_CONFIG_PARAMETERS;cand_keys 的讀取錯誤;計時的負載範圍;複製偵測上限 200 沒列進限制 |
requirements, security, regression | In-scope |
| 14 | INFO | 確認過的:第 17 輪 HIGH 的重現、433 個已 merge PR、跨平台 suite、diff 以外的寫入與 diff 一致、輸入沒有 prompt injection;DA 認為 #1 應降為 HIGH(第 17 輪在另一種日期順序下也錯) | 全部 | — |
處理方式
第 16 到 18 輪的走向是:每一輪為檔案身分加的規則,修好上一輪的形狀,卻在別處弄壞原本對的答案。第 19 輪開始前需要你決定範圍(見 dashboard)。第 19 輪的草稿已經修好 #1(每個 view 與該 revision 實際的樹對帳)與 #2(被刪的名字上若有沒歸屬的檔,算沒落地),fixture 181–212 全綠。
Round 18 verify found one CRITICAL and three HIGH problems, most of them round 18's own rules breaking shapes earlier rounds got right: a path two lines of history brought a file to was one file, and a move on one line took the other line's file with it (an edit at the old path read as no change); a name the branch made and removed again was in no net change, and the commit that left it was missed; a move and the original put back, squashed and edited later by main (#399's own shape), listed both commits. The maintainer chose to converge (2026-11-01): fix the common shapes and #399's own, and accept the rarer move combinations as known limits. - Each view of the files is checked against its revision's tree: a name the revision holds and the view lost is a file of its own, one the view holds and the revision does not is gone. - Readings of main: a file history puts at a name keeps it only when it has no place of its own at its name in the reference; history's exception applies only when the head has another file at the reference name; a name the reference gives another file is skipped only while that file has no place elsewhere (always for the other name of a copy); a tie in the content decision goes to history. - At the tip and the landing commit a deleted name holding a file the reading gives no file of the head counts as not landed; at the tip a deleted line is kept while any name of the file has it. - Copy pairing newest copy first in a fixed order, bounded (200 detections, 100,000 pairs, 64 KB of paths, beyond which a deletion stays a deletion). GIT_CONFIG and an inherited GIT_CONFIG_PARAMETERS are unset; a remote with a partial clone filter is a partial clone; GIT_CONFIG_COUNT is read in base 10; cand_keys fails on a read error; same_files runs only when the net change touches a followed file. - SPEC M1 lists the accepted limits (only these, verdict wrong either way): landed commits not a prefix with a moved, swapped or copied file; a copy and its original both landed with a line deleted before the copy; a pure move onto main's own file; nearly identical files swapped; beyond the copy-pairing bounds. - The rename fuzzers (fuzzrename.py, fuzzrename2.py) are in the test directory; the suite asks for 2400 s. - Fixtures 213-220. Suite 420/420 with bash 5, /bin/bash 3.2 and the path threshold at 0; Linux (git 2.30.2, mawk) 417/417; bounded.py: plain length 2 in full and four sampled modes, every number as in round 17, 0 unexplained; fuzzrename2 (353 cases), false clean/false listing: 0/1 (D3) where the landed commits are a prefix and 1/8 where they are not (round 18: 0/1 and 2/10; round 17: 6/44 and 11/15); fuzzrename (251 cases): 0 false clean, 2 false listings, both off the prefix route (M1); round 16's and 17's repro scripts: the same 30 answers as round 16; the round-18 reviewers' repros of the CRITICAL and HIGH findings fixed; mutants M900-M913: 7 caught, 5 unpinned (test.sh header). CPU time on PR #408 about 17 s. - SPEC (M1, rows 101, 106, 110-117, history, results), CHANGELOG, helper header, test.sh header. Refs #399
Verify Report — PR #418(第 19 輪,本機 commit
|
| # | 重現 | 第 19 輪 | 第 18 輪 | 第 17 輪 | 第 16 輪 | 3.5.0 | 正確答案 |
|---|---|---|---|---|---|---|---|
| 1 | 分支先改檔、之後才 merge main(main 已 squash 第一個 commit 並搬走檔);那次修改沒落地 | 0 | 0 | 3 | 3 | 3 | 3 |
| 2 | main 搬走檔並在原名放新檔;分支刪掉的一行沒落地(前段落地,分支沒搬檔) | 0 | 3 | 3 | — | — | 3 |
| 3 | 分支建立暫時的名字再搬走,main 在那個名字有自己的檔,整個 squash | 3 | 0 | 0 | — | — | 0 |
| 4 | 第一個 PR squash 後按 Update branch 繼續做,main 改了第一個 PR 的行,第二個 PR squash | 3 | 3 | 3 | 3 | 3 | 0 |
| 5 | main 在 PR 開著時改檔名,squash 整個分支後 follow-up 改同一行 | 3 | 3 | 3 | 3 | — | 0 |
#399
| # | Severity | Finding | Source | Action |
|---|---|---|---|---|
| 1 | CRITICAL | 假的 clean(退步):第 18 輪 CRITICAL 只修了一半。對帳只救回 head 仍持有的那個名字;分支在 sync merge 之前做的修改,仍歸到 main 搬走的那個檔,於是讀成已落地(上表第 1 列)。main 以 cherry-pick 或自己加同樣內容取得第一個 commit 時也一樣 | requirements, regression | Blocking |
| 2 | HIGH | 假的 clean(退步):收窄後的歷史例外只在 head 另有檔時才跳過參照名字;main 把檔搬走、在原名放無關的新檔時,tip 拿分支的檔去比 main 的新檔(上表第 2 列) | logic | Blocking |
| 3 | HIGH | 假的 listing(退步):「被刪的名字上若有沒歸屬的檔,算沒落地」這條規則在整個 squash 落地時也生效,分不出那是分支的檔還是 main 自己的檔(上表第 3 列;搬檔、複製後刪除、改名都會) | logic | Blocking |
| 4 | HIGH | 假的 listing(#399 本身的症狀,3.5.0 起都錯):上表第 4 列。sync 之後 merge-base 已含第一個 PR,但 git cherry 仍把第一個 commit 當候選,拿到第二個 PR 檢查,而那裡的行已被 main 改掉。決策表沒有這一列,bounded.py 也不曾在兩次 squash 之間 sync。D4 的理由在這裡不成立 |
DA | Blocking |
| 5 | MEDIUM | 假的 listing(第 16 輪起都錯):上表第 5 列。不在 M1 也不在 10/31 的接受清單;兩個 rename fuzzer 都不會在落地後做 follow-up,也不會在落地後讓 main 搬檔再 sync,所以「前段路線 0 個假 clean」沒量到這兩類 | DA | Blocking |
| 6 | MEDIUM | 互換再換回、只 squash 第一次互換(前段):各版本都回 0,正確是 3;不在 M1 | requirements | Blocking |
| 7 | MEDIUM | 決策表第 9 列(暫存檔、main 有同名檔)中間加一次搬移,第 16–19 輪都多列那次搬移 | logic | Blocking |
| 8 | MEDIUM | 複製配對的上限反而製造假的搬移:一次換掉上千個產生檔再 squash,第 17、18 輪回 0,第 19 輪回 4;上限算的是配對數不是迴圈次數,仍是平方複雜度 | regression, requirements, security | Blocking |
| 9 | MEDIUM | lr18/b1、b2 的清單在第 19 輪多了 b1;DA 指出那是整個沒落地的情況,exit 3 正確、清單差異在已記錄的範圍內,可降為 LOW | regression, DA | In-scope |
| 10 | LOW | 文件:M1 第 5 種、CHANGELOG、helper 開頭仍寫 64 KB 上限(最終版已改用過濾後的樹);fixture 217 沒釘住它被引用的複製順序;Linux suite 時間、fixture 220 的限制引用、CHANGELOG 漏了刪除名字那條規則;2400 秒 timeout 沒有餘裕;fuzzer 的「not a prefix」標籤沒檢查分支有沒有搬檔 | requirements, regression, logic | In-scope |
| 11 | INFO | 確認過的:#399 的原始形狀、第 18 輪的重現、逐案表、fuzz 計數、版本、suite;安全面;diff 以外的寫入;GIT_CONFIG_COUNT 的 +N、空值的 extensions.partialClone 比 git 嚴格(保守方向) |
全部 | — |
處理方式
第 16 到 19 輪,每一輪為「檔案搬移」加的規則都修好上一輪的形狀,又在別處弄壞原本對的答案;這一輪 DA 還指出,沒有搬移的常見流程(上表第 4 列)從 3.5.0 起就一直誤報。沒有搬移的部分由 bounded.py 逐一比對過,答案自第 17 輪起沒變;不穩定的是搬移那一層。接下來的方向需要維護者決定(見 dashboard)。
…#399) Round 19 verify failed again (two CRITICAL, one finding: an edit the branch made before merging main, whose file main had moved, read as landed), and the devil's advocate found #399's own symptom in a common flow every version since 3.5.0 got wrong: a first PR, GitHub's Update branch, main's edit of the first PR's line, a second PR listed the first PR's commit. Rounds 16 to 19 each fixed the move shapes the round before broke and broke others. The maintainer decided (2026-11-01) to stop following files across moves. - A file is its path. The file identities, readings, copy pairing and reconciliation of rounds 17 to 19 are gone (about 740 lines). - A move counts when git's rename detection inside a non-merge commit since the fork point finds one in a commit of the branch the baseline lacks, or one anywhere whose old or new name is a path such a commit touched. Then: 0 when the tip has the head's content at every path a candidate touched and both names of every such move; judged as usual when every such move is main's own, after every landing commit used, with nothing checked at the tip; exit 4 otherwise, naming the commit (never a file name). - A commit listed at the landing commit is checked again where the branch up to a first-parent merge of main into it landed (oldest first; the first part that contains it decides). Caches keyed by the landing commit. - bounded_sync.py builds the first PR / Update branch / second PR shape (round 19: 238 of 3,192 length-2 cases unexplained). - Fixtures 221-229; 42 move assertions now expect exit 4 and their list assertions are gone; 206 moved to the wide path. - Suite 397/397 (bash 5; fixtures 224 and 227-229, changed or added after it, run one by one on the same helper); bounded_sync.py length 2, all 3,192 cases: 0 unexplained (round 19: 238); bounded.py plain length 2 in full, --landing own and --landing two: every number as in rounds 17-19, 0 unexplained; fuzzrename2 (353 cases): 147 exit 4, 205 agree, 1 list that differs by path (exit code right); fuzzrename (251): 111 exit 4, 138 agree, 2 D1+D2; mutants M1000-M1012: 7 caught, 6 unpinned (test.sh header). CPU time on PR #408 about 11 s (round 16: 12 s). - SPEC (Definition: merges of main, Moves; M1 removed; rows 11-117 updated where a move counts, rows 118-127; history), CHANGELOG, README row, helper header, test.sh header. Refs #399
Verify Report — PR #418(第 20 輪,本機 commit
|
| # | Severity | Finding | Source | Action |
|---|---|---|---|---|
| 1 | HIGH | 假的 clean(第 19 輪一樣):git 在單一 commit 裡沒配成對的搬移不算數。一種是搬走後在舊名放新檔(shim),另一種是 git mv -f 搬到已存在的名字。分支刪掉的行在舊路徑上不見了,就讀成已落地,實際上 main 搬過去的檔裡還有那一行 |
DA | Blocking |
| 2 | MEDIUM | 第一個 PR 合併後、按 Update branch 前又推了一個 commit(GitHub 上常見的順序),第一個 PR 的 commit 仍被列出:補回比對要求「分支到 merge 的第一個 parent」整段在某處落地 | logic, regression | Blocking |
| 3 | MEDIUM | 同一個原因的另一種情況:第一個 PR 本身只收了一部分,完整收下的那個 commit 仍被列出 | DA | Blocking |
| 4 | MEDIUM | main 改目錄名,merge 的目錄改名偵測把分支新增的檔帶到新目錄;這個搬移不算數,結果是錯的 3 | DA | Blocking |
| 5 | MEDIUM | plugin.json 與 marketplace.json 的 3.5.1 描述仍寫「follow renames」 | requirements, codex | Blocking |
| 6 | MEDIUM | 決策寫「搬移或複製」回 exit 4,實作只把 git 記成搬移的算進去,複製照路徑讀 | requirements | 需要確認範圍 |
| 7 | LOW | 文件:CHANGELOG 仍說沒落地的改名會列出;Results 的 bounded_sync 表把 bounded.py 的表切開;部分量測說法暗示這輪沒跑的 run;exit 4 的列舉、test.sh 開頭、helper 註解仍有身分層的描述;README 漏了「沒有東西在 tip 檢查」;「跨兩個 commit 的搬移照路徑讀」在 squash 後不成立(squash 記成一次搬移,回 4);suite timeout 2400 秒與這輪量到的不符 | requirements, logic, regression | In-scope |
| 8 | LOW | 補回比對無法執行時(預算用完、錯誤),清單照列且沒有說明;git 跳過改名偵測時的 exit 4 不在 Moves 的定義裡;main 在落地前搬了又搬回的子句沒有 fixture;身分層的殘留(寫了沒人讀的檔、恆真條件);fixture-128 一個不涉及搬移的清單斷言在批次修改時被刪 | requirements, logic, regression | In-scope |
| 9 | LOW | idd-close Step 1.55 的程式片段有一行寫成兩個反斜線,照原樣執行會解析失敗(base 就有,但本 PR 改的正是這一段) | DA | In-scope |
| 10 | INFO | 補回比對共用 200 次檢查預算、一律先建好;partial clone 的不 fetch 在 git ≥ 2.45 沒有測試(人工確認正確);bounded_sync.py 只比答案、第一段一律完整落地 | logic, security, requirements | — |
處理方式
第 21 輪:
- 搬移偵測加上 git 的改寫拆分(
-B),讓 shim 與搬到既有名字也記成搬移;main 搬走分支檔案所在的目錄時,那次搬移也算數。 - 補回比對改成逐 commit 判斷:對每次 merge main,看這個 commit 自己的效果是否已在被 merge 進來的 main commit 上就位,不再要求整段落地。
- 複製維持照路徑讀,在 SPEC 寫明:沒有找到錯的結束碼,只有清單不同。
- 文件與 LOW 一併處理。
Round 20 verify found one HIGH and six MEDIUM, none a regression: a move git pairs only once a rewrite is broken (a file moved with a new file put at its old name, or moved onto a name that exists) did not count, and a deletion that never landed read as landed; a directory main renamed carried the branch's new file along and listed it; the Update-branch check needed the branch up to the merge to land as a whole, which fails when a commit was pushed before Update branch or the first PR left part out; the plugin description still said the gate follows renames. - Moves are looked for with git log -B -M; a copy git reports from a broken rewrite counts as a move (git breaks only a rewrite of a file over 400 bytes, documented). A move that takes a file out of a directory where the branch added one counts. - A commit listed at the landing commit is checked again in place at the main commit each first-parent merge of main brought in (any of them decides), not where the branch up to the merge landed. - bounded_sync.py: first PRs that take part of the first part (commits pushed before the merge, squashes that left some out). - Fixtures 230-237; fixture-128's list assertion restored. - Suite 408/408 (bash 5; fixtures 235-237 and the larger 237 run one by one on the same helper); bounded_sync.py length 2, all 6,264 cases: 0 unexplained (round 20 on the 3,072 added cases: 472); bounded.py plain length 2 in full, --landing own and --landing two: every number as in rounds 17-20; fuzzrename2 (353): 147 exit 4, 205 agree, the same 1 list that differs by path; fuzzrename (251): 115 exit 4, 134 agree, 2 D1+D2, 0 false; mutants: 13 caught, 4 unpinned (test.sh header). CPU time (load 7-27): PR #408 7.7 s (round 16: 8.5 s), a squash-landed 150-commit branch 16.9 s (round 16: 20.7 s). - Docs: plugin.json and marketplace.json 3.5.1 description, CHANGELOG (moves, Update branch, deletions bullet, suite timing), README row, SPEC (Moves, rows 128-135, Results, history), helper header and identity leftovers removed, test.sh header; idd-close Step 1.55's pipeline continuation had two backslashes; suite timeout 3600 s. Refs #399
Verify Report — PR #418(第 21 輪,本機 commit
|
| # | Severity | Finding | Source | Action |
|---|---|---|---|---|
| 1 | HIGH | 退步:-B 把改寫過的檔也算成 rename 的來源,大的 commit 因此超過 rename 上限,git 跳過偵測。helper 判斷「git 有沒有跳過」只算新增×刪除,沒有算到這些檔,結果第 20 輪的 exit 4 變成錯的 0 或 3 |
requirements, regression, logic, security | Blocking |
| 2 | HIGH | 假的 clean:小於約 400 bytes 的檔,搬走後在舊名放一行 shim,-B 不會拆開它,分支刪掉的行讀成已落地 |
requirements | Blocking |
| 3 | HIGH | 退步:目錄規則把「main 從分支新增檔案的目錄搬走一個兄弟檔、目錄還在」也算成搬移。完整 squash 變成 exit 4,真的 orphan 也被 exit 4 蓋掉(第 20 輪分別是 0 與 3) | logic, requirements, regression | Blocking |
| 4 | HIGH | 假的 clean:Update branch 時 merge 把分支的新檔帶到 main 改名後的目錄,P0 只比舊名,新檔沒落地也回 0(第 19、20 輪一樣) | logic(DA:預設設定下這是人解衝突,應為 MEDIUM) | Blocking |
| 5 | MEDIUM | 目錄規則只看直接的上層目錄;merge-ort 也會從子目錄裡的搬移推出上層目錄改名(例如目錄裡只有子目錄),結果是錯的 3,Update branch 後是錯的 0 | Codex, requirements, logic | Blocking |
| 6 | MEDIUM | 退步:第二次比對只看 merge 進來的 main commit。main 在 Update branch 前改了第一個 PR 的某一行、merge 保留分支那邊時,第 20 輪回 0,第 21 輪列出 | logic | Blocking |
| 7 | MEDIUM | git 跳過 rename 偵測、Update branch 又把分支的修改帶到新檔名時,P0 在 unc 檢查之前就回 0(第 19–21 輪一樣) | DA | Blocking |
| 8 | MEDIUM | 退步(效能):-B 讓每次執行都讀過 fork 以來所有改過的 blob。Akashic-Library 的 PR 平均約慢一倍,常改大檔的 repo 線性變慢;blobless partial clone 的 exit 4 幾乎加倍 |
regression, security | Blocking |
| 9 | MEDIUM | 決定寫「搬移或複製」,實作把複製照路徑讀,沒有記錄確認 | requirements | 需要確認範圍 |
| 10 | MEDIUM | 目錄規則的前提:git 預設設定下 merge 停在 CONFLICT (file location),要人接受建議才會搬;fixture 231 把衝突訊息丟掉 |
DA | Blocking |
| 11 | MEDIUM | -B 把分支自己的拆檔(複製後把原檔改成 shim)算成搬移,exit 4(第 20 輪 0 且正確) |
logic(DA:在決定範圍內) | 見處理方式 |
| 12 | LOW | 文件:400 bytes 的門檻差一、實際是 max(舊, 新);CHANGELOG「只有這些」的封閉清單沒跟上;test.sh 開頭新舊兩段互相矛盾;SPEC 的 fixture 範圍與 bounded_sync.py 說明過時;第 133 列的 S 欄與定義矛盾;exit 4 的說明句不符目錄規則與複製;方法說明殘留身分層的用語 | requirements, logic, regression | In-scope |
| 13 | LOW | main 自己在落地後的搬移照常判斷,超出決定「落地前後」的字面,沒有記錄確認 | requirements | 需要確認範圍 |
| 14 | LOW | 第二次比對把「main 後來也 merge 進去的別的分支」當成 merge main;一個 commit 分兩次落地仍被列出 | logic | 已知限制 |
| 15 | LOW | 實際歷史:-B 多一個 0→4(Akashic-Library #736,內容拆分) |
regression | 見處理方式 |
| 16 | INFO | exit 4 的 fixture 只斷言結束碼,設定錯誤也會通過 | regression | In-scope |
處理方式(第 22 輪)
- 拿掉
-B,搬移偵測分三段:- 每個非 merge commit 跑
-M,和第 20 輪相同。另外用 blob id 找「刪掉的檔,內容和同一 commit 改過的另一個檔完全相同」,也就是搬到既有的名字。 - 只對「新增檔案、又改了檔案,且屬於分支或碰到分支路徑」的 commit 跑
-C。從同一 commit 改過的檔複製出去的檔(shim、拆檔)算數。 - 分支 first-parent 上的每個 merge,對它的第一個 parent 跑 rename 偵測。分支的檔在 merge 裡被搬走就算數。
- 每個非 merge commit 跑
- 「git 跳過偵測」照 git 自己的條件算:複製的來源也包含改過的檔。git 警告了跳過、helper 卻算不出是哪個 commit 時,直接 exit 4。有這種 commit 刪掉(或改了)candidate 碰過的路徑時,不走 P0。
- 目錄規則只在搬移後原目錄已經不存在時才算,並照 merge-ort 的方式往上層推。用目錄規則找到的搬移不放進 P0 比對的名單。
- Update branch 的第二次比對兩處都看:「分支到 merge 為止」落地的地方(第 20 輪的比對),以及 merge 進來的 main commit(第 21 輪)。任一處就位就算落地。
- 複製:原檔在同一 commit 有改動的複製算數。原檔沒變的複製照路徑讀,因為原檔還在原名。這是對決定裡「複製」的解讀;這一點與第 13 項會請維護者確認。
- LOW 的文件問題一併處理,第 14 項記為已知限制。
…at is read (#399) Round 21 verify found five HIGH and thirteen MEDIUM, three of the HIGH regressions of round 21's own: -B made every rewritten file a rename source, so a wide commit passed git's rename limit and the skip went uncounted (a move read as a deletion: 0 or 3); the directory rule counted a sibling moved out of a directory that stays; -B read every changed blob since the fork point. Also a shim at a small file's old name read as clean; a merge of main that moved the branch's file read as clean through P0; a commit whose line main edited before Update branch, the merge keeping the branch's side, was listed again. - Renames come from -M in every non-merge commit (round 20's pass); copies from -C, run only on a commit that adds a file and changes one, of the branch or touching a path it touched, and counted when that commit replaced the original's content (more than half its lines gone); a move onto a name that exists from the blobs. - Renames inside the merges on the branch's first-parent line count when they move a path the branch touched. - The directory rule counts only a directory taken away, walking up as merge-ort does, and its names stay out of P0. - git's skipped detection is counted with git's own sources; a warning the count cannot place stops the check; P0 is not taken past a commit git did not look in that deleted (or replaced) a candidate's file. - The Update-branch check also looks where the branch up to the merge landed (round 20's check) besides the merged main commit. - bounded_sync.py --pre-edit: main edits the first PR's line before the merge; B2 covers main's own edit taken as the landing commit. - Fixtures 238-248; 231 asserts the squash's file-location conflict; rc_why checks the exit-4 reason. - Suite on the round's helper (fixtures 1-247) 433/433, fixture 248 run alone; bounded.py plain length 2 in full, --landing own and --landing two: every number as in rounds 17-21; bounded_sync.py 6,264: as round 21; --pre-edit 20,352: 17,772 agree, tip 2,264, B2 316, 0 unexplained; fuzzrename2 (353): 147 exit 4, 205 agree, the same 1 list; fuzzrename (251): 114 exit 4, 135 agree, 2 D1+D2, 0 false; mutants: 21 caught, 3 unpinned. The 337 merged PRs of this repository and Akashic-Library: all 0 at close time; against today's main 5 exit 4 (round 20: 4), about 1 % and 7 % more CPU than round 20; a blobless clone: 60 of 102 exit 4 at close time (round 20: 37, round 21: 71). - Docs: helper header, SPEC (Moves, rows 128-144, small world, history, results), CHANGELOG, README row, plugin.json and marketplace.json, test.sh header. Refs #399
Verify Report — PR #418(第 22 輪,本機 commit
|
| # | Severity | Finding | Source |
|---|---|---|---|
| 1 | HIGH | 退步:git 跳過偵測的 commit 碰到 candidate 的路徑時不走 P0,head 與 tip 整棵樹相同、沒有任何搬移也回 exit 4(第 20、21 輪回 0) | requirements, logic |
| 2 | HIGH | 目錄分兩個 commit 清空時目錄規則不算數,回錯的 3(第 21 輪回 4) | logic |
| 3 | HIGH | 分支的檔由非 first-parent 的 merge 帶進改名目錄時,P0 回錯的 0(第 20、21 輪相同) | logic |
| 4 | HIGH | 退步:複製規則把非搬移算進去(留一份舊檔再改寫原檔的 squash、小檔範本),回 exit 4 並蓋掉真的 orphan | logic |
| 5 | MEDIUM | 退步:格式化大 commit、內容相同的小檔被當成搬移;目錄之後又重建仍算數;replaced() 沒固定 diff 選項;git 2.30 上跳過偵測的計數不符、fixture 246 失敗 |
requirements, logic, security, regression |
| 6 | MEDIUM | SPEC 的封閉偏差清單漏列 Update branch 檢查的兩個限制 | requirements |
| 7 | LOW | 文件細節、replaced() 差一、binary 複製、--load 用 pickle、blobless 的說明 |
多個 |
判斷
第 20 輪之後,第 21、22 輪為抓 shim、搬到既有名字、目錄改名而加的偵測,都在沒有搬移的歷史上造成新的錯誤。範圍問題交由維護者決定。
Round 22 verify found four HIGH, and again answers changed for histories without moves. The maintainer decided on 2026-11-02 to shrink back. - Moves count from -M in non-merge commits and inside the branch's first-parent merges. Copy detection, blob pairing, the directory rule, their counts, the warning check and the P0 guard are gone. - Copies, shims, moves onto existing names and renamed directories are known limits in SPEC; fixtures pin them. - Both Update-branch checks stay. bounded_sync.py drops --save/--load. - Suite 430/430. bounded.py and bounded_sync.py (6,264; --pre-edit 20,352): 0 unexplained. Fuzzers: no wrong exit code. 337 merged PRs: every answer as round 20, same CPU. Refs #399
…isted (#399) Round 23 verify (devil's advocate): the second check cleared a commit that sets a line back to its merge-base value and never landed, after a merge of main; the merged main commit never touched the line. It now counts only when the commit is not already in place at that merge-base. Also: P0's list keeps a newline inside a name; the docs' closed list of limits, and leftovers of rounds 21-22. Fixtures 249, 250. Suite 435/435. Refs #399
) Round 24 verify: the guard missed the check where the branch up to the merge landed, and a set-back hunk mixed with a real edit. The second check now skips any commit with a hunk that only sets lines back to the merge-base's. Repeated text can leave a landed commit listed, the safe direction. Fixture 251. Suite 437/437. Refs #399
Verify Report — PR #418(第 23–25 輪,本機,未推送)
第 25 輪的 hunk 問題沒有兩全的判準:逐行判斷會把分支改寫自己的行誤認為還原,那正是 #399 的症狀。需要維護者決定範圍。 337 個 PR 的重放在第 23 輪與第 20 輪完全相同;第 24 輪之後的重放沒有跑完。 |
No criterion separates a set-back from a branch rewriting its own line. Pending the maintainer's acceptance of the limit. Refs #399
Refs #399
Summary
idd-closeStep 1.55 不再把 squash 合併、行被分支後續 commit 改寫的 commit 誤報成 orphan(PR #408 曾有 39 個)。Gate 回答「分支的內容有沒有落地」:tip 已有 head 的內容就回 0;否則找到分支落地的 commit,逐 commit 就地比對。檔案依路徑讀,不做 merge。Update branch 之後的第二個 PR 不再誤報第一個 PR 的 commit。git 記成搬移且碰到分支檔案的,exit 4(只印 note)。已知限制見 SPEC.md 與 CHANGELOG,由維護者 2026-11-02、2026-11-03 決定。
Verify
第 27 輪無 HIGH,MEDIUM 已修(之後的文件與 partial-clone 檢查順序修正沒有再跑 verify)。suites 65/65,CI 通過,337 個已合併 PR 的答案與第 20 輪相同。