fix(docs): 리뷰 라운드 — 의사코드 결함 3건 + doc-check 사각지대 수정

다른 에이전트 감사와 사용자 트레이싱으로 69466ab에서 나온 지적 반영.
핵심 알고리즘(하강 diff/인덱스 체인)엔 버그 없음이 확인됐고, 의사코드와
문서 정합성에서 나온 것들:

1. `nameClaims`가 Relate의 3-인자 계약 위반(`GetStrong(inst)` /
   `SetStrong(inst, claims)`) → `(inst, name[, key])`로 정정. 같은 커밋의
   `tagNameMap`은 이미 3-인자였어서 대조로 드러남.
2. TagHandler가 생존 이름의 홀더를 비웠다가 곧이은 process에서 `addTag`를
   헛되이 재호출 — 문서 서술("addTag 자체가 안 불림")과 정반대였음.
   생존 이름은 홀더를 유지하도록 정정하고, `addTag`도 `removeTag`처럼
   배치 호출로 통일(`{string}` 시그니처 도입 근거와 맞춤).
3. 그룹 process의 부분 실패 경로(순회 중 충돌 error) 문서화 — 피해가
   인스턴스 수명으로 한정되고 재현이 시끄럽게 반복됨을 트레이싱으로
   확인, 롤백 장치는 안 넣고 question.md 3번에 열어둠.

doc-check.py: REF 정규식이 줄 단위라 파일명과 절 제목이 줄바꿈에 걸친
인용을 통째로 놓치고 있었음(이번 2단계 분할뿐 아니라 9차 세션 1단계
분할 stale까지 숨어 있던 원인). 파일 전체 스캔 + 개행 허용으로 고치고,
새로 드러난 stale 참조 30여 곳을 dispatch-core/event-plan/ref-plan/
typing-limits로 정정. 오탐 없음(참조–인용 스팬 3줄 미만 전수 확인).

부수: 세션 로그에 리뷰 라운드 절 추가(커밋 메시지 줄수 오기 2291→1213
정정 포함), CLAUDE.md의 스파이크 이동 서술에 `19` 누락 보강,
luau-test/README 상단 요약표를 STATUS.md와 동기화.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KckSawrsJSJmDBcSojJPxZ
This commit is contained in:
qwreey 2026-08-14 00:29:25 +09:00
parent 69466abc47
commit 32e9db0773
Signed by: qwreey
GPG key ID: D28DB79297A214BD
21 changed files with 191 additions and 80 deletions

View file

