Skip to content

Commit 97d2258

Browse files
authored
[rustjava-partial-clone-refusal-decision] fix(scripts): 부분 클론을 거절하지 않는다 — 모호한 것은 «호출 하나»였다 (#78)
[rustjava-partial-clone-refusal-decision] fix(scripts): 부분 클론을 거절하지 않는다 — 모호한 것은 «호출 하나»였다
2 parents ddc6c4c + 3f3dd18 commit 97d2258

5 files changed

Lines changed: 202 additions & 5 deletions

File tree

‎REPORT.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,25 @@
11
# REPORT
2+
## [2026-09-19] 부분 클론도 «거절»할까 — ★**아니다. 모호했던 것은 «환경»이 아니라 «호출 하나»였다**(rustjava-partial-clone-refusal-decision)
3+
- 무엇을: 채택 제안 `2026-09-18-merge-drops-no-silent-git-failure#p0`(worklog json 기록). ★**거절하지 않는다** — 대신 `symbols()` 가 실패한 `git show` 를 «부재»로 읽기 «전»에 `git ls-tree` 로 그 경로가 트리에 있는지 묻는다. ★결정을 `preflight()` docstring 에 못박았다(다음 사람이 다시 묻지 않도록).
4+
- ★★**추측하지 않고 «진짜 부분 클론»을 만들어 쟀다**(`--filter=blob:none` · 범위는 알려진 사고 머지 `e53b2142^..e53b2142`):
5+
6+
| 클론 | promisor | 결과 | 시간 |
7+
|---|---|---|---|
8+
| 완전 | — | `6 dropped` · **rc 1** | 2.5s |
9+
| blobless | ★**도달 가능** | `6 dropped` · **rc 1** — ★**완전히 동일** | 15.7s |
10+
| blobless(신선) | ★**도달 불가** | `0 dropped` · ★★**rc 0 = green** | 8.3s |
11+
12+
- ★★**그래서 제안은 «절반만» 맞다**: 「부분 클론이면 아무것도 없는 것처럼 보인다」는 **그 자체로는 거짓**이다 — promisor 가 닿으면 git 이 blob 을 투명하게 받아 와 답이 **바이트 동일**하다. ⇒ ★**거절은 «돌아가는 설정»을 막는 것**이고, shallow 와 달리 부분 클론은 **없는 것을 가져올 수 있다**.
13+
★**그러나 조용한 green 은 «실재»한다** — 조건이 좁을 뿐(promisor **도달 불가**). 그 상태에서 검사기는 `✓ … (3 file(s) examined)` 를 찍고 **rc 0** 으로 끝냈다(완전 클론은 6건 보고).
14+
- ★**방법 주의(자기정정)**: 첫 오프라인 측정은 **오염됐다** — 앞선 온라인 실행이 그 blob 들을 이미 캐시해 «맞는 답»이 나왔다. 위 수는 **신선한 클론 + 읽기 «전»에 원격을 깨뜨린** 형상에서 다시 잰 것이다.
15+
- ★**위험이 실현되나 — 실측 0**: `.github` 어디에도 `filter:` 가 **없고**(`merge_drops` 는 `fetch-depth: 0`) ⇒ 오늘 이 거짓 green 은 «개발자 노트북 + blobless + 오프라인»에서만 난다. ★그것이 **거절을 고르지 않은 이유**이지, **모호함을 남길 이유는 아니다** — 닫는 비용이 git 호출 하나로 드러났기 때문이다.
16+
- ★**왜 `ls-tree` 인가(대안 둘을 각각 기각한 근거)**: ⒜**거절** — 탐지는 된다(`remote.origin.partialclonefilter = blob:none` · ★`rev-parse --is-shallow-repository` 는 **false** 라 현행 preflight 가 못 잡는다)지만 **맞는 답을 내는 경우까지 막는다** ⒝**에러 문면 대조** — 두 실패는 문면으로 갈리지만(`does not exist in` ↔ `could not fetch … from promisor remote`) **둘 다 exit 128** 이고, preflight 를 만든 회차가 이미 «git 판올림에 약하다»며 미뤘다 ⒞★**`ls-tree` 는 «트리 객체»로 답한다** — 부분 클론은 blob 이 없어도 **트리는 갖는다**. 두 클론에서 **동작 동일** 실측(있으면 1항목·없으면 빈 출력·rc 0).
17+
- ★**양방향 축**(제품 스크립트): 거짓 green 형상 — 전 **rc 0 `0 dropped`** ↔ 후 ★**rc 2 `cannot measure: …:jvm-bytecode/src/class_definition.rs is in that tree but its content could not be read…`**. ★**과차단 0**: blobless + promisor 도달 가능은 고친 뒤에도 **rc 1 · 6 dropped** · 완전 클론도 **불변**.
18+
- ★**비용 유의차 없음**: 같은 범위 3회씩 — 전 **7.78/7.99/7.05s** ↔ 후 **6.39/6.72/8.06s**(구간 겹침) · DoD 기본 범위 **0.46s → 0.33s**. 추가 호출은 `show` 가 **이미 실패한** 경로에서만 돈다.
19+
- ★**재다가 발견했고 «고치지 않았다»**: 이 검사기의 **출력 순서가 실행마다 다르다**(findings 가 set 에서 나온다). `origin/main` 판본을 `PYTHONHASHSEED=random` 으로 5회 돌려 **순서 2종**(4+1). rc·집합은 동일하고 **줄 순서만** 움직인다 ⇒ ★**선행 결함**이고, 전/후 diff 에서 잠깐 «회귀»처럼 보였기에 적는다(후속 카드).
20+
- 검증: DoD 9명령 · 아래 절.
21+
- ★후속 추천: **findings 를 정렬해 두 실행을 비교 가능하게 할 것인가**(S). 상세 = `docs/worklog/2026-09-19-partial-clone-blob-vs-absence.md`.
22+
223
## [2026-09-18] 리터럴이 «아닌» 이름으로 exception() 을 부르는 자리는 몇 개인가 — ★**0 이다**(rustjava-count-nonliteral-exception-call-sites)
324
- 무엇을: 채택 제안 `2026-09-18-named-exception-classes-are-loadable#p0`(worklog json `adoptedProposals` 기록). ★**순수 측정 회차 — `.rs` 0줄 · `scripts/` 0줄.** 산출은 «수»와 «술어»다.
425
- ★**답**: bare `exception(` **847** = 정의 **1** + ★**리터럴(java/javax) 846** + 리터럴(그 밖) **0** + ★★**비리터럴 «0»**.

‎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-partial-clone-refusal-decision] ★★**부분 클론을 «거절하지 않는다» — 모호했던 것은 환경이 아니라 «호출 하나»였다.** 채택 제안 `2026-09-18-merge-drops-no-silent-git-failure#p0`.
11+
★**진짜 blobless 클론으로 쟀다**: promisor **도달 가능**이면 답이 **완전 클론과 동일**(rc 1 · 6 dropped · 15.7s vs 2.5s) ⇒ ★거절은 «돌아가는 설정»을 막는 것.
12+
★**그러나 조용한 green 은 실재**: 신선한 blobless + promisor **도달 불가** → `0 dropped` · ★**rc 0**(완전 클론은 6건).
13+
★**처방**: `symbols()` 가 실패한 `show` 를 부재로 읽기 전에 **`ls-tree`** 로 트리 존재를 묻는다(부분 클론도 **트리는 갖는다**) ⇒ 문면 대조 없이 갈린다.
14+
★**양방향**: 전 rc 0(거짓 green) ↔ 후 ★**rc 2 «못 쟀다»** · ★과차단 0(도달 가능 blobless 는 여전히 rc 1) · 완전 클론 불변 · **비용 유의차 없음**(구간 겹침).
15+
★**위험 실현 0**: `.github` 에 `filter:` **0건**(`merge_drops` 는 `fetch-depth: 0`) — 그것이 «거절 안 함»의 근거이지 «모호함을 남길» 근거는 아니다.
16+
★**선행 결함 발견(미수정)**: 출력 순서가 실행마다 다르다(set) — `origin/main` 판본 5회에 순서 2종. rc·집합 불변(후속 카드).
17+
★결정을 `preflight()` docstring 에 못박았다 — 다음 회차가 같은 질문을 다시 하지 않도록.
1018
- [rustjava-count-nonliteral-exception-call-sites] ★★**사각의 «크기»를 쟀다 — 비리터럴 exception() 호출부는 «0» 이다.** 채택 제안 `2026-09-18-named-exception-classes-are-loadable#p0`. ★**순수 측정 · `.rs` 0줄 · `scripts/` 0줄.**
1119
★**수**: bare `exception(` **847** = 정의 1 + **리터럴 846** + 그 밖 리터럴 **0** + ★**비리터럴 0** ⇒ 검사기가 보는 집합 = 실제 호출부 집합(지금은 일치).
1220
★★**술어를 갈라야 답이 맞는다** — `exception(` 부분일치가 `assert_exception(` 등 **다른 함수 8종 41자리**(첫 인자가 `jvm`)를 쓸어담는다. 안 갈랐으면 ★**M=33 이라는 틀린 답**이었다.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"date": "2026-09-19",
3+
"taskId": "rustjava-partial-clone-refusal-decision",
4+
"summary": "Decided not to refuse partial clones. Measured on a real --filter=blob:none clone: with the promisor reachable the answer is identical to a full clone (rc 1, 6 dropped), so refusing would block a working setup. The silent green is real but needs the promisor to be unreachable - a fresh blobless clone with a broken remote printed 0 dropped and exited 0 where a full clone reports six. Fixed the ambiguity at the one call instead: symbols() asks ls-tree whether the path is in that tree before reading a failed git show as absence.",
5+
"decision": "do not refuse partial clones; disambiguate missing-blob from absent-path via ls-tree",
6+
"measurements": {
7+
"range_measured": "e53b2142^..e53b2142",
8+
"full_clone_rc": 1,
9+
"full_clone_dropped": 6,
10+
"blobless_promisor_reachable_rc": 1,
11+
"blobless_promisor_reachable_dropped": 6,
12+
"blobless_promisor_reachable_seconds": 15.66,
13+
"full_clone_seconds": 2.46,
14+
"blobless_promisor_unreachable_rc_before": 0,
15+
"blobless_promisor_unreachable_dropped_before": 0,
16+
"blobless_promisor_unreachable_rc_after": 2,
17+
"blobless_clone_seconds": 1.91,
18+
"blobless_git_dir_mb": 3.2,
19+
"workflows_using_partial_clone": 0,
20+
"timing_before_seconds": [7.78, 7.99, 7.05],
21+
"timing_after_seconds": [6.39, 6.72, 8.06],
22+
"dod_default_range_before_seconds": 0.46,
23+
"dod_default_range_after_seconds": 0.33
24+
},
25+
"verification": [
26+
"axis on the product script: fresh blobless clone with remote pointed at an invalid host, before = '0 definition(s) dropped' rc 0 (silent green), after = rc 2 'cannot measure: <rev>:jvm-bytecode/src/class_definition.rs is in that tree but its content could not be read'",
27+
"no over-blocking: blobless clone with the promisor reachable still returns rc 1 / 6 dropped after the fix",
28+
"full clone unchanged: rc 1 / 6 dropped before and after",
29+
"discriminator checked by execution in both clones: git ls-tree <rev> -- <path> prints one entry for a present path and empty output for an absent one, rc 0 either way, identical in the partial clone which lacks the blob",
30+
"detection axis for the refusal option (not taken): remote.origin.partialclonefilter = blob:none while rev-parse --is-shallow-repository = false, which is why the existing preflight misses it",
31+
"risk realisation: no workflow in .github sets filter:, and merge_drops checks out with fetch-depth: 0"
32+
],
33+
"changes": [
34+
"scripts/check-merge-dropped-symbols.py - symbols() raises CannotMeasure when ls-tree says the path is in the tree but show could not read it; preflight() docstring records the decision and the numbers behind it",
35+
"docs/worklog/2026-09-19-partial-clone-blob-vs-absence.{md,json}, REPORT.md, STATE.md"
36+
],
37+
"issues": [
38+
"The first offline measurement was contaminated: the earlier online run had cached the blobs, so it reported the correct answer. The recorded numbers come from a fresh clone with the remote broken before any read.",
39+
"Pre-existing and not fixed here: the checker's finding order is not stable between runs (emitted from a set). Measured on origin/main's own version with PYTHONHASHSEED=random - five runs, two orderings. rc and the finding set are identical; only line order moves.",
40+
"Message-based discrimination was available (fatal: path ... does not exist in vs fatal: could not fetch ... from promisor remote) but not used: both exit 128 and the round that added preflight already deferred message matching as version-fragile."
41+
],
42+
"adoptedProposals": [
43+
"2026-09-18-merge-drops-no-silent-git-failure#p0"
44+
],
45+
"proposals": [
46+
{
47+
"title": "Sort the checker's findings so two runs can be compared",
48+
"plainSummary": "The report lists what it found in a different order each time it runs, so comparing two runs shows differences that are not really there.",
49+
"userBenefit": "Anyone checking whether a change affected the result can diff two runs and trust the answer, instead of re-reading the list to see that the same items moved.",
50+
"why": "Measured this round on origin/main's own version: five runs under PYTHONHASHSEED=random produced two distinct orderings of the same six findings. It cost this round real time - the before/after diff looked like a regression until the original version was shown to disagree with itself. Findings are accumulated in a set and printed in iteration order; sorting at the print site is a one-line change.",
51+
"tradeoff": "Sorting fixes an order that no one has depended on, so the risk is close to zero; the only cost is that the output no longer reflects traversal order, which nothing reads. Leaving it means every future before/after comparison of this checker carries the same false signal.",
52+
"effort": "S",
53+
"target": "scripts/check-merge-dropped-symbols.py"
54+
}
55+
]
56+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# 2026-09-19 — Refuse partial clones too? No. The ambiguity was one call, not one environment.
2+
3+
Round: `rustjava-partial-clone-refusal-decision`
4+
Adopted proposal: `2026-09-18-merge-drops-no-silent-git-failure#p0`
5+
— *"The check now refuses to run in a shallow clone, but a clone fetched without file contents can
6+
still make it look like nothing is there."*
7+
8+
## Decision
9+
10+
**Do not refuse partial clones.** Fix the one call that could not tell "the path is not in this tree"
11+
from "the blob is not here": `symbols()` now asks `git ls-tree` before reading a failed `git show` as
12+
absence. The decision and its numbers are recorded in `preflight()`'s docstring so the next round
13+
does not re-ask.
14+
15+
## The proposal was half right, and the half matters
16+
17+
Everything below was measured on a real `--filter=blob:none` clone of this repository, not reasoned
18+
about. Range `e53b2142^..e53b2142` throughout — the known-accident merge, 6 dropped definitions.
19+
20+
| clone | promisor | result | time |
21+
|---|---|---|---|
22+
| full | — | `6 definition(s) dropped` · **rc 1** | 2.5 s |
23+
| blobless | **reachable** | `6 definition(s) dropped` · **rc 1** — *identical* | 15.7 s |
24+
| blobless (fresh) | **unreachable** | `0 definition(s) dropped` · ★**rc 0 — green** | 8.3 s |
25+
26+
So:
27+
28+
- **"A partial clone makes it look like nothing is there" is false on its own.** With the promisor
29+
reachable git fetches the blobs transparently and the answer is byte-identical. Refusing partial
30+
clones would refuse a setup that works — and unlike a shallow clone, a partial one can go get what
31+
it is missing.
32+
- **The silent green is real, but the trigger is narrower**: the promisor has to be *unreachable*. In
33+
that state the check printed `✓ … (3 file(s) examined)` and exited 0 where a full clone reports six
34+
dropped definitions. That is exactly the failure class this script exists for.
35+
36+
*Method note*: the first offline attempt reported the **correct** answer, because the earlier online
37+
run had already cached those blobs. The measurement above is from a **fresh** clone with the remote
38+
broken before any read. A contaminated clone answers the wrong question.
39+
40+
## Is the risk realised here? No — measured, and it does not change the decision
41+
42+
`git ls-files .github | grep filter:` → nothing; **no workflow uses a partial clone**, and
43+
`merge_drops` explicitly checks out with `fetch-depth: 0`. So today the false green needs a developer
44+
laptop with a blobless clone and no network. That is *why* refusing would be the wrong trade — it
45+
would cost a working configuration to close a case nobody is in — but it is not a reason to leave the
46+
ambiguity, because the cost of closing it turned out to be one git call.
47+
48+
## Why `ls-tree`, and not the two alternatives
49+
50+
- **Not refusal** (`remote.origin.partialclonefilter`): the detection works — measured `blob:none`,
51+
while `rev-parse --is-shallow-repository` returns false, which is exactly why the existing
52+
preflight misses this case — but it blocks the reachable-promisor case that gives the right answer.
53+
- **Not matching git's error text**: the two failures *are* distinguishable by message —
54+
`fatal: path 'X' does not exist in 'REV'` versus
55+
`fatal: could not fetch <sha> from promisor remote` — but both exit 128, and the round that added
56+
`preflight` already weighed and deferred message-matching as fragile across git versions. Nothing
57+
here changes that.
58+
- **`ls-tree` answers from the tree object**, which a partial clone holds even when it lacks blobs.
59+
Measured identical behaviour in both clones: path present → one entry, rc 0; path absent → empty
60+
output, rc 0. No message matching, no environment refused.
61+
62+
## Axis (bidirectional, on the product script)
63+
64+
| form | blobless + unreachable promisor | full clone |
65+
|---|---|---|
66+
| before | `0 dropped` · **rc 0** (silent green) | `6 dropped` · rc 1 |
67+
| after | ★**rc 2** `cannot measure: …:jvm-bytecode/src/class_definition.rs is in that tree but its content could not be read…` | `6 dropped` · rc 1 |
68+
69+
And the case that must **not** break: blobless with a reachable promisor, after the fix → **rc 1, 6
70+
dropped**. Over-blocking 0.
71+
72+
**Cost**: none measurable. Same range, three runs each — before 7.78 / 7.99 / 7.05 s, after 6.39 /
73+
6.72 / 8.06 s (overlapping ranges). DoD's default range `origin/main..HEAD`: 0.46 s → 0.33 s. The
74+
extra call only runs when `show` has already failed.
75+
76+
## Found while measuring, not fixed here
77+
78+
The checker's **output order is not stable between runs** — findings are emitted from a set. Measured
79+
on `origin/main`'s own version with `PYTHONHASHSEED=random`: five runs gave two different orderings
80+
(4 + 1). The rc and the set of findings are identical; only line order moves. This is pre-existing
81+
and unrelated to this round — it is noted because it briefly looked like a regression in the diff of
82+
before/after output, and the next person comparing two runs deserves to know. Filed as a proposal.

0 commit comments

Comments
 (0)