decide(base): Relate 프리미티브 신설, bindLifetime/canExecute 확정, retract 필수화

- Relate(inst-weak 릴레이션, SetWeak/GetWeak/SetStrong/GetStrong, 비싱글톤)
  신설 — base.perInstanceState(inst) placeholder를 정식 대체
- bindLifetime(inst,value)/canExecute(inst,value) 탑레벨 함수로 확정,
  Relate 위에 구현(gcconn/gchold), LifetimeHandle.bind식 네임스페이싱 기각
- canExecute 시그니처를 (handle)에서 (inst,value)로 재정정 — Observer 자신의
  Subscribed 상태를 먼저, inst의 gcconn.Connected를 그 다음 확인
- store-bind 재실행 구독 메커니즘 = state:Observer(fn):Subscribe() 재사용으로 명문화
- 핸들러 계약: retract 필드는 no-op이라도 항상 정의(생략 시 핸들러 교체
  순간 크래시) — 확정
- question.md/ROADMAP.md/architecture.md/README.md 전체 동기화
This commit is contained in:
qwreey 2026-08-08 00:54:48 +09:00
parent 33790df6ee
commit c865e99860
Signed by: qwreey
GPG key ID: D28DB79297A214BD
10 changed files with 395 additions and 73 deletions

View file

@ -39,6 +39,7 @@
| `ui-shorthand-plan.md` | **[2026-08-07 `research/`에서 승격]** `UICorner`/`UIPadding`/`UIScale` 인라인 편의 키 — 이름(v1 `Corner`/`PaddingAll`/`Scale`에서 Modifier 필드명과 안 겹치게 `UI` 프리픽스로 확정)·메커니즘(Handler)·패키지 배치(quad-roblox 코어)·store-bind 가능성까지 전부 확정. 이미지 라운드 트릭(`RoundSize`)은 드롭 — `archive/ui-shorthand-roundsize-dropped.md` 참고. `v=nil`이면 `process` 자신이 만든 자식 제거(`retract` 아님) |
| `tag-plan.md` | **[2026-08-07 여덟 번째 세션 신설]** `[Tag "Name"] = boolean``CollectionService` 얇은 래퍼, `process`가 add/remove 전부 처리, `retract` 불필요 |
| `attribute-plan.md` | **[2026-08-07 여덟 번째 세션 신설]** `[Attribute "Name"]``SetAttribute(name, nil)`이 네이티브 지우기라 `None` 센티널과 가장 깔끔하게 맞아떨어짐, `retract` 불필요. 타입 파라미터화 이름(`Attribute<T>` vs `BooleanAttribute`류)만 미확정 |
| `relate-plan.md` | **[2026-08-08 신설]** `Relate``inst`를 weak 키로 하는 범용 릴레이션 프리미티브(`SetWeak`/`GetWeak`/`SetStrong`/`GetStrong`, 비싱글톤 생성자). 구 `base.perInstanceState(inst)` placeholder를 대체·정식 승격, `lifecycle-pattern.md``bindLifetime`/`canExecute`가 그 위에 얹힘 |
## `reference/` — 온디맨드 참고 자료 (2026-08-07 신설)

View file

@ -133,8 +133,8 @@ quad/
│ │ ├── Handler.luau # 핸들러 계약 타입(isHandlable/priority/process/retract)
│ │ ├── StoreBind.luau # store 값 재귀 재실행 로직(범용, 엔진 무관)
│ │ └── Slot.luau # add/remove/clear 재조정 로직(추상 자식 참조 기준)
│ ├── LifetimeHandle.luau # Connected 계산 속성 "인터페이스"(타입/계약만)
│ ├── PerInstanceState.luau # per-instance 상태 저장 "인터페이스"
│ ├── Relate.luau # inst를 weak 키로 하는 범용 릴레이션(`SetWeak`/`GetWeak`/`SetStrong`/`GetStrong`), 비싱글톤 생성자(`base/relate-plan.md`) — 구 PerInstanceState/perInstanceState 대체
│ ├── LifetimeHandle.luau # `bindLifetime(inst,value)`/`canExecute(inst,value)` 탑레벨 함수 "인터페이스"(타입/계약만), 내부는 Relate 사용(`base/lifecycle-pattern.md`)
│ ├── Ref.luau # 범용 값 박스(.Value 읽기 + :Set()/:Callback()/:Wait() 셋), `Ref(default)`를 children 배열 숫자 슬롯에 직접 놓으면 (v=Ref) 매치 핸들러가 바인드 — 별도 CreatedRef 래퍼 없음
│ ├── PreRef.luau # Ref 런타임 재사용 + children 배열 전용, Modifier/Store 타입 차단, 호이스팅되는 pre-pass 특수화(별도 파일, `bind-system-plan.md` "PreRef 신설" 절, 2026-08-07 여섯 번째 세션에서 분리)
│ └── init.luau
@ -142,8 +142,7 @@ quad/
├── wally.toml
└── src/
├── RobloxFactory.luau # BaseModule 뮤테이션, 재호출 가드(같은 팩토리=무시/다른=에러)
├── LifetimeHandle.luau # 실제 구현(Instance 생존 확인)
├── PerInstanceState.luau # 실제 구현(weak-keyed table, Instance 키)
├── LifetimeHandle.luau # bindLifetime/canExecute 실제 구현 — GetPropertyChangedSignal("ClassName") 연결 트릭으로 gcconn 확보, Relate:SetStrong으로 gcconn/gchold 저장(`base/lifecycle-pattern.md`). Relate 자체는 순수 Lua라 quad-roblox 쪽 재구현 없음(quad-base 그대로 재사용)
├── Handlers/
│ ├── Property.luau
│ ├── Event.luau # ReflectionService 기반 자동 판별

View file

