Skip to content

Commit 117c875

Browse files
authored
[rustjava-merge-dropped-symbols-checker-swallows-git-failures] fix(scripts): git 실패를 삼키지 않는다 — 「못 쟀다」는 rc=2 로 말한다 (#74)
[rustjava-merge-dropped-symbols-checker-swallows-git-failures] fix(scripts): git 실패를 삼키지 않는다 — 「못 쟀다」는 rc=2 로 말한다
2 parents fd571cb + e611e83 commit 117c875

5 files changed

Lines changed: 225 additions & 15 deletions

File tree

‎REPORT.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,16 @@
11
# REPORT
2+
## [2026-09-18] 「조용한 실패」를 잡는 검사기에 «조용히 통과하는 길»이 있었다 (rustjava-merge-dropped-symbols-checker-swallows-git-failures)
3+
- 무엇을: `scripts/check-merge-dropped-symbols.py` 의 `run()` 이 git 실패를 `None` 으로 삼키고 호출부가 전부 `(… or "")` 로 받아 ★**「git 이 못 답했다」가 「없다고 답했다」로 접혔다** ⇒ `✓ (0 file(s) examined)` · **rc=0**.
4+
- ★**재현은 합성이 아니라 «진짜 얕은 클론»이다**(`--depth 10`): **전 rc=0** 에 `✓ 97660921 (0 …)` · `✓ 56bb54fa (0 …)` ↔ ★**완전 클론에서 `56bb54fa` 는 examined «20»** 이다. ⇒ 20개를 보던 머지가 0으로 접히고 run 전체가 green 이었다. **후 rc=2** `cannot measure: shallow clone: …(git fetch --unshallow)`.
5+
- ★**preflight 만이 아니라 raise 경로도 쟀다** — 범위 오류 · 루프 «안» diff 실패 · 비-git 디렉터리 **전부 rc=2** 이고 문면에 git stderr 를 그대로 싣는다.
6+
- ★★**급소 = «실패»와 «빈 결과»를 가르는 것**: `run()` 호출부 **8곳** 중 ★**`git show <rev>:<path>` 한 자리만 «실패가 답»**이다(경로가 그 트리에 없는 것은 **정상**). ⇒ rc 로는 못 가르므로 ★**환경 자체를 preflight 로 배제**하고 그 뒤의 `show` 실패는 **부재로만** 읽는다(전제를 주석에 명기).
7+
- ★**ⓒ CI 는 이미 `fetch-depth: 0`** 이다(`merge_drops` job · 주석에 이유까지) ⇒ ★**이 변경이 전 PR 을 막지 않는다**(해당 job 의 얕은 클론 빈도 **0**). ★그래서 처방이 「객체를 먼저 받는 것」이 아니라 **「rc 를 올리는 것」**으로 정해졌다.
8+
- ★**양방향**: ⒜실패 → **rc=2 「못 쟀다」**(`✓` 금지) ⒝★**정상인데 0** — `97660921`(`.md`/`.json` 만 바뀐 머지 · 완전 클론에서도 진짜 0) → **여전히 `✓` rc=0** ⒞**탐지 회귀 0** — `e53b2142` **6** · `514d5b08` **6** · `56bb54fa` examined **20** 불변.
9+
- ★**잃는 것**: 손으로 얕은 트리에서 돌리던 사람은 **이제 빨강**을 본다(그 초록이 거짓이었다) · 표시용·면제용 호출까지 일괄 raise 라 ★**더 자주 멈춘다**(안전한 예외가 `show` 하나뿐임을 표로 못박은 대가) · ★**preflight 는 «얕음»만 본다** — 부분 클론·손상 객체는 **여전히 `show` 의 부재로 읽힐 수 있다**(닫은 것은 가장 흔한 한 갈래) · F8 의 「0」이 정말 이 경로였는지는 **증명 못 한다**(가설과 정합할 뿐).
10+
- ★**범위**: 판정 술어·필터 폭·`PATTERNS` **무접촉** · **`.rs` 0건** · 새 검사기·새 워크플로 **0** · 종료코드는 **이미 있던 `2`** 를 쓴다.
11+
- 검증: 검사기 자기 실행 `✓ 430fef8a (11 file(s) examined)` rc=0 · `check-worklog-json` rc=0 · `check-dod-ci-parity` rc=0(명령 7개) · `cargo fmt` rc=0.
12+
- ★**이 PR 은 #71 에 «쌓여» 있다** — 검사기가 `origin/main` 에 **아직 없다**(PR #71 브랜치에만 있다) ⇒ base = `feat/rustjava-merge-drop-check`. ★게이트③ 계약 5 대로 **#71 머지 회차가 먼저 base 를 `main` 으로 재지정**해야 한다.
13+
- ★후속 추천: ⑴**부분 클론도 preflight 로 막을 것인가**(S · 남은 한 갈래) ⑵`show` 의 두 실패를 **stderr 문면으로 가를 것인가**(S · git 판올림에 약해 이번엔 환경 배제를 골랐다). 상세 = `docs/worklog/2026-09-18-merge-drops-no-silent-git-failure.md`.
214
## [2026-09-18] 검사기의 «거짓 초록» 둘과 «거짓 빨강» 하나 — 게이트² 반려 승계 (rustjava-lock-every-named-exception-class-is-loadable-fix)
315
- 무엇을: PR #72 의 검사기 결함 **3건** 정정. ★**베이스라인 0 인 검사기라 «거짓 초록 = 검사기 부재»** 다. ★런타임 클래스 추가 **0** · `loader.rs` `protos` **무접촉** · `jvm.rs` **무접촉**.
416
- ★**F1(거짓 초록)** 짧은 이름 충돌 — `Formatter`(`java/util` ↔ `java/util/logging`) · `JarURLConnection`(`java/net` ↔ `org/rustjava/net`) **2쌍 실재**. ★재현: 한쪽 등재를 지우고 그 이름을 `exception(` 에 넣으면 **전 `✓ … 263 loadable` rc=0**(거짓 초록) → **후 rc=1**. 둘째 쌍도 동일. ★처방 = 키를 **(모듈, 타입, 함수)** 로.

‎STATE.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@
77
(둘 다 이것보다 오래됐고 MERGEABLE/CONFLICTING 처분이 이미 걸려 있다). 겹침은 전부 **append 형 합집합**이라 해소는 기계적이다)
88

