docs(base): Observer/Effect Leaf dedup 추가 + Tag/Attribute 자기등록 모델 역전

State<Observer>/State<Effect>가 재-dispatch될 때 안쪽 값이 안 바뀌어도
Dispatch가 값 비교 없이 매번 재바인딩하던 것에 RefLeafHandler와 같은
old ~= v dedup을 추가(correctness 아니라 순수 성능 최적화 — == 비교가
매번 도는 Relate weak-table 쓰기보다 항상 쌈).

가장 큰 변경은 Tag/Attribute 핸들러 등록 모델 역전: 직전 커밋이 확정한
"TagHandler/AttributeKeyHandler/AttributeGroupHandler가 quad-base 모듈
로드 시점에 스스로 등록한다"는 결론 자체가 틀렸음이 드러남 — 이건
lifecycle-pattern.md가 이미 거부해둔 InitNamespace류 top-level 부작용
패턴과 같은 클래스였고, module-lifecycle-plan.md가 이미 확정해둔 "등록은
백엔드 팩토리가 BaseModule을 뮤테이션하는 시점" 원칙과 정면으로 어긋났음.
정정: 저 이름들은 참조 카운트/이름 claim 알고리즘 구현일 뿐이고,
HANDLER_PRIORITY_FALLBACK에 실제로 꽂히는 건 이를 감싸는 별도 이름의
TagFallbackHandler/AttributeKeyFallbackHandler/AttributeGroupFallbackHandler
— 등록 주체는 quad-base 모듈이 아니라 백엔드 팩토리. dispatch-core-plan.md/
tag-plan.md/attribute-plan.md/module-lifecycle-plan.md/architecture.md
전부 재반영, ROADMAP.md M10 체크리스트에 새 Fallback 파일 3개 추가(빠져
있으면 구현자가 만들 필요를 몰랐을 갭), 뒤집힌 원문은
archive/tag-attribute-load-time-registration-reversed.md.

