docs: 회신 3 반영 — H-229 별해 검토(인자 전달 불충분), H-231 error 유틸 설계 등재

- H-229: 사용자 별해(retractor가 inst를 인자로 받아 업밸류 캡처 제거) 검토 —
  retractor의 Observer 캡처 → observer.fn의 inst 캡처 경로가 남아 불충분.
  끝까지 끊으려면 클로저 상태를 Relate/weak 셀로 밀어내야 하는데 이는 확정
  설계(kSlotMap 삭제 — upvalue 캡처)의 역전이고 불변식이 전 핸들러 작성자에
  분산됨. gchold 앵커((a))는 불변식을 Dispatch 한 줄에 중앙화 — 사용자의
  안전 직감("후자가 안전해보여")과 같은 결론
- H-231(② 신설, 사용자 설계): error 유틸 — setLevel(fn, tag) 맵 +
  debug.info 스택 워커. H-219 drive 도착지 한계·H-212 스텁 level 미정·
  프레임 손 세기 전부 자연 해소. 사용자 유의점("클로저 수령분 등록 필요,
  안 그럼 중간에 짤림 / 아니면 최상단부터") 반영 — 권고: (i) 최상단 하강
  (경계 직전 blame, 등록 1회성) + 독립 패키지(가칭 quad-error) + 단위 2 전
  일괄 이관 + 선행 스파이크(인라이닝/코루틴/프레임 보정)
- H-230: level Enum화 반론(리터럴 전제)은 워커 설계로 소멸 — H-231에 흡수,
  H-230엔 우선순위 상수 배치만 남김

Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01LF78pXeFGD1ZSVD3ifteYG
This commit is contained in:
qwreey-agent-selene 2026-08-31 17:10:44 +09:00
parent 6165e55460
commit d2d69edcc3
No known key found for this signature in database
2 changed files with 76 additions and 2 deletions

View file

@ -34,7 +34,8 @@
| `H-227` | ① | 1 | 🟡 | (`/code-review high`) `local Dispatch = {} :: any`가 생산자 표면을 `quad-types` 선언과 대조 불능으로 만듦 — `H-25`가 막으려던 드리프트가 생산자 쪽에서 무검사 | ✅ 반영 — 로컬 함수 정의 후 `local Dispatch: Dispatch = { … }` 타입 주석 조립(analyze가 표면 검사) |
| `H-228` | ① | 1 | 🟢 | (탐사자) `describeHandler``name` 부재 시 `"?"`를 이름 자리에 찍는다 — `dispatch-core-plan.md` "핸들러 계약" 절의 `H-214` 블록은 *"없으면 priority만 보인다"*. 실측: `handler priority tie between "?" (priority 5) and "?" (priority 5)` | ✅ 반영 — 코드를 문서에 맞춤: 이름 부재 시 `(priority N)`만 |
| `H-229` | **②** | 1 | 🔴 | (사용자 되물음 → 검증 확인) **`H-218`의 누수는 위임 경로만의 문제가 아니다 — 일반 `Destroy` 경로 전체가 같은 패턴으로 샌다.** Destroy 시 retract를 안 부르는 건 계약("오직 같은 key에 새 값" 시나리오만)인데, gchold 섬은 Destroy로 무너져도 **`chains`의 강한 버킷 값이 retractor→`inst`를 별도 루트로 붙잡아** weak 항목이 영영 안 비워진다 — 반응형 바인딩이 하나라도 있던 모든 파괴 인스턴스가 소량 부기+`inst` userdata를 영구 잔존시키고, 죽은 Observer가 상류 State의 전파 순회에 계속 남는다(회당 `canExecute` 스킵 비용) | ⏳ §4 대기 — 분석 상세는 아래 `H-229` 절 |
| `H-230` | **②** | 1 | 🟢 | (사용자 제안) 공유 상수의 배치 — `HANDLER_PRIORITY_*`를 quad-base 밖(백엔드)에서도 의미 있게 쓰려면 `quad-const` 신설 또는 `quad-types` 승격이 필요하다는 제안 + error `level` 넘버도 Enum화하자는 제안 | ⏳ §4 대기 — 권고: 우선순위 상수는 `quad-types` 승격 (a), level Enum화는 비권고(프레임 수라 자리마다 값이 갈림) |
| `H-230` | **②** | 1 | 🟢 | (사용자 제안) 공유 상수의 배치 — `HANDLER_PRIORITY_*`를 quad-base 밖(백엔드)에서도 의미 있게 쓰려면 `quad-const` 신설 또는 `quad-types` 승격이 필요하다는 제안. ~~error `level` 넘버 Enum화~~는 회신 3의 워커 설계로 **`H-231`에 흡수**(리터럴 전제의 "프레임 수라 Enum 불가" 반론은 워커가 소멸시킴) | ⏳ §4 대기 — 권고: 우선순위 상수는 `quad-types` 승격 (a) |
| `H-231` | **②** | 1 | 🟡 | (사용자 설계, 회신 3) **error 유틸 모듈**`setLevel(fn, tag)` 등록 맵 + `debug.info(i, "f")` 스택 워커로 "원하는 계층까지 올려서" error를 내는 유틸(type-version-check처럼 독립 패키지). 프레임 수 손 세기(2·3 갈림, `H-219`의 drive 도착지 한계, `H-212` 스텁 level 미정)가 전부 자연 해소되고 level이 진짜 의미 Enum이 된다. 에러는 cold path라 `debug.info` 비용 무시 가능(사용자 논거, 동의) | ⏳ §4 대기 — 세부 문항·권고는 아래 `H-231` 절 |
### `H-212` — base 의사코드 error가 error 계약 이전 표기로 남아 있었다 (①)
@ -130,6 +131,71 @@ bind들이 자동으로 끊어지고 gchold가 사라질 수 있었다는 건데
전부 `Void`(캡처 없음)라 새지 않는다 — 새는 것은 반응형 바인딩이 있던
것들이다.
**[2026-08-31 회신 3 — 사용자 별해 검토]** 사용자 제안: *"retract 클로저가
inst를 받으면 될 것 같다는 느낌. retractFrom이나 process에서 이미 inst를
알고 … 온디맨드로 넘겨줄 수 있어. 그러면 retract 자체가 업밸류를 캐치하지
않게 돼"* + 스스로 단 캐비엇: *"어떤 방식으로든 실수가 나게 된다면 Strong
이라서 gc가 안 되는 사고가 날 수 있게 되어 후자(gchold 앵커)를 제공하는 게
안전해보이기는 해"*. **검토 결과 — 인자 전달만으로는 경로가 안 끊긴다**:
retractor가 `inst`를 직접 캡처하지 않아도, StoreBind류 retractor는 자기
Observer를 캡처해야 하고(구독 해제 대상) 그 **Observer의 콜백이 재디스패치를
위해 `inst`를 캡처**한다 — chains → retractor → observer → fn → `inst`
경로가 그대로 남는다. 이걸 끝까지 끊으려면 핸들러 상태를 클로저 밖(weak
셀/Relate 조회)으로 밀어내야 하는데, 그건 확정 설계("단발성 handoff는
upvalue 캡처로 충분" — 옛 `kSlotMap` 삭제, Handler 작성 체크리스트 4번)의
역전이고, **불변식이 모든 핸들러 작성자(서드파티 포함)에게 분산**되며
위반의 증상이 조용한 영구 누수다. gchold 앵커((a))는 캡처 인체공학을
그대로 두고 불변식을 **Dispatch 안의 한 줄**에 중앙화한다 — 사용자의 안전
직감과 같은 결론.
### `H-231` — error 유틸 모듈: 스택 워커로 level 손 세기를 없앤다 (②, 사용자 설계)
사용자 설계(회신 3): *"error 자체를 모듈로써 제공 … `debug.info(2, "f")`
하면 각 프레임에 대한 함수를 구할 수 있어. 간단하게 함수를 태그해주는 맵만
있다면 에러를 원하는 레벨까지 낮춰서 내준다를 지금처럼 어렵게 세어 가며 처리
안 해도 되고, 여러 래퍼가 들어가도 사용자 지점까지 확실히 올려낼 수 있어 …
type-version-check와 유사하게 외부에 유틸을 파. `setLevel(fn, num)` … 등록 안
된 함수는 level 0 … 그러면 num을 Enum으로 둘 수 있고, 우리가 한계라고 했던
부분이 해소돼"* + 유의점(추가 회신): *"클로저로 입력 받은 것도 입력 받은
측에서 level을 설정해야 할 수도 있어 … (안 그럼 중간에 짤릴 수도 있으니까)
아니면 최상단부터 구한 다음, 거기서 진입하는 게 옳을 수도 있어"*.
**평가 — 채택 권고.** 이 유틸이 있으면: `H-219`의 drive 도착지 한계(현행
유지로 닫았던 것)가 근본 해소되고, `H-212`가 "구현 시점 몫"으로 미룬 주입 op
스텁 level도 답이 생기며, `H-205`류 "헬퍼 프레임 하나에 2→3" 손 세기가
사라진다. 에러는 cold path라 비용 논거도 맞다. 사용자가 짚은 "중간에 짤림"이
정확히 급소다 — 세부 선택지:
- **워커 방향****(i) 최상단 하강(권고, 사용자 후자 제안)**: 스택 최상단
(사용자 루트)에서 내려오다 **처음 만나는 등록(quad) 프레임의 직전**에서
낸다. 등록이 필요한 건 quad의 정적 표면 + `addHandler`/주입 시점에 받는
제공자 클로저뿐(1회성) — 런타임에 생기는 익명 클로저(retractor, observer
콜백)는 경계 아래라 애초에 검사 안 되므로 "중간에 짤림"이 구조적으로 없다.
blame은 항상 "quad로 들어온 최상위 진입점의 호출부"(drive면 `New` 호출부,
전파면 `:Set` 호출부) — 일관되고 안전. /
(ii) 에러 지점 상승(첫 미등록 프레임에 blame): 샌드위치된 사용자
콜백까지 정밀 blame되지만, quad가 **만들거나 받는 모든** 클로저(retractor
포함 — 매 `process`마다 생성되는 핫패스!)를 등록해야 하고 누락 하나가
내부 클로저를 사용자로 오인시킨다. 정밀도 이득 대비 규율 표면이 너무 넓다.
- **배치** — type-version-check처럼 **독립 워크스페이스 패키지**(가칭
`quad-error`): 백엔드(EngineOps 스텁·핸들러)도 써야 하는데 그쪽은
quad-base를 의존하지 않는다. 이름은 사용자 확정 대상.
- **Enum 구성**`H-222`의 표와 1:1로: 예) `Level.User`(사용자 호출부까지) /
`Level.Provider`(제공자 계약 위반 — 같은 워커, 등록 집합만 다르게 볼지) /
`Level.Here`(내부 불변식 = 그 자리, 워커 불필요). 멤버 구성은 확정 대상.
- **선행 스파이크(luau-test)** — 추론으로 확정하지 않는다: (a) `debug.info`
인라이닝(-O2)·네이티브 코드젠에서 프레임을 어떻게 보이는지(등록 함수가
인라인되면 걷기가 짧아질 뿐인지), (b) 코루틴 경계(스택 루트가 코루틴
진입점일 때의 degrade), (c) 걷은 깊이 → `error(msg, n)` 프레임 계산이
유틸 자신의 프레임을 정확히 보정하는지.
- **이관 시점** — 기존 error 자리(M2 커밋분 + 단위 1)의 리터럴 level을
유틸 호출로 바꾸는 걸 단위 2 착수 전 일괄로 할지, 마일스톤 진행하며
점진으로 할지.
`architecture.md`의 error 계약 절(방금 신설한 제3 행 포함)은 채택 시 이
유틸 기준으로 재서술된다 — "도착지가 계약"이라는 원칙 자체는 그대로이고,
도착지를 세는 방법이 손에서 워커로 바뀌는 것.
## §4 배치 문항지 (사용자가 읽을 유일한 자리)
**⭐ [2026-08-31 회신 1]** 사용자: *"배치 문항은 중간확인 완료했어. 전부
@ -142,7 +208,13 @@ bind들이 자동으로 끊어지고 gchold가 사라질 수 있었다는 건데
(*"Destroy 호출되는 것도 retract가 안 먹어서 문제가 생긴다는 부분 아냐?
한번만 다시 봐줘"* → 검증 결과 **맞았다**, `H-229`)과 **상수 배치 제안**
(*"quad-const 등을 만드는 게 좋아보임 … 혹은 그러한 Enum 값도 타입으로 보고
quad-types에 할당하는 건 어떤지?"* → `H-230`). **아래 두 행이 회신 대기.**
quad-types에 할당하는 건 어떤지?"* → `H-230`).
**⭐ [2026-08-31 회신 3]** `H-229`에 사용자 별해(retractor 인자 전달) 검토
요청 — 검토 결과 Observer 캡처 경로가 남아 불충분, gchold 앵커 쪽이 맞다는
분석을 `H-229` 절에 추가. 그리고 **error 유틸 모듈 설계**(`setLevel` 맵 +
`debug.info` 스택 워커, "중간에 짤림" 유의점과 최상단 하강 대안까지 사용자
제안 → `H-231`). **아래 세 행이 회신 대기.**
| 번호 | 무엇 | 선택지 | 권고 | 권고 근거 |
|---|---|---|---|---|
@ -152,6 +224,7 @@ quad-types에 할당하는 건 어떤지?"* → `H-230`). **아래 두 행이
| `H-222` | 제공자(핸들러 작성자) 계약 위반 — retractor 생략·매치 실패류 — 의 **`level` 분류**가 `architecture.md` 계약 표(사용자 입력 2 / 내부 불변식 1)에 없다. 지금 코드는 잠정 2 | (a) **표에 세 번째 행 신설** — "제공자 계약 위반 = 2(그 계약을 어긴 호출 구조에 가장 가까운 프레임)" / (b) 표는 안 늘리고 잠정 2 유지(주석만) / (c) 내부 불변식으로 보고 1 | **(a)** | M3 단위 2~4·M5·M10의 provider-facing error 전부가 같은 분류를 반복해서 물을 자리라, 표에 한 줄 넣는 게 자리마다 잠정 표시를 다는 것보다 싸다. (c)는 "quad 자신의 버그"가 아니라 제공자의 버그라 표의 1행 정의와 안 맞다 |
| `H-229` | **일반 `Destroy` 경로의 chains 잔존** — Destroy는 retract를 안 부르는 게 계약이고 gchold 섬은 Destroy로 무너지지만, `chains`**강한 버킷 값**(retractor→`inst` 캡처)이 별도 루트로 남아 weak 항목이 영영 안 비워진다. 반응형 바인딩이 있던 모든 파괴 인스턴스가 부기+userdata 영구 잔존, 죽은 Observer가 상류 전파 순회에 잔류(상세는 `H-229` 절) | (a) **체인 리스트의 GC 앵커를 gchold로 통일**`chains`는 리스트를 `SetWeak`으로만 잡고, 버킷 첫 생성 시 `bindLifetime(inst, list)`로 리스트를 gchold에 앵커(Destroy → gchold 붕괴 → 리스트·retractor·`inst` 전부 회수; retractor는 **호출 안 함** — 계약 유지, 메모리만 해제). 선례: `slot-plan.md` 13차 세션이 두-`Relate` 상호 순환을 정확히 이 약(전부 weak + 앵커는 `bindLifetime` 하나)으로 고침. ⚠️ 결합 캐비엇: Dispatch가 생명주기 주입을 요구하게 돼 순수 디스패치 테스트도 mock 인스턴스가 필요해짐(단위 1 spec의 평범한 테이블 inst 수정) / (b) 버킷 첫 생성 시 `onDestroying(inst, …)` 훅으로 버킷 드롭 — 같은 결합 + Connection 부기 추가 / (c) 수용·문서화(파괴 인스턴스당 소량 영구 잔존 + 상류 emit 순회 비용) | **(a)** | 같은 병(값이 weak 키를 되참조하는 상호 순환)을 같은 약으로 고친 확정 선례가 있고, 새 개념 없이 기존 프리미티브(`bindLifetime`/gchold) 재사용이다. (b)는 앵커 대신 훅이라 Connection 관리가 하나 더 생기고, (c)는 장수명 게임의 동적 UI에서 무한 누적이라 라이브러리 목표(정확성 우선)와 안 맞다 |
| `H-230` | **공유 상수의 배치** — ① `HANDLER_PRIORITY_*`: 백엔드(M5 quad-roblox)가 핸들러 등록 시 필요. 지금도 `InitRoblox(module)`이 받는 인스턴스의 `module.Dispatch.*`로 닿긴 하지만, 사용자 제안대로 의미가 드러나는 단일 소스가 낫다. ② error `level` 넘버(1/2/3)의 Enum화 | (a) **우선순위 상수만 `quad-types`로 승격** — 이미 모든 패키지가 의존하는 의존성-0 계약 패키지고 런타임 테이블을 반환하므로 값 넷을 싣는 비용이 0에 가깝다(`Dispatch`는 require해 재노출, 단일 소스; 머리말의 "런타임 값은 없다시피" 서술 갱신). **level은 Enum화하지 않는다**`level`은 의미 enum이 아니라 **프레임 수**라 같은 의미("사용자 호출부")가 자리에 따라 2·3으로 갈리고(`architecture.md` *"프레임 수가 아니라 도착지가 계약"*, `H-205`의 level 3이 실사례), `ERROR_LEVEL_USER = 2` 상수는 헬퍼 프레임이 끼는 자리에서 거짓말을 하게 된다 / (b) `quad-const` 패키지 신설(상수 전부 이관) / (c) 현행 유지(모듈 인스턴스 경유) | **(a)** | (b)는 숫자 넷에 워크스페이스 멤버·pesde shim·relink 비용이 과하고(`H-165`류 함정도 하나 더 생김), 지금 상수가 이것뿐이라 "상수 패키지"가 설 자리가 아직 없다 — 상수가 실제로 불어나면 그때 분리해도 늦지 않다("실제로 관측된 문제에만 구조"). level 비권고 근거는 왼쪽 칸 |
| `H-231` | **error 유틸 모듈**(사용자 설계) — `setLevel(fn, tag)` + `debug.info` 스택 워커로 level 손 세기 제거. 채택 자체는 사용자 발안이라 사실상 정해졌고, **세부 넷**이 문항: 워커 방향 / 패키지 이름 / Enum 멤버 구성 / 기존 error 자리 이관 시점(상세는 `H-231` 절) | 방향: (i) **최상단 하강**(경계 직전 blame — 등록은 정적 표면+수령 클로저 1회성, 익명 내부 클로저에 강건) / (ii) 에러 지점 상승(정밀하나 retractor까지 핫패스 등록 + 누락 = 오인). 배치: 독립 패키지(가칭 `quad-error`) vs quad-base 잎. 이관: 단위 2 전 일괄 vs 점진 | **(i) + 독립 패키지 + 단위 2 전 일괄 이관** | (i)는 사용자의 "중간에 짤림" 유의점을 구조적으로 없애고 등록 규율이 1회성 표면에 갇힌다. 독립 패키지는 backends(EngineOps 스텁)가 quad-base 없이 쓰기 위함(type-version-check 선례). 일괄 이관은 자리가 아직 적을 때(M2+단위 1) 끝내야 마일스톤마다 두 표기가 섞이지 않아서 |
| `H-215` | 스파이크 `04`(Dispatch 체인 retractFrom, `rewrite-required/`) 처분 — `spec.dispatch.luau`가 체인 깊이·레벨별 힌트·3-인자 `retractFrom`·`SetStrong` 음성 대조군을 이미 실측했으나, 재귀 재발행은 spec-로컬 wrapping 핸들러 **근사**다(실제 `StoreBind`는 M4) | (a) 지금 폐기(`01`처럼 `done/` 이동) — 잔여 몫(실제 `StoreBind` 경유 재발행 경로)은 **M4 StoreBind spec이 진다**고 그 단위 계획에 명시 / (b) M4까지 `rewrite-required/`에 유지(현 ROADMAP 문구 "M3 착수 시 같이 처리"를 "M4에서"로 정정) / (c) 지금 재작성 | **(a)** | 체인 메커니즘 자체는 `spec.dispatch.luau` 12절이 실제 구현에 대고 고정했고(스파이크는 격리 재현이라 오히려 약함), `05`/`15`/`01` 폐기와 같은 근거 구조다. 잔여 몫을 M4 spec 항목으로 옮겨 적으면 잊히지 않는다 |
## §5 이상 없음 확인 (탐사자·구현이 확인만 하고 문제 없었던 자리)

1
.gitignore vendored
View file

@ -15,3 +15,4 @@ lune_packages/
# (에디터 타입 링킹용 산출물, 커밋 대상 아님)
sourcemap.json
.relink-manifest
*ignoreme*