quad/.claude/luau-test/rewrite-required/01-two-pass-array-hash-order.luau
qwreey 4622fbeec8
qa: 반영 후 감사 6라운드 — 실제 크래시 3건 포함 18건 수정
커밋 9b7f847(Detach/KeyGone/Owned/attachSlot 분해) 반영 후 각도를 바꿔가며
quad-doc-auditor를 6라운드 돌린 결과. 라운드별 확실 발견 4/6/2/3/3/0으로
6라운드에서 새 발견 0건 — 수렴 확인 후 종료. 경위 전량은
qa-request/pre-implementation-qa-round4-followup.md의 I절이 소스.

트레이싱 라운드(4~5)가 잡은 실제 크래시 — 셋 다 Detach가 신설한 경로가
기존 불변식과 부딪히는데 그쪽이 안 고쳐진 것:

- I-1 (치명): rawDetach가 소유권을 유지하는데 재마운트는 rawAdd →
  claimOwner를 거치고, claimOwner는 같은 owner의 재클레임도 무조건 error다
  (2026-08-13 감사가 Slot{a,a}를 막으려고 넣은 것). 문서가 권장하는
  "prev를 그대로 반환하면 재마운트" 패턴이 그대로 죽었음. fromDetached
  플래그로 그 경로만 좁게 예외 처리.
- I-2: 재마운트된 자식 Slot이 activateList를 두 번 실행해 구독이 이중으로
  생기고 mounted/keyIndex 클로저 상태가 통째로 새로 만들어짐 → 멱등 가드.
  가드만으로는 :List 구독이 옛 physicalTarget에 앵커된 채 남아 포탈
  재마운트 후 조용히 멈추므로, _listObserver 핸들 보관 + 재앵커까지 처리.
- I-3: _detachCleanup이 releaseOwner를 안 불러 Owned=false 요소가 죽은
  Slot을 owner로 달고 남음 → 두 분기 공통으로 호출.
- I-6: 위 수정의 회귀 트레이싱 — destroySlotTree에 _listObserver 해제 누락,
  claimOwner의 옛 논증 두 문단이 fromDetached와 정면 모순, 소유권 예시
  코드가 C-4와 모순.
- I-7: 사용자가 별도 상의해 가져온 두 건 — _detachCleanup 설치를
  mountSlotTree → activateList로 이관(:List 없는 Slot마다 no-op Effect를
  트리 크기만큼 심고 있었음), activateList의 inst → physicalTarget 리네이밍.
  이관 근거가 멱등 가드 이전 동작을 전제하고 있어 가드 분기의 재앵커까지
  같이 반영. 이로써 _listObserver/_detachCleanup이 같은 범주로 통일됨.

I-4(materializeSlotTree 중 예외 시 Blocker 잔류)는 사용자 판단으로 pcall
없이 문서화만 — 아직 밟은 적 없는 경로이고 옛 단일 attachSlot에도 있었을
구조적 갭.

문서 정합성 라운드(1~3)에서 나온 것: slot-plan의 "값 교체는 비파괴" 잔존,
분해 완료 후에도 남아 있던 "논의 대기 중" 배너, attachSlot의 flush 루프를
가리키던 문장 5곳, README 색인 행이 2026-08-19에서 멈춰 있던 것,
qa-round4 문항지/followup의 "회신 대기" 상태줄, todos의 용어 목록 이중 소스,
dispatch-core의 raw* 일반 계약에 rawDetach 누락.

luau-test: 스파이크 01이 "재작성 필요" 마커를 단 채 done/에 남아 있어
STATUS.md 자신의 "폴더가 곧 상태" 규칙을 어기고 있었음 → rewrite-required/로
이동하고 개수 정정. "만들어야 할 스파이크" 절 신설(아직 파일조차 없는 실측
항목이 어느 폴더로도 표현되지 않아 구조적으로 잊히던 자리).

doc-check.py ERROR 0.

Co-authored-by: qwreey <me@qwreey.moe>
2026-08-21 13:20:42 +09:00

60 lines
2.5 KiB
Text

--[[
검증 대상: base 디스패치 드라이버가 명시적으로 강제하는
"배열 파트(children/Ref) 먼저, 해시 파트(프로퍼티/이벤트) 나중" 두 패스 순회 계약.
배경: .claude/base/bind-system-plan.md "props 순회 순서" 절, ROADMAP.md M0 4번째 항목.
사용자가 이미 Luau REPL로 `for i,v in {a=1, 2, b=3} do ... end`가
`1, 2` 다음 `a, 1` `b, 3` 순서로 나오는 걸 확인했었지만(우연한 관찰),
base는 이 우연한 동작에 기대지 않고 배열 파트(1..#t)를 먼저, 그 다음
별도로 해시 파트만 골라내는 두 패스를 "명시적으로" 강제하기로 확정함
— 이 스크립트는 그 강제 버전이 실제로 계약대로 동작하는지 확인.
실행: `luau 01-two-pass-array-hash-order.luau` (Roblox 필요 없음, 순수 CLI)
기대 결과: "array pass"가 항상 "hash pass"보다 먼저 전부 출력되고,
array pass 안에서는 index 순서(1,2,3...)가 정확히 지켜져야 함.
]]
local function isArrayKey(k)
return type(k) == "number" and k == math.floor(k) and k >= 1
end
-- Dispatch.drive(inst, flattened)의 최소 스파이크 버전
local function drive(inst, flattened)
-- pass 1: 배열 파트, index 순서 보장
local n = #flattened
for i = 1, n do
local v = flattened[i]
print(string.format("[array pass] inst=%s i=%d v=%s", tostring(inst), i, tostring(v)))
end
-- pass 2: 해시 파트, 배열 인덱스(1..#t)는 건너뜀
-- 주의: pairs()/제네릭 for는 배열 파트도 다시 순회하므로 반드시 걸러내야 함
for k, v in flattened do
if not (isArrayKey(k) and k <= n) then
print(string.format("[hash pass] inst=%s k=%s v=%s", tostring(inst), tostring(k), tostring(v)))
end
end
end
local children = { "Ref1", "Child2", "Child3" }
local props = {
children[1],
children[2],
children[3],
Name = "TestFrame",
BackgroundTransparency = 0,
Event_Activated = "handler",
}
print("=== two-pass order 검증 ===")
drive("FakeInstance", props)
--[[
추가로 확인할 것 (실행 후 눈으로 확인):
1. array pass 3개가 hash pass보다 먼저, 그리고 i=1,2,3 순서로 나오는가?
2. hash pass에 array 항목(children)이 중복으로 안 섞여 나오는가?
3. 테이블 리터럴에서 해시 키를 적는 소스 텍스트 순서를 바꿔도(Name/
BackgroundTransparency/Event_Activated 순서를 바꿔서 재실행)
array pass 결과가 그대로인지 확인해볼 것 — 순서가 소스 텍스트가
아니라 오직 "배열/해시 파트 분리"에만 의존한다는 걸 재확인하는 목적.
]]