fix(slot,docs): 리뷰 지적 5건 정정 — 언마운트/파괴 모순, 계약 개수 모순, 해제 순서 역전

다른 에이전트 리뷰가 짚은 5건을 전부 사실 확인 후 수정.

1. (가장 심각) slot-plan.md의 SlotHandler.process 반환 클로저가 여전히
   destroySlotTree를 불러, 같은 커밋에서 확정한 "State<Slot> 교체는 파괴가
   아니라 언마운트" 결정과 정면 모순 — 그 결정이 적용돼야 할 최상위 dispatch
   경로가 바로 이 클로저인데 "구현상 바뀌어야 하는 것" 절이 reconcile만
   언급하고 빠뜨렸음. 그대로 뒀으면 언마운트 결정 자체가 무의미해질 뻔함.
   → 비파괴 경로 unmountSlotTree 신설(destroySlotTree와의 차이는 딱 둘:
     실제 Destroy 안 함, 자식 releaseOwner 안 함 — 자식이 계속 그 slot
     소유라 통째로 재마운트 가능 = 포탈). 클로저를 unmountSlotTree +
     setOffsetSource(None) -> setLength(0) + unbindLifetime + releaseOwner로
     교체. "자동 경로는 언마운트, 명시적 Remove/Clear/dispose만 파괴"로 정리.

2. bind-system-plan.md "확정된 것" 절이 폐기된 4-메소드 계약을 여전히 확정
   사실로 서술 — 같은 파일 상단은 이미 3-메소드로 갱신돼 자기 문서 내 모순.

3. question.md의 Slot 해제 순서가 거꾸로 — setLength(0)을 먼저 부르라고 돼
   있었는데, 같은 커밋이 slot-plan.md에 추가한 경고가 정확히 그 순서가
   죽어가는 서브트리 Source에 헛된 :Set()을 날린다고 명시.

4. ROADMAP.md M6에 이번 세션 Slot 결정이 전혀 미반영(M0/M2/M4/M10엔 반영됨)
   — 언마운트 전환/dispose/claimOwner vs claimOwnerAt 분리/버그 4건 추가.

5. CLAUDE.md 여섯 번째 세션 항목이 자체 규칙(2~4줄+링크) 위반, 63줄까지
   불어남 — 과거 3000줄 비대화를 유발한 패턴 재발. 15줄로 압축.

6번(StoreBind가 최초 발화에서도 retractFrom을 부르는 것)은 사용자 판단으로
그대로 둠 — 빈 슬롯 retractFrom은 무시 가능하고 일관성이 낫다는 결정.

추가: question.md 0-Y에 사용자 방향 기록 — 순환 타입을 만들기보다 State 타입
자체를 구울 때 인라이닝하는 쪽(Modifier flat 타입 생성과 같은 결). 사람이
직접 확인할 부분이라 0-Z(Attribute)와 함께 사용자 판단 목록으로 이관.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
qwreey 2026-08-13 16:32:04 +09:00
parent 124b706e2c
commit b90b3a6b94
Signed by: qwreey
GPG key ID: D28DB79297A214BD
5 changed files with 144 additions and 76 deletions

View file

@ -2669,9 +2669,15 @@ State/스트림)를 다뤘던 이전 시도가 있어 조사함 — **확인된
## 확정된 것 (더 이상 열린 질문 아님)
- **핸들러 계약**: `isHandlable(inst,k,v)` + `priority` + `process`(구
`bind`) + `retract`(구 `cleanup`) 4종 조합으로 확정 — tbox식 6-hook 세분화는 지금은
- **핸들러 계약**: `isHandlable(inst,k,v)` + `priority` +
`process(inst,k,v,index)` **3종**으로 확정 — tbox식 6-hook 세분화는 지금은
안 함. 실제 구현하며 부족한 지점이 보이면 그때 hook 추가(점진적 확장).
**[정정, 2026-08-13 다섯 번째 세션 — 이 항목이 갱신에서 누락돼 문서 상단
"핸들러 계약" 절과 모순돼 있던 걸 같은 날 리뷰에서 발견]** 예전엔
`process`(구 `bind`) + `retract`(구 `cleanup`) 4종이었으나, `retract`
별도 필드에서 **`process`의 반환값(retractor 클로저)** 으로 합쳐짐 —
이름과 개념은 그대로 유효하고 자리만 옮겨온 것(위 "핸들러 계약" 절이
정본).
- **Signal 클래스**: 안 만듦, 콜백 + `Connected` 계산 속성만(`base/
lifecycle-pattern.md`).
- **Ref**: 도입 확정(위 절 참고), 용도는 "id 기반 조회 대체"가 아니라 "외부

