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
17 changes: 17 additions & 0 deletions REPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,23 @@
- ★★**대가 — 「잃는 것이 없다」가 아니다**: ⒜순회 순서라는 성질을 **없앴다**(읽는 곳은 없다) ⒝★**아무도 잠그지 않는다.** 이 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] 비리터럴 사각을 «관문»으로 올릴까 — ★**아니다. 제안의 전제 «둘 다» 하루 만에 소멸했다**(2026-09-18-nonliteral-exception-call-sites-p0)
- 무엇을: 채택 제안 `2026-09-18-nonliteral-exception-call-sites#p0`(worklog json 기록). 검사기가 사각을 **세어서 찍고 ★절대 실패시키지 않는다**. ★결정을 **검사기 docstring 에 못박았다**(다음 회차가 관문을 다시 제안하기 «전»에 읽는 자리).
- ★★**전제 재측(`origin/main` @ `e9910a7a`)**: ⑴「baseline is **0**」 → ★**1이다**(`jvm/tests/test_exception_construction.rs:19`) ⑵「**crashing the whole runtime** 대신」 → ★**더는 죽지 않는다**(`…-exception-throws-instead-of-unwrap` 착지).
- ★**그 1자리는 «옳고», «비리터럴이어야만» 한다**: 그 테스트는 못 싣는 이름을 부르는데 리터럴로 쓰면 ★**바로 이 검사기가 red** 가 된다(작성 당시 실측). ⇒ ★**관문을 0으로 걸었으면 제안된 날 main 이 red** 였고, 그 대상은 **정상 코드**다.
★**제안이 자기 `why` 에서 이 비용을 예고했다** — 「변수로 이름을 넘기는 정당한 리팩터가 red 가 되어 논박당한다」. ★그 예고가 **약 4시간 뒤** 현실이 됐다.
- ★**막으려던 해악도 작아졌다**: 이제 못 싣는 이름은 **프로세스를 죽이지 않고** `NoClassDefFoundError` 를 낸다 ⇒ ★**「catch 가 안 걸린다」는 조용한 오동작**이지 «죽음»이 아니다(제안의 `userBenefit` 근거가 그만큼 약해졌다).
- ★**대신 «값싼 절반»을 지었다** — 제안의 진짜 걱정은 `tradeoff` 의 「회차 사이에 바닥이 측정되지 않는다」이고, 그것은 **관문 없이** 고쳐진다: 매 실행에 한 줄로 찍는다(pass·fail 무관).
`Blind spot: 1 call site(s) … ? jvm/tests/test_exception_construction.rs:19` + 기존 `✓ 43 … all 268 loadable`.
- ★**바꾼 수**: **1파일 · +90/−6** · ★**새 DoD 명령 0 · 새 CI 잡 0 · 종료코드 의미 불변**(0/1/2 그대로).
- ★**양방향 축(제품 코드)**: `jvm/src/jvm.rs` 에 런타임 조립 호출 주입 → **1 → 2** 이고 ★**`jvm/src/jvm.rs:1390` 을 이름으로 지목** · 원복 → **1**(트리 클린) · ★**rc 는 양쪽 다 0**(그것이 «관문이 아니다»의 뜻이다).
★**술어 민감도**: 「다른 함수 8종 분리」를 지우면 **42**(헬퍼 호출 33 + 헬퍼 «정의» 8 + 진짜 1) ⇒ ★그 수가 «느슨한 앵커의 산물»이 아님을 보인다.
- ★**대가(숨기지 않는다)**: ⒜**찍힌 수는 «무시할 수 있다»** — 관문은 강제하고 한 줄은 스크롤로 지나친다(그것이 반대편의 최강 논거다) ⒝**수는 술어만큼만 정확하다**(42가 그 실수의 모습이고, 이 회차 프로브 말고는 잠그는 것이 없다) ⒞**제품/테스트를 구별하지 않는다**(오늘 전건이 테스트인데 표시가 없다).
- ★★**내가 만든 회귀 둘을 재서 걷어냈다**(눈으로 잡은 것이 아니다): ⑴**`.rs` 전수를 두 번 걷기** 3.3~4.3s → **10.0~13.3s** ⇒ 단일 패스로 병합 ⑵**파일마다 개행 인덱스를 파이썬 루프로** 만들기(남은 ~2배의 «진짜» 원인) ⇒ `text.count` 로 되돌림(전 트리 매치가 ~850이라 «매치당 세기»가 «문자당 인덱싱»보다 싸다) ⑶앵커 `[A-Za-z0-9_]*exception\(` 가 단어문자 위치마다 시도하게 만든다 ⇒ 리터럴 앵커 + 앞 글자 검사로 교체. ⇒ ★**최종 비용 유의차 없음**(전 1.33/3.20/1.55/1.58s ↔ 후 1.73/1.88/1.44/1.54s · 구간 겹침).
- ★**되돌릴 조건**: ⑴런타임 조립 이름이 **제품 코드**에 나타나거나 ⑵**아무도 모르게 수가 는다**(그 한 줄이 정확히 그것을 막는다).
- 검증: DoD 9명령 · 아래 절.
- ★후속 추천: **그 사각이 «제품인지 테스트인지»를 말할 것인가**(S). 상세 = `docs/worklog/2026-09-19-nonliteral-blind-spot-is-reported-not-gated.md`.

