quad/.claude/qa-request/pre-implementation-handtrace-round9-followup.md
qwreey 7f5868302e
qa: 9라운드 Q4~Q10 + H-138/H-139/H-142 확정·반영, 감사 8라운드, code-review 반영 — H-143~H-146 판단 대기
Q4 EffectHandle 네 진입점 의사코드(Observer 것 재사용, Unsubscribe만 게이트 통과
뒤 cleanup) / Q5 M2 공통 기반에 Ref 최소형(+ Quad.Ref 필드) / Q6 WeakUnsubscribe
관대 명문화 / Q7 폐기 블록 archive/effect-internal-observer-cascade-reversed.md /
Q8 InstanceChildHandler 부기(Parent → setLength(1), retractor는 Parent=nil →
해제) / Q9 문항 전제 정정 — Tween 절 스케치 hint==nil 줄은 복사 오류, retractor는
function() end / Q10 :List reconcile 재실행도 배치 Blocker(ownsGate — 네스팅 불가) /
H-138 숏핸드 우선순위 > PropertyHandler, 충돌 방지는 UI 접두어 / H-139
New(name)(props)·Dispatch.drive 파이프라인 의사코드(bind-system-plan) — 쓰면서
빈 배열 파트 가드와 H-142 발견 / H-142 props에 Parent 금지(부모가 하는 일) —
순서 문제 소멸, D·Modifier 타입 제외 + PropertyHandler 거부(배선은 에이전트 선택으로
갈라 적음). H-129/H-131 정정.

감사 8라운드(5→3→4→5→3→1→3→1) — 1라운드가 drive 의사코드의 H-17 위반(post-pass
포함 전체 감쌈)을 잡음, 이후는 기록 문서 표기. /code-review high 10건 — 여섯
반영(그중 셋이 이 세션의 H-134 반영이 만든 것), 넷은 새 메커니즘이라 문항
H-143~H-146으로(question.md 최우선 절). doc-check ERROR 0.

Claude-Session: https://claude.ai/code/session_01546hjsYNLSMZdHdPyTZaGb
Co-authored-by: qwreey <me@qwreey.moe>
2026-08-27 15:52:48 +09:00

