decide(base): canBound 이름 확정, Compute previous 방어/스코핑 명확화, Modifier 핸들러값·State<Modifier> UB→error 전환

- Bound 플래그 → canBound(handle) 탑레벨 함수로 확정(canExecute와 같은 결)
- :Compute의 previous 인자 오버엔지니어링 의심 기각, 결과 노드 귀속으로 스코핑 명확화(self.Cache 안 씀 — 팬아웃 충돌 회피)
- Modifier 필드의 Ref/PreRef/Observer/Effect/Slot/Modifier 값: UB → isX predicate 기반 즉시 error
- State/Source가 Modifier를 값으로 담는 것(State<Modifier>)도 동일하게 error로 통일, Slot 등은 계속 허용
- Tween initValue/useTween 논의 신설(미확정)
- pre-implementation-audit.md 3-1/2-2/문서모순 절 해소 반영, question.md/ROADMAP.md 동기화

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYfAz3BsaTrmMM8hnzn6mj
This commit is contained in:
qwreey 2026-08-09 16:00:19 +09:00
parent 4f3badf414
commit 8169b90a0e
Signed by: qwreey
GPG key ID: D28DB79297A214BD
9 changed files with 320 additions and 51 deletions

View file

@ -697,8 +697,9 @@ flatten된 값은 해시 파트(프로퍼티 키)로 존재하게 되고, Store
- Source/Store 값으로 막는 이유: Store 값은 항상 process/retract 재귀 - Source/Store 값으로 막는 이유: Store 값은 항상 process/retract 재귀
경로로 도착하는데, 그 경로는 정의상 최초 배열 스캔보다 나중(또는 경로로 도착하는데, 그 경로는 정의상 최초 배열 스캔보다 나중(또는
아예 스캔 밖)이라 "프로퍼티보다 먼저"를 구조적으로 만족시킬 방법이 아예 스캔 밖)이라 "프로퍼티보다 먼저"를 구조적으로 만족시킬 방법이
없음 — `State<Modifier>`를 UB로 보고 타입으로 막기로 한 것과 정확히 없음 — `State<Modifier>`를 막기로 한 것(`modifier-plan.md` 7번,
같은 원칙의 재적용. 2026-08-09 세션부터 `isModifier` 기반 명시적 error)과 정확히 같은
원칙의 재적용.
- **`PreRef`는 배열 안 위치의 영향을 안 받는다 — 호이스팅.** 일반 - **`PreRef`는 배열 안 위치의 영향을 안 받는다 — 호이스팅.** 일반
`Ref`와 달리, 같은 인스턴스의 배열 파트 안에서 다른 `Ref`와 달리, 같은 인스턴스의 배열 파트 안에서 다른
children/`Ref`보다 뒤에 적었어도 그것들보다 먼저 fire됨(자바스크립트 children/`Ref`보다 뒤에 적었어도 그것들보다 먼저 fire됨(자바스크립트
@ -989,6 +990,45 @@ stale하다" 수준이 아니라 **영영 갱신이 안 일어날 수 있음**.
"Ref로 한 번 얻어서 수동으로 Parent만 하고 끝"처럼 능동적 관측 경로가 "Ref로 한 번 얻어서 수동으로 Parent만 하고 끝"처럼 능동적 관측 경로가
안 남아있으면, 이 최적화는 그냥 조용히 작동을 멈춘다. 안 남아있으면, 이 최적화는 그냥 조용히 작동을 멈춘다.
**[2026-08-09 세션] 오버엔지니어링 의심 재검토 — 기각, 현재 설계
유지.** `research/pre-implementation-audit.md` 3-1이 "클로저 업밸류로
이미 되는 걸 별도 API로 만든 것 아니냐"고 의심했던 것에 대한 사용자
반박: 클로저 업밸류 대안은 실제로 다음처럼 즉시실행함수(IIFE)로 감싸
업밸류를 준비해야 함 —
```lua
local computeFn = (function()
local prev
return function(self)
-- prev를 읽고 새 값을 계산, prev 갱신
prev = ...
return prev
end
end)()
someSource:Compute(computeFn)
```
이 준비 코드 자체가 이미 별도 `previous` 인자 하나보다 무겁고 번거로움
— "재사용하고 싶으면 그냥 캐시된 값을 바로 넘겨주면 되는" 게 더
단순하다는 게 사용자 논거. 반대로 `previous`가 없으면 `fn`은 매 호출마다
새 인스턴스를 만들어야 해서(예: `LocalizationTable.new()`) lazy든
아니든 재계산이 일어날 때마다 항상 비싼 재생성이 발생 — `previous`
막으려는 문제는 실재함. **`pre-implementation-audit.md` 3-1 해소 —
현재 `fn(self, previous)` 설계 그대로 유지, API 표면을 줄이지 않음.**
**스코핑 명확화(이번 세션에 확인, 새 결정 아님) — `previous``self`
(입력)가 아니라 "이 `:Compute` 호출 하나가 만들어낸 결과 State 노드"
자신에 귀속된다.** State가 `:With`/`:Compute` 호출마다 새 노드를
만든다는 건 이미 확정된 온톨로지(아래 "왜 State 체인을 Modifier처럼
플래튼하지 않는가" 절)라, `previous`도 그 새 노드의 내부 캐시 슬롯일
`self`에 얹히는 게 아님 — 같은 `self`에서 여러 `:Compute`가 갈라지는
팬아웃(`c1 = w:Compute(g1)`, `c2 = w:Compute(g2)`)이 있어도 `g1`/`g2`
각자의 `previous`는 각자의 결과 노드에 독립적으로 저장되므로 서로 안
섞임 — 새로 결정할 것 없이 기존 "노드별 캐시" 원칙의 당연한 귀결.
(참고: `self.Cache`처럼 `self` — 즉 입력 — 에 캐시를 얹는 모양은 이
스코핑과 안 맞아 채택하지 않음 — 팬아웃 시 여러 소비자가 같은
`self.Cache` 슬롯을 공유해 덮어쓰는 충돌이 생기기 때문.)
### `state:Observer(fn)` — 값을 안 실어주는 구독, children 배열에 직접 놓는 leaf 값 ### `state:Observer(fn)` — 값을 안 실어주는 구독, children 배열에 직접 놓는 leaf 값
**결정(2026-08-06 후속 세션, 사용자 확정)**: 별도 `ObserverHolder` **결정(2026-08-06 후속 세션, 사용자 확정)**: 별도 `ObserverHolder`
@ -1169,7 +1209,7 @@ State/Source도 `:With`/`:Compute`마다 새 노드가 나오는 같은 모양
객체를 mutate하고 그대로 돌려주는 것)지만 표면 문법은 비슷하게 객체를 mutate하고 그대로 돌려주는 것)지만 표면 문법은 비슷하게
체이닝 가능. 체이닝 가능.
### 이중 바인딩 금지 — leaf 부착과 `:Subscribe()`는 상호 배타적, `Bound` 플래그로 즉시 에러 (2026-08-07 일곱 번째 세션) ### 이중 바인딩 금지 — leaf 부착과 `:Subscribe()`는 상호 배타적, `canBound(handle)`로 즉시 에러 (2026-08-07 일곱 번째 세션, 2026-08-09 세션에서 이름 확정)
**규칙**: 같은 Observer/Effect 핸들 하나는 라이프사이클 바인딩 경로를 **규칙**: 같은 Observer/Effect 핸들 하나는 라이프사이클 바인딩 경로를
딱 하나만 가질 수 있음 — children 배열에 놓여 leaf에 자동 부착되거나 딱 하나만 가질 수 있음 — children 배열에 놓여 leaf에 자동 부착되거나
@ -1181,29 +1221,44 @@ State/Source도 `:With`/`:Compute`마다 새 노드가 나오는 같은 모양
**UB를 조용한 오동작이 아니라 즉시 에러로 만든다** — 판별 비용이 사실상 **UB를 조용한 오동작이 아니라 즉시 에러로 만든다** — 판별 비용이 사실상
0(불리언 필드 하나 확인)이라, 조용히 이상하게 동작하게 두는 것보다 0(불리언 필드 하나 확인)이라, 조용히 이상하게 동작하게 두는 것보다
바로 에러를 던져 버그를 그 자리에서 잡는 게 엔지니어링상 훨씬 쌈: 바로 에러를 던져 버그를 그 자리에서 잡는 게 엔지니어링상 훨씬 쌈.
**이름 확정 — `canBound(handle): boolean`, `canExecute`와 같은 결의
탑레벨 함수(2026-08-09 세션, 가칭 `Bound` 필드를 직접 노출하는 대신).**
`canExecute(inst, value)`가 "지금 살아있어서 실행돼도 되는가"를 묻는
탑레벨 predicate인 것과 똑같이, "아직 어느 경로로도 안 묶였는가"도
raw 필드(`self.Bound`)를 직접 보여주지 않고 같은 스타일의 탑레벨
함수로 감싼다 — Observer/Effect 둘 다 쓰는 범용 predicate라 특정
프리미티브 하나의 전용 소유물이 아니므로(`store-semantics.md`의
네이밍 케이싱 기준: "이 이름이 특정 프리미티브 타입 하나의 전용
소유물인가?"에 아니오라 소문자 탑레벨이 맞음, `architecture.md`
"코드 스타일 — 네이밍 케이싱" 절과 같은 기준):
```lua ```lua
-- :Subscribe() 진입부, children 배열 leaf 부착부 — 둘 다 진입 전 동일하게 확인 -- :Subscribe() 진입부, children 배열 leaf 부착부 — 둘 다 진입 전 동일하게 확인
if self.Bound then if not canBound(self) then
error("Observer/Effect가 이미 다른 경로로 바인딩됨 — leaf 부착과 :Subscribe()는 동시에 쓸 수 없음") error("Observer/Effect가 이미 다른 경로로 바인딩됨 — leaf 부착과 :Subscribe()는 동시에 쓸 수 없음")
end end
self.Bound = true -- 통과했으면 여기서 바인딩됨으로 표시(내부 구현 디테일 — 공개 표면은 canBound 하나뿐)
``` ```
- **`Bound`는 가칭** — 용어 정리 라운드에서 최종 이름 재검토 대상 - `canBound(handle)`은 "이 핸들이 아직 어느 경로로도 안 묶였으면
(`.claude/question.md`에 반영). `true`, 이미 한 번 묶였으면 `false`"를 답하는 순수 predicate — 내부
- 이 플래그는 어느 경로가 먼저 왔는지와 무관하게 "이미 바인딩됨"만 구현은 여전히 불리언 플래그 하나(예전 가칭 `Bound`)로 충분하지만,
표시 — 두 진입점이 똑같이 확인/설정하므로 순서와 무관하게 대칭적으로 공개 표면에서 그 raw 필드를 직접 보여주지 않고 함수로 감싼다는 점만
막힘. 바뀜. 동작 자체(둘 중 한 경로만 허용, 위반 시 그 자리에서 에러)는
안 바뀜.
- 이 predicate는 어느 경로가 먼저 왔는지와 무관하게 "이미 바인딩됨"만
답함 — 두 진입점이 똑같이 `canBound`를 확인하므로 순서와 무관하게
대칭적으로 막힘.
- **`:Unsubscribe()`는 여전히 "어떤 경로로 바인딩됐든 그 계약을 끊는다"는 - **`:Unsubscribe()`는 여전히 "어떤 경로로 바인딩됐든 그 계약을 끊는다"는
뜻으로 통일** — `Bound`가 어느 경로로 세워졌는지와 무관하게, 뜻으로 통일** — 바인딩이 어느 경로로 세워졌든, `:Unsubscribe()`
`:Unsubscribe()` 한 번으로 그 바인딩(leaf의 Destroying 연결이든 수동 번으로 그 바인딩(leaf의 Destroying 연결이든 수동 강참조 등록이든)을
강참조 등록이든)을 끝내고 최종 정리를 수행. 위 "`:Unsubscribe()`는 끝내고 최종 정리를 수행. 위 "`:Unsubscribe()`는 자동(리프) 케이스에도
자동(리프) 케이스에도 동일하게 씀" 절과 정합 — 이중 바인딩 금지 규칙과 동일하게 씀" 절과 정합 — 이중 바인딩 금지 규칙과 별개로, "단일
별개로, "단일 바인딩을 끊는" `:Unsubscribe()` 자체의 계약은 안 바뀜. 바인딩을 끊는" `:Unsubscribe()` 자체의 계약은 안 바뀜.
- **Effect도 동일 규칙 적용** — 내부적으로 Observer를 조합하는 경우든 - **Effect도 동일 규칙 적용** — 내부적으로 Observer를 조합하는 경우든
`state` 없는 경우든 같은 `Bound` 게이트를 그대로 재사용 `state` 없는 경우든 같은 `canBound` 게이트를 그대로 재사용
(`base/effect-plan.md`). 이전에 그 문서에 적어뒀던 "leaf 부착과 (`base/effect-plan.md`). 이전에 그 문서에 적어뒀던 "leaf 부착과
`:Subscribe()`를 동시에 쓰는 것도 안전"이라는 서술은 **이 규칙으로 `:Subscribe()`를 동시에 쓰는 것도 안전"이라는 서술은 **이 규칙으로
대체(정정)** — 안전하게 지원하는 게 아니라 애초에 막아야 하는 대체(정정)** — 안전하게 지원하는 게 아니라 애초에 막아야 하는
@ -1761,6 +1816,13 @@ Brand.get(x) == PreRefTag`) — `isState`처럼 집합 멤버십이 아님, 즉
서로 배타적인 형제 브랜드. `isModifier`도 같은 단순 항등 서로 배타적인 형제 브랜드. `isModifier`도 같은 단순 항등
(`Brand.get(x) == ModifierTag`). (`Brand.get(x) == ModifierTag`).
**같은 이유로 `isSlot`/`isEffect`도 명시(2026-08-09 세션)** —
`Brand.get(x) == SlotTag`/`Brand.get(x) == EffectTag`인 단순 항등
predicate, 태그 자체는 원래부터 목록에 있었지만(`SlotTag`) `isX`
wrapper로 명시적으로 안 적혀 있던 것을 `base/modifier-plan.md`
"핸들러 계층 값이 필드로 들어오면 즉시 error" 절이 필요로 해서 이번에
같이 적음.
**`None`은 이 레지스트리에 안 들어감 — 싱글턴이라 항등 비교로 충분.** **`None`은 이 레지스트리에 안 들어감 — 싱글턴이라 항등 비교로 충분.**
`Observer`/`Store`처럼 인스턴스가 여러 개 생기는 타입과 달리 `None` `Observer`/`Store`처럼 인스턴스가 여러 개 생기는 타입과 달리 `None`
quad 전체에서 딱 하나만 존재하므로 weak table 조회보다 `x == None` quad 전체에서 딱 하나만 존재하므로 weak table 조회보다 `x == None`

View file

@ -109,7 +109,8 @@ quad의 반응형 그래프/cleanup 인체공학만 재사용하는 경우)로
일곱 번째 세션 후속)**: 처음엔 "같은 liveness 게이트를 공유하니 일곱 번째 세션 후속)**: 처음엔 "같은 liveness 게이트를 공유하니
동시에 써도 안전"으로 적었으나, 애초에 한 핸들은 라이프사이클 바인딩 동시에 써도 안전"으로 적었으나, 애초에 한 핸들은 라이프사이클 바인딩
경로를 하나만 가져야 한다는 게 맞는 방향이라 판단이 뒤집힘 — 상세 경로를 하나만 가져야 한다는 게 맞는 방향이라 판단이 뒤집힘 — 상세
규칙과 `Bound` 플래그 기반 즉시-에러 메커니즘은 규칙과 `canBound(handle)` 기반 즉시-에러 메커니즘(구 가칭 `Bound`
플래그, 2026-08-09 세션에서 이름 확정)은
`base/bind-system-plan.md`의 "이중 바인딩 금지" 절 참고. leaf 부착 `base/bind-system-plan.md`의 "이중 바인딩 금지" 절 참고. leaf 부착
**후** `:Unsubscribe()`로 조기 해제하는 것(위 "Observer의 **후** `:Unsubscribe()`로 조기 해제하는 것(위 "Observer의
`:Unsubscribe()`는 자동 케이스에도 동일하게 씀" 패턴)은 여전히 정상 — `:Unsubscribe()`는 자동 케이스에도 동일하게 씀" 패턴)은 여전히 정상 —

View file

@ -184,14 +184,34 @@ Roblox API에 전혀 의존 안 하는 순수 Lua 테이블 조작이라, "base
**Modifier의 체이닝 엔진 자체는 quad-base에 완결된 구현으로 그대로 **Modifier의 체이닝 엔진 자체는 quad-base에 완결된 구현으로 그대로
존재해도 됨** — 주입할 엔진별 구현이 애초에 없음. 존재해도 됨** — 주입할 엔진별 구현이 애초에 없음.
**Modifier는 핸들러 계층을 모름 — 순수 데이터 merge 레이어.** Modifier는 **Modifier 필드에 핸들러 계층 값(Ref/PreRef/Observer/Effect/Slot/
`process`/`retract`/`isHandlable` 핸들러 레지스트리보다 아래층(위 "1. Modifier)이 들어오면 즉시 error — UB 아님(2026-08-09 세션, 정정).**
런타임 pluggable 핸들러 아님" 참고)이라, Ref/Slot 같은 핸들러 계층 이전 버전("권장 사용법은 아니지만 막을 이유도 없음 — 방어 로직 없는
개념을 아예 몰라도 됨 — 필드에 Ref나 Slot 같은 값이 들어가도 Modifier UB로 남겨둠")은 폐기. 재검토 근거(사용자): Modifier는 애초에 자식/Ref
입장에선 그냥 flatten해서 최종 props 테이블에 얹을 뿐. 권장하는 사용법은 같은 걸 다루는 목적이 아니고, 이런 값이 실제로 쓸모 있는 use case가
아니지만(Modifier는 스타일링 데이터 지향이니), 막을 이유도 없음 — 방어 없다고 확인된 이상 조용한 UB보다 그 자리에서 막는 쪽이 낫다 — 판별
로직 없는 UB로 남겨둠(오늘 세션 내내 반복된 "드문 오용까지 방어하려고 비용도 이미 있는 `Brand` 기반 predicate(`isRef`/`isPreRef`/
구조 복잡하게 안 만든다" 원칙과 동일). `isObserver`/`isEffect`/`isSlot`/`isModifier`, `bind-system-plan.md`
`Brand` 절)를 그대로 재사용하면 되므로 거의 공짜.
- **체크 지점 — 제네릭 `__index` setter가 최종 저장 직전에 검사.**
4번 절의 제네릭 setter(`clone[key] = value`, 또는 함수 인자면
`clone[key] = fn(old)`, 4-1번 표의 State 분기 결과도 포함)가 실제로
필드에 쓰려는 값을 확정한 직후, 그 값이 `isRef(v) or isPreRef(v) or
isObserver(v) or isEffect(v) or isSlot(v) or isModifier(v)`를
만족하면 `error`. 리터럴로 직접 넣은 경우든(`mod:SomeField(someRef)`류
오용) 변환 함수가 반환한 경우든(`mod:X(function(old) return someRef
end)`) 동일하게 걸림 — "콜백이냐 직접 실행이냐"를 구분하지 않고 최종
저장값 하나만 보면 충분(사용자 제안).
- **State/Source는 여전히 허용** — 이 체크는 핸들러 계층 값만 잡음,
4-1번 절의 "필드가 State일 수도 있음"과 안 부딪힘(`isState`가 참인
값은 이 체크를 그냥 통과함).
- **7번 절(`State<Modifier>` UB)과의 비대칭이 이걸로 줄어듦**
`pre-implementation-audit.md`가 지적했던 "같은 문서 안에서 한쪽은
방어(타입 차단 시도), 한쪽은 무방비 UB"라는 비일관성이, 이제 둘 다
"적극적으로 막는다"는 같은 방향으로 정리됨(메커니즘은 여전히 다름 —
하나는 타입 레벨 차단 시도+실패 시 UB 폴백, 하나는 런타임 `error`
이 차이 자체는 남지만 "막을 가치가 있는가"라는 판단은 통일됨).
### 4-1. 필드가 State일 수도 있음 — Setter가 State/plain 여부로 분기 ### 4-1. 필드가 State일 수도 있음 — Setter가 State/plain 여부로 분기
@ -248,17 +268,50 @@ Modifier가 immutable해야 하는 이유(변환마다 clone)와 State가 이미
명시적으로 쓰는 구조라 "암묵적 분기"가 애초에 존재하지 않음 — 새로 결정할 명시적으로 쓰는 구조라 "암묵적 분기"가 애초에 존재하지 않음 — 새로 결정할
것 없음. 것 없음.
### 7. State가 Modifier를 값으로 담는 것은 UB — 타입으로 막을 것 (2026-08-04, 로드맵 인수인계 라운드) ### 7. State/Source가 Modifier를 값으로 담는 것 — 명시적 error로 확정 (2026-08-04 신설, 2026-08-09 세션 정정)
Modifier "필드"가 State일 수 있는 것(4-1번)과는 별개로, **State 자체의 Modifier "필드"가 State일 수 있는 것(4-1번)과는 별개로, **State/Source
value가 Modifier인 경우**(예: `someState:With(fn)`이 Modifier를 반환)는 자체의 value가 Modifier인 경우**(예: `someState:With(fn)`이 Modifier를
지원 대상이 아님 — Modifier는 "flatten해서 한 번 적용"이 전제인 정적 값인데, 반환하거나 `someSource:Set(someModifier)`)는 지원 대상이 아님 — Modifier는
State에 담기면 그 값이 반응형으로 바뀔 수 있다는 뜻이 되어 매번 재-flatten이 "flatten해서 한 번 적용"이 전제인 정적 값인데, State/Source에 담기면 그
필요해지고, 이는 "정적 merge" 확정(1번)과 정면으로 충돌함 — **사용자 값이 반응형으로 바뀔 수 있다는 뜻이 되어 매번 재-flatten이 필요해지고,
이는 "정적 merge" 확정(1번, "Modifier는 런타임 pluggable 핸들러가 아니라
dispatch 밖에서만 처리되는 유일한 존재")과 정면으로 충돌함 — **사용자
확정**("state 안에 modifier가 있으면 그건 끔찍히 힘들꺼야... 타입 상 받지 확정**("state 안에 modifier가 있으면 그건 끔찍히 힘들꺼야... 타입 상 받지
못하게 만들어야 할 수도 있고"). **UB로 확정, 가능하면 타입 시스템으로 못하게 만들어야 할 수도 있고").
아예 못 넣게 막을 것**(`State<Modifier>` 같은 조합을 타입 정의 단계에서
거부) — 런타임 가드가 아니라 타입 차단을 우선 검토. **[정정, 2026-08-09 세션] "UB, 가능하면 타입 차단"에서 "명시적
`error`로 확정"으로 전환** — 위 "핸들러 계층 값이 필드로 들어오면
즉시 error" 절(Ref/PreRef/Observer/Effect/Slot/Modifier가 Modifier
*필드*로 들어오는 걸 막은 것)과 같은 방향으로 통일: `isModifier`
predicate(`Brand` 절)를 State/Source 쪽에도 적용해 **런타임에 직접
막는다.** 타입 차단(`State<Modifier>` 같은 조합을 타입 정의 단계에서
거부)은 여전히 되면 좋은 보너스로 계속 시도해볼 수 있지만
(`research/pre-implementation-audit.md` 2-2 — Luau에서 실제로 가능한지
미검증), **더 이상 유일한 방어선이 아님** — 타입이 뚫려도 런타임
`error`가 항상 잡아준다.
- **적용 지점**: "어떤 값이 Source/State의 현재 값으로 확정되는 모든
지점" — `Source:Set(value)` 호출 시, `Store({defaults})` 생성 시
`defaults` 키를 `Source(v)`로 만드는 시점, 그리고 State의
`:Compute(fn)` 결과를 캐시로 저장하기 직전(`fn`이 반환한 값이
`isModifier`면 캐싱 전에 `error`). 새 체크 지점을 여러 곳에 흩는 게
아니라, "값이 State/Source의 값으로 확정되는" 이미 존재하는 몇 안
되는 지점에 `isModifier` 검사 한 줄씩 얹는 것뿐.
- **Slot/Tag/Attribute/Tween 등 다른 핸들러 계층 값은 여전히 아무
문제 없이 State/Source에 담길 수 있음 — Modifier만의 예외임을
명확히.** (사용자 확인: "slot은 당연히 가능함, retract도 되는 애고
런타임 값이라") 이 값들은 전부 정상적으로 `process`/`retract`
재귀 경로(store-bind 재실행 모델, "확정된 디스패치 모델" 절)를 타는
진짜 런타임 dispatch 참가자라, State/Source 값으로 담겨 바뀌어도
기존 재귀 재-dispatch 메커니즘이 그대로 처리해줌 — 새로 막을 이유가
없음. Modifier만 유독 문제인 건 Modifier가 애초에 dispatch 경로를
아예 안 타는 유일한 존재(1번 절)라서, State/Source에 담기는 순간
"재귀 재-dispatch로 처리"할 대상 자체가 없어지기 때문 — 이 구분이
왜 Modifier만 막고 나머지는 다 허용하는지의 핵심 근거.
- **`Store<T>``T`는 Modifier가 될 수 없음(`base/store-semantics.md`
"따름정리" 절)도 이 결정을 그대로 물려받음** — Source가 State를
구조적으로 만족하므로 별도로 다시 논증할 필요 없이 동일하게 적용됨.
### 8. `:Apply(factory)` — 팩토리 함수 체이닝 지원 (2026-08-07) ### 8. `:Apply(factory)` — 팩토리 함수 체이닝 지원 (2026-08-07)

View file

@ -285,16 +285,19 @@ mutation에 정식 신호를 붙여주는 것뿐.
**따름정리 — `Store<T>``T`는 Modifier가 될 수 없음.** Modifier는 **따름정리 — `Store<T>``T`는 Modifier가 될 수 없음.** Modifier는
정적 flatten으로 dispatch와 완전히 별개인 단계에서 처리되고 정적 flatten으로 dispatch와 완전히 별개인 단계에서 처리되고
(`base/modifier-plan.md`), `State<Modifier>`가 UB로 확정된 것도 같은 (`base/modifier-plan.md`) — Store/State/dispatch 경로엔 애초에
이유(`modifier-plan.md` 7번) — Store/State/dispatch 경로엔 애초에 Modifier용 processor가 없음. **[정정, 2026-08-09 세션]** `State<Modifier>`
Modifier용 processor가 없음. **[2026-08-06 후속 세션 추가]** Source가 조합은 "UB, 가능하면 타입 차단"이 아니라 **명시적 `error`로 확정**
State를 구조적으로 만족하게 되면서 이 UB는 `Source<Modifier>`(Store를 (`modifier-plan.md` 7번) — `isModifier` predicate를 `Source:Set()`/
Store 생성 시 eager `Source(default)`/State의 `:Compute` 결과 캐싱
지점에서 확인해 런타임에 직접 막음, 타입 차단은 되면 좋은 보너스일
뿐 유일한 방어선이 아님. **[2026-08-06 후속 세션 추가]** Source가
State를 구조적으로 만족하게 되면서 이 제약은 `Source<Modifier>`(Store를
거치지 않는 독립 `Source(someModifier)`)에도 동일하게 적용됨을 명시 — 거치지 않는 독립 `Source(someModifier)`)에도 동일하게 적용됨을 명시 —
Source가 State 계약을 만족하는 이상 같은 이유(Modifier용 processor Source가 State 계약을 만족하는 이상 같은 이유(Modifier용 processor
부재)가 그대로 적용되고, 별도로 다시 논증할 필요 없음. 위 "하드 경계"와 부재)가 그대로 적용되고, 별도로 다시 논증할 필요 없음. 위 "하드 경계"와
같은 이유로, `Emit`이 Modifier의 정적 flatten과 충돌할 걱정 자체가 같은 이유로, `Emit`이 Modifier의 정적 flatten과 충돌할 걱정 자체가
성립하지 않음(둘이 만날 성립하지 않음(둘이 만날 지점이 없음).
지점이 없음).
## 여러 스토어 값을 묶어 처리하는 것 (dependency array) — 확정 ## 여러 스토어 값을 묶어 처리하는 것 (dependency array) — 확정

View file

@ -87,10 +87,10 @@ context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만**
즉시모드 GUI 뉘앙스), `List`(중립적이나 메커니즘을 안 알려줌) — 아직 즉시모드 GUI 뉘앙스), `List`(중립적이나 메커니즘을 안 알려줌) — 아직
미정, `research/additional-primitives-plan.md`의 "키 기반 동적 컬렉션 미정, `research/additional-primitives-plan.md`의 "키 기반 동적 컬렉션
재조정" 절 참고. 재조정" 절 참고.
- **`Bound`(3순위, 사소함, 2026-08-07 일곱 번째 세션 추가)**: Observer/ - **[해소됨, 2026-08-09 세션]** `Bound`**`canBound(handle): boolean`
Effect 핸들이 leaf 부착과 `:Subscribe()` 중 이미 어느 한쪽으로 탑레벨 함수로 확정**, `canExecute`와 같은 결(raw 필드를 직접 노출하는
바인딩됐는지 표시하는 내부 플래그 이름(`base/bind-system-plan.md` 대신 predicate 함수로 감쌈). `base/bind-system-plan.md` "이중 바인딩
"이중 바인딩 금지" 절) — 동작은 확정, 이름만 가칭. 금지" 절 참고.
- **`Brand`(3순위, 사소함, 2026-08-07 여덟 번째 세션 추가)**: 런타임 - **`Brand`(3순위, 사소함, 2026-08-07 여덟 번째 세션 추가)**: 런타임
nominal 타입 판별 통합 메커니즘(`Brand.set`/`Brand.get`, `isState` nominal 타입 판별 통합 메커니즘(`Brand.set`/`Brand.get`, `isState`
10종 branded 타입 전부로 일반화) — `bind-system-plan.md``Brand` 10종 branded 타입 전부로 일반화) — `bind-system-plan.md``Brand`
@ -138,11 +138,11 @@ context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만**
일반 반응형 프로퍼티 바인딩(`BackgroundColor3 = store.color`, 애니메이션 일반 반응형 프로퍼티 바인딩(`BackgroundColor3 = store.color`, 애니메이션
없음)이 결국 이름은 "Tween"인 파일을 거쳐가는 건지, 아니면 별도 범용 없음)이 결국 이름은 "Tween"인 파일을 거쳐가는 건지, 아니면 별도 범용
`Handlers/StoreBind.luau`가 있어야 하는 건지 확정 필요 — 우선순위1-1. `Handlers/StoreBind.luau`가 있어야 하는 건지 확정 필요 — 우선순위1-1.
- **`State<Modifier>` 타입 차단(엔지니어링 비용 감수)과 Ref/Slot이 Modifier - **[해소됨, 2026-08-09 세션]** `State<Modifier>` Ref/Slot이 Modifier
필드에 들어가는 건 UB 방치 — 같은 문서 안에서 정반대 원칙이 근거 설명 필드에 들어가는 것 — 이제 둘 다 `isX` predicate 기반 명시적 `error`
없이 나란히 적용됨.** 왜 이 경우만 예외로 방어하는지 명문화 필요, 또는 통일(`base/modifier-plan.md` 4번/7번 절, `base/store-semantics.md`
Luau에서 실제 타입 차단이 가능한지부터 확인(안 되면 그냥 UB로 격하) — "따름정리" 절). Luau 타입 차단은 "되면 좋은 보너스"로 격하되어 더
문서모순 절 + 우선순위2-2. 이상 필수 검증 항목 아님 — 문서모순 절 + 우선순위2-2도 갱신 완료.
- ~~`props.Modifier`/`props.Ref` forwarding 관례가 Lua 배열 리터럴 - ~~`props.Modifier`/`props.Ref` forwarding 관례가 Lua 배열 리터럴
nil-hole 함정에 그대로 노출됨~~ — **반영 완료(2026-08-07 열 번째 nil-hole 함정에 그대로 노출됨~~ — **반영 완료(2026-08-07 열 번째
세션)**. `props.Modifier or None`/`props.Ref or None` 관용구를 필수로 세션)**. `props.Modifier or None`/`props.Ref or None` 관용구를 필수로

View file

@ -313,6 +313,16 @@ Luau 솔버가 막히면 되돌릴 범위가 `store-semantics.md`의 절반 이
### 2-2. `State<Modifier>` 타입 차단이 Luau에서 실제로 가능한지 검증 계획이 없음 ### 2-2. `State<Modifier>` 타입 차단이 Luau에서 실제로 가능한지 검증 계획이 없음
**[대부분 해소, 2026-08-09 세션]** "가능하면 타입 차단, 안 되면 UB로
후퇴"라는 원래 걱정 자체가 무의미해짐 — `State<Modifier>`를 이제
`isModifier` predicate 기반 명시적 `error`로 막기로 확정
(`base/modifier-plan.md` 7번 정정, `base/store-semantics.md` "따름정리"
절)했으므로, 타입 차단은 성공하든 실패하든 런타임 에러라는 안전망이
항상 있음 — 아래 "제안"이 우려했던 "조용히 UB로 후퇴" 시나리오 자체가
발생하지 않음. 타입 차단이 Luau에서 실제로 가능한지는 여전히 미검증이지만,
이제 "되면 좋은 보너스"로 우선순위가 낮아짐 — M0/M7 필수 검증 항목에서
제외해도 됨. 아래는 원래 발견 당시 기록.
**위치**: `base/modifier-plan.md` 7번. **위치**: `base/modifier-plan.md` 7번.
**문제**: "가능하면 타입 시스템으로 아예 못 넣게 막을 것"이라 확정했지만, **문제**: "가능하면 타입 시스템으로 아예 못 넣게 막을 것"이라 확정했지만,
@ -501,6 +511,14 @@ M11 착수 시.
### 3-1. `:Compute(fn)``previous` 인자 — 클로저 업밸류로 이미 되는 걸 별도 API로 만든 것일 수 있음 ### 3-1. `:Compute(fn)``previous` 인자 — 클로저 업밸류로 이미 되는 걸 별도 API로 만든 것일 수 있음
**[해소됨, 2026-08-09 세션]** 오버엔지니어링 아님으로 확정, 현재
`fn(self, previous)` 설계 그대로 유지 — 클로저 업밸류 대안은 IIFE로
감싸야 하는 준비 비용이 오히려 더 크다는 게 사용자 반박 논거.
`previous``self`(입력)가 아니라 이 `:Compute` 호출 하나가 만든
결과 State 노드 자신에 귀속되므로 팬아웃 시에도 충돌 없음 — 상세는
`base/bind-system-plan.md`의 "previous" 절 참고. 아래는 원래 발견
당시 기록.
**위치**: `base/bind-system-plan.md` "`:Compute(fn)`의 선택적 두 번째 **위치**: `base/bind-system-plan.md` "`:Compute(fn)`의 선택적 두 번째
인자 — `previous`" 절. 인자 — `previous`" 절.
@ -565,6 +583,16 @@ Handler"라고만 서술해, 사실상 3개의 거의 동일한 형태(리터럴
보이지만, 문서 어디에도 "왜 이 경우엔 원칙에서 예외로 처리하는가"를 보이지만, 문서 어디에도 "왜 이 경우엔 원칙에서 예외로 처리하는가"를
명시적으로 인정/정당화하지 않고 그냥 나란히 적혀 있다. 위 2-2 항목 명시적으로 인정/정당화하지 않고 그냥 나란히 적혀 있다. 위 2-2 항목
(Luau에서 실제 차단 가능한지)과 묶어서 같이 정리할 문제. (Luau에서 실제 차단 가능한지)과 묶어서 같이 정리할 문제.
**[완전 해소, 2026-08-09 세션]** 양쪽 다 이제 같은 메커니즘 —
Ref/Slot(+Observer/Effect/Modifier 자기 자신)이 Modifier *필드*로
들어오는 것도, `State<Modifier>`처럼 Modifier가 State/Source *값*으로
담기는 것도 전부 `Brand` 기반 `isX` predicate로 런타임에 즉시
`error`(`base/modifier-plan.md` 4번/7번 절 정정, `base/
store-semantics.md` "따름정리" 절). 더 이상 "한쪽만 방어" 비대칭이
아님 — 남은 차이는 `State<Modifier>` 쪽에 "되면 좋은 보너스"로
타입 차단을 추가 시도해볼 여지가 있다는 것뿐(위 2-2번, 미검증이지만
더 이상 필수 방어선이 아니라 우선순위 낮음).
- **Destroying 훅 신뢰도에 대한 서술이 `lifecycle-pattern.md` 내부에서도, - **Destroying 훅 신뢰도에 대한 서술이 `lifecycle-pattern.md` 내부에서도,
`framework-comparison-findings.md`와의 사이에서도 어긋남** — 위 1-6 `framework-comparison-findings.md`와의 사이에서도 어긋남** — 위 1-6
항목에 상세, 여기서는 "아직 아무도 하나의 확정 문장으로 정리 안 함"이라는 항목에 상세, 여기서는 "아직 아무도 하나의 확정 문장으로 정리 안 함"이라는

View file

@ -126,6 +126,37 @@ delayTime)`는 순수 포지셔널 생성자인데, Luau엔 named call 문법이
구체적인 필드 이름/기본값/`TweenInfo` 재사용 경로의 정확한 문법은 아직 구체적인 필드 이름/기본값/`TweenInfo` 재사용 경로의 정확한 문법은 아직
확정 아님 — 나중 논의 대상으로 남김. 확정 아님 — 나중 논의 대상으로 남김.
## 초기 진입 애니메이션(`initValue`)과 트윈 우회(`useTween`) — 둘 다 미확정 (2026-08-09 세션)
**아직 논의 시작 단계 — 나중에 더 다룰 주제로만 남겨둠.** 사용자가 두
실사용 시나리오에서 출발한 제안:
1. **`Tween{initValue=...}`(검토 후 필요성 낮은 쪽으로 기움)** — 동기:
다이얼로그가 아래에서 위로 올라오며 열리는 것처럼, 처음 마운트될 때
시작 위치에서 목표값으로 자연스럽게 움직이는 "enter" 애니메이션을
내고 싶은데, 지금 구조로는 첫 마운트 시 이미 목표값이 프로퍼티에
세팅된 채로 시작해 트윈이 걸리지 않음. `initValue`로 먼저 그 값을
세팅해두고 그 다음 실제 목표값으로 트윈을 트리거하면 될 것 같다는
아이디어. **사용자가 스스로 재검토하며 "필요 없는 것 같다"는 쪽으로
기움** — 정확한 반례(재process/재마운트 시 `initValue`를 매번 다시
적용하면 안 되는데 "최초 1회"를 무엇으로 판별할지가 또 다른 설계
문제가 될 수 있음)는 다음 세션에서 확인 필요, 완전 폐기는 아직 아님.
2. **`Tween{useTween = state<boolean>}`(필요성은 확인, 모양은 미정)** —
동기: `Visible`로 여닫는 다이얼로그가 항상 아래에서 위로 슬라이드
올라와야 하는 게 기본이지만, 특정 상황(접근성 설정으로 애니메이션을
완전히 끄고 싶은 경우 등)엔 트윈을 건너뛰고 즉시 목표값으로 스냅해야
함 — 트윈을 무시하고 값을 그냥 세팅할 방법이 필요하다는 문제의식.
제안 동작: 트윈이 실행되려는 시점에 먼저 `useTween:Get()`을 읽어서
`false`면 진행 중이던 트윈을 전부 죽이고 목표값으로 즉시 `Set`,
`true`(또는 생략 시 기본값)면 평소대로 트윈.
**결론(미확정)**: `initValue`는 필요 없어 보이지만(사용자 소견) 완전
폐기는 아직 안 함. `useTween`류 트윈 우회 필요성은 실사용 근거가
뚜렷해 유지될 가능성이 높지만, **정확한 옵션 키 모양과 문서화 방식이
아직 전혀 안 정해짐**(사용자 강조: "어떤 모양을 주냐가, 문서화 어떤
식으로 하냐가 엄청 중요한듯") — 위 "트윈 옵션 값 모양" 절의 편의 필드
논의와 함께 M11 착수 전 나중 세션에서 마저 정리.
## 네임스페이스드 객체 (성능상 이유로 보류) ## 네임스페이스드 객체 (성능상 이유로 보류)
트윈 대상을 이름으로 찾는 별도 네임스페이스는 성능상 별로라고 판단 — 트윈 대상을 이름으로 찾는 별도 네임스페이스는 성능상 별로라고 판단 —
@ -143,3 +174,6 @@ CollectionService를 쓰는 게 나아 보이지만, 트윈 전용 네임스페
받을지 편의 필드+기본값으로 받을지, 소견은 후자 쪽이지만 확정 아님. 받을지 편의 필드+기본값으로 받을지, 소견은 후자 쪽이지만 확정 아님.
- 자연 완료(Completed) 시 per-instance 북키핑 정리 여부 — - 자연 완료(Completed) 시 per-instance 북키핑 정리 여부 —
`research/pre-implementation-audit.md` 2-10번 참고, M11 착수 시 확정. `research/pre-implementation-audit.md` 2-10번 참고, M11 착수 시 확정.
- `initValue`(진입 애니메이션)/`useTween`(트윈 우회) — 위 절 참고,
후자는 필요성은 확인됐으나 모양/문서화 미정, 전자는 필요성 자체가
낮은 쪽으로 기움.

View file

@ -1699,3 +1699,89 @@ realv 타입이 매 갱신마다 바뀔 수 있는데 '이전 핸들러'를 누
**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터). `DI`/`D`와 **다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터). `DI`/`D`와
`canExecute`/`isAlive` 두 개만 용어 정리 라운드에 계속 남음 — `canExecute`/`isAlive` 두 개만 용어 정리 라운드에 계속 남음 —
`question.md` 1순위/3순위 목록 참고. `question.md` 1순위/3순위 목록 참고.
## 2026-08-09 세션 — `canBound` 이름 확정, `:Compute``previous` 방어/스코핑
명확화, Modifier 핸들러 계층 값 UB→error 전환, Tween `initValue`/`useTween`
논의 신설
사용자가 `.claude/question.md`를 훑다 나온 여러 짧은 질문/제안을 한 번에
처리. 전부 `base/`/`research/`에 반영 완료:
1. **`Bound``canBound(handle): boolean` 탑레벨 함수로 확정** — 사용자
제안("canExecute 같은 게 있으니 canBound로 넣어도 되지 않나"), raw
불리언 필드를 직접 노출하는 대신 `canExecute`와 같은 결의 predicate
함수로 감쌈. 동작 자체(leaf 부착과 `:Subscribe()`는 상호 배타, 위반
시 즉시 에러)는 안 바뀜 — `base/bind-system-plan.md` "이중 바인딩
금지" 절, `base/effect-plan.md`, `.claude/question.md` 반영.
2. **`:Compute(fn)``previous` 인자 — 오버엔지니어링 의심 기각, 현재
설계 유지.** `pre-implementation-audit.md` 3-1이 "클로저 업밸류로
이미 되는 걸 별도 API로 만든 것 아니냐"고 의심했던 데 대해 사용자가
직접 반박 — 클로저 업밸류 대안은 IIFE로 감싸는 준비 비용이 오히려
`previous`라는 인자 하나보다 무겁고 번거로움. **부수적으로 스코핑도
명확화**: 처음엔 `self.Cache`처럼 `previous``self`(입력) 쪽에
얹는 모양이 제안됐으나, `self``:Compute`의 입력(receiver)이라
같은 `self`에서 여러 `:Compute`가 갈라지는 팬아웃(`w:Compute(g1)`,
`w:Compute(g2)`)이 있으면 `self.Cache` 슬롯이 충돌한다는 문제를
검토 중 발견 — `previous`는 그 대신 "이 `:Compute` 호출 하나가 만든
결과 State 노드" 자신에 귀속되는 것으로 정리(State가 호출마다 새
노드를 만든다는 기존 온톨로지의 당연한 귀결이라 새 결정은 아님).
`base/bind-system-plan.md`의 "previous" 절, `pre-implementation-audit.md`
3-1 반영.
3. **Modifier 필드에 핸들러 계층 값(Ref/PreRef/Observer/Effect/Slot/
Modifier)이 들어오면 UB 대신 즉시 `error`로 확정.** 기존
"권장 사용법은 아니지만 막을 이유도 없음 — 방어 로직 없는 UB"였던
것을, 이런 값의 실사용 case가 없다는 게 확인된 이상 조용한 UB보다
그 자리에서 막는 쪽이 낫다는 사용자 판단으로 전환 — 이미 있는
`Brand` 기반 predicate(`isRef`/`isPreRef`/`isObserver`/`isEffect`/
`isSlot`/`isModifier`)를 제네릭 `__index` setter가 최종 저장 직전에
확인하기만 하면 되므로 구현 비용 거의 0. `isSlot`/`isEffect`
predicate가 `Brand` 절에 명시적으로 없던 갭도 같이 보강.
`pre-implementation-audit.md`가 지적했던 "`State<Modifier>`는 방어,
Ref/Slot은 무방비"라는 비일관성이 이걸로 절반 해소(메커니즘 차이는
남지만 "막을 가치가 있다"는 판단은 통일) — `base/modifier-plan.md`
"핸들러 계층을 모름" 절, `base/bind-system-plan.md``Brand` 절,
`pre-implementation-audit.md` 문서모순 절 반영.
4. **UI shorthand(UICorner/UIPadding/UIScale)가 Modifier 체이닝에서도
되는지 — 이미 확정돼 있던 것 재확인, 새 결정 없음.** `mod:UICorner(8)`
그냥 제네릭 `__index` setter가 `UICorner` 필드를 채우는 것뿐이고,
그 필드가 Modifier flatten을 거쳐 최종 props 테이블에 얹히든
`Frame { UICorner = 8 }`처럼 순수 인라인으로 들어가든 UICorner
Handler 입장에선 구분이 없음 — `base/ui-shorthand-plan.md`에 이미
명시돼 있던 내용이라 문서 변경 없음.
5. **Tween `initValue`/`useTween` — 새 열린 논의 신설, 확정 아님.**
사용자가 두 실사용 시나리오(다이얼로그 진입 애니메이션, 트윈 우회)를
제기 — `initValue`(첫 마운트 시 시작값을 세팅 후 목표값으로 트윈)는
재검토 끝에 필요성이 낮은 쪽으로 기움(재process 시 "최초 1회" 판별
문제가 있어 보임), `useTween = state<boolean>`(트윈을 끄고 즉시
스냅)은 필요성은 확인됐으나 정확한 모양/문서화 방식이 전혀 안
정해짐 — `research/tween-plan.md`에 신규 절로 반영, M11 착수 전
나중 세션에서 마저 정리하기로 함.
**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — 이번 세션도 순수
설계 확정/보강이라 M0 착수 우선순위 자체는 그대로.
**같은 세션 후속 — `State<Modifier>`도 UB 대신 명시적 `error`로 확정,
"핸들러 계층 값 → error" 원칙을 State/Source 쪽까지 완전히 통일.**
사용자 질문: "Modifier 필드"뿐 아니라 "State/Source 자체의 값이
Modifier인 경우"(`State<Modifier>`, `modifier-plan.md` 7번)도 같은
방식으로 막아도 되는지 — 확정. `isModifier` predicate를
`Source:Set()`/Store 생성 시 eager `Source(default)`/State의
`:Compute` 결과 캐싱 지점에서 확인해 런타임 `error`, 타입 차단(Luau
가능 여부 미검증)은 필수 방어선이 아니라 되면 좋은 보너스로 격하.
**Slot은 대조적으로 계속 허용** — 사용자 확인("slot은 당연히 가능함,
retract도 되는 애고 런타임 값이라"): Slot/Tag/Attribute/Tween 등은
정상적으로 process/retract 재귀 경로를 타는 진짜 dispatch 참가자라
State/Source 값으로 담겨도 기존 재귀 재-dispatch가 그대로 처리해줌 —
Modifier만 예외인 건 Modifier가 애초에 dispatch 경로 자체를 안 타는
유일한 존재라서. `base/modifier-plan.md` 7번, `base/store-semantics.md`
"따름정리" 절, `research/pre-implementation-audit.md` 2-2/문서모순 절
(완전 해소로 갱신), `.claude/question.md`, `ROADMAP.md` M7 반영 완료 —
이걸로 `pre-implementation-audit.md`가 지적했던 "State<Modifier>
방어, Ref/Slot은 무방비"라는 비일관성이 완전히 해소됨.
**핸드오버 준비 완료** — 이번 대화(2026-08-08~09에 걸친 세션)에서 나온
결정은 전부 `base/`/`research/`/`question.md`/`ROADMAP.md`에 반영,
문서 간 참조도 동기화 완료. **다음 세션 예고(사용자 지정)**: Slot과
"State에서 Slot을 뽑아내는" 키 기반 동적 컬렉션 재조정(가칭 `Keyed`
탈락, 최종 이름 미정) — `.claude/question.md` 0번 "키 기반 동적
컬렉션 재조정"이 이미 최우선 항목으로 잡혀있으니 그걸 이어서 보면 됨.

View file

@ -187,7 +187,9 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
`GuiObjectModifier`)을 섞을 때의 타입 시그니처 실 Luau 테스트 `GuiObjectModifier`)을 섞을 때의 타입 시그니처 실 Luau 테스트
(`modifier-plan.md` 9-2번, 미검증 — 안 되면 일단 `Overridden(...: any): (`modifier-plan.md` 9-2번, 미검증 — 안 되면 일단 `Overridden(...: any):
any`로 느슨하게 열어두고 이 항목으로 되돌아올 것) any`로 느슨하게 열어두고 이 항목으로 되돌아올 것)
- [ ] `State<Modifier>` 조합 타입 차단 확인(`modifier-plan.md` 7번, UB 확정) - [ ] `State<Modifier>` 조합에 `isModifier` 기반 명시적 error 적용
(`modifier-plan.md` 7번, 2026-08-09 세션 확정) — 타입 차단은
되면 좋은 보너스로 선택 검증(필수 아님)
- [ ] `:Apply(factory)` 팩토리 함수 체이닝(`modifier-plan.md` 8번, 예약 키 - [ ] `:Apply(factory)` 팩토리 함수 체이닝(`modifier-plan.md` 8번, 예약 키
`Apply`가 제네릭 `__index` 필드 setter와 안 겹치는지 확인) `Apply`가 제네릭 `__index` 필드 setter와 안 겹치는지 확인)
- [ ] `:Peek<<T>>(key): T|State<T>|nil` 필드 읽기 접근자 + - [ ] `:Peek<<T>>(key): T|State<T>|nil` 필드 읽기 접근자 +