fix(slot,docs): 리뷰 지적 5건 정정 — 언마운트/파괴 모순, 계약 개수 모순, 해제 순서 역전
다른 에이전트 리뷰가 짚은 5건을 전부 사실 확인 후 수정.
1. (가장 심각) slot-plan.md의 SlotHandler.process 반환 클로저가 여전히
destroySlotTree를 불러, 같은 커밋에서 확정한 "State<Slot> 교체는 파괴가
아니라 언마운트" 결정과 정면 모순 — 그 결정이 적용돼야 할 최상위 dispatch
경로가 바로 이 클로저인데 "구현상 바뀌어야 하는 것" 절이 reconcile만
언급하고 빠뜨렸음. 그대로 뒀으면 언마운트 결정 자체가 무의미해질 뻔함.
→ 비파괴 경로 unmountSlotTree 신설(destroySlotTree와의 차이는 딱 둘:
실제 Destroy 안 함, 자식 releaseOwner 안 함 — 자식이 계속 그 slot
소유라 통째로 재마운트 가능 = 포탈). 클로저를 unmountSlotTree +
setOffsetSource(None) -> setLength(0) + unbindLifetime + releaseOwner로
교체. "자동 경로는 언마운트, 명시적 Remove/Clear/dispose만 파괴"로 정리.
2. bind-system-plan.md "확정된 것" 절이 폐기된 4-메소드 계약을 여전히 확정
사실로 서술 — 같은 파일 상단은 이미 3-메소드로 갱신돼 자기 문서 내 모순.
3. question.md의 Slot 해제 순서가 거꾸로 — setLength(0)을 먼저 부르라고 돼
있었는데, 같은 커밋이 slot-plan.md에 추가한 경고가 정확히 그 순서가
죽어가는 서브트리 Source에 헛된 :Set()을 날린다고 명시.
4. ROADMAP.md M6에 이번 세션 Slot 결정이 전혀 미반영(M0/M2/M4/M10엔 반영됨)
— 언마운트 전환/dispose/claimOwner vs claimOwnerAt 분리/버그 4건 추가.
5. CLAUDE.md 여섯 번째 세션 항목이 자체 규칙(2~4줄+링크) 위반, 63줄까지
불어남 — 과거 3000줄 비대화를 유발한 패턴 재발. 15줄로 압축.
6번(StoreBind가 최초 발화에서도 retractFrom을 부르는 것)은 사용자 판단으로
그대로 둠 — 빈 슬롯 retractFrom은 무시 가능하고 일관성이 낫다는 결정.
추가: question.md 0-Y에 사용자 방향 기록 — 순환 타입을 만들기보다 State 타입
자체를 구울 때 인라이닝하는 쪽(Modifier flat 타입 생성과 같은 결). 사람이
직접 확인할 부분이라 0-Z(Attribute)와 함께 사용자 판단 목록으로 이관.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
124b706e2c
commit
b90b3a6b94
5 changed files with 144 additions and 76 deletions
|
|
@ -2669,9 +2669,15 @@ State/스트림)를 다뤘던 이전 시도가 있어 조사함 — **확인된
|
|||
|
||||
## 확정된 것 (더 이상 열린 질문 아님)
|
||||
|
||||
- **핸들러 계약**: `isHandlable(inst,k,v)` + `priority` + `process`(구
|
||||
`bind`) + `retract`(구 `cleanup`) 4종 조합으로 확정 — tbox식 6-hook 세분화는 지금은
|
||||
- **핸들러 계약**: `isHandlable(inst,k,v)` + `priority` +
|
||||
`process(inst,k,v,index)` **3종**으로 확정 — tbox식 6-hook 세분화는 지금은
|
||||
안 함. 실제 구현하며 부족한 지점이 보이면 그때 hook 추가(점진적 확장).
|
||||
**[정정, 2026-08-13 다섯 번째 세션 — 이 항목이 갱신에서 누락돼 문서 상단
|
||||
"핸들러 계약" 절과 모순돼 있던 걸 같은 날 리뷰에서 발견]** 예전엔
|
||||
`process`(구 `bind`) + `retract`(구 `cleanup`) 4종이었으나, `retract`가
|
||||
별도 필드에서 **`process`의 반환값(retractor 클로저)** 으로 합쳐짐 —
|
||||
이름과 개념은 그대로 유효하고 자리만 옮겨온 것(위 "핸들러 계약" 절이
|
||||
정본).
|
||||
- **Signal 클래스**: 안 만듦, 콜백 + `Connected` 계산 속성만(`base/
|
||||
lifecycle-pattern.md`).
|
||||
- **Ref**: 도입 확정(위 절 참고), 용도는 "id 기반 조회 대체"가 아니라 "외부
|
||||
|
|
|
|||
|
|
@ -340,13 +340,27 @@ function SlotHandler.process(inst, k, slotValue, index)
|
|||
-- no-op을 심어두면 그 다음 진짜 교체 때 이전 서브트리를 정리할 주체가 사라짐.
|
||||
return function(hintValue)
|
||||
if slotValue == hintValue then return end -- 같은 Slot 재발행 → no-op
|
||||
destroySlotTree(slotValue) -- 재귀 파괴(자식 정리), 폐기·옮기지 않음 — slot 자신의 GC 앵커는 안 건드림(top-level 전용)
|
||||
unbindLifetime(inst, slotValue) -- top-level 자신의 GC 앵커 해제 — attachSlot의 top-level bindLifetime과 짝
|
||||
releaseOwner(slotValue, inst)
|
||||
-- [정정, 2026-08-13 감사 후속] 파괴가 아니라 **언마운트** —
|
||||
-- 아래 "`State<Slot>` 교체는 파괴가 아니라 언마운트" 절이 확정한
|
||||
-- 결정이 적용돼야 하는 자리가 바로 여기(최상위 dispatch 경로).
|
||||
-- 예전엔 destroySlotTree를 불러 그 결정과 정면으로 모순됐음.
|
||||
unmountSlotTree(slotValue)
|
||||
-- 이 위치가 더 이상 기여하지 않음을 owner에게 알림 — **순서 고정**
|
||||
-- (setLength가 끝에서 recompute를 돌리므로 offsetSource를 먼저 비워야
|
||||
-- 죽는 중인 Source에 헛된 :Set()이 안 감, 아래 ⚠️ 절 참고)
|
||||
Dispatch.setOffsetSource(inst, k, None)
|
||||
Dispatch.setLength(inst, k, 0)
|
||||
unbindLifetime(inst, slotValue) -- top-level 자신의 GC 앵커 해제 — SlotHandler.process의 bindLifetime과 짝
|
||||
releaseOwner(slotValue, inst) -- 이제 이 Slot은 아무에게도 안 묶임(다른 곳에 다시 넣을 수 있음)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**언마운트된 Slot은 그대로 재사용 가능** — `_elements`와 그 안의 소유권이
|
||||
전부 보존되므로(아래 `unmountSlotTree` 참고), `slot`을 들고 있던 코드가
|
||||
다른 곳에 다시 넣으면 `attachSlot`이 새 물리 부모로 다시 flush함. 아무도
|
||||
안 들고 있으면 그냥 GC. 지금 확실히 죽이려면 `dispose`(아래 절).
|
||||
|
||||
**[전면 정정, 2026-08-13 감사] `claimOwner`를 nested/top-level 두 함수로
|
||||
쪼개고, top-level은 `(inst, k)`까지 본다.** 원래는 하나의 `claimOwner`가
|
||||
"같은 ownerKey면 `false` 반환(no-op)"을 양쪽에 공유했는데, 그 분기가
|
||||
|
|
@ -1390,6 +1404,32 @@ Slot=그 `.Length`)이 됨. plain 요소만 있는 흔한 경우엔 항상 합==
|
|||
대해서만 한 번 돎:**
|
||||
|
||||
```lua
|
||||
-- [신설, 2026-08-13 감사 후속] 비파괴 언마운트 — `State<Slot>` 교체/`Extract`
|
||||
-- 계열이 쓰는 경로. `destroySlotTree`와 **딱 하나만 다름: 실제로 안 죽인다.**
|
||||
-- 물리 트리에서만 떼어내고 `_elements`/자식 소유권은 통째로 보존하므로,
|
||||
-- 같은 Slot을 나중에 다른 곳에 다시 마운트할 수 있음(= 포탈).
|
||||
local function unmountSlotTree(slot)
|
||||
for i, element in ipairs(slot._elements) do
|
||||
if isSlot(element) then
|
||||
unmountSlotTree(element) -- 재귀 — 중첩 Slot도 똑같이 비파괴
|
||||
else
|
||||
element.Parent = nil -- Destroy 아님(quad-roblox 글루가 수행)
|
||||
end
|
||||
-- releaseOwner를 **안 부름** — 자식들은 여전히 이 slot의 소유. 이게
|
||||
-- destroySlotTree와의 핵심 차이(파괴는 소유권까지 반납, 언마운트는 유지).
|
||||
end
|
||||
local bk = getBookkeeping(slot)
|
||||
if bk then
|
||||
for i, observer in pairs(bk.observers) do
|
||||
unbindLifetime(slot._mountedInst, observer) -- 물리 target에 걸린 배관만 해제
|
||||
end
|
||||
end
|
||||
slot._mounted, slot._mountedInst = false, nil
|
||||
slot.Offset = nil -- 마운트 전 상태로 복원(위 "`Slot.Offset`은 마운트 전엔 nil")
|
||||
-- slot 자신의 unbindLifetime / releaseOwner / owner쪽 setLength·setOffsetSource는
|
||||
-- 호출부 몫 — destroySlotTree와 동일한 층위 분리.
|
||||
end
|
||||
|
||||
local function destroySlotTree(slot)
|
||||
for i, element in ipairs(slot._elements) do
|
||||
-- [정정, 2026-08-13 감사] 소유권 반납을 먼저 — 아래 "소유권 반납은
|
||||
|
|
@ -1696,11 +1736,28 @@ Slot이 마운트될 때 **자기 하위 요소들까지 `bindLifetime`으로
|
|||
|
||||
#### 구현상 바뀌어야 하는 것
|
||||
|
||||
`reconcile`이 교체/소멸 시 `rawRemove`(파괴) 대신 **비파괴 경로**를 타야
|
||||
하고, 그 경로는 `destroySlotTree`가 지금 하는 일 중 **파괴만 빼고 나머지는
|
||||
그대로 해야 함**(자식 observer `unbindLifetime`, 옛 owner에 등록해둔
|
||||
`Dispatch.setLength`/`setOffsetSource` 해제, `_mounted`/`_mountedInst`
|
||||
복원, `releaseOwner`).
|
||||
**[반영 완료, 2026-08-13 감사 후속]** 비파괴 경로를 `unmountSlotTree`로
|
||||
신설하고(아래 "파괴" 절), 이걸 쓰는 자리를 둘로 못박음:
|
||||
|
||||
1. **`SlotHandler.process`가 반환하는 클로저**(최상위 dispatch 경로) —
|
||||
**이 결정이 적용돼야 하는 바로 그 자리인데 처음엔 빠뜨려서
|
||||
`destroySlotTree`를 계속 부르고 있었음**(다른 에이전트 리뷰가 지적,
|
||||
그대로 뒀으면 언마운트 결정 자체가 무의미해질 뻔함). 지금은 위 절의
|
||||
코드가 `unmountSlotTree` + `setOffsetSource(None)`/`setLength(0)` +
|
||||
`unbindLifetime` + `releaseOwner`를 부름.
|
||||
2. **`:List`의 `reconcile`** — 교체/소멸 시 `rawRemove`(파괴) 대신 같은
|
||||
비파괴 경로. 데이터에서 빠진 아이템도 파괴되지 않고 언마운트만 되며,
|
||||
아무도 안 들고 있으면 GC(quad 전역의 GC-native 원칙 그대로).
|
||||
|
||||
**여전히 파괴인 것**: 명시적 CRUD `Slot:Remove(index)`/`Slot:Clear()`
|
||||
(CRUD 표가 "제거 **+ 파괴**"로 이미 정의)와 `dispose`. 즉 **"자동 경로는
|
||||
언마운트, 명시적으로 지우라고 한 것만 파괴"**로 갈림 — `Ref`/`Attribute`의
|
||||
"지울 거면 명시적으로" 철학과 정확히 같은 결.
|
||||
|
||||
`unmountSlotTree`는 `destroySlotTree`가 하는 일 중 **실제 파괴와 자식
|
||||
소유권 반납만 빼고 나머지는 그대로 함**(자식 observer `unbindLifetime`,
|
||||
`_mounted`/`_mountedInst` 복원, `slot.Offset = nil`). 옛 owner에 등록해둔
|
||||
`Dispatch.setLength`/`setOffsetSource` 해제는 호출부 몫(아래).
|
||||
|
||||
**[정정, 사용자 지적] "해제 짝"이라는 새 API는 필요 없음** — 옛 owner에
|
||||
대해 그냥 **`Dispatch.setOffsetSource(ownerKey, position, None)` +
|
||||
|
|
|
|||
|
|
@ -45,6 +45,20 @@ API 전부에 걸리며, 2026-08-07 일곱 번째 세션(커링 스타일 확정
|
|||
|
||||
**M0 착수 전에 정하는 게 맞음** — 2번을 고르면 base 문서 다수가 영향받음.
|
||||
|
||||
**[사용자 방향, 2026-08-13]** 순환 타입을 만드는 것보다 **`State` 타입
|
||||
자체를 구울 때(코드 생성 시점) 인라이닝**해주는 쪽이 맞아 보인다는 의견 —
|
||||
즉 `State<T>`가 자기 자신을 재귀 참조하는 선언을 피하고, 타입 생성기가
|
||||
각 `T`에 대해 평탄한 타입을 뽑아주는 방향(`Modifier`의 클래스별 flat 타입을
|
||||
생성기로 뽑는 이미 확정된 패턴과 같은 결). 이러면 `08`에서 걸린
|
||||
`Recursive type being used with different parameters` 제약도 같이 비켜감.
|
||||
**다만 이건 사람이 직접 확인해야 할 부분으로 사용자가 보류** — 0-Z(Attribute)와
|
||||
함께 사용자가 직접 스케치하며 판단할 목록.
|
||||
|
||||
**관련 실측 근거**: `08`(재귀 타입 제약), `15`(read-only/read-write `Get`
|
||||
불일치 — 파싱 실패로 검증불가 상태라 재작성 필요), `14`(Ref 생성자 오버로드가
|
||||
정상 nilable 사용례까지 막음), `16`(`type function` API 불일치). 전부
|
||||
`audit/luau-test-first-run-2026-08-13.md`.
|
||||
|
||||
### 0-Z. ⭐ **최우선 — Attribute 이름 소유권을 무엇으로 판정할 것인가** (2026-08-13 여섯 번째 세션, 사용자가 다음 세션 심층 분석으로 이관)
|
||||
|
||||
**이게 지금 유일하게 `base/` 반영을 막고 있는 결정.** 아래 0-A의
|
||||
|
|
@ -157,9 +171,13 @@ Slot이 됨. 막는 게 소유권 규칙이 아니라 "제거 = 파괴"라는 re
|
|||
판단이라, 포탈은 별도 기능이 아니라 그 결정의 귀결. 안 지운 Slot은
|
||||
아무도 안 들고 있으면 GC되고, 지금 죽이려면 `dispose`(위 0-B).
|
||||
**[해소, 사용자 지적] "해제 짝"이라는 새 API는 필요 없음** — 옛 owner에
|
||||
대해 그냥 `setLength(ownerKey, position, 0)` + `setOffsetSource(ownerKey,
|
||||
position, None)`을 다시 부르면 됨(이미 확정된 "마운트 안 하는 위치는
|
||||
`0`/`None` 등록" 관용구 그대로). 즉 **해제 = 0/`None`으로 재등록**.
|
||||
대해 그냥 `setOffsetSource(ownerKey, position, None)` **다음에**
|
||||
`setLength(ownerKey, position, 0)`을 부르면 됨(이미 확정된 "마운트 안 하는
|
||||
위치는 `0`/`None` 등록" 관용구 그대로). 즉 **해제 = `None`/0으로 재등록**.
|
||||
**순서가 중요** — `setLength`가 끝에서 `recompute`를 돌리므로 먼저 부르면
|
||||
아직 남아있는 옛 `Source`(죽는 중인 서브트리의 것)에 헛된 `:Set()`이 날아감
|
||||
(`base/slot-plan.md`의 ⚠️ 절). 이 줄이 한때 순서를 거꾸로 적어놔서 같은 날
|
||||
리뷰에서 지적됨.
|
||||
`state<state<Frame>>`류로 offset이 밀리는 문제는 `state<state<Tag>>`와
|
||||
같은 범주로 **"그냥 확인된 것"으로 수용**(평탄화 도구가 처리, 케이스 드묾).
|
||||
상세는 `base/slot-plan.md` "`State<Slot>` 교체는 파괴가 아니라 언마운트" 절.
|
||||
|
|
|
|||
81
CLAUDE.md
81
CLAUDE.md
|
|
@ -121,6 +121,9 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션
|
|||
`Effect`/`Observer`/`Animate`/`Operator` 등 같은 계약을 공유하는 API
|
||||
전부에 걸림 — 선택지는 (1) 계약 유지 + 파라미터 주석 필수, (2) raw 값
|
||||
전환(**구조 변경 규모 큼**), (3) 혼합. **M0 착수 전에 정할 것.**
|
||||
사용자 방향: 순환 타입을 만들기보다 **`State` 타입을 구울 때 인라이닝**
|
||||
하는 쪽(생성기가 `T`별 평탄 타입을 뽑는, `Modifier` flat 타입과 같은 결)
|
||||
— 다만 사람이 직접 확인할 부분이라 0-Z와 함께 사용자 판단 대기.
|
||||
|
||||
**0-Z: Attribute 이름 소유권 결정.**
|
||||
2026-08-13 여섯 번째 세션에 `Dispatch` 재디스패치 모델이 "하강 diff"로
|
||||
|
|
@ -885,66 +888,18 @@ handoff용 저장이 불필요해짐 — `Relate`는 여러 위치/사이클을
|
|||
`modifier-plan.md` 전부 반영, `archive/checkpoint-handler-pattern-reversed.md`
|
||||
신설.
|
||||
|
||||
**2026-08-13 여섯 번째 세션 — c33ae04 커밋 전체 감사(버그 4건), Slot
|
||||
언마운트 전환, 재디스패치 모델 재설계**
|
||||
(`session/2026-08-13-06-commit-audit-dispatch-redesign-bugs.md`)
|
||||
사용자 지시로 직전 커밋을 메인 컨텍스트에서 직접 정독 — 인덱스 재설계
|
||||
**방향은 옳지만 새로 쓴 의사코드가 손 트레이싱을 안 거친 채 커밋**됐음이
|
||||
드러나 실제 버그 4건 발견·수정: (1) `Dispatch.process`의 `chains:SetStrong`이
|
||||
`h.process` 뒤에 있어 최초 마운트에서 하위 retractor가 통째로 유실,
|
||||
(2) Attribute 그룹이 `process`에서 `retractFrom`을 선행 호출해 점유
|
||||
체크(=소유권 충돌 감지)가 전혀 작동 안 함, (3) `SlotHandler`가 claim
|
||||
실패에도 파괴적 클로저를 반환해 `Frame{slot,slot}`에서 이중 파괴,
|
||||
(4) `Ref` retractor가 spurious 재발행에서도 `relate`를 지워 dedup 무력화.
|
||||
Slot 소유권은 nested=엄격 `claimOwner`/top-level=`claimOwnerAt(inst,k)`로
|
||||
분리하고 `rawRemove`의 `releaseOwner` 누락·`destroySlotTree`의 GC 타이밍
|
||||
의존 오류도 수정. `ROADMAP.md`가 2026-08-08 이전 모델로 남아있던 것, base
|
||||
내 "3종 vs 4종 계약" 모순, `luau-test/04`가 없어진 가드를 검증하던 것도
|
||||
정리(`04`는 인덱스 모델 + 버그 (1) 재현 음성 대조군으로 재작성).
|
||||
재발 방지로 `bind-system-plan.md`에 "Handler 작성 체크리스트"(7항목),
|
||||
`relate-plan.md`에 "언제 `Relate`를 쓰고 언제 쓰면 안 되는가" 신설.
|
||||
|
||||
**이어진 사용자 설계 결정(같은 세션, 최종 상태만)**:
|
||||
- **`State<Slot>` 교체 = 파괴가 아니라 언마운트**(`state<Frame>`와 동일,
|
||||
비파괴 추출은 `Splice`로 이미 지원) — **포탈이 별도 기능이 아니라 이
|
||||
결정의 귀결이 됨**. 해제는 `setOffsetSource(None)` → `setLength(0)`
|
||||
**순서 고정**(반대면 죽는 중인 서브트리의 offset `Source`에 헛된 `:Set()`이
|
||||
날아감) + `recompute`의 `nil` 관대 처리/`slot.Offset = nil` 방어.
|
||||
별도 unregister API는 불필요. `base/slot-plan.md`에 **확정 반영 완료**.
|
||||
- **`dispose(value)` 신설** — "트리가 아직 살아있길 요구하면 파괴를 거부하고
|
||||
error"(엔진은 조용히 넘어가지만 quad 자료구조가 깨지므로). 시그니처/범위는
|
||||
`question.md` 0-B.
|
||||
- **재디스패치를 "하강 diff"로 재설계** — 래핑 핸들러의 `retractFrom` 선행
|
||||
호출을 폐기하고 `Dispatch.process`가 **핸들러를 먼저 비교**(같으면 그 자리
|
||||
클로저에 새 값 넘기고 자기 process 재호출, 다르면 아래 전량 철거). 계기는
|
||||
힌트가 `None`/래퍼로 오염돼 깜빡임 방지가 조용히 꺼지는 결함(사용자 제기).
|
||||
이 모델이면 **힌트 타입이 구조적으로 보장되고 깊은 체인 힌트 유실까지
|
||||
사라짐**. 내가 낸 반론("StoreBind 구독 갈아타기")과 보완안(`oldValue` 전달)은
|
||||
둘 다 사용자 지적으로 철회 — 클로저가 이미 old를 캡처하고 있음.
|
||||
**아직 `research/dispatch-redispatch-diff-plan.md`에만 있음, base 미반영.**
|
||||
- **평탄화**(`state:Flatten()`)는 백로그 상세화만 — 반환 노드의 **동적
|
||||
의존성**이 최대 쟁점(`research/operator-sugar-plan.md`).
|
||||
- **남은 결정 하나: Attribute 이름 소유권** — 새 모델에선 양쪽 다
|
||||
`StoreBind`라 "같은 핸들러"로 판정돼 조용히 갈아탐. 사용자가 "이전
|
||||
결정(claimant `Relate`)을 다시 가져오는 게 맞아 보이나 다음 세션에 직접
|
||||
스케치하며 심층 분석"으로 이관 — `question.md` **0-Z(최우선)**.
|
||||
|
||||
**이어진 첫 실측 라운드(같은 세션)** — `luau`/`luau-analyze` 바이너리가
|
||||
사용 가능해져 **스파이크를 만들기 시작한 2026-08-09 이래 처음으로 실제로
|
||||
돌림**(CLAUDE.md가 "M0 착수 전 남은 유일한 게이트"로 꼽아온 항목). 결과
|
||||
전문은 `audit/luau-test-first-run-2026-08-13.md`.
|
||||
- **런타임 12개 전원 통과**(01~07/11/17~20). **`04`가 이번 세션 감사의
|
||||
버그를 음성 대조군으로 재현** — 체인 깊이가 3 대신 1로 무너지고 죽은
|
||||
store가 나중에 UI를 덮어쓰는 것까지 실측돼 감사→수정 사이클이 닫힘.
|
||||
**`07`은 보강해야 실제 검증이 됐음**(연쇄 GC 미검증 상태였고, 파일이
|
||||
스스로 적어둔 "weak 엔트리를 셀 방법 없음" 전제가 틀렸음) — GC-native
|
||||
아키텍처의 핵심 전제가 이걸로 실측 확정. **`18`이 `Relate` 상호 순환
|
||||
경고를 실증**(추측이 아니라 실제로 GC 안 됨).
|
||||
- **타입 쪽에서 진짜 이슈 하나 발견 → `question.md` 0-Y 신설**(위 참고).
|
||||
- `modifier-plan.md`의 "데이터를 테이블에 직접 두고"가 두 갈래로 읽히는
|
||||
**문서 결함**이 드러남 — self 최상위 리터럴 키에 두면 `__index`가
|
||||
`rawget` 성공 시 안 불려 같은 필드 재호출이 죽음(문서 3·4절 대표 용례가
|
||||
바로 그 패턴). 경고 문단 추가 + `17` 재작성으로 해소.
|
||||
- 스파이크 결함 수정: `17`(크래시), `11`(엉뚱한 이유로 통과), `19` B/C
|
||||
(폐기 설계 검증 → 현행 설계로 재작성, 음성 대조군 포함), `07`(보강).
|
||||
남은 것은 `13`/`15`/`16`의 스파이크 격리·API 재확인(설계 문제 아님).
|
||||
**2026-08-13 여섯 번째 세션 — c33ae04 감사(버그 4건), Slot 언마운트 전환,
|
||||
재디스패치 재설계안, 첫 실측 라운드**
|
||||
(`session/2026-08-13-06-commit-audit-dispatch-redesign-bugs.md`,
|
||||
실측 결과는 `audit/luau-test-first-run-2026-08-13.md`)
|
||||
직전 커밋을 직접 정독해 인덱스 재설계 의사코드의 실제 버그 4건 발견·수정
|
||||
(`chains:SetStrong` 순서로 하위 retractor 유실, Attribute 그룹의 점유 체크
|
||||
무력화, `SlotHandler`의 claim 실패 시 이중 파괴, `Ref` dedup 무력화).
|
||||
이어 사용자 결정으로 **`State<Slot>` 교체를 파괴→언마운트로 전환**(포탈이
|
||||
그 귀결이 됨), **`dispose(value)`**(트리가 요구하면 파괴 거부·error) 신설,
|
||||
**재디스패치를 "하강 diff"로 재설계**(`research/dispatch-redispatch-diff-plan.md`,
|
||||
**base 미반영 — `question.md` 0-Z 하나 남음**). `ROADMAP.md`/base 계약 개수
|
||||
모순/`luau-test` stale도 정리. 마지막으로 `luau` 바이너리가 생겨 **첫 실측**
|
||||
— 런타임 12개 전원 통과, `04`가 위 버그를 음성 대조군으로 재현, `07` 보강으로
|
||||
GC-native 전제 확정, `18`이 `Relate` 순환 경고 실증. 타입에선 `:Compute(fn)`
|
||||
lazy 핸들 계약이 Luau 추론과 충돌하는 게 드러나 `question.md` **0-Y** 신설.
|
||||
|
|
|
|||
32
ROADMAP.md
32
ROADMAP.md
|
|
@ -252,6 +252,38 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
|
||||
## M6 — Slot
|
||||
|
||||
- [ ] **[2026-08-13 여섯 번째 세션 — 이 세션의 Slot 결정 전부, 구현 전 필독]**
|
||||
- **`State<Slot>` 교체 = 파괴가 아니라 언마운트**(`state<Frame>`와 동일).
|
||||
비파괴 경로 `unmountSlotTree`를 `destroySlotTree`와 별도로 구현 —
|
||||
차이는 딱 둘: 실제 `Destroy()`를 안 하고, 자식 `releaseOwner`도 안 함
|
||||
(자식은 계속 그 slot 소유라 통째로 재마운트 가능 = 포탈).
|
||||
**쓰는 자리 둘**: `SlotHandler.process`가 반환하는 클로저, `:List`의
|
||||
`reconcile`. **여전히 파괴인 것**: 명시적 `Remove`/`Clear`/`dispose`.
|
||||
- **해제 시 owner 등록 되돌리는 순서 고정** —
|
||||
`setOffsetSource(inst,k,None)` **먼저**, `setLength(inst,k,0)` **나중**.
|
||||
반대로 하면 `setLength` 안의 `recompute`가 죽는 중인 서브트리의 offset
|
||||
`Source`에 헛된 `:Set()`을 날림. `recompute`는 `sourceList[i]`가 `nil`
|
||||
이어도 `None`처럼 skip(방어), 해제 시 `slot.Offset = nil`.
|
||||
- **소유권 판정을 둘로 분리** — nested(`rawAdd`)는 엄격 `claimOwner`
|
||||
(같은 owner 재클레임도 error, `Slot{a,a}` 차단, 반환값 없음),
|
||||
top-level은 `claimOwnerAt(element, inst, k)`(정확히 같은 `(inst,k)`의
|
||||
spurious 재발행만 `false`, `Frame{slot,slot}`은 error).
|
||||
`releaseOwner`는 불일치 시 즉시 error.
|
||||
- **`rawRemove`가 `releaseOwner`를 부를 것**(옛 의사코드에서 누락돼 있었음),
|
||||
**`destroySlotTree`가 자식 소유권 반납 + `_mounted`/`_mountedInst` 복원**
|
||||
(GC에 맡기면 재사용이 GC 타이밍 의존으로 비결정적 실패).
|
||||
- **`SlotHandler.process`는 claim 실패 시에도 파괴적 클로저를 반환해야 함**
|
||||
— no-op을 반환하면 다음 진짜 교체 때 정리 주체가 사라짐(`retractFrom`은
|
||||
클로저가 early-return해도 체인에서 항상 소비하므로).
|
||||
- 전부 `base/slot-plan.md`에 반영돼 있고, `luau-test/19` C 섹션이
|
||||
소유권 분기를 음성 대조군까지 포함해 실측 검증함.
|
||||
- [ ] **`dispose(value)`** — 대상이 아직 어느 트리에 의해 살아있길 요구되면
|
||||
**파괴를 거부하고 즉시 error**(떼어내주지 않음 — 떼는 건 `Set`=언마운트의
|
||||
몫). 엔진은 `Destroy`/`Clear`에 에러를 안 내지만 quad 자료구조가 깨지므로,
|
||||
quad가 관리 중인 값을 안전하게 지우는 유일한 경로. 마운트 위치는
|
||||
`elementOwner`가 이미 알고 있어 새 부기 불필요. 시그니처/대상 범위는
|
||||
`.claude/question.md` 0-B에서 미확정
|
||||
|
||||
- [x] **"여러 Slot이 형제로 섞일 때 순서 보장" 해소**(2026-08-09 여섯 번째
|
||||
세션) — `Dispatch.setLength`/`setOffsetSource` 메커니즘, `base/
|
||||
bind-system-plan.md` "Length/Offset" 절. `Slot.Length: State<number>`도
|
||||
|
|
|
|||
Loading…
Reference in a new issue