@ -195,27 +195,24 @@ identity 하나로** 판정할 수 있고, 그러면 claim 로직이 그룹을
```lua ```lua
-- AttributeKeyHandler(quad-base) — 이름 claim만 자기 상태로 가짐 -- AttributeKeyHandler(quad-base) — 이름 claim만 자기 상태로 가짐
-- Relate는 항상 (inst, key) 2단 — `base/relate-plan.md` API 참고.
-- 여기선 key = attribute 이름, value = 그 이름을 지금 잡고 있는 키 객체.
local nameClaims = Relate() -- {[inst(weak)] = {[name] = key(strong)}} local nameClaims = Relate() -- {[inst(weak)] = {[name] = key(strong)}}
function AttributeKeyHandler.process(inst, k, v, index) function AttributeKeyHandler.process(inst, k, v, index)
local claims = nameClaims:GetStrong(inst) local cur = nameClaims:GetStrong(inst, k.Name)
if not claims then
claims = {}
nameClaims:SetStrong(inst, claims)
end
local cur = claims[k.Name]
if cur ~= nil and cur ~= k then if cur ~= nil and cur ~= k then
error(`attribute "{k.Name}" is already bound by another owner`) error(`attribute "{k.Name}" is already bound by another owner`)
end end
claims[k.Name] = k nameClaims:SetStrong(inst, k.Name, k)
setAttribute(inst, k.Name, v) -- v가 nil이든 아니든 무조건(위 "메커니즘" 절) setAttribute(inst, k.Name, v) -- v가 nil이든 아니든 무조건(위 "메커니즘" 절)
return function() return function()
-- 엔진 부작용 없음 — claim 반납만. "내가 실제로 물러날 때만" 지움 -- 엔진 부작용 없음 — claim 반납만. "내가 실제로 물러날 때만" 지움
-- (`dispatch-core-plan.md` "Handler 작성 체크리스트" 4번). -- (`dispatch-core-plan.md` "Handler 작성 체크리스트" 4번).
if claims[k.Name] == k then if nameClaims:GetStrong(inst, k.Name) == k then
claims[k.Name] = nil nameClaims:SetStrong(inst, k.Name, nil)
end end
end end
end end
@ -330,6 +327,20 @@ function AttributeGroupHandler.process(inst, k, v, index)
end end
``` ```
- **[부분 실패 경로, 2026-08-14 리뷰에서 명시화] 순회 도중 소유권 충돌
error가 나면, 그 전에 이미 등록된 이름들은 이 사이클의 클로저가 만들어지지
못해 즉시 회수되지 않는다.** `error``process` 밖으로 전파되므로 `return
function() ... end`에 도달하지 못하고, `Dispatch`는 성공한 반환값만
저장하기 때문. 다만 **피해 범위는 그 인스턴스의 수명으로 한정됨**:
(1) `nameClaims`/`chains`는 `Relate``inst`에 대해 weak — 그 인스턴스가
GC되면 잔여 claim도 같이 사라짐, (2) 같은 자리가 다시 프로세스되면
`Dispatch`가 남겨둔 마커 슬롯 덕분에 (A) 분기를 타고, 이미 claim된 이름은
`cur == k`라 **에러 없이 그대로 통과**해 같은 자리에서 같은 error로 다시
멈춤 — 조용한 오작동이 아니라 **반복 재현되는 시끄러운 실패**. 그래서
별도 롤백 장치를 넣지 않음(코퍼스의 "에러=패닉 상태, 그 이후 정합성은
관리 대상 아님" 원칙과 같은 결). 원자적 롤백이 필요하다고 판단되면
그때 그룹 `process`에만 국소적으로 넣을 수 있음 — `question.md` 3번에
열어둠.
- **`groupKey(v, name)`는 그룹 값 객체별·이름별 메모이즈** — 같은 그룹 - **`groupKey(v, name)`는 그룹 값 객체별·이름별 메모이즈** — 같은 그룹
값이 재프로세스될 때 같은 키가 나와야 claim이 자기 자신과 안 부딪힘. 값이 재프로세스될 때 같은 키가 나와야 claim이 자기 자신과 안 부딪힘.
구현은 `Relate<그룹 값 → {[name] = key}>` 하나면 충분하고, **공개 구현은 `Relate<그룹 값 → {[name] = key}>` 하나면 충분하고, **공개

View file

@ -146,7 +146,7 @@ unbindLifetime(inst: any, value: any): ()
canExecute(inst: any, value: any): boolean canExecute(inst: any, value: any): boolean
``` ```
**`unbindLifetime` 추가 이유(2026-08-09 세션, `bind-system-plan.md`의 **`unbindLifetime` 추가 이유(2026-08-09 세션, `dispatch-core-plan.md`의
"Length/Offset" 논의에서 파생)**: `Dispatch.setLength`(같은 위치에 새 "Length/Offset" 논의에서 파생)**: `Dispatch.setLength`(같은 위치에 새
`State<number>`가 들어오면 이전 것에 걸어둔 Observer를 먼저 정리해야 함, `State<number>`가 들어오면 이전 것에 걸어둔 Observer를 먼저 정리해야 함,
`State<Slot>` 교체가 대표 사례)처럼 **`inst` 전체 생명주기보다 먼저, `State<Slot>` 교체가 대표 사례)처럼 **`inst` 전체 생명주기보다 먼저,

View file

@ -100,7 +100,7 @@ Lua 테이블 리터럴은 배열 파트/해시 파트 사이에 소스 텍스
- **실제 "지우기" 동작은 디스패치 쪽 `NoneHandler`가 담당** — merge가 끝난 - **실제 "지우기" 동작은 디스패치 쪽 `NoneHandler`가 담당** — merge가 끝난
뒤 최종 flatten 결과에 `None`이 남아있으면, base 드라이버가 그 키를 어떻게 뒤 최종 flatten 결과에 `None`이 남아있으면, base 드라이버가 그 키를 어떻게
처리하는지는 새 개념이 아니라 이미 확정된 디스패치 모델 그대로다. 처리하는지는 새 개념이 아니라 이미 확정된 디스패치 모델 그대로다.
상세는 `base/bind-system-plan.md`의 "`None` 센티널 — StoreBind와 상세는 `base/dispatch-core-plan.md`의 "`None` 센티널 — StoreBind와
같은 재귀 재디스패치 패턴 재사용" 절 참고 — 핵심만 요약하면 `NoneHandler` 같은 재귀 재디스패치 패턴 재사용" 절 참고 — 핵심만 요약하면 `NoneHandler`
`StoreBind` 핸들러와 완전히 같은 모양(`isHandlable`이 `v == None` `StoreBind` 핸들러와 완전히 같은 모양(`isHandlable`이 `v == None`
잡고, `process``v`를 진짜 `nil`로 바꿔 `process(inst, k, nil)`을 재귀 잡고, `process``v`를 진짜 `nil`로 바꿔 `process(inst, k, nil)`을 재귀
@ -312,7 +312,7 @@ Modifier에는 없음).
됨 — `mod:UICorner(8)`/`mod:FontSize(...)`처럼 DI 쪽 "제네릭 생성자 함수 됨 — `mod:UICorner(8)`/`mod:FontSize(...)`처럼 DI 쪽 "제네릭 생성자 함수
하나 + 자주 쓰는 것만 정적 필드로 미리 바인딩" 패턴 재사용. 하나 + 자주 쓰는 것만 정적 필드로 미리 바인딩" 패턴 재사용.
(주의: 이벤트는 이 관습의 유일한 예외라 인용 대상에서 제외 — 이벤트 바인딩은 (주의: 이벤트는 이 관습의 유일한 예외라 인용 대상에서 제외 — 이벤트 바인딩은
PA님 방식인 문자열 키 + 런타임 리플렉션으로 감, `base/bind-system-plan.md` PA님 방식인 문자열 키 + 런타임 리플렉션으로 감, `base/event-plan.md`
"이벤트 바인딩 정정" 절 참고. Modifier는 이벤트가 아니라 Store/인스턴스 "이벤트 바인딩 정정" 절 참고. Modifier는 이벤트가 아니라 Store/인스턴스
생성과 같은 카테고리라 dot-access 관습이 그대로 적용됨.) 생성과 같은 카테고리라 dot-access 관습이 그대로 적용됨.)

View file

@ -111,7 +111,7 @@ init하려 하면 오류, 없는데 뭔가 생성해서 bind하려 해도 오류
뭐라고 부를지("provider" vs "processor" vs 그냥 "plug") 아직 안 정함~~ 뭐라고 부를지("provider" vs "processor" vs 그냥 "plug") 아직 안 정함~~
**[해소됨]** — **`Handler`로 확정**, 위 항목이 가리키는 계약의 정식 이름. **[해소됨]** — **`Handler`로 확정**, 위 항목이 가리키는 계약의 정식 이름.
`Dispatch`(그 계약을 스캔/실행하는 엔진, 프리미티브 아닌 탑레벨 싱글톤)와 `Dispatch`(그 계약을 스캔/실행하는 엔진, 프리미티브 아닌 탑레벨 싱글톤)와
구분해서 쓸 것 — `base/bind-system-plan.md` "Dispatch는 프리미티브가 구분해서 쓸 것 — `base/dispatch-core-plan.md` "Dispatch는 프리미티브가
아니다" 절. **왜 다른 후보들을 기각했는지(2026-08-08 세션, 재확인)**: 아니다" 절. **왜 다른 후보들을 기각했는지(2026-08-08 세션, 재확인)**:
`Processor`는 계약 메소드 자체가 `process`라 이름 안에 같은 단어가 `Processor`는 계약 메소드 자체가 `process`라 이름 안에 같은 단어가
겹쳐 눈에 거슬림, `Provider``canProvide`처럼 "뭔가를 공급한다"는 겹쳐 눈에 거슬림, `Provider``canProvide`처럼 "뭔가를 공급한다"는
@ -125,7 +125,7 @@ init하려 하면 오류, 없는데 뭔가 생성해서 bind하려 해도 오류
번도 실행 안 된 상태에서 dispatch가 호출되면 어떻게 되는지 번도 실행 안 된 상태에서 dispatch가 호출되면 어떻게 되는지
(`pre-implementation-audit.md` 1-4) — 별도 케이스로 처리하지 않음. (`pre-implementation-audit.md` 1-4) — 별도 케이스로 처리하지 않음.
provider 미주입 상태는 결국 그 클래스의 핸들러가 레지스트리에 하나도 provider 미주입 상태는 결국 그 클래스의 핸들러가 레지스트리에 하나도
없는 상태이므로, `base/bind-system-plan.md` "우선순위 동률/매치 실패 없는 상태이므로, `base/dispatch-core-plan.md` "우선순위 동률/매치 실패
처리" 절의 일반 "매치 실패 시 즉시 error" 규칙 하나로 자연히 커버됨. 처리" 절의 일반 "매치 실패 시 즉시 error" 규칙 하나로 자연히 커버됨.
- base 유틸(per-instance 상태 저장소, 생명 바인드 유틸)이 인터페이스만 두고 - base 유틸(per-instance 상태 저장소, 생명 바인드 유틸)이 인터페이스만 두고
실제 구현은 백엔드 팩토리(`RobloxFactory(BaseModule)`류)가 뮤테이션으로 실제 구현은 백엔드 팩토리(`RobloxFactory(BaseModule)`류)가 뮤테이션으로

View file

@ -27,7 +27,7 @@
함.** 콜백 파라미터 타입은 호출부가 인라인으로 직접 명시 함.** 콜백 파라미터 타입은 호출부가 인라인으로 직접 명시
(`function(v: UDim2) ... end`) — Luau가 그 타입이 실제 프로퍼티 타입과 (`function(v: UDim2) ... end`) — Luau가 그 타입이 실제 프로퍼티 타입과
일치하는지 검증해주지 않음. 이미 확정된 "이벤트 바인딩은 콜백 시그니처를 일치하는지 검증해주지 않음. 이미 확정된 "이벤트 바인딩은 콜백 시그니처를
Luau가 검증 못 하는 대가를 받아들인다"는 결정(`bind-system-plan.md` "이벤트 Luau가 검증 못 하는 대가를 받아들인다"는 결정(`event-plan.md` "이벤트
바인딩 — `On.EventName` 도트액세스 안 씀" 절, "타입 안전성을 어느 정도 바인딩 — `On.EventName` 도트액세스 안 씀" 절, "타입 안전성을 어느 정도
포기하는 대가")과 같은 급의 트레이드오프라 새로 정당화할 것 없음 — 오히려 포기하는 대가")과 같은 급의 트레이드오프라 새로 정당화할 것 없음 — 오히려
`AttributeKey<<T>>`처럼 제네릭으로 정확히 맞추려는 시도는 이벤트 키보다 더 `AttributeKey<<T>>`처럼 제네릭으로 정확히 맞추려는 시도는 이벤트 키보다 더
@ -55,7 +55,7 @@
별도 `retract(inst,k,v)` 필드가 Disconnect를 담당한다고 적혀 있었으나, 별도 `retract(inst,k,v)` 필드가 Disconnect를 담당한다고 적혀 있었으나,
Handler 계약이 `process` 1-메소드로 합쳐지며 그 로직이 반환 클로저로 Handler 계약이 `process` 1-메소드로 합쳐지며 그 로직이 반환 클로저로
이동 — `connection``process`의 로컬 변수를 클로저가 upvalue로 그대로 이동 — `connection``process`의 로컬 변수를 클로저가 upvalue로 그대로
캡처하므로 별도 `Relate` 저장/재조회가 필요 없음(`bind-system-plan.md` 캡처하므로 별도 `Relate` 저장/재조회가 필요 없음(`dispatch-core-plan.md`
"핸들러 내부 상태 저장" 절). "핸들러 내부 상태 저장" 절).
- **`State<function>` 지원 — 새 메커니즘 없음.** 이미 확정된 "이벤트도 - **`State<function>` 지원 — 새 메커니즘 없음.** 이미 확정된 "이벤트도
store-bind 가능 — `false`로 disconnect" 메커니즘(`bind-system-plan.md`)이 store-bind 가능 — `false`로 disconnect" 메커니즘(`bind-system-plan.md`)이

View file

@ -1,6 +1,6 @@
# Relate — inst와 임의의 값을 weak하게 엮는 범용 릴레이션 프리미티브 # Relate — inst와 임의의 값을 weak하게 엮는 범용 릴레이션 프리미티브
**상태**: base — 2026-08-08 세션에서 신설, 확정. `base/bind-system-plan.md`의 **상태**: base — 2026-08-08 세션에서 신설, 확정. `base/dispatch-core-plan.md`의
"핸들러 내부 상태 저장"과 `base/lifecycle-pattern.md``bindLifetime`/ "핸들러 내부 상태 저장"과 `base/lifecycle-pattern.md``bindLifetime`/
`canExecute` 양쪽이 필요로 했던 "`inst`를 weak 키로 하는 저장소"가 지금까지 `canExecute` 양쪽이 필요로 했던 "`inst`를 weak 키로 하는 저장소"가 지금까지
`base.perInstanceState(inst)`라는 이름만 있고 인터페이스가 미정인 placeholder로 `base.perInstanceState(inst)`라는 이름만 있고 인터페이스가 미정인 placeholder로

View file

@ -202,7 +202,7 @@ Slot이 여럿 형제로 존재할 때, 최종 자식 순서가 저작 순서(
있었던 질문 — **메커니즘 확정으로 해소됨**: `Dispatch.setLength`/ 있었던 질문 — **메커니즘 확정으로 해소됨**: `Dispatch.setLength`/
`Dispatch.setOffsetSource` + 형제별 개수 누적합(`offset`)을 리액티브 `Dispatch.setOffsetSource` + 형제별 개수 누적합(`offset`)을 리액티브
프로퍼티(Roblox `LayoutOrder`)에 바인딩하는 방식 — 상세는 `base/ 프로퍼티(Roblox `LayoutOrder`)에 바인딩하는 방식 — 상세는 `base/
bind-system-plan.md`의 "Length/Offset — 여러 Slot이 형제로 섞일 때 순서 dispatch-core-plan.md`의 "Length/Offset — 여러 Slot이 형제로 섞일 때 순서
보장" 절 참고. **DOM류 물리 순서 백엔드에도 같은 base 메커니즘이 그대로 보장" 절 참고. **DOM류 물리 순서 백엔드에도 같은 base 메커니즘이 그대로
재사용됨**(offset이 바뀌어도 이미 마운트된 원소를 물리적으로 옮길 필요 재사용됨**(offset이 바뀌어도 이미 마운트된 원소를 물리적으로 옮길 필요
없음 — `insertBefore`가 뒤 형제를 자연히 밀어주므로, backend Handler의 없음 — `insertBefore`가 뒤 형제를 자연히 밀어주므로, backend Handler의
@ -1314,7 +1314,7 @@ UI에 직접 관측, (2) `Dispatch.setLength(inst, i, slot.Length)`가 형제
게 맞고, 그건 사용자가 별도 State로 계산해야 하는 몫. 게 맞고, 그건 사용자가 별도 State로 계산해야 하는 몫.
**동적 자식은 반드시 `Slot` 또는 `state<Frame>`류 store-bind를 통해서만 **동적 자식은 반드시 `Slot` 또는 `state<Frame>`류 store-bind를 통해서만
추가/제거 — 그 외 경로는 UB(2026-08-10 세션, `base/bind-system-plan.md`의 추가/제거 — 그 외 경로는 UB(2026-08-10 세션, `base/dispatch-core-plan.md`의
"Length/Offset" 절 반영).** 둘 다 `Dispatch.setLength`/`setOffsetSource`를 "Length/Offset" 절 반영).** 둘 다 `Dispatch.setLength`/`setOffsetSource`를
정확히 호출하는 유일한 정당 경로라, 이걸 우회해서(예: 외부 코드가 Slot이 정확히 호출하는 유일한 정당 경로라, 이걸 우회해서(예: 외부 코드가 Slot이
마운트해둔 부모 Instance에 직접 `.Parent = parentInst`로 자식을 끼워 마운트해둔 부모 Instance에 직접 `.Parent = parentInst`로 자식을 끼워
@ -1589,7 +1589,7 @@ Roblox뿐 아니라 web에도 그대로 필요.
`offset`/`sum`은 0-based 개수(카디널 수)고, `_elements`/`updateFn`의 `offset`/`sum`은 0-based 개수(카디널 수)고, `_elements`/`updateFn`의
`index`는 1-based Lua 배열 관례 — `index + offset` 공식이 이 둘을 `index`는 1-based Lua 배열 관례 — `index + offset` 공식이 이 둘을
의도적으로 섞는 것. 상세는 `base/bind-system-plan.md`의 "0-based 의도적으로 섞는 것. 상세는 `base/dispatch-core-plan.md`의 "0-based
개수" 절 참고. 개수" 절 참고.
## 반응형 raw 요소 — `State<T>`/`Source<T>`도 Slot 요소로 허용 (2026-08-11 일곱 번째 세션) ## 반응형 raw 요소 — `State<T>`/`Source<T>`도 Slot 요소로 허용 (2026-08-11 일곱 번째 세션)
@ -1834,7 +1834,7 @@ Slot이 마운트될 때 **자기 하위 요소들까지 `bindLifetime`으로
**[정정, 사용자 지적] "해제 짝"이라는 새 API는 필요 없음** — 옛 owner에 **[정정, 사용자 지적] "해제 짝"이라는 새 API는 필요 없음** — 옛 owner에
대해 그냥 **`Dispatch.setOffsetSource(ownerKey, position, None)` + 대해 그냥 **`Dispatch.setOffsetSource(ownerKey, position, None)` +
`Dispatch.setLength(ownerKey, position, 0)`을 다시 부르면 끝**. `Dispatch.setLength(ownerKey, position, 0)`을 다시 부르면 끝**.
이건 이미 확정된 관용구 그대로임(`base/bind-system-plan.md`의 "실제 이건 이미 확정된 관용구 그대로임(`base/dispatch-core-plan.md`의 "실제
마운트를 하지 않는 위치는 `None`을 등록 — `setLength`도 짝을 맞춰 `0`"). 마운트를 하지 않는 위치는 `None`을 등록 — `setLength`도 짝을 맞춰 `0`").
즉 **해제 = 0/`None`으로 재등록**이고 별도 unregister 함수가 없어도 됨 — 즉 **해제 = 0/`None`으로 재등록**이고 별도 unregister 함수가 없어도 됨 —
앞서 "이게 실제 작업량"이라고 적었던 판단은 과했음. 앞서 "이게 실제 작업량"이라고 적었던 판단은 과했음.

View file

@ -113,7 +113,7 @@ Handler는 quad 사용자가 아니라 **백엔드/핸들러 구현자가 채우
지점**이라는 완전히 다른 축의 개념이라, 여기 분류를 "왜 Handler가 지점**이라는 완전히 다른 축의 개념이라, 여기 분류를 "왜 Handler가
빠졌는지" 궁금해할 필요 없음 — 프리미티브 분류가 불완전한 게 아니라 빠졌는지" 궁금해할 필요 없음 — 프리미티브 분류가 불완전한 게 아니라
Handler가 애초에 다른 층위. 관련해서 Handler를 담는 엔진(`Dispatch`) 자체가 Handler가 애초에 다른 층위. 관련해서 Handler를 담는 엔진(`Dispatch`) 자체가
왜 프리미티브가 아니라 탑레벨 싱글톤인지는 `base/bind-system-plan.md`의 왜 프리미티브가 아니라 탑레벨 싱글톤인지는 `base/dispatch-core-plan.md`의
"Dispatch는 프리미티브가 아니다" 절 참고. "Dispatch는 프리미티브가 아니다" 절 참고.
과거 "미해결로 남은 것"으로 적었던 두 항목도 모두 해소됨: `:Compute` 과거 "미해결로 남은 것"으로 적었던 두 항목도 모두 해소됨: `:Compute`

View file

@ -98,7 +98,7 @@ nil`/`or None`(and/or 삼항)으로 적었으나, `Tag(...)`가 항상-truthy라
구 모델과 달리 **핸들러 타입이 사이클마다 바뀔 수 있음**(`Tag(...)` ↔ 구 모델과 달리 **핸들러 타입이 사이클마다 바뀔 수 있음**(`Tag(...)` ↔
`nil`, 값이 `Tag`가 아니게 되면 `TagHandler.isHandlable`이 더 이상 안 `nil`, 값이 `Tag`가 아니게 되면 `TagHandler.isHandlable`이 더 이상 안
맞음) — 그래서 `retract`가 실제로 필요해짐(`bind-system-plan.md` "확정된 맞음) — 그래서 `retract`가 실제로 필요해짐(`dispatch-core-plan.md` "확정된
디스패치 모델" 절의 일반 원칙 그대로). 디스패치 모델" 절의 일반 원칙 그대로).
**[전면 정정, 2026-08-12 열한 번째 세션] 아래는 이전 버전(단일 `relate`, **[전면 정정, 2026-08-12 열한 번째 세션] 아래는 이전 버전(단일 `relate`,
@ -156,6 +156,7 @@ TagHandler.priority = <일반>
TagHandler.isHandlable(inst, k, v) = isTag(v) -- Brand 기반, array-part 전용 TagHandler.isHandlable(inst, k, v) = isTag(v) -- Brand 기반, array-part 전용
function TagHandler.process(inst, k, v, index) function TagHandler.process(inst, k, v, index)
local added = {}
for name in v:Names() do for name in v:Names() do
local holders = tagNameMap:GetStrong(inst, name) local holders = tagNameMap:GetStrong(inst, name)
if not holders then if not holders then
@ -163,10 +164,13 @@ function TagHandler.process(inst, k, v, index)
tagNameMap:SetStrong(inst, name, holders) tagNameMap:SetStrong(inst, name, holders)
end end
if next(holders) == nil then if next(holders) == nil then
addTag(inst, { name }) -- 주입된 엔진 op(아래 "패키지 배치" 절) table.insert(added, name) -- 이 이름을 처음 거는 위치일 때만 실제 호출 대상
end end
holders[k] = true -- 이 "위치"가 이 이름을 걺(Tag 객체가 다른 위치와 같아도 무관) holders[k] = true -- 이 "위치"가 이 이름을 걺(Tag 객체가 다른 위치와 같아도 무관)
end end
if #added > 0 then
addTag(inst, added) -- 한 번에 — 웹 className 일괄 갱신을 위한 배치 계약
end
return function(nextValue) return function(nextValue)
-- nextValue는 nil(단순 철거)이거나 같은 핸들러가 곧 처리할 새 Tag — -- nextValue는 nil(단순 철거)이거나 같은 핸들러가 곧 처리할 새 Tag —
-- 타입이 계약으로 보장되므로 isTag 가드가 필요 없음(2026-08-13 열네 번째 세션). -- 타입이 계약으로 보장되므로 isTag 가드가 필요 없음(2026-08-13 열네 번째 세션).
@ -174,26 +178,33 @@ function TagHandler.process(inst, k, v, index)
-- 이름 집합도 절대 안 바뀜 — holders 순회 자체가 불필요한 순수 최적화 -- 이름 집합도 절대 안 바뀜 — holders 순회 자체가 불필요한 순수 최적화
local removed = {} local removed = {}
for name in v:Names() do for name in v:Names() do
-- [정정, 2026-08-14 리뷰] 생존 이름은 **홀더 등록 자체를 유지**한다.
-- 예전엔 홀더를 일단 빼고 removeTag 호출만 skip했는데, 그러면 곧
-- 이어지는 process가 빈 holders를 보고 `addTag`**다시** 부름
-- (엔진이 멱등이라 안 보였을 뿐, "진짜 바뀐 이름에만 호출"이라는
-- 이 절의 설계 목표와 어긋났고 아래 서술과도 모순이었음).
if nextValue ~= nil and nextValue:Contains(name) then continue end
local holders = tagNameMap:GetStrong(inst, name) -- 이미 등록됐으므로 항상 있음 local holders = tagNameMap:GetStrong(inst, name) -- 이미 등록됐으므로 항상 있음
holders[k] = nil -- 이 "위치"가 이 이름을 놓음(같은 Tag 객체를 다른 위치도 쓰고 있어도 무관) holders[k] = nil -- 이 "위치"가 이 이름을 놓음(같은 Tag 객체를 다른 위치도 쓰고 있어도 무관)
if next(holders) == nil and not (nextValue ~= nil and nextValue:Contains(name)) then if next(holders) == nil then
table.insert(removed, name) -- 곧 새 process가 재확정할 이름이면 skip(깜빡임 방지) table.insert(removed, name)
end end
end end
if #removed > 0 then if #removed > 0 then
removeTag(inst, removed) -- 한 번에 — 웹 className 일괄 갱신을 위한 배치 계약 removeTag(inst, removed) -- 한 번에
end end
end end
end end
``` ```
- **`addTag`는 온전히 `process`, `removeTag`는 온전히 반환하는 클로저** - **`addTag`는 온전히 `process`, `removeTag`는 온전히 반환하는 클로저**
— 서로 겹치는 diff 계산이 없음. 클로저가 이전 `Tag`(`v`, 자기 자신이 — 서로 겹치는 diff 계산이 없음. 클로저는 이전 `Tag`(`v`, 자기 자신이
캡처)가 걸었던 이름 전부를 소유 목록에서 빼되(항상 실행), 그 결과 캡처)가 걸었던 이름 중 **새 값이 더 이상 안 거는 것만** 소유 목록에서
목록이 비었을 때 **실제 `removeTag` 호출만** "새로 들어올 값이 빼고, 그 결과 목록이 빈 이름들을 모아 `removeTag`**한 번** 호출함.
그 이름을 여전히 Contains하는가"로 판단해 skip — 소유 목록 자체는 생존 이름은 소유 목록을 그대로 두므로(**[정정, 2026-08-14 리뷰]** 예전엔
항상 최신 객체로 갱신되므로(정확히 `v`를 빼고 새 `process`가 새 값을 일단 뺐다가 호출만 skip했는데 그러면 곧이은 `process`가 빈 목록을 보고
넣는 두 단계), 이름이 살아남는 경우에도 stale 레퍼런스가 안 남음. `addTag`를 다시 불렀음) 뒤이은 `process`에서 `addTag` 자체가 안 불림 —
"실제 엔진 호출은 진짜 바뀐 이름에만"이 코드 수준에서도 성립.
`process``v`가 새로 거는 이름 전부를 무조건 등록(소유 목록이 `process``v`가 새로 거는 이름 전부를 무조건 등록(소유 목록이
비어있던 경우에만 실제 `addTag`) — 자기 나름의 old-vs-new diff가 전혀 비어있던 경우에만 실제 `addTag`) — 자기 나름의 old-vs-new diff가 전혀
필요 없음(그 일을 클로저가 매번 정확히 해줌). 필요 없음(그 일을 클로저가 매번 정확히 해줌).

View file

@ -114,7 +114,7 @@ v1에서도 `Corner`/`PaddingAll`/`Scale`은 store 값으로 바인드 가능했
다시 세팅하는 정도로 충분. 구현 비용도 낮음: 각 Handler가 `process`에서 다시 세팅하는 정도로 충분. 구현 비용도 낮음: 각 Handler가 `process`에서
"이전에 자기가 찾거나 만든 자식 Instance"를 얻어야 하는데, 이건 이미 "이전에 자기가 찾거나 만든 자식 Instance"를 얻어야 하는데, 이건 이미
base가 범용 유틸로 제공하기로 확정한 per-instance weak-keyed 저장소 base가 범용 유틸로 제공하기로 확정한 per-instance weak-keyed 저장소
(`Relate:SetStrong(inst,k,...)`, `base/relate-plan.md`/`base/bind-system-plan.md` (`Relate:SetStrong(inst,k,...)`, `base/relate-plan.md`/`base/dispatch-core-plan.md`
"핸들러 내부 상태 저장" 절)를 그대로 재사용하면 됨 — PropertyHandler가 실행 중인 "핸들러 내부 상태 저장" 절)를 그대로 재사용하면 됨 — PropertyHandler가 실행 중인
Tween 상태를 기억해두는 것과 정확히 같은 패턴. 새 메커니즘 발명 불필요, 이미 Tween 상태를 기억해두는 것과 정확히 같은 패턴. 새 메커니즘 발명 불필요, 이미
있는 "store 바인드는 pluggable 바인드를 재실행하는 래핑" 원칙 있는 "store 바인드는 pluggable 바인드를 재실행하는 래핑" 원칙

View file

@ -11,9 +11,9 @@
| 폴더 | 뜻 | 누가 처리 | | 폴더 | 뜻 | 누가 처리 |
|---|---|---| |---|---|---|
| `review-required/` | **설계가 걸림 — 사람 결정 필요**(**[2026-08-13 13차 세션] 현재 비어 있음** — 마지막 한 건이던 `08`이 해소돼 `done/`으로 감) | ⭐ 사용자 | | `review-required/` | **설계가 걸림 — 사람 결정 필요**(**[2026-08-13 13차 세션] 현재 비어 있음** — 마지막 한 건이던 `08`이 해소돼 `done/`으로 감) | ⭐ 사용자 |
| `rewrite-required/` | 스파이크 코드가 깨짐(설계 문제 아님, `13`/`15`/`16`) | 에이전트 | | `rewrite-required/` | 스파이크가 낡음 — 코드가 깨졌거나(`13`/`15`/`16`), **설계가 바뀌어 옛 모델을 검증 중**(`04`/`19`, 2026-08-13 14차 세션 하강 diff) | 에이전트 |
| `not-run/` | 이 환경에서 못 돌림(Studio 전용 `10` + GC 헬퍼) | 사용자 or MCP 연결 후 | | `not-run/` | 이 환경에서 못 돌림(Studio 전용 `10` + GC 헬퍼) | 사용자 or MCP 연결 후 |
| `done/` | 통과 or 판정 끝, 더 할 일 없음(15개) | — | | `done/` | 통과 or 판정 끝, 더 할 일 없음(14개) | — |
**스파이크를 고치거나 돌렸으면 파일을 해당 폴더로 `git mv`하고 STATUS.md의 **스파이크를 고치거나 돌렸으면 파일을 해당 폴더로 `git mv`하고 STATUS.md의
줄도 같이 옮길 것** — 그게 곧 상태 갱신이다. 아래 파일 목록의 경로는 줄도 같이 옮길 것** — 그게 곧 상태 갱신이다. 아래 파일 목록의 경로는
@ -68,7 +68,7 @@ ROADMAP 항목 근거인지, 어떻게 실행하는지, 실행 후 뭘 확인해
| 파일 | 검증 대상 | 근거 문서 | | 파일 | 검증 대상 | 근거 문서 |
|---|---|---| |---|---|---|
| `01-two-pass-array-hash-order.luau` | 배열 파트(children/Ref) 먼저, 해시 파트(프로퍼티/이벤트) 나중이라는 두 패스 순회 계약 | `dispatch-core-plan.md` "props 순회 순서", ROADMAP M0-4 | | `01-two-pass-array-hash-order.luau` | 배열 파트(children/Ref) 먼저, 해시 파트(프로퍼티/이벤트) 나중이라는 두 패스 순회 계약 | `dispatch-core-plan.md` "props 순회 순서", ROADMAP M0-4 |
| `02-none-sentinel-vs-nil-holes.luau` | **[2026-08-09 커밋 f198fd9 반영해 전면 재작성]** 순서가 중요한 배열(PreRef pre-pass, sourceList)은 `None` 소진이 맞고, 순서가 안 중요하고 재사용이 필요한 배열(Ref 콜백/대기자)은 `nil`+슬롯 재사용이 맞다는 최종 구분 + `None`을 잘못 쓰면 배열이 무한정 자라는 버그의 정량적 재현 | `bind-system-plan.md` "왜 None이 아니라 nil인가"(2026-08-09 열한 번째 세션 최종 정정), ROADMAP M0-4 | | `02-none-sentinel-vs-nil-holes.luau` | **[2026-08-09 커밋 f198fd9 반영해 전면 재작성]** 순서가 중요한 배열(PreRef pre-pass, sourceList)은 `None` 소진이 맞고, 순서가 안 중요하고 재사용이 필요한 배열(Ref 콜백/대기자)은 `nil`+슬롯 재사용이 맞다는 최종 구분 + `None`을 잘못 쓰면 배열이 무한정 자라는 버그의 정량적 재현 | `ref-plan.md` "왜 None이 아니라 nil인가"(2026-08-09 열한 번째 세션 최종 정정), ROADMAP M0-4 |
| `03-recursive-store-bind-dispatch.luau` | `process`/`retract` 재귀 재-dispatch 기본 모델, 우선순위 스캔 | `dispatch-core-plan.md` "확정된 디스패치 모델", ROADMAP M0-3 | | `03-recursive-store-bind-dispatch.luau` | `process`/`retract` 재귀 재-dispatch 기본 모델, 우선순위 스캔 | `dispatch-core-plan.md` "확정된 디스패치 모델", ROADMAP M0-3 |
| `04-dispatch-chain-retractFrom.luau` | **[⚠️ 2026-08-13 열네 번째 세션: 하강 diff 확정으로 낡음 → `rewrite-required/`]** 아래는 옛 모델 기준 설명 — **[2026-08-13 감사에서 전면 재작성 + 파일명 변경]** 인덱스 기반 `chains`/`Dispatch.retractFrom`이 다단 재귀 위임에서 정확한지 — 3단 체인이 인덱스 1/2/3으로 안 겹치고 쌓이는지(= `State<State<T>>` **정상 동작**, UB 아님), 안/바깥 store 재발행 시 깊은 인덱스부터 정리되는지, hint가 target 인덱스에만 가는지 + **음성 대조군**: `chains:SetStrong``handler.process` 뒤에 두면 최초 마운트에서 하위 retractor가 유실되는 버그 재현. 옛 버전은 핸들러 identity 기반 추적과 "중복 push 즉시 error" 가드를 검증했는데 그 가드는 다섯 번째 세션 재설계로 **없어져서** 설계와 정반대를 테스트하고 있었음 | `dispatch-core-plan.md` "Dispatch 체인"(2026-08-13 다섯 번째 세션 재설계) + 2026-08-13 감사 | | `04-dispatch-chain-retractFrom.luau` | **[⚠️ 2026-08-13 열네 번째 세션: 하강 diff 확정으로 낡음 → `rewrite-required/`]** 아래는 옛 모델 기준 설명 — **[2026-08-13 감사에서 전면 재작성 + 파일명 변경]** 인덱스 기반 `chains`/`Dispatch.retractFrom`이 다단 재귀 위임에서 정확한지 — 3단 체인이 인덱스 1/2/3으로 안 겹치고 쌓이는지(= `State<State<T>>` **정상 동작**, UB 아님), 안/바깥 store 재발행 시 깊은 인덱스부터 정리되는지, hint가 target 인덱스에만 가는지 + **음성 대조군**: `chains:SetStrong``handler.process` 뒤에 두면 최초 마운트에서 하위 retractor가 유실되는 버그 재현. 옛 버전은 핸들러 identity 기반 추적과 "중복 push 즉시 error" 가드를 검증했는데 그 가드는 다섯 번째 세션 재설계로 **없어져서** 설계와 정반대를 테스트하고 있었음 | `dispatch-core-plan.md` "Dispatch 체인"(2026-08-13 다섯 번째 세션 재설계) + 2026-08-13 감사 |
| `05-store-state-diamond-propagation.luau` | push-invalidate/pull-recompute가 다이아몬드 의존성에서 중복 재계산 없이 동작하는지 | ROADMAP M0-1 | | `05-store-state-diamond-propagation.luau` | push-invalidate/pull-recompute가 다이아몬드 의존성에서 중복 재계산 없이 동작하는지 | ROADMAP M0-1 |
@ -82,7 +82,7 @@ ROADMAP 항목 근거인지, 어떻게 실행하는지, 실행 후 뭘 확인해
| `13-type-ref-preref-subtype.luau` | (A, 타입) `PreRef<T>``Ref<T>`를 구조적으로 만족하는지, (B, 런타임) `isRef`/`isPreRef` 합성이 재정정대로 동작하는지(`isRef(preRefInstance)`가 이제 `true`) + Leaf 핸들러가 `isRef(v) and not isPreRef(v)`로 명시적으로 좁혀야 하는 이유 | `brand-plan.md``Brand` 절(2026-08-09 열한 번째 세션 재정정) | | `13-type-ref-preref-subtype.luau` | (A, 타입) `PreRef<T>``Ref<T>`를 구조적으로 만족하는지, (B, 런타임) `isRef`/`isPreRef` 합성이 재정정대로 동작하는지(`isRef(preRefInstance)`가 이제 `true`) + Leaf 핸들러가 `isRef(v) and not isPreRef(v)`로 명시적으로 좁혀야 하는 이유 | `brand-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 열한 번째 세션]" 절 | | `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 후속 세션, 순서는 같은 날 세 번째 세션에 정정) | | `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>``T`의 각 필드를 `Source`로 감싼 타입을 Luau `type function`(`types.newtable`/`:setproperty`/`ty:properties()`)으로 실제 합성 가능한지, 결과가 구조적으로 `Source<T>` 필드를 만족하는지 | `bind-system-plan.md` "`store.key` 레코드 필드 타이핑" 절(2026-08-12 열일곱 번째 세션), `pre-implementation-audit.md` 1-10 | | `16-type-store-key-typefunction.luau` (타입체크 전용) | `Store<T>``T`의 각 필드를 `Source`로 감싼 타입을 Luau `type function`(`types.newtable`/`:setproperty`/`ty:properties()`)으로 실제 합성 가능한지, 결과가 구조적으로 `Source<T>` 필드를 만족하는지 | `typing-limits.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 | | `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` 실사례 | | `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` "요소 소유권" | | `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` "요소 소유권" |

View file

@ -192,6 +192,13 @@
남은 근거는 편의성과 Slot offset이 밀리고 당겨지는 케이스뿐이라 남은 근거는 편의성과 Slot offset이 밀리고 당겨지는 케이스뿐이라
우선순위가 더 내려감 — `state:Flatten()`류 콤비네이터 아이디어는 우선순위가 더 내려감 — `state:Flatten()`류 콤비네이터 아이디어는
그대로 백로그. 상세는 `research/operator-sugar-plan.md` 마지막 절. 그대로 백로그. 상세는 `research/operator-sugar-plan.md` 마지막 절.
- **[신설, 2026-08-14 리뷰] `AttributeGroupHandler.process`의 부분 실패
롤백** — 이름 순회 도중 소유권 충돌 error가 나면 그 전에 등록된 이름들이
이 사이클엔 회수되지 않음(클로저가 안 만들어짐). 피해는 그 인스턴스
수명으로 한정되고 재현도 시끄럽게 반복돼서 **지금은 별도 장치 없이
문서화만** 했는데(`base/attribute-plan.md` "메커니즘" 절), 원자적
롤백(그룹 `process`에만 국소적인 unwind)을 넣을지는 열어둠. 지금 결정
불필요 — M10 구현 시점에 판단.
- **[신설, 2026-08-13 열네 번째 세션] `Attribute.Merged`의 이름 중복** — - **[신설, 2026-08-13 열네 번째 세션] `Attribute.Merged`의 이름 중복** —
두 Store가 같은 이름을 가지면 지금은 `:NameMap()` 평탄화 단계에서 두 Store가 같은 이름을 가지면 지금은 `:NameMap()` 평탄화 단계에서
조용히 하나가 이김(dispatch 이전이라 이름 claim이 못 잡는 자리). 조용히 하나가 이김(dispatch 이전이라 이름 claim이 못 잡는 자리).

View file

@ -196,7 +196,7 @@ additional-primitives-plan.md`의 "문서화 백로그" 절이 원자료)**:
인가`)을 더 깊게 확장, `Blocker` 같은 파생 프리미티브가 이 목표 위에서 인가`)을 더 깊게 확장, `Blocker` 같은 파생 프리미티브가 이 목표 위에서
왜 자연스럽게 나왔는지까지 포함하는 설계 철학 에세이 왜 자연스럽게 나왔는지까지 포함하는 설계 철학 에세이
6. **왜 배열/해시 두 패스 순서를 안 뒤집는가, `PreRef`는 왜 그 예외로 6. **왜 배열/해시 두 패스 순서를 안 뒤집는가, `PreRef`는 왜 그 예외로
따로 필요한가** (2026-08-07 세 번째 세션 원자료, `bind-system-plan.md` 따로 필요한가** (2026-08-07 세 번째 세션 원자료, `ref-plan.md`
"`phase` 옵션 폐기" 절 마지막 항목이 이 자리를 지목해뒀던 것 — 지금까지 "`phase` 옵션 폐기" 절 마지막 항목이 이 자리를 지목해뒀던 것 — 지금까지
여기 안 옮겨져 있었음) — "프로퍼티/이벤트가 항상 children/Ref보다 여기 안 옮겨져 있었음) — "프로퍼티/이벤트가 항상 children/Ref보다
나중"이라는 순서를 고치는 대신 `PreRef`라는 별도 타입으로 예외를 나중"이라는 순서를 고치는 대신 `PreRef`라는 별도 타입으로 예외를
@ -213,7 +213,7 @@ additional-primitives-plan.md`의 "문서화 백로그" 절이 원자료)**:
**[해소됨, 2026-08-12 여섯 번째 세션]** 취소 가능 여부 — PreRef는 **[해소됨, 2026-08-12 여섯 번째 세션]** 취소 가능 여부 — PreRef는
구조적으로 `retract` 체인에 아예 안 올라가므로 취소 개념 자체가 없고, 구조적으로 `retract` 체인에 아예 안 올라가므로 취소 개념 자체가 없고,
대신 이미 fire된 PreRef를 재사용하면(두 번째 construction에 다시 대신 이미 fire된 PreRef를 재사용하면(두 번째 construction에 다시
놓으면) 즉시 `error` — "1회용, 재할당 불가"로 확정. `bind-system-plan.md` 놓으면) 즉시 `error` — "1회용, 재할당 불가"로 확정. `ref-plan.md`
"동적 경로로 도착한 PreRef는 런타임에도 명시적으로 에러" 절 바로 아래 "동적 경로로 도착한 PreRef는 런타임에도 명시적으로 에러" 절 바로 아래
"PreRef는 '취소'라는 개념이 없다" 항목 참고. "PreRef는 '취소'라는 개념이 없다" 항목 참고.

View file

@ -302,7 +302,7 @@ Attribute는 이미 "겹치면 error"로 소유 코드가 명확히 갈리는
**배경**: 2026-08-13 다섯 번째 세션의 인덱스 기반 `Dispatch` 재설계로 **배경**: 2026-08-13 다섯 번째 세션의 인덱스 기반 `Dispatch` 재설계로
`State<State<T>>`가 UB에서 **정상 지원 대상**이 됐음(각 재귀 단계가 `State<State<T>>`가 UB에서 **정상 지원 대상**이 됐음(각 재귀 단계가
다른 인덱스를 써서 슬롯 충돌이 없어짐, `base/bind-system-plan.md` 다른 인덱스를 써서 슬롯 충돌이 없어짐, `base/dispatch-core-plan.md`
"Dispatch 체인" 절). 하지만 **동작한다고 해서 권장 방향인 건 아님** "Dispatch 체인" 절). 하지만 **동작한다고 해서 권장 방향인 건 아님**
(사용자 판단: "UB는 아니지만 우리가 원치 않는 방향인건 맞습니다"). (사용자 판단: "UB는 아니지만 우리가 원치 않는 방향인건 맞습니다").

View file

@ -88,7 +88,7 @@ store.color }`처럼 애니메이션 없이 그냥 반응형으로 값만 바뀌
여러 단계(A→B→C)로 겹칠 때 자기 자신의 상태와 위임한 핸들러의 상태가 여러 단계(A→B→C)로 겹칠 때 자기 자신의 상태와 위임한 핸들러의 상태가
슬롯 하나를 두고 충돌하는 문제가 있어 기각되고, 대신 Dispatch 자신이 슬롯 하나를 두고 충돌하는 문제가 있어 기각되고, 대신 Dispatch 자신이
전체 체인을 배열로 들고 있는 쪽으로 정리됨. 상세는 `base/ 전체 체인을 배열로 들고 있는 쪽으로 정리됨. 상세는 `base/
bind-system-plan.md` "Dispatch 체인" 절, `ROADMAP.md` M2/M4. 아래는 dispatch-core-plan.md` "Dispatch 체인" 절, `ROADMAP.md` M2/M4. 아래는
원래 발견 당시 기록. 원래 발견 당시 기록.
**위치**: `base/dispatch-core-plan.md` "확정된 디스패치 모델" 절 90-91행 — **위치**: `base/dispatch-core-plan.md` "확정된 디스패치 모델" 절 90-91행 —
@ -137,7 +137,7 @@ bind-system-plan.md` "Dispatch 체인" 절, `ROADMAP.md` M2/M4. 아래는
**해소**: 별도 케이스로 처리하지 않음 — provider 미주입 상태는 결국 그 **해소**: 별도 케이스로 처리하지 않음 — provider 미주입 상태는 결국 그
클래스의 핸들러가 레지스트리에 하나도 없는 상태이므로, 위 1-3에서 확정된 클래스의 핸들러가 레지스트리에 하나도 없는 상태이므로, 위 1-3에서 확정된
일반 "매치 실패 시 즉시 error" 규칙 하나로 자연히 커버됨. 상세는 일반 "매치 실패 시 즉시 error" 규칙 하나로 자연히 커버됨. 상세는
`base/module-lifecycle-plan.md` 해당 항목, `base/bind-system-plan.md` `base/module-lifecycle-plan.md` 해당 항목, `base/dispatch-core-plan.md`
"우선순위 동률/매치 실패 처리" 절. 아래는 원래 발견 당시 기록. "우선순위 동률/매치 실패 처리" 절. 아래는 원래 발견 당시 기록.
**위치**: `base/module-lifecycle-plan.md` "Bind는 누가, 어떻게 구현하는가" **위치**: `base/module-lifecycle-plan.md` "Bind는 누가, 어떻게 구현하는가"
@ -299,7 +299,7 @@ base 인터페이스가 그보다 늦은 M8에서 만들어지는 순서 역전.
이미 쓰이는 패턴). 결과가 구조적으로 `Source<T>`를 만족하기만 하면 Luau가 이미 쓰이는 패턴). 결과가 구조적으로 `Source<T>`를 만족하기만 하면 Luau가
이름이 아니라 구조로 일치를 검사하므로 문제없음. 기술적으로 막힐 위험이 이름이 아니라 구조로 일치를 검사하므로 문제없음. 기술적으로 막힐 위험이
없어졌으니 M0/M3 어느 시점에 검증해도 무방 — `ROADMAP.md` 배치를 억지로 없어졌으니 M0/M3 어느 시점에 검증해도 무방 — `ROADMAP.md` 배치를 억지로
안 옮겨도 됨. 상세는 `base/bind-system-plan.md` "`store.key` 레코드 필드 안 옮겨도 됨. 상세는 `base/typing-limits.md` "`store.key` 레코드 필드
타이핑" 절, 실제 문법 실측은 타이핑" 절, 실제 문법 실측은
`luau-test``16-type-store-key-typefunction.luau`(신규). 아래는 `luau-test``16-type-store-key-typefunction.luau`(신규). 아래는
원래 발견 당시 기록. 원래 발견 당시 기록.

