docs(base): bind/store/state 3단계 분할, UI 숏핸드 Tween 지원, existing-instance-bind 기각

세 건을 한 커밋에 처리:

1. ui-shorthand-plan.md — Tween 지원 확정. 숏핸드가 자식 프로퍼티를 직접
   대입하지 않고 Dispatch.process(child, prop, ..., 1)로 위임하면 Tween이
   공짜로 따라옴(해석 코드는 PropertyHandler 하나에만 남음). "process 중
   inst를 바꾸는 건 키를 바꾸는 것과 같은 층위라 UB 아님"을
   dispatch-core-plan.md에 일반 규칙으로 명문화. wrap을 Tween<T>.Value에만
   적용되도록 들어올리는 헬퍼가 새로 필요한 유일한 부품. ROADMAP M10에
   통째로 빠져 있던 UI 숏핸드 항목도 보강.

2. existing-instance-bind — 기각, research/ → archive/. 사유: Length/Offset
   등 quad가 만든 트리를 전제한 부기를 바깥에서 밀고 당기는 버그 표면이
   치명적으로 넓어짐. "열려 있음"을 전제로 쓰인 본문 7곳도 같이 정정
   (architecture.md의 "아직 미정" 절은 유일 항목이었어서 절 자체를 갱신).

3. bind-system-plan.md 3단계 분할 + store-semantics.md 흡수(순수 이동):
   - base/store-plan.md 신설 — Store = 이름 붙은 Source 모음
   - base/source-state-plan.md 신설 — 반응형 코어(Source⊇State, 전파 모델,
     :With/:Compute/:Apply/previous, Observer, 구독·생명주기 게이트)
   - bind-system-plan.md 1238→203줄(인스턴스 생성·이벤트 네이밍 + 색인)
   - store-semantics.md 삭제
   참조 40여 곳 스윕. doc-check.py ERROR 0, WARN 101→84.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
qwreey 2026-08-14 05:10:49 +09:00
parent 2d9cc7b5f7
commit 17a2e4f05f
Signed by: qwreey
GPG key ID: D28DB79297A214BD
39 changed files with 1925 additions and 1588 deletions

File diff suppressed because one or more lines are too long

View file

@ -75,7 +75,7 @@ if self.Connection then return self.Connection.Connected end
이 오류가 여섯 세션 넘게 살아남은 이유는 **`canExecute`의 실제 호출부가
어느 문서에도 코드로 등장한 적이 없기 때문**. `bind-system-plan.md`/
`store-semantics.md`/`slot-plan.md`는 전부 "발화 시 `canExecute`로 게이팅됨"
`source-state-plan.md`(당시 store-semantics.md)/`slot-plan.md`는 전부 "발화 시 `canExecute`로 게이팅됨"
같은 **서술만** 하고 넘어갔고, `dispatch-core-plan.md`는 아예
"핸들러가 직접 `canExecute`를 재구현할 필요 없음 — Observer가 이미 자기
`Subscribed` 상태로 게이팅됨"이라고 적어 호출부를 없는 것처럼 만들었음.

View file

@ -1,6 +1,32 @@
# 이미 생성된 인스턴스에 대한 바인드 (후순위, UB 또는 마일스톤)
# [기각됨] 이미 생성된 인스턴스에 대한 바인드
**상태**: research — 명시적으로 후순위/UB 후보. 원본:
> **⛔ [2026-08-14 세션, 사용자 확정 — 기각]** `research/`에서
> `archive/`로 이전. **더 이상 "열린 가능성"이 아니라 미지원으로 확정.**
>
> **기각 사유(사용자)**: 이게 가능하다고 하면 `Dispatch.setOffsetSource`/
> `setLength`(`base/dispatch-core-plan.md`의 "Length/Offset" 절) 같은,
> quad가 자기가 만든 트리에 대해서만 성립한다고 전제하고 세운 부기를
> 바깥에서 **밀고 당기는** 부가 작용이 전부 가능해짐 — 즉 기능 하나가
> 열리는 게 아니라 **버그를 일으키기에 치명적인 표면이 대량으로 노출**됨.
> 그 표면을 다 막으려면 quad가 "내가 만들지 않은 Instance의 자식
> 구성이 언제 어떻게 바뀌는지"까지 추적해야 하는데, 그건
> `research/framework-comparison-findings.md`가 use-after-destroy
> 안전망을 기각할 때 쓴 것과 같은 이유(Instance 가상화/추적은 rbvm 같은
> 전문 라이브러리의 영역, quad가 재발명하면 오버엔지니어링)로 스코프
> 밖임.
>
> 아래 원문은 기각 전 서술 그대로 보존(당시엔 "미정 유지"가 결론이었음).
> 관련해서 이미 지적돼 있던 긴장은 두 곳 — `research/pre-implementation-audit.md`
> 2-4(Slot의 "엄격한 단일 마운트 소유권" 불변식과 근본적으로 충돌),
> `base/architecture.md`의 "복사 구현 지양, store 바인드 변경은 전체
> 변경" 원칙. 둘 다 이번 기각으로 해소됨.
>
> **관련 기능이 필요해지면**: quad가 만들지 **않은** Instance를 다루는
> 정상 경로는 `Ref`(외부 관리 instance를 점진적으로 다루기 위한 직접 참조
> 획득, `base/ref-plan.md`)와 `Effect`(`base/effect-plan.md`)뿐 — 그
> 둘로 안 되는 걸 이 문서로 되살리려 하지 말 것.
**상태**: 기각됨(원래 `research/` — 명시적으로 후순위/UB 후보였음). 원본:
`.claude/initreq/raw-userinput.md` "이미 생성된 객체에 대한 바인드?" 절.
## 문제

View file