View file

@ -340,13 +340,27 @@ function SlotHandler.process(inst, k, slotValue, index)
-- no-op을 심어두면 그 다음 진짜 교체 때 이전 서브트리를 정리할 주체가 사라짐.
return function(hintValue)
if slotValue == hintValue then return end -- 같은 Slot 재발행 → no-op
destroySlotTree(slotValue) -- 재귀 파괴(자식 정리), 폐기·옮기지 않음 — slot 자신의 GC 앵커는 안 건드림(top-level 전용)
unbindLifetime(inst, slotValue) -- top-level 자신의 GC 앵커 해제 — attachSlot의 top-level bindLifetime과 짝
releaseOwner(slotValue, inst)
-- [정정, 2026-08-13 감사 후속] 파괴가 아니라 **언마운트**
-- 아래 "`State<Slot>` 교체는 파괴가 아니라 언마운트" 절이 확정한
-- 결정이 적용돼야 하는 자리가 바로 여기(최상위 dispatch 경로).
-- 예전엔 destroySlotTree를 불러 그 결정과 정면으로 모순됐음.
unmountSlotTree(slotValue)
-- 이 위치가 더 이상 기여하지 않음을 owner에게 알림 — **순서 고정**
-- (setLength가 끝에서 recompute를 돌리므로 offsetSource를 먼저 비워야
-- 죽는 중인 Source에 헛된 :Set()이 안 감, 아래 ⚠️ 절 참고)
Dispatch.setOffsetSource(inst, k, None)
Dispatch.setLength(inst, k, 0)
unbindLifetime(inst, slotValue) -- top-level 자신의 GC 앵커 해제 — SlotHandler.process의 bindLifetime과 짝
releaseOwner(slotValue, inst) -- 이제 이 Slot은 아무에게도 안 묶임(다른 곳에 다시 넣을 수 있음)
end
end
```
**언마운트된 Slot은 그대로 재사용 가능** — `_elements`와 그 안의 소유권이
전부 보존되므로(아래 `unmountSlotTree` 참고), `slot`을 들고 있던 코드가
다른 곳에 다시 넣으면 `attachSlot`이 새 물리 부모로 다시 flush함. 아무도
안 들고 있으면 그냥 GC. 지금 확실히 죽이려면 `dispose`(아래 절).
**[전면 정정, 2026-08-13 감사] `claimOwner`를 nested/top-level 두 함수로
쪼개고, top-level은 `(inst, k)`까지 본다.** 원래는 하나의 `claimOwner`
"같은 ownerKey면 `false` 반환(no-op)"을 양쪽에 공유했는데, 그 분기가
@ -1390,6 +1404,32 @@ Slot=그 `.Length`)이 됨. plain 요소만 있는 흔한 경우엔 항상 합==
대해서만 한 번 돎:**
```lua
-- [신설, 2026-08-13 감사 후속] 비파괴 언마운트 — `State<Slot>` 교체/`Extract`
-- 계열이 쓰는 경로. `destroySlotTree`와 **딱 하나만 다름: 실제로 안 죽인다.**
-- 물리 트리에서만 떼어내고 `_elements`/자식 소유권은 통째로 보존하므로,
-- 같은 Slot을 나중에 다른 곳에 다시 마운트할 수 있음(= 포탈).
local function unmountSlotTree(slot)
for i, element in ipairs(slot._elements) do
if isSlot(element) then
unmountSlotTree(element) -- 재귀 — 중첩 Slot도 똑같이 비파괴
else
element.Parent = nil -- Destroy 아님(quad-roblox 글루가 수행)
end
-- releaseOwner를 **안 부름** — 자식들은 여전히 이 slot의 소유. 이게
-- destroySlotTree와의 핵심 차이(파괴는 소유권까지 반납, 언마운트는 유지).
end
local bk = getBookkeeping(slot)
if bk then
for i, observer in pairs(bk.observers) do
unbindLifetime(slot._mountedInst, observer) -- 물리 target에 걸린 배관만 해제
end
end
slot._mounted, slot._mountedInst = false, nil
slot.Offset = nil -- 마운트 전 상태로 복원(위 "`Slot.Offset`은 마운트 전엔 nil")
-- slot 자신의 unbindLifetime / releaseOwner / owner쪽 setLength·setOffsetSource는
-- 호출부 몫 — destroySlotTree와 동일한 층위 분리.
end
local function destroySlotTree(slot)
for i, element in ipairs(slot._elements) do
-- [정정, 2026-08-13 감사] 소유권 반납을 먼저 — 아래 "소유권 반납은
@ -1696,11 +1736,28 @@ Slot이 마운트될 때 **자기 하위 요소들까지 `bindLifetime`으로
#### 구현상 바뀌어야 하는 것
`reconcile`이 교체/소멸 시 `rawRemove`(파괴) 대신 **비파괴 경로**를 타야
하고, 그 경로는 `destroySlotTree`가 지금 하는 일 중 **파괴만 빼고 나머지는
그대로 해야 함**(자식 observer `unbindLifetime`, 옛 owner에 등록해둔
`Dispatch.setLength`/`setOffsetSource` 해제, `_mounted`/`_mountedInst`
복원, `releaseOwner`).
**[반영 완료, 2026-08-13 감사 후속]** 비파괴 경로를 `unmountSlotTree`
신설하고(아래 "파괴" 절), 이걸 쓰는 자리를 둘로 못박음:
1. **`SlotHandler.process`가 반환하는 클로저**(최상위 dispatch 경로) —
**이 결정이 적용돼야 하는 바로 그 자리인데 처음엔 빠뜨려서
`destroySlotTree`를 계속 부르고 있었음**(다른 에이전트 리뷰가 지적,
그대로 뒀으면 언마운트 결정 자체가 무의미해질 뻔함). 지금은 위 절의
코드가 `unmountSlotTree` + `setOffsetSource(None)`/`setLength(0)` +
`unbindLifetime` + `releaseOwner`를 부름.
2. **`:List``reconcile`** — 교체/소멸 시 `rawRemove`(파괴) 대신 같은
비파괴 경로. 데이터에서 빠진 아이템도 파괴되지 않고 언마운트만 되며,
아무도 안 들고 있으면 GC(quad 전역의 GC-native 원칙 그대로).
**여전히 파괴인 것**: 명시적 CRUD `Slot:Remove(index)`/`Slot:Clear()`
(CRUD 표가 "제거 **+ 파괴**"로 이미 정의)와 `dispose`. 즉 **"자동 경로는
언마운트, 명시적으로 지우라고 한 것만 파괴"**로 갈림 — `Ref`/`Attribute`의
"지울 거면 명시적으로" 철학과 정확히 같은 결.
`unmountSlotTree``destroySlotTree`가 하는 일 중 **실제 파괴와 자식
소유권 반납만 빼고 나머지는 그대로 함**(자식 observer `unbindLifetime`,
`_mounted`/`_mountedInst` 복원, `slot.Offset = nil`). 옛 owner에 등록해둔
`Dispatch.setLength`/`setOffsetSource` 해제는 호출부 몫(아래).
**[정정, 사용자 지적] "해제 짝"이라는 새 API는 필요 없음** — 옛 owner에
대해 그냥 **`Dispatch.setOffsetSource(ownerKey, position, None)` +

