From 10cd31be2c1f842a7c5987eca10d6822b17836c9 Mon Sep 17 00:00:00 2001 From: qwreey Date: Fri, 14 Aug 2026 00:45:03 +0900 Subject: [PATCH] =?UTF-8?q?docs(audit):=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=A0=84=EC=B2=B4=20=EC=9E=AC=EA=B0=90=EC=82=AC=20?= =?UTF-8?q?=E2=80=94=20=EB=B6=84=ED=95=A0=20=ED=9B=84=20=EC=9E=94=EC=97=AC?= =?UTF-8?q?=20stale=20=EC=B0=B8=EC=A1=B0=2014=EA=B3=B3=20=EC=A0=95?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 69466ab/32e9db0 이후 5개 영역 병렬 에이전트 감사 + 직접 트레이싱으로 프로젝트 전체를 다시 훑음. 핵심 설계(하강 diff 재디스패치, Tag 참조카운트, nameClaims, Slot-in-Slot 해체)는 재트레이싱해도 버그 없음 — 전부 같은 클래스의 문서 참조 잔여 오류였음: `bind-system-plan.md` 2단계 분할(14차 세션) 이후에도 그 파일을 계속 가리키는 느슨한 산문 인용(따옴표 절 제목이 아니라 "~가 말하는"/"~ 참고" 식이라 doc-check.py 정규식이 못 잡는 형태). - `ref-plan.md`/`ui-shorthand-plan.md`(2곳)/`tween-plan.md`/ `slot-plan.md`(3곳) — `dispatch-core-plan.md`로 정정. - `module-lifecycle-plan.md:21` — 32e9db0가 같은 파일 114/128행은 고쳤지만 21행만 놓쳤던 것. - `ROADMAP.md`(2곳)/`luau-test/README.md` — `None`/`recompute` 절 참조 정정. - `luau-test/done/02`/`03` 스파이크 주석 — 02는 `ref-plan.md`(9차 세션에 이미 옮겨간 절이었음, bind-system-plan.md였던 적 없음), 03은 `dispatch-core-plan.md`로 정정. - `audit/luau-test-first-run-2026-08-13.md` — "no-op 점유 마커"를 여전히 유효한 수정 근거처럼 서술하던 부분에 정정 각주 추가(점유 체크 자체는 하강 diff 재설계로 폐지됨, `chains:SetStrong` 순서 버그만 지금도 유효). doc-check.py ERROR 0 유지, WARN 101 그대로(전부 판단 필요한 기존 느슨한 인용, 이번 라운드가 새로 만든 건 없음). 5개 에이전트 전원 "설계 결함 0건" 보고로 수렴 판단. Co-Authored-By: Claude Opus 5 --- .claude/audit/luau-test-first-run-2026-08-13.md | 14 ++++++++++++++ .claude/base/module-lifecycle-plan.md | 2 +- .claude/base/ref-plan.md | 2 +- .claude/base/slot-plan.md | 6 +++--- .claude/base/tween-plan.md | 2 +- .claude/base/ui-shorthand-plan.md | 4 ++-- .claude/luau-test/README.md | 2 +- .../done/02-none-sentinel-vs-nil-holes.luau | 4 ++-- .../done/03-recursive-store-bind-dispatch.luau | 2 +- ROADMAP.md | 4 ++-- 10 files changed, 28 insertions(+), 14 deletions(-) diff --git a/.claude/audit/luau-test-first-run-2026-08-13.md b/.claude/audit/luau-test-first-run-2026-08-13.md index 5c1a244..5a1c167 100644 --- a/.claude/audit/luau-test-first-run-2026-08-13.md +++ b/.claude/audit/luau-test-first-run-2026-08-13.md @@ -62,6 +62,20 @@ 나중에 UI를 덮어쓰는** 증상까지 간다는 게 실측으로 확인됨. 수정 (`SetStrong` hoist + no-op 점유 마커)이 옳았다는 결정적 근거. +> **[2026-08-14 리뷰에서 정정] "no-op 점유 마커" 부분은 이후 폐기된 +> 옛 모델 서술입니다.** `chains:SetStrong`을 `process` 호출 *전에* 끝내야 +> 한다는 순서 버그(위 표, `retractor` 유실/`STALE` 재현)는 하강 diff +> 재설계(2026-08-13 열네 번째 세션, `base/dispatch-core-plan.md`)에서도 +> 그대로 유효 — 지금 `Dispatch.process`도 여전히 `chains:GetStrong`/ +> `SetStrong`으로 list를 먼저 확보한 뒤에 `h.process`를 부름. 다만 "점유 +> 마커"(같은 인덱스가 이미 점유돼 있으면 즉시 error)는 그 뒤 **Dispatch의 +> 점유 체크 자체가 폐지**되며(`dispatch-core-plan.md` "Dispatch 체인" 절) +> 없어진 옛 개념 — 지금은 `list[index] = { handler = h, retractor = NOOP }` +> placeholder만 미리 박아두는 것으로 대체됨(같은 "호출 전 자리 확보" +> 목적, 에러를 내는 메커니즘은 아님). 이 스파이크는 `rewrite-required/`로 +> 이동돼 있고(`luau-test/STATUS.md`), 재작성 시 이 절의 "정상(수정본)" +> 시나리오도 새 모델(placeholder, 점유 에러 없음)로 갱신할 것. + ## `07` — 스파이크를 보강해야 실제 검증이 됐음 원래 3번 섹션이 "강하게 붙잡아둔 10개가 살아있는가"라는 **sanity check만** diff --git a/.claude/base/module-lifecycle-plan.md b/.claude/base/module-lifecycle-plan.md index 07f8a1a..2155ed6 100644 --- a/.claude/base/module-lifecycle-plan.md +++ b/.claude/base/module-lifecycle-plan.md @@ -18,7 +18,7 @@ Slot과 맞물려서 잘 생각해서 구현해야 하는 부분. **기울어진 "Handler로 base를 받는" 게 아니라) — 이유: 여긴 가상돔이 없어서, base 쪽이 "누가 실제로 그려주는지" 모르는 채로 있다가 Roblox Handler를 주입받는 모양이 더 자연스러워 보임. (이름 자체는 이후 "Handler"로 확정 — -`base/bind-system-plan.md`의 핸들러 계약 절 참고, 이 문서는 여전히 +`base/dispatch-core-plan.md`의 핸들러 계약 절 참고, 이 문서는 여전히 초안 당시 표현인 "프로바이더"로 쓰여 있던 걸 정정.) ## pluggable 플러그 초기화는 누구 몫인가 diff --git a/.claude/base/ref-plan.md b/.claude/base/ref-plan.md index 24e9d40..5f6893a 100644 --- a/.claude/base/ref-plan.md +++ b/.claude/base/ref-plan.md @@ -6,7 +6,7 @@ > 했고 결정은 하나도 안 바뀜.** **상태**: base — 확정. `Dispatch`/`Brand`와의 관계는 -`base/bind-system-plan.md`(디스패치 코어)와 `base/brand-plan.md` 참고. +`base/dispatch-core-plan.md`(디스패치 코어)와 `base/brand-plan.md` 참고. ## Ref — 도입 확정, 단 용도는 재정의됨 diff --git a/.claude/base/slot-plan.md b/.claude/base/slot-plan.md index 72efb45..b5975ba 100644 --- a/.claude/base/slot-plan.md +++ b/.claude/base/slot-plan.md @@ -490,7 +490,7 @@ nested `Slot`을 파괴 후 재사용하는 경로가 정확히 이 케이스). 체크로 이걸 막고, `process`도 `old == slotValue` 체크로 대칭적으로 막음 — 둘 다 스킵돼야 완전한 no-op. - Slot 핸들러 자신이 감시 중인 값(배열/스토어)이 바뀔 때 child를 갱신하는 - 추적(구독)도 `base/bind-system-plan.md`가 말하는 "process 함수가 다른 값 + 추적(구독)도 `base/dispatch-core-plan.md`가 말하는 "process 함수가 다른 값 변경을 추적해도 됨" 범위에 속하고, `retract` 시점엔 그 추적만 풀면 됨 — Destroy 시점엔 `retract`가 호출되지 않는다는 원칙(`base/lifecycle-pattern.md`)도 동일하게 적용. @@ -1440,7 +1440,7 @@ end ``` `recompute`가 owner가 Slot이면 그 `.Length`에도 합계를 반영하도록 -확장됐으므로(`base/bind-system-plan.md` 참고) — `Slot.Length`는 더 +확장됐으므로(`base/dispatch-core-plan.md` 참고) — `Slot.Length`는 더 이상 raw 개수가 아니라 **"요소별 기여도의 합"**(plain=1, nested Slot=그 `.Length`)이 됨. plain 요소만 있는 흔한 경우엔 항상 합==개수라 체감 차이 없음. @@ -1558,7 +1558,7 @@ GC가 전부 한 번에 정리하니 손 안 대도 됨 — 이 구분은 새 죽는 경우)를 만나서 드러난 것뿐. - **Length 변경은 정확히 offset 변경으로만 전파됨, 별도 채널 없음** — - 수정된 `recompute`(`base/bind-system-plan.md` 참고)를 보면 `:Set()`이 + 수정된 `recompute`(`base/dispatch-core-plan.md` 참고)를 보면 `:Set()`이 호출되는 대상은 (a) 뒤 형제들의 offset, (b) owner가 Slot이면 그 `.Length` 딱 둘뿐. Length 값 자체는 읽히기만 함(`:Get()`/Observer 트리거) — (b)로 올라간 `.Length`도 한 단계 위에서는 그냥 또 다른 diff --git a/.claude/base/tween-plan.md b/.claude/base/tween-plan.md index c658d5a..c271d42 100644 --- a/.claude/base/tween-plan.md +++ b/.claude/base/tween-plan.md @@ -154,7 +154,7 @@ StoreBind가 State/Source 레이어를 전부 풀어낸 뒤의 값(그리고 이 `inst`가 죽으면 이 슬롯(엔진 Tween 객체+`Value` 포함)도 별도 정리 로직 없이 같이 GC됨. **[정정, 2026-08-12 열한 번째 세션, 2026-08-13 다섯 번째 세션에 클로저 반환 계약으로 서술 갱신]** `process`가 반환한 클로저는 store -재발행마다 항상 불리지만(`bind-system-plan.md` 일반 retract 계약 절 +재발행마다 항상 불리지만(`dispatch-core-plan.md` 일반 retract 계약 절 정정분 — "거의 안 불림"이었던 원 서술은 틀렸음), PropertyHandler의 그 클로저는 몸체가 no-op이라 실질적으로 하는 일이 없음 — 아래 절 참고. diff --git a/.claude/base/ui-shorthand-plan.md b/.claude/base/ui-shorthand-plan.md index 6f95dd8..c702b1d 100644 --- a/.claude/base/ui-shorthand-plan.md +++ b/.claude/base/ui-shorthand-plan.md @@ -83,7 +83,7 @@ Modifier 타입의 메소드 목록에 끼워 넣도록 챙기면 됨, 새로 ### `v`가 `nil`인 경우 — `process`가 직접 자식 제거, 반환 클로저는 관여 안 함 (2026-08-07 여덟 번째 세션) -`modifier-plan.md`의 `None` 센티널(`base/bind-system-plan.md`의 +`modifier-plan.md`의 `None` 센티널(`base/dispatch-core-plan.md`의 `NoneHandler` 재귀 재디스패치 절 참고)이 최종적으로 이 Handler의 `process(inst, k, nil)`을 호출하는 구체 사례 — 이 Handler에서 "`v`가 `nil`"은 만들어둔 `_quad_corner`류 자식이 있으면 그냥 지우는 것으로 확정. @@ -93,7 +93,7 @@ Modifier 타입의 메소드 목록에 끼워 넣도록 챙기면 됨, 새로 - **이건 반환 클로저가 아니라 `process` 자신의 로직** — **[정정, 2026-08-12 열한 번째 세션, 2026-08-13 다섯 번째 세션에 클로저 반환 계약으로 서술 갱신]** 그 클로저는 store 재발행마다 항상 불리지만(핸들러 타입이 안 - 바뀌어도, `bind-system-plan.md` 일반 retract 계약 절 정정분 참고), 이 + 바뀌어도, `dispatch-core-plan.md` 일반 retract 계약 절 정정분 참고), 이 Handler는 `process(inst,k,v,index)` 자체가 `v`가 `nil`이든 숫자든 전부 완결적으로 처리하므로(있으면 지우거나 만들거나) 반환 클로저가 할 일이 없어 `function() end`이면 충분 — 일반 프로퍼티 핸들러가 no-op 클로저를 diff --git a/.claude/luau-test/README.md b/.claude/luau-test/README.md index a8ac03d..602792a 100644 --- a/.claude/luau-test/README.md +++ b/.claude/luau-test/README.md @@ -86,7 +86,7 @@ ROADMAP 항목 근거인지, 어떻게 실행하는지, 실행 후 뭘 확인해 | `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 신규, 같은 날 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}`을 조용히 통과시키는 걸 재현. **[2026-08-13 열네 번째 세션] 0-Z가 확정되며 B 섹션이 낡음 → `rewrite-required/`** — 이제 "그룹 전용 키 + `AttributeKeyHandler`의 이름 claim"을 검증해야 함(A/C는 그대로 유효) | `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 여섯 번째 세션) | +| `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 열다섯 번째 세션), `dispatch-core-plan.md`의 `recompute` off-by-one 수정 사례(2026-08-11 여섯 번째 세션) | ## 공통 유틸리티 diff --git a/.claude/luau-test/done/02-none-sentinel-vs-nil-holes.luau b/.claude/luau-test/done/02-none-sentinel-vs-nil-holes.luau index a341f98..b812c4c 100644 --- a/.claude/luau-test/done/02-none-sentinel-vs-nil-holes.luau +++ b/.claude/luau-test/done/02-none-sentinel-vs-nil-holes.luau @@ -8,7 +8,7 @@ 사용자가 직접 찾아낸 버그: None으로 소진하면 그 슬롯이 영원히 non-nil로 남아있어서, `:Wait()`/`:Callback()`가 반복 호출될 때마다 배열이 끝없이 길어지는(예전 소진 슬롯을 재사용 못 하는) 진짜 버그였음. - .claude/base/bind-system-plan.md "왜 None이 아니라 nil인가" 절(2026-08-09 + .claude/base/ref-plan.md "왜 `None`이 아니라 `nil`인가" 절(2026-08-09 열한 번째 세션, 최종 정정)이 최신 소스 — 결론은 두 패턴이 서로 다른 문제를 풀고 있었다는 것: @@ -164,7 +164,7 @@ end 작은 값 유지)? 3. Part B-2 — None+table.insert 방식은 실제로 1000까지 자라는가 — 이게 바로 사용자가 찾아낸 "무한 성장" 버그의 정량적 재현. 이 결과가 - 기대와 다르면(예: 실제로는 안 자란다면) bind-system-plan.md의 정정 + 기대와 다르면(예: 실제로는 안 자란다면) ref-plan.md의 정정 근거 자체를 재검토해야 하니 반드시 알려줄 것. 4. Part B-3 — 새 등록이 소진된 빈 슬롯(index=idx2)을 실제로 재사용하는가 — 이게 "table.insert 대신 선형 탐색 재사용 등록 함수"가 실제로 diff --git a/.claude/luau-test/done/03-recursive-store-bind-dispatch.luau b/.claude/luau-test/done/03-recursive-store-bind-dispatch.luau index 40ec1e0..87f3943 100644 --- a/.claude/luau-test/done/03-recursive-store-bind-dispatch.luau +++ b/.claude/luau-test/done/03-recursive-store-bind-dispatch.luau @@ -1,6 +1,6 @@ --[[ 검증 대상: process(inst,k,v)/retract(inst,k,v) 기반 재귀 재-dispatch - 모델(.claude/base/bind-system-plan.md "확정된 디스패치 모델" 절)이 실제 + 모델(.claude/base/dispatch-core-plan.md "확정된 디스패치 모델" 절)이 실제 Luau 함수 재귀로 자연스럽게 짜이는지, 우선순위 스캔(isHandlable)이 기대대로 동작하는지에 대한 최소 스파이크. diff --git a/ROADMAP.md b/ROADMAP.md index e4c3116..323304f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -95,7 +95,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 (아래 항목) / `Dispatch.addHandler(handler)`(레지스트리 등록, quad-roblox가 팩토리 뮤테이션 시점에 호출) / `Dispatch.drive(inst, flattened)`(배열→해시 두 패스 순회하며 각 `(k,v)`에 - `Dispatch.process(inst,k,v,1)` 호출 — `bind-system-plan.md`의 `None` + `Dispatch.process(inst,k,v,1)` 호출 — `dispatch-core-plan.md`의 `None` 센티널 절, 2026-08-07 여덟 번째 세션에 네이밍 확정). **[2026-08-13 다섯 번째 세션 전면 재설계]** "이전 담당자와 다르면 그 `retract`"라는 옛 diff 모델은 폐기 — 정리 책임은 전적으로 @@ -509,7 +509,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 - [ ] 인라인 키/setter로 modifier 필드를 명시적으로 지우는 `None` 센티널 (이름 확정, `modifier-plan.md` 2-1번, `Peek` 반환 타입에 `None` 추가) + 이를 `nil`로 재디스패치하는 base 내장 `NoneHandler` - (`bind-system-plan.md`의 `None` 센티널 절, M2 dispatch 엔진의 + (`dispatch-core-plan.md`의 `None` 센티널 절, M2 dispatch 엔진의 "이전 매치 핸들러 추적" 항목과 함께 구현 — `StoreBind` 핸들러와 동일한 재귀 재디스패치 패턴이라 새 메커니즘 아님) — `None` 센티널 자체는 확정 완료. **[2026-08-13 열네 번째 세션 갱신]** `NoneHandler`가