@ -45,6 +45,14 @@ v1의 `ProcessQuadProperty`(`.claude/initreq/quad/src/class.lua:134-214`)는
- `retract(inst, key, value)` — 이전 처리를 무르는/멈추는 함수(아래 절,
`base/lifecycle-pattern.md` 참고). 모든 핸들러가 의미 있게 구현할 필요는
없음(예: 일반 프로퍼티 핸들러는 보통 no-op).
**`retract` 필드 자체는 생략 불가, no-op이라도 항상 정의할 것(2026-08-08
세션, 확정)** — `Dispatch.process`(아래 "확정된 디스패치 모델" 절)는
담당 핸들러 *타입*이 바뀔 때 이전 핸들러의 `retract`를 nil 체크 없이
무조건 호출함. 필드를 생략한 핸들러가 나중에(드물더라도, 예: Tween↔
프로퍼티 교체) 실제로 담당이 바뀌는 순간 `attempt to call a nil value`
바로 크래시 — "의미 있게 구현할 필요 없음"은 "구현이 사소해도 됨"이라는
뜻이지 "필드를 안 둬도 안전하다"는 뜻이 아님. 새 핸들러를 짤 때 이 필드가
없으면 리뷰/린트에서 걸러내야 함(M2 착수 시 확인 목록에 추가).
디스패치는 등록된 핸들러를 우선순위 순으로 스캔하며 `isHandlable`을 호출,
첫 매치가 처리(Fusion의 SpecialKey 우선순위 스캔과 유사하되 4단계 고정이 아니라
@ -114,14 +122,18 @@ src/schema/union.luau:48-68`) — 에러 메시지는 즉시 문자열로 만들
- **핸들러 내부 상태 저장**: `retract`가 "이전에 생성한 것"(예: 실행 중이던
Tween 객체)에 접근하려면 상태를 어딘가에 저장해야 함 — **`inst`를 키로 하는
weak-keyed 테이블에 `k`별로 저장**(예: 생성된 Tween을 담아뒀다가 나중에
멈추거나 끝냄). **base가 이걸 범용 유틸로 제공**(`base.perInstanceState(inst)`
류, 정확한 이름/모양은 구현 단계에서 확정) — 모든 핸들러가 재사용, 각자
WeakMap을 새로 만들지 않음. `base/lifecycle-pattern.md`의 "생명 바인드 유틸"과
짝을 이루는 유틸. **왜 GC-안전한가(2026-08-07 여섯 번째 세션, 명시화)**:
구조가 "`inst`로 weak-keyed된 바깥 릴레이션 안에 `k`별 안쪽 릴레이션이
중첩된" 모양이라, `inst`가 죽어 weak table 엔트리가 통째로 사라지는
순간 그 안에 중첩된 `k`별 Tween 인스턴스 릴레이션도 같이 GC됨 — 별도
cleanup 로직 불필요(Tween 핸들러가 여기 담아두는 실제 Tween 인스턴스도
멈추거나 끝냄). **[정정, 2026-08-08 세션] `base.perInstanceState(inst)`라는
이름/모양은 폐기 — `base/relate-plan.md``Relate` 프리미티브로 구체화됨.**
각 핸들러 모듈이 자기 톱레벨에 `local relate = Relate()`를 하나 두고
`relate:SetStrong(inst, k, tween)`/`relate:GetStrong(inst, k)`로 저장/조회 —
"모든 핸들러가 WeakMap을 재발명하지 않고 공유 유틸을 쓴다"는 원래 취지는
그대로, `Relate`가 그 공유 유틸의 정식 인터페이스. `base/lifecycle-pattern.md`
`bindLifetime`/`canExecute`도 같은 `Relate`를 내부적으로 씀(용도가 다르니
별도 `Relate()` 인스턴스). **왜 GC-안전한가(2026-08-07 여섯 번째 세션,
명시화)**: 구조가 "`inst`로 weak-keyed된 바깥 릴레이션 안에 `k`별 안쪽
릴레이션이 중첩된" 모양이라, `inst`가 죽어 weak table 엔트리가 통째로
사라지는 순간 그 안에 중첩된 `k`별 Tween 인스턴스 릴레이션도 같이 GC됨 —
별도 cleanup 로직 불필요(Tween 핸들러가 여기 담아두는 실제 Tween 인스턴스도
자동으로 같이 죽는 것까지 포함, `research/tween-plan.md` 참고).
- **다른 값 변경을 추적하는 것도 process 함수의 정상 범위**: 예를 들어 Slot
핸들러는 자기가 감시하는 값(배열/스토어)이 바뀌면 그에 따라 child를
@ -263,6 +275,42 @@ value)로 `Dispatch.process(inst,k,realv)`를 재귀 호출"하는 식으로 구
상관없이 동일한 재귀적 디스패치로 처리 가능 — 아래 "store가 store를 저장
가능한가"와 직결.
**"값이 바뀔 때마다"의 실제 구독 메커니즘 = `state:Observer(fn)` 재사용으로
확정(2026-08-08 세션).** 이전엔 이 절이 구독 메커니즘 자체를 추상적으로만
서술했는데(새 프리미티브를 발명하는 것처럼 읽힐 수 있었음), 실제로는 아래
"`state:Observer(fn)`" 절에서 이미 확정된 것을 그대로 재사용하면 됨 — 새
구독 primitive를 store-bind 전용으로 따로 만들 이유가 없음:
```lua
-- 예: 일반 프로퍼티 store-bind 핸들러의 process(inst, k, state)
local observer = state:Observer(function()
Dispatch.process(inst, k, state:Get())
end)
observer:Subscribe()
relate:SetStrong(inst, k, observer) -- retract에서 :Unsubscribe() 하려면 들고 있어야 함
```
- children-array leaf 부착(`Frame { observer }`)이 **아니라** `:Subscribe()`/
`:Unsubscribe()` 경로를 씀 — 이 Observer는 핸들러 내부 배관이라 사용자가
보는 leaf가 아니기 때문(위 "이중 바인딩 금지" 원칙과 정합적: 한 Observer
핸들은 두 바인딩 경로 중 하나만 써야 하는데, 이건 애초에 leaf가 아니므로
`:Subscribe()`가 유일한 선택).
- **`retract`가 할 일은 `observer:Unsubscribe()` 호출뿐** — 이게 위 "이벤트도
store-bind 가능" 절에서 이미 "엔지니어링 비용이 낮다"고 서술한 것과 같은
이유(새 디스패치 메커니즘 없이 기존 4종 계약만 구현), 다만 그 절이
구체적으로 가리키는 재사용 대상은 "재귀 process+retract 래핑 패턴"이었고
Observer 자체를 구독 메커니즘으로 쓴다는 것까지는 명시가 안 돼 있었던
갭이 이번에 메워짐.
- **핸들러가 직접 `canExecute`/liveness를 재구현할 필요 없음** — Observer가
이미 자기 `Subscribed` 상태로 게이팅됨(아래 `base/lifecycle-pattern.md`
`canExecute(inst, value)` 절 참고, Observer/Effect는 그 함수 안에서
특별 취급됨).
- Observer가 "등록 즉시 1회 실행"이므로 **최초 적용과 이후 재실행이 같은
코드 경로로 자동 통일**됨 — 프로퍼티 store-bind 핸들러가 "설치 시 1회
적용"을 별도로 안 짜도 되는 이유(위 Observer 절의 원래 근거 그대로).
- `relate``base/relate-plan.md``Relate` 인스턴스 — 이 핸들러 모듈
톱레벨에 `local relate = Relate()`로 하나 두고 재사용.
Slot이 store 바인드로 넘어오는 경우, pluggable 처리기에 `retract` 핸들러가
필요하다는 점(부모가 slot을 정리하고 다시 process하는 방식)도 이 래핑 방식과
자연스럽게 맞음 — `base/slot-plan.md` 참고.

View file

@ -130,35 +130,90 @@ GC에 묶이지 않음 — v1이 여기저기서 `PropertyChangedSignal`에 연
도구로 바인드된 옵저버는 `canExecute` predicate로 게이팅되어, 살아있지 않으면
실행 자체를 건너뛸 수 있음(죽은 대상에 대한 처리 시도 방지, 위 원칙과 직결).
**`canExecute`의 시그니처는 `(handle) -> boolean`이지 `() -> boolean`
아님(2026-08-07 여덟 번째 세션, 정정)** — 처음엔 "바인딩마다 클로즈오버된
zero-arg 람다"로 적었으나, 그러면 등록마다 클로저를 새로 만들어야 해서
아래 "base 유틸은 인터페이스, 실제 구현은 백엔드 팩토리가 주입" 절의 패턴
(base는 타입만 갖고, quad-roblox가 `BaseModule`을 뮤테이션해서 실 구현체를
채워넣음)과 잘 안 맞음 — 그 패턴이 성립하려면 `canExecute`는 **quad-roblox가
한 번만 주입하는 공유 함수**여야 하고, 그러려면 "어떤 등록을 확인할지"를
가리키는 인자(`handle`, 아래 gchold 스케치의 Connection이 이 역할)가 있어야
함. base 입장에선 `handle``any`(엔진마다 실체가 다를 수 있음).
### `bindLifetime`/`canExecute` — 확정(2026-08-08 세션)
**quad-roblox 구현 스케치(rbvm 패턴 재사용, base 결정 아님 — 참고용)**:
Instance당(꼭 하나일 필요는 없지만 보통 그게 싸서 하나로 감) weak-keyed
per-instance 저장소(`base.perInstanceState(inst)`)에 "gchold" 배열을 둠.
그 배열엔 절대 발화하지 않도록 골라 만든 신호에 연결한 Connection을
넣는데, 이 Connection의 콜백 클로저 안에 실제로 살려두고 싶은 옵저버를
업밸류로 캡쳐해둠(콜백은 안 불려도 클로저 자체가 살아있는 한 업밸류는
안 죽음) — `inst`가 GC되면 gchold 배열째로 같이 죽으므로 옵저버도 자연히
GC됨(`base/bind-system-plan.md` "핸들러 내부 상태 저장" 절의 weak-keyed
중첩 구조와 같은 원리). `canExecute(handle)`은 이 Connection(또는 이를
감싼 핸들)을 받아 `.Connected`를 확인하는 정도로 구현될 것.
**미확인 세부사항**: 옵저버 → Connection 역참조를 별도 weak 릴레이션으로
둘지, 아니면 그냥 Observer 테이블 안 평범한 필드로 넣을지(정적 해싱된
필드 접근이 weak 테이블 조회보다 싸서 후자가 나을 수 있음) — quad-roblox
구현 단계에서 실측 확인 필요, base 설계에 영향 없음.
**탑레벨 평범한 함수로 확정, 네임스페이스에 안 숨김.** `Dispatch.process`/
`Handler.xxx`는 "시스템 내부 배관"이라 네임스페이스가 맞지만, `bindLifetime`/
`canExecute``isState`/`isObserver`처럼 핸들러 작성자가 직접 호출하는
**1급 프리미티브 연산**이라 `LifetimeHandle.bind(...)`류로 감싸면 안 됨 —
`LifetimeHandle.luau` 파일 안에 있어도 되지만 export는 평평한 함수:
이건 `base/bind-system-plan.md`의 "핸들러 내부 상태 저장" 유틸과 짝을
이루는 별도 유틸 — 하나는 "상태를 어디에 저장할지"(weak-keyed per-instance
저장소), 다른 하나는 "언제까지 실행되어도 되는지"(생명 바인드 + canExecute)를
다룸. 둘 다 base가 제공하는 범용 유틸로 확정.
```lua
bindLifetime(inst: any, value: any): ()
canExecute(inst: any, value: any): boolean
```
base는 이 두 함수의 **인터페이스만**(타입 시그니처) 갖고, quad-roblox가
`BaseModule` 뮤테이션 시점에 실 구현을 채워넣는다는 원칙은 그대로(`canExecute`
관련 기존 절 참고) — 아래는 그 실 구현 스케치, `base/relate-plan.md`
`Relate` 프리미티브 위에 얹힘(2026-08-08 세션, gchold를 `perInstanceState`
직접 조작 대신 `Relate`로 구현):
```lua
-- quad-roblox 실 구현 스케치
local relate = Relate() -- 이 모듈 전용 인스턴스, 다른 핸들러와 key 충돌 없음
local GCCONN = "__gcconn"
local GCHOLD = "__gchold"
function bindLifetime(inst, value)
local gcconn = relate:GetStrong(inst, GCCONN)
if not gcconn then
-- ClassName은 절대 안 바뀌는 프로퍼티라 이 신호는 절대 발화하지 않음
-- (rbvm 패턴 그대로) — 콜백 클로저가 gchold를 업밸류로 캡쳐해 살려둠
local gchold = {}
relate:SetStrong(inst, GCHOLD, gchold)
gcconn = inst:GetPropertyChangedSignal("ClassName"):Connect(function()
local _ = gchold -- 발화 안 함, 클로저 생존이 곧 gchold 생존
end)
relate:SetStrong(inst, GCCONN, gcconn)
end
local gchold = relate:GetStrong(inst, GCHOLD)
table.insert(gchold, value) -- 강참조 생성, inst 죽으면 gcconn 클로저와 함께 GC
end
function canExecute(inst, value)
-- Observer/Effect는 자기 바인딩 경로(leaf 부착 또는 :Subscribe())의
-- 생존 여부를 스스로 알고 있음 — inst가 살아있어도 이 값이 먼저 죽어
-- 있을 수 있으므로(예: retract가 :Unsubscribe()만 하고 inst는 안 죽음)
-- 반드시 먼저 확인.
if (isObserver(value) or isEffect(value)) and not value.Subscribed then
return false
end
local gcconn = relate:GetStrong(inst, GCCONN)
return gcconn ~= nil and gcconn.Connected
end
```
**`canExecute`의 시그니처는 `(inst, value) -> boolean`(2026-08-08 세션,
재정정 — 원래 있던 "`(handle) -> boolean`, zero-arg 아님" 결정을 대체함).**
이전 라운드(2026-08-07 여덟 번째 세션)는 "등록마다 클로저를 새로 만들지
않기 위해 zero-arg 대신 `handle` 인자를 받는다"까지만 확정했는데, 실제로
`handle`이 뭘 가리키는지(단일 Connection? Observer 자신?)가 미정으로
남아있었음 — 이번에 `(inst, value)` 2-인자로 구체화됨. 이유: Observer 자신의
바인딩 생존(`Subscribed`)과 `inst` 자체 생존(gcconn)은 **독립적인 두 조건**이라
하나의 opaque `handle`로 뭉치면 "inst는 살아있지만 이 Observer는 이미
`:Unsubscribe()`됨" 케이스를 못 구별함 — 위 구현처럼 `value`의 타입에 따라
분기해서 먼저 확인하고, 그 다음 `inst` 공유 gcconn을 봄. "canExecute 하나로
전역 통일" 원칙(Slot 생존/Observer 게이팅/store-bind retract 전부 재사용)은
안 바뀜, 시그니처만 구체화된 것.
**Instance당 gcconn/gchold는 하나로 공유**(꼭 그럴 필요는 없지만 보통 그게
싸서) — `bindLifetime`을 여러 값에 대해 여러 번 불러도 같은 `inst`면 같은
`gcconn`/`gchold`를 재사용(첫 호출에서만 생성, 이후는 `relate:GetStrong`으로
바로 찾음). `Relate`의 lazy 생성 자체가 이 재사용 비용을 이미 다뤄줌 —
자세한 내부 구조는 `base/relate-plan.md`.
**실측 필요(M0/M2)**: Observer→liveness 역참조를 `value.Subscribed` 필드
직접 읽기로 확정했으나(위 구현), 실제 Luau 필드 접근 비용/weak table 조회
비용 비교는 여전히 quad-roblox 구현 단계에서 실측 확인 대상.
이건 `base/bind-system-plan.md`의 "핸들러 내부 상태 저장" 유틸(`Relate`
직접 사용)과 짝을 이루는 별도 유틸 — 하나는 "상태를 어디에 저장할지"
(`Relate:SetStrong`/`:SetWeak`), 다른 하나는 "언제까지 실행되어도 되는지"
(`bindLifetime` + `canExecute`)를 다룸. 후자는 내부적으로 전자가 제공하는
같은 `Relate` 프리미티브 위에 얹혀 구현됨(위 절) — 별도 저장 메커니즘을
새로 만든 게 아니라 `Relate` 하나를 두 용도로 재사용. 둘 다 base가 제공하는
범용 유틸로 확정.
**교차검증(2026-08-04 4차 라운드)**: 사용자가 공유해준 실제 참고 코드
(`.claude/initreq/artworks/EventDrivenProgramming/`, PA님 작성)는 GC-native가

View file

@ -0,0 +1,97 @@
# Relate — inst와 임의의 값을 weak하게 엮는 범용 릴레이션 프리미티브
**상태**: base — 2026-08-08 세션에서 신설, 확정. `base/bind-system-plan.md`
"핸들러 내부 상태 저장"과 `base/lifecycle-pattern.md``bindLifetime`/
`canExecute` 양쪽이 필요로 했던 "`inst`를 weak 키로 하는 저장소"가 지금까지
`base.perInstanceState(inst)`라는 이름만 있고 인터페이스가 미정인 placeholder로
남아있던 것 — 이번에 독립 프리미티브로 정식 승격, `perInstanceState`라는
이름/모양은 폐기.
## 왜 필요한가
Store-bind 핸들러(Tween 등)가 "이전에 만든 것"(실행 중인 Tween, gchold
Connection, gchold 배열 등)에 `retract`/`bindLifetime` 시점에 다시 접근하려면
그 값들을 `inst`에 매달아 저장해야 함. `inst`가 죽으면 이 저장물도 자동으로
같이 죽어야(GC-native, `base/lifecycle-pattern.md` 원칙) 하므로 바깥 키(`inst`)는
weak여야 함 — 그런데 그 안에 담기는 값은 경우에 따라 **강하게 붙잡아야
하는 것**(실행 중인 Tween 인스턴스, gcconn — 안 붙잡으면 존재 이유가 없어짐)과
**약하게만 참조해도 되는 것**(캐시성 값)이 둘 다 있음 — 이 둘을 하나의 테이블
`__mode`로는 표현 못 함(Luau/Lua 테이블의 weak 모드는 테이블 전체 단위).
## 왜 자동으로 강하게 들지 않는가 — 엔진이 결정할 일
**Relate 자신은 `inst``value`도 자동으로 홀드하지 않는다** — 어느 쪽을
얼마나 강하게 들지는 호출부(주로 `quad-roblox`)가 명시적으로 결정해야
함(2026-08-08 세션, 사용자 확정). 자동으로 결정해버리면 weak 키가 참조하는
값이 그 키를 다시 강하게 참조하는 사이클(예: 프로퍼티 핸들러가 만든 클로저가
`inst`를 업밸류로 캡쳐한 채로 그 클로저 자신이 `inst`에 매달린 strong 저장소에
들어가는 경우)이 너무 쉽게 생김 — 엔진 객체의 실제 생명주기를 아는 쪽
(`quad-roblox`)만이 "이 값은 strong으로 둬도 안전하다"를 판단할 수 있음.
그래서 `Relate`는 판단을 안 하고 **`SetWeak`/`SetStrong`으로 호출부가 매번
명시**하게 만드는 얇은 표면만 제공.
## API (확정)
```lua
Relate() -> relate -- 생성자, 싱글톤 아님
relate:SetStrong(inst: any, key: any, value: any) -- value를 강하게 보관
relate:GetStrong(inst: any, key: any): any?
relate:SetWeak(inst: any, key: any, value: any) -- value를 약하게만 참조
relate:GetWeak(inst: any, key: any): any?
```
- **`inst`(첫 인자)는 항상 weak** — 이 자유도는 아예 안 열어둠. 지금까지
나온 어떤 유스케이스도 "`inst` 쪽을 strong으로 두고 싶다"가 없었고, 열어두면
"`Relate`가 실수로 엔진 객체를 영구히 붙잡는" 사고 가능성만 늘어남.
`Weak`/`Strong`은 오직 **`value`의 보관 방식**을 가리킴.
- **비싱글톤 — 생성 가능한 값(`Ref`/`Store`/`Modifier`와 같은 프리미티브
컨벤션)**. 각 핸들러 모듈이 자기 톱레벨에 `local relate = Relate()`
하나씩 두고 재사용 — 서로 다른 `Relate` 인스턴스라 `key` 네이밍이 모듈
간에 겹칠 걱정이 없음(모듈 하나가 감당할 key 개수는 보통 한두 개뿐이라
`Relate()`를 여러 개 만드는 비용은 무시할 만함).
## 실제 구조 (확정, 2026-08-08 세션)
```
{ [inst(weak)]: { StrongMap: {[key]: value}?, WeakMap: {[key]: value(weak)}? }? }
```
- **바깥 테이블 하나**: `inst`로 weak-keyed(`__mode = "k"`), 값은 `{ StrongMap?, WeakMap? }`
형태의 서브테이블.
- **`StrongMap`/`WeakMap`은 각각 lazy 생성** — `Relate()` 호출 시점엔 아무
것도 미리 안 만듦. `inst`당 서브테이블도, 그 안의 `StrongMap`/`WeakMap`도
**`SetWeak`/`SetStrong`이 처음 불릴 때 인덱싱해보고 없으면 그때 생성**.
이유(사용자 확정, 성능 근거): Luau가 정적 분석으로 포인터 해싱을 캐싱해서
같은 자리에서 여러 번 인덱싱하는 건 이미 꽤 싸지지만, **테이블 생성
자체(array+hash part 초기화)는 상대적으로 비쌈** — 안 쓸 `inst`/모드
조합에 대해 테이블을 미리 만들어두는 건 순수 낭비.
- **`WeakMap`의 메타테이블은 항상 같은 객체를 재사용**(`{__mode = "v"}`류
하나를 모듈 로드 시 한 번만 만들어두고, 모든 `WeakMap` 생성에 그 객체를
그대로 `setmetatable`) — 메타테이블 내용이 매번 똑같으니 매번 새로 만들
이유가 없음. `StrongMap`은 메타테이블 자체가 필요 없어 그냥 `{}`.
- `GetWeak`/`GetStrong`은 각각 대응하는 서브맵이 아직 안 만들어졌으면(=한
번도 `Set`된 적 없음) 그냥 `nil` 반환 — 서브맵을 만들 필요 없음(읽기가
쓰기를 유발하면 안 됨).
**M2 착수 시 실측 확인**: 위 lazy 생성 전략과 `WeakMap` 공유 메타테이블
재사용이 실제 Luau에서 기대한 만큼 이득인지, `SetStrong`/`SetWeak`을 아주
자주 왕복 호출하는 핫패스(예: 매 프레임 store-bind 재실행)에서 서브테이블
존재 체크 자체가 새 비용이 되지는 않는지 — base 설계에는 영향 없는 순수
구현 최적화 문제.
## 대체하는 것
- `base/bind-system-plan.md` "핸들러 내부 상태 저장" 절의 `base.perInstanceState(inst)`
placeholder — Tween 등 핸들러가 `retract` 대상을 저장하는 용도, `SetStrong`으로.
- `base/lifecycle-pattern.md``bindLifetime`/`canExecute` — gcconn/gchold를
`Relate``SetStrong`으로 저장(둘 다 존재 이유가 "안 죽는 것"이므로
strong).
## 이름
`Relate` — 사용자 확정("이름은 Relate 괜찮아요? ... 좋습니다", 2026-08-08
세션). 다른 프리미티브(`Source`/`Ref`/`Store`/`Modifier`/`Effect`/`Blocker`)와
같은 "타입 이름이 곧 생성자" 컨벤션 그대로.

View file

@ -98,8 +98,8 @@ v1에서도 `Corner`/`PaddingAll`/`Scale`은 store 값으로 바인드 가능했
다시 세팅하는 정도로 충분. 구현 비용도 낮음: 각 Handler가 `process`에서
"이전에 자기가 찾거나 만든 자식 Instance"를 얻어야 하는데, 이건 이미
base가 범용 유틸로 제공하기로 확정한 per-instance weak-keyed 저장소
(`base.perInstanceState(inst)`, `base/bind-system-plan.md` "핸들러 내부
상태 저장" 절)를 그대로 재사용하면 됨 — Tween 핸들러가 실행 중인 Tween
(`Relate:SetStrong(inst,k,...)`, `base/relate-plan.md`/`base/bind-system-plan.md`
"핸들러 내부 상태 저장" 절)를 그대로 재사용하면 됨 — Tween 핸들러가 실행 중인 Tween
객체를 기억해두는 것과 정확히 같은 패턴. 새 메커니즘 발명 불필요, 이미
있는 "store 바인드는 pluggable 바인드를 재실행하는 래핑" 원칙
(`base/bind-system-plan.md` "확정된 디스패치 모델" 절)이 그대로 적용됨.

View file

@ -58,9 +58,10 @@ context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만**
**파급 효과(2026-08-06 추가)**: `DI`가 리네임되면 `DI.FrameModifier`
Modifier 클래스별 타입 프리픽스도 같이 바뀌어야 함 — `DI` 리네임 논의
때 이 연쇄까지 같이 고려할 것.
- **`PerInstanceState`(2순위)**: 핵심 프리미티브 `State`와 이름이 겹쳐서
실제로는 완전히 무관한 유틸(인스턴스별 weak-keyed 저장소)인데 혼동
유발 가능 — `PerInstanceStorage`/`InstanceData` 등 대안.
- **[해소됨, 2026-08-08 세션]** `PerInstanceState` — 이름 문제 자체가 없어짐.
`State`와 이름이 겹쳐 혼동 유발하던 그 유틸은 `Relate`로 대체·정식
승격됨(`base/relate-plan.md`) — 이름도 이미 사용자 확정("Relate 괜찮아요"),
`State`와 안 겹침.
- **`Slot`(2순위)**: Vue의 "slot"(콘텐츠 주입 지점)과 이름은 같지만 의미가
다름(quad의 Slot은 자식 배열 재조정 프리미티브) — Vue 배경 있는 사람이
헷갈릴 수 있음.
@ -138,23 +139,34 @@ context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만**
세션)**. `props.Modifier or None`/`props.Ref or None` 관용구를 필수로
확정(`base/component-composition-plan.md` "필수 관용구" 절) — M0에선
이 관용구 자체가 타입/런타임 양쪽에서 문제없이 동작하는지만 검증.
- **`canExecute`/`Connected`의 실제 구현 방식(Parent==nil vs Connection.
Connected vs Destroying 플래그)이 미확정인데 이미 Slot/Observer/store-bind
retract 전역에 재사용 확정됨** — M2/M3 착수 전 실측 필요 — 우선순위1-6.
**(2026-08-07 여덟 번째 세션 보강)** 시그니처는 `(handle) -> boolean`으로
확정(zero-arg 클로저 아님, `base/lifecycle-pattern.md` 참고)됐고
rbvm식 gchold 스케치(weak per-instance 배열에 절대 안 발화하는
Connection을 넣어 그 클로저 업밸류로 Observer를 살려두는 방식)도
후보로 적어뒀지만, 여전히 스케치 단계 — Observer→Connection 역참조를
weak 릴레이션으로 둘지 평범한 필드로 둘지 포함, 실측은 그대로 필요.
- **~~`canExecute`/`Connected`의 실제 구현 방식이 미확정~~ — 반영 완료
(2026-08-08 세션)** — 우선순위1-6 해소. `bindLifetime(inst,value)`/
`canExecute(inst,value)` 탑레벨 함수로 확정(네임스페이스 안 씀,
`LifetimeHandle.luau`는 이 둘의 인터페이스만 갖고 quad-roblox가 구현
주입), 시그니처는 `(handle)`이 아니라 `(inst, value)` 2-인자로 재정정
(Observer 자신의 `Subscribed` 상태를 먼저 보고, 그 다음 `inst`의 공유
gcconn을 봄 — 두 조건이 독립적이라 하나로 못 뭉침). gchold 저장소는
새 프리미티브 `Relate`(`base/relate-plan.md`) 위에 구현 — `base/
lifecycle-pattern.md`의 "`bindLifetime`/`canExecute` — 확정" 절 참고.
- **~~`LifetimeHandle` 인터페이스가 M8에 배치돼 있지만 M4/M6이 이미 그걸
필요로 함(로드맵 순서 역전)~~ — 반영 완료(2026-08-07 세 번째 세션)**:
`LifetimeHandle`/`PerInstanceState` 인터페이스(타입만)를 `ROADMAP.md`
`LifetimeHandle`/`Relate` 인터페이스(타입만)를 `ROADMAP.md`
M2로 옮기고, quad-roblox 실 구현만 M8에 남김 — 우선순위1-9 해소.
- 그 외(Slot CRUD 의미론 미정의, retract 시 "이전 핸들러" 추적 책임 소재,
우선순위 스캔 동률/매치실패 처리, `:Compute``previous` 인자가
오버엔지니어링일 수 있음, UI shorthand의 기존 UICorner 매칭 기준 등)는
`pre-implementation-audit.md` 본문 참고.
- **[신규, 2026-08-08 세션, 미확인]** `Frame { ref }`/`Frame { observer }`처럼
children 배열 숫자 슬롯에 직접 놓는 leaf 값을 실제로 매칭·바인드하는
Handler(`(i:number, v=Ref/Observer/PreRef)`)가 어느 패키지에 사는지 —
`base/bind-system-plan.md:301-303,852-854`는 "`(v=Ref)` 매치 핸들러가
처리한다"/"`isObserver`로 판별해 라이프사이클에 묶어준다"까지만 서술하고
파일 배치는 안 함(`architecture.md` 소스트리에도 이 Handler가 이름으로
안 나와 있음). 제안(미확정): `Ref`/`Observer`/`PreRef` 전부 `inst``any`
취급하는 engine-agnostic 타입이고 process가 `v:Set(inst)`/`LifetimeHandle`
위임 정도만 하면 되니, `Dispatch/StoreBind.luau`(이미 "범용, 엔진 무관"으로
분류)와 같은 층위로 `quad-base`에 두는 게 맞아 보임 — `quad-roblox/Handlers/`
아니라. 사용자 확인 필요, 아직 base에 반영 안 함.
### 3. 낮은 우선순위
@ -227,6 +239,8 @@ context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만**
| 컴포넌트 이식성(전역 store 참조 시 재사용성 문제) | `base/purity-and-effects-plan.md` |
| Blocker(값 기반 emit 지연/합치기) | `base/blocker-plan.md` |
| Effect(설치+확정 정리, `state` 있으면 Observer 조합해 재실행도 지원 — 확정) | `base/effect-plan.md` |
| `Relate`(inst-weak 릴레이션 프리미티브, `SetWeak`/`GetWeak`/`SetStrong`/`GetStrong`), `bindLifetime`/`canExecute`(inst,value) 탑레벨 함수 | `base/relate-plan.md`, `base/lifecycle-pattern.md` |
| `retract` 필드 생략 불가(no-op 허용, 누락 시 핸들러 교체 순간 크래시), store-bind 재실행은 `state:Observer(fn):Subscribe()` 재사용 | `base/bind-system-plan.md` |
| UICorner/UIPadding/UIScale 인라인 편의 키 — 이름·메커니즘·store-bind 가능성까지 확정 | `base/ui-shorthand-plan.md` |
| Batch(lexical) 기각, Context(+레이어드 Store) 기각 | `archive/batch-rejected.md`, `archive/context-rejected.md` |
| Fusion/Vide 비교 리서치(주의: 일부 서술은 이후 라운드에서 뒤집힘, 문서 내 정정 표시 참고) | `reference/comparison-fusion-vide.md` |

View file

@ -150,6 +150,13 @@ Modifier/Ref를 아예 안 넘기는 케이스를 반드시 포함시킬 것.**
### 1-6. `canExecute`/`Connected`의 실제 구현 방식이 미확정인 채로 코어 전역에 이미 재사용 확정됨
**[해소됨, 2026-08-08 세션]** `bindLifetime(inst,value)`/`canExecute(inst,value)`
탑레벨 함수로 확정, `Relate` 프리미티브(`base/relate-plan.md`) 위에 gcconn/
gchold를 얹는 구체 구현까지 나옴 — `base/lifecycle-pattern.md`
"`bindLifetime`/`canExecute` — 확정" 절이 최신. 아래는 이 결정이 나오기
전까지의 문제 서술(정확했던 문제 인식이라 그대로 둠, 남은 실측 항목은
`lifecycle-pattern.md` 쪽 "M0/M2 실측 필요" 캐비엇으로 이동).
**위치**: `base/lifecycle-pattern.md` "2026-08-04 검증 라운드에서 보강된
내용" 절, 특히 "`Destroying` 훅은 생각보다 덜 중요할 수 있음" 부분.
@ -566,8 +573,8 @@ Handler"라고만 서술해, 사실상 3개의 거의 동일한 형태(리터럴
- M9(컴포넌트 합성)이 M7(Modifier)·M8(Ref) 뒤에 오는 순서 — M9는 "M0
스파이크(named-parameter 전달)를 정식 Modifier/Ref로 검증"하는 단계라고
명시돼 있어 뒤늦은 검증이 아니라 의도된 정식화.
- `PerInstanceState` 실제 구현 시점(M8) — 이걸 필요로 하는 핸들러(Tag/
Attribute/Tween)가 전부 M10/M11이라 순서상 문제없음.
- `Relate`(구 `PerInstanceState`) 실제 구현 시점(M8) — 이걸 필요로 하는
핸들러(Tag/Attribute/Tween)가 전부 M10/M11이라 순서상 문제없음.
- Slot의 store-bind 의존(M6→M4) 순서.
---

View file

@ -1362,3 +1362,91 @@ nil-hole 위험도 실측이 우선순위 높아짐.
전부 커밋 `98bd46a` 이후 아직 커밋 안 된 이번 대화 전체 변경사항에
포함 — 다음 세션이 새로 알아야 할 건 없음, `ROADMAP.md` M0부터 그대로
시작.
## 2026-08-08 세션 — `Relate` 신규 프리미티브, `bindLifetime`/`canExecute`
탑레벨 함수로 확정, store-bind 재실행=Observer 재사용 명문화, `retract`
필드 생략 불가 확정
사용자가 store-bind/라이프사이클 관련 문서 갭 두 개를 질문하며 시작된 세션
— 답을 찾는 과정에서 지금까지 이름만 있던 placeholder(`base.perInstanceState`)가
실제로는 제대로 설계된 적 없는 프리미티브였다는 게 드러나 그 자리에서
설계까지 확정까지 감. 네 가지로 정리:
**1. store-bind의 "값이 바뀔 때마다 재귀 process" 구독 메커니즘 =
`state:Observer(fn):Subscribe()` 재사용으로 확정.** 기존 "확정된 디스패치
모델"/"재실행 래핑" 절이 구독을 추상적으로만 서술해서 마치 새 구독
프리미티브가 필요한 것처럼 읽혔는데, 실제로는 이미 확정된 Observer(등록
즉시 1회 실행이라 "최초 적용"과 "이후 갱신"이 공짜로 통일됨, 자기 `Subscribed`
liveness도 이미 있음)를 그대로 쓰면 됨 — `retract``observer:Unsubscribe()`
호출 하나로 끝. 새 구독 메커니즘 발명 없음. `base/bind-system-plan.md`
"Store 바인드는 특수 경우인가" 절 반영.
**2. `retract` 필드는 no-op이라도 항상 정의해야 함 — 생략 불가로 확정.**
"모든 핸들러가 의미 있게 구현할 필요는 없음(보통 no-op)"이라는 기존 서술이
"필드 자체를 생략해도 된다"로 오독될 수 있는 갭이었음 — `Dispatch.process`
담당 핸들러 타입이 바뀔 때 이전 핸들러의 `retract`를 nil 체크 없이 무조건
호출하므로, 필드를 생략한 핸들러가 실제로 교체되는 드문 순간(Tween↔프로퍼티
등)에 `attempt to call a nil value`로 크래시함. `base/bind-system-plan.md`
"핸들러 계약" 절에 명시, M2 체크리스트에 린트 대상으로 추가.
**3. `Relate` 신규 프리미티브 — `bindLifetime`/`canExecute`(가 의존하는
per-inst weak 저장소)를 제대로 설계.** 사용자 질문 경위: `Frame { observer }`처럼
children 배열에 직접 놓는 leaf 케이스와, property store-bind 핸들러가
**내부에서** 만드는 Observer(배열에 안 들어가므로 그 leaf 부착 경로를 안 탐)를
처음에 잘못 섞어서 답했다가 사용자가 "state 바인딩은 결국 k,inner v를
호출하니 i=number,v=observer로 다시 실행 안 된다"고 정정 — 후자는
`bindLifetime(inst, observer)` 같은 별도 배관이 필요하다는 걸로 이어짐.
이게 `base/lifecycle-pattern.md`가 이미 원 사용자 메모(2026-08-04)로
갖고 있던 "함수 안에서 만든 옵저버도 GC 대상 되어야 함" 절과 정확히
같은 문제였음이 드러남 — 그 절이 "범용 유틸이 있어야 한다"까지만 말하고
실제 인터페이스/이름이 없던 것.
- **탑레벨 평범한 함수로 확정, 네임스페이스 뒤에 안 숨김**`bindLifetime(inst,value)`/
`canExecute(inst,value)`. `Dispatch.process`류는 "시스템 배관"이라
네임스페이스가 맞지만 이 둘은 `isState`/`isObserver`처럼 핸들러 작성자가
직접 부르는 1급 프리미티브 연산이라 `LifetimeHandle.bind(...)`식으로
감싸면 안 된다는 사용자 지적(정확함, 처음 제 제안이 틀렸었음).
- **`canExecute` 시그니처를 `(handle)` 단일 인자에서 `(inst, value)`
2-인자로 재정정** — Observer 자신의 바인딩 생존(`Subscribed`)과 `inst`
자체 생존(gcconn)이 독립된 두 조건이라 opaque `handle` 하나로 못 뭉침.
구현은 `value`가 Observer/Effect면 자기 `Subscribed`부터 확인, 그 다음
`inst`의 공유 gcconn `.Connected`를 봄.
- **`Relate``inst`를 weak 키로 하는 범용 릴레이션, 신규 프리미티브로
독립 승격**(`base/relate-plan.md`, 1프리미티브-1파일 컨벤션). `Relate()`
비싱글톤 생성자 + `:SetWeak`/`:GetWeak`/`:SetStrong`/`:GetStrong`. 핵심
결정 세 개, 전부 사용자가 직접 제시:
1. **자동으로 아무것도 홀드 안 함**`inst``value`도 Relate 자신은
안 붙잡음, weak/strong 여부는 호출부(엔진을 아는 quad-roblox)가
매번 명시. 자동으로 정하면 weak 키가 참조하는 값이 그 키로 되돌아
강참조하는 사이클이 너무 쉽게 생김.
2. **`inst`(키) 축은 항상 weak로 고정, 자유도를 안 열어둠** — 강한 키가
필요한 유스케이스가 지금까지 하나도 없어서, 그 자유도 자체가 사고
가능성만 늘림. `Weak`/`Strong`은 오직 `value` 보관 방식.
3. **실 구조는 `{ [inst(weak)]: { StrongMap: {[k]:v}?, WeakMap: {[k]:v(weak)}? }? }`,
둘 다 lazy 생성**(첫 `Set` 호출 시에만 만듦) — Luau가 정적 분석으로
포인터 해싱을 캐싱해 반복 인덱싱은 이미 싸지지만 테이블 생성(array+hash
part 초기화) 자체는 비교적 비싸다는 게 이유. `WeakMap`의 메타테이블은
매번 새로 안 만들고 공유 객체 하나를 재사용.
- **비싱글톤인 이유**: 각 핸들러 모듈이 자기 톱레벨에 `local relate =
Relate()`를 하나씩 두면 key 네이밍이 모듈 간에 겹칠 걱정이 원천적으로
없음(`Ref`/`Store`류와 같은 "생성 가능한 값" 컨벤션).
- **`base.perInstanceState(inst)` 이름/placeholder는 완전히 폐기** —
`Relate`가 그 자리를 정식으로 대체. `bind-system-plan.md`(핸들러 내부
상태 저장 절)/`ui-shorthand-plan.md`/`architecture.md`(소스트리,
`Relate.luau`는 quad-base 전체가 순수 Lua라 quad-roblox 재구현 없음)/
`question.md`(용어 정리 목록에서 `PerInstanceState` 항목 삭제, 이름
갈등 자체가 해소됨)/`ROADMAP.md`(M2/M8/병행가능 세 곳) 전부 동기화.
**4. 아직 안 풀린 것 — `(i:number, v=Ref/Observer/PreRef)` children-array
leaf Handler가 quad-base/quad-roblox 중 어디 사는지.** 3번을 풀다가
갈라져 나온 별개 질문(`Frame { ref }` 자체를 매칭하는 Handler, store-bind
내부 Observer와는 무관) — 제 제안(엔진 특정 API가 필요 없으니 quad-base,
`Dispatch/StoreBind.luau`와 같은 층위)은 사용자 확인을 못 받은 채 대화가
3번으로 넘어감. `question.md` 2번에 미확인으로 남김, base에는 반영 안 함
— 다음에 확인 필요.
**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터). M0/M2 스파이크 코드가
검증해야 할 것 목록에 `Relate`의 lazy 서브테이블 생성/공유 메타테이블
전략, `bindLifetime`/`canExecute`의 실제 gcconn 트릭이 새로 추가됨 —
`base/lifecycle-pattern.md`/`base/relate-plan.md`의 "실측 필요" 캐비엇
참고. 4번(Ref/Observer leaf Handler 위치)도 M2 착수 전 확인 대상.

View file

@ -86,17 +86,29 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
`isState`처럼 집합 멤버십 아님) 전부의 기반. `isNone`만 예외로
레지스트리 없이 `x == None` 항등 비교 — `bind-system-plan.md`
`Brand` 절, 2026-08-07 여덟 번째 세션 신설)
- [ ] `LifetimeHandle.luau`/`PerInstanceState.luau` **인터페이스만**(타입
계약, 실 구현 없음 — quad-roblox 실 구현은 M8) — 원래 M8에만
있었으나 M4(StoreBind의 `Connected` 확인)/M6(Slot의 `canExecute`)이
이미 이 인터페이스를 전제로 서술돼 있어 로드맵 순서가 역전돼
있었음(`pre-implementation-audit.md` 우선순위1-9, `question.md` 2번
— 2026-08-07 세 번째 세션에 반영). **`canExecute`는 `(handle:
LifetimeHandle) -> boolean`으로 확정**(바인딩마다 클로저 만드는
zero-arg가 아니라, quad-roblox가 한 번만 주입하는 공유 함수 — "base
유틸은 인터페이스, 백엔드가 주입" 패턴과 맞춰야 해서.
`base/lifecycle-pattern.md`의 gchold 스케치 절, 2026-08-07 여덟 번째
세션 정정)
- [ ] `Relate.luau`(전체가 quad-base, 순수 Lua — `base/relate-plan.md`) —
`Relate()` 비싱글톤 생성자, `:SetWeak`/`:GetWeak`/`:SetStrong`/`:GetStrong`.
`inst`(첫 인자)는 항상 weak, `StrongMap`/`WeakMap` 서브테이블은 lazy
생성(첫 `Set` 호출 시에만), `WeakMap`은 공유 메타테이블(`{__mode="v"}`)
재사용 — 구 `base.perInstanceState(inst)`/`PerInstanceState.luau`를
대체(2026-08-08 세션 신설).
- [ ] `LifetimeHandle.luau` **인터페이스만**(`bindLifetime(inst,value)`/
`canExecute(inst,value)` 탑레벨 함수 타입 계약, 실 구현 없음 —
quad-roblox 실 구현은 M8) — 원래 M8에만 있었으나 M4(StoreBind의
`Connected` 확인)/M6(Slot의 `canExecute`)이 이미 이 인터페이스를
전제로 서술돼 있어 로드맵 순서가 역전돼 있었음(`pre-implementation-audit.md`
우선순위1-9, `question.md` 2번 — 2026-08-07 세 번째 세션에 반영).
**`canExecute``(inst, value) -> boolean`으로 재확정(2026-08-08
세션, `(handle)` 단일 인자 서술을 대체)** — Observer/Effect는 자기
`Subscribed` 상태를 먼저 확인, 그 다음 `inst`의 공유 gcconn(`Relate`로
저장)의 `.Connected`를 봄. `bindLifetime`/`canExecute` 둘 다 네임스페이스
없이 탑레벨 함수로 export(`Dispatch.xxx`류 시스템 네임싱과 구분,
`isState`/`isObserver`와 같은 1급 프리미티브 취급) — `base/
lifecycle-pattern.md`의 "`bindLifetime`/`canExecute` — 확정" 절 참고
- [ ] 핸들러 계약 검증: `retract` 필드가 없는 핸들러를 등록하면 리뷰/린트에서
걸러내기(no-op이라도 필드 자체는 항상 정의 — `Dispatch.process`가 핸들러
교체 시 nil 체크 없이 호출, `base/bind-system-plan.md` "핸들러 계약"
절, 2026-08-08 세션)
- [ ] mock 대상 테스트
## M3 — Store/State/Source
@ -203,10 +215,10 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
`thread``nil`이면
`coroutine.running()` 캡처+yield, 있으면 등록만 하고 즉시 `self`
반환(남의 thread를 여기서 대신 정지시킬 수 없어서)
- [ ] `LifetimeHandle` quad-roblox 실제 구현(Instance 생존 확인, 인터페이스
자체는 M2로 이동됨)
- [ ] `PerInstanceState` quad-roblox 실제 구현(weak-keyed table, 인터페이스
자체는 M2로 이동됨)
- [ ] `LifetimeHandle` quad-roblox 실제 구현`bindLifetime`/`canExecute`
본체(`GetPropertyChangedSignal("ClassName")` 연결 트릭으로 gcconn 확보,
`Relate:SetStrong`으로 gcconn/gchold 저장 — 인터페이스 자체는 M2로
이동됨, `Relate` 자체는 quad-base라 quad-roblox 쪽 재구현 없음)
## M9 — 컴포넌트 합성 레이어
@ -229,8 +241,9 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
## 특정 마일스톤에 안 묶이고 병행 가능
- [ ] 용어 정리 스윕 — `State`/`DI`/`PerInstanceState`/`Slot` 등
(`.claude/question.md` 1번), 최종 이름 확정되는 대로 아무 시점에나
- [ ] 용어 정리 스윕 — `State`/`DI`/`Slot` 등(`PerInstanceState`는 `Relate`
대체·해소됨) — `.claude/question.md` 1번, 최종 이름 확정되는 대로
아무 시점에나
- [ ] 각 마일스톤 완료 시 `.claude/qa-request/`/`.claude/archive/`에 기록,
필요하면 `CLAUDE.md` "최근 세션 요약"도 갱신