diff --git a/.claude/README.md b/.claude/README.md index e44309b..f3f2d48 100644 --- a/.claude/README.md +++ b/.claude/README.md @@ -33,8 +33,8 @@ | `typing-limits.md` | **[2026-08-13 열세 번째 세션 신설]** Luau 타입 시스템이 quad 설계에 대해 **못 해주는 것**을 한 군데 모은 확정 문서 — 여러 `base/` 문서에 캐비엇으로 흩어져 있던 걸 통합. 대전제는 "**Luau의 한계를 우회하려고 타입/API를 비틀지 않는다**"(비틀면 나중에 Luau가 고쳐줘도 자동 수혜를 못 받고 되돌리는 마이그레이션이 생김). 1번 항목이 가장 큼 — **재귀 제네릭이 다른 타입 인자로 자기를 반환하면(`Compute(self: State,...) -> State`) 타입 안전성이 에러 없이 조용히 사라짐**(구 `question.md` 0-Y, 스파이크 44개로 확정). 대응은 두 개: (a) 타입 선언을 "데이터부/메소드부"로 쪼개 콜백 파라미터 추론을 살리고, (b) **파생 State를 만드는 자리마다 결과 타입을 명시 주석으로 바인딩**(그 한 줄만 검증 안 되고 다운스트림 전체는 정상 체크됨). Luau RFC `relax-recursive-type-restriction`이 `Promise.andThen`으로 예시 든 바로 그 패턴이라 **지금 선언 그대로 두면 Luau 쪽 수정만으로 코드 변경 없이 풀림**(추적: `luau-lang/luau#2380`). 그 외 Modifier `Overridden` 서브타입/Attribute 제네릭 키 narrowing/nilable default 오버로드/`store.key` type function 한계도 여기 통합, 7번에 **새 타입·API 설계 시 체크리스트**. 실측 근거는 `audit/type-recursion-issue/` | | `lifecycle-pattern.md` | rbvm의 `Connected`+GC 관용구를 quad-v2가 채택하는 방식. **[2026-08-14 다섯 번째 세션, 시그니처 정정]** `bindLifetime(inst,value)`/`unbindLifetime(value)`/`canExecute(value)` — 뒤의 둘은 `inst`를 안 받음(`bindLifetime`이 바인딩 시점에 gcconn 참조를 `value` 쪽 `Relate`로 복사해두므로 `value` 하나로 생존을 물을 수 있고, 실제 호출부인 State 전파 루프엔 애초에 `inst`가 없음). `.Subscribed`는 전역 `:Subscribe()` 전용 필드로 분리(`bindLifetime`은 읽지도 쓰지도 않음), gcconn/gchold는 lazy가 아니라 **Instance 생성 시점**에 만들고 클로저가 `gchold`와 `inst`를 둘 다 캡처(userdata 포인터 동일성 = `inst`-키 `Relate` 전체의 전제). 옛 2-인자 모델은 `archive/canexecute-inst-arg-reversed.md`. **[2026-08-14 열한 번째 세션]** 별도 `canBound`가 다시 도입됨 — `bindLifetime`/`Observer:Subscribe()`의 이중 바인딩 가드는 `canBound`, State emit 전파 게이팅만 `canExecute`(판정 로직은 비공개 헬퍼 `isBoundAlive` 하나를 공유) | | `store-plan.md` | **[2026-08-14 신설 — `bind-system-plan.md` 3단계 분할 + 구 store-semantics.md 흡수]** Store = **이름 붙은 Source 모음, 그 이상 아님** — Store 부작용 허용이 기본 디자인(국소적 vs 경계를 넘는 부작용), `defaults`는 선택적 초기값 템플릿(원본을 나중에 mutate해도 UB 아님)이고 **eager 생성과 lazy 생성이 둘 다 필요**(Luau 타입은 런타임에 강제 안 되므로), `table.clone` 기반 eager 생성 스케치, `store.key`(dot-access)가 1급 경로이고 `store "key"` 문자열 커링은 동적 키용 미타입 폴백, 레코드 필드 타이핑은 Luau `type function`으로 해결 확인, `store.key = value` 폐기 → `store.key:Set(value)`(타입 대칭성+lazy 정직성), "Store가 Store를 저장 가능한가"는 **그런 경우를 안 만듦**으로 확정(`State>`와는 다른 축) | -| `source-state-plan.md` | **[2026-08-14 신설 — `bind-system-plan.md` 3단계 분할 + 구 store-semantics.md 흡수]** 반응형 코어: `Source`⊇`State` 구조적 서브타입(`RefSource` 폐기, 단방향 의존으로 Luau 솔버 회피 — 스파이크 `08` 통과), **push-invalidate/pull-recompute** 전파 모델과 "관측해야 실체화된다" 전역 원칙, State 체인 플래튼 기각(캐싱이 State의 존재 이유), `:With`도 매번 새 노드(clone 계열인 `Tag`/`Modifier`와 혼동 주의), `:Compute`의 lazy 핸들 계약(`:Get()` 누락이 반복되는 실수)·trailing args sugar·`fn(self, previous?, ...deps)` 순서·`previous`, `:Apply`, `:Emit()`(Source 원천 전용 하드 경계)과 `Store`/`Source`의 `T`가 Modifier일 수 없는 따름정리, `state:Observer(fn)`, `:Subscribe()`/`:Unsubscribe()`, **이중 바인딩 금지 게이트**(`canExecute` 하나로 통합), PA님 코드 교차검증 | -| `dispatch-core-plan.md` | **[2026-08-13 열네 번째 세션 신설 — `bind-system-plan.md` 2단계 분할 + 0-A/0-Z 반영]** 디스패치 코어: 핸들러 계약(`isHandlable`/`priority`/`process`가 retract 클로저를 반환) / **하강 diff 재디스패치**(래핑 핸들러의 `retractFrom` 선행 호출 폐기, `Dispatch.process`가 슬롯의 `handler`를 먼저 비교해 — 같으면 그 자리 클로저에 새 값을 넘기고 재`process`, 다르면 그 자리부터 전량 철거) / `chains` 인덱스 체인과 **3-인자** `Dispatch.retractFrom(inst,k,index)`(힌트 인자 소멸 — 값 전달 경로가 (A) 분기 하나로 통일) / `None` 센티널 / Handler 작성 체크리스트 8개 / Length·Offset 형제 순서 보장 / "store 바인드는 래핑" 결론. **새 결정 둘**: `HANDLER_PRIORITY_FALLBACK`(base 제공 핸들러의 기본 밴드 — 백엔드가 평범한 우선순위로 덮어쓰면 언제나 이김), **"base가 소유하는 핸들러와 주입되는 엔진 op"**(부기가 엔진 지식을 요구하지 않으면 알고리즘은 base, 마지막 한 줄만 주입 — `addTag`/`removeTag`/`setAttribute`, **[2026-08-14 열 번째 세션]** 같은 패턴을 Dispatch 밖의 `dispose(value)`/`disposeInst`에도 재사용). 옛 힌트 모델은 `archive/dispatch-hintvalue-model-reversed.md` | +| `source-state-plan.md` | **[2026-08-14 신설 — `bind-system-plan.md` 3단계 분할 + 구 store-semantics.md 흡수]** 반응형 코어: `Source`⊇`State` 구조적 서브타입(`RefSource` 폐기, 단방향 의존으로 Luau 솔버 회피 — 스파이크 `08` 통과), **push-invalidate/pull-recompute** 전파 모델과 "관측해야 실체화된다" 전역 원칙, State 체인 플래튼 기각(캐싱이 State의 존재 이유), `:With`도 매번 새 노드(clone 계열인 `Tag`/`Modifier`와 혼동 주의), `:Compute`의 lazy 핸들 계약(`:Get()` 누락이 반복되는 실수)·trailing args sugar·`fn(self, previous?, ...deps)` 순서·`previous`, `:Apply`, `:Emit()`(Source 원천 전용 하드 경계)과 `Store`/`Source`의 `T`가 Modifier일 수 없는 따름정리, `state:Observer(fn)`, `:Subscribe()`/`:Unsubscribe()`, **이중 바인딩 금지 게이트**(`canBound`, State emit 전파 게이팅은 `canExecute` — `base/lifecycle-pattern.md`의 "`canBound` vs `canExecute`" 절이 소스), PA님 코드 교차검증. **[2026-08-14 열두 번째 세션]** 새 절 "Observer/Effect Leaf dedup" — `RefLeafHandler`와 같은 `old ~= v` dedup(성능 최적화, correctness엔 불필요) | +| `dispatch-core-plan.md` | **[2026-08-13 열네 번째 세션 신설 — `bind-system-plan.md` 2단계 분할 + 0-A/0-Z 반영]** 디스패치 코어: 핸들러 계약(`isHandlable`/`priority`/`process`가 retract 클로저를 반환) / **하강 diff 재디스패치**(래핑 핸들러의 `retractFrom` 선행 호출 폐기, `Dispatch.process`가 슬롯의 `handler`를 먼저 비교해 — 같으면 그 자리 클로저에 새 값을 넘기고 재`process`, 다르면 그 자리부터 전량 철거) / `chains` 인덱스 체인과 **3-인자** `Dispatch.retractFrom(inst,k,index)`(힌트 인자 소멸 — 값 전달 경로가 (A) 분기 하나로 통일) / `None` 센티널 / Handler 작성 체크리스트 8개 / Length·Offset 형제 순서 보장 / "store 바인드는 래핑" 결론. **새 결정 둘**: `HANDLER_PRIORITY_FALLBACK`(base 제공 핸들러의 기본 밴드 — 백엔드가 평범한 우선순위로 덮어쓰면 언제나 이김), **"base가 소유하는 핸들러와 주입되는 엔진 op"**(부기가 엔진 지식을 요구하지 않으면 알고리즘은 base, 마지막 한 줄만 주입 — `addTag`/`removeTag`/`setAttribute`, **[2026-08-14 열 번째 세션]** 같은 패턴을 Dispatch 밖의 `dispose(value)`/`disposeInst`에도 재사용). 옛 힌트 모델은 `archive/dispatch-hintvalue-model-reversed.md`. **[2026-08-14 열두 번째 세션]** Observer/Effect Leaf도 `Ref`와 같은 identical-value dedup 채택(성능 최적화) | | `bind-system-plan.md` | **[2026-08-14, 3단계 분할로 203줄까지 축소 — 지금은 "인스턴스 생성/이벤트 네이밍 인체공학 + 분할 색인" 문서]** 반응형 코어는 `source-state-plan.md`, Store는 `store-plan.md`, 디스패치 코어는 `dispatch-core-plan.md`로 나갔음. 아래 이력은 분할 전 이 파일이 담고 있던 결정들의 기록(현행 소스는 각 분할 문서). pluggable key/value 핸들러 레지스트리 — `process`/`retract` 디스패치 모델, Ref, Store/State/Source 온톨로지 + 인체공학 질문 전부 확정. 디스패치 엔진은 `quad-base`가 인터페이스로 소유(2026-08-04 5차 라운드). **[2026-08-11 세션, 여섯 번째]** `Dispatch.setLength`/`setOffsetSource`의 owner 키가 물리 Instance로 한정될 필요 없음을 명시(Slot-in-Slot 재귀의 근거) — 같은 절 `recompute`의 off-by-one 버그 발견·수정(`offset`이 자기 자신을 포함해 누적되던 것), 재진입 방지 가드는 검토 후 기각(`Source⊇State` 단방향 원칙과 같은 카테고리의 UB로 명명, 각 Slot이 독립 `bk`를 가져 nesting만으로는 재진입 경로 자체가 없음을 확인). **[2026-08-12 열한 번째 세션, 전면 정정]** "핸들러 타입이 안 바뀌면 retract 없이 process가 diff"는 틀렸음 — `retract`는 store 재발행마다(핸들러 타입 무관) 항상 불림, `v`는 대체 값 자체일 수 있어 `nil`로 가정 금지. `Tag`/`Ref`/`Slot`/`Attribute` 전부 이 오류로 설계돼 있었음이 드러나 한 세션에 전부 정정(`archive/retract-always-fires-reversed.md`). **[2026-08-12 세션 후속]** `retractUnder`의 `A and B or C` 삼항 관용구 버그(`v`가 `false`일 때 `nil`로 새던 것)를 `if-then-else`로 수정한 게 계기가 되어 `and`/`or` 삼항 전면 금지 규칙으로 발전(`architecture.md` "코드 스타일" 절). **[2026-08-12 열일곱 번째 세션]** 우선순위 동률/매치 실패 처리(`HANDLER_PRIORITY_*` 상수+디버그 동률 감지, 매치실패는 즉시 error) 확정, `store.key` 레코드 필드 타이핑이 Luau `type function`으로 가능함을 스케치로 확인(`pre-implementation-audit.md` 1-3/1-4/1-10 해소). **[2026-08-12 스무 번째 세션]** Ref 사용 관례 명문화 — React `useRef`급 스코프 감각(만든 컴포넌트 자신이 쓰거나 자식에게 넘기는 용도, 경계 밖 반출·전역 장기 보관은 비권장). **[2026-08-12 스물한 번째 세션]** `:With`가 `Tag`/`Modifier`의 `:` clone 체이닝과 겉보기엔 같은 문법이지만 실제로는 정반대(clone 아니라 매번 새 State 노드)라는 혼동 경고 추가, `Compute`가 `-ed`(`Computed`)가 아닌 이유 절 신설(quad 자기 관례상 `Tag.Added`/`Modifier.Overridden`이 이미 "-ed = clone 후 즉시 확정된 값"을 선점해 lazy한 State에 재사용하면 충돌). **[2026-08-13 세션, 두 번째]** `State>`(store가 emit하는 값 자체가 또 State/Source)가 같은 `(inst,k)`에 같은 핸들러를 중복 push시켜 `retractUnder`의 첫-매치 cutoff가 안쪽 자신을 잘못 retract하는 실제 체인 파손 버그로 확인됨(손 트레이싱, `luau-test/04`가 no-op `retract` 스텁 때문에 이 증상을 못 잡던 사각지대였음도 같이 발견) — `Dispatch.process`에 중복 핸들러 즉시 error 가드 추가, "동일한 재귀적 디스패치로 처리 가능"이라던 낙관적 서술과 "Store가 Store를 저장 가능한가" 절도 정정. **[2026-08-13 세션, 네 번째]** 사각지대 손 트레이싱 라운드에서 `isHandlable` 필드를 선택적으로 허용(생략하면 스캔에 안 걸림)하고, 그런 "체크포인트" 핸들러를 명시적으로 체인에 꽂는 `Dispatch.processAs`/`Dispatch.retractSelfAndUnder`(target 자신 포함 철거) 신설 — `attribute-plan.md`의 그룹/직접쓰기 이름 소유권 충돌을 별도 레지스트리 없이 기존 재진입 가드로 흡수하는 데 씀. **[2026-08-13 세션, 다섯 번째, 전면 재설계 — 위 processAs/retractSelfAndUnder 대체]** `chains`를 핸들러 객체 identity가 아니라 **재귀 깊이 인덱스**로 추적하도록 재설계 — `Dispatch.process(inst,k,v,index)`가 핸들러 호출 *전에* 그 인덱스 점유 여부를 체크(핸들러 부작용 낭비 없음), `process`는 이제 `retract` 필드 대신 자기 retract 클로저(`(hintValue)->()`)를 반환. 같은 키 재귀는 `index+1`, 다른 키 위임은 항상 `1`부터 — 이걸로 `State>`가 UB에서 정상 지원 대상으로 재정정됨(각 재귀 단계가 다른 슬롯을 쓰니 identity 충돌 자체가 없어짐), `retractUnder`/`retractSelfAndUnder`도 `Dispatch.retractFrom(inst,k,index,v)` 하나로 통합(자기 포함/미만은 호출자가 넘기는 인덱스로 표현)되며 체크포인트 패턴 자체가 불필요해짐(`archive/checkpoint-handler-pattern-reversed.md`). 계기: `AttributeGroupHandler` 소유권 버그를 체크포인트로 고치다, 그 근본 원인(identity 기반 추적)을 되짚은 사용자 지적. **[2026-08-13 감사]** 위 재설계 의사코드에서 실제 버그 셋 발견·수정 — (1) `chains:SetStrong`이 `handler.process` *뒤*에 있어 최초 마운트에서 하위 위임 retractor가 통째로 유실되던 것(재귀가 자기 테이블을 만들었다 바깥이 덮어씀), (2) `Ref` retractor가 spurious 재발행에서도 `relate`를 지워 dedup이 무력화되던 것, (3) `Dispatch.drive`의 진입 인덱스(`1`) 미명시. 덧붙여 retractor 안에서는 *같은* 키에 대한 `retractFrom`도 `process`와 똑같이 금지(진행 중인 루프가 `#list`를 이미 캡처)임을 명문화 **[2026-08-13 열네 번째 세션] 2단계 분할 + 모델 교체 — 디스패치 코어 전체가 `dispatch-core-plan.md`로 나갔고(이 문서엔 반응형 코어와 인체공학만 남음), 나가면서 **하강 diff**로 재작성됨. 따라서 위 5차 세션 서술 중 "`Dispatch.process`가 인덱스 **점유 여부**를 먼저 체크"와 "`retractFrom(inst,k,index,v)` **4-인자**"는 **더 이상 현행이 아님**(점유 체크 폐지 → 핸들러 비교, 힌트 인자 소멸 → 3-인자) — 현행은 `dispatch-core-plan.md` | | `module-lifecycle-plan.md` | 프로바이더 패턴, bind/store 구현 책임 분리 — 확정 | | `slot-plan.md` | 뮤터블 자식 배열, 엄격한 단일 마운트 소유권, 재마운트 시 throw, base/roblox 패키지 경계까지 확정. **[2026-08-09 세 번째 세션]** `Add`/`Remove`/`Extract`/`Clear`/`Move`/`Swap` CRUD(복잡도 표기 포함), `isMounted` 이중 추적 분리, 요소 타입 제약(`nil`/`None`/핸들러 계층 값 금지, `Slot()` 제네릭), 키 기반 동적 컬렉션 재조정(`Slot:List(data, updateFn, keyFn?)`)까지 전부 확정 통합, base/roblox 경계에 reposition 훅 추가. **[2026-08-09 열한 번째 세션, 중간검토]** CRUD 식별 기준을 element 레퍼런스에서 인덱스 기준으로 재정정(`Remove(index)`/`Extract(index, newElement?)`/`Move(oldIndex, newIndex)`), `ExtractAll`/`Get`/`IndexOf` 신설. **[2026-08-11 세션]** `updateFn(item, index: number, offset: Source, prev: T?, userdata: UD?): (T|nil, UD?)`로 시그니처 확정(`Slot.Offset`도 `Length`처럼 공개 필드로 신설) — `LayoutOrder` 등은 Slot이 자동으로 안 세팅, `index`/`offset` raw 값만 전달하고 실제 반영은 `updateFn`이 "버림/다시 그림/source만 갱신" 세 갈래로 직접 처리(재사용 Source에 미리 `Set` 후 결국 다시 그리면 무의미한 연산이 되므로). **[2026-08-11 세션, 여섯 번째]** `Slot:Single(state, updateFn)` 확정(`:List` 위의 순수 sugar) — Slot-in-Slot 중첩도 확정, 요소 타입 제약에서 `Slot` 배제 해제(`T = Instance | Slot`), `Dispatch.setLength`/`setOffsetSource`를 Slot 자신을 owner 키로 재사용하는 재귀 `attachSlot`(새 프리미티브 없음), 파괴는 재귀 `Clear()` 대신 flat `destroySlotTree`+명시적 `unbindLifetime`. `Slot(initial?: {T})` 생성자 부활(순수 `:Add` sugar) + `_crudUsed`↔`_listed` 상호 배타 가드 신설. `base/dispatch-core-plan.md`의 `recompute` off-by-one 버그도 이 세션에 같이 수정됨. **[2026-08-11 세션, 일곱 번째]** 반응형 raw 요소(`Slot:Add`가 `State`/`Source`도 받음) 확정 — 새 메커니즘 아니라 `isState(element)`면 내부적으로 `Slot():Single(element)`(nested Slot)를 대신 삽입하는 순수 sugar(최초 검토했던 별도 position-keyed StoreBind 구독 안은 `None`/Length/Move-Swap 문제로 기각). `Slot:Single(state, updateFn?)`도 `updateFn` 선택 인자화(기본값 identity)로 이 sugar를 지지. `:List`의 `reconcile`도 nested-Slot을 반환하는 아이템의 `.Length`만큼 다음 형제 `index`가 건너뛰도록 `pos` 커밋 공식 수정. **[2026-08-12 열두 번째 세션]** 소유권 판정을 위치별 relate 비교에서, Slot 자신이 지금 어느 `inst`에 바인딩됐는지 직접 추적하는 `slotOwner`(slot→inst)로 전환(같은 Slot이 동시에 다른 위치에 마운트되는 경우까지 잡기 위함) — `owner==inst`면 emit 전파로 무시, 다른 inst면 즉시 error. **[2026-08-12 열세 번째 세션]** `slotOwner`/`kSlotMap`이 서로를 강하게 참조하는 두-`Relate` 상호 GC 순환 발견·수정 — 둘 다 `SetWeak`로 낮추고 실제 GC 앵커는 `bindLifetime`/`unbindLifetime` 하나로 통일(`attachSlot`에 `bindLifetime(physicalTarget, slot)` 추가, `destroySlotTree`에 짝인 `unbindLifetime` 추가). **[2026-08-12 열네 번째 세션]** 위 순환이 Luau에 ephemeron이 없어 실제로 GC 안 되는 게 공식 문서(luau.org/compatibility)로 확인됨 — "혹시 몰라서"가 아니라 확정된 필수 조치로 격상(`relate-plan.md`에 일반 규칙으로도 승격). **[2026-08-12 열다섯 번째 세션]** `Slot:Splice(index, removeCount, ...newElements)` CRUD 신설(구간 제거+삽입을 shift/recompute 1회로 묶는 순수 최적화, `newElements`는 의도적으로 vararg 유지 — `Tag:Added`의 `string|{string}` 전환과는 다른 이유). **[2026-08-12 열여섯 번째 세션]** `slotOwner`를 top-level/nested 이중 마운트 gap까지 잡는 `elementOwner`로 일반화, `bindLifetime`을 top-level 전용으로 축소(nested는 `_elements` 강참조로 transitively 생존). **[2026-08-13 세션]** `releaseOwner`가 소유권 불일치를 조용히 무시하던 걸 즉시 error로 강화, `bindLifetime`을 `attachSlot`의 조건 분기에서 `SlotHandler.process`(Handler 층위)로 이동해 `unbindLifetime`과 대칭을 맞춤. **[2026-08-13 세션, 다섯 번째, 전면 반영]** `Dispatch`가 핸들러 identity 대신 인덱스로 재추적되며 `SlotHandler.process`가 `retract` 별도 필드 없이 자기 retract 클로저를 반환하는 계약으로 전환 — `kSlotMap`이 완전히 불필요해짐(어느 `process` 호출이 반환한 클로저든 `slotValue`/`inst`를 동일하게 캡처해 대칭적으로 동작하므로), `base/dispatch-core-plan.md` "Dispatch 체인" 절 참고. **[2026-08-13 감사]** 그 "대칭적으로 동작"이 `claimOwner`의 false가 *같은 (inst,k) 재발행*일 때만 참이었음이 드러나 소유권 판정을 둘로 분리 — nested(`rawAdd`)는 엄격 `claimOwner`(같은 owner 재클레임도 error, `Slot{a,a}`가 조용히 통과하던 것 차단), top-level은 `claimOwnerAt(element,inst,k)`으로 위치까지 봐서 `Frame{slot,slot}`을 error로 잡음. 추가로 `rawRemove`의 `releaseOwner` 누락(산문엔 있고 의사코드엔 없었음)과 `destroySlotTree`가 자식 소유권/`_mounted`를 안 되돌려 GC 타이밍 의존 오류를 내던 것도 수정. `State` 재설정 경로가 안전함(reconcile이 제거→`rawAdd` 순서라 release→claim)은 별도 절로 확인 기록. **[2026-08-13 세션, 여섯 번째 — 전면 역전]** `State` 교체가 **파괴에서 언마운트로 뒤집힘**(`state`와 동일 — "이전 값을 지울지는 그 값을 만든 쪽이 정한다"는 `Ref`/`Attribute`와 같은 철학) — 이에 따라 (a) 비파괴 짝 `rawUnmount`/`unmountSlotTree` 신설(`rawRemove`/`destroySlotTree`와 딱 하나만 다름: 안 죽임)되고 `reconcile`이 직접 부르는 게 `rawAdd`/`rawUnmount`/`rawMove`로 바뀜, (b) **오래 "오버엔지니어링"으로 기각돼 있던 포탈이 별도 기능이 아니라 이 결정의 자연스러운 귀결이 됨**(옛 "폐기, 옮기지 않음" 결정은 역전, `archive/slot-discard-no-portal-reversed.md`), (c) 명시적 파괴 수단으로 base 탑레벨 `dispose(value)` 신설 — 아직 트리가 살아있길 요구하는 값이면 파괴를 **거부하고 error** **[2026-08-13 열네 번째 세션]** 하강 diff 반영 — `SlotHandler`의 클로저가 받는 값이 항상 `Slot`이거나 `nil`임이 계약으로 보장되고, 언마운트 경로의 `setOffsetSource(None)`/`setLength(0)` 순서는 그대로. **[2026-08-14 열 번째 세션]** `dispose`의 시그니처/범위(`question.md` 0-B) 확정 — `dispose(value: Slot | Instance)`, `isSlot`이 아니면 백엔드 주입 op `disposeInst(inst)`로 위임(`addTag`/`removeTag`/`setAttribute`와 같은 패턴), `Observer`/`Effect`는 GC-native lifecycle만으로 충분해 범위에서 명시적으로 제외 | @@ -104,6 +104,7 @@ | `dispatch-hintvalue-model-reversed.md` | **[2026-08-13 열네 번째 세션 신설 — 옛 이름은 research/ 아래의 dispatch-redispatch-diff-plan]** 뒤집힌 **"철거 후 재구축 + `hintValue` 힌트"** 재디스패치 모델 원문 + 역전을 이끈 분석 전문(`None`/`State` 래퍼가 힌트로 새는 재현 사례, 깊은 인덱스 힌트 유실, 옛 점유 체크가 Attribute 소유권을 대신하던 구조). 지금 유효한 모델은 `base/dispatch-core-plan.md` | | `checkpoint-handler-pattern-reversed.md` | **[역전됨, 2026-08-13 다섯 번째 세션 신설]** `AttributeGroupHandler`의 이름 소유권 충돌을 고치려고 만든 `Dispatch.processAs`/`Dispatch.retractSelfAndUnder` 체크포인트 핸들러 패턴(같은 날 네 번째 세션 신설) — `chains`를 핸들러 identity가 아니라 재귀 깊이 인덱스로 추적하는 더 근본적인 재설계로 대체되며 같은 날 바로 불필요해짐. `State>`도 이 재설계로 UB에서 정상 지원 대상으로 바뀜 | | `canexecute-inst-arg-reversed.md` | **[역전됨, 2026-08-14 다섯 번째 세션 신설]** `canExecute(inst,value)`/`unbindLifetime(inst,value)` 2-인자 시그니처와, 그 뿌리였던 **`bindLifetime`이 `.Subscribed`를 세팅한다**는 오염(2026-08-08 다섯 번째 세션에 "재정정"으로 들어와 2026-08-09의 `canBound`까지 그 위에 세워짐) — `.Subscribed`는 전역 `:Subscribe()` 전용 필드라 leaf 경로와 무관했고, `bindLifetime`이 gcconn 참조를 `value` 쪽으로 복사해두면 `value` 하나로 생존을 물을 수 있음. 같이 폐기된 것은 `canBound(handle)`(**2026-08-14 열한 번째 세션에 별도 진입점으로 재도입 — 이 문서 하단에 addendum**)와 gcconn/gchold의 lazy 생성. 오류가 여섯 세션을 살아남은 이유(`canExecute`의 실제 호출부가 어느 문서에도 코드로 없었음)와 그 일반 교훈("계약을 정할 때 호출부를 최소 하나는 의사코드로 같이 적을 것")도 정리. 현행은 `base/lifecycle-pattern.md` | +| `tag-attribute-load-time-registration-reversed.md` | **[역전됨, 2026-08-14 열두 번째 세션 신설]** "`TagHandler`/`AttributeKeyHandler`/`AttributeGroupHandler`가 quad-base 모듈 로드 시점에 스스로 등록한다"(열한 번째 세션 "네 번째, 최종 정정") — `base/lifecycle-pattern.md`가 이미 거부해둔 `InitNamespace`류 top-level 부작용 패턴과 같은 클래스라 틀렸음. 정정: 저 이름들은 참조 카운트/이름 claim **알고리즘 구현**일 뿐이고, `HANDLER_PRIORITY_FALLBACK`에 실제로 꽂히는 건 이걸 감싸는 별도 이름의 `...FallbackHandler`이며, 등록 주체는 quad-base 모듈이 아니라 백엔드 팩토리(`BaseModule` 뮤테이션 시점, 자기 전용 Handler들과 같이). 현행은 `base/dispatch-core-plan.md`의 "base가 소유하는 핸들러와 주입되는 엔진 op" 절 | ## 참고 diff --git a/.claude/archive/tag-attribute-load-time-registration-reversed.md b/.claude/archive/tag-attribute-load-time-registration-reversed.md new file mode 100644 index 0000000..2106257 --- /dev/null +++ b/.claude/archive/tag-attribute-load-time-registration-reversed.md @@ -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번째 세션 요약이 이 재역전을 링크로 가리킴. diff --git a/.claude/base/architecture.md b/.claude/base/architecture.md index 99e3b65..4758627 100644 --- a/.claude/base/architecture.md +++ b/.claude/base/architecture.md @@ -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) │ │ ├── Handler.luau # 핸들러 계약 타입(isHandlable/priority/process — process가 자기 retract 클로저를 반환) │ │ ├── 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 두 번째 세션 확정) -│ │ ├── Tag.luau # TagHandler — 이름별 참조 카운트(`tagNameMap`), 실제 호출은 주입된 addTag/removeTag(inst, {string}). quad-base가 모듈 로드 시점에 priority=HANDLER_PRIORITY_FALLBACK로 스스로 등록(`base/tag-plan.md`, 2026-08-13 열네 번째 세션 base로 이동) -│ │ ├── AttributeKey.luau # AttributeKeyHandler — 이름 claim(`nameClaims`, 소유권 충돌 즉시 error) + 주입된 setAttribute(inst,name,v) 호출, `None`→nil은 재디스패치로 자동(`base/attribute-plan.md` "이름 소유권" 절) -│ │ ├── Attribute.luau # AttributeGroupHandler — 그룹 전용 키(비공개 GetKey)로 이름마다 AttributeKey 경로에 인덱스 1 위임, 클로저가 자기 키 전부 retractFrom(`base/attribute-plan.md` "메커니즘" 절) +│ │ ├── 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}). `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` "이름 소유권" 절). `HANDLER_PRIORITY_FALLBACK`에는 이걸 감싸는 `AttributeKeyFallbackHandler`가 백엔드 팩토리 뮤테이션 시점에 등록됨 +│ │ ├── Attribute.luau # AttributeGroupHandler — 그룹 전용 키(비공개 GetKey)로 이름마다 AttributeKey 경로에 인덱스 1 위임, 클로저가 자기 키 전부 retractFrom(`base/attribute-plan.md` "메커니즘" 절). `AttributeGroupFallbackHandler`가 같은 방식으로 감쌈 │ │ └── Slot.luau # add/remove/clear 재조정 로직(추상 자식 참조 기준) │ ├── 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`) diff --git a/.claude/base/attribute-plan.md b/.claude/base/attribute-plan.md index 14c6dc9..549975b 100644 --- a/.claude/base/attribute-plan.md +++ b/.claude/base/attribute-plan.md @@ -446,7 +446,7 @@ quad-roblox** 소속이었음 — 그런데 실제로 엔진에 종속된 건 | 그룹 값 타입+API(`Attribute(...)`/`Merged`/`:NameMap`) | quad-base | | 단일 키 `AttributeKey<>(name)` + 이름별 weak 캐시 | 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` 층) | | **`setAttribute(inst, name, v)`** — `v == nil`이면 그 이름을 지움 | 백엔드가 주입 | @@ -455,18 +455,23 @@ quad-roblox** 소속이었음 — 그런데 실제로 엔진에 종속된 건 수 없음. 반대로 string/number/boolean은 어느 백엔드에나 있으므로 base에 둔다. "이 값이 이 백엔드에서 표현 가능한가"라는 **검증도 base가 아니라 주입된 `setAttribute`의 몫** — base는 값을 그대로 흘려보냄. -- **`AttributeKeyHandler`/`AttributeGroupHandler` 자신은 quad-base가 - 모듈 로드 시점에 스스로 등록** — `setAttribute`만 백엔드 팩토리가 - 채우는 타입 계약, 안 채운 슬롯의 base 기본값은 명시적으로 에러내는 - 스텁(2026-08-14 열한 번째 세션 — 한때 "등록 자체가 백엔드 선택"으로 - 잘못 정정됐다가 철회됨). 더 명확한 메시지나 진짜 원자적 실패를 원하는 - 백엔드는 opt-in으로 `HANDLER_PRIORITY_FALLBACK + 1`짜리 가로채기 - Handler를 추가로 등록 가능. 속성 처리 자체를 통째로 다른 알고리즘으로 - 바꾸고 싶은 백엔드는 `HANDLER_PRIORITY_FALLBACK`보다 확실히 높은 - 우선순위로 자기 Handler를 등록하면 base 것을 완전히 대체함(같은 - override 원리) — 상세는 `base/dispatch-core-plan.md`의 "base가 - 소유하는 핸들러와 주입되는 엔진 op" 절. `Tag`도 정확히 같은 - 구조(`base/tag-plan.md`). +- **`AttributeKeyHandler`/`AttributeGroupHandler` 자신은 참조 카운트/이름 + claim 알고리즘 구현일 뿐, 스스로 등록되는 주체가 아님(2026-08-14 열두 + 번째 세션 정정).** `HANDLER_PRIORITY_FALLBACK`에 실제로 꽂히는 건 + 이걸 감싸는 `AttributeKeyFallbackHandler`/ + `AttributeGroupFallbackHandler` — 등록 주체는 quad-base 모듈 자체가 + 아니라 백엔드 팩토리(`BaseModule` 뮤테이션 시점, 자기 전용 Handler들과 + 같이 등록). 옛 "quad-base 모듈 로드 시점에 스스로 등록" 모델은 + `archive/tag-attribute-load-time-registration-reversed.md`. + `setAttribute`만 백엔드 팩토리가 채우는 타입 계약, 안 채운 슬롯의 + base 기본값은 명시적으로 에러내는 스텁. 더 명확한 메시지나 진짜 + 원자적 실패를 원하는 백엔드는 opt-in으로 + `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 패키지로 쪼개지 않는다는 기존 판단은 그대로 (UICorner 숏핸드/Tween/Tag와 같은 결) — 다만 "어느 패키지의 코어인가"가 quad-roblox에서 quad-base로 바뀐 것. diff --git a/.claude/base/dispatch-core-plan.md b/.claude/base/dispatch-core-plan.md index 0bd5984..078e67e 100644 --- a/.claude/base/dispatch-core-plan.md +++ b/.claude/base/dispatch-core-plan.md @@ -400,10 +400,12 @@ end - `Dispatch.addHandler(handler: Handler)` — 핸들러를 우선순위 레지스트리에 등록. `Dispatch.process`/`getHandler`와 마찬가지로 base엔 인터페이스만 있고, quad-roblox의 concrete Handler들(PropertyHandler/EventHandler/ - OnChangeHandler/UICornerHandler/TagHandler/AttributeHandler 등)은 - 팩토리가 `BaseModule`을 + OnChangeHandler/UICornerHandler 등)은 팩토리가 `BaseModule`을 뮤테이션하는 시점에 이걸로 등록됨(아래 "base 유틸은 인터페이스" 절과 - 같은 패턴, 새 메커니즘 아님). + 같은 패턴, 새 메커니즘 아님). **`Tag`/`Attribute`의 base 소유 + Fallback Handler들(`TagFallbackHandler` 등)도 같은 팩토리 뮤테이션 + 시점에 같이 등록됨** — quad-base 모듈 로드 자체의 부작용이 아님, + 상세는 아래 "base가 소유하는 핸들러와 주입되는 엔진 op" 절. - Handler 자신의 필드는 계속 `process`/`retract`(이미 확정된 이름, `question.md`에 "특별한 문제 없음"으로 못박혀 있어 재검토 대상 아님) — 겹침은 실제 런타임 충돌이 아니라 프로즈 표기 문제였을 뿐이라, 항상 @@ -557,15 +559,30 @@ setAttribute(inst: any, name: string, v: any?): () -- v == nil이면 그 이름 매핑하면 됨(웹이면 `removeAttribute`). base 쪽 규칙 — "Attribute는 오직 명시적 `None`/`nil`로만 지워진다"(`base/attribute-plan.md`) — 은 그대로. -**[재정정, 2026-08-14 열한 번째 세션 — 앞선 "등록 자체도 백엔드의 -선택" 안은 틀렸음, 철회] `TagHandler`/`AttributeKeyHandler`/ -`AttributeGroupHandler`는 quad-base가 자기 모듈 로드 시점에 -`HANDLER_PRIORITY_FALLBACK`으로 스스로 `Dispatch.addHandler` 등록한다 -— 이게 기본이고 필요함.** `HANDLER_PRIORITY_FALLBACK`이라는 밴드 -자체가 정확히 이런 용도 — "아무도 이 자리를 안 가져갔을 때의 안전한 -기본 동작"을 base가 공짜로 제공하는 것. 모든 백엔드가 자동으로 -`Tag`/`Attribute` 부기(참조 카운트/이름 claim)를 얻고, 특별히 뭔가를 -하지 않아도 이 값들이 어떤 자리에 놓이든 최소한 매치는 됨. +**[재정정, 2026-08-14 열두 번째 세션] `TagHandler`/`AttributeKeyHandler`/ +`AttributeGroupHandler`는 참조 카운트/이름 claim **알고리즘 구현**일 +뿐이고, 스스로 등록되는 주체가 아니다.** `HANDLER_PRIORITY_FALLBACK`에 +실제로 꽂히는 건 그 알고리즘을 그대로 감싸는 **별도 이름의 엔티티** +(`TagFallbackHandler`/`AttributeKeyFallbackHandler`/ +`AttributeGroupFallbackHandler`) — "이게 기본 안전망으로 자동 설치되는 +대상"임을 이름 자체로 구분한다. **등록 주체는 quad-base 모듈 자체가 +아니라 필요한 엔진(백엔드 팩토리)** — 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가 시그니처만 소유하고 실제 구현은 팩토리가 뮤테이션으로 주입하는 **타입 계약**(`bindLifetime`/ @@ -592,10 +609,13 @@ setAttribute(inst: any, name: string, v: any?): () -- v == nil이면 그 이름 isHandlable = function(inst,k,v) return isTag(v) end, process = function(inst,k,v) error("이 백엔드는 Tag를 지원하지 않음") end } ``` - `TagHandler` 자신(`FALLBACK`)보다 한 단계 높아 스캔에서 먼저 매치되고, - "매치된 Handler 하나만 실행"이라는 기존 규칙 덕분에 `TagHandler.process` - (와 그 안의 `tagNameMap` mutation)는 아예 안 불림 — op 에러보다 - 이르고 정확한, 진짜 원자적 실패. 단 이건 **선택적 업그레이드**일 뿐 + 실제로 `FALLBACK`에 등록돼 있는 `TagFallbackHandler`보다 한 단계 + 높아 스캔에서 먼저 매치되고(2026-08-14 열두 번째 세션 정정 — `TagHandler` + 자신은 스스로 등록되지 않음, 위 "base가 소유하는 핸들러와 주입되는 + 엔진 op" 절 참고), "매치된 Handler 하나만 실행"이라는 기존 규칙 + 덕분에 `TagHandler.process`(와 그 안의 `tagNameMap` mutation)는 아예 + 안 불림 — op 에러보다 이르고 정확한, 진짜 원자적 실패. 단 이건 + **선택적 업그레이드**일 뿐 기본 요구사항은 아님 — base 기본 스텁 하나로도 이미 충분히 안전하게 실패함(`AttributeGroupHandler`의 "부분 실패 경로" 절이 이미 정리한 "에러=패닉 상태, 그 이후 정합성은 관리 대상 아님" 원칙 + `nameClaims`/ @@ -906,6 +926,21 @@ end - 그리고 `Relate`에 쓴 걸 클로저에서 지울 땐 **"내가 실제로 물러날 때만"** 지울 것 — 조건 밖에서 무조건 지우면 dedup이 무력화됨 (`RefLeafHandler`가 정확히 이 버그였음). +- **`Observer`/`Effect`의 Leaf 바인딩(`Dispatch/Leaf.luau`)도 `RefLeafHandler`와 + 같은 `old ~= v` dedup을 둠 — correctness 문제는 아니지만 순수 성능 + 최적화로 채택(2026-08-14 세션, 사용자 판단).** `State`/ + `State`가 재-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`를 통해서만 진입한다.** `handler.process(...)`를 직접 부르면 핸들러 비교와 `chains` 기록이 통째로 diff --git a/.claude/base/effect-plan.md b/.claude/base/effect-plan.md index c2be858..ef68701 100644 --- a/.claude/base/effect-plan.md +++ b/.claude/base/effect-plan.md @@ -70,9 +70,10 @@ leaf가 살아있는 동안만 유효, leaf가 죽으면 최종 정리 콜백 leaf당 실제 Destroying 바인딩 하나(공유 weak table로 되는 Observer보다 비쌈) — 필요할 때만 쓰는 걸로 충분. -**동적 경로 가드 — `k` 무관 매치, `HANDLER_PRIORITY_FALLBACK` +### 동적 경로 가드 — `k` 무관 매치, `HANDLER_PRIORITY_FALLBACK` + (2026-08-14 열한 번째 세션, `PreRef`/`Observer`와 같은 패턴, `base/ -source-state-plan.md`의 "동적 경로 가드" 절 참고).** `EffectHandle`도 +source-state-plan.md`의 "동적 경로 가드" 절 참고.) `EffectHandle`도 children 배열 리터럴 전용이라, 해시 파트 named 자리 등으로 동적으로 흘러들어오면 명확히 에러내야 함 — `{ priority = HANDLER_PRIORITY_FALLBACK, isHandlable = function(inst,k,v) return isEffect(v) end, process = diff --git a/.claude/base/lifecycle-pattern.md b/.claude/base/lifecycle-pattern.md index e838296..c07ae76 100644 --- a/.claude/base/lifecycle-pattern.md +++ b/.claude/base/lifecycle-pattern.md @@ -442,7 +442,7 @@ quad-roblox 구현 단계에서 실측 확인 대상 — 문제가 되면 gcconn 이건 `base/dispatch-core-plan.md`의 "핸들러 내부 상태 저장" 유틸(`Relate` 직접 사용)과 짝을 이루는 별도 유틸 — 하나는 "상태를 어디에 저장할지" (`Relate:SetStrong`/`:SetWeak`), 다른 하나는 "언제까지 실행되어도 되는지" -(`bindLifetime` + `canExecute`)를 다룸. 후자는 내부적으로 전자가 제공하는 +(`bindLifetime` + `canBound`/`canExecute`)를 다룸. 후자는 내부적으로 전자가 제공하는 같은 `Relate` 프리미티브 위에 얹혀 구현됨(위 절) — 별도 저장 메커니즘을 새로 만든 게 아니라 `Relate` 하나를 두 용도로 재사용. 둘 다 base가 제공하는 범용 유틸로 확정. diff --git a/.claude/base/module-lifecycle-plan.md b/.claude/base/module-lifecycle-plan.md index 8129ce2..2abbc9c 100644 --- a/.claude/base/module-lifecycle-plan.md +++ b/.claude/base/module-lifecycle-plan.md @@ -135,12 +135,18 @@ init하려 하면 오류, 없는데 뭔가 생성해서 bind하려 해도 오류 알고리즘이 통째로 quad-base로 옮겨오면서, 엔진에 실제로 손대는 마지막 한 줄만 이 경로로 주입받게 됨(`base/dispatch-core-plan.md` "base가 소유하는 핸들러와 주입되는 엔진 op" 절). **`TagHandler`/ - `AttributeKeyHandler`/`AttributeGroupHandler` 자신은 quad-base가 - 모듈 로드 시점에 `HANDLER_PRIORITY_FALLBACK`으로 스스로 등록** — - `addTag`/`removeTag`/`setAttribute`만 백엔드 팩토리가 뮤테이션으로 - 채우는 타입 계약. 아직 아무 팩토리도 안 채운 슬롯의 기본값은 - quad-base가 명시적으로 에러내는 스텁으로 미리 채워둠(조용한 no-op - 추측 아님 — base가 임의 엔진의 "맞는 기본 동작"을 알 수 없어서). + `AttributeKeyHandler`/`AttributeGroupHandler` 자신은 참조 카운트/이름 + claim 알고리즘 구현일 뿐 스스로 등록되는 주체가 아님(2026-08-14 열두 + 번째 세션 정정, 옛 "quad-base 모듈 로드 시점에 스스로 등록" 모델은 + `archive/tag-attribute-load-time-registration-reversed.md`) — + `HANDLER_PRIORITY_FALLBACK`에 실제로 꽂히는 건 이걸 감싸는 + `TagFallbackHandler`/`AttributeKeyFallbackHandler`/ + `AttributeGroupFallbackHandler`이고, 등록 주체는 quad-base 모듈 자체가 + 아니라 백엔드 팩토리(바로 위 문단과 같은 `BaseModule` 뮤테이션 경로 — + 새 예외 아님).** `addTag`/`removeTag`/`setAttribute`만 백엔드 팩토리가 + 뮤테이션으로 채우는 타입 계약. 아직 아무 팩토리도 안 채운 슬롯의 + 기본값은 quad-base가 명시적으로 에러내는 스텁으로 미리 채워둠(조용한 + no-op 추측 아님 — base가 임의 엔진의 "맞는 기본 동작"을 알 수 없어서). 더 명확한 메시지나 진짜 원자적 실패(부기 mutation 0회)를 원하는 백엔드는 opt-in으로 `HANDLER_PRIORITY_FALLBACK + 1`짜리 가로채기 Handler를 추가로 등록할 수 있음 — 상세는 diff --git a/.claude/base/ref-plan.md b/.claude/base/ref-plan.md index 343da9f..b5e7913 100644 --- a/.claude/base/ref-plan.md +++ b/.claude/base/ref-plan.md @@ -254,7 +254,9 @@ skip"이라는 dedup은 `process`가 "이전에 뭐가 있었는지"를 알아 local relate = Relate() -- Ref-leaf handler 전용, (inst,k)별 마지막으로 바인딩한 Ref 기억 — -- 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) local old = relate:GetStrong(inst, k) @@ -583,7 +585,7 @@ flatten된 값은 해시 파트(프로퍼티 키)로 존재하게 되고, Store 나지만, 나중에 named 자리 바인드 같은 실제 기능이 확정되면 base 가드를 건드리지 않고 그 기능의 Handler를 평범한 우선순위로 하나 등록하는 것만으로 자연히 우선함, `base/dispatch-core-plan.md`의 - "`HANDLER_PRIORITY_FALLBACK`" 절). 이 + "base가 소유하는 핸들러와 주입되는 엔진 op" 절). 이 Handler는 **`Dispatch.process`/`getHandler`의 정상 우선순위 스캔에 등록**되는 반면(pre-pass처럼 그 밖에서 도는 게 아님), 리터럴 배열의 `PreRef`는 pre-pass가 fire와 동시에 해당 슬롯을 소진(**[정정, @@ -758,7 +760,7 @@ dispatch-core-plan.md` "Length/Offset" 절의 계약을 특수 취급 없이 그 이유는 `ProcessedPreRef`와 동일 — "원래부터 빈 자리(`None`)"와 구별돼야 등록 책임 소재가 분명해지고, 배열에 구멍이 안 생김. -**동적 경로 가드 Handler도 거울상으로 하나 더** +### 동적 경로 가드 Handler도 거울상으로 하나 더 `PreRef`와 똑같이, `PostRef`도 **children 배열의 리터럴 아이템으로만** 놓을 수 있음 — Modifier 필드/Source/Store 값으로는 **타입으로 차단**(이유도 diff --git a/.claude/base/source-state-plan.md b/.claude/base/source-state-plan.md index c195fcd..bf9d1ee 100644 --- a/.claude/base/source-state-plan.md +++ b/.claude/base/source-state-plan.md @@ -897,23 +897,29 @@ retract/Destroy되면 자동으로 정리됨. `Ref`와 같은 방식으로 라이프사이클에 묶어주는 것 말고는 base가 더 해줄 일이 없음. 새 dispatch 메커니즘이 아니라 기존 children-array 참가자 패턴의 반복. -- **동적 경로 가드 — `k` 무관 매치, `HANDLER_PRIORITY_FALLBACK` - (2026-08-14 열한 번째 세션, `PreRef`의 동적 경로 가드와 같은 패턴).** - `Observer`도 children 배열 리터럴 전용이라, 해시 파트 named 자리 - 등으로 동적으로 흘러들어오면(타입 우회 버그) 명확히 에러내야 함 — - 전용 `Handler` 등록: `{ priority = HANDLER_PRIORITY_FALLBACK, - isHandlable = function(inst,k,v) return isObserver(v) end, process = - function(inst,k,v) error("Observer는 children 배열 리터럴에만 놓을 수 - 있음") end }`. `HANDLER_PRIORITY_FALLBACK`인 이유는 이게 무조건 막는 - 하드 블록이 아니라 `Tag`/`Attribute`/`PreRef`와 같은 "base가 소유하되 - 평범한 우선순위로 등록된 다른 Handler가 있으면 그쪽이 이기는" 자리이기 - 때문(`base/dispatch-core-plan.md`의 "`HANDLER_PRIORITY_FALLBACK`" 절) — - 지금은 아무도 그 자리를 안 가져가서 항상 이 가드가 에러를 내지만, 이 - Handler를 만드는 게 목적이 아니라 "지금은 확정된 기능이 없다"는 default를 - base가 값싸게 제공하는 것뿐. (**이 가드가 없던 이전엔** 확정된 "매치 - 실패는 즉시 error" 규칙에 의해 결과적으로 똑같이 에러가 났었음 — 이 - 가드는 동작을 바꾸는 게 아니라 에러 메시지를 명확하게 하고, 미래에 - override할 자리를 구조적으로 열어두는 것.) + +### 동적 경로 가드 — `k` 무관 매치, `HANDLER_PRIORITY_FALLBACK` + +(2026-08-14 열한 번째 세션, `PreRef`의 동적 경로 가드와 같은 패턴.) +`Observer`도 children 배열 리터럴 전용이라, 해시 파트 named 자리 +등으로 동적으로 흘러들어오면(타입 우회 버그) 명확히 에러내야 함 — +전용 `Handler` 등록: `{ priority = HANDLER_PRIORITY_FALLBACK, +isHandlable = function(inst,k,v) return isObserver(v) end, process = +function(inst,k,v) error("Observer는 children 배열 리터럴에만 놓을 수 +있음") end }`. `HANDLER_PRIORITY_FALLBACK`인 이유는 이게 무조건 막는 +하드 블록이 아니라 `Tag`/`Attribute`/`PreRef`와 같은 "base가 소유하되 +평범한 우선순위로 등록된 다른 Handler가 있으면 그쪽이 이기는" 자리이기 +때문(`base/dispatch-core-plan.md`의 "base가 소유하는 핸들러와 주입되는 +엔진 op" 절) — +지금은 아무도 그 자리를 안 가져가서 항상 이 가드가 에러를 내지만, 이 +Handler를 만드는 게 목적이 아니라 "지금은 확정된 기능이 없다"는 default를 +base가 값싸게 제공하는 것뿐. (**이 가드가 없던 이전엔** 확정된 "매치 +실패는 즉시 error" 규칙에 의해 결과적으로 똑같이 에러가 났었음 — 이 +가드는 동작을 바꾸는 게 아니라 에러 메시지를 명확하게 하고, 미래에 +override할 자리를 구조적으로 열어두는 것.) + +이어서, base가 제공하는 나머지 항목: + - **콜백 실행은 기존 `canExecute` predicate로 게이팅**(Slot 생존 확인과 동일한 재사용 — "canExecute 하나로 통일" 원칙, 새 메커니즘 발명 아님) — 발화 시점과 처리 시점 사이에 owning leaf가 이미 죽었으면 no-op. @@ -1159,7 +1165,8 @@ leaf 부착을 통한 간접 호출이든) 둘뿐** — 새 규칙을 따로 만 ```lua function bindLifetime(inst, value) - if canExecute(value) then + if canBound(value) then -- [정정, 2026-08-14 열두 번째 세션] 이 절이 확정한 대로 + -- bindLifetime의 게이트는 canBound, canExecute 아님 error("이미 바인딩된 값") -- 메시지 분기는 위 게이트 스케치 참고 end ... -- gchold 등록 + gcconn 참조 복사(base/lifecycle-pattern.md) @@ -1181,6 +1188,51 @@ end 뒤에 같은 값을 leaf로 놓거나 `:Subscribe()`하면 기존 두 진입점의 기존 체크가 그대로 걸러줌 — 이 방향은 별도 코드 추가 없이 이미 성립. +### Observer/Effect Leaf dedup — `RefLeafHandler`와 같은 패턴, 순수 성능 최적화(2026-08-14 세션) + +**correctness 문제는 아님 — `old ~= v`를 안 넣어도 안 깨짐.** `State`/ +`State`가 재-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차 라운드) — 둘 다 기존 확정 유지 `.claude/initreq/artworks/EventDrivenProgramming/`(Connection/Event/ diff --git a/.claude/base/tag-plan.md b/.claude/base/tag-plan.md index 30626f5..5989587 100644 --- a/.claude/base/tag-plan.md +++ b/.claude/base/tag-plan.md @@ -152,7 +152,9 @@ identity로 홀더를 추적하면 같은 객체를 두 위치(`k1`, `k2`)에 ```lua 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 전용 function TagHandler.process(inst, k, v, index) @@ -264,19 +266,34 @@ removeTag(inst: any, names: {string}): () vararg → `string | {string}`으로 되돌아갔던 것과 **같은 이유**(위 "값 모양" 절). 배치 호출 자체는 테이블로도 되므로 웹 `className` 일괄 갱신 요구도 그대로 충족됨. -- **등록 우선순위는 `HANDLER_PRIORITY_FALLBACK` — `TagHandler` 자신은 - quad-base가 모듈 로드 시점에 스스로 등록**(2026-08-14 열한 번째 세션 - 재확인 — 한때 "등록 자체가 백엔드 선택"으로 잘못 정정됐다가 철회됨). +- **등록 우선순위는 `HANDLER_PRIORITY_FALLBACK` — 단 여기 꽂히는 건 + `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`만 백엔드 팩토리가 채우는 타입 계약, 안 채운 슬롯의 base 기본값은 명시적으로 에러내는 스텁. 더 명확한 메시지나 진짜 원자적 실패를 원하는 백엔드는 opt-in으로 `HANDLER_PRIORITY_FALLBACK + 1`짜리 가로채기 Handler를 추가로 등록 가능. 태그 처리 자체를 통째로 다른 알고리즘으로 바꾸고 싶은 백엔드는 `HANDLER_PRIORITY_FALLBACK`보다 확실히 높은 우선순위로 자기 Handler를 - 등록하면 base `TagHandler`를 완전히 대체함(같은 override 원리). + 등록하면 base `TagFallbackHandler`를 완전히 대체함(같은 override 원리). 상세는 `base/dispatch-core-plan.md`의 "base가 소유하는 핸들러와 주입되는 엔진 op" 절 — `Attribute`도 정확히 같은 구조 - (`base/attribute-plan.md`). + (`base/attribute-plan.md`). 래퍼는 필드 셋뿐인 얇은 재노출: + + ```lua + TagFallbackHandler = { + priority = HANDLER_PRIORITY_FALLBACK, + isHandlable = TagHandler.isHandlable, + process = TagHandler.process, + } + ``` - 이건 새 아키텍처 개념이 아니라 이미 확정된 "base는 인터페이스/값, backend는 구현"(`LifetimeHandle`의 `bindLifetime`/`canExecute`, `Dispatch.addHandler` 자체가 그 패턴)을 핸들러 층까지 밀어붙인 것. diff --git a/.claude/question.md b/.claude/question.md index 4f53455..03c1431 100644 --- a/.claude/question.md +++ b/.claude/question.md @@ -18,7 +18,8 @@ > lazy 핸들 계약)는 열세 번째 세션에, `0-Z`(Attribute 이름 소유권)와 > `0-A`(재디스패치 하강 diff)는 열네 번째 세션에, **`0-W`(`Ref` 이중 > 배치 방지)는 2026-08-14 열한 번째 세션에 확정·`base/` 반영 완료** — -> "결정 대기" 절 자체가 지금은 비어 있음. 해소 전 원문과 결론은 +> 그래서 이 문서엔 이제 "결정 대기" 절 자체가 없음(비어서 헤딩째로 +> 삭제). 해소 전 원문과 결론은 > `archive/question-resolved.md`, 뒤집힌 옛 재디스패치 모델은 > `archive/dispatch-hintvalue-model-reversed.md`. > diff --git a/.claude/session/2026-08-14-12-observer-effect-leaf-dedup.md b/.claude/session/2026-08-14-12-observer-effect-leaf-dedup.md new file mode 100644 index 0000000..9ee195d --- /dev/null +++ b/.claude/session/2026-08-14-12-observer-effect-leaf-dedup.md @@ -0,0 +1,212 @@ +# 2026-08-14 열두 번째 세션 — Observer/Effect Leaf에 `Ref`와 같은 identical-value dedup 추가(성능 최적화) + +## 배경 + +이전 대화에서 State/State/State가 전부 설계상 +지원되는지(읽기만으로 판단) 확인한 뒤, 사용자가 이어서 "state의 +`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 대상에 넣어야 +한다는 게 이번에 다시 확인됨. diff --git a/CLAUDE.md b/CLAUDE.md index c0094f9..2f3d851 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -174,7 +174,7 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션 되짚음 — "이미 묶여 있는가"(bound 문맥)와 "지금 발화해도 되는가" (execute 문맥)는 판정 로직은 공유해도 호출부의 질문이 다르다는 사용자 지적, `base/lifecycle-pattern.md`의 "`canBound` vs `canExecute`" 절). - `question.md`의 "결정 대기" 절 자체가 지금은 비어 있음. + `question.md`엔 이제 "결정 대기" 절 자체가 없음(비어서 헤딩째로 삭제). **M0 착수 전 반드시 읽을 것 — 이 두 개는 "결정"이 아니라 "구현 규약"이라 여전히 유효**: @@ -266,7 +266,8 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션 게이티드 노드를 공용 `Gate`로 빼두는 것만은 그 시점에 해야 함**(따로 하면 같은 설계를 두 번 함). 주입 op 2개(`setTimeout`/`clearTimeout`)가 백엔드 팩토리 표면에 추가될 예정이라는 것도 M1 설계 시 인지. 설계는 - 네 라운드로 대부분 확정됐고 남은 열린 질문 4개는 `question.md` 3번. + 네 라운드로 대부분 확정됐고 남은 열린 질문은 `question.md` 3번(개수는 + 거기도 반복 안 함 — 소스는 `research/debounce-throttle-plan.md` 12절). 5. 자율 작업 루프/스케줄 설정 여부는 사용자 결정 대기 중 (`HUMAN_TODO.md` 2번 항목). @@ -1371,102 +1372,72 @@ Modifier 필드 금지 규칙과 혼동해 잘못 답했다가 사용자 지적 **2026-08-14 열한 번째 세션 — 코퍼스 전체 감사(서브에이전트 6개 병렬), `canBound`/`canExecute` 재분리로 `question.md` 0-W 해소** (`session/2026-08-14-11-corpus-audit-canbound-resplit.md`) -사용자 요청으로 `.claude/` 전체를 `doc-check.py` + 6개 병렬 서브에이전트로 -감사 — stale 세션 번호(luau-test 파일들의 "canExecute 재정정"이 "세 번째" -vs "다섯 번째"로 갈라짐, archive 교차확인 결과 다섯 번째가 맞음), 잘못된 -인용(`comparison-charm.md`가 이미 확정된 `:Compute`의 `previous` 인자를 -존재하지 않는 문서 인용으로 "미결"이라 잘못 서술), 자기모순 배너(`tag-plan.md` -의사코드와 "패키지 배치" 절 우선순위 불일치, `additional-primitives-plan.md`의 -"새 질문 없음" 배너가 이후 추가된 질문 2개와 모순) 등 15개 파일의 실제 -사실 오류를 발견·수정. 감사 후 사용자가 `question.md` 0-W(`Ref` 이중 -배치 방지)를 선택지 (a)로 확정 — 메커니즘은 `RefLeafHandler`가 새 -`Relate` 없이 `bindLifetime`/`unbindLifetime`을 재사용(이미 내장된 -이중 바인딩 가드를 그대로 탐). 이 과정에서 그 가드 이름이 `canExecute`인 -게 개념적으로 안 맞다는 지적 — "이미 묶여 있는가"(bound 문맥, `bindLifetime`/ -`Observer:Subscribe()`가 묻는 것)와 "지금 발화해도 되는가"(execute 문맥, -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 유지. 새로 연 설계 질문 없음. +6개 병렬 서브에이전트 감사로 stale 서술 15개 파일 정정, `question.md` +0-W(`Ref` 이중 배치 방지) 확정 — `canBound`를 `canExecute`와 별도 +진입점으로 재도입(판정 로직은 `isBoundAlive` 하나로 공유). 후속으로 +`PreRef`/`PostRef`/`Observer`/`Effect`의 non-number 키 유입을 +`HANDLER_PRIORITY_FALLBACK` 동적 경로 가드로 통일, Tag/Attribute +백엔드 op 미주입 처리 정책을 네 라운드 정정 끝에 확정(**그 최종 +결론 중 "TagHandler가 quad-base 모듈 로드 시점에 스스로 등록"은 +**[역전, 같은 날 열두 번째 세션]** — 원문·근거는 +`archive/tag-attribute-load-time-registration-reversed.md`). 이어진 +`git diff` 자기 감사와 `/code-review high`가 각각 추가로 3건씩 발견· +수정(대부분 `canBound` 재도입 때문에 stale해진, 이 세션이 안 건드린 +파일들 — 자기 감사가 "건드린 파일만" 훑는 사각지대를 재확인). +`doc-check.py` ERROR 0 유지. -**같은 세션 후속**: 손 트레이싱의 `Frame1 { Ref = r }` 표기가 실제 -API와 다르다는 사용자 지적(`Ref`는 항상 배열 리터럴이라 `k`는 숫자, -`"Ref"`라는 문자열 키가 아님) — `ref-plan.md`/`archive/question-resolved.md` -정정. 이어서 `PreRef`/`PostRef`/`Observer`/`Effect`가 non-number 키로 -들어오면 어떻게 할지 논의 — 사용자가 처음엔 "process에서 에러"를 -제안했다가 스스로 "가장 아래(FALLBACK)에 까는 게 맞다"고 정정, 넷 다 -`HANDLER_PRIORITY_FALLBACK`으로 등록하는 동적 경로 가드로 통일(하드 -블록이 아니라 `Tag`/`Attribute`처럼 나중에 평범한 우선순위 Handler로 -override 가능한 자리) — `ref-plan.md`/`source-state-plan.md`/ -`effect-plan.md`/`ROADMAP.md` 반영. 마지막으로 Tag/Attribute의 미주입 -백엔드 스텁 에러가 "미지원"과 "미등록"을 구분 안 하는 게 맞는지 확인 -질문 — 확정 원칙(스텁 입장에선 원천적으로 구별 불가) 재확인. 처음엔 -"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 -규칙을 그대로 이용, 새 메커니즘 없음. 같은 두 문서 재반영. +**2026-08-14 열두 번째 세션 — Observer/Effect Leaf에 `Ref`와 같은 dedup 추가(성능)** +(`session/2026-08-14-12-observer-effect-leaf-dedup.md`) +`State`/`State`가 재-dispatch될 때 안쪽 값이 안 바뀌어도 +Dispatch는 값 비교 없이 매번 `retractor`+`process`를 다시 부름 — 처음엔 +`bindLifetime`/`unbindLifetime`이 저렴한 weak-table 쓰기뿐이라(실제 +Roblox 커넥션은 Instance 생성 시 한 번만 만들어짐) 버그가 아니라고 +결론지었으나, 사용자가 "`==` 비교가 매번 도는 해싱 비용보다 항상 싸다"고 +지적 — correctness와 무관하게 순수 성능 이유로 `RefLeafHandler`와 같은 +`old ~= v` dedup을 그대로 채택. `base/dispatch-core-plan.md`(4번 절) +정정 + `base/source-state-plan.md`에 새 절 "Observer/Effect Leaf dedup" +신설(pseudocode 포함). -**세 번째 정정(틀림, 곧바로 재정정됨)** — 어시스턴트가 "TagHandler는 -quad-base가 자기 모듈 로드 시점에 스스로 등록 안 하고, 등록 여부는 -백엔드 팩토리의 (A)지원/(B)미지원 선택"이라는 모델을 제시했으나, 이는 -어시스턴트 자신의 오독이었음(사용자가 실제로 한 말이 아님). **네 번째, -최종 정정** — 사용자가 명확히 바로잡음: "HANDLER_PRIORITY_FALLBACK까지 -등록 안한다 했는데, 개소리 — FALLBACK은 아무 등록 없을 때 처리하려는 -걸 방어하는 것까지 포함하는 애임, 기본 등록은 필요함." **최종 모델**: -`TagHandler`/`AttributeKeyHandler`/`AttributeGroupHandler`는 quad-base가 -모듈 로드 시점에 `HANDLER_PRIORITY_FALLBACK`으로 **스스로 등록**(모든 -백엔드가 자동으로 부기를 얻음 — FALLBACK 밴드의 존재 이유 자체가 이 -"기본 안전 동작"을 공짜로 제공하는 것). `addTag`/`removeTag`/ -`setAttribute`만 백엔드 팩토리가 채우는 타입 계약이고, 안 채운 슬롯의 -base 기본값은 "그럴듯한 기본 동작을 추측"하지 않고 **명시적으로 -에러내는 스텁**(조용한 no-op은 provider 초기화를 잊은 실수를 가려버려 -기각). 더 명확한 메시지·진짜 원자적 실패를 원하는 백엔드만 **opt-in으로** -`HANDLER_PRIORITY_FALLBACK + 1`짜리 가로채기 Handler를 추가로 등록 — -필수가 아니라 선택적 업그레이드. `dispatch-core-plan.md`(정본, 다시 -전면 재작성)/`module-lifecycle-plan.md`/`tag-plan.md`/`attribute-plan.md`/ -`architecture.md` 재반영. **교훈**: 네 라운드 연속 정정이 있었던 이유는 -"누가 실제로 Dispatch.addHandler를 부르는가"라는 아키텍처 전제를 -확신 없이 매번 새로 단정하고 그 위에 patch를 쌓았기 때문 — 불확실한 -전제는 재확인 없이 단정하지 말 것. +**같은 세션 후속 — `/code-review` findings 11건 전부 반영.** +`isHandlable`이 `k` 타입을 안 봐서 죽어있던 FALLBACK 가드 수정, +`bindLifetime` pseudocode의 `canExecute`→`canBound` 정정, +"동적 경로 가드"(볼드 텍스트뿐 실제 헤딩 아니었음) 3곳을 `###`으로 +승격, `question.md`/`CLAUDE.md`/`HUMAN_TODO.md`가 공통으로 갖고 있던 +"결정 대기가 비어 있다"는 서술을 "그 헤딩 자체가 삭제됐다"로 정정, +11번째 세션 기록 ~103줄→~13줄 압축(전문은 +`session/2026-08-14-11-corpus-audit-canbound-resplit.md`에 보존). +**가장 큰 건**: 고치던 중 사용자가 "Tag/Attribute를 base가 스스로 +등록한다는 게 사실이 아님"을 지적 — 열한 번째 세션이 네 라운드 +정정 끝에 확정했던 그 결론 자체가 틀렸음이 드러남(`base/ +lifecycle-pattern.md`가 이미 거부해둔 `InitNamespace`류 top-level +부작용 패턴과 같은 클래스). 정정: `TagHandler`류는 참조 카운트 +**알고리즘 구현**일 뿐 스스로 등록 안 됨 — `HANDLER_PRIORITY_FALLBACK`엔 +별도 이름의 `TagFallbackHandler`류가 꽂히고, 등록 주체는 quad-base +모듈이 아니라 **백엔드 팩토리**(`BaseModule` 뮤테이션 시점, 자기 전용 +Handler들과 같이 — `module-lifecycle-plan.md`가 이미 확정해둔 패턴 +그대로, 새 예외 아님). `dispatch-core-plan.md`/`tag-plan.md`/ +`attribute-plan.md`/`architecture.md`/`module-lifecycle-plan.md` 전부 +재반영, 뒤집힌 원문은 `archive/ +tag-attribute-load-time-registration-reversed.md`. `doc-check.py` +ERROR 0 유지. -**같은 세션, 사용자 요청으로 전체 자기 감사**: 이 세션이 건드린 25개 -파일을 `git diff`로 처음부터 재정독 — 실제 문제 3건 발견·수정 -(`HUMAN_TODO.md`가 0-W 해소 이후에도 "남은 건 0-W"로 방치돼 있던 것, -`ref-plan.md`의 편집 중 생긴 어색한 줄바꿈, Tag/Attribute 등록 모델 -재정리 중 `tag-plan.md`/`attribute-plan.md`에서 실수로 빠뜨린 "백엔드가 -알고리즘 전체를 교체하고 싶으면 그냥 더 높은 우선순위로 등록" 문장 -복원). 나머지는 문제 없음 확인, `doc-check.py` ERROR 0 유지. +**같은 세션 후속 — 같은 실수의 다른 잔존 여부 전수 확인.** "모듈 +로드 시점에 스스로 등록"류 주장을 코퍼스 전체 grep — 두 매치는 +오탐(정적 lookup 테이블, React DevTools 비교 서술), 일반 Handler +등록 절(`dispatch-core-plan.md` 492~516줄)은 이미 정확했음. 진짜 +갭은 `ROADMAP.md` M10 체크리스트 — 새로 분리된 +`TagFallbackHandler`/`AttributeKeyFallbackHandler`/ +`AttributeGroupFallbackHandler` 파일 자체가 체크리스트에 없어서 +구현자가 만들 필요를 몰랐을 상태였음, 세 항목 추가 + 배너 정정 + +`architecture.md` 파일 트리 설명 보강. -**같은 세션, `/code-review high` 백그라운드 실행이 추가로 3건 발견**: -`git diff` 기반 자기 감사가 "이 세션이 건드린 파일"만 훑어서, "이 -세션이 안 건드렸지만 `canBound` 재도입 때문에 stale해진 파일"을 -놓쳤음 — `audit/gcconn-trick-verification.md` 상단 배너(고친 섹션 -바로 위인데 안 건드림, "canBound 폐기" 서술이 몇 줄 아래 새 서술과 -모순), `.claude/README.md` 인덱스 행 4곳(이 세션 동안 한 번도 안 열어본 -파일이라 5차 세션 서술 그대로 잔존), `luau-test/STATUS.md`의 스파이크 -`10` 재작성 가이드(가장 심각 — "이중 바인딩 게이트는 canExecute로 쓸 -것"이라고 옛 모델을 재작성 지침으로 명시 중이었음), `question.md` -"용어 정리" 절. 전부 정정. **교훈**: 이름 하나가 재도입되면 그 이름을 -문자열로 전체 코퍼스에 grep해야지, "내가 고친 파일들"만 재확인하는 -건 안 충분함. +**같은 세션 후속 — 두 번째 `/code-review high`가 3건 더 발견.** +`tag-plan.md:155`의 `TagHandler.priority = HANDLER_PRIORITY_FALLBACK` +pseudocode가 프로즈 정정과 모순됐던 것(가장 심각 — 실제 코드로 +복붙될 블록), `dispatch-core-plan.md:612`의 opt-in 예시가 여전히 +"`TagHandler` 자신(FALLBACK)"이라 서술하던 것 — 둘 다 정정 + +`TagFallbackHandler` 래퍼 pseudocode 신설. 별개로 `ref-plan.md:257`의 +`RefLeafHandler.isHandlable`이 `and not isPostRef(v)`를 빠뜨린 +**사전 존재 버그**(PostRef 도입 9차 세션 때 안 갱신됨, 이번 세션과 +무관)도 같이 잡혀 정정, `architecture.md`의 `Leaf.luau` 파일 트리에 +빠져있던 `Effect` 타입도 보강. `doc-check.py` ERROR 0 유지. diff --git a/HUMAN_TODO.md b/HUMAN_TODO.md index 664ec96..65449d1 100644 --- a/HUMAN_TODO.md +++ b/HUMAN_TODO.md @@ -149,7 +149,8 @@ git branch -D worktree-debounce-throttle-plan 디자인 결정 중 Lua/Roblox 엔진에 대한 깊은 경험이 필요한 것들은 합리적 기본값으로 진행하면서 `.claude/question.md`에 모아두는 중. 깨어있을 때 훑어보고 기본값이 마음에 안 드는 것만 답해주면 됨 — **[2026-08-14 열한 번째 세션 기준] -`question.md`의 "결정 대기" 절 자체가 비어 있음**(마지막 남았던 0-W +`question.md`엔 이제 "결정 대기" 절 자체가 없음**(비어서 헤딩째로 삭제 — +마지막 남았던 0-W `Ref` 이중 배치도 이 세션에 해소 — `base/ref-plan.md` "이중 배치 방지" 절, `archive/question-resolved.md`로 이전됨). diff --git a/ROADMAP.md b/ROADMAP.md index 347953e..7c539bc 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -694,6 +694,17 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 > 재배치**됐고(엔진 op `addTag`/`removeTag`/`setAttribute`만 주입), > 이름 소유권은 그룹 전용 키 + `AttributeKeyHandler`의 이름 claim이 > 판정함. 정본은 `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` 기반 자동 판별) @@ -718,8 +729,16 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 `setAttribute(inst,name,v)`를 `v`가 뭐든 무조건 호출 + **이름 claim**(`nameClaims` Relate, 다른 키 객체가 같은 이름에 들어오면 즉시 error, 반환 클로저는 자기 claim만 반납하고 엔진 부작용 없음). - `HANDLER_PRIORITY_FALLBACK`으로 등록. `question.md` 0-Z 결정 — + 알고리즘 구현일 뿐 스스로 등록되진 않음(아래 + `AttributeKeyFallbackHandler` 항목 참고). `question.md` 0-Z 결정 — `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, store2, ...)`/`Merged`/`:NameMap`, `Tag`와 동형 array-part 값 객체, `base/attribute-plan.md`) @@ -729,7 +748,14 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 등록한 키 전부에 `Dispatch.retractFrom(inst,key,1)`. **`process` 안에서 `retractFrom`을 먼저 부르면 안 됨**(철거는 전적으로 클로저 몫). 실제 `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(...)`/ `:Added`/`:Removed`/`:Contains`/`:Apply`/`Merged`/`:Names`, `base/tag-plan.md` — 2026-08-08 세 번째 세션 array-part 값 객체로 @@ -740,9 +766,13 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 때만 실제 `removeTag`, 그마저도 클로저가 받은 새 값이 그 이름을 `Contains`하면 skip해 깜빡임 방지. 제거할 이름은 모아서 **한 번에** `removeTag(inst, names)`. `process` 쪽 별도 diff 없음, `kTagMap`도 - 불필요(클로저가 `v`를 직접 캡처). `HANDLER_PRIORITY_FALLBACK`으로 - 등록 — 2026-08-12 열한 번째 / 2026-08-13 네·다섯·열네 번째 세션, - `base/tag-plan.md`) + 불필요(클로저가 `v`를 직접 캡처) — 2026-08-12 열한 번째 / + 2026-08-13 네·다섯·열네 번째 세션, `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/ InstanceShorthand.luau` — UI 편의 숏핸드 `UICorner`/`UIPadding` (+`UIPaddingOffset`)/`UIScale`(`base/ui-shorthand-plan.md`). 이