Skip to content

Commit 750d30d

Browse files
authored
[2026-09-19-partial-clone-blob-vs-absence-p0] fix(scripts): 머지 드롭 검사기의 출력 순서를 고정한다 — 두 회차를 diff 할 수 있게 (#83)
[2026-09-19-partial-clone-blob-vs-absence-p0] fix(scripts): 머지 드롭 검사기의 출력 순서를 고정한다 — 두 회차를 diff 할 수 있게
2 parents 64cc4f6 + 35f3479 commit 750d30d

5 files changed

Lines changed: 136 additions & 1 deletion

File tree

REPORT.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
# REPORT
2+
## [2026-09-19] 두 회차를 비교할 수 있게 «순서»를 고정했다 (2026-09-19-partial-clone-blob-vs-absence-p0)
3+
- 무엇을: 채택 제안 `2026-09-19-partial-clone-blob-vs-absence#p0`. `scripts/check-merge-dropped-symbols.py`**같은 결과를 회차마다 다른 순서로** 찍어, 두 회차를 diff 하면 ★**없는 차이가 보였다**. ★**코드 1줄**(+주석 8줄).
4+
-**재현**: `origin/main` 판본 · 범위 `e53b2142^..e53b2142`(8머지 · 6드롭 · rc 1) · `PYTHONHASHSEED=random` **10회** → ★**서로 다른 순서 «2종»**(6회/4회). 차이는 ★**경로 블록의 선후 하나뿐**이다.
5+
- ★★**제안의 «진단»은 정확하지 않았다 — 고치는 자리가 달라진다.** 제안은 「findings 가 set 에 모인다」고 했는데 ⑴`findings`**list** 이고 ⑵한 경로 «안»의 이름은 **이미 `sorted()`** 였다. 파일의 set **4개**를 전수로 보면 출력 순서에 닿는 것은 ★**`changed`(경로 집합) 하나뿐**이다. ⇒ 「print site 에서 정렬」은 **우연히** 맞는 처방이었다.
6+
-**그래서 «출처»에서 정렬했다**`for path in sorted(filter(None, changed))`. `findings``check()`**반환**도 하므로, `main()` 에서 정렬하면 **찍는 것만** 결정적이고 **반환값은 여전히** 해시 순서다.
7+
-**양방향(제품 호출부 · 사본 아님)**: 전 **2종** ↔ 후 **1종** ↔ 되돌리면 **2종** ↔ 복원 **1종**. ★**찾은 것은 그대로다**: 출력 **15줄** 동일 · 집합으로 정렬하면 **완전 일치** · rc **1** 불변.
8+
- ★★**대가 — 「잃는 것이 없다」가 아니다**: ⒜순회 순서라는 성질을 **없앴다**(읽는 곳은 없다) ⒝★**아무도 잠그지 않는다.** 이 repo 엔 `scripts/`**테스트 하네스가 없다**(`test*.py` **0개**) — 비결정성을 **되돌려 놓고 재니** 파이썬 검사기 **4종 전건 rc 0** · `cargo fmt` **rc 0**. ⇒ ★**이 계급에 대한 그물은 «0»이고, 이 수정은 규칙이 아니라 습관이다.** 후속 제안으로 남겼다(하네스는 1줄보다 큰 결정이다).
9+
- 후속 추천: `docs/worklog/2026-09-19-merge-drops-deterministic-order.{md,json}` — 「검사기 출력 결정성을 잠가라」(effort M).
10+
211
## [2026-09-19] 적재 가능 집합을 «로더에서 읽을까» — ★**아니다, 재유도를 유지한다**(rustjava-loadable-set-from-loader-vs-rederive-decision)
312
- 무엇을: 채택 제안 `2026-09-18-named-exception-classes-are-loadable#p2`(worklog json 기록). ★**순수 결정 회차 — `.rs` 0줄 · `scripts/` 0줄.** 산출 = `docs/loadable-set-source-of-truth.md`(선례 = `docs/test-data-target-policy.md`).
413
- ★★**전제부터 확인했다 — 「4결함 중 3이 재유도」는 «참»이다.** 산문이 아니라 **고침 커밋 `89c2e83c` 에서** 갈랐다: ⑴`as_proto` 전용 → `list_proto` 3건 누락 ⑵한 `impl` 의 첫 `name:` 오귀속 ⑶짧은 이름 키 충돌 = **재유도(loadable) 3건** · ⑷줄 단위 스캔이 rustfmt 가 쪼갠 34건 누락 = ★**호출부 스캔(named) 1건**.

STATE.md

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

99
## 완료
10+
- [2026-09-19-partial-clone-blob-vs-absence-p0] ★★**`check-merge-dropped-symbols.py` 의 출력 순서를 고정했다 — 두 회차를 diff 할 수 있다.** 채택 제안 `2026-09-19-partial-clone-blob-vs-absence#p0`. ★**코드 1줄**(+주석 8줄) · `.rs` 0줄.
11+
**재현**: `origin/main` 판본 · `PYTHONHASHSEED=random` **10회** → 순서 **2종**(같은 6건) ⇒ 없는 차이가 diff 에 보였다.
12+
★★**제안 진단은 부정확**: `findings` 는 set 이 아니라 **list** 이고 이름은 이미 정렬돼 있었다 — set 4개 중 출력에 닿는 것은 ★**`changed`(경로) 하나**다. ⇒ 「print site」가 아니라 ★**출처에서** 정렬했다(`check()`**반환값**도 결정적이어야 하므로).
13+
**양방향**: 2종 ↔ **1종** ↔ 되돌리면 2종. ★찾은 것 불변(15줄 · 집합 일치 · rc 1).
14+
**대가**: 아무도 잠그지 않는다 — `scripts/` 테스트 하네스 **0** · 비결정성을 넣어도 파이썬 검사기 **4종 rc 0** · fmt **rc 0** ⇒ ★**그물 «0»**. 후속 제안으로 남겼다.
1015
- [rustjava-loadable-set-from-loader-vs-rederive-decision] ★★**적재 가능 집합은 «재유도»를 유지한다 — 로더에서 읽지 않는다.** 채택 제안 `2026-09-18-named-exception-classes-are-loadable#p2`. ★**순수 결정 · 코드 0줄** · 산출 = `docs/loadable-set-source-of-truth.md`.
1116
**전제 확인**: 「4중 3이 재유도」는 ****(고침 커밋 `89c2e83c` 에서 갈랐다 — loadable 3 · named 1).
1217
★★**그 1건이 결정한다**: `named`(코드가 무엇을 넘기는가)는 **로더가 답할 수 없다** ⇒ 로더 읽기는 **파서 하나를 없앨 뿐 파싱을 못 없앤다**(그 절반에서 형제 회차가 ★**4시간 31분** 뒤에 또 잡았다 — ★**「다른 함수 8종 41자리」**를 쓸어담는 앵커 함정이고, 세면 **33** · 맞는 답은 **0** 이다).
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"date": "2026-09-19",
3+
"taskId": "2026-09-19-partial-clone-blob-vs-absence-p0",
4+
"summary": "check-merge-dropped-symbols.py printed the same findings in a different order on different runs, so diffing two runs showed differences that were not there. One line: iterate the changed-paths set in sorted order.",
5+
"decision": "Sort at the source (the `changed` set iteration in check()) rather than at the print site in main(), so that what check() *returns* is ordered too, not only what main() happens to print.",
6+
"measurements": {
7+
"range_used": "e53b2142^..e53b2142 (8 merges, 6 dropped definitions, rc 1)",
8+
"before_distinct_orderings_over_10_runs": 2,
9+
"after_distinct_orderings_over_10_runs": 1,
10+
"mutation_distinct_orderings_over_10_runs": 2,
11+
"findings_unchanged": "15 output lines before and after; identical as a set; rc 1 both",
12+
"nondeterminism_sources_audited": 4,
13+
"nondeterminism_sources_reaching_output_order": 1,
14+
"note_on_the_proposal_text": "The proposal said findings are 'accumulated in a set'. They are not -- `findings` is a list, and names within one path were already sorted(). The single real source is `changed`, a set of paths."
15+
},
16+
"verification": {
17+
"bidirectional_mutation": "product call site, not a copy: removing sorted() from scripts/check-merge-dropped-symbols.py takes 10 runs from 1 distinct ordering to 2; restoring it returns to 1",
18+
"existing_guard_measured_under_the_mutation": "zero: all 4 python checkers rc 0 and cargo fmt --check rc 0 with the nondeterminism present; clippy and cargo test were not re-run because they do not read this script's output",
19+
"dod": "all 9 DoD commands rc 0"
20+
},
21+
"changes": [
22+
"scripts/check-merge-dropped-symbols.py: `for path in filter(None, changed)` -> `for path in sorted(filter(None, changed))`, plus an 8-line comment recording why and the measurement"
23+
],
24+
"issues": [
25+
"Nothing locks the ordering. There is no test harness for the scripts/ checkers in this repo, so a future edit can reintroduce the same nondeterminism silently. Recorded as proposal p0 rather than built, because adding one would be a larger round than the fix."
26+
],
27+
"adoptedProposals": [
28+
"2026-09-19-partial-clone-blob-vs-absence#p0"
29+
],
30+
"proposals": [
31+
{
32+
"title": "Lock the checkers' output determinism so it cannot silently come back",
33+
"plainSummary": "The ordering was just fixed, but nothing checks that it stays fixed, so the same problem can return without anyone noticing.",
34+
"userBenefit": "A future edit that reintroduces run-to-run variation fails in CI instead of costing the next round the same wasted comparison this one paid for.",
35+
"why": "Measured this round: removing the one-word fix from scripts/check-merge-dropped-symbols.py takes the same range from 1 distinct ordering over 10 runs back to 2, and every repo axis stays green -- fmt, clippy, cargo test and all four python checkers pass with the nondeterminism present. So the guard that exists today is zero. The repo has no test harness for scripts/ at all (0 test*.py files), which is why this was not simply added here: it needs a harness decision, not a line.",
36+
"tradeoff": "A harness for four standalone scripts is real weight -- a new CI job and a place for it to live -- and two runs under different PYTHONHASHSEED values is a slow way to assert an invariant a reader can see in one line. The cheaper variant is to assert sortedness directly rather than re-running, which does not catch a second source appearing elsewhere. Leaving it means the fix is a habit, not a rule.",
37+
"effort": "M",
38+
"target": "scripts/ (all four checkers), .github/workflows/rust.yml"
39+
}
40+
]
41+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# 2026-09-19 — `check-merge-dropped-symbols.py` now prints in a stable order
2+
3+
Adopts `2026-09-19-partial-clone-blob-vs-absence#p0` ("Sort the checker's findings so two runs can
4+
be compared").
5+
6+
## What was wrong
7+
8+
The checker printed the same findings in a different order on different runs, so diffing two runs
9+
showed differences that were not really there. Reproduced on `origin/main`'s own version, range
10+
`e53b2142^..e53b2142` (8 merges, 6 dropped definitions, rc 1): **ten runs under
11+
`PYTHONHASHSEED=random` produced exactly two distinct orderings**, 6 of one and 4 of the other. The
12+
two differ only in which path's block comes first — `tests/test_class_format.rs` or
13+
`test-data/src/indy/make_indy_fixtures.py`.
14+
15+
## Where it actually came from — the proposal's diagnosis was close but not exact
16+
17+
The proposal said findings are "accumulated in a set and printed in iteration order". They are not:
18+
`findings` is a **list**, and the names within a single path were **already** `sorted()`. Auditing
19+
all four sets in the file:
20+
21+
| set | reaches output order? |
22+
|---|---|
23+
| `found` in `symbols()` | no — consumed by `sorted()` at the diff site |
24+
| `names` in `excused()` | no — membership tests only (`name not in accounted`) |
25+
| `theirs_symbols - symbols(...)` | no — already wrapped in `sorted()` |
26+
| **`changed` in `check()`** | **yes** — a set of paths, iterated directly |
27+
28+
So **one** of four candidate sources reaches the output, and it is the path iteration, not the
29+
finding accumulation. The symptom the proposal described is real; the sentence naming its cause
30+
is not, and a fix aimed literally at "the print site" would have worked by accident.
31+
32+
## The change
33+
34+
One line in `check()`:
35+
36+
```python
37+
for path in sorted(filter(None, changed)):
38+
```
39+
40+
Sorted **at the source rather than at the print site**, because `findings` is also *returned* by
41+
`check()`. Sorting in `main()` would order what gets printed and leave the return value still
42+
dependent on hash order, which is a smaller fix wearing the same clothes.
43+
44+
## Verification
45+
46+
Bidirectional, in the product call site (not a copy of it):
47+
48+
| version | distinct orderings / 10 runs | rc |
49+
|---|---|---|
50+
| `origin/main` (before) | **2** | 1 |
51+
| with the fix | **1** | 1 |
52+
| fix removed again (mutation) | **2** | 1 |
53+
| restored | **1** | 1 |
54+
55+
Findings themselves are untouched: 15 output lines before and after, identical when both are
56+
sorted as sets, same rc. The fix changes order and nothing else.
57+
58+
## What this costs
59+
60+
- **The output no longer reflects traversal order.** Nothing reads it, so the practical cost is
61+
zero, but it is a real property that was removed rather than nothing at all.
62+
- **Nothing locks it.** This repo has **no test harness for `scripts/`** (0 `test*.py` files).
63+
Measured with the nondeterminism deliberately put back: all four python checkers exit 0
64+
(`check-worklog-json`, `check-dod-ci-parity`, `check-named-exception-classes-are-loadable`,
65+
`check-merge-dropped-symbols` itself) and `cargo fmt --check` exits 0. The two Rust axes not
66+
re-run under the mutation — `clippy` and `cargo test` — do not read this script's output at all,
67+
so the honest summary is that **the guard against this class of defect is zero**. This fix is a
68+
habit, not a rule, and a later edit can undo it silently. That is the one genuine gap this round
69+
leaves, and it is filed as the follow-up proposal rather than built, because a harness is a
70+
larger decision than a one-line ordering fix.
71+
- **Scope deliberately not widened.** The other three `scripts/` checkers were not audited for the
72+
same class of defect; that is part of the same follow-up.

scripts/check-merge-dropped-symbols.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,15 @@ def check(merge):
243243
accounted = excused(merge)
244244
if excuses_everything(accounted):
245245
return [], 0
246-
for path in filter(None, changed):
246+
# `sorted` so that two runs can be diffed. `changed` is a set of paths, and iterating it takes
247+
# Python's per-process randomised string hash order: measured on origin/main's version over
248+
# `e53b2142^..e53b2142`, ten runs under PYTHONHASHSEED=random printed the same six findings in
249+
# two different orders, differing only in which path's block came first. That cost the round
250+
# that found it real time -- a before/after diff read as a regression until the unchanged
251+
# version was shown to disagree with itself. Names *within* a path were already sorted below,
252+
# so the path order was the only axis left. Sorting here rather than at the print site fixes
253+
# the order of what `check()` returns, not just what main() happens to print.
254+
for path in sorted(filter(None, changed)):
247255
theirs_symbols = symbols(theirs, path)
248256
if theirs_symbols is None:
249257
continue

0 commit comments

Comments
 (0)