Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions REPORT.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,25 @@
# REPORT
## [2026-09-19] 부분 클론도 «거절»할까 — ★**아니다. 모호했던 것은 «환경»이 아니라 «호출 하나»였다**(rustjava-partial-clone-refusal-decision)
- 무엇을: 채택 제안 `2026-09-18-merge-drops-no-silent-git-failure#p0`(worklog json 기록). ★**거절하지 않는다** — 대신 `symbols()` 가 실패한 `git show` 를 «부재»로 읽기 «전»에 `git ls-tree` 로 그 경로가 트리에 있는지 묻는다. ★결정을 `preflight()` docstring 에 못박았다(다음 사람이 다시 묻지 않도록).
- ★★**추측하지 않고 «진짜 부분 클론»을 만들어 쟀다**(`--filter=blob:none` · 범위는 알려진 사고 머지 `e53b2142^..e53b2142`):

| 클론 | promisor | 결과 | 시간 |
|---|---|---|---|
| 완전 | — | `6 dropped` · **rc 1** | 2.5s |
| blobless | ★**도달 가능** | `6 dropped` · **rc 1** — ★**완전히 동일** | 15.7s |
| blobless(신선) | ★**도달 불가** | `0 dropped` · ★★**rc 0 = green** | 8.3s |

- ★★**그래서 제안은 «절반만» 맞다**: 「부분 클론이면 아무것도 없는 것처럼 보인다」는 **그 자체로는 거짓**이다 — promisor 가 닿으면 git 이 blob 을 투명하게 받아 와 답이 **바이트 동일**하다. ⇒ ★**거절은 «돌아가는 설정»을 막는 것**이고, shallow 와 달리 부분 클론은 **없는 것을 가져올 수 있다**.
★**그러나 조용한 green 은 «실재»한다** — 조건이 좁을 뿐(promisor **도달 불가**). 그 상태에서 검사기는 `✓ … (3 file(s) examined)` 를 찍고 **rc 0** 으로 끝냈다(완전 클론은 6건 보고).
- ★**방법 주의(자기정정)**: 첫 오프라인 측정은 **오염됐다** — 앞선 온라인 실행이 그 blob 들을 이미 캐시해 «맞는 답»이 나왔다. 위 수는 **신선한 클론 + 읽기 «전»에 원격을 깨뜨린** 형상에서 다시 잰 것이다.
- ★**위험이 실현되나 — 실측 0**: `.github` 어디에도 `filter:` 가 **없고**(`merge_drops` 는 `fetch-depth: 0`) ⇒ 오늘 이 거짓 green 은 «개발자 노트북 + blobless + 오프라인»에서만 난다. ★그것이 **거절을 고르지 않은 이유**이지, **모호함을 남길 이유는 아니다** — 닫는 비용이 git 호출 하나로 드러났기 때문이다.
- ★**왜 `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).
- ★**양방향 축**(제품 스크립트): 거짓 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** · 완전 클론도 **불변**.
- ★**비용 유의차 없음**: 같은 범위 3회씩 — 전 **7.78/7.99/7.05s** ↔ 후 **6.39/6.72/8.06s**(구간 겹침) · DoD 기본 범위 **0.46s → 0.33s**. 추가 호출은 `show` 가 **이미 실패한** 경로에서만 돈다.
- ★**재다가 발견했고 «고치지 않았다»**: 이 검사기의 **출력 순서가 실행마다 다르다**(findings 가 set 에서 나온다). `origin/main` 판본을 `PYTHONHASHSEED=random` 으로 5회 돌려 **순서 2종**(4+1). rc·집합은 동일하고 **줄 순서만** 움직인다 ⇒ ★**선행 결함**이고, 전/후 diff 에서 잠깐 «회귀»처럼 보였기에 적는다(후속 카드).
- 검증: DoD 9명령 · 아래 절.
- ★후속 추천: **findings 를 정렬해 두 실행을 비교 가능하게 할 것인가**(S). 상세 = `docs/worklog/2026-09-19-partial-clone-blob-vs-absence.md`.

## [2026-09-18] 리터럴이 «아닌» 이름으로 exception() 을 부르는 자리는 몇 개인가 — ★**0 이다**(rustjava-count-nonliteral-exception-call-sites)
- 무엇을: 채택 제안 `2026-09-18-named-exception-classes-are-loadable#p0`(worklog json `adoptedProposals` 기록). ★**순수 측정 회차 — `.rs` 0줄 · `scripts/` 0줄.** 산출은 «수»와 «술어»다.
- ★**답**: bare `exception(` **847** = 정의 **1** + ★**리터럴(java/javax) 846** + 리터럴(그 밖) **0** + ★★**비리터럴 «0»**.
Expand Down
8 changes: 8 additions & 0 deletions STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@
(둘 다 이것보다 오래됐고 MERGEABLE/CONFLICTING 처분이 이미 걸려 있다). 겹침은 전부 **append 형 합집합**이라 해소는 기계적이다)