99
## 완료
10+
- [rustjava-merge-dropped-symbols-checker-swallows-git-failures] ★★**「조용한 실패」 검사기에 «조용히 통과하는 길»이 있었다 — 닫았다.**
11+
★**재현 = 진짜 얕은 클론**(`--depth 10`): 전 **rc=0** `✓ 56bb54fa (0 file(s) examined)` ↔ ★완전 클론에선 **examined 20** ⇒ 20→0 으로 접히고 green. 후 **rc=2 `cannot measure: shallow clone…`**.
12+
★raise 경로도 쟀다 — 범위 오류·루프 내 diff 실패·비-git **전부 rc=2**(git stderr 동봉).
13+
★★**급소**: `run()` 호출부 8곳 중 ★**`git show <rev>:<path>` 하나만 «실패가 답»**(경로 부재는 정상) ⇒ rc 로 못 가르니 **환경을 preflight 로 배제**.
14+
★**ⓒ CI 는 이미 `fetch-depth: 0`** ⇒ 전 PR 을 막지 않는다(그 job 의 얕은 클론 빈도 **0**) — 그래서 처방이 «rc 올리기»로 정해졌다.
15+
★**양방향**: 실패→rc=2 · ★**정상 0**(`97660921`)→**여전히 ✓ rc=0** · 탐지 회귀 0(`e53b2142` 6 · `514d5b08` 6 · `56bb54fa` 20).
16+
★**잃는 것**: 얕은 트리 수동 실행은 이제 빨강 · 표시/면제 호출까지 일괄 raise 라 **더 자주 멈춘다** · ★preflight 는 «얕음»만 본다(부분 클론은 남았다) · F8 「0」의 인과는 **미증명**.
17+
★`.rs` 0건 · 술어·필터 폭 무접촉 · 종료코드는 기존 `2` 재사용. ★**PR 은 #71 에 쌓여 있다**(검사기가 main 에 없다) — 계약 5 재지정 필요.
1018
- [rustjava-lock-every-named-exception-class-is-loadable-fix] ★★**검사기의 «거짓 초록» 2건 + «거짓 빨강» 1건 정정**(게이트² 반려 승계 · PR #72).
1119
★**F1** 짧은 이름 충돌(`Formatter`·`JarURLConnection` **2쌍**) ⇒ 한쪽 등재를 지워도 **전 rc=0(거짓 초록)** → **후 rc=1**. 키를 **(모듈,타입,함수)** 로.
1220
★**F2** 줄 단위 스캔이 다중 줄 호출을 못 봄 ⇒ **전 rc=0(안 보임) → 후 rc=1**. ★**846 − 34 = 812** 로 검수자 수와의 차이를 설명했다.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"date": "2026-09-18",
3+
"taskId": "rustjava-merge-dropped-symbols-checker-swallows-git-failures",
4+
"summary": "The check that exists to catch a silent loss had a silent pass in it: run() returned None on a failing git, every caller wrote `run(...) or \"\"`, and the result was `✓ (0 file(s) examined)` with rc 0. git calls now raise, main() reports `cannot measure: …` with rc 2, and the one call where a non-zero exit is a real answer keeps tolerating it behind a preflight. The predicate for what counts as a dropped symbol is untouched.",
5+
"changes": [
6+
"scripts/check-merge-dropped-symbols.py — new CannotMeasure exception; run() raises unless absence_is_an_answer=True; preflight() rejects a shallow clone; main() catches in two places and returns 2; the exit-code docstring says 'could not measure' rather than 'could not run'.",
7+
"Six call sites lost their `or \"\"` fallback (parents, merge-base, two diffs, the trailer message, the display subject). symbols()'s `git show` is the single tolerated failure."
8+
],
9+
"verification": [
10+
"REPRODUCED IN A REAL ENVIRONMENT, not a mock: cloning this repository at --depth 10 and running the pre-change script over one merge range printed `✓ 97660921 (0 file(s) examined)` and `✓ 56bb54fa (0 file(s) examined)` and exited 0. In a full clone the same merges examine 0 and 20 files respectively — so a merge that reads 20 files collapsed to 0 and the run stayed green.",
11+
"AFTER, same shallow clone, same range: rc=2 with `cannot measure: shallow clone: a merge's second parent is not here, so every read of it would look empty. Fetch the full history (git fetch --unshallow) and run again.`",
12+
"THE RAISE PATH WAS CHECKED SEPARATELY FROM THE PREFLIGHT, all rc=2, all carrying git's own stderr: a bad range (rev-list exits 128), a failure inside the per-merge loop (diff forced to fail), and running outside a git repository.",
13+
"CALL-SITE CENSUS: 8 uses of run(). Seven must raise. One must not — `git show <rev>:<path>` also exits non-zero when the path is simply not in that tree, which is ordinary and means 'no definitions here'. rc alone cannot separate that from a missing object, so the ambiguity is removed by ruling out the environment in preflight() rather than by reading stderr text.",
14+
"CI ALREADY FETCHES FULL HISTORY: the merge_drops job pins fetch-depth: 0 with a comment saying why. rc!=0 fails that job because the script is its last step. So raising on failure does not red existing PRs — the frequency of a shallow checkout in that job is 0. That measurement is what decided the prescription (raise) over the alternative the ticket named (fetch the objects first): CI already fetches them.",
15+
"BIDIRECTIONAL: (a) failure -> rc=2 and a 'cannot measure' line, never ✓. (b) a genuinely empty merge, 97660921, whose changed files are all .md/.json so nothing is readable by the patterns — still `✓ (0 file(s) examined)` rc=0. (c) detection unchanged: e53b2142 findings 6, 514d5b08 findings 6, 56bb54fa examined 20, 430fef8a examined 11, all identical to before.",
16+
"cargo untouched: git diff --numstat is one file, scripts/check-merge-dropped-symbols.py 68/15, and .rs files changed = 0. check-worklog-json rc=0, check-dod-ci-parity rc=0 (7 commands), cargo fmt rc=0, and the checker's own run over origin/main..HEAD is `✓ 430fef8a (11 file(s) examined)` rc=0."
17+
],
18+
"issues": [
19+
"Anyone who ran this by hand in a shallow tree now sees red where they saw green. The green was false, and the message says how to fix it, but it looks like a new breakage to whoever meets it first.",
20+
"Display-only and exemption-only calls (the subject line, the trailer message) were raised too, so the check can now stop for reasons that used to degrade quietly. That was chosen over per-site judgement because such judgement goes stale; the one safe exception is pinned in a table in the worklog.",
21+
"preflight() only rules out shallowness. A partial clone (--filter=blob:none) or a corrupt object can still make `git show` fail in a way that reads as absence. The other call sites raise, so the remaining hole is show-only — narrower than before but not closed.",
22+
"Whether F8's original '0' really came through this path is not proven; there is no execution record from that round. The behaviour is consistent with it, which is all that can be said.",
23+
"THIS PR IS STACKED ON PR #71. scripts/check-merge-dropped-symbols.py does not exist on origin/main yet — it only exists on PR #71's branch — so this work cannot be cut from main. The base is feat/rustjava-merge-drop-check, which makes this a child PR: gate3 contract 5 requires the round that merges #71 to re-parent it to main first, or GitHub closes it when the base branch is deleted."
24+
],
25+
"adoptedProposals": [],
26+
"proposals": [
27+
{
28+
"title": "Decide whether a partial clone should also be refused up front",
29+
"plainSummary": "The check now refuses to run in a shallow clone, but a clone fetched without file contents can still make it look like nothing is there.",
30+
"userBenefit": "The same false green this round removed can still happen in a clone made with --filter, which is the shape CI systems increasingly default to.",
31+
"why": "preflight() tests one thing: is the repository shallow. That covers the common case and was measured. A partial clone is different — the commits are all present, so every other git call succeeds, but `git show <rev>:<path>` cannot fetch the blob and fails in exactly the way that now means 'the path is not in this tree'. The remaining hole is that one call, and it is the one call this round deliberately left tolerant.",
32+
"tradeoff": "Refusing partial clones outright would block a legitimate and cheap way to run CI, and the check may work fine there if the blobs get fetched on demand — which depends on the remote's configuration rather than ours. Detecting it properly means asking git about promisor remotes, which is a smaller, quieter API than --is-shallow-repository and one more thing to keep working.",
33+
"effort": "S",
34+
"target": "scripts/check-merge-dropped-symbols.py"
35+
}
36+
]
37+
}

0 commit comments

Comments
 (0)