Skip to content

Commit 2bc24f4

Browse files
authored
[rustjava-assert-loadable-rederivation-did-not-come-up-short] feat(scripts): 되유도를 믿지 않고 «단언»한다 (#82)
[rustjava-assert-loadable-rederivation-did-not-come-up-short] feat(scripts): 되유도를 믿지 않고 «단언»한다
2 parents f65e061 + 5e298a7 commit 2bc24f4

5 files changed

Lines changed: 199 additions & 2 deletions

‎REPORT.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,19 @@
8080
- 검증: DoD 9명령 · 아래 절.
8181
- ★후속 추천: **그 사각이 «제품인지 테스트인지»를 말할 것인가**(S). 상세 = `docs/worklog/2026-09-19-nonliteral-blind-spot-is-reported-not-gated.md`.
8282

83+
## [2026-09-19] 되유도를 «믿지 않고 단언한다» — 검사기가 자기 읽기를 스스로 증명한다(rustjava-assert-loadable-rederivation-did-not-come-up-short)
84+
- 무엇을: 채택 제안 `2026-09-19-loadable-set-source-of-truth#p0`(worklog json 기록). ★**제안에 `how` 필드가 없다** — `why` 를 따르고 `tradeoff` 에서는 **의도적으로 갈렸다**(아래).
85+
- ★**먼저 쟀다 — 지금 «적게 잡히는» 것이 있는가: 없다.** 같은 것을 네 가지로 센다: `_proto()` 출현 **268** · `_proto(),` 줄 **268** · `crate::classes::` **268** · `REGISTERED` 파싱 **268** · 해석된 고유 이름 **268**.
86+
★**단언이 «틀리게» 울 조건도 함께 쟀다**(이게 핵심이다): 한 줄에 등재 둘 **0** · 쉼표 없는 `_proto()` **0** · 주석 속 `_proto()` **0** · 중복 이름 **0**.
87+
⇒ ★**green 에서 시작하는 회귀 방어**이지 «현존 결함 수리»가 아니다 — 지어내지 않고 그대로 적는다.
88+
- ★**지은 것 — 두 축, 둘 다 fail-closed**: ⑴**파싱 ↔ 증인**(`REGISTERED` 는 «의심 대상»이라 자기 매치를 세는 것은 증명이 아니다 ⇒ 등재가 **없이는 쓰일 수 없는 토큰**으로 두 번째로 센다) ⑵**등재 수 ↔ 고유 이름 수**(둘이 한 이름으로 접히면 해석이 틀린 것 — ★**접힌 쌍을 이름으로 찍는다**). **1파일 +38/−2.**
89+
- ★★**양방향 축 — 제품 스크립트를 «실제로 있었던 결함»으로 되돌려 쟀다**: ⑴초판의 `as_proto` 전용 정규식 → ★**rc=2** 「265 registrations parsed but 268 proto calls」 ⑵초판의 짧은 이름 키 → ★**rc=2** 「268 registrations resolved to only 263 names」 + ★**`java/util/Formatter <- java::util::Formatter::as_proto(), java::util::logging::Formatter::as_proto()`** 를 포함해 다섯 쌍을 지목 · 원형상 **rc=0**.
90+
★**그 둘째 메시지가 이 회차의 요지다** — 게이트②가 «소스를 나란히 읽어» 찾아낸 그 충돌쌍을 ★**이제 스크립트가 말한다**.
91+
- ★**브리프의 세 질문에 답한다**: ⒜**무엇과 비교하나** = ★**소스 자신**(같은 파일을 두 번째로 센다 · 저장된 기준선도, 직전 실행값도 아니다) ⒝**첫 실행·정당한 감소** = ★**애초에 생기지 않는다**(기억하는 수가 없다 — 클래스를 지우면 네 수가 «함께» 내려가 green 유지). ★이 형상을 고른 이유가 바로 그것이다 ⒞**죽는가 말하는가** = ★**죽는다(exit 2 «cannot measure»)**. 형제 회차는 「세어 찍고 절대 실패시키지 않는다」를 골랐고 ★**나는 갈렸다** — 그쪽은 «알려진 사각을 재는» 것이고 이쪽은 ★**검사기가 «자기 입력»을 잘못 읽는** 것이다. 이 파일은 이미 그 계급을 exit 2 로 다룬다(해석 불가 등재).
92+
- ★**대가**: ⒜숫자 둘이 «참이어야» 한다 — `loader.rs` 가 등재 배열 «밖»에서 `as_proto()` 를 부르면 **정상 트리에서 red**(오늘은 268 전건이 등재다) ⒝★**«진짜» 중복 등재는 false red** 가 된다(오늘 0 · 메시지가 이름을 대지만 위험은 실재) ⒞★**바닥이지 증명이 아니다** — 틀리되 **서로 다른** 이름으로 매핑되면 268 을 유지하고 통과한다(실측된 거짓 초록 둘은 «과소계수» 계급이었다) ⒟비용 **유의차 없음**(전 6.47/8.01/4.57s ↔ 후 6.35/5.68/7.53s · 구간 겹침 · 부하가 커 편차가 효과보다 크다).
93+
- ★**제안의 `tradeoff` 에서 갈렸다(의도)**: 제안은 「`loader.rs` 의 **텍스트 형태(한 줄 한 등재)** 에 묶인다」를 대가로 예고했다 ⇒ ★**줄이 아니라 «출현»을 세어 그 묶임을 없앴다**(줄 수와 오늘 동일 268 이라 잃는 것도 없다).
94+
- 검증: DoD 9명령 · 아래 절.
95+
8396
## [2026-09-19] 적재 가능 집합을 «로더에서 읽을까» — ★**아니다, 재유도를 유지한다**(rustjava-loadable-set-from-loader-vs-rederive-decision)
8497
- 무엇을: 채택 제안 `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`).
8598
- ★★**전제부터 확인했다 — 「4결함 중 3이 재유도」는 «참»이다.** 산문이 아니라 **고침 커밋 `89c2e83c` 에서** 갈랐다: ⑴`as_proto` 전용 → `list_proto` 3건 누락 ⑵한 `impl` 의 첫 `name:` 오귀속 ⑶짧은 이름 키 충돌 = **재유도(loadable) 3건** · ⑷줄 단위 스캔이 rustfmt 가 쪼갠 34건 누락 = ★**호출부 스캔(named) 1건**.

