docs(base): dispose(value) 시그니처/범위 확정 — question.md 0-B 해소

범위를 Slot+엔진 객체로 좁히고 Observer/Effect는 명시적으로 제외
(GC-native bindLifetime만으로 충분, 트리 부기 없음). isSlot이 아니면
disposeInst 주입 op으로 위임(addTag/removeTag/setAttribute와 같은
패턴). 부수적으로 OnDestroyed 이름 재검토 조건도 종결.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
qwreey 2026-08-14 06:18:28 +09:00
parent 2c575d91f0
commit 573dd452af
Signed by: qwreey
GPG key ID: D28DB79297A214BD
11 changed files with 294 additions and 84 deletions

File diff suppressed because one or more lines are too long

View file

@ -627,6 +627,42 @@ context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만**
`useMemo` deps도 대부분 정적) — 의도적 비지원으로 확정. 상세는
`research/framework-comparison-findings.md` 3번 절.
- **[해소됨, 2026-08-14 열 번째 세션]** 0-B. `dispose(any)` — 시그니처/범위.
**결론**: 범위를 `Slot`+엔진 객체(Instance)로만 좁히고 **Observer/Effect는
명시적으로 제외** — 시그니처는 `dispose(value: Slot | Instance)`,
내부적으로 `if isSlot(value) then <Slot 자체 소유권 판정 재사용> else
disposeInst(value) end`로 분기. `unbindLifetime`과의 역할 분담도 이걸로
해소: `dispose`는 **트리 소유권 부기(elementOwner/lengthList 등)가 있는
대상**이 아직 요구되는데 강제로 죽이려는 경우를 막는 게 목적이고,
`unbindLifetime`은 Observer/Effect류의 GC 앵커(gcconn)를 조기 해제하는
것 — 서로 다른 축이라 대체 불가.
- **Observer/Effect가 빠지는 이유**: 이 둘은 children 배열 leaf 위치에서
`Dispatch/Leaf.luau`가 매치해 내부적으로 `bindLifetime`/`canExecute`/
`unbindLifetime`(GC-native, gcconn 기반)로만 관리됨(`base/
source-state-plan.md` "이중 바인딩 금지" 절) — Slot의 `elementOwner`/
`lengthList`/`sourceList` 같은 "죽으면 offset/length가 깨지는" 트리
부기 자체가 없어서, 도중에 GC되거나 `unbindLifetime`으로 조기 해제돼도
구조적으로 안전함. 즉 dispose가 막아야 하는 문제(트리 부기 붕괴)가
Observer/Effect에는 원천적으로 발생하지 않음. `State<Observer>`/
`State<Slot>` 등 반응형 leaf 값은 이미 확정된 일반 원칙("모든
`(inst,k)``T``State<T>``StoreBind`가 균일하게 재귀 처리")의
자연스러운 귀결일 뿐 별도 설계가 필요 없었음 — Modifier 필드/Slot
원소 금지 규칙(핸들러 계층 값 즉시 error)은 **다른 컨텍스트**(Modifier
필드, `Slot:Add`/`:List`의 원소)라 여기 적용 안 됨, 혼동하지 말 것.
- **base/backend 분리**: `dispose``isSlot`이 아닐 때 위임하는
`disposeInst(inst: any): ()``addTag`/`removeTag`/`setAttribute`와
같은 "base가 소유하는 핸들러와 주입되는 엔진 op" 패턴(`base/
dispatch-core-plan.md`) — quad-roblox는 `inst:Destroy()`로 구현.
- **네이밍**: `free()`는 GC 언어 맥락과 안 맞아 기각, `Destroy`는 엔진
`:Destroy()` 메소드와 동명이라 사용자가 착각할 위험이 있어 기각 —
`dispose` 유지.
- 정본은 `base/slot-plan.md` "`dispose(any)`" 절 +
`base/dispatch-core-plan.md` "base가 소유하는 핸들러와 주입되는 엔진 op"
절. 이 해소로 `base/lifecycle-hooks-plan.md``OnDestroyed` 이름
재검토 조건("0-B가 'quad가 만드는 모든 것의 유일한 파괴 경로'로
풀리면")도 **발동하지 않는 쪽으로 영구 종결**`OnDestroyed`
최종 이름.
## 참고: 지금까지 확정된 것 (요약)
전부 `base/`에 문서화되어 더 이상 열려있지 않음 — 상세 근거/논의 과정이

View file

@ -183,7 +183,7 @@ quad/
├── wally.toml
└── src/
├── RobloxFactory.luau # BaseModule 뮤테이션, 재호출 가드(같은 팩토리=무시/다른=에러) — 주입 대상엔 bindLifetime/canExecute 외에 addTag/removeTag/setAttribute도 포함(2026-08-13 열네 번째 세션)
├── EngineOps.luau # 주입되는 엔진 op 구현: addTag(inst,{string})/removeTag(inst,{string})=CollectionService, setAttribute(inst,name,v)=inst:SetAttribute(v==nil이면 삭제) (`base/dispatch-core-plan.md` "base가 소유하는 핸들러와 주입되는 엔진 op" 절)
├── EngineOps.luau # 주입되는 엔진 op 구현: addTag(inst,{string})/removeTag(inst,{string})=CollectionService, setAttribute(inst,name,v)=inst:SetAttribute(v==nil이면 삭제), disposeInst(inst)=inst:Destroy()(`dispose(value)`가 `isSlot`이 아닐 때 위임, `base/slot-plan.md`) (`base/dispatch-core-plan.md` "base가 소유하는 핸들러와 주입되는 엔진 op" 절)
├── LifetimeHandle.luau # bindLifetime/canExecute 실제 구현 — GetPropertyChangedSignal("ClassName") 연결 트릭으로 gcconn 확보, Relate:SetStrong으로 gcconn/gchold 저장(`base/lifecycle-pattern.md`). Relate 자체는 순수 Lua라 quad-roblox 쪽 재구현 없음(quad-base 그대로 재사용)
├── Handlers/
│ ├── Property.luau # 일반 프로퍼티 세팅 + `isTween(realv)` 분기(3-상태 릴레이션 슬롯 `RobloxTween|true|nil`, hasBeenSet 억제, override 정책) — 구 `Handlers/Tween.luau`(높은 우선순위 store-bind 핸들러)는 폐기(`archive/tween-special-bind-key-reversed.md`)

View file

@ -526,7 +526,12 @@ end
백엔드마다 재구현하면 **같은 참조 카운트/소유권 알고리즘이 통째로
복제**됨 — `architecture.md`의 "엔진마다 큰 구현을 중복하지 않기 위해
디스패치 엔진을 base가 인터페이스로 소유한다"는 원칙이 그대로 적용되는
자리(2026-08-13 열네 번째 세션, 사용자 판단으로 재배치).
자리(2026-08-13 열네 번째 세션, 사용자 판단으로 재배치). **같은 패턴이
Dispatch 바깥에도 적용됨** — `dispose(value)`(`base/slot-plan.md`)는
Dispatch 핸들러가 아니라 독립 탑레벨 유틸이지만, `isSlot`이 아닌 값은
`elementOwner` 같은 순수 부기 판정 뒤에 마지막 한 줄만
`disposeInst(inst: any): ()`로 위임(quad-roblox는 `inst:Destroy()`) —
2026-08-14 열 번째 세션에 같은 원칙으로 확정.
- **backend 소유**: `Property`/`Event`/`OnChange`(Reflection·시그널 같은
엔진 개념 자체가 로직), `InstanceChild`, `Slot`의 실제 부모 조작
(재조정 알고리즘은 base `Dispatch/Slot.luau`, 물리 마운트만 backend) —

View file

@ -304,15 +304,14 @@ construction에 재사용**하는 것("이미 한 번 fire된 PreRef 객체를
있어 위 "①" 절의 대조 설명 없이 이름만 보면 헷갈릴 수 있음, 문서화
시 명시할 것.
- `OnDestroyed`는 최초 가칭이던 `OnDisposed`보다 사용자가 선호 —
**이 이름으로 확정하되, `dispose()` 범위(0-B)가 풀리면 재검토 여지만
남겨둠**(아래 "열린 질문" 절). `OnDisposed`가 제안된 이유는
미래 `dispose()` 함수(`question.md`
"0-B. `dispose(any)` — 시그니처/범위")와 이름을 맞추자는 발상이었는데,
대조해보면 트리거 자체가 다름:
**`OnDestroyed`로 영구 확정**(2026-08-14 열 번째 세션, `dispose()` 범위
확정으로 재검토 조건 자체가 종결됨 — 아래). `OnDisposed`가 제안된
이유는 미래 `dispose()` 함수(`archive/question-resolved.md` 0-B)와
이름을 맞추자는 발상이었는데, 대조해보면 트리거 자체가 다름:
- `dispose(value)`는 사용자가 **의도적으로** 부르는 명시적 파괴
API로 설계 중(대상이 아직 트리에 의해 살아있길 요구되면 파괴를
**거부하고 error**, `question.md` 0-B). "언제 부를지"를 호출자가
고르는 능동적 경로.
API(대상이 아직 트리에 의해 살아있길 요구되면 파괴를 **거부하고
error**, `base/slot-plan.md`). "언제 부를지"를 호출자가 고르는
능동적 경로.
- 반면 이 문서의 훅은 `Effect`의 leaf-death cleanup에 얹히므로,
실제 트리거는 **물리 Instance가 죽는 시점**(엔진 `Destroying`
신호, `bindLifetime`이 감시하는 이벤트)임 — 그 죽음이 `dispose()`
@ -320,16 +319,14 @@ construction에 재사용**하는 것("이미 한 번 fire된 PreRef 객체를
정리했는지는 이 훅 입장에서 구분도 안 되고 상관도 없음.
- 그래서 `OnDisposed`는 "`dispose()`를 불렀을 때만 발화한다"는
잘못된 인상을 줄 위험이 있고, `OnDestroyed`가 실제 트리거(엔진
`Destroying`)를 더 정직하게 반영함 — **지금 추천은 `OnDestroyed`**.
- 단 이 판단은 **0-B가 아직 미확정**이라는 전제 위에 있음: 만약
나중에 `dispose()`가 "Slot뿐 아니라 Instance/Effect까지 포함해
quad가 만드는 모든 것의 유일한 파괴 경로"로 확정되면(0-B의
"미확정: 대상 범위" 항목이 그쪽으로 풀리면), 그때는 `dispose()`
이름을 맞추는 재검토가 자연스러워질 수 있음.
- 이름 자체는 런타임에 아무 의미가 없는 순수 네이밍(값은 그냥
`PreRef`/`EffectHandle`)이라 바꾸는 비용은 0에 가까움 — 그래서
위험 부담이 낮은 `OnDestroyed`로 확정하고, `dispose()` 범위가
확정되면 재검토하는 게 결론.
`Destroying`)를 더 정직하게 반영함 — **`OnDestroyed`가 최종 이름**.
- **[해소, 2026-08-14 열 번째 세션]** 재검토 조건이던 "`dispose()`가
Slot뿐 아니라 Instance/Effect까지 포함해 quad가 만드는 모든 것의
유일한 파괴 경로로 확정되면"은 **발동하지 않는 쪽으로 결정됨**
`dispose()`의 범위는 오히려 `Slot`+`Instance`로 좁혀지고
`Observer`/`Effect`는 명시적으로 제외됐음(`archive/
question-resolved.md` 0-B). `OnDestroyed`↔`dispose()` 이름을 맞출
이유 자체가 사라져 더 이상 재검토 대상 아님.
- **`OnRendered`/`PostRef` — 이름 확정(2026-08-14 아홉 번째 세션).**
`PostRef``PreRef`와의 대칭성이 이름에서 바로 읽혀 이견 없음.
`OnRendered`는 채택되며 이름도 그대로 확정 — 다만 **"렌더"가 quad엔
@ -362,16 +359,9 @@ construction에 재사용**하는 것("이미 한 번 fire된 PreRef 객체를
## 열린 질문
**[2026-08-14 아홉 번째 세션] 설계상 열린 질문 없음** — 채택 여부/
메커니즘/스코프/패키지 배치가 전부 확정됨(위 각 절). 하나만 성격이
다른 항목으로 남음:
- **`OnDestroyed` 이름 재검토 여지** — 지금 이름은 확정이지만, 미래
`dispose()`의 대상 범위(`question.md` "0-B. `dispose(any)`
시그니처/범위")가 "quad가 만드는 모든 것의 유일한 파괴 경로"로 풀리면
`OnDisposed`와 맞추는 재검토가 자연스러워질 수 있음(위 "이름 컨벤션"
절의 대조 참고). `question.md`의 **용어 정리 대기열**에 3순위로 올려둠
`Slot`/`Brand`처럼 "base에 확정돼 있지만 이름만 재검토 대상"인
기존 항목들과 같은 취급이고, 이름은 런타임에 아무 의미가 없어 바꾸는
비용이 0에 가까움.
- 그 외 확정된 결정 없음 — 착수 시점에 위 항목들을 순서대로 확인.
**[2026-08-14 열 번째 세션] 열린 질문 없음** — 채택 여부/메커니즘/
스코프/패키지 배치에 이어, 마지막까지 남아있던 `OnDestroyed` 이름
재검토 조건도 `dispose()` 범위(0-B)가 `Slot`+`Instance`로 좁혀지고
`Observer`/`Effect`는 제외되는 쪽으로 확정되며 발동 없이 종결됨(위
"이름 컨벤션" 절) — `OnDestroyed`가 최종 이름. 착수 시점에 위 각 절을
순서대로 확인하면 됨.

View file

@ -1780,10 +1780,10 @@ Slot이 마운트될 때 **자기 하위 요소들까지 `bindLifetime`으로
것과 정확히 같은 문제 — quad는 그 값이 이미 죽었다는 걸 모른 채 언마운트
경로를 탐. 순서는 항상 **`Set`(언마운트) → 그 다음 정리**.
#### `dispose(any)` — quad가 만든 것을 안전하게 지우는 유일한 경로 (신설, 사용자 제안)
#### `dispose(value)` — quad가 관리 중인 값을 안전하게 지우는 유일한 경로 (신설, 사용자 제안, **2026-08-14 열 번째 세션에 시그니처/범위 확정 — `question.md` 0-B 해소**)
위 UB를 "조심하세요"로만 두지 않기 위해, **base 레벨 탑레벨 유틸
`dispose(value)`** 를 제공하는 방향으로 확정:
`dispose(value: Slot | Instance): ()`** 를 제공:
- 의미(**[정정, 사용자 확정]** 최초안은 "마운트돼 있으면 먼저 떼어낸 뒤
파괴"였으나 더 단순하게 확정): **대상이 아직 어느 트리에 의해 살아있길
@ -1804,11 +1804,52 @@ Slot이 마운트될 때 **자기 하위 요소들까지 `bindLifetime`으로
거꾸로 읽으면 되므로 **새 부기가 필요 없음**. 사용자 지적: "이미 두
곳에 넣는 게 에러나도록 하기로 했으니, 어디 마운트되었냐가 따져지고,
그래서 이미 가능한 일".
- 네이밍/시그니처(`dispose(any)`가 맞는지, 타입을 어떻게 좁힐지),
Slot 외의 대상(Instance/Observer/Effect)까지 커버하는 범위,
`unbindLifetime`과의 역할 분담은 **미확정**`.claude/question.md`
올림. 여기서는 "언마운트로 바꾼 대신 명시적 파괴 수단을 제공한다"는
방향만 확정.
**범위 — `Slot` + 엔진 객체(Instance)만, `Observer`/`Effect`는 명시적으로
제외**(2026-08-14 열 번째 세션, 사용자 확정):
```lua
function dispose(value)
if isSlot(value) then
-- 위 elementOwner 기반 판정 재사용 — 요구 중이면 error, 아니면 재귀 파괴
...
else
disposeInst(value) -- 아래 주입 op
end
end
```
- **왜 `Observer`/`Effect`는 dispose 대상이 아닌가**: 이 둘은 children
배열 leaf 위치에 놓이면 `Dispatch/Leaf.luau`가 매치해 내부적으로
`bindLifetime(inst, value)`를 호출하고(`base/source-state-plan.md`
"이중 바인딩 금지" 절), 생존은 그 GC 앵커(gcconn)만으로 판정됨 —
Slot처럼 "죽는 순간 `elementOwner`/`lengthList`/`sourceList`가
어긋나는" 트리 부기 자체가 없음. 즉 dispose가 막으려는 문제(부기
붕괴)가 Observer/Effect에는 원천적으로 발생하지 않음 — 아무도 안 들고
있으면 그냥 GC, 조기에 끊고 싶으면 `unbindLifetime`으로 충분하고
`dispose`가 다룰 이유가 없음.
**주의 — Modifier 필드/`Slot:Add`·`:List` 원소 금지 규칙("핸들러 계층 값이
들어오면 즉시 error")과 헷갈리지 말 것.** 그건 Modifier 필드나 Slot의
CRUD 원소 자리에 관한 별개 규칙이고, children 배열의 leaf 위치(정적
`Frame{observer}`류)나 그 leaf가 `State<Observer>`/`State<Effect>`로
반응형으로 바뀌는 경우는 전혀 다른 컨텍스트 — 후자는 이미 확정된 일반
원칙("모든 `(inst,k)``T``State<T>``StoreBind`가 균일하게 재귀
처리")의 자연스러운 귀결이라 별도 설계 없이 그냥 됨.
- **`unbindLifetime`과의 역할 분담**: `dispose`는 **트리 소유권 부기가
있는 대상**(Slot/Instance)이 아직 요구되는데 강제로 죽이려는 시도를
막는 것이고, `unbindLifetime`은 Observer/Effect류의 GC 앵커를 조기
해제하는 것 — 축이 달라 서로 대체 불가.
**base/backend 분리 — `disposeInst`는 주입 op**(`base/dispatch-core-plan.md`
"base가 소유하는 핸들러와 주입되는 엔진 op" 절과 같은 패턴, `addTag`/
`removeTag`/`setAttribute`가 선례): `dispose``isSlot`이 아닌 값을
받으면 base가 시그니처만 소유하는 `disposeInst(inst: any): ()`로 위임 —
quad-roblox는 `inst:Destroy()`로 구현. 웹 등 다른 백엔드는 자기 방식으로
매핑.
**네이밍**: `free()`는 GC-native 언어 맥락과 안 맞아 기각, `Destroy`
엔진 자체 `:Destroy()` 메소드와 동명이라 사용자가 "그냥 `:Destroy()`
부르는 거 아님?"으로 착각할 위험이 있어 기각 — `dispose` 유지.
#### 구현상 바뀌어야 하는 것

View file

@ -26,7 +26,7 @@
## 결정 대기 — M0는 안 막음
### 0-W. 같은 `Ref` 객체가 두 자리에 놓이는 걸 막을 것인가 (2026-08-13 열세 번째 세션 신설, 0-Z 확인 중 발견)
### 0-W. 같은 `Ref` 객체가 두 자리에 놓이는 걸 막을 것인가 (2026-08-13 열세 번째 세션 신설, 0-Z 확인 중 발견, 2026-08-14 열 번째 세션 기준 M4 구현 세부만 막는 유일한 잔여 항목 — 0-B는 해소돼 `archive/question-resolved.md`로 이전)
**0-Z(Attribute, 열네 번째 세션에 해소됨)를 보다가 "Ref에도 같은 문제가
있냐"는 사용자 질문에서 나온 것 — 있고, 막는 장치가 전혀 없음.** 단
@ -76,26 +76,6 @@
단 증상이 "조용한 값 소실"이라 다른 UB보다 나쁨), (c) 마지막 쓰기 승리를
정식 동작으로 인정(비권장, `Ref`의 "확정된 값 박스" 의미와 충돌).
### 0-B. `dispose(any)` — 시그니처/범위 (2026-08-13 여섯 번째 세션 신설, 사용자 제안)
`State<Slot>` 교체를 파괴가 아니라 **언마운트**로 확정하면서(`state<Frame>`와
동일, `base/slot-plan.md`), 명시적 파괴 수단으로 base 탑레벨 `dispose(value)`
제공하기로 방향 확정. "이 값이 지금 어디 마운트돼 있는가"는 이미
`elementOwner`가 들고 있어(다중 마운트 error 판정용) 새 부기가 필요 없음.
**[확정, 사용자] 시맨틱은 "거부"** — 대상이 아직 어느 트리에 의해
살아있길 요구되고 있으면 **파괴를 거부하고 즉시 error**. 떼어내주지
않음(떼어내는 건 `Set`=언마운트의 몫, `dispose`는 그 뒤). 근거: 엔진은
`Destroy`/`Clear`에 에러를 안 내지만 quad의 `_elements`/`lengthList`/
`sourceList`/`elementOwner`는 그 순간 어긋나므로, **quad가 관리 중인 값을
안전하게 지우는 유일한 경로**가 이것이고 "지금 지우면 안 되는 상태"를
잡아주는 게 존재 이유. 이걸로 "`Set` 전에 직접 `Destroy()`"가 UB에서
명확한 에러로 바뀜.
**미확정**: 시그니처(`dispose(any)`가 맞는지, 타입을 어떻게 좁힐지),
대상 범위(Slot 외에 Instance/Observer/Effect까지 커버하는지),
`unbindLifetime`과의 역할 분담.
## 1. 용어 정리 — 아직 안 정해진 것만 (사용자 요청, 진행 중)
사용자 원 메모: "quad는 register라던가 좀 부정확하거나 느낌이 바로 와닿지
@ -167,16 +147,6 @@
이미 없앴으니 급하지 않지만, 최종 이름은 여전히 이 목록의 다른
가칭들과 함께 검토 대상. `base/attribute-plan.md` "그룹 `Attribute(...)`"
절 참고.
- **`OnDestroyed`(3순위, 사소함, 2026-08-14 아홉 번째 세션 추가)**:
`base/lifecycle-hooks-plan.md`가 이 이름으로 **확정**하되, 위 0-B
(`dispose(any)` — 시그니처/범위)가 "quad가 만드는 모든 것의 유일한 파괴
경로"로 풀리면 `OnDisposed`와 맞추는 재검토 여지를 남겨둠. 지금
`OnDestroyed`인 이유는 실제 트리거가 `dispose()` 호출이 아니라 엔진
`Destroying` 신호라서(그 문서 "이름 컨벤션" 절). 이름은 런타임에 아무
의미가 없는 순수 네이밍이라 바꾸는 비용이 0에 가까움 — **0-B가 풀리기
전엔 이 항목을 열지 말 것**(형제 `OnCreated`/`OnRendered`는 재검토
대상 아님, 다만 `OnRendered`엔 "부모에 붙기 전에 불린다"는 캐비엇이
있어 이름이 아니라 *문서화*로 대응하기로 확정됨).
- **참고 — 이미 지나간 사례**: `register`(v1) → `State`(v2) 리네임은
"모호함"은 풀었지만 "다른 뜻으로 이미 쓰이는 단어"라는 새 문제를 만든
셈 — 이번 정리에서 같은 패턴을 조심할 것.

View file

@ -0,0 +1,136 @@
# 2026-08-14 열 번째 세션 — `dispose(value)` 시그니처/범위 확정 (`question.md` 0-B 해소)
## 배경
이전 대화(다른 세션이 동시에 작업 중이라 이 세션은 처음엔 읽기 전용으로
시작)에서 사용자가 `question.md` 0-B(`dispose(any)` — 시그니처/범위,
2026-08-13 여섯 번째 세션 신설)에 대해 물었고, 남은 미확정 항목(시그니처,
Slot 외 대상 범위, `unbindLifetime`과의 역할 분담)을 확인하는 것으로
시작했다.
## 1차 논의 — Observer/Effect 범위 관련 오류와 정정
사용자가 먼저 "Observer/Effect는 dispose 범위에서 빼야 한다"는 결론을
제시하며 근거로 "`State<Observer>`/`State<Effect>`가 이미 지원 사양"이라고
주장했다. 어시스턴트는 처음에 이걸 검증하며 `modifier-plan.md`의 "Modifier
필드에 핸들러 계층 값(Ref/PreRef/PostRef/Observer/Effect/Slot/Modifier)이
들어오면 즉시 error" 규칙과 `slot-plan.md`의 Slot 원소 금지 규칙을 근거로
"State<Observer>가 지원된다는 명시적 문서는 없다"고 (잘못) 답했다.
사용자가 "modifier가 왜 나온 말인지 전혀 모르겠다"며 반박 — Modifier
필드 금지 규칙은 이 논의와 무관한 별개 컨텍스트였다. 재조사 결과 사용자
말이 맞았음이 확인됨:
- `base/architecture.md`(Leaf.luau 파일 설명)와 `base/effect-plan.md`
이미 **children 배열의 leaf 위치**(`k=number`)에 `Ref`/`PreRef`/`PostRef`
뿐 아니라 **`Observer`/`Effect`도 명시적으로 지원 대상**으로 확정해뒀음
(`Dispatch/Leaf.luau`가 `(i:number, v=Observer/Effect)`를 매치).
- `base/source-state-plan.md` "이중 바인딩 금지" 절이 "children 배열에
Observer를 직접 놓으면 `Dispatch/Leaf.luau`가 매치해 내부적으로
`bindLifetime(inst, observer)`를 호출한다"고 이미 명시 — leaf 부착이
`bindLifetime` 호출 그 자체.
- `base/dispatch-core-plan.md``Dispatch/StoreBind.luau`는 "범용,
엔진 무관"이고 **`k`는 무엇이든 받음** — 즉 children 배열의 `k=number`
슬롯에도 똑같이 적용되는 일반 재귀 재디스패치 계층이라, `State<Observer>`/
`State<Effect>`가 그 자리에 놓여도 StoreBind가 실체를 뽑아 재귀
`Dispatch.process`로 넘기고 `Leaf.luau`가 풀린 값을 매치 — 새 메커니즘
없이 이미 있는 일반 원칙("모든 (inst,k)는 T든 State\<T\>든 균일하게
처리")의 자연스러운 귀결.
- Modifier 필드 금지 규칙(`modifier-plan.md:240`)과 Slot 원소 금지 규칙
(`slot-plan.md:81`)은 **완전히 다른 컨텍스트**(Modifier 자신의 필드,
`Slot:Add`/`:List`의 CRUD 원소)라 children 배열 leaf 위치와 무관함 —
어시스턴트가 이 두 컨텍스트를 혼동한 게 오류의 원인이었음.
어시스턴트가 이 오류를 인정하고 정정, `State<Observer>`/`State<Effect>`가
실제로 확정 사양임을 사용자에게 재확인했다.
## 2차 논의 — 모델을 Opus로 올릴지 질문, 반영 시작
사용자가 "모델을 opus로 올리고 반영 시작할까? 너는 어렵게 느껴? 아니면
가능한 정도야?"라고 질문. 어시스턴트는 이번 반영 작업(정해진 패턴을
따라 문서 여러 곳에 확정된 결정을 박아넣는 기계적 작업)이 Sonnet으로
충분하다고 판단해 답변, CLAUDE.md의 "토큰 맥싱" 방침과도 일치한다고
설명. 사용자가 "가능하다면 문서 반영하고, 세션 기록 남겨. 다른쪽 전부
꺼서, 작업해도 좋음. 핸드오버 준비하고 커밋해줘"로 승인.
## 최종 확정 — `dispose(value)`
**범위**: `Slot` + 엔진 객체(`Instance`)만. **`Observer`/`Effect`는
명시적으로 제외**.
- **이유**: Observer/Effect는 children 배열 leaf에서 `bindLifetime`/
`canExecute`/`unbindLifetime`(GC-native, gcconn 기반)로만 관리되고,
Slot처럼 "죽는 순간 `elementOwner`/`lengthList`/`sourceList`가
어긋나는" 트리 부기 자체가 없음. dispose가 막아야 하는 문제(quad 내부
자료구조 붕괴)가 Observer/Effect에는 원천적으로 발생하지 않아 dispose가
다룰 이유가 없음. 아무도 안 들고 있으면 그냥 GC, 조기에 끊고 싶으면
`unbindLifetime`으로 충분.
**시그니처**: `dispose(value: Slot | Instance): ()`
```lua
function dispose(value)
if isSlot(value) then
-- 기존 elementOwner 기반 소유권 판정 재사용: 요구 중이면 error, 아니면 재귀 파괴
...
else
disposeInst(value) -- 백엔드 주입 op
end
end
```
**base/backend 분리**: `isSlot`이 아닌 값은 `disposeInst(inst: any): ()`
위임 — `base/dispatch-core-plan.md`의 "base가 소유하는 핸들러와 주입되는
엔진 op" 패턴(`addTag`/`removeTag`/`setAttribute`가 선례) 그대로 재사용.
quad-roblox는 `inst:Destroy()`로 구현.
**네이밍 경위**(사용자가 직접 검토): `free()`는 GC-native 언어 맥락과 안
맞아 기각. `Destroy`는 엔진 자체 `:Destroy()` 메소드와 동명이라 사용자가
"그냥 `:Destroy()` 부르는 거 아님?"으로 착각할 위험이 있어 기각. `dispose`
유지.
**`unbindLifetime`과의 역할 분담**: `dispose`는 트리 소유권 부기가 있는
대상(Slot/Instance)이 아직 요구되는데 강제로 죽이려는 시도를 막는 것,
`unbindLifetime`은 Observer/Effect류 GC 앵커의 조기 해제 — 축이 달라
대체 불가.
## 부수 해소 — `OnDestroyed` 이름 재검토 조건 종결
`base/lifecycle-hooks-plan.md`가 "0-B가 'quad가 만드는 모든 것의 유일한
파괴 경로'로 풀리면 `OnDisposed`와 이름을 맞추는 재검토가 자연스러워질 수
있다"는 조건부 열린 항목을 갖고 있었음. 이번 해소로 `dispose()`의 범위가
오히려 좁아지고(Slot+Instance만) Observer/Effect가 제외됐으므로, 그 조건은
**발동하지 않는 쪽으로 영구 종결** — `OnDestroyed`가 최종 이름, 용어 정리
대기열에서도 제외.
## 반영한 문서
- `.claude/question.md` — 0-B 섹션 제거(해소), 0-W만 남은 "결정 대기"
항목으로 정리, `1. 용어 정리``OnDestroyed` 조건부 항목 제거.
- `.claude/archive/question-resolved.md` — 0-B 결론+근거 추가(원 문서
형식과 동일하게 "해소됨" 표시, 원문 요약 포함).
- `.claude/base/slot-plan.md``dispose(value)` 절 전면 갱신(범위/시그니처/
disposeInst 위임/네이밍 근거 확정 반영).
- `.claude/base/dispatch-core-plan.md` — "base가 소유하는 핸들러와
주입되는 엔진 op" 절에 dispose/disposeInst를 재사용 사례로 추가.
- `.claude/base/architecture.md``EngineOps.luau` 파일 설명에
`disposeInst` 추가.
- `.claude/base/lifecycle-hooks-plan.md``OnDestroyed` 이름 컨벤션 절과
"열린 질문" 절 모두 조건 종결로 갱신.
- `ROADMAP.md` — M6 `dispose(value)` 체크리스트 항목에 확정된 시그니처/
분기 반영, "0-B 미확정" 문구 제거.
- `HUMAN_TODO.md` — 3번 항목에서 0-B를 해소로 갱신.
- `CLAUDE.md` — "지금 할 일" 0번 섹션에서 0-B를 해소로 갱신(0-W만 잔여).
- `.claude/README.md``slot-plan.md`/`lifecycle-hooks-plan.md`/
`dispatch-core-plan.md` 세 행에 이번 세션 반영 내용 추가.
`doc-check.py` ERROR 0 유지 확인(WARN 84, 편집 전과 동일 — 새로 생긴
불일치 없음).
## 참고 — 세션 중 발견한 별개 사실
다른 세션(quad-4a)이 동시에 커밋한 `2c575d9`(PostRef 반영 후 코퍼스
정합성 감사)가 이 세션 시작 시점엔 이미 반영돼 있었음 — 대화 시작
시점의 CLAUDE.md 스냅샷(시스템 리마인더)이 살짝 stale했으므로, 실제
편집 전 `git log`/파일 재확인으로 최신 상태를 다시 잡고 작업함(세션
번호를 "아홉 번째" 다음인 "열 번째"로 올바르게 잡을 수 있었던 것도 이
재확인 덕분).

View file

@ -163,8 +163,9 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션
`question.md`의 최우선 항목이 **전부 비었음**`0-Y`(`:Compute` lazy
핸들 계약)는 13차 세션에, **`0-Z`(Attribute 이름 소유권)와 `0-A`(재디스패치
하강 diff)는 14차 세션에 확정·`base/` 반영 완료**. 남은 `0-W`(같은 `Ref`
이중 배치)/`0-B`(`dispose` 시그니처)는 M0가 아니라 각각 M4/M6 구현 세부를
막을 뿐임.
이중 배치)는 M0가 아니라 M4 구현 세부만 막음 — **[2026-08-14 열 번째
세션] `0-B`(`dispose` 시그니처/범위)도 해소**되어 `question.md`
"결정 대기"엔 이제 `0-W` 하나만 남음.
**M0 착수 전 반드시 읽을 것 — 이 두 개는 "결정"이 아니라 "구현 규약"이라
여전히 유효**:
@ -1343,3 +1344,27 @@ base 승격**
`OnDestroyed` 이름 재검토 여지 하나(0-B 확정 시, `question.md` 용어
대기열 3순위). ROADMAP M8/백로그·README·brand/architecture/slot/modifier/
typing-limits 전파 완료, `doc-check.py` ERROR 0.
**2026-08-14 열 번째 세션 — `dispose(value)` 시그니처/범위 확정, `question.md` 0-B 해소**
(`session/2026-08-14-10-dispose-scope-resolved.md`)
사용자가 0-B의 남은 미확정(시그니처/대상 범위/`unbindLifetime`과의 역할
분담)을 직접 확정: **범위는 `Slot`+엔진 객체(`Instance`)만, `Observer`/
`Effect`는 명시적으로 제외**(둘은 children 배열 leaf에서 `bindLifetime`/
`canExecute`(GC-native)만으로 관리되고 Slot 같은 트리 부기 자체가 없어
dispose가 막는 문제가 원천적으로 안 생김). 시그니처는 `dispose(value:
Slot | Instance)` — `isSlot`이면 기존 `elementOwner` 판정 재사용, 아니면
`disposeInst(inst)`(`addTag`/`removeTag`/`setAttribute`와 같은 "base
소유+op 주입" 패턴)로 위임. 네이밍은 `free`(GC 언어 맥락과 안 맞음)/
`Destroy`(엔진 `:Destroy()`와 혼동 위험) 둘 다 기각하고 `dispose` 유지.
과정에서 어시스턴트가 "Observer/Effect가 `State<>`로 지원되는지"를 처음에
Modifier 필드 금지 규칙과 혼동해 잘못 답했다가 사용자 지적으로 정정 —
실제로는 children 배열 leaf(`Dispatch/Leaf.luau`)가 이미 `Observer`/
`Effect`를 지원 대상으로 확정해뒀고, `StoreBind`가 "범용, `k`는 무엇이든
받음"이라 `State<Observer>`도 별도 설계 없이 기존 재귀 디스패치 원칙만으로
됨. 부수 해소로 `base/lifecycle-hooks-plan.md``OnDestroyed` 이름
재검토 조건("0-B가 모든 것의 유일한 파괴 경로로 풀리면")도 반대 방향
(범위가 좁아짐)으로 확정되며 발동 없이 영구 종결. `question.md`/
`archive/question-resolved.md`/`base/slot-plan.md`/
`base/dispatch-core-plan.md`/`base/architecture.md`/
`base/lifecycle-hooks-plan.md`/`ROADMAP.md`/`HUMAN_TODO.md`/`README.md`
전부 반영, `doc-check.py` ERROR 0 유지.

View file

@ -149,8 +149,9 @@ git branch -D worktree-debounce-throttle-plan
디자인 결정 중 Lua/Roblox 엔진에 대한 깊은 경험이 필요한 것들은 합리적 기본값으로
진행하면서 `.claude/question.md`에 모아두는 중. 깨어있을 때 훑어보고 기본값이
마음에 안 드는 것만 답해주면 됨 — **[2026-08-13 열네 번째 세션 기준]
M0 착수를 막는 항목은 하나도 없음**(0-W `Ref` 이중 배치는 M4, 0-B
`dispose` 시그니처는 M6 구현 세부만 막음).
M0 착수를 막는 항목은 하나도 없음**(남은 건 0-W `Ref` 이중 배치, M4 구현
세부만 막음 — 0-B `dispose` 시그니처/범위는 2026-08-14 열 번째 세션에
해소돼 `archive/question-resolved.md`로 이전됨).
---
Sources (MCP 리서치): [Roblox/studio-rust-mcp-server](https://github.com/Roblox/studio-rust-mcp-server), [How to Connect Claude Code to Roblox Studio — Clauder Navi](https://www.clauder-navi.com/en/claude-roblox-studio)

View file

@ -370,12 +370,18 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
클로저가 early-return해도 체인에서 항상 소비하므로).
- 전부 `base/slot-plan.md`에 반영돼 있고, `luau-test/19` C 섹션이
소유권 분기를 음성 대조군까지 포함해 실측 검증함.
- [ ] **`dispose(value)`** — 대상이 아직 어느 트리에 의해 살아있길 요구되면
**파괴를 거부하고 즉시 error**(떼어내주지 않음 — 떼는 건 `Set`=언마운트의
몫). 엔진은 `Destroy`/`Clear`에 에러를 안 내지만 quad 자료구조가 깨지므로,
quad가 관리 중인 값을 안전하게 지우는 유일한 경로. 마운트 위치는
`elementOwner`가 이미 알고 있어 새 부기 불필요. 시그니처/대상 범위는
`.claude/question.md` 0-B에서 미확정
- [ ] **`dispose(value: Slot | Instance)`** — 대상이 아직 어느 트리에 의해
살아있길 요구되면 **파괴를 거부하고 즉시 error**(떼어내주지 않음 —
떼는 건 `Set`=언마운트의 몫). 엔진은 `Destroy`/`Clear`에 에러를 안
내지만 quad 자료구조가 깨지므로, quad가 관리 중인 값을 안전하게
지우는 유일한 경로. 마운트 위치는 `elementOwner`가 이미 알고 있어
새 부기 불필요. `isSlot(value)`면 그 경로, 아니면 백엔드가 주입하는
`disposeInst(inst): ()`(`addTag`/`removeTag`/`setAttribute`와 같은
"base 소유+op 주입" 패턴, quad-roblox는 `inst:Destroy()`)로 위임.
**`Observer`/`Effect`는 범위 밖**(GC-native `bindLifetime`/
`unbindLifetime`만으로 충분, 트리 부기 없음) — 2026-08-14 열 번째
세션에 `question.md` 0-B 해소, 정본은 `base/slot-plan.md`
"`dispose(value)`" 절
- [x] **"여러 Slot이 형제로 섞일 때 순서 보장" 해소**(2026-08-09 여섯 번째
세션) — `Dispatch.setLength`/`setOffsetSource` 메커니즘, `base/