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
13 changes: 13 additions & 0 deletions REPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,19 @@
- 검증: DoD 9명령 · 아래 절.
- ★후속 추천: **그 사각이 «제품인지 테스트인지»를 말할 것인가**(S). 상세 = `docs/worklog/2026-09-19-nonliteral-blind-spot-is-reported-not-gated.md`.

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

## [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 @@ -57,6 +57,13 @@
★**축**: 제품 코드 주입 → **1→2**(파일:줄 지목) · 원복 → 1 · rc 양쪽 0 · 술어 민감도 **42**.
★**대가**: 찍힌 수는 무시할 수 있다 · 수는 술어만큼만 정확 · 제품/테스트 미구별(후속 카드).
★**회귀 둘을 스스로 만들고 재서 걷어냈다**(두 번 걷기 · 개행 인덱스) ⇒ 최종 비용 **유의차 없음**.
- [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.**
★**먼저 쟀다**: 같은 것을 네 가지로 세어 **전부 268** · ★**틀리게 울 조건도 0**(한 줄 둘·쉼표 없음·주석 속·중복 이름) ⇒ **green 에서 시작하는 회귀 방어**다.
★**두 축**: ⑴파싱 ↔ **독립 증인**(의심 대상의 자기 매치는 증명이 아니다) ⑵등재 수 ↔ 고유 이름 수(★접힌 쌍을 **이름으로** 찍는다).
★**양방향**: 초판 결함 «둘»을 제품 스크립트에 되살려 각각 **rc=2**(265↔268 · 268→263 + 충돌쌍 지목) · 원형상 **rc=0**.
★**브리프 3문**: 비교 대상 = **소스 자신**(기준선·직전값 아님) · 첫 실행/정당한 감소 = ★**생기지 않는다**(기억이 없다) · ★**죽는다(exit 2)** — 형제 회차의 「찍고 안 죽는다」와 **갈렸고 사유를 적었다**(사각 측정 ↔ 자기 입력 오독).
★**대가**: 등재 배열 «밖» 호출이면 false red · 진짜 중복 등재도 false red(오늘 0) · ★**바닥이지 증명 아님**(다른 이름으로 틀리면 통과) · 비용 유의차 없음.
★**제안 `tradeoff` 와 갈렸다**: 「줄 형태에 묶인다」를 ★**출현 계수**로 없앴다(오늘 줄 수와 동일).
- [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,52 @@
{
"date": "2026-09-19",
"taskId": "rustjava-assert-loadable-rederivation-did-not-come-up-short",
"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.",
"measurements": {
"proto_call_occurrences": 268,
"proto_comma_lines": 268,
"crate_classes_occurrences": 268,
"registrations_parsed": 268,
"distinct_names": 268,
"registrations_sharing_a_line": 0,
"proto_without_trailing_comma": 0,
"proto_in_comments": 0,
"duplicate_resolved_names": 0,
"files_changed": 1,
"lines_added": 38,
"lines_removed": 2,
"runtime_before_seconds": [
6.47,
8.01,
4.57
],
"runtime_after_seconds": [
6.35,
5.68,
7.53
]
},
"verification": [
"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",
"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'",
"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",
"unmutated: rc 0, 43 named classes across 846 call sites, all 268 loadable",
"cost: before 6.47/8.01/4.57s vs after 6.35/5.68/7.53s, overlapping"
],
"changes": [
"scripts/check-named-exception-classes-are-loadable.py - PROTO_CALL witness plus two fail-closed assertions in loadable_classes()",
"docs/worklog/2026-09-19-assert-rederivation-did-not-come-up-short.{md,json}, REPORT.md, STATE.md"
],
"issues": [
"The proposal has no how field; this implementation follows its why and departs from its tradeoff.",
"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).",
"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.",
"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.",
"It is a floor, not a proof: a registration mapped to a wrong but distinct name keeps both counts at 268 and passes.",
"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."
],
"adoptedProposals": [
"2026-09-19-loadable-set-source-of-truth#p0"
],
"proposals": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# 2026-09-19 — The check now proves its own reading of the loader, instead of trusting it

Round: `rustjava-assert-loadable-rederivation-did-not-come-up-short`
Adopted proposal: `2026-09-19-loadable-set-source-of-truth#p0`

The proposal has **no `how` field** — `title`, `plainSummary`, `userBenefit`, `why`, `tradeoff`,
`effort`, `target` only. What follows says where this implementation matches its `why` and where it
deliberately departs from its `tradeoff`.

## First: is anything short today?

No. Measured on `origin/main` @ `ad9eb1a6`, four independent counts of the same thing:

| count | value |
|---|---|
| `_proto()` occurrences in `loader.rs` | **268** |
| `_proto(),` lines | 268 |
| `crate::classes::` occurrences | 268 |
| registrations `REGISTERED` parses | 268 |
| distinct names resolved | **268** |

Also measured, because they are what would make the assertion fire *wrongly*: registrations sharing
a line **0**, `_proto()` without a trailing comma **0**, `_proto()` inside a comment **0**, duplicate
resolved names **0**.

So the assertion starts green and guards a regression rather than fixing a present defect. The
proposal says as much; this round confirms it with numbers rather than assuming it.

## What was added — two axes, both fail-closed

1. **Parsed vs. witnessed.** `REGISTERED` is the pattern under suspicion, so counting its own matches
proves nothing. `PROTO_CALL` counts the same calls a second way, by the one token a registration
cannot be written without. Mismatch ⇒ `cannot measure` (exit 2).
2. **Registrations vs. distinct names.** Two registrations resolving to one name means the resolution
is wrong — it is exactly what bare-name keying did. Mismatch ⇒ exit 2, **naming the collapsed
pairs** so a genuine duplicate registration can be told from a mis-attribution at a glance.

**Changed: 1 file, +38/−2.**

## Axis — bidirectional, on the product script, by re-creating the two real defects

| the script, mutated back to a defect it actually had | result |
|---|---|
| `((?:as\|list)_proto)` → `(as_proto)` (the first draft) | **rc 2** — `265 registrations parsed but 268 proto calls are in rustjava-runtime/src/loader.rs` |
| 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()`, … |
| unmutated | **rc 0** — `✓ 43 named exception class(es) across 846 call site(s); all 268 loadable` |

The second message is the point of the round: gate 2 found that colliding pair by reading the source
alongside the script. The script now says it.

## The three questions the brief asked

**⒜ What is it compared against?** The source itself — a second count of the same file. Not a stored
baseline, not the previous run.

**⒝ First run, and legitimate decreases?** They do not arise, and that is *why* this shape was
chosen. A remembered number would have to answer both; a self-contained invariant answers neither
because it never remembers anything. Removing a class legitimately drops all counts together and
stays green.

**⒞ Die or speak?** **Die — exit 2, `cannot measure`.** The sibling round
(`2026-09-18-nonliteral-exception-call-sites#p0`) chose "count and print, never fail" for the
non-literal blind spot, and this round deliberately differs: that is a *known limitation* being
sized, this is the check *mis-reading its own input*. The file already has a category for the
latter — it dies on a registered entry whose name cannot be resolved — and this is the same failure
one step earlier. A check whose loadable set is short reports real classes as unloadable and missing
ones as present; printing that and exiting 0 would be the silent pass the file's docstring is about.

## What this costs

- **Two more numbers that have to stay true.** If `loader.rs` ever calls `as_proto()` outside the
registration array, `PROTO_CALL` counts it and the check goes red on a correct tree. Measured
today: every one of the 268 calls is a registration (`crate::classes::` count matches exactly).
- **A false red is possible for a genuine duplicate registration** — two entries deliberately naming
one class. There are none today, and the message names them, but it would be a red on a tree that
is arguably fine.
- **It is a floor, not a proof** — the proposal's own words. A registration mapped to a *wrong but
distinct* name keeps both counts at 268 and passes. This catches undercounts, which is what both
measured false greens were.
- Runtime: **no significant change** — before 6.47 / 8.01 / 4.57 s, after 6.35 / 5.68 / 7.53 s
(overlapping; the machine is loaded and the spread is wider than the effect).

## Departure from the proposal's `tradeoff`

It predicted the check would be *"coupled to the textual shape of `loader.rs` (one registration per
line), which is true today and is not guaranteed."* That coupling was avoidable and was avoided:
counting `_proto()` **occurrences** rather than lines makes the witness independent of line layout
and of trailing-comma style. Measured equal to the line count today (268 = 268), so nothing is lost
by the more robust form.
40 changes: 38 additions & 2 deletions scripts/check-named-exception-classes-are-loadable.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@
IS_DEFINITION = re.compile(r"\bfn\s+$")


# The independent witness for "did the parse come up short". `REGISTERED` is the pattern under
# suspicion, so counting its own matches proves nothing; this counts the same calls a second way,
# by the one token a registration cannot be written without. Occurrences rather than lines, and no
# trailing comma, so it does not care how the list is formatted -- measured on this file: `_proto()`
# 268, `_proto(),` lines 268, `crate::classes::` 268, registrations parsed 268, all agreeing.
PROTO_CALL = re.compile(r"_proto\(\)")

def die(message):
print(f"cannot measure: {message}", file=sys.stderr)
raise SystemExit(2)
Expand Down Expand Up @@ -229,20 +236,49 @@ def loadable_classes():
if field:
name_of[(module, type_name, function.group(1))] = field.group(1)

registered = REGISTERED.findall(read(LOADER))
loader_text = read(LOADER)
registered = REGISTERED.findall(loader_text)
if not registered:
die(f"no proto registrations found in {LOADER.relative_to(ROOT)}")

names, unresolved = set(), []
# Did this parse come up short? The whole check rests on `registered` being every registration,
# and the failure mode is silent: a pattern that matches fewer entries yields a smaller loadable
# set, and a smaller loadable set makes missing classes look present. Measured on the first draft
# of this file: it matched only `as_proto`, parsed 265 of 268 and resolved 263 names, and said
# nothing. Counting the calls a second, independent way turns that into an exit 2.
witness = len(PROTO_CALL.findall(loader_text))
if len(registered) != witness:
die(
f"{len(registered)} registrations parsed but {witness} proto calls are in "
f"{LOADER.relative_to(ROOT)}. The pattern that reads them is missing some, so the loadable "
"set is short and every class it lost would read as 'not loadable'. Fix REGISTERED rather "
"than trusting this run."
)

names, unresolved, name_of_entry = set(), [], {}
for path, function in registered:
parts = path.split("::")
key = ("::".join(parts[:-1]), parts[-1], function)
if key in name_of:
names.add(name_of[key])
name_of_entry.setdefault(name_of[key], []).append(f"{path}::{function}()")
else:
unresolved.append(f"{path}::{function}()")
if unresolved:
die("registered entries with no resolvable name: " + ", ".join(sorted(set(unresolved))))

# Two registrations that resolve to one name mean the resolution is wrong, not that the runtime
# has a duplicate: it is what the first draft did when it keyed types by bare name and let one of
# a colliding pair answer for its twin (265 parsed, 263 names). Named rather than counted, so the
# reader can tell a genuine duplicate registration from a mis-attribution at a glance.
collapsed = {name: entries for name, entries in name_of_entry.items() if len(entries) > 1}
if collapsed:
detail = "; ".join(f"{name} <- {', '.join(sorted(entries))}" for name, entries in sorted(collapsed.items()))
die(
f"{len(registered)} registrations resolved to only {len(names)} names. Two registrations "
f"naming one class means this file read them wrong, and a short loadable set reads as "
f"'not loadable': {detail}"
)
return names


Expand Down
Loading