View file

@ -53,7 +53,7 @@ v1(`.claude/initreq/quad/src`) 조사 결과, API는 성격이 다른 두 계층
### 3-1. (a)는 얇게 재현 가능 — opt-in 서브패키지로 격리하면 근거 문제도 해소됨 ### 3-1. (a)는 얇게 재현 가능 — opt-in 서브패키지로 격리하면 근거 문제도 해소됨
- **이벤트 self 관습**: 클로저 한 겹으로 재현 가능. `base/bind-system-plan.md` - **이벤트 self 관습**: 클로저 한 겹으로 재현 가능. `base/event-plan.md`
"이벤트 핸들러는 self를 받지 않는다" 절이 든 반대 근거 4가지(Ref 중복 "이벤트 핸들러는 self를 받지 않는다" 절이 든 반대 근거 4가지(Ref 중복
채널, Modifier 정적 flatten과 경쟁, quad-debug 추적 밖 mutate 경로, 클로저 채널, Modifier 정적 flatten과 경쟁, quad-debug 추적 밖 mutate 경로, 클로저
비용)는 **코어에 넣을 때** 문제가 되는 것들 — 별도 opt-in 패키지 비용)는 **코어에 넣을 때** 문제가 되는 것들 — 별도 opt-in 패키지

View file