@ -311,7 +311,7 @@ context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만**
계산 끝난 값"으로 오해하기 쉬움 — Vue/Svelte 생태계에서는 lazy와
`computed`라는 이름이 공존해도 문제없지만, quad 안에서는 다름. 같은
이유로 `:Compute`(동사 원형) 메소드 이름도 `Computed`가 아니라
`Compute`인 게 맞다고 재확인(`base/bind-system-plan.md` "네이밍 —
`Compute`인 게 맞다고 재확인(`base/source-state-plan.md` "네이밍 —
`Compute``-ed`가 아닌 이유" 절).
- `Pipe` — 검토했으나 기각. (1) "캐시한다"는 동작이 파이프라는 비유와
안 맞음(파이프는 통과시키는 채널 이미지라 값을 들고 있다/캐시한다는
@ -351,7 +351,7 @@ context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만**
`base/slot-plan.md` "`Slot:List(...)`" 절.
- **[해소됨, 2026-08-09 세션]** `Bound`**`canBound(handle): boolean`
탑레벨 함수로 확정**, `canExecute`와 같은 결(raw 필드를 직접 노출하는
대신 predicate 함수로 감쌈). `base/bind-system-plan.md` "이중 바인딩
대신 predicate 함수로 감쌈). `base/source-state-plan.md` "이중 바인딩
금지" 절 참고.
- **`Brand`(3순위, 사소함, 2026-08-07 여덟 번째 세션 추가)**: 런타임
nominal 타입 판별 통합 메커니즘(`Brand.set`/`Brand.get`, `isState`
@ -422,7 +422,7 @@ context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만**
해소).
- **[해소됨, 2026-08-09 세션]** `State<Modifier>`와 Ref/Slot이 Modifier
필드에 들어가는 것 — 이제 둘 다 `isX` predicate 기반 명시적 `error`
통일(`base/modifier-plan.md` 4번/7번 절, `base/store-semantics.md`
통일(`base/modifier-plan.md` 4번/7번 절, `base/source-state-plan.md`
"따름정리" 절). Luau 타입 차단은 "되면 좋은 보너스"로 격하되어 더
이상 필수 검증 항목 아님 — 문서모순 절 + 우선순위2-2도 갱신 완료.
- ~~`props.Modifier`/`props.Ref` forwarding 관례가 Lua 배열 리터럴
@ -448,7 +448,7 @@ context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만**
이름이었음, 2026-08-13 다섯 번째 세션에 인덱스 기반으로 재설계되며 개명)로 이미
해소(`pre-implementation-audit.md` 1-2, `bind-system-plan.md` "Dispatch
체인" 절). **[해소됨, 2026-08-09 세션]** `:Compute``previous` 인자
오버엔지니어링 의심도 기각(`bind-system-plan.md` "previous" 절,
오버엔지니어링 의심도 기각(`source-state-plan.md` "previous" 절,
`pre-implementation-audit.md` 3-1). **[해소됨]** UI shorthand의 기존
UICorner 매칭 기준도 `base/ui-shorthand-plan.md`에 이미 확정 반영돼
있던 것을 이번에 `pre-implementation-audit.md` 2-11에도 해소 표시로
@ -495,8 +495,11 @@ context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만**
엘비스 연산자류) 후보 추가 — Haskell 비교 리서치 중 나옴, 카탈로그 확정
규칙에 그대로 맞아 포함 근거는 있음. 상세는 `research/operator-sugar-plan.md`.
구현 자체는 맨 마지막 우선순위(순수 슈가, 없어도 무방) — 여전함.
- `research/existing-instance-bind-plan.md` — 스코프 논의만 필요, 구현
착수를 막지 않음.
- **[해소됨(기각), 2026-08-14 세션]** 이미 생성된 인스턴스 재바인드 —
"스코프 논의만 필요"로 오래 열려 있었으나 사용자 확정으로 **기각**,
`archive/existing-instance-bind-rejected.md`로 이전(사유: Length/Offset
등 quad가 만든 트리를 전제한 부기를 바깥에서 밀고 당기는 버그 표면이
치명적으로 넓어짐).
- **[해소됨, 2026-08-13 세 번째 세션]** v1 `objectListClass.__newIndex` 오타
기능(재현 테스트 필요했던 항목) — 사용자가 당시 실수였음을 확인. v2는 이제
오브젝트에 id를 주입하고 id로 조회하는 개념(`GetObjects`류) 자체가 없어져
@ -565,7 +568,7 @@ context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만**
|---|---|
| 전체 아키텍처 결정(디스패치 모델, DOMless, 태그/Ref, Signal 미채택 등) | `base/architecture.md` |
| Store/State/Source 온톨로지, 인스턴스 생성/이벤트 인체공학, Ref, 남은 API 이름 | `base/bind-system-plan.md` |
| Store 부작용 허용, `:With`+`:Compute`, dot-access 문법 | `base/store-semantics.md` |
| Store 부작용 허용, `:With`+`:Compute`, dot-access 문법 | `base/store-plan.md` |
| 프로바이더 패턴, bind/store 구현 책임 분리 | `base/module-lifecycle-plan.md` |
| Slot 재조정, 재마운트 시 throw, **[2026-08-13 6번째 세션 역전] retract=언마운트**(파괴 아님, portal이 그 귀결 — 옛 "retract=폐기"는 뒤집힘) | `base/slot-plan.md` |
| `Connected`+GC 라이프사이클 패턴 | `base/lifecycle-pattern.md` |

View file

@ -2,7 +2,7 @@
**역전 일시**: 2026-08-06 (세 번째 세션). **원 확정 일시**: 2026-08-04
(`component-composition-plan.md` 컴포넌트화 논의 3~4번 항목).
**현재 유효한 설계**: `base/store-semantics.md`의 "Source가 State를
**현재 유효한 설계**: `base/source-state-plan.md`의 "Source가 State를
만족함 — 구조적 서브타입" 절이 최종 소스. 이 파일은 더 이상 능동적으로
참고할 필요 없음(구현에 안 씀) — 왜 이 경로를 갔다가 되돌아왔는지가
`quadnomicon`(프레임워크 설계자용 심화 콘텐츠, `research/documentation-plan.md`
@ -69,5 +69,5 @@ State를 만족**(Svelte `Writable<T> extends Readable<T>`와 같은 모양)
"별도 프록시 타입을 만든다"에서 "Source 자체를 서브타입으로 승격한다"로
바뀐 것. 이 반전 자체가 "타입이 없던 v1 습관을 재검토 없이 typed 재작성에
그대로 가져오면 안 된다"는 더 큰 교훈의 구체적 사례이기도 함(사용자 회고,
`base/store-semantics.md` 참고) — `quadnomicon`에서 "설계가 왜 이렇게
`base/source-state-plan.md` 참고) — `quadnomicon`에서 "설계가 왜 이렇게
반전됐는가" 사례로 쓰기 좋음.

View file

@ -95,7 +95,7 @@ relate4의 살아있는 엔트리 총 개수: 5 (기대 5)
relate4의 살아있는 엔트리 총 개수: 0 (기대 0)
```
**`base/bind-system-plan.md` "왜 GC-안전한가"`base/relate-plan.md`
**`base/lifecycle-pattern.md`(GC-native 관용구)`base/relate-plan.md`
전체가 기대고 있는 전제가 실측 확인됨** — quad의 GC-native 아키텍처
(명시적 Destroy 강제 없음, `bindLifetime`으로 매달아둔 자원이 inst와 함께
자동 소멸)가 실제로 성립함.

View file

@ -197,8 +197,10 @@ quad/
**남은 것**: Slot 코어 로직의 정확한 API(`research`→`base` 승격된
`slot-plan.md` 참고)와 각 파일의 정확한 함수/타입 이름은 구현 단계에서.
existing-instance-bind는 여전히 `research/`에 남아있고 이 구조 확정을
막지 않음(`purity-and-effects-plan.md`/`tween-plan.md`는 이미 `base/`로 승격 완료).
existing-instance-bind는 **[2026-08-14 세션] 기각되어 `archive/`
이전**됐고(`archive/existing-instance-bind-rejected.md`), 애초에 이 구조
확정을 막던 항목도 아니었음(`purity-and-effects-plan.md`/`tween-plan.md`는
이미 `base/`로 승격 완료).
## 코드 스타일 — 네이밍 케이싱 (2026-08-08 두 번째 세션 신설)
@ -238,7 +240,7 @@ existing-instance-bind는 여전히 `research/`에 남아있고 이 구조 확
타입의 정적 결합 함수), 아니면(범용 유틸이거나 프리미티브가 아닌 엔진
소속) 소문자. `Dispatch`/`Brand`가 프리미티브가 아닌 이유는
`base/dispatch-core-plan.md`의 "Dispatch는 프리미티브가 아니다" 절/
`base/store-semantics.md`의 "세 번째 카테고리 — Handler" 절 참고.
`base/source-state-plan.md`의 "일반 원칙 — 독립 존재 가능한 프리미티브 vs 원천에 종속된 파생 데이터" 절(세 번째 카테고리 문단) 참고.
## 코드 스타일 — Luau 문법 관례: `if-then-else`/`const` (2026-08-12 세션 신설)
@ -309,7 +311,7 @@ Store 생성 시 미리 만들어둔 경우), 아직 없으면 그 자리에서
"Store 생성 시 전부 eager하게만 만들어진다"로 한 차례 더 정리됐다가, Luau
타입이 런타임에 강제되지 않아 defaults 없이 만든 키를 나중에 `:Set()`하면
크래시난다는 점이 지적돼 lazy `__index`+저장 생성도 같이 필요함이 확인됨 —
상세는 `base/store-semantics.md` "Source가 State를 만족함" 절). 전파는
상세는 `base/source-state-plan.md` "Source가 State를 만족함" 절). 전파는
push-invalidate(신호만)/
pull-recompute(`Get()` 시점) — Fusion식 eager 노드 없이도 다이아몬드
의존성 중복 재계산 문제가 풀림. State는 쓰기 대상이 아니고, 값을 쓰는
@ -319,14 +321,18 @@ pull-recompute(`Get()` 시점) — Fusion식 eager 노드 없이도 다이아몬
다룰 땐 Store와 별개인 가벼운 `Source` 프리미티브를 독립적으로도 씀.
`store.key` dot-access를 타입 추론 1급 경로로 삼는 것도 3차 라운드에서
정식 확정됨 — **더 이상 열린 질문 아님**, 남은 건 정확한 API 이름뿐.
상세는 `base/store-semantics.md`의 "Source가 State를 만족함" 절과
`base/bind-system-plan.md`의 "Store/State/Source 온톨로지" 절 참고.
상세는 `base/source-state-plan.md`의 "Source가 State를 만족함"/"핵심
온톨로지" 절 참고.
## 아직 미정 (research/로 분리됨)
이미 생성된 인스턴스에 대한 바인드 — `.claude/research/existing-instance-bind-plan.md`
참고, 전체 색인은 `.claude/README.md`. 바인드 디스패치/Slot/모듈
**[2026-08-14 세션] 이 절에 유일하게 남아있던 항목(이미 생성된 인스턴스에
대한 바인드)이 기각되어 `archive/existing-instance-bind-rejected.md`
이전됨** — 지금 `research/`에 남은 것은 전부 "착수 시점 미정"이지
아키텍처를 미정으로 남기는 항목이 아님. 전체 색인은 `.claude/README.md`.
바인드 디스패치/Slot/모듈
라이프사이클/Modifier/컴포넌트화(컴포넌트 경계 modifier/Ref 전달 포함)는
위 "구현 착수" 섹션대로 확정되어 `.claude/base/`로 승격됨
(`bind-system-plan.md`/`dispatch-core-plan.md`/`module-lifecycle-plan.md`/
(`bind-system-plan.md`/`dispatch-core-plan.md`/`source-state-plan.md`/
`store-plan.md`/`module-lifecycle-plan.md`/
`slot-plan.md`/`modifier-plan.md`/`component-composition-plan.md`).

View file

@ -34,8 +34,8 @@ Store 여러 개를 한 번에 attribute로 묶어 바인드하는 그룹 `Attri
이미 확정. UICorner 숏핸드/Tween처럼
별도 전용 문서가 없던 걸 2026-08-07 여덟 번째 세션에 메꿈("1 프리미티브 1
파일" 관례를 Tag/Attribute에도 적용해야 한다는 사용자 지적) —
`bind-system-plan.md`"Attribute 특수 키 — 타입 파라미터화" 절(2026-08-06
신설) 내용을 그대로 옮기고, 논의한 `None`/`process`/`retract` 동작을 추가.
`bind-system-plan.md`Attribute 특수 키/타입 파라미터화 절(2026-08-06
신설, 지금은 이 문서로 옮겨져 그쪽엔 색인만 남음) 내용을 그대로 옮기고, 논의한 `None`/`process`/`retract` 동작을 추가.
## 단일 키 — `AttributeKey<<T>>` (구 `Attribute<<T>>`)
@ -262,7 +262,7 @@ Store 필드 여러 개를 각각 `[AttributeKey<<T>> "name"] = store.name`으
- **`Attribute`를 Store의 서브타입/확장으로**: Attribute가 Store를
상속(IS-A)하면 `Store<T>``T`가 다시 Attribute(=Store)일 수 있게
되어, 이미 확정된 제약("핸들러 계층 값은 Source에 못 들어감" —
`store-semantics.md`의 `Store<T>``T`는 Modifier 불가 규칙과 같은
`base/source-state-plan.md`의 `Store<T>``T`는 Modifier 불가 규칙과 같은
이유)과 부딪히는 "Store 안에 Store"를 실제로 만들어냄. Attribute는
Store를 **참조(HAS-A)**만 해야지 **상속(IS-A)**하면 안 됨 — 기각.
@ -362,7 +362,8 @@ end
이름은 그냥 재등록이 안 될 뿐. 별도 diff 분기가 없음.
- **값 비교(`:Get()`으로 old/new 비교)는 안 함** — State
계약("값은 항상 선언된 Compute 재실행 결과, 캐시 비교 금지",
`store-semantics.md` "하드 경계" 절)과 어긋나고, `source`
`base/source-state-plan.md`의 "Source 값을 직접 mutate한 뒤 전파 — `:Emit()`"
절 "하드 경계" 문단)과 어긋나고, `source`
`State`/`Source`면 `Dispatch/StoreBind`가 알아서 언랩+구독까지 다
해줌(그룹 Handler가 따로 구독 관리 안 함)이라 굳이 비교할 이유가 없음.
- **[확정, 2026-08-12 세션 후속, 사용자 결정] 클로저는 `setAttribute`

File diff suppressed because it is too large Load diff

View file

@ -17,9 +17,8 @@
위험을 구조적으로 우회한다.
**store 개발(M3)과 밀접하게 연관됨** — `state:Block(blocker)`가 State
위에 얹히는 메소드이므로 `base/store-semantics.md`의 Store/State/Source
온톨로지, 특히 push-invalidate/pull-recompute 전파 모델(`base/
bind-system-plan.md` "전파 모델 확정" 절)을 전제로 함. 별도 파일로 두되
위에 얹히는 메소드이므로 `base/source-state-plan.md`의 Source/State
온톨로지, 특히 push-invalidate/pull-recompute 전파 모델(`base/source-state-plan.md` "전파 모델 확정" 절)을 전제로 함. 별도 파일로 두되
State와 같은 마일스톤(`ROADMAP.md` M3)에서 함께 구현할 것.
## 메커니즘 (확정)
@ -45,7 +44,7 @@ gated state의 동작:
**`:Get()`엔 영향 없음** — 블록은 emit **전파**만 지연시킨다. 블록 중이라도
누군가 명시적으로 `:Get()`하면 그 순간의 실제 값을 정상적으로 계산해서
준다 — `store-semantics.md`의 "`Get()`은 라이브 레퍼런스를 준다" 원칙과 일치.
준다 — `base/source-state-plan.md`의 "Source 값을 직접 mutate한 뒤 전파 — `:Emit()`" 절("`Get()`은 라이브 레퍼런스를 준다" 캐비엇)과 일치.
## 사용 예시

View file

@ -67,7 +67,7 @@ Svelte `Writable<T> extends Readable<T>`와 같은 모양), `store.key`는 Store
쓰기 가능"이라는 위 2번 규칙과도 자연히 맞아떨어짐). 쓰기 문법도 같이
바뀜: `store.key = v`가 아니라 `store.key:Set(v)`(레코드 타입 읽기/쓰기
대칭 + lazy 동작에 `=`가 암시하는 "즉시 커밋"이 안 맞는다는 논거). 상세
근거·타입 설계·Luau 솔버 검증 필요 항목은 `base/store-semantics.md`의
근거·타입 설계·Luau 솔버 검증 필요 항목은 `base/source-state-plan.md`의
"Source가 State를 만족함" 절이 최종 소스.
**[이전에 확정했다가 폐기된 `StoreSource` 프록시 설계는 이 결론으로

View file

@ -721,6 +721,22 @@ end
전제하므로(quad 자신이 "props 순회 순서" 절에서 이 관례에 의존), 0을
쓰면 그 항목이 `ipairs` 순회에서 조용히 빠지고 `quad-debug`가 나중에
`chains`를 그대로 순회해서 보여주려는 계획과도 부딪힘.
- **위임 대상은 다른 `k`뿐 아니라 다른 `inst`여도 됨 — UB 아님
(2026-08-14 세션, 명시화).** `chains``(inst,k)` 쌍으로 인덱싱되므로
`(inst,k1)`을 처리하던 핸들러가 `(inst,k2)`로 위임하는 것과
`(child,k2)`로 위임하는 것은 Dispatch 입장에서 **구조적으로 완전히 같은
일**임(둘 다 별개의 새 배열, 그래서 둘 다 인덱스 `1`부터). 즉 핸들러가
**자기가 관리하는 자식 Instance를 먼저 만들거나 찾아둔 뒤 그 자식에
대해 `Dispatch.process(child, prop, v, 1)`을 부르는 패턴은 정상**이고,
이게 `base/ui-shorthand-plan.md``UICorner`/`UIPadding`/`UIScale`
숏핸드가 Tween을 공짜로 얻는 방식임(그 자식 프로퍼티를 최종 처리하는
`PropertyHandler`이고, Tween 해석은 원래 거기 하나에만 있음 —
`base/tween-plan.md`). 단 **그 자식의 수명은 위임한 핸들러가 책임진다**
— Dispatch는 `(child,prop)` 체인이 누구 소유인지 모르므로, 자식을
없앨 때 `retractFrom(child, prop, 1)`까지 부르는 건 위임한 쪽 몫
(자식 Instance 자체를 버리면 `chains``inst`로 weak-keyed라 결국
GC되지만, 실행 중인 Tween/구독처럼 즉시 끊어야 하는 게 있으면 명시적
정리가 필요).
- **`handler.process(inst,k,v,index)``Dispatch.process`를 거치지 않고
직접 호출하는 것은 UB — 반드시 `Dispatch.process`를 통해서만 진입할
것.** 이유: 핸들러 비교·`chains` 저장 bookkeeping이 `Dispatch.process`
@ -767,11 +783,15 @@ end
`State<State<State<...>>>`도 인덱스가 늘어날 뿐 정상 동작하고, 위
"깊은 체인에서도 힌트가 안 사라짐" 항목대로 **깜빡임 방지 최적화까지
정상 작동**함 — 유일하게 남는 UB는 위 "순환" 항목.
- **부수 효과 — 미래 재바인드/quad-debug에 유리**: 이 체인이 Dispatch에
중앙화돼 있으므로, `research/existing-instance-bind-plan.md`가 다룰
미래의 재바인드는 `Dispatch.process(inst, k, newV, 1)` **한 줄**로 "이
키의 체인을 새 값에 맞춰 갈아 끼우기"가 됨(옛 모델에선 `retractFrom` +
`process` 두 줄이었음 — 하강 diff가 그 선행 철거를 흡수). 완전 해제만
- **부수 효과 — quad-debug에 유리**: 이 체인이 Dispatch에 중앙화돼
있으므로, 임의 시점의 재바인드도 `Dispatch.process(inst, k, newV, 1)`
**한 줄**로 "이 키의 체인을 새 값에 맞춰 갈아 끼우기"가 됨(옛 모델에선
`retractFrom` + `process` 두 줄이었음 — 하강 diff가 그 선행 철거를
흡수). **[2026-08-14 세션]** 이 문장이 원래 근거로 들던 "미래의
existing-instance-bind"는 기각됐지만
(`archive/existing-instance-bind-rejected.md`), 여기서 말하는 성질은
quad가 **자기가 만든** 인스턴스의 store 재발행에서 매번 쓰는 그 경로
자체라 그대로 유효. 완전 해제만
원하면 `Dispatch.retractFrom(inst, k, 1)`. `research/debug-tooling-plan.md`
"무엇이 무엇에 연결됐는가" 그래프도 이 `chains` 구조를 그대로 읽으면 됨 —
`handler`가 슬롯에 같이 저장되므로 "이 자리를 지금 누가 담당하는가"를
@ -1011,7 +1031,7 @@ lazy 생성.
**이 케이스를 명시적으로 UB로 명명(2026-08-11 세션, 사용자 제안)** —
`Source<T>``State<T>`를 "단방향"으로만 만족한다는 이미 확정된 원칙
(`base/store-semantics.md` "Source가 State를 만족함" 절 — 파생값이
(`base/source-state-plan.md` "Source가 State를 만족함" 절 — 파생값이
자기 upstream Source로 거꾸로 쓰기를 하지 않는다는 것)과 **같은 카테고리의
위반**이라는 게 근거: `recompute`가 만드는 `offset`/`Length`는 전부
`lengthList`(그 Slot의 upstream 입력)에서 파생된 다운스트림 값인데,
@ -1173,7 +1193,7 @@ value)로 `Dispatch.process(inst,k,realv,index+1)`를 재귀 호출"하는 식
**"값이 바뀔 때마다"의 실제 구독 메커니즘 = `state:Observer(fn)` 재사용으로
확정(2026-08-08 세션).** 이전엔 이 절이 구독 메커니즘 자체를 추상적으로만
서술했는데(새 프리미티브를 발명하는 것처럼 읽힐 수 있었음), 실제로는
`base/bind-system-plan.md`의 "`state:Observer(fn)`" 절에서 이미 확정된 것을 그대로 재사용하면 됨 — 새
`base/source-state-plan.md`의 "`state:Observer(fn)`" 절에서 이미 확정된 것을 그대로 재사용하면 됨 — 새
구독 primitive를 store-bind 전용으로 따로 만들 이유가 없음:
```lua

View file

@ -47,10 +47,10 @@ leaf가 죽을 때 **마지막 cleanup을 한 번 더 호출**. 결과적으로
- **다수 의존성은 `:With(...)`로 먼저 하나의 State로 묶어서 넘길 것**
React식 별도 deps 배열을 새로 만들지 않음, quad가 이미 가진 다중 의존성
결합 관용구(`base/bind-system-plan.md` "`:With` + `:Compute`" 절)를
결합 관용구(`base/source-state-plan.md` "`:With` + `:Compute`" 절)를
그대로 재사용해 같은 일 하는 두 번째 경로를 안 만듦. **`Effect(fn, a, b,
c)`처럼 trailing args로 바로 받는 sugar는 의도적으로 안 만듦**(2026-08-11
세션, `bind-system-plan.md` "`:Compute(fn, ...)` — 추가 의존성을 trailing
세션, `source-state-plan.md` "`:Compute(fn, ...)` — 추가 의존성을 trailing
args로 직접 받는 sugar" 절 참고) — `Compute`와 달리 Effect/Observer는
자기 자신이 결과를 담는 State 노드가 아니라서, 의존성이 둘 이상이면 그걸
합칠 **새 노드**(`:With`가 만드는 것)가 실제로 필요함. 그 비용을 sugar로
@ -162,7 +162,7 @@ quad의 반응형 그래프/cleanup 인체공학만 재사용하는 경우)로
규칙과 `canExecute(value)` 기반 즉시-에러 메커니즘(구 가칭 `Bound`
플래그 → 2026-08-09 세션에 `canBound`로 명명 → **2026-08-14 세 번째
세션에 `canBound` 폐기, `canExecute`로 통합**)은
`base/bind-system-plan.md`의 "이중 바인딩 금지" 절 참고. **[정정,
`base/source-state-plan.md`의 "이중 바인딩 금지" 절 참고. **[정정,
2026-08-09 여섯 번째 세션] leaf 부착 후 조기 해제는 `:Unsubscribe()`
아니라 `unbindLifetime(value)`** — leaf 부착 자체가 내부적으로
`bindLifetime(inst, value)` 호출이라, 그 해제도 짝인 `unbindLifetime`

View file

@ -106,7 +106,7 @@ RenderStepped/마우스 무브처럼 안 쓸 때 Connection을 살려두는 것
낭비인 경우)나, 단순 on/off가 아니라 로직 자체가 바뀌는 드문 케이스.
자주 재계산되는 State에 이벤트를 직접 물리면 매 재계산마다 Disconnect+
Connect가 도는 숨은 churn 비용도 있음(Store Set은 dedup 안 함,
`store-semantics.md`) — 그래서 남용하지 말라는 캐비엇.
`base/store-plan.md`) — 그래서 남용하지 말라는 캐비엇.
**그래도 일관성 있게 지원은 해둠.** "저빈도엔 필요 없다"가 "그러니 예외로
빼고 못 하게 막자"로 이어질 이유는 없음 — 프로퍼티/태그/어트리뷰트가

View file

@ -245,7 +245,7 @@ local InstData = Relate() -- inst -> gchold/gcconn (위 (0)에서 채워짐)
local BindData = Relate() -- value -> gchold/gcconn (bindLifetime이 채움)
function bindLifetime(inst, value)
-- 이중 바인딩 금지(base/bind-system-plan.md) — 게이트가 곧 canExecute.
-- 이중 바인딩 금지(base/source-state-plan.md) — 게이트가 곧 canExecute.
-- "지금 실행 가능하다"는 곧 "이미 유효한 바인딩을 갖고 있다"는 뜻.
if canExecute(value) then
-- 어느 경로로 묶여있는지만 메시지에 실어줌. `.Subscribed`를 무조건
@ -308,7 +308,7 @@ end
#### (2) 전역 경로 — `:Subscribe()`/`:Unsubscribe()`
`inst`에 안 묶이는(모듈 최상위 디버그 print류) Observer/Effect 전용. 상세
규칙과 경고는 `base/bind-system-plan.md`의 "`:Subscribe()`/`:Unsubscribe()`"
규칙과 경고는 `base/source-state-plan.md`의 "`:Subscribe()`/`:Unsubscribe()`"
절이 소스이고, 여기선 `canExecute`가 보는 상태만 못박음:
```lua
@ -336,7 +336,8 @@ end
강참조 루트(생존 보장), 필드는 `canExecute`가 매 발화마다 읽는 O(1) 경로 +
에러 메시지에서 "전역이냐 leaf냐"를 가르는 판별자. 둘은 항상 같이
쓰고 같이 지우는 한 세트(`:Unsubscribe()`가 필드만 내리고 테이블을 안
비우면 반쪽짜리 해제가 됨 — `bind-system-plan.md`에 이미 확정된 규칙 그대로).
비우면 반쪽짜리 해제가 됨 — `base/source-state-plan.md`에 이미 확정된 규칙
그대로).
#### (3) `canBound` 폐기 — 게이트는 `canExecute` 하나
@ -381,8 +382,8 @@ Instance 생성 시 한 번만 만들어지고, 이후는 `InstData:GetWeak`으
quad-roblox 구현 단계에서 실측 확인 대상 — 문제가 되면 gcconn을 `value`
직접 필드로 내리는 선택지가 있음(옛 초안이 `self.Connection`으로 스케치했던
모양). 지금 `Relate` 쪽으로 둔 이유는 "Observer 값 자체에 부작용을 안
남기고 외부 weak 인덱싱을 선호"라는 기존 사용자 방침(`base/bind-system-plan.md`의
`state:Observer(fn)` 절 구현 노트)이고, 성능 근거가 나오면 뒤집어도 되는
남기고 외부 weak 인덱싱을 선호"라는 기존 사용자 방침(`base/source-state-plan.md`의
"`state:Observer(fn)`" 절 구현 노트)이고, 성능 근거가 나오면 뒤집어도 되는
순수 구현 세부.
이건 `base/dispatch-core-plan.md`의 "핸들러 내부 상태 저장" 유틸(`Relate`
@ -405,11 +406,10 @@ connection을 얻어 `disconnect()`하는 명시적 dispose 경로를 추가로
**재사용 사례(2026-08-04 2차 라운드)**: Store/State의 무효화(invalidate)
신호를 받는 리스너 클로저도 정확히 이 유틸로 등록됨 — `base/
store-semantics.md`가 예전에 "state 옵저빙 결과로 slot을 조작할 때 생존
store-plan.md`가 예전에 "state 옵저빙 결과로 slot을 조작할 때 생존
여부를 어떻게 확인할지" 미해결로 남겨뒀던 문제가, 사실은 새 메커니즘이
필요한 게 아니라 이 canExecute 게이트를 그대로 적용하면 되는 사례였음(별도
`isInit` 분기 불필요). 상세는 `base/bind-system-plan.md`의 "Store/State/
Source 온톨로지" 절 참고.
`isInit` 분기 불필요). 상세는 `base/source-state-plan.md`의 "Slot 생존 확인" 절 참고.
## 2026-08-04 검증 라운드에서 보강된 내용

View file

@ -40,10 +40,11 @@ cascade 문제가 그대로 오는데, 이건 이미 확정된 "Store 바인드
"필수 관용구" 절 참고).
관련: 이미 마운트된 Instance에 재바인드할 때 Default→실값 flatten을 다시
해야 하는지/clone이 필요한지는 별개 미정 문제로
`research/existing-instance-bind-plan.md`의 "Default 값과 얽히는 문제" 절
참고 — 여기서 다루는 건 컴포지션 타임의 modifier 값 자체 flatten이라 층위가
다름.
해야 하는지/clone이 필요한지는 별개 문제였는데, **[2026-08-14 세션] 그
재바인드 기능 자체가 기각**되어 질문이 없어짐
(`archive/existing-instance-bind-rejected.md`의 "Default 값과 얽히는 문제"
절이 당시 서술) — 어차피 여기서 다루는 건 컴포지션 타임의 modifier 값 자체
flatten이라 층위가 달랐음.
**참고 — Property(일반 프로퍼티)에 Attribute식 "이름 소유권 레지스트리"를
적용하는 안은 검토 후 기각(2026-08-12 열일곱 번째 세션).** `Attribute`
@ -142,7 +143,7 @@ mutable하게 구현하면 같은 modifier 레퍼런스를 공유하는 형제
`Store({defaults})`와 같은 "`Type(args)` 팩토리" 관습을 그대로 적용하면
됨, Modifier는 초기 필드가 필수가 아니므로 `args`가 비어도 되는
`Modifier()`. `mod:FontSize(20)`처럼 체이닝하는 모든 예시가 실은 이
`Modifier()`가 만든 빈 인스턴스 위에서 시작함. `base/store-semantics.md`
`Modifier()`가 만든 빈 인스턴스 위에서 시작함. `base/source-state-plan.md`
"독립 존재 가능한 프리미티브" 절의 예시 목록도 이걸로 갱신.
### 4. Setter는 리터럴 값과 변환 함수 둘 다 받음, 별도 Getter는 없음
@ -308,7 +309,7 @@ Modifier에는 없음).
"누가 modifier에 타입을 붙여주냐"는 새 문제가 아니라, Store/인스턴스 생성에
이미 적용한 "정적으로 알려진 건 dot-access, 동적인 건 문자열 폴백" 프로젝트
전역 관습(`base/bind-system-plan.md` "타입 추론 문제" 절)을 그대로 적용하면
전역 관습(`base/store-plan.md` "타입 추론 문제" 절)을 그대로 적용하면
됨 — `mod:UICorner(8)`/`mod:FontSize(...)`처럼 DI 쪽 "제네릭 생성자 함수
하나 + 자주 쓰는 것만 정적 필드로 미리 바인딩" 패턴 재사용.
(주의: 이벤트는 이 관습의 유일한 예외라 인용 대상에서 제외 — 이벤트 바인딩은
@ -379,7 +380,7 @@ predicate(`Brand` 절)를 State/Source 쪽에도 적용해 **런타임에 직접
분류. State/Source에 `Tween<T>`가 담기는 것 자체는 여전히 문제없이
허용되지만(위 타입 대수 절 참고), 그 이유는 "재귀 dispatch 참가자라서"가
아니라 "그냥 raw 값이라서"로 바뀜.
- **`Store<T>``T`는 Modifier가 될 수 없음(`base/store-semantics.md`
- **`Store<T>``T`는 Modifier가 될 수 없음(`base/source-state-plan.md`
"따름정리" 절)도 이 결정을 그대로 물려받음** — Source가 State를
구조적으로 만족하므로 별도로 다시 논증할 필요 없이 동일하게 적용됨.

View file

@ -56,7 +56,7 @@ init하려 하면 오류, 없는데 뭔가 생성해서 bind하려 해도 오류
부수적으로 확인된 것:
- **Store 자체의 연산은 더 단순해져도 됨** — v1의 `:Add`/`:With`/`:Tween` 같은
이름 붙은 체이닝 연산(named modifier)은 명시적으로 안 만들기로 확정, 대신
일반 함수를 받는 형태로 통일(`base/store-semantics.md` 참고). "너무 verbose한
일반 함수를 받는 형태로 통일(`base/source-state-plan.md` 참고). "너무 verbose한
연산들은 오히려 일관성을 해친다"는 게 이유. (주의: 아래의 v2 `:With(...)`
이름만 같을 뿐 여기서 안 만들기로 한 v1의 `:With`와는 다른 연산임 — v1은
"함수/테이블에서 값을 가져오는" 가공 연산이었고, v2는 그냥 "여러 State를
@ -64,7 +64,7 @@ init하려 하면 오류, 없는데 뭔가 생성해서 bind하려 해도 오류
- **여러 store 값을 묶어 유연하게 처리하는 방법**(`useEffect`류 dependency
array)은 있으면 좋겠다는 요청이었고 — **API 시그니처도 확정됨**:
`:With(...)`로 의존성을 모으고 `:Compute(fn)`으로 파생 State를 만드는
형태, 상세는 `base/store-semantics.md`의 "여러 스토어 값을 묶어 처리하는
형태, 상세는 `base/store-plan.md`의 "여러 스토어 값을 묶어 처리하는
것" 절 참고.
- `can execute store bind` 후킹 자체는 `Connected` 계산 속성으로 대체된다는
잠정 제안이 그대로 유지되고, 여기에 더해 **완전 소멸(Destroy) 시점엔 아무

View file

@ -36,7 +36,7 @@
## 문서화 방향
- `base/store-semantics.md`("Store는 부작용을 허용하는 게 기본 디자인")와
- `base/store-plan.md`("Store는 부작용을 허용하는 게 기본 디자인")와
같은 결의 문제 — Store 자체의 부작용 허용 여부와는 별개로, **컴포넌트가
"자기 입력 밖의 상태"에 의존하면 이식성이 깨진다**는 원칙을 문서에 별도로
명시.

View file

@ -84,7 +84,7 @@ Instance를 직접 받으므로 — `base/dispatch-core-plan.md` "확정된 디
못 씀, 그래서 콜백도 같이 필요) **세 메소드로 확정(2026-08-07 여섯 번째
세션)**. `:Set`/`:Callback`/`:Wait` 전부 **mutation 패턴이라 자기 자신
(`Ref<T>`)을 반환** — `store.key:Set(value)`류 "값을 바꾸는 연산엔 `:`
체이닝 허용" 원칙(`base/store-semantics.md` 190행)의 자연스러운 재적용.
체이닝 허용" 원칙(`base/store-plan.md`의 "Store 값 설정 문법" 절)의 자연스러운 재적용.
이 self-반환 덕에 Luau의 `if`-표현식과 결합해 흔한 관용구를 한 줄로
쓸 수 있음(사용자 제시 예):
```luau
@ -486,10 +486,13 @@ flatten된 값은 해시 파트(프로퍼티 키)로 존재하게 되고, Store
다른 뜻으로 쓰이는 `Dispatch.process(inst,k,v)` 오케스트레이터와 겹쳐서
안 좋음). **`flatten(nonFlatten) -> flatten` 함수 자체에 얹는 방안은
검토 후 기각** — flatten은 Modifier 값을 합치는 순수 변환(현재 `inst`
안 받음, `research/existing-instance-bind-plan.md`가 다루는 "이미
마운트된 Instance 재바인드 시 Default→실값 flatten을 다시 해야 하는가"
질문이 실제로 열려있어 flatten이 한 인스턴스 생애주기 동안 **여러 번
재호출될 가능성이 있음** — 여기에 PreRef fire를 얹으면 재바인드마다
안 받음). 원래 근거는 "`archive/existing-instance-bind-rejected.md`가
다루던 '이미 마운트된 Instance 재바인드 시 Default→실값 flatten을 다시
해야 하는가' 질문이 열려있어 flatten이 한 인스턴스 생애주기 동안
**여러 번 재호출될 가능성이 있다**"였고, **[2026-08-14 세션] 그
재바인드 기능 자체가 기각되며 이 위험은 사라졌지만 결론(기각)은
유지** — flatten은 여전히 `inst`를 모르는 순수 변환이라 fire 지점으로
부적절함 — 여기에 PreRef fire를 얹으면 재바인드마다
PreRef가 또 fire되어 "이 인스턴스 하나의 construction 훅"이라는 PreRef의
정의 자체가 깨짐. `Dispatch.drive`는 최초 마운트 시 정확히 한 번만
불리는 게 이미 전제라 이 위험이 없음.

View file

