Skip to content

Commit 8a633bc

Browse files
jun0claude
andcommitted
[rustjava-checker-output-determinism-has-no-guard] feat(scripts): 검사기 출력 순서를 잠근다 — 습관을 규칙으로
채택 제안 `2026-09-19-merge-drops-deterministic-order#p0`. 불변식 한 줄: `scripts/*.py` 의 어떤 `for`·컴프리헨션도 `sorted(...)` 밖에서 set 을 순회하지 않는다. AST 로 단언한다(재실행 0). 왜 정적인가 — 제안이 제시한 두 대안을 둘 다 쓰지 않았다: * 두 PYTHONHASHSEED 재실행: set 은 해시 순서가 정렬 순서와 «다를 때만» 보이므로 사고의 경로 2개에서 2회 비교는 회차당 약 절반 눈을 감는다. * `assert sorted`: 제안 자신이 적은 약점 — 다른 곳의 두 번째 출처를 못 잡는다. 그물 0 재현: 비결정성을 되돌린 채 파이썬 검사기 4종 rc 0 · cargo fmt rc 0. 양방향 2×2(제품 호출부): M1 check-merge-dropped-symbols.py:254 제거 → rc 1 ↔ 복원 rc 0 · M2 다른 파일 check-dod-ci-parity.py:215 → rc 1 ↔ 복원 rc 0. 정상 = `7 script(s): 0 unordered iteration(s)` · 0.06초. 대가: 새 CI 잡 하나(제안이 real weight 라 부른 그것) · 측정된 사각 1건 — 튜플 언패킹으로 받은 set 은 못 본다(`ci_runs, ci_tcs = parse_ci(...)` 에 정렬 없는 순회를 넣으면 rc 0 통과). 후속 제안 #p0 으로 남겼다. DoD 10명령 rc 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 5d732b1 commit 8a633bc

7 files changed

Lines changed: 253 additions & 0 deletions

File tree

‎.github/workflows/rust.yml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,15 @@ jobs:
8181
- uses: actions/checkout@v7
8282
- run: python3 scripts/check-dod-ci-parity.py
8383

84+
# a checker that iterates a set prints its findings in a different order on different runs, so two
85+
# rounds cannot diff their output — and every other axis stays green while it does. This reads the
86+
# checkers' AST rather than re-running them. One runner, not the matrix.
87+
script_output_order:
88+
runs-on: ubuntu-latest
89+
steps:
90+
- uses: actions/checkout@v7
91+
- run: python3 scripts/check-script-output-order.py
92+
8493
# Jvm::exception unwraps new_class(), so naming a class the loader cannot resolve panics instead
8594
# of throwing. This compares the names against the registered protos (see the script's docstring
8695
# for what it cannot see). One runner, not the matrix.

