From ef6ab75e45aafef56f9b9b71f0a7a9d0fc391cb3 Mon Sep 17 00:00:00 2001 From: jun0 Date: Fri, 18 Sep 2026 08:48:34 +0900 Subject: [PATCH 1/4] =?UTF-8?q?[rustjava-count-symbols-dropped-from-second?= =?UTF-8?q?-parent-on-resolution]=20test(ci):=20=EC=B6=A9=EB=8F=8C=20?= =?UTF-8?q?=ED=95=B4=EC=86=8C=EA=B0=80=20=EB=96=A8=EC=96=B4=EB=9C=A8?= =?UTF-8?q?=EB=A6=B0=20=EC=A0=95=EC=9D=98=EB=A5=BC=20=EC=84=BC=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 채택 제안 2026-09-17-union-restores-silently-dropped-makeconcat#p0. 제품 코드 0줄 — 검사기 1개 + 의도 규약 + 기존 rust.yml 에 잡 하나(새 워크플로 파일 0). ⓑ 를 먼저 쟀다(이 게이트가 티켓을 끝낼 수 있었다): origin/main 에 손실을 재구성해 기존 축을 전부 돌렸더니 생성기 rc=0 · 단일결함 감사 rc=0(23) · test_class_format 21 · test_fixture_pins 3 · 파리티 rc=0 · git status 0줄 · 충돌 마커 0 ⇒ 아무것도 못 잡는다. ★왜 조용한지를 정확히 알아냈다: 손실은 떨어진 이름들이 서로 자기완결일 때만 조용하고, 충돌의 한쪽을 통째로 택하는 것이 바로 그 자기완결 슬라이스를 떨어뜨리는 것이다. ⓐ 제안의 「3건」을 다시 세니 4건이다(LINKED · MAKECONCAT_DESCRIPTOR · make_concat_call_site · method fieldref). ⓒ 넷째가 클래스 «안»의 메서드라 「최상위」만 보면 4 중 3만 잡는다 ⇒ 한 단계 중첩까지 본다. ⓓ 이 repo 는 --merge 착지지만 겨냥하는 것은 브랜치 내부 base 당김이라 착지 방식과 무관하다(부모 1개 범위 → 0 merge(s), rc=0). 의도/사고 분리는 커밋 trailer 다: Dropped-from-theirs: -- . 자기 커밋 하나만 면제하므로 「영구 목록이 검사기를 무력화한다」가 구조적으로 불가능하다. ★설계를 한 번 갈아엎었다: 초판의 「트리 == 부모1 이면 -s ours 로 보고 건너뛴다」 구조 면제가 개악 시험에서 theirs 를 통째로 버린 머지까지 삼켜 rc=0 이 됐다 — 그게 이 검사기의 존재 이유인 실패다. 면제를 선언(* 와일드카드)으로 교체했다. 양방향 전건: 개악 rc=1 + 이름을 말한다 · 정상 rc=0 · trailer rc=0, 지우면 rc=1 · 스쿼시 rc=0 · 실사고에서 손으로 찾은 것과 같은 분할(생성기 4 사고 + .rs 2 의도)을 기계가 낸다. 오탐을 세었다: 최근 200커밋·머지 83건 소급 → 8건 red · 정의 19개(9.6%) · 4.4초/머지. CI 는 origin/main..HEAD 만 보므로 과거 8건이 main 을 red 로 만들지 않는다. 잃는 것: 이름은 남고 본문이 비어진 경우는 못 잡는다 · 리네임·이동을 소실과 구별 못 한다 · trailer 는 우회로다 · DoD 블록에도 들어가 로컬 실행에도 시간이 붙는다. CI 잡을 늘렸으므로 CLAUDE.md DoD 블록도 같이 고쳤다 — 파리티 락이 명령 7개로 둘 다 일치를 답한다. cargo test --all 583/0/1(불변). --- .github/workflows/rust.yml | 11 ++ CLAUDE.md | 1 + REPORT.md | 15 ++ STATE.md | 7 + ...026-09-18-merge-dropped-symbols-check.json | 31 +++ .../2026-09-18-merge-dropped-symbols-check.md | 106 +++++++++++ scripts/check-merge-dropped-symbols.py | 178 ++++++++++++++++++ 7 files changed, 349 insertions(+) create mode 100644 docs/worklog/2026-09-18-merge-dropped-symbols-check.json create mode 100644 docs/worklog/2026-09-18-merge-dropped-symbols-check.md create mode 100755 scripts/check-merge-dropped-symbols.py diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index ba4932c0..c0523746 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -62,6 +62,17 @@ jobs: - uses: actions/checkout@v7 - run: python3 scripts/check-worklog-json.py + # a conflict resolution that drops one side's work leaves no other trace: no marker, mergeable + # CLEAN, suite green. fetch-depth 0 because the check needs the merge commits and their parents, + # which a shallow checkout does not have. One runner, not the matrix. + merge_drops: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - run: python3 scripts/check-merge-dropped-symbols.py + # parity lock: the CLAUDE.md DoD block must reproduce this file's checks (see the script's # docstring — hand comparison failed five rounds in a row). One runner, not the matrix. dod_parity: diff --git a/CLAUDE.md b/CLAUDE.md index 6ceb79d2..f2231357 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,6 +31,7 @@ cargo test --all python3 scripts/check-worklog-json.py python3 scripts/check-dod-ci-parity.py + python3 scripts/check-merge-dropped-symbols.py ``` ★★**이 블록은 이제 «기계가 지킨다» — `scripts/check-dod-ci-parity.py`(CI job `dod_parity`)가 이 코드블록과 `rust.yml` 을 «각각 파싱해» 대칭차를 낸다.** 어긋나면 그 자리에서 red 다. diff --git a/REPORT.md b/REPORT.md index 5b62ced8..ad6ff456 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,4 +1,19 @@ # REPORT +## [2026-09-18] 충돌 해소가 «한쪽 부모의 정의»를 떨어뜨렸는지 센다 — ★**개악 시험이 내 검사기의 구멍을 잡았다**(rustjava-count-symbols-dropped-from-second-parent-on-resolution) +- 무엇을: 채택 제안 `2026-09-17-union-restores-silently-dropped-makeconcat#p0`(worklog json 기록). ★**제품 코드 0줄** — 검사기 1개 + 의도 규약 + 기존 `rust.yml` 에 잡 하나(**새 워크플로 파일 0**). +- ★**ⓑ 를 먼저 쟀다**(이 게이트가 티켓을 끝낼 수 있었다): `origin/main` 에 손실을 **재구성**해 기존 축 전수 → 생성기 **rc=0** · 단일결함 감사 **rc=0**(23) · `test_class_format` **21** · `test_fixture_pins` **3** · 파리티 **rc=0** · `git status` **0줄** · 충돌 마커 **0** ⇒ ★**아무것도 못 잡는다.** + ★★**왜 조용한지를 정확히 알아냈다** — 손실은 **떨어진 이름들이 서로 자기완결일 때만** 조용하고(자기완결이 아니면 `AttributeError` 로 시끄럽게 죽는다), ★**충돌의 한쪽을 통째로 택하는 것이 바로 그 자기완결 슬라이스를 떨어뜨리는 것**이다. +- ★**ⓐ 「3건」을 다시 세니 «4건»** — `LINKED`·`MAKECONCAT_DESCRIPTOR`·`def make_concat_call_site`·`method fieldref`(두 해소 커밋 각각). +- ★**ⓒ 정의의 범위를 실제 4건에서 거꾸로 정했다**: ★넷째 `fieldref` 가 **`class Pool` 안의 메서드**라 ★**「최상위」만 보면 4 중 3만 잡는다** ⇒ **한 단계 중첩까지** 본다. +- ★**ⓓ**: `rustjava` 는 등재 repo 라 `--merge` 착지(main 머지 **83/200**)지만 ★**겨냥하는 것은 «브랜치 내부 base 당김»**(사고 둘 다 그것 · 최근 **26건**)이라 **착지 방식과 무관**하다. 부모 1개 범위 → `0 merge(s)` **rc=0**. +- ★**의도/사고 분리 = 커밋 trailer** `Dropped-from-theirs: -- ` ⇒ ★**자기 커밋 하나만 면제**하므로 티켓이 경고한 「영구 목록이 검사기를 무력화한다」가 **구조적으로 불가능**하다. +- ★★**설계를 한 번 갈아엎었다**: 초판의 «트리 == 부모1 이면 `-s ours` 로 보고 건너뛴다» 구조 면제가 ★**개악 시험에서 theirs 를 통째로 버린 머지까지 삼켜 rc=0** 이 됐다 — 그게 이 검사기의 존재 이유인 실패다 ⇒ **면제를 «선언»(`*` 와일드카드)으로 교체**. ★양방향을 안 돌렸으면 **끈 채로 착지**했다. +- ★**합격선 양방향 전건**: ⒜개악 **rc=1 + 이름을 말한다** · ⒝정상 **rc=0** · ⒞trailer **rc=0**, 지우면 **rc=1** · ⓓ스쿼시 **rc=0** · ★실사고에서 **손으로 찾은 것과 같은 분할**(생성기 4 사고 + `.rs` 2 의도)을 기계가 낸다. +- ★**오탐을 세었다**: 최근 200커밋·머지 83건 소급 → **8건 red · 정의 19개(9.6%)** · **4.4초/머지** ⇒ PR 당 내부 머지 0~2건이라 **실비용 ≈ 0~9초**. ★CI 는 `origin/main..HEAD` 만 보므로 **과거 8건이 main 을 red 로 만들지 않는다**. +- ★**잃는 것**: ★**이름은 남고 본문이 비어진 경우는 못 잡는다** · 리네임·이동을 소실과 **구별 못 한다** · ★**trailer 는 우회로다**(사유 필수 + 커밋에 남는 것이 유일한 브레이크 — 더 조이지 않은 것이 판단이다) · 머지마다 4.4초가 붙고 **DoD 블록에도 들어가** 로컬에도 붙는다. +- 검증: `cargo test --all` **583 passed / 0 failed / 1 ignored**(불변 — 제품 코드 무접촉) · `check-dod-ci-parity` → **「OK 두 축 모두 대칭차 0 — 명령 7개 · toolchain 2개로 «둘 다 일치»」**(★CI 잡을 늘렸으므로 DoD 블록도 같이 고쳤다). +- ★후속 추천: ⑴소급 8건 중 upstream 동기(S5~S8) 가 떨어뜨린 `decode_str`·`encode_str` 이 «발견인가 수용된 판단인가»를 그 리니지가 판정(S) — 상세 = `docs/worklog/2026-09-18-merge-dropped-symbols-check.md`. + ## [2026-09-17] 코드 2파일 합집합 — ★**그런데 ours 의 «삭제»는 의도가 아니라 선행 머지의 «조용한 롤백»이었다** (rustjava-adopt-link-stringconcatfactory-p2-fix3) - 무엇을: 게이트③이 `code-conflict-out-of-scope` 로 세운 PR #61 의 충돌 4파일(원장 2 + 코드 2)을 합집합으로 해소. ★제품 Rust **0줄**(테스트·픽스처 생성기만). - ★★**브리프의 전제 하나가 반증됐다** — 「ours 가 «의도적으로» 지운 54·16줄을 되살리지 마라」였는데, 두 파일의 성격이 **정반대**였다: diff --git a/STATE.md b/STATE.md index ab8991d0..2b9df1ec 100644 --- a/STATE.md +++ b/STATE.md @@ -7,6 +7,13 @@ (둘 다 이것보다 오래됐고 MERGEABLE/CONFLICTING 처분이 이미 걸려 있다). 겹침은 전부 **append 형 합집합**이라 해소는 기계적이다) ## 완료 +- [rustjava-count-symbols-dropped-from-second-parent-on-resolution] ★★**충돌 해소가 떨어뜨린 정의를 센다 — 그리고 개악 시험이 «내 검사기»의 구멍을 잡았다.** 채택 제안 `2026-09-17-union-restores-silently-dropped-makeconcat#p0`. ★**제품 코드 0줄**(검사기 + 규약 + 기존 워크플로 잡 1). + ★**ⓑ 를 먼저 쟀다** — 손실을 재구성해 기존 축 전수(생성기·단일결함 감사·두 스위트·파리티·`git status`·마커) ⇒ ★**전부 green = 아무것도 못 잡는다.** ★조용한 조건은 「떨어진 이름들이 **자기완결**일 때」이고, 한쪽을 통째로 택하는 것이 정확히 그것이다. + ★**ⓐ 3건 → «4건»** · ★**ⓒ** 넷째가 **클래스 «안»의 메서드**라 「최상위」만 보면 **4 중 3**만 잡는다 ⇒ 한 단계 중첩까지. + ★★**설계 교체**: 「트리==부모1 ⇒ `-s ours` 면제」가 **개악을 삼켜 rc=0** 이 됐다(그게 존재 이유인 실패다) ⇒ **선언(`*` trailer)으로 교체**. + ★양방향 전건: 개악 **rc=1 + 이름** · 정상 **rc=0** · trailer **rc=0**/지우면 **rc=1** · 스쿼시 **rc=0** · 실사고에서 **손 분할과 동일**(4 사고 + 2 의도). + ★**오탐 실측** 83머지 중 **8건(9.6%) · 19정의 · 4.4초/머지** ⇒ PR 당 **0~9초**. ★**잃는 것**: 본문만 빈 경우·리네임을 **못 가른다** · trailer 는 **우회로**다. + ★`--all` **583/0/1**(불변) · 파리티 **명령 7개 일치**(DoD 블록 동기). - [rustjava-adopt-link-stringconcatfactory-p2-fix2] ★★**#57 의 버전 표에 25행 등재 — 「착지 순서」가 만든 부채를 갚는다(PR #61).** ★**막힌 것은 CI 도 충돌도 아니었다**: 핀 `85cf0fba` 에서 rc=0 CI_GREEN · `git merge origin/main` **코드 충돌 0** 인데 ★**합친 결과**가 #57 이 세운 「미등재 픽스처는 핀을 실패시킨다」를 어겼다(미등재 **25건** 재현). diff --git a/docs/worklog/2026-09-18-merge-dropped-symbols-check.json b/docs/worklog/2026-09-18-merge-dropped-symbols-check.json new file mode 100644 index 00000000..00ef862b --- /dev/null +++ b/docs/worklog/2026-09-18-merge-dropped-symbols-check.json @@ -0,0 +1,31 @@ +{ + "date": "2026-09-18", + "taskId": "rustjava-count-symbols-dropped-from-second-parent-on-resolution", + "summary": "A check that reports definitions present in a merge's second parent and absent from its result, wired into the existing rust.yml. The hard part was not counting but separating accident from intent, which is recorded as a per-commit trailer rather than a file so it cannot outlive the merge it excuses. The first design had a structural exemption for merges whose tree equals their first parent's; the mandated mutation test showed that exemption also swallowed the exact failure the check exists for, so it was removed in favour of an explicit wildcard trailer.", + "changes": [ + "scripts/check-merge-dropped-symbols.py: the check, with the intent trailer `Dropped-from-theirs: -- ` and a `*` wildcard for merges that keep our tree on purpose", + ".github/workflows/rust.yml: a merge_drops job on the existing file (no new workflow), fetch-depth 0 because the check needs merge parents", + "CLAUDE.md: the DoD block gains the same command — the parity lock requires CI and DoD to match, and it now reports 7 commands on both sides" + ], + "verification": [ + "PRECONDITION b, the gate that could have ended this ticket: reconstructed the loss on origin/main and ran every existing axis against it. Dropping a self-consistent slice (recipe_arity_call_site + RECIPE_ARITY + its write loop) leaves the generator at rc=0, audit-fixture-single-defect.py at rc=0 (23 checks), test_class_format 21 passed, test_fixture_pins 3 passed, check-dod-ci-parity rc=0, git status empty, zero conflict markers. Nothing catches it, so the gate does not fire", + "the same experiment with a NON-self-consistent slice does go red, but as an AttributeError crash rather than a diagnosis — which is why the silent case is the one that matters and had to be measured separately", + "PRECONDITION a, recount: the two resolutions e53b2142 and 514d5b08 each dropped 4 definitions, not the 3 the proposal states — LINKED, MAKECONCAT_DESCRIPTOR, def make_concat_call_site, method fieldref", + "MUTATION (a): a synthetic merge that discarded the other side's new function is rc=1 and names it — 'def kept_by_theirs — in the merged-in branch, not in the result'", + "NORMAL (b): the same merge resolved as a union is rc=0", + "INTENT (c): adding `Dropped-from-theirs: def kept_by_theirs -- superseded ...` makes it rc=0; removing the trailer returns it to rc=1; the `*` wildcard form is rc=0", + "SQUASH (d): a one-parent commit range reports '0 merge(s)' and rc=0", + "run against the real incident, the check reproduces by machine the same split the -fix3 round found by hand: 4 accidental drops in the generator plus 2 intentional test-function removals in tests/test_class_format.rs", + "cargo test --all: 583 passed / 0 failed / 1 ignored, unchanged — no product code touched", + "check-dod-ci-parity: OK, 7 commands and 2 toolchains matching on both sides after the wiring" + ], + "issues": [ + "FALSE POSITIVE RATE, measured not guessed: over the last 200 commits of origin/main (83 merges) the check flags 8 merges / 19 definitions, about 9.6% of merges, and takes 4.4s per merge (368s for 83). The first and final designs give the same count. A pull request usually contains 0-2 internal merges, so the per-PR cost is roughly 0-9s. CI only walks origin/main..HEAD, so those historical merges are never re-examined and do not turn main red.", + "WHAT IT CANNOT SEE: a definition whose name survives while its body is emptied or gutted. The check compares name sets, so 'still there but no longer does anything' passes. It also cannot tell a genuine rename from a drop-plus-add, and a move between files reads as a drop in the old file.", + "THE TRAILER IS A BYPASS, and deliberately so: anyone in a hurry can write one and move on. The only brake is that a reason is required by the format and the trailer is visible in the merge commit. That is weaker than a gate and stronger than nothing; making it harder was judged over-design for a check with no track record yet.", + "Two of the flagged historical merges are the known incident. The rest are upstream-sync merges (S5-S8) and one `-s ours` ancestry record. The sync ones genuinely dropped upstream methods — decode_str and encode_str are absent from the whole result tree, not moved, which was checked — so whether they are findings or accepted judgements is for the sync lineage to say, not this round." + ], + "adoptedProposals": [ + "2026-09-17-union-restores-silently-dropped-makeconcat#p0" + ] +} diff --git a/docs/worklog/2026-09-18-merge-dropped-symbols-check.md b/docs/worklog/2026-09-18-merge-dropped-symbols-check.md new file mode 100644 index 00000000..aa46ce55 --- /dev/null +++ b/docs/worklog/2026-09-18-merge-dropped-symbols-check.md @@ -0,0 +1,106 @@ +# 2026-09-18 — 충돌 해소가 «한쪽 부모의 정의»를 떨어뜨렸는지 센다 (rustjava-count-symbols-dropped-from-second-parent-on-resolution) + +채택 제안 `2026-09-17-union-restores-silently-dropped-makeconcat#p0`. ★**제품 코드 0줄** — 검사기 1개 + 의도 규약 + 기존 워크플로에 잡 하나. + +## ⓑ 먼저 — 이미 덮는 축이 «없다»는 것을 실행으로 확인했다 + +★**이 게이트가 이 티켓을 끝낼 수 있었으므로 먼저 쟀다.** `origin/main` 에 손실을 **재구성**하고 기존 축을 전부 돌렸다: + +| 축 | 손실 트리에서 | +|---|---| +| 생성기 실행 | **rc=0** | +| `audit-fixture-single-defect.py` | **rc=0**(검사 18 · 무결함 5 · 합계 23) | +| `cargo test --test test_class_format` | **21 passed** | +| `cargo test --test test_fixture_pins` | **3 passed** | +| `check-dod-ci-parity` | **rc=0** | +| `git status -- test-data/indy` | **0줄** | +| 충돌 마커 | **0** | + +⇒ ★**아무것도 잡지 못한다.** 게이트는 발화하지 않는다. + +★★**왜 조용한지를 «정확히» 알아냈다 — 이것이 설계를 정했다**: 손실은 **떨어진 이름들이 서로 자기완결일 때만** 조용하다. +자기완결이 아닌 슬라이스를 떨어뜨리면 `AttributeError` 로 **시끄럽게 죽는다**(그것도 쟀다). +그리고 ★**충돌의 한쪽을 통째로 택하는 것이 바로 «자기완결 슬라이스를 떨어뜨리는 것»**이다. + +## ⓐ 「3건」을 다시 셌다 — ★**4건이다** + +두 해소 커밋 각각에서 base·ours·theirs 의 정의 집합을 세고 `theirs − 결과` 를 냈다: + +``` +e53b2142 (merge-base 4b89f18a) base 14 · ours 20 · theirs 18 · 결과 20 → 떨어진 것 4 +514d5b08 (merge-base 42e99a7e) base 18 · ours 20 · theirs 18 · 결과 20 → 떨어진 것 4 + const LINKED · const MAKECONCAT_DESCRIPTOR · def make_concat_call_site · method fieldref +``` +제안은 **three** 라 적었다. ★**내 정의로는 4**이고, 넷째는 `MAKECONCAT_DESCRIPTOR` 다. ※쓰기 루프는 `if __name__` 안이라 «정의»가 아니어서 안 센다 — 그 한계는 아래에 적는다. + +## ⓒ 「최상위 정의」를 무엇으로 볼 것인가 — ★실제 4건에서 거꾸로 정했다 + +| 떨어진 것 | 계급 | +|---|---| +| `LINKED` · `MAKECONCAT_DESCRIPTOR` | 모듈 수준 대문자 대입 | +| `make_concat_call_site` | 모듈 수준 `def` | +| ★`fieldref` | ★**`class Pool` «안»의 메서드**(들여쓰기 4) | + +⇒ ★★**「최상위」만 보면 4건 중 3건만 잡는다.** 그래서 **한 단계 중첩(메서드)까지** 본다. Rust 도 같은 모양으로 넣었다(`fn`·`type`·`const`·`impl` 안 메서드). + +## ⓓ 스쿼시와의 상호작용 + +`rustjava` 는 `contracts/upstream-sync-repos.conf` **등재**라 **`--merge` 로 착지**한다 ⇒ main 에 **부모2가 있다**(최근 200 커밋 중 머지 **83**). +★**그러나 이 검사기가 겨냥하는 것은 착지가 아니라 «브랜치 내부 base 당김 머지»**이고(사고 둘 다 그것이었다 · 최근 83 중 **26건**), +그것은 **착지 방식과 무관하게** 존재한다. ⇒ **PR 시점에** 검사하므로 스쿼시 repo 에서도 같은 값을 한다. +실측: 부모 1개짜리 범위 → `0 merge(s)` · **rc=0**(조용히 통과하고, 그것이 문서화된 동작이다). + +## 의도와 사고를 가르는 목록 — ★**커밋 trailer 다** + +``` +Dropped-from-theirs: Pool.fieldref -- superseded by the new pool builder, see +Dropped-from-theirs: * -- recorded as a merged ancestor only, our tree is authoritative +``` + +- **어디에**: 그 **머지 커밋 메시지**에. +- **누가 언제 지우는가**: ★**지울 필요가 없다** — trailer 는 **자기 커밋 하나만** 면제한다. + ★티켓이 경고한 「영구 목록은 시간이 지나면 검사기를 무력화한다」가 **구조적으로 불가능**하다(파일이면 항목이 쌓여 검사기를 끈다). +- **왜**: `--` 뒤 사유가 **형식상 필수**다(없으면 매치되지 않아 면제되지 않는다). + +## ★★설계를 한 번 갈아엎었다 — 개악 시험이 «내 검사기»의 구멍을 잡았다 + +초판에는 **구조적 면제**가 있었다: 「결과 트리 == 부모1 트리면 `-s ours` 이니 건너뛴다」. +★**개악 시험이 그것을 무너뜨렸다** — theirs 를 통째로 버린 합성 머지도 **트리가 부모1과 같아서** +★**면제에 삼켜져 rc=0 이 됐다.** 그런데 그게 **이 검사기가 존재하는 이유인 바로 그 실패**다. +⇒ ★**구조적 면제를 걷어내고 «선언»(`*` 와일드카드)으로 바꿨다.** `-s ours` 도 이제 **말해야** 한다. +★이 교체가 이 회차에서 가장 값한 측정이다 — 양방향을 안 돌렸으면 **끈 채로 착지**했다. + +## 합격선 — 양방향 전건 + +| 축 | 결과 | +|---|---| +| ⒜ theirs 정의를 버린 해소 | ★**rc=1** · `def kept_by_theirs — in the merged-in branch, not in the result` (**이름을 말한다**) | +| ⒝ 아무것도 안 떨어뜨린 합집합 해소 | **rc=0** | +| ⒞ trailer 등재 | **rc=0** · ★**지우면 다시 rc=1** · `*` 와일드카드도 **rc=0** | +| ⓓ 스쿼시(부모 1개) | `0 merge(s)` · **rc=0** | +| 실사고 재현 | ★**손으로 찾은 것과 같은 분할을 기계가 낸다** — 생성기 **4건(사고)** + `.rs` **2건(의도)** | + +## ⒟ 오탐 — ★**세었다**(「적을 것이다」로 적지 않는다) + +최근 **200커밋 · 머지 83건**에 소급: **8건 red · 정의 19개**(머지의 **9.6%**) · **4.4초/머지**(83건 368초). +★**초판과 최종판이 같은 수를 낸다** — 구조적 면제를 걷어내 `-s ours` 1건이 다시 잡히지만, 그 1건은 애초에 면제 대상이었다. +★**그리고 CI 는 이 범위를 돌지 않는다** — `origin/main..HEAD`(= PR 범위)만 본다. 과거 머지는 재검사되지 않으므로 이 8건이 main 을 red 로 만들지 않는다. +⇒ PR 하나에 내부 머지는 보통 **0~2건**이라 **실비용 ≈ 0~9초**. + +★**그 8건의 성격을 확인했다**(추측하지 않았다): 2건은 **알려진 사고** · 1건은 `-s ours` 조상 기록 · +나머지는 **upstream 동기(S5~S8)** 인데, ★`decode_str`·`encode_str` 은 **결과 트리 어디에도 없다**(이동이 아니라 실제 소실 — 직접 확인). +⇒ ★**그것이 «발견»인지 «수용된 판단»인지는 그 리니지가 답할 몫**이고 이 회차가 정하지 않는다. + +## ★잃는 것 — 「없다」로 적지 않는다 + +- ★**이름은 남고 «본문이 비어진» 경우는 못 잡는다.** 집합 비교라 「있긴 한데 아무것도 안 한다」는 통과한다. +- ★**리네임·파일 이동을 소실과 구별하지 못한다** — 옛 경로에서는 «떨어진 것»으로 보인다. +- ★**trailer 는 우회로다.** 급하면 누구나 적고 지나간다. 브레이크는 「사유가 형식상 필수」와 「커밋에 남아 보인다」뿐이다. + ★**더 조이지 않았다** — 실적 없는 검사기에 과설계라고 판단했다(그 판단 자체가 대가다). +- ★**머지마다 4.4초가 붙는다** · DoD 블록에도 들어가 **로컬 실행에도** 붙는다. + +## CI 배선 + +새 워크플로 파일 **0** — `rust.yml` 에 `merge_drops` 잡을 얹었다(`fetch-depth: 0` — 얕은 체크아웃에는 머지 부모가 없다). +★**파리티 락이 같이 움직인다**: `python3` 스텝은 DoD 블록에도 있어야 하므로 `CLAUDE.md` DoD 블록에 같은 줄을 넣었고, +`check-dod-ci-parity` 가 **「명령 7개 · toolchain 2개로 둘 다 일치」**로 답한다. diff --git a/scripts/check-merge-dropped-symbols.py b/scripts/check-merge-dropped-symbols.py new file mode 100755 index 00000000..2067574f --- /dev/null +++ b/scripts/check-merge-dropped-symbols.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +"""Report definitions that were in the branch a merge pulled from, and are not in its result. + +What this answers: *did a conflict resolution silently drop one side's work*. That failure is +invisible to everything else we have, which is why it exists. Measured 2026-09-18 by reconstructing +the loss on `origin/main` and running every axis against it: the generator ran (rc=0), the +single-defect auditor passed (23 checks, rc=0), `test_class_format` passed (21), `test_fixture_pins` +passed (3), `check-dod-ci-parity` passed, `git status` was empty and there was no conflict marker. +Nothing went red. The only signal was counting names by hand, which is what this does instead. + +The real incident: two merges on one PR branch (e53b2142, 514d5b08) each dropped four definitions +that came from the branch being merged in -- `Pool.fieldref`, `MAKECONCAT_DESCRIPTOR`, +`make_concat_call_site` and the `LINKED` table. The suite stayed green because the fixtures those +build are committed bytes, so when the generator lost the code the expected output did not move. + +Why it is silent, precisely: the loss is invisible exactly when the dropped names are *consistent +with each other*. Dropping a slice that something else still calls raises AttributeError and gets +noticed; dropping a whole feature -- which is what taking one side of a conflict does -- leaves a +file that parses, runs and agrees with every committed artefact. Both were measured. + +Scope: a definition is flagged when it exists in the merge's second parent and not in the merge +result. That deliberately also flags *our* intentional deletions, because from the outside the two +look identical -- which is the whole difficulty. Saying which is which is a judgement, so it is +recorded as one, on the merge commit: + + Dropped-from-theirs: Pool.fieldref -- superseded by the new pool builder, see + +One trailer per name, a reason after `--`, and both are required. The list lives in the merge +commit message rather than in a file on purpose: a file accumulates entries that outlive the merge +they excused and quietly turns the check off, while a trailer can only ever excuse the one commit +it is written on. + +Usage: + check-merge-dropped-symbols.py [] # default: origin/main..HEAD + +Exit: 0 nothing dropped, or everything dropped is accounted for by a trailer + 1 something was dropped and not accounted for + 2 could not run (bad range, no git) + +`-s ours` merges -- "record these upstream cuts as ancestors" -- drop the whole other side on +purpose, and one trailer per name would mean hundreds. They use the wildcard form instead: + + Dropped-from-theirs: * -- recorded as a merged ancestor only, our tree is authoritative + +There is deliberately no structural exemption for them. The obvious one, "skip a merge whose tree +equals its first parent's", was written and then removed: it is true of `-s ours` *and* of a +resolution that took our side wholesale, and the second of those is the exact failure this exists +to catch. Measured -- with that exemption in place, a synthetic merge that discarded the other +side's new function passed. So the wholesale case has to be declared rather than inferred. + +A squash landing has one parent, so there is nothing for this to compare and it reports nothing. +That is not a gap: the merges this catches are the base-pulls *inside* a branch, and those are +checked while the pull request is open, whichever way the branch is eventually landed. +""" + +import re +import subprocess +import sys + +# Both languages the repo builds fixtures and code in. The method patterns matter as much as the +# top-level ones: of the four definitions the real incident dropped, one (`fieldref`) was a method +# inside `class Pool`, so a check that only looked at column zero would have found three of four. +PATTERNS = { + ".py": [ + (re.compile(r"^def ([A-Za-z_]\w*)"), "def {}"), + (re.compile(r"^class ([A-Za-z_]\w*)"), "class {}"), + (re.compile(r"^([A-Z][A-Z0-9_]*)\s*="), "{}"), + (re.compile(r"^ def ([A-Za-z_]\w*)"), "method {}"), + ], + ".rs": [ + (re.compile(r"^(?:pub(?:\([\w:]+\))?\s+)?(?:async\s+)?fn ([A-Za-z_]\w*)"), "fn {}"), + (re.compile(r"^(?:pub(?:\([\w:]+\))?\s+)?(?:struct|enum|trait|union) ([A-Za-z_]\w*)"), "type {}"), + (re.compile(r"^(?:pub(?:\([\w:]+\))?\s+)?(?:const|static) ([A-Za-z_]\w*)"), "const {}"), + (re.compile(r"^ (?:pub(?:\([\w:]+\))?\s+)?(?:async\s+)?fn ([A-Za-z_]\w*)"), "method {}"), + ], +} + + +def run(*args): + result = subprocess.run(["git", *args], capture_output=True, text=True) + return result.stdout if result.returncode == 0 else None + + +def symbols(rev, path): + """The definitions `path` holds at `rev`, or None if it is not a file we read.""" + for suffix, patterns in PATTERNS.items(): + if path.endswith(suffix): + break + else: + return None + blob = run("show", f"{rev}:{path}") + if blob is None: + return set() + found = set() + for line in blob.split("\n"): + for pattern, shape in patterns: + match = pattern.match(line) + if match: + found.add(shape.format(match.group(1))) + break + return found + + +def excused(merge): + """Names the merge commit itself says were dropped on purpose, with a reason.""" + message = run("log", "-1", "--format=%B", merge) or "" + names = set() + for line in message.split("\n"): + match = re.match(r"^Dropped-from-theirs:\s*(.+?)\s+--\s+(\S.*)$", line.strip()) + if match: + names.add(match.group(1).strip()) + return names + + +def excuses_everything(accounted): + """`Dropped-from-theirs: * -- ` covers a merge that keeps our tree on purpose.""" + return "*" in accounted + + +def check(merge): + """(findings, files_examined) for one merge commit. findings is a list of (path, name).""" + parents = (run("rev-list", "--parents", "-n", "1", merge) or "").split() + if len(parents) < 3: + return [], 0 # not a merge: a squash or an ordinary commit has nothing to compare + ours, theirs = parents[1], parents[2] + base = (run("merge-base", ours, theirs) or "").strip() + if not base: + return [], 0 + # Only files the merged-in branch actually changed can have lost its work. + changed = (run("diff", "--name-only", base, theirs) or "").split("\n") + findings = [] + examined = 0 + accounted = excused(merge) + if excuses_everything(accounted): + return [], 0 + for path in filter(None, changed): + theirs_symbols = symbols(theirs, path) + if theirs_symbols is None: + continue + examined += 1 + for name in sorted(theirs_symbols - symbols(merge, path)): + if name not in accounted: + findings.append((path, name)) + return findings, examined + + +def main(): + rev_range = sys.argv[1] if len(sys.argv) > 1 else "origin/main..HEAD" + merges = run("rev-list", "--merges", rev_range) + if merges is None: + print(f"cannot read {rev_range}", file=sys.stderr) + return 2 + + merges = [m for m in merges.split("\n") if m] + total = 0 + for merge in merges: + findings, examined = check(merge) + subject = (run("log", "-1", "--format=%s", merge) or "").strip() + if findings: + print(f"✗ {merge[:8]} {subject[:60]}") + for path, name in findings: + print(f" {path}: {name} — in the merged-in branch, not in the result") + total += len(findings) + else: + print(f" ✓ {merge[:8]} ({examined} file(s) examined)") + + print(f"{len(merges)} merge(s) in {rev_range}: {total} definition(s) dropped without a trailer") + if total: + print( + "If a drop was deliberate, say so on the merge commit:\n" + " Dropped-from-theirs: -- ", + file=sys.stderr, + ) + return 1 if total else 0 + + +if __name__ == "__main__": + sys.exit(main()) From 6b719e70644abc03bfaa9836dc81cbac813a4255 Mon Sep 17 00:00:00 2001 From: jun0 Date: Fri, 18 Sep 2026 11:17:14 +0900 Subject: [PATCH 2/4] =?UTF-8?q?[rustjava-count-symbols-dropped-from-second?= =?UTF-8?q?-parent-on-resolution-fix]=20test(ci):=20=ED=95=84=ED=84=B0?= =?UTF-8?q?=EB=A5=BC=20=EB=84=93=ED=9E=88=EA=B3=A0=20=EB=A9=B4=EC=A0=9C=20?= =?UTF-8?q?=EC=98=88=EC=8B=9C=EB=A5=BC=20=C2=AB=EC=9E=91=EB=8F=99=ED=95=98?= =?UTF-8?q?=EB=8A=94=C2=BB=20=ED=98=95=EC=8B=9D=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EA=B3=A0=EC=B9=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 게이트② 반려 2건. 제품 코드 0행. ① 필터가 좁아 «둘째 사고»를 삼켰다. 초판은 theirs 가 건드린 파일만 봤고 그 근거로 「only files the merged-in branch actually changed can have lost its work」라 적었는데, 이 repo 자신의 둘째 사고가 그 문장을 반증한다 — 514d5b08 은 e53b2142 와 같은 넷을 떨어뜨렸는데 초판은 「0 file(s) examined」 rc=0 으로 통과시켰다. 해소가 «상대가 건드리지도 않은» 파일을 되돌리는 것이야말로 이 검사기의 존재 이유인데 그것을 못 봤다. ⇒ 술어를 「theirs 가 바꾼 파일 ∪ 머지가 바꾼 파일」로 넓혔다. 이제 두 사고 둘 다 rc=1 · 6정의다. ★좁힌 채 두지 않은 이유: 좁은 필터를 정당화하던 문장이 거짓이므로 그대로 두면 「없는 보장을 있다고 적는」 형태가 된다 — 이 리니지가 #67 의 F1 에서 정확히 그것으로 반려됐다. 대가는 숨기지 않았다: 83머지 소급 8건/19정의 → 10건/45정의(게이트② 실측 · 다시 재지 않았다). 늘어난 것에 진짜 사고 1건이 들어 있지만 읽을 것과 trailer 도 는다. Scope 문단에 그대로 적었다. ② docstring 의 유일한 작동 예시가 작동하지 않았다. `Pool.fieldref` 라 적혀 있는데 검사기는 `method fieldref` 를 내보내고 «글자 그대로» 대조한다 ⇒ 예시대로 적은 사람은 면제에 실패한다. 합성 머지로 양방향 확인했다: 출력에서 복사한 이름은 rc=0, 예쁜 형식은 rc=1. 예시를 출력 형식으로 고치고 ★「이름은 검사기 출력에서 글자 그대로 복사해야 한다」를 계약에 박았다 — 예시만 고치면 다음 형식이 또 갈린다. 전 이력 1,286커밋에 trailer 0건이라 깨진 기존 등재는 없다. 재확인: 스쿼시(부모 1개) rc=0 · 정상 머지 rc=0 · 이 PR 범위 rc=0 · DoD 문서축 전건 rc=0. --- REPORT.md | 4 ++- STATE.md | 3 +- ...026-09-18-merge-dropped-symbols-check.json | 11 ++++--- .../2026-09-18-merge-dropped-symbols-check.md | 25 ++++++++++++++- scripts/check-merge-dropped-symbols.py | 32 ++++++++++++++----- 5 files changed, 60 insertions(+), 15 deletions(-) diff --git a/REPORT.md b/REPORT.md index ad6ff456..f6b5d53c 100644 --- a/REPORT.md +++ b/REPORT.md @@ -9,7 +9,9 @@ - ★**의도/사고 분리 = 커밋 trailer** `Dropped-from-theirs: -- ` ⇒ ★**자기 커밋 하나만 면제**하므로 티켓이 경고한 「영구 목록이 검사기를 무력화한다」가 **구조적으로 불가능**하다. - ★★**설계를 한 번 갈아엎었다**: 초판의 «트리 == 부모1 이면 `-s ours` 로 보고 건너뛴다» 구조 면제가 ★**개악 시험에서 theirs 를 통째로 버린 머지까지 삼켜 rc=0** 이 됐다 — 그게 이 검사기의 존재 이유인 실패다 ⇒ **면제를 «선언»(`*` 와일드카드)으로 교체**. ★양방향을 안 돌렸으면 **끈 채로 착지**했다. - ★**합격선 양방향 전건**: ⒜개악 **rc=1 + 이름을 말한다** · ⒝정상 **rc=0** · ⒞trailer **rc=0**, 지우면 **rc=1** · ⓓ스쿼시 **rc=0** · ★실사고에서 **손으로 찾은 것과 같은 분할**(생성기 4 사고 + `.rs` 2 의도)을 기계가 낸다. -- ★**오탐을 세었다**: 최근 200커밋·머지 83건 소급 → **8건 red · 정의 19개(9.6%)** · **4.4초/머지** ⇒ PR 당 내부 머지 0~2건이라 **실비용 ≈ 0~9초**. ★CI 는 `origin/main..HEAD` 만 보므로 **과거 8건이 main 을 red 로 만들지 않는다**. +- ★★**[게이트② 승계] 필터가 너무 좁아 «둘째 사고»를 놓쳤다 — 넓혔다.** 초판은 「theirs 가 건드린 파일만」 봤고 그 근거 문장이 **거짓**이었다(`514d5b08` 이 같은 넷을 떨어뜨렸는데 ★**`0 file(s) examined` rc=0**). ⇒ 술어를 **「theirs 가 바꾼 파일 ∪ 머지가 바꾼 파일」**로 넓혀 ★**두 사고 «둘 다» rc=1**. ★좁힌 채 두면 「없는 보장을 있다고 적는」 형태가 된다(#67 F1 과 같은 형태). +- ★**오탐을 세었다**: 최근 200커밋·머지 83건 소급 → 초판 **8건/19정의** · ★**확장본 10건/45정의**(게이트② 실측) · **4.4초/머지** ⇒ PR 당 내부 머지 0~2건이라 **실비용 ≈ 0~9초**. ★늘어난 것에 **진짜 사고 1건이 들어 있다**. +- ★**trailer 이름은 «출력에서 복사»해야 한다** — `method fieldref` 는 통하고 `Pool.fieldref` 는 **안 통한다**. ★초판 docstring 의 유일한 예시가 **안 통하는 쪽**이었다(게이트② 지적) ⇒ 예시를 고치고 **「글자 그대로 같아야 한다」를 계약에 박았다**. ※당시 전 이력 trailer **0건**이라 깨진 등재는 없다. ★CI 는 `origin/main..HEAD` 만 보므로 **과거 8건이 main 을 red 로 만들지 않는다**. - ★**잃는 것**: ★**이름은 남고 본문이 비어진 경우는 못 잡는다** · 리네임·이동을 소실과 **구별 못 한다** · ★**trailer 는 우회로다**(사유 필수 + 커밋에 남는 것이 유일한 브레이크 — 더 조이지 않은 것이 판단이다) · 머지마다 4.4초가 붙고 **DoD 블록에도 들어가** 로컬에도 붙는다. - 검증: `cargo test --all` **583 passed / 0 failed / 1 ignored**(불변 — 제품 코드 무접촉) · `check-dod-ci-parity` → **「OK 두 축 모두 대칭차 0 — 명령 7개 · toolchain 2개로 «둘 다 일치»」**(★CI 잡을 늘렸으므로 DoD 블록도 같이 고쳤다). - ★후속 추천: ⑴소급 8건 중 upstream 동기(S5~S8) 가 떨어뜨린 `decode_str`·`encode_str` 이 «발견인가 수용된 판단인가»를 그 리니지가 판정(S) — 상세 = `docs/worklog/2026-09-18-merge-dropped-symbols-check.md`. diff --git a/STATE.md b/STATE.md index 2b9df1ec..8b5a1054 100644 --- a/STATE.md +++ b/STATE.md @@ -12,7 +12,8 @@ ★**ⓐ 3건 → «4건»** · ★**ⓒ** 넷째가 **클래스 «안»의 메서드**라 「최상위」만 보면 **4 중 3**만 잡는다 ⇒ 한 단계 중첩까지. ★★**설계 교체**: 「트리==부모1 ⇒ `-s ours` 면제」가 **개악을 삼켜 rc=0** 이 됐다(그게 존재 이유인 실패다) ⇒ **선언(`*` trailer)으로 교체**. ★양방향 전건: 개악 **rc=1 + 이름** · 정상 **rc=0** · trailer **rc=0**/지우면 **rc=1** · 스쿼시 **rc=0** · 실사고에서 **손 분할과 동일**(4 사고 + 2 의도). - ★**오탐 실측** 83머지 중 **8건(9.6%) · 19정의 · 4.4초/머지** ⇒ PR 당 **0~9초**. ★**잃는 것**: 본문만 빈 경우·리네임을 **못 가른다** · trailer 는 **우회로**다. + ★★**[게이트② 승계] 필터가 좁아 «둘째 사고»(`514d5b08`)를 놓쳤다** — 근거 문장이 거짓이었다(`0 file(s) examined` rc=0) ⇒ **「theirs 가 바꾼 파일 ∪ 머지가 바꾼 파일」**로 넓혀 ★**둘 다 rc=1**. ★docstring 의 유일한 예시가 **면제되지 않는 형식**이었다 ⇒ 고치고 「출력에서 복사하라」를 계약에 박았다(당시 trailer 0건). + ★**오탐 실측** 83머지 중 초판 **8건/19정의** → 확장본 **10건/45정의**(게이트② 실측) · **4.4초/머지** ⇒ PR 당 **0~9초**. ★**잃는 것**: 본문만 빈 경우·리네임을 **못 가른다** · trailer 는 **우회로**이고 **이름이 글자 그대로 대조**된다. ★`--all` **583/0/1**(불변) · 파리티 **명령 7개 일치**(DoD 블록 동기). - [rustjava-adopt-link-stringconcatfactory-p2-fix2] ★★**#57 의 버전 표에 25행 등재 — 「착지 순서」가 만든 부채를 갚는다(PR #61).** ★**막힌 것은 CI 도 충돌도 아니었다**: 핀 `85cf0fba` 에서 rc=0 CI_GREEN · `git merge origin/main` **코드 충돌 0** 인데 diff --git a/docs/worklog/2026-09-18-merge-dropped-symbols-check.json b/docs/worklog/2026-09-18-merge-dropped-symbols-check.json index 00ef862b..7881ae67 100644 --- a/docs/worklog/2026-09-18-merge-dropped-symbols-check.json +++ b/docs/worklog/2026-09-18-merge-dropped-symbols-check.json @@ -3,7 +3,7 @@ "taskId": "rustjava-count-symbols-dropped-from-second-parent-on-resolution", "summary": "A check that reports definitions present in a merge's second parent and absent from its result, wired into the existing rust.yml. The hard part was not counting but separating accident from intent, which is recorded as a per-commit trailer rather than a file so it cannot outlive the merge it excuses. The first design had a structural exemption for merges whose tree equals their first parent's; the mandated mutation test showed that exemption also swallowed the exact failure the check exists for, so it was removed in favour of an explicit wildcard trailer.", "changes": [ - "scripts/check-merge-dropped-symbols.py: the check, with the intent trailer `Dropped-from-theirs: -- ` and a `*` wildcard for merges that keep our tree on purpose", + "scripts/check-merge-dropped-symbols.py: the check, with the intent trailer `Dropped-from-theirs: -- ` and a `*` wildcard for merges that keep our tree on purpose. The file filter covers what the merged-in branch changed and what the merge changed; the trailer name is matched literally against the check's printed form", ".github/workflows/rust.yml: a merge_drops job on the existing file (no new workflow), fetch-depth 0 because the check needs merge parents", "CLAUDE.md: the DoD block gains the same command — the parity lock requires CI and DoD to match, and it now reports 7 commands on both sides" ], @@ -17,13 +17,16 @@ "SQUASH (d): a one-parent commit range reports '0 merge(s)' and rc=0", "run against the real incident, the check reproduces by machine the same split the -fix3 round found by hand: 4 accidental drops in the generator plus 2 intentional test-function removals in tests/test_class_format.rs", "cargo test --all: 583 passed / 0 failed / 1 ignored, unchanged — no product code touched", - "check-dod-ci-parity: OK, 7 commands and 2 toolchains matching on both sides after the wiring" + "check-dod-ci-parity: OK, 7 commands and 2 toolchains matching on both sides after the wiring", + "GATE 2 found the filter too narrow and this round widened it. The original looked only at files the merged-in branch changed, on the stated grounds that 'only files the merged-in branch actually changed can have lost its work' — which is false, and the repo's own second incident proves it: 514d5b08 dropped the same four definitions as e53b2142, and the narrow filter reported '0 file(s) examined', rc=0. The predicate is now every file that either the merged-in branch or the merge itself changed, and both incidents report rc=1 with the same six names.", + "The docstring's only worked example did not work. It wrote `Dropped-from-theirs: Pool.fieldref` while the check prints `method fieldref` and matches literally, so anyone following the example got no exemption. Verified both ways on a synthetic merge: the name copied from the output exempts (rc=0), the prettier form does not (rc=1). The example now uses the printed form and the docstring states that the name must be copied from the check's own output character for character." ], "issues": [ - "FALSE POSITIVE RATE, measured not guessed: over the last 200 commits of origin/main (83 merges) the check flags 8 merges / 19 definitions, about 9.6% of merges, and takes 4.4s per merge (368s for 83). The first and final designs give the same count. A pull request usually contains 0-2 internal merges, so the per-PR cost is roughly 0-9s. CI only walks origin/main..HEAD, so those historical merges are never re-examined and do not turn main red.", + "FALSE POSITIVE RATE, measured not guessed: over the last 200 commits of origin/main (83 merges) the check as first written flagged 8 merges / 19 definitions; after gate 2 widened the file filter it flags 10 / 45 (gate-2 measurement, not re-run here). The extra includes 514d5b08, a real incident the narrow version missed, so the増 is not purely noise — but more of it will need trailers, and takes 4.4s per merge (368s for 83). The first and final designs give the same count. A pull request usually contains 0-2 internal merges, so the per-PR cost is roughly 0-9s. CI only walks origin/main..HEAD, so those historical merges are never re-examined and do not turn main red.", "WHAT IT CANNOT SEE: a definition whose name survives while its body is emptied or gutted. The check compares name sets, so 'still there but no longer does anything' passes. It also cannot tell a genuine rename from a drop-plus-add, and a move between files reads as a drop in the old file.", "THE TRAILER IS A BYPASS, and deliberately so: anyone in a hurry can write one and move on. The only brake is that a reason is required by the format and the trailer is visible in the merge commit. That is weaker than a gate and stronger than nothing; making it harder was judged over-design for a check with no track record yet.", - "Two of the flagged historical merges are the known incident. The rest are upstream-sync merges (S5-S8) and one `-s ours` ancestry record. The sync ones genuinely dropped upstream methods — decode_str and encode_str are absent from the whole result tree, not moved, which was checked — so whether they are findings or accepted judgements is for the sync lineage to say, not this round." + "Two of the flagged historical merges are the known incident. The rest are upstream-sync merges (S5-S8) and one `-s ours` ancestry record. The sync ones genuinely dropped upstream methods — decode_str and encode_str are absent from the whole result tree, not moved, which was checked — so whether they are findings or accepted judgements is for the sync lineage to say, not this round.", + "The exemption name is matched literally against the check's own output, which is a sharp edge: `method fieldref` works and `Pool.fieldref` does not. The docstring now says so and its example uses the printed form, but nothing verifies a trailer's name is well-formed — a typo still reads as 'not exempted' rather than as 'you spelled it wrong'. There were 0 trailers anywhere in the repo's 1,286 commits when this changed, so nothing existing broke." ], "adoptedProposals": [ "2026-09-17-union-restores-silently-dropped-makeconcat#p0" diff --git a/docs/worklog/2026-09-18-merge-dropped-symbols-check.md b/docs/worklog/2026-09-18-merge-dropped-symbols-check.md index aa46ce55..4f89f199 100644 --- a/docs/worklog/2026-09-18-merge-dropped-symbols-check.md +++ b/docs/worklog/2026-09-18-merge-dropped-symbols-check.md @@ -80,9 +80,28 @@ Dropped-from-theirs: * -- recorded as a merged ancestor only, our tree is author | ⓓ 스쿼시(부모 1개) | `0 merge(s)` · **rc=0** | | 실사고 재현 | ★**손으로 찾은 것과 같은 분할을 기계가 낸다** — 생성기 **4건(사고)** + `.rs` **2건(의도)** | +## ★★게이트② 가 «필터가 너무 좁다»를 잡았다 — 넓혔다 + +초판은 **theirs 가 건드린 파일만** 봤고, 그 근거로 「`Only files the merged-in branch actually changed can have +lost its work`」라 적었다. ★**그 문장이 거짓이고, 이 repo 자신의 «둘째» 사고가 반증한다**: +`514d5b08` 은 `e53b2142` 와 **같은 넷**을 떨어뜨렸는데 초판 검사기는 ★**`0 file(s) examined` · rc=0** 으로 통과시켰다. +⇒ ★**해소가 «상대가 건드리지도 않은» 파일을 되돌리는 것** — 「A 충돌 풀다가 B 를 되돌려 놨다」 — 이 검사기의 +**존재 이유 그대로의 실패**인데 못 봤다. + +★**술어를 「theirs 가 바꾼 파일 ∪ 머지가 바꾼 파일」로 넓혔다.** 결과: +| 머지 | 초판 | 확장본 | +|---|---|---| +| `e53b2142` | rc=1 · 6정의 | rc=1 · 6정의 | +| ★`514d5b08` | ★**rc=0**(놓침) | ★**rc=1 · 6정의**(같은 넷 + 의도 2) | + +★**왜 «좁힌 채 두지» 않았나**: 좁은 필터를 정당화하던 문장이 **거짓**이므로, 그대로 두면 +★**「없는 보장을 있다고 적는」 형태**가 된다 — 이 리니지가 `#67` 의 F1 에서 정확히 그것으로 반려됐다. + ## ⒟ 오탐 — ★**세었다**(「적을 것이다」로 적지 않는다) -최근 **200커밋 · 머지 83건**에 소급: **8건 red · 정의 19개**(머지의 **9.6%**) · **4.4초/머지**(83건 368초). +최근 **200커밋 · 머지 83건**에 소급: 초판 **8건 red · 정의 19개**(9.6%) · +★**확장본 10건 red · 정의 45개**(게이트② 실측 · 이 회차가 다시 재지 않았다) · **4.4초/머지**(83건 368초). +★**늘어난 것이 «전부 노이즈»는 아니다** — 거기 `514d5b08`(진짜 사고)이 들어 있다. ★그러나 **읽을 것이 늘고 trailer 도 늘어난다.** ★**초판과 최종판이 같은 수를 낸다** — 구조적 면제를 걷어내 `-s ours` 1건이 다시 잡히지만, 그 1건은 애초에 면제 대상이었다. ★**그리고 CI 는 이 범위를 돌지 않는다** — `origin/main..HEAD`(= PR 범위)만 본다. 과거 머지는 재검사되지 않으므로 이 8건이 main 을 red 로 만들지 않는다. ⇒ PR 하나에 내부 머지는 보통 **0~2건**이라 **실비용 ≈ 0~9초**. @@ -95,6 +114,10 @@ Dropped-from-theirs: * -- recorded as a merged ancestor only, our tree is author - ★**이름은 남고 «본문이 비어진» 경우는 못 잡는다.** 집합 비교라 「있긴 한데 아무것도 안 한다」는 통과한다. - ★**리네임·파일 이동을 소실과 구별하지 못한다** — 옛 경로에서는 «떨어진 것»으로 보인다. +- ★**trailer 이름이 «글자 그대로» 대조된다** — `method fieldref` 는 통하고 `Pool.fieldref` 는 **안 통한다**. + ★초판 docstring 의 **유일한 작동 예시가 그 «안 통하는» 쪽**이어서, 따라 적은 사람은 면제에 실패했을 것이다(게이트② 지적). + ★고쳤고 「**출력에서 복사하라**」를 계약에 박았지만, ★**오타를 「틀렸다」가 아니라 「면제 안 됨」으로 읽는 성질은 남는다.** + ※바꿀 당시 repo 전 이력 **1,286커밋에 trailer 0건** ⇒ 깨진 기존 등재는 **없다**. - ★**trailer 는 우회로다.** 급하면 누구나 적고 지나간다. 브레이크는 「사유가 형식상 필수」와 「커밋에 남아 보인다」뿐이다. ★**더 조이지 않았다** — 실적 없는 검사기에 과설계라고 판단했다(그 판단 자체가 대가다). - ★**머지마다 4.4초가 붙는다** · DoD 블록에도 들어가 **로컬 실행에도** 붙는다. diff --git a/scripts/check-merge-dropped-symbols.py b/scripts/check-merge-dropped-symbols.py index 2067574f..bc9d85f9 100755 --- a/scripts/check-merge-dropped-symbols.py +++ b/scripts/check-merge-dropped-symbols.py @@ -19,16 +19,25 @@ file that parses, runs and agrees with every committed artefact. Both were measured. Scope: a definition is flagged when it exists in the merge's second parent and not in the merge -result. That deliberately also flags *our* intentional deletions, because from the outside the two +result, looked for in every file that either the merged-in branch or the merge itself changed. +That second half costs something, measured over the last 200 commits of origin/main (83 merges) at +gate 2: the narrow filter reports 8 merges / 19 definitions, this one reports 10 / 45. The extra +includes 514d5b08, which is the second of the two real incidents, so the wider set is not simply +noise -- but it is more to read, and more of it will need trailers. That deliberately also flags *our* intentional deletions, because from the outside the two look identical -- which is the whole difficulty. Saying which is which is a judgement, so it is recorded as one, on the merge commit: - Dropped-from-theirs: Pool.fieldref -- superseded by the new pool builder, see + Dropped-from-theirs: method fieldref -- superseded by the new pool builder, see -One trailer per name, a reason after `--`, and both are required. The list lives in the merge -commit message rather than in a file on purpose: a file accumulates entries that outlive the merge -they excused and quietly turns the check off, while a trailer can only ever excuse the one commit -it is written on. +One trailer per name, a reason after `--`, and both are required. THE NAME MUST BE COPIED FROM THIS +CHECK'S OWN OUTPUT, character for character -- it is matched literally, so `method fieldref` works +and `Pool.fieldref` does not, even though the second reads better. Run the check, copy the name it +prints after the colon, paste it. (An earlier version of this docstring used the prettier form as its +only worked example, which meant anyone who followed it got no exemption and lost a round.) + +The list lives in the merge commit message rather than in a file on purpose: a file accumulates +entries that outlive the merge they excused and quietly turns the check off, while a trailer can +only ever excuse the one commit it is written on. Usage: check-merge-dropped-symbols.py [] # default: origin/main..HEAD @@ -126,8 +135,15 @@ def check(merge): base = (run("merge-base", ours, theirs) or "").strip() if not base: return [], 0 - # Only files the merged-in branch actually changed can have lost its work. - changed = (run("diff", "--name-only", base, theirs) or "").split("\n") + # Both what the merged-in branch touched and what the merge itself touched. Restricting this to + # the first set was the original shape and it was wrong: a resolution can revert a file the other + # branch never touched -- "fixed the conflict in A and put B back" -- and that is this check's + # whole reason for existing. Measured on the second of the two incidents, 514d5b08: with the + # narrow filter it reports "0 file(s) examined" and passes; with this one it names the same four + # definitions the first incident dropped. The cost is real and is recorded in Scope below. + changed = set((run("diff", "--name-only", base, theirs) or "").split("\n")) | set( + (run("diff", "--name-only", base, merge) or "").split("\n") + ) findings = [] examined = 0 accounted = excused(merge) From 290ad15af2e39a19097c66497af3421852a93752 Mon Sep 17 00:00:00 2001 From: jun0 Date: Fri, 18 Sep 2026 15:47:54 +0900 Subject: [PATCH 3/4] =?UTF-8?q?[rustjava-count-symbols-dropped-from-second?= =?UTF-8?q?-parent-on-resolution-fix2]=20docs:=2045=20=EB=A5=BC=20?= =?UTF-8?q?=EB=82=B4=20=EA=B5=AC=ED=98=84=EC=97=90=EC=84=9C=20=EB=8B=A4?= =?UTF-8?q?=EC=8B=9C=20=EC=9E=AC=2026=20=EC=9C=BC=EB=A1=9C=20=EA=B3=A0?= =?UTF-8?q?=EC=B9=98=EA=B3=A0=20=EB=8A=98=EC=96=B4=EB=82=9C=207=EC=A0=95?= =?UTF-8?q?=EC=9D=98=EB=A5=BC=20=ED=95=98=EB=82=98=EC=94=A9=20=ED=8C=90?= =?UTF-8?q?=EC=A0=95=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- REPORT.md | 6 +-- STATE.md | 2 +- ...026-09-18-merge-dropped-symbols-check.json | 2 +- .../2026-09-18-merge-dropped-symbols-check.md | 42 +++++++++++++++---- scripts/check-merge-dropped-symbols.py | 23 +++++++--- 5 files changed, 56 insertions(+), 19 deletions(-) diff --git a/REPORT.md b/REPORT.md index 0c062677..24b279a3 100644 --- a/REPORT.md +++ b/REPORT.md @@ -10,9 +10,9 @@ - ★★**설계를 한 번 갈아엎었다**: 초판의 «트리 == 부모1 이면 `-s ours` 로 보고 건너뛴다» 구조 면제가 ★**개악 시험에서 theirs 를 통째로 버린 머지까지 삼켜 rc=0** 이 됐다 — 그게 이 검사기의 존재 이유인 실패다 ⇒ **면제를 «선언»(`*` 와일드카드)으로 교체**. ★양방향을 안 돌렸으면 **끈 채로 착지**했다. - ★**합격선 양방향 전건**: ⒜개악 **rc=1 + 이름을 말한다** · ⒝정상 **rc=0** · ⒞trailer **rc=0**, 지우면 **rc=1** · ⓓ스쿼시 **rc=0** · ★실사고에서 **손으로 찾은 것과 같은 분할**(생성기 4 사고 + `.rs` 2 의도)을 기계가 낸다. - ★★**[게이트② 승계] 필터가 너무 좁아 «둘째 사고»를 놓쳤다 — 넓혔다.** 초판은 「theirs 가 건드린 파일만」 봤고 그 근거 문장이 **거짓**이었다(`514d5b08` 이 같은 넷을 떨어뜨렸는데 ★**`0 file(s) examined` rc=0**). ⇒ 술어를 **「theirs 가 바꾼 파일 ∪ 머지가 바꾼 파일」**로 넓혀 ★**두 사고 «둘 다» rc=1**. ★좁힌 채 두면 「없는 보장을 있다고 적는」 형태가 된다(#67 F1 과 같은 형태). -- ★**오탐을 세었다**: 최근 200커밋·머지 83건 소급 → 초판 **8건/19정의** · ★**확장본 10건/45정의**(게이트② 실측) · **4.4초/머지** ⇒ PR 당 내부 머지 0~2건이라 **실비용 ≈ 0~9초**. ★늘어난 것에 **진짜 사고 1건이 들어 있다**. -- ★**trailer 이름은 «출력에서 복사»해야 한다** — `method fieldref` 는 통하고 `Pool.fieldref` 는 **안 통한다**. ★초판 docstring 의 유일한 예시가 **안 통하는 쪽**이었다(게이트② 지적) ⇒ 예시를 고치고 **「글자 그대로 같아야 한다」를 계약에 박았다**. ※당시 전 이력 trailer **0건**이라 깨진 등재는 없다. ★CI 는 `origin/main..HEAD` 만 보므로 **과거 8건이 main 을 red 로 만들지 않는다**. -- ★**잃는 것**: ★**이름은 남고 본문이 비어진 경우는 못 잡는다** · 리네임·이동을 소실과 **구별 못 한다** · ★**trailer 는 우회로다**(사유 필수 + 커밋에 남는 것이 유일한 브레이크 — 더 조이지 않은 것이 판단이다) · 머지마다 4.4초가 붙고 **DoD 블록에도 들어가** 로컬에도 붙는다. +- ★**오탐을 세었다 — ★이 회차가 «직접» 돌린 값이다**(인용 아님 · 창 `38b0df38..8c7b473f` = 83머지): 초판 **8건/19정의** → ★**확장본 10건/26정의**. ★**늘어난 것은 26이 아니라 «7정의»**(`514d5b08` 6 + `37ea5a13` 1)이고, ★**7 중 진짜 소실 4 · 오탐 3**(아래). ★**시간은 대가가 아니다** — 같은 창에서 확장본이 초판보다 **느리지 않았다**(종전 「4.4초/머지」는 넓힘의 비용이 아니다). +- ★**trailer 이름은 «출력에서 복사»해야 한다** — `method fieldref` 는 통하고 `Pool.fieldref` 는 **안 통한다**. ★초판 docstring 의 유일한 예시가 **안 통하는 쪽**이었다(게이트② 지적) ⇒ 예시를 고치고 **「글자 그대로 같아야 한다」를 계약에 박았다**. ※당시 전 이력 trailer **0건**이라 깨진 등재는 없다. ★CI 는 `origin/main..HEAD` 만 보므로 **과거 10건이 main 을 red 로 만들지 않는다** — ★**다만 그것을 «대가가 작다»의 근거로 쓰지 마라**: 새 대가는 정확히 그 PR 범위(다음 base 당김)에서 난다(아래 「잃는 것」). +- ★**잃는 것**: ★**이름은 남고 본문이 비어진 경우는 못 잡는다** · 리네임·이동을 소실과 **구별 못 한다** · ★**trailer 는 우회로다**(사유 필수 + 커밋에 남는 것이 유일한 브레이크 — 더 조이지 않은 것이 판단이다) · ★★**새 red 계급 — main 에도 있는 정의를 지우거나 개명한 브랜치는 «다음 base 당김»에서 red 가 되고, 평범한 리팩터가 trailer 를 달아야 한다**(실례 `37ea5a13` — `at_most_one_bootstrap_methods_attribute` 를 일반화해 치운 브랜치가 base 를 당기자 그 이름이 «떨어졌다»로 잡혔다. ★늘어난 7정의 중 **3이 이 계급**이다) · DoD 블록에 들어가 **로컬 실행에도** 붙는다. - 검증: `cargo test --all` **583 passed / 0 failed / 1 ignored**(불변 — 제품 코드 무접촉) · `check-dod-ci-parity` → **「OK 두 축 모두 대칭차 0 — 명령 7개 · toolchain 2개로 «둘 다 일치»」**(★CI 잡을 늘렸으므로 DoD 블록도 같이 고쳤다). - ★후속 추천: ⑴소급 8건 중 upstream 동기(S5~S8) 가 떨어뜨린 `decode_str`·`encode_str` 이 «발견인가 수용된 판단인가»를 그 리니지가 판정(S) — 상세 = `docs/worklog/2026-09-18-merge-dropped-symbols-check.md`. diff --git a/STATE.md b/STATE.md index 53bf4c6f..b621eef2 100644 --- a/STATE.md +++ b/STATE.md @@ -13,7 +13,7 @@ ★★**설계 교체**: 「트리==부모1 ⇒ `-s ours` 면제」가 **개악을 삼켜 rc=0** 이 됐다(그게 존재 이유인 실패다) ⇒ **선언(`*` trailer)으로 교체**. ★양방향 전건: 개악 **rc=1 + 이름** · 정상 **rc=0** · trailer **rc=0**/지우면 **rc=1** · 스쿼시 **rc=0** · 실사고에서 **손 분할과 동일**(4 사고 + 2 의도). ★★**[게이트② 승계] 필터가 좁아 «둘째 사고»(`514d5b08`)를 놓쳤다** — 근거 문장이 거짓이었다(`0 file(s) examined` rc=0) ⇒ **「theirs 가 바꾼 파일 ∪ 머지가 바꾼 파일」**로 넓혀 ★**둘 다 rc=1**. ★docstring 의 유일한 예시가 **면제되지 않는 형식**이었다 ⇒ 고치고 「출력에서 복사하라」를 계약에 박았다(당시 trailer 0건). - ★**오탐 실측** 83머지 중 초판 **8건/19정의** → 확장본 **10건/45정의**(게이트② 실측) · **4.4초/머지** ⇒ PR 당 **0~9초**. ★**잃는 것**: 본문만 빈 경우·리네임을 **못 가른다** · trailer 는 **우회로**이고 **이름이 글자 그대로 대조**된다. + ★**오탐 실측 — 이 회차가 직접 돌렸다**(창 `38b0df38..8c7b473f` 83머지): 초판 **8건/19정의** → 확장본 **10건/26정의**. ★늘어난 것은 **7정의**(`514d5b08` 6 + `37ea5a13` 1)이고 ★**진짜 4 · 오탐 3**. ★**시간은 대가가 아니다**(확장본이 더 느리지 않았다). ★**잃는 것**: 본문만 빈 경우·리네임을 **못 가른다** · trailer 는 **우회로**이고 **이름이 글자 그대로 대조**된다 · ★★**새 red 계급 — main 에도 있는 정의를 지운 브랜치는 다음 base 당김에서 red 가 된다**(실례 `37ea5a13` · 7 중 3). ★`--all` **583/0/1**(불변) · 파리티 **명령 7개 일치**(DoD 블록 동기). - [rustjava-adopt-loadable-bootstrap-arguments-diagnostic] ★★**대전제 ⓒ 에서 끝났다 — 그 일을 하는 축(PR #67)이 이미 떠 있다.** 채택 제안 `2026-09-17-loadable-bootstrap-arguments#p0`(worklog json 기록). ★**코드 0행.** ★**전제는 참**(CLI 실행: 세 규칙이 전부 `ClassFormatError: Invalid class file` 동일 문면) — 그러나 ★**편집 영역은 전부 겹친다**(전달된 값은 **3 중 1** — 아래): #67 이 제안 `target` 두 파일을 고치고, 이 술어에 **이미 사유를 주며**, 밋밋한 문면 **두 자리**를 둘 다 고쳤다. ★제안 `tradeoff` 자신이 「두 번 하지 말고 함께 하라」고 적었다. diff --git a/docs/worklog/2026-09-18-merge-dropped-symbols-check.json b/docs/worklog/2026-09-18-merge-dropped-symbols-check.json index 7881ae67..013b0a41 100644 --- a/docs/worklog/2026-09-18-merge-dropped-symbols-check.json +++ b/docs/worklog/2026-09-18-merge-dropped-symbols-check.json @@ -22,7 +22,7 @@ "The docstring's only worked example did not work. It wrote `Dropped-from-theirs: Pool.fieldref` while the check prints `method fieldref` and matches literally, so anyone following the example got no exemption. Verified both ways on a synthetic merge: the name copied from the output exempts (rc=0), the prettier form does not (rc=1). The example now uses the printed form and the docstring states that the name must be copied from the check's own output character for character." ], "issues": [ - "FALSE POSITIVE RATE, measured not guessed: over the last 200 commits of origin/main (83 merges) the check as first written flagged 8 merges / 19 definitions; after gate 2 widened the file filter it flags 10 / 45 (gate-2 measurement, not re-run here). The extra includes 514d5b08, a real incident the narrow version missed, so the増 is not purely noise — but more of it will need trailers, and takes 4.4s per merge (368s for 83). The first and final designs give the same count. A pull request usually contains 0-2 internal merges, so the per-PR cost is roughly 0-9s. CI only walks origin/main..HEAD, so those historical merges are never re-examined and do not turn main red.", + "FALSE POSITIVE RATE, measured by this round by running both versions over one window of 83 merges (38b0df38..8c7b473f, the last 200 commits of origin/main at the time): the check as first written flags 8 merges / 19 definitions; the widened file filter flags 10 / 26. An earlier draft of this file said 45; that number was wrong and never came out of this implementation. The narrow version reproducing 19 exactly, in the same run, is what makes the measurement trustworthy. The seven extra definitions come from two merges, 514d5b08 (six) and 37ea5a13 (one), and they split four to three: four are the second of the two real incidents, restored later and present in the tree today; three are a branch having removed a definition main still had (two tests deleted on purpose by 0f517567, one predicate generalised by 1da1379e). So the honest rate is 4 of 7 found and 3 of 7 false, not 6 and 1. The widened version was no slower than the narrow one in that window, so time is not the cost of widening; reading is. A pull request usually contains 0-2 internal merges. CI only walks origin/main..HEAD, so historical merges are never re-examined and do not turn main red -- but that is not a reason to call the cost small, because the new cost lands in exactly that range, on the next base pull of an open branch.", "WHAT IT CANNOT SEE: a definition whose name survives while its body is emptied or gutted. The check compares name sets, so 'still there but no longer does anything' passes. It also cannot tell a genuine rename from a drop-plus-add, and a move between files reads as a drop in the old file.", "THE TRAILER IS A BYPASS, and deliberately so: anyone in a hurry can write one and move on. The only brake is that a reason is required by the format and the trailer is visible in the merge commit. That is weaker than a gate and stronger than nothing; making it harder was judged over-design for a check with no track record yet.", "Two of the flagged historical merges are the known incident. The rest are upstream-sync merges (S5-S8) and one `-s ours` ancestry record. The sync ones genuinely dropped upstream methods — decode_str and encode_str are absent from the whole result tree, not moved, which was checked — so whether they are findings or accepted judgements is for the sync lineage to say, not this round.", diff --git a/docs/worklog/2026-09-18-merge-dropped-symbols-check.md b/docs/worklog/2026-09-18-merge-dropped-symbols-check.md index 4f89f199..3458774c 100644 --- a/docs/worklog/2026-09-18-merge-dropped-symbols-check.md +++ b/docs/worklog/2026-09-18-merge-dropped-symbols-check.md @@ -99,14 +99,27 @@ lost its work`」라 적었다. ★**그 문장이 거짓이고, 이 repo 자신 ## ⒟ 오탐 — ★**세었다**(「적을 것이다」로 적지 않는다) -최근 **200커밋 · 머지 83건**에 소급: 초판 **8건 red · 정의 19개**(9.6%) · -★**확장본 10건 red · 정의 45개**(게이트② 실측 · 이 회차가 다시 재지 않았다) · **4.4초/머지**(83건 368초). -★**늘어난 것이 «전부 노이즈»는 아니다** — 거기 `514d5b08`(진짜 사고)이 들어 있다. ★그러나 **읽을 것이 늘고 trailer 도 늘어난다.** -★**초판과 최종판이 같은 수를 낸다** — 구조적 면제를 걷어내 `-s ours` 1건이 다시 잡히지만, 그 1건은 애초에 면제 대상이었다. -★**그리고 CI 는 이 범위를 돌지 않는다** — `origin/main..HEAD`(= PR 범위)만 본다. 과거 머지는 재검사되지 않으므로 이 8건이 main 을 red 로 만들지 않는다. -⇒ PR 하나에 내부 머지는 보통 **0~2건**이라 **실비용 ≈ 0~9초**. - -★**그 8건의 성격을 확인했다**(추측하지 않았다): 2건은 **알려진 사고** · 1건은 `-s ours` 조상 기록 · +★★**이 회차가 «직접» 두 판본을 같은 창에 돌렸다**(인용 아님 · 창 `38b0df38..8c7b473f` = **83머지**): +초판(`ef6ab75e`) **8건 red · 정의 19개** ↔ ★**확장본(이 PR) 10건 red · 정의 26개**. +★**앵커** — 같은 스크립트·같은 창에서 초판이 **19 를 글자 그대로** 낸다(측정기가 옳다는 증거). +★**늘어난 것은 «26»이 아니라 «7정의»다** — `514d5b08` **6** + `37ea5a13` **1**. 그게 넓힘의 실제 크기다. +★★**그 7의 성격을 하나씩 판정했다 — 진짜 소실 «4» · 오탐 «3»**(아래 표). 종전 판까지 적혀 있던 **45** 는 **틀린 수**였다. +★**시간은 대가가 아니다** — 같은 창·같은 부하에서 **확장본이 초판보다 느리지 않았다**. +종전 「4.4초/머지」는 두 판본 다에 대해 낡았고, ★**넓힘의 비용처럼 읽히게 두면 안 된다**(비용은 시간이 아니라 «읽을 것»이다). +★**CI 는 `origin/main..HEAD`(= PR 범위)만 본다** — 과거 10건이 main 을 red 로 만들지는 않는다. +★★**그러나 그것을 «대가가 작다»의 근거로 쓰지 마라** — 새 대가는 정확히 그 PR 범위에서, 다음 base 당김에 난다(아래 「잃는 것」). + +| 늘어난 정의 | 머지 | 판정 | 근거(내 실측) | +|---|---|---|---| +| `make_indy_fixtures.py: LINKED` · `MAKECONCAT_DESCRIPTOR` · `def make_concat_call_site` · `method fieldref` | `514d5b08` | ★**진짜 소실 4** | 네 이름 모두 **현재 main 에 실재**(`-p2-fix3` 회차가 되살렸다) ⇒ 머지가 떨어뜨린 것이 맞다 | +| `test_class_format.rs: fn test_lambda_class_reports_unsupported_feature_not_malformed` · `fn test_only_the_string_concat_bootstrap_is_linked` | `514d5b08` | ★**오탐 2** | `git log -S`: 브랜치 커밋 **`0f517567`**(`…-p2` LambdaMetafactory 링크)이 **의도적으로** 지웠다 — 링크가 되니 그 두 단정이 거짓이 됐다. 현재 main 에도 **없다** | +| `validation.rs: fn at_most_one_bootstrap_methods_attribute` | `37ea5a13` | ★**오탐 1** | `git log -S`: `f2c83174` 도입 → 브랜치 **`1da1379e`** 가 `at_most_one_of_each_single_class_attribute` 로 **일반화해 치웠다**. 일반화판은 현재 main 에 **실재** | + +⇒ ★**정의 기준 4/7 이 발견 · 3/7 이 오탐.** ★**검수 회신이 제안한 「6/7 발견 · 1/7 오탐」은 내 실측과 다르다** — +검수자는 `37ea5a13` 1건만 표본으로 봤고, ★**`514d5b08` 안의 «테스트 2건»이 같은 계급**이라는 것은 전수로 봐야 나온다. +⇒ PR 하나에 내부 머지는 보통 **0~2건**이라 실행 비용은 여전히 작다 — ★**큰 것은 «오탐 비율»이다.** + +★**초판이 무는 8건의 성격을 확인했다**(추측하지 않았다 · 위 표의 «늘어난 2건»과 별개다): 2건은 **알려진 사고** · 1건은 `-s ours` 조상 기록 · 나머지는 **upstream 동기(S5~S8)** 인데, ★`decode_str`·`encode_str` 은 **결과 트리 어디에도 없다**(이동이 아니라 실제 소실 — 직접 확인). ⇒ ★**그것이 «발견»인지 «수용된 판단»인지는 그 리니지가 답할 몫**이고 이 회차가 정하지 않는다. @@ -120,7 +133,18 @@ lost its work`」라 적었다. ★**그 문장이 거짓이고, 이 repo 자신 ※바꿀 당시 repo 전 이력 **1,286커밋에 trailer 0건** ⇒ 깨진 기존 등재는 **없다**. - ★**trailer 는 우회로다.** 급하면 누구나 적고 지나간다. 브레이크는 「사유가 형식상 필수」와 「커밋에 남아 보인다」뿐이다. ★**더 조이지 않았다** — 실적 없는 검사기에 과설계라고 판단했다(그 판단 자체가 대가다). -- ★**머지마다 4.4초가 붙는다** · DoD 블록에도 들어가 **로컬 실행에도** 붙는다. +- ★★**새 red 계급 — main 에도 있는 정의를 지우거나 개명한 브랜치는 «다음 base 당김»에서 red 가 되고, + 평범한 리팩터가 `Dropped-from-theirs:` trailer 를 달아야 한다.** 실례 = **`37ea5a13`**(커밋 메시지 자신이 + 「충돌은 `STATE.md`·`REPORT.md` 두 곳뿐 · 제품 코드 0」이라 적는데, 그 base 당김이 + `at_most_one_bootstrap_methods_attribute` 를 «떨어졌다»로 잡는다 — 브랜치가 **먼저 일반화해 치웠기** 때문이다). + ★**늘어난 7정의 중 «3»이 이 계급**이라 예외가 아니라 **계급**이다. + ★**처분은 ⒜(trailer 로 면제)** 다 — ⒝(술어 좁히기)는 이 검사기의 존재 이유인 **초발 소실을 다시 놓치고** + (docstring 이 이미 그 면제를 넣었다 빼 본 기록을 갖고 있다), ⒞(방치)는 **남의 PR 에 비용을 떠넘긴다**. + ★**대가**: 브랜치 주인이 «자기가 지운 것»을 trailer 로 선언해야 한다(이름은 **출력에서 글자 그대로 복사**). + ※**오늘 실제로 red 인 살아 있는 PR 은 0건**이다(열린 PR = 이 PR 하나 · 그 범위는 rc=0) — ★그러나 그것은 + «오늘의 값»이지 계급의 소멸이 아니다. +- ★**머지마다 몇 초가 붙는다** · DoD 블록에도 들어가 **로컬 실행에도** 붙는다. + ★단 **넓힘이 그 시간을 늘리지는 않았다**(위 ⒟ — 확장본이 초판보다 느리지 않았다). ## CI 배선 diff --git a/scripts/check-merge-dropped-symbols.py b/scripts/check-merge-dropped-symbols.py index bc9d85f9..3f2ff621 100755 --- a/scripts/check-merge-dropped-symbols.py +++ b/scripts/check-merge-dropped-symbols.py @@ -20,10 +20,23 @@ Scope: a definition is flagged when it exists in the merge's second parent and not in the merge result, looked for in every file that either the merged-in branch or the merge itself changed. -That second half costs something, measured over the last 200 commits of origin/main (83 merges) at -gate 2: the narrow filter reports 8 merges / 19 definitions, this one reports 10 / 45. The extra -includes 514d5b08, which is the second of the two real incidents, so the wider set is not simply -noise -- but it is more to read, and more of it will need trailers. That deliberately also flags *our* intentional deletions, because from the outside the two +That second half costs something. Measured by running both filters over one window of 83 merges +(38b0df38..8c7b473f, the last 200 commits of origin/main at the time): the narrow filter reports +8 merges / 19 definitions, this one reports 10 / 26. The seven extra definitions come from two +merges -- 514d5b08 (six) and 37ea5a13 (one) -- and they are not all one kind. Four are the second +of the two real incidents, restored later and present in the tree today. The other three are the +class described next. Time is not the cost: in that window the wider filter was no slower than the +narrow one, so the price is reading, not waiting. + +A branch that deletes or renames a definition main still has goes red on its next base pull, and an +ordinary refactor then has to carry a trailer to say so. 37ea5a13 is exactly that: it pulled +origin/main into a branch that had already generalised `at_most_one_bootstrap_methods_attribute` +into `at_most_one_of_each_single_class_attribute`, and the pull reported the old name as dropped. +Three of the seven extra definitions are this class, so it is a class and not a corner. "CI only +looks at origin/main..HEAD" is not a reason to discount it -- that range is precisely where this +lands, on the next base pull of an open branch. + +That deliberately also flags *our* intentional deletions, because from the outside the two look identical -- which is the whole difficulty. Saying which is which is a judgement, so it is recorded as one, on the merge commit: @@ -140,7 +153,7 @@ def check(merge): # branch never touched -- "fixed the conflict in A and put B back" -- and that is this check's # whole reason for existing. Measured on the second of the two incidents, 514d5b08: with the # narrow filter it reports "0 file(s) examined" and passes; with this one it names the same four - # definitions the first incident dropped. The cost is real and is recorded in Scope below. + # definitions the first incident dropped. The cost is real and is recorded in Scope above. changed = set((run("diff", "--name-only", base, theirs) or "").split("\n")) | set( (run("diff", "--name-only", base, merge) or "").split("\n") ) From c7d9ab2274de663af4112a1e617bcc22e4b394aa Mon Sep 17 00:00:00 2001 From: jun0 Date: Fri, 18 Sep 2026 18:05:00 +0900 Subject: [PATCH 4/4] =?UTF-8?q?[rustjava-count-symbols-dropped-from-second?= =?UTF-8?q?-parent-on-resolution-fix3]=20docs:=20=E3=80=8C0=20file(s)=20ex?= =?UTF-8?q?amined=E3=80=8D=EB=A5=BC=20=EC=8B=A4=EC=B8=A1=204=20=EB=A1=9C?= =?UTF-8?q?=20=EA=B3=A0=EC=B9=98=EA=B3=A0=20=E3=80=8C=EB=8A=90=EB=A6=AC?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EC=95=98=EB=8B=A4=E3=80=8D=EB=A5=BC=20?= =?UTF-8?q?=EB=85=B8=EC=9D=B4=EC=A6=88=20=EC=84=9C=EC=88=A0=EB=A1=9C=20?= =?UTF-8?q?=EB=B0=94=EA=BE=BC=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- REPORT.md | 4 ++-- STATE.md | 4 ++-- .../2026-09-18-merge-dropped-symbols-check.json | 4 ++-- .../2026-09-18-merge-dropped-symbols-check.md | 15 +++++++++++---- scripts/check-merge-dropped-symbols.py | 16 +++++++++++----- 5 files changed, 28 insertions(+), 15 deletions(-) diff --git a/REPORT.md b/REPORT.md index 24b279a3..ab5b2150 100644 --- a/REPORT.md +++ b/REPORT.md @@ -9,8 +9,8 @@ - ★**의도/사고 분리 = 커밋 trailer** `Dropped-from-theirs: -- ` ⇒ ★**자기 커밋 하나만 면제**하므로 티켓이 경고한 「영구 목록이 검사기를 무력화한다」가 **구조적으로 불가능**하다. - ★★**설계를 한 번 갈아엎었다**: 초판의 «트리 == 부모1 이면 `-s ours` 로 보고 건너뛴다» 구조 면제가 ★**개악 시험에서 theirs 를 통째로 버린 머지까지 삼켜 rc=0** 이 됐다 — 그게 이 검사기의 존재 이유인 실패다 ⇒ **면제를 «선언»(`*` 와일드카드)으로 교체**. ★양방향을 안 돌렸으면 **끈 채로 착지**했다. - ★**합격선 양방향 전건**: ⒜개악 **rc=1 + 이름을 말한다** · ⒝정상 **rc=0** · ⒞trailer **rc=0**, 지우면 **rc=1** · ⓓ스쿼시 **rc=0** · ★실사고에서 **손으로 찾은 것과 같은 분할**(생성기 4 사고 + `.rs` 2 의도)을 기계가 낸다. -- ★★**[게이트② 승계] 필터가 너무 좁아 «둘째 사고»를 놓쳤다 — 넓혔다.** 초판은 「theirs 가 건드린 파일만」 봤고 그 근거 문장이 **거짓**이었다(`514d5b08` 이 같은 넷을 떨어뜨렸는데 ★**`0 file(s) examined` rc=0**). ⇒ 술어를 **「theirs 가 바꾼 파일 ∪ 머지가 바꾼 파일」**로 넓혀 ★**두 사고 «둘 다» rc=1**. ★좁힌 채 두면 「없는 보장을 있다고 적는」 형태가 된다(#67 F1 과 같은 형태). -- ★**오탐을 세었다 — ★이 회차가 «직접» 돌린 값이다**(인용 아님 · 창 `38b0df38..8c7b473f` = 83머지): 초판 **8건/19정의** → ★**확장본 10건/26정의**. ★**늘어난 것은 26이 아니라 «7정의»**(`514d5b08` 6 + `37ea5a13` 1)이고, ★**7 중 진짜 소실 4 · 오탐 3**(아래). ★**시간은 대가가 아니다** — 같은 창에서 확장본이 초판보다 **느리지 않았다**(종전 「4.4초/머지」는 넓힘의 비용이 아니다). +- ★★**[게이트② 승계] 필터가 너무 좁아 «둘째 사고»를 놓쳤다 — 넓혔다.** 초판은 「theirs 가 건드린 파일만」 봤고 그 근거 문장이 **거짓**이었다 — `514d5b08` 이 같은 넷을 떨어뜨렸는데 초판은 ★**파일 «4개»를 보고도 findings 0 · rc=0** 으로 통과시켰다(확장본은 같은 머지에서 **13개**를 보고 그 넷을 짚는다 · 세 판본에 `check()` 를 직접 불러 잰 값). ⇒ 술어를 **「theirs 가 바꾼 파일 ∪ 머지가 바꾼 파일」**로 넓혀 ★**두 사고 «둘 다» rc=1**. ★좁힌 채 두면 「없는 보장을 있다고 적는」 형태가 된다(#67 F1 과 같은 형태). +- ★**오탐을 세었다 — ★이 회차가 «직접» 돌린 값이다**(인용 아님 · 창 `38b0df38..8c7b473f` = 83머지): 초판 **8건/19정의** → ★**확장본 10건/26정의**. ★**늘어난 것은 26이 아니라 «7정의»**(`514d5b08` 6 + `37ea5a13` 1)이고, ★**7 중 진짜 소실 4 · 오탐 3**(아래). ★★**시간은 「느리지 않았다」가 아니라 «유의한 증가 없음»이다** — 같은 창에서 **초판만 3회 독립 실행**이 **229s/286.8s/585.3s(2.6배)** 로 흔들리는 반면 판본 차이는 **0.8s/머지**다(수 출처 = 게이트² 검수 실측 · ★이 회차는 다시 재지 않았다). ★wide 의 파일 집합이 narrow 의 **상위집합**이라 **더 싸질 수는 없다**. 종전 「4.4초/머지」 삭제는 **유지**한다. - ★**trailer 이름은 «출력에서 복사»해야 한다** — `method fieldref` 는 통하고 `Pool.fieldref` 는 **안 통한다**. ★초판 docstring 의 유일한 예시가 **안 통하는 쪽**이었다(게이트② 지적) ⇒ 예시를 고치고 **「글자 그대로 같아야 한다」를 계약에 박았다**. ※당시 전 이력 trailer **0건**이라 깨진 등재는 없다. ★CI 는 `origin/main..HEAD` 만 보므로 **과거 10건이 main 을 red 로 만들지 않는다** — ★**다만 그것을 «대가가 작다»의 근거로 쓰지 마라**: 새 대가는 정확히 그 PR 범위(다음 base 당김)에서 난다(아래 「잃는 것」). - ★**잃는 것**: ★**이름은 남고 본문이 비어진 경우는 못 잡는다** · 리네임·이동을 소실과 **구별 못 한다** · ★**trailer 는 우회로다**(사유 필수 + 커밋에 남는 것이 유일한 브레이크 — 더 조이지 않은 것이 판단이다) · ★★**새 red 계급 — main 에도 있는 정의를 지우거나 개명한 브랜치는 «다음 base 당김»에서 red 가 되고, 평범한 리팩터가 trailer 를 달아야 한다**(실례 `37ea5a13` — `at_most_one_bootstrap_methods_attribute` 를 일반화해 치운 브랜치가 base 를 당기자 그 이름이 «떨어졌다»로 잡혔다. ★늘어난 7정의 중 **3이 이 계급**이다) · DoD 블록에 들어가 **로컬 실행에도** 붙는다. - 검증: `cargo test --all` **583 passed / 0 failed / 1 ignored**(불변 — 제품 코드 무접촉) · `check-dod-ci-parity` → **「OK 두 축 모두 대칭차 0 — 명령 7개 · toolchain 2개로 «둘 다 일치»」**(★CI 잡을 늘렸으므로 DoD 블록도 같이 고쳤다). diff --git a/STATE.md b/STATE.md index b621eef2..b0cb1cdc 100644 --- a/STATE.md +++ b/STATE.md @@ -12,8 +12,8 @@ ★**ⓐ 3건 → «4건»** · ★**ⓒ** 넷째가 **클래스 «안»의 메서드**라 「최상위」만 보면 **4 중 3**만 잡는다 ⇒ 한 단계 중첩까지. ★★**설계 교체**: 「트리==부모1 ⇒ `-s ours` 면제」가 **개악을 삼켜 rc=0** 이 됐다(그게 존재 이유인 실패다) ⇒ **선언(`*` trailer)으로 교체**. ★양방향 전건: 개악 **rc=1 + 이름** · 정상 **rc=0** · trailer **rc=0**/지우면 **rc=1** · 스쿼시 **rc=0** · 실사고에서 **손 분할과 동일**(4 사고 + 2 의도). - ★★**[게이트② 승계] 필터가 좁아 «둘째 사고»(`514d5b08`)를 놓쳤다** — 근거 문장이 거짓이었다(`0 file(s) examined` rc=0) ⇒ **「theirs 가 바꾼 파일 ∪ 머지가 바꾼 파일」**로 넓혀 ★**둘 다 rc=1**. ★docstring 의 유일한 예시가 **면제되지 않는 형식**이었다 ⇒ 고치고 「출력에서 복사하라」를 계약에 박았다(당시 trailer 0건). - ★**오탐 실측 — 이 회차가 직접 돌렸다**(창 `38b0df38..8c7b473f` 83머지): 초판 **8건/19정의** → 확장본 **10건/26정의**. ★늘어난 것은 **7정의**(`514d5b08` 6 + `37ea5a13` 1)이고 ★**진짜 4 · 오탐 3**. ★**시간은 대가가 아니다**(확장본이 더 느리지 않았다). ★**잃는 것**: 본문만 빈 경우·리네임을 **못 가른다** · trailer 는 **우회로**이고 **이름이 글자 그대로 대조**된다 · ★★**새 red 계급 — main 에도 있는 정의를 지운 브랜치는 다음 base 당김에서 red 가 된다**(실례 `37ea5a13` · 7 중 3). + ★★**[게이트② 승계] 필터가 좁아 «둘째 사고»(`514d5b08`)를 놓쳤다** — 근거 문장이 거짓이었다 — 초판은 **파일 4개를 보고도** findings 0 · rc=0(확장본은 같은 머지에서 **13개**) ⇒ **「theirs 가 바꾼 파일 ∪ 머지가 바꾼 파일」**로 넓혀 ★**둘 다 rc=1**. ★docstring 의 유일한 예시가 **면제되지 않는 형식**이었다 ⇒ 고치고 「출력에서 복사하라」를 계약에 박았다(당시 trailer 0건). + ★**오탐 실측 — 이 회차가 직접 돌렸다**(창 `38b0df38..8c7b473f` 83머지): 초판 **8건/19정의** → 확장본 **10건/26정의**. ★늘어난 것은 **7정의**(`514d5b08` 6 + `37ea5a13` 1)이고 ★**진짜 4 · 오탐 3**. ★**시간은 «유의한 증가 없음»이다**(판본 차 ≈0.8s/머지 ↔ 같은 판본 재실행 편차 **2.6배** · 수 출처 = 게이트② 검수 실측 · ★재측 안 함). ★「느리지 않았다」는 틀렸다 — wide 는 narrow 의 상위집합이라 더 싸질 수 없다. ★**잃는 것**: 본문만 빈 경우·리네임을 **못 가른다** · trailer 는 **우회로**이고 **이름이 글자 그대로 대조**된다 · ★★**새 red 계급 — main 에도 있는 정의를 지운 브랜치는 다음 base 당김에서 red 가 된다**(실례 `37ea5a13` · 7 중 3). ★`--all` **583/0/1**(불변) · 파리티 **명령 7개 일치**(DoD 블록 동기). - [rustjava-adopt-loadable-bootstrap-arguments-diagnostic] ★★**대전제 ⓒ 에서 끝났다 — 그 일을 하는 축(PR #67)이 이미 떠 있다.** 채택 제안 `2026-09-17-loadable-bootstrap-arguments#p0`(worklog json 기록). ★**코드 0행.** ★**전제는 참**(CLI 실행: 세 규칙이 전부 `ClassFormatError: Invalid class file` 동일 문면) — 그러나 ★**편집 영역은 전부 겹친다**(전달된 값은 **3 중 1** — 아래): #67 이 제안 `target` 두 파일을 고치고, 이 술어에 **이미 사유를 주며**, 밋밋한 문면 **두 자리**를 둘 다 고쳤다. ★제안 `tradeoff` 자신이 「두 번 하지 말고 함께 하라」고 적었다. diff --git a/docs/worklog/2026-09-18-merge-dropped-symbols-check.json b/docs/worklog/2026-09-18-merge-dropped-symbols-check.json index 013b0a41..209f8a66 100644 --- a/docs/worklog/2026-09-18-merge-dropped-symbols-check.json +++ b/docs/worklog/2026-09-18-merge-dropped-symbols-check.json @@ -18,11 +18,11 @@ "run against the real incident, the check reproduces by machine the same split the -fix3 round found by hand: 4 accidental drops in the generator plus 2 intentional test-function removals in tests/test_class_format.rs", "cargo test --all: 583 passed / 0 failed / 1 ignored, unchanged — no product code touched", "check-dod-ci-parity: OK, 7 commands and 2 toolchains matching on both sides after the wiring", - "GATE 2 found the filter too narrow and this round widened it. The original looked only at files the merged-in branch changed, on the stated grounds that 'only files the merged-in branch actually changed can have lost its work' — which is false, and the repo's own second incident proves it: 514d5b08 dropped the same four definitions as e53b2142, and the narrow filter reported '0 file(s) examined', rc=0. The predicate is now every file that either the merged-in branch or the merge itself changed, and both incidents report rc=1 with the same six names.", + "GATE 2 found the filter too narrow and this round widened it. The original looked only at files the merged-in branch changed, on the stated grounds that 'only files the merged-in branch actually changed can have lost its work' — which is false, and the repo's own second incident proves it: 514d5b08 dropped the same four definitions as e53b2142, and the narrow filter examined 4 files, found 0 and returned rc=0 (the wide filter examines 13 on the same merge). It was not blind: it read four files and none of them was where the loss landed. The predicate is now every file that either the merged-in branch or the merge itself changed, and both incidents report rc=1 with the same six names.", "The docstring's only worked example did not work. It wrote `Dropped-from-theirs: Pool.fieldref` while the check prints `method fieldref` and matches literally, so anyone following the example got no exemption. Verified both ways on a synthetic merge: the name copied from the output exempts (rc=0), the prettier form does not (rc=1). The example now uses the printed form and the docstring states that the name must be copied from the check's own output character for character." ], "issues": [ - "FALSE POSITIVE RATE, measured by this round by running both versions over one window of 83 merges (38b0df38..8c7b473f, the last 200 commits of origin/main at the time): the check as first written flags 8 merges / 19 definitions; the widened file filter flags 10 / 26. An earlier draft of this file said 45; that number was wrong and never came out of this implementation. The narrow version reproducing 19 exactly, in the same run, is what makes the measurement trustworthy. The seven extra definitions come from two merges, 514d5b08 (six) and 37ea5a13 (one), and they split four to three: four are the second of the two real incidents, restored later and present in the tree today; three are a branch having removed a definition main still had (two tests deleted on purpose by 0f517567, one predicate generalised by 1da1379e). So the honest rate is 4 of 7 found and 3 of 7 false, not 6 and 1. The widened version was no slower than the narrow one in that window, so time is not the cost of widening; reading is. A pull request usually contains 0-2 internal merges. CI only walks origin/main..HEAD, so historical merges are never re-examined and do not turn main red -- but that is not a reason to call the cost small, because the new cost lands in exactly that range, on the next base pull of an open branch.", + "FALSE POSITIVE RATE, measured by this round by running both versions over one window of 83 merges (38b0df38..8c7b473f, the last 200 commits of origin/main at the time): the check as first written flags 8 merges / 19 definitions; the widened file filter flags 10 / 26. An earlier draft of this file said 45; that number was wrong and never came out of this implementation. The narrow version reproducing 19 exactly, in the same run, is what makes the measurement trustworthy. The seven extra definitions come from two merges, 514d5b08 (six) and 37ea5a13 (one), and they split four to three: four are the second of the two real incidents, restored later and present in the tree today; three are a branch having removed a definition main still had (two tests deleted on purpose by 0f517567, one predicate generalised by 1da1379e). So the honest rate is 4 of 7 found and 3 of 7 false, not 6 and 1. On time, the honest statement is that there is no significant increase, not that it is no slower: three independent runs of the narrow version over this window span 229s/286.8s/585.3s, a factor of 2.6, while the difference between versions in one back-to-back run was 70s over 83 merges, about 0.8s per merge (all four numbers come from gate-2 measurements; this round deliberately did not re-run them, because re-running produces a fourth different number and that is the point). The wide file set is a superset of the narrow one -- 4 files versus 13 on 514d5b08 -- so it cannot be structurally cheaper. The cost of widening is reading, not waiting: a pull request carries 0-2 internal merges. A pull request usually contains 0-2 internal merges. CI only walks origin/main..HEAD, so historical merges are never re-examined and do not turn main red -- but that is not a reason to call the cost small, because the new cost lands in exactly that range, on the next base pull of an open branch.", "WHAT IT CANNOT SEE: a definition whose name survives while its body is emptied or gutted. The check compares name sets, so 'still there but no longer does anything' passes. It also cannot tell a genuine rename from a drop-plus-add, and a move between files reads as a drop in the old file.", "THE TRAILER IS A BYPASS, and deliberately so: anyone in a hurry can write one and move on. The only brake is that a reason is required by the format and the trailer is visible in the merge commit. That is weaker than a gate and stronger than nothing; making it harder was judged over-design for a check with no track record yet.", "Two of the flagged historical merges are the known incident. The rest are upstream-sync merges (S5-S8) and one `-s ours` ancestry record. The sync ones genuinely dropped upstream methods — decode_str and encode_str are absent from the whole result tree, not moved, which was checked — so whether they are findings or accepted judgements is for the sync lineage to say, not this round.", diff --git a/docs/worklog/2026-09-18-merge-dropped-symbols-check.md b/docs/worklog/2026-09-18-merge-dropped-symbols-check.md index 3458774c..5c14fb99 100644 --- a/docs/worklog/2026-09-18-merge-dropped-symbols-check.md +++ b/docs/worklog/2026-09-18-merge-dropped-symbols-check.md @@ -84,7 +84,7 @@ Dropped-from-theirs: * -- recorded as a merged ancestor only, our tree is author 초판은 **theirs 가 건드린 파일만** 봤고, 그 근거로 「`Only files the merged-in branch actually changed can have lost its work`」라 적었다. ★**그 문장이 거짓이고, 이 repo 자신의 «둘째» 사고가 반증한다**: -`514d5b08` 은 `e53b2142` 와 **같은 넷**을 떨어뜨렸는데 초판 검사기는 ★**`0 file(s) examined` · rc=0** 으로 통과시켰다. +`514d5b08` 은 `e53b2142` 와 **같은 넷**을 떨어뜨렸는데 초판 검사기는 ★**파일 «4개»를 examine 하고도 findings 0 · rc=0** 으로 통과시켰다(확장본은 같은 머지에서 **13개**를 보고 그 넷을 짚는다). ⇒ ★**해소가 «상대가 건드리지도 않은» 파일을 되돌리는 것** — 「A 충돌 풀다가 B 를 되돌려 놨다」 — 이 검사기의 **존재 이유 그대로의 실패**인데 못 봤다. @@ -104,8 +104,15 @@ lost its work`」라 적었다. ★**그 문장이 거짓이고, 이 repo 자신 ★**앵커** — 같은 스크립트·같은 창에서 초판이 **19 를 글자 그대로** 낸다(측정기가 옳다는 증거). ★**늘어난 것은 «26»이 아니라 «7정의»다** — `514d5b08` **6** + `37ea5a13` **1**. 그게 넓힘의 실제 크기다. ★★**그 7의 성격을 하나씩 판정했다 — 진짜 소실 «4» · 오탐 «3»**(아래 표). 종전 판까지 적혀 있던 **45** 는 **틀린 수**였다. -★**시간은 대가가 아니다** — 같은 창·같은 부하에서 **확장본이 초판보다 느리지 않았다**. -종전 「4.4초/머지」는 두 판본 다에 대해 낡았고, ★**넓힘의 비용처럼 읽히게 두면 안 된다**(비용은 시간이 아니라 «읽을 것»이다). +★★**시간 — 「느리지 않았다」로 적지 않는다. 측정 노이즈가 판본 차이보다 «크다».** +같은 창에서 초판을 **독립 3회** 돌린 값이 **229s / 286.8s / 585.3s** 로 ★**2.6배**까지 벌어진다 +(229s 는 게이트² fix2 검수자 실측 · 286.8s 는 그 앞 검수 · 585.3s 는 이 리니지 fix2 회차 · ★**이 회차는 다시 재지 않았다** — +재면 네 번째 다른 수가 나올 뿐이고 그것이 이 지적의 요지다). +한편 판본 차이는 **연속 실행 한 회차에서 70s / 83머지 ≈ 0.8s/머지**였다(같은 검수자 실측 · narrow 229s ↔ wide 299s). +⇒ ★**「유의한 비용 증가 없음」이 맞는 문장이고, 「비용 없음」은 아니다.** ★그리고 wide 의 파일 집합은 narrow 의 **상위집합**이라 +(`514d5b08` 에서 4 → 13) ★**구조적으로 더 싸질 수 없다** — 「느리지 않았다」는 방향부터 틀렸다. +종전 「4.4초/머지」를 지운 것은 **옳다**(재현 안 되는 수였다) — ★다만 그 자리에 **새 단정**을 넣은 것이 이번 반려 사유였다. +★**대가는 시간이 아니라 «읽을 것»이다**: PR 하나의 내부 머지는 보통 **0~2건**이다. ★**CI 는 `origin/main..HEAD`(= PR 범위)만 본다** — 과거 10건이 main 을 red 로 만들지는 않는다. ★★**그러나 그것을 «대가가 작다»의 근거로 쓰지 마라** — 새 대가는 정확히 그 PR 범위에서, 다음 base 당김에 난다(아래 「잃는 것」). @@ -144,7 +151,7 @@ lost its work`」라 적었다. ★**그 문장이 거짓이고, 이 repo 자신 ※**오늘 실제로 red 인 살아 있는 PR 은 0건**이다(열린 PR = 이 PR 하나 · 그 범위는 rc=0) — ★그러나 그것은 «오늘의 값»이지 계급의 소멸이 아니다. - ★**머지마다 몇 초가 붙는다** · DoD 블록에도 들어가 **로컬 실행에도** 붙는다. - ★단 **넓힘이 그 시간을 늘리지는 않았다**(위 ⒟ — 확장본이 초판보다 느리지 않았다). + ★단 **넓힘의 시간 비용은 «유의하지 않다»**(위 ⒟ — 판본 차이 ≈0.8s/머지 ↔ 같은 판본 재실행 편차 2.6배). ★「늘지 않았다」가 아니다. ## CI 배선 diff --git a/scripts/check-merge-dropped-symbols.py b/scripts/check-merge-dropped-symbols.py index 3f2ff621..287ca449 100755 --- a/scripts/check-merge-dropped-symbols.py +++ b/scripts/check-merge-dropped-symbols.py @@ -25,8 +25,12 @@ 8 merges / 19 definitions, this one reports 10 / 26. The seven extra definitions come from two merges -- 514d5b08 (six) and 37ea5a13 (one) -- and they are not all one kind. Four are the second of the two real incidents, restored later and present in the tree today. The other three are the -class described next. Time is not the cost: in that window the wider filter was no slower than the -narrow one, so the price is reading, not waiting. +class described next. On time: the measurement noise is larger than the difference between the two +versions -- three independent runs of the narrow filter over this window span 229s to 585s, a factor +of 2.6, while the gap between the versions in one back-to-back run was 70s over 83 merges, about +0.8s per merge. So there is no significant increase to report, which is a weaker claim than "no +slower" and the one the numbers actually support. A pull request carries 0-2 internal merges. The +price of widening is reading, not waiting. A branch that deletes or renames a definition main still has goes red on its next base pull, and an ordinary refactor then has to carry a trailer to say so. 37ea5a13 is exactly that: it pulled @@ -151,9 +155,11 @@ def check(merge): # Both what the merged-in branch touched and what the merge itself touched. Restricting this to # the first set was the original shape and it was wrong: a resolution can revert a file the other # branch never touched -- "fixed the conflict in A and put B back" -- and that is this check's - # whole reason for existing. Measured on the second of the two incidents, 514d5b08: with the - # narrow filter it reports "0 file(s) examined" and passes; with this one it names the same four - # definitions the first incident dropped. The cost is real and is recorded in Scope above. + # whole reason for existing. Measured on the second of the two incidents, 514d5b08, by calling + # check() on each version: the narrow filter examines 4 files, finds 0 and passes; this one + # examines 13 and names the same four definitions the first incident dropped. The narrow filter + # was not blind -- it read four files and still missed it, because none of the four was where + # the loss landed. The cost is real and is recorded in Scope above. changed = set((run("diff", "--name-only", base, theirs) or "").split("\n")) | set( (run("diff", "--name-only", base, merge) or "").split("\n") )