@ -51,9 +51,12 @@ Destroy 전까지 userdata 동일성을 고정해주므로, 모든 `inst`-키 `R
시점에 무조건 실행된다**(옛 lazy 생성에서 이번에 전환된 이유).
**따름 정리 — quad 바깥에서 온 Instance를 `Relate` 키로 쓰는 건 UB.** quad가
만들지 않은 Instance(`research/existing-instance-bind-plan.md`가 다루는
영역)는 이 셋업을 안 거쳤을 수 있으므로, 그 스코프를 열 때 "키로 쓰기 전에
gcconn 셋업을 먼저 건다"를 같이 설계해야 함.
만들지 않은 Instance는 이 셋업을 안 거쳤을 수 있으므로, 그런 스코프를
여는 설계를 한다면 "키로 쓰기 전에 gcconn 셋업을 먼저 건다"를 같이 설계해야
함. **[2026-08-14 세션]** 이 UB를 실제로 건드릴 뻔했던 유일한 기능(이미
생성된 인스턴스 재바인드)은 기각됐으므로
(`archive/existing-instance-bind-rejected.md`) 지금 열려 있는 경로는 없음
— 이 따름 정리는 앞으로 비슷한 제안이 나올 때의 판단 기준으로만 유지.
## 일반 규칙 — 다른 곳에서 안전하게 유지되는 것은 항상 `SetWeak` (2026-08-14 다섯 번째 세션)

File diff suppressed because it is too large Load diff

224
.claude/base/store-plan.md Normal file
View file

@ -0,0 +1,224 @@
# Store — 이름 붙은 Source 모음, 그 이상 아님
> **📄 [2026-08-14 신설] `bind-system-plan.md` 3단계 분할 + store-semantics.md
> 흡수.** Store가 "Source들을 담고, 없으면 만들어주는 도구"로 좁혀지고 나서도
> 관련 서술이 store-semantics.md(부작용 허용, 값 설정 문법)와
> `bind-system-plan.md`(dot-access 타이핑, Store가 Store를 담는가)에 반씩
> 흩어져 있었음 — 한 군데로 합쳤고 **내용/결정은 이동·병합 자체로는 안
> 바뀜**. 반응형 코어(Source/State 자체)는 짝 문서
> **`base/source-state-plan.md`**.
**상태**: base — Store가 부작용을 허용한다는 핵심 결정, "이름 붙은 Source
모음"이라는 정의, eager+lazy 생성, `store.key` dot-access 타이핑, `:Set()`
문법 전환까지 전부 확정. 원본: `.claude/initreq/raw-userinput.md`
"store는 부작용을 허용함" / "스토어는 스토어를 저장 가능한가" 절.
## Store는 부작용을 허용하는 게 기본 디자인
부작용 없이(파라메터 패싱만으로) 쓰는 것도 물론 가능하지만, 라이브러리 차원에서
막지 않는다. 부작용 유무는 **사용자가 직접 문서화**하는 관례로 둔다 — 라이브러리가
순수성을 강제하지 않음.
다만 한 가지는 명확히 구분: **렌더 리턴 위에서 무언가를 observe하는 것은 그냥
부작용**이다 (`useEffect`와 유사한 것으로 문서화). 이건 "허용되는 부작용"이 아니라
"당연히 부작용"이라는 뜻 — 문서화 시 이 경계를 분명히 할 것 (`base/
purity-and-effects-plan.md`와 연결됨).
**보강(2026-08-04 검증 라운드): 부작용은 심각도가 다른 두 갈래로 나뉜다.**
1. **국소적 부작용** — 입력으로 받았거나 자신이 만들어 소유한 대상에 대한
부작용(예: 렌더 리턴 아래에서 옵저빙해서 자기 slot을 갱신). 이건 편의성이
커서 적극 환영하는 영역.
2. **경계를 넘는 부작용** — globalStore처럼 컴포넌트 바깥의 전역 상태를
다루는 경우. 게임 UI 특성상(스킬/주변 환경에 영향받는 UI 등) 완전히
막을 수는 없지만, 라이브러리로 재사용하려는 컴포넌트가 이런 부작용을
가지면 이식성이 떨어짐(`base/purity-and-effects-plan.md`와 연결).
**해소됨(2026-08-04 2차 라운드)**: "state를 옵저빙해서 나온 결과로 slot에
`clear`/`add` 같은 연산을 할 때, 그 시점에 대상 slot이 이미 죽어있으면
어떻게 되는가"는 별도 메커니즘 없이 `canExecute` 재사용으로 해결됨 —
`base/source-state-plan.md`의 "Slot 생존 확인" 절이 소스.
## Store = Source들의 이름 붙은 모음 (eager + lazy 생성)
`store.a`처럼 키로 접근하면 **이미 만들어진 Source가 있으면 그대로 반환,
없으면 그 자리에서 만들어 저장한 뒤 반환** — 더 이상 별도 State wrapper를
매번 만들거나 따로 캐싱하지 않음(Source 자체가 이미 State를 만족하므로
wrapper 계층 자체가 불필요해짐, `base/source-state-plan.md`의 "Source가
State를 만족함" 절).
- **`defaults`는 선택**(안 줘도 됨, 순수 편의용 초기값 템플릿) —
`Store({defaults})`가 내부적으로 `{[key] = Source(default), ...}`
다름없게 됨.
- **[정정, 2026-08-07] "Store 생성 시 전부 eager하게만 만들어진다"는 이전
서술은 부정확 — eager와 lazy가 둘 다 필요하다.** Luau 타입은 런타임에
강제되지 않으므로 `Store<<SomeType>>()`처럼 `defaults` 없이 만든 뒤
`.Key:Set(v)`를 부르는 경우, `__index`가 "없으면 그 자리에서 만들어
저장"까지 해주지 않으면 `.Key``nil`이라 크래시남. 그래서 **Store
생성 시점의 eager 생성**(각 `defaults` 키마다 미리 만들어둠)과
**`store.key` 접근 시점의 lazy 생성**(아직 없는 키를 그 자리에서 만들어
저장, 이후 재접근은 재생성 없이 그대로 반환)이 **둘 다** 필요함.
- **`defaults` 테이블 원본을 나중에 mutate해도 UB가 아님** — 라이브 백킹
스토리지가 아니라 "아직 안 만들어진 Source를 만들 때 참고하는 초기값
템플릿"으로만 반복 참조되기 때문(`bind-system-plan.md`에 남아있던
"defaults 테이블 직접 mutate는 UB"라는 옛 서술은 2026-08-07에 정정됨).
별도 `__values`류 그림자 실값 저장소도 불필요 — Source 객체 자체가
저장소 역할을 함.
- **구현 스케치(2026-08-07, 성능 근거): eager 생성은 `table.clone(defaults)`
후 그 결과를 순회하며 각 슬롯을 `Source(v)`로 교체하는 모양이어야 함**
(`local sources = table.clone(defaults); for k, v in sources do
sources[k] = Source(v) end` 류) — 빈 테이블을 새로 만들어 키를 하나씩
넣는 것보다, `table.clone`으로 원본의 해시/배열 슬롯 구조를 그대로
재사용하는 쪽이 Luau VM 입장에서 더 쌈(직접 해시 슬롯을 처음부터
구성하는 것보다 기존 슬롯을 복제하는 게 저렴). `Source()`(인자 없이
호출)는 `Source(nil)`과 동치 — `defaults`에 값이 없는 키를 `store.key`
접근 시점에 lazy 생성할 때 이 무인자 형태를 씀.
v1이 모든 값을 Store 하나에 몰아넣던 습관은 "당시 정적 타입이 없어 단순하게
쓰는 게 편해서"였다는 게 사용자의 회고적 재평가 — 지금은 타입이 핵심
제약이라 그 전제 자체가 더 이상 안 맞고, 2026-08-06 후속 세션의 정리로
Store는 "이름 붙은 Source 모음, 그 이상 아님"으로 더 단순해짐. 값 하나만
반응형으로 다루고 싶으면 Store를 통째로 만들지 말고 독립
`Source(default)`를 쓸 것(`base/source-state-plan.md`의 "Source는 독립
공개 프리미티브로 격상" 절).
## Store 값 설정 문법 — `myStore.key = value` 폐기, `source:Set(value)`로 전환 (2026-08-06 후속 세션, 정정)
**이전 버전("v1 인체공학 유지, `__newindex` 기반 `myStore.key = value`
그대로")은 폐기됨.** `base/source-state-plan.md`의 "Source가 State를
만족함" 절 타입 설계와 맞물려 재검토된 결과:
1. **타입 대칭성**: `store.key`가 이제 `Source<T>`를 직접 반환하는
평범한 레코드 필드(`{key: Source<number>}`)로 타이핑되는데, 레코드
필드는 읽기/쓰기 타입이 같아야 Luau 구조적 타이핑이 깨끗하게 성립함.
`store.key = value`(raw `T` 대입)를 유지하면 읽기(`Source<T>`)/쓰기(`T`)
타입이 갈려 mismatch가 남음 — `store.key:Set(value)`로 통일하면 필드
타입이 항상 `Source<T>`로 대칭적이라 문제 자체가 안 생김(사용자 지적).
2. **의미론적 정직성**: `=` 대입 문법은 관례상 "그 자리에서 즉시 확정되는
부작용 없는 값 쓰기"를 암시하는데, quad의 실제 동작은 **lazy**
`Set`은 무효화 신호만 쏘고, 실제 재계산은 나중에 누군가 관측(`Get()`)할
때만 일어남("Emit으로 필요한 사람 있어? 하고 물어보고, 있어야 진짜
계산 시작"). 이건 `=`가 암시하는 "즉시 커밋"과 정서가 안 맞고, 메소드
호출(`:Set()`)이 "이건 프로세스를 트리거하는 연산"이라는 걸 더 정직하게
신호함(사용자 확정 논거).
3. `:Set()`은 이미 확정된 "값을 바꾸는 연산엔 `:` 체이닝 허용" 원칙(`base/
architecture.md`)에도 자연스럽게 들어맞음 — 문법 자체가 새로 생기는 게
아니라 기존 원칙의 정상적인 적용.
**남는 것**: `myStore "key"`(문자열 커링)는 이미 3차 라운드에서 동적 키
전용 미타입 폴백으로 격하돼 있었으므로 이번 정정과 무관하게 그대로 유지.
`:` 체이닝 원칙도 `:Set()` 자체가 그 사례라 유지.
`base/architecture.md`의 "복사 구현 지양, 팩토리 함수로 대체" 원칙과 함께
읽을 것 — v1의 문제는 metatable 체이닝으로 매번 새 테이블을 할당하며
"불변 빌더"를 흉내낸 것이었지, `:` 체이닝 문법 자체나 커링 문법 자체가
아니었음.
## 타입 추론 문제 — `store.key`(dot-access)를 1급 경로로 확정 (2026-08-04 3차 라운드)
- `store "key"`(문자열 커링)로 `state<T>`를 오버로드 함수 타입으로 정확히
추론하려는 시도는 포기하고, **`store.key`(dot-access)를 1급 경로로 확정**
— Store 타입을 `{key: Source<number>, other: Source<string>}`류 평범한
레코드 타입으로 지으면 일반 구조적 필드 타이핑으로 자동 해결되고, 문자열
리터럴 narrowing 문제 자체가 안 생김([정정, 2026-08-06] 원래 `State<T>`
필드로 적혀있었으나 Source가 State를 만족하는 구조로 바뀌며 `Source<T>`
갱신 — `store.key = value` 쓰기 문법이 `:Set()`으로 옮겨가 이 필드가
더 이상 `__newindex`로 쓰이지 않으므로 읽기/쓰기 타입 대칭 문제도 같이
해소됨, 위 "Store 값 설정 문법" 절 참고).
`store "key"` 문자열 커링은 동적 키가 필요할 때 쓰는 미타입(`Source<any>`)
폴백으로 격하.
- 이 패턴은 Store에만 국한되지 않고 **인스턴스 생성까지 관통하는 프로젝트
전역 관습으로 확정**됨 — 단 이벤트는 이후 4차 라운드에서 이 관습의
**유일한 예외**로 빠졌음(PA님 방식인 문자열 키+런타임 리플렉션으로 전환).
`base/bind-system-plan.md`의 "인스턴스 생성 / 이벤트 네이밍 인체공학"
절이 최신 확정 내용.
### `store.key` 레코드 필드 타이핑 — Luau 타입함수로 해결 확인 (2026-08-12 열일곱 번째 세션, `pre-implementation-audit.md` 1-10 해소)
위 절이 "`store.key`를 평범한 레코드 필드 타이핑으로 자동 해결"이라
서술했지만, `Store<T>`가 입력 `T`(예: `{ty: string}`)를 받아
`{ty: Source<string>}`류 결과 타입을 실제로 어떻게 합성하는지는 미검증으로
남아있었음. **Luau의 `type function`**(컴파일타임에 타입을 인자로 받아 새
타입을 조립하는 기능, https://luau.org/types/type-functions/ ,
https://luau.org/types-library/ — tbox에서도 이미 쓰이는 검증된 패턴)으로
정확히 풀림:
```luau
type function WrapStore(ty: type): type
-- Source<T> 형태를 그대로 조립(:Get/:Set/:Compute/:With 등)
local result = types.newtable()
result:setproperty(types.singleton("Get"), types.newfunction(...))
return result
end
type function ProcessStoreType(ty: type): type
local props = ty:properties() :: { [type]: { read: type?, write: type? } }
local result = types.newtable()
for i, v in props do
-- i는 프로퍼티 이름을 담은 singleton 타입, i:value()로 실제 문자열
result:setproperty(i, WrapStore(v))
end
return result
end
```
`ProcessStoreType<{ty: string}>``{ty: Source<string>}`가 나옴 — 결과는
선언 시점에 이름 붙은 `Source<string>` 그 자체가 아니라 구조를 그대로 풀어낸
(flatten) 익명 타입이지만, **Luau는 이름이 아니라 "만족하는가"로 구조적
일치를 검사**하므로 문제없이 `Source<string>` 자리에 대입 가능 — 오히려 이
방식과 정확히 맞는 조합. 이걸로 `store.key`가 실제로 타입 명시 가능함이
확인돼 M0/M3 어느 시점에 검증해도 기술적으로 막힐 위험은 없음 —
`ROADMAP.md`의 M0/M3 배치를 강제로 바꿀 필요는 없어짐, 검증 난이도
문제였던 것만 해소. (이 방식이 못 해주는 것은 `base/typing-limits.md`
따로 정리.)
## Store가 Store를 저장 가능한가
사용자 원 메모: "슬롯을 스토어처럼 생각 가능하다면 이건 가능하다고 봐야하는가?
아니면 아예 다른 값으로 둬야 하는가? table/number 같은 프리미티브 타입이나
ref 타입처럼 생각하는 게 맞는 거 같음 — 그걸 처리하는 플러그를 만드는 걸로."
**2026-08-04 6차 확정: 그런 경우는 없다고 본다.** "재실행 래핑으로
기계적으로는 커버 가능하다"는 제안은 메커니즘상 틀리지 않지만, 실제 설계
의도와 안 맞음 — Store는 Source에 준하는 존재로 모든 반응형 값의 "시작점"
역할만 함. 시작점은 다른 변화하는 무언가에 연결되는 것을 제공하고자 하지
않음(= Store가 다른 Store/State를 값으로 담아 자동으로 따라가게 하는 용도로
쓰지 않음). Store에서 값을 꺼내 State를 옵저빙하다가 콜백으로 다른 Store 값을
바꾸는 식의 수동 연결은 있을 수 있지만, 잘 짜인 UI에서 실사용 사례를 거의
보지 못했다는 게 사용자 판단 — 그래서 이 케이스를 위해 별도로 신경 쓰지 않음.
**[2026-08-13 세션, 스코프 명확화, 같은 날 다섯 번째 세션에 결론 갱신]**
이 절은 "Store *필드*가 Store/State를 담는가"(예: `store.a = otherStore`)
얘기이고, "State가 *emit하는 값*이 State/Source인가"(`State<State<T>>`,
예: `store.key`에 대입된 값 자체가 State)는 다른 축. 이 절의 "별도로
신경 쓰지 않음"(Store 필드 얘기)은 그대로 유지 — 후자(`State<State<T>>`)는
한때 실제 체인 파손 버그로 확인돼 `Dispatch.process`가 명시적으로 error
하도록 막았었으나, 같은 날 다섯 번째 세션에 `chains`의 인덱스 기반
재설계로 그 버그의 근본 원인이 없어져 **지금은 정상 지원 대상**
(`base/dispatch-core-plan.md`의 "Dispatch 체인" 절 참고 — 열네 번째
세션의 하강 diff로 깜빡임 방지 힌트까지 깊은 체인에서 유지됨) — "신경 안 씀"의
의미가 "조용히 UB"도 "즉시 실패"도 아니라 "그냥 정상적으로 동작함"으로
다시 한번 바뀜.
**따라서 "Store가 Store를 담는 경우 이중 해제(double-dispose) 방지가
필요한가"라는 질문도 성립 안 함으로 종결** — 두 가지 독립적인 이유로
이중 해소됨. (1) 애초에 그런 경우를 만들지 않기로 확정(위 문단). (2) 설령
발생해도 State/Source 그래프 구독이 전부 weak-keyed GC-native(명시적
`dispose()` 호출이 아예 없음, `base/lifecycle-pattern.md`의 GC 위임 원칙
재사용)라 "같은 걸 두 번 해제"할 행위 자체가 존재하지 않음(GC는 멱등).
## Store가 담을 수 없는 값 — Modifier
`Store<T>`/`Source<T>`의 `T`는 Modifier가 될 수 없음(런타임 `error`) —
근거와 검사 지점은 `base/source-state-plan.md`의 "따름정리" 절이 소스.
## 여러 스토어 값을 묶어 처리하는 것 (dependency array) — 확정
`useEffect`처럼 여러 store 값을 디펜던시로 묶어 파생값을 계산하고 싶다는
요구는 `:With(...)`로 의존성을 모으고 `:Compute(fn)`으로 파생 State를
만드는 것으로 확정 — `Store.Combine({a,b}, fn)`류 포지셔널 인자 방식은
기각됨. 정확한 lazy 인자 규칙(self/with 값 둘 다 State 핸들로 넘기고
`:Get()`을 실제로 읽을 때만 계산), v1 `myStore "a,b"` 콤마-조인 문자열
방식의 폐기, 여러 값을 한 번에 바꿀 때 재계산을 한 번으로 묶는 `Blocker`
연결은 전부 `base/source-state-plan.md`의 "여러 값을 묶어 파생값 만들기"
절이 소스.

View file

@ -1,346 +0,0 @@
# Store 의미론 — 부작용 허용, State는 Source 위의 조합 가능한 캐시 레이어
**상태**: base — Store가 부작용을 허용한다는 핵심 결정과 State/Source
온톨로지 구조 자체는 확정(2026-08-04 검증 라운드에서 새로 열려 같은 세션
2~4차 라운드에 걸쳐 확정까지 마침 — 최신 상세는 `base/bind-system-plan.md`
참고). 원본: `.claude/initreq/raw-userinput.md` "store는 부작용을 허용함" /
"state는 어떻게 구현하는가" 절.
> **[2026-08-13 열세 번째 세션, 해소]** self/deps를 lazy `State` 핸들로
> 넘기는 `:Compute`/`:With` 콜백 계약은 한때 미해결(구 `question.md`
> 0-Y)이었으나 **그대로 유지로 확정**됨. 남은 것은 quad 설계 문제가
> 아니라 Luau의 현 한계(파생 State의 반환 타입이 정적으로 검증되지
> 않아 사용처에서 명시 주석 바인딩이 필요) — 전역 규약은
> **`base/typing-limits.md`**, 실측 근거는 `audit/type-recursion-issue/`.
## Store는 부작용을 허용하는 게 기본 디자인
부작용 없이(파라메터 패싱만으로) 쓰는 것도 물론 가능하지만, 라이브러리 차원에서
막지 않는다. 부작용 유무는 **사용자가 직접 문서화**하는 관례로 둔다 — 라이브러리가
순수성을 강제하지 않음.
다만 한 가지는 명확히 구분: **렌더 리턴 위에서 무언가를 observe하는 것은 그냥
부작용**이다 (`useEffect`와 유사한 것으로 문서화). 이건 "허용되는 부작용"이 아니라
"당연히 부작용"이라는 뜻 — 문서화 시 이 경계를 분명히 할 것 (`base/
purity-and-effects-plan.md`와 연결됨).
**보강(2026-08-04 검증 라운드): 부작용은 심각도가 다른 두 갈래로 나뉜다.**
1. **국소적 부작용** — 입력으로 받았거나 자신이 만들어 소유한 대상에 대한
부작용(예: 렌더 리턴 아래에서 옵저빙해서 자기 slot을 갱신). 이건 편의성이
커서 적극 환영하는 영역.
2. **경계를 넘는 부작용** — globalStore처럼 컴포넌트 바깥의 전역 상태를
다루는 경우. 게임 UI 특성상(스킬/주변 환경에 영향받는 UI 등) 완전히
막을 수는 없지만, 라이브러리로 재사용하려는 컴포넌트가 이런 부작용을
가지면 이식성이 떨어짐(`base/purity-and-effects-plan.md`와 연결).
**해소됨(2026-08-04 2차 라운드)**: state를 옵저빙해서 나온 결과로 slot에
`clear`/`add` 같은 연산을 할 때, 그 시점에 대상 slot이 이미 죽어있으면
어떻게 되는가 — 별도 메커니즘을 새로 만들 필요 없이, `base/
lifecycle-pattern.md`의 "생명 바인드 유틸"(canExecute predicate)을 state-
invalidate 리스너 클로저 등록에도 그대로 재사용하면 됨: 발화 시
`canExecute(value)`(2026-08-14 다섯 번째 세션 최종 시그니처, `inst`를 안 받음
`base/lifecycle-pattern.md` 참고) 하나만 확인, 거짓이면 no-op. 한때 검토했던 `isInit=false`
허용/`isInit=true`+생존확인 거짓이면 불허 분기 초안은 폐기 — `canExecute`
하나로 통일(사용자 확정). 상세는 `base/bind-system-plan.md`
"Store/State/Source 온톨로지" 절 참고.
## 정정(2026-08-04 검증 라운드): `State` 프리미티브는 실제로 필요하다
**후속(2026-08-04 2차 라운드)**: 아래 온톨로지의 전파 모델(push-invalidate/
pull-recompute)·`:Compute` 인자 규칙·State 쓰기 금지·`Source` 독립
프리미티브화·Slot 생존 확인까지 전부 확정됨 — 최신 상세는 `base/bind-system-plan.md`의 "Store/State/Source 온톨로지 — 핵심 메커니즘 확정"
절이 최종 소스, 이 절은 배경/온톨로지 명칭 정의로만 유지.
**이전 버전의 이 절("State 프리미티브는 만들지 않는다")은 틀렸음 — 사용자가
검증 라운드에서 직접 정정.** 정확한 모델:
- **Store는 "source 집합체"이자 state를 만들어주는 존재.** 실제 값이 존재하고
변경될 수 있는 단일 지점은 source(v1의 "값의 근원"에 해당) — store는 이런
source들의 모음.
- **State는 source(또는 다른 state)를 받아 캐싱만 하는 존재, 자기 고유의
독립적 value 개념이 없다.** 단일 값에 대한 state 생성은 store가 자동으로
해주지만, 그 결과를 다시 분기하고 싶으면(하나의 파생 스트림에서 여러
소비자가 각자 다른 추가 compute를 얹고 싶은 경우) `state(state)`처럼 기존
state의 결과를 받아 새 state를 만드는 조합이 필요.
- **[정정, 2026-08-06 후속 세션] store에서 값을 얻는 연산(`store.key`)은
Source를 직접 반환한다 — 더 이상 별도 State 인스턴스를 감싸서 반환하지
않음.** 상세는 아래 "Source가 State를 만족함" 절 참고. 이 항목의 원래
버전("항상 새 state 인스턴스를 반환")은 틀림 — Store가 별도 wrapper
없이 자기 안에 만들어둔 Source를 그대로 돌려주는 쪽으로 재정리됨.
- 이건 quad2-try(폐기된 이전 시도)의 `Pipe` copy-on-write 절충안을 대체하는
방향으로 좁혀짐 — 별도 `Pipe` 타입을 만들어 소유권/버전 가드를 넣는 대신
State 자체가 "파이핑 결합체"이고 `state(state)`로 분기하면 될 걸로 보임
(`Pipe` 후보는 사실상 폐기 쪽으로 기움). 상세는 `base/bind-system-plan.md`
"Store/State/Source 온톨로지" 절 참고 — **이 절 이후 2~4차 라운드에 걸쳐
전부 확정됨, 더 이상 진행 중인 스레드 아님.**
## 일반 원칙 — 독립 존재 가능한 프리미티브 vs 원천에 종속된 파생 데이터 (2026-08-06 후속 세션)
위 "State는 자기 고유의 독립적 value 개념이 없다"는 관찰을 일반 원칙으로
확장(사용자 관찰): quad의 개념들은 두 부류로 갈린다.
- **독립 존재 가능한 프리미티브** — Source, Ref, Store, Modifier. 다른
무언가 없이 그 자체로 `Type(args)` 팩토리 함수로 만들어짐(`Source(default)`/
`Ref(default)`/`Store({defaults})`/`Modifier()`, 위 "생성자
스타일 확정" 참고 — `Modifier()`는 빈 인스턴스, 실제 필드는
`mod:UICorner(8)`류 체이닝으로 그 위에 얹음).
- **원천에 종속된 파생 데이터** — State, Observer. 자기 혼자 존재할 수
없고 항상 특정 원천(Source/다른 State)에 의존 — 그래서 이 둘은 자유
함수 생성자가 없고, 항상 원천에 대한 메소드 호출로만 얻어진다
(`store.key`/`state:Compute(fn)`/`state:With(...)` → State,
`state:Observer(fn)` → Observer). "클래스 같은 독립 타입"이라기보다
"State를 관측·핸들링하는 데이터"에 가까움.
이게 `base/bind-system-plan.md``state:Observer(fn)`가 메소드고
`Observer(state, fn)`라는 자유 함수가 없는 더 근본적인 이유 — 단순히
"읽기 편해서"가 아니라 Observer 자체가 State처럼 원천 없인 존재할 수
없는 카테고리라서. 앞으로 새 개념을 추가할 때도 이 두 부류 중 어디에
속하는지가 생성자 모양(자유 함수 팩토리 vs 원천에 대한 메소드)을
결정하는 기준으로 쓸 수 있음.
**세 번째 카테고리 — Handler는 둘 중 어디에도 안 낌(2026-08-08 두 번째
세션, 명시화).** `Handler`(`isHandlable`/`priority`/`process` 3종 계약 —
`process`가 자기 retract 클로저를 반환, 2026-08-13 다섯 번째 세션 정정,
`base/dispatch-core-plan.md` "핸들러 계약" 절)는 위 분류가 다루는
"quad 사용자가 직접 다루는 리액티브 값"이 아니라 **그 자체로는 구현체가
없는 순수 타입 계약**이라 애초에 이 분류표의 대상이 아님 — Source/Ref처럼
`Type(args)` 자유 함수로 인스턴스를 만들 수도 없고(계약을 만족하는 값은
`PropertyHandler`/`TagHandler`/`Dispatch/StoreBind.luau`의 `NoneHandler`처럼
**구현하는 쪽**이 리터럴 테이블로 직접 채워 넣는 것), State/Observer처럼
어떤 원천에 종속된 파생물도 아님(애초에 "원천"이라는 개념 자체가 안 맞음).
Handler는 quad 사용자가 아니라 **백엔드/핸들러 구현자가 채우는 확장
지점**이라는 완전히 다른 축의 개념이라, 여기 분류를 "왜 Handler가
빠졌는지" 궁금해할 필요 없음 — 프리미티브 분류가 불완전한 게 아니라
Handler가 애초에 다른 층위. 관련해서 Handler를 담는 엔진(`Dispatch`) 자체가
왜 프리미티브가 아니라 탑레벨 싱글톤인지는 `base/dispatch-core-plan.md`
"Dispatch는 프리미티브가 아니다" 절 참고.
과거 "미해결로 남은 것"으로 적었던 두 항목도 모두 해소됨: `:Compute`
캐싱/무효화 전략은 push-invalidate(신호만)/pull-recompute(`Get()` 시점)로
확정(`base/bind-system-plan.md` "전파 모델" 절), `store "key"` 커링의 타입
추론 문제는 `store.key`(dot-access)를 1급 경로로 확정하며 해소(같은 문서
"타입 추론 문제" 절, 3차 라운드).
## Source가 State를 만족함 — 구조적 서브타입, RefSource 개념 폐기 (2026-08-06 후속 세션)
**배경**: `store.key`가 매번 새 State를 감싸 반환하던 이전 모델의 타입
문제(레코드 타입 `{key: State<number>}`가 읽기/쓰기 비대칭이라 Luau
타이핑이 안 맞음, 위 "Source가 State를 만족함" 논의에서 도출)를 풀다가
사용자가 제안한 더 근본적인 재구성. `RefSource<T>`(store 슬롯을 가리키는
전용 타입)를 따로 만드는 중간안도 검토했으나, 최종적으로 **Source 자체가
State를 만족하도록 만들고, RefSource라는 별도 타입은 폐기**하는 쪽으로
수렴.
**확정 방향**:
- **`Source<T>`가 구조적으로 `State<T>`를 만족(단방향 호환)** — State
자리엔 Source를 넣을 수 있지만 역은 안 됨(Svelte의 `Writable<T> extends
Readable<T>`와 같은 모양). Source는 State가 주는 모든 것(`:Get()`,
`:With(...)`, `:Compute(fn)`) 위에 `:Set(value)`/`:Emit()`을
추가로 가짐([정정, 2026-08-07] `.value`는 State/Source에서 제외되고
`Get()`으로 통일됨, `.value` 표기는 Ref 전용으로 좁혀짐 — `base/
bind-system-plan.md` "`:With`/`:Compute` — self 인자도 lazy 핸들로
통일" 절 참고).
- **`:With`/`:Compute`는 Source에서도 항상 `State<U>`를 반환** — Source
자신을 변형하는 게 아니라, "Source의 State 뷰를 뽑아 그 위에 파이핑"하는
것과 동치. 구현은 metatable `__index` 델리게이션(Source의 메소드
테이블이 State의 메소드 테이블로 폴백)으로 충분 — `Modifier`의 제네릭
`__index` 트릭(`base/modifier-plan.md`)과 같은 패턴이라 로직 중복이
생기지 않음.
- **`RefSource<T>` 같은 별도 타입은 불필요, `Store({defaults})`
내부적으로 `{[key] = Source(default), ...}`나 다름없게 됨.**
`defaults`**선택**(안 줘도 됨, 순수 편의용) — `store.key`는 이미
만들어져 있는 키면 그 Source를 그대로 돌려주지만, **아직 안 만들어진
키면 그 자리에서 `Source(defaults의 해당 값 또는 nil)`을 만들어 저장한
뒤 돌려줌**([정정, 2026-08-07] eager 생성만으로 충분하다고 서술했던
이전 버전은 부정확 — Luau 타입은 런타임에 강제되지 않으므로
`Store<<SomeType>>()`처럼 defaults 없이 만든 뒤 `.Key:Set(v)`를 부르는
경우, `__index`가 "없으면 그 자리에서 만들어 저장"까지 해주지 않으면
`.Key``nil`이라 크래시남 — 그래서 Store 생성 시점의 eager 생성(각
`defaults` 키마다 미리 만들어둠, 이건 여전히 필요)과 `store.key` 접근
시점의 lazy 생성(아직 없는 키를 그 자리에서 만들어 저장, 이후 재접근은
재생성 없이 그대로 반환)이 **둘 다** 필요함). `defaults` 테이블 자체는
라이브 백킹 스토리지로 쓰이지 않고 "아직 안 만들어진 Source를 만들 때
참고하는 초기값 템플릿"으로 반복 참조될 뿐이라, Store 생성 후 원본
`defaults` 테이블을 밖에서 바꿔도 문제없음(UB 아님 — 이 항목도
`bind-system-plan.md`에 남아있던 "defaults 테이블 직접 mutate는 UB"라는
옛 서술과 충돌해 2026-08-07에 같이 정정함, 아래 참고). 별도 `__values`
그림자 실값 저장소도 불필요 — Source 객체 자체가 저장소 역할을 함. 이
모델은 이전에 검토했던 "State를 weak table로 캐싱" 절충안보다 더 싸다
(래퍼 생성/캐싱 단계 자체가 사라짐). v1이 모든 값을 Store 하나에
몰아넣던 습관은 "당시 정적 타입이 없어 단순하게 쓰는 게 편해서"였다는
게 사용자의 회고적 재평가 — 지금은 타입이 핵심 제약이라 그 전제 자체가
더 이상 안 맞고, 이번 정리로 Store는 "이름 붙은 Source 모음, 그 이상
아님"으로 더 단순해짐.
- **구현 스케치(2026-08-07, 성능 근거): eager 생성은 `table.clone(defaults)`
후 그 결과를 순회하며 각 슬롯을 `Source(v)`로 교체하는 모양이어야 함**
(`local sources = table.clone(defaults); for k, v in sources do
sources[k] = Source(v) end` 류) — 빈 테이블을 새로 만들어 키를 하나씩
넣는 것보다, `table.clone`으로 원본의 해시/배열 슬롯 구조를 그대로
재사용하는 쪽이 Luau VM 입장에서 더 쌈(직접 해시 슬롯을 처음부터
구성하는 것보다 기존 슬롯을 복제하는 게 저렴). `Source()`(인자 없이
호출)는 `Source(nil)`과 동치 — `defaults`에 값이 없는 키를 `store.key`
접근 시점에 lazy 생성할 때 이 무인자 형태를 씀.
- **이 서브타입 관계는 `quad2-try`에서 기각한 컴포넌트/클래스 OOP 상속과는
다른 층위.** 그때 금지한 건 사용자가 짜는 컴포넌트 계층 구조(`Class:Extend()`류
매직)였고, 지금은 두 프리미티브 타입 사이의 구조적 서브타이핑(런타임
구현 델리게이션 포함)이라 그 금지와 충돌하지 않음.
- **동적 키 폴백(`store "key"`)은 이제 `State<any>`가 아니라 `Source<any>`
반환**하는 것으로 자연히 갱신됨(위 "타입 추론 문제" 절과 연동).
**[해소됨, 2026-08-13 첫 실측 라운드]** 핵심 질문(Source가 State를 구조적으로
만족하는 제네릭 메소드 체이닝)은 `08-type-source-satisfies-state.luau`(`luau-test/review-required/`)로
실측 통과 확인됨 — 아래 우려대로 "두 제네릭 타입 별칭이 서로를 참조하는
상호 재귀"는 실제로 위험했지만, 그 아래 제안한 단방향 의존(`State`가
`Source`를 참조 안 함) 회피책이 그대로 맞아떨어짐. **다만 좁은 잔여
케이스 하나는 남음**: `State<T>`가 **자기 자신**을 다른 타입 인자로
재귀 참조하면(`Recursive type being used with different parameters`)
막힘 — 이건 아래 논의 대상이던 "두 타입 간 상호 재귀"와는 다른 문제로,
**[2026-08-13 열세 번째 세션 결론] Luau의 현 한계로 확정**되어
`base/typing-limits.md` 1번이 담당함(구 `question.md` 0-Y는 해소).
당시 검토됐던 "구울 때 인라이닝"(T별 코드 생성) 방향은 **채택 안 함**
제네릭 자체를 없애버려 나중에 Luau가 고쳐져도 수혜를 못 받기 때문.
아래는 그 판단에 이른 원래 추론 과정(구분 기준 등)이라 계속
유효한 배경 — `Source<T>``:Compute` 시그니처가 자기 자신(`Source<T>`)과
`State<U>`를 동시에 참조하는 제네릭 메소드라, Luau 솔버가 재귀 타입
조합에서 막히지 않는지가 원래 질문이었음. 구분해서 볼 것:
- **자기 자신을 가리키는 self 타이핑**(`{ Compute: <U>(self: Source<T>, ...) -> State<U> }`
같은 패턴)은 Luau에서 극히 흔하고 대체로 안전 — 모든 메소드 테이블
클래스가 쓰는 패턴이라 이것 자체가 위험 신호는 아님.
- **진짜 위험한 건 두 제네릭 타입 별칭이 서로를 참조하는 상호 재귀**
(`Source<T>` 정의가 `State<T>`를 참조하고, `State<T>`도 거꾸로
`Source<T>`를 참조하는 경우) — 이게 Luau 솔버가 알려진 대로 취약한
패턴. **`State<T>``Source`를 전혀 참조하지 않도록 먼저 독립적으로
정의하고, `Source<T>``State<T>`를 참조하는 단방향 의존으로 두면**
이 위험한 패턴 자체를 피할 수 있어 보임 — 다만 이것도 추론이라 실제
Luau로 확인 전엔 확정 아님.
- 사용자는 `&`(교차 타입) 조합보다 **타입을 손으로 펼쳐 쓰는(flatten)
쪽을 선호**(엔지니어링 비용을 감수하더라도 솔버 안정성 우선) — 이건
런타임 구현의 델리게이션(위 항목)과는 별개 축이라 서로 충돌 안 함:
타입은 펼쳐 쓰고 구현은 공유하는 조합이 가능함.
- `ROADMAP.md` M0의 "Store/State propagation" 스파이크 항목에 이 구체적
케이스(Source가 State를 만족하는 제네릭 메소드 체이닝)를 포함해서
검증할 것.
**이름 주의 — [해소됨, 2026-08-12 스무 번째 세션]**: `Source`/`State`라는
이름이 한때 용어 정리 대상(특히 `State`)이었으나, **`State`는 현재 이름
그대로 유지로 최종 확정됨**(`Computed`/`Derived`/`Pipe` 전부 기각 — 근거는
`bind-system-plan.md` "네이밍 — `Compute``-ed`가 아닌 이유" 절과
`question.md` 1번). 더 이상 가칭이 아님.
## Store 값 설정 문법 — `myStore.key = value` 폐기, `source:Set(value)`로 전환 (2026-08-06 후속 세션, 정정)
**이전 버전("v1 인체공학 유지, `__newindex` 기반 `myStore.key = value`
그대로")은 폐기됨.** 아래 "Source가 State를 만족함" 절의 타입 설계와
맞물려 재검토된 결과:
1. **타입 대칭성**: `store.key`가 이제 `Source<T>`를 직접 반환하는
평범한 레코드 필드(`{key: Source<number>}`)로 타이핑되는데, 레코드
필드는 읽기/쓰기 타입이 같아야 Luau 구조적 타이핑이 깨끗하게 성립함.
`store.key = value`(raw `T` 대입)를 유지하면 읽기(`Source<T>`)/쓰기(`T`)
타입이 갈려 mismatch가 남음 — `store.key:Set(value)`로 통일하면 필드
타입이 항상 `Source<T>`로 대칭적이라 문제 자체가 안 생김(사용자 지적).
2. **의미론적 정직성**: `=` 대입 문법은 관례상 "그 자리에서 즉시 확정되는
부작용 없는 값 쓰기"를 암시하는데, quad의 실제 동작은 **lazy**
`Set`은 무효화 신호만 쏘고, 실제 재계산은 나중에 누군가 관측(`Get()`)할
때만 일어남("Emit으로 필요한 사람 있어? 하고 물어보고, 있어야 진짜
계산 시작"). 이건 `=`가 암시하는 "즉시 커밋"과 정서가 안 맞고, 메소드
호출(`:Set()`)이 "이건 프로세스를 트리거하는 연산"이라는 걸 더 정직하게
신호함(사용자 확정 논거).
3. `:Set()`은 이미 확정된 "값을 바꾸는 연산엔 `:` 체이닝 허용" 원칙(`base/
architecture.md`)에도 자연스럽게 들어맞음 — 문법 자체가 새로 생기는 게
아니라 기존 원칙의 정상적인 적용.
**남는 것**: `myStore "key"`(문자열 커링)는 이미 3차 라운드에서 동적 키
전용 미타입 폴백으로 격하돼 있었으므로 이번 정정과 무관하게 그대로 유지.
`:` 체이닝 원칙도 `:Set()` 자체가 그 사례라 유지.
`base/architecture.md`의 "복사 구현 지양, 팩토리 함수로 대체" 원칙과 함께
읽을 것 — v1의 문제는 metatable 체이닝으로 매번 새 테이블을 할당하며
"불변 빌더"를 흉내낸 것이었지, `:` 체이닝 문법 자체나 커링 문법 자체가
아니었음.
## Source 값을 직접 mutate한 뒤 전파 — `:Emit()` (2026-08-06 후속 세션, 호출부 정정)
**결정**: Source가 들고 있는 값을 새 값으로 교체하지 않고 제자리에서
mutate한 뒤, `:Emit()`으로 무효화 신호만 별도로 쏘는 것을 **Source
원천(store가 직접 들고 있는 값)에 한해 허용**한다.
**[정정, 같은 세션 후반]** 원래 `Store:Emit(key)`(Store에 key를 넘겨
호출)로 적혀있었으나, 아래 "Source가 State를 만족함" 절에서 `store.key`
자체가 Source를 직접 반환하는 것으로 바뀌면서 `Emit`도 Source의 평범한
메소드로 이동 — `store.key:Emit()`(key 인자 불필요, 이미 손에 든 Source
핸들에 바로 호출). `Store:Emit(key)`라는 별도 경로는 유지할 이유가
없어져 폐기(같은 걸 하는 두 번째 경로를 남기지 않는다는 이번 세션 전반의
원칙과 일치 — `store.key = value``store.key:Set(value)` 정리와 같은 결).
**존재 이유(우선순위순)**:
1. **clone이 아예 불가능한 값이 있음.** userdata나 외부 라이브러리
객체(엔진 Instance 등)는 `table.clone`으로 새 값을 만들 수 없음 —
이런 값은 "새 값을 만들어 Set"이라는 대안 자체가 없으므로, in-place
mutation + `Emit`이 변경을 전파하는 유일한 수단.
2. Lua의 불변 업데이트가 verbose함(JS의 `{...t, x=1}` 같은 문법이 없어
`table.clone` 후 필드 덮어쓰기 + 재대입 필요) — 이걸 줄여주는 부차적
이득도 있지만, 이게 주된 이유는 아님(1번이 진짜 이유).
**왜 새 구멍이 아닌가**: `Get()`은 원래도 라이브 테이블 레퍼런스를
돌려주므로, 그 레퍼런스를 mutate하는 것 자체는 `Emit` 유무와 무관하게
Lua에서 항상 가능한 일. `Emit`이 없으면 그 mutation은 "조용히 반영 안
되는"(dependent가 재계산 안 됨, UI가 stale한 채 멈춤) 상태로 남을 뿐이라
오히려 `Emit` 없는 쪽이 더 나쁜 버그 클래스 — `Emit`은 이미 가능한
mutation에 정식 신호를 붙여주는 것뿐.
**남는 캐비엇(문서에 반드시 명시)**: `Get()`으로 이전에 그 테이블을
읽어서 어딘가(로컬 변수, 다른 코드가 들고 있는 참조)에 캐시해둔 게
있다면, mutation 순간 그것도 같이 바뀐다 — 새 테이블이 아니라 같은
레퍼런스라서. **`Get()` 결과를 나중 비교(`==`)나 diff 캐시 용도로 들고
있으면 안 됨 — 항상 다시 `Get()`할 것.**
**하드 경계 — Source 원천에만 허용, 중간/파생 State에는 없음.** `:With`/
`:Compute`로 만들어진 파생 State에는 `Emit`이라는 개념 자체가 없다 —
허용하면 "이 State의 현재 값이 뭘 근거로 계산됐는가"를 아무도 설명할 수
없게 되어(quad-debug가 추적하려는 "무엇이 무엇을 계산했는가" 그래프가
깨짐) 디버깅이 사실상 불가능해짐. State의 값은 항상 "선언된 Compute
함수를 실제로 실행한 결과"여야 한다는 불변식이 깨지면 안 됨. 무거운
파생 객체를 재사용하고 싶은 경우(Compute의 결과 자체가 무거운 userdata인
경우)를 위한 별도 메커니즘은 `base/bind-system-plan.md`의 "`:Compute(fn)`의
선택적 두 번째 인자 — `previous`" 절 참고 — 이건 `Emit`과 다른 메커니즘.
**따름정리 — `Store<T>``T`는 Modifier가 될 수 없음.** Modifier는
정적 flatten으로 dispatch와 완전히 별개인 단계에서 처리되고
(`base/modifier-plan.md`) — Store/State/dispatch 경로엔 애초에
Modifier용 processor가 없음. **[정정, 2026-08-09 세션]** `State<Modifier>`
조합은 "UB, 가능하면 타입 차단"이 아니라 **명시적 `error`로 확정**
(`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가 State 계약을 만족하는 이상 같은 이유(Modifier용 processor
부재)가 그대로 적용되고, 별도로 다시 논증할 필요 없음. 위 "하드 경계"와
같은 이유로, `Emit`이 Modifier의 정적 flatten과 충돌할 걱정 자체가
성립하지 않음(둘이 만날 지점이 없음).
## 여러 스토어 값을 묶어 처리하는 것 (dependency array) — 확정
`useEffect`처럼 여러 store 값을 디펜던시로 묶어 파생값을 계산하고 싶다는
요구가 있었음(v1의 `myStore "a,b"` 콤마-조인 문자열 방식은 폐기 대상 —
`reference/quad-v1-architecture.md`의 "문자열 DSL" 문제점 참고). **v1의
`:Add`/`:With`/`:Tween`처럼 값을 직접 가공하는 이름 붙은(named) 체이닝 연산은
만들지 않음** — 대신 일반 함수를 받아 처리. (주의: 아래의 v2 `:With(...)`
이름만 같을 뿐 v1의 `:With`와는 다른 연산임 — v1은 "함수/테이블에서 값을
가져오는" 가공 연산이었고, v2는 그냥 "여러 State를 의존성으로 모으는" 수집
연산.) 최종 형태는 `:With(...)`로 의존성을 모으고
`:Compute(fn)`으로 파생 State를 만드는 것으로 확정 — `Store.Combine({a,b},
fn)`류 포지셔널 인자 방식은 기각됨. 정확한 lazy 인자 규칙(self/with 값 둘 다
State 핸들로 넘기고 `:Get()`을 실제로 읽을 때만 계산)은 `base/bind-system-plan.md`의 "Store/State/Source 온톨로지" 절 참고.
**여러 소스를 한 번에 바꿔도 파생값 재계산/재대입이 한 번만 되게 하려면
`Blocker` 참고.** 위 `:With`+`:Compute`만으로는 "state1, state2를 연달아
Set하면 결합된 파생값이 두 번 재계산/재대입된다"는 문제(즉시 pull하는
store-bind 소비자 기준)는 안 풀림 — 이건 별도 확정 프리미티브
`base/blocker-plan.md`가 다룸(State 개발과 같은
마일스톤, `ROADMAP.md` M3에서 함께 구현). lexical `Batch(fn)`으로 풀려던
초기 시도는 코루틴 yield 위에서 구조적으로 위험해 기각됨 —
`archive/batch-rejected.md` 참고.