‎CLAUDE.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
python3 scripts/check-dod-ci-parity.py
3434
python3 scripts/check-named-exception-classes-are-loadable.py
3535
python3 scripts/check-merge-dropped-symbols.py
36+
python3 scripts/check-script-output-order.py
3637
```
3738
★★**이 블록은 이제 «기계가 지킨다» — `scripts/check-dod-ci-parity.py`(CI job `dod_parity`)가
3839
이 코드블록과 `rust.yml` 을 «각각 파싱해» 대칭차를 낸다.** 어긋나면 그 자리에서 red 다.

‎REPORT.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
11
# REPORT
2+
## [2026-09-20] 검사기 출력 순서를 «잠갔다» — 습관을 규칙으로 (rustjava-checker-output-determinism-has-no-guard)
3+
- 무엇을: 채택 제안 `2026-09-19-merge-drops-deterministic-order#p0`. `scripts/check-script-output-order.py` 신설 — ★**`scripts/*.py` 의 어떤 `for`·컴프리헨션도 `sorted(...)` 밖에서 set 을 순회하지 않는다**를 **AST 로** 단언한다. CI 잡 `script_output_order` 1개 + DoD 10번째 줄.
4+
- 왜: 전 회차가 한 단어로 고친 비결정성을 ★**아무도 잠그지 않았다**. ★**「그물 0」을 이 트리에서 재현했다** — 제품 호출부에 비결정성을 되돌려 놓고 재니 파이썬 검사기 **4종 전건 rc 0** · `cargo fmt` **rc 0**. ★해소 여부도 먼저 쟀다: `scripts/`·`rust.yml` 최종 커밋은 **`35f34797`**(그 수정 자신) · 추적 파일의 `PYTHONHASHSEED` 는 **산문과 주석뿐**이다.
5+
- ★**왜 «정적»인가 — 제안이 제시한 두 대안을 둘 다 쓰지 않았다.** ⒜**두 `PYTHONHASHSEED` 재실행**: set 은 해시 순서가 정렬 순서와 «다를 때만» 보이므로, 사고의 경로 2개에서 2회 비교는 ★**회차당 약 절반 눈을 감는다**(느린 것이 문제가 아니다). ⒝**`assert sorted`**: 제안 자신이 적은 약점 — 「다른 곳의 두 번째 출처를 못 잡는다」. ⇒ 정적 축은 **둘 다 갖지 않는다**(재실행 0 · 새 출처 포착을 M2 로 실증).
6+
- ★**양방향 2×2**(전부 **제품 호출부** · 사본 아님): **M1** `check-merge-dropped-symbols.py:254` 의 `sorted()` 제거 → **rc 1**(파일·줄 지목) ↔ 복원 **rc 0** · **M2** ★**다른 파일의 «새» 출처** `check-dod-ci-parity.py:215` `sorted(only_ci)` → `only_ci` → **rc 1** ↔ 복원 **rc 0**. 정상 = `7 script(s): 0 unordered iteration(s)` · **0.06초**.
7+
- ★**대가**: ⒜**새 CI 잡 하나** — 제안이 「real weight」라 부른 그것이고 값을 깎지 않았다(툴체인 없는 checkout + `python3` = 기존 doc 잡 4개와 같은 형상). ⒝★**측정된 사각 1건** — 튜플 언패킹으로 받은 set 은 못 본다. 오늘 실제로 하나 있다(`ci_runs, ci_tcs = parse_ci(...)`): 거기에 정렬 없는 `for t in ci_tcs:` 를 넣으니 잠금이 ★**rc 0 으로 통과**했다. 지금 틀린 곳은 없지만 **구멍은 진짜다**. ⒞dict 는 안 본다(삽입 순서 · set 이 먹이면 set 에서 잡힌다).
8+
- ★**제안보다 넓힌 곳 하나**: `target` 은 「scripts/ (all four checkers)」인데 glob 을 **`scripts/*.py`** 로 썼다 — 한 단어 차이이고 포함된 **7개 전건이 오늘 통과**한다.
9+
- 검증: DoD 10명령 rc 0 · `dod_parity` 「명령 9개 · toolchain 2개로 둘 다 일치」.
10+
- ★후속 추천: **튜플 언패킹 반환으로 오는 set 을 따라가라**(S · 위 ⒝의 그 구멍). 상세 = `docs/worklog/2026-09-20-lock-script-output-order.{md,json}`.
11+
212
## [2026-09-19] 오류 경로의 «또 하나»는 `java/lang/String` 이고 — ★**재귀한다**(rustjava-error-path-needs-java-lang-string-measure-first)
313
- 무엇을: 채택 제안 `2026-09-19-fallback-class-absence-fails-at-construction#p0`(worklog json 기록). ★**제안의 조건이 「측정이 먼저」였고 그대로 했다** — 선행 회차가 String 에 대해 **아무것도 주장하지 않았고**(자기 worklog 에 「실측 아님」이라 적었다) 셋 중 무엇인지가 열려 있었다.
414
- ★★**답 = ⒜ 재귀한다**(⒝ 깨끗한 실패도, ⒞ 이미 상주도 아니다). 하니스로 String 을 숨겨 이분법으로 경계를 찾았다: 상한 **116 생존 ↔ 117 `stack overflow, aborting`(SIGABRT · rc 134)** · ★**양쪽 2회씩 재현** · ★**상한 100000 도 abort** ⇒ 상한은 «하니스가 양보하는 지점»이지 **바닥이 아니다**.