‎STATE.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@
5757
★**축**: 제품 코드 주입 → **1→2**(파일:줄 지목) · 원복 → 1 · rc 양쪽 0 · 술어 민감도 **42**.
5858
★**대가**: 찍힌 수는 무시할 수 있다 · 수는 술어만큼만 정확 · 제품/테스트 미구별(후속 카드).
5959
★**회귀 둘을 스스로 만들고 재서 걷어냈다**(두 번 걷기 · 개행 인덱스) ⇒ 최종 비용 **유의차 없음**.
60+
- [rustjava-assert-loadable-rederivation-did-not-come-up-short] ★★**되유도를 믿지 않고 «단언»한다 — 두 축 모두 fail-closed(exit 2).** 채택 제안 `2026-09-19-loadable-set-source-of-truth#p0`. ★**1파일 +38/−2.**
61+
★**먼저 쟀다**: 같은 것을 네 가지로 세어 **전부 268** · ★**틀리게 울 조건도 0**(한 줄 둘·쉼표 없음·주석 속·중복 이름) ⇒ **green 에서 시작하는 회귀 방어**다.
62+
★**두 축**: ⑴파싱 ↔ **독립 증인**(의심 대상의 자기 매치는 증명이 아니다) ⑵등재 수 ↔ 고유 이름 수(★접힌 쌍을 **이름으로** 찍는다).
63+
★**양방향**: 초판 결함 «둘»을 제품 스크립트에 되살려 각각 **rc=2**(265↔268 · 268→263 + 충돌쌍 지목) · 원형상 **rc=0**.
64+
★**브리프 3문**: 비교 대상 = **소스 자신**(기준선·직전값 아님) · 첫 실행/정당한 감소 = ★**생기지 않는다**(기억이 없다) · ★**죽는다(exit 2)** — 형제 회차의 「찍고 안 죽는다」와 **갈렸고 사유를 적었다**(사각 측정 ↔ 자기 입력 오독).
65+
★**대가**: 등재 배열 «밖» 호출이면 false red · 진짜 중복 등재도 false red(오늘 0) · ★**바닥이지 증명 아님**(다른 이름으로 틀리면 통과) · 비용 유의차 없음.
66+
★**제안 `tradeoff` 와 갈렸다**: 「줄 형태에 묶인다」를 ★**출현 계수**로 없앴다(오늘 줄 수와 동일).
6067
- [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`.
6168
★**전제 확인**: 「4중 3이 재유도」는 **참**(고침 커밋 `89c2e83c` 에서 갈랐다 — loadable 3 · named 1).
6269
★★**그 1건이 결정한다**: `named`(코드가 무엇을 넘기는가)는 **로더가 답할 수 없다** ⇒ 로더 읽기는 **파서 하나를 없앨 뿐 파싱을 못 없앤다**(그 절반에서 형제 회차가 ★**4시간 31분** 뒤에 또 잡았다 — ★**「다른 함수 8종 41자리」**를 쓸어담는 앵커 함정이고, 세면 **33** · 맞는 답은 **0** 이다).
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"date": "2026-09-19",
3+
"taskId": "rustjava-assert-loadable-rederivation-did-not-come-up-short",
4+
"summary": "The loadable re-derivation now proves itself instead of being trusted: registrations parsed are compared against an independent count of the proto calls in loader.rs, and registrations are compared against distinct resolved names. Both mismatches exit 2 (cannot measure). Starts green - all four counts are 268 today - and re-creating either of the two historical defects turns it red with a message naming what collapsed.",
5+
"measurements": {
6+
"proto_call_occurrences": 268,
7+
"proto_comma_lines": 268,
8+
"crate_classes_occurrences": 268,
9+
"registrations_parsed": 268,
10+
"distinct_names": 268,
11+
"registrations_sharing_a_line": 0,
12+
"proto_without_trailing_comma": 0,
13+
"proto_in_comments": 0,
14+
"duplicate_resolved_names": 0,
15+
"files_changed": 1,
16+
"lines_added": 38,
17+
"lines_removed": 2,
18+
"runtime_before_seconds": [
19+
6.47,
20+
8.01,
21+
4.57
22+
],
23+
"runtime_after_seconds": [
24+
6.35,
25+
5.68,
26+
7.53
27+
]
28+
},
29+
"verification": [
30+
"premise measured on origin/main @ ad9eb1a6: four independent counts of the registrations all give 268, and the four shapes that would make the assertion fire wrongly are all 0",
31+
"axis 1, product script mutated back to the first draft's as_proto-only pattern: rc 2, '265 registrations parsed but 268 proto calls are in rustjava-runtime/src/loader.rs'",
32+
"axis 2, product script mutated back to the first draft's bare-name keying: rc 2, '268 registrations resolved to only 263 names', naming java/util/Formatter <- java::util::Formatter::as_proto(), java::util::logging::Formatter::as_proto() among five",
33+
"unmutated: rc 0, 43 named classes across 846 call sites, all 268 loadable",
34+
"cost: before 6.47/8.01/4.57s vs after 6.35/5.68/7.53s, overlapping"
35+
],
36+
"changes": [
37+
"scripts/check-named-exception-classes-are-loadable.py - PROTO_CALL witness plus two fail-closed assertions in loadable_classes()",
38+
"docs/worklog/2026-09-19-assert-rederivation-did-not-come-up-short.{md,json}, REPORT.md, STATE.md"
39+
],
40+
"issues": [
41+
"The proposal has no how field; this implementation follows its why and departs from its tradeoff.",
42+
"Departure: the proposal expected coupling to the textual shape of loader.rs (one registration per line). Counting _proto() occurrences rather than lines avoids that, and measures equal today (268 = 268).",
43+
"A genuine duplicate registration - two entries deliberately naming one class - would be a false red. There are none today and the message names them, but the risk is real.",
44+
"If loader.rs ever calls as_proto() outside the registration array, the witness counts it and the check reddens on a correct tree. Measured today: all 268 calls are registrations.",
45+
"It is a floor, not a proof: a registration mapped to a wrong but distinct name keeps both counts at 268 and passes.",
46+
"Differs from the sibling round 2026-09-18-nonliteral-exception-call-sites#p0, which chose to print and never fail. That sized a known blind spot; this catches the check mis-reading its own input, which the file already treats as exit 2."
47+
],
48+
"adoptedProposals": [
49+
"2026-09-19-loadable-set-source-of-truth#p0"
50+
],
51+
"proposals": []
52+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# 2026-09-19 — The check now proves its own reading of the loader, instead of trusting it
2+
3+
Round: `rustjava-assert-loadable-rederivation-did-not-come-up-short`
4+
Adopted proposal: `2026-09-19-loadable-set-source-of-truth#p0`
5+
6+
The proposal has **no `how` field** — `title`, `plainSummary`, `userBenefit`, `why`, `tradeoff`,
7+
`effort`, `target` only. What follows says where this implementation matches its `why` and where it
8+
deliberately departs from its `tradeoff`.
9+
10+
## First: is anything short today?
11+
12+
No. Measured on `origin/main` @ `ad9eb1a6`, four independent counts of the same thing:
13+
14+
| count | value |
15+
|---|---|
16+
| `_proto()` occurrences in `loader.rs` | **268** |
17+
| `_proto(),` lines | 268 |
18+
| `crate::classes::` occurrences | 268 |
19+
| registrations `REGISTERED` parses | 268 |
20+
| distinct names resolved | **268** |
21+
22+
Also measured, because they are what would make the assertion fire *wrongly*: registrations sharing
23+
a line **0**, `_proto()` without a trailing comma **0**, `_proto()` inside a comment **0**, duplicate
24+
resolved names **0**.
25+
26+
So the assertion starts green and guards a regression rather than fixing a present defect. The
27+
proposal says as much; this round confirms it with numbers rather than assuming it.
28+
29+
## What was added — two axes, both fail-closed
30+
31+
1. **Parsed vs. witnessed.** `REGISTERED` is the pattern under suspicion, so counting its own matches
32+
proves nothing. `PROTO_CALL` counts the same calls a second way, by the one token a registration
33+
cannot be written without. Mismatch ⇒ `cannot measure` (exit 2).
34+
2. **Registrations vs. distinct names.** Two registrations resolving to one name means the resolution
35+
is wrong — it is exactly what bare-name keying did. Mismatch ⇒ exit 2, **naming the collapsed
36+
pairs** so a genuine duplicate registration can be told from a mis-attribution at a glance.
37+
38+
**Changed: 1 file, +38/−2.**
39+
40+
## Axis — bidirectional, on the product script, by re-creating the two real defects
41+
42+
| the script, mutated back to a defect it actually had | result |
43+
|---|---|
44+
| `((?:as\|list)_proto)` → `(as_proto)` (the first draft) | **rc 2** — `265 registrations parsed but 268 proto calls are in rustjava-runtime/src/loader.rs` |
45+
| key by bare type name (the first draft) | **rc 2** — `268 registrations resolved to only 263 names`, then names them: `java/util/Formatter <- java::util::Formatter::as_proto(), java::util::logging::Formatter::as_proto()`, … |
46+
| unmutated | **rc 0** — `✓ 43 named exception class(es) across 846 call site(s); all 268 loadable` |
47+
48+
The second message is the point of the round: gate 2 found that colliding pair by reading the source
49+
alongside the script. The script now says it.
50+
51+
## The three questions the brief asked
52+
53+
**⒜ What is it compared against?** The source itself — a second count of the same file. Not a stored
54+
baseline, not the previous run.
55+
56+
**⒝ First run, and legitimate decreases?** They do not arise, and that is *why* this shape was
57+
chosen. A remembered number would have to answer both; a self-contained invariant answers neither
58+
because it never remembers anything. Removing a class legitimately drops all counts together and
59+
stays green.
60+
61+
**⒞ Die or speak?** **Die — exit 2, `cannot measure`.** The sibling round
62+
(`2026-09-18-nonliteral-exception-call-sites#p0`) chose "count and print, never fail" for the
63+
non-literal blind spot, and this round deliberately differs: that is a *known limitation* being
64+
sized, this is the check *mis-reading its own input*. The file already has a category for the
65+
latter — it dies on a registered entry whose name cannot be resolved — and this is the same failure
66+
one step earlier. A check whose loadable set is short reports real classes as unloadable and missing
67+
ones as present; printing that and exiting 0 would be the silent pass the file's docstring is about.
68+
69+
## What this costs
70+
71+
- **Two more numbers that have to stay true.** If `loader.rs` ever calls `as_proto()` outside the
72+
registration array, `PROTO_CALL` counts it and the check goes red on a correct tree. Measured
73+
today: every one of the 268 calls is a registration (`crate::classes::` count matches exactly).
74+
- **A false red is possible for a genuine duplicate registration** — two entries deliberately naming
75+
one class. There are none today, and the message names them, but it would be a red on a tree that
76+
is arguably fine.
77+
- **It is a floor, not a proof** — the proposal's own words. A registration mapped to a *wrong but
78+
distinct* name keeps both counts at 268 and passes. This catches undercounts, which is what both
79+
measured false greens were.
80+
- Runtime: **no significant change** — before 6.47 / 8.01 / 4.57 s, after 6.35 / 5.68 / 7.53 s
81+
(overlapping; the machine is loaded and the spread is wider than the effect).
82+
83+
## Departure from the proposal's `tradeoff`
84+
85+
It predicted the check would be *"coupled to the textual shape of `loader.rs` (one registration per
86+
line), which is true today and is not guaranteed."* That coupling was avoidable and was avoided:
87+
counting `_proto()` **occurrences** rather than lines makes the witness independent of line layout
88+
and of trailing-comma style. Measured equal to the line count today (268 = 268), so nothing is lost
89+
by the more robust form.