View file

@ -53,8 +53,11 @@ Roblox Instance 이름과 맞춘 `UICorner`/`UIPadding`(+`UIPaddingOffset`)/
이미 있는 pluggable Handler로 그대로 커버됨. `UICorner`/`UIPadding`/
`UIScale` 같은 특수 키를 인식하는 Handler(`isHandlable`이 그 키를 매칭)가
"이름 붙은 자식을 찾거나 만들고 프로퍼티 세팅"을 `process(inst, k, v, index)`
구현 — v1의 하드코딩 if/elseif 대신 정식 핸들러 계약(`isHandlable`/
"이름 붙은 자식을 찾거나 만들고 그 자식의 프로퍼티를 세팅"을
`process(inst, k, v, index)`에 구현(**[2026-08-14 세션]** 그 마지막
"세팅"은 직접 대입이 아니라 `Dispatch.process(child, prop, ..., 1)`
되돌려주는 위임으로 확정 — 아래 "Tween 지원" 절)
— v1의 하드코딩 if/elseif 대신 정식 핸들러 계약(`isHandlable`/
`priority`/`process`, 2026-08-13 다섯 번째 세션 전까진 `retract`가 별도
필드였음)을 따르는 것만 다름. `modifier-plan.md`
이미 예시로 든 `mod:UICorner(8)`은 이 특수 키를 flatten해서 props에
@ -105,13 +108,108 @@ Modifier 타입의 메소드 목록에 끼워 넣도록 챙기면 됨, 새로
API는 아니지만 공짜도 아니므로, 잦은 토글이 예상되는 값을 이 숏핸드에
직접 물리는 건 문서화 시점에 캐비엇으로 명시할 것(지금은 메모만).
## store-bind — 이 숏핸드도 지원, Tween만큼 무겁게 안 가도 됨
## Tween 지원 — 자식 프로퍼티를 `Dispatch.process`로 다시 흘려보내면 공짜 (2026-08-14 세션 확정)
**[역전] "트윈처럼 애니메이션까지 지원할 필요는 없음"이라던 아래 store-bind
절의 서술은 폐기.** 그 판단은 Tween이 아직 *독립 Dispatch 핸들러*였던
시절(우선순위를 다투는 특수 bind key, `archive/tween-special-bind-key-reversed.md`)
기준이라 "이 숏핸드도 그 경쟁에 끼워 넣어야 하나"가 비용이었는데,
2026-08-10 재설계로 Tween이 **값-레벨 래퍼 `Tween<T>` + PropertyHandler
내부 분기**가 되면서(`base/tween-plan.md`) 그 비용이 통째로 사라짐 —
이제는 숏핸드가 **자식 프로퍼티 세팅을 자기 손으로 하지 않고 Dispatch에
되돌려주기만 하면** Tween이 저절로 따라옴.
**메커니즘 — 인스턴스 관리 후 `process`로 위임**:
```lua
-- 개념 스케치. Handler 계약은 base/dispatch-core-plan.md가 정본
function UICornerHandler.process(inst, k, v, index)
if v == nil then
-- 기존 규칙 그대로: 만들어둔 자식이 있으면 지움(아래 "v가 nil인 경우" 절)
destroyManagedChild(inst, k)
return function() end
end
local child = ensureManagedChild(inst, k) -- 없으면 Instance.new + Parent, 있으면 재사용
Dispatch.process(child, "CornerRadius", mapTweenValue(v, toUDim), 1)
return function(hint)
if hint == nil then destroyManagedChild(inst, k) end
end
end
```
- **`process` 도중에 대상 `inst`를 바꾸는 것은 UB가 아님(사용자 확정)** —
키가 바뀔 수 있는 것과 정확히 같음. `chains``(inst,k)` 쌍으로
인덱싱되므로 `(inst, "UICorner")``(child, "CornerRadius")` 위임은
Dispatch 입장에서 `Attribute` 그룹이 다른 키로 위임하는 것과 구조적으로
동일한 일이고, 새 체인이라 인덱스는 `1`부터. 일반 규칙은
`base/dispatch-core-plan.md`의 "인덱스의 의미" 절에 같이 명문화해뒀음.
- **Tween 해석 코드를 여기 복제하지 않는 게 핵심 이득**`Tween<T>`
실제로 읽는 코드는 여전히 `PropertyHandler` 하나뿐이라는
`base/tween-plan.md`의 불변식이 유지됨. 3-상태 릴레이션 슬롯
(`{Tween, Value} | true | nil`), `Tween.Cancel`/`Tween.Finish` override
정책, "첫 세팅은 애니메이션 없이 즉시" 규칙까지 전부 `(child, prop)`
자리에서 그대로 재사용됨 — 이 문서가 따로 정할 게 없음.
- **타입 대수도 그대로** — 숏핸드 키의 값 타입이 `number`였다면 이제
`number | Tween<number> | State<number | Tween<number>>`가 됨
(`T' = T | Tween<T>` 치환, `tween-plan.md` "타입 대수" 절). StoreBind가
State 레이어를 먼저 다 풀어내므로 이 Handler가 실제로 보는 `v`
`number` 아니면 `Tween<number>` 둘 중 하나.
**한 가지 진짜로 필요한 부품 — `wrap`을 Tween 위로 들어올리기.** 숏핸드는
"스칼라를 받아 자식 프로퍼티 타입으로 감싸는" 변환을 갖고 있음(`UICorner = 8`
`CornerRadius = UDim.new(0, 8)`, 열린 질문 절의 룩업 테이블 `wrap=fn`).
`v``Tween<number>`면 그 변환을 **`Tween`을 벗기지 않고 `.Value`에만**
적용해야 함:
```lua
-- Tween<T>는 immutable 값 객체라 clone 후 Value만 교체(Brand 재설정은 Tween()이 함)
local function mapTweenValue(v, wrap)
if isTween(v) then
local opts = table.clone(v)
opts.Value = wrap(v.Value)
return Tween(opts)
end
return wrap(v)
end
```
- `UIScale`처럼 `wrap`이 항등(스칼라를 그대로 `Scale`에 씀)인 키는 이
헬퍼를 거쳐도 결과가 같으므로 분기 없이 일관되게 씀.
- `UIPadding`처럼 **자식의 프로퍼티 여러 개**(`PaddingTop`/`Bottom`/
`Left`/`Right`)에 같은 값을 쓰는 키는 각 프로퍼티마다 `Dispatch.process`
따로 부름 — 각자 독립된 `(child, prop)` 체인이 되고, PropertyHandler의
트윈 슬롯도 프로퍼티별로 따로 잡혀서 자연스럽게 4개가 같이 애니메이션됨.
- **`Tween` 값 자체는 `quad-base`, 이 숏핸드 Handler는 `quad-roblox`** —
`isTween`/`Tween()`을 base에서 가져다 쓰는 것뿐이라 패키지 경계
(`tween-plan.md` "패키지 경계" 절)와 안 부딪힘.
**캐비엇 — 자식이 새로 만들어진 사이클에서는 트윈이 안 걸린다(의도된 동작).**
PropertyHandler의 "첫 세팅은 애니메이션 없이 즉시"(`prev == nil`) 규칙이
`(child, prop)` 기준이므로, `UICorner``nil`↔숫자를 오가며 자식이 파괴/
재생성되면 그 직후 첫 값은 트윈 없이 스냅됨. 이건 버그가 아니라 그 규칙이
막으려는 것(기본값에서 목표값으로 날아오는 진입 애니메이션)과 정확히 같은
상황 — 계속 애니메이션되길 원하면 자식이 살아있도록 `nil`로 내리지 말고
값만 바꿀 것.
**자식을 없앨 때의 정리 책임은 이 Handler에 있음** — `v``nil`이 되거나
retractor가 `nil` 힌트로 불려 자식을 파괴할 때, 실행 중인 엔진 Tween이
남아있을 수 있으므로 `Dispatch.retractFrom(child, prop, 1)`을 같이
부르는 게 정석(자식 Instance를 `Destroy`하면 엔진 트윈도 같이 죽고
`chains`도 weak-keyed라 결국 GC되지만, "즉시" 끊는 건 명시적 호출뿐).
`retractor` 안에서 **다른 키**에 대한 `retractFrom`을 부르는 건 허용된
경로임(`base/dispatch-core-plan.md`의 retract 계약 — 금지된 건 같은
`(inst,k)`에 대한 재진입).
## store-bind — 이 숏핸드도 지원
v1에서도 `Corner`/`PaddingAll`/`Scale`은 store 값으로 바인드 가능했음
(`myStore "key"` 체이닝으로 다른 프로퍼티와 동일하게 취급됨) — quad-v2도
이 능력을 유지한다. 트윈처럼 애니메이션까지 지원할 필요는 없음(API 표면만
복잡해짐) — 그냥 값이 바뀌면 `CornerRadius`/`Padding`/`Scale` 프로퍼티를
다시 세팅하는 정도로 충분. 구현 비용도 낮음: 각 Handler가 `process`에서
이 능력을 유지한다. **[정정, 2026-08-14 세션]** 이 절의 원 서술은 "트윈처럼
애니메이션까지 지원할 필요는 없음(API 표면만 복잡해짐) — 그냥 값이 바뀌면
프로퍼티를 다시 세팅하는 정도로 충분"이었으나, 위 "Tween 지원" 절에서
뒤집혔음(자식 프로퍼티를 `Dispatch.process`로 되돌려주면 Tween이 공짜로
따라오므로 "안 하는 게 더 비싸지는" 상황이 됨). 구현 비용은 여전히 낮음:
각 Handler가 `process`에서
"이전에 자기가 찾거나 만든 자식 Instance"를 얻어야 하는데, 이건 이미
base가 범용 유틸로 제공하기로 확정한 per-instance weak-keyed 저장소
(`Relate:SetStrong(inst,k,...)`, `base/relate-plan.md`/`base/dispatch-core-plan.md`
@ -138,7 +236,9 @@ Tween 상태를 기억해두는 것과 정확히 같은 패턴. 새 메커니즘
## 남은 열린 질문 (단순화 후보, 사소함)
- UICorner/UIPadding/UIScale 3개 거의 동일한 형태의 Handler를 각각 만들지,
`{key -> {ChildClassName, ChildDefaultName, Property, wrap=fn}}` 룩업
테이블로 구동되는 단일 `Handlers/InstanceShorthand.luau`로 통합할지 —
`{key -> {ChildClassName, ChildDefaultName, Properties, wrap=fn}}` 룩업
테이블로 구동되는 단일 `Handlers/InstanceShorthand.luau`로 통합할지
(`Properties`가 단수가 아니라 목록인 이유는 `UIPadding`이 자식 프로퍼티
4개에 같은 값을 쓰기 때문 — 위 "Tween 지원" 절) —
`research/pre-implementation-audit.md` 3-2번 참고, 강제 사항 아님,
구현 시점에 결정할 정도의 사소한 개선 후보.

View file

@ -74,14 +74,14 @@ ROADMAP 항목 근거인지, 어떻게 실행하는지, 실행 후 뭘 확인해
| `05-store-state-diamond-propagation.luau` | push-invalidate/pull-recompute가 다이아몬드 의존성에서 중복 재계산 없이 동작하는지 | ROADMAP M0-1 |
| `06-component-boundary-nil-hole-props.luau` | `props.Modifier or None` 관용구가 컴포넌트 경계 nil-hole을 막는지 + `Params` 타입 체크 | `component-composition-plan.md` "필수 관용구", ROADMAP M0-5 |
| `07-relate-weak-table-gc.luau` | `Relate`의 lazy 서브테이블 생성 + weak-key GC가 실제로 동작하는지 | `relate-plan.md` "M2 착수 시 실측 확인" **[2026-08-13 보강]** 4번 섹션 신설 — `_countEntries()`(테스트 전용) + weak-value canary로 **"inst가 죽으면 중첩 StrongMap 안의 payload까지 연쇄 GC되는가"를 직접 검증**(원래는 sanity check만 하고 헤더의 핵심 주장은 미검증이었음). 파일이 스스로 적어둔 "weak table 엔트리를 셀 표준 API가 없다"는 전제도 틀렸음 — outer가 `__mode="k"`라 GC 후 `pairs`에서 사라짐 |
| `08-type-source-satisfies-state.luau` (타입체크 전용) | `Source<T>``State<T>`를 구조적으로 만족하는 제네릭 타입이 솔버에서 안전한지 | `store-semantics.md` "검증 필요", ROADMAP M0-2 |
| `08-type-source-satisfies-state.luau` (타입체크 전용) | `Source<T>``State<T>`를 구조적으로 만족하는 제네릭 타입이 솔버에서 안전한지 | `base/source-state-plan.md` "Source가 State를 만족함", ROADMAP M0-2 |
| `09-type-modifier-overridden-subtype.luau` (타입체크 전용) | `FrameModifier <: GuiObjectModifier`처럼 서브타입 관계인 Modifier를 `Overridden`으로 섞을 때 타입이 통과하는지 | `modifier-plan.md` 9-2번, ROADMAP M7 |
| `10-roblox-studio-checks.server.luau` (Studio 전용) | **[⚠️ 2026-08-14 다섯 번째 세션: A 섹션이 폐기된 모델을 검증 중 → `rewrite-required/`]** (A) `bindLifetime`/`unbindLifetime`/`canExecute`의 gcconn 트릭 + 이중 바인딩 게이트(Destroy 시 Connected 전환 포함), (B) Attribute의 Instance 참조 타입 지원, (C) CollectionService 태그/GetTagged 왕복. **A는 재작성 대상** — 파일 속 `canBound`, `bindLifetime``value.Subscribed = true` 세팅, 2-인자 `canExecute(inst, value)`는 전부 폐기됨(게이트는 `if canExecute(v) then error(...) end` 하나, `canExecute``value` 단독 1-인자, gcconn/gchold는 **Instance 생성 시점**에 생성). **[2026-08-13]** A 섹션 앞부분(ClassName 신호 미발화, Destroy 시 Connected 즉시 전환)은 사용자 자작 스크립트로 부분 확인됐고 **새 모델에서도 그대로 유효**(오히려 더 중요 — `canExecute``.Connected`를 직접 읽는 게 leaf 경로 판정의 전부), `audit/gcconn-trick-verification.md` 참고. 이중 바인딩 게이트/재바인딩 허용/B/C는 이 공식 파일로 아직 확인 안 됨 | `lifecycle-pattern.md` "`bindLifetime`/`canExecute`/`unbindLifetime` — 확정", `archive/canexecute-inst-arg-reversed.md`, `bind-system-plan.md` "이중 바인딩 금지", CLAUDE.md 2026-08-06 세션, `debug-tooling-plan.md` |
| `10-roblox-studio-checks.server.luau` (Studio 전용) | **[⚠️ 2026-08-14 다섯 번째 세션: A 섹션이 폐기된 모델을 검증 중 → `rewrite-required/`]** (A) `bindLifetime`/`unbindLifetime`/`canExecute`의 gcconn 트릭 + 이중 바인딩 게이트(Destroy 시 Connected 전환 포함), (B) Attribute의 Instance 참조 타입 지원, (C) CollectionService 태그/GetTagged 왕복. **A는 재작성 대상** — 파일 속 `canBound`, `bindLifetime``value.Subscribed = true` 세팅, 2-인자 `canExecute(inst, value)`는 전부 폐기됨(게이트는 `if canExecute(v) then error(...) end` 하나, `canExecute``value` 단독 1-인자, gcconn/gchold는 **Instance 생성 시점**에 생성). **[2026-08-13]** A 섹션 앞부분(ClassName 신호 미발화, Destroy 시 Connected 즉시 전환)은 사용자 자작 스크립트로 부분 확인됐고 **새 모델에서도 그대로 유효**(오히려 더 중요 — `canExecute``.Connected`를 직접 읽는 게 leaf 경로 판정의 전부), `audit/gcconn-trick-verification.md` 참고. 이중 바인딩 게이트/재바인딩 허용/B/C는 이 공식 파일로 아직 확인 안 됨 | `lifecycle-pattern.md` "`bindLifetime`/`canExecute`/`unbindLifetime` — 확정", `archive/canexecute-inst-arg-reversed.md`, `source-state-plan.md` "이중 바인딩 금지", CLAUDE.md 2026-08-06 세션, `debug-tooling-plan.md` |
| `11-modifier-illegal-value-error.luau` | Modifier 필드에 Ref/PreRef/Observer/Effect/Slot/Modifier가 들어오면 즉시 error, State/Source가 확정하는 값이 Modifier면 즉시 error(2026-08-09 세션에 "UB"에서 전환된 규칙) | `modifier-plan.md` "핸들러 계층 값 즉시 error" 절 + 7번 절 |
| `12-type-attribute-generic-key-narrowing.luau` (타입체크 전용) | `[AttributeKey<<T>> "name"] = value`(구 `Attribute<<T>>`)처럼 제네릭 DI 키를 쓸 때 `value`의 타입이 실제로 `T`로 좁혀지는지 — base 문서 자신이 "미검증"이라 명시한 항목 | `attribute-plan.md` "[실측 필요, M0/M10]" (2026-08-09 열한 번째 세션 신설) |
| `13-type-ref-preref-subtype.luau` | (A, 타입) `PreRef<T>``Ref<T>`를 구조적으로 만족하는지, (B, 런타임) `isRef`/`isPreRef` 합성이 재정정대로 동작하는지(`isRef(preRefInstance)`가 이제 `true`) + Leaf 핸들러가 `isRef(v) and not isPreRef(v)`로 명시적으로 좁혀야 하는 이유 | `brand-plan.md``Brand` 절(2026-08-09 열한 번째 세션 재정정) |
| `14-type-nilable-default-overload.luau` (타입체크 전용) | `Source(default)`/`Ref(default)`의 `default` 생략이 `T`가 nilable일 때만 안전하다는 캐비엇을, 함수 오버로드(교차 타입)로 실제로 타입 레벨에서 막을 수 있는지 | `bind-system-plan.md` "[보강, 2026-08-09 열한 번째 세션]" 절 |
| `15-type-compute-trailing-deps-typepack.luau` (타입체크 전용) | `:Compute(fn, ...)`의 trailing deps를 `fn`에 위치 인자(lazy State 핸들)로도 노출하는 확장, 최종 시그니처 `fn(self, previous?, ...deps)` — 이형(heterogeneous) 다중 deps를 제네릭 타입 팩(`U...`)으로 표현 가능한지, `previous?`가 팩 앞(정정된 순서)에서만 통과하고 팩 뒤(옛 순서)에서는 막히는지 | `bind-system-plan.md` "trailing deps를 fn에 lazy positional 인자로도 노출" 절(2026-08-11 후속 세션, 순서는 같은 날 세 번째 세션에 정정) |
| `14-type-nilable-default-overload.luau` (타입체크 전용) | `Source(default)`/`Ref(default)`의 `default` 생략이 `T`가 nilable일 때만 안전하다는 캐비엇을, 함수 오버로드(교차 타입)로 실제로 타입 레벨에서 막을 수 있는지 | `source-state-plan.md` "State는 쓰기 대상이 아님" 절의 `default` 생략 캐비엇 |
| `15-type-compute-trailing-deps-typepack.luau` (타입체크 전용) | `:Compute(fn, ...)`의 trailing deps를 `fn`에 위치 인자(lazy State 핸들)로도 노출하는 확장, 최종 시그니처 `fn(self, previous?, ...deps)` — 이형(heterogeneous) 다중 deps를 제네릭 타입 팩(`U...`)으로 표현 가능한지, `previous?`가 팩 앞(정정된 순서)에서만 통과하고 팩 뒤(옛 순서)에서는 막히는지 | `source-state-plan.md` "trailing deps를 fn에 lazy positional 인자로도 노출" 절(2026-08-11 후속 세션, 순서는 같은 날 세 번째 세션에 정정) |
| `16-type-store-key-typefunction.luau` (타입체크 전용) | `Store<T>``T`의 각 필드를 `Source`로 감싼 타입을 Luau `type function`(`types.newtable`/`:setproperty`/`ty:properties()`)으로 실제 합성 가능한지, 결과가 구조적으로 `Source<T>` 필드를 만족하는지 | `typing-limits.md` "`store.key` 레코드 필드 타이핑" 절(2026-08-12 열일곱 번째 세션), `pre-implementation-audit.md` 1-10 |
| `17-modifier-index-tableclone-chaining.luau` | Modifier의 제네릭 `__index`+`table.clone` 체이닝 — 임의 필드 이름에 대해 즉석 setter가 만들어지는지, `table.clone`이 메타테이블을 참조로 공유해 여러 단계 clone에서도 체이닝이 안 끊기는지, 원본이 mutate 안 되는지, 형제 분기끼리 오염 안 되는지 | `modifier-plan.md` "런타임은 클래스별 코드 없이 base에 딱 하나만 있으면 됨" 절 + "`table.clone`의 정확한 동작 — 확인됨" 절(2026-08-12 열일곱 번째 세션), `pre-implementation-audit.md` 1-11 |
| `18-relate-mutual-cycle-gc.luau` | **[2026-08-13 신규]** 서로 다른 두 `Relate`가 서로의 키를 상대방의 강한 값으로 제공하는 상호 순환은 Luau에 ephemeron이 없어 GC가 못 푼다는 주장(지금까지 공식 문서 인용으로만 뒷받침됨) — 음성 대조군(순환 재현)과 양성 대조군(한쪽을 weak-value로 낮추면 풀리는지) 둘 다 실측 | `relate-plan.md` "위험한 패턴" 절(2026-08-12 열세/열네 번째 세션), `slot-plan.md``kSlotMap`/`slotOwner`/`elementOwner` 실사례 |

View file

@ -4,7 +4,7 @@
State 참조가 섞인 제네릭 :Compute) 설계가 Luau 타입 솔버에서 안전하게
추론되는지 — 실제 실행이 아니라 타입 체크(luau-analyze) 대상.
배경: .claude/base/store-semantics.md "검증 필요(확정 아님, M0 스파이크
배경: .claude/base/source-state-plan.md "검증 필요(확정 아님, M0 스파이크
대상)" 절, ROADMAP.md M0 2번째 항목.
핵심 우려: State<T>가 거꾸로 Source를 참조하는 "상호 재귀"는 Luau

