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

27 KiB

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 → indexbk가 소유, 토큰 폐기
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 아래나 둘 다 괜찮은데, 맥락 상 컨티뉴를 두고 아래 두는게 좋아보임."

확정된 모양:

        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로 죽고, 그 errorrecomputeBlocker: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 ≥ ilengthList[i]가 살아 있다. 즉 이 재배치가 정상 경로를 안 바꾼다.
  • 실측(9라운드 참조 구현 ref9/): 이 처방 전엔 d10_remove_at_tail.luauattempt 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.mdrecompute 의사코드(+ 되감기 절의 근거 문장 — "1로 클램프한다" 주석은 그대로 유효하다).


Q2 — 재마운트: Offset/_baseObserver를 생성자로 올리고, 순서는 (c), 파괴는 _destroyed (H-125, 확정)

결정: 문항의 (a)/(b)/(c) 중 (c)를 고르되, 사용자가 그보다 나은 형태를 제시해 그쪽으로 확정했다slot.Offsetslot._baseObserverSlot 생성자에서 만든다. 그러면 (c)가 고치려던 순서 문제의 분기 자체가 없어진다.

왜 (a)가 아닌가 — 그리고 문항의 근거 하나는 틀렸다

  • 사용자가 (a)를 반대한 근거는 *":List인데 어디선가 이미 그려진 적 있다면 … offset 설정 결과가 전파될 방법이 없음. layoutorder 등을 설정하는 유저 함수 부분에 문제가 있을것"*이었는데, 그 부분은 실측에서 성립하지 않았다. setOffsetSourceS.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 위로:

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를 본다. getOffsetAtlengthList를 직접 읽으므로 게이트와 무관하다("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 콜백 머리에 미실체화 가드:

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/_listActivatednil로 지우던 것을 전부 그만둔다 — 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_mountedfalse로 되돌려놔서 "마운트된 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.mdSlot(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.mdSlot.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 → indexbk가 소유한다; 토큰 폐기 (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 안에서 요소는 유일하다(_elementsNone이 안 들어가고 claimOwner가 이중 배치를 막는다).

2. Dispatch.setLength(ownerKey, i, len, anchor, element) — 5번째 인자는 그 자리의 inst|slot. gatedRecompute요소를 캡처bk.indexOfElement[element]를 조회한다.

사용자: "Dispatch.setLength 가 이제 받아야할 것은 inst|slot 이야. 그거 이외 클로저로 저걸 해줄 이유가 없어보이는데. 게다가 슬롯 아니면 nil 이 나오는것도 이상해."

  • 요소를 캡처하면 갱신할 것이 없다 — 요소의 신원은 안 변하고, 요소 → indexreindexFrom이 자기 이유로 이미 정확하게 유지한다. 위치를 캡처하던 옛 모양(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-1reindexFromspliceArrays*에서 분리한 근거가 소멸한다 — *"spliceArrays*bk를 만지므로 실체화된 뒤에만 부를 수 있다"*였는데, getBookkeeping은 lazy라 절대 nil이 아니다. 대신 미실체화 Slot에 :Add만 해도 bk(+ recomputeBlocker)가 생긴다. Q2의 _baseObserver 가드가 막은 것(모든 Slot의 eager bk)과 범위가 다르다(요소를 넣은 Slot만). 이 정도는 괜찮다고 보고 반영했다 — 아니면 알려줄 것.
  • 쓰기 지점이 둘이다 — 등록은 setLength(bk.indexOfElement[element] = i), 이동은 reindexFrom. lengthListsetLength(등록)/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. unmountSlotTreeif 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. 미실체화 :Addbk를 만든다(Q3 절의 열어둔 확인) — 그대로 반영.
  6. todos.md의 6라운드 서술("slot._elemIndex 신설")은 역사 기록이라 이동 표기만 덧붙였다.

[2026-08-27 사용자 확인] 위 1~5 전부 의도대로. 원문: (1) "length, elem->index 를 위치를 재설정 해준다면 괜찮아"rawReplace의 직접 쓰기는 "자리는 그대로, 주인만 교체"라 lengthList·indexOfElement 둘 다 그 자리를 다시 쓰는 것으로 성립. (2) "owned = false 은 state -> slot(single) 형태가 구현되는 것이라 맞아" — 그 Slot은 요소를 만든 적이 없으니 파괴가 아니라 언마운트이고 _destroyed가 안 서는 게 맞다. (3) "미실체화 add 가 bk 만드는것도 맞아. 그래야 인덱싱 매핑을 만드니까". (4) "slot._baseObserver 는 unbind 만 했고, nil 로 지우는것만 안 한다면 맞아".

아직 안 한 것: /code-review high, 커밋 — Q4Q10 반영 뒤 한 번에. README 색인과 todos.md 00번 갱신은 감사 1라운드 지적으로 그 자리에서 했다. 감사 루프는 Q1Q3 반영분에 대해 돌았고 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.mddispatch-core/slot-plan 색인 행에 Q1/Q2 요약 추가 / architecture.mdSlot.luau 한 줄에 생성자 필드·_destroyed 표기(판단 항목 — 8라운드 행들이 같은 밀도라 넣음)
6 5라운드 수정분 + 신설 규칙 문단 확실 0 · 의심 1 *"Owned=false_destroyed가 안 선다"*가 README 요약과 followup에만 있고 slot-plan.md 산문엔 없던 것 — 사용자 확정 인용과 함께 명문화. 수렴(확실 0)