diff --git a/REPORT.md b/REPORT.md index 15232f05..e7dceb24 100644 --- a/REPORT.md +++ b/REPORT.md @@ -41,6 +41,31 @@ ★그래도 적는 이유: ★**제안이 걱정한 드리프트가 «실재»한다는 첫 직접 증거**다. - 검증: `cargo test --all` **572 / 0 failed / 1 ignored**(doc 주석만 바꿔 **불변**) · DoD 7명령 rc=0. - 후속 추천: 루트 5건이 **왜** 재현되지 않는지 규명(M) — 상세 = `docs/worklog/2026-09-17-javac-fixture-provenance-verified.md`. +## [2026-09-17] `ClassFileError` 가 «원인»을 실어야 하는가 — ★**판정: 할 값 있다. 단 제안의 이름·이유·범위가 셋 다 틀렸다** (rustjava-adopt-cp-tag-passthrough-detectable-p1) +- 무엇을: 낱말이 **`Decide`** 인 제안을 **판정**했다. ★**코드 변경은 «틀린 주석 한 곳» 정정뿐** — 구현은 후속으로 넘겼다. +- 왜: 채택 제안 `2026-09-16-cp-tag-passthrough-detectable#p1`. +- 사용자 영향: 없다(판정). ★바뀐 것은 ★**착지한 트리가 «거짓 전제»를 나르지 않게 된 것**이다. +- ★★**제안의 «역사»가 거짓이다 — 두 겹으로**: ⑴`822504b` 는 `classfile/src/error.rs` 를 **«자르지» 않고 «만들었다»** + (`new file` · 내용이 지금과 **동일한 2변형**) ⑵그 이전에는 `ClassInfo::parse` 가 **`Option`** 를 돌려줬다 + (실패에 정보 **0**) ⇒ ★**그 커밋은 «후퇴»가 아니라 «개선»이었고, 「again」·「restore」는 성립하지 않는다.** +- ★★**「upstream 이 해야 한다」도 거짓**: `upstream/main` 기준 **5커밋 뒤**이고 그중 이 파일들을 만지는 것 **0건** · + upstream 이 `error.rs` 를 만진 커밋은 **1건(생성)** 뿐 · ★**우리는 이미 이 crate 에서 크게 갈렸다** + (`constant_pool.rs` **+211/−6** · `validation.rs` **+137/−0** · `attribute.rs` **+129/−3** · `opcode.rs` **+83/−7**). + ※`AGENTS.md` 의 read-only 규율은 **upstream 으로 «보내는 것»**을 금할 뿐 로컬 변경을 금하지 않는다. +- ★★**ⓑ 설계는 «한 enum 건너» 이미 증명돼 있다**: `ClassDefinitionError::UnsupportedFeature(&'static str)` 가 + **5곳**에서 쓰이며 `"ldc of a method handle"` 같은 문장을 낸다 ⇒ ★**새 발명이 아니라 «일관성 회복»**이고, 모양도 자명하다. +- ★**이득(과장하지 않는다)**: kind-only 단언 **8곳**이 원인을 이름 부를 수 있게 되고, 참조 JVM 격차가 줄어든다 + (OpenJDK 는 `Multiple BootstrapMethods attributes…`·`argument_index 65535 has bad constant type` 를 내는데 우리는 전부 `Invalid class file`). + ★**그러나 「픽스처보다 강한 자물쇠」는 «절반만» 참이다** — 원인은 「어느 검사가 울렸나」를, 픽스처는 「그 검사가 관측 가능한가」를 잠근다. + ★**증거**: 직전 `-fix` 가 찾은 구멍(신원 4축 중 3축 미관측)은 **링크 축**이라 원인을 실었어도 **안 잡혔다**. +- ★★**비용 — 제안의 `target: classfile/src/error.rs` 는 틀렸다(1파일이 아니라 3층)**: 평탄화가 아래로 두 번 더 일어난다 + (`InvalidFormat` → `ClassDefinitionError::InvalidClassFile`(From 이 원인을 버린다) → ★**하드코딩 문자열 2곳**). + ⇒ ★**`error.rs` 만 고치면 관측 변화가 «0»** 이다. ★**진짜 비용은 `validate_class` 의 8항 `||` 사슬을 쪼개는 것**이고, + 그것은 이 티켓이 **금지한 리팩터**다 ⇒ ★**여기서 구현하지 않고 «범위를 바로잡아» 넘겼다.** +- 검증: `cargo test --all` **572 / 0 failed / 1 ignored**(주석만 바꿔 불변) · DoD 7명령 rc=0. +- 후속 추천: 원인을 **3층에 관통**시키고 `validate_class` 를 **검사마다 반환**으로 쪼갠다(M) — + ★「⑴만 하고 멈추면 관측 변화 0 · ⑷ 없이 하면 평탄함이 «이사»할 뿐」까지 제안에 적었다. + 상세 = `docs/worklog/2026-09-17-classfile-error-cause-decision.md`. ## [2026-09-17] 신원 4축을 «각각» 관측 가능하게 했다 — 감사의 「고칠 것이 없다」를 정정한다 (rustjava-adopt-cp-tag-passthrough-detectable-p0-fix) - 무엇을: `string_concat.rs` 의 부트스트랩 신원 **4축**(kind·class·name·descriptor) 중 ★**3축이 «관측되지 않고» 있었다** — 근접 실패 픽스처가 **class 축 하나**뿐이었기 때문이다. 나머지 3축의 픽스처를 만들었다. ★**제품 코드 무접촉.** diff --git a/STATE.md b/STATE.md index 3c297a14..1d37cacf 100644 --- a/STATE.md +++ b/STATE.md @@ -79,6 +79,43 @@ ★★**게이트③ 2회차 — 형제 #59 가 그 사이 착지(`66bc49e8`)해 base 를 다시 당겼다.** 충돌은 또 **원장 2파일뿐**(코드 충돌 0). ★**이번엔 시간순이 «뒤집혔다»** — main 쪽 항목(`3b04712e` 06:52)이 이 회차(`38c02a2d` 05:22)보다 **뒤**라 **위**에 얹었다. ★두 번의 base 당김을 거쳐도 이 PR 의 기여 numstat 은 **불변**(`94/0` 검증 스크립트 · `14/2` 핀 테스트 · worklog 2건). +- [rustjava-adopt-cp-tag-passthrough-detectable-p1] ★★**판정 — `ClassFileError` 에 «원인»을 실을 값은 있다. 단 제안의 이름·이유·범위가 «셋 다» 틀렸다.** + 채택 제안 `2026-09-16-cp-tag-passthrough-detectable#p1`(worklog json `adoptedProposals` 기록). 낱말이 **`Decide`** 다 + ⇒ ★**코드 변경은 «틀린 주석 한 곳» 정정뿐** · 구현은 **범위를 바로잡아 후속으로** 넘겼다. + ★★**⑴역사가 거짓이다**: `822504b` 는 `error.rs` 를 **«자르지» 않고 «만들었다»**(`new file` · 지금과 동일한 2변형)이고, + 그 이전 `ClassInfo::parse` 는 **`Option`**(실패에 정보 **0** · `.unwrap()` 투성이)였다 ⇒ ★**그 커밋은 «개선»이었다.** + ⇒ ★**「carry a cause **again**」·「restoring」은 성립하지 않는다 — 이 리니지에 원인이 실렸던 시기는 «없다».** + ★★**⑵「upstream 이 해야 한다」도 거짓**: `upstream/main` 기준 **5커밋 뒤** · 그중 이 파일들을 만지는 것 **0건** · + upstream 의 `error.rs` 접촉은 **1건(생성)** 뿐(이 crate 에서 가장 안정된 파일) · ★**우리는 이미 이 crate 에서 크게 갈렸다** + (`constant_pool.rs` +211/−6 · `validation.rs` +137/−0 · `attribute.rs` +129/−3 · `opcode.rs` +83/−7). + ※`AGENTS.md` read-only 는 **upstream 으로 «보내는 것»** 금지이지 로컬 변경 금지가 아니다. + ★★**⑶범위도 틀렸다 — `target: classfile/src/error.rs` 는 1파일인데 평탄화는 «3층»이다**: + `InvalidFormat`(생산 9곳) → `ClassDefinitionError::InvalidClassFile`(**From 이 원인을 버린다**) → + ★**`"Invalid class file"` 하드코딩 2곳**(`src/runtime.rs:189` · `test-utils/src/lib.rs:334`). + ⇒ ★**`error.rs` 만 고치면 «관측 변화 0»** — 아무도 원인을 넣지 않고 아무도 읽지 않는, 이 저장소가 규탄하는 그 형태다. + ★★**진짜 비용은 `validate_class` 의 «8항 `||` 사슬»을 쪼개는 것**이다(원인이 갈리는 유일한 자리) — + ★그것은 이 티켓이 **명시적으로 금지한 리팩터**(계약 3)라 ★**여기서 구현하지 않았다.** + ★★**ⓑ 그런데 설계는 «한 enum 건너» 이미 증명돼 있다** — `ClassDefinitionError::UnsupportedFeature(&'static str)` 가 + **5곳**에서 쓰이며 `"ldc of a method handle"` 같은 문장을 낸다 ⇒ ★**새 발명이 아니라 «일관성 회복»**이고 이것이 「할 값 있다」의 근거다. + ★**이득을 과장하지 않는다**: kind-only 단언 **8곳**이 원인을 이름 부를 수 있고 참조 JVM 격차가 준다. + ★**그러나 제안의 「픽스처보다 강한 자물쇠」는 «절반만» 참** — 원인은 «어느 검사가 울렸나», 픽스처는 «그 검사가 관측 가능한가»를 잠근다. + ★**증거**: 직전 `-fix` 가 찾은 구멍(신원 4축 중 3축 미관측)은 **링크 축**이라 ★**원인을 실었어도 안 잡혔다** ⇒ **대체가 아니라 «더하기»다.** + ★**지금 고친 것**: `tests/test_class_format.rs` 머리 주석 — ★**제안이 근거로 인용한 바로 그 문장**이 거짓이었다 + (「cut 822504b」·「Restoring it needs upstream variants」) ⇒ 그 자리에서 정정했다. + ★**착지한 트리가 거짓을 나르면 다음 사람이 같은 전제로 같은 제안을 다시 만든다.** + ★두 번째 언급(「`ClassFileError` 가 평탄화한다」)은 **참**이라 **건드리지 않았다**(과잉 편집 0). + ★`cargo test --all` **572 / 0 failed / 1 ignored**(주석만 바꿔 **불변**) · DoD **7줄 전건 rc=0**. + ★★**게이트③ 착지 — PR #56 · `--merge`**(등재 repo · `merge_strategy: merge` 선언분). 게이트② **approve** · + 핀 `53ca3409` **불이동**(착수 실측 2026-09-17T00:42:56Z · 핀에서 `ci-presence` **rc=0 CI_GREEN**). + ★**충돌은 원장 2파일뿐**(`REPORT.md`·`STATE.md`) — 형제 **#55** 착지분과 겹쳤고 **코드 파일 충돌 0**. + ★★**`tests/test_class_format.rs` 는 «자동 병합»됐고 그것을 믿지 않고 쟀다** — 양측 델타가 둘 다 살아 있다 + (이쪽 **10/3** · main 측 **32/0** · 추가·삭제줄 다중집합 **전건 일치** · 스위트 14/14 green). + 원장 해소는 전건 보존·합집합·**시간순**: 이 회차(`53ca3409` 04:31)가 main 쪽 최신 항목(`30a31bda` 04:04)보다 **뒤**라 위에 얹었다. + ★줄 소실 **0**(양방향) · 합집합 밖 신규줄 **0** · 계약 12 착지 diff numstat **해소 전후 동일**. + ★배포 **0**(이 저장소에 배포 워크플로 없음) · 자식 PR **0건** · 주기 자동 커밋 **0건**. + ★★**게이트③ 2회차 — 형제 #59(`66bc49e8`)·#58(`6e016a27`)이 그 사이 착지해 base 를 다시 당겼다.** 충돌은 또 **원장 2파일뿐**(코드 충돌 0). + ★**시간순이 이 회차를 «맨 아래»로 보낸다**(06:52 · 05:22 > 04:31) — 「내 것이 위」가 아니라 «잰 시각»이 규칙이다. + ★`tests/test_class_format.rs` 는 두 번 다 **자동 병합**됐고 두 번 다 양방향으로 쟀다(이쪽 **10/3 불변** · main 델타 **누락 0**). - [rustjava-adopt-cp-tag-passthrough-detectable-p0-fix] ★★**신원 4축을 «각각» 관측 가능하게 했다 — 감사의 「고칠 것이 없다」를 정정한다.** 게이트② **request-changes** 승계(PR #55 · 핀 `ab13a3c7`). ★**제품 코드 무접촉** — 없던 것은 **픽스처**다. ★★**무엇이 틀렸나**: 직전 감사의 **M7**(「신원 4축 검사 제거」)은 네 비교를 ★**한꺼번에** 지운다 ⇒ 그 red 가 증명하는 것은 diff --git a/docs/worklog/2026-09-17-classfile-error-cause-decision.json b/docs/worklog/2026-09-17-classfile-error-cause-decision.json new file mode 100644 index 00000000..6da62877 --- /dev/null +++ b/docs/worklog/2026-09-17-classfile-error-cause-decision.json @@ -0,0 +1,38 @@ +{ + "schema": "worklog/v1", + "date": "2026-09-17", + "taskId": "rustjava-adopt-cp-tag-passthrough-detectable-p1", + "summary": "Decided whether ClassFileError should carry a cause. Verdict: worth doing, but the proposal's history, blame and scope are all wrong, and doing it properly needs a refactor this ticket forbids. Recorded the corrected scope and fixed the false comment the proposal cited.", + "changes": [ + "tests/test_class_format.rs: the header comment claimed the variants were cut upstream at 822504b and that restoring them needs upstream changes. Both are false; replaced with what is actually true and why.", + "docs/worklog/2026-09-17-classfile-error-cause-decision.{md,json}: the decision, its evidence, and the scope handed to a follow-up" + ], + "verification": [ + "current state: classfile/src/error.rs still has two variants, and 8 assertions in tests/test_class_format.rs check only the ClassFormatError kind", + "822504b created classfile/src/error.rs as a new file with these same two variants; before it, ClassInfo::parse returned Option, so failure carried nothing at all — the commit was an improvement, not a cut", + "upstream is not the blocker: we are 5 commits behind upstream/main and none of them touch these files; upstream has touched classfile/src/error.rs exactly once (its creation); our fork already diverges by +211/-6 in constant_pool.rs, +137/-0 in validation.rs, +129/-3 in attribute.rs, +83/-7 in opcode.rs", + "the pattern already exists one enum over: ClassDefinitionError::UnsupportedFeature(&'static str) is used at 5 sites and produces messages like 'ldc of a method handle'", + "the flattening happens at three layers, not one: ClassFileError::InvalidFormat (9 production sites) -> ClassDefinitionError::InvalidClassFile (the From impl drops any cause) -> a hardcoded \"Invalid class file\" string at src/runtime.rs:189 and test-utils/src/lib.rs:334", + "validate_class evaluates an eight-term || chain and returns one InvalidFormat, so a per-check cause requires splitting that chain — a refactor this ticket forbids", + "cargo test --all: 572 passed / 0 failed / 1 ignored, unchanged (the only code change is a comment)", + "DoD 7 commands all rc=0" + ], + "issues": [ + "The proposal's claim that a cause would be 'a stronger lock than a carefully shaped fixture' is half true: a cause locks which check fired, a fixture locks whether the check is observable at all. The hole the sibling -fix round found was in linking, not parse errors, so a cause would not have caught it.", + "Implementing only the proposal's stated target (classfile/src/error.rs) would produce no observable change, since nothing would set the cause and nothing would read it." + ], + "adoptedProposals": [ + "2026-09-16-cp-tag-passthrough-detectable#p1" + ], + "proposals": [ + { + "title": "Thread a cause through ClassFileError, and split validate_class so it can differ", + "plainSummary": "Give class file rejections a reason, all the way out to the exception message a user sees.", + "userBenefit": "A rejected class file says what is wrong with it — 'unknown constant pool tag', 'bootstrap argument names nothing' — instead of the same 'Invalid class file' for every cause, which is what a real JVM does.", + "why": "Decided in this round after measuring: the design already exists one enum over (ClassDefinitionError::UnsupportedFeature carries &'static str at 5 sites), upstream is not the blocker, and 8 assertions currently can only check the exception kind. The work is ClassFileError::InvalidFormat(&'static str), the same on ClassDefinitionError::InvalidClassFile so the From impl stops dropping it, the two hardcoded message sites (src/runtime.rs, test-utils/src/lib.rs), and splitting validate_class's eight-term || chain so each check can name itself.", + "tradeoff": "Doing only the error type changes nothing observable, and doing it without splitting the || chain just moves the flatness rather than removing it — so this is an all-or-nothing scope, and it includes a refactor of validate_class. It also does not replace the shaped-fixture discipline: a cause says which check fired, not whether each axis of a multi-axis check is observable.", + "effort": "M", + "target": "classfile/src/error.rs, classfile/src/validation.rs, jvm-bytecode/src/error.rs, src/runtime.rs, test-utils/src/lib.rs" + } + ] +} diff --git a/docs/worklog/2026-09-17-classfile-error-cause-decision.md b/docs/worklog/2026-09-17-classfile-error-cause-decision.md new file mode 100644 index 00000000..b55f8371 --- /dev/null +++ b/docs/worklog/2026-09-17-classfile-error-cause-decision.md @@ -0,0 +1,94 @@ +# 2026-09-17 — `ClassFileError` 가 «원인»을 실어야 하는가 — 판정 + +티켓 `rustjava-adopt-cp-tag-passthrough-detectable-p1` — 채택 제안 `2026-09-16-cp-tag-passthrough-detectable#p1`. +낱말이 **`Decide`** 다. ★**코드 변경은 «틀린 주석 한 곳» 정정뿐**이고 산출물은 **판정과 그 근거**다. + +## 판정 + +> ★**할 값이 있다. 단 제안이 적은 이름·이유·범위가 «셋 다» 틀렸으므로 그대로 집행하면 안 된다.** +> ★**이 회차에서 구현하지 «않는다»** — 제대로 하려면 `validate_class` 의 8항 `||` 사슬을 쪼개야 하고, +> 그것은 이 티켓이 **명시적으로 금지한 리팩터**다(계약 3). ⇒ **범위를 바로잡아 후속으로 넘긴다.** + +## ⓐ 제안이 «지금도» 참인가 — 현상은 참이다 + +`classfile/src/error.rs` 는 지금도 **2변형**뿐이다(`InvalidFormat` · `UnsupportedVersion(u16)`), +그래서 모든 파스·검증 실패가 **같은 문장**으로 나온다. 실측: `tests/test_class_format.rs` 에서 +★**`ClassFormatError` «종류만» 단언하는 자리가 8곳**이다. ⇒ 「왜 거부됐는지 단언할 수 없다」는 **참**. + +## ⓒ ★그러나 제안의 «역사»는 거짓이다 — 두 겹으로 + +제안 제목: 「carry a cause **again**」 · why: 「(cut 822504b)」 · tradeoff: 「**needs upstream changes** … +this repo is a fork whose upstream contact is deliberately read-only」. + +실측: +1. ★**822504b 는 «자르지» 않았다 — 그 파일을 «만들었다».** `git show 822504b -- classfile/src/error.rs` 는 + **`new file`** 이고 내용이 지금과 **동일한 2변형**이다. +2. ★**그 이전에는 원인이 아니라 «아무것도» 없었다** — `git show 822504b^:classfile/src/class.rs` 기준 + `ClassInfo::parse` 는 **`Option`** 를 돌려줬다(실패에 정보 0 · 게다가 `.unwrap()` 투성이). + ⇒ ★**822504b 는 «후퇴»가 아니라 «개선»이었다**(`Option` → `Result<_, ClassFileError>`). +3. ⇒ ★**「again」도 「restore」도 성립하지 않는다.** 이 리니지에 원인이 실려 있던 시기는 **없다.** + +그리고 「upstream 이 해야 한다」는 ★**우리 fork 의 실측과 어긋난다**: +- `upstream/main` 기준 우리는 **5커밋 뒤**이고, 그 5개 중 이 파일들을 만지는 것은 **0건**이다. +- `classfile/src/error.rs` 를 upstream 이 만진 커밋은 **1건**(=생성)뿐 — ★**이 crate 에서 가장 안정된 파일**이다. +- ★**우리는 이미 이 crate 에서 크게 갈라져 있다**: `constant_pool.rs` **+211/−6** · `validation.rs` **+137/−0** · + `attribute.rs` **+129/−3** · `opcode.rs` **+83/−7**. +⇒ ★**「순수 로컬이 아니다」는 거짓**이다. `AGENTS.md` 의 read-only 규율은 **upstream 으로 «보내는 것»**을 금할 뿐, +로컬 변경을 금하지 않는다(우리는 매 회차 이 crate 를 고친다). + +## ⓑ ★이미 같은 축이 «한 enum 건너» 있다 — 이것이 판정을 「할 값 있다」로 민다 + +`jvm-bytecode/src/error.rs` 의 형제 타입은 **이미 원인을 싣는다**: +`UnsupportedFeature(&'static str)`(**5곳**에서 사용 · `"ldc of a method handle"` 같은 문장이 그 결과다) · +`UnsupportedClassVersion(u16)`. ⇒ ★**설계는 이 저장소에서 이미 «증명»돼 있고, `InvalidClassFile` 만 예외다.** +★그러니 이것은 «새 발명»이 아니라 **일관성 회복**이고, 모양도 자명하다(`InvalidFormat(&'static str)`). + +## 이득 — 실측하되 «과장하지 않는다» + +- ★**8곳**의 kind-only 단언이 원인을 직접 이름 부를 수 있게 된다. +- ★**참조 JVM 격차가 줄어든다**: OpenJDK 26 은 같은 파일들에 `Multiple BootstrapMethods attributes in class file X` · + `argument_index 65535 has bad constant type` 를 낸다(이 리니지가 직접 관측한 문장들). 우리는 전부 `Invalid class file` 이다. + ★**이 리니지의 주제가 «정직한 진단»인데 진단 문면 자체가 평탄하다** — 그 모순이 이 제안의 진짜 값이다. +- ★★**그러나 제안의 「a stronger lock than a carefully shaped fixture」는 «절반만» 참이다.** + 원인은 **「어느 검사가 울렸나」**를 잠그고, 픽스처는 **「그 검사가 관측 가능한가」**를 잠근다 — **다른 자물쇠다.** + ★**증거**: 바로 직전 `-fix` 회차가 찾은 구멍(신원 4축 중 3축 미관측)은 ★**파스 오류가 아니라 «링크» 축**이라 + 원인을 실었어도 **잡히지 않았다.** ⇒ ★**원인은 픽스처 규율을 «대체»하지 못한다. 더한다.** + +## 비용 — 제안이 적은 `target: classfile/src/error.rs` 는 «틀렸다»(1파일이 아니라 3층) + +평탄화는 ★**아래로 두 번 더** 일어난다(실측): +``` +ClassFileError::InvalidFormat (classfile/src/error.rs · 생산 9곳: validation 6 · class 3) + → ClassDefinitionError::InvalidClassFile (jvm-bytecode/src/error.rs · From impl 이 원인을 버린다) + → jvm.exception("java/lang/ClassFormatError", "Invalid class file") ← ★문자열 «하드코딩» 2곳 + (src/runtime.rs:189 · test-utils/src/lib.rs:334) +``` +⇒ ★**`error.rs` 만 고치면 관측 가능한 변화가 «0» 이다** — 원인을 아무도 넣지 않고 아무도 읽지 않는, +이 저장소가 반복해 규탄한 「통과하지만 아무것도 재지 않는」 형태가 된다. + +★★**그리고 진짜 비용은 따로 있다**: `validate_class` 는 **8항 `||` 사슬**을 한 번에 평가하고 **하나의** `InvalidFormat` 을 낸다. +원인을 «검사마다» 다르게 하려면 ★**그 사슬을 쪼개야** 한다 — ★그것은 이 티켓이 금지한 **리팩터**다(계약 3). +⇒ ★**그래서 여기서 구현하지 않는다.** 범위를 틀린 채로 집행하는 것보다 **바로잡아 넘기는 것**이 싸다. + +## 후속에 넘기는 «정확한» 범위 + +⑴`ClassFileError::InvalidFormat(&'static str)` — 형제 타입의 `UnsupportedFeature(&'static str)` 와 **같은 모양**(no_std 안전). +⑵`ClassDefinitionError::InvalidClassFile(&'static str)` + `From` 이 원인을 **버리지 않게**. +⑶예외 문면 **2곳**이 그 문자열을 쓰게(`src/runtime.rs` · `test-utils/src/lib.rs`) — ★**두 곳을 같이 고쳐야** 테스트가 본다. +⑷`validate_class` 의 8항 사슬을 **검사마다 반환**으로 쪼갠다(원인이 갈리는 유일한 자리). +⑸그 뒤에 **단언을 조인다** — kind-only 8곳 중 «원인이 갈리는» 것부터. +★**⑴만 하고 멈추면 안 된다**(관측 변화 0). ★**⑷ 없이 ⑴~⑶만 하면** 모든 검증 실패가 같은 원인 문자열을 이고 **평탄함이 이사할 뿐**이다. + +## 이 회차가 «지금» 고친 것 — 틀린 주석 한 곳 + +`tests/test_class_format.rs` 머리 주석이 **제안의 근거로 인용된 바로 그 문장**이고, 그것이 **거짓**이었다 +(「cut 822504b」·「Restoring it needs upstream variants」). ⇒ ★**그 자리에서 고쳤다** — 착지한 트리가 거짓을 나르면 +다음 사람이 같은 잘못된 전제로 같은 제안을 다시 만든다. ★**두 번째 언급(`ClassFileError` 가 평탄화한다)은 «참»이라 건드리지 않았다.** + +## 잃는 것 / 안 하면 무엇이 나쁜가 + +⒜**잃는 것(이 회차)**: ★**주석 한 곳의 변경뿐** — 동작·테스트 수 불변(`cargo test --all` **572 / 0 failed / 1 ignored**). +★**판정을 «지금» 집행하지 않는 대가**는 있다: 8곳의 단언이 당분간 kind-only 로 남고, 그 사이 새로 들어오는 +검증 규칙도 같은 평탄한 문장을 쓴다(이 회차 직전에 착지한 `#p0`·`#p1` 이 정확히 그 예다 — 둘 다 `InvalidFormat` 을 낸다). +⒝**안 하면**: 사용자는 «무엇이 잘못됐는지» 못 듣고, 테스트는 ★**「깨졌다」와 「다른 이유로 깨졌다」를 구별하지 못한다** — +그 구별 불가가 #49 의 테스트를 단언 조이기로 못 고치게 만든 **바로 그 원인**이다(그 회차는 픽스처를 다시 지어 우회했다). diff --git a/tests/test_class_format.rs b/tests/test_class_format.rs index 0e58e9b7..3b68fc82 100644 --- a/tests/test_class_format.rs +++ b/tests/test_class_format.rs @@ -28,9 +28,16 @@ fn hello_class() -> Vec { fs::read("test-data/Hello.class").unwrap() } -// Only the exception *kind* is asserted, not the message: upstream `ClassFileError` -// (cut 822504b) collapses every parse failure into a flat "Invalid class file", -// so per-cause wording is no longer available. Restoring it needs upstream variants. +// Only the exception *kind* is asserted, not the message: `ClassFileError::InvalidFormat` carries +// no cause, and the two places that turn it into a Java exception hardcode the string "Invalid +// class file", so there is no per-cause wording to assert. +// +// This note used to say the variants were "cut" upstream at 822504b and that restoring them "needs +// upstream variants". Both halves were wrong, measured: that commit *created* classfile/src/error.rs +// — before it, `ClassInfo::parse` returned `Option`, so failure carried nothing at all — and this +// fork already diverges by hundreds of lines in this crate, so nothing about the change is upstream's +// to make. What it does need is a cause threaded through three layers and `validate_class`'s eight-term +// `||` chain split so the cause can differ per check. See docs/worklog/2026-09-17-classfile-error-cause-decision.md. #[tokio::test] async fn test_truncated_class_raises_class_format_error() { let (dir, path) = fixture("TruncatedHello.class", &hello_class()[..60]);