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
9 changes: 9 additions & 0 deletions REPORT.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
# REPORT
## [2026-09-19] 두 회차를 비교할 수 있게 «순서»를 고정했다 (2026-09-19-partial-clone-blob-vs-absence-p0)
- 무엇을: 채택 제안 `2026-09-19-partial-clone-blob-vs-absence#p0`. `scripts/check-merge-dropped-symbols.py` 가 **같은 결과를 회차마다 다른 순서로** 찍어, 두 회차를 diff 하면 ★**없는 차이가 보였다**. ★**코드 1줄**(+주석 8줄).
- ★**재현**: `origin/main` 판본 · 범위 `e53b2142^..e53b2142`(8머지 · 6드롭 · rc 1) · `PYTHONHASHSEED=random` **10회** → ★**서로 다른 순서 «2종»**(6회/4회). 차이는 ★**경로 블록의 선후 하나뿐**이다.
- ★★**제안의 «진단»은 정확하지 않았다 — 고치는 자리가 달라진다.** 제안은 「findings 가 set 에 모인다」고 했는데 ⑴`findings` 는 **list** 이고 ⑵한 경로 «안»의 이름은 **이미 `sorted()`** 였다. 파일의 set **4개**를 전수로 보면 출력 순서에 닿는 것은 ★**`changed`(경로 집합) 하나뿐**이다. ⇒ 「print site 에서 정렬」은 **우연히** 맞는 처방이었다.
- ★**그래서 «출처»에서 정렬했다** — `for path in sorted(filter(None, changed))`. `findings` 는 `check()` 가 **반환**도 하므로, `main()` 에서 정렬하면 **찍는 것만** 결정적이고 **반환값은 여전히** 해시 순서다.
- ★**양방향(제품 호출부 · 사본 아님)**: 전 **2종** ↔ 후 **1종** ↔ 되돌리면 **2종** ↔ 복원 **1종**. ★**찾은 것은 그대로다**: 출력 **15줄** 동일 · 집합으로 정렬하면 **완전 일치** · rc **1** 불변.
- ★★**대가 — 「잃는 것이 없다」가 아니다**: ⒜순회 순서라는 성질을 **없앴다**(읽는 곳은 없다) ⒝★**아무도 잠그지 않는다.** 이 repo 엔 `scripts/` 용 **테스트 하네스가 없다**(`test*.py` **0개**) — 비결정성을 **되돌려 놓고 재니** 파이썬 검사기 **4종 전건 rc 0** · `cargo fmt` **rc 0**. ⇒ ★**이 계급에 대한 그물은 «0»이고, 이 수정은 규칙이 아니라 습관이다.** 후속 제안으로 남겼다(하네스는 1줄보다 큰 결정이다).
- 후속 추천: `docs/worklog/2026-09-19-merge-drops-deterministic-order.{md,json}` — 「검사기 출력 결정성을 잠가라」(effort M).

## [2026-09-19] 적재 가능 집합을 «로더에서 읽을까» — ★**아니다, 재유도를 유지한다**(rustjava-loadable-set-from-loader-vs-rederive-decision)
- 무엇을: 채택 제안 `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`).
- ★★**전제부터 확인했다 — 「4결함 중 3이 재유도」는 «참»이다.** 산문이 아니라 **고침 커밋 `89c2e83c` 에서** 갈랐다: ⑴`as_proto` 전용 → `list_proto` 3건 누락 ⑵한 `impl` 의 첫 `name:` 오귀속 ⑶짧은 이름 키 충돌 = **재유도(loadable) 3건** · ⑷줄 단위 스캔이 rustfmt 가 쪼갠 34건 누락 = ★**호출부 스캔(named) 1건**.
Expand Down
5 changes: 5 additions & 0 deletions STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
(둘 다 이것보다 오래됐고 MERGEABLE/CONFLICTING 처분이 이미 걸려 있다). 겹침은 전부 **append 형 합집합**이라 해소는 기계적이다)

