From 4c5ee1b66a47e69fa18405ed8884f8e20d20de6c Mon Sep 17 00:00:00 2001 From: qwreey Date: Wed, 12 Aug 2026 20:24:04 +0900 Subject: [PATCH] =?UTF-8?q?decide(dispatch,modifier,store):=20pre-implemen?= =?UTF-8?q?tation-audit=20=EC=9A=B0=EC=84=A0=EC=88=9C=EC=9C=841=20?= =?UTF-8?q?=EB=A7=88=EC=A7=80=EB=A7=89=20=EB=84=B7=20=ED=95=B4=EC=86=8C,?= =?UTF-8?q?=20=EB=88=84=EB=9D=BD=20=EC=8A=A4=ED=8C=8C=EC=9D=B4=ED=81=AC=20?= =?UTF-8?q?=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자가 우선순위 동률/매치실패(1-3), provider 미주입(1-4), store.key type function 타이핑(1-10), Modifier __index+table.clone 트릭(1-11)에 대해 구체적 결정을 제시 - 전부 base/ 문서에 반영해 우선순위1 11개 전원 해소. 핸드오버 점검 중 1-10/1-11이 설계 레벨로만 확인됐고 실제 Luau 스파이크 파일이 없었던 갭을 발견해 16/17 신규 추가, ROADMAP.md M2/M3/M7 체크리스트 누락분도 보강. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01VVG74qV2nQVykhvMRQW2UC --- .claude/base/bind-system-plan.md | 74 ++++++++++++ .claude/base/modifier-plan.md | 28 +++++ .claude/base/module-lifecycle-plan.md | 6 + .../16-type-store-key-typefunction.luau | 67 +++++++++++ ...17-modifier-index-tableclone-chaining.luau | 70 +++++++++++ .claude/luau-test/README.md | 27 ++++- .claude/question.md | 18 +-- .claude/research/pre-implementation-audit.md | 70 +++++++++-- .../2026-08-12-17-priority1-audit-resolved.md | 111 ++++++++++++++++++ CLAUDE.md | 38 ++++-- ROADMAP.md | 21 +++- 11 files changed, 493 insertions(+), 37 deletions(-) create mode 100644 .claude/luau-test/16-type-store-key-typefunction.luau create mode 100644 .claude/luau-test/17-modifier-index-tableclone-chaining.luau create mode 100644 .claude/session/2026-08-12-17-priority1-audit-resolved.md diff --git a/.claude/base/bind-system-plan.md b/.claude/base/bind-system-plan.md index 971fff5..4da2c24 100644 --- a/.claude/base/bind-system-plan.md +++ b/.claude/base/bind-system-plan.md @@ -63,6 +63,41 @@ v1의 `ProcessQuadProperty`(`.claude/initreq/quad/src/class.lua:134-214`)는 src/schema/union.luau:48-68`) — 에러 메시지는 즉시 문자열로 만들지 말고 매치 실패 시에만 클로저 호출. +**우선순위 동률/매치 실패 처리 — 확정(2026-08-12 열일곱 번째 세션, +`pre-implementation-audit.md` 1-3/1-4 해소).** + +- **동률(같은 `priority` 값)에 대한 tiebreak 규칙은 강제하지 않는다.** + "등록 순서가 이긴다" 같은 규칙을 강제하면 `NoneHandler`/`StoreBind`처럼 + 이미 서로 `isHandlable`이 안 겹치는 내장 핸들러에까지 전부 그 규칙을 + 지켜가며 순서를 신경 써야 하고, 나중에 서드파티 핸들러가 늘어나면 더 + 골치아파짐(사용자 판단). 대신 **목적별로 이름 붙은 우선순위 상수** + (`HANDLER_PRIORITY_HIGH`/`HANDLER_PRIORITY_NORMAL`/`HANDLER_PRIORITY_LOW` + 등, 여전히 열린 숫자 공간 위의 편의 상수라 `HANDLER_PRIORITY_HIGH + 1`처럼 + 세밀 조정도 가능)를 제공해 애초에 동률이 잘 안 나오게 유도 — "우선순위 + 밴드 + 오프셋"은 여러 업계에서 이미 흔한 패턴. 실제로 동률이 나면 그건 + 대개 핸들러 설계 실수라, 강제 규칙보다 아래 디버그 가시성으로 대응하는 + 쪽이 맞음. +- **매치 실패(`isHandlable`을 만족하는 핸들러가 하나도 없음)는 조용한 + 무시 없이 즉시 `error`.** 에러 메시지엔 값의 `Brand`(있으면)와 + `typeof(v)`를 함께 출력하고, "quad-roblox 등 필요한 provider가 + 초기화됐는지 확인하라"는 안내만 덧붙임 — 그 이상의 특수 분기는 두지 + 않음(다른 라이브러리에서도 흔한 "매치 실패=에러" 패턴 그대로). + **이걸로 `module-lifecycle-plan.md`의 "provider가 아직 주입 안 된 + 상태에서 dispatch가 호출되면?" 케이스(`pre-implementation-audit.md` + 1-4)도 별도 분기 없이 자동으로 해소됨** — provider 미주입 상태는 + 결국 그 클래스를 다루는 핸들러가 레지스트리에 하나도 없는 상태이므로 + "매치 실패"와 정확히 같은 경로로 수렴함. 오타 키/미지원 조합/provider + 미주입을 서로 다른 에러 종류로 구분할 필요가 없음. +- **디버그 모드 — 핸들러 등록/정렬 시점에 동률 감지 시 print 경고 + + 전체 핸들러 목록 조회 함수.** 우선순위는 핸들러 등록 시점에 정적으로 + sort되므로 동률 감지 자체는 그 시점에 공짜로 가능 — `priority`가 같은 + 두 핸들러가 등록되면 콘솔에 경고를 찍고, `Dispatch.listHandlers()`류 + 함수로 현재 등록된 전체 핸들러(이름/priority)를 덤프할 수 있게 함. + 구현 비용이 거의 없고 실제 개발 중 디버깅에 바로 도움되는 항목이라 + M2(Dispatch 엔진) 착수 시 기본 기능으로 같이 넣음 — 런타임 플러그인인 + `quad-debug`(후순위, `research/debug-tooling-plan.md`)와는 다른 층위의, + 라이브러리 자체에 내장된 개발자 편의 기능. + ## 확정된 디스패치 모델: `process(inst, k, v)` / `retract(inst, k, v)` **사용자가 직접 준 구체적인 모델 — 이 문서의 이전 초안보다 우선함.** 아래가 @@ -2258,6 +2293,45 @@ Modifier처럼 플래튼하지 않는가"는 설계 근거를 알고 싶은 사 가드를 넣어 재설계하는 대신, State 자체가 파이핑 결합체이고 `state(state)`로 분기하는 위 모델로 완전히 대체됨. +**`store.key` 레코드 필드 타이핑 — Luau 타입함수로 해결 확인 +(2026-08-12 열일곱 번째 세션, `pre-implementation-audit.md` 1-10 해소).** + +위 "타입 추론 문제" 절이 "`store.key`를 평범한 레코드 필드 타이핑으로 자동 +해결"이라 서술했지만, `Store`가 입력 `T`(예: `{ty: string}`)를 받아 +`{ty: Source}`류 결과 타입을 실제로 어떻게 합성하는지는 미검증으로 +남아있었음. **Luau의 `type function`**(컴파일타임에 타입을 인자로 받아 새 +타입을 조립하는 기능, https://luau.org/types/type-functions/ , +https://luau.org/types-library/ — tbox에서도 이미 쓰이는 검증된 패턴)으로 +정확히 풀림: + +```luau +type function WrapStore(ty: type): type + -- Source 형태를 그대로 조립(:Get/:Set/:Compute/:With 등) + local result = types.newtable() + result:setproperty(types.singleton("Get"), types.newfunction(...)) + return result +end + +type function ProcessStoreType(ty: type): type + local props = ty:properties() :: { [type]: { read: type?, write: type? } } + local result = types.newtable() + for i, v in props do + -- i는 프로퍼티 이름을 담은 singleton 타입, i:value()로 실제 문자열 + result:setproperty(i, WrapStore(v)) + end + return result +end +``` + +`ProcessStoreType<{ty: string}>` → `{ty: Source}`가 나옴 — 결과는 +선언 시점에 이름 붙은 `Source` 그 자체가 아니라 구조를 그대로 풀어낸 +(flatten) 익명 타입이지만, **Luau는 이름이 아니라 "만족하는가"로 구조적 +일치를 검사**하므로 문제없이 `Source` 자리에 대입 가능 — 오히려 이 +방식과 정확히 맞는 조합. 이걸로 `store.key`가 실제로 타입 명시 가능함이 +확인돼 M0/M3 어느 시점에 검증해도 기술적으로 막힐 위험은 없음 — +`ROADMAP.md`의 M0/M3 배치를 강제로 바꿀 필요는 없어짐, 검증 난이도 +문제였던 것만 해소. + **PA님 코드와의 교차검증(2026-08-04 4차 라운드) — 둘 다 기존 확정 유지** `.claude/initreq/artworks/EventDrivenProgramming/`(Connection/Event/ diff --git a/.claude/base/modifier-plan.md b/.claude/base/modifier-plan.md index cd667ff..58380d8 100644 --- a/.claude/base/modifier-plan.md +++ b/.claude/base/modifier-plan.md @@ -45,6 +45,21 @@ cascade 문제가 그대로 오는데, 이건 이미 확정된 "Store 바인드 참고 — 여기서 다루는 건 컴포지션 타임의 modifier 값 자체 flatten이라 층위가 다름. +**참고 — Property(일반 프로퍼티)에 Attribute식 "이름 소유권 레지스트리"를 +적용하는 안은 검토 후 기각(2026-08-12 열일곱 번째 세션).** `Attribute` +(`base/attribute-plan.md`)의 그룹 위임은 이름별 전용 키 객체가 "지금 이 +이름을 누가 쓰고 있는가"를 스스로 추적해 충돌을 `error`로 잡아주는데, 이 +패턴을 일반 Instance 프로퍼티(`BackgroundColor3` 등)에도 그대로 적용할 수 +있을지 검토했으나 기각됨 — **Attribute 이름은 호출자가 자유롭게 짓는 +네임스페이스라 자기 전용 키 객체를 새로 만들 수 있지만, Instance 프로퍼티 +이름은 엔진이 이미 정해둔 유한 집합이라 호출자가 자기만의 전용 키를 새로 +못 만든다.** 그래서 "이 프로퍼티를 지금 누가 소유하고 있는가"라는 질문 +자체가 원천적으로 성립하지 않음 — 여러 modifier/컴포넌트가 같은 프로퍼티를 +건드리는 게 오히려 흔한 정상 시나리오(테마 오버라이드 등)이기도 함. 이게 +Property가 소유권 추적 대신 **덮어쓰기 우선순위(위 "2. Merge 우선순위" +절)**로 처리되는 이유 — 새로 결정한 게 아니라 이미 확정된 설계가 왜 +그 모양인지를 명문화한 것. + ### 2. Merge 우선순위: 배열 순서와 인라인은 독립된 두 규칙 `Frame { modifier1, modifier2, Name = ... }` 평탄화 시: @@ -167,6 +182,19 @@ Compute를 합치고 싶다"는 동기였는데, 이미 두 가지로 다 커버 메타테이블까지 그대로 복사해주는 Luau 동작 덕분에 클론해도 체이닝이 안 끊김. flatten도 그 테이블 필드를 직접 읽으면 됨. +**`table.clone`의 정확한 동작 — 확인됨(2026-08-12 열일곱 번째 세션, +`pre-implementation-audit.md` 1-11 해소).** 새 빈 테이블을 만들고 원본의 +키를 네이티브 슬롯 단위로 복사(얕은 복사, 값 자체는 안 파고듦)한 뒤, +원본의 `getmetatable` 결과를 그대로 그 새 테이블에 `setmetatable` — **이 +메타테이블은 복사되는 게 아니라 같은 참조를 공유**함. 그래서 위 제네릭 +`__index` 함수(메타테이블에 딱 하나 있는 그 함수 자체)는 원본과 clone +사이에서 물리적으로 동일 객체이고, `mod:FontSize(14)` → `__index`가 리턴한 +클로저가 `table.clone(self)`로 새 테이블을 또 만들 때도 그 새 테이블 역시 +같은 메타테이블을 공유하며 체이닝이 끊기지 않음 — M7 전체 설계("클래스별 +런타임 코드 없이 base에 제네릭 `__index` 하나면 충분")가 의존하던 두 Luau +동작(제네릭 `__index`가 임의 key를 잡아 클로저를 즉석 생성하는 것, +`table.clone`이 메타테이블 참조를 보존하는 것) 모두 공식 동작대로 확인됨. + **런타임은 클래스별 코드 없이 base에 딱 하나만 있으면 됨(2026-08-06 후속 세션, 핵심 통찰).** `mod:FontSize(14)`는 `mod.FontSize(mod, 14)`로 풀리는 문법 설탕이고, `mod.FontSize`는 `FontSize`가 리터럴 키로 안 박혀있으니 diff --git a/.claude/base/module-lifecycle-plan.md b/.claude/base/module-lifecycle-plan.md index ce940ae..4a37f17 100644 --- a/.claude/base/module-lifecycle-plan.md +++ b/.claude/base/module-lifecycle-plan.md @@ -119,6 +119,12 @@ init하려 하면 오류, 없는데 뭔가 생성해서 bind하려 해도 오류 처리한다"는 의미가 빠져 있음 — `Handler`가 계약 4종 (`isHandlable`/`priority`/`process`/`retract`) 전체를 가장 정확히 담는다는 결론. +- **[해소됨, 2026-08-12 열일곱 번째 세션]** provider(팩토리)가 아직 한 + 번도 실행 안 된 상태에서 dispatch가 호출되면 어떻게 되는지 + (`pre-implementation-audit.md` 1-4) — 별도 케이스로 처리하지 않음. + provider 미주입 상태는 결국 그 클래스의 핸들러가 레지스트리에 하나도 + 없는 상태이므로, `base/bind-system-plan.md` "우선순위 동률/매치 실패 + 처리" 절의 일반 "매치 실패 시 즉시 error" 규칙 하나로 자연히 커버됨. - base 유틸(per-instance 상태 저장소, 생명 바인드 유틸)이 인터페이스만 두고 실제 구현은 백엔드 팩토리(`RobloxFactory(BaseModule)`류)가 뮤테이션으로 주입한다는 패턴이 확정됨 — 상세는 `base/bind-system-plan.md`의 "base diff --git a/.claude/luau-test/16-type-store-key-typefunction.luau b/.claude/luau-test/16-type-store-key-typefunction.luau new file mode 100644 index 0000000..bbd019f --- /dev/null +++ b/.claude/luau-test/16-type-store-key-typefunction.luau @@ -0,0 +1,67 @@ +--!strict +--[[ + 검증 대상: `Store`가 레코드 타입 T의 각 필드를 Source로 감싼 타입을 + Luau `type function`으로 실제로 합성할 수 있는지 — 실제 실행이 아니라 + 타입 체크(luau-analyze/luau-lsp) 대상. + + 배경: .claude/base/bind-system-plan.md "`store.key` 레코드 필드 타이핑" + 절(2026-08-12 열일곱 번째 세션) — 사용자가 제시한 WrapStore/ + ProcessStoreType 스케치를 그대로 옮김. `research/pre-implementation-audit.md` + 1-10 해소 근거. + + 핵심 우려: (a) `type function` 문법 자체(`types.newtable`/`:setproperty`/ + `types.singleton`/`ty:properties()`/`types.newfunction`)가 실제 설치된 + Luau 버전에서 정확히 이 API 모양인지 — type function은 비교적 최근/ + 계속 진화 중인 기능이라 버전에 따라 세부 이름이 다를 수 있음 + (https://luau.org/types/type-functions/ , https://luau.org/types-library/ + 최신 문서 기준으로 맞춰볼 것 — 아래는 사용자가 손으로 쓴 스케치를 최대한 + 그대로 옮긴 것이라 API 이름이 실제와 다를 수 있음, 그 자체가 이 + 스파이크의 목적). (b) 결과 타입 `ProcessStoreType<{ty: string}>`이 + 실제로 `{ty: Source}`을 "만족"해서(이름이 아니라 구조로) + `Source` 필드가 필요한 자리에 그대로 대입 가능한지. + + 실행: `luau-analyze 16-type-store-key-typefunction.luau` 또는 + `luau-lsp analyze` — 문법/API 에러가 나면 정확한 에러 메시지(어느 줄, + 어떤 API 호출)를 그대로 알려줄 것 — 그게 다음 시도에서 API 이름을 + 고치는 데 바로 쓰임. 이 파일 자체가 한 번에 통과할 걸 기대하기보다, + "type function으로 이 방향 자체가 막히는지 아닌지"를 확인하는 게 목적. + + 기대 결과: 맨 아래 `checkTy`/`checkCount` 대입이 타입 에러 없이 통과 + (= `Processed.ty`가 구조적으로 `Source`을 만족). +]] + +export type Source = { + Get: (self: Source) -> T, + Set: (self: Source, value: T) -> (), +} + +type function WrapStore(ty: type): type + -- Source 형태를 그대로 조립 — 실사용에선 :Set/:Compute/:With 등도 + -- 같은 방식으로 추가해야 하지만, 이 스파이크는 "필드가 Source로 감싸 + -- 지는가"라는 구조적 만족 여부만 보면 되므로 :Get만 붙임 + local result = types.newtable() + result:setproperty(types.singleton("Get"), types.newfunction({}, { ty })) + return result +end + +type function ProcessStoreType(ty: type): type + local props = ty:properties() :: { [type]: { read: type?, write: type? } } + local result = types.newtable() + for i, v in props do + -- i는 프로퍼티 이름을 담은 singleton 타입, i:value()로 실제 문자열 + local fieldType = v.read or v.write + if fieldType then + result:setproperty(i, WrapStore(fieldType)) + end + end + return result +end + +type Input = { ty: string, count: number } +type Processed = ProcessStoreType + +local processed: Processed = nil :: any + +-- 검증 포인트: Processed의 각 필드가 실제로 Source 구조를 만족하는지 +local checkTy: string = processed.ty:Get() +local checkCount: number = processed.count:Get() diff --git a/.claude/luau-test/17-modifier-index-tableclone-chaining.luau b/.claude/luau-test/17-modifier-index-tableclone-chaining.luau new file mode 100644 index 0000000..2a79c71 --- /dev/null +++ b/.claude/luau-test/17-modifier-index-tableclone-chaining.luau @@ -0,0 +1,70 @@ +--[[ + 검증 대상: Modifier의 "제네릭 __index + table.clone" 트릭 — 클래스별 + 런타임 코드 없이 하나의 제네릭 __index만으로 임의 필드 setter 체이닝이 + 실제로 동작하는지, 그리고 table.clone이 매 단계 메타테이블을 복사가 + 아니라 참조로 유지해 체이닝이 안 끊기는지. + + 배경: .claude/base/modifier-plan.md "런타임은 클래스별 코드 없이 base에 + 딱 하나만 있으면 됨" 절 + "`table.clone`의 정확한 동작 — 확인됨" + 절(2026-08-12 열일곱 번째 세션) — 사용자가 설명한 메커니즘(원본 키를 + 네이티브 슬롯 단위로 얕은 복사 + 메타테이블은 복사가 아니라 + getmetatable/setmetatable로 참조 공유)을 실제 코드로 재현. + `research/pre-implementation-audit.md` 1-11 해소 근거. + + 실행: `luau 17-modifier-index-tableclone-chaining.luau` + + 기대 결과: 아래 모든 assert가 통과 — 특히 (B) 메타테이블이 매 clone마다 + 원본과 물리적으로 동일 객체인지(참조 공유 확인), (C) 원본이 각 체이닝 + 단계에서 전혀 mutate 안 됐는지(immutable 보장), (D) 서로 다른 체이닝 + 경로(형제 분기)가 서로 오염 안 시키는지가 핵심. +]] + +local function genericIndex(_, key) + return function(selfArg, arg) + local clone = table.clone(selfArg) + if type(arg) == "function" then + clone[key] = arg(selfArg[key]) + else + clone[key] = arg + end + return clone + end +end + +local ModifierMT = { __index = genericIndex } + +local function Modifier() + return setmetatable({}, ModifierMT) +end + +-- (A) 필드가 하나도 미리 등록 안 돼 있어도 임의 메소드 이름으로 체이닝되는지 +local mod0 = Modifier() +local mod1 = mod0:FontSize(14) +local mod2 = mod1:Round(8) +local mod3 = mod2:FontSize(function(old) + return old * 1.2 +end) + +assert(mod1.FontSize == 14, "mod1.FontSize expected 14") +assert(mod2.FontSize == 14 and mod2.Round == 8, "mod2 expected FontSize=14, Round=8") +assert(mod3.FontSize == 14 * 1.2, "mod3 expected FontSize=16.8 (14*1.2)") + +-- (B) getmetatable이 매 clone마다 원본과 physically 동일 객체인지 +-- (table.clone이 메타테이블을 복사가 아니라 참조로 공유한다는 주장의 핵심 검증) +assert(getmetatable(mod0) == ModifierMT, "mod0 metatable should be ModifierMT") +assert(getmetatable(mod1) == getmetatable(mod0), "mod1 metatable should be same reference as mod0's") +assert(getmetatable(mod2) == getmetatable(mod0), "mod2 metatable should be same reference as mod0's") +assert(getmetatable(mod3) == getmetatable(mod0), "mod3 metatable should be same reference as mod0's") + +-- (C) 원본은 절대 mutate 안 됨(immutable 체이닝 보장) +assert(mod0.FontSize == nil, "mod0 must stay untouched (FontSize)") +assert(mod0.Round == nil, "mod0 must stay untouched (Round)") +assert(mod1.Round == nil, "mod1 must stay untouched (Round) -- clone은 앞 단계까지만 반영") + +-- (D) 서로 다른 체이닝 경로(형제 분기)가 서로 오염 안 시키는지 +local branchA = mod1:Round(4) +local branchB = mod1:Round(100) +assert(branchA.Round == 4 and branchB.Round == 100, "sibling branches must not cross-contaminate") +assert(mod1.Round == nil, "mod1 itself must stay untouched by either branch") + +print("17-modifier-index-tableclone-chaining: all checks passed") diff --git a/.claude/luau-test/README.md b/.claude/luau-test/README.md index 3b4663a..404fb23 100644 --- a/.claude/luau-test/README.md +++ b/.claude/luau-test/README.md @@ -23,10 +23,15 @@ ROADMAP 항목 근거인지, 어떻게 실행하는지, 실행 후 뭘 확인해 | 환경 | 필요한 것 | 해당 파일 | |---|---|---| -| **순수 Luau CLI** (`luau`) | [luau-lang/luau 릴리즈](https://github.com/luau-lang/luau/releases)의 `luau` 인터프리터, 또는 `lune` | 01, 02, 03, 04, 05, 06(런타임 부분), 07, 11, 13(런타임 부분) | -| **Luau 타입체커** (`luau-analyze` 또는 `luau-lsp`) | 같은 릴리즈에 포함된 `luau-analyze`, 또는 `luau-lsp analyze`/에디터 인라인 진단 | 06(타입 부분), 08, 09, 12, 13(타입 부분), 14, 15 | +| **순수 Luau CLI** (`luau`) | [luau-lang/luau 릴리즈](https://github.com/luau-lang/luau/releases)의 `luau` 인터프리터, 또는 `lune` | 01, 02, 03, 04, 05, 06(런타임 부분), 07, 11, 13(런타임 부분), 17 | +| **Luau 타입체커** (`luau-analyze` 또는 `luau-lsp`) | 같은 릴리즈에 포함된 `luau-analyze`, 또는 `luau-lsp analyze`/에디터 인라인 진단 | 06(타입 부분), 08, 09, 12, 13(타입 부분), 14, 15, 16 | | **Roblox Studio** | 별도 계정으로 로그인(`HUMAN_TODO.md` 1번, `SAFETY.md` 준수) | 10 | +**16은 특히 `type function`이라는 비교적 최근/계속 진화 중인 Luau 기능을 +쓰므로, luau-analyze 버전이 오래되면 아예 문법 자체를 못 알아볼 수 있음** +— 그 경우는 실패가 아니라 "이 Luau 버전에서 type function 자체가 아직 +지원 안 됨"이라는 별개의 신호이니 버전 정보와 함께 알려줄 것. + **12/13/14는 특히 `luau-lsp`로 확인해달라고 요청받은 것들** — `luau-analyze`도 같은 타입 솔버를 쓰므로 원리적으로는 같은 결과가 나와야 하지만, `luau-lsp`가 에디터에서 인라인으로 에러 위치/메시지를 보여줘서 "정확히 어느 표현식이 @@ -56,6 +61,8 @@ ROADMAP 항목 근거인지, 어떻게 실행하는지, 실행 후 뭘 확인해 | `13-type-ref-preref-subtype.luau` | (A, 타입) `PreRef`가 `Ref`를 구조적으로 만족하는지, (B, 런타임) `isRef`/`isPreRef` 합성이 재정정대로 동작하는지(`isRef(preRefInstance)`가 이제 `true`) + Leaf 핸들러가 `isRef(v) and not isPreRef(v)`로 명시적으로 좁혀야 하는 이유 | `bind-system-plan.md`의 `Brand` 절(2026-08-09 열한 번째 세션 재정정) | | `14-type-nilable-default-overload.luau` (타입체크 전용) | `Source(default)`/`Ref(default)`의 `default` 생략이 `T`가 nilable일 때만 안전하다는 캐비엇을, 함수 오버로드(교차 타입)로 실제로 타입 레벨에서 막을 수 있는지 | `bind-system-plan.md` "[보강, 2026-08-09 열한 번째 세션]" 절 | | `15-type-compute-trailing-deps-typepack.luau` (타입체크 전용) | `:Compute(fn, ...)`의 trailing deps를 `fn`에 위치 인자(lazy State 핸들)로도 노출하는 확장, 최종 시그니처 `fn(self, previous?, ...deps)` — 이형(heterogeneous) 다중 deps를 제네릭 타입 팩(`U...`)으로 표현 가능한지, `previous?`가 팩 앞(정정된 순서)에서만 통과하고 팩 뒤(옛 순서)에서는 막히는지 | `bind-system-plan.md` "trailing deps를 fn에 lazy positional 인자로도 노출" 절(2026-08-11 후속 세션, 순서는 같은 날 세 번째 세션에 정정) | +| `16-type-store-key-typefunction.luau` (타입체크 전용) | `Store`가 `T`의 각 필드를 `Source`로 감싼 타입을 Luau `type function`(`types.newtable`/`:setproperty`/`ty:properties()`)으로 실제 합성 가능한지, 결과가 구조적으로 `Source` 필드를 만족하는지 | `bind-system-plan.md` "`store.key` 레코드 필드 타이핑" 절(2026-08-12 열일곱 번째 세션), `pre-implementation-audit.md` 1-10 | +| `17-modifier-index-tableclone-chaining.luau` | Modifier의 제네릭 `__index`+`table.clone` 체이닝 — 임의 필드 이름에 대해 즉석 setter가 만들어지는지, `table.clone`이 메타테이블을 참조로 공유해 여러 단계 clone에서도 체이닝이 안 끊기는지, 원본이 mutate 안 되는지, 형제 분기끼리 오염 안 되는지 | `modifier-plan.md` "런타임은 클래스별 코드 없이 base에 딱 하나만 있으면 됨" 절 + "`table.clone`의 정확한 동작 — 확인됨" 절(2026-08-12 열일곱 번째 세션), `pre-implementation-audit.md` 1-11 | ## 갱신 이력 @@ -101,6 +108,13 @@ deps의 제네릭 타입 팩 표현 가능 여부와 `previous?`를 팩 뒤에 검증하는 (D) 양성 대조군을 신규 추가 — 이제 진짜 불확실성은 (B) 이형 다중 deps의 제네릭 팩 표현 가능 여부 하나뿐. +**6차 (2026-08-12, 열일곱 번째 세션, `16`/`17` 신규)**: `pre-implementation-audit.md` +우선순위1의 마지막 두 항목(1-10 `store.key` 타이핑, 1-11 Modifier +`__index`+`table.clone` 트릭)이 이 세션에 설계 레벨로는 해소됐지만, 실제 +Luau로 부딪혀본 적은 없다는 걸 핸드오버 점검 중 발견 — `16`(type function +스케치, 타입체크 전용)/`17`(제네릭 __index 체이닝, 런타임)로 신규 추가. +둘 다 이전까지 이 폴더 어디에도 커버 대상이 없던 완전히 새 항목. + ## 결과 확인 후 할 일 각 파일 결과를 알려주면, 실제로 걸리는 부분이 있는지 보고 필요하면 @@ -130,3 +144,12 @@ deps의 제네릭 타입 팩 표현 가능 여부와 `previous?`를 팩 뒤에 - `13`은 A(타입)/B(런타임) 둘 다 확인해줄 것 — B의 assert가 실패하면 `Dispatch/Leaf.luau` 설계(`isRef(v) and not isPreRef(v)`) 자체가 잘못 짜인 것이니 우선순위 높게 알려줄 것. +- `16`은 **어느 쪽으로 나와도 유용** — 통과하면 `store.key` 타이핑을 이 + type function 방식으로 그대로 채택, API 이름이 틀려서 막히면(`type + function`이 비교적 새 기능이라 가능성 높음) 정확한 에러 메시지를 그대로 + 알려줄 것 — 다음 시도의 API 이름을 고치는 데 바로 쓰임. 아예 이 Luau + 버전이 `type function` 자체를 지원 안 하면 그것도 알려줄 것(다른 대안 + 필요). +- `17`은 전부 PASS가 기대값 — 특히 (B) 메타테이블 참조 동일성 assert가 + 실패하면 M7 전체 설계("클래스별 런타임 코드 불필요")의 핵심 전제가 + 무너지는 것이니 최우선으로 알려줄 것. diff --git a/.claude/question.md b/.claude/question.md index 7f45937..29345a7 100644 --- a/.claude/question.md +++ b/.claude/question.md @@ -195,14 +195,16 @@ context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만** 있던 것을 이번에 `pre-implementation-audit.md` 2-11에도 해소 표시로 동기화. **[해소됨, 2026-08-09 세 번째 세션]** Slot CRUD 의미론 (`add`/`remove`/`clear`) 미정의(1-7)/`isMounted` 이중 추적 혼용(1-8) — - `base/slot-plan.md` 참고. **[2026-08-12 감사 세션 정정] 아직 실제로 - 열려있는 건 하나가 아니라 넷** — `pre-implementation-audit.md` 원문에 - `[해소됨]` 마커가 없는 항목이 1-3 외에도 셋 더 있음: 우선순위 스캔 - 동률/매치실패 처리(1-3), provider 미주입 상태 dispatch 처리(1-4), - `store.key`의 레코드 필드 타이핑을 M0로 앞당길지(1-10), Modifier - `__index`+`table.clone` 트릭 검증(1-11) — 이전 요약이 이 셋을 누락한 - 채로 "1-3 하나뿐"이라 잘못 축약해뒀던 것, `pre-implementation-audit.md` - 본문 및 "다음 액션 제안" 절 참고. + `base/slot-plan.md` 참고. **[해소됨, 2026-08-12 열일곱 번째 세션]** + 나머지 넷도 전부 확정 — 우선순위 스캔 동률/매치실패 처리(1-3, tiebreak + 강제 없이 `HANDLER_PRIORITY_*` 상수+디버그 print/목록 함수, 매치실패는 + 즉시 error), provider 미주입 상태 dispatch 처리(1-4, 매치실패 규칙에 + 자연 흡수), `store.key`의 레코드 필드 타이핑(1-10, Luau `type function`으로 + 가능함 확인), Modifier `__index`+`table.clone` 트릭 검증(1-11, 메타테이블 + 참조 공유 방식 확인) — 상세는 `pre-implementation-audit.md` 해당 항목, + `base/bind-system-plan.md`/`base/modifier-plan.md` 참고. **우선순위1 + 11개 전부 해소됨** — M0 착수 전 남은 건 `.claude/luau-test/` 스파이크 + 실측 결과 확인뿐(아직 사용자가 안 돌려봄). - **[해소됨, 2026-08-08 두 번째 세션]** `Frame { ref }`/`Frame { observer }`처럼 children 배열 숫자 슬롯에 직접 놓는 leaf 값을 매칭·바인드하는 Handler (`(i:number, v=Ref/Observer/PreRef)`)의 패키지 배치 — 원래 제안대로 diff --git a/.claude/research/pre-implementation-audit.md b/.claude/research/pre-implementation-audit.md index df369c1..ab2c2e0 100644 --- a/.claude/research/pre-implementation-audit.md +++ b/.claude/research/pre-implementation-audit.md @@ -106,7 +106,15 @@ bind-system-plan.md` "Dispatch 체인" 절, `ROADMAP.md` M2/M4. 아래는 `retract`를 호출하는 식으로 지금 결정해두는 게 좋아 보임 — M2/M4에서 바로 부딪힐 지점. -### 1-3. 우선순위 스캔의 동률 처리, 매치 실패 시 동작이 정의 안 됨 +### 1-3. 우선순위 스캔의 동률 처리, 매치 실패 시 동작이 정의 안 됨 — [해소됨, 2026-08-12 열일곱 번째 세션] + +**해소**: 동률은 tiebreak 규칙을 강제하지 않고 대신 목적별 우선순위 상수 +(`HANDLER_PRIORITY_HIGH` 등, "밴드+오프셋" 패턴)로 애초에 안 나게 유도, +매치 실패는 조용한 무시 없이 즉시 `error`(브랜드+`typeof` 출력, provider +초기화 확인 안내)로 확정. 핸들러 등록 시점 동률 감지 print 경고와 +`Dispatch.listHandlers()`류 전체 목록 조회 함수도 M2 기본 기능으로 +확정 — 상세는 `base/bind-system-plan.md` "우선순위 동률/매치 실패 처리" +절. 아래는 원래 발견 당시 기록. **위치**: `base/bind-system-plan.md` "핸들러 계약" 절 — "디스패치는 등록된 핸들러를 우선순위 순으로 스캔하며 `isHandlable`을 호출, 첫 매치가 처리." @@ -121,7 +129,13 @@ bind-system-plan.md` "Dispatch 체인" 절, `ROADMAP.md` M2/M4. 아래는 결정해두면 구현 중 인터럽트를 막을 수 있음. 동률은 "등록 순서가 tiebreak" 정도로 명시만 해둬도 충분. M2(Dispatch 엔진) 착수 직전 확인. -### 1-4. provider(팩토리) 미주입 상태에서 dispatch가 호출되면 어떻게 되는지 세 번째 케이스가 빠짐 +### 1-4. provider(팩토리) 미주입 상태에서 dispatch가 호출되면 어떻게 되는지 세 번째 케이스가 빠짐 — [해소됨, 2026-08-12 열일곱 번째 세션] + +**해소**: 별도 케이스로 처리하지 않음 — provider 미주입 상태는 결국 그 +클래스의 핸들러가 레지스트리에 하나도 없는 상태이므로, 위 1-3에서 확정된 +일반 "매치 실패 시 즉시 error" 규칙 하나로 자연히 커버됨. 상세는 +`base/module-lifecycle-plan.md` 해당 항목, `base/bind-system-plan.md` +"우선순위 동률/매치 실패 처리" 절. 아래는 원래 발견 당시 기록. **위치**: `base/module-lifecycle-plan.md` "Bind는 누가, 어떻게 구현하는가" 절 — 재호출 가드는 "같은 팩토리=무시, 다른 팩토리=에러" 두 케이스로 @@ -274,7 +288,18 @@ base 인터페이스가 그보다 늦은 M8에서 만들어지는 순서 역전. `Connected` 기반)"만 담당하도록 분리. M1 mock에도 이 인터페이스의 트리비얼 스텁(항상 true)을 붙여두면 M4/M6 테스트가 자연스러워짐. -### 1-10. `store.key`의 레코드 필드 타이핑 검증이 M0가 아니라 M3로 밀려 있음 +### 1-10. `store.key`의 레코드 필드 타이핑 검증이 M0가 아니라 M3로 밀려 있음 — [해소됨, 2026-08-12 열일곱 번째 세션] + +**해소**: 걱정했던 "검증 난이도" 자체가 사라짐 — Luau `type function` +(`https://luau.org/types/type-functions/`)으로 `Store`가 `T`의 각 +필드를 `Source`로 감싼 레코드 타입을 실제로 합성 가능함을 확인(tbox에서도 +이미 쓰이는 패턴). 결과가 구조적으로 `Source`를 만족하기만 하면 Luau가 +이름이 아니라 구조로 일치를 검사하므로 문제없음. 기술적으로 막힐 위험이 +없어졌으니 M0/M3 어느 시점에 검증해도 무방 — `ROADMAP.md` 배치를 억지로 +안 옮겨도 됨. 상세는 `base/bind-system-plan.md` "`store.key` 레코드 필드 +타이핑" 절, 실제 문법 실측은 +`.claude/luau-test/16-type-store-key-typefunction.luau`(신규). 아래는 +원래 발견 당시 기록. **위치**: `ROADMAP.md` M0 vs M3 `"store.key dot-access 타입 추론 확인"`. @@ -292,7 +317,20 @@ M1(스캐폴딩)·M2(디스패치 엔진) 투자가 먼저 이뤄진 뒤에야 안전하게 추론되는지"도 같이 넣을 것 — 어차피 같은 스파이크 파일에서 몇 줄 추가로 검증 가능. -### 1-11. Modifier의 "제네릭 `__index` + `table.clone` 메타테이블 보존" 트릭이 검증 안 된 채 M7 전체 설계의 전제가 됨 +### 1-11. Modifier의 "제네릭 `__index` + `table.clone` 메타테이블 보존" 트릭이 검증 안 된 채 M7 전체 설계의 전제가 됨 — [해소됨, 2026-08-12 열일곱 번째 세션] + +**해소**: `table.clone`의 정확한 동작(새 테이블에 원본 키를 네이티브 슬롯 +단위로 얕은 복사, 메타테이블은 복사가 아니라 `getmetatable`/`setmetatable`로 +같은 참조를 공유)을 사용자가 직접 확인 — 제네릭 `__index` 함수가 원본과 +clone 사이에서 물리적으로 동일 객체로 유지되므로 체이닝이 끊기지 않는다는 +M7의 전제가 Luau 공식 동작대로 성립함. 별도로, 프로퍼티에 Attribute식 +소유권 레지스트리를 적용하는 대안도 검토했으나 기각(엔진이 정한 유한 +프로퍼티 이름 집합은 호출자가 전용 키를 못 만들어 소유권 판정이 원천적으로 +불가능 — Property가 소유권 추적 대신 Modifier의 override 우선순위를 쓰는 +이유). 상세는 `base/modifier-plan.md` "`table.clone`의 정확한 동작" 절과 +"Property에 Attribute식 소유권 레지스트리 기각" 절, 실제 실행 확인은 +`.claude/luau-test/17-modifier-index-tableclone-chaining.luau`(신규). +아래는 원래 발견 당시 기록. **위치**: `base/modifier-plan.md` "런타임은 클래스별 코드 없이 base에 딱 하나만 있으면 됨" 절, `ROADMAP.md` M7. @@ -652,16 +690,22 @@ Handler"라고만 서술해, 사실상 3개의 거의 동일한 형태(리터럴 ## 다음 액션 제안 -- **M0 착수 전**: 1-5(props.Modifier/Ref nil-hole)는 `ROADMAP.md` M0에 - 반영 완료. 1-10(`store.key` 레코드 필드 타이핑)을 M0로 앞당기는 것 검토, - 1-11(Modifier `__index` 트릭)도 비용이 낮으니 M0 후보로 포함 검토는 계속 - 열려있음. -- **M2(Dispatch) 착수 전**: 1-3, 1-4를 한 번에 확정(전부 base dispatch - 엔진의 에러/상태관리 규칙이라 같이 결정하는 게 효율적). 1-2는 2026-08-08 - 세 번째 세션에 Dispatch 체인+`retractUnder`로 이미 해소됨(위 1-2번 항목 - 참고) — 남은 건 M2 스파이크에서 다단 체인 케이스가 실제로 맞게 - 동작하는지 실측하는 것뿐. +**[2026-08-12 열일곱 번째 세션 갱신] 우선순위1 11개 전부 해소됨** — 1-3/ +1-4/1-10/1-11이 이 세션에서 마저 확정되어(위 각 항목 참고), 더 이상 M0 +착수 전에 열려있는 우선순위1 항목이 없음. 아래는 남은 실측/검증 항목만 +정리(전부 "설계는 확정, 실제 Luau/구현으로 부딪혀볼 것만 남음" 상태): + +- **M2(Dispatch) 착수 시**: 1-2/1-3/1-4는 설계 확정 완료 — M2 스파이크에서 + 다단 체인 케이스, 동률 감지 디버그 print, 매치 실패 에러 경로가 실제로 + 맞게 동작하는지 실측. - **M2/M3 착수 전**: 1-6(canExecute 실제 구현) 실측(1-9는 반영 완료, 위 항목 참고). +- **M0 스파이크**: 1-10(`store.key` type function)과 1-11(`table.clone` + 메타테이블 공유)은 메커니즘 자체는 이제 확실하지만, 실제 Luau 코드로 + 돌려보는 것 자체는 여전히 스파이크 대상 — `.claude/luau-test/16-type- + store-key-typefunction.luau`/`17-modifier-index-tableclone-chaining.luau`로 + 신규 추가됨(2026-08-12 열일곱 번째 세션 핸드오버 점검 중 발견된 갭, + 이전까진 이 두 항목을 커버하는 스파이크 파일 자체가 없었음). 아직 + 사용자가 안 돌려봄, 결과 미확인. - **나머지**: 해당 마일스톤 착수 시점에 이 문서를 다시 열어 관련 항목만 확인하면 됨 — 지금 전부 결정할 필요는 없음. diff --git a/.claude/session/2026-08-12-17-priority1-audit-resolved.md b/.claude/session/2026-08-12-17-priority1-audit-resolved.md new file mode 100644 index 0000000..cd3ffa0 --- /dev/null +++ b/.claude/session/2026-08-12-17-priority1-audit-resolved.md @@ -0,0 +1,111 @@ +# 2026-08-12 열일곱 번째 세션 — `pre-implementation-audit.md` 우선순위1 마지막 넷 전부 해소 + +**배경**: 이전 세션(열여섯 번째)까지 `research/pre-implementation-audit.md`의 +우선순위1 11개 중 7개는 해소됐지만 4개(1-3/1-4/1-10/1-11)가 열려있었음. +사용자가 이번 세션에서 4개 전부에 대해 구체적인 결정을 한 번에 제시. + +## 1-3. 우선순위 스캔 동률/매치 실패 처리 + +사용자 판단: 동률(같은 `priority`)을 엄격한 tiebreak 규칙(예: 등록 순서)으로 +잡으려 하면 `None`/`Store`처럼 이미 `isHandlable`이 안 겹치는 내장 핸들러까지 +전부 그 규칙을 신경 써야 하고, 나중에 서드파티 핸들러가 늘어나면 계속 +골치아파짐. 대신 **목적별로 이름 붙은 우선순위 상수**(`HANDLER_PRIORITY_HIGH` +등, 열린 숫자 공간 위의 편의 상수라 `+1`류 세밀 조정도 가능 — "우선순위 +밴드+오프셋"은 여러 업계에서 흔한 패턴)로 애초에 동률이 잘 안 나오게 유도. +실제로 동률이 나면 대개 설계 실수이므로, 강제 규칙보다 **디버그 모드**(핸들러 +등록/정렬 시점에 동률 감지 시 print 경고, `Dispatch.listHandlers()`류 전체 +핸들러 목록 조회 함수)로 대응 — 구현 비용이 거의 없고 실제 개발에 바로 +도움이 되는 항목이라 M2 기본 기능으로 확정. + +## 1-4. provider(팩토리) 미주입 상태에서 dispatch 호출 + +사용자 판단: 별도 처리 불필요. 핸들러가 없으면(`isHandlable`을 만족하는 +핸들러가 하나도 없으면) `Brand`/`typeof(v)`를 출력하고 그냥 에러로 죽으면 +됨 — "quad-roblox 등 provider가 초기화됐는지 확인하라"는 안내 문구 정도만 +추가. 이게 1-3의 "매치 실패=즉시 error" 규칙과 정확히 같은 경로라는 걸 +이번 세션에 확인 — provider 미주입 상태는 결국 "그 클래스의 핸들러가 +레지스트리에 없는 상태"이므로 매치 실패와 별개 케이스가 아님. 오타 키/ +미지원 조합/provider 미주입을 서로 다른 에러로 구분할 필요 없음(다른 +라이브러리에서도 흔한 패턴). + +## 1-10. `store.key` 레코드 필드 타이핑 + +사용자가 Luau `type function`(https://luau.org/types/type-functions/, +https://luau.org/types-library/) 기반 구체적 구현 방법을 제시 — `T`가 +테이블 타입이면 `ty:properties()`로 각 필드를 순회하며 `Source`로 감싼 +새 타입을 조립하는 `WrapStore`/`ProcessStoreType` 타입함수 스케치. +`ProcessStoreType<{ty: string}>` → `{ty: Source}`가 나오는데, 이건 +선언 시점에 이름 붙은 `Source` 자체가 아니라 구조를 풀어낸 익명 +타입이지만 Luau가 이름이 아니라 "만족하는가"로 구조적 일치를 검사하므로 +문제없이 대입 가능 — 오히려 이 방식과 정확히 맞는 조합이라는 걸 확인. +`type function`은 TBox에서도 이미 쓰이는 검증된 패턴. 이걸로 "검증 난이도" +자체가 사라져 M0/M3 어느 시점에 실제로 부딪혀봐도 막힐 위험 없음 — +`ROADMAP.md`의 M0/M3 배치를 억지로 옮길 필요는 없어짐. + +## 1-11. `table.clone` + `__index` 메타테이블 트릭 + +사용자가 `table.clone`의 정확한 동작을 직접 확인해줌: 새 빈 테이블을 만들고 +원본 키를 네이티브 슬롯 단위로 얕은 복사(깊은 복사 아님), 그 다음 원본의 +`getmetatable` 결과를 그대로 새 테이블에 `setmetatable` — **메타테이블 +자체는 복사되는 게 아니라 같은 참조를 공유**. 이걸로 M7 전체 설계가 +의존하던 두 Luau 동작(제네릭 `__index`가 임의 key를 잡아 즉석 클로저를 +만드는 것, `table.clone`이 메타테이블 참조를 보존하는 것) 모두 공식 +동작대로 확인됨. + +**추가 논의 — Property에 Attribute식 소유권 레지스트리를 적용하는 안, +검토 후 기각.** 사용자가 "프로퍼티도 Attribute처럼 처리해도 될까" 생각해봤다가 +스스로 기각한 근거를 공유: Attribute 이름은 호출자가 자유롭게 짓는 +네임스페이스라 자기 전용 키 객체를 만들 수 있지만, Instance 프로퍼티 +이름은 엔진이 이미 정해둔 유한 집합이라 호출자가 전용 키를 새로 못 만듦. +그래서 "이 프로퍼티를 지금 누가 소유하고 있는가"라는 질문 자체가 원천적으로 +성립 안 함(여러 modifier가 같은 프로퍼티를 건드리는 게 오히려 정상 시나리오, +테마 오버라이드 등) — 이게 Property가 소유권 추적 대신 Modifier의 override +우선순위 패턴을 쓰는 이유라는 걸 명문화. + +## 결과 + +- `base/bind-system-plan.md`: "우선순위 동률/매치 실패 처리" 절 신설(핸들러 + 계약 절), "`store.key` 레코드 필드 타이핑" 절 신설(타입 추론 문제 절 + 바로 뒤). +- `base/module-lifecycle-plan.md`: provider 미주입 케이스 해소 표시 추가. +- `base/modifier-plan.md`: `table.clone` 정확한 동작 절 추가, Property + 소유권 레지스트리 기각 절 추가. +- `research/pre-implementation-audit.md`: 1-3/1-4/1-10/1-11 전부 + `[해소됨]` 표시, "다음 액션 제안" 절 갱신 — **우선순위1 11개 전부 해소**. +- `.claude/question.md` 2번 절 동기화. + +M0 착수 전 남은 유일한 게이트는 `.claude/luau-test/` 스파이크 실측 +결과 확인(아직 사용자가 안 돌려봄) — 설계 자체는 더 이상 막힌 게 없음. + +## 핸드오버 점검(같은 세션, 후속) — 누락된 스파이크 파일 발견 + +사용자가 "세션 중 알게된 지식 누락/stale 점검" 요청 — 위 문서 반영을 다시 +훑던 중, 1-10/1-11은 `pre-implementation-audit.md`/`base/`에 "확인됨"으로 +표시했지만 실제로는 **사용자의 설명/지식을 바탕으로 한 설계 레벨 확인이지, +실제 Luau 코드로 돌려본 적은 없다**는 걸 재확인. 그런데 `.claude/luau-test/` +15개 파일 중 이 둘(Luau `type function`으로 `store.key` 타이핑, 제네릭 +`__index`+`table.clone` 체이닝)을 커버하는 스파이크가 하나도 없었음 — +둘 다 이번 세션에 처음 나온 구체 메커니즘이라 이전 스파이크 라운드 +(1~5차)에 있을 수 없었던 게 당연하지만, 그렇다고 "실측 대상"이라고만 +말해두고 실제 파일을 안 만들어두면 다음에 또 놓치기 쉬움. + +**조치**: +- `.claude/luau-test/16-type-store-key-typefunction.luau` 신규(타입체크 + 전용) — 사용자가 준 `WrapStore`/`ProcessStoreType` 스케치를 그대로 + 옮김, `luau-analyze`/`luau-lsp`로 실측. +- `.claude/luau-test/17-modifier-index-tableclone-chaining.luau` 신규 + (런타임) — 제네릭 `__index` 체이닝(A), 메타테이블 참조 동일성(B), + 원본 불변(C), 형제 분기 비오염(D) 네 갈래로 assert. +- `.claude/luau-test/README.md` 파일 목록 표/실행환경 표/"결과 확인 후 + 할 일"/갱신 이력(6차) 갱신. +- `ROADMAP.md`도 같이 감사 — M2에 우선순위 동률/매치실패 처리(디버그 + 모드 `Dispatch.listHandlers()` 포함) 체크리스트 항목이 통째로 빠져있던 + 것, M3/M7의 `store.key`/`table.clone` 항목에 새 스파이크 파일 링크가 + 없던 것을 같이 보강. +- `pre-implementation-audit.md`의 1-10/1-11 해소 블럭과 "다음 액션 제안"에 + 새 스파이크 파일 이름을 명시적으로 링크. + +**교훈**: "설계는 확정됐다"와 "실제로 실행해서 확인했다"를 같은 `[해소됨]` +마커로 뭉뚱그리면, 정작 실측이 필요한 지점이 조용히 새는 패턴이 생김 — +앞으로 `[해소됨]` 표시를 붙일 때 그 해소가 (a) 순수 설계 결정인지 (b) 실측 +검증까지 끝난 것인지 구분해서 적을 것. diff --git a/CLAUDE.md b/CLAUDE.md index 897036c..f0a1d02 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -111,21 +111,16 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션 ## 지금 할 일 (우선순위순) 1. **구현 시작 — 루트 `ROADMAP.md`의 M0부터.** 설계 단계는 2026-08-04 로드맵 - 인수인계 라운드로 종료. M0 착수 직전 확인할 것 둘: - - **`.claude/luau-test/`(2026-08-09 신설) 스파이크 결과** — 아직 사용자가 + 인수인계 라운드로 종료. `research/pre-implementation-audit.md` 우선순위1은 + 2026-08-12 열일곱 번째 세션에 마지막 넷(1-3/1-4/1-10/1-11)까지 전부 + 해소되어 **11개 전원 완료** — M0 착수 전 남은 유일한 게이트는: + - **`.claude/luau-test/`(2026-08-09 신설, 2026-08-12 열일곱 번째 세션에 + 16/17 추가로 총 17개) 스파이크 결과** — 아직 사용자가 `luau`/`luau-analyze`/`luau-lsp`/Roblox Studio로 안 돌려봄, 결과가 나오면 그것부터 반영할 것(`luau-test/README.md`가 파일별로 뭘 우선 확인해야 하는지 이미 적어둠). 걸리는 게 있으면 `base/` 문서부터 고치고, - 없으면 그대로 M0 실제 코드 작성에 재사용. - - **`research/pre-implementation-audit.md` 우선순위1** — 신설 당시 - 11개였으나 대부분 이후 세션에서 해소됨(Tween/Tag 재설계, Slot CRUD, - `canExecute` 시그니처, `LifetimeHandle` 로드맵 순서 등). **[2026-08-12 - 감사 세션 정정]** 실제로 아직 열려있는 건 넷(원문에 `[해소됨]` 마커가 - 없는 항목 기준) — 우선순위 스캔 동률/매치실패 처리(1-3), provider - 미주입 상태 dispatch 처리(1-4), `store.key` 레코드 필드 타이핑을 M0로 - 앞당길지(1-10), Modifier `__index`+`table.clone` 트릭 검증(1-11). - 이전엔 "1-3 하나뿐"이라 잘못 축약돼 있었음(1-4/1-10/1-11 누락) — - `.claude/question.md` 2번이 최신 상태. + 없으면 그대로 M0 실제 코드 작성에 재사용. 설계 자체는 더 이상 막힌 + 게 없음 — `.claude/question.md` 2번이 최신 상태. 2. **용어 정리 — 1차 제안 이후 대부분 확정, 소수만 남음.** 최신 소스는 `.claude/question.md` 1번(개수 반복 안 함, 항목 추가/해소될 때마다 여기가 stale해지는 패턴이 반복됐어서). 아직 열려있는 것만 짚으면: `State`(1순위, @@ -668,3 +663,22 @@ transitively 생존). `and`/`or` 삼항 관용구 전면 금지(기존 "항상-t 예외" 조항 폐기), 코퍼스 전체 실제 코드 6곳을 `if-then-else`로 교체. Attribute 자동 unset이 필요해지면 쓸 `:Apply` opt-in 유틸을 `research/operator-sugar-plan.md`에 백로그로 추가(착수 안 함). + +**2026-08-12 열일곱 번째 세션 — 우선순위1 마지막 넷 전부 해소** +(`session/2026-08-12-17-priority1-audit-resolved.md`) +`pre-implementation-audit.md` 우선순위1 중 열려있던 마지막 넷을 사용자가 +한 번에 확정: 우선순위 동률/매치실패(1-3, tiebreak 강제 대신 +`HANDLER_PRIORITY_*` 상수+디버그 모드 동률 감지/핸들러 목록 함수, 매치실패는 +즉시 error), provider 미주입 dispatch(1-4, 매치실패 규칙에 자연 흡수), +`store.key` 레코드 필드 타이핑(1-10, Luau `type function`으로 `Store`→ +`{[K]: Source}` 합성 가능함을 구체 스케치로 확인), Modifier +`__index`+`table.clone` 트릭(1-11, 메타테이블이 복사 아닌 참조 공유임을 +확인). 부수적으로 Property에 Attribute식 소유권 레지스트리를 적용하는 안을 +검토 후 기각(엔진이 정한 유한 프로퍼티 이름 집합은 호출자가 전용 키를 못 +만들어 소유권 판정 불가 — Property가 override 우선순위를 쓰는 이유). +**우선순위1 11개 전원 해소** — M0 착수 전 남은 건 `.claude/luau-test/` +스파이크 실측뿐. **핸드오버 점검 중 발견**: 1-10/1-11은 설계는 확정됐지만 +이를 실제로 실측할 스파이크 파일 자체가 없었던 갭 — `16-type-store-key- +typefunction.luau`/`17-modifier-index-tableclone-chaining.luau` 신규 +추가(총 17개), `ROADMAP.md` M2/M3/M7 체크리스트에도 누락됐던 항목(디버그 +모드 동률 감지+`listHandlers`, `store.key`/`table.clone` 실측 링크) 보강. diff --git a/ROADMAP.md b/ROADMAP.md index 49113e2..e6adbf5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -137,6 +137,14 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 걸러내기(no-op이라도 필드 자체는 항상 정의 — `Dispatch.process`가 핸들러 교체 시 nil 체크 없이 호출, `base/bind-system-plan.md` "핸들러 계약" 절, 2026-08-08 세션) +- [ ] 우선순위 동률/매치 실패 처리(2026-08-12 열일곱 번째 세션 확정, + `base/bind-system-plan.md` "우선순위 동률/매치 실패 처리" 절) — + `HANDLER_PRIORITY_HIGH`/`_NORMAL`/`_LOW` 등 목적별 우선순위 상수, + 매치 실패(`isHandlable`을 만족하는 핸들러 없음)는 `Brand`+`typeof(v)` + 출력 후 즉시 error(provider 초기화 확인 안내 포함 — provider + 미주입 상태도 이 경로로 자동 커버, `pre-implementation-audit.md` + 1-3/1-4), 핸들러 등록/정렬 시점 동률 감지 print 경고 + + `Dispatch.listHandlers()` 디버그 유틸 - [ ] `Dispatch/Leaf.luau` — `(i:number, v=Ref/Observer/PreRef)` children-array leaf 매칭 Handler, `StoreBind.luau`와 같은 층위(범용/엔진무관) — quad-base 소속으로 확정(2026-08-08 두 번째 세션, `base/ @@ -153,7 +161,12 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 ## M3 — Store/State/Source - [ ] `Source.luau`/`State.luau`/`Store.luau` -- [ ] `store.key` dot-access 타입 추론 확인 +- [ ] `store.key` dot-access 타입 추론 확인 — Luau `type function` + (`WrapStore`/`ProcessStoreType`)으로 `Store`가 `T`의 각 필드를 + `Source`로 감싼 레코드 타입을 합성 가능함을 확인(2026-08-12 열일곱 + 번째 세션, `base/bind-system-plan.md` "`store.key` 레코드 필드 + 타이핑" 절) — 실제 문법이 통과하는지는 + `.claude/luau-test/16-type-store-key-typefunction.luau`로 실측 필요 - [ ] `:Compute(fn, ...)` — trailing args로 추가 의존성 직접 받는 sugar (2026-08-11 세션, `base/bind-system-plan.md` "`:Compute(fn, ...)`" 절) — `:With(...):Compute(fn)` 체인과 달리 노드 1개(Compute 노드 @@ -369,7 +382,11 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 `Type(args)` 팩토리 관습, `modifier-plan.md` 3번) - [ ] flatten-before-dispatch(`isModifier(v)`로 배열 항목 중 Modifier만 판별해 필드 merge, 나머지는 안 건드리고 통과 — 2026-08-07 열 번째 - 세션 명시, `modifier-plan.md` 1번), immutable `table.clone` 체이닝 + 세션 명시, `modifier-plan.md` 1번), immutable `table.clone` 체이닝 — + `table.clone`이 메타테이블을 복사 아닌 참조로 공유해 제네릭 `__index` + 기반 체이닝이 안 끊긴다는 메커니즘은 확인됨(2026-08-12 열일곱 번째 + 세션, `modifier-plan.md` "`table.clone`의 정확한 동작" 절) — 실제 + Luau 실행 확인은 `.claude/luau-test/17-modifier-index-tableclone-chaining.luau` - [ ] `Modifier.Overridden(mod1, mod2, ...)`(이름 확정, 구 `Merge`→`Override`, 2026-08-08 세션) — 필드별 raw 덮어쓰기, 특별한 State/함수 분기 불필요(`modifier-plan.md` 9번)