View file

@ -75,7 +75,7 @@ local function isModifier(v)
return type(v) == "table" and v[ModifierBrand] == true
end
local function isState(v)
-- Source가 State를 구조적으로 만족(store-semantics.md) — 여기선 둘 다
-- Source가 State를 구조적으로 만족(source-state-plan.md) — 여기선 둘 다
-- ".__isStateLike" 태그로 단순화해서 흉내
return type(v) == "table" and v.__isStateLike == true
end

View file

@ -140,7 +140,7 @@ print(resultA, resultB, resultD)
`a:Get()`/`b:Get()`가 정확한 타입으로 좁혀지는지)가 전부 에러 없이
통과하는가 — 통과하면 `fn(self, previous?, ...deps)` 순서가
base 문서 확정대로 최종 시그니처로 채택됨.
5. 결과에 따라 `base/bind-system-plan.md` "trailing deps를 fn에 lazy
5. 결과에 따라 `base/source-state-plan.md` "trailing deps를 fn에 lazy
positional 인자로도 노출" 절에 실측 결과로 반영할 것:
- B/D 통과, C 실패(예상대로) → 지금 base 문서에 적힌
`fn(self, previous?, ...deps)` 시그니처 그대로 최종 확정.

View file

@ -211,8 +211,6 @@
합성 시점 1회 체크로 error를 내는 게 이 문서 다른 결정들과 결이
같지만, "Merged는 뒤가 이긴다"를 의도된 override로 볼 여지도 있어
사용자 확인 필요 — `base/attribute-plan.md` "열린 질문" 절.
- `research/existing-instance-bind-plan.md` — 스코프 논의만 필요, 구현
착수를 막지 않음.
- **`quad-debug` 세부 API 이름** — `research/debug-tooling-plan.md` 참고.
채널 실현 가능성(BindableEvent/Function이 플러그인↔Play 중 게임 경계를
넘는지)까지 사용자가 Studio에서 직접 실측 검증 완료 — 기술적 불확실성은