@ -119,7 +119,9 @@ clear 하게 준비해두고, 커밋하세요."*
`확정된 디스패치 모델`/`None 센티널`/`Dispatch는 프리미티브가 아니다`/ `확정된 디스패치 모델`/`None 센티널`/`Dispatch는 프리미티브가 아니다`/
`Dispatch 체인`/`Handler 작성 체크리스트`/`Length·Offset`/`store 바인드는 `Dispatch 체인`/`Handler 작성 체크리스트`/`Length·Offset`/`store 바인드는
래핑` 블록(1074줄)을 **`base/dispatch-core-plan.md`**로 옮기고 새 모델로 래핑` 블록(1074줄)을 **`base/dispatch-core-plan.md`**로 옮기고 새 모델로
재작성. `bind-system-plan.md`는 2263 → 1219줄(반응형 코어 + 인체공학). 재작성. `bind-system-plan.md`**2291 → 1213줄**(반응형 코어 + 인체공학).
(커밋 `69466ab` 메시지엔 이 숫자가 "2263→1219"로 잘못 적혀 있음 —
분할 직전/직후가 아니라 어림한 값이었음. 히스토리 재작성 대신 여기 정정.)
인바운드 참조는 `doc-check.py` 출력을 그대로 입력으로 삼아 기계적으로 인바운드 참조는 `doc-check.py` 출력을 그대로 입력으로 삼아 기계적으로
고침(파일별 라인 지정 치환) — 15개 파일 30여 곳. 고침(파일별 라인 지정 치환) — 15개 파일 30여 곳.
@ -196,3 +198,61 @@ end
"국소 레지스트리 + 즉시 error" 모양이 자연스러운 선택지라는 점을 "국소 레지스트리 + 즉시 error" 모양이 자연스러운 선택지라는 점을
질문 문서에 덧붙임. 질문 문서에 덧붙임.
- `doc-check.py` **ERROR 0** 유지 확인. - `doc-check.py` **ERROR 0** 유지 확인.
---
## 6. 후속 리뷰 라운드 (2026-08-14, 다른 에이전트 감사 + 사용자 트레이싱)
커밋 `69466ab`를 다른 에이전트 셋이 감사하고 사용자가 직접 트레이싱한
결과를 받음. **핵심 알고리즘(하강 diff, 인덱스 체인)에는 버그 없음**으로
확인됐고, 의사코드/문서 정합성에서 **실제 결함 3건 + stale 참조 다수**가
나와 전부 수정:
**1. `nameClaims``Relate`의 3-인자 계약을 위반** (실제 버그)
`GetStrong(inst)`/`SetStrong(inst, claims)`로 1·2-인자를 쓰고 있었음 —
`relate-plan.md`가 확정한 API는 항상 `(inst, key[, value])`. 같은 커밋의
`tagNameMap`은 정확히 3-인자를 쓰고 있어 대조로 바로 드러남.
`GetStrong(inst, k.Name)`/`SetStrong(inst, k.Name, k)`로 정정(중간
`claims` 테이블 자체가 없어져 코드도 짧아짐).
**2. `TagHandler`의 서술과 의사코드 불일치** (실제 버그)
`Tag(A)→Tag(B)`에서 생존 이름("selected")을 손 트레이싱하면: 클로저가
`holders[k]=nil`로 홀더를 **비운 뒤** `removeTag` 호출만 skip → 곧이은
`process`가 빈 홀더를 보고 **`addTag`를 다시 호출**함. 문서는 정반대로
"이미 걸려있던 이름은 소유 목록이 안 비어 있어 `addTag` 자체가 안 불림"
이라고 적고 있었음. 엔진이 멱등이라 눈에 안 보였을 뿐, "실제 호출은 진짜
바뀐 이름에만"이라는 이 절의 설계 목표와 어긋남.
→ **생존 이름은 홀더 등록 자체를 유지**하도록 정정(`nextValue`가 그
이름을 `Contains`하면 `continue`). 덤으로 `addTag``removeTag`처럼
**배치 호출**로 바꿈 — `{string}` 시그니처를 도입한 근거(웹 `className`
일괄 갱신)와 코드가 안 맞던 것도 같이 해소.
**3. 그룹 `process`의 부분 실패 경로가 문서에 없었음** (문서 갭)
이름 순회 도중 소유권 충돌 `error`가 나면 클로저가 안 만들어져 앞서
등록된 이름들이 그 사이클에 회수되지 않음. **다만 감사가 말한 "영구
누수"보다는 좁음** — `nameClaims`/`chains`가 `inst`에 weak라 인스턴스
GC와 함께 사라지고, 재프로세스 시 이미 claim된 이름은 `cur == k`
에러 없이 통과해 **같은 자리에서 같은 error로 반복 재현**됨(조용한
오작동이 아님). 그래서 롤백 장치는 안 넣고 그 경로를 명시적으로
문서화, 원자적 롤백 여부는 `question.md` 3번에 열어둠.
**4. 문서 분할 후 stale 절 참조 20여 곳** — `doc-check.py`의 사각지대였음
`REF` 정규식이 **줄 단위**로 돌아서 파일명과 "절 제목"이 줄바꿈에 걸친
자연스러운 인용(`` `base/\nbind-system-plan.md`의 "Length/Offset" ``)을
통째로 놓치고 있었음. 이번 분할뿐 아니라 **9차 세션의 1단계 분할 stale도
같은 이유로 계속 숨어 있었음**(`event-plan`/`ref-plan`/`typing-limits`로
간 절들).
→ 검사기를 **파일 전체 스캔 + 개행 허용**으로 고치고(줄 번호는 매치
오프셋에서 역산), 새로 드러난 WARN을 기준으로 30여 곳을 기계적으로
정정. WARN 56 → 120건으로 늘었다가 정정 후 101건(남은 건 의역 인용 등
판단이 필요한 것들).
**5. 사소한 것**: 커밋 메시지의 줄수(2263→1219)가 실제(2291→1213)와 다름
(히스토리 재작성 대신 위 4-1에 정정 기록), `CLAUDE.md``04`만 언급하고
`19` 이동을 빠뜨린 것, `luau-test/README.md` 상단 요약표가 STATUS.md와
어긋난 것 — 전부 수정.
**교훈**: 문서 분할 시 stale 참조는 "검사기가 ERROR 0이면 됐다"로 끝나지
않음 — **검사기 자신의 사각지대**를 먼저 의심할 것. 이번엔 감사가 그
사각지대를 짚어줬고, 고치자마자 1단계 분할의 오래된 stale까지 같이
드러났다.