## 완료
- [rustjava-partial-clone-refusal-decision] ★★**부분 클론을 «거절하지 않는다» — 모호했던 것은 환경이 아니라 «호출 하나»였다.** 채택 제안 `2026-09-18-merge-drops-no-silent-git-failure#p0`.
★**진짜 blobless 클론으로 쟀다**: promisor **도달 가능**이면 답이 **완전 클론과 동일**(rc 1 · 6 dropped · 15.7s vs 2.5s) ⇒ ★거절은 «돌아가는 설정»을 막는 것.
★**그러나 조용한 green 은 실재**: 신선한 blobless + promisor **도달 불가** → `0 dropped` · ★**rc 0**(완전 클론은 6건).
★**처방**: `symbols()` 가 실패한 `show` 를 부재로 읽기 전에 **`ls-tree`** 로 트리 존재를 묻는다(부분 클론도 **트리는 갖는다**) ⇒ 문면 대조 없이 갈린다.
★**양방향**: 전 rc 0(거짓 green) ↔ 후 ★**rc 2 «못 쟀다»** · ★과차단 0(도달 가능 blobless 는 여전히 rc 1) · 완전 클론 불변 · **비용 유의차 없음**(구간 겹침).
★**위험 실현 0**: `.github` 에 `filter:` **0건**(`merge_drops` 는 `fetch-depth: 0`) — 그것이 «거절 안 함»의 근거이지 «모호함을 남길» 근거는 아니다.
★**선행 결함 발견(미수정)**: 출력 순서가 실행마다 다르다(set) — `origin/main` 판본 5회에 순서 2종. rc·집합 불변(후속 카드).
★결정을 `preflight()` docstring 에 못박았다 — 다음 회차가 같은 질문을 다시 하지 않도록.
- [rustjava-count-nonliteral-exception-call-sites] ★★**사각의 «크기»를 쟀다 — 비리터럴 exception() 호출부는 «0» 이다.** 채택 제안 `2026-09-18-named-exception-classes-are-loadable#p0`. ★**순수 측정 · `.rs` 0줄 · `scripts/` 0줄.**
★**수**: bare `exception(` **847** = 정의 1 + **리터럴 846** + 그 밖 리터럴 **0** + ★**비리터럴 0** ⇒ 검사기가 보는 집합 = 실제 호출부 집합(지금은 일치).
★★**술어를 갈라야 답이 맞는다** — `exception(` 부분일치가 `assert_exception(` 등 **다른 함수 8종 41자리**(첫 인자가 `jvm`)를 쓸어담는다. 안 갈랐으면 ★**M=33 이라는 틀린 답**이었다.
Expand Down
56 changes: 56 additions & 0 deletions docs/worklog/2026-09-19-partial-clone-blob-vs-absence.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"date": "2026-09-19",
"taskId": "rustjava-partial-clone-refusal-decision",
"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.",
"decision": "do not refuse partial clones; disambiguate missing-blob from absent-path via ls-tree",
"measurements": {
"range_measured": "e53b2142^..e53b2142",
"full_clone_rc": 1,
"full_clone_dropped": 6,
"blobless_promisor_reachable_rc": 1,
"blobless_promisor_reachable_dropped": 6,
"blobless_promisor_reachable_seconds": 15.66,
"full_clone_seconds": 2.46,
"blobless_promisor_unreachable_rc_before": 0,
"blobless_promisor_unreachable_dropped_before": 0,
"blobless_promisor_unreachable_rc_after": 2,
"blobless_clone_seconds": 1.91,
"blobless_git_dir_mb": 3.2,
"workflows_using_partial_clone": 0,
"timing_before_seconds": [7.78, 7.99, 7.05],
"timing_after_seconds": [6.39, 6.72, 8.06],
"dod_default_range_before_seconds": 0.46,
"dod_default_range_after_seconds": 0.33
},
"verification": [
"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'",
"no over-blocking: blobless clone with the promisor reachable still returns rc 1 / 6 dropped after the fix",
"full clone unchanged: rc 1 / 6 dropped before and after",
"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",
"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",
"risk realisation: no workflow in .github sets filter:, and merge_drops checks out with fetch-depth: 0"
],
"changes": [
"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",
"docs/worklog/2026-09-19-partial-clone-blob-vs-absence.{md,json}, REPORT.md, STATE.md"
],
"issues": [
"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.",
"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.",
"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."
],
"adoptedProposals": [
"2026-09-18-merge-drops-no-silent-git-failure#p0"
],
"proposals": [
{
"title": "Sort the checker's findings so two runs can be compared",
"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.",
"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.",
"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.",
"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.",
"effort": "S",
"target": "scripts/check-merge-dropped-symbols.py"
}
]
}
82 changes: 82 additions & 0 deletions docs/worklog/2026-09-19-partial-clone-blob-vs-absence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# 2026-09-19 — Refuse partial clones too? No. The ambiguity was one call, not one environment.