View file

@ -33,7 +33,7 @@ init.luau`, ~1000줄) + `charm-sync`(클라/서버 상태 복제 diff 레이어)
대입 문법과 같은 트레이드오프.** `atom<T>(initialValue, equals?)`
(`init.luau:519-527`)가 인자 개수로 read/write를 분기하는 방식 —
`store.key = value`를 버리고 `store.key:Set(value)`로 간 이유
(`base/store-semantics.md:208-233`, 읽기/쓰기 타입 비대칭)와 같은 문제.
(`base/store-plan.md` "Store 값 설정 문법" 절, 읽기/쓰기 타입 비대칭)와 같은 문제.
charm 스스로도 README(185-196행)에서 `atom()``signal()`(진짜 get/set
쌍) 위에 얹은 편의 sugar로 취급 — charm 안에서도 "진짜 1급 형태는 아니다"로
다뤄지는 걸 참고.
@ -57,7 +57,7 @@ init.luau`, ~1000줄) + `charm-sync`(클라/서버 상태 복제 diff 레이어)
스킵" 기본값을 도입하고 싶어질 때, charm처럼 **모든 노드에 암묵적으로**
거는 방식은 `Blocker`가 이미 명시한 "특정 게이트 지점에서만 opt-in"
원칙(`base/blocker-plan.md:65-68`)과 "Source는 스스로를 자동 변형하지
않는다"는 `store-semantics.md` 기조에 둘 다 어긋남 — 반면교사로 남겨둘 것.
않는다"는 `base/source-state-plan.md` 기조에 둘 다 어긋남 — 반면교사로 남겨둘 것.
## 참고할만한 부분
@ -76,7 +76,7 @@ init.luau`, ~1000줄) + `charm-sync`(클라/서버 상태 복제 diff 레이어)
getter에 **이전 계산 결과**를 인자로 넘겨줌(`init.luau:538`,
`(previousValue: T?) -> T`, README 276-287행, `computed.test.
luau:84-104`가 홀수 업데이트를 스킵하는 걸로 실제 검증) — quad의
`store-semantics.md:280-284`가 이미 띄워둔 "`:Compute(fn)`에 선택적
`base/source-state-plan.md`가 이미 띄워둔 "`:Compute(fn)`에 선택적
두 번째 `previous` 인자" 안과 거의 동일한 모양. 새로 수입할 아이디어가
아니라 **이미 검토 중인 안이 실제로 동작한다는 정황 증거**로 인용
가치 있음.
@ -86,7 +86,7 @@ init.luau`, ~1000줄) + `charm-sync`(클라/서버 상태 복제 diff 레이어)
(`patch.luau:91-131`)이 immutable 재구축(레벨마다 `table.clone`, 순수
signal용)과 in-place mutate+`:Emit()`류 변형(반응형 프록시용) 둘 다
제공 — quad가 이미 다른 이유로 갖고 있는 clone-vs-mutate+`Emit` 분리
(`base/store-semantics.md:240-284`)와 우연히 같은 모양. `patch.
(`base/source-state-plan.md`의 `:Emit()`)와 우연히 같은 모양. `patch.
luau:32-57`(`stringifySparseArray`)는 실전에서 놓치기 쉬운 페이로드
함정을 문서화함 — RemoteEvent/JSON 직렬화가 성긴 배열의 trailing hole을
조용히 드롭해서, 보낼 땐 문자열 키로 재인코딩하고 받을 땐 숫자 키로
@ -128,7 +128,7 @@ previous-in-getter). 지금 당장 base 문서를 고칠 만한 발견은 없음
computed.test.luau:84-104` · `packages/charm/test/observe.test.luau:92-196` ·
`packages/charm-sync/src/patch.luau:10,19-30,32-57,59-89,91-131` ·
`packages/charm-sync/src/server.luau:27-32,124-133,192-207,209-250` ·
`README.md:185-196,262-287` · `base/store-semantics.md:208-233,240-284` ·
`README.md:185-196,262-287` · `base/store-plan.md` · `base/source-state-plan.md` ·
`base/blocker-plan.md:25-44,65-68` · `base/lifecycle-pattern.md`(GC-native
원칙) · `archive/batch-rejected.md` · `base/bind-system-plan.md:180-266`
(None 센티널) · `research/additional-primitives-plan.md`(Blocker/키 기반

View file

@ -59,7 +59,7 @@ Store/Slot/Tween/bind-dispatch 설계 결정에 근거로 인용될 때만 열
| 축 | Fusion | Vide | quad-v2 시사점 |
|---|---|---|---|
| 전파 모델 | push+pull 하이브리드, eager 집합만 즉시 재계산, 생성순 정렬로 글리치 방지 | 순수 push, 즉시 동기 재평가, 다이아몬드 중복 재평가 미해결(저자 인정) | ⚠️ **[정정] 아래 서술은 리서치 당시(2026-08-03 이전) 검토 방향이며 이후 뒤집힘 — 최종 확정은 `base/bind-system-plan.md`의 "전파 모델 확정" 절 참고**(push-invalidate는 신호만 쏘고 값은 안 실음, 재계산은 `Get()` 시점 pull-recompute로만, Fusion식 eager 노드·생성순 정렬은 아예 채택 안 함 — quad엔 그런 다단계 즉시 재계산이 필요한 소비자가 없다는 판단). 당시 스냅샷 원문: "Store는 값 자체에 항상 eager 발화, retract(구 cleanup)가 key/value를 먼저 확인" 요구사항은 Vide의 push 모델 + eval-전-retract 패턴에 더 가까움. 단 Vide의 naive BFS 대신 Fusion의 생성순 정렬 글리치 방지 규율은 채택할 것. |
| 전파 모델 | push+pull 하이브리드, eager 집합만 즉시 재계산, 생성순 정렬로 글리치 방지 | 순수 push, 즉시 동기 재평가, 다이아몬드 중복 재평가 미해결(저자 인정) | ⚠️ **[정정] 아래 서술은 리서치 당시(2026-08-03 이전) 검토 방향이며 이후 뒤집힘 — 최종 확정은 `base/source-state-plan.md`의 "전파 모델 확정" 절 참고**(push-invalidate는 신호만 쏘고 값은 안 실음, 재계산은 `Get()` 시점 pull-recompute로만, Fusion식 eager 노드·생성순 정렬은 아예 채택 안 함 — quad엔 그런 다단계 즉시 재계산이 필요한 소비자가 없다는 판단). 당시 스냅샷 원문: "Store는 값 자체에 항상 eager 발화, retract(구 cleanup)가 key/value를 먼저 확인" 요구사항은 Vide의 push 모델 + eval-전-retract 패턴에 더 가까움. 단 Vide의 naive BFS 대신 Fusion의 생성순 정렬 글리치 방지 규율은 채택할 것. |
| 정리/스코프 | 배열+메타테이블, dependency-agnostic, bind 시점에만 lifetime soft-check | dependency edge와 구조적 owner를 분리한 2중 관계, destroy는 owned만 cascade, 활성 스코프 destroy 하드 가드 | 둘 다 GC 비의존 eager 수동 정리 — rbvm의 Connected+GC 관용구와 정반대 축. quad의 Slot은 Vide처럼 "마운트 소유권"과 "반응 의존성"을 별개 관계로 분리하는 게 안전해 보임(`base/lifecycle-pattern.md`의 rbvm 패턴과는 다른 층위 — rbvm은 인스턴스 파괴 감지, 이건 Slot 내부 소유권 모델). |
| 키/값 디스패치 개방성 | SpecialKey 모양은 열려있으나 우선순위 4단계 하드고정 | action()은 등록 없는 태그 인식 방식이지만 key/value 버림, 콜백+우선순위만 | quad는 Fusion의 "디스패처가 key+value+target을 다 받는" 풍부함과 Vide의 "등록 없이 태그로 인식" 인체공학을 합치되, 우선순위 축은 열린 숫자 공간으로 일반화해야 함(`base/bind-system-plan.md`). |
@ -67,7 +67,7 @@ Store/Slot/Tween/bind-dispatch 설계 결정에 근거로 인용될 때만 열
- Vide의 암묵적(ambient stack) 의존성 추적 vs Fusion의 명시적 `use()` 축은
push/pull 축과 독립적인 별개 결정. **[정정] 리서치 당시(2026-08-03 이전)엔
"quad는 아직 미정"이었으나, `base/bind-system-plan.md`의 "여러 Store 값을
"quad는 아직 미정"이었으나, `base/source-state-plan.md`의 "여러 값을
묶어 파생값 만들기 — `:With` + `:Compute`" 절에서 이미 명시적 모델로
확정됨** — Vide식 암묵적 ambient stack 추적은 "함수 실행 중과 끝 사이를
확인하고 부작용이 필요"한 방식이라 Lua에서 깔끔하지 않다는 이유로 기각,

View file

@ -27,7 +27,7 @@
1. **초기화**`RobloxFactory(QuadBase)`로 base+backend 조립 (`module-lifecycle-plan.md`, `bind-system-plan.md`)
2. **Instance 만들기** — DOMless 즉시 생성 모델, 제네릭 `new<Class>` + 자주 쓰는 ~25개 클래스 정적 필드(`Frame`, `TextButton` 등) (`architecture.md`, `bind-system-plan.md`)
3. **속성 채우기**`[Attribute "Name"]`, ~~`[Tag ""] = true`~~ **[2026-08-13 정정] 구모델(폐기, `archive/tag-hash-key-model-reversed.md`) — 실제로는 `Tag(...)` array-part 값 객체** 특수 바인드 키 (`architecture.md`)
4. **반응형 기초**`Source`/`Store` 생성, `store.key`(dot-access)로 Source 읽기(Source는 State를 만족), `store.key:Set(value)`로 쓰기, State는 항상 읽기 전용 (`bind-system-plan.md`, `store-semantics.md`; 2026-08-06 후속 세션에서 dot-access가 Source를 직접 반환하고 쓰기가 `:Set()`으로 바뀜)
4. **반응형 기초**`Source`/`Store` 생성, `store.key`(dot-access)로 Source 읽기(Source는 State를 만족), `store.key:Set(value)`로 쓰기, State는 항상 읽기 전용 (`base/source-state-plan.md`, `base/store-plan.md`; 2026-08-06 후속 세션에서 dot-access가 Source를 직접 반환하고 쓰기가 `:Set()`으로 바뀜)
5. **스타일링** — Modifier 기본 체이닝(`:FontSize(14)`), 배열/인라인 merge 우선순위 규칙 (`modifier-plan.md`)
6. **자식 전달** — Slot 기본 개념(children 배열, add/remove/clear), 마운트된 slot 재마운트 시 throw (`slot-plan.md`)
7. **컴포넌트 작성** — 컴포넌트 = 순수 함수, 리프 프로퍼티엔 State만 바인딩, 전역 store 직접 참조 금지(이식성) (`component-composition-plan.md`, `purity-and-effects-plan.md`)
@ -35,7 +35,7 @@
9. **이벤트** — self(Instance) 안 받음, 문자열 키(`Frame { MouseButton1Click = fn }`) (`bind-system-plan.md`)
10. **생명주기** — GC 위임(수동 정리 불필요), Destroy 이후 대상 재사용 금지 (`lifecycle-pattern.md`)
11. **Ref 기초** — 외부 관리 Instance 참조/마이그레이션용, `Ref(default):Callback(fn)`을 children 배열 숫자 슬롯에 직접 놓기 + 배열 위치로 자식 전/후 표현, "프로퍼티보다도 먼저" 필요할 때만 `PreRef`(2026-08-07 세 번째 세션, `phase` 옵션 폐기) (`architecture.md`, `bind-system-plan.md`)
12. **파생값 최소 예시**`:With(...)` + `:Compute(fn)` 기본형 (`bind-system-plan.md`, `store-semantics.md`)
12. **파생값 최소 예시**`:With(...)` + `:Compute(fn)` 기본형 (`base/source-state-plan.md`)
13. **Tween 기초**~~`[Tween(key, ...)] = storeValue`~~ **[2026-08-13 정정] 구모델(폐기, `archive/tween-special-bind-key-reversed.md`) — 실제로는 `Tween(opts) -> Tween<T>` 값-레벨 래퍼**, 취소 시 현재 보간값에서 자연스럽게 이어짐 (`base/tween-plan.md`)
14. **UI 숏핸드(quad-roblox 한정)**`UICorner`/`UIPadding`/`UIPaddingOffset`/`UIScale` 인라인 키 (`base/ui-shorthand-plan.md`)
@ -75,7 +75,7 @@ v1 폐기 API/버그/구조 결함 전부 v2 설계를 정당화하는 내부
### component-composition-plan.md / module-lifecycle-plan.md
- 초심자: 컴포넌트=순수 함수 / 리프 프로퍼티 바인딩(**[정정, 2026-08-09 열한 번째 세션] "State만"이 아님 — 단순 원본 토글(`Frame{Visible=source}`)은 Source 직접 바인딩이 정상 경로, 여러 값에서 파생된 계산 결과일 때만 자연히 State가 됨, `component-composition-plan.md` 5번 절 참고**) / `props.Modifier`/`props.Ref` named parameter 경계 전달(**`props.Modifier or None`/`props.Ref or None` 필수 관용구 — 안 쓰면 nil-hole 버그, 2026-08-07 열 번째 세션 확정**) / `InitRoblox(Module)` 팩토리 초기화
- api: State(파생, 읽기전용) vs Source(원본, 쓰기가능) 경계 요약(→심화) / Slot 반환 컴포넌트는 Modifier/Ref 파라미터 미선언 / `Modifier.Overridden(mod1, mod2, ...)` 유틸(구 `Merge`, `props.Modifier` 단일 슬롯용 특수 상황으로 한정 소개 — 아래 modifier-plan.md 절 참고) / Bind는 유일 슬롯(재호출 no-op, 충돌 에러, →심화) / `:With`/`:Compute`로 파생 State 생성 시그니처 / 모듈 싱글톤 스코프
- 심화: v1 `Extend` 자동 store 소유 폐지 이유(React 벤치마킹) / Source가 State를 구조적으로 만족하는 서브타입 설계(2026-08-06 후속 세션 — `StoreSource` 프록시 중간안은 폐기되고 이걸로 대체됨, `store-semantics.md` 참고) / named-parameter 경계 방식 채택 이유(Compose/Fusion/Vide/v1 선례 수렴) / 다중 루트 반환 개념 제거 근거 / 팩토리 초기화 패턴 채택 이유(RBVM `InitNamespace` 반례) / Store 책임 분리(base가 `LifetimeHandle` 소유) / v1 named 체이닝 연산 폐기
- 심화: v1 `Extend` 자동 store 소유 폐지 이유(React 벤치마킹) / Source가 State를 구조적으로 만족하는 서브타입 설계(2026-08-06 후속 세션 — `StoreSource` 프록시 중간안은 폐기되고 이걸로 대체됨, `base/source-state-plan.md` 참고) / named-parameter 경계 방식 채택 이유(Compose/Fusion/Vide/v1 선례 수렴) / 다중 루트 반환 개념 제거 근거 / 팩토리 초기화 패턴 채택 이유(RBVM `InitNamespace` 반례) / Store 책임 분리(base가 `LifetimeHandle` 소유) / v1 named 체이닝 연산 폐기
- skip: Compose/Fusion/Vide/v1 프레임워크 비교 원자료 / provider/processor 네이밍 미정 등 열린 질문 메모
### lifecycle-pattern.md / purity-and-effects-plan.md
@ -106,9 +106,9 @@ v1 폐기 API/버그/구조 결함 전부 v2 설계를 정당화하는 내부
콘텐츠 작성 시점에 결정.
- skip: 세션 날짜/확정 이력, 문서 승격/정정 안내
### store-semantics.md / tween-plan.md / ui-shorthand-plan.md
### store-plan.md / source-state-plan.md / tween-plan.md / ui-shorthand-plan.md
- 초심자: Store 생성+`myStore.key:Set(value)` 문법 / `store.key`로 State 얻기 개념 / Tween 기본 바인드 키+취소 기본 동작 / UI 숏핸드 인라인 키 기본 예시(`Frame { UIPaddingOffset = 50 }`)
- api: `:With`+`:Compute` 시그니처(→심화) / `source:Emit()` 존재+"Get() 결과 캐시 금지" 캐비엇(버그 유발 포인트라 api에도 명시 가치 있음, →심화; 2026-08-06 후속 세션에서 `Store:Emit(key)`→`source:Emit()`로 호출부 변경, `store-semantics.md` 참고) / ~~Tween 핸들러가 Instance 직접 받음(Ref 불필요)~~ **[2026-08-13 정정] 구모델(폐기) — 실제로는 `Tween(opts)` 값-레벨 래퍼가 Property 자리에 놓이고 `PropertyHandler``isTween`으로 분기** / retract는 Destroy 시 호출 안 됨(→심화) / UI 숏핸드 키 목록 레퍼런스 표 / Modifier와 순수 인라인 키 동등성
- api: `:With`+`:Compute` 시그니처(→심화) / `source:Emit()` 존재+"Get() 결과 캐시 금지" 캐비엇(버그 유발 포인트라 api에도 명시 가치 있음, →심화; 2026-08-06 후속 세션에서 `Store:Emit(key)`→`source:Emit()`로 호출부 변경, `base/source-state-plan.md` 참고) / ~~Tween 핸들러가 Instance 직접 받음(Ref 불필요)~~ **[2026-08-13 정정] 구모델(폐기) — 실제로는 `Tween(opts)` 값-레벨 래퍼가 Property 자리에 놓이고 `PropertyHandler``isTween`으로 분기** / retract는 Destroy 시 호출 안 됨(→심화) / UI 숏핸드 키 목록 레퍼런스 표 / Modifier와 순수 인라인 키 동등성
- 심화: Source·Store·State·Observer 온톨로지(독립 프리미티브 vs 파생 데이터 원칙, 생성자 모양 근거) / `Emit`이 Source 전용인 이유(디버깅 그래프 무결성) / `Store<T>`의 T가 Modifier 불가인 이유 / ~~Tween을 반응 그래프 밖 특수 bind key로 둔 이유(Fusion 반면교사)~~ **[2026-08-13 정정] 이 근거 자체가 폐기된 구모델 서술 — 현재는 Tween이 반응 그래프 "밖"이 아니라 Property 값 타입 치환(`T|Tween<T>`)으로 자연스럽게 들어와 있음, `base/tween-plan.md` 참고** / RoundSize 포팅 불필요 vs UICorner/UIPadding/UIScale 필요 이유 / "작고 opt-in 아닌 편의 기능은 코어 포함" 원칙
- 열린 질문(문서화 보류): tween-plan.md의 오버라이드/삭제후재시작/끝점이동 옵션 키 이름 미정 / ui-shorthand의 RoundSize 완전 드롭 여부
- skip: 세션 정정 이력, v1 소스 조사 경위
@ -133,10 +133,10 @@ v1 폐기 API/버그/구조 결함 전부 v2 설계를 정당화하는 내부
8. 왜 "다중 루트 반환" 개념을 없앴는가 — `component-composition-plan.md`
9. 왜 Slot은 단일 마운트 소유권을 강제하는가(v1/Fusion/Vide 대비) — `slot-plan.md`, `comparison-fusion-vide.md`
10. ~~왜 Tween은 반응 그래프 밖에 있는가~~ **[2026-08-13 정정] 위 §2 심화 항목과 같은 stale 표현 — 실제로는 Property 값 타입 치환(`T|Tween<T>`)으로 그래프 안에 자연스럽게 있음** — `base/tween-plan.md`
11. 왜 `:Emit()`은 Source 전용이고 파생 State엔 없는가(호출부는 `source:Emit()`, 2026-08-06 후속 세션에서 `Store:Emit(key)`→이 형태로 정리) — `store-semantics.md`
12. 독립 프리미티브 vs 파생 데이터 — 생성자 모양을 결정하는 원칙 — `store-semantics.md`
11. 왜 `:Emit()`은 Source 전용이고 파생 State엔 없는가(호출부는 `source:Emit()`, 2026-08-06 후속 세션에서 `Store:Emit(key)`→이 형태로 정리) — `base/source-state-plan.md`
12. 독립 프리미티브 vs 파생 데이터 — 생성자 모양을 결정하는 원칙 — `base/source-state-plan.md`
14. 왜 컴포넌트는 전역 store를 직접 참조하면 안 되는가(이식성) — `purity-and-effects-plan.md`
15. 왜 Source가 State를 구조적으로 만족하는가(Svelte Writable/Readable과 같은 서브타입 모양, `RefSource`/`StoreSource` 중간안이 왜 기각됐는가) — `store-semantics.md`, `component-composition-plan.md`
15. 왜 Source가 State를 구조적으로 만족하는가(Svelte Writable/Readable과 같은 서브타입 모양, `RefSource`/`StoreSource` 중간안이 왜 기각됐는가) — `base/source-state-plan.md`, `component-composition-plan.md`
16. **State 파생 체인 동작 원리** — emit이 아래로 전파되고, `Get()` 요청이
위로 거슬러 올라가 재계산된 뒤 다시 아래로 내려오는 흐름을 명확히
설명(Blocker/Effect 둘 다 이 흐름 위에서 동작하므로 선행 이해로 필요)
@ -218,7 +218,7 @@ additional-primitives-plan.md`의 "문서화 백로그" 절이 원자료)**:
"PreRef는 '취소'라는 개념이 없다" 항목 참고.
7. **왜 `Compute(fn, ...)`는 여러 의존성을 편하게 받고 `Effect`/`Observer`는
안 받는가** (2026-08-11 세션 원자료, `bind-system-plan.md` "`:Compute(fn,
안 받는가** (2026-08-11 세션 원자료, `source-state-plan.md` "`:Compute(fn,
...)` — 추가 의존성을 trailing args로 직접 받는 sugar" 절) — 겉보기엔
비일관적인 API 표면(하나는 React `useMemo`식 trailing deps sugar를 받고,
다른 둘은 명시적 `:With` 호출을 강제)이 실은 "sugar가 새 노드 생성 비용을

View file

@ -113,7 +113,7 @@ UICorner/UIPadding/UIScale 숏핸드가 만드는 자식)는 `_`나 `QUAD_` 같
## 2. Store 부작용을 게임 시스템에서 깔끔하게 쓰는 패턴 문서
**배경**: quad의 Store는 부작용 허용이 기본 설계(`base/architecture.md`,
`base/store-semantics.md`에서 확정)인데, 실제 게임 개발(스킬, 쿨타임,
`base/store-plan.md`에서 확정)인데, 실제 게임 개발(스킬, 쿨타임,
재화 같은 도메인 상태를 담당하는 store, 부분별 모듈화 등)에서 이 자유도를
깔끔한 패턴으로 쓰는 법이 아직 문서화 안 됨 — 사용자 원 발언: "원래
의도했던 편한 부작용 허용을 좀더 더럽지 않은 패턴으로 수행하는 방법도
@ -166,7 +166,7 @@ UICorner/UIPadding/UIScale 숏핸드가 만드는 자식)는 `_`나 `QUAD_` 같
있으므로 여기선 소재 제목과 위치만 남김:
- **왜 State 체인을 Modifier처럼 플래튼하지 않는가** — 결정문은
`base/bind-system-plan.md` "왜 State 체인을 Modifier처럼 플래튼하지
`base/source-state-plan.md` "왜 State 체인을 Modifier처럼 플래튼하지
않는가" 절. State의 정의가 "캐싱하는 존재"라 다이아몬드 파생에서
캐시 공유를 잃으면 `previous` 메커니즘이 막으려던 중복 계산 문제가
재발한다는 게 핵심 논거.

View file

@ -46,7 +46,7 @@
- ~~Store dot-access가 매 접근마다 새 State를 할당~~**[해소됨,
2026-08-06 세 번째 세션]** 이 항목이 직접 트리거가 되어 Source/State
관계 자체를 재구성(`store-semantics.md` "Source가 State를 만족함" 절) —
관계 자체를 재구성(`base/source-state-plan.md` "Source가 State를 만족함" 절) —
Store가 이제 생성 시 만들어둔 Source를 그대로 반환해 wrapper 할당 자체가
없어짐, 구현 단계 최적화가 아니라 설계로 완전히 없앰(캐싱/풀링보다도 쌈).

View file

@ -83,7 +83,7 @@ price:Apply(addTaxAndShipping)
1. **재사용 가능한 커링 팩토리는 `:Compute`로는 안전하게 못 만든다 —
진짜 버그 가능성.** quad는 Vide식 암묵적 자동 추적을 이미 기각했음
(`bind-system-plan.md` "암묵적 자동 추적 기각") — 의존성은 오직
(`base/source-state-plan.md`의 "여러 값을 묶어 파생값 만들기" 절) — 의존성은 오직
`:With`/`:Compute`의 **그 호출문 자체**에 나열된 trailing args로만
등록됨. 그래서 `local addTax = Sum(tax, shipping)`처럼 한 번 만들어
재사용하고 싶은 값을 `price:Compute(addTax)`처럼 바로 꽂으면,
@ -93,14 +93,14 @@ price:Apply(addTaxAndShipping)
`price:Compute(addTax, tax, shipping)`처럼 이미 `Sum(...)`에 넘긴
deps를 호출부에서 또 나열해야 하는데, 이게 바로 2026-08-11 세션에서
"trailing deps를 fn 위치 인자로 노출"하게 만든 그 중복/드리프트
위험(`bind-system-plan.md` 해당 절)과 완전히 같은 클래스의 문제 —
위험(`base/source-state-plan.md` 해당 절)과 완전히 같은 클래스의 문제 —
재사용 가능한 이름을 만드는 의미 자체가 없어짐.
**`:Apply`는 이 문제가 원천적으로 없음**: factory가 내부에서
`self:Compute(fn, tax, shipping)`을 직접 호출해 자기가 캡처한 deps를
스스로 다시 넘기므로(호출자가 재입력하는 게 아니라 factory 자신이
한 번 캡처한 값을 그대로 전달), 중복 없이 안전하게 재사용됨 —
`price:Apply(addTax)`, `otherPrice:Apply(addTax)` 둘 다 안전.
2. **기존 문서 관용구와 일치.** `bind-system-plan.md`의 `:Apply` 절이
2. **기존 문서 관용구와 일치.** `base/source-state-plan.md`의 `:Apply` 절이
이미 `state:Apply(makeFormatter("ko-KR"))`를 "커링 팩토리 + `:Apply`"의
정석 예시로 들어둠 — `Operator.*`/`Animate`가 이 관용구를 따르는 게
자연스러움. `Animate``:Compute`를 골랐던 건 오히려 이 기존
@ -346,7 +346,7 @@ offset이 밀리고 당겨지는 케이스(이건 이미 "그냥 확인된 것"
이게 이 항목을 단순 슈가로 못 만드는 이유이자, 백로그에서 따로 더
파야 하는 지점:
- quad는 **암묵적 자동 추적을 기각**했고(`base/bind-system-plan.md`),
- quad는 **암묵적 자동 추적을 기각**했고(`base/source-state-plan.md`),
의존성은 `:With`**정적으로** 선언하게 돼 있음. 게다가 "`:With`의
동적 의존성 미지원"은 2026-08-12 열여덟 번째 세션에 **의도된
트레이드오프로 확정**됨(`research/framework-comparison-findings.md`) —

View file

@ -3,7 +3,8 @@
**상태**: research — 사용자 상의 필요. 2026-08-06 세션에서 신설. `.claude/base/`
전체가 "확정"으로 표시돼 있지만, 실제 `ROADMAP.md` M0 착수를 앞두고 구현자
시점에서 다시 크리티컬하게 훑어본 결과. 방법론: `base/` + 근접
`research/`(tween-plan, ui-shorthand-plan, existing-instance-bind-plan)를
`research/`(tween-plan, ui-shorthand-plan, existing-instance-bind-plan —
앞의 둘은 이후 `base/`로 승격, 마지막은 2026-08-14에 기각되어 `archive/`)를
4개 클러스터로 나눠 서브에이전트로 병렬 정독시키고, 각각 세 가지 렌즈로
리뷰하게 했음 —
@ -310,7 +311,7 @@ base 인터페이스가 그보다 늦은 M8에서 만들어지는 순서 역전.
**위치**: `ROADMAP.md` M0 vs M3 `"store.key dot-access 타입 추론 확인"`.
**문제**: M0의 정의 자체가 "추론만으로 확정하고 실제 Luau로 부딪혀본 적
없는 것"을 검증하는 단계다. `base/store-semantics.md`가 요청한 M0 항목(
없는 것"을 검증하는 단계다. `base/source-state-plan.md`가 요청한 M0 항목(
"Source가 State를 만족하는 제네릭 메소드 체이닝"의 솔버 안정성)은 이미
반영됐지만, 이건 `:Compute` 같은 제네릭 메소드 체이닝만 다루고 `{key:
Source<number>}` 같은 **레코드 필드로서의 dot-access 타이핑**(읽기/쓰기
@ -373,7 +374,7 @@ Luau의 현 한계로 확정되어 `base/typing-limits.md` 1번이 담당**함
아니라 narrow edge case)라 아래 원래 제안(M0 스파이크에 폴백 한 줄
박아두기)은 더 이상 적용 대상 없음. 원래 서술은 배경 기록으로 남김:
**위치**: `base/store-semantics.md` "Source가 State를 만족함" 절 —
**위치**: `base/source-state-plan.md` "Source가 State를 만족함" 절 —
"검증 필요(확정 아님, M0 스파이크 대상)... 다만 이것도 추론이라 실제
Luau로 확인 전엔 확정 아님."
@ -381,7 +382,7 @@ Luau로 확인 전엔 확정 아님."
실패했을 때 뭘 하는지가 문서 어디에도 없다.** 이 타입 구조(Source⊂State
서브타입)는 `store.key`의 반환 타입, `:Set()` 문법, `:Emit()`의 위치,
dot-access 타입추론, `RefSource` 폐기 결정까지 전부 이 위에 얹혀 있어서,
Luau 솔버가 막히면 되돌릴 범위가 `store-semantics.md`의 절반 이상에 걸침.
Luau 솔버가 막히면 되돌릴 범위가 `source-state-plan.md`의 절반 이상에 걸침.
**제안**: M0 스파이크 계획에 "실패 시 폴백은 RefSource 부활 vs 다른 대안"
한 줄이라도 미리 박아두면, 실패했을 때 다시 사용자 자문을 구하느라 멈추는
@ -392,7 +393,7 @@ Luau 솔버가 막히면 되돌릴 범위가 `store-semantics.md`의 절반 이
**[대부분 해소, 2026-08-09 세션]** "가능하면 타입 차단, 안 되면 UB로
후퇴"라는 원래 걱정 자체가 무의미해짐 — `State<Modifier>`를 이제
`isModifier` predicate 기반 명시적 `error`로 막기로 확정
(`base/modifier-plan.md` 7번 정정, `base/store-semantics.md` "따름정리"
(`base/modifier-plan.md` 7번 정정, `base/source-state-plan.md` "따름정리"
절)했으므로, 타입 차단은 성공하든 실패하든 런타임 에러라는 안전망이
항상 있음 — 아래 "제안"이 우려했던 "조용히 UB로 후퇴" 시나리오 자체가
발생하지 않음. 타입 차단이 Luau에서 실제로 가능한지는 여전히 미검증이지만,
@ -403,7 +404,7 @@ Luau 솔버가 막히면 되돌릴 범위가 `store-semantics.md`의 절반 이
**문제**: "가능하면 타입 시스템으로 아예 못 넣게 막을 것"이라 확정했지만,
Luau 제네릭은 "T가 특정 타입이면 거부"하는 부정 제약을 기본 지원하지
않는다. `store-semantics.md`는 이보다 단순한 `Source<T> satisfies
않는다. `source-state-plan.md`는 이보다 단순한 `Source<T> satisfies
State<T>` 조합조차 M0 스파이크 대상(솔버가 죽을 수 있음)으로 잡아뒀는데,
이보다 어려운 문제(제네릭 타입 파라미터 배제 제약)인 `State<Modifier>`
차단은 어디에도 검증 대상으로 언급되지 않는다. 실제로 안 되면 "UB,
@ -432,9 +433,15 @@ State<T>` 조합조차 M0 스파이크 대상(솔버가 죽을 수 있음)으로
고려하라고 명시해뒀으니, 그 시점에 이 질문도 같이 열어 "래퍼 없음"이
구조적으로도 최종 확정인지 한 번 더 확인할 것. M1 스캐폴딩 전에.
### 2-4. existing-instance-bind가 Slot의 "엄격한 단일 마운트 소유권" 불변식과 근본적으로 긴장
### 2-4. existing-instance-bind가 Slot의 "엄격한 단일 마운트 소유권" 불변식과 근본적으로 긴장**[해소됨, 2026-08-14 세션: 기능 자체가 기각]**
**위치**: `research/existing-instance-bind-plan.md` 전체 vs `base/
> **[해소]** 이 긴장은 "재바인드를 어떻게 안전하게 할 것인가"를 풀어서가
> 아니라 **그 기능이 통째로 기각**되어 없어졌음
> (`archive/existing-instance-bind-rejected.md`) — 아래 분석은 그 기각
> 판단을 뒷받침한 근거 중 하나로 보존. Slot 소유권 모델에 "흡수(adopt)"
> 개념을 추가하는 방향도 같이 폐기.
**위치**: `archive/existing-instance-bind-rejected.md` 전체 vs `base/
slot-plan.md` "핵심 제약: 소유권 귀속과 단일 마운트".
**문제**: 문서가 스스로 언급한 긴장(Modifier flatten의 clone 비용)과는
@ -451,10 +458,10 @@ Slot의 "own"한 대상인지 아닌지가 완전히 미정. 문서는 "핸들
API"를 얹어야 할 때 Slot의 "own한 것만 CRUD 대상" 불변식 자체를 건드려야
할 수 있음.
**제안**: 착수 안 해도 되지만, "이 기능이 실제로 필요해지면 Slot의
소유권 모델에 '흡수(adopt)' 개념을 추가해야 할 수도 있다"는 캐비엇을
`existing-instance-bind-plan.md`에 한 줄 추가해둘 것 — 기존 "Modifier
flatten과 긴장" 캐비엇 옆에 병기.
**당시 제안(이제 불필요)**: "이 기능이 실제로 필요해지면 Slot의
소유권 모델에 '흡수(adopt)' 개념을 추가해야 할 수도 있다"는 캐비엇을
문서에 병기해둘 것 — **2026-08-14 기각으로 대체됨**(캐비엇 대신 기각
사유가 `archive/existing-instance-bind-rejected.md` 상단 배너에 있음).
### 2-5. `Modifier.Overridden`(구 `Merge`) 시 서로 다른 클래스의 Modifier가 섞이는 게 허용되는지 — **런타임은 해소, 타입 레벨은 여전히 미정**
@ -466,7 +473,7 @@ flatten과 긴장" 캐비엇 옆에 병기.
계층을 모름 — 순수 데이터 merge 레이어"(1번 절) 원칙도 이미 있었으므로,
**런타임 레벨에서는 대상 클래스가 다른 Modifier끼리 `Overridden`해도 막을
이유가 없음**(필드명만 보고 그대로 덮어쓸 뿐 — Luau 타입은 런타임에
강제되지 않는다는 점은 `store-semantics.md`에도 이미 명시된 전제).
강제되지 않는다는 점은 `store-plan.md`에도 이미 명시된 전제).
**여전히 미정인 것 — 타입 레벨**: Modifier가 target 클래스별 제네릭
타입(`Modifier<Frame>` 등)이라면, `Modifier.Overridden<T>(mod1: Modifier<T>,
@ -606,10 +613,10 @@ M11 착수 시.
감싸야 하는 준비 비용이 오히려 더 크다는 게 사용자 반박 논거.
`previous``self`(입력)가 아니라 이 `:Compute` 호출 하나가 만든
결과 State 노드 자신에 귀속되므로 팬아웃 시에도 충돌 없음 — 상세는
`base/bind-system-plan.md`의 "previous" 절 참고. 아래는 원래 발견
`base/source-state-plan.md`의 "previous" 절 참고. 아래는 원래 발견
당시 기록.
**위치**: `base/bind-system-plan.md` "`:Compute(fn)`의 선택적 두 번째
**위치**: `base/source-state-plan.md` "`:Compute(fn)`의 선택적 두 번째
인자 — `previous`" 절.
**문제**: quad는 "렌더 함수가 계속 재실행되지 않고, `Compute`에 전달한
@ -651,7 +658,7 @@ Handler"라고만 서술해, 사실상 3개의 거의 동일한 형태(리터럴
### 이미 고침 (이번 세션)
- `architecture.md`의 "Store/State/Source 온톨로지 확정 요약" 절이
`store-semantics.md`의 최신 재구성(Source가 State를 만족, `store.key`
`source-state-plan.md`의 최신 재구성(Source가 State를 만족, `store.key`
Source를 직접 반환, `store.key:Set()`)을 못 따라가고 있던 것 — 이 감사
세션 도중 발견해 직접 정정(커밋 `4b839b0`에서 별도로 이미 반영됨을 뒤늦게
확인 — 같은 문제를 두 세션이 독립적으로 발견한 셈).
@ -679,7 +686,7 @@ Handler"라고만 서술해, 사실상 3개의 거의 동일한 형태(리터럴
들어오는 것도, `State<Modifier>`처럼 Modifier가 State/Source *값*으로
담기는 것도 전부 `Brand` 기반 `isX` predicate로 런타임에 즉시
`error`(`base/modifier-plan.md` 4번/7번 절 정정, `base/
store-semantics.md` "따름정리" 절). 더 이상 "한쪽만 방어" 비대칭이
source-state-plan.md` "따름정리" 절). 더 이상 "한쪽만 방어" 비대칭이
아님 — 남은 차이는 `State<Modifier>` 쪽에 "되면 좋은 보너스"로
타입 차단을 추가 시도해볼 여지가 있다는 것뿐(위 2-2번, 미검증이지만
더 이상 필수 방어선이 아니라 우선순위 낮음).

