diff --git a/.claude/README.md b/.claude/README.md index 3526028..87bc2d9 100644 --- a/.claude/README.md +++ b/.claude/README.md @@ -14,7 +14,7 @@ | `qa-request/` | 구현 완료(코드/에이전트 검증까지 끝남) + 사용자 본인의 실기기(Roblox Studio) QA만 남음 — 지금은 구현 자체가 시작 전이라 비어있음 | | `archive/` | 완료 + 사용자가 실사용/실기기로 직접 검증까지 마침 — 지금은 비어있음 | | `feedback/` | 실사용 피드백을 정리한 긴 로그 — 지금은 비어있음(구현 시작 전) | -| `initreq/` | 프로젝트 착수 시 클론해둔 참고 레포(quad v1, fusion, vide, rbvm, tbox, code-docker) + 원본 요청(`req.md`, `raw-userinput.md`) + `quad2-try`(이전에 시도했다 폐기한 v2 재작성 시도 — 리서치 완료, 결론은 `research/bind-system-plan.md`) — 읽기 전용 리서치 소스, 여기 내용을 옮기지 말고 항상 원본 그대로 유지 | +| `initreq/` | 프로젝트 착수 시 클론해둔 참고 레포(quad v1, fusion, vide, rbvm, tbox, code-docker) + 원본 요청(`req.md`, `raw-userinput.md`) + `quad2-try`(이전에 시도했다 폐기한 v2 재작성 시도 — 리서치 완료, 결론은 `base/bind-system-plan.md`) — 읽기 전용 리서치 소스, 여기 내용을 옮기지 말고 항상 원본 그대로 유지 | `research/`의 문서가 설계 확정되면 `base/`로 승격(또는 구현 착수 시 `qa-request/`행). 지금은 구현 라운드 전(설계 단계)이라 전부 `base/`/`research/`에만 @@ -28,15 +28,15 @@ | `quad-v1-architecture.md` | v1(`initreq/quad`) 내부 동작 스냅샷 — "이 문제를 안 반복하려면"의 기준선 | | `comparison-fusion-vide.md` | Fusion/Vide 아키텍처 비교 리서치 — 설계 결정 근거 자료 | | `lifecycle-pattern.md` | rbvm의 `Connected`+GC 관용구를 quad-v2가 채택하는 방식 | -| `store-semantics.md` | Store는 부작용 허용이 기본. State는 Store 위의 조합 가능한 캐시 레이어로 실제로 필요함(2026-08-04 정정) — Store/State/Source 온톨로지는 `research/bind-system-plan.md`에서 진행 중 | +| `store-semantics.md` | Store는 부작용 허용이 기본. State는 Store 위의 조합 가능한 캐시 레이어로 실제로 필요함(2026-08-04 정정) — 온톨로지 핵심 메커니즘은 2026-08-04 2차 라운드에서 확정, 최신 상세는 `base/bind-system-plan.md` | +| `bind-system-plan.md` | pluggable key/value 핸들러 레지스트리 — `process`/`retract` 디스패치 모델, Ref, Store/State/Source 온톨로지 + 인체공학 질문 전부 확정. 디스패치 엔진은 `quad-base`가 인터페이스로 소유(2026-08-04 5차 라운드) | — | +| `module-lifecycle-plan.md` | 프로바이더 패턴, bind/store 구현 책임 분리 — 확정 | — | +| `slot-plan.md` | 뮤터블 자식 배열, 엄격한 단일 마운트 소유권, 재마운트 시 throw, base/roblox 패키지 경계까지 확정 | — | ## `research/` — 아직 착수 전, 상의 필요 | 문서 | 내용 | 우선순위 | |---|---|---| -| `bind-system-plan.md` | pluggable key/value 핸들러 레지스트리 — `process`/`retract` 디스패치 모델(확정), Ref(확정), quad2-try 리서치 결과. **Store/State/Source 온톨로지는 2026-08-04에 새로 열린 미해결 설계 스레드**(다음 세션 최우선) | 최상 — 다른 모든 설계가 이 위에서 조립됨 | -| `module-lifecycle-plan.md` | 프로바이더 패턴, bind/store 구현 책임 분리 — 확정됨 | 최상 — 확정, 구현 착수 시 API 세부만 조정 | -| `slot-plan.md` | 뮤터블 자식 배열, 엄격한 단일 마운트 소유권, 재마운트 시 throw | 상 — bind-system 확정 후 | | `tween-plan.md` | 트윈을 Store 밖 특수 bind key로 처리, 기본 오버라이드는 Cancel | 중 — 세부 옵션만 남음 | | `purity-and-effects-plan.md` | 컴포넌트 "순수성"이 아니라 "이식성" 문제로 재정의 — 문서 경고 수준으로 확정 | 하 — 문서화 성격, 급하지 않음 | | `existing-instance-bind-plan.md` | 이미 생성된 인스턴스 재바인드 — 착수 안 하되 "미지원" 확정도 안 함, 열린 가능성 유지 | 하 — v2 초기 스코프 제외 | diff --git a/.claude/base/architecture.md b/.claude/base/architecture.md index a244563..56f2eb4 100644 --- a/.claude/base/architecture.md +++ b/.claude/base/architecture.md @@ -35,7 +35,7 @@ quad는 이제 "스크립트"가 아니라 **라이브러리**다. DOMless Roblo 개념을 추가하면 라이브러리 복잡도가 너무 올라간다고 판단 — 당장은 TagService 그대로 사용. **대신 Ref가 도입됨** — 단 Ref의 용도는 "id로 조회"가 아니라 "외부에서 이미 관리되고 있는 instance를 quad로 점진적으로 마이그레이션/ - 래핑하기 위해 직접 참조를 얻는 것"(`research/bind-system-plan.md`의 Ref 절 + 래핑하기 위해 직접 참조를 얻는 것"(`base/bind-system-plan.md`의 Ref 절 참고) — 둘을 혼동하지 말 것. 6. **함수지향 디폴트, `:` 체이닝은 예외적으로만.** 스토어 바인드처럼 체인이 정말 편한 경우만 `:` 사용, 나머지는 외부 함수가 인스턴스를 인자로 받는 모양. @@ -44,7 +44,7 @@ quad는 이제 "스크립트"가 아니라 **라이브러리**다. DOMless Roblo store 바인드를 받을 수도 있음. 8. **특수 이벤트는 특수 플러깅으로.** `PropertyChangedSignal`, `PropertyChangedEvent ""` 같은 것들은 일반 이벤트 바인드가 아니라 pluggable 바인드 핸들러 중 하나로 - 구현(`research/bind-system-plan.md`). + 구현(`base/bind-system-plan.md`). 9. **Tracker 미구현.** v1의 소스 변경 감지 자동 재렌더 기능(hot-reload watcher, 실제로는 `.claude/initreq/quad/src/tracker.lua` — v1에서도 이미 `exports.lua`에 연결 안 된 죽은 코드였음, `base/quad-v1-architecture.md` 참고)은 렌더 @@ -70,16 +70,88 @@ quad는 이제 "스크립트"가 아니라 **라이브러리**다. DOMless Roblo `InitRoblox(Module)` 같은 팩토리 함수가 생성된 모듈을 뮤테이션하는 도구를 주는 방식. +## 구현 착수: 소스 트리 구조 확정 (2026-08-04, 5차 라운드) + +**상태**: 소스 트리 레이아웃과 `quad-base`/`quad-roblox` 패키지 경계 확정 — +아래가 다음 세션에서 실제로 만들 구조. 지금은 문서 확정까지만, 실제 +폴더/`wally.toml`/`project.json` 스캐폴딩은 다음 세션. + +**패키징 방식(모노레포, RbxUtil 선례 채택)**: 최종적으로는 여러 개의 독립 +wally 패키지로 나누고 싶지만, 지금 Luau 툴링(특히 wally로 설치된 패키지의 +타입 정보 단절·`luau-lsp`의 심볼릭 링크 해석 문제 — 최근 `luau-lsp 1.63.0` +에서야 수정됨)이 아직 불안정해서 **당장은 모놀리식**으로 감. `Sleitnick/ +RbxUtil`이 정확히 이 패턴(루트 하나로 통합 개발/테스트, 서브폴더마다 자체 +`wally.toml`로 독립 퍼블리시)을 쓰는 선례라 그대로 채택. `.luaurc`의 +`aliases`는 **런타임 require에서 아직 엔진이 지원 안 함**(Roblox 스태프가 +지원 예정이라고만 밝힌 상태, 2026-01 기준) — 그래서 alias는 편집기 +자동완성/타입체크용으로만 곁들이고, 실제 크로스패키지 require는 상대경로로 +쓴다. 나중에 실제로 레포를 쪼갤 때는 Rojo `project.json`의 트리 매핑 규칙만 +유지하면 되고, require는 그 시점에 한 번 기계적으로 바꾸는 정도로 감수. + +**패키지 경계**: `quad-base`는 다른 렌더 백엔드(GTK 등, 항목 12 참고)에서도 +재사용 가능해야 한다는 전제 — Store/State/Source 온톨로지+전파뿐 아니라 +**pluggable 디스패치 엔진 자체도 "인터페이스"로 base가 소유**한다(엔진마다 +큰 구현을 중복하지 않기 위함 — rbvm이 relation을 하나로 통합하려 했던 것과 +같은 동기). `quad-roblox`는 그 인터페이스의 **실제 구현체**만 제공. + +``` +quad/ +├── .luaurc # @quad-base, @quad-roblox alias (편집기 경험용, 런타임 비의존) +├── default.project.json # 루트 통합 개발/테스트용 Rojo 프로젝트 +├── quad-base/ +│ ├── wally.toml +│ └── src/ +│ ├── Source.luau # 값의 근원, 단일 지점 +│ ├── State.luau # 캐시만 하는 non-owning 핸들, state(state) 분기 +│ ├── Store.luau # source 집합체, dot-access, __newindex +│ ├── Dispatch/ +│ │ ├── init.luau # process/retract 엔진, isHandlable 우선순위 스캔 +│ │ ├── Handler.luau # 핸들러 계약 타입(isHandlable/priority/process/retract) +│ │ ├── StoreBind.luau # store 값 재귀 재실행 로직(범용, 엔진 무관) +│ │ └── Slot.luau # add/remove/clear 재조정 로직(추상 자식 참조 기준) +│ ├── LifetimeHandle.luau # Connected 계산 속성 "인터페이스"(타입/계약만) +│ ├── PerInstanceState.luau # per-instance 상태 저장 "인터페이스" +│ ├── Ref.luau # CreatedRef 메커니즘(숫자 슬롯 참가자) +│ └── init.luau +└── quad-roblox/ + ├── wally.toml + └── src/ + ├── RobloxFactory.luau # BaseModule 뮤테이션, 재호출 가드(같은 팩토리=무시/다른=에러) + ├── LifetimeHandle.luau # 실제 구현(Instance 생존 확인) + ├── PerInstanceState.luau # 실제 구현(weak-keyed table, Instance 키) + ├── Handlers/ + │ ├── Property.luau + │ ├── Event.luau # ReflectionService 기반 자동 판별 + │ ├── Attribute.luau + │ ├── Tag.luau # CollectionService + │ ├── Tween.luau # 높은 우선순위 store-bind 핸들러 + │ ├── Slot.luau # base Slot 재조정 로직의 실제 적용/해제(Instance Parent 조작) + │ └── InstanceChild.luau # k:number, v:Instance — 중첩 인스턴스 자식(예: Frame { Frame {} }) + ├── DI/ + │ └── init.luau # 제네릭 생성자 + ~25개 정적 필드(UIInstances) + └── init.luau +``` + +**남은 것**: Slot 코어 로직의 정확한 API(`research`→`base` 승격된 +`slot-plan.md` 참고)와 각 파일의 정확한 함수/타입 이름은 구현 단계에서. +Tween/purity/existing-instance-bind는 여전히 `research/`에 남아있고 이 +구조 확정을 막지 않음. + ## 아직 미정 (research/로 분리됨) -바인드 시스템 디스패치, Slot 설계 세부, Tween 플러깅, 모듈 라이프사이클/누가 -Store를 구현하는가, 순수함수 범위, 이미 생성된 인스턴스에 대한 바인드 — -`.claude/research/` 각 문서 참고, 전체 색인은 `.claude/README.md`. +Tween 플러깅, 순수함수 범위, 이미 생성된 인스턴스에 대한 바인드 — +`.claude/research/` 각 문서 참고, 전체 색인은 `.claude/README.md`. 바인드 +디스패치/Slot/모듈 라이프사이클은 위 "구현 착수" 섹션대로 확정되어 +`.claude/base/`로 승격됨(`bind-system-plan.md`/`module-lifecycle-plan.md`/ +`slot-plan.md`). -**가장 시급한 미정 사항(2026-08-04부터, 다음 세션 최우선)**: Store/State/ -Source 온톨로지 — Store는 source(실제 값이 존재하는 단일 지점) 집합체이고, -`store "key"`처럼 접근할 때마다 그 source를 감싸는 새 State(자기 고유 -value 없는 조합 가능한 캐시)를 반환한다는 모델까지는 나왔으나, `:Compute` -캐싱/무효화 전략, Luau 타입 시스템에서 커링 호출의 타입 추론 문제 등 세부는 -전부 열려있음. `research/bind-system-plan.md`의 "Store/State/Source -온톨로지" 절, `.claude/question.md`의 "최우선 새 열린 질문" 절 참고. +**Store/State/Source 온톨로지(2026-08-04 두 라운드에 걸쳐 확정)**: Store는 +source(실제 값이 존재하는 단일 지점) 집합체이고, `store.key`로 접근할 때마다 +그 source를 감싸는 새 State(자기 고유 value 없는 조합 가능한 캐시)를 +반환한다. 전파는 push-invalidate(신호만)/pull-recompute(`Get()` 시점) — +Fusion식 eager 노드 없이도 다이아몬드 의존성 중복 재계산 문제가 풀림. State는 +쓰기 대상이 아니고(값 쓰기는 항상 Store의 `__newindex`), 값 하나만 다룰 땐 +Store와 별개인 가벼운 `Source` 프리미티브를 씀. 남은 건 정확한 API 이름과 +"`store.key` dot-access를 타입 추론 1급 경로로 삼는다"는 제안의 정식 확인 +뿐 — `base/bind-system-plan.md`의 "Store/State/Source 온톨로지" 절, +`.claude/question.md` 참고. diff --git a/.claude/research/bind-system-plan.md b/.claude/base/bind-system-plan.md similarity index 55% rename from .claude/research/bind-system-plan.md rename to .claude/base/bind-system-plan.md index d6744cf..0235c8b 100644 --- a/.claude/research/bind-system-plan.md +++ b/.claude/base/bind-system-plan.md @@ -1,9 +1,12 @@ -# Bind 시스템 — pluggable key/value 핸들러 (핵심 모델 확정, 세부 사항만 남음) +# Bind 시스템 — pluggable key/value 핸들러 (base로 승격됨) -**상태**: research — 핵심 디스패치 모델(`process`/`retract`, 핸들러 4종 계약, -Signal 미채택, Ref 역할)은 사용자 확인 완료로 사실상 확정. 남은 건 세부 -시그니처(dependency array API, `CreatedRef` 모양) 뿐 — 이것들이 정리되면 -`base/`로 승격 예정. 원본: `.claude/initreq/raw-userinput.md` +**상태**: base — 핵심 디스패치 모델(`process`/`retract`, 핸들러 4종 계약, +Signal 미채택, Ref 역할)과 소스 트리 상 패키지 경계(디스패치 엔진은 +`quad-base`가 인터페이스로 소유, `quad-roblox`는 실제 구현만)까지 전부 +2026-08-04 세션에서 확정되어 `research/`에서 승격됨(`base/architecture.md`의 +"구현 착수: 소스 트리 구조 확정" 절 참고). 남은 건 세부 시그니처(dependency +array API, `CreatedRef` 모양) 뿐 — 구현 단계에서 자연히 정리됨. 원본: +`.claude/initreq/raw-userinput.md` "key와 value에 대한 바인드 연산은 pluggable 하도록 구성하기" / "스토어는 스토어를 저장 가능한가" / "Ref는 고민중" 절. v1의 문제점은 `base/quad-v1-architecture.md` ("ProcessQuadProperty" 하드코딩 디스패처), 참고 패턴은 `.claude/initreq/tbox` @@ -111,7 +114,7 @@ src/schema/union.luau:48-68`) — 에러 메시지는 즉시 문자열로 만들 Slot이 store 바인드로 넘어오는 경우, pluggable 처리기에 `retract` 핸들러가 필요하다는 점(부모가 slot을 정리하고 다시 process하는 방식)도 이 래핑 방식과 -자연스럽게 맞음 — `research/slot-plan.md` 참고. +자연스럽게 맞음 — `base/slot-plan.md` 참고. ## Store가 Store를 저장 가능한가 @@ -188,51 +191,138 @@ read ...`처럼, State끼리 자유롭게 합성/파이핑 가능한 것이 최 같은 축의 문제 — 옵션 2가 그 원칙과 더 잘 맞아 보이지만, 실현 가능성 자체가 아직 검증 안 됨. -## Store/State/Source 온톨로지 — 진행 중인 설계 스레드 (2026-08-04 검증 라운드에서 새로 열림) +## Store/State/Source 온톨로지 — 핵심 메커니즘 확정 (2026-08-04 2차 라운드) -**이 절은 아직 결론난 설계가 아니다** — 검증 라운드 중 사용자가 실시간으로 -설계를 전개하며 나온 내용을 그대로 기록. 다음 세션에서 이어서 다룰 것. -`base/store-semantics.md`의 "State 프리미티브는 실제로 필요하다" 정정과 -직결됨. +**상태**: 전파 모델/`:Compute` 인자 규칙/State 쓰기 금지/Slot 생존 확인/타입 +추론(dot-access) 전부 `AskUserQuestion`으로 확인 완료. 남은 건 정확한 함수/ +생성자 이름뿐(구현 단계). `base/store-semantics.md`의 "State 프리미티브는 +실제로 필요하다" 정정에서 이어짐. -**핵심 온톨로지**: +**핵심 온톨로지** (변경 없음): - **Source** — 실제 값이 존재하고 변경될 수 있는 단일 지점(v1의 "값의 근원"). -- **Store** — source들의 집합체. `store.a`/`store "a"`처럼 키로 접근하면 - 그 source를 감싼 **새 State**를 매번 만들어 반환(state가 store에 캐시되어 - 재사용되는 게 아님 — source만 store에 귀속된 유일한 실체). +- **Store** — source들의 집합체. `store.a`처럼 키로 접근하면 그 source를 + 감싼 **새 State**를 매번 만들어 반환(state가 store에 캐시되어 재사용되는 + 게 아님 — source만 store에 귀속된 유일한 실체). - **State** — source(또는 다른 state)의 결과를 캐싱만 하는 존재, 자기 고유의 독립적 value 개념이 없음. `state(state)`로 기존 state의 결과를 받아 새 state를 만들어 분기 가능 — 이게 사실상 Unix 파이프 영감의 "State끼리 합성 가능"이라는 원래 목표를 구현하는 방식. -- `:With(...)`/`:Compute(fn)`은 실제로는 state 위의 연산 — - `store "key1":With(store "key2"):Compute(function(key1) return key1 + - store.key2.value end)`처럼, with한 값을 fn이 클로저로 직접 읽는 모양. -**미해결 세부 사항**: -- **`:Compute` 캐싱/무효화 전략** — 매번 새로 계산할지, 캐싱해두고 무효화 - 플래그로 관리할지 미정. 후보: 값이 바뀌었는데 듣는 소비자가 없으면 연산은 - 미루고 `invalid=true`만 세워두고, 필요해질 때(듣는 사람이 생기거나 값을 - 읽을 때) 실제로 연산하고 `true`→캐싱 후 `false`로 되돌리는 dirty-flag 방식. - "store가 state를 **만드느냐** 아니면 **저장하느냐**"의 문제와 직결 — - 사용자 판단은 "만든다" 쪽(저장한다고 하면 한 곳에서 `:Compute`를 붙이면 - 다른 소비자도 전부 그 compute된 값을 읽게 되어버리는 오염 문제 발생). -- **`emit` 필요 여부** — store가 값 변경 시 관련된 모든 state에 emit해야 - 하는 구조가 맞는지 확신은 없지만, 그 외의 방법이 안 보인다는 게 사용자 - 현재 판단. `state(from) / state() -> (state, setState)` 같은 팩토리 - 모양도 후보로 언급됨(React의 `useState`류 페어 반환과 유사). -- **Luau 타입 시스템 제약** — `store "key"` 같은 커링 호출로 `state`의 - `T`를 정확히 추론하려면 오버로드 함수 타입(`(("a") -> number) | (("b") - -> boolean)`)이 필요한데, Luau는 문자열 리터럴 인자를 자동으로 `as const` - 취급하지 않아서 타입이 좁혀지지 않는 문제가 있음. `store.states.a`처럼 - 필드 접근으로 우회하거나, `store.a`가 바로 state를 반환하고 - `state.value = x`로 설정 가능하게 하는 대안도 검토됐으나, 후자는 "다른 - source로부터 파생된 state에 value를 직접 설정하면 안 된다"는 문제와 - 충돌(store가 실제 값을 담는 유일한 주체여야 함). `state`(compute - 결과)가 제대로 바인딩 안 됐을 때 생기는 타입 문제는 일단 UB로 두기로 함. -- **`Pipe`(quad2-try 후보)는 사실상 폐기 쪽으로 기움** — 별도 `Pipe` 타입에 - 소유권/버전 가드를 넣어 재설계하는 대신, State 자체를 파이핑 결합체로 - 보고 `state(state)`로 분기하는 쪽이 엔지니어링상 더 쉬워 보인다는 게 - 사용자의 최신 판단(2026-08-03 라운드의 "Pipe COW가 유력 후보"보다 우선함). +**전파 모델 확정: push-invalidate(신호만) / pull-recompute(`Get()` 시점에만) — +Fusion식 eager 노드·생성순 정렬은 안 만듦** + +- `Source`는 값이 바뀌면 구독 중인 State들에게 **"무효화됐다"는 신호만 + 쏜다** — 새 값 자체는 신호에 안 실림("state는 세터를 내보내기보다 + 업데이트 됐다는 신호만 쏜다" — 사용자 확정 문구). +- 신호를 받은 State는 자기 `invalid` 플래그만 세우고, 이미 `invalid`였다면 + 그 아래로 더 전파하지 않는다 — 다이아몬드 의존성에서 중복 워크를 막는 + 장치(Vide가 저자 스스로 `todo.md`에 미해결로 남긴 문제의 해결책). +- 실제 재계산은 `Get()`(또는 `.value` 인덱싱)이 호출되는 시점에만 일어남 — + "필요할 때 계산" 원칙(사용자 확정). Fusion의 `timeliness="eager"` 노드/ + 생성순 정렬 장치는 만들지 않음 — quad엔 그런 다단계 즉시 재계산이 필요한 + 소비자가 없다는 판단. 유일하게 "즉시 반응해야 하는" 소비자는 store-bind + pluggable 핸들러(위 "확정된 디스패치 모델" 절)인데, 이건 무효화 신호를 + 받는 즉시 자기가 알아서 `Get()`을 호출해 pull하는 방식으로 충분함 — + State 스스로 "지금 나를 보는 eager 소비자가 있나" 같은 부기가 전혀 + 필요 없음. +- `emit`은 이 무효화 신호 하나로 좁혀짐 — 값을 안 실어보내므로 저렴함 + ("emit 필요 여부" 열린 질문은 이걸로 해소). + +**`:With`/`:Compute` — self 인자도 lazy 핸들로 통일** + +- 최초안(self 값은 포지셔널 raw 값, with한 값만 클로저로 읽음)에는 실제 + 단점이 있었음 — self가 raw 값이면 `fn` 호출 전에 항상 self를 먼저 + `Get()`해야 하므로, `fn` 내부 로직이 with한 다른 값을 보고 "이 경우엔 self + 계산 자체가 필요 없다"고 판단해도 이미 늦음(예: `:With(noprint)`이고 + `noprint.value == true`면 앞단 계산을 통째로 생략하고 싶은 경우). +- **해결(사용자 확정)**: self도 raw 값이 아니라 **State 핸들 그 자체**를 + `fn`의 포지셔널 인자로 넘긴다 — `fn(self: State)`, 내부에서 + `self.value`(또는 `self:Get()`)를 실제로 읽을 때만 계산이 트리거됨. + with한 값과 동일한 lazy 원칙을 self에도 그대로 적용 — 별도 + `ComputeWithout` 변형은 불필요, `Compute` 하나로 일관. +- `.value`는 `Get()`을 감싼 읽기 전용 계산 속성(`base/lifecycle-pattern.md`의 + `Connected`와 동일한 "저장되는 필드가 아니라 계산된 속성" 패턴 재사용) — + `:Get()`과 `.value` 둘 다 지원, `.value`가 관용적 표기. +- 예시 갱신: `store "key1":With(store "key2"):Compute(function(key1) return + key1.value + store.key2.value end)` — `key1`은 이제 raw 숫자가 아니라 + State. + +**State는 쓰기 대상이 아님 — 확정, Source는 독립 공개 프리미티브로 격상** + +- `.value`는 항상 읽기 전용. 값을 쓰는 경로는 오직 Store의 `__newindex` + (`store.key = value`, 이미 확정된 문법)뿐 — State에는 대응하는 쓰기 API가 + 아예 없음. "State에 `.value = x`를 허용하면 다른 source에서 파생된 + state에 직접 쓰기가 가능해져 버린다"는 이전 우려는 이걸로 근본적으로 + 해소(그런 API 자체가 없음). +- **`Source`는 Store의 내부 구현 디테일이 아니라 별도의 가벼운 공개 + 프리미티브로 노출** — Store는 다수의 source를 등록/관리하는 무거운 + 구조라, 값 하나만 반응형으로 다루고 싶을 때 Store를 통째로 만드는 건 + 비효율이라는 게 사용자 판단("store가 source 수십 개 만드는건 비효율이니 + 둘이 다른 구현이라 봐도 될듯"). `Source(initial)` 류의 독립 생성자 + (정확한 이름은 구현 단계에서 확정)가 Store와 나란히 존재. + +**Slot 생존 확인 — 별도 메커니즘 아님, `canExecute` 재사용으로 확정** + +- `base/store-semantics.md`에 있던 "`isInit=false`면 허용, `isInit=true`+ + 생존확인 거짓이면 불허" 분기 초안은 폐기. state-invalidate 리스너 + 클로저도 `base/lifecycle-pattern.md`의 "생명 바인드 유틸"(canExecute + predicate)로 등록하면, 발화 시 `canExecute()` 하나만 확인하고 거짓이면 + 그냥 no-op — `isInit` 분기라는 별도 개념 자체가 불필요(사용자 확정: + "canExecute 하나로 통일"). + +**타입 추론 문제 — 확정(2026-08-04 3차 라운드)** + +- `store "key"`(문자열 커링)로 `state`를 오버로드 함수 타입으로 정확히 + 추론하려는 시도는 포기하고, **`store.key`(dot-access)를 1급 경로로 확정** + — Store 타입을 `{key: State, other: State}`류 평범한 + 레코드 타입으로 지으면 일반 구조적 필드 타이핑으로 자동 해결되고, 문자열 + 리터럴 narrowing 문제 자체가 안 생김. `store "key"` 문자열 커링은 동적 + 키가 필요할 때 쓰는 미타입(`State`) 폴백으로 격하. +- 이 패턴은 Store에만 국한되지 않고 **인스턴스 생성(`DI.Frame`)/이벤트 + (`On.EventName`)까지 관통하는 프로젝트 전역 관습으로 확정**됨 — 아래 + "인스턴스 생성 / 이벤트 네이밍 인체공학" 절 참고. + +**`Pipe`(quad2-try 후보)는 폐기 확정** — 별도 `Pipe` 타입에 소유권/버전 +가드를 넣어 재설계하는 대신, State 자체가 파이핑 결합체이고 +`state(state)`로 분기하는 위 모델로 완전히 대체됨. + +**PA님 코드와의 교차검증(2026-08-04 4차 라운드) — 둘 다 기존 확정 유지** + +`.claude/initreq/artworks/EventDrivenProgramming/`(Connection/Event/ +Observable/Observer)을 조사한 결과, 두 지점에서 기존 확정과 실제로 다른 +선택이 나와 재검토했으나 결론은 변경 없음: + +- **전파 모델**: PA님의 pub-sub은 push-invalidate가 아니라 **push-값** + (`Event:fire(...)`가 인자를 그대로 콜백에 전달, `Observable`의 `__newindex`가 + 새 값을 실어 즉시 `changed:fire(key, value)`, dirty-flag/`Get()` pull 단계 + 자체가 없음). 한때 "leaf(source 하나→sink 하나, 파생 없음)는 PA님처럼 + push-값으로 단순화하고 push-invalidate/pull-recompute는 실제 `:Compute` + 파생이 있을 때만 쓰자"는 이원화를 검토했으나 **기각** — invalidate+`Get()` + 방식도 leaf에서 딱히 더 복잡하지 않고(불리언 플래그 하나 + `Get()`/`emit` + 둘로 나뉘는 정도), 오히려 두 메커니즘을 병행하면 "leaf State가 나중에 + `:Compute`로 감싸일 때 두 메커니즘을 어떻게 연결하는가"라는 새 경계 문제가 + 생겨 이원화가 더 복잡함. **결정적으로, PA님 코드엔 애초에 `:Compute`/`:With` + 같은 파생·합성 개념 자체가 없음** — quad-v2가 lazy pull을 도입한 이유(여러 + 소비자가 하나의 파생 State를 공유할 때 오염 방지, 안 쓰이는 연산 스킵)를 + PA님 시스템은 처음부터 안 풀려던 문제라, 대등한 반례가 아니었음. **결론: + push-invalidate/pull-recompute로 통일 유지, 변경 없음.** 사용자 최종 확인 + 문구: "store 전파 처리는 우리 방식이 맞음. 이건 vide 에서 없었던것과 + 동일함, [PA님] 저기도 디자인 상 해결 못하는 문제가 된거거든. 비 필요 + 연산과 중복 연산을 지우는건 디자인 단계에서 구성할 일임. 우린 디자인 + 단계부터 해당 문제를 해결하고 싶었던거야." +- **라이프사이클**: PA님 코드는 GC-native가 아니라 **전부 수동 해제** + (`Connection.connected`는 계산 속성이 아니라 저장된 bool, `Observer`의 + 8개 `subscribeXxx` 헬퍼 전부 명시적 `:unsubscribe()` 필요, weak table은 + `Observable`의 subject↔observable 캐시 한 곳뿐). rbvm 기반으로 확정한 + "GC 위임, 명시적 dispose 없음" 원칙과 반대 선택이라 재확인 질문했으나, + **GC-native 유지로 확정** — 지금까지 이 정도 규모(명시적 dispose가 꼭 + 필요할 만큼 큰 자원)를 요구하는 실제 사례가 없었다는 게 사용자 판단. 다만 + **완전히 막다른 길은 아님**을 기록해둠: rbvm처럼 관계를 양쪽 다 weak-keyed로 + 두고 모든 걸 connection 람다에 담아 "연결이 살아있는 동안만 살아있게" 하는 + 방식이면, 나중에 GC만으로 정말 부족한 케이스가 생겨도 그 connection을 얻어 + `disconnect()`하는 명시적 dispose 경로를 추가로 얹는 게 가능한 디자인 — + 지금 마일스톤에서는 필요 없어서 안 함(사용자: "필요하다면 dispose 핸들러를 + 만들어주는 것도 가능한 디자인, 다만 지금까지 요구가 없었음"). ## quad2-try 리서치 결과 (완료) — 이전 시도에서 뭘 가져오고 뭘 버릴지 @@ -256,7 +346,7 @@ State/스트림)를 다뤘던 이전 시도가 있었음. 조사 결과 요약: - **Slot은 이 시도에서도 사실상 빈 스텁**이었음 — `Insert`의 실제 구현부가 전부 주석 처리되어 있고, `Notify()`도 빈 함수. 심지어 구 v1(`quad-2`)의 `DEV_CHANGELOG.txt`에도 "TODO: slot 기능 구현"이 마지막까지 미완료로 남아있었음 - — **가져올 게 전혀 없음**, `research/slot-plan.md`의 from-scratch 설계를 + — **가져올 게 전혀 없음**, `base/slot-plan.md`의 from-scratch 설계를 그대로 진행하면 됨(재조사 불필요). - 다른 서브패키지(`quad-roblox`/`quad-gtk`/`quad-lang`/`quad-gen`/`quad-compat`/ `quad-debug`/`quad-docs`)는 전부 파일이 0개인 빈 디렉토리 — `quad-core` 밖엔 @@ -315,38 +405,97 @@ copy-on-write 절충안은 2026-08-04 검증 라운드에서 사실상 폐기 RobloxFactory(QuadBase)` 세 줄 정도로 직접 조립하면 됨(별도 번들 `quad` 패키지로 재수출할 필요 없음, 필요하면 만들어도 됨). -**열린 질문**: `RobloxFactory`를 같은 `BaseModule`에 여러 번 호출하면 어떻게 -되어야 하는가 — 이미 초기화됐으면 무시(rbvm의 `InitNamespace`류 가드와 -유사하되, "라이브러리마다 수동 init" 패턴과는 다름)하는 쪽으로 기울어짐. -서로 다른 두 곳에서 같은 `base`를 require해서 `RobloxFactory`와 -`AnotherFactory`(가상의 예)를 각각 실행하는 경우처럼 충돌 가능성이 있는 -시나리오가 향후 모듈 스코핑(`New()`, `base/architecture.md` 13번) 논의를 -다시 촉발할 수 있음 — 지금은 열어만 둠. +**확정(2026-08-04 3차 라운드)**: `RobloxFactory`를 같은 `BaseModule`에 여러 +번 호출했을 때 — **같은 팩토리로 재호출하면 무시(no-op)**, hot-reload처럼 +초기화 스크립트가 다시 도는 경우를 안전하게 만듦. **다른 팩토리 +(`AnotherFactory` 등, 가상의 예)로 재호출하면 에러** — 이건 `base/module-lifecycle-plan.md`의 "bind는 유일 슬롯" 원칙(이미 구현체가 있는데 또 +다른 구현체로 init하려 하면 오류)이 다루던 것과 정확히 같은 케이스, 이 +문서의 이전 "무시" 잠정안과 그 문서의 "오류" 잠정안이 서로 모순되는 게 +아니라 **같은 팩토리 재호출(무시) vs 다른 팩토리로 유일 슬롯 충돌(에러)이라는 +서로 다른 케이스를 각각 가리키고 있었음**. 구현은 모듈 테이블에 "누가 +초기화했는지" 마커(`_initializedBy = "roblox"`류, 정확한 이름은 구현 단계)만 +두면 됨. 모듈 스코핑(`New()`, `base/architecture.md` 13번)과의 관계도 실은 +열려있던 게 아니라 자연히 풀림 — `New()`가 생기면 각 인스턴스가 별도 +테이블이 되므로 이 마커도 테이블별로 독립적으로 스코핑됨, 재설계 불필요. -## 인스턴스 생성 / 이벤트 네이밍 인체공학 (2026-08-04 검증 라운드에서 새로 나온 열린 질문) +## 인스턴스 생성 / 이벤트 네이밍 인체공학 — 확정(2026-08-04 3~4차 라운드, PA님 실 코드로 검증됨) `Quad "Frame"`처럼 문자열로 인스턴스 종류를 지정하는 방식은 타입 추론이 -어려움(위 온톨로지 절의 Luau 오버로드 문제와 같은 원인). PA님 DI 스타일은 -`DI.Frame`/`DI.TextLabel`처럼 필드 접근으로 만들어서 자동완성이 자연스럽게 -됨 — 목록에 없는 타입은 `DI.New<> "Frame"`류로 폴백. 이벤트도 -`Event ""` 대신 `On...`류 이름으로 필드 접근하면 타입/자동완성이 쉬워질 수 -있음. 아직 방향 결정 안 됨 — 다음 세션에서 다룰 것. +어려움(위 온톨로지 절의 Luau 오버로드 문제와 같은 원인). 사용자가 실제 +참고 코드를 `.claude/initreq/artworks/DeclarativeProgramming/ +DeclarativeInstance.luau`(PA님 작성, UI 포함 전반적 설계 패턴을 시범 적용한 +데모 모듈)에 공유해줘서 직접 확인 — **"DI"는 Dependency Injection이 아니라 +"Declarative Instance"(선언형 인스턴스 생성)**. + +**인스턴스 생성 — PA님 코드 그대로 채택**: 처음 제안했던 "필드=1급 타입 +경로, 문자열=폴백"이라는 2트랙(`DI.Frame` vs `DI.New<> "Frame"`) 구상 +보다 실제로는 더 단순했음(`DeclarativeInstance.luau:104-160`) — +**제네릭 생성자 함수 하나(`new(className): from>`)가 알려진 타입과 모르는 타입을 전부 커버**하고, 그중 UI에서 자주 +쓰는 클래스 ~25개(`Frame`/`TextButton`/`UICorner` 등, `UIInstances` 타입 +테이블에 등록된 것들)만 모듈 로드 시점에 **즉시(eager)** `constructor.Frame = +new("Frame")`처럼 필드로 미리 채워둠 — `__index` 메타메소드 지연 생성이 +아니라 그냥 정적 테이블. quad-v2도 이 모양 그대로 채택: 하나의 제네릭 +생성자 + 자주 쓰는 것만 정적으로 미리 바인딩. + +**이벤트 바인딩 — `On.EventName` 도트액세스 안 씀, PA님 방식(평범한 문자열 +키 + 런타임 리플렉션)으로 전환**: `DeclarativeInstance.luau:13-91`의 +`assign(instance, key, value)`가 `ReflectionService:GetPropertiesOfClass`/ +`GetEventsOfClass`로 클래스별 프로퍼티/이벤트 타입을 캐싱해두고, 키가 +`RBXScriptSignal` 타입이면 자동으로 `instance[key]:Connect(value)`로 처리함 +— `Frame { MouseButton1Click = fn }`처럼 별도 네임스페이스 없이 그냥 문자열 +키로 씀. 이건 타입 안전성을 어느 정도 포기하는 대가지만(콜백 시그니처까지 +Luau가 검증 못 함 — `apply(instance: T, properties: U): T & U`가 스키마 +검증 없이 구조적으로만 merge), 이미 UB로 남긴 "테이블 리터럴 안 키별 값 +타입 자동 검증 불가"와 같은 급의 한계라 손해가 크지 않고, `On.` 접두어 없이 +문법이 더 간결해짐 — **사용자 확정**("PA 님 방식 괜찮은듯. 타이핑은 인라인이 +되긴 하겠지 정도면 괜찮다"). quad-v2 구현에서는 이 "키가 이벤트인가" +판별을 `isHandlable`로 감싼 pluggable 핸들러(`quad-roblox`가 `Reflection +Service` 기반으로 구현)로 두면 됨 — 별도 `On` 모듈/필드 접근 구조 자체가 +불필요해짐. + +**Store 쪽 dot-access는 그대로 유지**: `store.key`(1급 타입 경로)/ +`store "key"`(문자열 커링, 동적 키 폴백)는 이벤트와 달리 실질적으로 Luau가 +타입을 좁혀주는 이득이 있어서(Store 자체가 `{key: State, ...}`류 +평범한 레코드 타입으로 지어짐) 그대로 유지 — 이벤트만 예외였을 뿐, "정적으로 +알려진 것=필드 접근" 원칙 자체가 깨진 건 아님. + +**PA님 코드와 대조해서 재확인한 것(변경 없음)**: +- **OOP 회피 결정은 오히려 보강됨** — PA님의 `ObjectOrientedProgramming/ + class.luau`도 `setmetatable(methods, {__index = parent})` 체이닝 상속이라 + quad-v2가 피하기로 한 quad2-try `Base:Extends`와 같은 모양이고, 제네릭을 + 파일마다 중첩해서 재선언해야 하는 보일러플레이트까지 동일하게 나타남. +- **Instance 태그는 CollectionService 직접 사용 그대로 유지** — PA님의 + `EventDrivenProgramming/Observer.luau`의 `subscribeTaggedInstance`도 얇은 + `CollectionService` 래퍼일 뿐. `DataOrientedProgramming/TagService.luau`는 + 이것과 무관하게 plain-table 엔티티(비-Instance 데이터)용 커스텀 태그 + 인덱스라 지금 quad-v2 스코프 밖 — Instance가 아닌 데이터에 태깅이 필요해질 + 미래 시나리오를 위한 참고 자료로만 기록. +- **Store/State 전파 모델, 라이프사이클 — 둘 다 재검토 후 기존 확정 유지** + (아래 "Store/State/Source 온톨로지" 절의 "PA님 코드와의 교차검증" 참고). ## 남은 열린 질문 (`.claude/question.md`에도 취합) -- **`:Compute`가 `with`한 값을 정확히 어떻게 읽는가** — 클로저로 직접 캡처하는 - 방향은 확정(위 온톨로지 절), 캐싱/무효화 전략(dirty-flag 등)과 `emit` 필요 - 여부는 아직 미정. +이 문서의 핵심 설계 질문은 2026-08-04 세 라운드(전파 모델/`:Compute`/State +쓰기 금지/Slot 생존 확인 → dot-access 타입 추론/인스턴스·이벤트 네이밍/ +`RobloxFactory` 재호출 가드)를 거치며 전부 확정됨. 남은 건 순수 API 표면 +이름뿐: + +- **`state()`/`Source()`/`Get()`/`DI`(또는 다른 이름) 등 정확한 함수·생성자· + 모듈 이름** — 방향은 전부 확정, 이름만 구현 단계에서 남음(`On` 모듈은 + 이벤트 바인딩이 PA님 방식으로 바뀌며 아예 불필요해짐 — 위 "인스턴스 생성 / + 이벤트 네이밍" 절 참고). - **`CreatedRef`(가칭)의 정확한 함수/옵션 이름** — children 배열에 아이템으로 넣는다는 방향과 생성/마운트 두 시점 모두 지원한다는 것은 확정, 정확한 API 이름만 남음. - **매 `process()` 호출마다 우선순위 스캔 비용** — 실제 구현/벤치마크 단계에서 확인 필요(디자인 자체는 확정됐으므로 더 이상 사용자 확인 대상 아님, 구현 검증 대상). -- Store가 Store를 담는 경우의 실제 소유권(누가 내부 Store를 destroy하는가) — - 이 문서의 "재실행 래핑" 제안이 맞다면 자연히 바깥 Store bind가 내부 Store의 - 라이프타임도 감싸게 될 텐데, 이중 해제(double-dispose) 방지가 필요한지 확인. - 단, `base/lifecycle-pattern.md`의 "destroy 시점엔 아무것도 안 함" 원칙상 이중 - 해제 자체가 걱정할 필요 없는 개념일 수도 있음 — 재검토 필요. -- `RobloxFactory` 중복 호출/충돌 시나리오, 인스턴스 생성·이벤트 네이밍 - 인체공학 — 위 두 절 참고, 둘 다 새로 열린 질문. + +**해소된 것**: "Store가 Store를 담는 경우 이중 해제(double-dispose) 방지가 +필요한가"는 재검토 결과 질문 자체가 성립 안 함으로 결론 — State/Source +그래프 구독이 전부 weak-keyed GC-native(명시적 `dispose()` 호출이 아예 없음, +`base/lifecycle-pattern.md`의 GC 위임 원칙 재사용)라 "같은 걸 두 번 해제"할 +행위 자체가 존재하지 않음(GC는 멱등). "`:Compute`가 with한 값을 어떻게 +읽는가"/"emit 필요 여부"도 전파 모델 확정으로 해소, `RobloxFactory` 중복 +호출/충돌 시나리오·인스턴스 생성/이벤트 네이밍도 위 절에서 전부 확정. diff --git a/.claude/base/comparison-fusion-vide.md b/.claude/base/comparison-fusion-vide.md index d47740e..5a90494 100644 --- a/.claude/base/comparison-fusion-vide.md +++ b/.claude/base/comparison-fusion-vide.md @@ -51,12 +51,12 @@ Store/Slot/Tween/bind-dispatch 설계 결정에 인용되는 원본 비교 자 |---|---|---|---| | 전파 모델 | push+pull 하이브리드, eager 집합만 즉시 재계산, 생성순 정렬로 글리치 방지 | 순수 push, 즉시 동기 재평가, 다이아몬드 중복 재평가 미해결(저자 인정) | "Store는 값 자체에 항상 eager 발화, cleanup이 key/value를 먼저 확인" 요구사항은 Vide의 push 모델 + eval-전-cleanup 패턴에 더 가까움. 단 Vide의 naive BFS 대신 Fusion의 생성순 정렬 글리치 방지 규율은 채택할 것. | | 정리/스코프 | 배열+메타테이블, dependency-agnostic, bind 시점에만 lifetime soft-check | dependency edge와 구조적 owner를 분리한 2중 관계, destroy는 owned만 cascade, 활성 스코프 destroy 하드 가드 | 둘 다 GC 비의존 eager 수동 정리 — rbvm의 Connected+GC 관용구와 정반대 축. quad의 Slot은 Vide처럼 "마운트 소유권"과 "반응 의존성"을 별개 관계로 분리하는 게 안전해 보임(`base/lifecycle-pattern.md`의 rbvm 패턴과는 다른 층위 — rbvm은 인스턴스 파괴 감지, 이건 Slot 내부 소유권 모델). | -| 키/값 디스패치 개방성 | SpecialKey 모양은 열려있으나 우선순위 4단계 하드고정 | action()은 등록 없는 태그 인식 방식이지만 key/value 버림, 콜백+우선순위만 | quad는 Fusion의 "디스패처가 key+value+target을 다 받는" 풍부함과 Vide의 "등록 없이 태그로 인식" 인체공학을 합치되, 우선순위 축은 열린 숫자 공간으로 일반화해야 함(`research/bind-system-plan.md`). | +| 키/값 디스패치 개방성 | SpecialKey 모양은 열려있으나 우선순위 4단계 하드고정 | action()은 등록 없는 태그 인식 방식이지만 key/value 버림, 콜백+우선순위만 | quad는 Fusion의 "디스패처가 key+value+target을 다 받는" 풍부함과 Vide의 "등록 없이 태그로 인식" 인체공학을 합치되, 우선순위 축은 열린 숫자 공간으로 일반화해야 함(`base/bind-system-plan.md`). | ## 추가로 기록해둘 것 - Vide의 암묵적(ambient stack) 의존성 추적 vs Fusion의 명시적 `use()` 축은 - push/pull 축과 독립적인 별개 결정 — quad는 아직 미정(`research/bind-system-plan.md` + push/pull 축과 독립적인 별개 결정 — quad는 아직 미정(`base/bind-system-plan.md` 열린 질문 참고). Fusion의 명시적 `use()`는 `checkLifetime` 같은 bind-time 체크를 가능하게 하는 부수 효과가 있음. - 두 라이브러리 다 mount 시 단일 소유권 가드가 없다는 것 자체가 quad Slot의 diff --git a/.claude/base/lifecycle-pattern.md b/.claude/base/lifecycle-pattern.md index b1765a8..882c996 100644 --- a/.claude/base/lifecycle-pattern.md +++ b/.claude/base/lifecycle-pattern.md @@ -71,7 +71,7 @@ rbvm 전역에 약한 테이블(weak table, `__mode = "k"/"v"/"kv"`)로 private - `InitNamespace`/`Registered`-가드/`NewLib` 3종 세트로 "라이브러리마다 하나하나 수동 init" 하는 방식은 정확히 사용자가 피하고 싶다고 한 패턴 — 순서 있는 dispose-hook 리스트 자체는 재사용해도, 수동 init 관례는 그대로 베끼지 말 것 - (팩토리 함수로 대체 — `research/module-lifecycle-plan.md` 참고). + (팩토리 함수로 대체 — `base/module-lifecycle-plan.md` 참고). ## 확정: Signal 클래스는 안 만든다 @@ -97,7 +97,7 @@ Destroy되면 그 대상에 묶인 것들(Tween 등)도 자연히 죽은 상태 이 원칙 때문에 "값 교체 시 이전 처리를 무르는 것"(아래 `retract`)과 "완전 소멸 시 정리"는 **하나로 통일** — 후자는 애초에 안 만듦. `research/ -tween-plan.md`/`research/slot-plan.md`의 "cleanup" 표기는 전부 `retract`로 +tween-plan.md`/`base/slot-plan.md`의 "cleanup" 표기는 전부 `retract`로 갱신됨(이름 변경 근거는 아래). ## 함수 안에서 만든 옵저버도 GC 대상이 되어야 함 — 범용 "생명 바인드 유틸" 필요 @@ -118,11 +118,29 @@ GC에 묶이지 않음 — v1이 여기저기서 `PropertyChangedSignal`에 연 가질 수 있어서, `Connected`가 false면 실행 자체를 건너뛸 수 있음(죽은 대상에 대한 처리 시도 방지, 위 원칙과 직결). -이건 `research/bind-system-plan.md`의 "핸들러 내부 상태 저장" 유틸과 짝을 +이건 `base/bind-system-plan.md`의 "핸들러 내부 상태 저장" 유틸과 짝을 이루는 별도 유틸 — 하나는 "상태를 어디에 저장할지"(weak-keyed per-instance 저장소), 다른 하나는 "언제까지 실행되어도 되는지"(생명 바인드 + canExecute)를 다룸. 둘 다 base가 제공하는 범용 유틸로 확정. +**교차검증(2026-08-04 4차 라운드)**: 사용자가 공유해준 실제 참고 코드 +(`.claude/initreq/artworks/EventDrivenProgramming/`, PA님 작성)는 GC-native가 +아니라 전부 수동 `:unsubscribe()`/`:disconnect()`로 관리됨 — rbvm 기반 +GC-native 원칙과 반대 선택이라 재확인했으나 **GC-native 유지로 확정**(지금까지 +명시적 dispose가 꼭 필요할 만큼 큰 자원을 다루는 실제 사례가 없었음). **막다른 +길은 아님을 기록**: rbvm처럼 관계를 양쪽 다 weak-keyed로 두고 모든 걸 connection +람다에 담아두는 방식이면, 나중에 GC만으로 부족한 케이스가 실제로 생겨도 그 +connection을 얻어 `disconnect()`하는 명시적 dispose 경로를 추가로 얹는 게 +가능한 디자인 — 필요성이 드러나면 그때 얹을 하이브리드 여지로만 남겨둠. + +**재사용 사례(2026-08-04 2차 라운드)**: Store/State의 무효화(invalidate) +신호를 받는 리스너 클로저도 정확히 이 유틸로 등록됨 — `base/ +store-semantics.md`가 예전에 "state 옵저빙 결과로 slot을 조작할 때 생존 +여부를 어떻게 확인할지" 미해결로 남겨뒀던 문제가, 사실은 새 메커니즘이 +필요한 게 아니라 이 canExecute 게이트를 그대로 적용하면 되는 사례였음(별도 +`isInit` 분기 불필요). 상세는 `base/bind-system-plan.md`의 "Store/State/ +Source 온톨로지" 절 참고. + ## 2026-08-04 검증 라운드에서 보강된 내용 **`Connected` 체크는 rbvm 패턴을 그대로 베끼는 게 아니라 base가 인터페이스로만 diff --git a/.claude/research/module-lifecycle-plan.md b/.claude/base/module-lifecycle-plan.md similarity index 85% rename from .claude/research/module-lifecycle-plan.md rename to .claude/base/module-lifecycle-plan.md index aa25af0..927b691 100644 --- a/.claude/research/module-lifecycle-plan.md +++ b/.claude/base/module-lifecycle-plan.md @@ -1,7 +1,8 @@ -# 모듈 라이프사이클 — 프로바이더 패턴, bind/store는 누가 구현하는가 (착수 전) +# 모듈 라이프사이클 — 프로바이더 패턴, bind/store는 누가 구현하는가 (base로 승격됨) -**상태**: research — 방향은 있지만 "누가 store를 구현하는가"는 사용자 스스로 -"진짜 애매한 지점"이라고 남긴 미해결 항목. 원본: +**상태**: base — "누가 store를 구현하는가"까지 포함해 전부 확정되어 +`research/`에서 승격됨(`base/architecture.md`의 "구현 착수: 소스 트리 구조 +확정" 절 참고). 원본: `.claude/initreq/raw-userinput.md` "넘버 바인드는 누가 처리?" / "모듈은 스코핑 되는가" / "pluggable 하다면 해당 플러그를 초기화하는 건 누구 몫?" / "다시 돌아와서… bind는 누가 어떻게 구현" / "스토어는 누가 구현해…" 절. 확정된 상위 결정은 @@ -32,7 +33,7 @@ RBVM처럼 `init namespace` 하나하나 부르는 방식은 별로(`base/lifecy 인터페이스 상 `bind`를 두고 이것도 pluggable하게 할지 고민 — 단 **1개만 존재할 수 있는 형태**로 구현하는 게 맞다고 기울어짐: 이미 bind 구현체가 있는데 또 init하려 하면 오류, 없는데 뭔가 생성해서 bind하려 해도 오류. 즉 "pluggable -슬롯이지만 유일하게 채워질 수 있는 슬롯" — 위의 `research/bind-system-plan.md`가 +슬롯이지만 유일하게 채워질 수 있는 슬롯" — 위의 `base/bind-system-plan.md`가 말하는 "여러 핸들러가 우선순위로 경쟁"하는 것과는 다른 층위: **핸들러 레지스트리 자체(그 배후의 실제 bind 구현/백엔드)는 유일해야 하고, 그 안에 등록되는 개별 핸들러들은 여럿+우선순위 경쟁이 맞는 모양.** @@ -46,8 +47,7 @@ init하려 하면 오류, 없는데 뭔가 생성해서 bind하려 해도 오류 계산 속성)를 소유하는 게 맞다고 확정. 추가로 명확해진 것 — **store 바인드가 수행하는 "처리된 값을 다시 `process(inst,k,realv)`로 넘기는" 재실행 로직 자체도 base가 한 번만 구현**해야 함(모든 백엔드/핸들러가 각자 재구현하면 안 -됨). 근거: "모든 곳에서 다시 구현하는 건 나쁘니까." → `research/ -bind-system-plan.md`의 "확정된 디스패치 모델" 절이 바로 이 base 제공 로직. +됨). 근거: "모든 곳에서 다시 구현하는 건 나쁘니까." → `base/bind-system-plan.md`의 "확정된 디스패치 모델" 절이 바로 이 base 제공 로직. 부수적으로 확인된 것: - **Store 자체의 연산은 더 단순해져도 됨** — v1의 `:Add`/`:With`/`:Tween` 같은 @@ -55,8 +55,7 @@ bind-system-plan.md`의 "확정된 디스패치 모델" 절이 바로 이 base 일반 함수를 받는 형태로 통일(`base/store-semantics.md` 참고). "너무 verbose한 연산들은 오히려 일관성을 해친다"는 게 이유. - **여러 store 값을 묶어 유연하게 처리하는 방법**(`useEffect`류 dependency - array)은 있으면 좋겠다는 요청 — API 시그니처는 미정, `research/ - bind-system-plan.md`의 남은 열린 질문 참고. + array)은 있으면 좋겠다는 요청 — API 시그니처는 미정, `base/bind-system-plan.md`의 남은 열린 질문 참고. - `can execute store bind` 후킹 자체는 `Connected` 계산 속성으로 대체된다는 잠정 제안이 그대로 유지되고, 여기에 더해 **완전 소멸(Destroy) 시점엔 아무 처리도 필요 없다**는 원칙까지 확정됨(`base/lifecycle-pattern.md`) — 즉 이 @@ -90,7 +89,10 @@ bind-system-plan.md`의 "확정된 디스패치 모델" 절이 바로 이 base pluggable 참가자라는 점은 확정, 이름만 미정. - base 유틸(per-instance 상태 저장소, 생명 바인드 유틸)이 인터페이스만 두고 실제 구현은 백엔드 팩토리(`RobloxFactory(BaseModule)`류)가 뮤테이션으로 - 주입한다는 패턴이 확정됨 — 상세는 `research/bind-system-plan.md`의 "base - 유틸은 인터페이스, 실제 구현은 백엔드 팩토리가 주입" 절 참고. 이 패턴을 - 중복 호출했을 때의 가드 동작(멱등 처리)과 모듈 스코핑(`New()`)의 관계는 - 여전히 열려있음. + 주입한다는 패턴이 확정됨 — 상세는 `base/bind-system-plan.md`의 "base + 유틸은 인터페이스, 실제 구현은 백엔드 팩토리가 주입" 절 참고. **중복 호출 + 가드/`New()`와의 관계는 2026-08-04 3차 라운드에서 확정**: 같은 팩토리로 + 재호출하면 무시(no-op), 다른 팩토리로 재호출하면 에러(유일 슬롯 충돌 — + 바로 아래 "Bind는 누가, 어떻게 구현하는가" 절의 원칙과 일치) — `New()`가 + 생기면 인스턴스별 테이블이 분리되므로 이 가드도 자연히 인스턴스별로 + 스코핑됨, 별도 재설계 불필요. diff --git a/.claude/base/quad-v1-architecture.md b/.claude/base/quad-v1-architecture.md index 3e699b1..322e1c1 100644 --- a/.claude/base/quad-v1-architecture.md +++ b/.claude/base/quad-v1-architecture.md @@ -68,12 +68,12 @@ v2에서 태그 시스템으로 대체 예정(`base/store-and-tags.md` 참고). 1. Metatable 체이닝으로 "불변 빌더" 흉내내기 → 대신 팩토리 함수로 필요한 곳만 복사 (`raw-userinput.md` "복사 구현은 지양" 항목, `.claude/initreq/raw-userinput.md:83-86`). 2. 하드코딩된 중앙 디스패처 → pluggable `isHandlable(key,value)` + 우선순위 핸들러 - 레지스트리 (`research/bind-system-plan.md`). + 레지스트리 (`base/bind-system-plan.md`). 3. 흩어진 "GC 안 되게 참조 붙잡기" 핫팩 → rbvm 스타일 `Connected` 계산 속성 + 명시적 라이프타임 홀더 (`base/lifecycle-pattern.md`). 4. mount가 여러 책임(부모 부기+파괴+child 레지스트리)을 한 모듈에 다 지는 구조 → Slot이 child CRUD를 전담, mount는 단일-마운트 강제만 전담 - (`research/slot-plan.md`). + (`base/slot-plan.md`). 5. tracker.lua, lang.lua 내장 → 둘 다 라이브러리 범위 밖으로 분리(스토리북/ 외부 로케일 라이브러리에 위임). diff --git a/.claude/research/slot-plan.md b/.claude/base/slot-plan.md similarity index 73% rename from .claude/research/slot-plan.md rename to .claude/base/slot-plan.md index d8cd338..ec266e6 100644 --- a/.claude/research/slot-plan.md +++ b/.claude/base/slot-plan.md @@ -1,10 +1,27 @@ -# Slot — 뮤터블 자식 배열, 엄격한 단일 마운트 소유권 (착수 전) +# Slot — 뮤터블 자식 배열, 엄격한 단일 마운트 소유권 (base로 승격됨) -**상태**: research — 설계 방향은 상당히 잡혀 있으나 세부(특히 소유권 이전/해제 -시맨틱)는 사용자와 확인 필요. 원본: `.claude/initreq/raw-userinput.md` "slot을 -구현하도록 하기로 했음" 절. Fusion의 `Children` SpecialKey와 Vide의 mount 무가드 -비교는 `base/comparison-fusion-vide.md` 참고 — 결론: **두 라이브러리 어디에도 -이런 엄격한 단일 마운트 가드가 없음, quad의 진짜 개선점.** +**상태**: base — 설계 방향(소유권 귀속, 재마운트 시 throw, retract=폐기)과 +소스 트리 상 패키지 경계까지 확정되어 `research/`에서 승격됨(`base/ +architecture.md`의 "구현 착수: 소스 트리 구조 확정" 절 참고). 원본: +`.claude/initreq/raw-userinput.md` "slot을 구현하도록 하기로 했음" 절. Fusion의 +`Children` SpecialKey와 Vide의 mount 무가드 비교는 `base/comparison-fusion-vide.md` +참고 — 결론: **두 라이브러리 어디에도 이런 엄격한 단일 마운트 가드가 없음, +quad의 진짜 개선점.** + +## base/roblox 패키지 경계 (2026-08-04, 5차 라운드 확정) + +Slot의 add/remove/clear 재조정 로직(추상 자식 참조 기준 — "이 자리에 뭐가 +있어야 하는가"를 결정하는 순수 로직)은 `quad-base/src/Dispatch/Slot.luau`가 +소유. 실제 트리 조작(Instance `Parent` 설정/`Destroy`)은 `quad-roblox/src/ +Handlers/Slot.luau`가 그 위에서 적용/해제만 담당 — 다른 모든 인터페이스/구현 +분리와 동일한 패턴(`base/architecture.md`의 소스 트리 참고). Slot 자체는 +당연히 Instance들을 담게 될 것으로 취급. + +**추가로 필요해진 핸들러**: Slot과는 별개로, `k`가 number이고 `v`가 이미 +만들어진 Instance인 경우(중첩 인스턴스를 자식으로 직접 넣는 경우, 예: +`Frame { Frame {} }`)를 위한 핸들러도 필요 — `quad-roblox/src/Handlers/ +InstanceChild.luau`. Slot은 "뮤터블 배열"을 다루고 이 핸들러는 "정적으로 +하나 박아넣는" 더 단순한 경우라 별개로 둠. ## 개념 @@ -57,11 +74,11 @@ Slot이 store 바인드로 들어오는 경우, pluggable 처리기에 `retract` `process`하면 되므로, 부모에게 위임(자식 slot 요소 자체가 스스로 정리를 실행하는 게 아니라). -이건 `research/bind-system-plan.md`의 "Store 바인드는 재실행 래핑" 확정 +이건 `base/bind-system-plan.md`의 "Store 바인드는 재실행 래핑" 확정 모델과 맞물림 — slot이 store 값으로 오면, store 바인드 핸들러가 이전 slot 상태를 `retract`하고 새 slot 상태로 다시 `process`하는 사이클을 돈다는 뜻. Slot 핸들러 자신이 감시 중인 값(배열/스토어)이 바뀔 때 child를 갱신하는 -추적(구독)도 `research/bind-system-plan.md`가 말하는 "process 함수가 다른 값 +추적(구독)도 `base/bind-system-plan.md`가 말하는 "process 함수가 다른 값 변경을 추적해도 됨" 범위에 속하고, `retract` 시점엔 그 추적만 풀면 됨 — Destroy 시점엔 `retract`가 호출되지 않는다는 원칙(`base/lifecycle-pattern.md`)도 동일하게 적용. diff --git a/.claude/base/store-semantics.md b/.claude/base/store-semantics.md index 2823c4d..63b3b42 100644 --- a/.claude/base/store-semantics.md +++ b/.claude/base/store-semantics.md @@ -1,8 +1,7 @@ # Store 의미론 — 부작용 허용, State는 Store 위의 조합 가능한 캐시 레이어 **상태**: base — 부작용 허용/Store 문법 부분은 확정. State/Source 온톨로지는 -2026-08-04 검증 라운드에서 새로 열린 진행 중인 설계 스레드(`research/ -bind-system-plan.md` 참고). 원본: `.claude/initreq/raw-userinput.md` +2026-08-04 검증 라운드에서 새로 열린 진행 중인 설계 스레드(`base/bind-system-plan.md` 참고). 원본: `.claude/initreq/raw-userinput.md` "store는 부작용을 허용함" / "state는 어떻게 구현하는가" 절. ## Store는 부작용을 허용하는 게 기본 디자인 @@ -26,17 +25,23 @@ purity-and-effects-plan.md`와 연결됨). 막을 수는 없지만, 라이브러리로 재사용하려는 컴포넌트가 이런 부작용을 가지면 이식성이 떨어짐(`research/purity-and-effects-plan.md`와 연결). -**미해결 열린 질문**: state를 옵저빙해서 나온 결과로 slot에 `clear`/`add` 같은 -연산을 할 때, 그 시점에 대상 slot이 이미 죽어있으면 어떻게 되는가 — state가 -생성되는 지점과 slot이 적용되는 지점이 서로 다른 스코프라, "state 변경이 -발생했을 때 그 slot이 아직 살아있는지"를 어떻게 연관지어 확인할지 아직 명확한 -설계가 없음(`isInit=false`일 때는 허용, `isInit=true`이고 생존 확인 함수가 -거짓이면 불허 정도의 방향은 있으나 미완성). 사용자 본인도 "더 리서치가 필요" -하다고 명시적으로 표시 — `research/bind-system-plan.md`의 "Store/State/Source -온톨로지" 스레드와 함께 다룰 것. +**해소됨(2026-08-04 2차 라운드)**: state를 옵저빙해서 나온 결과로 slot에 +`clear`/`add` 같은 연산을 할 때, 그 시점에 대상 slot이 이미 죽어있으면 +어떻게 되는가 — 별도 메커니즘을 새로 만들 필요 없이, `base/ +lifecycle-pattern.md`의 "생명 바인드 유틸"(canExecute predicate)을 state- +invalidate 리스너 클로저 등록에도 그대로 재사용하면 됨: 발화 시 +`canExecute()` 하나만 확인, 거짓이면 no-op. 한때 검토했던 `isInit=false`면 +허용/`isInit=true`+생존확인 거짓이면 불허 분기 초안은 폐기 — `canExecute` +하나로 통일(사용자 확정). 상세는 `base/bind-system-plan.md`의 +"Store/State/Source 온톨로지" 절 참고. ## 정정(2026-08-04 검증 라운드): `State` 프리미티브는 실제로 필요하다 +**후속(2026-08-04 2차 라운드)**: 아래 온톨로지의 전파 모델(push-invalidate/ +pull-recompute)·`:Compute` 인자 규칙·State 쓰기 금지·`Source` 독립 +프리미티브화·Slot 생존 확인까지 전부 확정됨 — 최신 상세는 `base/bind-system-plan.md`의 "Store/State/Source 온톨로지 — 핵심 메커니즘 확정" +절이 최종 소스, 이 절은 배경/온톨로지 명칭 정의로만 유지. + **이전 버전의 이 절("State 프리미티브는 만들지 않는다")은 틀렸음 — 사용자가 검증 라운드에서 직접 정정.** 정확한 모델: @@ -54,7 +59,7 @@ purity-and-effects-plan.md`와 연결됨). - 이건 quad2-try(폐기된 이전 시도)의 `Pipe` copy-on-write 절충안을 대체하는 방향으로 좁혀짐 — 별도 `Pipe` 타입을 만들어 소유권/버전 가드를 넣는 대신 State 자체가 "파이핑 결합체"이고 `state(state)`로 분기하면 될 걸로 보임 - (`Pipe` 후보는 사실상 폐기 쪽으로 기움). 상세는 `research/bind-system-plan.md`의 + (`Pipe` 후보는 사실상 폐기 쪽으로 기움). 상세는 `base/bind-system-plan.md`의 "Store/State/Source 온톨로지" 절 참고 — **아직 완전히 결론난 설계는 아니고, 구현 단계에서 더 다뤄야 할 진행 중인 스레드.** @@ -62,7 +67,7 @@ purity-and-effects-plan.md`와 연결됨). 없으면 연산을 미루는 dirty-flag 방식 등), Luau 타입 시스템에서 `store "key"` 같은 커링 호출이 오버로드 함수 타입으로 `state`를 정확히 추론하기 어려운 문제(문자열 리터럴이 as-const로 좁혀지지 않는 문제) — 둘 다 열린 채로 -`research/bind-system-plan.md`에서 계속 다룰 것. +`base/bind-system-plan.md`에서 계속 다룰 것. ## Store 값 설정 문법 — v1 인체공학 유지 (확정) @@ -82,12 +87,13 @@ purity-and-effects-plan.md`와 연결됨). 인체공학(사용자가 좋아하는 부분)은 유지하되 내부 구현(체이닝이 아니라 팩토리 함수)만 바꾼다. -## 여러 스토어 값을 묶어 처리하는 것 (dependency array) — 연구 필요 +## 여러 스토어 값을 묶어 처리하는 것 (dependency array) — 확정 `useEffect`처럼 여러 store 값을 디펜던시로 묶어 파생값을 계산하고 싶다는 -요구가 있음(v1의 `myStore "a,b"` 콤마-조인 문자열 방식은 폐기 대상 — -`base/quad-v1-architecture.md`의 "문자열 DSL" 문제점 참고). 단, **v1의 -`:Add`/`:With`/`:Tween` 같은 이름 붙은(named) 체이닝 연산은 만들지 않기로 -확정** — 대신 일반 함수를 받아 처리하는 쪽이 일관적이라는 판단. 구체적인 API -모양(`Store.Combine({a,b}, function(a,b) ... end)`류)은 아직 미정 — -`research/bind-system-plan.md` 참고. +요구가 있었음(v1의 `myStore "a,b"` 콤마-조인 문자열 방식은 폐기 대상 — +`base/quad-v1-architecture.md`의 "문자열 DSL" 문제점 참고). **v1의 +`:Add`/`:With`/`:Tween` 같은 이름 붙은(named) 체이닝 연산은 만들지 않음** — +대신 일반 함수를 받아 처리. 최종 형태는 `:With(...)`로 의존성을 모으고 +`:Compute(fn)`으로 파생 State를 만드는 것으로 확정 — `Store.Combine({a,b}, +fn)`류 포지셔널 인자 방식은 기각됨. 정확한 lazy 인자 규칙(self/with 값 둘 다 +State 핸들로 넘기고 `.value`를 실제로 읽을 때만 계산)은 `base/bind-system-plan.md`의 "Store/State/Source 온톨로지" 절 참고. diff --git a/.claude/question.md b/.claude/question.md index 25e52e5..bd2dee3 100644 --- a/.claude/question.md +++ b/.claude/question.md @@ -5,6 +5,31 @@ 있음. 사용자가 Lua/Roblox 엔진에 대해 깊이 아는 사람이라는 전제로, 우선순위 높은 것부터 정렬. +## 2026-08-04 5차 라운드 완료 — 소스 트리 구조 확정 + +`.claude/base/architecture.md`의 "구현 착수: 소스 트리 구조 확정" 절 참고. +`base/bind-system-plan.md`/`base/module-lifecycle-plan.md`/`base/slot-plan.md`가 +이 라운드에서 `research/`에서 승격됨. + +- **패키징**: 최종 목표는 다중 wally 패키지지만, 지금 Luau 툴링(wally 타입 + 단절, `luau-lsp` 심볼릭 링크 해석 문제)이 불안정해서 당장은 모놀리식 — + `Sleitnick/RbxUtil` 패턴(루트 통합 개발/테스트, 서브폴더마다 자체 + `wally.toml`) 채택. `.luaurc` alias는 런타임 require에서 아직 엔진 미지원 — + 편집기 경험용으로만 사용, 런타임 require는 상대경로. +- **패키지 경계**: `quad-base` = Store/State/Source 온톨로지+전파 **+** + pluggable 디스패치 엔진(`process`/`retract`, 핸들러 계약, `LifetimeHandle`/ + `PerInstanceState` 인터페이스, Ref, Slot 코어 재조정 로직) — 전부 + "인터페이스"로, 다른 엔진(GTK 등)에서도 재사용 가능해야 한다는 전제. + `quad-roblox` = 위 인터페이스의 실제 구현체(`RobloxFactory`, Property/Event/ + Attribute/Tag/Tween/Slot 적용 핸들러, `DI` 인스턴스 생성자) — 이유: 엔진마다 + 큰 구현을 중복하지 않기 위함(rbvm의 relation 통합 시도와 같은 동기). +- **Slot 패키지 경계**: 재조정 로직(add/remove/clear)은 base, 실제 Instance + `Parent`/`Destroy` 조작은 roblox의 핸들러가 담당 — `base/slot-plan.md` + "base/roblox 패키지 경계" 절. +- **새 핸들러 필요성 확인**: `k:number, v:Instance`(중첩 인스턴스를 직접 + 자식으로 넣는 경우, `Frame { Frame {} }`)를 위한 `InstanceChild` 핸들러가 + Slot과 별개로 필요 — `quad-roblox/src/Handlers/InstanceChild.luau`. + ## 2026-08-04 검증 라운드 완료 아래 "확정됨" 절 전체(architecture.md 14개 항목, lifecycle-pattern.md, @@ -18,51 +43,100 @@ slot-plan.md, tween-plan.md)를 `AskUserQuestion`으로 하나씩 예/아니오 전체가 새로운 열린 설계 스레드로 떠올랐음** — 아래 "최우선 새 열린 질문" 참고. - Slot의 `retract` 동작이 "부모 위임" 잠정안에서 "폐기(옮기지 않음)"로 확정 - — `research/slot-plan.md`. + — `base/slot-plan.md`. - quad2-try의 `Pipe` copy-on-write 후보는 사실상 폐기, `state(state)` 조합 - 모델로 대체 — `research/bind-system-plan.md`. + 모델로 대체 — `base/bind-system-plan.md`. - `Connected` 체크/GC 위임/`Destroying` 훅 관련 뉘앙스 보강(엔진별 인터페이스 주입, quad는 rbvm보다 즉시정리 필요성이 낮음) — `base/lifecycle-pattern.md`. - base 유틸(per-instance 저장소, 생명 바인드)은 인터페이스만, 실제 구현은 - `RobloxFactory(BaseModule)`류 백엔드 팩토리가 주입 — `research/ - bind-system-plan.md`. + `RobloxFactory(BaseModule)`류 백엔드 팩토리가 주입 — `base/bind-system-plan.md`. ## 최우선 새 열린 질문 (검증 라운드에서 새로 터져나옴) -- **Store/State/Source 온톨로지 전체** — store는 source 집합체, state는 - source를 감싸는 조합 가능한 캐시(자기 고유 value 없음), `state(state)`로 - 분기. `:Compute`의 캐싱/무효화 전략(dirty-flag 등), `emit` 필요 여부, Luau - 타입 시스템에서 `store "key"` 커링 호출의 `state` 추론 문제까지 전부 - 미정 — 다음 세션 최우선 논의 대상. → `research/bind-system-plan.md`의 - "Store/State/Source 온톨로지" 절. -- **부작용이 slot 생존 여부와 어떻게 연관되는가** — state 옵저빙 결과로 - slot을 조작할 때, 그 시점에 대상 slot이 죽어있으면 어떻게 처리할지 사용자도 - 아직 명확한 답이 없다고 명시. → `base/store-semantics.md`. -- **인스턴스 생성/이벤트 네이밍 인체공학** — `Quad "Frame"` 문자열 방식 vs - `DI.Frame` 필드 접근 방식(자동완성/타입추론 트레이드오프). → `research/ - bind-system-plan.md`. -- `RobloxFactory` 같은 백엔드 팩토리를 같은 base에 중복 호출했을 때의 가드 - 동작, 모듈 스코핑(`New()`)과의 관계. → `research/bind-system-plan.md`. +**전부 확정됨** — 아래 "2026-08-04 3차 라운드" 절 참고. 이 섹션에 새 항목이 +생기면 여기 추가. + +## 2026-08-04 4차 라운드 완료 — PA님 실 코드(`initreq/artworks`) 교차검증 + +사용자가 실제 참고 코드를 공유(`.claude/initreq/artworks/`, PA님 작성) — +아래 두 항목이 3차 라운드 잠정안에서 정정됨, 나머지는 재검토 후 기존 확정 +유지: + +- **"DI" = Declarative Instance**(Dependency Injection 아님) — 3차 라운드의 + 오해 정정. +- **이벤트 바인딩 정정**: `On.EventName` 도트액세스 안 씀 — PA님 방식(평범한 + 문자열 키 + `ReflectionService` 기반 자동 판별, `Frame { MouseButton1Click + = fn }`)으로 전환. Store의 `store.key`는 실질적 타입 이득이 있어 dot-access + 유지, 이벤트만 예외. +- **인스턴스 생성**: 2트랙(`DI.Frame`/`DI.New<>`) 대신 PA님 코드처럼 + 제네릭 생성자 함수 하나 + 자주 쓰는 클래스만 정적 필드로 미리 바인딩하는 + 더 단순한 모양으로 정정. +- **전파 모델(push-invalidate/pull-recompute)·라이프사이클(GC-native)은 + 재검토 후 기존 확정 유지** — PA님 코드가 반례처럼 보였으나(전자는 push-값 + 단순 pub-sub, 후자는 전부 수동 해제) 대등한 비교가 아니었거나(파생/합성 + 개념 자체가 없음) 지금 필요성이 없다는 게 사용자 판단. 라이프사이클은 + 나중에 하이브리드로 확장 가능한 여지만 기록. +- OOP 회피 결정은 PA님의 `class.luau`도 같은 체이닝 상속 보일러플레이트를 + 보여 오히려 보강됨. Instance 태그는 CollectionService 직접 사용 유지. + +→ 상세: `base/bind-system-plan.md`의 "인스턴스 생성 / 이벤트 네이밍 +인체공학" 절, "Store/State/Source 온톨로지"의 "PA님 코드와의 교차검증" 절, +`base/lifecycle-pattern.md`의 "교차검증" 절. + +## 2026-08-04 3차 라운드 완료 — dot-access 관습 확정, RobloxFactory 가드 확정 (일부 4차 라운드에서 정정됨) + +- **dot-access를 프로젝트 전역 관습으로 확정**: "정적으로 알려진 것=필드 + 접근, 동적인 것=문자열 호출 폴백"이 Store(`store.key`/`store "key"`)와 + 인스턴스 생성에 적용됨 — **이벤트는 4차 라운드에서 예외로 정정**(위 참고). +- **`RobloxFactory` 재호출 가드 확정**: 같은 팩토리로 재호출 시 무시 + (no-op, hot-reload 안전), 다른 팩토리로 재호출 시 에러(유일 슬롯 충돌). + `New()`와는 인스턴스별 테이블 분리로 자연히 공존 — 재설계 불필요. (4차 + 라운드에서 변경 없음) + +→ 상세: `base/bind-system-plan.md`의 "인스턴스 생성 / 이벤트 네이밍 +인체공학" 절, "base 유틸은 인터페이스..." 절의 재호출 가드 부분. + +## 2026-08-04 2차 라운드 완료 — Store/State/Source 온톨로지 핵심 메커니즘 + +위 최우선 질문 중 "Store/State/Source 온톨로지 전체"와 "부작용이 slot 생존 +여부와 어떻게 연관되는가"는 `AskUserQuestion`으로 확인 완료, 더 이상 열려있지 +않음: + +- **전파 모델**: push-invalidate(신호만, 값 안 실음) / pull-recompute(`Get()` + 시점에만 재계산) — Fusion식 eager 노드·생성순 정렬은 안 만듦. +- **`:Compute`의 self 인자**: raw 값이 아니라 State 핸들 자체를 넘겨서 self도 + with한 값과 동일하게 lazy하게(`.value`를 실제로 읽을 때만 계산) 처리 — + 별도 `ComputeWithout` 불필요. +- **State는 쓰기 대상이 아님**: `.value`는 읽기 전용, 값 쓰기는 항상 Store의 + `__newindex`로만. `Source`는 Store 내부 디테일이 아니라 값 하나만 다룰 때 + 쓰는 별도의 가벼운 공개 프리미티브로 격상. +- **Slot 생존 확인**: 별도 메커니즘 없이 기존 "생명 바인드 유틸"의 + `canExecute`로 통일 게이트. +- **`store.key` dot-access 타입 추론 제안**: 3차 라운드에서 정식 확정됨(위 + "2026-08-04 3차 라운드 완료" 절 참고). + +→ 상세: `base/bind-system-plan.md`의 "Store/State/Source 온톨로지 — +핵심 메커니즘 확정" 절, `base/store-semantics.md`, `base/lifecycle-pattern.md`. ## 확정됨 (2026-08-03 질의응답 라운드, 더 이상 열려있지 않음) - **Store 책임 분리**: base가 `LifetimeHandle` 추상화 + store-bind의 재실행 로직(`process(inst,k,realv)` 재귀)을 소유, provider는 "언제 죽었다고 - 판단할지"(Roblox `Destroying` 등)만 결정. → `research/module-lifecycle-plan.md`, - `research/bind-system-plan.md` + 판단할지"(Roblox `Destroying` 등)만 결정. → `base/module-lifecycle-plan.md`, + `base/bind-system-plan.md` - **Signal 클래스**: 안 만듦 — 콜백 + `Connected` 계산 속성만. → `base/ lifecycle-pattern.md` - **핸들러 계약**: `isHandlable`+`priority`+`process`+`retract` 4종 유지, - tbox식 세분화는 지금 안 함. → `research/bind-system-plan.md` + tbox식 세분화는 지금 안 함. → `base/bind-system-plan.md` - **Ref**: 도입하되 용도는 "id 조회 대체"가 아니라 "외부 관리 instance를 점진적으로 마이그레이션/래핑하기 위한 직접 참조 획득". Tween 등 어떤 핸들러도 대상 획득에 Ref가 필요하지 않음(항상 `inst`를 직접 받음). - → `research/bind-system-plan.md` + → `base/bind-system-plan.md` - **`retract`(구 cleanup) 호출 시점**: 값 교체 시에만 호출, Destroy 시엔 호출 안 함(quad는 자신이 만든 instance의 생명주기 중간에 있지 않으므로 destroy-time 정리 자체가 불필요/불가능). → `base/lifecycle-pattern.md` - **핸들러 내부 상태 저장**: base가 범용 weak-keyed per-instance 저장 유틸 - 제공(모든 핸들러 재사용). → `research/bind-system-plan.md`, + 제공(모든 핸들러 재사용). → `base/bind-system-plan.md`, `base/lifecycle-pattern.md` - **Store 값 설정 문법**: `__newindex`(`myStore.key = v`) 유지, 괄호 생략 커링/`:` 체이닝 인체공학도 유지 — 바뀌는 건 내부 구현(팩토리 함수)뿐. @@ -75,14 +149,14 @@ slot-plan.md, tween-plan.md)를 `AskUserQuestion`으로 하나씩 예/아니오 - **트윈 오버라이드 기본값**: 멈춤(Cancel), 새 트윈은 현재 보간된 값에서 시작. 나머지 세 동작(오버라이드/삭제후재시작/끝점이동후재시작)은 옵션으로 선택 가능. → `research/tween-plan.md` -- **Slot 재마운트 에러**: 즉시 throw. → `research/slot-plan.md` +- **Slot 재마운트 에러**: 즉시 throw. → `base/slot-plan.md` - **`CreatedRef` 콜백 타이밍**: 생성 시점/마운트 시점 둘 다 옵션으로 지원. - → `research/bind-system-plan.md` + → `base/bind-system-plan.md` - **여러 store 값 묶기**: `Store.Combine`류 포지셔널 인자 방식과 Vide식 암묵적 추적 둘 다 기각 — `:With(...)` + `:Compute(fn)`(fn은 with한 값을 포지셔널 인자가 아니라 클로저로 읽음) 방식으로 확정. Unix 파이프에서 영감받은 완전 합성 가능한 State 스트림이 이상향이나 기술적 난이도 미확정 — 과거 시도 - (`quad2-try/quad-core`) 리서치 진행 중. → `research/bind-system-plan.md` + (`quad2-try/quad-core`) 리서치 진행 중. → `base/bind-system-plan.md` ## quad2-try(이전 폐기된 시도) 리서치 완료 — 추가 확정 @@ -95,7 +169,7 @@ slot-plan.md, tween-plan.md)를 `AskUserQuestion`으로 하나씩 예/아니오 간단하다는 판단(위 "최우선 새 열린 질문"의 Store/State/Source 온톨로지 절로 흡수됨). - **`Depend(...)` 액션, `:With` 네이밍**은 이전 시도에서도 지향했던 것과 일치 - — 그대로 채택. → `research/bind-system-plan.md` + — 그대로 채택. → `base/bind-system-plan.md` ## 순수성/이식성, 기존 인스턴스 바인드 — 확인 완료, 낮은 우선순위로 유지 @@ -114,7 +188,7 @@ slot-plan.md, tween-plan.md)를 `AskUserQuestion`으로 하나씩 예/아니오 심각하게 볼지 — 지금은 "별도 네임스페이스 개념은 복잡도 대비 이득이 적다"는 판단으로 보류 중. → `base/architecture.md` 5번 항목. - Store가 Store를 담는 경우 이중 해제(double-dispose) 방지가 실제로 필요한 - 상황이 있는지 — 구현 단계에서 실사례로 재검증. → `research/bind-system-plan.md` + 상황이 있는지 — 구현 단계에서 실사례로 재검증. → `base/bind-system-plan.md` --- 전체 순서/우선순위는 루트 `CLAUDE.md`가 최종 소스 — 위 표는 힌트일 뿐 그쪽이 diff --git a/.claude/research/tween-plan.md b/.claude/research/tween-plan.md index 7cf60e3..7071a2c 100644 --- a/.claude/research/tween-plan.md +++ b/.claude/research/tween-plan.md @@ -32,13 +32,13 @@ ## 정정: Ref 불필요 — 핸들러는 항상 대상 Instance를 직접 받는다 -**이전 초안의 전제가 틀렸음.** Tween 핸들러도 `research/bind-system-plan.md`의 +**이전 초안의 전제가 틀렸음.** Tween 핸들러도 `base/bind-system-plan.md`의 "확정된 디스패치 모델"을 그대로 따르는 store-bind 핸들러 중 하나 — `process(inst, k, v)`가 항상 대상 Instance(`inst`)를 직접 받으므로, 트윈 대상을 얻기 위해 Ref나 네임스페이스드 조회가 필요하지 않음. Tween의 store-bind 핸들러는 "`k`는 무엇이든, `v`가 Store인 것"을 잡아내는 우선순위 매우 높은 핸들러로 등록되고, `inst`는 이미 파라미터로 주어짐. (Ref 자체는 도입되지만 전혀 다른 -용도 — `research/bind-system-plan.md`의 Ref 절 참고.) +용도 — `base/bind-system-plan.md`의 Ref 절 참고.) ## `retract`(구 cleanup)로 확정된 오버라이드 시맨틱 @@ -62,7 +62,7 @@ Ref나 네임스페이스드 조회가 필요하지 않음. Tween의 store-bind 라이브러리가 강제하지 않고, `[Tween(key, tweenData, {onOverride=...})]`처럼 키 설정으로 사용자가 고를 수 있게 열어둠 — `retract(inst, k, v)`가 이전 값(v)을 받으므로 여기서 선택된 동작을 구현. `retract`가 접근해야 할 "이전에 -생성한 실제 Tween 객체"는 `research/bind-system-plan.md`가 말하는 base 제공 +생성한 실제 Tween 객체"는 `base/bind-system-plan.md`가 말하는 base 제공 범용 유틸(`inst`를 키로 하는 weak-keyed per-instance 상태 저장소)에 담아두면 됨. diff --git a/CLAUDE.md b/CLAUDE.md index 19cd65a..86676d9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,11 +30,25 @@ v2 재작성 시도(`.claude/initreq/quad2-try`)도 리서치 완료 — OOP 상 절충안은 한때 살려볼 후보였으나 **2026-08-04에 사실상 폐기로 재평가**됨(State 자체가 `state(state)`로 분기하는 쪽으로 대체). -**지금 유일하게 결론 안 난 핵심 설계 이슈는 Store/State/Source 온톨로지** -— 검증 라운드 중 "State 프리미티브는 안 만든다"던 기존 결정이 틀렸다는 게 -드러나며 새로 열림. `.claude/research/bind-system-plan.md`의 "Store/State/ -Source 온톨로지" 절, `.claude/question.md`의 "최우선 새 열린 질문" 절 참고 — -아래 "지금 할 일" 1번이 다음 세션이 여기서부터 시작해야 함을 명시. +**Store/State/Source 온톨로지 및 관련 인체공학 질문은 2026-08-04 네 라운드에 +걸쳐 전부 확정됨**(사용자가 공유해준 실제 참고 코드 `.claude/initreq/ +artworks/`, PA님 작성, 로 4차 교차검증까지 마침) — push-invalidate/ +pull-recompute 전파 모델, `:Compute` self/with 인자를 둘 다 lazy State +핸들로 통일, State는 쓰기 불가(값 쓰기는 항상 Store의 `__newindex`), +`Source`는 Store와 별개인 독립 프리미티브로 격상, Slot 생존 확인은 기존 +canExecute 유틸 재사용으로 해소, `store.key` dot-access를 타입 추론 1급 +경로로(인스턴스 생성도 같은 관습, 단 이벤트는 PA님 방식인 평범한 문자열 +키+런타임 리플렉션으로 예외), `RobloxFactory` 재호출 가드(같은 팩토리=무시, +다른 팩토리=에러)까지 확정. 남은 건 정확한 API 표면 이름뿐 — +`.claude/base/bind-system-plan.md` 전체, `.claude/question.md`의 +"2026-08-04" 절들 참고. + +**소스 트리 구조도 확정됨(2026-08-04 5차 라운드)**: `bind-system-plan.md`/ +`module-lifecycle-plan.md`/`slot-plan.md` 모두 `research/`에서 `base/`로 +승격 완료. 모노레포(`quad-base`/`quad-roblox` 서브폴더, RbxUtil 패턴)로 +당장은 모놀리식 진행, 패키지 경계(디스패치 엔진까지 base가 인터페이스로 +소유)까지 확정 — `base/architecture.md`의 "구현 착수: 소스 트리 구조 확정" +절 참고. 아래 "지금 할 일" 1번이 다음 단계(실제 스캐폴딩)를 명시. ## 계획 문서 구조 @@ -80,40 +94,76 @@ Source 온톨로지" 절, `.claude/question.md`의 "최우선 새 열린 질문" ## 지금 할 일 (우선순위순) -1. **[다음 세션 최우선] Store/State/Source 온톨로지 설계.** 2026-08-04 검증 - 라운드 중 "State 프리미티브는 안 만든다"는 이전 결정이 틀렸다는 게 - 밝혀지면서 새로 터져나온 핵심 설계 이슈 — Store=source 집합체, State= - source를 감싸는 조합 가능한 캐시(`state(state)`로 분기), `:Compute` - 캐싱/무효화 전략, Luau 타입 시스템에서 커링 호출의 `state` 추론 문제 - 등이 전부 미정. `.claude/research/bind-system-plan.md`의 "Store/State/ - Source 온톨로지" 절에 지금까지 나온 내용이 정리되어 있음 — 이어서 설계를 - 구체화할 것. `.claude/question.md`의 "최우선 새 열린 질문" 절도 함께 참고. -2. 위 온톨로지가 어느 정도 정리되면 `research/bind-system-plan.md`/ - `research/module-lifecycle-plan.md`를 `base/`로 승격하고, - `base/architecture.md`에 "구현 착수" 섹션을 추가해 실제 소스 트리 구조 - (어느 서브패키지가 뭘 갖는지)를 확정 — 이 시점부터 `qa-request/`/`archive/` - 폴더가 실제로 쓰이기 시작함. -3. 남은 세부 시그니처(`CreatedRef` 정확한 이름, 인스턴스 생성/이벤트 네이밍 - 인체공학, `RobloxFactory`류 팩토리 중복 호출 가드)는 온톨로지 설계와 - 자연스럽게 같이 확정 가능. -4. `research/purity-and-effects-plan.md`(특히 "state 옵저빙 결과로 slot을 - 조작할 때 생존 여부 확인" 열린 질문), `research/existing-instance-bind-plan.md`는 - 급하지 않음 — 스코프 논의만 필요, 구현 착수를 막지 않음. -5. 자율 작업 루프/스케줄 설정 여부는 사용자 결정 대기 중 +1. **[다음 세션 최우선] 실제 스캐폴딩.** 소스 트리 구조는 문서로 이미 확정됨 + (`base/architecture.md`의 "구현 착수: 소스 트리 구조 확정" 절) — 다음 + 세션에서 실제로 `quad-base/`, `quad-roblox/` 폴더, 각각의 `wally.toml`, + 루트 `default.project.json`, `.luaurc`를 만들 것. 이 시점부터 `qa-request/`/ + `archive/` 폴더가 실제로 쓰이기 시작함. +2. 남은 세부 시그니처(`CreatedRef`/`state()`/`Source()`/`DI`류 정확한 + 이름)는 위 항목과 자연스럽게 같이 확정 가능 — PA님 실 코드(`.claude/ + initreq/artworks/`)를 이미 받아서 교차검증 완료(아래 인수인계 메모 + 참고), `On` 모듈은 이벤트 바인딩 방식이 바뀌며 아예 불필요해짐. +3. `research/existing-instance-bind-plan.md`는 급하지 않음 — 스코프 논의만 + 필요, 구현 착수를 막지 않음. +4. 자율 작업 루프/스케줄 설정 여부는 사용자 결정 대기 중 (`HUMAN_TODO.md` 2번 항목). -## 인수인계 메모 (2026-08-04 세션 종료 시점) +## 인수인계 메모 (2026-08-04 세션 종료 시점, 5차 라운드까지 반영) + +**5차 라운드(소스 구조 확정)**: 4차 라운드 종료 시점에 서브에이전트로 먼저 +계획 문서 전체의 정합성을 점검(차질 없음 확인) 후 진행. 패키징 방식은 +서브에이전트 웹 리서치로 확인(`.luaurc` alias 런타임 미지원, wally 심볼릭 +링크/타입 문제, `Sleitnick/RbxUtil`의 모노레포+개별 wally.toml 선례, +`pesde`는 아직 이름) — 모노레포로 당장 진행, 나중에 실제 분리 결정. +패키지 경계는 사용자가 "base=인터페이스, roblox=구현"이라는 원칙을 명확히 +해서 확정 — Store/State/Source 온톨로지뿐 아니라 `process`/`retract` +디스패치 엔진, `LifetimeHandle`/`PerInstanceState` 인터페이스, Ref, Slot +코어 재조정 로직까지 전부 `quad-base`가 소유(다른 엔진에서도 재사용 +가능해야 한다는 전제, 엔진마다 큰 구현 중복 방지가 목적). Slot도 같은 +원칙 적용 확정, 그 과정에서 `k:number,v:Instance` 중첩 인스턴스 자식용 +`InstanceChild` 핸들러가 추가로 필요하다는 게 밝혀짐. `bind-system-plan.md`/ +`module-lifecycle-plan.md`/`slot-plan.md` 세 문서 모두 `research/`에서 +`base/`로 승격 완료, `base/architecture.md`에 전체 소스 트리가 문서화됨 — +실제 폴더/파일 스캐폴딩은 다음 세션(위 "지금 할 일" 1번). + +## 인수인계 메모 (2026-08-04 세션 종료 시점, 4차 라운드까지 반영) 2026-08-03에 확정됐다고 표시된 결정 전체(architecture.md 14개 + lifecycle- pattern/store-semantics/bind-system-plan/module-lifecycle-plan/slot-plan/ tween-plan)를 `AskUserQuestion`으로 하나씩 예/아니오 검증 완료 — 상세는 -`.claude/question.md`의 "2026-08-04 검증 라운드 완료" 절. 대부분 그대로 -확인됐지만, 검증 과정에서 사용자가 실시간으로 설계를 더 전개하면서 **"State -프리미티브는 안 만든다"는 기존 결정이 틀렸다는 게 밝혀짐** — Store/State/ -Source 온톨로지 전체가 이번 세션에서 새로 열린 가장 중요한 설계 스레드로 -떠올랐고, 아직 결론이 안 났음(위 "지금 할 일" 1번). 그 외 자잘한 정정들(Slot -retract=폐기 확정, Pipe COW 후보 폐기 등)은 각 문서에 바로 반영해둠 — 재조사 -불필요. +`.claude/question.md`의 "2026-08-04 검증 라운드 완료" 절. 검증 과정에서 +사용자가 실시간으로 설계를 더 전개하면서 **"State 프리미티브는 안 만든다"는 +기존 결정이 틀렸다는 게 밝혀짐** — Store/State/Source 온톨로지 전체가 이 +세션에서 새로 열린 가장 중요한 설계 스레드로 떠올랐음. + +**같은 날 이어진 2차/3차 라운드에서 그 온톨로지와 인체공학 질문 전부를 +확정함**: push-invalidate/pull-recompute 전파 모델(Fusion식 eager 노드/생성순 +정렬 불필요), `:Compute`의 self/with 인자를 둘 다 lazy State 핸들로 통일 +(별도 `ComputeWithout` 불필요), State는 쓰기 불가(값 쓰기는 Store의 +`__newindex`로만) 확정, `Source`는 Store 내부 디테일이 아니라 값 하나만 +다룰 때 쓰는 독립 공개 프리미티브로 격상, Slot 생존 확인 문제는 새 메커니즘 +없이 기존 canExecute 유틸 재사용으로 해소(부수 효과로 "Store가 Store를 담을 +때 이중 해제 방지 필요한가" 백로그 항목도 "명시적 dispose가 없어 질문 자체가 +성립 안 함"으로 닫힘), `store.key` dot-access를 타입 추론 1급 경로로 삼는 +관습을 인스턴스 생성까지 프로젝트 전역으로 확정, `RobloxFactory` 재호출 +가드(같은 팩토리=무시, 다른 팩토리=에러, `New()`와는 인스턴스별 테이블 +분리로 자연히 공존)까지 확정. + +**4차 라운드에서 사용자가 실제 참고 코드(`.claude/initreq/artworks/`, PA님 +작성 — UI 포함 전반적 설계 패턴을 시범 적용한 데모 모듈)를 공유해줘서 +교차검증**: "DI"는 Dependency Injection이 아니라 Declarative Instance였음 +(정정). 인스턴스 생성은 2트랙 구상보다 단순한 "제네릭 생성자 함수 하나 + +자주 쓰는 클래스만 정적 필드로 미리 바인딩" 모양으로 정정. **이벤트 +바인딩은 `On.EventName` 도트액세스를 접고 PA님 방식(평범한 문자열 키 + +`ReflectionService` 기반 자동 판별)으로 전환** — Store의 dot-access는 실질적 +타입 이득이 있어 그대로 유지, 이벤트만 예외. 전파 모델(push-invalidate/ +pull-recompute)과 라이프사이클(GC-native)은 PA님 코드가 반례처럼 보였으나 +(각각 파생 개념이 없는 단순 pub-sub, 전부 수동 해제) 재검토 후 **기존 +확정 유지** — 라이프사이클은 나중에 하이브리드로 확장 가능한 여지만 기록. +OOP 회피 결정은 PA님의 `class.luau`도 같은 체이닝 상속 문제를 보여 오히려 +보강됨. **더 이상 열려있는 핵심 설계 질문은 없음** — 남은 건 API 표면 이름뿐 +(위 "지금 할 일" 참고). 그 외 자잘한 정정들(Slot retract=폐기 확정, Pipe COW +후보 폐기 등)은 각 문서에 바로 반영해둠 — 재조사 불필요. 이전 세션(2026-08-03) 종료 시점 메모: `.claude/` 전체 스캐폴드 + 대부분의 핵심 아키텍처 결정을 완료, 로컬 git 저장소 초기화+첫 커밋(원격 없음,