Binary file not shown.

View file

@ -61,7 +61,11 @@ def rel(p):
# ---------- 1 & 2: 참조 무결성 ---------- # ---------- 1 & 2: 참조 무결성 ----------
# `(경로/)파일.md` 또는 `파일.luau` (+ 바로 뒤에 "절 제목"이 오면 절 검사도) # `(경로/)파일.md` 또는 `파일.luau` (+ 바로 뒤에 "절 제목"이 오면 절 검사도)
REF = re.compile(r'`\.?/?((?:[\w.-]+/)*[\w.@-]+\.(?:md|luau))`(\s*(?:의\s*)?"([^"\n]{2,60})")?') # [2026-08-14] 파일명과 "절 제목"이 **줄바꿈에 걸쳐 있어도** 잡도록 개행 허용.
# 예전엔 줄 단위로 돌려서 `base/\nbind-system-plan.md`의 "Length/Offset" 같은
# 자연스러운 줄바꿈 인용을 통째로 놓쳤고, 실제로 문서 분할 후 stale 참조
# 20여 곳이 이 사각지대로 빠져나갔음(14차 세션 리뷰에서 발견).
REF = re.compile(r'`\.?/?((?:[\w.-]+/\s*)*[\w.@-]+\.(?:md|luau))`(\s*(?:의\s*)?"([^"]{2,60})")?')
def resolve(target, src): def resolve(target, src):
"""참조 문자열을 실제 경로로 해석 — 상대/부분 경로를 관대하게 매칭.""" """참조 문자열을 실제 경로로 해석 — 상대/부분 경로를 관대하게 매칭."""
@ -122,9 +126,16 @@ def check_refs(docs):
for d in docs: for d in docs:
# archive는 히스토리라 절 참조까지 강제하지 않음(파일 존재만) # archive는 히스토리라 절 참조까지 강제하지 않음(파일 존재만)
is_archive = '/archive/' in rel(d) is_archive = '/archive/' in rel(d)
for ln, line in enumerate(open(d, encoding='utf-8'), 1): # 줄 단위가 아니라 **파일 전체**를 한 번에 스캔 — 파일명/절 제목이
for m in REF.finditer(line): # 줄바꿈에 걸친 인용도 잡기 위함(위 REF 주석 참고). 줄 번호는 매치
# 시작 오프셋으로 역산.
text = open(d, encoding='utf-8').read()
for m in REF.finditer(text):
ln = text.count('\n', 0, m.start()) + 1
target, _, section = m.group(1), m.group(2), m.group(3) target, _, section = m.group(1), m.group(2), m.group(3)
target = re.sub(r'\s+', '', target)
if section is not None:
section = re.sub(r'\s+', ' ', section).strip()
if not interesting(target): if not interesting(target):
continue continue
p = resolve(target, d) p = resolve(target, d)