## 완료
- [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줄.
★**재현**: `origin/main` 판본 · `PYTHONHASHSEED=random` **10회** → 순서 **2종**(같은 6건) ⇒ 없는 차이가 diff 에 보였다.
★★**제안 진단은 부정확**: `findings` 는 set 이 아니라 **list** 이고 이름은 이미 정렬돼 있었다 — set 4개 중 출력에 닿는 것은 ★**`changed`(경로) 하나**다. ⇒ 「print site」가 아니라 ★**출처에서** 정렬했다(`check()` 의 **반환값**도 결정적이어야 하므로).
★**양방향**: 2종 ↔ **1종** ↔ 되돌리면 2종. ★찾은 것 불변(15줄 · 집합 일치 · rc 1).
★**대가**: 아무도 잠그지 않는다 — `scripts/` 테스트 하네스 **0** · 비결정성을 넣어도 파이썬 검사기 **4종 rc 0** · fmt **rc 0** ⇒ ★**그물 «0»**. 후속 제안으로 남겼다.
- [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`.
★**전제 확인**: 「4중 3이 재유도」는 **참**(고침 커밋 `89c2e83c` 에서 갈랐다 — loadable 3 · named 1).
★★**그 1건이 결정한다**: `named`(코드가 무엇을 넘기는가)는 **로더가 답할 수 없다** ⇒ 로더 읽기는 **파서 하나를 없앨 뿐 파싱을 못 없앤다**(그 절반에서 형제 회차가 ★**4시간 31분** 뒤에 또 잡았다 — ★**「다른 함수 8종 41자리」**를 쓸어담는 앵커 함정이고, 세면 **33** · 맞는 답은 **0** 이다).
Expand Down
41 changes: 41 additions & 0 deletions docs/worklog/2026-09-19-merge-drops-deterministic-order.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"date": "2026-09-19",
"taskId": "2026-09-19-partial-clone-blob-vs-absence-p0",
"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.",
"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.",
"measurements": {
"range_used": "e53b2142^..e53b2142 (8 merges, 6 dropped definitions, rc 1)",
"before_distinct_orderings_over_10_runs": 2,
"after_distinct_orderings_over_10_runs": 1,
"mutation_distinct_orderings_over_10_runs": 2,
"findings_unchanged": "15 output lines before and after; identical as a set; rc 1 both",
"nondeterminism_sources_audited": 4,
"nondeterminism_sources_reaching_output_order": 1,
"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."
},
"verification": {
"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",
"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",
"dod": "all 9 DoD commands rc 0"
},
"changes": [
"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"
],
"issues": [
"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."
],
"adoptedProposals": [
"2026-09-19-partial-clone-blob-vs-absence#p0"
],
"proposals": [
{
"title": "Lock the checkers' output determinism so it cannot silently come back",
"plainSummary": "The ordering was just fixed, but nothing checks that it stays fixed, so the same problem can return without anyone noticing.",
"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.",
"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.",
"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.",
"effort": "M",
"target": "scripts/ (all four checkers), .github/workflows/rust.yml"
}
]
}
72 changes: 72 additions & 0 deletions docs/worklog/2026-09-19-merge-drops-deterministic-order.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# 2026-09-19 — `check-merge-dropped-symbols.py` now prints in a stable order

Adopts `2026-09-19-partial-clone-blob-vs-absence#p0` ("Sort the checker's findings so two runs can
be compared").

## What was wrong

The checker printed the same findings in a different order on different runs, so diffing two runs
showed differences that were not really there. Reproduced on `origin/main`'s own version, range
`e53b2142^..e53b2142` (8 merges, 6 dropped definitions, rc 1): **ten runs under
`PYTHONHASHSEED=random` produced exactly two distinct orderings**, 6 of one and 4 of the other. The
two differ only in which path's block comes first — `tests/test_class_format.rs` or
`test-data/src/indy/make_indy_fixtures.py`.

## Where it actually came from — the proposal's diagnosis was close but not exact

The proposal said findings are "accumulated in a set and printed in iteration order". They are not:
`findings` is a **list**, and the names within a single path were **already** `sorted()`. Auditing
all four sets in the file:

| set | reaches output order? |
|---|---|
| `found` in `symbols()` | no — consumed by `sorted()` at the diff site |
| `names` in `excused()` | no — membership tests only (`name not in accounted`) |
| `theirs_symbols - symbols(...)` | no — already wrapped in `sorted()` |
| **`changed` in `check()`** | **yes** — a set of paths, iterated directly |

So **one** of four candidate sources reaches the output, and it is the path iteration, not the
finding accumulation. The symptom the proposal described is real; the sentence naming its cause
is not, and a fix aimed literally at "the print site" would have worked by accident.

## The change

One line in `check()`:

```python
for path in sorted(filter(None, changed)):
```

Sorted **at the source rather than at the print site**, because `findings` is also *returned* by
`check()`. Sorting in `main()` would order what gets printed and leave the return value still
dependent on hash order, which is a smaller fix wearing the same clothes.

## Verification

Bidirectional, in the product call site (not a copy of it):

| version | distinct orderings / 10 runs | rc |
|---|---|---|
| `origin/main` (before) | **2** | 1 |
| with the fix | **1** | 1 |
| fix removed again (mutation) | **2** | 1 |
| restored | **1** | 1 |

Findings themselves are untouched: 15 output lines before and after, identical when both are
sorted as sets, same rc. The fix changes order and nothing else.

## What this costs

- **The output no longer reflects traversal order.** Nothing reads it, so the practical cost is
zero, but it is a real property that was removed rather than nothing at all.
- **Nothing locks it.** This repo has **no test harness for `scripts/`** (0 `test*.py` files).
Measured with the nondeterminism deliberately put back: all four python checkers exit 0
(`check-worklog-json`, `check-dod-ci-parity`, `check-named-exception-classes-are-loadable`,
`check-merge-dropped-symbols` itself) and `cargo fmt --check` exits 0. The two Rust axes not
re-run under the mutation — `clippy` and `cargo test` — do not read this script's output at all,
so the honest summary is that **the guard against this class of defect is zero**. This fix is a
habit, not a rule, and a later edit can undo it silently. That is the one genuine gap this round
leaves, and it is filed as the follow-up proposal rather than built, because a harness is a
larger decision than a one-line ordering fix.
- **Scope deliberately not widened.** The other three `scripts/` checkers were not audited for the
same class of defect; that is part of the same follow-up.
10 changes: 9 additions & 1 deletion scripts/check-merge-dropped-symbols.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,15 @@ def check(merge):
accounted = excused(merge)
if excuses_everything(accounted):
return [], 0
for path in filter(None, changed):
# `sorted` so that two runs can be diffed. `changed` is a set of paths, and iterating it takes
# Python's per-process randomised string hash order: measured on origin/main's version over
# `e53b2142^..e53b2142`, ten runs under PYTHONHASHSEED=random printed the same six findings in
# two different orders, differing only in which path's block came first. That cost the round
# that found it real time -- a before/after diff read as a regression until the unchanged
# version was shown to disagree with itself. Names *within* a path were already sorted below,
# so the path order was the only axis left. Sorting here rather than at the print site fixes
# the order of what `check()` returns, not just what main() happens to print.
for path in sorted(filter(None, changed)):
theirs_symbols = symbols(theirs, path)
if theirs_symbols is None:
continue
Expand Down
Loading