‎STATE.md‎

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

99
## 완료
10+
- [rustjava-checker-output-determinism-has-no-guard] ★★**검사기 출력 순서를 잠갔다 — 습관을 규칙으로.** 채택 제안 `2026-09-19-merge-drops-deterministic-order#p0` · 신설 `scripts/check-script-output-order.py`(AST) + CI 잡 `script_output_order` + DoD 10번째 줄.
11+
★**불변식 한 줄**: `scripts/*.py` 의 어떤 `for`·컴프리헨션도 **`sorted(...)` 밖에서 set 을 순회하지 않는다**.
12+
★**「그물 0」 재현**: 비결정성을 되돌린 채 파이썬 검사기 **4종 rc 0** · `cargo fmt` **rc 0**. ★해소 여부 선행 확인 — `scripts/`·`rust.yml` 최종 커밋은 **`35f34797`**(그 수정 자신).
13+
★**정적을 고른 이유**: 두 `PYTHONHASHSEED` 재실행은 ★**회차당 약 절반 눈을 감고**(해시 순서 = 정렬 순서면 무증상), `assert sorted` 는 **다른 곳의 새 출처를 못 잡는다**(제안 자신의 약점 기술).
14+
★**양방향 2×2**(제품 호출부): M1 `check-merge-dropped-symbols.py:254` 제거 → rc 1 ↔ 복원 rc 0 · M2 ★**다른 파일** `check-dod-ci-parity.py:215` → rc 1 ↔ 복원 rc 0. 정상 `7 script(s): 0` · 0.06초.
15+
★**측정된 사각**: 튜플 언패킹 반환 set 은 못 본다 — `ci_runs, ci_tcs = parse_ci(...)` 에 정렬 없는 순회를 넣으니 ★**rc 0 통과**. 후속 제안 `#p0`.
1016
- [rustjava-error-path-needs-java-lang-string-measure-first] ★★**오류 경로의 또 하나 `java/lang/String` — ⒜ «재귀한다»로 확정하고 선재 확인을 넣었다.** 채택 제안 `2026-09-19-fallback-class-absence-fails-at-construction#p0`. ★제안의 조건이 「측정이 먼저」였고 그대로 했다.
1117
★**실측**: 상한 **116 생존 ↔ 117 SIGABRT «stack overflow, aborting»**(양쪽 2회 재현) · ★**상한 100000 도 abort** ⇒ 바닥이 없다.
1218
★**⒞ 아님을 구조로**: `bootstrap_classes` **6개에 String 없음** · `from_rust_class` 는 이름을 **`[B`(nameBytes)** 로 넣는다 ⇒ 처음 필요한 곳은 프로퍼티 루프.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"date": "2026-09-20",
3+
"taskId": "rustjava-checker-output-determinism-has-no-guard",
4+
"summary": "Adopted 2026-09-19-merge-drops-deterministic-order#p0. scripts/check-script-output-order.py refuses any `for`/comprehension in scripts/*.py that iterates a set outside sorted(). Static (AST), no re-runs, one new CI job.",
5+
"decision": "Assert the invariant statically rather than re-running each checker under two PYTHONHASHSEED values. A set is only *visibly* unordered when its hash order differs from the sorted one, so a two-run comparison is a coin flip per run -- with the two paths of the 2026-09-19 incident it agrees with itself about half the time. The AST pass does not need the bug to be observable, costs no re-run, and fires on a new set anywhere in scripts/, which is the objection the proposal itself raised against the cheap `assert sorted` variant.",
6+
"measurements": {
7+
"guard_before": "zero, re-measured on this tree: with the nondeterminism restored at the product call site, check-worklog-json / check-dod-ci-parity / check-named-exception-classes-are-loadable / check-merge-dropped-symbols all rc 0 and `cargo fmt --all -- --check` rc 0",
8+
"already_fixed_elsewhere": "no: last commit touching scripts/ or rust.yml is 35f34797, the one-word fix itself; PYTHONHASHSEED appears in no lock, only in prose and the fixed script's comment",
9+
"scope_widened_from_proposal": "proposal said `scripts/ (all four checkers)`; the glob is scripts/*.py (7 files incl. the two surveys and this script) because the wider glob costs one word and all 7 pass today",
10+
"unordered_iterations_today": 0,
11+
"ci_cost": "one new job (script_output_order), checkout + python3, no toolchain -- same shape as the other four doc/script jobs",
12+
"runtime": "0.06s over 7 files"
13+
},
14+
"verification": {
15+
"bidirectional_M1": "product call site, not a copy: removing sorted() from scripts/check-merge-dropped-symbols.py:254 -> rc 1, names that file and line; restoring -> rc 0",
16+
"bidirectional_M2": "a *new* source in a different file: `for c in sorted(only_ci)` -> `for c in only_ci` in check-dod-ci-parity.py:215 -> rc 1, names that file and line; restoring -> rc 0",
17+
"green_baseline": "rc 0, `7 script(s): 0 unordered iteration(s) that could reach output`",
18+
"blind_spot_measured": "a set bound by tuple-unpacking a call return is missed: injecting an unsorted `for t in ci_tcs:` into check-dod-ci-parity.py leaves the lock rc 0. Recorded as proposal p0, not papered over.",
19+
"dod": "all 10 DoD commands rc 0; dod_parity reports 9 commands / 2 toolchains matching on both sides"
20+
},
21+
"changes": [
22+
"scripts/check-script-output-order.py: new, ~60 lines of ast walk (set-valued names/functions to a fixpoint, then every For/comprehension iterable)",
23+
".github/workflows/rust.yml: new job script_output_order",
24+
"CLAUDE.md: the DoD block gains the 10th command (dod_parity requires both sides to move together)"
25+
],
26+
"issues": [
27+
"Set-ness is inferred syntactically, so a set arriving by tuple-unpacked call return, import or parameter is invisible to the pass. One such name exists today and is named in the docstring."
28+
],
29+
"adoptedProposals": [
30+
"2026-09-19-merge-drops-deterministic-order#p0"
31+
],
32+
"proposals": [
33+
{
34+
"title": "Close the one measured hole in the output-order lock: sets that arrive by tuple-unpacked call return",
35+
"plainSummary": "The new lock cannot see a set that was handed back from a function through a multiple assignment, so one kind of unordered output could still slip past it.",
36+
"userBenefit": "The lock stops having a shape it is known to miss, so a round can rely on a green run meaning what it says instead of having to remember the exception.",
37+
"why": "Measured this round, not assumed: `ci_runs, ci_tcs = parse_ci(...)` in check-dod-ci-parity.py binds a real set the pass does not recognise, and injecting an unsorted `for t in ci_tcs:` leaves the lock rc 0. Nothing is wrong there today -- every read of it goes through sorted() or a set operator -- but the hole is real and it is in the file the lock is meant to protect.",
38+
"tradeoff": "Following a value out of a call return means either inferring one return shape per function (a second, smaller fixpoint over tuple returns) or annotating the two or three call sites, and both are more analysis than the incident that started this needed. Leaving it means the docstring carries a named exception, which is the thing that rots.",
39+
"effort": "S",
40+
"target": "scripts/check-script-output-order.py"
41+
}
42+
]
43+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# 2026-09-20 — 검사기 출력 순서를 «잠갔다»
2+
3+
채택 제안 `2026-09-19-merge-drops-deterministic-order#p0`.
4+
티켓 `rustjava-checker-output-determinism-has-no-guard`.
5+
6+
## 먼저 잰 것 — 아직 안 고쳐졌나
7+
8+
- `scripts/`·`rust.yml` 을 만진 마지막 커밋은 **`35f34797`**, 그 한 줄 수정 자신이다. 잠금은 없다.
9+
- 추적 파일에서 `PYTHONHASHSEED` 는 **산문(REPORT·STATE·worklog)과 고쳐진 스크립트의 주석에만** 있다 —
10+
어떤 검사에도 없다.
11+
- 그물이 «0» 이라는 제안의 주장을 **이 트리에서 재현**: 제품 호출부에 비결정성을 되돌려 놓고 재니
12+
파이썬 검사기 **4종 전건 rc 0** · `cargo fmt --all -- --check` **rc 0**.
13+
14+
## 무엇을 했나
15+
16+
`scripts/check-script-output-order.py` — **`scripts/*.py` 의 어떤 `for`·컴프리헨션도
17+
`sorted(...)` 밖에서 set 을 순회하지 않는다**를 AST 로 단언한다. 새 CI 잡 `script_output_order`
18+
하나 + DoD 블록 10번째 줄(대칭 잠금이 둘을 함께 움직이게 한다).
19+
20+
## 왜 «정적»인가 — 제안이 제시한 두 대안을 다 쓰지 않았다
21+
22+
- **두 `PYTHONHASHSEED` 로 재실행**: set 은 해시 순서가 정렬 순서와 «다를 때만» 눈에 보인다.
23+
2026-09-19 사고의 경로 2개에서는 그 확률이 약 절반이라, 2회 비교는 회차당 동전 던지기다.
24+
느린 것이 문제가 아니라 **절반은 눈을 감는다**는 것이 문제다.
25+
- **`assert sorted` 로 직접 단언**: 제안 자신이 적은 약점 그대로 — 「다른 곳에 생긴 두 번째 출처를
26+
못 잡는다」. 정적 축은 그 두 약점을 **둘 다** 갖지 않는다(재실행 0 · 새 출처 포착 · M2 로 실증).
27+
28+
## 양방향 — 두 개악, 두 복원 (전부 제품 호출부 · 사본 아님)
29+
30+
| | 개악 | 결과 |
31+
|---|---|---|
32+
| M1 | `check-merge-dropped-symbols.py:254` 의 `sorted()` 제거 | **rc 1** · 그 파일·줄을 지목 → 복원 **rc 0** |
33+
| M2 | `check-dod-ci-parity.py:215` `for c in sorted(only_ci)` → `for c in only_ci` (★**다른 파일의 새 출처**) | **rc 1** · 그 파일·줄을 지목 → 복원 **rc 0** |
34+
35+
정상 = `7 script(s): 0 unordered iteration(s) that could reach output` · rc 0 · 0.06초.
36+
37+
## 대가 — 숨기지 않는다
38+
39+
- **새 CI 잡 하나**. 제안이 「real weight」라고 부른 그것이고, 값을 깎지 않았다(툴체인 없는
40+
checkout + `python3` 두 줄 = 기존 doc 잡 4개와 같은 형상).
41+
- ★**측정된 사각 1건**: 튜플 언패킹으로 함수 반환을 받은 set 은 이 패스가 못 본다.
42+
오늘 실제로 하나 있다 — `ci_runs, ci_tcs = parse_ci(...)`. 거기에 정렬 없는 `for t in ci_tcs:` 를
43+
넣어 보니 잠금은 **rc 0** 으로 통과했다. 지금 그 이름은 전부 `sorted()`/집합 연산으로만 읽히므로
44+
틀린 곳은 없지만, **구멍은 진짜다.** 후속 제안 `#p0` 으로 남겼다(문서에만 두면 썩는 계급이다).
45+
- **dict 는 안 본다**: 삽입 순서를 지키므로 결정성은 그것을 만든 쪽에 달렸고, set 이 dict 를 먹이면
46+
set 에서 잡힌다.
47+
48+
## 제안 문면보다 넓힌 곳 한 군데
49+
50+
제안의 `target` 은 「scripts/ (all four checkers)」였는데 glob 을 `scripts/*.py` 로 썼다 —
51+
한 단어 차이이고, 포함된 7개(감사·조사 스크립트 2개 + 이 파일 포함) **전건이 오늘 통과**한다.
52+
회차 간 diff 를 하는 것은 검사기만이 아니다.

0 commit comments

Comments
 (0)