Round: `rustjava-partial-clone-refusal-decision`
Adopted proposal: `2026-09-18-merge-drops-no-silent-git-failure#p0`
— *"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."*

## Decision

**Do not refuse partial clones.** Fix the one call that could not tell "the path is not in this tree"
from "the blob is not here": `symbols()` now asks `git ls-tree` before reading a failed `git show` as
absence. The decision and its numbers are recorded in `preflight()`'s docstring so the next round
does not re-ask.

## The proposal was half right, and the half matters

Everything below was measured on a real `--filter=blob:none` clone of this repository, not reasoned
about. Range `e53b2142^..e53b2142` throughout — the known-accident merge, 6 dropped definitions.

| clone | promisor | result | time |
|---|---|---|---|
| full | — | `6 definition(s) dropped` · **rc 1** | 2.5 s |
| blobless | **reachable** | `6 definition(s) dropped` · **rc 1** — *identical* | 15.7 s |
| blobless (fresh) | **unreachable** | `0 definition(s) dropped` · ★**rc 0 — green** | 8.3 s |

So:

- **"A partial clone makes it look like nothing is there" is false on its own.** With the promisor
reachable git fetches the blobs transparently and the answer is byte-identical. Refusing partial
clones would refuse a setup that works — and unlike a shallow clone, a partial one can go get what
it is missing.
- **The silent green is real, but the trigger is narrower**: the promisor has to be *unreachable*. In
that state the check printed `✓ … (3 file(s) examined)` and exited 0 where a full clone reports six
dropped definitions. That is exactly the failure class this script exists for.

*Method note*: the first offline attempt reported the **correct** answer, because the earlier online
run had already cached those blobs. The measurement above is from a **fresh** clone with the remote
broken before any read. A contaminated clone answers the wrong question.

## Is the risk realised here? No — measured, and it does not change the decision

`git ls-files .github | grep filter:` → nothing; **no workflow uses a partial clone**, and
`merge_drops` explicitly checks out with `fetch-depth: 0`. So today the false green needs a developer
laptop with a blobless clone and no network. That is *why* refusing would be the wrong trade — it
would cost a working configuration to close a case nobody is in — but it is not a reason to leave the
ambiguity, because the cost of closing it turned out to be one git call.

## Why `ls-tree`, and not the two alternatives

- **Not refusal** (`remote.origin.partialclonefilter`): the detection works — measured `blob:none`,
while `rev-parse --is-shallow-repository` returns false, which is exactly why the existing
preflight misses this case — but it blocks the reachable-promisor case that gives the right answer.
- **Not matching git's error text**: the two failures *are* distinguishable by message —
`fatal: path 'X' does not exist in 'REV'` versus
`fatal: could not fetch <sha> from promisor remote` — but both exit 128, and the round that added
`preflight` already weighed and deferred message-matching as fragile across git versions. Nothing
here changes that.
- **`ls-tree` answers from the tree object**, which a partial clone holds even when it lacks blobs.
Measured identical behaviour in both clones: path present → one entry, rc 0; path absent → empty
output, rc 0. No message matching, no environment refused.

## Axis (bidirectional, on the product script)

| form | blobless + unreachable promisor | full clone |
|---|---|---|
| before | `0 dropped` · **rc 0** (silent green) | `6 dropped` · rc 1 |
| 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 |

And the case that must **not** break: blobless with a reachable promisor, after the fix → **rc 1, 6
dropped**. Over-blocking 0.

**Cost**: none measurable. Same range, three runs each — before 7.78 / 7.99 / 7.05 s, after 6.39 /
6.72 / 8.06 s (overlapping ranges). DoD's default range `origin/main..HEAD`: 0.46 s → 0.33 s. The
extra call only runs when `show` has already failed.

## Found while measuring, not fixed here

The checker's **output order is not stable between runs** — findings are emitted from a set. Measured
on `origin/main`'s own version with `PYTHONHASHSEED=random`: five runs gave two different orderings
(4 + 1). The rc and the set of findings are identical; only line order moves. This is pre-existing
and unrelated to this round — it is noted because it briefly looked like a regression in the diff of
before/after output, and the next person comparing two runs deserves to know. Filed as a proposal.
Loading
Loading