View file

@ -0,0 +1,137 @@
# 2026-08-14 일곱 번째 세션 — UI 숏핸드 Tween 지원, existing-instance-bind 기각, `bind-system-plan.md` 3단계 분할
사용자가 한 메시지에 세 건을 지시했고, 세 번째(문서 분할)에 대해서는
"이 분리가 합당한가"를 먼저 판단해 달라고 요청함.
## 1. `ui-shorthand-plan.md`에 Tween 지원 추가
**사용자 제안 원문 요지**: "ui-shorthand-plan 은 이제 Tween 을 지원하게
되어야할듯. 이건 process 상 똑같이 활용하면 되는 부분으로 보임 → 프로세스
중 inst 를 바꾸는것이 UB 는 아님. 키도 바뀔 수 있는거 처럼, 따라서
인스턴스 메니징 생성 후 이것을 process 처리하는것으로 해결."
확인 결과 지적이 정확했고, 오히려 **기존 서술이 stale**이었음:
`ui-shorthand-plan.md`의 store-bind 절이 "트윈처럼 애니메이션까지 지원할
필요는 없음(API 표면만 복잡해짐)"이라고 못박아뒀는데, 그 판단의 전제는
Tween이 **독립 Dispatch 핸들러**(우선순위를 다투는 특수 bind key)였던
시절이었음. 2026-08-10 세션에 Tween이 값-레벨 래퍼 `Tween<T>` +
`PropertyHandler` 내부 분기로 재설계되면서(`base/tween-plan.md`,
`archive/tween-special-bind-key-reversed.md`) 그 비용이 통째로 사라졌는데
이 문서만 안 따라와 있었던 것 — 즉 "새 기능 추가"가 아니라 **역전 반영**.
확정된 메커니즘:
- 숏핸드 Handler의 `process`가 자식(`_quad_corner`류)을 찾거나 만든 뒤,
프로퍼티를 **직접 대입하지 않고** `Dispatch.process(child, "CornerRadius",
..., 1)`로 되돌려줌.
- `chains``(inst,k)` 쌍으로 인덱싱되므로 "다른 `inst`로 위임"은 Dispatch
입장에서 "다른 `k`로 위임"(Attribute 그룹이 이미 하는 것)과 **구조적으로
완전히 같은 일** — 사용자가 말한 "키도 바뀔 수 있는 것처럼"이 정확히
이것. 이 일반 규칙을 `base/dispatch-core-plan.md`의 "인덱스의 의미" 절에
새 불릿으로 명문화(위임 대상 자식의 **수명 책임은 위임한 핸들러**에
있다는 단서 포함).
- 이걸로 Tween 해석 코드가 `PropertyHandler` 하나에만 존재한다는
`tween-plan.md`의 불변식이 유지됨 — 3-상태 릴레이션 슬롯, `Tween.Cancel`/
`Tween.Finish` override 정책, "첫 세팅은 애니메이션 없이 즉시"가 전부
`(child, prop)` 자리에서 그대로 재사용.
**이 세션에 새로 발견해 채운 부품 하나** — 숏핸드는 스칼라를 자식 프로퍼티
타입으로 감싸는 `wrap`(`UICorner = 8` → `CornerRadius = UDim.new(0,8)`)을
갖고 있는데, `v``Tween<number>`면 그 변환을 **`Tween`을 벗기지 않고
`.Value`에만** 적용해야 함. `table.clone``Value`만 갈아끼워 `Tween(opts)`
재브랜딩하는 `mapTweenValue(v, wrap)` 헬퍼로 해결(그 자체가 새 메커니즘은
아니고, `Tween<T>`가 immutable 값 객체라는 기존 성질의 사용).
부수적으로 정리한 것:
- `UIPadding`처럼 자식 프로퍼티 **여러 개**에 같은 값을 쓰는 키는 프로퍼티마다
`Dispatch.process`를 따로 부름 → 각자 독립 체인, 트윈 슬롯도 따로.
열린 질문 절의 룩업 테이블 스케치도 `Property``Properties`(목록)로 정정.
- 캐비엇: 자식이 파괴/재생성되는 사이클에서는 `prev == nil`(첫 세팅) 규칙
때문에 트윈이 안 걸리고 스냅됨 — 버그가 아니라 그 규칙이 막으려는 진입
애니메이션과 정확히 같은 상황이라 의도된 동작으로 문서화.
- **ROADMAP에 UI 숏핸드 항목 자체가 없던 갭 발견**`ui-shorthand-plan.md`
"M10 전후로 구현" 이라고 이미 지정해뒀는데 체크리스트엔 한 줄도 없었음.
M10 끝에 구현 포인트 6개와 함께 신규 추가.
## 2. `existing-instance-bind-plan.md` 기각 → `archive/`
**사용자 판단**: "이게 가능하다 하면 offset source 나 length source 등을
밀고 당기고 하는 많은 부가적 작용을 가능케 하고, 버그를 일으키기에 치명적
표면을 많이 노출시킴."
오래 "미지원으로 확정하지는 않고 열린 가능성으로 유지"였던 항목 —
`archive/existing-instance-bind-rejected.md`로 이전하고 기각 사유 배너를
달았음. 배너에 같이 적어둔 것: quad가 만들지 않은 Instance의 자식 구성
변화까지 추적하려면 Instance 가상화가 필요한데, 그건
`research/framework-comparison-findings.md`가 use-after-destroy 안전망을
기각할 때 쓴 것과 같은 이유(rbvm 같은 전문 라이브러리의 영역)로 스코프 밖.
**파생 정리(체크리스트 2번 — 배너가 부정하는 본문을 같은 커밋에서 고칠 것)**:
경로만 바꾸면 되는 게 아니라 "열려 있음"을 전제로 쓰인 문장이 여러 곳에
있었음.
- `base/architecture.md` — "아직 미정(research/로 분리됨)" 절에 남아있던
**유일한 항목**이 이거였음 → 절 자체를 "이제 아키텍처를 미정으로 남기는
항목은 없음"으로 갱신. 소스 트리 절의 "여전히 research/에 남아있고"도 정정.
- `base/ref-plan.md` — PreRef fire를 `flatten`에 얹지 않기로 한 근거가 "재바인드
때문에 flatten이 여러 번 재호출될 수 있어서"였음 → 그 위험은 사라졌지만
결론(기각)은 유지되므로 근거를 "flatten은 `inst`를 모르는 순수 변환이라"로
교체.
- `base/relate-plan.md` — "quad 밖 Instance를 `Relate` 키로 쓰는 건 UB" 따름
정리는 유지하되, 그 UB를 실제로 건드릴 뻔한 유일한 경로가 닫혔음을 명시.
- `base/modifier-plan.md`(Default→실값 flatten 미정 문제), `base/dispatch-core-plan.md`
(인덱스 체인의 "미래 재바인드에 유리" 부수 효과 서술 — quad-debug/자기
인스턴스 재발행 근거만 남기고 재프레이밍), `research/pre-implementation-audit.md`
2-4(Slot 단일 마운트 소유권과의 긴장 → "기능이 기각되어 해소"),
`question.md`(항목 제거) + `archive/question-resolved.md`(해소 기록),
`ROADMAP.md` 백로그(항목 제거), `CLAUDE.md` 2곳.
## 3. `bind-system-plan.md` 3단계 분할 + `store-semantics.md` 흡수
**사용자 지시**: "bind 쪽에서 state 를 분리해야한다고 생각함.
store-semantics.md bind-system-plan.md 둘에서 정보를 추합해서 문서정리를
수행해. Store가 Store를 저장 가능한가 → 이런것 같은건, store-plan.md
문서로 합쳐 신설. 이젠 Store 는 source 를 여럿 담고, 없으면 만들어주는
도구일 뿐이라, 쉽게 분리가능. state/source 는 아주 가까운 요소라
source-state-plan.md 로 저장하면 될것 같음. 이 분리가 합당한거 같아?"
**판단: 합당함.** 근거 셋 —
1. 두 문서가 실제로 같은 주제를 반씩 나눠 갖고 **서로를 "상세는 저쪽 참고"로
가리키는 핑퐁**이 여러 곳에 있었음(온톨로지, `store.key`가 Source를 반환,
`:Set()` 전환, Slot 생존 확인이 각각 양쪽에 반씩). 어느 쪽이 정본인지
문장마다 달라서 읽는 사람이 매번 두 파일을 왕복해야 했음.
2. Store가 "이름 붙은 Source 모음, 그 이상 아님"으로 좁혀진 게 2026-08-06에
이미 확정 — 사용자 말대로 **분리 가능해진 지 오래**였고, 남은 Store 고유
내용(부작용 정책, `defaults` 템플릿, eager/lazy 생성, dot-access 타이핑,
`type function`, "Store가 Store를 담는가")은 반응형 코어와 의존 관계가 거의
없음.
3. `Source`⊇`State`는 구조적 서브타입이라 **한 파일이 맞음** — 쪼개면
`:With`/`:Compute` 반환 타입 설명이 두 파일에 중복됨.
**한 가지 캐비엇을 같이 보고함**: 분할 후 `bind-system-plan.md`에 남는 건
인스턴스 생성/이벤트 네이밍 인체공학 + 팩토리 주입 + 색인뿐이라(1238줄 →
203줄) 파일 이름이 내용보다 넓어짐. 리네임은 참조 churn이 커서 **이번엔 안
하고** 제목만 "인스턴스 생성·이벤트 네이밍 인체공학 + 분할 색인"으로 바꿔둠.
결과 줄 수: `bind-system-plan.md` 1238→203, `store-semantics.md` 346→삭제,
`source-state-plan.md` 1148 신설, `store-plan.md` 224 신설(합 1584→1575,
사실상 순수 이동).
### 참조 스윕
`store-semantics.md`**`doc-check.py``OURS` 패턴에 안 걸리는 이름**
(`-plan`/`-reversed` 등으로 안 끝남)이라, 삭제해도 ERROR가 아니라
"외부 문서명일 수 있음" WARN으로만 잡히는 걸 발견 — 그래서 ERROR 목록만
믿지 말고 `grep`으로 전수(라이브 문서 40여 곳)를 직접 훑어 고침. 절 참조는
스크립트를 하나 짜서 "옛 파일에 없고 새 파일에 있는 절 제목"을 자동으로
재지정하고, 의역 인용 9건만 손으로 정리.
최종 `doc-check.py`: **ERROR 0, WARN 85**(작업 전 101 — 분할로 새로 생긴
것을 다 닫고, 겸사겸사 기존 의역 WARN 몇 개도 같이 정리해서 오히려 줄었음).
### 동시 세션 주의
`.claude/worktrees/debounce-throttle-plan/`에 다른 세션의 워크트리가
`.claude/` 전체 복사본을 갖고 있음 — 이번 분할이 그쪽 사본에는 반영되지
않았으므로, 그 세션이 메인에 병합할 때 `store-semantics.md`/
`bind-system-plan.md`를 가리키는 참조가 되살아날 수 있음. 병합 시
`doc-check.py`를 반드시 다시 돌릴 것.

