4라운드 종결 때 "안 만든다"고 했던 5라운드를 사용자 요청으로 신설(205문항). 범위를 셋으로 좁힘 — (1) 4라운드에 문항이 아예 없던 영역(project-setup / quad-types, 그리고 문서가 아니라 실제 커밋된 M1 코드), (2) 그 이후 확정된 것 (Detach/KeyGone/Owned/attachSlot 분해), (3) 큰 문서의 심화. 회신을 4차에 걸쳐 받아 전량 반영했고, 커밋 전 감사를 각도를 바꿔 2라운드 돌렸다. 주요 확정/역전: - slot._detached lazy화, KeyGone엔 새 값 반환도 error, Owned=false에서 Detach는 _detached에 안 들어감(rawUnmount) - Slot:Replace 신설 + rawReplace/rawAdd 의사코드 신설(문서에 정의가 없었음) - raw* 인자를 index로 전부 통일 — 오래 열려 있던 캐비엇 종결. 래핑은 raw* 바깥에서만(공개 표면 + settle), raw*는 물리 요소만 다룸 - 물리 조작을 주입 op로(mountInst/unmountInst/disposeInst, 이름 가칭) — base는 Parent를 모른다는 지적. mountInst는 0-based 절대 offset을 받음 - Dispatch.setLength에 anchor(생략 시 ownerKey) — 부기 키와 생명주기 앵커 분리, 4라운드 D-56 역전(archive로) - Dispatch.getOffsetAt 신설(pull) + 접두합 캐시(offsetDirtyFrom), setOffsetSource(None)은 얼리 리턴, None의 뜻을 "발행 채널 없음"으로 정정 - recompute가 owner 베이스에서 시작(중첩 offset이 부모 베이스를 못 받던 결함), _baseObserver로 깊은 전파, Offset Source identity 재사용(포탈), bk.N or 0(빈 Slot 크래시) - Effect(fn, ...deps) 확정 — Ref도 의존성(옛 "trailing args sugar 안 만듦" 역전), Tween:Mapped, groupClaimKeys 키 = (inst, groupValue) → k - 게이팅 먼저(M2로 앞당김) — 다만 대상이 Blocker가 아니라 공용 Gate 노드로 바뀌었고, 설계는 사용자 지시로 다음 세션(M2 착수를 막는 유일한 항목) 새 research 둘: gate-primitive.md(다음 세션이 이어받을 재료), state-epoch-validation.md(전파 중 Get이 섞인 값을 캐시하는 glitch — 정확성 결정이라 M3 전 결론 필요). 감사가 잡은 것 중 큰 것: 확정한 Owned가 Slot:List 시그니처에 배선이 안 돼 코드에 도달 못 하던 것, effect-plan.md의 역전 배너 없는 자기모순, 그리고 손대지 않은 문서(ROADMAP 백로그·debounce-throttle-plan)가 "Gate는 M3에서"로 남아 있던 사각지대. doc-check.py ERROR 0. 상세는 qa-request/pre-implementation-qa-round5-followup.md (A~K절, 마지막이 최신). Co-authored-by: qwreey <me@qwreey.moe>
3.9 KiB
[역전됨, 2026-08-21] bindLifetime의 첫 인자가 Slot일 수 있다는 요구사항
상태: 역전됨. 2026-08-20 구현 전 QA 4라운드 D-56에서 확정돼
base/lifecycle-pattern.md의 (1-1) 절로 들어갔다가, 2026-08-21 구현 전 QA
5라운드 C-4에서 통째로 뒤집혔다.
왜 뒤집혔나: 사용자 지적 — "애초에 Slot 이 effect 나 다른 요소들을 소유할
수가 없다 … 실제 observer/effect 는 실제 inst 에 불림. slot in slot 에서 slot 을
유지하는건 이미 slot 의 강참조 배열이 해결해주는데, 우리가 왜 slot 을 소유 대상으로
둘 수 있게 한거였는지 다시 생각해봐야할 부분인듯?" 검토 결과 부기 키와
생명주기 앵커를 한 인자가 겸하고 있던 게 원인이었고, Dispatch.setLength가
anchor를 따로 받도록 바꾸는 것만으로 이 요구사항 전체가 사라졌다. 지금 유효한
결론은 base/dispatch-core-plan.md의 "setLength 구현" 절 뒤 문단과
base/lifecycle-pattern.md의 (1-1) 포인터.
부수 효과: 이 절이 요구하던 isBoundAlive의 "세 번째 분기"(gcconn도
.Subscribed도 없는 Slot-owned 바인딩을 판정하는 분기)는 형태가 미정인 채로
열려 있던 항목이었는데, 역전과 함께 필요 자체가 없어졌다.
역전 전 원문
(1-1) ⚠️ 첫 인자가 물리 Instance가 아닐 수도 있다 — 백엔드가 반드시 핸들링할 것 (2026-08-20 구현 전 QA 4라운드 D-56)
위 구현 스케치는 inst가 항상 Roblox Instance라고 가정하고 InstData에서
gcconn/gchold를 찾는데, 실제 호출부 중엔 inst 자리에 Slot이 오는 경로가
이미 있다. Dispatch.setLength(ownerKey, i, len)이 그것 —
base/dispatch-core-plan.md의 "setLength 구현" 절이 bindLifetime(ownerKey, observer)를 부르는데, 그 ownerKey는 Slot-in-Slot 중첩에서 Slot 자신이다
(base/slot-plan.md의 "재귀 메커니즘" 절 — attachSlot이 ownerKey로 자기
자신을 넘겨 최상위/중첩을 같은 함수로 통합한 그 설계).
사용자 판정(2026-08-20): "ownerKey 가 Slot일 수도 있음. 각 엔진의 bindLifetime 은 이를 잘 핸들링 해줘야함. 즉, Slot안에, 또는 바깥에 SetStrong 으로 gchold 비슷한걸 수행하면 됨."
- 계약 두 개(위 절)는 그대로 유지된다 — 바뀌는 건 "그 계약을 무엇으로
구현하는가"뿐. 물리 Instance면 gcconn 트릭이 두 계약을 다 만족시키고,
Slot이면 Slot 자신이 살아있는 동안
value를 붙잡는 강참조(Slot 안의 필드든,Relate(slot)에SetStrong이든)와value가 그 Slot의 생존을 되물을 수 있는 근거를 백엔드가 제공하면 된다. - 왜 gcconn을 못 쓰는가: gcconn 트릭은
inst:GetPropertyChangedSignal("ClassName")에 의존하므로 엔진 객체가 아닌 값(Slot은 평범한 Lua 테이블)엔 걸 수가 없다. Slot은 대신 자기 자신이 reachable한가가 곧 생존이라,Relate(slot)가 weak-keyed인 것만으로 "Slot이 죽으면 기록도 같이 사라진다"가 성립한다. isBoundAlive의 판정 분기도 이 경로를 알아야 함 — 지금 코드는 gcconn이 없으면 곧바로.Subscribed폴백으로 떨어지는데, Slot-owned 바인딩은 gcconn도.Subscribed도 없어서 살아있는데canBound가 참으로 잘못 나온다(= 이중 바인딩 가드가 이 경로에선 안 걸림). 백엔드 구현이 세 번째 분기를 추가하거나, Slot 쪽 홀더 존재 자체를 판정 근거로 삼아야 함.- ⚠️ 정확한 형태는 아직 미확정 — M2/M3 구현 시 확정할 것. "Slot 안"(필드)
이냐 "바깥"(
Relate)이냐,isBoundAlive의 세 번째 분기를 어떤 모양으로 둘지가 열려 있다. 지금 확정된 건 "첫 인자가 Instance라고 가정하면 안 된다"는 요구사항 자체뿐.