diff --git a/.claude/base/architecture.md b/.claude/base/architecture.md index 12700f8..998a51e 100644 --- a/.claude/base/architecture.md +++ b/.claude/base/architecture.md @@ -191,10 +191,16 @@ CollectionService 태그 등)를 흉내낼 필요가 없고, quad-base 자체 ## Store/State/Source 온톨로지 — 확정됨 (요약) Store는 source(실제 값이 존재하는 단일 지점) 집합체이고, `store.key`로 -접근하면 Store 생성 시 이미 만들어둔 그 Source 객체를 그대로 반환한다 -(별도 wrapper 없음 — **[2026-08-06 후속 세션 정정]** 원래 "매번 새 State를 -감싸 반환"이었으나, `Source`가 구조적으로 `State`를 만족하도록 재구성되며 -wrapper 계층 자체가 불필요해짐). 전파는 push-invalidate(신호만)/ +접근하면 이미 만들어져 있는 Source 객체를 그대로 반환하거나(defaults로 +Store 생성 시 미리 만들어둔 경우), 아직 없으면 그 자리에서 만들어 저장한 +뒤 반환한다(별도 wrapper 없음 — **[2026-08-06 후속 세션 정정, 2026-08-07 +추가 정정]** 원래 "매번 새 State를 감싸 반환"이었으나, `Source`가 구조적으로 +`State`를 만족하도록 재구성되며 wrapper 계층 자체가 불필요해짐. 이후 +"Store 생성 시 전부 eager하게만 만들어진다"로 한 차례 더 정리됐다가, Luau +타입이 런타임에 강제되지 않아 defaults 없이 만든 키를 나중에 `:Set()`하면 +크래시난다는 점이 지적돼 lazy `__index`+저장 생성도 같이 필요함이 확인됨 — +상세는 `base/store-semantics.md` "Source가 State를 만족함" 절). 전파는 +push-invalidate(신호만)/ pull-recompute(`Get()` 시점) — Fusion식 eager 노드 없이도 다이아몬드 의존성 중복 재계산 문제가 풀림. State는 쓰기 대상이 아니고, 값을 쓰는 경로는 `source:Set(value)`(Source가 State보다 넓은 인터페이스를 가짐 — diff --git a/.claude/base/bind-system-plan.md b/.claude/base/bind-system-plan.md index 300076f..506850b 100644 --- a/.claude/base/bind-system-plan.md +++ b/.claude/base/bind-system-plan.md @@ -535,9 +535,14 @@ read ...`처럼, State끼리 자유롭게 합성/파이핑 가능한 것이 최 **구조적으로 State를 만족(단방향 호환)** — `.value`/`:Get()`/`:With`/`:Compute` 전부 지원 위에 `:Set(value)`/`:Emit()` 추가. - **Store** — Source들의 이름 붙은 모음, 그 이상 아님. `store.a`처럼 키로 - 접근하면 Store 생성 시 이미 만들어둔 **그 Source를 그대로 반환**(더 이상 - 별도 State wrapper를 매번 만들거나 따로 캐싱하지 않음 — Source 자체가 - 이미 State를 만족하므로 wrapper 계층 자체가 불필요해짐). + 접근하면 **이미 만들어진 Source가 있으면 그대로 반환, 없으면 그 자리에서 + 만들어 저장한 뒤 반환**(더 이상 별도 State wrapper를 매번 만들거나 따로 + 캐싱하지 않음 — Source 자체가 이미 State를 만족하므로 wrapper 계층 + 자체가 불필요해짐. **[정정, 2026-08-07]** "Store 생성 시 전부 eager하게만 + 만들어진다"는 이전 서술은 부정확 — `defaults`가 선택이고 Luau 타입이 + 런타임에 강제 안 되므로, 생성 시점 eager 생성(각 `defaults` 키)과 + `store.key` 접근 시점 lazy 생성(아직 없는 키를 그 자리에서 만듦)이 둘 다 + 필요함, 상세는 `base/store-semantics.md` 참고). - **State** — source(또는 다른 state)의 결과를 캐싱만 하는 존재, 자기 고유의 독립적 value 개념이 없음. `state(state)`로 기존 state의 결과를 받아 새 state를 만들어 분기 가능 — 이게 사실상 Unix 파이프 영감의 "State끼리 @@ -658,12 +663,18 @@ Modifier처럼 플래튼하지 않는가"는 설계 근거를 알고 싶은 사 `Ref()`로 안 만들어질 특별한 이유는 없었고(이전 절에서 API 모양만 다루고 생성자를 명시 안 해서 생긴 공백), `architecture.md`의 "복사 구현 지양, 팩토리 함수로 대체" 원칙과도 정확히 일치. `Store({defaults})`도 - 같은 스타일로 지원(안 하고 `Store()`만 있어도 되지만, 구현이 쉬우면 - 지원) — 내부적으로 입력 테이블을 그대로 들고 있지 않고 `__real`/ - metatable 저장 + `__newindex`/`__index` 프록시로 감싸면 됨. 이후 - 사용자가 그 defaults 테이블 원본을 직접 mutate하는 건 UB로 둠(방어 - 로직 불필요 — 오늘 세션에서 반복 확인된 "드문 오용 케이스를 위해 - 구조를 복잡하게 만들지 않는다"는 태도와 일치). + 같은 스타일로 지원(`defaults`는 선택 — 안 주고 `Store()`만 호출해도 + 됨, 순수 편의용 초기값 템플릿). + **[정정, 2026-08-07]** 아래 두 문장은 이후 라운드에서 정정된 옛 서술 — + 실제 메커니즘·mutate 취급은 `base/store-semantics.md` "Source가 State를 + 만족함" 절이 최종 소스: (a) "`__newindex`/`__index` 프록시로 감싸면 + 됨"은 이후 `store.key = value` 쓰기 문법 자체가 `:Set()`으로 옮겨가며 + `__newindex`는 더 이상 관여 안 함(읽기 쪽 `__index`는 "없으면 그 자리에서 + Source를 만들어 저장"하는 lazy 생성 용도로 여전히 필요, 위 store-semantics.md + 참고). (b) "defaults 테이블 원본을 직접 mutate하는 건 UB로 둠"도 최신 + 모델과 안 맞음 — `defaults`는 라이브 백킹 스토리지가 아니라 "아직 안 + 만들어진 Source를 만들 때 참고하는 초기값 템플릿"으로만 쓰이므로, 생성 + 후 원본을 바꿔도 문제없고 UB가 아님. **Slot 생존 확인 — 별도 메커니즘 아님, `canExecute` 재사용으로 확정** diff --git a/.claude/base/store-semantics.md b/.claude/base/store-semantics.md index 3ff65d9..5ff73c8 100644 --- a/.claude/base/store-semantics.md +++ b/.claude/base/store-semantics.md @@ -118,14 +118,30 @@ State를 만족하도록 만들고, RefSource라는 별도 타입은 폐기**하 `__index` 트릭(`base/modifier-plan.md`)과 같은 패턴이라 로직 중복이 생기지 않음. - **`RefSource` 같은 별도 타입은 불필요, `Store({defaults})`가 - 내부적으로 `{[key] = Source(default), ...}`나 다름없게 됨** — `store.key`는 - Store 생성 시 이미 만들어둔 Source를 그대로 돌려줄 뿐, 매번 새로 만들거나 - 별도로 캐싱할 wrapper 객체 자체가 없음. 이전에 검토했던 "State를 - weak table로 캐싱" 절충안보다 더 싸다(래퍼 생성/캐싱 단계 자체가 - 사라짐). v1이 모든 값을 Store 하나에 몰아넣던 습관은 "당시 정적 타입이 - 없어 단순하게 쓰는 게 편해서"였다는 게 사용자의 회고적 재평가 — 지금은 - 타입이 핵심 제약이라 그 전제 자체가 더 이상 안 맞고, 이번 정리로 Store는 - "이름 붙은 Source 모음, 그 이상 아님"으로 더 단순해짐. + 내부적으로 `{[key] = Source(default), ...}`나 다름없게 됨.** + `defaults`는 **선택**(안 줘도 됨, 순수 편의용) — `store.key`는 이미 + 만들어져 있는 키면 그 Source를 그대로 돌려주지만, **아직 안 만들어진 + 키면 그 자리에서 `Source(defaults의 해당 값 또는 nil)`을 만들어 저장한 + 뒤 돌려줌**([정정, 2026-08-07] eager 생성만으로 충분하다고 서술했던 + 이전 버전은 부정확 — Luau 타입은 런타임에 강제되지 않으므로 + `Store<>()`처럼 defaults 없이 만든 뒤 `.Key:Set(v)`를 부르는 + 경우, `__index`가 "없으면 그 자리에서 만들어 저장"까지 해주지 않으면 + `.Key`가 `nil`이라 크래시남 — 그래서 Store 생성 시점의 eager 생성(각 + `defaults` 키마다 미리 만들어둠, 이건 여전히 필요)과 `store.key` 접근 + 시점의 lazy 생성(아직 없는 키를 그 자리에서 만들어 저장, 이후 재접근은 + 재생성 없이 그대로 반환)이 **둘 다** 필요함). `defaults` 테이블 자체는 + 라이브 백킹 스토리지로 쓰이지 않고 "아직 안 만들어진 Source를 만들 때 + 참고하는 초기값 템플릿"으로 반복 참조될 뿐이라, Store 생성 후 원본 + `defaults` 테이블을 밖에서 바꿔도 문제없음(UB 아님 — 이 항목도 + `bind-system-plan.md`에 남아있던 "defaults 테이블 직접 mutate는 UB"라는 + 옛 서술과 충돌해 2026-08-07에 같이 정정함, 아래 참고). 별도 `__values`류 + 그림자 실값 저장소도 불필요 — Source 객체 자체가 저장소 역할을 함. 이 + 모델은 이전에 검토했던 "State를 weak table로 캐싱" 절충안보다 더 싸다 + (래퍼 생성/캐싱 단계 자체가 사라짐). v1이 모든 값을 Store 하나에 + 몰아넣던 습관은 "당시 정적 타입이 없어 단순하게 쓰는 게 편해서"였다는 + 게 사용자의 회고적 재평가 — 지금은 타입이 핵심 제약이라 그 전제 자체가 + 더 이상 안 맞고, 이번 정리로 Store는 "이름 붙은 Source 모음, 그 이상 + 아님"으로 더 단순해짐. - **이 서브타입 관계는 `quad2-try`에서 기각한 컴포넌트/클래스 OOP 상속과는 다른 층위.** 그때 금지한 건 사용자가 짜는 컴포넌트 계층 구조(`Class:Extend()`류 매직)였고, 지금은 두 프리미티브 타입 사이의 구조적 서브타이핑(런타임 diff --git a/.claude/question.md b/.claude/question.md index 2acd39e..1a5e265 100644 --- a/.claude/question.md +++ b/.claude/question.md @@ -92,6 +92,17 @@ additional-primitives-plan.md`. 요지: 주기가 없다"는 기존 원칙과 이름이 충돌해 보일 수 있음), `Draw`(짧지만 즉시모드 GUI 뉘앙스), `List`(중립적이나 메커니즘을 안 알려줌) — 아직 미정, `research/additional-primitives-plan.md` 1번 절 참고. +- **State/Source 읽기 접근자 — `.value` vs `:Get()`/`:Pull()`(3순위, + 2026-08-07 추가)**: 지금 `.value`는 `:Get()`을 감싼 계산 속성이고 "관용적 + 표기"로 앞세워져 있는데, 이게 quad가 명문화한 "관측해야 실체화된다" + 원칙을 프로퍼티 문법이 무디게 만드는 건 아닌지 재검토 중. `Finalize`는 + quad가 이미 "정리/해제"쪽 의미로 자리 잡아둔 단어(Effect의 leaf-death + cleanup, `retract`, `Destroying`)와 충돌 위험이 있어 기각 — 대신 + `Pull()`(아키텍처 문서 자체가 이 메커니즘을 부르는 "push-invalidate/ + pull-recompute"와 일치)을 검토했다가, 사용자가 `Get()`/`:Set()` 동사 + 짝을 맞추는 쪽이 더 나을 수 있다고 재고 중 — **아직 결정 안 됨**, + `.value` 자체를 유지할지(Ref처럼 진짜 non-lazy인 것에만 남길지)도 같이 + 볼 것. - **"프로바이더"(3순위, 사소함)**: `base/module-lifecycle-plan.md`가 "provider"라고 불러온, `isHandlable`로 참여 여부를 결정하고 우선순위대로 스캔되는 pluggable 참가자 개념 — 정확한 이름을 "provider"/"processor"/