/code-review를 두 라운드 돌려 findings 15건 확정 반영 — 죽어있던
Observer/Effect FALLBACK 동적 경로 가드(k 타입 미체크), bindLifetime
pseudocode의 canExecute/canBound 혼용, "결정 대기" 절이 삭제됐는데 비어
있다고 서술하던 3개 파일, "동적 경로 가드"가 볼드 텍스트뿐 실제 헤딩이
아니라 깨져있던 절 참조 6곳(###으로 승격), CLAUDE.md 11번째 세션 기록
~103줄→~13줄 압축, Tag/Attribute 정정이 프로즈만 고치고 놓친 pseudocode
2곳. 별개로 RefLeafHandler.isHandlable이 PostRef 도입 이후 안 갱신돼
`and not isPostRef(v)`를 빠뜨렸던 사전 존재 버그도 같이 발견·정정.
doc-check.py ERROR 0 유지.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
qwreey 2026-08-14 12:32:17 +09:00
parent e35905ceca
commit 7f3bc4da4e
Signed by: qwreey
GPG key ID: D28DB79297A214BD
16 changed files with 570 additions and 175 deletions

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,61 @@
# [역전됨] Tag/Attribute Handler는 "quad-base 모듈 로드 시점에 스스로 등록" — 등록 주체와 이름이 둘 다 틀렸음
**상태**: archive — 2026-08-14 열두 번째 세션(이 대화)에서 역전. 정본은
`base/dispatch-core-plan.md`의 "base가 소유하는 핸들러와 주입되는 엔진 op" 절.
## 뒤집힌 주장 (2026-08-14 열한 번째 세션, "네 번째, 최종 정정")
`TagHandler`/`AttributeKeyHandler`/`AttributeGroupHandler`가 **quad-base
자기 모듈 로드 시점**(top-level, require 시)에 `HANDLER_PRIORITY_FALLBACK`
우선순위로 **스스로** `Dispatch.addHandler`를 호출해 등록한다고 확정했었음
`dispatch-core-plan.md`/`tag-plan.md`/`attribute-plan.md`/
`module-lifecycle-plan.md`/`architecture.md` 5개 파일에 반영됐었음.
## 왜 틀렸나 (사용자 정정, 2026-08-14 열두 번째 세션)
두 가지가 섞여 있었음:
1. **등록 주체가 틀림 — "모듈 로드 시점"은 이 프로젝트가 이미 명시적으로
거부한 패턴.** `base/lifecycle-pattern.md` "rbvm에서 그대로 가져오면
안 되는 것" 절이 "`InitNamespace`/`Registered`-가드/`NewLib` 3종 세트로
라이브러리마다 하나하나 수동 init 하는 방식은 정확히 사용자가 피하고
싶다고 한 패턴"이라고 이미 못박아뒀는데, "quad-base가 자기 모듈 로드
시점에 스스로 등록"은 이름만 다를 뿐 같은 클래스의 top-level 부작용임.
`base/module-lifecycle-plan.md`가 이미 확정해둔 일반 원칙("base
유틸은 인터페이스만, 실제 등록/구현은 백엔드 팩토리가 `BaseModule`
뮤테이션하는 시점에 주입")과도 정면으로 어긋남 — 같은 문서
124-154줄이 "이 결론이 Dispatch의 handler 레지스트리에도 그대로
적용된다"고 이미 일반화해뒀던 걸 이 Tag/Attribute 결론만 예외로 뒀던
셈.
2. **이름이 틀림 — "TagHandler 자신이 등록되는 주체"와 "TagHandler라는
공유 알고리즘 구현"을 하나로 뭉갬.** `TagHandler`/`AttributeKeyHandler`/
`AttributeGroupHandler`는 참조 카운트/이름 claim 알고리즘 그 자체를
가리키는 이름이 맞고, 이건 엔진 지식을 요구하지 않는 **공유 코드**라서
base에 위치하는 것뿐 — 이름 자체가 "자동으로 설치되는 안전망"이라는
의미까지 담고 있지 않음.
## 정정된 모델
- `TagHandler`/`AttributeKeyHandler`/`AttributeGroupHandler` = 참조
카운트/이름 claim **알고리즘 구현**(그대로 base 소유, 이름도 그대로).
- **실제로 `HANDLER_PRIORITY_FALLBACK`에 꽂히는 건 별도로 "Fallback"이
이름에 들어간 엔티티** — `TagFallbackHandler`/
`AttributeKeyFallbackHandler`/`AttributeGroupFallbackHandler`. 위
알고리즘을 그대로 감싸 쓰되, "이게 기본 안전망으로 자동 설치되는
대상"임을 이름으로 구분.
- **등록 주체는 quad-base 모듈 자체가 아니라 필요한 엔진(백엔드
팩토리)** — quad-roblox 같은 백엔드의 팩토리 함수가 `BaseModule`
구성할 때, 자기 전용 Handler(Property/Event/OnChange/UICorner)들과
**같이** 이 base 소유 Fallback Handler들도 등록해준다. 엔진 저자
입장에선 "자동/공짜"(직접 코드를 안 짜도 됨)이지만, 메커니즘은 팩토리
뮤테이션 시점의 정상 경로 — `module-lifecycle-plan.md`가 이미 확정해둔
패턴을 그대로 따르는 것뿐, 새 예외가 아님.
## 영향 범위(정정 완료)
`base/dispatch-core-plan.md`(핸들러 계약 절, "base가 소유하는 핸들러와
주입되는 엔진 op" 절), `base/tag-plan.md`, `base/attribute-plan.md`,
`base/module-lifecycle-plan.md`, `base/architecture.md`(Tag.luau 파일
설명) — 전부 이 세션에 재반영. `CLAUDE.md`의 11번째 세션 서술(네 라운드
연속 정정 서사)은 **역사적 기록으로 그대로 둠**(그 세션 시점엔 이게
최선의 결론이었음) — 12번째 세션 요약이 이 재역전을 링크로 가리킴.

View file

@ -167,10 +167,10 @@ quad/
│ │ ├── init.luau # process 엔진 — `chains`(inst,k별 인덱스 배열, 슬롯마다 {handler, retractor}) + 하강 diff(핸들러가 같으면 그 자리 클로저에 새 값을 넘기고 재process, 다르면 그 자리부터 retractFrom) + 3-인자 `retractFrom(inst,k,index)` (`dispatch-core-plan.md` "Dispatch 체인" 절, 2026-08-08 신설 → 2026-08-13 다섯 번째 세션 인덱스화 → 같은 날 열네 번째 세션 하강 diff) │ │ ├── init.luau # process 엔진 — `chains`(inst,k별 인덱스 배열, 슬롯마다 {handler, retractor}) + 하강 diff(핸들러가 같으면 그 자리 클로저에 새 값을 넘기고 재process, 다르면 그 자리부터 retractFrom) + 3-인자 `retractFrom(inst,k,index)` (`dispatch-core-plan.md` "Dispatch 체인" 절, 2026-08-08 신설 → 2026-08-13 다섯 번째 세션 인덱스화 → 같은 날 열네 번째 세션 하강 diff)
│ │ ├── Handler.luau # 핸들러 계약 타입(isHandlable/priority/process — process가 자기 retract 클로저를 반환) │ │ ├── Handler.luau # 핸들러 계약 타입(isHandlable/priority/process — process가 자기 retract 클로저를 반환)
│ │ ├── StoreBind.luau # store 값 재귀 재실행 로직(범용, 엔진 무관) │ │ ├── StoreBind.luau # store 값 재귀 재실행 로직(범용, 엔진 무관)
│ │ ├── Leaf.luau # (i:number, v=Ref/Observer/PreRef/PostRef) children-array leaf 매칭 Handler(일반 Ref 매치는 `isRef(v) and not isPreRef(v) and not isPostRef(v)`), StoreBind와 같은 층위(범용/엔진무관, 2026-08-08 두 번째 세션 확정) │ │ ├── Leaf.luau # (i:number, v=Ref/Observer/Effect/PreRef/PostRef) children-array leaf 매칭 Handler(일반 Ref 매치는 `isRef(v) and not isPreRef(v) and not isPostRef(v)`, Observer/Effect는 `ObserverEffectLeafHandler` 하나가 `type(k)=="number" and (isObserver(v) or isEffect(v))`로 같이 매치 — `base/source-state-plan.md` "Observer/Effect Leaf dedup" 절, 2026-08-14 열두 번째 세션), StoreBind와 같은 층위(범용/엔진무관, 2026-08-08 두 번째 세션 확정)
│ │ ├── Tag.luau # TagHandler — 이름별 참조 카운트(`tagNameMap`), 실제 호출은 주입된 addTag/removeTag(inst, {string}). quad-base가 모듈 로드 시점에 priority=HANDLER_PRIORITY_FALLBACK로 스스로 등록(`base/tag-plan.md`, 2026-08-13 열네 번째 세션 base로 이동) │ │ ├── Tag.luau # TagHandler — 이름별 참조 카운트(`tagNameMap`), 실제 호출은 주입된 addTag/removeTag(inst, {string}). `HANDLER_PRIORITY_FALLBACK`에는 이걸 감싸는 `TagFallbackHandler`가 백엔드 팩토리 뮤테이션 시점에 등록됨(`base/tag-plan.md`, 2026-08-13 열네 번째 세션 base로 이동)
│ │ ├── AttributeKey.luau # AttributeKeyHandler — 이름 claim(`nameClaims`, 소유권 충돌 즉시 error) + 주입된 setAttribute(inst,name,v) 호출, `None`→nil은 재디스패치로 자동(`base/attribute-plan.md` "이름 소유권" 절) │ │ ├── AttributeKey.luau # AttributeKeyHandler — 이름 claim(`nameClaims`, 소유권 충돌 즉시 error) + 주입된 setAttribute(inst,name,v) 호출, `None`→nil은 재디스패치로 자동(`base/attribute-plan.md` "이름 소유권" 절). `HANDLER_PRIORITY_FALLBACK`에는 이걸 감싸는 `AttributeKeyFallbackHandler`가 백엔드 팩토리 뮤테이션 시점에 등록됨
│ │ ├── Attribute.luau # AttributeGroupHandler — 그룹 전용 키(비공개 GetKey)로 이름마다 AttributeKey 경로에 인덱스 1 위임, 클로저가 자기 키 전부 retractFrom(`base/attribute-plan.md` "메커니즘" 절) │ │ ├── Attribute.luau # AttributeGroupHandler — 그룹 전용 키(비공개 GetKey)로 이름마다 AttributeKey 경로에 인덱스 1 위임, 클로저가 자기 키 전부 retractFrom(`base/attribute-plan.md` "메커니즘" 절). `AttributeGroupFallbackHandler`가 같은 방식으로 감쌈
│ │ └── Slot.luau # add/remove/clear 재조정 로직(추상 자식 참조 기준) │ │ └── Slot.luau # add/remove/clear 재조정 로직(추상 자식 참조 기준)
│ ├── Relate.luau # inst를 weak 키로 하는 범용 릴레이션(`SetWeak`/`GetWeak`/`SetStrong`/`GetStrong`), 비싱글톤 생성자(`base/relate-plan.md`) — 구 PerInstanceState/perInstanceState 대체 │ ├── Relate.luau # inst를 weak 키로 하는 범용 릴레이션(`SetWeak`/`GetWeak`/`SetStrong`/`GetStrong`), 비싱글톤 생성자(`base/relate-plan.md`) — 구 PerInstanceState/perInstanceState 대체
│ ├── LifetimeHandle.luau # `bindLifetime(inst,value)`/`unbindLifetime(value)`/`canBound(value)`/`canExecute(value)` 탑레벨 함수 "인터페이스"(타입/계약만), 내부는 Relate 사용(`base/lifecycle-pattern.md`) │ ├── LifetimeHandle.luau # `bindLifetime(inst,value)`/`unbindLifetime(value)`/`canBound(value)`/`canExecute(value)` 탑레벨 함수 "인터페이스"(타입/계약만), 내부는 Relate 사용(`base/lifecycle-pattern.md`)

View file

@ -446,7 +446,7 @@ quad-roblox** 소속이었음 — 그런데 실제로 엔진에 종속된 건
| 그룹 값 타입+API(`Attribute(...)`/`Merged`/`:NameMap`) | quad-base | | 그룹 값 타입+API(`Attribute(...)`/`Merged`/`:NameMap`) | quad-base |
| 단일 키 `AttributeKey<<T>>(name)` + 이름별 weak 캐시 | quad-base | | 단일 키 `AttributeKey<<T>>(name)` + 이름별 weak 캐시 | quad-base |
| 스칼라 편의 패밀리(`StringAttribute`/`NumberAttribute`/`BooleanAttribute`) | quad-base | | 스칼라 편의 패밀리(`StringAttribute`/`NumberAttribute`/`BooleanAttribute`) | quad-base |
| `AttributeKeyHandler`(이름 claim 포함) / `AttributeGroupHandler`(전용 키 위임) | quad-base, `HANDLER_PRIORITY_FALLBACK`으로 quad-base가 스스로 등록 | | `AttributeKeyHandler`(이름 claim 포함) / `AttributeGroupHandler`(전용 키 위임) | quad-base, `HANDLER_PRIORITY_FALLBACK`으로는 이걸 감싸는 `AttributeKeyFallbackHandler`/`AttributeGroupFallbackHandler`가 백엔드 팩토리 뮤테이션 시점에 등록됨(아래 참고) |
| 엔진 고유 타입 패밀리(`Color3Attribute`/`UDim2Attribute`/`InstanceAttribute`류) | 백엔드(quad-roblox의 `D`/`DI` 층) | | 엔진 고유 타입 패밀리(`Color3Attribute`/`UDim2Attribute`/`InstanceAttribute`류) | 백엔드(quad-roblox의 `D`/`DI` 층) |
| **`setAttribute(inst, name, v)`** — `v == nil`이면 그 이름을 지움 | 백엔드가 주입 | | **`setAttribute(inst, name, v)`** — `v == nil`이면 그 이름을 지움 | 백엔드가 주입 |
@ -455,18 +455,23 @@ quad-roblox** 소속이었음 — 그런데 실제로 엔진에 종속된 건
수 없음. 반대로 string/number/boolean은 어느 백엔드에나 있으므로 base에 수 없음. 반대로 string/number/boolean은 어느 백엔드에나 있으므로 base에
둔다. "이 값이 이 백엔드에서 표현 가능한가"라는 **검증도 base가 아니라 둔다. "이 값이 이 백엔드에서 표현 가능한가"라는 **검증도 base가 아니라
주입된 `setAttribute`의 몫** — base는 값을 그대로 흘려보냄. 주입된 `setAttribute`의 몫** — base는 값을 그대로 흘려보냄.
- **`AttributeKeyHandler`/`AttributeGroupHandler` 자신은 quad-base가 - **`AttributeKeyHandler`/`AttributeGroupHandler` 자신은 참조 카운트/이름
모듈 로드 시점에 스스로 등록** — `setAttribute`만 백엔드 팩토리가 claim 알고리즘 구현일 뿐, 스스로 등록되는 주체가 아님(2026-08-14 열두
채우는 타입 계약, 안 채운 슬롯의 base 기본값은 명시적으로 에러내는 번째 세션 정정).** `HANDLER_PRIORITY_FALLBACK`에 실제로 꽂히는 건
스텁(2026-08-14 열한 번째 세션 — 한때 "등록 자체가 백엔드 선택"으로 이걸 감싸는 `AttributeKeyFallbackHandler`/
잘못 정정됐다가 철회됨). 더 명확한 메시지나 진짜 원자적 실패를 원하는 `AttributeGroupFallbackHandler` — 등록 주체는 quad-base 모듈 자체가
백엔드는 opt-in으로 `HANDLER_PRIORITY_FALLBACK + 1`짜리 가로채기 아니라 백엔드 팩토리(`BaseModule` 뮤테이션 시점, 자기 전용 Handler들과
Handler를 추가로 등록 가능. 속성 처리 자체를 통째로 다른 알고리즘으로 같이 등록). 옛 "quad-base 모듈 로드 시점에 스스로 등록" 모델은
바꾸고 싶은 백엔드는 `HANDLER_PRIORITY_FALLBACK`보다 확실히 높은 `archive/tag-attribute-load-time-registration-reversed.md`.
우선순위로 자기 Handler를 등록하면 base 것을 완전히 대체함(같은 `setAttribute`만 백엔드 팩토리가 채우는 타입 계약, 안 채운 슬롯의
override 원리) — 상세는 `base/dispatch-core-plan.md`의 "base가 base 기본값은 명시적으로 에러내는 스텁. 더 명확한 메시지나 진짜
소유하는 핸들러와 주입되는 엔진 op" 절. `Tag`도 정확히 같은 원자적 실패를 원하는 백엔드는 opt-in으로
구조(`base/tag-plan.md`). `HANDLER_PRIORITY_FALLBACK + 1`짜리 가로채기 Handler를 추가로 등록
가능. 속성 처리 자체를 통째로 다른 알고리즘으로 바꾸고 싶은 백엔드는
`HANDLER_PRIORITY_FALLBACK`보다 확실히 높은 우선순위로 자기 Handler를
등록하면 base 것을 완전히 대체함(같은 override 원리) — 상세는
`base/dispatch-core-plan.md`의 "base가 소유하는 핸들러와 주입되는
엔진 op" 절. `Tag`도 정확히 같은 구조(`base/tag-plan.md`).
- 단일 키를 별도 opt-out 패키지로 쪼개지 않는다는 기존 판단은 그대로 - 단일 키를 별도 opt-out 패키지로 쪼개지 않는다는 기존 판단은 그대로
(UICorner 숏핸드/Tween/Tag와 같은 결) — 다만 "어느 패키지의 코어인가"가 (UICorner 숏핸드/Tween/Tag와 같은 결) — 다만 "어느 패키지의 코어인가"가
quad-roblox에서 quad-base로 바뀐 것. quad-roblox에서 quad-base로 바뀐 것.

View file

@ -400,10 +400,12 @@ end
- `Dispatch.addHandler(handler: Handler)` — 핸들러를 우선순위 레지스트리에 - `Dispatch.addHandler(handler: Handler)` — 핸들러를 우선순위 레지스트리에
등록. `Dispatch.process`/`getHandler`와 마찬가지로 base엔 인터페이스만 등록. `Dispatch.process`/`getHandler`와 마찬가지로 base엔 인터페이스만
있고, quad-roblox의 concrete Handler들(PropertyHandler/EventHandler/ 있고, quad-roblox의 concrete Handler들(PropertyHandler/EventHandler/
OnChangeHandler/UICornerHandler/TagHandler/AttributeHandler 등)은 OnChangeHandler/UICornerHandler 등)은 팩토리가 `BaseModule`
팩토리가 `BaseModule`
뮤테이션하는 시점에 이걸로 등록됨(아래 "base 유틸은 인터페이스" 절과 뮤테이션하는 시점에 이걸로 등록됨(아래 "base 유틸은 인터페이스" 절과
같은 패턴, 새 메커니즘 아님). 같은 패턴, 새 메커니즘 아님). **`Tag`/`Attribute`의 base 소유
Fallback Handler들(`TagFallbackHandler` 등)도 같은 팩토리 뮤테이션
시점에 같이 등록됨** — quad-base 모듈 로드 자체의 부작용이 아님,
상세는 아래 "base가 소유하는 핸들러와 주입되는 엔진 op" 절.
- Handler 자신의 필드는 계속 `process`/`retract`(이미 확정된 이름, - Handler 자신의 필드는 계속 `process`/`retract`(이미 확정된 이름,
`question.md`에 "특별한 문제 없음"으로 못박혀 있어 재검토 대상 아님) — `question.md`에 "특별한 문제 없음"으로 못박혀 있어 재검토 대상 아님) —
겹침은 실제 런타임 충돌이 아니라 프로즈 표기 문제였을 뿐이라, 항상 겹침은 실제 런타임 충돌이 아니라 프로즈 표기 문제였을 뿐이라, 항상
@ -557,15 +559,30 @@ setAttribute(inst: any, name: string, v: any?): () -- v == nil이면 그 이름
매핑하면 됨(웹이면 `removeAttribute`). base 쪽 규칙 — "Attribute는 오직 매핑하면 됨(웹이면 `removeAttribute`). base 쪽 규칙 — "Attribute는 오직
명시적 `None`/`nil`로만 지워진다"(`base/attribute-plan.md`) — 은 그대로. 명시적 `None`/`nil`로만 지워진다"(`base/attribute-plan.md`) — 은 그대로.
**[재정정, 2026-08-14 열한 번째 세션 — 앞선 "등록 자체도 백엔드의 **[재정정, 2026-08-14 열두 번째 세션] `TagHandler`/`AttributeKeyHandler`/
선택" 안은 틀렸음, 철회] `TagHandler`/`AttributeKeyHandler`/ `AttributeGroupHandler`는 참조 카운트/이름 claim **알고리즘 구현**일
`AttributeGroupHandler`는 quad-base가 자기 모듈 로드 시점에 뿐이고, 스스로 등록되는 주체가 아니다.** `HANDLER_PRIORITY_FALLBACK`
`HANDLER_PRIORITY_FALLBACK`으로 스스로 `Dispatch.addHandler` 등록한다 실제로 꽂히는 건 그 알고리즘을 그대로 감싸는 **별도 이름의 엔티티**
— 이게 기본이고 필요함.** `HANDLER_PRIORITY_FALLBACK`이라는 밴드 (`TagFallbackHandler`/`AttributeKeyFallbackHandler`/
자체가 정확히 이런 용도 — "아무도 이 자리를 안 가져갔을 때의 안전한 `AttributeGroupFallbackHandler`) — "이게 기본 안전망으로 자동 설치되는
기본 동작"을 base가 공짜로 제공하는 것. 모든 백엔드가 자동으로 대상"임을 이름 자체로 구분한다. **등록 주체는 quad-base 모듈 자체가
`Tag`/`Attribute` 부기(참조 카운트/이름 claim)를 얻고, 특별히 뭔가를 아니라 필요한 엔진(백엔드 팩토리)** — quad-roblox 같은 백엔드가
하지 않아도 이 값들이 어떤 자리에 놓이든 최소한 매치는 됨. `BaseModule`을 구성할 때 자기 전용 Handler들(Property/Event/OnChange/
UICorner)과 **같이** 이 base 소유 Fallback Handler들도 등록해준다(위
`Dispatch.addHandler` 절과 같은 경로, `base/module-lifecycle-plan.md`
이미 확정해둔 "base는 인터페이스만, 등록/구현은 백엔드 팩토리가
`BaseModule`을 뮤테이션하는 시점에" 원칙을 그대로 따르는 것뿐 — 새
예외가 아님). "quad-base가 자기 모듈 로드 시점에 스스로 등록"이라던
옛 모델은 정확히 `base/lifecycle-pattern.md`가 이미 거부해둔
`InitNamespace`류 top-level 부작용 패턴과 같은 클래스라 틀렸음 — 원문·
근거는 `archive/tag-attribute-load-time-registration-reversed.md`.
`HANDLER_PRIORITY_FALLBACK`이라는 밴드 자체가 정확히 이런 용도 —
"아무도 이 자리를 안 가져갔을 때의 안전한 기본 동작"을 base가 값싸게
제공하는 것. 엔진 저자 입장에서 "자동/공짜"인 이유는 직접 알고리즘을
안 짜도 되기 때문이지 quad-base 모듈 자체가 부작용을 내서가 아님 —
모든 백엔드가 (자기 팩토리 뮤테이션 한 번으로) `Tag`/`Attribute` 부기를
얻고, 특별히 뭔가를 더 하지 않아도 이 값들이 어떤 자리에 놓이든 최소한
매치는 됨.
`addTag`/`removeTag`/`setAttribute`는 base가 시그니처만 소유하고 `addTag`/`removeTag`/`setAttribute`는 base가 시그니처만 소유하고
실제 구현은 팩토리가 뮤테이션으로 주입하는 **타입 계약**(`bindLifetime`/ 실제 구현은 팩토리가 뮤테이션으로 주입하는 **타입 계약**(`bindLifetime`/
@ -592,10 +609,13 @@ setAttribute(inst: any, name: string, v: any?): () -- v == nil이면 그 이름
isHandlable = function(inst,k,v) return isTag(v) end, isHandlable = function(inst,k,v) return isTag(v) end,
process = function(inst,k,v) error("이 백엔드는 Tag를 지원하지 않음") end } process = function(inst,k,v) error("이 백엔드는 Tag를 지원하지 않음") end }
``` ```
`TagHandler` 자신(`FALLBACK`)보다 한 단계 높아 스캔에서 먼저 매치되고, 실제로 `FALLBACK`에 등록돼 있는 `TagFallbackHandler`보다 한 단계
"매치된 Handler 하나만 실행"이라는 기존 규칙 덕분에 `TagHandler.process` 높아 스캔에서 먼저 매치되고(2026-08-14 열두 번째 세션 정정 — `TagHandler`
(와 그 안의 `tagNameMap` mutation)는 아예 안 불림 — op 에러보다 자신은 스스로 등록되지 않음, 위 "base가 소유하는 핸들러와 주입되는
이르고 정확한, 진짜 원자적 실패. 단 이건 **선택적 업그레이드**일 뿐 엔진 op" 절 참고), "매치된 Handler 하나만 실행"이라는 기존 규칙
덕분에 `TagHandler.process`(와 그 안의 `tagNameMap` mutation)는 아예
안 불림 — op 에러보다 이르고 정확한, 진짜 원자적 실패. 단 이건
**선택적 업그레이드**일 뿐
기본 요구사항은 아님 — base 기본 스텁 하나로도 이미 충분히 안전하게 기본 요구사항은 아님 — base 기본 스텁 하나로도 이미 충분히 안전하게
실패함(`AttributeGroupHandler`의 "부분 실패 경로" 절이 이미 정리한 실패함(`AttributeGroupHandler`의 "부분 실패 경로" 절이 이미 정리한
"에러=패닉 상태, 그 이후 정합성은 관리 대상 아님" 원칙 + `nameClaims`/ "에러=패닉 상태, 그 이후 정합성은 관리 대상 아님" 원칙 + `nameClaims`/
@ -906,6 +926,21 @@ end
- 그리고 `Relate`에 쓴 걸 클로저에서 지울 땐 **"내가 실제로 물러날 - 그리고 `Relate`에 쓴 걸 클로저에서 지울 땐 **"내가 실제로 물러날
때만"** 지울 것 — 조건 밖에서 무조건 지우면 dedup이 무력화됨 때만"** 지울 것 — 조건 밖에서 무조건 지우면 dedup이 무력화됨
(`RefLeafHandler`가 정확히 이 버그였음). (`RefLeafHandler`가 정확히 이 버그였음).
- **`Observer`/`Effect`의 Leaf 바인딩(`Dispatch/Leaf.luau`)도 `RefLeafHandler`
같은 `old ~= v` dedup을 둠 — correctness 문제는 아니지만 순수 성능
최적화로 채택(2026-08-14 세션, 사용자 판단).** `State<Observer>`/
`State<Effect>`가 재-dispatch될 때 안쪽 값이 실제로 안 바뀌어도(같은
객체가 다시 옴) (A) 분기는 무조건 `retractor(v)`→`h.process(inst,k,v,index)`를
다시 부름 — `Ref`와 달리 이걸 그냥 둬도 **깨지진 않음**: `bindLifetime`/
`unbindLifetime``Relate` weak 테이블 쓰기 몇 개뿐이라(`base/
lifecycle-pattern.md`) 같은 값에 unbind 직후 바로 rebind해도 실제 Roblox
커넥션을 만들거나 끊지 않고, 사용자에게 보이는 재통지도 없음(`Observer`/
`Effect``fn`은 이 leaf 바인딩이 아니라 자기 내부 구독이 따로 발화시킴 —
`base/effect-plan.md`). 하지만 **`==` 비교(바이트코드 1개+분기)가 매번 여러
weak 테이블 쓰기(해싱 비용)를 도는 것보다 항상 더 쌈** — 이득이 공짜에
가까운데 안 넣을 이유가 없다는 판단으로 `RefLeafHandler`와 동일한 패턴을
그대로 적용. 상세 pseudocode는 `base/source-state-plan.md`
"Observer/Effect Leaf dedup" 절.
**5. `Dispatch`를 통해서만 진입한다.** **5. `Dispatch`를 통해서만 진입한다.**
`handler.process(...)`를 직접 부르면 핸들러 비교와 `chains` 기록이 통째로 `handler.process(...)`를 직접 부르면 핸들러 비교와 `chains` 기록이 통째로

View file

@ -70,9 +70,10 @@ leaf가 살아있는 동안만 유효, leaf가 죽으면 최종 정리 콜백
leaf당 실제 Destroying 바인딩 하나(공유 weak table로 되는 Observer보다 leaf당 실제 Destroying 바인딩 하나(공유 weak table로 되는 Observer보다
비쌈) — 필요할 때만 쓰는 걸로 충분. 비쌈) — 필요할 때만 쓰는 걸로 충분.
**동적 경로 가드 — `k` 무관 매치, `HANDLER_PRIORITY_FALLBACK` ### 동적 경로 가드 — `k` 무관 매치, `HANDLER_PRIORITY_FALLBACK`
(2026-08-14 열한 번째 세션, `PreRef`/`Observer`와 같은 패턴, `base/ (2026-08-14 열한 번째 세션, `PreRef`/`Observer`와 같은 패턴, `base/
source-state-plan.md`의 "동적 경로 가드" 절 참고).** `EffectHandle` source-state-plan.md`의 "동적 경로 가드" 절 참고.) `EffectHandle`
children 배열 리터럴 전용이라, 해시 파트 named 자리 등으로 동적으로 children 배열 리터럴 전용이라, 해시 파트 named 자리 등으로 동적으로
흘러들어오면 명확히 에러내야 함 — `{ priority = HANDLER_PRIORITY_FALLBACK, 흘러들어오면 명확히 에러내야 함 — `{ priority = HANDLER_PRIORITY_FALLBACK,
isHandlable = function(inst,k,v) return isEffect(v) end, process = isHandlable = function(inst,k,v) return isEffect(v) end, process =

View file

@ -442,7 +442,7 @@ quad-roblox 구현 단계에서 실측 확인 대상 — 문제가 되면 gcconn
이건 `base/dispatch-core-plan.md`의 "핸들러 내부 상태 저장" 유틸(`Relate` 이건 `base/dispatch-core-plan.md`의 "핸들러 내부 상태 저장" 유틸(`Relate`
직접 사용)과 짝을 이루는 별도 유틸 — 하나는 "상태를 어디에 저장할지" 직접 사용)과 짝을 이루는 별도 유틸 — 하나는 "상태를 어디에 저장할지"
(`Relate:SetStrong`/`:SetWeak`), 다른 하나는 "언제까지 실행되어도 되는지" (`Relate:SetStrong`/`:SetWeak`), 다른 하나는 "언제까지 실행되어도 되는지"
(`bindLifetime` + `canExecute`)를 다룸. 후자는 내부적으로 전자가 제공하는 (`bindLifetime` + `canBound`/`canExecute`)를 다룸. 후자는 내부적으로 전자가 제공하는
같은 `Relate` 프리미티브 위에 얹혀 구현됨(위 절) — 별도 저장 메커니즘을 같은 `Relate` 프리미티브 위에 얹혀 구현됨(위 절) — 별도 저장 메커니즘을
새로 만든 게 아니라 `Relate` 하나를 두 용도로 재사용. 둘 다 base가 제공하는 새로 만든 게 아니라 `Relate` 하나를 두 용도로 재사용. 둘 다 base가 제공하는
범용 유틸로 확정. 범용 유틸로 확정.

View file

@ -135,12 +135,18 @@ init하려 하면 오류, 없는데 뭔가 생성해서 bind하려 해도 오류
알고리즘이 통째로 quad-base로 옮겨오면서, 엔진에 실제로 손대는 마지막 알고리즘이 통째로 quad-base로 옮겨오면서, 엔진에 실제로 손대는 마지막
한 줄만 이 경로로 주입받게 됨(`base/dispatch-core-plan.md` "base가 한 줄만 이 경로로 주입받게 됨(`base/dispatch-core-plan.md` "base가
소유하는 핸들러와 주입되는 엔진 op" 절). **`TagHandler`/ 소유하는 핸들러와 주입되는 엔진 op" 절). **`TagHandler`/
`AttributeKeyHandler`/`AttributeGroupHandler` 자신은 quad-base가 `AttributeKeyHandler`/`AttributeGroupHandler` 자신은 참조 카운트/이름
모듈 로드 시점에 `HANDLER_PRIORITY_FALLBACK`으로 스스로 등록** — claim 알고리즘 구현일 뿐 스스로 등록되는 주체가 아님(2026-08-14 열두
`addTag`/`removeTag`/`setAttribute`만 백엔드 팩토리가 뮤테이션으로 번째 세션 정정, 옛 "quad-base 모듈 로드 시점에 스스로 등록" 모델은
채우는 타입 계약. 아직 아무 팩토리도 안 채운 슬롯의 기본값은 `archive/tag-attribute-load-time-registration-reversed.md`) —
quad-base가 명시적으로 에러내는 스텁으로 미리 채워둠(조용한 no-op `HANDLER_PRIORITY_FALLBACK`에 실제로 꽂히는 건 이걸 감싸는
추측 아님 — base가 임의 엔진의 "맞는 기본 동작"을 알 수 없어서). `TagFallbackHandler`/`AttributeKeyFallbackHandler`/
`AttributeGroupFallbackHandler`이고, 등록 주체는 quad-base 모듈 자체가
아니라 백엔드 팩토리(바로 위 문단과 같은 `BaseModule` 뮤테이션 경로 —
새 예외 아님).** `addTag`/`removeTag`/`setAttribute`만 백엔드 팩토리가
뮤테이션으로 채우는 타입 계약. 아직 아무 팩토리도 안 채운 슬롯의
기본값은 quad-base가 명시적으로 에러내는 스텁으로 미리 채워둠(조용한
no-op 추측 아님 — base가 임의 엔진의 "맞는 기본 동작"을 알 수 없어서).
더 명확한 메시지나 진짜 원자적 실패(부기 mutation 0회)를 원하는 더 명확한 메시지나 진짜 원자적 실패(부기 mutation 0회)를 원하는
백엔드는 opt-in으로 `HANDLER_PRIORITY_FALLBACK + 1`짜리 가로채기 백엔드는 opt-in으로 `HANDLER_PRIORITY_FALLBACK + 1`짜리 가로채기
Handler를 추가로 등록할 수 있음 — 상세는 Handler를 추가로 등록할 수 있음 — 상세는

View file

@ -254,7 +254,9 @@ skip"이라는 dedup은 `process`가 "이전에 뭐가 있었는지"를 알아
local relate = Relate() -- Ref-leaf handler 전용, (inst,k)별 마지막으로 바인딩한 Ref 기억 — local relate = Relate() -- Ref-leaf handler 전용, (inst,k)별 마지막으로 바인딩한 Ref 기억 —
-- process의 spurious 재바인딩 dedup 전용(클로저 캡처로는 대체 불가) -- process의 spurious 재바인딩 dedup 전용(클로저 캡처로는 대체 불가)
RefLeafHandler.isHandlable(inst, k, v) = isRef(v) and not isPreRef(v) RefLeafHandler.isHandlable(inst, k, v) = isRef(v) and not isPreRef(v) and not isPostRef(v)
-- [2026-08-14 열두 번째 세션 정정] PostRef 도입(아홉 번째 세션) 당시 이 자리가
-- 안 갱신돼 있었음 — 아래 "타입/판별" 절의 최종 공식과 일치시킴
function RefLeafHandler.process(inst, k, v, index) function RefLeafHandler.process(inst, k, v, index)
local old = relate:GetStrong(inst, k) local old = relate:GetStrong(inst, k)
@ -583,7 +585,7 @@ flatten된 값은 해시 파트(프로퍼티 키)로 존재하게 되고, Store
나지만, 나중에 named 자리 바인드 같은 실제 기능이 확정되면 base 나지만, 나중에 named 자리 바인드 같은 실제 기능이 확정되면 base
가드를 건드리지 않고 그 기능의 Handler를 평범한 우선순위로 하나 가드를 건드리지 않고 그 기능의 Handler를 평범한 우선순위로 하나
등록하는 것만으로 자연히 우선함, `base/dispatch-core-plan.md` 등록하는 것만으로 자연히 우선함, `base/dispatch-core-plan.md`
"`HANDLER_PRIORITY_FALLBACK`" 절). 이 "base가 소유하는 핸들러와 주입되는 엔진 op" 절). 이
Handler는 **`Dispatch.process`/`getHandler`의 정상 우선순위 스캔에 Handler는 **`Dispatch.process`/`getHandler`의 정상 우선순위 스캔에
등록**되는 반면(pre-pass처럼 그 밖에서 도는 게 아님), 리터럴 배열의 등록**되는 반면(pre-pass처럼 그 밖에서 도는 게 아님), 리터럴 배열의
`PreRef`는 pre-pass가 fire와 동시에 해당 슬롯을 소진(**[정정, `PreRef`는 pre-pass가 fire와 동시에 해당 슬롯을 소진(**[정정,
@ -758,7 +760,7 @@ dispatch-core-plan.md` "Length/Offset" 절의 계약을 특수 취급 없이 그
이유는 `ProcessedPreRef`와 동일 — "원래부터 빈 자리(`None`)"와 구별돼야 이유는 `ProcessedPreRef`와 동일 — "원래부터 빈 자리(`None`)"와 구별돼야
등록 책임 소재가 분명해지고, 배열에 구멍이 안 생김. 등록 책임 소재가 분명해지고, 배열에 구멍이 안 생김.
**동적 경로 가드 Handler도 거울상으로 하나 더** ### 동적 경로 가드 Handler도 거울상으로 하나 더
`PreRef`와 똑같이, `PostRef`**children 배열의 리터럴 아이템으로만** 놓을 `PreRef`와 똑같이, `PostRef`**children 배열의 리터럴 아이템으로만** 놓을
수 있음 — Modifier 필드/Source/Store 값으로는 **타입으로 차단**(이유도 수 있음 — Modifier 필드/Source/Store 값으로는 **타입으로 차단**(이유도

View file

@ -897,23 +897,29 @@ retract/Destroy되면 자동으로 정리됨.
`Ref`와 같은 방식으로 라이프사이클에 묶어주는 것 말고는 base가 `Ref`와 같은 방식으로 라이프사이클에 묶어주는 것 말고는 base가
더 해줄 일이 없음. 새 dispatch 메커니즘이 아니라 기존 children-array 더 해줄 일이 없음. 새 dispatch 메커니즘이 아니라 기존 children-array
참가자 패턴의 반복. 참가자 패턴의 반복.
- **동적 경로 가드 — `k` 무관 매치, `HANDLER_PRIORITY_FALLBACK`
(2026-08-14 열한 번째 세션, `PreRef`의 동적 경로 가드와 같은 패턴).** ### 동적 경로 가드 — `k` 무관 매치, `HANDLER_PRIORITY_FALLBACK`
`Observer`도 children 배열 리터럴 전용이라, 해시 파트 named 자리
등으로 동적으로 흘러들어오면(타입 우회 버그) 명확히 에러내야 함 — (2026-08-14 열한 번째 세션, `PreRef`의 동적 경로 가드와 같은 패턴.)
전용 `Handler` 등록: `{ priority = HANDLER_PRIORITY_FALLBACK, `Observer`도 children 배열 리터럴 전용이라, 해시 파트 named 자리
isHandlable = function(inst,k,v) return isObserver(v) end, process = 등으로 동적으로 흘러들어오면(타입 우회 버그) 명확히 에러내야 함 —
function(inst,k,v) error("Observer는 children 배열 리터럴에만 놓을 수 전용 `Handler` 등록: `{ priority = HANDLER_PRIORITY_FALLBACK,
있음") end }`. `HANDLER_PRIORITY_FALLBACK`인 이유는 이게 무조건 막는 isHandlable = function(inst,k,v) return isObserver(v) end, process =
하드 블록이 아니라 `Tag`/`Attribute`/`PreRef`와 같은 "base가 소유하되 function(inst,k,v) error("Observer는 children 배열 리터럴에만 놓을 수
평범한 우선순위로 등록된 다른 Handler가 있으면 그쪽이 이기는" 자리이기 있음") end }`. `HANDLER_PRIORITY_FALLBACK`인 이유는 이게 무조건 막는
때문(`base/dispatch-core-plan.md`의 "`HANDLER_PRIORITY_FALLBACK`" 절) — 하드 블록이 아니라 `Tag`/`Attribute`/`PreRef`와 같은 "base가 소유하되
지금은 아무도 그 자리를 안 가져가서 항상 이 가드가 에러를 내지만, 이 평범한 우선순위로 등록된 다른 Handler가 있으면 그쪽이 이기는" 자리이기
Handler를 만드는 게 목적이 아니라 "지금은 확정된 기능이 없다"는 default를 때문(`base/dispatch-core-plan.md`의 "base가 소유하는 핸들러와 주입되는
base가 값싸게 제공하는 것뿐. (**이 가드가 없던 이전엔** 확정된 "매치 엔진 op" 절) —
실패는 즉시 error" 규칙에 의해 결과적으로 똑같이 에러가 났었음 — 이 지금은 아무도 그 자리를 안 가져가서 항상 이 가드가 에러를 내지만, 이
가드는 동작을 바꾸는 게 아니라 에러 메시지를 명확하게 하고, 미래에 Handler를 만드는 게 목적이 아니라 "지금은 확정된 기능이 없다"는 default를
override할 자리를 구조적으로 열어두는 것.) base가 값싸게 제공하는 것뿐. (**이 가드가 없던 이전엔** 확정된 "매치
실패는 즉시 error" 규칙에 의해 결과적으로 똑같이 에러가 났었음 — 이
가드는 동작을 바꾸는 게 아니라 에러 메시지를 명확하게 하고, 미래에
override할 자리를 구조적으로 열어두는 것.)
이어서, base가 제공하는 나머지 항목:
- **콜백 실행은 기존 `canExecute` predicate로 게이팅**(Slot 생존 확인과 - **콜백 실행은 기존 `canExecute` predicate로 게이팅**(Slot 생존 확인과
동일한 재사용 — "canExecute 하나로 통일" 원칙, 새 메커니즘 발명 아님) 동일한 재사용 — "canExecute 하나로 통일" 원칙, 새 메커니즘 발명 아님)
— 발화 시점과 처리 시점 사이에 owning leaf가 이미 죽었으면 no-op. — 발화 시점과 처리 시점 사이에 owning leaf가 이미 죽었으면 no-op.
@ -1159,7 +1165,8 @@ leaf 부착을 통한 간접 호출이든) 둘뿐** — 새 규칙을 따로 만
```lua ```lua
function bindLifetime(inst, value) function bindLifetime(inst, value)
if canExecute(value) then if canBound(value) then -- [정정, 2026-08-14 열두 번째 세션] 이 절이 확정한 대로
-- bindLifetime의 게이트는 canBound, canExecute 아님
error("이미 바인딩된 값") -- 메시지 분기는 위 게이트 스케치 참고 error("이미 바인딩된 값") -- 메시지 분기는 위 게이트 스케치 참고
end end
... -- gchold 등록 + gcconn 참조 복사(base/lifecycle-pattern.md) ... -- gchold 등록 + gcconn 참조 복사(base/lifecycle-pattern.md)
@ -1181,6 +1188,51 @@ end
뒤에 같은 값을 leaf로 놓거나 `:Subscribe()`하면 기존 두 진입점의 기존 뒤에 같은 값을 leaf로 놓거나 `:Subscribe()`하면 기존 두 진입점의 기존
체크가 그대로 걸러줌 — 이 방향은 별도 코드 추가 없이 이미 성립. 체크가 그대로 걸러줌 — 이 방향은 별도 코드 추가 없이 이미 성립.
### Observer/Effect Leaf dedup — `RefLeafHandler`와 같은 패턴, 순수 성능 최적화(2026-08-14 세션)
**correctness 문제는 아님 — `old ~= v`를 안 넣어도 안 깨짐.** `State<Observer>`/
`State<Effect>`가 재-dispatch될 때 안쪽 값이 그대로여도(같은 객체가 다시 옴)
Dispatch의 (A) 분기(`base/dispatch-core-plan.md` "Dispatch 체인" 절)는 무조건
`retractor(v)`→`process(inst,k,v,index)`를 다시 부름 — 이걸 그냥 둬도
`bindLifetime`/`unbindLifetime`이 `Relate` weak 테이블 쓰기 몇 개뿐이라(위
"(1)" 코드 블록, `base/lifecycle-pattern.md`) 실제 Roblox 커넥션을 만들거나
끊지 않고, 사용자에게 보이는 재통지도 없음(`fn` 재실행은 이 leaf 바인딩이
아니라 자기 내부 구독이 따로 트리거함).
**그래도 dedup을 넣기로 함(사용자 판단)** — `==` 비교 하나(바이트코드 1개
+ 분기)가 매번 여러 weak 테이블 읽기/쓰기(해싱 비용)를 도는 것보다 항상 더
싸서, 이득이 공짜에 가까운데 안 넣을 이유가 없음. `RefLeafHandler`(`base/
ref-plan.md` "`Ref`의 retract" 절)와 완전히 같은 모양을 그대로 재사용:
```lua
local relate = Relate() -- Observer/Effect-leaf 전용, (inst,k)별 마지막으로 바인딩한 값 기억 —
-- process 재실행 시 identical-value dedup(순수 성능 최적화,
-- Ref처럼 재통지 부작용이 있어서가 아님)
ObserverEffectLeafHandler.isHandlable(inst, k, v) =
type(k) == "number" and (isObserver(v) or isEffect(v))
-- k 타입까지 반드시 체크 — 안 그러면 바로 위 "동적 경로 가드" FALLBACK
-- Handler(named 자리로 흘러온 값을 에러내려는 것)가 이 자리에 먼저
-- 매치돼버려 죽은 코드가 됨(2026-08-14 열두 번째 세션 수정)
function ObserverEffectLeafHandler.process(inst, k, v, index)
local old = relate:GetStrong(inst, k)
if old ~= v then -- 이미 같은 값이 이 자리를 차지 중이면 재바인딩 skip
bindLifetime(inst, v) -- Effect는 내부적으로 자기 Observer까지 cascade(`base/effect-plan.md`)
end
relate:SetStrong(inst, k, v)
return function(nextValue)
if nextValue ~= v then
unbindLifetime(v)
-- [`RefLeafHandler`와 같은 주의] relate 정리는 반드시 이 분기 *안*에서만 —
-- 밖에 두면 spurious 재발행(nextValue == v)에서도 기록이 지워져 곧바로
-- 이어지는 process가 `old ~= v`를 항상 참으로 보고 dedup이 무력화됨.
if relate:GetStrong(inst, k) == v then relate:SetStrong(inst, k, nil) end
end
end
end
```
## PA님 코드와의 교차검증(2026-08-04 4차 라운드) — 둘 다 기존 확정 유지 ## PA님 코드와의 교차검증(2026-08-04 4차 라운드) — 둘 다 기존 확정 유지
`.claude/initreq/artworks/EventDrivenProgramming/`(Connection/Event/ `.claude/initreq/artworks/EventDrivenProgramming/`(Connection/Event/

View file

@ -152,7 +152,9 @@ identity로 홀더를 추적하면 같은 객체를 두 위치(`k1`, `k2`)에
```lua ```lua
local tagNameMap = Relate() -- {[inst(weak)] = {[tagName]: {[k]: true}}} — 이름별 현재 걸고 있는 위치들 local tagNameMap = Relate() -- {[inst(weak)] = {[tagName]: {[k]: true}}} — 이름별 현재 걸고 있는 위치들
TagHandler.priority = HANDLER_PRIORITY_FALLBACK -- TagHandler 자신은 `.priority` 없음(직접 등록 안 됨) — 아래 "패키지 배치" 절의
-- `TagFallbackHandler = { priority = HANDLER_PRIORITY_FALLBACK, isHandlable = TagHandler.isHandlable,
-- process = TagHandler.process }`가 실제로 등록되는 얇은 래퍼(2026-08-14 열두 번째 세션 정정)
TagHandler.isHandlable(inst, k, v) = isTag(v) -- Brand 기반, array-part 전용 TagHandler.isHandlable(inst, k, v) = isTag(v) -- Brand 기반, array-part 전용
function TagHandler.process(inst, k, v, index) function TagHandler.process(inst, k, v, index)
@ -264,19 +266,34 @@ removeTag(inst: any, names: {string}): ()
vararg → `string | {string}`으로 되돌아갔던 것과 **같은 이유**(위 "값 vararg → `string | {string}`으로 되돌아갔던 것과 **같은 이유**(위 "값
모양" 절). 배치 호출 자체는 테이블로도 되므로 웹 `className` 일괄 모양" 절). 배치 호출 자체는 테이블로도 되므로 웹 `className` 일괄
갱신 요구도 그대로 충족됨. 갱신 요구도 그대로 충족됨.
- **등록 우선순위는 `HANDLER_PRIORITY_FALLBACK``TagHandler` 자신은 - **등록 우선순위는 `HANDLER_PRIORITY_FALLBACK` — 단 여기 꽂히는 건
quad-base가 모듈 로드 시점에 스스로 등록**(2026-08-14 열한 번째 세션 `TagHandler` 자신이 아니라 그걸 감싸는 `TagFallbackHandler`(2026-08-14
재확인 — 한때 "등록 자체가 백엔드 선택"으로 잘못 정정됐다가 철회됨). 열두 번째 세션 정정 — `TagHandler`는 참조 카운트 알고리즘 구현일
뿐, 스스로 등록되는 주체가 아님).** 등록 주체는 quad-base 모듈 자체가
아니라 백엔드 팩토리 — quad-roblox 같은 백엔드가 `BaseModule`
구성할 때 자기 전용 Handler들과 같이 이 `TagFallbackHandler`도 등록해줌
(`base/module-lifecycle-plan.md`가 이미 확정해둔 "base는 인터페이스만,
등록은 팩토리 뮤테이션 시점" 원칙 그대로). 옛 "quad-base 모듈 로드
시점에 스스로 등록" 모델은
`archive/tag-attribute-load-time-registration-reversed.md`.
`addTag`/`removeTag`만 백엔드 팩토리가 채우는 타입 계약, 안 채운 `addTag`/`removeTag`만 백엔드 팩토리가 채우는 타입 계약, 안 채운
슬롯의 base 기본값은 명시적으로 에러내는 스텁. 더 명확한 메시지나 슬롯의 base 기본값은 명시적으로 에러내는 스텁. 더 명확한 메시지나
진짜 원자적 실패를 원하는 백엔드는 opt-in으로 진짜 원자적 실패를 원하는 백엔드는 opt-in으로
`HANDLER_PRIORITY_FALLBACK + 1`짜리 가로채기 Handler를 추가로 등록 `HANDLER_PRIORITY_FALLBACK + 1`짜리 가로채기 Handler를 추가로 등록
가능. 태그 처리 자체를 통째로 다른 알고리즘으로 바꾸고 싶은 백엔드는 가능. 태그 처리 자체를 통째로 다른 알고리즘으로 바꾸고 싶은 백엔드는
`HANDLER_PRIORITY_FALLBACK`보다 확실히 높은 우선순위로 자기 Handler를 `HANDLER_PRIORITY_FALLBACK`보다 확실히 높은 우선순위로 자기 Handler를
등록하면 base `TagHandler`를 완전히 대체함(같은 override 원리). 등록하면 base `TagFallbackHandler`를 완전히 대체함(같은 override 원리).
상세는 `base/dispatch-core-plan.md`의 "base가 소유하는 핸들러와 상세는 `base/dispatch-core-plan.md`의 "base가 소유하는 핸들러와
주입되는 엔진 op" 절 — `Attribute`도 정확히 같은 구조 주입되는 엔진 op" 절 — `Attribute`도 정확히 같은 구조
(`base/attribute-plan.md`). (`base/attribute-plan.md`). 래퍼는 필드 셋뿐인 얇은 재노출:
```lua
TagFallbackHandler = {
priority = HANDLER_PRIORITY_FALLBACK,
isHandlable = TagHandler.isHandlable,
process = TagHandler.process,
}
```
- 이건 새 아키텍처 개념이 아니라 이미 확정된 "base는 인터페이스/값, - 이건 새 아키텍처 개념이 아니라 이미 확정된 "base는 인터페이스/값,
backend는 구현"(`LifetimeHandle`의 `bindLifetime`/`canExecute`, backend는 구현"(`LifetimeHandle`의 `bindLifetime`/`canExecute`,
`Dispatch.addHandler` 자체가 그 패턴)을 핸들러 층까지 밀어붙인 것. `Dispatch.addHandler` 자체가 그 패턴)을 핸들러 층까지 밀어붙인 것.

View file

@ -18,7 +18,8 @@
> lazy 핸들 계약)는 열세 번째 세션에, `0-Z`(Attribute 이름 소유권)와 > lazy 핸들 계약)는 열세 번째 세션에, `0-Z`(Attribute 이름 소유권)와
> `0-A`(재디스패치 하강 diff)는 열네 번째 세션에, **`0-W`(`Ref` 이중 > `0-A`(재디스패치 하강 diff)는 열네 번째 세션에, **`0-W`(`Ref` 이중
> 배치 방지)는 2026-08-14 열한 번째 세션에 확정·`base/` 반영 완료** — > 배치 방지)는 2026-08-14 열한 번째 세션에 확정·`base/` 반영 완료** —
> "결정 대기" 절 자체가 지금은 비어 있음. 해소 전 원문과 결론은 > 그래서 이 문서엔 이제 "결정 대기" 절 자체가 없음(비어서 헤딩째로
> 삭제). 해소 전 원문과 결론은
> `archive/question-resolved.md`, 뒤집힌 옛 재디스패치 모델은 > `archive/question-resolved.md`, 뒤집힌 옛 재디스패치 모델은
> `archive/dispatch-hintvalue-model-reversed.md`. > `archive/dispatch-hintvalue-model-reversed.md`.
> >

View file

@ -0,0 +1,212 @@
# 2026-08-14 열두 번째 세션 — Observer/Effect Leaf에 `Ref`와 같은 identical-value dedup 추가(성능 최적화)
## 배경
이전 대화에서 State<Ref>/State<Effect>/State<Observer>가 전부 설계상
지원되는지(읽기만으로 판단) 확인한 뒤, 사용자가 이어서 "state<Observer>
`state`가 emit됐는데 내부 Observer 객체 자체는 안 바뀐 경우, unbind/bind가
안 일어나고 process/retract가 nop인지"를 물었다.
## 1차 조사 — Dispatch 계층엔 값 동일성 비교가 없음
`base/dispatch-core-plan.md`의 "Dispatch 체인" 절(`Dispatch.process`의
(A) 분기)을 추적한 결과: 핸들러 **타입**이 같으면 `v`가 이전 값과 같은
객체든 아니든 무조건 `slot.retractor(v)`→`h.process(inst,k,v,index)`가
다시 불림 — Dispatch 자신은 값 동일성을 전혀 비교하지 않고, 값 단위
dedup은 필요한 핸들러가 직접 `Relate`로 구현해야 하는 몫(`RefLeafHandler`가
`old ~= v``Relate`로 체크하는 게 그 예, `base/ref-plan.md` "`Ref`의
retract" 절). `Dispatch/Leaf.luau`가 매치하는 Observer/Effect 쪽엔 이
dedup이 문서 어디에도 없다는 걸 확인하고 사용자에게 "확정된 설계는 아님,
잠재적 갭"으로 보고했다.
## 2차 — "메인에서 작업해도 됨, 문제 되는 부분 찾아 고쳐" 지시 후 재조사
사용자가 다른 에이전트는 쉬고 있으니 메인 브랜치에서 직접 작업해도
된다며 이 갭을 고치라고 지시. 처음엔 곧바로 `RefLeafHandler`와 같은
dedup을 추가하려 했으나, 그 전에 `base/lifecycle-pattern.md`
`bindLifetime`/`unbindLifetime` **실제 구현 스케치**를 다시 추적해보니
애초에 "버그"가 아니었음이 드러났다:
- `bindLifetime`/`unbindLifetime`은 `Relate` weak 테이블에 대한 필드
쓰기 몇 개뿐 — 실제 Roblox 커넥션(`gcconn`)은 **Instance 생성 시점에
단 한 번만** 만들어져 모든 `bindLifetime` 호출이 공유(위 문서 "(0)"
절). 같은 값에 unbind 직후 바로 rebind해도 커넥션을 만들거나 끊지
않음 — 저렴하고 안전.
- `Ref`가 dedup이 **꼭 필요했던** 이유는 `RefLeafHandler.process`
`v:Set(inst)`를 호출해서 — 이건 사용자가 등록한 `:Callback()`
스퓨리어스하게 재통지하는 **관측 가능한 부작용**. Observer/Effect의
Leaf 바인딩(`bindLifetime`만 호출)엔 이런 부작용이 없음 — `fn` 재실행은
leaf 바인딩이 아니라 Observer/Effect 자기 내부 구독이 따로 트리거함.
이 재조사를 근거로 처음엔 `base/dispatch-core-plan.md`에 "Observer/Effect
Leaf는 이 dedup이 불필요 — 확인 후 기각"이라는 노트를 추가하고, 사용자에게
"버그가 아니었다"고 보고했다(이 시점까지는 커밋 전).
## 3차 — 사용자 반론: 성능상 `==` 비교가 항상 더 쌈, 그래도 넣는다
사용자가 correctness 문제가 아니라는 판단엔 동의하면서도, **성능
관점에서 `==` 비교(바이트코드 1개 + 분기)가 매번 여러 `Relate` weak
테이블 읽기/쓰기(해싱 비용)를 도는 것보다 항상 더 싸다**고 지적 — 이득이
공짜에 가까운데 안 넣을 이유가 없다는 판단으로, `RefLeafHandler`와 같은
패턴을 그냥 넣기로 확정.
## 최종 반영
- `base/dispatch-core-plan.md` "핸들러 내부 상태 저장" 절(4번) — "불필요,
확인 후 기각" 노트를 "채택함(correctness 아니라 순수 성능 최적화)"으로
교체, 근거(gcconn 공유로 안전함 + `==` 비교가 항상 더 쌈) 둘 다 명시.
- `base/source-state-plan.md`에 새 절 **"Observer/Effect Leaf dedup"**
신설(위 "bindLifetime이 이 게이트의 두 번째 진입점이다" 절 바로 뒤,
"PA님 코드와의 교차검증" 절 앞) — `RefLeafHandler`와 완전히 같은 모양의
pseudocode(`old ~= v` 체크, retractor 안에서만 `relate` 정리하는
기존 주의사항까지 그대로 재사용).
- `doc-check.py` ERROR 0 유지 확인(중간에 절 제목이 줄바꿈에 걸쳐
인용되면서 생긴 WARN 1건은 그 자리에서 한 줄로 재정렬해 해소).
## 교훈
- **"문제로 보인다"와 "실제로 문제다"는 구현 스케치를 끝까지 추적해야
갈린다** — `bindLifetime`/`unbindLifetime`의 실제 비용(weak table 쓰기
몇 개, 커넥션 재생성 없음)을 확인하기 전까진 이게 실제 버그인지 판단할
근거가 없었음. 처음 보고("잠재적 갭")는 근거 없이 과하게 신중했고, 재조사
후 "버그 아님"도 성능 관점을 놓쳐 성급했음 — 두 번 다 사용자가 다음
질문으로 바로잡음.
- **"correctness엔 불필요"와 "넣을 가치가 없다"는 다른 결론** — 안전하다고
최적화까지 자동으로 기각되는 건 아님, 비용 대비 이득을 따로 판단해야 함.
## 4차 — `/code-review` 실행, 확정 findings 10건 + 추가 발견 1건
사용자가 `/code-review`(effort: high)를 실행 — 이 코퍼스는 소스코드가
없는 설계 문서 저장소라 "버그"는 자기모순/dead pseudocode/깨진 절
참조로 정의됨. 결과 10건을 `ReportFindings`로 렌더한 뒤, 같은
task-id가 한 번 더(다른 표본의 finder 조합으로) notify하며 1건을
추가로 찾아냄(`effect-plan.md`/`ref-plan.md`/`source-state-plan.md`의
"동적 경로 가드"가 볼드 텍스트일 뿐 실제 마크다운 헤딩이 아닌데 6곳이
절 제목처럼 인용 — `doc-check.py`로 직접 재확인해 사전에 존재하던
WARN임을 검증, "이번 diff가 새로 만들었다"는 리뷰의 프레이밍만 부정확
했음). 사용자가 "전부 고쳐줘"로 확정.
## 5차 — 고치던 중 사용자 개입: Tag/Attribute 자기등록 모델 자체가 틀림
findings #5(`dispatch-core-plan.md:402`)/#6(`:560`)을 고치려던 참에
사용자가 끼어들어 "tag, attribute를 base가 스스로 등록한다는 사실이
아닌거 알지?"라고 지적 — 이건 열한 번째 세션이 네 라운드 정정 끝에
확정했던 "`TagHandler`/`AttributeKeyHandler`/`AttributeGroupHandler`가
quad-base 모듈 로드 시점에 스스로 등록"이라는 결론 그 자체였다.
사용자가 정정한 모델: (1) `TagHandler` 등 그 이름들은 참조 카운트/이름
claim **알고리즘 구현**일 뿐 — 공유 코드라 base에 위치하는 것뿐,
스스로 등록되는 주체가 아님. (2) `HANDLER_PRIORITY_FALLBACK`에 실제로
꽂히는 건 이를 감싸는 **별도 이름의 엔티티**(`TagFallbackHandler` 등)여야
함 — 이름 자체로 "자동 안전망"임을 구분. (3) 등록 주체는 quad-base
모듈이 아니라 **필요한 엔진(백엔드 팩토리)**`BaseModule` 뮤테이션
시점에 자기 전용 Handler들과 같이 등록. 사후 검증 결과 이건
`base/lifecycle-pattern.md`가 이미 명시적으로 거부해둔 `InitNamespace`
top-level 부작용 패턴과 정확히 같은 클래스의 실수였고,
`base/module-lifecycle-plan.md`가 이미 일반화해둔 "base는 인터페이스만,
등록/구현은 팩토리 뮤테이션 시점"이라는 원칙을 이 Tag/Attribute
결론만 예외로 뒀던 것이었다.
**반영**: `archive/tag-attribute-load-time-registration-reversed.md`
신설(뒤집힌 원문+근거), `base/dispatch-core-plan.md`("base가 소유하는
핸들러와 주입되는 엔진 op" 절 전면 재작성 + `addHandler` 절 수정),
`base/tag-plan.md`, `base/attribute-plan.md`, `base/architecture.md`
(Tag.luau 파일 설명), `base/module-lifecycle-plan.md` 전부 새 이름
(`TagFallbackHandler`/`AttributeKeyFallbackHandler`/
`AttributeGroupFallbackHandler`)과 새 등록 주체 서술로 갱신.
`CLAUDE.md`의 11번째 세션 서술은 **역사적 기록으로 그대로 둠**(그
세션 시점엔 최선의 결론이었음) — 이 재역전만 짧게 링크로 남김.
## 최종 반영 (code-review findings 11건 전부)
1. `source-state-plan.md` `ObserverEffectLeafHandler.isHandlable`
`type(k) == "number"` 체크 추가(FALLBACK 가드가 죽은 코드였던 것 수정).
2. `source-state-plan.md``bindLifetime` pseudocode `canExecute`
`canBound`로 정정.
3. `README.md` source-state-plan.md 행의 "canExecute 하나로 통합" 서술을
`canBound`/`canExecute` 분리 모델로 갱신.
4. `lifecycle-pattern.md` 445줄 "bindLifetime + canExecute"에 `canBound`
추가.
5-6. `dispatch-core-plan.md`의 Tag/Attribute 등록 모델 전면 재작성(위
5차 참고, 원래 findings의 "stale 문장"/"미설명 배너" 둘 다 이걸로 해소).
7. `ref-plan.md:585`/`source-state-plan.md:910`의 존재하지 않는
`"HANDLER_PRIORITY_FALLBACK"` 절 인용을 실제 헤딩 제목("base가
소유하는 핸들러와 주입되는 엔진 op")으로 정정.
8. `question.md`/`CLAUDE.md`/`HUMAN_TODO.md`의 "결정 대기 절이 비어
있음" 서술을 "헤딩째로 삭제됨"으로 정정(헤딩 자체가 이미 없었음).
9. `CLAUDE.md`의 Debounce/Throttle "열린 질문 4개"를 개수 언급 없이
`question.md`/`debounce-throttle-plan.md` §12로 위임(실제는 6개 —
"개수는 소스 하나만" 원칙 적용).
10. `CLAUDE.md`의 11번째 세션 기록을 ~103줄에서 ~13줄로 압축(전문은
이미 `session/2026-08-14-11-...md`에 보존돼 있어 손실 없음).
11. `effect-plan.md`/`ref-plan.md`/`source-state-plan.md`의 볼드
"동적 경로 가드" 3곳을 전부 실제 `###` 헤딩으로 승격 — 6곳의 절
인용이 전부 자연히 해소됨.
`doc-check.py` ERROR 0 / WARN 93→85 유지 확인(마지막에 새 archive
파일을 README 색인에 추가하지 않아 ERROR 1건 잠깐 발생 — 바로 수정).
## 교훈
- **문서가 "네 라운드 정정 끝에 확정"했다고 자기 서술해도 그게 진짜
맞다는 보장은 아님** — 같은 결론이 그 코퍼스 안의 다른 확정 원칙
(`module-lifecycle-plan.md`의 팩토리 뮤테이션 패턴)과 충돌하는지는
"여러 번 검토했다"는 사실과 별개로 매번 다시 확인해야 함.
- **사용자가 "아닌 거 알지?"로 끼어들면 그 자리에서 멈추고 확인부터**
이미 진행 중이던 수정 계획(findings #5/#6)을 그대로 밀어붙였으면 틀린
전제 위에 새 "정정"을 또 쌓을 뻔했음.
## 6차 — 같은 실수의 다른 잔존 여부 전수 확인
사용자가 "다른 부분도 이런 실수 나온 거 있는지 봐줘"라고 요청 —
"모듈 로드 시점/require 시점에 스스로 등록·실행"류 top-level 부작용
주장을 코퍼스 전체에서 grep. 두 개는 오탐으로 확인(`bind-system-plan.md`
133줄은 그냥 정적 lookup 테이블 채우기라 부작용 없음, `research/
debug-tooling-plan.md`362줄은 React DevTools 비교 서술이라 quad 자신의
설계가 아님). `dispatch-core-plan.md`의 일반 Handler 등록 절(492-516줄,
`NoneHandler`/`StoreBind`/`Leaf`)은 이미 "BaseModule 테이블에 딸린
state"로 정확히 서술돼 있어 문제 없음 확인.
**진짜 갭 발견**: `ROADMAP.md` M10 체크리스트가 여전히 `TagHandler`/
`AttributeKeyHandler`/`AttributeGroupHandler` 자체를
`HANDLER_PRIORITY_FALLBACK`으로 등록한다고 서술 중이었고, 새로 분리된
`TagFallbackHandler`/`AttributeKeyFallbackHandler`/
`AttributeGroupFallbackHandler` 파일 자체가 체크리스트에 아예 없었음
— 이대로면 구현자가 이 세 파일을 만들 필요를 몰랐을 것. M10 배너에
정정 노트 추가, 세 알고리즘 항목에서 "스스로 등록 안 함" 문구로
정정, 새 Fallback 파일 셋을 체크리스트 항목으로 추가.
`base/architecture.md`의 AttributeKey.luau/Attribute.luau 파일 트리
설명에도 같은 Fallback 언급 보강(Tag.luau는 5차에서 이미 반영됨).
`doc-check.py` ERROR 0 유지.
## 7차 — 두 번째 `/code-review high`, 5차 정정의 잔존 흔적 3건 + 별개 버그 1건
사용자가 `/code-review high`를 재실행 — 5차의 Tag/Attribute 정정이
프로즈만 고치고 실제 pseudocode/다른 서술은 놓친 곳들을 정확히 잡아냄:
1. `tag-plan.md:155``TagHandler.priority = HANDLER_PRIORITY_FALLBACK`
pseudocode가 100줄 아래 프로즈 정정과 모순(실제 코드로 복붙될
블록이라 가장 심각). `TagHandler``.priority` 없음으로 수정,
"패키지 배치" 절에 `TagFallbackHandler = { priority = ...,
isHandlable = TagHandler.isHandlable, process = TagHandler.process }`
래퍼 pseudocode 신설.
2. `dispatch-core-plan.md:612` — opt-in 가로채기 예시가 "`TagHandler`
자신(FALLBACK)"이라고 서술, 몇 줄 위 재정정 블록과 모순 — 실제
FALLBACK에 있는 건 `TagFallbackHandler`로 정정.
3. `ref-plan.md:257``RefLeafHandler.isHandlable`이 `and not
isPostRef(v)`를 빠뜨림(PostRef 도입 9차 세션 때 이 자리가 안
갱신됨) — 같은 파일 783줄의 최종 공식과 불일치했던 걸 발견, 이번
세션과 무관한 **별개의 사전 존재 버그**였음(Tag/Attribute 정정과
무관). 정정.
4. `architecture.md:170``Leaf.luau` 파일 트리가 `v=Ref/Observer/
PreRef/PostRef`만 나열하고 `Effect`가 빠져 있었음(이번 세션 1~3차가
만든 `ObserverEffectLeafHandler`와 불일치) — `Effect` 추가 + 결합
핸들러 언급 보강.
전부 직접 재확인 후 반영(finder가 인용한 줄 번호/문맥을 실제로 열어
확인). `doc-check.py` ERROR 0 유지.
**교훈**: 프로즈만 고치고 pseudocode 블록을 안 고치는 게 이 세션에서
반복된 패턴(5차의 근본 원인과 같은 클래스) — "핸들러 계약을 코드
블록으로 정의하는 절"은 그 코드 블록 자체를 grep 대상에 넣어야
한다는 게 이번에 다시 확인됨.

165
CLAUDE.md
View file

@ -174,7 +174,7 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션
되짚음 — "이미 묶여 있는가"(bound 문맥)와 "지금 발화해도 되는가" 되짚음 — "이미 묶여 있는가"(bound 문맥)와 "지금 발화해도 되는가"
(execute 문맥)는 판정 로직은 공유해도 호출부의 질문이 다르다는 사용자 (execute 문맥)는 판정 로직은 공유해도 호출부의 질문이 다르다는 사용자
지적, `base/lifecycle-pattern.md`의 "`canBound` vs `canExecute`" 절). 지적, `base/lifecycle-pattern.md`의 "`canBound` vs `canExecute`" 절).
`question.md`의 "결정 대기" 절 자체가 지금은 비어 있음. `question.md`엔 이제 "결정 대기" 절 자체가 없음(비어서 헤딩째로 삭제).
**M0 착수 전 반드시 읽을 것 — 이 두 개는 "결정"이 아니라 "구현 규약"이라 **M0 착수 전 반드시 읽을 것 — 이 두 개는 "결정"이 아니라 "구현 규약"이라
여전히 유효**: 여전히 유효**:
@ -266,7 +266,8 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션
게이티드 노드를 공용 `Gate`로 빼두는 것만은 그 시점에 해야 함**(따로 게이티드 노드를 공용 `Gate`로 빼두는 것만은 그 시점에 해야 함**(따로
하면 같은 설계를 두 번 함). 주입 op 2개(`setTimeout`/`clearTimeout`)가 하면 같은 설계를 두 번 함). 주입 op 2개(`setTimeout`/`clearTimeout`)가
백엔드 팩토리 표면에 추가될 예정이라는 것도 M1 설계 시 인지. 설계는 백엔드 팩토리 표면에 추가될 예정이라는 것도 M1 설계 시 인지. 설계는
네 라운드로 대부분 확정됐고 남은 열린 질문 4개는 `question.md` 3번. 네 라운드로 대부분 확정됐고 남은 열린 질문은 `question.md` 3번(개수는
거기도 반복 안 함 — 소스는 `research/debounce-throttle-plan.md` 12절).
5. 자율 작업 루프/스케줄 설정 여부는 사용자 결정 대기 중 5. 자율 작업 루프/스케줄 설정 여부는 사용자 결정 대기 중
(`HUMAN_TODO.md` 2번 항목). (`HUMAN_TODO.md` 2번 항목).
@ -1371,102 +1372,72 @@ Modifier 필드 금지 규칙과 혼동해 잘못 답했다가 사용자 지적
**2026-08-14 열한 번째 세션 — 코퍼스 전체 감사(서브에이전트 6개 병렬), **2026-08-14 열한 번째 세션 — 코퍼스 전체 감사(서브에이전트 6개 병렬),
`canBound`/`canExecute` 재분리로 `question.md` 0-W 해소** `canBound`/`canExecute` 재분리로 `question.md` 0-W 해소**
(`session/2026-08-14-11-corpus-audit-canbound-resplit.md`) (`session/2026-08-14-11-corpus-audit-canbound-resplit.md`)
사용자 요청으로 `.claude/` 전체를 `doc-check.py` + 6개 병렬 서브에이전트로 6개 병렬 서브에이전트 감사로 stale 서술 15개 파일 정정, `question.md`
감사 — stale 세션 번호(luau-test 파일들의 "canExecute 재정정"이 "세 번째" 0-W(`Ref` 이중 배치 방지) 확정 — `canBound``canExecute`와 별도
vs "다섯 번째"로 갈라짐, archive 교차확인 결과 다섯 번째가 맞음), 잘못된 진입점으로 재도입(판정 로직은 `isBoundAlive` 하나로 공유). 후속으로
인용(`comparison-charm.md`가 이미 확정된 `:Compute``previous` 인자를 `PreRef`/`PostRef`/`Observer`/`Effect`의 non-number 키 유입을
존재하지 않는 문서 인용으로 "미결"이라 잘못 서술), 자기모순 배너(`tag-plan.md` `HANDLER_PRIORITY_FALLBACK` 동적 경로 가드로 통일, Tag/Attribute
의사코드와 "패키지 배치" 절 우선순위 불일치, `additional-primitives-plan.md` 백엔드 op 미주입 처리 정책을 네 라운드 정정 끝에 확정(**그 최종
"새 질문 없음" 배너가 이후 추가된 질문 2개와 모순) 등 15개 파일의 실제 결론 중 "TagHandler가 quad-base 모듈 로드 시점에 스스로 등록"은
사실 오류를 발견·수정. 감사 후 사용자가 `question.md` 0-W(`Ref` 이중 **[역전, 같은 날 열두 번째 세션]** — 원문·근거는
배치 방지)를 선택지 (a)로 확정 — 메커니즘은 `RefLeafHandler`가 새 `archive/tag-attribute-load-time-registration-reversed.md`). 이어진
`Relate` 없이 `bindLifetime`/`unbindLifetime`을 재사용(이미 내장된 `git diff` 자기 감사와 `/code-review high`가 각각 추가로 3건씩 발견·
이중 바인딩 가드를 그대로 탐). 이 과정에서 그 가드 이름이 `canExecute` 수정(대부분 `canBound` 재도입 때문에 stale해진, 이 세션이 안 건드린
게 개념적으로 안 맞다는 지적 — "이미 묶여 있는가"(bound 문맥, `bindLifetime`/ 파일들 — 자기 감사가 "건드린 파일만" 훑는 사각지대를 재확인).
`Observer:Subscribe()`가 묻는 것)와 "지금 발화해도 되는가"(execute 문맥, `doc-check.py` ERROR 0 유지.
State emit 전파 루프만 묻는 것)는 판정값은 같아도 다른 질문이라, 2026-08-14
다섯 번째 세션에 "canBound 폐기, canExecute로 통합"했던 걸 부분적으로
되짚어 `canBound`를 별도 진입점으로 재도입(판정 로직은 비공개 헬퍼
`isBoundAlive` 하나로 계속 공유 — 코드 중복 없이 호출부 의미만 분리,
다섯 번째 세션이 고친 시그니처/오염 정정 자체는 안 바뀜). `base/
lifecycle-pattern.md`(정본)/`base/ref-plan.md`/`archive/
canexecute-inst-arg-reversed.md`(addendum)/`question.md`→`archive/
question-resolved.md`/`ROADMAP.md`/`base/source-state-plan.md`/
`base/effect-plan.md`/`base/architecture.md`/`base/dispatch-core-plan.md`/
`research/pre-implementation-audit.md`/`luau-test/README.md`/`audit/
gcconn-trick-verification.md`/CLAUDE.md 자신까지 전부 반영,
`doc-check.py` ERROR 0 유지. 새로 연 설계 질문 없음.
**같은 세션 후속**: 손 트레이싱의 `Frame1 { Ref = r }` 표기가 실제 **2026-08-14 열두 번째 세션 — Observer/Effect Leaf에 `Ref`와 같은 dedup 추가(성능)**
API와 다르다는 사용자 지적(`Ref`는 항상 배열 리터럴이라 `k`는 숫자, (`session/2026-08-14-12-observer-effect-leaf-dedup.md`)
`"Ref"`라는 문자열 키가 아님) — `ref-plan.md`/`archive/question-resolved.md` `State<Observer>`/`State<Effect>`가 재-dispatch될 때 안쪽 값이 안 바뀌어도
정정. 이어서 `PreRef`/`PostRef`/`Observer`/`Effect`가 non-number 키로 Dispatch는 값 비교 없이 매번 `retractor`+`process`를 다시 부름 — 처음엔
들어오면 어떻게 할지 논의 — 사용자가 처음엔 "process에서 에러"를 `bindLifetime`/`unbindLifetime`이 저렴한 weak-table 쓰기뿐이라(실제
제안했다가 스스로 "가장 아래(FALLBACK)에 까는 게 맞다"고 정정, 넷 다 Roblox 커넥션은 Instance 생성 시 한 번만 만들어짐) 버그가 아니라고
`HANDLER_PRIORITY_FALLBACK`으로 등록하는 동적 경로 가드로 통일(하드 결론지었으나, 사용자가 "`==` 비교가 매번 도는 해싱 비용보다 항상 싸다"고
블록이 아니라 `Tag`/`Attribute`처럼 나중에 평범한 우선순위 Handler로 지적 — correctness와 무관하게 순수 성능 이유로 `RefLeafHandler`와 같은
override 가능한 자리) — `ref-plan.md`/`source-state-plan.md`/ `old ~= v` dedup을 그대로 채택. `base/dispatch-core-plan.md`(4번 절)
`effect-plan.md`/`ROADMAP.md` 반영. 마지막으로 Tag/Attribute의 미주입 정정 + `base/source-state-plan.md`에 새 절 "Observer/Effect Leaf dedup"
백엔드 스텁 에러가 "미지원"과 "미등록"을 구분 안 하는 게 맞는지 확인 신설(pseudocode 포함).
질문 — 확정 원칙(스텁 입장에선 원천적으로 구별 불가) 재확인. 처음엔
"FALLBACK보다 높은 우선순위 Handler로 덮어씌우기" 관례를 문서화했으나,
사용자가 곧바로 더 단순한 안으로 정정 — **op 등록 자체를 필수로
바꿈**: 백엔드 팩토리는 `addTag`/`removeTag`/`setAttribute`를 항상
명시적으로 채워야 하고(`nop`도 정당한 구현, 미지원이면
`function() error(...) end`), base의 미주입 스텁에 기대는 정상 경로는
없앰 — Dispatch Handler/우선순위 조정 불필요해서 훨씬 가볍고, 부수
효과로 "provider 미주입"(어떤 팩토리도 안 돌았다는 좁은 경우) vs
"미지원"(팩토리가 명시적으로 에러 등록)이 자연히 갈라짐.
`dispatch-core-plan.md`/`module-lifecycle-plan.md` 반영. **바로 다시
정정** — 사용자가 op 필수화만으론 부족함을 지적: `addTag` 에러는
`TagHandler.process` 본문 안, 부기(`tagNameMap` 등)가 이미 일부
mutate된 뒤에야 나서 원자적 실패가 아님(`Tag()`가 반쪽짜리 상태로
남을 수 있음). 결론: op 필수화는 유지하되, 미지원 백엔드는 **추가로**
`HANDLER_PRIORITY_FALLBACK + 1` 우선순위의 순수 가로채기 Handler도
등록해 `TagHandler.process`가 아예 안 불리게(부기 mutation 0회) 하는
걸 권장으로 추가 — "매치된 Handler 하나만 실행"이라는 기존 Dispatch
규칙을 그대로 이용, 새 메커니즘 없음. 같은 두 문서 재반영.
**세 번째 정정(틀림, 곧바로 재정정됨)** — 어시스턴트가 "TagHandler는 **같은 세션 후속 — `/code-review` findings 11건 전부 반영.**
quad-base가 자기 모듈 로드 시점에 스스로 등록 안 하고, 등록 여부는 `isHandlable``k` 타입을 안 봐서 죽어있던 FALLBACK 가드 수정,
백엔드 팩토리의 (A)지원/(B)미지원 선택"이라는 모델을 제시했으나, 이는 `bindLifetime` pseudocode의 `canExecute`→`canBound` 정정,
어시스턴트 자신의 오독이었음(사용자가 실제로 한 말이 아님). **네 번째, "동적 경로 가드"(볼드 텍스트뿐 실제 헤딩 아니었음) 3곳을 `###`으로
최종 정정** — 사용자가 명확히 바로잡음: "HANDLER_PRIORITY_FALLBACK까지 승격, `question.md`/`CLAUDE.md`/`HUMAN_TODO.md`가 공통으로 갖고 있던
등록 안한다 했는데, 개소리 — FALLBACK은 아무 등록 없을 때 처리하려는 "결정 대기가 비어 있다"는 서술을 "그 헤딩 자체가 삭제됐다"로 정정,
걸 방어하는 것까지 포함하는 애임, 기본 등록은 필요함." **최종 모델**: 11번째 세션 기록 ~103줄→~13줄 압축(전문은
`TagHandler`/`AttributeKeyHandler`/`AttributeGroupHandler`는 quad-base가 `session/2026-08-14-11-corpus-audit-canbound-resplit.md`에 보존).
모듈 로드 시점에 `HANDLER_PRIORITY_FALLBACK`으로 **스스로 등록**(모든 **가장 큰 건**: 고치던 중 사용자가 "Tag/Attribute를 base가 스스로
백엔드가 자동으로 부기를 얻음 — FALLBACK 밴드의 존재 이유 자체가 이 등록한다는 게 사실이 아님"을 지적 — 열한 번째 세션이 네 라운드
"기본 안전 동작"을 공짜로 제공하는 것). `addTag`/`removeTag`/ 정정 끝에 확정했던 그 결론 자체가 틀렸음이 드러남(`base/
`setAttribute`만 백엔드 팩토리가 채우는 타입 계약이고, 안 채운 슬롯의 lifecycle-pattern.md`가 이미 거부해둔 `InitNamespace`류 top-level
base 기본값은 "그럴듯한 기본 동작을 추측"하지 않고 **명시적으로 부작용 패턴과 같은 클래스). 정정: `TagHandler`류는 참조 카운트
에러내는 스텁**(조용한 no-op은 provider 초기화를 잊은 실수를 가려버려 **알고리즘 구현**일 뿐 스스로 등록 안 됨 — `HANDLER_PRIORITY_FALLBACK`
기각). 더 명확한 메시지·진짜 원자적 실패를 원하는 백엔드만 **opt-in으로** 별도 이름의 `TagFallbackHandler`류가 꽂히고, 등록 주체는 quad-base
`HANDLER_PRIORITY_FALLBACK + 1`짜리 가로채기 Handler를 추가로 등록 — 모듈이 아니라 **백엔드 팩토리**(`BaseModule` 뮤테이션 시점, 자기 전용
필수가 아니라 선택적 업그레이드. `dispatch-core-plan.md`(정본, 다시 Handler들과 같이 — `module-lifecycle-plan.md`가 이미 확정해둔 패턴
전면 재작성)/`module-lifecycle-plan.md`/`tag-plan.md`/`attribute-plan.md`/ 그대로, 새 예외 아님). `dispatch-core-plan.md`/`tag-plan.md`/
`architecture.md` 재반영. **교훈**: 네 라운드 연속 정정이 있었던 이유는 `attribute-plan.md`/`architecture.md`/`module-lifecycle-plan.md` 전부
"누가 실제로 Dispatch.addHandler를 부르는가"라는 아키텍처 전제를 재반영, 뒤집힌 원문은 `archive/
확신 없이 매번 새로 단정하고 그 위에 patch를 쌓았기 때문 — 불확실한 tag-attribute-load-time-registration-reversed.md`. `doc-check.py`
전제는 재확인 없이 단정하지 말 것. ERROR 0 유지.
**같은 세션, 사용자 요청으로 전체 자기 감사**: 이 세션이 건드린 25개 **같은 세션 후속 — 같은 실수의 다른 잔존 여부 전수 확인.** "모듈
파일을 `git diff`로 처음부터 재정독 — 실제 문제 3건 발견·수정 로드 시점에 스스로 등록"류 주장을 코퍼스 전체 grep — 두 매치는
(`HUMAN_TODO.md`가 0-W 해소 이후에도 "남은 건 0-W"로 방치돼 있던 것, 오탐(정적 lookup 테이블, React DevTools 비교 서술), 일반 Handler
`ref-plan.md`의 편집 중 생긴 어색한 줄바꿈, Tag/Attribute 등록 모델 등록 절(`dispatch-core-plan.md` 492~516줄)은 이미 정확했음. 진짜
재정리 중 `tag-plan.md`/`attribute-plan.md`에서 실수로 빠뜨린 "백엔드가 갭은 `ROADMAP.md` M10 체크리스트 — 새로 분리된
알고리즘 전체를 교체하고 싶으면 그냥 더 높은 우선순위로 등록" 문장 `TagFallbackHandler`/`AttributeKeyFallbackHandler`/
복원). 나머지는 문제 없음 확인, `doc-check.py` ERROR 0 유지. `AttributeGroupFallbackHandler` 파일 자체가 체크리스트에 없어서
구현자가 만들 필요를 몰랐을 상태였음, 세 항목 추가 + 배너 정정 +
`architecture.md` 파일 트리 설명 보강.
**같은 세션, `/code-review high` 백그라운드 실행이 추가로 3건 발견**: **같은 세션 후속 — 두 번째 `/code-review high`가 3건 더 발견.**
`git diff` 기반 자기 감사가 "이 세션이 건드린 파일"만 훑어서, "이 `tag-plan.md:155``TagHandler.priority = HANDLER_PRIORITY_FALLBACK`
세션이 안 건드렸지만 `canBound` 재도입 때문에 stale해진 파일"을 pseudocode가 프로즈 정정과 모순됐던 것(가장 심각 — 실제 코드로
놓쳤음 — `audit/gcconn-trick-verification.md` 상단 배너(고친 섹션 복붙될 블록), `dispatch-core-plan.md:612`의 opt-in 예시가 여전히
바로 위인데 안 건드림, "canBound 폐기" 서술이 몇 줄 아래 새 서술과 "`TagHandler` 자신(FALLBACK)"이라 서술하던 것 — 둘 다 정정 +
모순), `.claude/README.md` 인덱스 행 4곳(이 세션 동안 한 번도 안 열어본 `TagFallbackHandler` 래퍼 pseudocode 신설. 별개로 `ref-plan.md:257`
파일이라 5차 세션 서술 그대로 잔존), `luau-test/STATUS.md`의 스파이크 `RefLeafHandler.isHandlable``and not isPostRef(v)`를 빠뜨린
`10` 재작성 가이드(가장 심각 — "이중 바인딩 게이트는 canExecute로 쓸 **사전 존재 버그**(PostRef 도입 9차 세션 때 안 갱신됨, 이번 세션과
것"이라고 옛 모델을 재작성 지침으로 명시 중이었음), `question.md` 무관)도 같이 잡혀 정정, `architecture.md``Leaf.luau` 파일 트리에
"용어 정리" 절. 전부 정정. **교훈**: 이름 하나가 재도입되면 그 이름을 빠져있던 `Effect` 타입도 보강. `doc-check.py` ERROR 0 유지.
문자열로 전체 코퍼스에 grep해야지, "내가 고친 파일들"만 재확인하는
건 안 충분함.

View file

@ -149,7 +149,8 @@ git branch -D worktree-debounce-throttle-plan
디자인 결정 중 Lua/Roblox 엔진에 대한 깊은 경험이 필요한 것들은 합리적 기본값으로 디자인 결정 중 Lua/Roblox 엔진에 대한 깊은 경험이 필요한 것들은 합리적 기본값으로
진행하면서 `.claude/question.md`에 모아두는 중. 깨어있을 때 훑어보고 기본값이 진행하면서 `.claude/question.md`에 모아두는 중. 깨어있을 때 훑어보고 기본값이
마음에 안 드는 것만 답해주면 됨 — **[2026-08-14 열한 번째 세션 기준] 마음에 안 드는 것만 답해주면 됨 — **[2026-08-14 열한 번째 세션 기준]
`question.md`의 "결정 대기" 절 자체가 비어 있음**(마지막 남았던 0-W `question.md`엔 이제 "결정 대기" 절 자체가 없음**(비어서 헤딩째로 삭제 —
마지막 남았던 0-W
`Ref` 이중 배치도 이 세션에 해소 — `base/ref-plan.md` "이중 배치 방지" `Ref` 이중 배치도 이 세션에 해소 — `base/ref-plan.md` "이중 배치 방지"
절, `archive/question-resolved.md`로 이전됨). 절, `archive/question-resolved.md`로 이전됨).

View file

@ -694,6 +694,17 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
> 재배치**됐고(엔진 op `addTag`/`removeTag`/`setAttribute`만 주입), > 재배치**됐고(엔진 op `addTag`/`removeTag`/`setAttribute`만 주입),
> 이름 소유권은 그룹 전용 키 + `AttributeKeyHandler`의 이름 claim이 > 이름 소유권은 그룹 전용 키 + `AttributeKeyHandler`의 이름 claim이
> 판정함. 정본은 `base/attribute-plan.md`/`base/tag-plan.md`. > 판정함. 정본은 `base/attribute-plan.md`/`base/tag-plan.md`.
>
> **[2026-08-14 열두 번째 세션 정정]** `TagHandler`/`AttributeKeyHandler`/
> `AttributeGroupHandler`는 참조 카운트/이름 claim **알고리즘 구현**일
> 뿐 — `HANDLER_PRIORITY_FALLBACK`에 실제로 등록되는 건 이를 감싸는
> 별도 파일 `TagFallbackHandler`/`AttributeKeyFallbackHandler`/
> `AttributeGroupFallbackHandler`이고, 등록 주체는 quad-base 모듈
> 자체가 아니라 **백엔드 팩토리**(`RobloxFactory`가 `BaseModule`
> 뮤테이션 시점에 자기 전용 Handler들과 같이 등록). 아래 체크리스트의
> `Handler` 파일 항목은 전부 이 구분을 반영하도록 갱신됨 — 뒤집힌
> 옛 모델은
> `archive/tag-attribute-load-time-registration-reversed.md`.
- [ ] `Handlers/Event.luau`(`ReflectionService` 기반 자동 판별) - [ ] `Handlers/Event.luau`(`ReflectionService` 기반 자동 판별)
@ -718,8 +729,16 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
`setAttribute(inst,name,v)``v`가 뭐든 무조건 호출 + **이름 `setAttribute(inst,name,v)``v`가 뭐든 무조건 호출 + **이름
claim**(`nameClaims` Relate, 다른 키 객체가 같은 이름에 들어오면 claim**(`nameClaims` Relate, 다른 키 객체가 같은 이름에 들어오면
즉시 error, 반환 클로저는 자기 claim만 반납하고 엔진 부작용 없음). 즉시 error, 반환 클로저는 자기 claim만 반납하고 엔진 부작용 없음).
`HANDLER_PRIORITY_FALLBACK`으로 등록. `question.md` 0-Z 결정 — 알고리즘 구현일 뿐 스스로 등록되진 않음(아래
`AttributeKeyFallbackHandler` 항목 참고). `question.md` 0-Z 결정 —
`base/attribute-plan.md` "이름 소유권" 절) `base/attribute-plan.md` "이름 소유권" 절)
- [ ] **[2026-08-14 열두 번째 세션 신설]** `quad-base/Dispatch/
AttributeKeyFallback.luau`(`AttributeKeyFallbackHandler` — 위
`AttributeKeyHandler`를 그대로 감싸 `HANDLER_PRIORITY_FALLBACK`으로
등록되는 별도 이름의 엔티티. 등록 주체는 `RobloxFactory`
`BaseModule` 뮤테이션 시점에 자기 전용 Handler들과 같이 —
`base/dispatch-core-plan.md` "base가 소유하는 핸들러와 주입되는
엔진 op" 절)
- [ ] `Attribute.luau`(quad-base — 그룹 값 타입+API: `Attribute(store1, - [ ] `Attribute.luau`(quad-base — 그룹 값 타입+API: `Attribute(store1,
store2, ...)`/`Merged`/`:NameMap`, `Tag`와 동형 array-part 값 객체, store2, ...)`/`Merged`/`:NameMap`, `Tag`와 동형 array-part 값 객체,
`base/attribute-plan.md`) `base/attribute-plan.md`)
@ -729,7 +748,14 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
등록한 키 전부에 `Dispatch.retractFrom(inst,key,1)`. 등록한 키 전부에 `Dispatch.retractFrom(inst,key,1)`.
**`process` 안에서 `retractFrom`을 먼저 부르면 안 됨**(철거는 전적으로 **`process` 안에서 `retractFrom`을 먼저 부르면 안 됨**(철거는 전적으로
클로저 몫). 실제 `setAttribute`/store-bind 구독/이름 claim은 전부 단일 클로저 몫). 실제 `setAttribute`/store-bind 구독/이름 claim은 전부 단일
키 경로 재사용 — `base/attribute-plan.md` "메커니즘" 절) 키 경로 재사용 — `base/attribute-plan.md` "메커니즘" 절. 알고리즘
구현일 뿐 스스로 등록되진 않음, 아래 `AttributeGroupFallbackHandler`
항목 참고)
- [ ] **[2026-08-14 열두 번째 세션 신설]** `quad-base/Dispatch/
AttributeGroupFallback.luau`(`AttributeGroupFallbackHandler` — 위
`AttributeGroupHandler`를 그대로 감싸 `HANDLER_PRIORITY_FALLBACK`으로
등록되는 별도 이름의 엔티티, 등록 주체는 `AttributeKeyFallbackHandler`
동일하게 `RobloxFactory`)
- [ ] `Tag.luau`(quad-base — 값 타입+immutable clone 체이닝: `Tag(...)`/ - [ ] `Tag.luau`(quad-base — 값 타입+immutable clone 체이닝: `Tag(...)`/
`:Added`/`:Removed`/`:Contains`/`:Apply`/`Merged`/`:Names`, `:Added`/`:Removed`/`:Contains`/`:Apply`/`Merged`/`:Names`,
`base/tag-plan.md` — 2026-08-08 세 번째 세션 array-part 값 객체로 `base/tag-plan.md` — 2026-08-08 세 번째 세션 array-part 값 객체로
@ -740,9 +766,13 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
때만 실제 `removeTag`, 그마저도 클로저가 받은 새 값이 그 이름을 때만 실제 `removeTag`, 그마저도 클로저가 받은 새 값이 그 이름을
`Contains`하면 skip해 깜빡임 방지. 제거할 이름은 모아서 **한 번에** `Contains`하면 skip해 깜빡임 방지. 제거할 이름은 모아서 **한 번에**
`removeTag(inst, names)`. `process` 쪽 별도 diff 없음, `kTagMap` `removeTag(inst, names)`. `process` 쪽 별도 diff 없음, `kTagMap`
불필요(클로저가 `v`를 직접 캡처). `HANDLER_PRIORITY_FALLBACK`으로 불필요(클로저가 `v`를 직접 캡처) — 2026-08-12 열한 번째 /
등록 — 2026-08-12 열한 번째 / 2026-08-13 네·다섯·열네 번째 세션, 2026-08-13 네·다섯·열네 번째 세션, `base/tag-plan.md`. 알고리즘
`base/tag-plan.md`) 구현일 뿐 스스로 등록되진 않음, 아래 `TagFallbackHandler` 항목 참고)
- [ ] **[2026-08-14 열두 번째 세션 신설]** `quad-base/Dispatch/
TagFallback.luau`(`TagFallbackHandler` — 위 `TagHandler`를 그대로
감싸 `HANDLER_PRIORITY_FALLBACK`으로 등록되는 별도 이름의 엔티티,
등록 주체는 `AttributeKeyFallbackHandler`와 동일하게 `RobloxFactory`)
- [ ] **[2026-08-14 세션에 누락 발견, 신규]** `quad-roblox/Handlers/ - [ ] **[2026-08-14 세션에 누락 발견, 신규]** `quad-roblox/Handlers/
InstanceShorthand.luau` — UI 편의 숏핸드 `UICorner`/`UIPadding` InstanceShorthand.luau` — UI 편의 숏핸드 `UICorner`/`UIPadding`
(+`UIPaddingOffset`)/`UIScale`(`base/ui-shorthand-plan.md`). 이 (+`UIPaddingOffset`)/`UIScale`(`base/ui-shorthand-plan.md`). 이