646 lines
48 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 9라운드 손 트레이싱 발견 — **사용자 결정과 반영 결과**
**무엇인가**: `.claude/qa-request/pre-implementation-handtrace-round9.md`의 발견
(`H-124`~)을 사용자와 대화형으로 처리한 결과. **결정의 소스는 이 문서**이고,
발견 원문·값 트레이스·실측 전사·"이상 없다고 확인한 것" 목록은 그 파일이
소스다(여기서 다시 서술하지 않음).
**진행 방식**: 그 문서 §4가 배치 회신용으로 묶어둔 **결정 문항 Q1~Q10** 순서를
따른다. 8라운드와 같다.
**✅ [2026-08-27] Q1~Q10·`H-138`·`H-139`·`H-142`까지 전량 처리·반영됐다.** 아래
표가 항목별 상태의 소스다. (경위: 처음엔 "문항을 다 처리한 뒤 일괄 반영"으로
잡았으나, Q1~Q3가 같은 `slot-plan.md` 구간에 몰려 있고 서로 얽혀(Q2의 생성자
이동이 Q3의 `_elemIndex` 삭제와 같은 줄) 사용자 지시로 Q3까지 먼저 반영하고
체크포인트 커밋했고, Q4 이후는 같은 날 이어진 세션이 결정 뒤 반영했다.)
| 문항 | 발견 | 상태 |
|---|---|---|
| Q1 | `H-124` `recompute` 루프 순서 | ✅ **확정** — (a), `continue` 형태 |
| Q2 | `H-125` 재마운트 캐시 | ✅ **확정** — 생성자 이동 + (c) 순서 + `_destroyed` |
| Q3 | `H-126` splice 빈자리 (+ `H-137` 소멸, `H-141` 신설) | ✅ **확정**`element → index``bk`가 소유, 토큰 폐기 |
| Q4 | `H-127` `EffectHandle:Unsubscribe` 순서 | ✅ **확정·반영** — (a) 의사코드, Observer 게이트 먼저 |
| Q5 | `H-128` M2×M8 `Ref` | ✅ **확정·반영** — (a) M2 공통 기반에 `Ref` 최소형 |
| Q6 | `H-133` `WeakUnsubscribe` 비대칭 | ✅ **확정·반영** — (a) 의도된 관대함 |
| Q7 | `H-130` 폐기 블록 편집 | ✅ **확정·반영** — (b) `archive/`로 이전 |
| Q8 | `H-134` `InstanceChildHandler` 부기 | ✅ **확정·반영** — (a) |
| Q9 | `H-135` 숏핸드 retractor | ✅ **확정·반영** — 문항 전제 정정, Tween 절 스케치 한 줄이 복사 오류(🟡→🟢) |
| Q10 | `H-136` reconcile 배치 Blocker | ✅ **확정·반영** — (a) |
| — | `H-129`/`H-131` | ✅ 정정 반영(판단 불필요) |
| — | `H-138` 숏핸드 우선순위 | ✅ **확정·반영** — 숏핸드가 높다, 충돌 방지는 `UI` 접두어 |
| — | `H-139` `New`/`D` 파이프라인 | ✅ **의사코드 신설** — 쓰면서 `H-142` 발견 |
| — | `H-132`/`H-137`/`H-140` | ✅ Q1~Q3 처리 때 닫힘 |
| — | `H-142` 해시 파트 `Parent` 순서 | ✅ **확정·반영** — props에 `Parent` 금지(순서 문제 소멸) |
| — | `H-143`~`H-146` (`/code-review high` 발견 중 새 메커니즘 넷) | ⏳ **판단 대기**`fn``Unsubscribe` 고아 cleanup / 재`Subscribe` 재설치 / `indexOfElement` 잔존 / 루트 마운트 경로 |
**[2026-08-27] Q1~Q3는 `base/`·`ROADMAP.md`에 반영했다** — 반영 중 드러난 것과
열어둔 확인은 아래 "반영 기록" 절. **같은 날 이어서 Q4~Q8·Q10·`H-138`·`H-139`를
확정·반영했다**(아래 Q4 이하 절). **[같은 날 마지막] Q9 확인과 `H-142` 판단도
닫혀 9라운드 발견은 전량 처리됐다** — 그 뒤 감사 8라운드와 `/code-review high`
돌렸고, **리뷰가 낸 새 메커니즘 넷(`H-143`~`H-146`)이 판단 대기**(아래
"`/code-review high`" 절).
**사용자 회신 원문(Q4~Q10 일괄, 2026-08-27)**: *"Q5 까지는 권고에 전부 동의함.
WeakSubscribed 자체가 사라질 수 있는 요소라서, 그 사라지는걸 유저가 정하게 하는
요소라서 에러를 내야할지 말아야할지 애매한 부분이긴 한듯. 다만 weak 에 대한
홀드를 유저가 유지하는게 강제라면 b가 되긴 해야하나, 그럴 이유가 없어서 a가
되는게 맞는듯. 7번은 프로젝트 컨벤션대로, 권고 b적용. 8번도 권고대로. 9는 뭔가
이상한데? nil은 파괴가 맞고 파괴 자체가 트윈을 지움. 메니지드를 지우는 스케치가
왜 트윈에 있는지도 모르겠는 부분..? 트윈은 inst 를 바꾸어 재프로세싱 하는거라
연관이 없는 부분인데. 다시 생각하고 말해볼래? Q10 은 이제 그래도 되어보임.
이전에는 native* 가 없어 모두 dom 식 elem 입출력이 강제가 아니였는데, 이젠
그렇기 때문에 최초 방식으로 recompute 되는것 처럼 처리되어도 되는 지점.
부작용으론, offset 이 먼저 설정되어진 다음 후행 요소들이 밀리거나 당겨진 다음
후행으로 넘어가느냐가 차이가 나는데, 이미 우린 set upto 와 valid upto 가 나뉜
지점이고, 싱크라서 괜찮아보임. 권고대로 진입해도 된다 보는데 어떻게 생각하는지?
H138은 당연히 숏핸드 우선순위가 높음. 안 그러면 프로퍼티 핸들러가 숏핸드 계층을
인지하고 준비한다는 말이 돼. 그리고 프로퍼티에 UI를 붙인 이유도 우연히 겹치는걸
막기 위함임. UI 프리픽스는 로블록스 프로퍼티에 발견되진 않거든. H139 은 실 구현
전에 의사코드를 써보자. 그걸로 인해서 감추어졌던 설계 결함이나 폭탄이 발견된
경우가 많아서, 커지기 전에 확인해볼 필요가 있음. 중요 계층이라서"*
---
## Q1 — `recompute`의 되감기 판정을 `lengthList[i]` 읽기 **앞**으로 (`H-124`, 확정)
**결정: (a).** 되감기 판정이 먼저 오고, `local v = bk.lengthList[i]``sum`
누적은 되감기가 **없을 때만** 돈다. 형태는 `continue`.
**사용자 논거** — 갈래 (a)를 고르면서 코드 모양까지 정정했다:
> *"단순히 sum 을 안 만들어도 되는게, 되감는다면 sum 이 이전걸로 구해져서 새로
> 계산한 sum 자체를 안 씀. v 얻고 sum 계산하는걸 else 아래 두는것도
> 괜찮아보이는듯. 컨티뉴나 else 아래나 둘 다 괜찮은데, 맥락 상 컨티뉴를 두고
> 아래 두는게 좋아보임."*
확정된 모양:
```lua
local abs = Dispatch.getOffsetAt(ownerKey, i)
bk.offsetSetUpTo = i
if offset ~= None and offset:Get() ~= abs then
offset:Set(abs) -- ← 사용자 코드가 돌 수 있는 자리
end
if bk.offsetSetUpTo < i then -- ⭐ 되감기 판정이 **먼저**
i = math.max(bk.offsetSetUpTo, 1)
sum = prefix[i]
continue
end
local v = bk.lengthList[i] -- 되감지 않을 때만 읽는다
sum += (if isState(v) then v:Get() else v)
i += 1
```
- **무엇이 깨져 있었나**: 옛 순서는 `lengthList[i]` 읽기·누적이 되감기 판정보다
**한 줄 앞**이었다. `offset:Set(abs)` 안의 사용자 코드가 요소를 제거해
`i > bk.N`이 되면(커서가 마지막 자리일 때 아무 자리나 제거, 또는 `rawSplice`/
`rawClear`의 다중 제거) `sum += nil`로 죽고, 그 `error`
`recomputeBlocker:On()``OffWithoutEmit()` 사이에서 나므로 **그 owner의
차단기가 영원히 켜진 채 남는다**(그 Slot의 레이아웃이 영구 동결 — `H-87`
"독 든 Blocker"와 같은 부류).
- **`H-113`의 *"`sum`은 안 낡는다"* 논증은 유지된다** — 그 논증의 근거는
"`lengthList[i]` 읽기가 `Set` 뒤"인데, 되감는 경우엔 그 자리를 **재방문**하며
읽으므로 여전히 `Set` 뒤다. 오히려 옛 요소의 길이를 한 번 더 더했다가 버리는
낭비가 사라진다.
- **제거가 커서보다 뒤(`p > i`)면 되감기 자체가 안 걸린다** —
`math.min(offsetSetUpTo, p-1)`에서 `p-1 ≥ i`이므로 판정이 거짓이고,
그때는 `bk.N ≥ p-1 ≥ i``lengthList[i]`가 살아 있다. 즉 이 재배치가 정상
경로를 안 바꾼다.
- **실측**(9라운드 참조 구현 `ref9/`): 이 처방 전엔 `d10_remove_at_tail.luau`
`attempt to perform arithmetic (add) on number and nil`로 죽었고, 처방 뒤
**정상 종료 + 최종값 정확**(`s2.Offset 0 / s3.Offset 1 / O.Length 2`).
`d11_two_ops`(2-연산 경로) · `d13_splice_at_cursor`(커서 자리 splice) ·
`d14_baseline`(형제 offset 전파) 전부 회귀 없음.
**반영 대상**: `base/dispatch-core-plan.md``recompute` 의사코드(+ 되감기 절의
근거 문장 — "1로 클램프한다" 주석은 그대로 유효하다).
---
## Q2 — 재마운트: **`Offset`/`_baseObserver`를 생성자로 올리고**, 순서는 (c), 파괴는 `_destroyed` (`H-125`, 확정)
**결정: 문항의 (a)/(b)/(c) 중 (c)를 고르되, 사용자가 그보다 나은 형태를
제시해 그쪽으로 확정했다** — `slot.Offset``slot._baseObserver`를 **Slot
생성자에서** 만든다. 그러면 (c)가 고치려던 순서 문제의 **분기 자체**가 없어진다.
### 왜 (a)가 아닌가 — 그리고 문항의 근거 하나는 틀렸다
- 사용자가 (a)를 반대한 근거는 *"`:List`인데 어디선가 이미 그려진 적 있다면 …
offset 설정 결과가 전파될 방법이 없음. layoutorder 등을 설정하는 유저 함수
부분에 문제가 있을것"*이었는데, **그 부분은 실측에서 성립하지 않았다.**
`setOffsetSource``S.Offset:Set(newBase)`는 전파 루프를 정상적으로 돌고,
유저 체인의 말단 Observer는 **요소 자신의 인스턴스**에 `bindLifetime`돼 있어
(언마운트는 요소를 파괴하지 않고 `unbindLifetime`도 안 한다) `canExecute`
참이다. 건너뛰어지는 건 `_baseObserver` 하나뿐이다.
→ **그래서 `H-125`의 피해 범위는 "자식 서브트리 전체"가 아니라 *부기를
경유하는 중첩 Slot의 `Offset`*으로 좁혀진다**(발견 문서에 정정 반영).
- **(a)를 기각한 실제 이유는 소스 이원화**다. *"베이스가 바뀌면 1번부터
무효화"*(`H-3`의 3번)의 코드 경로는 `_baseObserver` 콜백 하나인데, (a)는 같은
규칙을 `materializeSlotTree` 진입부에 한 벌 더 둔다 — 그 절의 존재 이유가
정확히 *"표는 산문으로만 있었고 실제 코드 경로가 하나도 없었다"*를 닫는
것이라, 소스를 둘로 늘리는 건 후퇴다.
### 확정된 것 (넷)
**1. `Slot` 생성자가 `Offset`과 `_baseObserver`를 만든다**`Length`와 같은 자리.
> **사용자**: *"slot 자체를 생성할 때 offset/observer 이 같이 생성되지 말아야할
> 이유가 있음? 우린 stale 한 offset 을 허락하고 기본 생성에 0 이기 때문에, 초기
> 생성에 넣는거로 해줄 순 없는거야?"* / *"baseObserver 자체도, offset 에서
> 나므로, 초기에 생성하니, 같이 생성하면 돼. **단순히 bind/unbind 로 관리해야지
> 그것 자체를 제거/생성 하는건 안 맞아보임.**"*
- **새 결정이 아니라 이미 확정된 것의 반영이다** — `SL-75`/`D-60`이
*"마운트 전엔 `nil`이 아니라 `0`이고, 언마운트해도 `nil`로 되돌리지 않는다"*로
확정했는데 의사코드만 lazy(`slot.Offset or Source(0)`)로 남아 산문과 어긋나
있었다. `Length`와의 대칭도 맞는다(둘 다 공개 필드인데 하나만 생성자에서 나던
비대칭).
- **`H-125`가 살던 분기가 사라진다**: 옛 `materializeSlotTree`는 첫 마운트(생성
→ "등록 즉시 1회"가 **우연히** 두 필드를 0으로)와 재마운트(`bindLifetime`만 →
**바인드는 발화가 아니라서** 안 만듦)로 갈렸고, 그 비대칭이 버그의 집이었다.
이제 갈래가 없다.
**2. `materializeSlotTree`의 순서** — 게이트와 바인드가 emit **위로**:
```lua
slot._physicalTarget = physicalTarget
local blocker = getBlocker(slot)
blocker:On() -- ← emit 위로
bindLifetime(physicalTarget, slot._baseObserver) -- ← emit 위로(생성 분기 없음)
Dispatch.setOffsetSource(ownerKey, position, slot.Offset) -- 여기서 베이스가 바뀐다
```
- `blocker:On()`도 같이 올려야 한다 — 안 그러면 emit이 깨운 콜백이 게이트 없이
`recompute(slot)`를 완주하는데, 그 시점 `bk(slot)`은 **언마운트 전 옛
부기**(`Relate(slot)` 위에 살아남는다)라 옛 `N`·옛 자식 목록으로 돈다.
- **간섭 없음**: 이 Blocker는 그 Slot 자신의 것이고 `setOffsetSource`는 **부모
owner의** blocker를 본다. `getOffsetAt``lengthList`를 직접 읽으므로 게이트와
무관하다(*"Blocker가 막는 건 `recompute`지 부기 등록이 아니다"*).
- **기존 근거 유지**: *"`_baseObserver` 생성이 `blocker:On()` 뒤인 게
중요하다"*(등록 즉시 1회가 자식 등록 전에 `recompute`를 태움)는 그대로다 —
생성이 생성자로 갔고 **바인드도 `blocker:On()` 뒤**다.
- `local offsetSource = slot.Offset or Source(0)``slot.Offset = offsetSource`
두 줄, 그리고 `if slot._baseObserver then … else … end` 분기가 **전부 삭제**된다.
**3. `_baseObserver` 콜백 머리에 미실체화 가드**:
```lua
if slot._physicalTarget == nil then return end
```
- 없으면 생성자의 "등록 즉시 1회"가 **모든 Slot**에 대해 `getBookkeeping`
강제 호출해 `bk` + `recomputeBlocker`(Blocker 객체)를 **eager 생성**한다 — 한
번도 마운트 안 되는 Slot까지. 가드를 넣으면 실측상 `books`에 항목이 안 생긴다.
- 의미도 맞다 — 미실체화 Slot의 베이스 변경엔 할 일이 없다.
**4. 파괴는 `slot._destroyed` 플래그가 말한다 — 핸들은 안 지운다.**
> **사용자**: *"`slot._baseObserver` 가 두 일을 하는건 위험함. 이전에도
> `invalidAfter` 처럼, 두 일을 겸하는걸 만들다가 사고가 난 적 많아."* /
> (이름) *"`dispose` 는 형질이 다른 엔진 요소를 포함할 수 있는 것에 대한 공동
> 소멸자인 네이밍. 자신이 삭제되고 그 여부는 `destroy` 가 맞아보이고, `dispose`
> 는 슈거로써 `destroy` 와 별도의 맥락에서 해석해야해."*
- `destroySlotTree``_baseObserver`/`_listObserver`/`_listActivated`를 `nil`
지우던 것을 **전부 그만둔다 — `unbindLifetime`만 한다.** 그 nil이 들던 근거
(*"안 풀면 `gchold[physicalTarget]`이 observer를 계속 강하게 붙잡는다"*)는
실은 **`unbindLifetime`이 하는 일**이고, nil 대입은 slot → observer 참조
하나를 놓는 것뿐인데 slot 자신이 쓰레기라 그건 공짜다. 사용자가 좀비 slot을
계속 들고 있어도 observer는 unbind 상태라 `canExecute`가 거짓이라 발화하지
않는다.
- **세 필드가 겸하던 뜻을 플래그가 가져간다.** `_listObserver == nil`은 원래
*"`data`가 reactive가 아니다(plain table)"*라는 자기 의미가 있고(그걸 놓쳐서
`activateList` 재마운트 분기에 `bindLifetime(nil)` 버그가 났었다),
`_listActivated == nil`은 *"아직 최초 population을 안 했다"*였다. 거기에
"파괴됨"을 겹쳐 싣던 게 `invalidAfter`와 같은 모양이었다.
- **`_baseObserver`의 불변식이 한 문장이 된다**: 생성자에서 나서 Slot과 함께
죽는다, **절대 `nil`이 아니다.** `Length`/`Offset`과 같은 층위.
- **파괴된 Slot의 재사용은 error**(`level 2`, 메시지는 영어 —
`base/architecture.md`의 error 계약). 지금까지 **어디에도 안 적혀 있던
자리**다 — `slot-plan.md`는 *"언마운트된 Slot은 그대로 재사용 가능"*이라고만
하고 파괴 쪽은 침묵했으며, `destroySlotTree``_mounted``false`
되돌려놔서 *"마운트된 Slot의 재마운트는 즉시 throw"* 가드에도 안 걸렸다.
실제로 `destroySlotTree`**`_elements`를 안 비운다**(요소만 `nativeDispose`)
— 파괴된 Slot은 파괴된 Instance를 든 좀비이고, 재마운트하면 죽은 Instance를
다시 `Parent` 대입한다.
- **`attachSlot`/`materializeSlotTree` 진입**에 가드 — 필수.
- **공개 CRUD 진입**(`:Add`/`:Remove`/…)에도 가드 — `_elements`가 안
비워지므로 죽은 Slot에 `Add`하면 **조용히** 좀비 배열이 자란다.
`_crudUsed`/`_listed` assert 옆에 한 줄.
- **이중 `dispose`는 얼리리턴 no-op** — teardown 경로가 겹치는 건 실재하고
GC-native 기조와 맞는다. 막는 것은 **마운트/CRUD**뿐이다.
플래그를 세우는 것도 얼리리턴도 **`destroySlotTree` 쪽**에 두므로 다형
진입점 `dispose(value)`는 그걸 공짜로 물려받고 별도 가드가 필요 없다.
### 실측
9라운드 참조 구현(`ref9/`)에 `none`/`(a)`/`(c)`/`ctor` 네 변형을 넣고 같은
매트릭스를 돌렸다 — `O = { a, b, S }`, `S = { C(중첩 Slot), plain }`(베이스 2)를
마운트 → 언마운트 → 베이스 0인 다른 owner에 재마운트:
```
[none] 재마운트: S.Offset=0 C.Offset=2 (기대 0) userLO=1 (기대 1) ❌
[a] 재마운트: S.Offset=0 C.Offset=0 userLO=1 ✅
[c] 재마운트: S.Offset=0 C.Offset=0 userLO=1 ✅
[ctor] 재마운트: S.Offset=0 C.Offset=0 userLO=1 ✅
(넷 다) 앞에 z 삽입 후: S.Offset=1 C.Offset=1 userLO=2 ✅
```
`ctor` 변형으로 `d14`/`d10`/`d11`/`d13` 회귀 없음. 미실체화 Slot 생성 직후
`books`에 항목이 안 생기는 것(3번 가드), 미실체화 `rawAdd`가 그대로 얼리리턴하는
것도 같이 확인.
### 반영 대상
- `base/slot-plan.md``Slot(initial)` 생성자(필드 목록에 `Offset`/`_baseObserver`
추가) · `materializeSlotTree`(순서 + 분기 삭제 + 콜백 가드) ·
`unmountSlotTree`/`destroySlotTree`(핸들 보존, `_destroyed` 세팅, 이중 호출
얼리리턴) · 공개 CRUD 가드 · *"언마운트된 Slot은 그대로 재사용 가능"* 절에
파괴 쪽 계약 신설 · **`rawAdd``_physicalTarget == nil` 얼리리턴은 유지하되
근거 문장 정정**(*"`slot.Offset`이 아직 nil이라 `getOffsetAt`에서 즉시
죽는다"*는 이제 거짓 — 남는 근거는 중첩 Slot의
`attachSlot(element, nil, …)``bindLifetime(nil, …)` 하나다).
- `base/dispatch-core-plan.md``Slot.Offset` 서술(*"마운트 시점에
`setOffsetSource`가 등록하는 그 Source를 `self.Offset`으로도 저장"*)을 생성자
기준으로.
- `ROADMAP.md` — M6의 `Slot.Offset` 체크박스, 그리고 아래 `H-140`.
### 부수 발견 — `H-140`(🟢, 이 대화에서 나옴)
**`ROADMAP.md:1124`가 아직 *"해제 시 `slot.Offset = nil`"***. `SL-75`/`D-60`이
전면 정정한 문장인데(`slot-plan.md:3593`이 *"옛 서술은 … 그러면 포탈이
무너진다"*로 폐기) **구현자가 실제로 보는 체크박스**에 살아남았다. 그대로 짜면
포탈 구독자가 영구히 끊기고 이번 생성자 불변식도 같이 무너진다 — 9라운드
사냥 목록 #7(*"`ROADMAP.md` 체크박스가 `base/`보다 낡음"*) 그대로다. 발견
문서에 `H-140`으로 등록했고 처분은 정정 하나다(판단 불필요).
---
## Q3 — `element → index`는 `bk`가 소유한다; 토큰 폐기 (`H-126` + `H-137` + `H-141`, 확정)
**결정: 문항의 (a)/(b)를 넘어 원인을 닫았다.** Q3의 표면 증상(`spliceArraysUp`이
비운 자리의 `observers`/`tokens`)을 파다가, **같은 뜻의 맵이 두 층에 있고 그
중 하나(`bk.tokens`/`bk.indexOfToken`)는 사용자가 정한 적 없는 것**임이 드러났다.
### 무엇이 있었나 — `token`의 출처
- 7라운드 `H-102`의 사용자 지시는 *"이미 `slot._elemIndex`: realElem → index 를
관리중"* / *"그것을 dispatch 로 격상시키는게 더 나아보이는 지점"* — **요소 →
인덱스 맵을 Dispatch로 올리라**는 것이었다.
- `base/`에 내려앉을 때 키가 `len`(그 자리의 길이 State)으로 구현됐고, 8라운드
직전 `/code-review high`가 *"`len`은 자리마다 유일하지 않다"*를 잡으면서 **그
자리에서 `token = {}`이 발명됐다**(2026-08-25). 원래 키(요소)로 되돌아가는
대신 새 신원을 만든 것이고, 사용자 인용문은 그 옆에 그대로 남아 승인된
메커니즘처럼 읽혔다.
- **사용자**: *"내가 등장시킨 적 없는 token 이 나와서 당황스러움"* / *"난 층위 상
어떠한 값이든, 마운트된 부기객체 -> index(기여량이 아님) 를 얻고자 했음"*.
"기여량이 아님"이 정확히 어긋난 지점이다 — 구현은 기여량(`len`)을 키로 잡았다.
### 확정된 것
**1. `element → index` 맵은 `bk`가 소유한다 — `bk.indexOfElement`. owner가 Slot이든
`inst`든 규칙 하나.**
> **사용자**: *"slot 이든 inst 든 elem -> index 는 Dispatch bk 에 있어. 클로저가
> 필요한 지점으로 안 보여. 문제의 elem->index 를 누가 관리하느냐가 어디서
> 관리하느냐가 명확하지 않아서 자꾸 사고가 나는듯 한데."*
- **`slot._elemIndex`는 삭제**한다 — 같은 뜻의 맵을 두 층에 두던 것이 사고의
원인. `indexOfRaw(self, element)``getBookkeeping(self).indexOfElement[element]`
조회가 되고, `reindexFrom(self, from)`은 **그 맵을 갱신하는 헬퍼**가 된다
(시프트하는 자리 전부가 부르는 것은 `H-1` 그대로).
- **`bk.tokens`/`bk.indexOfToken`은 삭제** — `token` 개념 소멸.
- **`None`/진짜 `nil` 자리는 기록하지 않는다** — 길이가 상수 `0`이라 지속
등록(Observer)이 없고, `nil`은 애초에 키가 못 된다. Slot 안에서 요소는
유일하다(`_elements`엔 `None`이 안 들어가고 `claimOwner`가 이중 배치를 막는다).
**2. `Dispatch.setLength(ownerKey, i, len, anchor, element)` — 5번째 인자는 그
자리의 `inst|slot`.** `gatedRecompute`는 **요소를 캡처**해 `bk.indexOfElement[element]`
조회한다.
> **사용자**: *"Dispatch.setLength 가 이제 받아야할 것은 inst|slot 이야. 그거 이외
> 클로저로 저걸 해줄 이유가 없어보이는데. 게다가 슬롯 아니면 nil 이 나오는것도
> 이상해."*
- 요소를 캡처하면 **갱신할 것이 없다** — 요소의 신원은 안 변하고, `요소 → index`
`reindexFrom`이 자기 이유로 이미 정확하게 유지한다. 위치를 캡처하던 옛
모양(splice마다 갱신)과 토큰(배열 + 역방향 맵 둘 다 갱신)이 관리하던 것이
전부 사라진다.
- `element`를 생략한 호출(상수 길이 자리 — plain 요소 없이 `Nil`/`None` 핸들러)은
지속 클로저가 안 생기므로 캡처한 `i`가 그대로 유효하다. 실제로 `len`
State인 호출은 **중첩 Slot의 `.Length`뿐**이고 그때 `element`는 그 Slot
자신이다.
**3. Q3 본문 — `spliceArraysUp`이 비운 자리는 세 배열 전부 처리한다.**
`lengthList[index] = 0`(`H-5`) · `sourceList[index] = None` · **`observers[index] = nil`**.
`spliceArraysDown`은 당긴 뒤 꼬리(`N`)의 세 자리를 `nil`로. 근거는 `H-126`
실측 — 복사 루프로 짜 `observers[index]`에 옛 값이 남으면 이어지는
`setLength(self, index, …)``oldObserver` 언바인드가 **밀려난 요소의 관측자를
죽인다**(`vacate=false`에서 `B.Offset`이 2에 멈춤).
### 기각된 것 (전부 이 세션에서 제가 냈다가 철회한 안)
- **`observer.pos` / `observer.inst`** — 프리미티브 인스턴스에 임의 페이로드를
얹는 것으로, **검토 후 안 만들기로 한 `Effect<UD>:Userdata()`/`SetUserdata`를
다시 여는 문**(사용자: *"그건 닫은 Effect 의 userdata 허용을 거의 여는
셈이야"*).
- **`setLength`에 조회 클로저 / `subject` 인자** — 소유 층위를 정하는 대신 우회하는
새 개념. 사용자가 정한 적 없는 이름.
- **토큰 유지(이름만 변경)** — 맵이 두 층에 남는 원인을 그대로 둔다.
### 소멸·신설
- **`H-137` 소멸** — `rawMove`/`rawSwap` 규약의 토큰 누락은 토큰이 없어지며
발견 자체가 사라진다(규약 4가 이동 구간 전부에 `setLength`를 다시 태워
`indexOfElement`를 다시 쓴다).
- **`H-141` 🟡 신설** — *확정의 근거로 인용된 사용자 발언이 실제로는 다른 것을
승인한 것*(토큰 옆의 격상 인용, 그리고 *"splice 요구 목록에 항목이 늘지
않는다"*는 명분을 구현이 스스로 깬 것). 9라운드 사냥 목록 #1의 인용문 판.
### 반영에서 열어둔 확인 (판단 요청)
- **`H-1`이 `reindexFrom``spliceArrays*`에서 분리한 근거가 소멸한다** —
*"`spliceArrays*`는 `bk`를 만지므로 실체화된 뒤에만 부를 수 있다"*였는데,
`getBookkeeping`은 lazy라 절대 `nil`이 아니다. 대신 **미실체화 Slot에 `:Add`
해도 `bk`(+ `recomputeBlocker`)가 생긴다**. Q2의 `_baseObserver` 가드가 막은
것(*모든* Slot의 eager `bk`)과 범위가 다르다(*요소를 넣은* Slot만). 이 정도는
괜찮다고 보고 반영했다 — 아니면 알려줄 것.
- **쓰기 지점이 둘이다** — 등록은 `setLength`(`bk.indexOfElement[element] = i`),
이동은 `reindexFrom`. `lengthList``setLength`(등록)/`spliceArrays*`(이동)로
갈리는 것과 같은 모양이라 그대로 뒀다.
---
## Q4 — `EffectHandle:Unsubscribe()`는 Observer 게이트를 **먼저** 통과한 뒤 cleanup (`H-127`, 확정 (a))
- `effect-plan.md`의 "`EffectHandle:Subscribe()`" 절에 의사코드 한 블록 신설 —
`Subscribe`/`WeakSubscribe`/`WeakUnsubscribe`는 **Observer의 함수를 메소드
테이블에 그대로 배정**(같은 레지스트리·같은 `canBound` 게이트·같은
`.Subscribed`), `Unsubscribe``Observer.Unsubscribe(self)` 통과 뒤
`self:_consumeCleanup()`. `WeakUnsubscribe`는 cleanup을 안 건드린다(약한
구독은 GC에 맡기는 경로라 해제가 종료 신호가 아니다).
- 산문의 번호 목록(플래그 → cleanup → fail-fast)은 **의미 목록이지 실행 순서가
아니라고** 그 자리에 적었다. `lifecycle-pattern.md`의 *"아래가 네 진입점
전량이고 소스다"*는 *"Observer의 네 진입점 … `EffectHandle`은 같은 넷을 그대로
재사용"*으로.
## Q5 — M2 공통 기반에 `Ref` 최소형 체크박스 (`H-128`, 확정 (a))
- `ROADMAP.md` M2 "공통 기반" 절에 체크박스 신설 — `.Value`/`.Revision`/`:Set`/
`:WeakCallback`/`:Callback`/`:Uncallback`/`isRef` + `EpochBrand:register`.
M8 `Ref.luau` 체크박스는 *"최소형은 M2로 앞당겨졌다 — 여기 남는 건 `:Wait`,
`PreRef`/`PostRef`, 디스패치 핸들러"*로. 절 머리의 *"셋 다 State-free"*는
개수 없이 *"여기 있는 것 전부"*로.
## Q6 — `WeakUnsubscribe`의 관대함은 의도된 것 (`H-133`, 확정 (a))
- `lifecycle-pattern.md` (2) 의사코드 주석 + 산문 bullet 신설. **사용자 논거**
(위 원문): 약한 등록의 생존은 사용자가 쥔 참조에 달린 것이고 quad가 그 홀드를
강제하지 않으므로 "없는 항목의 약한 해제"를 오류로 볼 근거가 없다 — 홀드
유지가 강제였다면 (b)여야 했다. 강한 등록은 quad가 살려두는 것이라 "없음"이
곧 호출부 실수라 엄격. leaf 바인딩된 값에 `WeakUnsubscribe`도 같은 이유로
통과(무해).
## Q7 — 폐기 블록을 `archive/`로 (`H-130`, 확정 (b))
- `effect-plan.md`의 ⛔⛔ 배너 아래 블록(`_observers` 배열 + cascade)을
`archive/effect-internal-observer-cascade-reversed.md`로 옮기고 포인터 한
문단만 남겼다. 옮긴 원문은 **`9dd8213`이 배너 아래를 편집한 흔적 그대로**이고
파일 머리에 그 경위를 적었다. 새 규칙은 안 만들었다 — `conventions.md`
핸드오버 체크리스트 3번이 이미 이 실패 모드를 규정한다(사용자: *"프로젝트
컨벤션대로"*).
## Q8 — `InstanceChildHandler`도 부기를 등록한다 (`H-134`, 확정 (a))
- `dispatch-core-plan.md` 말단 표에 행 신설 + `H-39` 블록에 "다섯째" 문단:
`process` 맨 앞에서 `setOffsetSource(inst, k, None)`
**`setLength(inst, k, 1, inst, v)`** → `v.Parent = inst`; 반환 클로저는
`None``0` 순서로 해제. `ROADMAP.md` M5 체크박스에 같은 두 줄. 5번째
인자(요소)는 Q3의 `setLength` 시그니처를 따른 것 — 상수 길이라 지속 클로저는
안 생기지만 등록 모양을 다른 말단과 맞췄다.
## Q9 — 재고: 두 "확정"이 아니라 Tween 절 스케치의 **복사 오류** (`H-135`, 확정)
사용자가 문항의 전제를 정정했다(*"nil은 파괴가 맞고 파괴 자체가 트윈을 지움.
메니지드를 지우는 스케치가 왜 트윈에 있는지도 모르겠는 부분..? 트윈은 inst 를
바꾸어 재프로세싱 하는거라 연관이 없는 부분인데"*). 다시 본 결과:
- `v == nil``process`가 자식을 파괴하는 규칙은 두 절이 **같다**. 이상한 건
Tween 절 스케치의 마지막 줄 `return function(hint) if hint == nil then
destroyManagedChild(inst,k) end end` 하나 — 그 절의 주제는 *자식 프로퍼티
세팅을 `Dispatch.process(child, "CornerRadius", …)`로 되돌려준다*뿐이라
관리 자식의 생사를 정할 자리가 아닌데, `v == nil`(값)과 `hint == nil`
(retractor 인자)을 겹쳐 적은 **복사 오류**로 보인다.
- 제가 (B) 분기에서 *"트윈이 끊긴다"*를 피해로 든 것은 틀렸다 — 자식이
파괴되면 그 위의 트윈이 사라지는 건 당연한 결과지 결함이 아니다. 그 줄을
지워야 하는 이유는 **파괴 경로가 하나여야 한다**(`process(inst,k,nil)`)는 것
하나이고, 이중 파괴는 그 중복의 증상이다.
- 트레이스: `(inst,"UICorner")` 체인은 어떤 값이든 `UICornerHandler.process`
`number|Tween|nil`로 닿고(`StoreBind`가 State를 풀고 `NoneHandler``nil`로),
키 자체가 사라지는 건 인스턴스 teardown뿐이라 그때는 자식이 부모와 같이
죽는다 — retractor가 할 일이 없다.
- **✅ [2026-08-27 확정]** 사용자: *"9 맞음. v == nil 로 신규 들어오면 파괴가 맞고
retract 는 nop 맞아. 파괴 자체가 사실 inst 바꿔서 넣은 process 를 처리할 필요
없게 만들어버리고, 우린 파괴에 대해서 retract 안하던게 맞아서, Tween 과 무관한
것도 맞지."* — `ui-shorthand-plan.md` Tween 절 스케치의 그 줄을
`return function() end`로 고치고 정정 bullet을 달았다. `H-135`는 🟡→🟢.
## Q10 — `reconcile` 재실행도 배치 Blocker로 (`H-136`, 확정 (a))
- `slot-plan.md``reconcile` 머리(중복 키 선행 패스 뒤)에 `getBlocker(self)`
획득, 꼬리에 `OffWithoutEmit()` + `recomputeBlocker` 확인 후 `recompute` 1회.
**Blocker가 네스팅이 안 되므로**(불리언, `blocker-plan.md`) 최초 population
처럼 바깥 배치 안에서 오면 `IsOn()`으로 알아보고 손대지 않는다(`ownsGate` —
이 판정은 사용자가 승인한 (a)의 문구 밖에 있는 **구현 세부**다. 같은
`reconcile` 클로저가 바깥 배치 안(최초 population)과 밖(재실행) 양쪽에서
불리는데 Blocker에 네스팅이 없어서 필요한 것이고, 함수 지역 변수라 표면엔
안 드러난다).
- **사용자 논거**(위 원문): `native*` 계층이 생기기 전엔 DOM식 요소 입출력이
강제가 아니라 raw op마다 `recompute`가 돌아야 했지만, 이제는 물리 위치를
`native*`가 부기와 무관하게 정확히 넣으므로 최초 population처럼 한 번에
따라잡아도 된다. 부작용으로 "offset이 먼저 잡힌 뒤 후행 요소가 밀리느냐,
밀린 뒤 넘어가느냐"가 갈리지만 `offsetSetUpTo`/`offsetCacheValidUpTo`가
분리돼 있고 동기라 괜찮다. `dispatch-core-plan.md`*"한 사이클 … 한 번만"*
문단에 같은 근거를 적었다.
- 제 판단: 동의 — `raw*`의 명시 호출이 이미 `getBlocker(self):IsOn()`을 보므로
(`H-119`) 추가 배선이 없고, `getOffsetAt`/`physIndex`는 Blocker와 무관하게
정확하다(최초 population과 같은 논증). `updateFn`이 던지면 Blocker가 켜진 채
남는 것도 `materializeSlotTree`와 같은 부류다.
## `H-138` — 숏핸드 핸들러가 `PropertyHandler`보다 우선순위가 높다 (확정)
- `ui-shorthand-plan.md` "메커니즘" 절에 문단 신설, "결론" 절의 접두어 확정에
두 번째 근거 추가. **사용자 논거**(위 원문): 리플렉션 거부에 기대면 하위
계층(프로퍼티)이 상위 계층(숏핸드)의 키 집합을 알아야 하는 역방향 의존이
생긴다; 충돌 방지는 우선순위가 아니라 `UI` 접두어의 몫(Roblox 프로퍼티
이름엔 `UI` 접두어가 없다). 구체 상수는 구현 시.
## `H-139` — `New(name)(props)` 파이프라인 의사코드 (신설) + 거기서 나온 것
- `bind-system-plan.md` "인스턴스 생성 / 이벤트 네이밍 인체공학" 절에
`New` ①~④(물리 생성 → gcconn/gchold → `flatten``Dispatch.drive`)와
`drive` (a)~(c)(pre-pass → 단일 일반화 `for` 본체 + 배치 Blocker →
`postRefList`) 의사코드 신설. 단계 안의 규칙은 각 소스 문서가 정본이고 여기는
**순서**만 확정. `ROADMAP.md` M3 `Dispatch.drive` 항목에서 가리킨다.
- 이름 충돌(모듈 팩토리 `New()` vs 생성자 `New "Frame"`)은 산문 표기 규칙
한 줄로 닫았다(패키지가 달라 런타임 충돌은 없다).
- **쓰면서 드러난 것 셋** — 사용자가 예상한 그대로:
1. ~~**배치를 닫는 자리가 어디에도 없었다.**~~ **⚠️ 이 주장은 틀렸다**(감사
1라운드) — `dispatch-core-plan.md``H-17` 절이 이미 *"`drive` 전체
(post-pass 포함)를 감싼다, `PostRef` 콜백은 게이트가 켜진 채 실행된다"*로
정해뒀는데 제가 그 절을 안 보고 "해시 파트 앞에서 닫는" 의사코드를 썼고,
사용자 확인 1번은 **그 틀린 전제 위에서** 받은 것이다. 의사코드를 `H-17`대로
(진입 직후 On, `postRefList` 뒤 Off + `recompute`) 고쳤다 — 확인 1번은
무효, 계약은 원래 것 그대로. 실제로 낡아 있던 건 같은 문서 "해법의 핵심"
4번의 옛 문구(*"배열 파트 순회 전체"*) 하나라 같이 정정.
2. **자식 없는 Instance에도 Blocker/`bk`가 생기는 경로.** `getBlocker(inst)`
무조건 부르면 `Frame { Size = … }`마다 Blocker + `bk` + `Relate` 항목이
eager 생성된다 — Q2/Q3가 Slot 쪽에서 막은 것과 같은 부류.
`flattened[1] ~= nil` 가드로 막았다. 확인 요청.
3. **`H-142` 🟡 신설 — 해시 파트 안의 `Parent` 순서가 미정.** `Frame { Parent
= x, Size = … }`에서 `Parent` 대입이 다른 프로퍼티보다 먼저 올 수 있다
(Luau 해시 순회 순서는 계약이 아니다). 처방 후보(순서 미루기/문서화/무시)가
전부 새 메커니즘이라 정하지 않고 올렸다.
**✅ [2026-08-27 확정 — 순서가 아니라 키 금지]** 사용자: *"Parent대입 자체가
오면 안 돼. 그건 부모에서 할 일이거든. 자신이 바로 하는 경우는 없어. 그걸
허용해준다는것 자체가, '외부에서 직접 Parent 설정해주지 말것' 을 해치는
요인이 되기도 해.(암묵적으고 가능하도록 둬버려서)"* — `slot-plan.md`
"동적 자식은 반드시 `Slot` 또는 `state<Frame>`류 store-bind를 통해서만" 원칙의
정적 리터럴 판. 반영: `bind-system-plan.md` 파이프라인 절에 규칙 + `ROADMAP.md` M5
(`D` 생성기가 props 타입에서 `Parent` 제외 / `PropertyHandler.isHandlable`
`"Parent"` 거부 → 기존 "매치 핸들러 없음 → 즉시 error"에 걸림). **런타임
배선은 제 선택**(새 메커니즘 없이 기존 계약 재사용)이라 문서에 그렇게
갈라 적었다.
- **1·2 확인 완료**(사용자: *"1과 2는 확인했어. 2는 특히 생성 이후 다시 process
를 주는걸 우린 안 하기로 해서 충분히 가능한 일"*). 2에 대해 사용자가 남긴
의심 — *"Frame{} 으로 나온 bk없는게 Slot 안에 멀쩡히 들어가는데 문제
없을까"* — 를 코퍼스의 호출부 전수로 확인했다: **문제 없다.**
- `getBookkeeping`/`getOffsetAt`/`setLength`/`setOffsetSource`의 첫 인자
(`ownerKey`)는 코퍼스 전체에서 **Slot 자신(`self`/`slot`) 또는 `drive`
도는 최상위 `inst`뿐**이다. Slot에 들어간 `Frame{}`은 언제나 **요소**
(`setLength`의 5번째)나 **앵커/물리 target**(4번째, `bindLifetime`·
`nativeInsert`의 첫 인자)으로만 나온다 — 앵커는 gchold(②에서 무조건
생성)를 쓰지 `bk`를 안 본다.
- 그 Frame이 owner가 되는 경우는 자기 props에 배열 파트가 있을 때뿐이고,
그땐 `flattened[1] ~= nil`이라 가드가 안 걸린다. `outerSlot:Add(innerSlot)`
처럼 나중에 중첩 Slot이 그 Frame을 물리 target으로 써도 owner는
`outerSlot`이다.
- "`bk.base`"는 이미 걷어낸 필드다(`dispatch-core-plan.md` 3번, 사용자
지적으로 — 최상위 `inst`의 베이스는 항상 0이라 저장할 게 없다). 설령
예상 못 한 경로가 `getBookkeeping(frame)`을 불러도 **lazy 생성**이라
크래시가 아니라 그때 만들어질 뿐이다.
## 반영 기록 — Q1~Q3 (2026-08-27)
**바뀐 파일**: `base/dispatch-core-plan.md`(`recompute` 루프 재배치 / `setLength`
5번째 인자 + `bk.indexOfElement` / `H-102` 문단 정정 + `H-141` 배너 / 저장 위치·
초기화 열거 / `_baseObserver` 즉시 발화 주석 / `Slot.Offset` 생성 자리 / `anchor`
절에 `element` 축 추가) · `base/slot-plan.md`(`Slot(initial)` 생성자 /
"파괴된 Slot은 재사용 불가" 절 신설 / `Slot:Add`·`Slot:List` 가드 /
`makeBaseObserver` + `materializeSlotTree` 순서 재배치 / `unmountSlotTree` 흔적
가드 제거 / `destroySlotTree` 핸들 보존·`_destroyed`·이중 호출 no-op / `H-1` 블록·
`H-29` 규약 1·`raw*` 주석·`rawAdd` 배너 근거·`rawReplace`·`indexOfRaw` 정의 /
splice 요구 목록 재작성 + `H-102` 항목 폐기 배너 + `H-132` 개수 제거) ·
`ROADMAP.md`(M3 `setLength` 시그니처·`getBookkeeping` 초기화 / M6 필드 목록·
`H-140`·`_elemIndex` 언급·`Slot.Offset` 체크박스) · 발견 문서(`H-141` 등록,
`H-137` 소멸 표기, `H-125` 범위 정정).
**반영 중 드러난 것 — 사용자가 바꾼 적 없는데 있던 것** (판단 요청 포함):
1. **`token` 자체**(`H-141`) — 위 Q3 절.
2. **`unmountSlotTree``if slot._baseObserver then` 가드** — 옛 lazy 생성의
흔적. 생성자 불변식("항상 있다")에 맞춰 무조건 호출로. 동작 차이 없음.
3. **`rawReplace`가 맵을 직접 쓴다** — 옛 `self._elemIndex[old] = nil; [new] = index`
두 줄이 `bk.indexOfElement`로 그대로 옮겨졌다. `setLength(…, newElement)`
등록하지만 **미실체화 분기는 `setLength`를 안 부르므로** 직접 쓰기가 남는다.
쓰기 지점이 `setLength`/`reindexFrom`/`rawReplace` 셋이 됐다 — 규칙("등록은
`setLength`, 이동은 `reindexFrom`")에서 `rawReplace`만 예외. 괜찮은지 확인
요청.
4. **`Owned = false`인 Slot은 `_destroyed`가 안 선다** — `destroySlotTree`
그 분기에서 `unmountSlotTree`로 빠져 꼬리(플래그 세팅)에 안 닿는다. "파괴
대신 언마운트만"이라는 그 분기의 뜻과 정합하고(요소를 만든 적 없으니 좀비도
없다) 재사용도 그대로 가능하다 — 의도대로라고 보고 그대로 뒀다. 확인 요청.
5. **미실체화 `:Add`가 `bk`를 만든다**(Q3 절의 열어둔 확인) — 그대로 반영.
6. `todos.md`의 6라운드 서술(*"`slot._elemIndex` 신설"*)은 역사 기록이라
이동 표기만 덧붙였다.
**[2026-08-27 사용자 확인] 위 1~5 전부 의도대로.** 원문: (1) *"length,
elem->index 를 위치를 재설정 해준다면 괜찮아"* — `rawReplace`의 직접 쓰기는
"자리는 그대로, 주인만 교체"라 `lengthList`·`indexOfElement` 둘 다 그 자리를
다시 쓰는 것으로 성립. (2) *"owned = false 은 state<Frame> -> slot(single) 형태가
구현되는 것이라 맞아"* — 그 Slot은 요소를 만든 적이 없으니 파괴가 아니라
언마운트이고 `_destroyed`가 안 서는 게 맞다. (3) *"미실체화 add 가 bk 만드는것도
맞아. 그래야 인덱싱 매핑을 만드니까"*. (4) *"slot._baseObserver 는 unbind 만 했고,
nil 로 지우는것만 안 한다면 맞아"*.
**아직 안 한 것**(Q1~Q3 체크포인트 시점 문장 — **[2026-08-27 후속] Q4~Q10도
전량 반영됐고**, 그 반영분의 감사 루프는 아래 "감사 루프 (2026-08-27, Q4~Q10
반영분)" 절; 남은 건 라운드 전체에 대한 `/code-review high`와 커밋뿐):
`/code-review high`, 커밋 — Q4~Q10 반영 뒤 한 번에. README
색인과 `todos.md` 00번 갱신은 감사 1라운드 지적으로 그 자리에서 했다. 감사
루프는 Q1~Q3 반영분에 대해 돌았고 6라운드에서 수렴했다(아래 "감사 루프" 절).
## 감사 루프 (2026-08-27, Q1~Q3 반영분)
관례대로 `quad-doc-auditor` 한 턴에 하나, diff 범위, 라운드마다 각도 변경.
| 라운드 | 각도 | 새 발견 | 처분 |
|---|---|---|---|
| 1 | `base/` 정합성 | 확실 1 · 판단 1 | `todos.md` 00번이 *"아직 안 돌렸다"*로 남아 있던 것 갱신 / README 색인(미뤄둔 것) 그 자리에서 추가 |
| 2 | 인덱스 레이어 + 새 문단 자기모순 | 확실 1 · 의심 1 | `dispatch-core-plan.md`의 Length/Offset 두 API 요약 선언(1347행)이 4-인자로 남은 것 갱신 / `_destroyed` 가드가 `:Add`·`:List` 의사코드에만 있어 CRUD 표 머리에 "mutate 연산 전부" 일반 규칙 한 줄 신설 |
| 3 | `archive/`·`reference/`·`luau-test/`·`audit/` 인용처 + 인용문 역방향 + 옛 발견 번호 재인용 | 확실 3 | `audit/handtrace-round7-reference-impl/README.md` 대조표에 9라운드 행 셋 + *"토큰 역참조"* 각주 / `ROADMAP.md` 머리 배너에 9라운드 소스 한 줄 / `README.md` `slot-plan` 색인 행의 `_elemIndex`에 통합 표기 |
| 4 | 앞 라운드 수정분 자체 + followup/발견 문서 내부 정합 | 확실 1 | `CLAUDE.md`·`project-context.md`(둘 다 `@import`)가 결정 소스로 8라운드까지만 나열 — 9라운드 한 줄 추가. 그 외 앞 수정분·두 문서 내부 정합은 줄 단위 대조로 이상 없음 |
| 5 | 4라운드 수정분 + 델타 밖 `base/` 문서 | 확실 1 · 판단 1 | `README.md``dispatch-core`/`slot-plan` 색인 행에 Q1/Q2 요약 추가 / `architecture.md``Slot.luau` 한 줄에 생성자 필드·`_destroyed` 표기(판단 항목 — 8라운드 행들이 같은 밀도라 넣음) |
| 6 | 5라운드 수정분 + 신설 규칙 문단 | **확실 0** · 의심 1 | *"`Owned=false`는 `_destroyed`가 안 선다"*가 README 요약과 followup에만 있고 `slot-plan.md` 산문엔 없던 것 — 사용자 확정 인용과 함께 명문화. **수렴**(확실 0) |
## `/code-review high` (2026-08-27, Q4~Q10 반영분 + 감사 8라운드 뒤)
10건(검증 12 CONFIRMED · 5 PLAUSIBLE · 1 REFUTED). **여섯은 기존 규칙 적용이라
그 자리에서 반영**, **넷은 처방이 새 메커니즘·표면이라 문항으로**(`-round9.md`의
`H-143`~`H-146`, `question.md` 최우선 절).
반영한 여섯:
1. **`InstanceChildHandler` retractor가 옛 자식을 안 내렸다** — `Parent = nil`
추가(파괴 아님 — `slot-plan.md`의 "`State<Slot>` 교체는 파괴가 아니라
언마운트" 절 근거 1이 바로 `state<Frame>`의 이 동작). `state:Set(B)`에서 A가
물리 자식으로 남은 채 `lengthList[k] == 1`이 되던 것.
2. **같은 핸들러의 순서**`setLength``Parent`였던 것을 `Parent`
`setLength`로("일반 계약 — 물리와 부기의 순서" 3번과 같게). 단건 경로에서
`recompute`가 부착 전에 돌았다.
3. **같은 핸들러의 5번째 인자 `v`** — 제가 "모양을 맞추려" 넣은 것인데, 해제
`setLength(…, 0)`이 옛 키를 안 지워 교체마다 `bk.indexOfElement`에 옛 자식이
강참조로 쌓였다. Q3 계약대로 상수 길이는 **생략**. (Slot 쪽 같은 구멍은
`H-145`로.)
4. **`H-17`에 빈 배열 파트 가드가 없었다** — 사용자 확인 2번이 스케치에만 적혀
정본(`dispatch-core-plan.md`)은 여전히 "무조건 On"이었다. 정본과 "해법의
핵심" 1번에 반영.
5. **`quad-types``Quad``Ref` 필드** — `H-128`로 최소형이 M2에 왔는데 그
필드 체크박스는 M8에 남아 `H-25` 공백을 다시 열었다. M2 `H-80` 목록에 `Ref`
추가, M8 항목은 흡수 표기.
6. **Modifier 메소드 목록의 `Parent`** — props 타입에서만 빼면
`Modifier():Parent(x)`가 타입을 통과한다. `bind-system-plan.md` `H-142` 타입
항목 + ROADMAP M7 체크박스.
7. **`effect-plan.md` 번호 목록 재정렬** — "실행 순서가 아니다" 괄호만 달고 옛
순서를 남겨둔 것이 `H-130`과 같은 모양이라 게이트 → 플래그 → cleanup으로
다시 씀.
기각 1(REFUTED): "`drive` 의사코드가 `dispatch-core-plan.md`를 중복한다" — 사용자
요청으로 신설한 블록. 캡에 밀린 하위 발견(`Slot:List` 잉여 블록 잔존 등)은 이미
"잉여" 주석으로 처리된 것과 같은 항목.
## 감사 루프 (2026-08-27, Q4~Q10 반영분)
관례대로 `quad-doc-auditor` 한 턴에 하나, diff 범위, 라운드마다 각도 변경.
| 라운드 | 각도 | 새 발견 | 처분 |
|---|---|---|---|
| 1 | `base/` 정합성 | 확실 3 · 판단 1 · 의심 1 | ⭐ `drive` 의사코드가 `H-17`(*"`drive` 전체를 감싼다, `PostRef` 콜백은 게이트 안"*)을 어김 — 제 *"닫는 자리가 없다"* 주장이 틀렸고 사용자 확인 1번은 무효(`H-139` 절) / `question.md` 최우선 절 "Q4~Q10 대기" 잔재 / README `effect-plan``:Callback` 잔재 / ROADMAP `Property.luau`에 "거부 배선은 에이전트 선택" caveat / Blocker 시작점 "진입 직후"로 |
| 2 | 인덱스 레이어 + 새 문단 자기모순 | 확실 1 · 의심 2 | ROADMAP M8 `Ref.luau` 체크박스가 자기 `H-128` 노트와 모순 → "나머지(`:Wait`)"로 재작성 / Q9 헤더 "확인 대기" 잔재 / `Slot:List` wrap이 `reconcile` 게이트와 중복 → "잉여" 주석 |
| 3 | 주변 폴더 인용처 + 인용문 역방향 + 수정분 | 확실 2 · 의심 2 | ROADMAP 머리 배너 "Q4~Q10 대기" 잔재 / 이 파일 "아직 안 한 것" 문장 / (의심) `ownsGate`는 승인 문구 밖의 구현 세부 — 지역 변수라 그대로, `H-136` 절에 그렇게 표기 / (의심) M8 체크박스의 *"서술도 같이 갔다"*가 과장 → 문구 정정. 인용문 역방향은 전부 일치 |
| 4 | 1~3라운드 수정분 + 발견/결정 문서 내부 정합 | 확실 3 · 의심 2 | 발견 문서 요약 표에 `H-142` 행 없음 / `H-135` 심각도 표 vs 헤더 / §4 아래 Q9 "확인 대기" 잔재 / (의심) `Slot:List` "잉여" 주석 범위를 블록 전체로 / (의심) M8 체크박스의 표면 목록 반복 → M2 참조로 |
| 5 | 4라운드 수정분 + 델타 밖 `base/` | 확실 1 · 의심 2 | `source-state-plan.md`*둘 다 error · 계약은 하나* 문장에 `H-133` 캐비엇 / `blocker-plan.md` 재진입 절에 "`IsOn()` 소유권 판정은 네스팅이 아니다" 한 줄 / `architecture.md` `Ref.luau` 한 줄에 `Epoch` 표면·M2 최소형 |
| 6 | 5라운드 수정분 + 신설 규칙 vs 기존 규정 | 확실 1 · 의심 0 | 이 파일 머리 배너 *"진행 중이다"* 잔재(5라운드 동안 빠졌던 것) → "전량 처리"로. 신설 규칙 여섯 자리(`Parent` 금지 / gcconn 시점 / `New` 표기 / `WeakUnsubscribe` 비소진 / 숏핸드 우선순위 / 다섯째 말단) 전부 기존 규정과 충돌 없음 |
| 7 | 6라운드 수정분 + 세션 기록 사실성 | 확실 3 · 의심 0 | README `bind-system-plan` 행이 "배치 닫는 자리 … `Parent` 순서 미정"으로 1라운드 정정·`H-142` 확정 이전 상태 / `todos.md` 00번에서 "`/code-review high`·커밋 남음" 액션이 사라짐 → 복원 / 이 파일의 "48줄" 개수 주장(실제 47) → 개수 삭제. 세션 파일·summary·todos 본문 서술은 전부 사실과 일치 |
| 8 | 7라운드 수정분만(좁게) | 확실 1 | `project-context.md`의 볼드 마커가 홀수(이번 갱신이 닫는 `**`를 지움) → 짝 맞춤. 지정 세 자리는 회귀 없음. **여기서 멈춤** — 1라운드 이후 설계 내용 발견은 0이고 [2026-08-27 8라운드 시점] 남은 건 기록 문서의 표기뿐이라, 비용 대비 `/code-review high`로 넘어가는 게 낫다고 판단(추이 5→3→4→5→3→1→3→1, 단조 수렴은 아님) |