diff --git a/.claude/README.md b/.claude/README.md index 2cbd6a9..fbdd944 100644 --- a/.claude/README.md +++ b/.claude/README.md @@ -31,7 +31,7 @@ | `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 패키지 경계까지 확정 | -| `modifier-plan.md` | Modifier는 런타임 plug 아닌 정적 merge, immutable+clone 기반 체이닝 — 메커니즘 확정, getter 이름만 남음 | +| `modifier-plan.md` | Modifier는 런타임 plug 아닌 정적 merge, immutable+clone 기반 체이닝 — 메커니즘 확정. **[2026-08-07 다섯 번째 세션 추가]** `:Apply(factory)` 팩토리 체이닝, `Override`(구 `Merge`) 값 결합+성능 기준, `:Peek`/`isState` 필드 읽기까지 전부 확정, 이름만 용어 정리 라운드까지 잠정 | | `purity-and-effects-plan.md` | 컴포넌트 "순수성"이 아니라 "이식성" 문제로 재정의 — 문서 경고 수준으로 확정 | | `component-composition-plan.md` | 컴포넌트=플레인 함수, State/Source 읽기·쓰기 경계, Source가 State를 구조적으로 만족 — modifier/Ref 컴포넌트 경계 통과까지 전부 확정, 남은 건 API 이름뿐. **[2026-08-07 정리]** 폐기된 `StoreSource` 프록시 설계로의 역전 이력은 본문에서 빼고 `archive/store-source-proxy-reversed.md` 포인터로 압축 | | `blocker-plan.md` | **[2026-08-07 신설]** `Blocker` — 여러 Source를 한꺼번에 바꿔도 파생값 재계산이 한 번만 되게, State 마일스톤(M3)과 함께 개발. 메커니즘+이름 확정 | diff --git a/.claude/base/bind-system-plan.md b/.claude/base/bind-system-plan.md index 1b524d1..0a678b7 100644 --- a/.claude/base/bind-system-plan.md +++ b/.claude/base/bind-system-plan.md @@ -1121,6 +1121,43 @@ Instance 참조 등 제한된 프리미티브 집합, 테이블 등 복합 타 확인 전 소견일 뿐 — `.claude/question.md`에 반영, 다음 세션에서 사용자 판단 필요. +## `isState(x): boolean` — State/Source 판별 predicate (2026-08-07 다섯 번째 세션) + +**배경**: `base/modifier-plan.md` 9번 절의 `:Peek<>(key): T|State|nil` +(Modifier 필드를 확정하지 않고 raw 그대로 읽는 접근자)이 나오면서, 사용자 +코드가 그 결과를 State/plain으로 분기하려면 판별 수단이 필요해짐 — 같은 +필요가 실은 새로 생긴 게 아니라 Modifier의 함수형 setter(`modifier-plan.md` +4-1번, "현재 필드가 State냐 plain이냐"로 동작이 갈림)가 지금까지도 내부적으로 +풀어야 했던 문제인데 그 판별 방법 자체가 문서에 명시된 적이 없었음 — 이번에 +`isState`로 명문화하며 그 구멍도 같이 메움. + +**Source도 같이 잡힘, 별도 `isSource` 불필요** — Source가 State를 구조적으로 +만족(위 "Source가 State를 만족함" 관련 내용은 `base/store-semantics.md` +참고)하므로, `isState(source) == true`가 자연스러운 동작이고 그걸로 충분함. + +**구현: weak-key 레지스트리, duck-typing 아님.** + +``` +local registry = setmetatable({}, {__mode = "k"}) +-- State/Source를 만드는 모든 생성 지점(Source(...), :With(...), :Compute(fn) 등)에서: +registry[newHandle] = true +-- predicate: +local function isState(x) + return registry[x] == true +end +``` + +**duck-typing(예: `type(x) == "table" and x.Compute ~= nil`)을 쓰지 않는 +이유**: `Peek`가 돌려주는 `T`는 Modifier 필드에 들어갈 수 있는 임의의 +값(테이블, Roblox userdata 등)이라 — 우연히 비슷한 모양의 필드/메소드를 +가진 `T`에 false positive가 나거나, 일부 Roblox userdata는 정의 안 된 키 +인덱싱 자체에서 에러를 던지므로 duck-typing이 `pcall`로 감싸야 하는 지저분한 +엔지니어링이 되거나 최악의 경우 그냥 엔진이 죽는 상황까지 생길 수 있음. +weak-key 레지스트리는 rbvm 네임스페이스 추적(`base/lifecycle-pattern.md`)과 +같은 이미 확정된 패턴 재사용이라 새 아이디어 아님 — weak 키라 등록된 State/ +Source가 GC되면 레지스트리 엔트리도 자동으로 사라짐(살려두는 목적의 강참조 +레지스트리인 Observer의 `:Subscribe` 레지스트리와는 반대 성격). + ## 남은 열린 질문 (`.claude/question.md`에도 취합) 이 문서의 핵심 설계 질문은 2026-08-04 세 라운드(전파 모델/`:Compute`/State diff --git a/.claude/base/component-composition-plan.md b/.claude/base/component-composition-plan.md index bada990..198fdff 100644 --- a/.claude/base/component-composition-plan.md +++ b/.claude/base/component-composition-plan.md @@ -238,25 +238,29 @@ Fusion/Vide(named prop 전달, `[Children]`류 예약 키)가 서로 다른 이 가야 하는지는 저작자가 자기 코드에 뭐라고 쓰느냐로 완전히 결정됨(자동 전파가 없기 때문에 성립하는 단순함). -### 3. 여러 modifier를 하나로 합치는 공개 유틸 필요 — `Modifier.Merge`(가칭) +### 3. 여러 modifier를 하나로 합치는 공개 유틸 필요 — `Modifier.Override`(가칭, 2026-08-07 다섯 번째 세션에서 `Merge`→개명, 동작 확정) caller가 named parameter 하나에 여러 modifier를 몰아넣고 싶을 때 (`Frame{modifier1, modifier2}`의 컴포넌트판)를 위해, 기존 flatten 규칙(배열 순서상 나중 것이 필드 단위로 이김, `modifier-plan.md` 2번)을 그대로 재사용하는 -결합 함수를 공개 API로 노출: `Modifier.Merge(mod1, mod2, ...) -> Modifier`. +결합 함수를 공개 API로 노출: `Modifier.Override(mod1, mod2, ...) -> Modifier`. 새 병합 규칙이 아니라 이미 확정된 flatten을 함수로 한 번 더 꺼내 쓸 수 있게 하는 것뿐 — **사용자 요청**("modifier를 합칠 방법도 존재한다면 좋을것 -같아"). `MyComp { Modifier = Modifier.Merge(theme, override) }` → 컴포넌트 +같아"). `MyComp { Modifier = Modifier.Override(theme, override) }` → 컴포넌트 내부는 항상 이미 합쳐진 단일 값만 받으므로 컴포넌트 저작자가 배열 처리를 신경 쓸 필요 없음. Ref는 필드 충돌 개념이 없어 이 문제 자체가 없음(여러 Ref를 받으면 그냥 전부 실행하면 됨, `modifier-plan.md` §4-2) — 별도 결합 -유틸 불필요. +유틸 불필요. **정확한 동작(baked 값 교체 경고, 순서 의존성, `Apply`와의 +역할 구분, `:Peek`/`isState`)은 `base/modifier-plan.md` 9번 절이 최종 +소스** — `Merge`로 전부 대체해 `Apply`만 강제하는 방안도 이번에 검토했으나, +이 3번 절에서 확정한 실사용 니즈(단일 named parameter 슬롯에 독립적으로 +만들어진 modifier 값들을 밀어넣는 경우)를 못 풀어서 기각됨. ## 남은 열린 질문 (`.claude/question.md`에도 취합, 전부 후순위 — 이름만 남음) - **정확한 API 이름**: `Component`(플레인 함수 규약이라 별도 래퍼가 필요한지 자체도 불확실 — 아마 불필요), `Source`/`State` 독립 생성자·타입 이름, - 컴포넌트 경계용 `props.Modifier`/`props.Ref` 필드명, `Modifier.Merge` + 컴포넌트 경계용 `props.Modifier`/`props.Ref` 필드명, `Modifier.Override` 함수명은 전부 가칭. (`GetSource` 계열 접근자는 위 3번 정정으로 아예 불필요해짐 — `store.key`가 직접 Source를 반환하므로 별도 접근자 자체가 없음.) `base/bind-system-plan.md`의 "남은 열린 질문" 절(정확한 함수/ diff --git a/.claude/base/modifier-plan.md b/.claude/base/modifier-plan.md index 476f8be..0e5894e 100644 --- a/.claude/base/modifier-plan.md +++ b/.claude/base/modifier-plan.md @@ -3,11 +3,14 @@ **상태**: base — 핵심 메커니즘(런타임 plug 아님/정적 merge, immutable 값+clone 기반 체이닝, 이중 setter)은 2026-08-04 세션 채팅 논의로 확정. **Getter는 별도로 안 만들기로 확정(2026-08-06 후속 세션)** — 아래 "4. -Setter는 리터럴 값과 변환 함수 둘 다 받음" 절 참고. Modifier가 컴포넌트 경계를 어떻게 -통과하는지(named parameter로 전달, multi-root 개념 폐기)는 별개 문제로 -**[정정] `research/component-composition-plan.md`는 2026-08-04 세션에 수렴 -완료돼 `base/component-composition-plan.md`로 승격됨 — 이 문서는 -"Modifier 값 자체가 어떻게 동작하는가"만 다룸. +Setter는 리터럴 값과 변환 함수 둘 다 받음" 절 참고. **팩토리 함수 체이닝 +(`:Apply`), 값 결합(`Override`, 구 `Merge`), 필드 읽기(`:Peek`)+판별 +(`isState`)은 2026-08-07 세션들에 걸쳐 확정 — 8/9번 절 참고, 한 줄 요약은 +`Apply`="변경을 수행", `Override`="이미 계산된 다른 mod를 합침".** Modifier가 +컴포넌트 경계를 어떻게 통과하는지(named parameter로 전달, multi-root +개념 폐기)는 별개 문제로 **[정정] `research/component-composition-plan.md`는 +2026-08-04 세션에 수렴 완료돼 `base/component-composition-plan.md`로 +승격됨 — 이 문서는 "Modifier 값 자체가 어떻게 동작하는가"만 다룸. ## 문제 @@ -247,6 +250,177 @@ setter 클로저(4번)와 이름이 겹치면 안 됨 — `__index`가 고정 합성하도록 구현. 따라서 **`Apply`는 Modifier 필드 이름으로 예약됨**(실제 스타일 프로퍼티 이름과 겹칠 일은 거의 없어 보이지만 문서화 필요). +**권장 관용구, 문서화 필요(2026-08-07 다섯 번째 세션)**: 특정 modifier를 +계속 변형/보정하고 싶은 경우(스타일 프리셋, 커링된 팩토리 등)엔 항상 +`Apply`를 기본 선택지로 유도할 것 — 아래 9번의 `Override`는 "이미 따로 +만들어진 modifier 값 두 개 이상을 합쳐야 하는" 경우로만 좁혀서 문서화(용도 +구분 절 참고). + +**`Apply`는 `factory(self)` 그 이상도 이하도 아님 — 특별한 계약 없음, +문서화 필요.** `factory` 내부가 `Peek`으로 State를 기대했는데 없다고 +`error`를 던지거나, 특정 조건에서 그냥 죽어버리는 것도 `Apply` 입장에선 +아무 문제 아님 — `Apply`는 `factory`가 뭘 하든 관여하지 않는 순수 함수 +호출 sugar일 뿐이라, 유효성 검사/기본값 처리/에러 핸들링은 전부 `factory` +저작자 책임. 문서에는 "`:Apply(f)`는 `f(mod)`를 체이닝 문법으로 쓴 것뿐, +Apply 자체가 뭔가를 검증하거나 보장해준다고 오해하지 말 것"을 명시. + +### 9. Modifier 결합 — `Modifier.Override(mod1, mod2, ...)`, `:Peek`, `isState` (2026-08-07 다섯 번째 세션) + +**배경**: `base/component-composition-plan.md` 3번 절이 이미 "여러 +modifier를 하나로 합치는 공개 유틸이 필요하다"고 확정하며 `Modifier.Merge` +가칭을 남겨뒀었음(컴포넌트 경계는 `props.Modifier` named parameter 단일 +슬롯이라, 리프 레벨 `Frame{mod1, mod2}` 배열 flatten이 거기까진 안 닿아서 +생기는 진짜 필요 — `Apply`만으로는 안 풀림: `Apply`는 팩토리 함수를 받는 +콤비네이터라, 이미 따로따로 만들어진 modifier *값* 두 개를 하나로 합치려면 +호출부가 그 값 중 하나를 즉석에서 팩토리로 다시 쓰도록 강제하게 됨 — +`Apply`로 완전 대체/강제 통합하는 방안도 이번에 검토했으나 이 실사용 +니즈를 못 풀어서 기각). 이번 세션에서 실제 동작을 확정. + +**이름 변경**: `Merge` → **`Override`로 확정**(사용자 제안). "Merge"는 +중립적 합침을 암시하지만 실제 동작은 명시적으로 나중 인자가 이기는 +"덮어쓰기"라, 이름이 의미를 정직하게 반영해야 함 — `component-composition-plan.md`의 +참조도 이번에 같이 갱신함. + +**용도를 좁게 문서화할 것 — "진짜 합칠 필요가 있는 경우"로 한정 +(2026-08-07 다섯 번째 세션, 사용자 강조).** `Override`는 범용 조합 +도구가 아니라 `Frame{mod1, mod2}`의 컴포넌트 경계판, 그 이상도 이하도 +아님 — 이게 없으면 `props.Modifier` 같은 단일 슬롯에 여러 독립 modifier +값을 넣을 방법이 아예 없어지므로 프리미티브로 남겨두는 것뿐. **"특정 +modifier를 계속 바꿔나가고 싶다"는 요구는 `Override`가 아니라 위 8번 +`Apply`로 풀도록 유도** — 간결한 커링/일급 함수 전달이 기본 관용구가 +되도록, API 문서에서 `Override`를 "값 두 개 이상을 합쳐야 하는 특수 +상황"으로만 소개하고 스타일 변형/보정의 기본 진입점으로는 절대 먼저 +보여주지 않을 것. + +**동작 = 기존 flatten을 함수로 노출한 것, 새 규칙 없음.** +`Modifier.Override(mod1, mod2, ...)`는 뒤 인자가 필드 단위로 이긴다(2번 +절 "배열 순서" 규칙과 동일). 구현은 단순 필드별 덮어쓰기 — 특별한 +State/함수 분기가 필요 없음: setter가 이미 호출 시점에 함수를 즉시 +실행하고 State 필드는 즉시 `:Compute`로 파생시켜 저장하므로(4번/4-1번), +Modifier가 들고 있는 모든 필드는 항상 "그 시점에 이미 완전히 처리된 +(baked) 값"임 — `Override`는 그 baked 값을 필드별로 그대로 교체할 뿐. + +**경고, 반드시 문서화**: baked 값 교체는 그 필드에서 파생된 다른 필드에 +소급 반영되지 않는다. 예: `Boldify`가 `Font` 필드를 읽어(`Peek`, 아래 +참고) `FontWeight`를 계산해 넣어둔 modifier를, 나중에 `Font`를 바꾸는 +다른 modifier와 `Override`로 합치면 `Font`는 새 값으로 바뀌지만 +`FontWeight`는 예전 `Font` 기준으로 계산된 채 그대로 남는다 — 사용자 +실수 범주지만 조용히 틀린 결과가 나오는 케이스라 API 문서(경고 박스)로 +명시 필요. `A:Override(B)`와 `B:Override(A)`가 다른 결과를 낸다는 순서 +의존성도 같은 경고 박스에 같이 명시. + +**용도 구분 — `Apply` vs `Override`, 둘 다 유지, 서로 대체 안 함**: +한 줄로 요약하면 **`Apply`는 "특정 대상에 대해 변경을 수행한다", `Override`는 +"특정 대상에 이미 계산된(baked) 다른 mod를 합친다"** — 문서화 시 이 한 +문장을 그대로 핵심 구분 기준으로 앞세울 것(2026-08-07 다섯 번째 세션, +사용자 정리). 재사용 가능한 스타일 "변형"(팩토리, 파라미터화 가능)은 +`Apply`, 독립적으로 이미 만들어진 modifier "값" 두 개 이상을 한 슬롯에 +밀어넣어야 하는 경우(주로 컴포넌트 경계)는 `Override`. + +**9-1. 판단 기준을 "이질적/동질적"이 아니라 "계산 의존성 유무"로 명시할 것, +`Apply`를 mutable로 바꾸는 방안은 기각 (2026-08-07 다섯 번째 세션 후속)** + +**동기**: `Apply` 체이닝이 호출마다 clone을 만들기 때문에, 항목 수천 개짜리 +리스트 UI처럼 무거운 Modifier를 대량으로 재생성하는 상황에서 이 clone +비용이 누적되는 게 아닌지 사용자가 우려 — 대안으로 (a) `Apply`/setter를 +아예 mutable로 바꾸는 방안, (b) `Override`를 "여러 값을 합칠 특수 상황"이 +아니라 "성능 최적화 수단"으로 승격하는 방안을 검토. + +**(a) `Apply`를 mutable로 바꾸는 방안 — 기각.** 3번 절에서 immutable+clone을 +확정한 이유가 정확히 "같은 modifier 레퍼런스를 공유하는 형제 서브트리가 +mutate로 오염되는 것"을 막기 위해서였음 — 이건 특정 세션 판단이 아니라 +2026-08-04부터 계속 지켜온 하드 제약. `Apply`/setter가 mutable이면 여러 +컴포넌트가 참조하는 공유 테마 상수 하나에 어느 한쪽이 체이닝만 해도 다른 +쪽까지 같이 바뀌는 클래스의 버그가 그대로 돌아옴 — clone 비용 절감이 +이 안전성보다 우선순위가 높다고 볼 근거가 없어 기각. (단, `table.clone`은 +Luau native shallow-copy라 Modifier 필드 수(한 자리~여남은 개) 기준 +개별 clone 비용 자체는 이미 3번 절에서 무시 가능하다고 판단됨 — 이번에 +새로 문제 삼는 건 "번 비용의 크기"가 아니라 "체인 길이 × 인스턴스 수로 +누적되는 clone *횟수*"라는 별개 축.) + +**(a-1) 절충안 — "`Apply` 진입 시 한 번만 clone하고 그 안에서는 +mutable로" — 검토했으나 기각.** clone 횟수를 체인 길이만큼이 아니라 +`Apply` 호출당 1번으로 줄이자는 아이디어(`Apply` 경계에서만 복사, 내부 +setter들은 그 복사본을 그대로 mutate). **기각 이유**: 이렇게 해도 버그 +클래스 자체가 안 없어짐 — `Apply`를 거치지 않고 setter를 직접 호출하는 +흔한 경로(`mod:FontSize(...)`처럼 체이닝 자체가 아니라 단발 호출)는 +여전히 mutable이라, 공유 레퍼런스에 대고 단발 setter 하나만 불러도(예: +서브트리 어딘가에서 폰트 두께만 살짝 바꾸는 경우) 그대로 오염됨 — +"`Apply` 안에서는 안전, 밖에서는 안 안전"처럼 **어디서 터지느냐만 +달라질 뿐 문제 자체는 그대로 남는 비일관적인 절충**이라 실익이 없음. +전부 clone하는 지금 방식이 버그 클래스를 균일하게 없애는 유일한 방법 — +확정. + +**(b) 판단 기준 — "동질적 vs 이질적 프로퍼티"가 아니라 "필드 간 계산 +의존성 유무"로 명시.** 사용자가 처음엔 "동질적(폰트 굵기 보정처럼 연관된 +속성끼리)은 `Apply`, 이질적(배경/텍스트/위치처럼 무관한 속성끼리)은 +`Override`"로 구분을 제안했으나, 실제 기준은 주제의 이질성 자체가 아니라 +**한쪽이 다른 쪽의 이미 baked된 값을 읽어야 하는가(`Peek`으로 데이터가 +흘러가는가)**임 — 이질적으로 보여도 계산 의존성이 있으면 `Apply`가 +맞고(예: "배경색에 맞춰 텍스트 명도를 자동 보정" — 배경/텍스트라는 이질적 +주제인데도 의존성이 있어 `Peek`+`Apply`가 필요), 반대로 동질적으로 보여도 +서로 완전히 독립이면(예: 여러 개의 `FontSize` 프리셋 중 하나를 통째로 +갈아끼우는 경우) `Override`도 무방함. `Override`는 필드 단위 raw 교체일 +뿐 `Peek`으로 값을 읽어 다른 필드에 반영하는 데이터 흐름이 아예 없으므로 +(위 "동작" 절), 계산 의존성이 있는 조합엔 애초에 못 씀 — 이게 진짜 판별 +기준. 문서에는 "이질적/동질적"이라는 표면적 구분 대신 이 기준으로 적을 것. + +**실제 최적화 권장 패턴**: 계산 의존성이 없고 재사용 가능한 조각(예: +배경 스타일 하나, 텍스트 스타일 하나, 레이아웃 위치 하나 — 각각 서로 +다른 서브시스템/모듈에서 한 번만 만들어지는 값)은 **모듈 상수/한 번만 +생성한 값으로 만들어두고, 인스턴스마다 `Override`로 결합**하는 게 +`Apply` 체인으로 매번 처음부터 다시 파생시키는 것보다 저렴함 — 조각 자체를 +매번 재계산 안 해도 되고, `Override`는 필드별 단순 복사 한 번으로 끝나서 +여러 단계 clone이 누적되는 `Apply` 체인보다 쌈. **주의**: 이건 "`Override`가 +내부적으로 값을 캐싱해준다"는 뜻이 아님 — `Override` 자체엔 캐싱/메모이제이션 +같은 새 메커니즘이 전혀 없고(순수 필드 복사), "캐싱"은 그냥 사용자가 조각 +Modifier 값을 변수/모듈 상수로 만들어 재사용하는, 기존에도 항상 가능했던 +평범한 값 재사용일 뿐 — 라이브러리에 새 캐싱 레이어를 추가하는 게 아니라는 +점을 문서에서 분명히 할 것(라이브러리 차원의 자동 메모이제이션은 지금 +검토 대상 아님 — 실제로 필요하다고 확인되면 그때 별도로 논의). + +**문서 배치**: 초심자 문서엔 `Override`를 아예 안 보여주고(위 "용도를 좁게 +문서화" 절), 이 "언제 `Apply` vs `Override`, 성능 기준" 절 전체는 api/심화 +문서 전용 — `research/documentation-content-map.md`의 modifier-plan.md +분류에 반영 완료. + +**미검토로 남긴 것**: `Apply` 체인이 실측으로 병목이라고 확인되면 그때 +"unsafe/fast-path mutable 빌더" 같은 별도 opt-in을 검토할 수 있으나, 지금은 +근거 없는 선제 최적화라 설계하지 않음 — CLAUDE.md의 "드문 오용/가상 미래 +요구까지 방어/최적화하려고 구조를 복잡하게 만들지 않는다" 원칙과 동일. + +**`:Peek<>(key): T | State | nil`** — Modifier 필드를 확정하지 +않고 그대로 읽는 접근자. 이름을 `Get`이 아니라 `Peek`로 정한 이유: 이 +프로젝트 전역에서 `State:Get()`은 "확정한다"(pull + recompute + 최종값 +반환)는 의미로 이미 자리잡았는데, Modifier의 읽기는 정반대(들고 있는 +그대로, State면 State 핸들 그대로) — 같은 동사를 반대 의미로 쓰면 안 +되므로 다른 이름 필요. 반환 타입을 `T`로 확정해 돌려주지 않고 +`T|State|nil` raw 그대로 노출하는 이유: 4-1번 절의 함수형 setter가 +받는 `old` 인자와 정확히 같은 원칙("현재 저장된 그대로 넘김") 재사용 — +자동으로 `:Get()`해서 `T`로 확정해버리면 반응성이 조용히 끊기는데 +타입엔 그 사실이 안 드러나서 위험함. `.RealValue.Font` 같은 별도 +인덱싱 표면은 기각 — 이미 `__index`가 필드 setter 합성용으로 예약돼 +있는데(`Apply`가 첫 예약 사례) 또 다른 프록시 네임스페이스를 얹으면 +setter 표면과 read 표면이 헷갈리고, 타이핑 이득도 메소드 방식과 별 +차이 없음. + +`:Peek`는 팩토리 함수(`Apply`에 넘기는 콤비네이터) 안에서 쓰는 게 +전형적 — "이 modifier가 현재 어떤 상태인지 보고 그걸 바탕으로 값을 +계산"한다는 문맥이 명확해서 오해 소지가 적음. Peek 결과가 State일 때 +그걸 즉시 읽어 스냅샷으로 쓸지, State 핸들을 그대로 물고 가 `:Compute`로 +새 파생 State를 만들지는 유저 선택 — 전자는 이후 원본이 바뀌어도 반영 +안 되는 캐비엇이 있지만, 이건 quad가 대신 풀어줄 문제가 아니라 문서화 +(경고)로 충분(이미 있는 "`Get()` 결과 캐싱 금지" 캐비엇과 같은 클래스). + +**`isState(x): boolean` 필요 — `base/bind-system-plan.md`에 정의**. +`Peek`가 raw union을 돌려주므로 사용자 코드가 State/plain을 분기하려면 +판별 수단이 필요함(Source가 State를 구조적으로 만족하므로 `isState`가 +Source도 같이 잡아줌 — 별도 `isSource` 불필요). 상세 근거/구현 방식은 +`bind-system-plan.md`의 `isState` 절 참고 — 요지만: duck-typing 대신 +weak-key 레지스트리 기반, 그리고 이 판별 로직 자체는 새로 만드는 게 +아니라 4-1번 setter 분기가 이미 내부적으로 해야 하는 걸 public 유틸로 +승격하는 것뿐. + ## 열린 질문 (`.claude/question.md`에도 취합) - **[해소됨]** Getter 정확한 이름/모양 — 2026-08-06 후속 세션에서 getter @@ -256,3 +430,6 @@ setter 클로저(4번)와 이름이 겹치면 안 됨 — `__index`가 고정 `base/component-composition-plan.md`에서 해소됨**(named parameter로 전달, "다중 루트" 개념 자체는 폐기) — 더 이상 열린 질문 아님, 이 문서가 다루는 "값 자체의 동작"과는 별개 문제였다는 점만 참고로 남김. +- `Override`/`Peek`/`isState` — 동작은 위 9번 절에서 확정, 정확한 이름은 + 다른 가칭들과 마찬가지로 `.claude/question.md`의 용어 정리 라운드까지 + 잠정. diff --git a/.claude/question.md b/.claude/question.md index d5d9e32..da44a8e 100644 --- a/.claude/question.md +++ b/.claude/question.md @@ -87,6 +87,11 @@ context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만** 즉시모드 GUI 뉘앙스), `List`(중립적이나 메커니즘을 안 알려줌) — 아직 미정, `research/additional-primitives-plan.md`의 "키 기반 동적 컬렉션 재조정" 절 참고. +- **`Override`/`Peek`/`isState`(3순위, 사소함, 2026-08-07 다섯 번째 + 세션 추가)**: Modifier 결합 유틸(구 `Merge`)과 필드 읽기 접근자, State/ + Source 판별 predicate 세 개의 이름 — 동작은 전부 확정(`base/ + modifier-plan.md` 9번, `base/bind-system-plan.md`의 `isState` 절), + 이름만 다른 가칭들과 같이 용어 정리 라운드에서 재검토. - **"프로바이더"(3순위, 사소함)**: `base/module-lifecycle-plan.md`가 "provider"라고 불러온, `isHandlable`로 참여 여부를 결정하고 우선순위대로 스캔되는 pluggable 참가자 개념 — 정확한 이름을 "provider"/"processor"/ @@ -197,8 +202,8 @@ context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만** | 프로바이더 패턴, bind/store 구현 책임 분리 | `base/module-lifecycle-plan.md` | | Slot 재조정, 재마운트 시 throw, retract=폐기 | `base/slot-plan.md` | | `Connected`+GC 라이프사이클 패턴 | `base/lifecycle-pattern.md` | -| Modifier(정적 merge, immutable 체이닝, State 필드 지원) | `base/modifier-plan.md` | -| 컴포넌트화(플레인 함수, State/Source 경계, 컴포넌트 경계 modifier/Ref는 named parameter로 전달, multi-root 개념 폐기, `Modifier.Merge`) | `base/component-composition-plan.md` | +| Modifier(정적 merge, immutable 체이닝, State 필드 지원, `Apply`/`Override`/`Peek`/`isState`) | `base/modifier-plan.md` | +| 컴포넌트화(플레인 함수, State/Source 경계, 컴포넌트 경계 modifier/Ref는 named parameter로 전달, multi-root 개념 폐기, `Modifier.Override`) | `base/component-composition-plan.md` | | 컴포넌트 이식성(전역 store 참조 시 재사용성 문제) | `base/purity-and-effects-plan.md` | | Blocker(값 기반 emit 지연/합치기) | `base/blocker-plan.md` | | Effect(leaf 죽음에 확정 정리 — 단 Observer와의 관계는 위 0번 열린 질문 참고) | `base/effect-plan.md` | diff --git a/.claude/research/documentation-content-map.md b/.claude/research/documentation-content-map.md index 0be0516..c05bcbb 100644 --- a/.claude/research/documentation-content-map.md +++ b/.claude/research/documentation-content-map.md @@ -74,7 +74,7 @@ v1 폐기 API/버그/구조 결함 전부 v2 설계를 정당화하는 내부 ### component-composition-plan.md / module-lifecycle-plan.md - 초심자: 컴포넌트=순수 함수 / 리프 프로퍼티엔 State만 바인딩 / `props.Modifier`/`props.Ref` named parameter 경계 전달 / `InitRoblox(Module)` 팩토리 초기화 -- api: State(파생, 읽기전용) vs Source(원본, 쓰기가능) 경계 요약(→심화) / Slot 반환 컴포넌트는 Modifier/Ref 파라미터 미선언 / `Modifier.Merge(mod1, mod2, ...)` 유틸 / Bind는 유일 슬롯(재호출 no-op, 충돌 에러, →심화) / `:With`/`:Compute`로 파생 State 생성 시그니처 / 모듈 싱글톤 스코프 +- api: State(파생, 읽기전용) vs Source(원본, 쓰기가능) 경계 요약(→심화) / Slot 반환 컴포넌트는 Modifier/Ref 파라미터 미선언 / `Modifier.Override(mod1, mod2, ...)` 유틸(구 `Merge`, `props.Modifier` 단일 슬롯용 특수 상황으로 한정 소개 — 아래 modifier-plan.md 절 참고) / Bind는 유일 슬롯(재호출 no-op, 충돌 에러, →심화) / `:With`/`:Compute`로 파생 State 생성 시그니처 / 모듈 싱글톤 스코프 - 심화: v1 `Extend` 자동 store 소유 폐지 이유(React 벤치마킹) / Source가 State를 구조적으로 만족하는 서브타입 설계(2026-08-06 후속 세션 — `StoreSource` 프록시 중간안은 폐기되고 이걸로 대체됨, `store-semantics.md` 참고) / named-parameter 경계 방식 채택 이유(Compose/Fusion/Vide/v1 선례 수렴) / 다중 루트 반환 개념 제거 근거 / 팩토리 초기화 패턴 채택 이유(RBVM `InitNamespace` 반례) / Store 책임 분리(base가 `LifetimeHandle` 소유) / v1 named 체이닝 연산 폐기 - skip: Compose/Fusion/Vide/v1 프레임워크 비교 원자료 / provider/processor 네이밍 미정 등 열린 질문 메모 @@ -86,8 +86,8 @@ v1 폐기 API/버그/구조 결함 전부 v2 설계를 정당화하는 내부 ### modifier-plan.md / slot-plan.md - 초심자: Modifier 기본 체이닝+merge 우선순위 규칙 실제 예시 / Slot 기본 개념(children 배열)+클래스가 슬롯 받는 방법(Named Slot 없음) / 마운트된 slot 재마운트 시 즉시 throw -- api: Setter가 리터럴/변환 함수 둘 다 받음(→심화: getter 없는 이유) / 필드가 State일 수 있는 4가지 조합 표(→심화: 반응성 유지/끊김 이유) / `Modifier.Rounded(8)` dot-access 생성자 관습 / Slot은 인스턴스당 여럿 가능 / 중첩 인스턴스 자식 처리 / retract 시 slot 내용 폐기(→심화: portal 없는 이유) -- 심화: 정적 merge vs 런타임 pluggable 기각 이유(CSS cascade) / immutable+clone 체이닝 이유(형제 오염 방지) / getter 미채택 이유 / `__index` 런타임 구현 통찰 / Modifier가 핸들러 계층을 모르는 이유 / base/roblox 패키지 경계(Dispatch/Slot vs Handlers/Slot) / Slot 단일 마운트 소유권이 v1/Fusion/Vide 대비 개선인 이유 / retract=폐기 확정 히스토리(portal 검토 후 기각) +- api: Setter가 리터럴/변환 함수 둘 다 받음(→심화: getter 없는 이유) / 필드가 State일 수 있는 4가지 조합 표(→심화: 반응성 유지/끊김 이유) / `Modifier.Rounded(8)` dot-access 생성자 관습 / Slot은 인스턴스당 여럿 가능 / 중첩 인스턴스 자식 처리 / retract 시 slot 내용 폐기(→심화: portal 없는 이유) / `:Apply(factory)` 기본 체이닝 관용구(→심화: 언제 `Apply` vs `Override`인지 성능 기준) / `:Peek<>(key)` + `isState`(→심화: `Get`과 이름을 다르게 한 이유) +- 심화: 정적 merge vs 런타임 pluggable 기각 이유(CSS cascade) / immutable+clone 체이닝 이유(형제 오염 방지) / getter 미채택 이유 / `__index` 런타임 구현 통찰 / Modifier가 핸들러 계층을 모르는 이유 / base/roblox 패키지 경계(Dispatch/Slot vs Handlers/Slot) / Slot 단일 마운트 소유권이 v1/Fusion/Vide 대비 개선인 이유 / retract=폐기 확정 히스토리(portal 검토 후 기각) / **왜 `Apply`가 기본이고 `Override`는 최적화 특수 케이스인가**(계산 의존성 있는 조합 vs 독립적 재사용 가능 조각의 병합 — 2026-08-07 다섯 번째 세션, `modifier-plan.md` 9번) / 왜 `Apply`가 clone 대신 mutate하지 않는가(형제 오염 방지가 개별 clone 비용 절감보다 우선) - 열린 질문(문서화 보류): 여러 Slot이 형제로 섞일 때 순서 보장 — 미확정 - skip: 세션 날짜/확정 이력, 문서 승격/정정 안내 diff --git a/.claude/research/pre-implementation-audit.md b/.claude/research/pre-implementation-audit.md index c3302cc..5d546a2 100644 --- a/.claude/research/pre-implementation-audit.md +++ b/.claude/research/pre-implementation-audit.md @@ -340,24 +340,29 @@ API"를 얹어야 할 때 Slot의 "own한 것만 CRUD 대상" 불변식 자체 `existing-instance-bind-plan.md`에 한 줄 추가해둘 것 — 기존 "Modifier flatten과 긴장" 캐비엇 옆에 병기. -### 2-5. `Modifier.Merge` 시 서로 다른 클래스의 Modifier가 섞이는 게 허용되는지 미정 +### 2-5. `Modifier.Override`(구 `Merge`) 시 서로 다른 클래스의 Modifier가 섞이는 게 허용되는지 — **런타임은 해소, 타입 레벨은 여전히 미정** -**위치**: `base/modifier-plan.md` "2. Merge 우선순위" + `base/ -component-composition-plan.md` 3번(`Modifier.Merge`). +**위치**: `base/modifier-plan.md` "2. Merge 우선순위" + 9번(`Override`) + +`base/component-composition-plan.md` 3번. -**문제**: `question.md`가 언급하는 `DI.FrameModifier`류 "클래스별 타입 -프리픽스"를 보면 Modifier는 대상 인스턴스 클래스별로 타입이 나뉜다. merge는 -"필드명 기준, 나중 게 이김"이라고만 확정돼 있는데 — `Modifier.Merge`의 -가장 그럴듯한 실사용 시나리오가 바로 "공통 테마 Modifier(여러 GuiObject -클래스에 걸친 공통 필드) + 클래스별 override Modifier"를 합치는 것이다. -두 Modifier가 정적으로 다른 타입이면 `Modifier.Merge` 시그니처가 애초에 -타입 에러로 막아주는지(같은 제네릭 파라미터만 merge 허용), 아니면 필드명만 -보고 런타임에 섞이는 순수 데이터 레이어라 다른 클래스끼리도 그냥 합쳐지는 -지가 전혀 명시돼 있지 않다. +**[2026-08-07 다섯 번째 세션 갱신] 런타임 동작은 이제 명확함**: `modifier-plan.md` +9번에서 `Override`가 "필드별 raw 덮어쓰기"로 확정됐고, "Modifier는 핸들러 +계층을 모름 — 순수 데이터 merge 레이어"(1번 절) 원칙도 이미 있었으므로, +**런타임 레벨에서는 대상 클래스가 다른 Modifier끼리 `Override`해도 막을 +이유가 없음**(필드명만 보고 그대로 덮어쓸 뿐 — Luau 타입은 런타임에 +강제되지 않는다는 점은 `store-semantics.md`에도 이미 명시된 전제). -**제안**: Modifier가 target 클래스별 제네릭 타입(`Modifier` 등)인지, -있다면 계층 구조(공통 base + 클래스별 확장)가 뭔지, `Modifier.Merge`가 -이걸 타입으로 강제하는지 최소 한 문장으로 확정할 것. M7 착수 시. +**여전히 미정인 것 — 타입 레벨**: Modifier가 target 클래스별 제네릭 +타입(`Modifier` 등)이라면, `Modifier.Override(mod1: Modifier, +mod2: Modifier): Modifier`처럼 같은 `T`만 받도록 타입으로 강제할지, +아니면 공통 base 타입(여러 GuiObject 클래스에 걸친 공통 필드)과 클래스별 +확장 사이의 계층 구조를 별도로 두고 `Override`가 그 계층을 넘나들 수 +있게 할지는 아직 결정된 바 없음 — "공통 테마 Modifier + 클래스별 override +Modifier를 합친다"는 시나리오가 `Override`의 가장 그럴듯한 실사용 +예시라 이 타입 설계가 실제로 막히면 바로 걸릴 문제. + +**제안**: Modifier의 클래스별 typed 생성자 계층(2-8번과 같은 지점) 설계 +시 `Override`의 제네릭 시그니처도 같이 확정할 것. M7 착수 시. ### 2-6. Modifier 필드에 State/Source를 인자로 넘기는 케이스가 세터 표에서 빠짐 @@ -377,10 +382,10 @@ component-composition-plan.md` 3번(`Modifier.Merge`). ### 2-7. 여러 Ref를 하나의 named parameter로 넘길 때 nested-array flatten 여부 불명 -**위치**: `base/component-composition-plan.md` 3번(`Modifier.Merge`) vs -"Ref는... 별도 결합 유틸 불필요" 문장. +**위치**: `base/component-composition-plan.md` 3번(`Modifier.Override`, +구 `Merge`) vs "Ref는... 별도 결합 유틸 불필요" 문장. -**문제**: Modifier는 여러 개를 합치려면 `Modifier.Merge`가 명시적으로 +**문제**: Modifier는 여러 개를 합치려면 `Modifier.Override`가 명시적으로 필요한데, 바로 다음 문장은 Ref는 "여러 Ref를 받으면 그냥 전부 실행하면 됨 — 별도 결합 유틸 불필요"라고 한다. `props.Ref = {ref1, ref2}`처럼 배열을 넘기면 리프 디스패처가 그 중첩 배열을 재귀적으로 펼쳐서 각 Ref를 @@ -402,7 +407,7 @@ component-composition-plan.md` 3번(`Modifier.Merge`). 것)은 "DI 쪽 '제네릭 생성자 함수 하나 + 자주 쓰는 것만 정적 필드' 패턴 재사용"이라 문서 스스로 밝히듯 quad-roblox의 DI 타입 생성 계층(M5)에 강하게 결합돼 있다. 그런데 `ROADMAP.md` M7 체크리스트(flatten-before-dispatch, -`Modifier.Merge`, `State` 차단)엔 이 클래스별 타입 생성 작업이 +`Modifier.Override`, `State` 차단)엔 이 클래스별 타입 생성 작업이 전혀 없고, M5 DI 체크리스트에도 Modifier 언급이 없음. **제안**: M5 또는 M7 체크리스트에 "quad-roblox 클래스별 typed Modifier diff --git a/CLAUDE.md b/CLAUDE.md index 7f4e13e..ada599e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -740,6 +740,102 @@ M7 착수 시 `modifier-plan.md` 8번 참고하면 됨. `TweenInfo.new(...)` 포지셔널 생성자가 읽기 어렵다는 문제의식, 소견은 편의 필드 쪽이지만 확정 아님, 나중 논의 대상으로만 남김. +## 2026-08-07 다섯 번째 세션 — Modifier 결합(`Override`)/읽기 접근자(`Peek`)/`isState` 확정, FuncSource 기각 사유 문서화 + +**출발점**: `:Apply`(4번째 세션 신설)처럼 Modifier에 더 있으면 좋을 게 +있는지 사용자가 제기 — `Merge`류 결합 유틸의 우선순위 문제, 그리고 +Modifier 자신이 자기 필드 값을 못 읽는 게 애매하다는 지적(예: +`Boldify`가 폰트별 굵기 보정을 하려면 현재 `Font` 필드를 읽어야 함). +같은 스레드에서 "Source가 항상 정해진 값만 담아야 하는 이유가 확정된 +건지, FuncSource(람다로 계산+self-emit하는 Source) 같은 건 왜 없는지"도 +같이 물어옴. + +**핵심 결론(전부 base 문서에 반영 완료)**: +- **`Modifier.Override(mod1, mod2, ...)`** — `component-composition-plan.md` + 3번 절에 2026-08-04부터 가칭 `Merge`로 이미 확정돼 있던 결합 유틸의 + 실제 동작을 확정하고 이름을 `Override`로 개명(중립적 "합침"이 아니라 + 명시적 "덮어쓰기"라 이름이 의미를 정직하게 반영해야 함). 뒤 인자가 + 필드 단위로 이김(기존 배열 flatten 규칙 재사용), 구현은 단순 필드별 + raw 교체 — setter가 이미 호출 시점에 함수/State를 즉시 처리해 저장하므로 + Modifier 필드는 항상 baked 값이라 특별한 분기 불필요. "baked 값 교체는 + 거기서 파생된 다른 필드에 소급 반영 안 됨"(Boldify가 FontWeight를 계산해 + 둔 뒤 Font가 Override로 바뀌어도 FontWeight는 예전 값 그대로)과 순서 + 의존성(`A:Override(B)` ≠ `B:Override(A)`) 둘 다 문서 경고 대상으로 확정. + **`Apply`로 전부 대체해 `Override`를 없애는 방안도 검토했으나 기각** — + 컴포넌트 경계(`props.Modifier`는 단일 named parameter라 배열 flatten이 + 안 닿음)라는 이미 확정된 실사용 니즈를 `Apply`만으로는 못 풀어서. + `base/modifier-plan.md` 9번 절. +- **`:Peek<>(key): T|State|nil`** — Modifier 필드를 확정(pull+recompute) + 하지 않고 raw 그대로 읽는 접근자. `Get`이 아니라 `Peek`인 이유는 이 + 프로젝트에서 `State:Get()`이 이미 "확정한다"는 의미로 굳어져 있어서 — + Modifier의 읽기는 정반대(State면 State 핸들 그대로) 동작이라 같은 + 동사를 못 씀. 반환 타입을 `T`로 자동 확정하지 않고 union 그대로 + 노출하는 이유는 4-1번 절 함수형 setter의 `old` 인자와 같은 원칙("현재 + 저장된 그대로 넘김") 재사용 — 자동 확정하면 타입에 안 드러나는 채로 + 반응성이 조용히 끊김. `.RealValue` 같은 별도 인덱싱 표면은 기각(이미 + `__index`가 setter 합성용으로 예약돼 있어 표면이 겹침). +- **`isState(x): boolean`** — `Peek`의 raw union을 분기하려면 필요. + Source가 State를 구조적으로 만족하므로 이거 하나로 Source도 같이 + 잡힘(`isSource` 불필요). duck-typing 대신 weak-key 레지스트리로 구현 + (rbvm 네임스페이스 추적과 같은 패턴 재사용) — `Peek`가 돌려주는 `T`가 + 임의의 테이블/userdata일 수 있어 duck-typing은 false positive나 일부 + Roblox userdata의 인덱싱 에러(pcall 필요)로 이어질 위험이 있음. 이 + 판별 로직 자체는 새 개념이 아니라 4-1번 setter가 이미 내부적으로 + 해야 했던 "필드가 State냐 plain이냐" 판별을 public 유틸로 승격한 것. + `base/bind-system-plan.md`의 `isState` 절. +- **FuncSource(값이 람다로 계산되고 self-emit하는 Source) 기각** — 사용자가 + 스스로 기각 논리를 제시했고("이미 Compute가 커버함"), 검증 결과 이미 + 확정된 두 원칙에서 그대로 연역됨: (1) Source는 "시작점"이라 다른 + 반응형 값에 자동 연결 안 됨(2026-08-04 6차 라운드, "Store가 Store를 + 담지 않는다" 확정 때 나온 원칙) — FuncSource는 다른 반응형 값에 종속된 + 계산이면서 겉으로는 origin인 척하는 것이라 이 원칙과 직접 충돌. + (2) `:With`가 clone 빌더가 아니라 진짜 노드여야 하는 이유(2026-08-07 + 세 번째 세션)가 "의존성이 구조적으로 안 보이면 디버그 그래프가 + 깨진다"였는데, FuncSource의 람다가 클로저로 캡쳐한 의존성은 정확히 + 그 문제를 재현함. 실제로 커버 안 되는 유스케이스도 없음 — "다른 + 반응형 값에서 계산"은 `Compute`, "clone 불가능한 값을 밖에서 바꾸고 + 알림"은 원천 Source+`Emit`으로 이미 전부 커버됨. 새 결정이 아니라 + 기존 확정 사항의 논리적 귀결이라 별도 base 절 신설 없이 여기 세션 + 요약으로만 기록(quadnomicon 소재로 재사용 가능하도록). + +**같은 세션 바로 후속 — 문서화 톤 보강(사용자 강조)**: `Override`는 범용 +조합 도구가 아니라 `Frame{mod1, mod2}`의 컴포넌트 경계판(단일 named +parameter 슬롯에 독립적으로 만들어진 값 두 개 이상을 넣어야 하는 특수 +상황)으로 좁게 문서화할 것 — "특정 modifier를 계속 바꿔나간다"는 요구는 +항상 `Apply` + 커링/일급 함수 전달을 기본 관용구로 유도. `Apply` 자체도 +`factory(self)` 호출 sugar 그 이상이 아니라는 걸 명시 — `factory`가 +`Peek`한 값이 기대와 다르면 `error`를 던지든 뭘 하든 전부 `factory` +저작자 책임, `Apply`가 검증/보장을 대신 해준다고 오해하면 안 됨. 둘 다 +`base/modifier-plan.md` 8/9번 절에 반영 완료. + +**같은 세션 두 번째 후속 — `Apply` vs `Override` 성능 기준 확정.** +"무거운 Modifier를 대량 생성할 때 `Apply`의 clone 비용이 누적되지 +않냐"는 우려에서 두 방안 검토 후 결론: **`Apply`를 mutable로 바꾸는 +방안은 기각**(3번 절 immutable 확정 이유 — 형제 서브트리 오염 방지 — +가 clone 비용 절감보다 우선순위 높음, 재확인). 대신 **판단 기준을 +"이질적/동질적 프로퍼티"가 아니라 "필드 간 계산 의존성 유무"로 +명확화** — 한쪽이 `Peek`으로 다른 쪽의 baked 값을 읽어 반영해야 하면 +이질적으로 보여도 `Apply`, 서로 완전히 독립이면 동질적으로 보여도 +`Override` 가능. 계산 의존성 없는 재사용 조각(배경/텍스트/레이아웃처럼 +서로 다른 서브시스템이 한 번만 만드는 값)은 모듈 상수로 만들어두고 +인스턴스마다 `Override`로 결합하는 게 실제 최적화 패턴 — 단 이건 +"`Override`가 내부적으로 캐싱해준다"가 아니라 사용자가 값을 재사용하는 +평범한 패턴일 뿐, 라이브러리에 새 캐싱 레이어가 생기는 게 아님을 +문서에 명시하기로 함. `base/modifier-plan.md` 9-1번 절. + +**같은 세션 세 번째 후속 — "`Apply` 경계에서만 clone, 안쪽은 mutable" +절충안도 검토 후 기각.** clone 횟수를 체인 길이가 아니라 `Apply` 호출당 +1번으로 줄이는 절충을 사용자가 직접 제시했으나, `Apply`를 거치지 않고 +setter를 단발로 직접 호출하는 흔한 경로는 여전히 mutable이라 공유 +레퍼런스가 그대로 오염될 수 있음(서브트리에서 폰트 두께만 바꿔도 터짐) +— "어디서 터지느냐만 달라지는" 비일관적 절충이라 실익 없다고 판단해 +기각. 전부 clone하는 현재 방식 유지 확정. `base/modifier-plan.md` +9-1번 (a-1) 절. + +**다음 세션이 할 일**: 안 바뀜(위 2026-08-06 네 번째 세션 절 참고, +`ROADMAP.md` M0부터) — 이번 세션도 순수 설계 확정이라 M0 착수 우선순위 +자체는 그대로. + **미해결로 남긴 것 — 임의로 결론내지 않음**: Effect가 `state:Effect()` 형태로 Observer를 확장하는 변형인지, 완전히 독립된 free function인지가 불명확함(사용자가 "확인 필요, 아니라면 논의해야 할 상태로 남겨두라"고 diff --git a/ROADMAP.md b/ROADMAP.md index 751eaf4..dfedd72 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -92,10 +92,14 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 ## M7 — Modifier - [ ] flatten-before-dispatch, immutable `table.clone` 체이닝 -- [ ] `Modifier.Merge`(가칭) +- [ ] `Modifier.Override(mod1, mod2, ...)`(가칭, 구 `Merge`) — 필드별 raw + 덮어쓰기, 특별한 State/함수 분기 불필요(`modifier-plan.md` 9번) - [ ] `State` 조합 타입 차단 확인(`modifier-plan.md` 7번, UB 확정) - [ ] `:Apply(factory)` 팩토리 함수 체이닝(`modifier-plan.md` 8번, 예약 키 `Apply`가 제네릭 `__index` 필드 setter와 안 겹치는지 확인) +- [ ] `:Peek<>(key): T|State|nil` 필드 읽기 접근자 + + `isState(x): boolean`(weak-key 레지스트리 기반, quad-base 공용 + 유틸 — `modifier-plan.md` 9번, `bind-system-plan.md`의 `isState` 절) - [ ] 인라인 키로 modifier 필드를 명시적으로 지우는 문제 확인 — `None` (가칭) 센티널 프리미티브 도입 여부(`modifier-plan.md` 2-1번, 아직 미정 — 착수 전 사용자 확인 필요, 확정 안 되면 이번 마일스톤은