From 573dd452afb3cf1130b410427c594bd54aa74428 Mon Sep 17 00:00:00 2001 From: qwreey Date: Fri, 14 Aug 2026 06:18:28 +0900 Subject: [PATCH] =?UTF-8?q?docs(base):=20dispose(value)=20=EC=8B=9C?= =?UTF-8?q?=EA=B7=B8=EB=8B=88=EC=B2=98/=EB=B2=94=EC=9C=84=20=ED=99=95?= =?UTF-8?q?=EC=A0=95=20=E2=80=94=20question.md=200-B=20=ED=95=B4=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 범위를 Slot+엔진 객체로 좁히고 Observer/Effect는 명시적으로 제외 (GC-native bindLifetime만으로 충분, 트리 부기 없음). isSlot이 아니면 disposeInst 주입 op으로 위임(addTag/removeTag/setAttribute와 같은 패턴). 부수적으로 OnDestroyed 이름 재검토 조건도 종결. Co-Authored-By: Claude Sonnet 5 --- .claude/README.md | 6 +- .claude/archive/question-resolved.md | 36 +++++ .claude/base/architecture.md | 2 +- .claude/base/dispatch-core-plan.md | 7 +- .claude/base/lifecycle-hooks-plan.md | 52 +++---- .claude/base/slot-plan.md | 55 ++++++- .claude/question.md | 32 +---- .../2026-08-14-10-dispose-scope-resolved.md | 136 ++++++++++++++++++ CLAUDE.md | 29 +++- HUMAN_TODO.md | 5 +- ROADMAP.md | 18 ++- 11 files changed, 294 insertions(+), 84 deletions(-) create mode 100644 .claude/session/2026-08-14-10-dispose-scope-resolved.md diff --git a/.claude/README.md b/.claude/README.md index bd74388..18bd00b 100644 --- a/.claude/README.md +++ b/.claude/README.md @@ -34,10 +34,10 @@ | `lifecycle-pattern.md` | rbvm의 `Connected`+GC 관용구를 quad-v2가 채택하는 방식. **[2026-08-14 다섯 번째 세션, 시그니처 정정]** `bindLifetime(inst,value)`/`unbindLifetime(value)`/`canExecute(value)` — 뒤의 둘은 `inst`를 안 받음(`bindLifetime`이 바인딩 시점에 gcconn 참조를 `value` 쪽 `Relate`로 복사해두므로 `value` 하나로 생존을 물을 수 있고, 실제 호출부인 State 전파 루프엔 애초에 `inst`가 없음). `.Subscribed`는 전역 `:Subscribe()` 전용 필드로 분리(`bindLifetime`은 읽지도 쓰지도 않음), 별도 `canBound`는 폐기되어 `canExecute` 하나로 통합, gcconn/gchold는 lazy가 아니라 **Instance 생성 시점**에 만들고 클로저가 `gchold`와 `inst`를 둘 다 캡처(userdata 포인터 동일성 = `inst`-키 `Relate` 전체의 전제). 옛 2-인자 모델은 `archive/canexecute-inst-arg-reversed.md` | | `store-plan.md` | **[2026-08-14 신설 — `bind-system-plan.md` 3단계 분할 + 구 store-semantics.md 흡수]** Store = **이름 붙은 Source 모음, 그 이상 아님** — Store 부작용 허용이 기본 디자인(국소적 vs 경계를 넘는 부작용), `defaults`는 선택적 초기값 템플릿(원본을 나중에 mutate해도 UB 아님)이고 **eager 생성과 lazy 생성이 둘 다 필요**(Luau 타입은 런타임에 강제 안 되므로), `table.clone` 기반 eager 생성 스케치, `store.key`(dot-access)가 1급 경로이고 `store "key"` 문자열 커링은 동적 키용 미타입 폴백, 레코드 필드 타이핑은 Luau `type function`으로 해결 확인, `store.key = value` 폐기 → `store.key:Set(value)`(타입 대칭성+lazy 정직성), "Store가 Store를 저장 가능한가"는 **그런 경우를 안 만듦**으로 확정(`State>`와는 다른 축) | | `source-state-plan.md` | **[2026-08-14 신설 — `bind-system-plan.md` 3단계 분할 + 구 store-semantics.md 흡수]** 반응형 코어: `Source`⊇`State` 구조적 서브타입(`RefSource` 폐기, 단방향 의존으로 Luau 솔버 회피 — 스파이크 `08` 통과), **push-invalidate/pull-recompute** 전파 모델과 "관측해야 실체화된다" 전역 원칙, State 체인 플래튼 기각(캐싱이 State의 존재 이유), `:With`도 매번 새 노드(clone 계열인 `Tag`/`Modifier`와 혼동 주의), `:Compute`의 lazy 핸들 계약(`:Get()` 누락이 반복되는 실수)·trailing args sugar·`fn(self, previous?, ...deps)` 순서·`previous`, `:Apply`, `:Emit()`(Source 원천 전용 하드 경계)과 `Store`/`Source`의 `T`가 Modifier일 수 없는 따름정리, `state:Observer(fn)`, `:Subscribe()`/`:Unsubscribe()`, **이중 바인딩 금지 게이트**(`canExecute` 하나로 통합), PA님 코드 교차검증 | -| `dispatch-core-plan.md` | **[2026-08-13 열네 번째 세션 신설 — `bind-system-plan.md` 2단계 분할 + 0-A/0-Z 반영]** 디스패치 코어: 핸들러 계약(`isHandlable`/`priority`/`process`가 retract 클로저를 반환) / **하강 diff 재디스패치**(래핑 핸들러의 `retractFrom` 선행 호출 폐기, `Dispatch.process`가 슬롯의 `handler`를 먼저 비교해 — 같으면 그 자리 클로저에 새 값을 넘기고 재`process`, 다르면 그 자리부터 전량 철거) / `chains` 인덱스 체인과 **3-인자** `Dispatch.retractFrom(inst,k,index)`(힌트 인자 소멸 — 값 전달 경로가 (A) 분기 하나로 통일) / `None` 센티널 / Handler 작성 체크리스트 8개 / Length·Offset 형제 순서 보장 / "store 바인드는 래핑" 결론. **새 결정 둘**: `HANDLER_PRIORITY_FALLBACK`(base 제공 핸들러의 기본 밴드 — 백엔드가 평범한 우선순위로 덮어쓰면 언제나 이김), **"base가 소유하는 핸들러와 주입되는 엔진 op"**(부기가 엔진 지식을 요구하지 않으면 알고리즘은 base, 마지막 한 줄만 주입 — `addTag`/`removeTag`/`setAttribute`). 옛 힌트 모델은 `archive/dispatch-hintvalue-model-reversed.md` | +| `dispatch-core-plan.md` | **[2026-08-13 열네 번째 세션 신설 — `bind-system-plan.md` 2단계 분할 + 0-A/0-Z 반영]** 디스패치 코어: 핸들러 계약(`isHandlable`/`priority`/`process`가 retract 클로저를 반환) / **하강 diff 재디스패치**(래핑 핸들러의 `retractFrom` 선행 호출 폐기, `Dispatch.process`가 슬롯의 `handler`를 먼저 비교해 — 같으면 그 자리 클로저에 새 값을 넘기고 재`process`, 다르면 그 자리부터 전량 철거) / `chains` 인덱스 체인과 **3-인자** `Dispatch.retractFrom(inst,k,index)`(힌트 인자 소멸 — 값 전달 경로가 (A) 분기 하나로 통일) / `None` 센티널 / Handler 작성 체크리스트 8개 / Length·Offset 형제 순서 보장 / "store 바인드는 래핑" 결론. **새 결정 둘**: `HANDLER_PRIORITY_FALLBACK`(base 제공 핸들러의 기본 밴드 — 백엔드가 평범한 우선순위로 덮어쓰면 언제나 이김), **"base가 소유하는 핸들러와 주입되는 엔진 op"**(부기가 엔진 지식을 요구하지 않으면 알고리즘은 base, 마지막 한 줄만 주입 — `addTag`/`removeTag`/`setAttribute`, **[2026-08-14 열 번째 세션]** 같은 패턴을 Dispatch 밖의 `dispose(value)`/`disposeInst`에도 재사용). 옛 힌트 모델은 `archive/dispatch-hintvalue-model-reversed.md` | | `bind-system-plan.md` | **[2026-08-14, 3단계 분할로 203줄까지 축소 — 지금은 "인스턴스 생성/이벤트 네이밍 인체공학 + 분할 색인" 문서]** 반응형 코어는 `source-state-plan.md`, Store는 `store-plan.md`, 디스패치 코어는 `dispatch-core-plan.md`로 나갔음. 아래 이력은 분할 전 이 파일이 담고 있던 결정들의 기록(현행 소스는 각 분할 문서). pluggable key/value 핸들러 레지스트리 — `process`/`retract` 디스패치 모델, Ref, Store/State/Source 온톨로지 + 인체공학 질문 전부 확정. 디스패치 엔진은 `quad-base`가 인터페이스로 소유(2026-08-04 5차 라운드). **[2026-08-11 세션, 여섯 번째]** `Dispatch.setLength`/`setOffsetSource`의 owner 키가 물리 Instance로 한정될 필요 없음을 명시(Slot-in-Slot 재귀의 근거) — 같은 절 `recompute`의 off-by-one 버그 발견·수정(`offset`이 자기 자신을 포함해 누적되던 것), 재진입 방지 가드는 검토 후 기각(`Source⊇State` 단방향 원칙과 같은 카테고리의 UB로 명명, 각 Slot이 독립 `bk`를 가져 nesting만으로는 재진입 경로 자체가 없음을 확인). **[2026-08-12 열한 번째 세션, 전면 정정]** "핸들러 타입이 안 바뀌면 retract 없이 process가 diff"는 틀렸음 — `retract`는 store 재발행마다(핸들러 타입 무관) 항상 불림, `v`는 대체 값 자체일 수 있어 `nil`로 가정 금지. `Tag`/`Ref`/`Slot`/`Attribute` 전부 이 오류로 설계돼 있었음이 드러나 한 세션에 전부 정정(`archive/retract-always-fires-reversed.md`). **[2026-08-12 세션 후속]** `retractUnder`의 `A and B or C` 삼항 관용구 버그(`v`가 `false`일 때 `nil`로 새던 것)를 `if-then-else`로 수정한 게 계기가 되어 `and`/`or` 삼항 전면 금지 규칙으로 발전(`architecture.md` "코드 스타일" 절). **[2026-08-12 열일곱 번째 세션]** 우선순위 동률/매치 실패 처리(`HANDLER_PRIORITY_*` 상수+디버그 동률 감지, 매치실패는 즉시 error) 확정, `store.key` 레코드 필드 타이핑이 Luau `type function`으로 가능함을 스케치로 확인(`pre-implementation-audit.md` 1-3/1-4/1-10 해소). **[2026-08-12 스무 번째 세션]** Ref 사용 관례 명문화 — React `useRef`급 스코프 감각(만든 컴포넌트 자신이 쓰거나 자식에게 넘기는 용도, 경계 밖 반출·전역 장기 보관은 비권장). **[2026-08-12 스물한 번째 세션]** `:With`가 `Tag`/`Modifier`의 `:` clone 체이닝과 겉보기엔 같은 문법이지만 실제로는 정반대(clone 아니라 매번 새 State 노드)라는 혼동 경고 추가, `Compute`가 `-ed`(`Computed`)가 아닌 이유 절 신설(quad 자기 관례상 `Tag.Added`/`Modifier.Overridden`이 이미 "-ed = clone 후 즉시 확정된 값"을 선점해 lazy한 State에 재사용하면 충돌). **[2026-08-13 세션, 두 번째]** `State>`(store가 emit하는 값 자체가 또 State/Source)가 같은 `(inst,k)`에 같은 핸들러를 중복 push시켜 `retractUnder`의 첫-매치 cutoff가 안쪽 자신을 잘못 retract하는 실제 체인 파손 버그로 확인됨(손 트레이싱, `luau-test/04`가 no-op `retract` 스텁 때문에 이 증상을 못 잡던 사각지대였음도 같이 발견) — `Dispatch.process`에 중복 핸들러 즉시 error 가드 추가, "동일한 재귀적 디스패치로 처리 가능"이라던 낙관적 서술과 "Store가 Store를 저장 가능한가" 절도 정정. **[2026-08-13 세션, 네 번째]** 사각지대 손 트레이싱 라운드에서 `isHandlable` 필드를 선택적으로 허용(생략하면 스캔에 안 걸림)하고, 그런 "체크포인트" 핸들러를 명시적으로 체인에 꽂는 `Dispatch.processAs`/`Dispatch.retractSelfAndUnder`(target 자신 포함 철거) 신설 — `attribute-plan.md`의 그룹/직접쓰기 이름 소유권 충돌을 별도 레지스트리 없이 기존 재진입 가드로 흡수하는 데 씀. **[2026-08-13 세션, 다섯 번째, 전면 재설계 — 위 processAs/retractSelfAndUnder 대체]** `chains`를 핸들러 객체 identity가 아니라 **재귀 깊이 인덱스**로 추적하도록 재설계 — `Dispatch.process(inst,k,v,index)`가 핸들러 호출 *전에* 그 인덱스 점유 여부를 체크(핸들러 부작용 낭비 없음), `process`는 이제 `retract` 필드 대신 자기 retract 클로저(`(hintValue)->()`)를 반환. 같은 키 재귀는 `index+1`, 다른 키 위임은 항상 `1`부터 — 이걸로 `State>`가 UB에서 정상 지원 대상으로 재정정됨(각 재귀 단계가 다른 슬롯을 쓰니 identity 충돌 자체가 없어짐), `retractUnder`/`retractSelfAndUnder`도 `Dispatch.retractFrom(inst,k,index,v)` 하나로 통합(자기 포함/미만은 호출자가 넘기는 인덱스로 표현)되며 체크포인트 패턴 자체가 불필요해짐(`archive/checkpoint-handler-pattern-reversed.md`). 계기: `AttributeGroupHandler` 소유권 버그를 체크포인트로 고치다, 그 근본 원인(identity 기반 추적)을 되짚은 사용자 지적. **[2026-08-13 감사]** 위 재설계 의사코드에서 실제 버그 셋 발견·수정 — (1) `chains:SetStrong`이 `handler.process` *뒤*에 있어 최초 마운트에서 하위 위임 retractor가 통째로 유실되던 것(재귀가 자기 테이블을 만들었다 바깥이 덮어씀), (2) `Ref` retractor가 spurious 재발행에서도 `relate`를 지워 dedup이 무력화되던 것, (3) `Dispatch.drive`의 진입 인덱스(`1`) 미명시. 덧붙여 retractor 안에서는 *같은* 키에 대한 `retractFrom`도 `process`와 똑같이 금지(진행 중인 루프가 `#list`를 이미 캡처)임을 명문화 **[2026-08-13 열네 번째 세션] 2단계 분할 + 모델 교체 — 디스패치 코어 전체가 `dispatch-core-plan.md`로 나갔고(이 문서엔 반응형 코어와 인체공학만 남음), 나가면서 **하강 diff**로 재작성됨. 따라서 위 5차 세션 서술 중 "`Dispatch.process`가 인덱스 **점유 여부**를 먼저 체크"와 "`retractFrom(inst,k,index,v)` **4-인자**"는 **더 이상 현행이 아님**(점유 체크 폐지 → 핸들러 비교, 힌트 인자 소멸 → 3-인자) — 현행은 `dispatch-core-plan.md` | | `module-lifecycle-plan.md` | 프로바이더 패턴, bind/store 구현 책임 분리 — 확정 | -| `slot-plan.md` | 뮤터블 자식 배열, 엄격한 단일 마운트 소유권, 재마운트 시 throw, base/roblox 패키지 경계까지 확정. **[2026-08-09 세 번째 세션]** `Add`/`Remove`/`Extract`/`Clear`/`Move`/`Swap` CRUD(복잡도 표기 포함), `isMounted` 이중 추적 분리, 요소 타입 제약(`nil`/`None`/핸들러 계층 값 금지, `Slot()` 제네릭), 키 기반 동적 컬렉션 재조정(`Slot:List(data, updateFn, keyFn?)`)까지 전부 확정 통합, base/roblox 경계에 reposition 훅 추가. **[2026-08-09 열한 번째 세션, 중간검토]** CRUD 식별 기준을 element 레퍼런스에서 인덱스 기준으로 재정정(`Remove(index)`/`Extract(index, newElement?)`/`Move(oldIndex, newIndex)`), `ExtractAll`/`Get`/`IndexOf` 신설. **[2026-08-11 세션]** `updateFn(item, index: number, offset: Source, prev: T?, userdata: UD?): (T|nil, UD?)`로 시그니처 확정(`Slot.Offset`도 `Length`처럼 공개 필드로 신설) — `LayoutOrder` 등은 Slot이 자동으로 안 세팅, `index`/`offset` raw 값만 전달하고 실제 반영은 `updateFn`이 "버림/다시 그림/source만 갱신" 세 갈래로 직접 처리(재사용 Source에 미리 `Set` 후 결국 다시 그리면 무의미한 연산이 되므로). **[2026-08-11 세션, 여섯 번째]** `Slot:Single(state, updateFn)` 확정(`:List` 위의 순수 sugar) — Slot-in-Slot 중첩도 확정, 요소 타입 제약에서 `Slot` 배제 해제(`T = Instance | Slot`), `Dispatch.setLength`/`setOffsetSource`를 Slot 자신을 owner 키로 재사용하는 재귀 `attachSlot`(새 프리미티브 없음), 파괴는 재귀 `Clear()` 대신 flat `destroySlotTree`+명시적 `unbindLifetime`. `Slot(initial?: {T})` 생성자 부활(순수 `:Add` sugar) + `_crudUsed`↔`_listed` 상호 배타 가드 신설. `base/dispatch-core-plan.md`의 `recompute` off-by-one 버그도 이 세션에 같이 수정됨. **[2026-08-11 세션, 일곱 번째]** 반응형 raw 요소(`Slot:Add`가 `State`/`Source`도 받음) 확정 — 새 메커니즘 아니라 `isState(element)`면 내부적으로 `Slot():Single(element)`(nested Slot)를 대신 삽입하는 순수 sugar(최초 검토했던 별도 position-keyed StoreBind 구독 안은 `None`/Length/Move-Swap 문제로 기각). `Slot:Single(state, updateFn?)`도 `updateFn` 선택 인자화(기본값 identity)로 이 sugar를 지지. `:List`의 `reconcile`도 nested-Slot을 반환하는 아이템의 `.Length`만큼 다음 형제 `index`가 건너뛰도록 `pos` 커밋 공식 수정. **[2026-08-12 열두 번째 세션]** 소유권 판정을 위치별 relate 비교에서, Slot 자신이 지금 어느 `inst`에 바인딩됐는지 직접 추적하는 `slotOwner`(slot→inst)로 전환(같은 Slot이 동시에 다른 위치에 마운트되는 경우까지 잡기 위함) — `owner==inst`면 emit 전파로 무시, 다른 inst면 즉시 error. **[2026-08-12 열세 번째 세션]** `slotOwner`/`kSlotMap`이 서로를 강하게 참조하는 두-`Relate` 상호 GC 순환 발견·수정 — 둘 다 `SetWeak`로 낮추고 실제 GC 앵커는 `bindLifetime`/`unbindLifetime` 하나로 통일(`attachSlot`에 `bindLifetime(physicalTarget, slot)` 추가, `destroySlotTree`에 짝인 `unbindLifetime` 추가). **[2026-08-12 열네 번째 세션]** 위 순환이 Luau에 ephemeron이 없어 실제로 GC 안 되는 게 공식 문서(luau.org/compatibility)로 확인됨 — "혹시 몰라서"가 아니라 확정된 필수 조치로 격상(`relate-plan.md`에 일반 규칙으로도 승격). **[2026-08-12 열다섯 번째 세션]** `Slot:Splice(index, removeCount, ...newElements)` CRUD 신설(구간 제거+삽입을 shift/recompute 1회로 묶는 순수 최적화, `newElements`는 의도적으로 vararg 유지 — `Tag:Added`의 `string|{string}` 전환과는 다른 이유). **[2026-08-12 열여섯 번째 세션]** `slotOwner`를 top-level/nested 이중 마운트 gap까지 잡는 `elementOwner`로 일반화, `bindLifetime`을 top-level 전용으로 축소(nested는 `_elements` 강참조로 transitively 생존). **[2026-08-13 세션]** `releaseOwner`가 소유권 불일치를 조용히 무시하던 걸 즉시 error로 강화, `bindLifetime`을 `attachSlot`의 조건 분기에서 `SlotHandler.process`(Handler 층위)로 이동해 `unbindLifetime`과 대칭을 맞춤. **[2026-08-13 세션, 다섯 번째, 전면 반영]** `Dispatch`가 핸들러 identity 대신 인덱스로 재추적되며 `SlotHandler.process`가 `retract` 별도 필드 없이 자기 retract 클로저를 반환하는 계약으로 전환 — `kSlotMap`이 완전히 불필요해짐(어느 `process` 호출이 반환한 클로저든 `slotValue`/`inst`를 동일하게 캡처해 대칭적으로 동작하므로), `base/dispatch-core-plan.md` "Dispatch 체인" 절 참고. **[2026-08-13 감사]** 그 "대칭적으로 동작"이 `claimOwner`의 false가 *같은 (inst,k) 재발행*일 때만 참이었음이 드러나 소유권 판정을 둘로 분리 — nested(`rawAdd`)는 엄격 `claimOwner`(같은 owner 재클레임도 error, `Slot{a,a}`가 조용히 통과하던 것 차단), top-level은 `claimOwnerAt(element,inst,k)`으로 위치까지 봐서 `Frame{slot,slot}`을 error로 잡음. 추가로 `rawRemove`의 `releaseOwner` 누락(산문엔 있고 의사코드엔 없었음)과 `destroySlotTree`가 자식 소유권/`_mounted`를 안 되돌려 GC 타이밍 의존 오류를 내던 것도 수정. `State` 재설정 경로가 안전함(reconcile이 제거→`rawAdd` 순서라 release→claim)은 별도 절로 확인 기록. **[2026-08-13 세션, 여섯 번째 — 전면 역전]** `State` 교체가 **파괴에서 언마운트로 뒤집힘**(`state`와 동일 — "이전 값을 지울지는 그 값을 만든 쪽이 정한다"는 `Ref`/`Attribute`와 같은 철학) — 이에 따라 (a) 비파괴 짝 `rawUnmount`/`unmountSlotTree` 신설(`rawRemove`/`destroySlotTree`와 딱 하나만 다름: 안 죽임)되고 `reconcile`이 직접 부르는 게 `rawAdd`/`rawUnmount`/`rawMove`로 바뀜, (b) **오래 "오버엔지니어링"으로 기각돼 있던 포탈이 별도 기능이 아니라 이 결정의 자연스러운 귀결이 됨**(옛 "폐기, 옮기지 않음" 결정은 역전, `archive/slot-discard-no-portal-reversed.md`), (c) 명시적 파괴 수단으로 base 탑레벨 `dispose(value)` 신설 — 아직 트리가 살아있길 요구하는 값이면 파괴를 **거부하고 error**(시그니처/범위는 `question.md` 0-B로 열림) **[2026-08-13 열네 번째 세션]** 하강 diff 반영 — `SlotHandler`의 클로저가 받는 값이 항상 `Slot`이거나 `nil`임이 계약으로 보장되고, 언마운트 경로의 `setOffsetSource(None)`/`setLength(0)` 순서는 그대로 | +| `slot-plan.md` | 뮤터블 자식 배열, 엄격한 단일 마운트 소유권, 재마운트 시 throw, base/roblox 패키지 경계까지 확정. **[2026-08-09 세 번째 세션]** `Add`/`Remove`/`Extract`/`Clear`/`Move`/`Swap` CRUD(복잡도 표기 포함), `isMounted` 이중 추적 분리, 요소 타입 제약(`nil`/`None`/핸들러 계층 값 금지, `Slot()` 제네릭), 키 기반 동적 컬렉션 재조정(`Slot:List(data, updateFn, keyFn?)`)까지 전부 확정 통합, base/roblox 경계에 reposition 훅 추가. **[2026-08-09 열한 번째 세션, 중간검토]** CRUD 식별 기준을 element 레퍼런스에서 인덱스 기준으로 재정정(`Remove(index)`/`Extract(index, newElement?)`/`Move(oldIndex, newIndex)`), `ExtractAll`/`Get`/`IndexOf` 신설. **[2026-08-11 세션]** `updateFn(item, index: number, offset: Source, prev: T?, userdata: UD?): (T|nil, UD?)`로 시그니처 확정(`Slot.Offset`도 `Length`처럼 공개 필드로 신설) — `LayoutOrder` 등은 Slot이 자동으로 안 세팅, `index`/`offset` raw 값만 전달하고 실제 반영은 `updateFn`이 "버림/다시 그림/source만 갱신" 세 갈래로 직접 처리(재사용 Source에 미리 `Set` 후 결국 다시 그리면 무의미한 연산이 되므로). **[2026-08-11 세션, 여섯 번째]** `Slot:Single(state, updateFn)` 확정(`:List` 위의 순수 sugar) — Slot-in-Slot 중첩도 확정, 요소 타입 제약에서 `Slot` 배제 해제(`T = Instance | Slot`), `Dispatch.setLength`/`setOffsetSource`를 Slot 자신을 owner 키로 재사용하는 재귀 `attachSlot`(새 프리미티브 없음), 파괴는 재귀 `Clear()` 대신 flat `destroySlotTree`+명시적 `unbindLifetime`. `Slot(initial?: {T})` 생성자 부활(순수 `:Add` sugar) + `_crudUsed`↔`_listed` 상호 배타 가드 신설. `base/dispatch-core-plan.md`의 `recompute` off-by-one 버그도 이 세션에 같이 수정됨. **[2026-08-11 세션, 일곱 번째]** 반응형 raw 요소(`Slot:Add`가 `State`/`Source`도 받음) 확정 — 새 메커니즘 아니라 `isState(element)`면 내부적으로 `Slot():Single(element)`(nested Slot)를 대신 삽입하는 순수 sugar(최초 검토했던 별도 position-keyed StoreBind 구독 안은 `None`/Length/Move-Swap 문제로 기각). `Slot:Single(state, updateFn?)`도 `updateFn` 선택 인자화(기본값 identity)로 이 sugar를 지지. `:List`의 `reconcile`도 nested-Slot을 반환하는 아이템의 `.Length`만큼 다음 형제 `index`가 건너뛰도록 `pos` 커밋 공식 수정. **[2026-08-12 열두 번째 세션]** 소유권 판정을 위치별 relate 비교에서, Slot 자신이 지금 어느 `inst`에 바인딩됐는지 직접 추적하는 `slotOwner`(slot→inst)로 전환(같은 Slot이 동시에 다른 위치에 마운트되는 경우까지 잡기 위함) — `owner==inst`면 emit 전파로 무시, 다른 inst면 즉시 error. **[2026-08-12 열세 번째 세션]** `slotOwner`/`kSlotMap`이 서로를 강하게 참조하는 두-`Relate` 상호 GC 순환 발견·수정 — 둘 다 `SetWeak`로 낮추고 실제 GC 앵커는 `bindLifetime`/`unbindLifetime` 하나로 통일(`attachSlot`에 `bindLifetime(physicalTarget, slot)` 추가, `destroySlotTree`에 짝인 `unbindLifetime` 추가). **[2026-08-12 열네 번째 세션]** 위 순환이 Luau에 ephemeron이 없어 실제로 GC 안 되는 게 공식 문서(luau.org/compatibility)로 확인됨 — "혹시 몰라서"가 아니라 확정된 필수 조치로 격상(`relate-plan.md`에 일반 규칙으로도 승격). **[2026-08-12 열다섯 번째 세션]** `Slot:Splice(index, removeCount, ...newElements)` CRUD 신설(구간 제거+삽입을 shift/recompute 1회로 묶는 순수 최적화, `newElements`는 의도적으로 vararg 유지 — `Tag:Added`의 `string|{string}` 전환과는 다른 이유). **[2026-08-12 열여섯 번째 세션]** `slotOwner`를 top-level/nested 이중 마운트 gap까지 잡는 `elementOwner`로 일반화, `bindLifetime`을 top-level 전용으로 축소(nested는 `_elements` 강참조로 transitively 생존). **[2026-08-13 세션]** `releaseOwner`가 소유권 불일치를 조용히 무시하던 걸 즉시 error로 강화, `bindLifetime`을 `attachSlot`의 조건 분기에서 `SlotHandler.process`(Handler 층위)로 이동해 `unbindLifetime`과 대칭을 맞춤. **[2026-08-13 세션, 다섯 번째, 전면 반영]** `Dispatch`가 핸들러 identity 대신 인덱스로 재추적되며 `SlotHandler.process`가 `retract` 별도 필드 없이 자기 retract 클로저를 반환하는 계약으로 전환 — `kSlotMap`이 완전히 불필요해짐(어느 `process` 호출이 반환한 클로저든 `slotValue`/`inst`를 동일하게 캡처해 대칭적으로 동작하므로), `base/dispatch-core-plan.md` "Dispatch 체인" 절 참고. **[2026-08-13 감사]** 그 "대칭적으로 동작"이 `claimOwner`의 false가 *같은 (inst,k) 재발행*일 때만 참이었음이 드러나 소유권 판정을 둘로 분리 — nested(`rawAdd`)는 엄격 `claimOwner`(같은 owner 재클레임도 error, `Slot{a,a}`가 조용히 통과하던 것 차단), top-level은 `claimOwnerAt(element,inst,k)`으로 위치까지 봐서 `Frame{slot,slot}`을 error로 잡음. 추가로 `rawRemove`의 `releaseOwner` 누락(산문엔 있고 의사코드엔 없었음)과 `destroySlotTree`가 자식 소유권/`_mounted`를 안 되돌려 GC 타이밍 의존 오류를 내던 것도 수정. `State` 재설정 경로가 안전함(reconcile이 제거→`rawAdd` 순서라 release→claim)은 별도 절로 확인 기록. **[2026-08-13 세션, 여섯 번째 — 전면 역전]** `State` 교체가 **파괴에서 언마운트로 뒤집힘**(`state`와 동일 — "이전 값을 지울지는 그 값을 만든 쪽이 정한다"는 `Ref`/`Attribute`와 같은 철학) — 이에 따라 (a) 비파괴 짝 `rawUnmount`/`unmountSlotTree` 신설(`rawRemove`/`destroySlotTree`와 딱 하나만 다름: 안 죽임)되고 `reconcile`이 직접 부르는 게 `rawAdd`/`rawUnmount`/`rawMove`로 바뀜, (b) **오래 "오버엔지니어링"으로 기각돼 있던 포탈이 별도 기능이 아니라 이 결정의 자연스러운 귀결이 됨**(옛 "폐기, 옮기지 않음" 결정은 역전, `archive/slot-discard-no-portal-reversed.md`), (c) 명시적 파괴 수단으로 base 탑레벨 `dispose(value)` 신설 — 아직 트리가 살아있길 요구하는 값이면 파괴를 **거부하고 error** **[2026-08-13 열네 번째 세션]** 하강 diff 반영 — `SlotHandler`의 클로저가 받는 값이 항상 `Slot`이거나 `nil`임이 계약으로 보장되고, 언마운트 경로의 `setOffsetSource(None)`/`setLength(0)` 순서는 그대로. **[2026-08-14 열 번째 세션]** `dispose`의 시그니처/범위(`question.md` 0-B) 확정 — `dispose(value: Slot | Instance)`, `isSlot`이 아니면 백엔드 주입 op `disposeInst(inst)`로 위임(`addTag`/`removeTag`/`setAttribute`와 같은 패턴), `Observer`/`Effect`는 GC-native lifecycle만으로 충분해 범위에서 명시적으로 제외 | | `modifier-plan.md` | Modifier는 런타임 plug 아닌 정적 merge, immutable+clone 기반 체이닝 — 메커니즘 확정. **[2026-08-07 다섯 번째 세션 추가]** `:Apply(factory)` 팩토리 체이닝, `Overridden`(구 `Merge`→`Override`, 2026-08-08 세션에서 이름까지 확정) 값 결합+성능 기준, `:Peek`/`isState` 필드 읽기까지 전부 확정(`Peek`/`isState`는 이름만 용어 정리 라운드까지 잠정). **[2026-08-12 열일곱 번째 세션]** `table.clone`이 메타테이블을 참조로 공유한다는 핵심 전제(M7 "클래스별 코드 없이 제네릭 `__index` 하나로 충분" 설계의 근거)가 실제 Luau 동작으로 확인됨(`pre-implementation-audit.md` 1-11 해소). Property에 Attribute식 이름 소유권 레지스트리를 적용하는 안은 검토 후 기각(엔진이 정한 유한 프로퍼티 이름 집합은 전용 키를 못 만들어 소유권 판정 자체가 성립 안 함 — Property가 override 우선순위를 쓰는 이유) | | `purity-and-effects-plan.md` | 컴포넌트 "순수성"이 아니라 "이식성" 문제로 재정의 — 문서 경고 수준으로 확정 | | `component-composition-plan.md` | 컴포넌트=플레인 함수, State/Source 읽기·쓰기 경계, Source가 State를 구조적으로 만족 — modifier/Ref 컴포넌트 경계 통과까지 전부 확정, 남은 건 API 이름뿐. **[2026-08-07 정리]** 폐기된 `StoreSource` 프록시 설계로의 역전 이력은 본문에서 빼고 `archive/store-source-proxy-reversed.md` 포인터로 압축 | @@ -53,7 +53,7 @@ | `brand-plan.md` | **[2026-08-13 아홉 번째 세션, `bind-system-plan.md`에서 분리]** `Brand` — 런타임 nominal 타입 판별 통합 메커니즘(`Brand.set`/`Brand.get`), `isState`를 branded 타입 전부로 일반화(`isPostRef` 포함, 2026-08-14 아홉 번째 세션). 동작/구현은 확정, **이름 `Brand` 자체만 용어 정리 대기**(`question.md` 1번). **분리는 순수 이동** | | `tween-plan.md` | **[2026-08-12 세션, `research/`에서 승격]** 값-레벨 `Tween` 래퍼(PropertyHandler가 소비, 구 특수 bind key 모델은 `archive/tween-special-bind-key-reversed.md`). 3-상태 릴레이션 슬롯(`{Tween,Value}\|true\|nil`), `T'=T\|Tween` 타입 치환. 옵션 값 모양은 `Info: TweenInfo?` 우선+편의 필드 폴백, override는 `Tween.Cancel`(기본)/`Tween.Finish` 2값. `Animate(info)`는 `Tween` opts를 `T\|State`로 받아 `:Apply`로 꽂는 sugar. 자연완료 시 per-instance 북키핑은 정리 안 해도 됨으로 확정(목표값 도달 상태라 부작용 없음, Completed 이벤트 구독 장치는 오버엔지니어링으로 판단). `initValue`는 사용자가 직접 처리(에이전트 범위 제외) | | `fallback-plan.md` | **[2026-08-14 세션, `research/`에서 승격]** `Fallback`/`Traceback` — 컴포넌트 함수를 감싸 에러 시 플레이스홀더를 그려주는 순수 슈가(`additional-primitives-plan.md`의 "Error Boundary는 빈 자리 아님" 결론 위에 얹힘). `Fallback`은 `pcall` 기반(trace 없음), `Traceback`은 `xpcall`+`debug.traceback` 기반(trace 항상 있음) — 플래그 대신 별도 함수로 분리(`Ref`/`PreRef`와 같은 패턴). `err: any`(Lua `error()`가 임의 값을 던질 수 있음, `error(msg)` 기본 호출의 위치 접두 캐비엇 포함) 확정. 패키지는 `quad-base`, 이름 확정. 메커니즘 실측은 `audit/fallback-xpcall-verification.md`. 구현 우선순위는 형제 백로그(`quad-mock`/`quad-debug`/`Operator`)와 동급, 맨 뒤 | -| `lifecycle-hooks-plan.md` | **[2026-08-14 아홉 번째 세션, `research/`에서 승격]** 생명주기 훅 슈가 `OnCreated`/`OnRendered`/`OnDestroyed` — 각각 `PreRef():Callback(fn)`/`PostRef():Callback(fn)`/`Effect(function() return fn end)`를 반환하는 **순수 팩토리 함수**라 새 타입/Dispatch 개념이 전혀 안 생김(호출 즉시 평가돼 기존 인스턴스로 사라짐), 여러 개 나란히 등록도 자연 지원(단 **같은 계열끼리의 순서는 미보장**). 마지막 열린 항목이던 `OnRendered`는 사용자가 **채택 확정** — 메커니즘은 `PostRef`(`base/ref-plan.md`), 원래 열어뒀던 (a)/(b)/(c) 중 **(a)**. 캐비엇: `OnRendered`는 서브트리 완성은 보장하지만 **이 인스턴스가 부모에 붙기 전**에 불림(React `componentDidMount`와 다름) — 문서화 필수. 패키지 `quad-base` 확정. `OnDestroyed` 이름만 `dispose()` 범위(0-B) 확정 시 재검토 여지(용어 대기열 3순위) | +| `lifecycle-hooks-plan.md` | **[2026-08-14 아홉 번째 세션, `research/`에서 승격]** 생명주기 훅 슈가 `OnCreated`/`OnRendered`/`OnDestroyed` — 각각 `PreRef():Callback(fn)`/`PostRef():Callback(fn)`/`Effect(function() return fn end)`를 반환하는 **순수 팩토리 함수**라 새 타입/Dispatch 개념이 전혀 안 생김(호출 즉시 평가돼 기존 인스턴스로 사라짐), 여러 개 나란히 등록도 자연 지원(단 **같은 계열끼리의 순서는 미보장**). 마지막 열린 항목이던 `OnRendered`는 사용자가 **채택 확정** — 메커니즘은 `PostRef`(`base/ref-plan.md`), 원래 열어뒀던 (a)/(b)/(c) 중 **(a)**. 캐비엇: `OnRendered`는 서브트리 완성은 보장하지만 **이 인스턴스가 부모에 붙기 전**에 불림(React `componentDidMount`와 다름) — 문서화 필수. 패키지 `quad-base` 확정. **[2026-08-14 열 번째 세션]** `dispose()` 범위(0-B)가 `Slot`+`Instance`로 좁혀지고 `Observer`/`Effect`는 제외되는 쪽으로 확정되며 `OnDestroyed` 이름 재검토 조건이 발동 없이 종결 — `OnDestroyed`가 최종 이름, 용어 대기열에서도 제외 | ## `reference/` — 온디맨드 참고 자료 (2026-08-07 신설) diff --git a/.claude/archive/question-resolved.md b/.claude/archive/question-resolved.md index e13dca4..1cb81a1 100644 --- a/.claude/archive/question-resolved.md +++ b/.claude/archive/question-resolved.md @@ -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 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`/ + `State` 등 반응형 leaf 값은 이미 확정된 일반 원칙("모든 + `(inst,k)`는 `T`든 `State`든 `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/`에 문서화되어 더 이상 열려있지 않음 — 상세 근거/논의 과정이 diff --git a/.claude/base/architecture.md b/.claude/base/architecture.md index e04faf1..f4cf506 100644 --- a/.claude/base/architecture.md +++ b/.claude/base/architecture.md @@ -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`) diff --git a/.claude/base/dispatch-core-plan.md b/.claude/base/dispatch-core-plan.md index bef79e9..966b96e 100644 --- a/.claude/base/dispatch-core-plan.md +++ b/.claude/base/dispatch-core-plan.md @@ -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) — diff --git a/.claude/base/lifecycle-hooks-plan.md b/.claude/base/lifecycle-hooks-plan.md index 7c6be46..0298d86 100644 --- a/.claude/base/lifecycle-hooks-plan.md +++ b/.claude/base/lifecycle-hooks-plan.md @@ -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`가 최종 이름. 착수 시점에 위 각 절을 +순서대로 확인하면 됨. diff --git a/.claude/base/slot-plan.md b/.claude/base/slot-plan.md index 235a3c3..a48d7a5 100644 --- a/.claude/base/slot-plan.md +++ b/.claude/base/slot-plan.md @@ -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`/`State`로 + 반응형으로 바뀌는 경우는 전혀 다른 컨텍스트 — 후자는 이미 확정된 일반 + 원칙("모든 `(inst,k)`는 `T`든 `State`든 `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` 유지. #### 구현상 바뀌어야 하는 것 diff --git a/.claude/question.md b/.claude/question.md index 9c484bc..5266a8d 100644 --- a/.claude/question.md +++ b/.claude/question.md @@ -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` 교체를 파괴가 아니라 **언마운트**로 확정하면서(`state`와 -동일, `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) 리네임은 "모호함"은 풀었지만 "다른 뜻으로 이미 쓰이는 단어"라는 새 문제를 만든 셈 — 이번 정리에서 같은 패턴을 조심할 것. diff --git a/.claude/session/2026-08-14-10-dispose-scope-resolved.md b/.claude/session/2026-08-14-10-dispose-scope-resolved.md new file mode 100644 index 0000000..b4e6557 --- /dev/null +++ b/.claude/session/2026-08-14-10-dispose-scope-resolved.md @@ -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`/`State`가 이미 지원 사양"이라고 +주장했다. 어시스턴트는 처음에 이걸 검증하며 `modifier-plan.md`의 "Modifier +필드에 핸들러 계층 값(Ref/PreRef/PostRef/Observer/Effect/Slot/Modifier)이 +들어오면 즉시 error" 규칙과 `slot-plan.md`의 Slot 원소 금지 규칙을 근거로 +"State가 지원된다는 명시적 문서는 없다"고 (잘못) 답했다. + +사용자가 "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`/ + `State`가 그 자리에 놓여도 StoreBind가 실체를 뽑아 재귀 + `Dispatch.process`로 넘기고 `Leaf.luau`가 풀린 값을 매치 — 새 메커니즘 + 없이 이미 있는 일반 원칙("모든 (inst,k)는 T든 State\든 균일하게 + 처리")의 자연스러운 귀결. +- Modifier 필드 금지 규칙(`modifier-plan.md:240`)과 Slot 원소 금지 규칙 + (`slot-plan.md:81`)은 **완전히 다른 컨텍스트**(Modifier 자신의 필드, + `Slot:Add`/`:List`의 CRUD 원소)라 children 배열 leaf 위치와 무관함 — + 어시스턴트가 이 두 컨텍스트를 혼동한 게 오류의 원인이었음. + +어시스턴트가 이 오류를 인정하고 정정, `State`/`State`가 +실제로 확정 사양임을 사용자에게 재확인했다. + +## 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`/파일 재확인으로 최신 상태를 다시 잡고 작업함(세션 +번호를 "아홉 번째" 다음인 "열 번째"로 올바르게 잡을 수 있었던 것도 이 +재확인 덕분). diff --git a/CLAUDE.md b/CLAUDE.md index 668464f..b96ba21 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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`도 별도 설계 없이 기존 재귀 디스패치 원칙만으로 +됨. 부수 해소로 `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 유지. diff --git a/HUMAN_TODO.md b/HUMAN_TODO.md index ade7be6..2f536b0 100644 --- a/HUMAN_TODO.md +++ b/HUMAN_TODO.md @@ -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) diff --git a/ROADMAP.md b/ROADMAP.md index 21ab40b..1bf2b03 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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/