View file

@ -213,7 +213,8 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션
실제 코드 작성에 재사용. 실제 코드 작성에 재사용.
- **[주의] 위 "남은 것은 셋뿐"은 여섯 번째 세션 기준** — 13차 세션에 - **[주의] 위 "남은 것은 셋뿐"은 여섯 번째 세션 기준** — 13차 세션에
`08``done/`으로 가며 `review-required/`가 비었고, **14차 세션에 `08``done/`으로 가며 `review-required/`가 비었고, **14차 세션에
`04`가 하강 diff 재설계로 전제가 바뀌어 `rewrite-required/`로 갔음**. `04``19`가 하강 diff 재설계로 전제가 바뀌어 `rewrite-required/`
갔음**(`19`는 B 섹션만 낡음, A/C는 유효).
**개수의 소스는 항상 `luau-test/STATUS.md`.** 지금 M0 착수를 막는 **개수의 소스는 항상 `luau-test/STATUS.md`.** 지금 M0 착수를 막는
설계 결정은 없고, 0-Y/0-A가 남긴 규약(`base/typing-limits.md`/ 설계 결정은 없고, 0-Y/0-A가 남긴 규약(`base/typing-limits.md`/
`base/dispatch-core-plan.md`)은 착수 전 필독. `base/dispatch-core-plan.md`)은 착수 전 필독.
@ -1097,6 +1098,16 @@ claim**으로 확정. 이걸로 마지막 게이트가 열려 **0-A(하강 diff
알고리즘이 백엔드마다 복제됨), 그 실패 모드를 위해 **`HANDLER_PRIORITY_FALLBACK`** 알고리즘이 백엔드마다 복제됨), 그 실패 모드를 위해 **`HANDLER_PRIORITY_FALLBACK`**
신설(사용자 제안 — base 핸들러는 최하위 밴드, 백엔드가 덮어쓰면 언제나 신설(사용자 제안 — base 핸들러는 최하위 밴드, 백엔드가 덮어쓰면 언제나
이김). 옛 모델은 `archive/dispatch-hintvalue-model-reversed.md`로 이전, 이김). 옛 모델은 `archive/dispatch-hintvalue-model-reversed.md`로 이전,
스파이크 `04`/`19`는 옛 모델을 검증 중이라 `rewrite-required/`로 이동. 스파이크 `04`(체인/`retractFrom`)와 `19`(B 섹션 = Attribute 소유권)는 옛
**M0 착수를 막는 결정이 이제 없음** — 새로 연 것은 사소한 둘뿐 모델을 검증 중이라 `rewrite-required/`로 이동 — `19`의 A/C 섹션은 그대로
(`Attribute.Merged` 이름 중복, `hintValue` 이름 재검토). 유효.
**M0 착수를 막는 결정이 이제 없음** — 새로 연 것은 사소한 셋뿐
(`Attribute.Merged` 이름 중복, `hintValue` 이름 재검토, 그룹 `process`
부분 실패 롤백). **[2026-08-14 후속 리뷰 라운드]** 다른 에이전트 감사 +
사용자 트레이싱으로 의사코드 결함 3건 수정 — `nameClaims``Relate`
3-인자 계약을 위반, `TagHandler`가 생존 이름의 홀더를 비웠다 되돌려
`addTag`를 헛되이 재호출(+`addTag` 배치 누락), 그룹 `process` 부분 실패
경로 미문서화. 더 큰 수확은 **`doc-check.py` 자신의 사각지대 발견** —
`REF` 정규식이 줄 단위라 파일명과 절 제목이 줄바꿈에 걸친 인용을 통째로
놓치고 있었고, 고치자 이번 분할뿐 아니라 **9차 세션 1단계 분할의 stale
참조까지** 무더기로 드러나 30여 곳 정정.

