quad/.claude/qa-request/pre-implementation-handtrace-round9-followup.md
qwreey 031495cc0b
qa: 9라운드 손 트레이싱 실행 + Q1~Q3 확정·반영 — 체크포인트 (Q4~Q10 다음 세션)
8라운드가 써둔 지시서(-round9-brief.md)대로 커밋 9dd8213의 델타를 재트레이싱해
발견 18건(H-124~H-141)을 냈고(qa-request/pre-implementation-handtrace-round9.md),
§4 문항 중 Q1~Q3를 사용자와 확정해 base/·ROADMAP에 반영했다. 결정의 소스는
-round9-followup.md(진행 표가 상태의 소스). 🔴 둘 다 luau 실측 재현.

Q1 H-124 — recompute가 lengthList[i]를 되감기 판정보다 먼저 읽어, offset:Set 안의
  사용자 코드가 커서 뒤 자리 수를 줄이면 sum += nil로 죽고 recomputeBlocker가
  영구 On. 되감기 판정을 앞으로(continue), 읽기·누적은 되감지 않을 때만.
Q2 H-125 — 재마운트 시 setOffsetSource가 slot.Offset을 바꾸는 순간 _baseObserver가
  unbind 상태라 두 필드가 0으로 안 내려가 옛 베이스의 offsetCache[1]을 씀.
  사용자 확정: Offset·_baseObserver를 Slot 생성자로(첫 마운트/재마운트 분기
  소멸), materializeSlotTree는 blocker:On → bindLifetime → setOffsetSource,
  파괴는 _destroyed 플래그 하나(핸들은 unbind만, mutate CRUD·:List·마운트 진입
  error, Owned=false는 안 섬, 이중 dispose no-op).
Q3 H-126/H-137/H-141 — element→index 맵이 Slot 층(slot._elemIndex)과 Dispatch
  층(bk.tokens/indexOfToken)에 두 벌 있었고, 후자의 token은 사용자가 정한 적
  없는 것(2026-08-25 /code-review가 발명해 사용자 인용문 옆에 앉아 있던 것).
  bk.indexOfElement 하나로 통일, setLength 5번째 인자 element, splice가 비운
  자리는 세 배열 전부 처리. H-137 소멸.

부수: H-140(ROADMAP의 폐기된 "해제 시 slot.Offset = nil" 잔존) 정정,
H-125 피해 범위를 "중첩 Slot의 Offset"으로 축소(유저 체인은 요소 인스턴스에
바인드돼 전파됨 — 실측), G각도로 "for d in seen do는 유효한 Luau가 아니다"가
거짓임을 확인, keyof<{}> 빈 Store 실측 클린.

conventions.md 신설: "/code-review(그리고 메인 세션)가 내놓는 새 필드·인자·
이름·메커니즘은 발견이지 결정이 아니다" — 이 세션에서 메인 세션도 같은 실수를
세 번 했다(subject 인자 / Observer 위치 필드 = 기각된 Effect userdata 재개방 /
조회 클로저).

검증: quad-doc-auditor 6라운드(확실 1→1→3→1→1→0, base/ 본문 결함은 1라운드
이후 0건 — 나머지는 인덱스 레이어·인용처), doc-check ERROR 0. /code-review high는
Q4~Q10 반영 뒤 한 번에(같은 파일을 또 건드려 diff가 섞이므로 지금 체크포인트).

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

398 lines
27 KiB
Markdown

# 9라운드 손 트레이싱 발견 — **사용자 결정과 반영 결과**
**무엇인가**: `.claude/qa-request/pre-implementation-handtrace-round9.md`의 발견
(`H-124`~)을 사용자와 대화형으로 처리한 결과. **결정의 소스는 이 문서**이고,
발견 원문·값 트레이스·실측 전사·"이상 없다고 확인한 것" 목록은 그 파일이
소스다(여기서 다시 서술하지 않음).
**진행 방식**: 그 문서 §4가 배치 회신용으로 묶어둔 **결정 문항 Q1~Q10** 순서를
따른다. 8라운드와 같다.
**⚠️ [2026-08-27 기준] 진행 중이다.** 아래 표가 어디까지 왔는지의 소스다.
처음엔 "문항을 다 처리한 뒤 일괄 반영"으로 잡았으나, 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~Q10 | `H-127`~`H-133` | ⏳ 대기 |
| — | `H-134`~`H-140` | ⏳ 대기(레인 B·부수 발견) |
**[2026-08-27] Q1~Q3는 `base/`·`ROADMAP.md`에 반영했다** — 반영 중 드러난 것과
열어둔 확인은 아래 "반영 기록" 절.
---
## 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*`(이동)로
갈리는 것과 같은 모양이라 그대로 뒀다.
---
## 반영 기록 — 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 로 지우는것만 안 한다면 맞아"*.
**아직 안 한 것**: `/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) |