View file

@ -31,7 +31,7 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션
이전에 시도했다 폐기한 v2 재작성 시도(`.claude/initreq/quad2-try`)도 리서치
완료 — OOP 상속/커스텀 파서/Slot 스텁/`Pipe` copy-on-write 절충안은 확인된
죽은 접근이라 반복 조사 금지(`base/bind-system-plan.md` 참고).
죽은 접근이라 반복 조사 금지(`base/bind-system-plan.md` "확정된 것" 절 참고).
## 계획 문서 구조
@ -42,13 +42,14 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션
base 문서가 근거로 인용하는 온디맨드 참고 자료(v1 내부 동작 스냅샷,
Fusion/Vide 비교 리서치) — 항상 읽을 필요는 없고 인용될 때만 열어볼 것.
- `.claude/research/` — 아직 착수 전, 사용자와 상의 필요한 설계 논의.
`existing-instance-bind-plan.md`/`debug-tooling-plan.md`/
`debug-tooling-plan.md`/
`documentation-plan.md`/`documentation-content-map.md`/
`framework-comparison-findings.md`/`additional-primitives-plan.md`(2026-08-09
세 번째 세션에 마지막 열린 항목까지 전부 해소, 이제 배경 자료용)/
`pre-implementation-audit.md`/`v1-compat-plan.md`
— 전부 후순위(`tween-plan.md`는 2026-08-12 세션에 마지막 열린 항목까지
전부 해소돼 `base/`로 승격, 더 이상 여기 없음). 최신 목록·우선순위는
전부 해소돼 `base/`로 승격, 이미 생성된 인스턴스 재바인드는
2026-08-14 세션에 기각돼 `archive/existing-instance-bind-rejected.md`로 이전, 더 이상 여기 없음). 최신 목록·우선순위는
`.claude/README.md`가 소스, 여기서 개수 반복 안 함(과거에 "두 개뿐"이라
적어놨다가 새 문서 추가될 때마다 안 갱신되는 패턴이 반복돼서 아예 안
세기로 함).
@ -230,8 +231,9 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션
`canExecute`(3순위 — `isAlive`는 검토 후 기각, `can` 계열 접두 유지
방향으로 기울었으나 구체 대안 미정), `Brand`(3순위), `Tag`/`Added`/
`Removed`/`Merged`(3순위), `Attribute`/`AttributeKey`(3순위).
3. `research/existing-instance-bind-plan.md`는 급하지 않음 — 스코프 논의만
필요, 구현 착수를 막지 않음.
3. **[2026-08-14 세션에 해소]** 오래 열려 있던 "이미 생성된 인스턴스
재바인드"는 **기각**되어 `archive/existing-instance-bind-rejected.md`
이전됨 — 더 이상 상의할 스코프 항목이 아님.
4. **[백로그]** 범용 렌더 디버깅 도구 `quad-mock`(Tween mock 등 동적 동작
지원, M0 mock 테스트 하네스와는 별개), 런타임 디버깅 플러그인
`quad-debug`(Studio 플러그인, 실물 Instance→코드 위치 역추적 — 채널
@ -946,7 +948,7 @@ Slot/Attribute)의 private `Relate` 상태 저장소가 대거 줄어듦(process
반환하는 클로저가 upvalue로 직접 캡처하므로 process→retract 사이 단발성
handoff용 저장이 불필요해짐 — `Relate`는 여러 위치/사이클을 가로지르는
누적 상태에만 남음). `bind-system-plan.md`/`tag-plan.md`/
`attribute-plan.md`/`slot-plan.md`/`architecture.md`/`store-semantics.md`/
`attribute-plan.md`/`slot-plan.md`/`architecture.md`/store-semantics.md(현 `store-plan.md`/`source-state-plan.md`)/
`modifier-plan.md` 전부 반영, `archive/checkpoint-handler-pattern-reversed.md`
신설.
@ -1234,3 +1236,33 @@ PreRef pre-pass 한 스윕에서 `isPostRef`도 같이 소진해 `postRefList`
잠시 보류했다가, 그 세션이 정리된 뒤 이 항목을 원래 자리(세 번째)에서
지금 자리(여섯 번째)로 옮기고 번호를 재조정 — 동시 편집 충돌 시
"내용은 안 섞여도 순서/번호가 꼬일 수 있다"는 사례로 남김.
**2026-08-14 일곱 번째 세션 — UI 숏핸드 Tween 지원, existing-instance-bind
기각, `bind-system-plan.md` 3단계 분할(`store-plan`/`source-state-plan` 신설)**
(`session/2026-08-14-07-store-source-split-shorthand-tween.md`)
사용자가 한 메시지로 세 건 지시. (1) **UI 숏핸드 Tween 지원** — 새 기능
추가가 아니라 **역전 반영**이었음(`ui-shorthand-plan.md`가 "트윈까지 지원할 필요 없음"이라 못박아둔 것은 Tween이 아직 독립 Dispatch 핸들러이던 시절 판단인데 2026-08-10
값-레벨 래퍼 재설계를 안 따라와 있었음). 확정 메커니즘은 사용자 제안 그대로
— 숏핸드가 자식을 만들거나 찾은 뒤 프로퍼티를 **직접 대입하지 않고**
`Dispatch.process(child, prop, ..., 1)`로 위임하면 Tween이 공짜로 따라옴
(해석 코드는 `PropertyHandler` 하나에만 남는 불변식 유지). "process 중
`inst`를 바꾸는 것은 키를 바꾸는 것과 같은 층위라 UB 아님"을
`dispatch-core-plan.md`에 일반 규칙으로 명문화(위임한 자식의 수명 책임은
위임한 핸들러). 새로 필요한 부품은 스칼라→프로퍼티 `wrap``Tween<T>.Value`
에만 적용되도록 들어올리는 헬퍼 하나뿐. **ROADMAP M10에 UI 숏핸드 항목이
통째로 빠져 있던 갭도 발견·보강.** (2) **`existing-instance-bind` 기각** —
"열린 가능성"에서 미지원 확정으로, `archive/existing-instance-bind-rejected.md`
(사유: Length/Offset 등 quad가 만든 트리를 전제한 부기를 바깥에서 밀고
당기는 버그 표면이 치명적으로 넓어짐). "열려 있음"을 전제로 쓰인 본문
문장 7곳(특히 `architecture.md`의 "아직 미정" 절 — 유일 항목이었음,
`ref-plan.md`의 flatten 기각 근거)까지 같은 커밋에서 정정.
(3) **문서 분할** — 사용자가 합당성 판단을 먼저 요청했고, 두 문서(`bind-system-plan.md` + store-semantics.md)가 같은
주제를 반씩 나눠 갖고 서로를 "상세는 저쪽" 핑퐁하던 게 실재해 **합당하다고
판단 후 수행**: `base/store-plan.md`(Store=이름 붙은 Source 모음)와
`base/source-state-plan.md`(반응형 코어) 신설, store-semantics.md는 완전
흡수되어 삭제, `bind-system-plan.md`는 1238→203줄(인스턴스 생성·이벤트
네이밍 + 분할 색인만). 캐비엇으로 "남은 내용보다 파일 이름이 넓어졌지만
리네임 churn이 커서 이번엔 제목만 변경"을 보고. **교훈**`doc-check.py`
`OURS` 패턴이 `-plan`류 접미사 기준이라 store-semantics.md 같은 이름은
삭제해도 ERROR가 아니라 WARN으로만 잡힘, 그래서 ERROR 목록만 믿지 말고
grep 전수를 같이 돌려야 함. 최종 ERROR 0 / WARN 85(작업 전 101).

View file

@ -34,7 +34,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
- [ ] Source가 State를 구조적으로 만족하는 제네릭 타입(`:Compute<U>(self:
Source<T>, ...) -> State<U>`류, self 타이핑 + State 참조 혼합)이
Luau 솔버에서 안전하게 추론되는지 확인(2026-08-06 세 번째 세션,
`base/store-semantics.md` "Source가 State를 만족함" 절 — `State<T>`
`base/source-state-plan.md` "Source가 State를 만족함" 절 — `State<T>`
`Source`를 참조하지 않는 단방향 의존으로 두면 위험한 상호 재귀는
피할 수 있어 보이나 실제 검증 전엔 확정 아님)
- [ ] `process`(+반환 retractor 클로저) 재귀 재-process 디스패치를 실제로
@ -240,7 +240,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
타이핑" 절) — 실제 문법이 통과하는지는
`luau-test``16-type-store-key-typefunction.luau`로 실측 필요
- [ ] `:Compute(fn, ...)` — trailing args로 추가 의존성 직접 받는 sugar
(2026-08-11 세션, `base/bind-system-plan.md` "`:Compute(fn, ...)`"
(2026-08-11 세션, `base/source-state-plan.md` "`:Compute(fn, ...)`"
절) — `:With(...):Compute(fn)` 체인과 달리 노드 1개(Compute 노드
자신에 구독만 추가)로 끝나야 함, 새 노드 생성 없이 구현되는지 M0/M3
스파이크에서 확인. `Effect`/`Observer`는 대칭 sugar 없이 `:With` 명시
@ -249,7 +249,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
previous?, dep1, ..., depN)` — 순서는 Luau 값 레벨 `...`가 파라미터
리스트 맨 끝이어야 하는 것과 같은 이유로 `previous?`가 deps 팩
**앞**에 와야 함, 2026-08-11 후속 세션 제안 → 같은 날 세 번째
세션에 순서 정정, `base/bind-system-plan.md` "trailing deps를 fn에
세션에 순서 정정, `base/source-state-plan.md` "trailing deps를 fn에
lazy positional 인자로도 노출" 절) — 방향/순서는 확정,
`luau-test``15-type-compute-trailing-deps-typepack.luau`
이형 다중 deps를 제네릭 타입 팩으로 표현 가능한지만 실측 필요(안
@ -257,7 +257,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
- [ ] `Blocker.luau`(`base/blocker-plan.md` 참고 — 여러 Source를
한꺼번에 바꿔도 파생값 재계산/재대입이 한 번만 되게 하는 primitive,
State와 밀접히 연관돼 있어 같은 마일스톤에서 개발)
- [ ] `state:Apply(factory)`(`base/bind-system-plan.md` "`state:Apply(factory)`"
- [ ] `state:Apply(factory)`(`base/source-state-plan.md` "`state:Apply(factory)`"
절, 2026-08-07 일곱 번째 세션) — `factory(self)`를 체이닝 문법으로
부르는 순수 설탕, `factory: (State<T>) -> U): U`로 열린 타입. Source도
기존 `:With`/`:Compute` 델리게이션에 얹혀 자동 포함
@ -273,8 +273,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
마지막 cleanup을 1회 트리거해야 함(2026-08-07 일곱 번째 세션)
- [ ] Observer/Effect 이중 바인딩 금지 — `canExecute(value)` 게이트로
`:Subscribe()`(전역)와 `bindLifetime`(inst-scoped, leaf 부착도
내부적으로 이걸 호출)이 동시에 걸리면 즉시 `error`(`base/
bind-system-plan.md` "이중 바인딩 금지" 절, 2026-08-07 일곱 번째
내부적으로 이걸 호출)이 동시에 걸리면 즉시 `error`(`base/source-state-plan.md` "이중 바인딩 금지" 절, 2026-08-07 일곱 번째
세션 신설, 2026-08-09 여섯 번째 세션에서 "leaf 부착=bindLifetime
호출"로 정정 — 진짜 독립 경로는 둘뿐).
**[역전, 2026-08-14 다섯 번째 세션] 별도 predicate `canBound(handle)`
@ -681,6 +680,18 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
불필요(클로저가 `v`를 직접 캡처). `HANDLER_PRIORITY_FALLBACK`으로
등록 — 2026-08-12 열한 번째 / 2026-08-13 네·다섯·열네 번째 세션,
`base/tag-plan.md`)
- [ ] **[2026-08-14 세션에 누락 발견, 신규]** `quad-roblox/Handlers/
InstanceShorthand.luau` — UI 편의 숏핸드 `UICorner`/`UIPadding`
(+`UIPaddingOffset`)/`UIScale`(`base/ui-shorthand-plan.md`). 이
마일스톤 전후로 구현하기로 그 문서가 이미 지정해뒀는데 체크리스트에
항목 자체가 없었음. 구현 포인트: (a) 재사용 대상은 quad가 만든 고정
이름(`_quad_corner`류) 자식으로 한정, (b) `v == nil`이면 그 자식 제거,
(c) **자식 프로퍼티는 직접 대입하지 말고 `Dispatch.process(child,
prop, wrapped, 1)`로 위임** — 이걸로 Tween이 공짜로 따라옴(해석은
`PropertyHandler` 하나에만 남음), (d) 스칼라→프로퍼티 타입 `wrap`
`Tween<T>``.Value`에만 적용되도록 들어올릴 것, (e) `UIPadding`
자식 프로퍼티 4개에 각각 위임, (f) 자식을 없앨 때 `retractFrom(child,
prop, 1)`도 같이. M11(Tween) 이후에 하면 (c)~(d)를 바로 검증 가능
## M11 — Tween
@ -728,8 +739,6 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
## 백로그 (스코프 밖 — 필요성이 실제로 드러나면 그때 설계)
- [ ] `research/existing-instance-bind-plan.md` — Modifier 정적 flatten과
긴장 관계 있음, 재검토 시 그 문서부터 다시 볼 것
- [ ] 범용 렌더 디버깅 도구로서의 quad-mock(Tween mock 등 동적 동작 포함,
M1의 quad-base 테스트용 mock과는 별개)
- [ ] `quad-debug`/`quad-debug-roblox-plugin` — 실물 Instance→코드 위치