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>
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 → 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 아래나 둘 다 괜찮은데, 맥락 상 컨티뉴를 두고 아래 두는게 좋아보임."
확정된 모양:
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 위로:
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 콜백 머리에 미실체화 가드:
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/_listedassert 옆에 한 줄.
- 이중
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의 eagerbk)과 범위가 다르다(요소를 넣은 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 범위 정정).
반영 중 드러난 것 — 사용자가 바꾼 적 없는데 있던 것 (판단 요청 포함):
token자체(H-141) — 위 Q3 절.unmountSlotTree의if slot._baseObserver then가드 — 옛 lazy 생성의 흔적. 생성자 불변식("항상 있다")에 맞춰 무조건 호출로. 동작 차이 없음.rawReplace가 맵을 직접 쓴다 — 옛self._elemIndex[old] = nil; [new] = index두 줄이bk.indexOfElement로 그대로 옮겨졌다.setLength(…, newElement)도 등록하지만 미실체화 분기는setLength를 안 부르므로 직접 쓰기가 남는다. 쓰기 지점이setLength/reindexFrom/rawReplace셋이 됐다 — 규칙("등록은setLength, 이동은reindexFrom")에서rawReplace만 예외. 괜찮은지 확인 요청.Owned = false인 Slot은_destroyed가 안 선다 —destroySlotTree가 그 분기에서unmountSlotTree로 빠져 꼬리(플래그 세팅)에 안 닿는다. "파괴 대신 언마운트만"이라는 그 분기의 뜻과 정합하고(요소를 만든 적 없으니 좀비도 없다) 재사용도 그대로 가능하다 — 의도대로라고 보고 그대로 뒀다. 확인 요청.- 미실체화
:Add가bk를 만든다(Q3 절의 열어둔 확인) — 그대로 반영. 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
색인과 Q3 반영분에 대해 돌았고 6라운드에서 수렴했다(아래 "감사 루프" 절).todos.md 00번 갱신은 감사 1라운드 지적으로 그 자리에서 했다. 감사
루프는 Q1
감사 루프 (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) |