View file

@ -45,6 +45,20 @@ API 전부에 걸리며, 2026-08-07 일곱 번째 세션(커링 스타일 확정
**M0 착수 전에 정하는 게 맞음** — 2번을 고르면 base 문서 다수가 영향받음.
**[사용자 방향, 2026-08-13]** 순환 타입을 만드는 것보다 **`State` 타입
자체를 구울 때(코드 생성 시점) 인라이닝**해주는 쪽이 맞아 보인다는 의견 —
`State<T>`가 자기 자신을 재귀 참조하는 선언을 피하고, 타입 생성기가
`T`에 대해 평탄한 타입을 뽑아주는 방향(`Modifier`의 클래스별 flat 타입을
생성기로 뽑는 이미 확정된 패턴과 같은 결). 이러면 `08`에서 걸린
`Recursive type being used with different parameters` 제약도 같이 비켜감.
**다만 이건 사람이 직접 확인해야 할 부분으로 사용자가 보류** — 0-Z(Attribute)와
함께 사용자가 직접 스케치하며 판단할 목록.
**관련 실측 근거**: `08`(재귀 타입 제약), `15`(read-only/read-write `Get`
불일치 — 파싱 실패로 검증불가 상태라 재작성 필요), `14`(Ref 생성자 오버로드가
정상 nilable 사용례까지 막음), `16`(`type function` API 불일치). 전부
`audit/luau-test-first-run-2026-08-13.md`.
### 0-Z. ⭐ **최우선 — Attribute 이름 소유권을 무엇으로 판정할 것인가** (2026-08-13 여섯 번째 세션, 사용자가 다음 세션 심층 분석으로 이관)
**이게 지금 유일하게 `base/` 반영을 막고 있는 결정.** 아래 0-A의
@ -157,9 +171,13 @@ Slot이 됨. 막는 게 소유권 규칙이 아니라 "제거 = 파괴"라는 re
판단이라, 포탈은 별도 기능이 아니라 그 결정의 귀결. 안 지운 Slot은
아무도 안 들고 있으면 GC되고, 지금 죽이려면 `dispose`(위 0-B).
**[해소, 사용자 지적] "해제 짝"이라는 새 API는 필요 없음** — 옛 owner에
대해 그냥 `setLength(ownerKey, position, 0)` + `setOffsetSource(ownerKey,
position, None)`을 다시 부르면 됨(이미 확정된 "마운트 안 하는 위치는
`0`/`None` 등록" 관용구 그대로). 즉 **해제 = 0/`None`으로 재등록**.
대해 그냥 `setOffsetSource(ownerKey, position, None)` **다음에**
`setLength(ownerKey, position, 0)`을 부르면 됨(이미 확정된 "마운트 안 하는
위치는 `0`/`None` 등록" 관용구 그대로). 즉 **해제 = `None`/0으로 재등록**.
**순서가 중요** — `setLength`가 끝에서 `recompute`를 돌리므로 먼저 부르면
아직 남아있는 옛 `Source`(죽는 중인 서브트리의 것)에 헛된 `:Set()`이 날아감
(`base/slot-plan.md`의 ⚠️ 절). 이 줄이 한때 순서를 거꾸로 적어놔서 같은 날
리뷰에서 지적됨.
`state<state<Frame>>`류로 offset이 밀리는 문제는 `state<state<Tag>>`
같은 범주로 **"그냥 확인된 것"으로 수용**(평탄화 도구가 처리, 케이스 드묾).
상세는 `base/slot-plan.md` "`State<Slot>` 교체는 파괴가 아니라 언마운트" 절.

View file

@ -121,6 +121,9 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션
`Effect`/`Observer`/`Animate`/`Operator` 등 같은 계약을 공유하는 API
전부에 걸림 — 선택지는 (1) 계약 유지 + 파라미터 주석 필수, (2) raw 값
전환(**구조 변경 규모 큼**), (3) 혼합. **M0 착수 전에 정할 것.**
사용자 방향: 순환 타입을 만들기보다 **`State` 타입을 구울 때 인라이닝**
하는 쪽(생성기가 `T`별 평탄 타입을 뽑는, `Modifier` flat 타입과 같은 결)
— 다만 사람이 직접 확인할 부분이라 0-Z와 함께 사용자 판단 대기.
**0-Z: Attribute 이름 소유권 결정.**
2026-08-13 여섯 번째 세션에 `Dispatch` 재디스패치 모델이 "하강 diff"로
@ -885,66 +888,18 @@ handoff용 저장이 불필요해짐 — `Relate`는 여러 위치/사이클을
`modifier-plan.md` 전부 반영, `archive/checkpoint-handler-pattern-reversed.md`
신설.
**2026-08-13 여섯 번째 세션 — c33ae04 커밋 전체 감사(버그 4건), Slot
언마운트 전환, 재디스패치 모델 재설계**
(`session/2026-08-13-06-commit-audit-dispatch-redesign-bugs.md`)
사용자 지시로 직전 커밋을 메인 컨텍스트에서 직접 정독 — 인덱스 재설계
**방향은 옳지만 새로 쓴 의사코드가 손 트레이싱을 안 거친 채 커밋**됐음이
드러나 실제 버그 4건 발견·수정: (1) `Dispatch.process``chains:SetStrong`
`h.process` 뒤에 있어 최초 마운트에서 하위 retractor가 통째로 유실,
(2) Attribute 그룹이 `process`에서 `retractFrom`을 선행 호출해 점유
체크(=소유권 충돌 감지)가 전혀 작동 안 함, (3) `SlotHandler`가 claim
실패에도 파괴적 클로저를 반환해 `Frame{slot,slot}`에서 이중 파괴,
(4) `Ref` retractor가 spurious 재발행에서도 `relate`를 지워 dedup 무력화.
Slot 소유권은 nested=엄격 `claimOwner`/top-level=`claimOwnerAt(inst,k)`로
분리하고 `rawRemove``releaseOwner` 누락·`destroySlotTree`의 GC 타이밍
의존 오류도 수정. `ROADMAP.md`가 2026-08-08 이전 모델로 남아있던 것, base
내 "3종 vs 4종 계약" 모순, `luau-test/04`가 없어진 가드를 검증하던 것도
정리(`04`는 인덱스 모델 + 버그 (1) 재현 음성 대조군으로 재작성).
재발 방지로 `bind-system-plan.md`에 "Handler 작성 체크리스트"(7항목),
`relate-plan.md`에 "언제 `Relate`를 쓰고 언제 쓰면 안 되는가" 신설.
**이어진 사용자 설계 결정(같은 세션, 최종 상태만)**:
- **`State<Slot>` 교체 = 파괴가 아니라 언마운트**(`state<Frame>`와 동일,
비파괴 추출은 `Splice`로 이미 지원) — **포탈이 별도 기능이 아니라 이
결정의 귀결이 됨**. 해제는 `setOffsetSource(None)``setLength(0)`
**순서 고정**(반대면 죽는 중인 서브트리의 offset `Source`에 헛된 `:Set()`
날아감) + `recompute``nil` 관대 처리/`slot.Offset = nil` 방어.
별도 unregister API는 불필요. `base/slot-plan.md`**확정 반영 완료**.
- **`dispose(value)` 신설** — "트리가 아직 살아있길 요구하면 파괴를 거부하고
error"(엔진은 조용히 넘어가지만 quad 자료구조가 깨지므로). 시그니처/범위는
`question.md` 0-B.
- **재디스패치를 "하강 diff"로 재설계** — 래핑 핸들러의 `retractFrom` 선행
호출을 폐기하고 `Dispatch.process`**핸들러를 먼저 비교**(같으면 그 자리
클로저에 새 값 넘기고 자기 process 재호출, 다르면 아래 전량 철거). 계기는
힌트가 `None`/래퍼로 오염돼 깜빡임 방지가 조용히 꺼지는 결함(사용자 제기).
이 모델이면 **힌트 타입이 구조적으로 보장되고 깊은 체인 힌트 유실까지
사라짐**. 내가 낸 반론("StoreBind 구독 갈아타기")과 보완안(`oldValue` 전달)은
둘 다 사용자 지적으로 철회 — 클로저가 이미 old를 캡처하고 있음.
**아직 `research/dispatch-redispatch-diff-plan.md`에만 있음, base 미반영.**
- **평탄화**(`state:Flatten()`)는 백로그 상세화만 — 반환 노드의 **동적
의존성**이 최대 쟁점(`research/operator-sugar-plan.md`).
- **남은 결정 하나: Attribute 이름 소유권** — 새 모델에선 양쪽 다
`StoreBind`라 "같은 핸들러"로 판정돼 조용히 갈아탐. 사용자가 "이전
결정(claimant `Relate`)을 다시 가져오는 게 맞아 보이나 다음 세션에 직접
스케치하며 심층 분석"으로 이관 — `question.md` **0-Z(최우선)**.
**이어진 첫 실측 라운드(같은 세션)** — `luau`/`luau-analyze` 바이너리가
사용 가능해져 **스파이크를 만들기 시작한 2026-08-09 이래 처음으로 실제로
돌림**(CLAUDE.md가 "M0 착수 전 남은 유일한 게이트"로 꼽아온 항목). 결과
전문은 `audit/luau-test-first-run-2026-08-13.md`.
- **런타임 12개 전원 통과**(01~07/11/17~20). **`04`가 이번 세션 감사의
버그를 음성 대조군으로 재현** — 체인 깊이가 3 대신 1로 무너지고 죽은
store가 나중에 UI를 덮어쓰는 것까지 실측돼 감사→수정 사이클이 닫힘.
**`07`은 보강해야 실제 검증이 됐음**(연쇄 GC 미검증 상태였고, 파일이
스스로 적어둔 "weak 엔트리를 셀 방법 없음" 전제가 틀렸음) — GC-native
아키텍처의 핵심 전제가 이걸로 실측 확정. **`18``Relate` 상호 순환
경고를 실증**(추측이 아니라 실제로 GC 안 됨).
- **타입 쪽에서 진짜 이슈 하나 발견 → `question.md` 0-Y 신설**(위 참고).
- `modifier-plan.md`의 "데이터를 테이블에 직접 두고"가 두 갈래로 읽히는
**문서 결함**이 드러남 — self 최상위 리터럴 키에 두면 `__index`
`rawget` 성공 시 안 불려 같은 필드 재호출이 죽음(문서 3·4절 대표 용례가
바로 그 패턴). 경고 문단 추가 + `17` 재작성으로 해소.
- 스파이크 결함 수정: `17`(크래시), `11`(엉뚱한 이유로 통과), `19` B/C
(폐기 설계 검증 → 현행 설계로 재작성, 음성 대조군 포함), `07`(보강).
남은 것은 `13`/`15`/`16`의 스파이크 격리·API 재확인(설계 문제 아님).
**2026-08-13 여섯 번째 세션 — c33ae04 감사(버그 4건), Slot 언마운트 전환,
재디스패치 재설계안, 첫 실측 라운드**
(`session/2026-08-13-06-commit-audit-dispatch-redesign-bugs.md`,
실측 결과는 `audit/luau-test-first-run-2026-08-13.md`)
직전 커밋을 직접 정독해 인덱스 재설계 의사코드의 실제 버그 4건 발견·수정
(`chains:SetStrong` 순서로 하위 retractor 유실, Attribute 그룹의 점유 체크
무력화, `SlotHandler`의 claim 실패 시 이중 파괴, `Ref` dedup 무력화).
이어 사용자 결정으로 **`State<Slot>` 교체를 파괴→언마운트로 전환**(포탈이
그 귀결이 됨), **`dispose(value)`**(트리가 요구하면 파괴 거부·error) 신설,
**재디스패치를 "하강 diff"로 재설계**(`research/dispatch-redispatch-diff-plan.md`,
**base 미반영 — `question.md` 0-Z 하나 남음**). `ROADMAP.md`/base 계약 개수
모순/`luau-test` stale도 정리. 마지막으로 `luau` 바이너리가 생겨 **첫 실측**
— 런타임 12개 전원 통과, `04`가 위 버그를 음성 대조군으로 재현, `07` 보강으로
GC-native 전제 확정, `18``Relate` 순환 경고 실증. 타입에선 `:Compute(fn)`
lazy 핸들 계약이 Luau 추론과 충돌하는 게 드러나 `question.md` **0-Y** 신설.

View file

@ -252,6 +252,38 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
## M6 — Slot
- [ ] **[2026-08-13 여섯 번째 세션 — 이 세션의 Slot 결정 전부, 구현 전 필독]**
- **`State<Slot>` 교체 = 파괴가 아니라 언마운트**(`state<Frame>`와 동일).
비파괴 경로 `unmountSlotTree``destroySlotTree`와 별도로 구현 —
차이는 딱 둘: 실제 `Destroy()`를 안 하고, 자식 `releaseOwner`도 안 함
(자식은 계속 그 slot 소유라 통째로 재마운트 가능 = 포탈).
**쓰는 자리 둘**: `SlotHandler.process`가 반환하는 클로저, `:List`
`reconcile`. **여전히 파괴인 것**: 명시적 `Remove`/`Clear`/`dispose`.
- **해제 시 owner 등록 되돌리는 순서 고정**
`setOffsetSource(inst,k,None)` **먼저**, `setLength(inst,k,0)` **나중**.
반대로 하면 `setLength` 안의 `recompute`가 죽는 중인 서브트리의 offset
`Source`에 헛된 `:Set()`을 날림. `recompute``sourceList[i]``nil`
이어도 `None`처럼 skip(방어), 해제 시 `slot.Offset = nil`.
- **소유권 판정을 둘로 분리** — nested(`rawAdd`)는 엄격 `claimOwner`
(같은 owner 재클레임도 error, `Slot{a,a}` 차단, 반환값 없음),
top-level은 `claimOwnerAt(element, inst, k)`(정확히 같은 `(inst,k)`
spurious 재발행만 `false`, `Frame{slot,slot}`은 error).
`releaseOwner`는 불일치 시 즉시 error.
- **`rawRemove``releaseOwner`를 부를 것**(옛 의사코드에서 누락돼 있었음),
**`destroySlotTree`가 자식 소유권 반납 + `_mounted`/`_mountedInst` 복원**
(GC에 맡기면 재사용이 GC 타이밍 의존으로 비결정적 실패).
- **`SlotHandler.process`는 claim 실패 시에도 파괴적 클로저를 반환해야 함**
— no-op을 반환하면 다음 진짜 교체 때 정리 주체가 사라짐(`retractFrom`은
클로저가 early-return해도 체인에서 항상 소비하므로).
- 전부 `base/slot-plan.md`에 반영돼 있고, `luau-test/19` C 섹션이
소유권 분기를 음성 대조군까지 포함해 실측 검증함.
- [ ] **`dispose(value)`** — 대상이 아직 어느 트리에 의해 살아있길 요구되면
**파괴를 거부하고 즉시 error**(떼어내주지 않음 — 떼는 건 `Set`=언마운트의
몫). 엔진은 `Destroy`/`Clear`에 에러를 안 내지만 quad 자료구조가 깨지므로,
quad가 관리 중인 값을 안전하게 지우는 유일한 경로. 마운트 위치는
`elementOwner`가 이미 알고 있어 새 부기 불필요. 시그니처/대상 범위는
`.claude/question.md` 0-B에서 미확정
- [x] **"여러 Slot이 형제로 섞일 때 순서 보장" 해소**(2026-08-09 여섯 번째
세션) — `Dispatch.setLength`/`setOffsetSource` 메커니즘, `base/
bind-system-plan.md` "Length/Offset" 절. `Slot.Length: State<number>`