diff --git a/.claude/README.md b/.claude/README.md index e7d6db9..a27d8ad 100644 --- a/.claude/README.md +++ b/.claude/README.md @@ -15,7 +15,7 @@ | `qa-request/` | 구현 완료(코드/에이전트 검증까지 끝남) + 사용자 본인의 실기기(Roblox Studio) QA만 남음 — 지금은 구현 자체가 시작 전이라 비어있음 | | `archive/` | 완료 + 사용자가 실사용/실기기로 직접 검증까지 마침 (구현 대상). **[2026-08-06 확장]** 완전히 뒤집힌 설계 결정을 원문+역전 이유+diff와 함께 보존하는 용도로도 사용(제목 `[역전됨]` — 한 번 확정했다가 뒤집힌 것) — 더 이상 능동적으로 참고 안 해도 되지만(토큰 낭비 방지 위해 `base/`/`research/`에서 뺌) `quadnomicon` 소재로는 나중에 쓸 수 있음. **[2026-08-07 확장]** 후보였다가 채택 안 된 것(확정한 적 없이 검토 후 기각)도 같은 방식으로 보존, 제목은 구분을 위해 `[기각됨]` — `[역전됨]`과 의미가 다르므로 혼동하지 말 것. **[2026-08-07 세 번째 확장]** 설계 반전/기각과 별개로, 에이전트가 문서 작성 중 스스로 낸 개념 혼동을 정정한 이력은 `[에이전트 실수]` 태그로 `agent-mistake.md` 하나에 모음(CLAUDE.md 세션 로그 중복 방지) | | `feedback/` | 실사용 피드백을 정리한 긴 로그 — 지금은 비어있음(구현 시작 전) | -| `luau-test/` | **[2026-08-09 신설]** `base/` 확정 사항 중 "추론만으로 확정하고 실제 Luau로 부딪혀본 적 없는 것"(M0 스파이크 대상)을 `luau`/`luau-analyze`/`luau-lsp`/Roblox Studio로 사용자가 직접 돌려볼 독립 실행 스크립트 모음. **[2026-08-13]** `10`의 일부가 처음으로 실측 확인됨(부분) — 확인된 결과 자체는 `audit/`에 기록, 이 폴더는 스크립트/색인만 유지. 나머지 16개+`10`의 남은 부분은 여전히 결과 미확인 — `luau-test/README.md`가 색인 | +| `luau-test/` | **[2026-08-09 신설]** `base/` 확정 사항 중 "추론만으로 확정하고 실제 Luau로 부딪혀본 적 없는 것"(M0 스파이크 대상)을 `luau`/`luau-analyze`/`luau-lsp`/Roblox Studio로 사용자가 직접 돌려볼 독립 실행 스크립트 모음. **[2026-08-13 여섯 번째 세션, 첫 실측]** `luau`/`luau-analyze` 바이너리가 생겨 처음으로 실제 실행 — **런타임 12개 전원 통과**, 타입 쪽에서 `:Compute(fn)` lazy 핸들 계약이 Luau 추론과 충돌하는 게 드러남(`question.md` 0-Y). **`STATUS.md`가 상태의 소스**(pass / 사람 결정 필요 / 스파이크 깨짐 / 미실행 분류 — 사람이 먼저 볼 것만 위에), `luau-test/README.md`는 각 파일의 검증 의도·배경, 실행 결과 상세는 `audit/luau-test-first-run-2026-08-13.md` | | `audit/` | **[2026-08-13 신설]** `luau-test/` 등 스파이크를 실제로 돌려본 뒤 "무엇이 확인됐고 무엇이 아직 안 됐는지"를 기록하는 곳 — 스크립트/계획 자체가 아니라 **실측 결과**만 다룸. base/luau-test와 달리 부분 확인(일부만 통과)도 있는 그대로 기록, 완전히 해소되면 관련 `base/`/`luau-test/README.md` 캐비엇을 지우고 이 문서는 근거로 남김 | | `session/` | **[2026-08-11 신설]** 세션별 상세 로그 원문(시행착오·정정 전 서술 포함, `quadnomicon` 개발로그 소재용) — 루트 `CLAUDE.md`가 3196줄까지 불어나 성능 저하를 유발해서 분리함. 파일명 `YYYY-MM-DD-NN-slug.md`, CLAUDE.md의 "세션 히스토리" 절에서 각 항목이 여기로 링크. 항상 읽을 필요 없음 — 결정의 논의 과정이 궁금할 때만 | | `initreq/` | 프로젝트 착수 시 클론해둔 참고 레포(quad v1, fusion, vide, rbvm, tbox, code-docker) + PA님 실 코드(`artworks/`, 4차 라운드 교차검증 근거) + 원본 요청(`req.md`, `raw-userinput.md`) + `quad2-try`(이전에 시도했다 폐기한 v2 재작성 시도 — 리서치 완료, 결론은 `base/bind-system-plan.md`) — 읽기 전용 리서치 소스, 여기 내용을 옮기지 말고 항상 원본 그대로 유지 | diff --git a/.claude/base/architecture.md b/.claude/base/architecture.md index a15eb51..d023c37 100644 --- a/.claude/base/architecture.md +++ b/.claude/base/architecture.md @@ -211,7 +211,8 @@ existing-instance-bind는 여전히 `research/`에 남아있고 이 구조 확 고유의 어휘"가 아니라 여러 타입에 걸쳐 쓰이거나(`isX`류) 프리미티브 자체가 아닌 것(Dispatch/Brand는 `Type(args)` 생성자가 없는 내부 엔진)의 구성원이라 PascalCase 대상이 아님. Handler 계약 필드(`isHandlable`/ - `priority`/`process`/`retract`)도 여기 속함 — 이건 애초에 "함수"라기보다 + `priority`/`process` — 2026-08-13 다섯 번째 세션에 `retract`가 `process`의 + 반환값으로 합쳐지기 전엔 4종이었음)도 여기 속함 — 이건 애초에 "함수"라기보다 구현체가 채워 넣는 구조체 필드. - **경계 판단 기준**: 새 이름을 지을 때 "이게 특정 프리미티브 타입 하나의 전용 소유물인가?"로 물으면 됨 — 그렇다면 대문자(생성자/메서드/그 diff --git a/.claude/base/attribute-plan.md b/.claude/base/attribute-plan.md index 10e63d5..633dbcf 100644 --- a/.claude/base/attribute-plan.md +++ b/.claude/base/attribute-plan.md @@ -176,7 +176,12 @@ Dispatch의 재진입 가드에 얹어 감지)은 이 버그를 고치긴 했으 이유는 `archive/checkpoint-handler-pattern-reversed.md`. **최종(세 번째 버전) — 체크포인트도 `owners`도 없이, 항상 인덱스 1부터 -직접 위임.** 그룹이 이름마다 공개 `AttributeKey(name)`으로 그냥 +직접 위임.** **[캐비엇, 2026-08-13 여섯 번째 세션] 여기서 "최종"은 *현행 +`hintValue` 모델 기준*이고, 이 주제 자체가 `question.md` **0-Z**로 다시 +열려 있음** — 문서 상단 ⚠️ 배너가 예고하는 "하강 diff" 재디스패치 모델에선 +그룹 A/B가 둘 다 `StoreBind`로 보여 "같은 핸들러"로 판정되므로 **아래 점유 +체크만으로는 그룹↔그룹 충돌을 못 잡음**(`research/dispatch-redispatch-diff-plan.md` +5절). 0-Z가 정해지면 이 절이 네 번째 버전으로 다시 갱신됨. 그룹이 이름마다 공개 `AttributeKey(name)`으로 그냥 `Dispatch.process(inst, key, source, 1)`를 부르면 끝 — **"인덱스 1이 이미 점유돼 있는가"라는 `Dispatch.process` 자신의 점유 체크가 소유권 충돌 감지를 그대로 대신함**: 다른 그룹이나 직접 쓰기가 이미 그 이름을 @@ -242,8 +247,8 @@ Frame { Attribute(styleStore), Attribute(stateStore) } -- 여러 개 나란히 **[2026-08-13 감사에서 추가] `:NameMap()`은 원래 "메커니즘" 절 의사코드에만 등장하고 이 API 목록엔 빠져 있었음** — Handler가 이름 집합을 순회하려면 -반드시 필요한 공개 접근자라 여기 명시(`Tag:Names()`가 `tag-plan.md`에서 -같은 이유로 빠져 있던 것과 같은 누락). +반드시 필요한 공개 접근자라 여기 명시(`Tag:Names()`도 `tag-plan.md`에서 +같은 누락이었고, 같은 감사에서 함께 추가됨 — 지금은 양쪽 다 문서화돼 있음). `Attribute.Merged`가 내부적으로 하는 일은 각 Store에서 이름 붙은 `Source` 슬롯을 그대로 가져와 자기 자신의 key→Source 맵에 넣는 것 — 아래 "레이어드 diff --git a/.claude/base/bind-system-plan.md b/.claude/base/bind-system-plan.md index caa2276..2d98855 100644 --- a/.claude/base/bind-system-plan.md +++ b/.claude/base/bind-system-plan.md @@ -326,8 +326,10 @@ end ``` - **매치 predicate는 `isHandlable`** — `canExecute`가 아님. 둘은 완전히 - 다른 개념이라 혼동하지 말 것: `isHandlable(k,v)`는 KV 매치 predicate(핸들러 - 계약 4종 중 하나, 이 절에서 다루는 것), `canExecute`는 인자로 받은 특정 + 다른 개념이라 혼동하지 말 것: `isHandlable(inst,k,v)`는 KV 매치 predicate + (핸들러 계약 3종 중 하나, 이 절에서 다루는 것 — 예전엔 `(k,v)` 2-인자에 + 4종 계약이었으나 각각 2026-08-07 여덟 번째/2026-08-13 다섯 번째 세션에 + 바뀜, 이 문단만 갱신에서 누락돼 있던 걸 같은 날 감사에서 발견), `canExecute`는 인자로 받은 특정 바인딩/등록 하나가 "지금 살아있어서 실행돼도 되는가"만 보는 별개의 라이프타임 게이트(`base/lifecycle-pattern.md` "생명 바인드 유틸" 절) — KV 매치와 무관. @@ -1309,7 +1311,7 @@ tween-plan.md`도 이에 맞춰 갱신됨). Ref의 진짜 용도는 다름: ### `Ref`의 retract — `State` 재바인드 시 이전 Ref에 `nil` (2026-08-12 여덟 번째 세션, `TagHandler`와 같은 메커니즘 재사용) **배경**: `Ref`는 이미 "일반 프로퍼티/Modifier 필드/Store 값 어디든 자유롭게 -들어감"(위 "동적 경로 가드" 절)이 확정돼 있어 — `State`가 실제로 +들어감"(아래 "동적 경로 가드" 절)이 확정돼 있어 — `State`가 실제로 가능하고, 그러면 Store 값이 `refA`에서 `refB`로 바뀌는 경우가 생김. 이때 `refA`가 계속 "확정된 값(대개 이전 `inst`)"을 들고 있으면, 그 자리가 이제 `refB`로 넘어갔다는 걸 모르는 코드가 `refA.Value`를 계속 유효하다고 믿는 diff --git a/.claude/base/lifecycle-pattern.md b/.claude/base/lifecycle-pattern.md index f8c42af..96db162 100644 --- a/.claude/base/lifecycle-pattern.md +++ b/.claude/base/lifecycle-pattern.md @@ -307,8 +307,18 @@ Roblox 엔진 자체가 Destroy 시 Tag/Attribute/실행 중인 Tween을 전부 쉬움) — 실제 의미는 "이전에 적용한 처리를 무른다/멈춘다"이므로 **`retract`** 로 통일. (`revert`, `rescind`도 검토했으나 `retract`가 "이전에 취한 조치를 철회한다"는 의미로 가장 정확 — `process`/`retract` 쌍으로 자연스럽게 대구를 -이룸.) 대부분의 문서에서 이 이름으로 갱신됨 — 잔여 "cleanup" 표기가 남은 -문서가 있을 수 있으며, 그 확인/정리는 진행 중. +이룸.) 대부분의 문서에서 이 이름으로 갱신됨. + +**[확인 완료, 2026-08-13 여섯 번째 세션] `base/effect-plan.md`의 "cleanup"은 +잔여 stale이 아니라 의도된 별개 개념** — 매 감사마다 재지적되므로 여기 +못박아 둠. 두 층위가 다름: +- **`retract`**: Handler 계약의 것. `process`가 반환하는 클로저로, quad + **내부 배관**이 "이전 처리를 무른다". +- **`cleanup`**: `Effect(fn)`에서 **사용자가 작성한 `fn`이 반환하는 콜백**. + React `useEffect`의 그것과 동형이고, 사용자 API 표면의 어휘라 `retract`로 + 통일할 대상이 아님(오히려 통일하면 React 배경 사용자에게 더 낯설어짐). + +즉 "cleanup 잔여 확인"은 `effect-plan.md`에 대해서는 **끝난 것**으로 봐도 됨. **[2026-08-13 다섯 번째 세션] `retract`는 더 이상 Handler의 *필드*가 아니라 `process`가 반환하는 클로저의 *역할 이름*이다.** 개념/이름 diff --git a/.claude/base/modifier-plan.md b/.claude/base/modifier-plan.md index a0ca67c..ef533b2 100644 --- a/.claude/base/modifier-plan.md +++ b/.claude/base/modifier-plan.md @@ -197,10 +197,12 @@ Compute를 합치고 싶다"는 동기였는데, 이미 두 가지로 다 커버 **런타임은 클래스별 코드 없이 base에 딱 하나만 있으면 됨(2026-08-06 후속 세션, 핵심 통찰).** `mod:FontSize(14)`는 `mod.FontSize(mod, 14)`로 풀리는 -문법 설탕이고, `mod.FontSize`는 `FontSize`가 리터럴 키로 안 박혀있으니 +문법 설탕이고, `mod.FontSize`는 `FontSize`가 **self의 리터럴 키로 절대 안 +박히므로**(아래 ⚠️ — 이게 설계상 필수 조건이지 우연이 아님) 항상 `__index(self, key)`가 잡음 — 그러니 `__index`가 **어떤 key가 오든** 그 key를 클로저에 캡쳐한 `function(self, arg) local clone = table.clone(self) -... end`류 함수를 즉석에서 만들어 리턴하기만 하면 끝. +... end`류 함수를 즉석에서 만들어 리턴하기만 하면 끝(단 그 clone은 필드를 +self 최상위가 아니라 **내부 저장소**에 써야 함 — 역시 아래 ⚠️). > **⚠️ [2026-08-13 여섯 번째 세션, 실측 중 발견] 필드 값은 `self`의 리터럴 > 키에 저장하면 안 되고, 반드시 `__index`를 거치는 내부 저장소에 둬야 함.** diff --git a/.claude/base/relate-plan.md b/.claude/base/relate-plan.md index ecb94bd..64e74e7 100644 --- a/.claude/base/relate-plan.md +++ b/.claude/base/relate-plan.md @@ -61,7 +61,7 @@ weak여야 함 — 그런데 그 안에 담기는 값은 경우에 따라 **강 있는지 먼저 확인할 것 — 갖고 있다면 **두 `Relate` 중 최소 한쪽은 `SetWeak`로 낮추고, 실제 GC 앵커는 `bindLifetime`/`unbindLifetime` 하나로 통일**할 것(구체 사례는 `base/slot-plan.md` "Slot과 Store -바인드의 관계" 절 참고 — `kSlotMap`(inst→slot)/`slotOwner`(slot→inst)가 +바인드의 관계" 절 참고 — `kSlotMap`(`(inst,k)`별 마지막 Slot)/`slotOwner`(slot→inst)가 정확히 이 패턴이었음. **[2026-08-13 다섯 번째 세션 이후]** 그 두 `Relate`는 지금은 존재하지 않음 — `kSlotMap`은 Handler 계약이 클로저 반환으로 바뀌며 불필요해져 삭제됐고 `slotOwner`는 `elementOwner`(전부 `SetWeak`)로 diff --git a/.claude/base/slot-plan.md b/.claude/base/slot-plan.md index 013158c..3d6f15e 100644 --- a/.claude/base/slot-plan.md +++ b/.claude/base/slot-plan.md @@ -437,7 +437,7 @@ claimOwner(element, self) -- self = 담는 Slot. 이미 누가(같은 self 포 -- rawRemove(self, index)/rawExtract 안, 요소를 내보내는 자리 releaseOwner(element, self) --- destroySlotTree(slot) 안, 자식들을 파괴하기 직전(위 "파괴" 절) +-- destroySlotTree(slot) 안, 자식들을 파괴하기 직전(아래 "파괴" 절) releaseOwner(element, slot) ``` @@ -484,6 +484,13 @@ nested `Slot`을 파괴 후 재사용하는 경로가 정확히 이 케이스). Destroy 시점엔 `retract`가 호출되지 않는다는 원칙(`base/lifecycle-pattern.md`)도 동일하게 적용. +> **🔄 [역전됨, 2026-08-13 여섯 번째 세션] 아래 "폐기, 옮기지 않음" 결정은 +> 뒤집혔습니다 — 지금은 `State` 교체가 **파괴가 아니라 언마운트**이고, +> **portal은 별도 기능이 아니라 그 결정의 자연스러운 귀결**입니다. +> 최신 설계는 이 문서 아래쪽 "`State` 교체는 파괴가 아니라 언마운트" +> 절(+ `unmountSlotTree`)이 정본. 아래 문단은 그 결론에 이르기까지의 +> 히스토리로만 남겨둡니다 — **여기 적힌 "확정"을 그대로 믿고 구현하지 말 것.** + **확정(2026-08-04 검증 라운드, 메커니즘은 위처럼 2026-08-12 아홉 번째 세션에 정정): retract/재바인드되는 slot은 옮겨지지 않고 그냥 폐기된다.** Slot은 바인딩되는 순간 그 안의 요소를 전부 own해버리는 데이터형 — 새 slot @@ -940,6 +947,14 @@ end 계속 돎. 리스트가 200개+가 되면 "보이는 건 20개인데 200개가 전부 계속 돌아가는" 문제가 실제 비용으로 드러남. +**[캐비엇, 2026-08-13 여섯 번째 세션] 아래 근거는 "제거 = 진짜 파괴"를 +전제로 쓰였는데 그 전제가 언마운트 재설계로 바뀌었음** — 다만 **결론은 +그대로 유효**함: 언마운트도 물리 트리에서 떼어내므로 `Visible=false` 토글과 +달리 렌더/레이아웃 비용이 사라지고, 아무도 안 들고 있으면 GC되어 이벤트 +연결·재계산도 결국 정리됨. 즉 "lazy하지 않음" 문제는 파괴가 아니라 +**언마운트만으로도 해소**되고, 오히려 사용자가 그 요소를 들고 있다가 다시 +쓸 수 있게 되는 이득이 붙음. + **해법**: `updateFn`을 매 사이클 호출하되, `prev`를 줘서 "바꿀 게 없으면 그대로 돌려주기만 하면 되는" 저렴한 경로를 만들고, filter 탈락은 `nil` 반환으로 **진짜 파괴**되게 함 — Visible 토글이 아니라 실제 Remove. @@ -1056,7 +1071,11 @@ function activateList(self, inst) end if result ~= prev then - if prev ~= nil then rawRemove(self, prev) end -- 파괴 + -- [정정, 2026-08-13 여섯 번째 세션] 파괴가 아니라 **언마운트** — + -- rawUnmount는 rawRemove와 같되 element를 Destroy하지 않고 + -- 소유권만 반납(unmountSlotTree 사용, 아래 "파괴" 절). + -- 명시적 CRUD Remove/Clear/dispose만 여전히 파괴. + if prev ~= nil then rawUnmount(self, prev) end if result ~= nil then rawAdd(self, result, pos) end -- 새로 배치, 압축 위치 기준 mounted[key] = result elseif prev ~= nil and keyIndex[key] ~= pos then @@ -1069,7 +1088,7 @@ function activateList(self, inst) for key in pairs(keyIndex) do -- 직전 사이클에 존재했던 전체 key if not seen[key] then local prev = mounted[key] - if prev ~= nil then rawRemove(self, prev) end + if prev ~= nil then rawUnmount(self, prev) end -- 위와 같은 이유로 비파괴 mounted[key], userdata[key] = nil, nil end end @@ -1140,7 +1159,10 @@ raw `i`를 그대로 위치 인자로 썼는데, 앞쪽 item이 filter로 마운 실행)의 로컬 변수(클로저 업밸류) — 별도 전역 weak table(`Relate` 등) 불필요, `inst`/`self`가 살아있는 동안만 존재하면 되고 죽으면 클로저도 같이 GC됨(아래 "구독 시점" 절). -- **`reconcile`이 직접 호출하는 건 `rawAdd`/`rawRemove`/`rawMove`뿐** — +- **`reconcile`이 직접 호출하는 건 `rawAdd`/`rawUnmount`/`rawMove`뿐** + (**[정정, 2026-08-13 여섯 번째 세션]** 예전엔 `rawRemove`(파괴)였으나 + 언마운트 전환으로 바뀜 — 데이터에서 빠진 아이템도 파괴되지 않고 + 언마운트만 되며, 아무도 안 들고 있으면 GC) — `rawExtract`/`rawSwap`/`rawClear`도 (위 "모든 공개 CRUD는 가드+위임" 구조상) 당연히 존재하지만, `:List`의 reconcile 알고리즘 자체가 그 셋을 쓸 일이 없을 뿐(제거는 항상 파괴 확정이라 `Extract` 아닌 `Remove` @@ -1236,9 +1258,12 @@ GC에 위임" 원칙 그대로. ## 열린 질문 (`.claude/question.md`에도 취합) -- 재마운트 에러 처리(throw), retract 시 폐기(옮기지 않음) 둘 다 확정. 남은 건 - 실제 구현 단계에서 이 "폐기" 동작이 실사용에서 불편하지 않은지 재검증하는 - 정도 — 설계 방향 자체는 더 이상 열려있지 않음. +- 재마운트 에러 처리(throw)는 확정. **[역전됨, 2026-08-13 여섯 번째 세션] + "retract 시 폐기(옮기지 않음)"는 뒤집혔음** — 지금은 `State` 교체가 + **언마운트**이고 이전 Slot은 파괴되지 않음(파괴는 명시적 `Remove`/`Clear`/ + `dispose`만). "실사용에서 불편하지 않은지 재검증"이라던 남은 항목도 그 + 재설계로 해소됨 — 최신 설계는 아래 "`State` 교체는 파괴가 아니라 + 언마운트" 절이 정본. - "클래스가 슬롯을 받는 방법"(Named Slot 없음)도 확정됨(위 "클래스가 슬롯을 받는 방법" 절 참고). - **[해소됨, 2026-08-09 세 번째 세션]** `add`/`remove`/`clear` CRUD 의미론, @@ -1461,6 +1486,21 @@ end -- 이미 못박았듯 reconcile(위 "여러 Slot이 섞일 때" 절 근처)은 element 기준으로 -- rawRemove(self, prev)를 부름. 둘 중 하나로 통일할지 얇은 변환 계층을 둘지는 -- 아직 M6 구현 세부로 열려 있음 — 이 블록은 그 결정 전 illustrative 예시. +-- [신설, 2026-08-13 여섯 번째 세션] rawRemove의 비파괴 짝 — `:List`의 +-- reconcile과 `Extract` 계열이 씀. rawRemove와 **딱 하나만 다름: 안 죽인다.** +function rawUnmount(self, index) + local element = self._elements[index] + local bk = getBookkeeping(self) + if bk.observers[index] then + unbindLifetime(self._mountedInst, bk.observers[index]) + end + releaseOwner(element, self) -- 소유권은 반납(이제 다른 곳에 넣을 수 있음) + if isSlot(element) then unmountSlotTree(element) else element.Parent = nil end + + spliceArraysDown(self, index) + recompute(self, bk) +end + function rawRemove(self, index) local element = self._elements[index] local bk = getBookkeeping(self) diff --git a/.claude/base/store-semantics.md b/.claude/base/store-semantics.md index a40ad29..a550183 100644 --- a/.claude/base/store-semantics.md +++ b/.claude/base/store-semantics.md @@ -202,9 +202,11 @@ State를 만족하도록 만들고, RefSource라는 별도 타입은 폐기**하 케이스(Source가 State를 만족하는 제네릭 메소드 체이닝)를 포함해서 검증할 것. -**이름 주의**: `Source`/`State`라는 이름 자체가 `CLAUDE.md` "지금 할 일" -2번의 용어 정리 대상(특히 `State`)과 겹침 — 구조(서브타입 관계, RefSource -폐기)는 지금 확정해도 정확한 이름은 용어 정리 라운드까지 가칭으로 남김. +**이름 주의 — [해소됨, 2026-08-12 스무 번째 세션]**: `Source`/`State`라는 +이름이 한때 용어 정리 대상(특히 `State`)이었으나, **`State`는 현재 이름 +그대로 유지로 최종 확정됨**(`Computed`/`Derived`/`Pipe` 전부 기각 — 근거는 +`bind-system-plan.md` "네이밍 — `Compute`가 `-ed`가 아닌 이유" 절과 +`question.md` 1번). 더 이상 가칭이 아님. ## Store 값 설정 문법 — `myStore.key = value` 폐기, `source:Set(value)`로 전환 (2026-08-06 후속 세션, 정정) diff --git a/.claude/luau-test/README.md b/.claude/luau-test/README.md index 20cd196..8aded91 100644 --- a/.claude/luau-test/README.md +++ b/.claude/luau-test/README.md @@ -1,5 +1,10 @@ # .claude/luau-test — M0 착수 전 실 Luau 기술검증 스파이크 모음 +> **⚡ 지금 뭘 봐야 하는지부터 보려면 [`STATUS.md`](STATUS.md)** — 파일별 +> pass / 사람 결정 필요 / 스파이크 깨짐 / 미실행 분류. 이 README는 각 파일이 +> **무엇을 왜 검증하는지**(의도·배경)를 담고, 상태는 STATUS.md가 소스. + + **[2026-08-09 이동]** 처음엔 레포 루트 `luau-ignoreme/`(git 자동 제외 폴더)에 만들었으나, 사용자가 직접 확인해볼 만한 검증 코드라 커밋해서 레포에 남기기로 함 — `.claude/luau-test/`로 옮기고 일반 추적 대상으로 @@ -52,7 +57,7 @@ ROADMAP 항목 근거인지, 어떻게 실행하는지, 실행 후 뭘 확인해 | `04-dispatch-chain-retractFrom.luau` | **[2026-08-13 감사에서 전면 재작성 + 파일명 변경]** 인덱스 기반 `chains`/`Dispatch.retractFrom`이 다단 재귀 위임에서 정확한지 — 3단 체인이 인덱스 1/2/3으로 안 겹치고 쌓이는지(= `State>` **정상 동작**, UB 아님), 안/바깥 store 재발행 시 깊은 인덱스부터 정리되는지, hint가 target 인덱스에만 가는지 + **음성 대조군**: `chains:SetStrong`을 `handler.process` 뒤에 두면 최초 마운트에서 하위 retractor가 유실되는 버그 재현. 옛 버전은 핸들러 identity 기반 추적과 "중복 push 즉시 error" 가드를 검증했는데 그 가드는 다섯 번째 세션 재설계로 **없어져서** 설계와 정반대를 테스트하고 있었음 | `bind-system-plan.md` "Dispatch 체인"(2026-08-13 다섯 번째 세션 재설계) + 2026-08-13 감사 | | `05-store-state-diamond-propagation.luau` | push-invalidate/pull-recompute가 다이아몬드 의존성에서 중복 재계산 없이 동작하는지 | ROADMAP M0-1 | | `06-component-boundary-nil-hole-props.luau` | `props.Modifier or None` 관용구가 컴포넌트 경계 nil-hole을 막는지 + `Params` 타입 체크 | `component-composition-plan.md` "필수 관용구", ROADMAP M0-5 | -| `07-relate-weak-table-gc.luau` | `Relate`의 lazy 서브테이블 생성 + weak-key GC가 실제로 동작하는지 | `relate-plan.md` "M2 착수 시 실측 확인" | +| `07-relate-weak-table-gc.luau` | `Relate`의 lazy 서브테이블 생성 + weak-key GC가 실제로 동작하는지 | `relate-plan.md` "M2 착수 시 실측 확인" **[2026-08-13 보강]** 4번 섹션 신설 — `_countEntries()`(테스트 전용) + weak-value canary로 **"inst가 죽으면 중첩 StrongMap 안의 payload까지 연쇄 GC되는가"를 직접 검증**(원래는 sanity check만 하고 헤더의 핵심 주장은 미검증이었음). 파일이 스스로 적어둔 "weak table 엔트리를 셀 표준 API가 없다"는 전제도 틀렸음 — outer가 `__mode="k"`라 GC 후 `pairs`에서 사라짐 | | `08-type-source-satisfies-state.luau` (타입체크 전용) | `Source`가 `State`를 구조적으로 만족하는 제네릭 타입이 솔버에서 안전한지 | `store-semantics.md` "검증 필요", ROADMAP M0-2 | | `09-type-modifier-overridden-subtype.luau` (타입체크 전용) | `FrameModifier <: GuiObjectModifier`처럼 서브타입 관계인 Modifier를 `Overridden`으로 섞을 때 타입이 통과하는지 | `modifier-plan.md` 9-2번, ROADMAP M7 | | `10-roblox-studio-checks.server.luau` (Studio 전용) | (A) `bindLifetime`/`unbindLifetime`/`canExecute`/`canBound`의 gcconn 트릭 + 이중 바인딩 게이트(Destroy 시 Connected 전환 포함), (B) Attribute의 Instance 참조 타입 지원, (C) CollectionService 태그/GetTagged 왕복 — **[2026-08-13]** A 섹션 앞부분(신호 미발화, Destroy 시 Connected 즉시 전환)은 사용자 자작 스크립트로 부분 확인됨, `audit/gcconn-trick-verification.md` 참고. A-1/A-2(`canBound` 게이트)/B/C는 이 공식 파일로 아직 확인 안 됨 | `lifecycle-pattern.md`, `bind-system-plan.md` "이중 바인딩 금지", CLAUDE.md 2026-08-06 세션, `debug-tooling-plan.md` | @@ -64,7 +69,7 @@ ROADMAP 항목 근거인지, 어떻게 실행하는지, 실행 후 뭘 확인해 | `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 | | `18-relate-mutual-cycle-gc.luau` | **[2026-08-13 신규]** 서로 다른 두 `Relate`가 서로의 키를 상대방의 강한 값으로 제공하는 상호 순환은 Luau에 ephemeron이 없어 GC가 못 푼다는 주장(지금까지 공식 문서 인용으로만 뒷받침됨) — 음성 대조군(순환 재현)과 양성 대조군(한쪽을 weak-value로 낮추면 풀리는지) 둘 다 실측 | `relate-plan.md` "위험한 패턴" 절(2026-08-12 열세/열네 번째 세션), `slot-plan.md`의 `kSlotMap`/`slotOwner`/`elementOwner` 실사례 | -| `19-ownership-refcount-relate-patterns.luau` | **[2026-08-13 신규]** "retract는 항상 불림" 정정(2026-08-12 열한 번째 세션) 이후 새로 생긴 세 소유권/참조카운트 알고리즘(A: Tag `kTagMap`/`tagNameMap` 참조 카운트 — 여러 위치가 같은 이름을 겹쳐 가져도 마지막 홀더가 빠질 때만 실제 `RemoveTag`, B: ~~Attribute `rawNew`+`owners` 소유권~~ — **[2026-08-13 감사] 이 섹션은 폐기된 설계를 검증 중이라 재작성 필요**: `rawNew`/`owners` 수동 레지스트리는 두 번 뒤집혀 사라졌고, 지금은 그룹이 공개 `AttributeKey(name)`으로 항상 인덱스 1에 위임하고 `Dispatch.process`의 **점유 체크**가 소유권 충돌을 대신 잡음 — 검증 대상도 그쪽으로 바뀌어야 함, C: Slot `elementOwner`의 `claimOwner`/`releaseOwner` — **[2026-08-13 감사] 이 섹션도 갱신 필요**: nested는 엄격 `claimOwner`(같은 owner 재클레임도 error), top-level만 `claimOwnerAt`으로 `(inst,k)`까지 봐서 spurious 재발행을 구분)가 실제로 정확히 갈리는지 | `tag-plan.md` "메커니즘" 절, `attribute-plan.md` "이름 소유권" 절, `slot-plan.md` "요소 소유권 — `elementOwner`" 절(전부 2026-08-12 세션들, 03/04/11번과는 다른 새 알고리즘 모양이라 별도 실측 필요하다고 판단) | +| `19-ownership-refcount-relate-patterns.luau` | **[2026-08-13 신규, 같은 날 B/C 전면 재작성 — 지금은 현행 설계 기준]** 세 소유권/참조카운트 알고리즘 검증. **A**: Tag `tagNameMap` 참조 카운트(여러 위치가 같은 이름을 겹쳐 가져도 마지막 홀더가 빠질 때만 실제 `RemoveTag`. 옛 `kTagMap`은 클로저 캡처로 대체돼 삭제됨). **B**: Attribute 이름 소유권 — 공개 `AttributeKey(name)` 캐시 + `Dispatch.process`의 인덱스 1 **점유 체크**가 충돌을 잡는지(옛 `rawNew`+`owners` 수동 레지스트리는 폐기). **C**: Slot 소유권 — nested 엄격 `claimOwner`(같은 owner 재클레임도 error) vs top-level `claimOwnerAt(inst,k)`(정확히 같은 자리 재발행만 no-op). **셋 다 음성 대조군 포함** — 옛 로직이 `Slot{a,a}`/`Frame{slot,slot}`을 조용히 통과시키는 걸 재현. **캐비엇**: B는 `question.md` 0-Z(하강 diff 모델에선 그룹↔그룹을 점유 체크만으론 못 잡음)가 정해지면 다시 손봐야 함 | `tag-plan.md` "메커니즘", `attribute-plan.md` "이름 소유권", `slot-plan.md` "요소 소유권" | | `20-slot-splice-index-arithmetic.luau` | **[2026-08-13 신규]** `Slot:Splice(index, removeCount, ...newElements)`의 shift+recompute 1회 계산이, `Extract`/`Add` 반복으로 재현한 참조 구현과 항상 같은 결과를 내는지 — 제거/삽입 길이가 다를 때(delta 양수/음수) 뒤 요소가 밀리는 방향과 양을 헷갈리는 off-by-one 위험(이 프로젝트가 `Dispatch.recompute`에서 실제로 냈던 것과 같은 클래스의 버그)을 경계값 케이스로 검증 | `slot-plan.md` "확정" CRUD 표 + "`Splice` 신설" 절(2026-08-12 열다섯 번째 세션), `bind-system-plan.md`의 `recompute` off-by-one 수정 사례(2026-08-11 여섯 번째 세션) | ## 공통 유틸리티 @@ -207,10 +212,13 @@ error)를 추가하고, `04`의 3~4단계를 "가드가 실제로 걸리는지 + gcconn-trick-verification.md`), 재확인 불필요. A-2(재-bindLifetime 허용 여부)가 실패하면 `canBound`/`unbindLifetime` 설계 자체를 재검토해야 함 — **이건 아직 미확인, 공식 `10` 파일로 꼭 돌려볼 것.** -- `04`는 3단계에서 `error로 막혔는가: true`, 4단계에서 체인 길이가 2로 - 복구되는 게 기대값 — 만약 3단계가 error 없이 통과해버리면(`ok == true`) - 가드 pseudocode 자체가 어딘가 안 맞는 것이니 바로 알려줄 것(체인 파손이 - 다시 조용히 넘어가는 회귀). +- `04`는 **[2026-08-13 여섯 번째 세션에 전면 재작성 — 옛 판정 기준 폐기]** + 두 시나리오를 연달아 돌림. **정상 설계**는 [1] 체인 깊이 3, [3] 옛 inner + 구독 0, [4] `STALE` 미반영이 기대값. **음성 대조군**은 [1] 깊이가 **1**로 + 무너지고 [4]에서 `STALE`이 반영되는 게 기대값(= 버그가 재현돼야 정상). + 대조군이 정상 시나리오와 똑같이 나오면 스파이크 모델링이 실제 구현과 + 어긋난 것이니 스파이크 쪽을 먼저 의심할 것. (옛 기준이던 "3단계에서 + error로 막혔는가"는 다섯 번째 세션이 그 가드 자체를 없애서 폐기됨.) - `11`은 전부 PASS가 기대값 — FAIL이 하나라도 있으면 어느 케이스인지 그대로 알려줄 것(특히 "변환 함수가 반환한 값" 케이스는 놓치기 쉬운 경로라 실제 구현에서도 잘 짜였는지 중요한 신호). @@ -239,9 +247,13 @@ error)를 추가하고, `04`의 3~4단계를 "가드가 실제로 걸리는지 + 절과 `slot-plan.md`의 `kSlotMap`/`slotOwner`/`elementOwner` GC 설계 전체를 최우선으로 재검토해야 함(Slot GC 안전성의 유일한 근거였음). - `19`는 전부 PASS가 기대값 — A 섹션이 FAIL이면 `tag-plan.md`의 참조 - 카운트 알고리즘 자체를, C 섹션이 FAIL이면(특히 "같은 owner 재클레임은 - no-op"이 안 되면) `slot-plan.md`의 `elementOwner` 설계를 최우선으로 - 재검토할 것 — C가 깨지면 재귀 재emit마다 마운트된 서브트리 전체가 + 카운트 알고리즘 자체를, C 섹션이 FAIL이면 `slot-plan.md`의 `elementOwner` + 설계를 최우선으로 재검토할 것. **[2026-08-13 판정 기준 정정]** C의 기대 + 동작이 바뀌었음 — 예전의 "같은 owner 재클레임은 no-op"은 버그로 판정돼 + 둘로 쪼개짐: **nested `claimOwner`는 같은 owner 재클레임도 error**(엄격), + **top-level `claimOwnerAt(element,inst,k)`만** 정확히 같은 `(inst,k)`의 + 재발행에서 `false`. 옛 문구를 그대로 적용하면 정상 동작(nested error)을 + 실패로 오판함 — C가 깨지면 재귀 재emit마다 마운트된 서브트리 전체가 파괴됐다 재생성되는 파괴적 버그로 직결됨. - `20`도 전부 PASS가 기대값 — FAIL이 있으면 어느 케이스(특히 delta 부호가 바뀌는 경계값)인지와 최종 배열/제거분이 어떻게 달랐는지 그대로 알려줄 diff --git a/.claude/luau-test/STATUS.md b/.claude/luau-test/STATUS.md new file mode 100644 index 0000000..42019a1 --- /dev/null +++ b/.claude/luau-test/STATUS.md @@ -0,0 +1,87 @@ +# 스파이크 상태판 — **사람이 먼저 볼 것만 위에** + +> 마지막 갱신: 2026-08-13 여섯 번째 세션(첫 실측 라운드). +> 상세 결과는 `.claude/audit/luau-test-first-run-2026-08-13.md`. +> 실행법: `luau <파일>` (런타임) / `luau-analyze <파일>` (타입 전용). + +## 🔴 사람 결정 필요 — 설계가 걸린 것 (2건) + +| 파일 | 무엇이 걸렸나 | 어디로 | +|---|---|---| +| `15-type-compute-trailing-deps-typepack.luau` | **`:Compute(fn)`의 lazy 핸들 계약이 Luau 추론과 충돌.** `state:Compute(function(s) return s:Get()*2 end)`가 타입 에러. 콜백이 raw 값을 받으면 완전 클린 — 즉 표기 조정이 아니라 계약 자체가 원인. `Effect`/`Observer`/`Animate`/`Operator` 등 같은 계약 공유 API 전부에 걸림 | `question.md` **0-Y** | +| `08-type-source-satisfies-state.luau` | 핵심 질문(Source⊇State)은 통과. 다만 `State`가 **자기 자신**을 다른 타입 인자로 재귀 참조하면 `Recursive type being used with different parameters` — 사용자 방향은 "구울 때 인라이닝" | `question.md` **0-Y** 하단 | + +## 🟠 스파이크 자체가 깨져 있음 — 재작성 필요 (3건, 설계 문제 아님) + +| 파일 | 상태 | 무엇을 고쳐야 하나 | +|---|---|---| +| `13-type-ref-preref-subtype.luau` | 타입 A섹션은 ✅ 통과 / **런타임 B섹션 실행 불가** | B가 A의 더미 스텁(`fakePreRef = nil`)에 막혀 도달 못 함 — 두 섹션 분리 | +| `15-type-compute-trailing-deps-typepack.luau` | **파싱 실패**(SyntaxError) | 음성 대조군의 타입 표기가 `TypeError`가 아니라 `SyntaxError`로 걸려 **파일 전체가 아무것도 검증 못 함** — 대조군을 별도 파일/블록으로 격리 | +| `16-type-store-key-typefunction.luau` | ❌ 실패 | `types.newfunction` 시그니처가 설치된 버전의 실제 API와 안 맞음 — 실제 API 재확인 후 재시도 | + +## ⚪ 아직 안 돌림 + +| 파일 | 이유 | +|---|---| +| `10-roblox-studio-checks.server.luau` | **Studio 전용**(`luau` CLI로 못 돌림). A 섹션 일부만 사용자가 자작 스크립트로 실측 — `audit/gcconn-trick-verification.md`. A-1/A-2/B/C는 여전히 미확인 | + +--- + +## ✅ 통과 — 설계 성립 확인됨 (런타임 12개) + +| 파일 | 확인된 것 | +|---|---| +| `01-two-pass-array-hash-order` | 배열 파트 전체 → 해시 파트 순. `Dispatch.drive` 두 패스 계약과 `PreRef` 호이스팅의 전제 | +| `02-none-sentinel-vs-nil-holes` | `nil` 소진 시 `#t` 50→49로 무너짐 / `None`은 항상 50. 반대로 Ref 콜백 배열은 `None` 쓰면 죽은 슬롯 1000개 잔존 — **두 배열의 규칙이 서로 반대여야 함**이 정량 확인 | +| `03-recursive-store-bind-dispatch` | StoreBind 재귀 재-dispatch, `None`→`nil` 흐름, 무한재귀 없이 종료 | +| `04-dispatch-chain-retractFrom` | 인덱스 기반 체인 + **음성 대조군이 감사 버그를 재현**(아래 별도 절) | +| `05-store-state-diamond-propagation` | 다이아몬드에서 재계산 정확히 1회, invalidate 2번째는 즉시 중단 | +| `06-component-boundary-nil-hole-props` | `or None` 없으면 앞쪽 nil-hole로 슬롯 소실, 관용구 쓰면 항상 보존 | +| `07-relate-weak-table-gc` | **연쇄 GC 확정**(아래 별도 절) — GC-native 아키텍처의 핵심 전제 | +| `09-type-modifier-overridden-subtype` | 문서가 우려한 `FrameModifier`↔`GuiObjectModifier` 서브타입 깨짐이 그대로 재현, fallback(`any`)은 정상 | +| `11-modifier-illegal-value-error` | Modifier 필드/Source에 핸들러 계층 값 넣으면 즉시 error — 16개 케이스 전원 | +| `17-modifier-index-tableclone-chaining` | 제네릭 `__index` + `table.clone` 체이닝, 메타테이블 참조 공유, 형제 분기 무오염 | +| `18-relate-mutual-cycle-gc` | **두 `Relate` 상호 순환은 실제로 GC 안 됨**(아래 별도 절) | +| `19-ownership-refcount-relate-patterns` | Tag 참조 카운트 / Attribute 점유 체크 / Slot `claimOwner` vs `claimOwnerAt` — **음성 대조군 포함** 전원 통과 | +| `20-slot-splice-index-arithmetic` | `Splice` 산술 11개 경계 케이스 전부 참조 구현과 일치 | +| `12-type-attribute-generic-key-narrowing` | ❌지만 **설계 영향 없음** — 제네릭 키 narrowing이 안 되는 건 `attribute-plan.md`가 이미 fallback으로 예비해둔 결과(타입 패밀리가 유일하게 믿을 경로) | +| `14-type-nilable-default-overload` | ⚠️ 부분 — 의도한 오용은 막지만 정상 nilable 사용례까지 막아 현 스케치로는 채택 불가. 설계 결정은 아직 필요 없음(대안이 이미 UB 경고로 존재) | + +### 특별히 중요한 통과 3건 + +**`04` — 직전 감사가 찾은 버그가 음성 대조군으로 재현됨** + +| 관측 지점 | 정상(수정본) | 대조군(버그) | +|---|---|---| +| 최초 마운트 후 체인 깊이 | **3** | **1** | +| 재발행 후 옛 store 구독 | 0 (끊김) | 1 (안 끊김) | +| 죽은 store를 건드리면 | 값 유지 | **`STALE`로 덮어써짐** | + +`chains:SetStrong`을 `handler.process` 뒤에 두면 하위 retractor가 통째로 +유실되고, 결국 **버려진 store가 나중에 UI를 덮어쓰는** 데까지 감. + +**`07` — GC-native 아키텍처의 핵심 전제** +``` +inst 5개만 살린 상태 → 살아남은 payload 5 / 엔트리 5 (기대치 일치) +모든 참조를 놓은 뒤 → 살아남은 payload 0 / 엔트리 0 (기대치 일치) +``` +`bindLifetime`으로 매달아둔 자원이 `inst`와 함께 연쇄 소멸함이 확인됨. +(이 스파이크는 원래 sanity check만 하고 있어서 이번에 보강한 것 — 파일이 +스스로 적어둔 "weak table 엔트리를 셀 방법 없음"이라는 전제가 틀렸음.) + +**`18` — `relate-plan.md`의 상호 순환 경고** +``` +상호 강참조 순환: inst=true, value=true (GC 못 풂) +한쪽을 weak-value로 낮춤: inst=false, value=false (풀림) +``` +추측이 아니라 **실제로 GC가 안 됨** — `Slot`의 두-`Relate` 수정이 필수 +조치였음이 입증. + +--- + +## 이 표를 갱신하는 방법 + +스파이크를 돌리거나 고칠 때마다 **여기 분류부터 옮기고**, 상세 서술은 +`audit/`의 실행 결과 문서에 쓸 것. 이 파일은 "지금 뭘 봐야 하는가"만 +빠르게 답하는 용도라 길어지면 안 됨(파일별 검증 의도/배경은 +`luau-test/README.md`가 이미 담당). diff --git a/.claude/research/documentation-content-map.md b/.claude/research/documentation-content-map.md index 56e010f..139bcc2 100644 --- a/.claude/research/documentation-content-map.md +++ b/.claude/research/documentation-content-map.md @@ -68,7 +68,7 @@ v1 폐기 API/버그/구조 결함 전부 v2 설계를 정당화하는 내부 ### bind-system-plan.md (943줄, 최대 문서) - 초심자: Source/Store/State 기본 정의+생성자, State 읽기 전용 규칙 / dot-access가 값 읽기 1급 경로 / `:With`+`:Compute` 최소 사용법 / Ref 기본 개념(children 배열에 직접 놓기, 별도 `CreatedRef` 없음) / 이벤트 self 미채택 기본 규칙+문자열 키 / 인스턴스 생성(제네릭+정적 필드) / 라이브러리 초기화 3줄(`RobloxFactory(QuadBase)`) -- api: `state:Observer(fn)` 사용법(→심화: weak-table 내부 인덱싱) / `:Subscribe()`/`:Unsubscribe()` 시그니처(→심화: 강참조 레지스트리 구조) / Ref 일반화 표면 API(→심화: "왜 값이 아니라 콜백인가") / 이벤트 store-bind 존재+권장 안 함 가이드(→심화: 엔지니어링 비용 근거) / 핸들러 4종 계약(`isHandlable`/`priority`/`process`/`retract`) / `Attribute` 특수 키 후보(미확정 명시 필요) +- api: `state:Observer(fn)` 사용법(→심화: weak-table 내부 인덱싱) / `:Subscribe()`/`:Unsubscribe()` 시그니처(→심화: 강참조 레지스트리 구조) / Ref 일반화 표면 API(→심화: "왜 값이 아니라 콜백인가") / 이벤트 store-bind 존재+권장 안 함 가이드(→심화: 엔지니어링 비용 근거) / 핸들러 3종 계약(`isHandlable`/`priority`/`process` — `process`가 자기 retract 클로저를 반환, 2026-08-13 다섯 번째 세션에 4종에서 축소) / `AttributeKey` 특수 키(2026-08-11 아홉 번째 세션에 `Attribute`에서 개명, 그룹 값 `Attribute(...)`와 구분 — 확정됨) - 심화: push-invalidate/pull-recompute 전파 모델+"관측해야 실체화된다" 원칙+`previous` 캐비엇 / **왜 State를 Modifier처럼 플래튼하지 않는가**(이미 문서화 완료, 아래 3번 참고) / Store가 Store를 못 담는 이유 / 이벤트 self 미채택 4가지 근거 / store-bind 재귀 래핑 내부 메커니즘, retract가 Destroy 시 호출 안 되는 이유 / 같은 팩토리 재호출 no-op·다른 팩토리 충돌 에러 내부 안전장치 - skip: quad2-try 리서치 결과 섹션 전체(OOP 상속/커스텀 파서/Slot 스텁/`Pipe` 폐기 이력) / PA님 코드 교차검증 절(역사적 검증 기록) / "남은 열린 질문"/"확정된 것" 메타 요약 diff --git a/.claude/session/2026-08-13-06-commit-audit-dispatch-redesign-bugs.md b/.claude/session/2026-08-13-06-commit-audit-dispatch-redesign-bugs.md index 3965b13..c68ae19 100644 --- a/.claude/session/2026-08-13-06-commit-audit-dispatch-redesign-bugs.md +++ b/.claude/session/2026-08-13-06-commit-audit-dispatch-redesign-bugs.md @@ -488,3 +488,63 @@ dispatch-redispatch-diff-plan.md`에만 있음** — base만 읽고 구현하면 이미 base에 확정 반영됨**(재디스패치 모델과 독립적인 결정이라 먼저 들어감) — 이 비대칭을 헷갈리지 말 것. +## 여섯 번째 라운드 (같은 세션, 마무리) — 첫 실측 + 코퍼스 전반 감사 + +### 스파이크 첫 실측 + +`luau`/`luau-analyze`가 사용 가능해져 2026-08-09 이래 처음으로 돌림. +런타임 12개 전원 통과. 상세는 `audit/luau-test-first-run-2026-08-13.md`, +상태 분류는 신설한 `luau-test/STATUS.md`(사용자 요청: "사람이 보기엔 뭐를 +바로 봐야하는지 눈에 안 띄어서"). + +핵심 셋: `04`가 이번 세션 감사의 버그를 음성 대조군으로 재현(체인 깊이 +3→1, 죽은 store가 UI를 덮어씀), `07`은 보강해야 실제 검증이 됐고(연쇄 GC +미검증 상태였음 + "weak 엔트리를 셀 방법 없다"는 자기 전제가 틀렸음), +`18`이 `Relate` 상호 순환 경고를 실증. + +타입에서 진짜 이슈 하나 — `:Compute(fn)`의 lazy 핸들 계약이 Luau 양방향 +추론과 충돌. 최소 재현으로 직접 좁힘(에이전트 보고를 액면 그대로 안 받음): +`read`/`self` 표기 조정으로는 안 풀리고 콜백이 raw 값을 받으면 완전 클린 — +즉 계약 자체가 원인. `question.md` **0-Y** 신설. 사용자 방향은 "순환 타입을 +만들기보다 `State` 타입을 구울 때 인라이닝"(`Modifier` flat 타입 생성과 +같은 결)이고, 사람이 직접 확인할 부분이라 0-Z와 함께 사용자 판단 목록. + +부수 수확: `17` 크래시의 원인이 실은 `modifier-plan.md`의 문서 결함이었음 +— "데이터를 테이블에 직접 두고"가 "self 최상위 리터럴 키"로 읽히는데, +그러면 `__index`가 `rawget` 성공 시 안 불려 **같은 필드 재호출이 죽음**. +그 재호출 패턴이 문서 3·4절의 대표 용례라 실사용에서 즉시 터지는 경로. + +### 코퍼스 전반 모순·stale 감사 (사용자 요청) + +에이전트 둘로 base 내부 / base↔바깥 정합성을 나눠 감사. **오탐 방지를 위해 +"의도적으로 미반영인 것"(⚠️ 배너가 예고하는 `hintValue` 건, `research/`의 +미래 설계안, `archive/`·`session/`의 원문 보존)은 보고 대상에서 제외**하도록 +지시. 보고받은 것은 전부 직접 사실 확인 후 수정: + +**가장 위험했던 것 — `ROADMAP.md`가 최우선 게이트를 전혀 안 짚고 있었음.** +base 4개 문서엔 ⚠️ 배너를 달아뒀는데 정작 **구현 순서의 소스**인 ROADMAP엔 +없어서, M2/M4/M10 담당자가 그대로 옛 모델로 구현할 위험이 실재했음 — 세 +마일스톤 전부에 배너/포인터 추가. + +**두 번째 — `slot-plan.md` 앞부분이 뒤집힌 결정을 "확정"으로 자칭.** +487-495행("폐기, 옮기지 않음, portal은 오버엔지니어링")과 "열린 질문" 절이 +역전 표시 없이 남아 있어, 문서를 앞에서부터 읽는 구현자가 구 모델로 짤 +위험. 🔄 역전 배너 추가. 더 심각하게 **`reconcile` 의사코드가 여전히 +`rawRemove`(파괴)를 부르고 있었음** — 같은 문서가 "[반영 완료]"라 태그해둔 +것과 정면 모순이라 `rawUnmount`를 신설해 교체. filter/toggle 근거 절도 +"제거=파괴" 전제 위에 서 있어 캐비엇 추가(결론 자체는 언마운트로도 유효). + +**그 외**: `ROADMAP.md` M11 Tween이 이미 확정된 결정 넷을 미결로 두고 +있던 것(override 정책/옵션 값 모양/`Animate` 시그니처/`initValue`)과 죽은 +링크 4곳, `bind-system-plan.md`/`architecture.md`의 "4종 계약" 잔여, +`store-semantics.md`의 "`State`는 가칭" stale(2026-08-12에 확정됨), +`luau-test/README.md`의 `04`/`19` 판정 기준이 재작성된 파일을 못 따라간 것 +(**`19` C섹션 기준이 정상 동작을 실패로 오판하게 돼 있었음**), +`documentation-content-map.md`의 4종 계약/`Attribute` 옛 이름, +`modifier-plan.md`의 "정정이 원문에 소급 안 됨" 패턴. + +**판단해서 안 고친 것**: `effect-plan.md`의 "cleanup" 14곳 — +Handler `retract`(내부 배관)와 달리 **`Effect(fn)`에서 사용자가 작성한 +`fn`이 반환하는 React식 콜백**이라 다른 층위. 매 감사마다 재지적되므로 +`lifecycle-pattern.md`에 "확인 완료, 의도된 별개 개념"으로 못박아 둠. + diff --git a/CLAUDE.md b/CLAUDE.md index 7f9d1c4..279f888 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -903,3 +903,9 @@ handoff용 저장이 불필요해짐 — `Relate`는 여러 위치/사이클을 — 런타임 12개 전원 통과, `04`가 위 버그를 음성 대조군으로 재현, `07` 보강으로 GC-native 전제 확정, `18`이 `Relate` 순환 경고 실증. 타입에선 `:Compute(fn)` lazy 핸들 계약이 Luau 추론과 충돌하는 게 드러나 `question.md` **0-Y** 신설. +스파이크 상태는 이제 **`luau-test/STATUS.md`가 소스**(pass/사람 결정 필요/ +스파이크 깨짐/미실행 분류). 마지막으로 코퍼스 전반 모순·stale 감사 — +`ROADMAP.md`가 최우선 게이트(0-Z)를 전혀 안 짚고 M11 Tween을 이미 끝난 +결정인데 미결로 두던 것, `slot-plan.md` 앞부분이 뒤집힌 "폐기, portal 안 함"을 +여전히 "확정"으로 자칭하던 것(구현자가 앞에서부터 읽으면 구 모델로 짤 위험), +`reconcile`이 여전히 파괴 경로였던 것(→ `rawUnmount` 신설) 등을 정정. diff --git a/ROADMAP.md b/ROADMAP.md index 03d50f8..b4ae9c7 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -63,6 +63,18 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 ## M2 — 디스패치 엔진 +> **⚠️ 착수 전 필독 — 아래 체크리스트는 *현행* 모델(래핑 핸들러가 재-dispatch +> 전에 `Dispatch.retractFrom`을 선행 호출)을 기준으로 쓰여 있고, 그 모델은 +> 교체가 예정돼 있습니다.** 새 모델("하강 diff": 선행 `retractFrom`을 폐기하고 +> `Dispatch.process`가 **핸들러를 먼저 비교** — 같으면 그 자리 클로저에 새 +> 값을 넘기고 자기 `process` 재호출, 다르면 그 자리부터 전량 철거)은 +> `.claude/research/dispatch-redispatch-diff-plan.md`에 있고, `.claude/question.md` +> **0-Z**(Attribute 이름 소유권) 하나만 정해지면 base와 이 문서에 한 번에 +> 반영됩니다. **0-Z가 미해결인 채로 M2/M4/M10을 구현하면 곧 갈아엎어야 하는 +> 코드를 짜게 됩니다** — 먼저 0-Z를 해소할 것. (base 4개 문서에도 같은 취지의 +> ⚠️ 배너가 달려 있는데, ROADMAP에만 없어서 2026-08-13 감사에서 지적됨.) + + - [ ] `Dispatch/init.luau` — `Dispatch.getHandler(inst,k,v): Handler?`(순수 스캔, `isHandlable`+`priority`) / `Dispatch.process(inst,k,v,index)` (오케스트레이터: 그 인덱스 점유 여부 체크 → getHandler → 매치된 @@ -230,6 +242,10 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 ## M4 — 첫 end-to-end 반응형 업데이트 +> **⚠️ M2의 ⚠️ 배너와 같은 주의** — 재디스패치 모델이 교체 예정, +> `question.md` 0-Z 먼저 해소할 것. + + - [ ] `Dispatch/StoreBind.luau`(재귀 재실행 로직, 엔진 무관 — 재-dispatch 전 `Dispatch.retractFrom(inst,k,index+1,realv)` 호출 필수, 그 다음 `Dispatch.process(inst,k,realv,index+1)`. `base/bind-system-plan.md` @@ -467,7 +483,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 - [ ] 프로퍼티류 필드 타입에 `T' = T | Tween` 치환 반영(타입 생성 스크립트가 `Position: UDim2` 자리를 `UDim2 | Tween`로 만들면 끝, Modifier 런타임/`__index` 자체엔 변경 없음 — `modifier-plan.md` - 10번, 2026-08-10 세션, `research/tween-plan.md`) + 10번, 2026-08-10 세션, `base/tween-plan.md`) ## M8 — Ref @@ -513,6 +529,11 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 ## M10 — Event / OnChange / Attribute / Tag +> **⚠️ M2의 ⚠️ 배너와 같은 주의** — 재디스패치 모델이 교체 예정이고, +> 특히 **Attribute 이름 소유권(`question.md` 0-Z)이 이 마일스톤의 직접 +> 대상**임. 0-Z 먼저 해소할 것. + + - [ ] `Handlers/Event.luau`(`ReflectionService` 기반 자동 판별) - [ ] `Handlers/OnChange.luau`(`OnChange(name)` DI 키 팩토리+Handler, `GetPropertyChangedSignal` 바인딩 — 제네릭 없이 콜백 타입은 인라인 @@ -550,7 +571,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 ## M11 — Tween **[2026-08-10 세션, 구조 재설계]** 독립 Dispatch 핸들러 모델에서 값-레벨 -`Tween` 래퍼 모델로 전환 — 상세는 `research/tween-plan.md`(전면 +`Tween` 래퍼 모델로 전환 — 상세는 `base/tween-plan.md`(전면 재작성), 구 모델은 `archive/tween-special-bind-key-reversed.md`. - [ ] `quad-base/Tween.luau`(값 타입만 — `Tween(opts)` 팩토리, `isTween`/ @@ -561,16 +582,25 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 없음, 엔진 객체=활성 트윈) + 첫 세팅은 무조건 애니메이션 없이 스냅(hasBeenSet 억제) + 활성 트윈 정리는 override 정책 완료 후에만 새 값 세팅(순서 뒤바뀌면 트윈 다음 프레임이 방금 세팅한 값을 덮어씀) -- [ ] override 정책 4가지(기본 Cancel/Override/Delete-restart/ - Move-to-end-restart) 중 기본값 외 옵션 키 이름/시그니처 확정, - Tween→plain 전환에 5번째 옵션이 필요한지 확인 -- [ ] `research/tween-plan.md` "트윈 옵션 값 모양" 확정(TweenInfo 그대로 - vs 편의 필드+기본값 — 소견은 후자) -- [ ] `quad-roblox/Animate.luau`(편의 콤비네이터 — `:Apply`로 체이닝, - `useTween` 우회는 이걸로 자연히 커버되어 별도 옵션 필드 불필요, - 정확한 시그니처는 M11에서 확정) -- [ ] `initValue`(진입 애니메이션) 필요성 재검토 — 필요해지면 hasBeenSet - 억제 동작과의 상충부터 풀 것(`research/tween-plan.md` 참고) +- [x] **override 정책 확정 완료**(2026-08-12 세션, `base/tween-plan.md` + "확정: `Tween{...}` 최종 모양" 절) — 검토했던 4가지가 **`Tween.Cancel` + (기본)/`Tween.Finish` 2값으로 압축**됨(로블록스 `TweenBase` API 현실상 + 나머지가 관찰상 Cancel과 동일). Tween→plain 전환도 두 옵션 모두 + "정리 후 즉시 덮어쓰기"로 수렴해 5번째 옵션 불필요로 확정. + **구현 시 순서 주의**: 이전 트윈 정리 → 그 다음 새 값 세팅 +- [x] **트윈 옵션 값 모양 확정 완료**(2026-08-12 세션, `base/tween-plan.md`) + — `Info: TweenInfo?` 우선 + 편의 필드(`Time`/`Style`/...) 폴백, + 기본값은 로블록스 `TweenInfo.new()` 자체 기본값과 일치. 옵션 필드는 + 전부 plain만(State 불가) +- [ ] `quad-roblox/Animate.luau` — **시그니처도 이미 확정 완료**(2026-08-12 + 두 번째/세 번째 세션, `base/tween-plan.md`): `Tween` opts(`Value` 제외)를 + `T|State`로 받아 각 필드를 resolve한 뒤 `Tween{...}`을 반환하는 + `function(self)...end` — `:Apply(Animate{...})`로 체이닝(`:Compute`가 + 아님, `research/operator-sugar-plan.md` "왜 `:Apply`인가"). `CanAnimate` + 필드 포함(`false`면 `Tween`으로 안 감싸고 plain 값 그대로). M11은 + **구현만** 하면 됨 +- [x] **`initValue`(진입 애니메이션) — 에이전트 범위 제외로 확정** + (2026-08-12 세션, 사용자가 직접 처리하기로) — 재검토 항목 아님 ## 특정 마일스톤에 안 묶이고 병행 가능