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

48 KiB
Raw Blame History

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 → indexbk가 소유, 토큰 폐기
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 발견 중 새 메커니즘 넷) 판단 대기fnUnsubscribe 고아 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 아래나 둘 다 괜찮은데, 맥락 상 컨티뉴를 두고 아래 두는게 좋아보임."

확정된 모양:

        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*(이동)로 갈리는 것과 같은 모양이라 그대로 뒀다.

Q4 — EffectHandle:Unsubscribe()는 Observer 게이트를 먼저 통과한 뒤 cleanup (H-127, 확정 (a))

  • effect-plan.md의 "EffectHandle:Subscribe()" 절에 의사코드 한 블록 신설 — Subscribe/WeakSubscribe/WeakUnsubscribeObserver의 함수를 메소드 테이블에 그대로 배정(같은 레지스트리·같은 canBound 게이트·같은 .Subscribed), UnsubscribeObserver.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; 반환 클로저는 None0 순서로 해제. ROADMAP.md M5 체크박스에 같은 두 줄. 5번째 인자(요소)는 Q3의 setLength 시그니처를 따른 것 — 상수 길이라 지속 클로저는 안 생기지만 등록 모양을 다른 말단과 맞췄다.

Q9 — 재고: 두 "확정"이 아니라 Tween 절 스케치의 복사 오류 (H-135, 확정)

사용자가 문항의 전제를 정정했다("nil은 파괴가 맞고 파괴 자체가 트윈을 지움. 메니지드를 지우는 스케치가 왜 트윈에 있는지도 모르겠는 부분..? 트윈은 inst 를 바꾸어 재프로세싱 하는거라 연관이 없는 부분인데"). 다시 본 결과:

  • v == nilprocess가 자식을 파괴하는 규칙은 두 절이 같다. 이상한 건 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.processnumber|Tween|nil로 닿고(StoreBind가 State를 풀고 NoneHandlernil로), 키 자체가 사라지는 건 인스턴스 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.mdreconcile 머리(중복 키 선행 패스 뒤)에 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-139New(name)(props) 파이프라인 의사코드 (신설) + 거기서 나온 것

  • bind-system-plan.md "인스턴스 생성 / 이벤트 네이밍 인체공학" 절에 New④(물리 생성 → gcconn/gchold → flattenDispatch.drive)와 drive (a)(c)(pre-pass → 단일 일반화 for 본체 + 배치 Blocker → postRefList) 의사코드 신설. 단계 안의 규칙은 각 소스 문서가 정본이고 여기는 순서만 확정. ROADMAP.md M3 Dispatch.drive 항목에서 가리킨다.
  • 이름 충돌(모듈 팩토리 New() vs 생성자 New "Frame")은 산문 표기 규칙 한 줄로 닫았다(패키지가 달라 런타임 충돌은 없다).
  • 쓰면서 드러난 것 셋 — 사용자가 예상한 그대로:
    1. 배치를 닫는 자리가 어디에도 없었다. ⚠️ 이 주장은 틀렸다(감사 1라운드) — dispatch-core-plan.mdH-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. 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 로 지우는것만 안 한다면 맞아".

아직 안 한 것(Q1Q3 체크포인트 시점 문장 — **[2026-08-27 후속] Q4Q10도 전량 반영됐고**, 그 반영분의 감사 루프는 아래 "감사 루프 (2026-08-27, Q4Q10 반영분)" 절; 남은 건 라운드 전체에 대한 /code-review high와 커밋뿐): /code-review high, 커밋 — Q4Q10 반영 뒤 한 번에. 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.mddispatch-core/slot-plan 색인 행에 Q1/Q2 요약 추가 / architecture.mdSlot.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.mdH-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. 같은 핸들러의 순서setLengthParent였던 것을 ParentsetLength로("일반 계약 — 물리와 부기의 순서" 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-typesQuadRef 필드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, 단조 수렴은 아님)