‎scripts/check-named-exception-classes-are-loadable.py‎

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,13 @@
107107
IS_DEFINITION = re.compile(r"\bfn\s+$")
108108

109109

110+
# The independent witness for "did the parse come up short". `REGISTERED` is the pattern under
111+
# suspicion, so counting its own matches proves nothing; this counts the same calls a second way,
112+
# by the one token a registration cannot be written without. Occurrences rather than lines, and no
113+
# trailing comma, so it does not care how the list is formatted -- measured on this file: `_proto()`
114+
# 268, `_proto(),` lines 268, `crate::classes::` 268, registrations parsed 268, all agreeing.
115+
PROTO_CALL = re.compile(r"_proto\(\)")
116+
110117
def die(message):
111118
print(f"cannot measure: {message}", file=sys.stderr)
112119
raise SystemExit(2)
@@ -229,20 +236,49 @@ def loadable_classes():
229236
if field:
230237
name_of[(module, type_name, function.group(1))] = field.group(1)
231238

232-
registered = REGISTERED.findall(read(LOADER))
239+
loader_text = read(LOADER)
240+
registered = REGISTERED.findall(loader_text)
233241
if not registered:
234242
die(f"no proto registrations found in {LOADER.relative_to(ROOT)}")
235243

236-
names, unresolved = set(), []
244+
# Did this parse come up short? The whole check rests on `registered` being every registration,
245+
# and the failure mode is silent: a pattern that matches fewer entries yields a smaller loadable
246+
# set, and a smaller loadable set makes missing classes look present. Measured on the first draft
247+
# of this file: it matched only `as_proto`, parsed 265 of 268 and resolved 263 names, and said
248+
# nothing. Counting the calls a second, independent way turns that into an exit 2.
249+
witness = len(PROTO_CALL.findall(loader_text))
250+
if len(registered) != witness:
251+
die(
252+
f"{len(registered)} registrations parsed but {witness} proto calls are in "
253+
f"{LOADER.relative_to(ROOT)}. The pattern that reads them is missing some, so the loadable "
254+
"set is short and every class it lost would read as 'not loadable'. Fix REGISTERED rather "
255+
"than trusting this run."
256+
)
257+
258+
names, unresolved, name_of_entry = set(), [], {}
237259
for path, function in registered:
238260
parts = path.split("::")
239261
key = ("::".join(parts[:-1]), parts[-1], function)
240262
if key in name_of:
241263
names.add(name_of[key])
264+
name_of_entry.setdefault(name_of[key], []).append(f"{path}::{function}()")
242265
else:
243266
unresolved.append(f"{path}::{function}()")
244267
if unresolved:
245268
die("registered entries with no resolvable name: " + ", ".join(sorted(set(unresolved))))
269+
270+
# Two registrations that resolve to one name mean the resolution is wrong, not that the runtime
271+
# has a duplicate: it is what the first draft did when it keyed types by bare name and let one of
272+
# a colliding pair answer for its twin (265 parsed, 263 names). Named rather than counted, so the
273+
# reader can tell a genuine duplicate registration from a mis-attribution at a glance.
274+
collapsed = {name: entries for name, entries in name_of_entry.items() if len(entries) > 1}
275+
if collapsed:
276+
detail = "; ".join(f"{name} <- {', '.join(sorted(entries))}" for name, entries in sorted(collapsed.items()))
277+
die(
278+
f"{len(registered)} registrations resolved to only {len(names)} names. Two registrations "
279+
f"naming one class means this file read them wrong, and a short loadable set reads as "
280+
f"'not loadable': {detail}"
281+
)
246282
return names
247283

248284

0 commit comments

Comments
 (0)