## [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
7 changes: 7 additions & 0 deletions STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@
★★**제안 진단은 부정확**: `findings` 는 set 이 아니라 **list** 이고 이름은 이미 정렬돼 있었다 — set 4개 중 출력에 닿는 것은 ★**`changed`(경로) 하나**다. ⇒ 「print site」가 아니라 ★**출처에서** 정렬했다(`check()` 의 **반환값**도 결정적이어야 하므로).
★**양방향**: 2종 ↔ **1종** ↔ 되돌리면 2종. ★찾은 것 불변(15줄 · 집합 일치 · rc 1).
★**대가**: 아무도 잠그지 않는다 — `scripts/` 테스트 하네스 **0** · 비결정성을 넣어도 파이썬 검사기 **4종 rc 0** · fmt **rc 0** ⇒ ★**그물 «0»**. 후속 제안으로 남겼다.
- [2026-09-18-nonliteral-exception-call-sites-p0] ★★**비리터럴 사각은 «관문»이 아니라 «보고»다 — 제안의 전제 둘 다 소멸.** 채택 제안 `2026-09-18-nonliteral-exception-call-sites#p0`.
★**전제 재측**: 「baseline 0」 → ★**1**(그 1자리는 **정상**이고 «비리터럴이어야만» 한다 — 리터럴이면 이 검사기가 red) · 「죽는다」 → ★**더는 안 죽는다**(#76 착지) ⇒ 해악이 «죽음»에서 «틀린 catch»로 내려갔다.
★**관문을 0으로 걸었으면 제안된 날 main 이 red** 였다 — ★제안이 자기 `why` 에 그 비용을 예고했고 **4시간 뒤** 현실이 됐다.
★**지은 것**: 매 실행에 사각을 **세어 찍는다**(never fail) · **1파일 +90/−6** · 새 DoD 명령 **0** · 새 CI 잡 **0** · 종료코드 불변.
★**축**: 제품 코드 주입 → **1→2**(파일:줄 지목) · 원복 → 1 · rc 양쪽 0 · 술어 민감도 **42**.
★**대가**: 찍힌 수는 무시할 수 있다 · 수는 술어만큼만 정확 · 제품/테스트 미구별(후속 카드).
★**회귀 둘을 스스로 만들고 재서 걷어냈다**(두 번 걷기 · 개행 인덱스) ⇒ 최종 비용 **유의차 없음**.
- [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
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"date": "2026-09-19",
"taskId": "2026-09-18-nonliteral-exception-call-sites-p0",
"summary": "Decided not to gate the non-literal blind spot. Both premises of the adopted proposal had expired within a day: the baseline is 1 rather than 0 (a test must pass an unloadable name through a variable, because a literal there makes this very check red), and an unloadable name no longer crashes the runtime since the exception-throws round landed - it raises NoClassDefFoundError instead of the intended exception. Built the cheap half instead: the checker now counts and prints the blind spot and never fails on it.",
"decision": "report the blind spot, do not gate it",
"measurements": {
"baseline_claimed_by_proposal": 0,
"baseline_measured_now": 1,
"nonliteral_site": "jvm/tests/test_exception_construction.rs:19",
"files_changed": 1,
"lines_added": 90,
"lines_removed": 6,
"new_dod_commands": 0,
"new_ci_jobs": 0,
"blind_spot_with_helper_split_removed": 42,
"runtime_before_seconds": [1.33, 3.20, 1.55, 1.58],
"runtime_after_seconds": [1.73, 1.88, 1.44, 1.54],
"runtime_after_first_draft_seconds": [10.03, 13.27, 11.13],
"named_classes": 43,
"literal_call_sites": 846,
"loadable_classes": 268
},
"verification": [
"axis on product code: injecting a run-time-assembled self.exception(name, ...) into jvm/src/jvm.rs moves the report from 1 to 2 and names jvm/src/jvm.rs:1390; reverting returns it to 1 with a clean tree; rc stays 0 both ways, which is the decision",
"predicate sensitivity: removing the helper-name split makes the report say 42 (33 helper calls + 8 helper definitions + the 1 real site), so the number is not an artefact of a loose anchor",
"premise re-measured against origin/main @ e9910a7a: baseline 1, and the axis test for the exception-throws round passes, i.e. an unloadable name returns NoClassDefFoundError rather than aborting",
"cost: before 1.33/3.20/1.55/1.58s vs after 1.73/1.88/1.44/1.54s, overlapping ranges"
],
"changes": [
"scripts/check-named-exception-classes-are-loadable.py - counts and prints run-time-assembled call sites, never fails on them; decision and its two dead premises recorded in the docstring; scan of the two axes merged into one pass",
"docs/worklog/2026-09-19-nonliteral-blind-spot-is-reported-not-gated.{md,json}, REPORT.md, STATE.md"
],
"issues": [
"A printed number can be scrolled past; a gate cannot. That is the trade this round chose and it is the strongest argument for the other answer.",
"The count is only as good as its predicate - the 42 above is what a one-line mistake looks like - and nothing tests it except this round's probes.",
"The report does not distinguish product code from tests. Every non-literal site today is a test; a product one would read identically.",
"I introduced two runtime regressions while building this (a second full walk of every .rs, then a per-character newline index) and removed both; the first draft ran 10-13s against a 1.3-3.2s baseline."
],
"adoptedProposals": [
"2026-09-18-nonliteral-exception-call-sites#p0"
],
"proposals": [
{
"title": "Say whether a run-time-assembled exception name is in product code or in a test",
"plainSummary": "The safety check now reports the places where an error class name is built while the program runs. It does not say whether those places are real product code or just test scaffolding, and only one of those is worth worrying about.",
"userBenefit": "A genuinely risky site in the runtime would stand out immediately instead of blending in with test helpers that are fine.",
"why": "This round decided against failing on the count, precisely because the only site today is a test that has to be written that way. That judgement depends entirely on the product/test split, and the report does not carry it - so the next reader has to re-derive it by opening each path. The repo already knows the split: the non-literal round measured 781 product versus 65 test call sites using nothing more than the path.",
"tradeoff": "It is a second classification to keep honest, and path-based heuristics are exactly the kind of thing that rots when a directory is renamed; an alternative is to leave the paths and trust the reader. It also risks implying that a test site is always acceptable, which is only true while it is deliberate.",
"effort": "S",
"target": "scripts/check-named-exception-classes-are-loadable.py"
}
]
}
108 changes: 108 additions & 0 deletions docs/worklog/2026-09-19-nonliteral-blind-spot-is-reported-not-gated.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# 2026-09-19 — Should the non-literal blind spot be a gate? No. Both of the proposal's premises expired.

Round: `2026-09-18-nonliteral-exception-call-sites-p0`
Adopted proposal: `2026-09-18-nonliteral-exception-call-sites#p0` —
*"Decide whether nonliteral exception() call sites should be a check, now that the baseline is 0."*

## Decision

**No gate.** `check-named-exception-classes-are-loadable.py` now **counts and prints** the blind spot
and **never fails on it**. The reasoning is recorded in the checker's own docstring, where the next
round will read it before re-proposing the gate.

## The proposal is one day old and both of its premises are already false

It was written by the round that measured the blind spot at 0. Re-measured against `origin/main`
@ `e9910a7a`:

| premise, quoted from the proposal | status now |
|---|---|
| *"now that the baseline is **0**"* | **false — it is 1.** `jvm/tests/test_exception_construction.rs:19` |
| *"instead of **crashing the whole runtime**"* | **false — it no longer crashes.** |

**The one non-literal site is correct, and it has to be non-literal.** That test asks
`Jvm::exception` for a class no loader can provide. Written as a literal, *this very check* reports
it and goes red — measured when it was first written. So it passes the name through a variable. A
gate at zero would have been **red on `main` the day it was proposed**, against a site that is right.

The proposal predicted this in its own `why` field: *"a gate whose baseline is 0 has its own cost —
it turns a legitimate future refactor (passing a name through a variable) into a red that must be
argued down."* That cost stopped being hypothetical roughly four hours after the sentence was
written.

**And the harm it guards is smaller than the proposal's `userBenefit` says.** Since
`rustjava-jvm-exception-throws-instead-of-unwrap` landed (`e9910a7a`), an unloadable name does not
abort the process — it returns the `NoClassDefFoundError` the loader raised. So a run-time-assembled
unloadable name now means *the caller catches the wrong class*, which is a real bug and a quiet one,
but it is not the crash the gate was argued for.

## What was built instead

The proposal's actual worry is in its `tradeoff`: *"not adding it means the floor stays unmeasured
between rounds."* That is fixed without the gate — the number is printed on every run, pass or fail:

```
Blind spot: 1 call site(s) build the class name at run time, so this check
does not see them. Not an error -- an unloadable name there raises NoClassDefFoundError
rather than the intended exception, which is a wrong catch, not a crash:
? jvm/tests/test_exception_construction.rs:19
✓ 43 named exception class(es) across 846 call site(s); all 268 loadable
```

**Changed: 1 file, +90/−6 lines (`git diff --numstat`), 0 new commands, 0 new CI jobs.** Exit codes are untouched (0/1/2
mean exactly what they meant).

## Axis — bidirectional, on product code

| probe | result |
|---|---|
| inject a run-time-assembled `self.exception(name, …)` into **`jvm/src/jvm.rs`** | `Blind spot: **2**` and it names `jvm/src/jvm.rs:1390` |
| revert | `Blind spot: **1**`, tree clean |
| remove the helper-name split from the predicate | `Blind spot: **42**` — 33 helper calls + 8 helper definitions + the 1 real site |

The third probe is the one that shows the number *means* something: `exception(` is a substring of
eight helper functions in the test trees whose first parameter is `jvm`, not a class name. Without
that split the report would be off by a factor of 42.

**Note on the axis clause**: the acceptance asks for "revert it and get red". This change is
deliberately incapable of red — that is the decision. So the axis is the *number* moving and naming
the new site, plus `rc` staying 0 in both directions, which is what "reported, not gated" has to
mean.

## What this costs

- **A number that nobody is forced to act on.** A gate makes you deal with it; a printed line can be
scrolled past. That is the trade this round chose, and it is the strongest argument for the gate.
- **The count is only as good as its predicate** — the 42 above is what a one-line mistake looks
like. It is not tested by anything except the probes in this round.
- **The gate's *input set* is now a thing this file can get wrong, and that is a cost the first
version of this round paid.** "Exit codes are untouched" was true of what 0/1/2 *mean*, and it hid
the axis that actually matters: merging the two scans put the report axis's prefix filter on the
gate axis too, so a call written `raise_exception("java/lang/X", …)` was dropped from **both** —
not gated, not reported. Measured by gate 2 and reproduced here: an injected
`raise_exception("java/lang/TotallyUnloadableProbe", …)` gave **rc 0** against that form where the
previous version gave **rc 1**. Fixed by keeping the gate axis unfiltered. The lesson is not the
bug, it is that "the exit codes are unchanged" says nothing about **what is fed into them**.
- **Product versus test is not distinguished.** Today all non-literal sites are tests; the report
does not say so, and a product site would read identically.
- Runtime: **no significant change** — before 1.33 / 3.20 / 1.55 / 1.58 s, after 1.73 / 1.88 / 1.44 /
1.54 s (overlapping). Getting there took two rewrites; see below.

## Three rewrites before this was free

The first two were regressions I introduced; the third is what finally paid for the feature. All
measured, none spotted by eye:

1. **A second full walk of every `*.rs`** — the report scanned the tree again. 3.3–4.3 s → 10.0–13.3 s.
Merged into one pass shared by both axes.
2. **A per-character newline index** built in Python for every file, to make line numbers cheap.
It *was* the remaining regression (still ~2×). Replaced with `text.count("\n", 0, …)` — there are
~850 matches in the whole tree, so counting per match beats indexing per character.
3. Then the anchor itself: `[A-Za-z0-9_]*exception\(` forced the engine to try every word-character
position. Anchoring on the literal `exception\(` and testing the preceding character instead is
the same question and restored parity.

## What would reopen this

- A run-time-assembled name appears in **product** code (every site today is a test), or
- the count grows without a round noticing — which is exactly what the printed line is for.
Loading
Loading