View file

@ -42,7 +42,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
- [ ] props 순회의 "배열 파트 먼저, 해시 파트 나중" 두 패스 계약이 실제 - [ ] props 순회의 "배열 파트 먼저, 해시 파트 나중" 두 패스 계약이 실제
Luau 테이블에서 관찰한 대로 동작하는지 확인, `PreRef` pre-pass + Luau 테이블에서 관찰한 대로 동작하는지 확인, `PreRef` pre-pass +
일반 `Ref`의 위치 기반 순서까지 최소 스파이크로 검증 일반 `Ref`의 위치 기반 순서까지 최소 스파이크로 검증
(2026-08-07 세 번째 세션, `base/bind-system-plan.md` "`phase` 옵션 (2026-08-07 세 번째 세션, `base/ref-plan.md` "`phase` 옵션
폐기 → 위치로 표현, `PreRef` 신설" 절) — **PreRef pre-pass의 소진은 폐기 → 위치로 표현, `PreRef` 신설" 절) — **PreRef pre-pass의 소진은
`nil`이 아니라 `None`으로(2026-08-07 열 번째 세션 정정, 사용자가 `nil`이 아니라 `None`으로(2026-08-07 열 번째 세션 정정, 사용자가
Luau REPL로 반례 제시 — 키가 듬성듬성해지면 순회가 index 순서를 Luau REPL로 반례 제시 — 키가 듬성듬성해지면 순회가 index 순서를
@ -180,7 +180,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
- [ ] `Dispatch/Leaf.luau``(i:number, v=Ref/Observer/PreRef)` children-array - [ ] `Dispatch/Leaf.luau``(i:number, v=Ref/Observer/PreRef)` children-array
leaf 매칭 Handler, `StoreBind.luau`와 같은 층위(범용/엔진무관) — leaf 매칭 Handler, `StoreBind.luau`와 같은 층위(범용/엔진무관) —
quad-base 소속으로 확정(2026-08-08 두 번째 세션, `base/ quad-base 소속으로 확정(2026-08-08 두 번째 세션, `base/
bind-system-plan.md` "Dispatch는 프리미티브가 아니다" 절) dispatch-core-plan.md` "Dispatch는 프리미티브가 아니다" 절)
- [ ] `chains`(Relate 기반, `{[inst(weak)]={[k]={[index]={handler, retractor}}}}` - [ ] `chains`(Relate 기반, `{[inst(weak)]={[k]={[index]={handler, retractor}}}}`
**재귀 깊이 인덱스 → (담당 핸들러, 그가 반환한 retractor 클로저)**) + **재귀 깊이 인덱스 → (담당 핸들러, 그가 반환한 retractor 클로저)**) +
**3-인자** `Dispatch.retractFrom(inst,k,index)` — 재귀 재-dispatch **3-인자** `Dispatch.retractFrom(inst,k,index)` — 재귀 재-dispatch
@ -213,7 +213,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
- [ ] `store.key` dot-access 타입 추론 확인 — Luau `type function` - [ ] `store.key` dot-access 타입 추론 확인 — Luau `type function`
(`WrapStore`/`ProcessStoreType`)으로 `Store<T>``T`의 각 필드를 (`WrapStore`/`ProcessStoreType`)으로 `Store<T>``T`의 각 필드를
`Source`로 감싼 레코드 타입을 합성 가능함을 확인(2026-08-12 열일곱 `Source`로 감싼 레코드 타입을 합성 가능함을 확인(2026-08-12 열일곱
번째 세션, `base/bind-system-plan.md` "`store.key` 레코드 필드 번째 세션, `base/typing-limits.md` "`store.key` 레코드 필드
타이핑" 절) — 실제 문법이 통과하는지는 타이핑" 절) — 실제 문법이 통과하는지는
`luau-test``16-type-store-key-typefunction.luau`로 실측 필요 `luau-test``16-type-store-key-typefunction.luau`로 실측 필요
- [ ] `:Compute(fn, ...)` — trailing args로 추가 의존성 직접 받는 sugar - [ ] `:Compute(fn, ...)` — trailing args로 추가 의존성 직접 받는 sugar
@ -328,7 +328,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
- [x] **"여러 Slot이 형제로 섞일 때 순서 보장" 해소**(2026-08-09 여섯 번째 - [x] **"여러 Slot이 형제로 섞일 때 순서 보장" 해소**(2026-08-09 여섯 번째
세션) — `Dispatch.setLength`/`setOffsetSource` 메커니즘, `base/ 세션) — `Dispatch.setLength`/`setOffsetSource` 메커니즘, `base/
bind-system-plan.md` "Length/Offset" 절. `Slot.Length: State<number>` dispatch-core-plan.md` "Length/Offset" 절. `Slot.Length: State<number>`
이때 확정(CRUD/`:List` 여부 무관 항상 노출, 순서 계산과 "n개 검색됨" 이때 확정(CRUD/`:List` 여부 무관 항상 노출, 순서 계산과 "n개 검색됨"
UI 둘 다 겸함) — 구현 시 이 두 API를 `:List`/CRUD의 `raw*`가 호출. UI 둘 다 겸함) — 구현 시 이 두 API를 `:List`/CRUD의 `raw*`가 호출.
- [x] **Slot의 `Add`/`Remove`/`Extract`/`ExtractAll`/`Clear`/`Move`/`Swap`/ - [x] **Slot의 `Add`/`Remove`/`Extract`/`ExtractAll`/`Clear`/`Move`/`Swap`/
@ -377,7 +377,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
위치"(`candidateIndex`, filter로 압축됨), `key`와도 무관(순서/레이아웃 위치"(`candidateIndex`, filter로 압축됨), `key`와도 무관(순서/레이아웃
전용, 식별 목적 아님) — 문서화 시 셋(원본 raw index/`key`/`updateFn`의 전용, 식별 목적 아님) — 문서화 시 셋(원본 raw index/`key`/`updateFn`의
`index`)을 혼동하지 않게 주의. **`offset``Slot.Offset`을 그대로 `index`)을 혼동하지 않게 주의. **`offset``Slot.Offset`을 그대로
전달**(형제 Slot/정적 자식 누적합, `base/bind-system-plan.md`의 전달**(형제 Slot/정적 자식 누적합, `base/dispatch-core-plan.md`의
"Length/Offset" 절) — `index`/`offset` 둘 다 **raw 값으로만 전달, "Length/Offset" 절) — `index`/`offset` 둘 다 **raw 값으로만 전달,
`Slot`/Handler가 `LayoutOrder` 등을 자동으로 세팅해주지 않음** `Slot`/Handler가 `LayoutOrder` 등을 자동으로 세팅해주지 않음**
(2026-08-11 세션 확정 — 자동 바인딩은 컴포넌트가 이미 지정한 값을 (2026-08-11 세션 확정 — 자동 바인딩은 컴포넌트가 이미 지정한 값을
@ -443,7 +443,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
요소를 모른 채 충돌하는 gap이 있었음(2026-08-11 세션, `base/ 요소를 모른 채 충돌하는 gap이 있었음(2026-08-11 세션, `base/
slot-plan.md` "CRUD API 확정" 절). slot-plan.md` "CRUD API 확정" 절).
- [x] **`recompute` off-by-one 버그 수정**(2026-08-11 세션, `base/ - [x] **`recompute` off-by-one 버그 수정**(2026-08-11 세션, `base/
bind-system-plan.md` "Length/Offset" 절) — `sum` 누적과 dispatch-core-plan.md` "Length/Offset" 절) — `sum` 누적과
`offset:Set` 순서가 뒤바뀌어 `Offset`이 자기 자신을 포함해버리던 `offset:Set` 순서가 뒤바뀌어 `Offset`이 자기 자신을 포함해버리던
버그(예: 유일한 자식인데도 `Offset`이 0이 아니게 됨) 수정. 재진입 버그(예: 유일한 자식인데도 `Offset`이 0이 아니게 됨) 수정. 재진입
방지 가드는 검토 후 기각 — 각 Slot이 `Relate(자기 자신)`으로 방지 가드는 검토 후 기각 — 각 Slot이 `Relate(자기 자신)`으로
@ -526,7 +526,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
- [ ] `Ref.luau`(`.Value` 읽기 전용 필드 + `:Set(value)`/`:Callback(fn)`/ - [ ] `Ref.luau`(`.Value` 읽기 전용 필드 + `:Set(value)`/`:Callback(fn)`/
`:Wait(thread?)`, 전부 self 반환) + `PreRef.luau`(별도 파일, Ref `:Wait(thread?)`, 전부 self 반환) + `PreRef.luau`(별도 파일, Ref
런타임 재사용 + children 배열 전용, Modifier/Store 타입 차단, 런타임 재사용 + children 배열 전용, Modifier/Store 타입 차단,
위치 무관 호이스팅 pre-pass — `base/bind-system-plan.md` "`phase` 위치 무관 호이스팅 pre-pass — `base/ref-plan.md` "`phase`
옵션 폐기 → 위치로 표현, `PreRef` 신설" 절 + "API 모양" 절) 옵션 폐기 → 위치로 표현, `PreRef` 신설" 절 + "API 모양" 절)
- [ ] `(v=Ref)` 매치 핸들러 — children 배열의 숫자 슬롯에 놓인 - [ ] `(v=Ref)` 매치 핸들러 — children 배열의 숫자 슬롯에 놓인
`Ref(default)` 인스턴스를 인식해 바인드(별도 `CreatedRef` 래퍼 `Ref(default)` 인스턴스를 인식해 바인드(별도 `CreatedRef` 래퍼