docs+feat: 단위 1 감사 루프 5라운드 반영 + §4 배치 회신(H-214/H-215 전부 (a))

감사 루프(각도 교대, 새 발견 1~5라운드 4→3→2→1→2건 뒤 반영 완료):
- 기계적 누락 4(CLAUDE.md 볼드 중첩 / STATUS 요약 표 / quad-types-plan M3
  완료 표시 / luau-test README 01 행) + STATUS 부채 2(합류 문단·done 나열)
- H-212 확장: dispatch-core-plan Length/Offset 절·주입 op 스텁·가로채기
  예시 error 영어화(스텁 level 숫자는 M5/M10 몫으로 명시)
- H-214 근거 정정: "이름으로 덤프"는 research가 아니라 같은 문서 자기 자신
- H-216(①): slot-plan.md 한국어·level 없는 error 11곳 정리(입력 검증 2,
  releaseOwner만 1) / H-217(①): attribute·debounce-throttle·ref·source-state
  마지막 4곳 — base/ 코드 리터럴 한국어 error 0

§4 배치 회신(사용자: "전부 권고안에 동의해"):
- H-214 (a): Handler 계약에 선택 필드 name: string?(진단 전용) — quad-types
  타입, 동률 경고에 양쪽 이름, TODO 마커 제거, 계약 절 신설 항목, spec 11번
- H-215 (a): 스파이크 04 폐기·done/ 이동 — 잔여(실제 StoreBind 경유 재귀
  재발행)는 ROADMAP M4 mock 항목이 명시적으로 짐. 재검증 대기 절 [x]

테스트·luau-analyze·doc-check ERROR 0 전부 클린.

Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01LF78pXeFGD1ZSVD3ifteYG
This commit is contained in:
qwreey-agent-selene 2026-08-31 15:53:05 +09:00
parent a6d0c7f9a7
commit da67a8c009
No known key found for this signature in database
18 changed files with 202 additions and 53 deletions

View file

@ -459,7 +459,7 @@ function AttributeGroupHandler.process(inst, k, v, index)
-- 기록되는 중간 상태가 안 생긴다 — 위 그 항목이 소스). -- 기록되는 중간 상태가 안 생긴다 — 위 그 항목이 소스).
local claimed = groupClaimKeys:GetStrong(inst, v) local claimed = groupClaimKeys:GetStrong(inst, v)
if claimed ~= nil and claimed ~= k then if claimed ~= nil and claimed ~= k then
error("Attribute: 같은 그룹 값이 이 인스턴스의 두 위치에 놓임") error("Attribute: the same group value is placed at two positions of this instance", 2)
end end
groupClaimKeys:SetStrong(inst, v, k) groupClaimKeys:SetStrong(inst, v, k)

View file

@ -909,7 +909,7 @@ end
local function makeGate(reset: boolean, opts) local function makeGate(reset: boolean, opts)
if opts.Leading == false and opts.Trailing == false then if opts.Leading == false and opts.Trailing == false then
error("Leading/Trailing 둘 다 false면 아무것도 통과하지 않음") error("Debounce/Throttle: Leading and Trailing both false would pass nothing through", 2)
end end
local leading = if reset then opts.Leading == true else opts.Leading ~= false local leading = if reset then opts.Leading == true else opts.Leading ~= false
local trailing = opts.Trailing ~= false local trailing = opts.Trailing ~= false

View file

@ -129,6 +129,19 @@ v1의 `ProcessQuadProperty`(`.claude/initreq/quad/src/class.lua:134-214`)는
> 이야기**다 — (A) 분기에서 클로저가 불릴 땐 아래가 그대로 살아 있고, > 이야기**다 — (A) 분기에서 클로저가 불릴 땐 아래가 그대로 살아 있고,
> 그게 바로 "깜빡임 없이 갈아끼우기"가 성립하는 이유다. > 그게 바로 "깜빡임 없이 갈아끼우기"가 성립하는 이유다.
- **⭐ [2026-08-31 신설, `H-214` (a) 사용자 확정] 선택 필드 `name: string?`
진단 전용.** 위 필수 3종에 더해, 핸들러는 자기 이름을 선택적으로 실을 수
있다. 쓰는 곳은 진단뿐 — 동률 경고 print, `Dispatch.listHandlers()`
돌려주는 목록(아래 "우선순위 동률/매치 실패 처리" 절의 "이름/priority"가
이 필드), 나중의 `quad-debug` 체인 슬롯 덤프(아래 "부수 효과 — quad-debug에
유리" 항목). **스캔·매치·체인 부기엔 아무 영향 없고, 없으면 priority만
보인다.** 배경: `listHandlers`와 체인 덤프 서술이 "이름"을 전제하는데 계약
3종엔 이름이 없다는 게 M3 단위 1 구현에서 드러났고
(`qa-request/m3-implementation-round12.md` `H-214`), 사용자가 선택 필드
안을 채택했다(2026-08-31, *"전부 권고안에 동의해"*). 별도 등록 인자
(`addHandler(h, name)`) 안은 이름이 레지스트리에 살게 돼 체인 슬롯
덤프(슬롯엔 handler 객체만 저장)가 역조회를 요구해서 기각.
디스패치는 등록된 핸들러를 우선순위 순으로 스캔하며 `isHandlable`을 호출, 디스패치는 등록된 핸들러를 우선순위 순으로 스캔하며 `isHandlable`을 호출,
첫 매치가 처리(Fusion의 SpecialKey 우선순위 스캔과 유사하되 4단계 고정이 아니라 첫 매치가 처리(Fusion의 SpecialKey 우선순위 스캔과 유사하되 4단계 고정이 아니라
열린 레지스트리). tbox의 `TUnion` 런타임 체커가 이미 이 "순서대로 스캔, 첫 매치 열린 레지스트리). tbox의 `TUnion` 런타임 체커가 이미 이 "순서대로 스캔, 첫 매치
@ -877,9 +890,13 @@ Fallback Handler들도 존재하지 않아**, 위 "매치 실패는 즉시 `erro
- **아직 아무 팩토리도 채우지 않은 슬롯의 기본값은 quad-base가 준다 — - **아직 아무 팩토리도 채우지 않은 슬롯의 기본값은 quad-base가 준다 —
단 "동작하는 구현을 추측"하지 않고 명시적으로 에러내는 스텁으로.** 단 "동작하는 구현을 추측"하지 않고 명시적으로 에러내는 스텁으로.**
`BaseModule.addTag = function() error("addTag가 구현되지 않음 — `BaseModule.addTag = function() error("quad: addTag is not available —
provider가 초기화됐는지, 이 백엔드가 Tag를 지원하는지 확인하라") end` check that the provider is initialized and that this backend supports
류. base가 "그럴듯한 기본 동작"(예: 조용한 no-op)을 대신 만들어주는 Tag") end` 류(**[2026-08-31 `H-212` 확장]** 예시 메시지를 영어로 —
M2가 커밋한 `LifetimeHandle.luau`의 미주입 스텁과 같은 어조. `level`
숫자는 이 스텁이 quad 내부 프레임(`TagHandler.process`)을 거쳐 불려
"사용자 호출부"까지의 프레임 수를 문서 시점엔 못 세므로 여기 안 박는다 —
M5/M10 구현 시점에 도착지 계약(`architecture.md`)대로 정할 것). base가 "그럴듯한 기본 동작"(예: 조용한 no-op)을 대신 만들어주는
건 기각 — 임의의 엔진에 뭐가 맞는 기본값인지 base는 알 수 없고, 건 기각 — 임의의 엔진에 뭐가 맞는 기본값인지 base는 알 수 없고,
조용한 no-op은 실수(provider 초기화를 잊음)를 가려버림. 명시적 에러가 조용한 no-op은 실수(provider 초기화를 잊음)를 가려버림. 명시적 에러가
유일하게 안전한 기본값. 유일하게 안전한 기본값.
@ -893,7 +910,7 @@ Fallback Handler들도 존재하지 않아**, 위 "매치 실패는 즉시 `erro
```lua ```lua
{ priority = HANDLER_PRIORITY_FALLBACK + 1, { priority = HANDLER_PRIORITY_FALLBACK + 1,
isHandlable = function(inst,k,v) return isTag(v) end, isHandlable = function(inst,k,v) return isTag(v) end,
process = function(inst,k,v) error("이 백엔드는 Tag를 지원하지 않음") end } process = function(inst,k,v) error("this backend does not support Tag") end }
``` ```
실제로 `FALLBACK`에 등록돼 있는 `TagFallbackHandler`보다 한 단계 실제로 `FALLBACK`에 등록돼 있는 `TagFallbackHandler`보다 한 단계
높아 스캔에서 먼저 매치되고(2026-08-14 열두 번째 세션 정정 — `TagHandler` 높아 스캔에서 먼저 매치되고(2026-08-14 열두 번째 세션 정정 — `TagHandler`
@ -1783,7 +1800,7 @@ function Dispatch.getOffsetAt(ownerKey, at)
-- ⭐ [2026-08-25, 7라운드 `H-106`] `nil` 가드 — `recompute`만 갖고 있던 -- ⭐ [2026-08-25, 7라운드 `H-106`] `nil` 가드 — `recompute`만 갖고 있던
-- `C-6` 진단이 이 경로에선 우회돼 익명 산술 에러로 먼저 터졌다. -- `C-6` 진단이 이 경로에선 우회돼 익명 산술 에러로 먼저 터졌다.
if bk.lengthList[i] == nil then if bk.lengthList[i] == nil then
error("Dispatch.getOffsetAt: lengthList[" .. i .. "] nil — bookkeeping is broken", 1) error("Dispatch.getOffsetAt: lengthList[" .. i .. "] is nil — bookkeeping is broken", 1)
end end
cur += contribution(bk, i) -- lengthList[i](State면 :Get()) cur += contribution(bk, i) -- lengthList[i](State면 :Get())
bk.offsetCache[i + 1] = cur -- **지금 자리의 길이가 다음 자리의 offset을 정한다** bk.offsetCache[i + 1] = cur -- **지금 자리의 길이가 다음 자리의 offset을 정한다**
@ -1960,7 +1977,7 @@ local function recompute(ownerKey, bk)
-- 부기가 깨진 것 — 조용히 건너뛰면 위치 하나가 순서 계산에서 빠지는 -- 부기가 깨진 것 — 조용히 건너뛰면 위치 하나가 순서 계산에서 빠지는
-- 추적 어려운 오작동이 된다. 상세는 base/slot-plan.md의 "추가 방어 조치". -- 추적 어려운 오작동이 된다. 상세는 base/slot-plan.md의 "추가 방어 조치".
if offset == nil then if offset == nil then
error("Dispatch.recompute: sourceList[" .. i .. "]가 nil — 부기가 깨졌음(계약상 None이어야 함)") error("Dispatch.recompute: sourceList[" .. i .. "] is nil — bookkeeping is broken (the contract says None)", 1)
end end
local abs = Dispatch.getOffsetAt(ownerKey, i) -- 절대 offset(캐시 경유) local abs = Dispatch.getOffsetAt(ownerKey, i) -- 절대 offset(캐시 경유)
bk.offsetSetUpTo = i -- 여기까지 Set 완료 bk.offsetSetUpTo = i -- 여기까지 Set 완료

View file

@ -107,6 +107,8 @@ quad-roblox는 `quad-types`의 좁은 `Quad`만 본다.
- 규칙이 쓰인 계기는 `Dispatch`이고, **M3**가 `Dispatch: Dispatch` 필드와 - 규칙이 쓰인 계기는 `Dispatch`이고, **M3**가 `Dispatch: Dispatch` 필드와
그 타입 재수출을 여기 추가한다 — `ROADMAP.md` M3 체크리스트에 **항목으로 그 타입 재수출을 여기 추가한다 — `ROADMAP.md` M3 체크리스트에 **항목으로
명시**한다(지금까지 아무도 이 필요성을 항목화해두지 않았다). 명시**한다(지금까지 아무도 이 필요성을 항목화해두지 않았다).
**[2026-08-31] M3 단위 1로 완료** — `Handler`/`Dispatch` export type과
`Quad.Dispatch` 필드가 실제로 추가됐다(그 체크박스는 `[x]`).
- **[2026-08-24 정정] 다만 규칙이 *처음 적용된* 마일스톤은 M2다** — - **[2026-08-24 정정] 다만 규칙이 *처음 적용된* 마일스톤은 M2다** —
마일스톤 순서 교체로 반응형 코어가 앞에 오면서, `Source`/`State`/`Store` 마일스톤 순서 교체로 반응형 코어가 앞에 오면서, `Source`/`State`/`Store`
필드 추가가 `Dispatch`보다 먼저 왔다(`ROADMAP.md` M2의 `H-25` 파생 항목 — 필드 추가가 `Dispatch`보다 먼저 왔다(`ROADMAP.md` M2의 `H-25` 파생 항목 —

View file

@ -975,8 +975,9 @@ flatten된 값은 해시 파트(프로퍼티 키)로 존재하게 되고, Store
- **구현**: pre-pass가 첫 fire 때 해당 `PreRef` 객체에 내부 플래그 - **구현**: pre-pass가 첫 fire 때 해당 `PreRef` 객체에 내부 플래그
(`_fired = true`)를 세팅. pre-pass가 배열을 훑다 `isPreRef(v)` (`_fired = true`)를 세팅. pre-pass가 배열을 훑다 `isPreRef(v)`
슬롯을 만났는데 그 객체가 이미 `_fired`면, fire하지 않고 그 자리에서 슬롯을 만났는데 그 객체가 이미 `_fired`면, fire하지 않고 그 자리에서
즉시 `error("PreRef는 1회용 — 이미 다른 construction에 쓰인 즉시 `error("PreRef instance reused", 2)`(문구는
PreRef를 재사용할 수 없음, 매번 새로 만들 것")`. 위 "동적 경로 가드" `bind-system-plan.md`의 파이프라인 의사코드와 같은 리터럴 — 1회용이라
매번 새로 만들라는 뜻). 위 "동적 경로 가드"
Handler(정상 본체 루프에서 매치)와는 별개 코드 경로 — 이 가드는 Handler(정상 본체 루프에서 매치)와는 별개 코드 경로 — 이 가드는
pre-pass 자신 안에, `_fired`가 아닌 정상 fire는 그대로 통과. pre-pass 자신 안에, `_fired`가 아닌 정상 fire는 그대로 통과.
- **관용구**: `Slot:List``updateFn`처럼 반복 호출되는 자리에서 - **관용구**: `Slot:List``updateFn`처럼 반복 호출되는 자리에서

View file

@ -430,7 +430,7 @@ local function claimOwner(element, ownerKey, fromDetached)
-- `fromDetached` 없이 같은 owner라는 것만으로 통과시키면 -- `fromDetached` 없이 같은 owner라는 것만으로 통과시키면
-- `Slot{a, a}`가 다시 조용히 새어나간다(2026-08-13 감사가 막은 것). -- `Slot{a, a}`가 다시 조용히 새어나간다(2026-08-13 감사가 막은 것).
if not (fromDetached and cur == ownerKey) then if not (fromDetached and cur == ownerKey) then
error("이 요소는 이미 마운트돼 있음 — 다중 마운트 금지") error("Slot: this element is already mounted — multiple mounts are not allowed", 2)
end end
return -- 이미 내 것이므로 SetWeak도 불필요 return -- 이미 내 것이므로 SetWeak도 불필요
end end
@ -445,7 +445,7 @@ local function claimOwnerAt(element, inst, k)
return false -- 정확히 이 자리가 이미 들고 있음 — 재확인만, no-op return false -- 정확히 이 자리가 이미 들고 있음 — 재확인만, no-op
end end
if current ~= nil then if current ~= nil then
error("이 요소는 이미 다른 곳에 마운트돼 있음 — 다중 마운트 금지") error("Slot: this element is already mounted elsewhere — multiple mounts are not allowed", 2)
end end
elementOwner:SetWeak(element, OWNER, inst) elementOwner:SetWeak(element, OWNER, inst)
elementOwner:SetWeak(element, OWNER_POS, k) elementOwner:SetWeak(element, OWNER_POS, k)
@ -460,7 +460,7 @@ local function releaseOwner(element, ownerKey)
-- 무시 없이 즉시 error" 원칙과 같은 결로 즉시 error. -- 무시 없이 즉시 error" 원칙과 같은 결로 즉시 error.
local current = elementOwner:GetWeak(element, OWNER) local current = elementOwner:GetWeak(element, OWNER)
if current ~= ownerKey then if current ~= ownerKey then
error("releaseOwner: 이 element는 이 ownerKey가 소유하고 있지 않음 — 호출측 소유권 추적이 깨졌음") error("releaseOwner: this element is not owned by this ownerKey — ownership tracking is broken", 1)
end end
elementOwner:SetWeak(element, OWNER, nil) elementOwner:SetWeak(element, OWNER, nil)
elementOwner:SetWeak(element, OWNER_POS, nil) elementOwner:SetWeak(element, OWNER_POS, nil)
@ -1554,7 +1554,7 @@ function activateList(self, physicalTarget)
for i, item in ipairs(items) do for i, item in ipairs(items) do
local key = keyFn(item, i) -- keyFn은 raw i를 받음(:List 파라미터 설명 참고) local key = keyFn(item, i) -- keyFn은 raw i를 받음(:List 파라미터 설명 참고)
if seen[key] then if seen[key] then
error("Slot:List — duplicate key: " .. tostring(key)) error("Slot:List — duplicate key: " .. tostring(key), 2)
end end
seen[key] = true seen[key] = true
keys[i] = key keys[i] = key
@ -1632,8 +1632,8 @@ function activateList(self, physicalTarget)
-- KeyGone을 받은 자리는 "데이터가 다시 나타날 때를 위한 캐싱" -- KeyGone을 받은 자리는 "데이터가 다시 나타날 때를 위한 캐싱"
-- (= Detach) 아니면 파괴뿐이고, **새 마운트/생성은 거부**한다. -- (= Detach) 아니면 파괴뿐이고, **새 마운트/생성은 거부**한다.
if result ~= nil then if result ~= nil then
error("Slot:List — KeyGone에는 nil/None(파괴) 또는 Detach(홀드)만 반환할 수 있음 " error("Slot:List — KeyGone accepts only nil/None (destroy) or Detach (hold) "
.. "(자리가 없어진 키에 새 요소를 마운트할 수 없고, prev 유지도 모순)") .. "(cannot mount a new element at a key whose slot is gone; keeping prev is contradictory)", 2)
end end
settle(key, result, detach, 0) -- slotPos는 의미 없음(자리를 안 차지함) settle(key, result, detach, 0) -- slotPos는 의미 없음(자리를 안 차지함)
userdata[key] = ud -- 유저가 nil을 반환해야 지워짐 userdata[key] = ud -- 유저가 nil을 반환해야 지워짐
@ -3271,7 +3271,7 @@ function Slot:Add(element, index)
assert(not self._listed, "Slot: :List가 설치된 Slot엔 수동 CRUD를 쓸 수 없음") assert(not self._listed, "Slot: :List가 설치된 Slot엔 수동 CRUD를 쓸 수 없음")
-- (2) index 범위 검증 — **clamp 안 함**, 범위 밖이면 error -- (2) index 범위 검증 — **clamp 안 함**, 범위 밖이면 error
if index ~= nil and (index < 1 or index > #self._elements + 1) then if index ~= nil and (index < 1 or index > #self._elements + 1) then
error("Slot:Add — index가 범위 밖(1.." .. (#self._elements + 1) .. "): " .. tostring(index)) error("Slot:Add — index out of range (1.." .. (#self._elements + 1) .. "): " .. tostring(index), 2)
end end
-- (3) 요소 타입 검증은 `wrapElement`가 한다(위 그 함수 — `isSlot`/`isState`/`isInst`) -- (3) 요소 타입 검증은 `wrapElement`가 한다(위 그 함수 — `isSlot`/`isState`/`isInst`)
local wrapped = wrapElement(element) local wrapped = wrapElement(element)
@ -3316,13 +3316,13 @@ local function wrapElement(v)
if not isState(v) then if not isState(v) then
-- 진단을 위해 핸들러 계층 값은 따로 잡는다(왜 안 되는지 근거가 다르다) -- 진단을 위해 핸들러 계층 값은 따로 잡는다(왜 안 되는지 근거가 다르다)
if isRef(v) or isPreRef(v) or isPostRef(v) or isObserver(v) or isEffect(v) or isModifier(v) then if isRef(v) or isPreRef(v) or isPostRef(v) or isObserver(v) or isEffect(v) or isModifier(v) then
error("Slot: 핸들러 계층 값(Ref/PreRef/PostRef/Observer/Effect/Modifier)은 요소가 될 수 없음") error("Slot: handler-layer values (Ref/PreRef/PostRef/Observer/Effect/Modifier) cannot be elements", 2)
end end
if v == nil or v == None then if v == nil or v == None then
error("Slot: nil/None은 요소가 될 수 없음 — 실제로 마운트 가능한 값만") error("Slot: nil/None cannot be an element — only actually mountable values", 2)
end end
if not isInst(v) then -- 백엔드 주입 술어(위 `native*` 절) if not isInst(v) then -- 백엔드 주입 술어(위 `native*` 절)
error("Slot: 이 백엔드가 마운트할 수 없는 값") error("Slot: this backend cannot mount this value", 2)
end end
return v return v
end end
@ -3608,7 +3608,7 @@ function dispose(value)
-- 안 들어간다(`Relate`는 항상 3-인자 `SetWeak`/2-인자 `GetWeak`, 409행). -- 안 들어간다(`Relate`는 항상 3-인자 `SetWeak`/2-인자 `GetWeak`, 409행).
-- `H-71`로 dedup 기록까지 `SetWeak`이 되며 강/약 짝맞춤이 더 중요해졌다. -- `H-71`로 dedup 기록까지 `SetWeak`이 되며 강/약 짝맞춤이 더 중요해졌다.
if elementOwner:GetWeak(value, OWNER) ~= nil then if elementOwner:GetWeak(value, OWNER) ~= nil then
error("dispose: 이 값은 아직 트리가 살아있길 요구 중임 — 먼저 Remove/Extract 할 것") error("dispose: this value still requires its tree to be alive — Remove/Extract it first", 2)
end end
-- (2) [`H-43`] Slot도 Instance도 아닌 값이 백엔드로 그냥 흘러가지 않게 -- (2) [`H-43`] Slot도 Instance도 아닌 값이 백엔드로 그냥 흘러가지 않게
if isSlot(value) then if isSlot(value) then
@ -3616,7 +3616,7 @@ function dispose(value)
elseif isInst(value) then elseif isInst(value) then
nativeDispose(value) -- 아래 주입 op nativeDispose(value) -- 아래 주입 op
else else
error("dispose: 이 백엔드가 파괴할 수 없는 값") error("dispose: this backend cannot dispose this value", 2)
end end
end end
``` ```

View file

@ -1691,7 +1691,7 @@ function bindLifetime(inst, value)
-- bindLifetime의 게이트는 canBound, canExecute 아님 -- bindLifetime의 게이트는 canBound, canExecute 아님
-- [정정, 2026-08-18 구현 전 QA] 방향이 뒤집혀 있었음 — -- [정정, 2026-08-18 구현 전 QA] 방향이 뒤집혀 있었음 —
-- canBound 참 = 묶어도 됨이라 에러는 not 쪽 -- canBound 참 = 묶어도 됨이라 에러는 not 쪽
error("이미 바인딩된 값") -- 메시지 분기는 위 게이트 스케치 참고 error("bindLifetime: value is already bound", 2) -- 실제 메시지 분기는 위 게이트 스케치 참고
end end
... -- gchold 등록 + gcconn 참조 복사(base/lifecycle-pattern.md) ... -- gchold 등록 + gcconn 참조 복사(base/lifecycle-pattern.md)
end end

View file

@ -73,10 +73,10 @@ ROADMAP 항목 근거인지, 어떻게 실행하는지, 실행 후 뭘 확인해
| 파일 | 검증 대상 | 근거 문서 | | 파일 | 검증 대상 | 근거 문서 |
|---|---|---| |---|---|---|
| `01-two-pass-array-hash-order.luau` | 배열 파트(children/Ref) 먼저, 해시 파트(프로퍼티/이벤트) 나중이라는 두 패스 순회 계약 | `dispatch-core-plan.md` "props 순회 순서", ROADMAP M0-4 | | `01-two-pass-array-hash-order.luau` | **[2026-08-31 폐기 — `done/`로 이동, `spec.drive.luau` 1번이 대체(상시 회귀)]** 배열 파트(children/Ref) 먼저, 해시 파트(프로퍼티/이벤트) 나중이라는 순회 계약 — 파일 자체는 폐기된 두 패스 구현(`F-4-1` 이전) 기준이라 역사로만 남음. 상태의 소스는 `STATUS.md` | `dispatch-core-plan.md` "props 순회 순서", ROADMAP M0-4 |
| `02-none-sentinel-vs-nil-holes.luau` | **[2026-08-09 커밋 f198fd9 반영해 전면 재작성]** 순서가 중요한 배열(PreRef pre-pass, sourceList)은 `None` 소진이 맞고, 순서가 안 중요하고 재사용이 필요한 배열(Ref 콜백/대기자)은 `nil`+슬롯 재사용이 맞다는 최종 구분 + `None`을 잘못 쓰면 배열이 무한정 자라는 버그의 정량적 재현 | `ref-plan.md` "왜 None이 아니라 nil인가"(2026-08-09 열한 번째 세션 최종 정정), ROADMAP M0-4 | | `02-none-sentinel-vs-nil-holes.luau` | **[2026-08-09 커밋 f198fd9 반영해 전면 재작성]** 순서가 중요한 배열(PreRef pre-pass, sourceList)은 `None` 소진이 맞고, 순서가 안 중요하고 재사용이 필요한 배열(Ref 콜백/대기자)은 `nil`+슬롯 재사용이 맞다는 최종 구분 + `None`을 잘못 쓰면 배열이 무한정 자라는 버그의 정량적 재현 | `ref-plan.md` "왜 None이 아니라 nil인가"(2026-08-09 열한 번째 세션 최종 정정), ROADMAP M0-4 |
| `03-recursive-store-bind-dispatch.luau` | `process`/`retract` 재귀 재-dispatch 기본 모델, 우선순위 스캔 | `dispatch-core-plan.md` "확정된 디스패치 모델", ROADMAP M0-3 | | `03-recursive-store-bind-dispatch.luau` | `process`/`retract` 재귀 재-dispatch 기본 모델, 우선순위 스캔 | `dispatch-core-plan.md` "확정된 디스패치 모델", ROADMAP M0-3 |
| `04-dispatch-chain-retractFrom.luau` | **[⚠️ 2026-08-13 열네 번째 세션: 하강 diff 확정으로 낡음 → `rewrite-required/`]** 아래는 옛 모델 기준 설명 — **[2026-08-13 감사에서 전면 재작성 + 파일명 변경]** 인덱스 기반 `chains`/`Dispatch.retractFrom`이 다단 재귀 위임에서 정확한지 — 3단 체인이 인덱스 1/2/3으로 안 겹치고 쌓이는지(= `State<State<T>>` **정상 동작**, UB 아님), 안/바깥 store 재발행 시 깊은 인덱스부터 정리되는지, hint가 target 인덱스에만 가는지 + **음성 대조군**: `chains:SetStrong``handler.process` 뒤에 두면 최초 마운트에서 하위 retractor가 유실되는 버그 재현. 옛 버전은 핸들러 identity 기반 추적과 "중복 push 즉시 error" 가드를 검증했는데 그 가드는 다섯 번째 세션 재설계로 **없어져서** 설계와 정반대를 테스트하고 있었음 | `dispatch-core-plan.md` "Dispatch 체인"(2026-08-13 다섯 번째 세션 재설계) + 2026-08-13 감사 | | `04-dispatch-chain-retractFrom.luau` | **[2026-08-31 폐기 — `done/`로 이동, `spec.dispatch.luau`가 대체(`H-215` (a)), 잔여 `StoreBind` 경유 경로는 ROADMAP M4 mock 항목. 상태의 소스는 `STATUS.md`]** **[⚠️ 2026-08-13 열네 번째 세션: 하강 diff 확정으로 낡음 → `rewrite-required/`]** 아래는 옛 모델 기준 설명 — **[2026-08-13 감사에서 전면 재작성 + 파일명 변경]** 인덱스 기반 `chains`/`Dispatch.retractFrom`이 다단 재귀 위임에서 정확한지 — 3단 체인이 인덱스 1/2/3으로 안 겹치고 쌓이는지(= `State<State<T>>` **정상 동작**, UB 아님), 안/바깥 store 재발행 시 깊은 인덱스부터 정리되는지, hint가 target 인덱스에만 가는지 + **음성 대조군**: `chains:SetStrong``handler.process` 뒤에 두면 최초 마운트에서 하위 retractor가 유실되는 버그 재현. 옛 버전은 핸들러 identity 기반 추적과 "중복 push 즉시 error" 가드를 검증했는데 그 가드는 다섯 번째 세션 재설계로 **없어져서** 설계와 정반대를 테스트하고 있었음 | `dispatch-core-plan.md` "Dispatch 체인"(2026-08-13 다섯 번째 세션 재설계) + 2026-08-13 감사 |
| `05-store-state-diamond-propagation.luau` | **[2026-08-29 폐기 — `done/`로 이동, `spec.state`/`spec.effect` 3번이 대체]** push-invalidate/pull-recompute가 다이아몬드 의존성에서 중복 재계산 없이 동작하는지. **[2026-08-19 재작성 → 2026-08-21 다시 `rewrite-required/`]** 2026-08-19엔 당시 모델("emit은 항상 전파, 중복 재계산은 `:Get()` 시점 캐시로만 막힘")로 짜서 통과했으나, **소스 에포크 비교 채택(`base/state-epoch-plan.md`)으로 다이아몬드 두 번째 통지가 접히게 되어** 핵심 assert가 정반대가 됨 — 이제 Observer는 변경당 **1회**만 울어야 한다. 상태의 소스는 `STATUS.md` | ROADMAP M0-1 | | `05-store-state-diamond-propagation.luau` | **[2026-08-29 폐기 — `done/`로 이동, `spec.state`/`spec.effect` 3번이 대체]** push-invalidate/pull-recompute가 다이아몬드 의존성에서 중복 재계산 없이 동작하는지. **[2026-08-19 재작성 → 2026-08-21 다시 `rewrite-required/`]** 2026-08-19엔 당시 모델("emit은 항상 전파, 중복 재계산은 `:Get()` 시점 캐시로만 막힘")로 짜서 통과했으나, **소스 에포크 비교 채택(`base/state-epoch-plan.md`)으로 다이아몬드 두 번째 통지가 접히게 되어** 핵심 assert가 정반대가 됨 — 이제 Observer는 변경당 **1회**만 울어야 한다. 상태의 소스는 `STATUS.md` | ROADMAP M0-1 |
| `06-component-boundary-nil-hole-props.luau` | `props.Modifier or None` 관용구가 컴포넌트 경계 nil-hole을 막는지 + `Params` 타입 체크 | `component-composition-plan.md` "필수 관용구", ROADMAP M0-5 | | `06-component-boundary-nil-hole-props.luau` | `props.Modifier or None` 관용구가 컴포넌트 경계 nil-hole을 막는지 + `Params` 타입 체크 | `component-composition-plan.md` "필수 관용구", ROADMAP M0-5 |
| `07-relate-weak-table-gc.luau` | `Relate`의 lazy 서브테이블 생성 + weak-key GC가 실제로 동작하는지 | `relate-plan.md` "M2 착수 시 실측 확인" **[2026-08-13 보강]** 4번 섹션 신설 — `_countEntries()`(테스트 전용) + weak-value canary로 **"inst가 죽으면 중첩 StrongMap 안의 payload까지 연쇄 GC되는가"를 직접 검증**(원래는 sanity check만 하고 헤더의 핵심 주장은 미검증이었음). 파일이 스스로 적어둔 "weak table 엔트리를 셀 표준 API가 없다"는 전제도 틀렸음 — outer가 `__mode="k"`라 GC 후 `pairs`에서 사라짐 | | `07-relate-weak-table-gc.luau` | `Relate`의 lazy 서브테이블 생성 + weak-key GC가 실제로 동작하는지 | `relate-plan.md` "M2 착수 시 실측 확인" **[2026-08-13 보강]** 4번 섹션 신설 — `_countEntries()`(테스트 전용) + weak-value canary로 **"inst가 죽으면 중첩 StrongMap 안의 payload까지 연쇄 GC되는가"를 직접 검증**(원래는 sanity check만 하고 헤더의 핵심 주장은 미검증이었음). 파일이 스스로 적어둔 "weak table 엔트리를 셀 표준 API가 없다"는 전제도 틀렸음 — outer가 `__mode="k"`라 GC 후 `pairs`에서 사라짐 |

View file

@ -3,7 +3,9 @@
> 마지막 갱신: **2026-08-31** — M3 단위 1이 `01`을 닫음: 재작성이 물어야 했던 > 마지막 갱신: **2026-08-31** — M3 단위 1이 `01`을 닫음: 재작성이 물어야 했던
> 언어 동작(일반화 `for`의 배열→해시 순서)을 `quad-base/test/spec.drive.luau` > 언어 동작(일반화 `for`의 배열→해시 순서)을 `quad-base/test/spec.drive.luau`
> 1번이 상시 회귀로 실측하므로 **폐기 → `done/`**, 재작성 안 함(round12 brief > 1번이 상시 회귀로 실측하므로 **폐기 → `done/`**, 재작성 안 함(round12 brief
> §6 사용자 승인). 직전 갱신 **2026-08-29** — M2 구현이 스파이크 셋을 닫음: `05`(다이아몬드, > §6 사용자 승인). **같은 날 `04`도 폐기 → `done/`**(`H-215` (a) 사용자 확정 —
> `spec.dispatch.luau`가 체인 계약을 실측, 잔여인 실제 `StoreBind` 경유
> 재귀 재발행은 `ROADMAP.md` M4 mock 테스트 항목이 짐). 직전 갱신 **2026-08-29** — M2 구현이 스파이크 셋을 닫음: `05`(다이아몬드,
> `spec.state`/`spec.effect` 3번이 대체)·`15`(타입팩, `H-176` 기각 실측) **폐기 → > `spec.state`/`spec.effect` 3번이 대체)·`15`(타입팩, `H-176` 기각 실측) **폐기 →
> `done/`**, 신규 `26`(`:Apply` 교집합 오버로드, `H-179`) `done/` 직행, "만들어야 할 > `done/`**, 신규 `26`(`:Apply` 교집합 오버로드, `H-179`) `done/` 직행, "만들어야 할
> 스파이크"의 중간 State GC(`spec.state` 11번)·`CheckedQuad` 재실행(`23`) 닫힘 — > 스파이크"의 중간 State GC(`spec.state` 11번)·`CheckedQuad` 재실행(`23`) 닫힘 —
@ -54,9 +56,9 @@
| 폴더 | 뜻 | 개수 | 누가 처리 | | 폴더 | 뜻 | 개수 | 누가 처리 |
|---|---|---|---| |---|---|---|---|
| `review-required/` | **설계가 걸림 — 사람 결정 필요** | **0** | ⭐ 사용자 | | `review-required/` | **설계가 걸림 — 사람 결정 필요** | **0** | ⭐ 사용자 |
| `rewrite-required/` | 스파이크가 낡음(코드가 깨졌거나, 설계가 바뀌어 옛 모델을 검증 중) | 8 | 에이전트 | | `rewrite-required/` | 스파이크가 낡음(코드가 깨졌거나, 설계가 바뀌어 옛 모델을 검증 중) | 6 | 에이전트 |
| `not-run/` | 이 환경에서 못 돌림(Studio 전용) | 0(+헬퍼 1) | 사용자 or MCP 연결 후 에이전트 | | `not-run/` | 이 환경에서 못 돌림(Studio 전용) | 0(+헬퍼 1) | 사용자 or MCP 연결 후 에이전트 |
| `done/` | 통과 or 판정 끝, 더 할 일 없음 | 16 | — | | `done/` | 통과 or 판정 끝, 더 할 일 없음 | 18 | — |
**⚠️ [2026-08-25 신설] 타입 스파이크는 `./scripts/test.sh`가 하는 리링크를 **⚠️ [2026-08-25 신설] 타입 스파이크는 `./scripts/test.sh`가 하는 리링크를
먼저 거쳐야 한다.** `luau` CLI가 심볼릭 링크를 못 타는데(디렉토리·파일 둘 먼저 거쳐야 한다.** `luau` CLI가 심볼릭 링크를 못 타는데(디렉토리·파일 둘
@ -107,9 +109,12 @@
`10`**Studio 전용이라 재작성해도 이 환경에서는 못 돌린다** — 재작성 `10`**Studio 전용이라 재작성해도 이 환경에서는 못 돌린다** — 재작성
후 다시 `not-run/`으로 내려가 사용자/MCP를 기다리는 자리다. 후 다시 `not-run/`으로 내려가 사용자/MCP를 기다리는 자리다.
**[2026-08-21] `01``05`가 합류** — 둘 다 같은 "설계가 바뀐" 유형이다. **[2026-08-21 신설 — 2026-08-29·2026-08-31에 해소됨]** `01``05`가 이때
통과 상태로 `done/`에 두면 `01`은 구현이 안 하는 두 루프 순회를, `05` 합류했었다 — 둘 다 같은 "설계가 바뀐" 유형(통과 상태로 `done/`에 두면 `01`
**이제 접히는 중복 통지가 안 접힌다는 것**을 "검증됨"으로 오독하게 된다. 구현이 안 하는 두 루프 순회를, `05`는 이제 접히는 중복 통지가 안 접힌다는
것을 "검증됨"으로 오독). **지금은 둘 다 여기 없다**`05`는 2026-08-29에,
`01`은 2026-08-31에 각각 spec이 대체하며 폐기·`done/` 이동(아래 `done/` 표의
각 행이 소스).
**⭐ [2026-08-26, 8라운드 `H-122`/`H-123`] `11-modifier-illegal-value-error` **⭐ [2026-08-26, 8라운드 `H-122`/`H-123`] `11-modifier-illegal-value-error`
합류했다** — 그 파일의 Store 생성자가 **eager `Source(v)` 모델**을 코드로 합류했다** — 그 파일의 Store 생성자가 **eager `Source(v)` 모델**을 코드로
@ -152,7 +157,6 @@
| 파일 | 상태 | 무엇을 고쳐야 하나 | | 파일 | 상태 | 무엇을 고쳐야 하나 |
|---|---|---| |---|---|---|
| `04-dispatch-chain-retractFrom.luau` | 옛 모델 기준으로는 ✅ 통과였음 | (1) `chains` 슬롯이 `{handler, retractor}`가 되고 `Dispatch.process`가 핸들러를 먼저 비교하는 **하강 diff**로 재작성, (2) `retractFrom`은 **3-인자**(힌트 인자 없음), (3) "힌트가 target 인덱스에만 간다"를 검증하던 부분은 **정반대**로 뒤집힘 — 이제 각 레벨이 자기 값을 받는지를 검증해야 함. **살릴 것**: `chains:SetStrong` 순서 음성 대조군(그 버그는 새 모델에서도 그대로 유효) |
| `19-ownership-refcount-relate-patterns.luau` | A/C ✅ 유효, **B 섹션이 낡음** | B가 검증하던 "공개 `AttributeKey(name)` + 인덱스 1 점유 체크"가 폐기됨 — **그룹 전용 키 + `AttributeKeyHandler`의 이름 claim**으로 재작성하고, 음성 대조군도 "두 그룹이 같은 이름 → 즉시 error", "그룹↔직접 쓰기 → 즉시 error"로 바꿀 것(0-Z 확정 내용). A/C는 손댈 것 없음 | | `19-ownership-refcount-relate-patterns.luau` | A/C ✅ 유효, **B 섹션이 낡음** | B가 검증하던 "공개 `AttributeKey(name)` + 인덱스 1 점유 체크"가 폐기됨 — **그룹 전용 키 + `AttributeKeyHandler`의 이름 claim**으로 재작성하고, 음성 대조군도 "두 그룹이 같은 이름 → 즉시 error", "그룹↔직접 쓰기 → 즉시 error"로 바꿀 것(0-Z 확정 내용). A/C는 손댈 것 없음 |
| `22-runtime-ref-preref-postref-brand.luau` | 옛 `Brand` API 기준으로는 ✅ 통과였음 | **[2026-08-21] `Brand`가 인스턴스 브랜드로 재작성됨** — 파일 안의 `Brand.set(x, tag)`/`Brand.get(x)`/`XxxTag` 변수를 `Brand()` + `SomeBrand:register(x)`/`SomeBrand:is(x)`로 바꿔 쓸 것(`base/brand-plan.md`). **검증 대상(`isPreRef`/`isPostRef` 배타 + 둘 다 `isRef`엔 `true`, Leaf 핸들러 흉내)은 그대로**라 assert는 손댈 게 없다. **새로 넣을 것**: 다중 태깅이 실제로 되는지 — 한 값을 두 브랜드에 등록하고 양쪽 `:is`가 다 `true`인지(`Source`가 `SourceBrand`+`EpochBrand`인 자리, `base/state-epoch-plan.md` §2) | | `22-runtime-ref-preref-postref-brand.luau` | 옛 `Brand` API 기준으로는 ✅ 통과였음 | **[2026-08-21] `Brand`가 인스턴스 브랜드로 재작성됨** — 파일 안의 `Brand.set(x, tag)`/`Brand.get(x)`/`XxxTag` 변수를 `Brand()` + `SomeBrand:register(x)`/`SomeBrand:is(x)`로 바꿔 쓸 것(`base/brand-plan.md`). **검증 대상(`isPreRef`/`isPostRef` 배타 + 둘 다 `isRef`엔 `true`, Leaf 핸들러 흉내)은 그대로**라 assert는 손댈 게 없다. **새로 넣을 것**: 다중 태깅이 실제로 되는지 — 한 값을 두 브랜드에 등록하고 양쪽 `:is`가 다 `true`인지(`Source`가 `SourceBrand`+`EpochBrand`인 자리, `base/state-epoch-plan.md` §2) |
| `16-type-store-key-typefunction.luau` | 옛 접근 기준으로는 ✅ 통과였음 | **[2026-08-25] 검증 대상이 폐기됨** — `WrapStore`/`ProcessStoreType` 합성 자체가 사라졌다. **재작성 지침**: 타입 함수 없는 평범한 레코드 모양(`base/store-plan.md`의 "`store.key` 레코드 필드 타이핑" 절)을 검증하고, 음성 대조군에 **예약 키 충돌**(`CheckReservedKeys<keyof<T>>`가 `types.never`로 무너뜨리는지 — **[2026-08-26 `H-112`]** 인자가 `T`가 아니라 `keyof<T>`다)과 **없는 키 접근**을 포함할 것 | | `16-type-store-key-typefunction.luau` | 옛 접근 기준으로는 ✅ 통과였음 | **[2026-08-25] 검증 대상이 폐기됨** — `WrapStore`/`ProcessStoreType` 합성 자체가 사라졌다. **재작성 지침**: 타입 함수 없는 평범한 레코드 모양(`base/store-plan.md`의 "`store.key` 레코드 필드 타이핑" 절)을 검증하고, 음성 대조군에 **예약 키 충돌**(`CheckReservedKeys<keyof<T>>`가 `types.never`로 무너뜨리는지 — **[2026-08-26 `H-112`]** 인자가 `T`가 아니라 `keyof<T>`다)과 **없는 키 접근**을 포함할 것 |
@ -173,8 +177,11 @@
(개수는 위 표와 폴더가 소스 — 여기서 다시 세지 않는다.) (개수는 위 표와 폴더가 소스 — 여기서 다시 세지 않는다.)
**지금 `done/`에 있는 런타임 스파이크는 `02`/`03`/`06`/`07`/`11`/`17`/`18`/`20`, **[2026-08-31 기준] 지금 `done/`에 있는 런타임(비타입) 스파이크는
전원 통과**(crash 0 / FAIL 0). 나머지는 타입 스파이크다. `01`/`02`/`03`/`04`/`05`/`06`/`07`/`17`/`18`/`20`** — `11`은 2026-08-26에
`rewrite-required/`로 나갔고(위 표), `01`/`04`/`05`는 폐기 상태로 들어왔다
(셋은 "통과"가 아니라 spec 대체 폐기 — 각 행 참고), 나머지는 전원 통과
(crash 0 / FAIL 0). 나머지는 타입 스파이크다.
**[2026-08-21] `22`는 여기서 빠졌다** — `Brand` 인스턴스 브랜드 재작성으로 **[2026-08-21] `22`는 여기서 빠졌다** — `Brand` 인스턴스 브랜드 재작성으로
파일이 쓰는 `Brand.set`/`Brand.get`이 옛 API가 되어 `rewrite-required/` 파일이 쓰는 `Brand.set`/`Brand.get`이 옛 API가 되어 `rewrite-required/`
이동(검증 대상 자체는 유효, 위 표의 재작성 지침 참고). 이동(검증 대상 자체는 유효, 위 표의 재작성 지침 참고).
@ -189,6 +196,7 @@
| 파일 | 확인된 것 | | 파일 | 확인된 것 |
|---|---| |---|---|
| `26-type-apply-object-factory-overload.luau` (타입체크 전용) | ✅ **[2026-08-29 M2 단위 4]** `state:Apply(factory)`의 파라미터 타입은 **교집합 오버로드**(함수 팩토리 제네릭 `U` / `__apply` 객체 `any` 반환) — 유니온 하나는 필드가 더 있는 객체(`Blocker`)를 못 받는다. 기대 진단 2건(음성 대조군)만 — 근거: `qa-request/m2-implementation-round11.md` `H-179`, `quad-types/src/init.luau` `State<T>.Apply` | | `26-type-apply-object-factory-overload.luau` (타입체크 전용) | ✅ **[2026-08-29 M2 단위 4]** `state:Apply(factory)`의 파라미터 타입은 **교집합 오버로드**(함수 팩토리 제네릭 `U` / `__apply` 객체 `any` 반환) — 유니온 하나는 필드가 더 있는 객체(`Blocker`)를 못 받는다. 기대 진단 2건(음성 대조군)만 — 근거: `qa-request/m2-implementation-round11.md` `H-179`, `quad-types/src/init.luau` `State<T>.Apply` |
| `04-dispatch-chain-retractFrom.luau` | **[2026-08-31 폐기 → `done/`로 이동, 재작성 안 함 — `H-215` (a) 사용자 확정]** M3 단위 1의 `quad-base/test/spec.dispatch.luau`(12절)가 재작성 지침의 검증 대상 — 하강 diff (A)/(B), 3-인자 `retractFrom`, 각 레벨이 자기 값을 받는 것, `SetStrong` 순서 음성 대조군 — 을 실제 구현에 대고 실측한다. **잔여 몫 하나**(실제 `StoreBind` 경유 재귀 재발행 경로 — spec은 로컬 wrapping 핸들러 근사)는 `ROADMAP.md` M4의 mock 테스트 항목이 명시적으로 진다. 파일은 역사로만 남김. 아래는 폐기 전 상태: 옛 모델(핸들러 identity 추적) 기준 ✅ 통과였고 하강 diff 확정으로 재작성 대기였음 |
| `01-two-pass-array-hash-order.luau` | **[2026-08-31 폐기 → `done/`로 이동, 재작성 안 함]** M3 단위 1의 `quad-base/test/spec.drive.luau` 1번이 재작성이 물어야 했던 그 질문 — **일반화 `for` 한 번**이 배열 파트 전체를 해시보다 먼저, 배열 안은 index 순서로 주는가(`F-4-1`) — 를 실제 `Dispatch.drive`에 대고 상시 회귀로 실측한다(round12 brief §6, 사용자 승인). 파일은 역사로만 남긴다. 아래는 폐기 전 상태: 숫자 `for` + 일반화 `for` **두 루프** 버전이라 옛 형태 기준으로는 ✅ 통과였으나, 구현이 단일 일반화 `for`로 정정되며(`base/dispatch-core-plan.md` "props 순회 순서" 절) 언어 동작 자체를 묻도록 재작성 대기였음 | | `01-two-pass-array-hash-order.luau` | **[2026-08-31 폐기 → `done/`로 이동, 재작성 안 함]** M3 단위 1의 `quad-base/test/spec.drive.luau` 1번이 재작성이 물어야 했던 그 질문 — **일반화 `for` 한 번**이 배열 파트 전체를 해시보다 먼저, 배열 안은 index 순서로 주는가(`F-4-1`) — 를 실제 `Dispatch.drive`에 대고 상시 회귀로 실측한다(round12 brief §6, 사용자 승인). 파일은 역사로만 남긴다. 아래는 폐기 전 상태: 숫자 `for` + 일반화 `for` **두 루프** 버전이라 옛 형태 기준으로는 ✅ 통과였으나, 구현이 단일 일반화 `for`로 정정되며(`base/dispatch-core-plan.md` "props 순회 순서" 절) 언어 동작 자체를 묻도록 재작성 대기였음 |
| `05-store-state-diamond-propagation.luau` | **[2026-08-29 폐기 → `done/`로 이동, 재작성 안 함]** M2 단위 2·3의 `quad-base/test/spec.state.luau` 3번(다이아몬드에서 두 번째 도착이 규칙 3으로 접힘, 조인 1회 계산)·`spec.effect.luau` 3번(Effect도 1회)이 실제 구현에서 같은 것을 고정한다 — 이 스파이크가 물으려던 "변경당 1회"의 답. 아래는 폐기 전 상태: 2026-08-19 재작성분은 그 시점 모델 기준 ✅ 통과였음 — 근거: **[2026-08-21] 모델이 또 바뀌었다** — 소스 에포크 비교 채택(`base/state-epoch-plan.md`)으로 다이아몬드에서 **두 번째 통지가 접힌다**. 그래서 이 스파이크의 핵심 assert("`:Get()`을 안 부르는 Observer가 변경당 경로 수(2)만큼 운다")가 **정반대**가 됐다 — 이제 **변경당 1회**여야 한다. **살릴 것**: `invalid` 기반 dedup이면 두 번째 변경부터 침묵하는 것을 잡는 음성 대조군(그 금지는 지금도 유효). **새로 넣을 것**: DFS 도중 `Get()`이 섞인 값을 캐시하던 glitch가 에포크로 사라지는지(그 문서 §1의 시나리오) | | `05-store-state-diamond-propagation.luau` | **[2026-08-29 폐기 → `done/`로 이동, 재작성 안 함]** M2 단위 2·3의 `quad-base/test/spec.state.luau` 3번(다이아몬드에서 두 번째 도착이 규칙 3으로 접힘, 조인 1회 계산)·`spec.effect.luau` 3번(Effect도 1회)이 실제 구현에서 같은 것을 고정한다 — 이 스파이크가 물으려던 "변경당 1회"의 답. 아래는 폐기 전 상태: 2026-08-19 재작성분은 그 시점 모델 기준 ✅ 통과였음 — 근거: **[2026-08-21] 모델이 또 바뀌었다** — 소스 에포크 비교 채택(`base/state-epoch-plan.md`)으로 다이아몬드에서 **두 번째 통지가 접힌다**. 그래서 이 스파이크의 핵심 assert("`:Get()`을 안 부르는 Observer가 변경당 경로 수(2)만큼 운다")가 **정반대**가 됐다 — 이제 **변경당 1회**여야 한다. **살릴 것**: `invalid` 기반 dedup이면 두 번째 변경부터 침묵하는 것을 잡는 음성 대조군(그 금지는 지금도 유효). **새로 넣을 것**: DFS 도중 `Get()`이 섞인 값을 캐시하던 glitch가 에포크로 사라지는지(그 문서 §1의 시나리오) |
| `15-type-compute-trailing-deps-typepack.luau` | **[2026-08-28 폐기 → `done/`로 이동, 재작성 안 함]** M2 단위 2가 실제 `quad-types` 선언에서 타입팩 형태를 실측해 기각했다(`qa-request/m2-implementation-round11.md` `H-176`: strict에서 콜백 dep 추론이 깨져 정상 호출까지 막힘 → deps 자리 `...any`). 이 스파이크가 물으려던 (B)의 답이 나왔으므로 파일은 역사로만 `done/`에 남긴다. 아래는 폐기 전 상태: **파싱 실패**(SyntaxError) — 근거: 음성 대조군의 타입 표기가 `TypeError`가 아니라 `SyntaxError`로 걸려 **파일 전체가 아무것도 검증 못 함** — 대조군을 별도 파일/블록으로 격리 | | `15-type-compute-trailing-deps-typepack.luau` | **[2026-08-28 폐기 → `done/`로 이동, 재작성 안 함]** M2 단위 2가 실제 `quad-types` 선언에서 타입팩 형태를 실측해 기각했다(`qa-request/m2-implementation-round11.md` `H-176`: strict에서 콜백 dep 추론이 깨져 정상 호출까지 막힘 → deps 자리 `...any`). 이 스파이크가 물으려던 (B)의 답이 나왔으므로 파일은 역사로만 `done/`에 남긴다. 아래는 폐기 전 상태: **파싱 실패**(SyntaxError) — 근거: 음성 대조군의 타입 표기가 `TypeError`가 아니라 `SyntaxError`로 걸려 **파일 전체가 아무것도 검증 못 함** — 대조군을 별도 파일/블록으로 격리 |
@ -213,8 +221,9 @@
### 특별히 중요한 통과 3건 ### 특별히 중요한 통과 3건
**`04` — 직전 감사가 찾은 버그가 음성 대조군으로 재현됨**(파일은 지금 **`04` — 직전 감사가 찾은 버그가 음성 대조군으로 재현됨**(**[2026-08-31]**
`rewrite-required/`에 있음 — 아래 관측 자체는 새 모델에서도 유효) 파일은 폐기돼 `done/`에 있음(`H-215` (a)) — 아래 관측 자체는 새 모델에서도
유효하고, 같은 음성 대조가 `spec.dispatch.luau` 6번에 상시 회귀로 산다)
| 관측 지점 | 정상(수정본) | 대조군(버그) | | 관측 지점 | 정상(수정본) | 대조군(버그) |
|---|---|---| |---|---|---|

View file

@ -16,9 +16,12 @@
| 번호 | 갈래 | 단위 | 심각도 | 한 줄 | 상태 | | 번호 | 갈래 | 단위 | 심각도 | 한 줄 | 상태 |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| `H-212` | ① | 1 | 🟢 | `dispatch-core-plan.md` "Dispatch 체인" 의사코드의 error 세 자리가 한국어·`level` 없음 — 그 절(2026-08-13)보다 늦게 확정된 `architecture.md` error 계약(영어, level 이분, 2026-08-25)이 미반영 | ✅ 반영(`dispatch-core-plan.md` 의사코드 영어+level, `H-212` 문단) | | `H-212` | ① | 1 | 🟢 | `dispatch-core-plan.md` "Dispatch 체인" 의사코드의 error 세 자리가 한국어·`level` 없음 — 그 절(2026-08-13)보다 늦게 확정된 `architecture.md` error 계약(영어, level 이분, 2026-08-25)이 미반영 | ✅ 반영(`dispatch-core-plan.md` 의사코드 영어+level, `H-212` 문단). **[감사 3라운드 확장]** 같은 문서의 Length/Offset 절 두 자리(`getOffsetAt` 한국어 혼용 / `recompute` 한국어·level 없음)와 주입 op 스텁·가로채기 예시 메시지도 같은 부류라 같이 정정 — 스텁의 `level` 숫자만은 구현 시점(M5/M10) 몫으로 남김(도착지까지의 프레임 수를 문서 시점에 못 셈) |
| `H-213` | ① | 1 | 🟢 | `HANDLER_PRIORITY_*` 상수의 실제 숫자값을 어느 문서도 안 정했다 — 문서가 정한 건 이름·순서(HIGH > NORMAL > LOW > FALLBACK)·열린 공간(± 오프셋)뿐 | ✅ 구현이 채움: 1000 / 0 / -1000 / -1000000 (밴드 간 ± 오프셋 여유, `Dispatch/init.luau` 주석) | | `H-213` | ① | 1 | 🟢 | `HANDLER_PRIORITY_*` 상수의 실제 숫자값을 어느 문서도 안 정했다 — 문서가 정한 건 이름·순서(HIGH > NORMAL > LOW > FALLBACK)·열린 공간(± 오프셋)뿐 | ✅ 구현이 채움: 1000 / 0 / -1000 / -1000000 (밴드 간 ± 오프셋 여유, `Dispatch/init.luau` 주석) |
| `H-214` | **②** | 1 | 🟡 | `listHandlers`가 "이름/priority를 반환"이고 동률 경고·`quad-debug` 체인 덤프도 핸들러 이름을 원하는데, Handler 계약(3종)엔 `name` 필드가 없다 — 새 필드라 자율 반영 불가 | ⏳ §4 대기 — 코드는 핸들러 객체 배열 반환 + `TODO(H-214)` 마커 | | `H-214` | **②** | 1 | 🟡 | `listHandlers`가 "이름/priority를 반환"이고 동률 경고·`quad-debug` 체인 덤프도 핸들러 이름을 원하는데, Handler 계약(3종)엔 `name` 필드가 없다 — 새 필드라 자율 반영 불가 | ✅ **(a) 사용자 확정**(2026-08-31 §4 회신) — 계약에 선택 필드 `name: string?`(진단 전용). `quad-types`·`Dispatch/init.luau`(동률 경고에 이름, `TODO` 마커 제거)·`dispatch-core-plan.md` "핸들러 계약" 절·`spec.dispatch` 11번 반영 |
| `H-215` | **②** | 1 | 🟢 | 스파이크 `04`(Dispatch 체인 retractFrom) 처분 — `spec.dispatch.luau`가 검증 대상 대부분을 실측했는데 재귀 재발행 경로는 로컬 wrapping 핸들러 근사라 실제 `StoreBind`(M4) 몫이 남는다. 폐기는 `01`처럼 사용자 승인 사안(감사 2라운드 발견) | ✅ **(a) 사용자 확정**(2026-08-31 §4 회신) — 폐기·`done/` 이동, 잔여 몫(실제 `StoreBind` 경유 재발행)은 ROADMAP M4 mock 테스트 항목에 명시. `STATUS.md`·`README.md`·ROADMAP 재검증 대기 절 `[x]` |
| `H-216` | ① | 1 | 🟢 | `slot-plan.md`의 옛 error 예시 11곳(감사 5라운드가 계수 정정 — 처음 12로 잘못 셌다)이 한국어·`level` 없음(대표: `Slot:Add` 범위 검증, 요소 타입 3종, `dispose` 둘) — `H-212`와 같은 부류로, 그 문서 자신이 별도 절에서 "level 2, 영어" 규칙을 알고 있으면서 예시가 안 따라온 자리. M3 단위 2~4·M6이 옮겨 적기 전에 정리(감사 4라운드 발견) | ✅ 반영(`slot-plan.md` — 사용자 입력 검증은 level 2, `releaseOwner` 소유권 추적 파손만 내부 불변식 level 1) |
| `H-217` | ① | 1 | 🟢 | 같은 부류 마지막 4곳 — `attribute-plan.md`(그룹 이중 배치)/`debounce-throttle-plan.md`(Leading·Trailing 둘 다 false)/`ref-plan.md`(`PreRef` 재사용 — 파이프라인 의사코드의 `"PreRef instance reused"`와 다른 문구로 갈라져 있던 것도 통일)/`source-state-plan.md`(`bindLifetime` 이중 바인드 — mock 실구현 문구에 맞춤)가 한국어·`level` 없음(감사 5라운드 전수 스윕) | ✅ 반영 — 전부 영어+level 2. **base/ 코드 리터럴의 한국어 error는 이제 0**(잔여는 산문·옛 모델 인용·주석뿐) |
### `H-212` — base 의사코드 error가 error 계약 이전 표기로 남아 있었다 (①) ### `H-212` — base 의사코드 error가 error 계약 이전 표기로 남아 있었다 (①)
@ -43,8 +46,11 @@
### `H-214``listHandlers`/동률 경고가 원하는 핸들러 "이름"이 계약에 없다 (②) ### `H-214``listHandlers`/동률 경고가 원하는 핸들러 "이름"이 계약에 없다 (②)
`dispatch-core-plan.md` "우선순위 동률/매치 실패 처리" 절: *"`Dispatch.listHandlers()`는 `dispatch-core-plan.md` "우선순위 동률/매치 실패 처리" 절: *"`Dispatch.listHandlers()`는
현재 등록된 전체 핸들러(이름/priority)를 **반환**"*. `research/debug-tooling-plan.md` 현재 등록된 전체 핸들러(이름/priority)를 **반환**"*. **[감사 3라운드 정정]**
쪽 서술(체인 슬롯을 "이름으로 바로 덤프")도 같은 걸 전제한다. 그런데 핸들러 "체인 슬롯을 이름으로 바로 덤프" 서술도 **같은 문서**의 "부수 효과 —
quad-debug에 유리" 항목에 있다(처음엔 `research/debug-tooling-plan.md`로 잘못
적었었다 — 그 파일엔 그 구절이 없고, 대신 핸들러가 **선택적으로** 구현하는
`describe` 훅(가칭, 미정)이 이름 관련 전례로 있다). 그런데 핸들러
계약은 `isHandlable`/`priority`/`process` **3종으로 못 박혀** 있고(같은 문서 계약은 `isHandlable`/`priority`/`process` **3종으로 못 박혀** 있고(같은 문서
"핸들러 계약" 절: *"다음 3개를 제공하는"*), `name`을 붙이는 건 **새 필드**라 "핸들러 계약" 절: *"다음 3개를 제공하는"*), `name`을 붙이는 건 **새 필드**라
규약 §2의 ② 갈래다. 기각 이력 grep: Handler에 이름 필드를 검토·기각한 기록 규약 §2의 ② 갈래다. 기각 이력 grep: Handler에 이름 필드를 검토·기각한 기록
@ -54,11 +60,22 @@
동률 경고 print는 priority 값만 찍는다. 코드 마커 `TODO(H-214)` 1곳 동률 경고 print는 priority 값만 찍는다. 코드 마커 `TODO(H-214)` 1곳
(`Dispatch/init.luau`의 `listHandlers`). (`Dispatch/init.luau`의 `listHandlers`).
**[2026-08-31 종결 — (a) 사용자 확정]** 계약에 선택 필드 `name: string?`
(진단 전용 — 동률 경고·`listHandlers`·나중의 `quad-debug` 덤프, 스캔·매치
무영향). 반영: `quad-types` `Handler` 타입 / `Dispatch/init.luau`(동률 경고에
양쪽 이름, 마커 제거) / `dispatch-core-plan.md` "핸들러 계약" 절 신설 항목 /
`spec.dispatch` 11번(이름 왕복·부재 시 nil). 코드 마커 0.
## §4 배치 문항지 (사용자가 읽을 유일한 자리) ## §4 배치 문항지 (사용자가 읽을 유일한 자리)
**⭐ [2026-08-31 회신 — 단위 1 몫 전량 종결]** 사용자: *"배치 문항은 중간확인
완료했어. 전부 권고안에 동의해. 나중에 천천히 반영해줘"* — `H-214`·`H-215`
둘 다 **권고 (a) 채택**, 같은 날 반영 완료(각 행 상태 참고). 열린 문항 0.
| 번호 | 무엇 | 선택지 | 권고 | 권고 근거 | | 번호 | 무엇 | 선택지 | 권고 | 권고 근거 |
|---|---|---|---|---| |---|---|---|---|---|
| `H-214` | `listHandlers`·동률 경고·(나중의) `quad-debug` 덤프가 쓸 핸들러 **이름** — Handler 계약(3종)엔 `name`이 없다 | (a) 계약에 **선택 필드 `name: string?`** 추가 — 있으면 경고·덤프·`listHandlers`가 쓰고 없으면 priority만 / (b) 이름 없이 감 — `listHandlers`는 핸들러 객체 배열만 반환(지금 임시 구현), "이름/priority" 서술을 문서에서 걷어냄 / (c) 다른 방식(별도 등록 인자 `addHandler(h, name)` 등) | **(a)** | 문서 두 곳(`dispatch-core-plan.md` "우선순위 동률/매치 실패 처리", `research/debug-tooling-plan.md`)이 이미 "이름"을 전제하고, 선택 필드면 기존 3종 계약을 안 깬다. (c)는 이름이 핸들러 자신이 아니라 레지스트리에 살게 돼 체인 슬롯 덤프(슬롯엔 handler 객체만 저장)가 역조회를 또 요구함 | | `H-214` | `listHandlers`·동률 경고·(나중의) `quad-debug` 덤프가 쓸 핸들러 **이름** — Handler 계약(3종)엔 `name`이 없다 | (a) 계약에 **선택 필드 `name: string?`** 추가 — 있으면 경고·덤프·`listHandlers`가 쓰고 없으면 priority만 / (b) 이름 없이 감 — `listHandlers`는 핸들러 객체 배열만 반환(지금 임시 구현), "이름/priority" 서술을 문서에서 걷어냄 / (c) 다른 방식(별도 등록 인자 `addHandler(h, name)` 등) | **(a)** | `dispatch-core-plan.md` **두 자리**("우선순위 동률/매치 실패 처리"의 `listHandlers` 이름/priority + "부수 효과 — quad-debug에 유리"의 체인 슬롯 이름 덤프)가 이미 "이름"을 전제하고(**[감사 3라운드 정정]** 후자를 처음엔 `research/debug-tooling-plan.md`로 잘못 인용 — 거긴 대신 선택적 `describe` 훅(가칭)이 전례), 선택 필드면 기존 3종 계약을 안 깬다. **(b)를 고르면 두 자리 다 걷어야 한다.** (c)는 이름이 핸들러 자신이 아니라 레지스트리에 살게 돼 체인 슬롯 덤프(슬롯엔 handler 객체만 저장)가 역조회를 또 요구함 |
| `H-215` | 스파이크 `04`(Dispatch 체인 retractFrom, `rewrite-required/`) 처분 — `spec.dispatch.luau`가 체인 깊이·레벨별 힌트·3-인자 `retractFrom`·`SetStrong` 음성 대조군을 이미 실측했으나, 재귀 재발행은 spec-로컬 wrapping 핸들러 **근사**다(실제 `StoreBind`는 M4) | (a) 지금 폐기(`01`처럼 `done/` 이동) — 잔여 몫(실제 `StoreBind` 경유 재발행 경로)은 **M4 StoreBind spec이 진다**고 그 단위 계획에 명시 / (b) M4까지 `rewrite-required/`에 유지(현 ROADMAP 문구 "M3 착수 시 같이 처리"를 "M4에서"로 정정) / (c) 지금 재작성 | **(a)** | 체인 메커니즘 자체는 `spec.dispatch.luau` 12절이 실제 구현에 대고 고정했고(스파이크는 격리 재현이라 오히려 약함), `05`/`15`/`01` 폐기와 같은 근거 구조다. 잔여 몫을 M4 spec 항목으로 옮겨 적으면 잊히지 않는다 |
## §5 이상 없음 확인 (탐사자·구현이 확인만 하고 문제 없었던 자리) ## §5 이상 없음 확인 (탐사자·구현이 확인만 하고 문제 없었던 자리)

View file

@ -2061,3 +2061,17 @@ Q4(`EffectHandle` 네 진입점 의사코드 — Observer 것 재사용, `Unsubs
`mN-implementation-roundNN`, 라운드 번호는 마일스톤 가로질러 단순 증가 — `mN-implementation-roundNN`, 라운드 번호는 마일스톤 가로질러 단순 증가 —
`README.md` `qa-request/` 행이 소스), 툴링 픽스 둘(`H-210` rojo 트리 / `README.md` `qa-request/` 행이 소스), 툴링 픽스 둘(`H-210` rojo 트리 /
`H-211` Relate 타입)도 이 세션. 다음 액션은 M3 착수 규약 문항. `H-211` Relate 타입)도 이 세션. 다음 액션은 M3 착수 규약 문항.
- **`session/2026-08-31-02-m3-kickoff-unit1-dispatch-core.md`** — M3 착수. round12
규약 확정(§0 Q1~Q3 전부 권고 (a), §6 첫 단위 계획 승인 — 대화형 선택지 한 번),
단위 1(디스패치 코어) 구현: `Handler` 타입(quad-types 소유)+`Quad.Dispatch`(`H-25`),
`InitDispatch(module)`(하강 diff process/3-인자 retractFrom/addHandler/
listHandlers/drive (b) 루프만 — ⓪⓪'는 단위 2, (a)(c)는 M8), `spec.dispatch` 12절
+`spec.drive` 3절 ALL PASS. 발견: `H-212`(① base 의사코드 error 영어+level 정정)/
`H-213`(① 우선순위 밴드 리터럴)/`H-214`(② 핸들러 이름 필드 — §4 문항). 스파이크
`01` 폐기(`spec.drive`가 상시 회귀로 대체). `H-165` 재현 — pesde install로 해소.
**같은 날 단위 끝 절차** — 감사 루프 5라운드(1: 기계적 누락 4 / 2: STATUS 부채 2+`H-215` 등재 /
3: error 영어화 확장+`H-214` 근거 정정 / 4: `H-216` slot-plan 11곳 / 5: 계수 정정+`H-217`
4곳 — base 코드 리터럴 한국어 error 0), §4 배치 회신 도착(*"전부 권고안에 동의해"*) —
`H-214` (a) Handler 선택 필드 `name: string?` / `H-215` (a) 스파이크 `04` 폐기(잔여
`StoreBind` 경유 경로는 ROADMAP M4 mock 항목). 단위 1 §4 열린 문항 0·코드 마커 0.

View file

@ -0,0 +1,71 @@
# 2026-08-31 (02) — M3 착수: round12 규약 확정 + 단위 1(디스패치 코어) 구현
## 무엇이 있었나
사용자 지시 *"M3 를 작업 시작하자. m3-implementation-round12 문서를 보면 돼"*로
시작. `m3-implementation-round12-brief.md`가 §0 회신 대기 상태였고, §6(첫 단위
계획)은 "Q1·Q2가 (a)로 닫히면 채워 사용자 확정을 받는다"였다.
## 규약 확정 (대화형 선택지)
§6 초안을 먼저 채운 뒤 §0 세 문항과 §6 승인을 **한 번의 대화형 선택지**로
물었다(밤샘 배치가 아니라 사용자가 자리에 있던 시점). 회신:
- **Q1 (a)** — M2 골격 그대로 + M3 전용 게이트(새 핸들러 전 "Handler 작성
체크리스트" 필독) 하나 추가.
- **Q2 (a)** — 단위 넷(코어 → 부기 → `None`/`Nil` 핸들러 → Leaf·가드·종합).
- **Q3 (a)** — M2 하자는 규모로 가름(경미 = round12에 ① / 설계 결정 규모 =
그때 `m2-implementation-round13` 신설).
- **§6 승인** — "승인 — 이대로 착수". 요지: `Dispatch/Handler.luau` 타입 전용
잎 / `InitDispatch(module)` 팩토리, `chains`는 인스턴스별 `Relate`, 우선순위
상수도 이 파일 / `drive`는 단위 1에서 (b) 본체 루프만(⓪⓪'는 단위 2, (a)(c)는
M8) / `spec.dispatch` + `spec.drive`(spec-로컬 테스트 핸들러) / 스파이크 `01`
재작성은 `spec.drive`가 상시 회귀로 대체.
기록 커밋 `f14ba09`(brief §0 회신 블록 + `round12.md` 스켈레톤 + 인덱스 3층).
## 단위 1 구현 (커밋 `590b0fe`, `a6d0c7f`)
- 타입은 quad-types 소유(`Handler`/`Dispatch` export + `Quad.Dispatch` 필드,
`H-25`), `Dispatch/Handler.luau`는 재export 잎. **`H-165`를 예고대로 밟았다** —
pesde shim이 새 export type을 몰라 analyze가 Unknown type, `pesde install`
재실행으로 해소.
- `Dispatch/init.luau`: "Dispatch 체인" 절 의사코드를 그대로 옮김. 옮기다 나온
발견 셋은 `round12.md`가 소스 — `H-212`(① base 의사코드 error가 한국어·level
없음 — error 계약(08-25)이 미반영, 문서·코드 같은 커밋 정정), `H-213`(①
우선순위 밴드 리터럴 값은 문서 미정 — 1000/0/-1000/-1000000 채움),
`H-214`(② `listHandlers`·동률 경고가 원하는 핸들러 **이름**이 계약 3종에
없음 — §4 문항으로, 코드는 핸들러 객체 배열 반환 + `TODO(H-214)` 마커).
- 매치 실패 메시지의 브랜드 출력은 기각된 Brand 역조회를 재도입하지 않고
모듈 공개 술어(`is*`) 프로브로(실패 경로에서만).
- 신 솔버 타입 이슈 둘: `type(x)=="function"` 내로잉이 콜러블이 아님(캐스트),
`table.sort` 비교자 파라미터 무주석이 unknown — 둘 다 주석/캐스트로.
- 테스트: `./scripts/test.sh` 전부 PASS, `luau-analyze` 클린, selene 에러 0.
`spec.drive` 1번이 `F-4-1` 언어 동작(일반화 `for`의 배열→해시 순서)을 실측 —
스파이크 `01` 폐기·`done/` 이동(STATUS.md·ROADMAP 재검증 대기 절 `[x]`).
- ROADMAP M3 체크박스 중 단위 1 몫 여섯을 `[x]`(drive 범위 절단 주석 포함).
## 단위 끝 절차 — 감사 루프 5라운드 + §4 배치 회신
감사 루프(`quad-doc-auditor` 한 턴 하나, 라운드마다 각도 변경): 1라운드
4건(CLAUDE.md 볼드 중첩 / STATUS.md 요약 표 7·17 / quad-types-plan M3 완료
표시 / luau-test README `01` 행) → 2라운드 2건+판단 1(STATUS.md "합류" 문단
해소 표시·done 나열 갱신 / 스파이크 `04` 처분 → `H-215` §4 등재) → 3라운드
2건(`dispatch-core-plan.md` Length/Offset 절·주입 op 스텁 error 영어화 —
`H-212` 확장 / `H-214` 근거 정정: "이름으로 덤프"는 같은 문서 자기 자신) →
4라운드 회귀 0 + 정보성 1(`slot-plan.md` 한국어 error 11곳 → `H-216` ①로
정리) → 5라운드 2건(`H-216` 계수 12→11 / 잔여 4곳 → `H-217` ①: attribute·
debounce-throttle·ref(`"PreRef instance reused"` 문구 통일)·source-state
(mock 실구현 문구에 맞춤)). **base/ 코드 리터럴의 한국어 error 0.**
**§4 배치 회신(사용자, 감사 5라운드 도중)**: *"배치 문항은 중간확인 완료했어.
전부 권고안에 동의해. 나중에 천천히 반영해줘"* — `H-214`·`H-215` 전부 (a).
반영: Handler 계약에 선택 필드 `name: string?`(진단 전용 — quad-types /
동률 경고에 이름 / `TODO` 마커 제거 / 계약 절 신설 항목 / spec 11번), 스파이크
`04` 폐기·`done/` 이동(잔여 `StoreBind` 경유 재발행 경로는 ROADMAP M4 mock
항목에 명시). 테스트·analyze·doc-check 전부 클린.
## 이 다음
`/code-review high` → 커밋 → fable 탐사자(round12.md에 발견 이어붙임) →
사용자에게 "§4를 보라" 한 줄.

View file

@ -2,7 +2,7 @@
Roblox 엔진용 DOMless UI 렌더러 **quad**를 처음부터 다시 짜는 프로젝트. Roblox 엔진용 DOMless UI 렌더러 **quad**를 처음부터 다시 짜는 프로젝트.
**⭐ [2026-08-31 기준] M0(스파이크 검증)/M1(스캐폴딩)/M2(반응형 코어 — **⭐ [2026-08-31 기준] M0(스파이크 검증)/M1(스캐폴딩)/M2(반응형 코어 —
Source/State/Store)까지 완료, **M3(디스패치 엔진) 진행 중**** — M2는 자율 구현 Source/State/Store)까지 완료, M3(디스패치 엔진) 진행 중** — M2는 자율 구현
구간(규약 `.claude/qa-request/m2-implementation-round11-brief.md`, 발견·배치 구간(규약 `.claude/qa-request/m2-implementation-round11-brief.md`, 발견·배치
문항 `-round11.md`)으로 돌아 2026-08-28 착수~08-31 종결: 단위 넷 구현·감사· 문항 `-round11.md`)으로 돌아 2026-08-28 착수~08-31 종결: 단위 넷 구현·감사·
리뷰·탐사 완료, `ROADMAP.md` M2 체크박스 전부 `[x]`, §4 문항·코드 마커 0. 리뷰·탐사 완료, `ROADMAP.md` M2 체크박스 전부 `[x]`, §4 문항·코드 마커 0.

View file

@ -176,8 +176,13 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
구현의 `spec.state.luau` 3번(다이아몬드 규칙 3, 조인 1회)· 구현의 `spec.state.luau` 3번(다이아몬드 규칙 3, 조인 1회)·
`spec.effect.luau` 3번이 실제 구현에서 같은 것을 고정해 스파이크는 `spec.effect.luau` 3번이 실제 구현에서 같은 것을 고정해 스파이크는
폐기, `done/`으로 이동(`luau-test/STATUS.md`의 그 행이 소스) 폐기, `done/`으로 이동(`luau-test/STATUS.md`의 그 행이 소스)
- [ ] **`04`(Dispatch 체인 retractFrom)** — 하강 diff 확정으로 무효화. - [x] **`04`(Dispatch 체인 retractFrom)** — **[2026-08-31 닫힘, 재작성 안
**M3 착수 시 같이 처리**하는 게 자연스러움 함 — `H-215` (a) 사용자 확정]** M3 단위 1의 `spec.dispatch.luau`
검증 대상 대부분(체인 깊이·레벨별 힌트·3-인자 `retractFrom`·`SetStrong`
음성 대조군)을 실제 구현에 대고 실측 — 스파이크는 폐기, `done/`으로
이동. 잔여 몫(실제 `StoreBind` 경유 재귀 재발행 경로 — spec은 로컬
핸들러 근사)은 **M4 mock 테스트 항목이 명시적으로 진다**(그 체크박스에
적어둠)
- [ ] **`19`(소유권/참조카운트 Relate 패턴)** — **B 섹션만** 낡음 - [ ] **`19`(소유권/참조카운트 Relate 패턴)** — **B 섹션만** 낡음
(공개 `AttributeKey(name)` + 인덱스 1 점유 체크가 폐기되고 그룹 전용 (공개 `AttributeKey(name)` + 인덱스 1 점유 체크가 폐기되고 그룹 전용
키 + `AttributeKeyHandler`의 이름 claim으로 바뀜, `0-Z` 확정). 키 + `AttributeKeyHandler`의 이름 claim으로 바뀜, `0-Z` 확정).
@ -967,7 +972,11 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
인덱스 N/N+1로 안 겹치고 정상 동작하는지**(2026-08-13 다섯 번째 인덱스 N/N+1로 안 겹치고 정상 동작하는지**(2026-08-13 다섯 번째
세션에 UB→정상 지원으로 재정정) + **최초 마운트 직후 첫 재발행에서 세션에 UB→정상 지원으로 재정정) + **최초 마운트 직후 첫 재발행에서
인덱스 2의 retractor가 실제로 불리는지**(위 M3의 `SetStrong` 순서 인덱스 2의 retractor가 실제로 불리는지**(위 M3의 `SetStrong` 순서
버그가 정확히 여기서 증상으로 나타남) 버그가 정확히 여기서 증상으로 나타남).
**[2026-08-31 `H-215` (a)]** 이 항목이 폐기된 스파이크 `04`의 잔여
몫도 진다 — M3 단위 1 `spec.dispatch.luau`는 재귀 재발행을 로컬
wrapping 핸들러로 근사했으므로, **실제 `StoreBind` 경유** 재발행에서
깊은 인덱스부터 정리되는 것까지 여기서 실측할 것
## M5 — quad-roblox 최소 프로바이더 ## M5 — quad-roblox 최소 프로바이더

View file

@ -179,8 +179,11 @@ local function Init(module: any)
if module.debug then if module.debug then
for _, existing in handlers do for _, existing in handlers do
if existing.priority == handler.priority then if existing.priority == handler.priority then
-- `name` is the optional diagnostics-only contract field
-- (`H-214` (a)) — fall back to priority alone when absent.
print( print(
`quad.Dispatch: handler priority tie at {handler.priority}` `quad.Dispatch: handler priority tie at {handler.priority}`
.. ` between "{existing.name or "?"}" and "{handler.name or "?"}"`
.. " — ties have no defined order; offset from a HANDLER_PRIORITY_* band" .. " — ties have no defined order; offset from a HANDLER_PRIORITY_* band"
) )
break break
@ -196,10 +199,9 @@ local function Init(module: any)
function Dispatch.listHandlers(): { Handler } function Dispatch.listHandlers(): { Handler }
-- Pure query — returns the registered handlers in scan order, prints -- Pure query — returns the registered handlers in scan order, prints
-- nothing (`D-8`); whether to display them is the caller's business. -- nothing (`D-8`); whether to display them is the caller's business.
-- A copy, so callers can't disturb the scan order. -- A copy, so callers can't disturb the scan order. Each handler carries
-- TODO(H-214): the Handler contract has no `name` field, but this doc'd -- the doc'd name/priority via the optional `name` contract field
-- surface ("이름/priority") and the tie warning/`quad-debug` chain dump -- (`H-214` (a) — absent name just shows as nil).
-- want one — pending §4 batch decision.
return table.clone(handlers) return table.clone(handlers)
end end

View file

@ -301,9 +301,11 @@ do
q.Dispatch.addHandler(low) q.Dispatch.addHandler(low)
q.Dispatch.addHandler(fallback) q.Dispatch.addHandler(fallback)
q.Dispatch.addHandler(high) q.Dispatch.addHandler(high)
high.name = "HighLeaf" -- 선택 진단 필드(`H-214` (a)) — 매치·순서엔 무영향
local listed = q.Dispatch.listHandlers() local listed = q.Dispatch.listHandlers()
assert(#listed == 3, "returns every registered handler") assert(#listed == 3, "returns every registered handler")
assert(listed[1] == high and listed[2] == low and listed[3] == fallback, "scan (priority) order") assert(listed[1] == high and listed[2] == low and listed[3] == fallback, "scan (priority) order")
assert(listed[1].name == "HighLeaf" and listed[2].name == nil, "optional name rides along; absent stays nil")
table.clear(listed) -- 사본이라 레지스트리에 영향 없음 table.clear(listed) -- 사본이라 레지스트리에 영향 없음
assert(q.Dispatch.getHandler({}, "k", 5) == high, "mutating the returned list does not disturb the registry") assert(q.Dispatch.getHandler({}, "k", 5) == high, "mutating the returned list does not disturb the registry")
print("PASS") print("PASS")

View file

@ -127,11 +127,16 @@ export type Source<T> = State<T> & {
} }
-- ── 디스패치 엔진(M3 단위 1) ────────────────────────────────────────── -- ── 디스패치 엔진(M3 단위 1) ──────────────────────────────────────────
-- `Handler` — 핸들러 계약 **3종**(`.claude/base/dispatch-core-plan.md` "핸들러 -- `Handler` — 핸들러 계약 **필수 3종 + 선택 `name`**(`.claude/base/
-- 계약"): `isHandlable`은 부작용 없는 순수 판별(inst도 받음), `process`는 자기 -- dispatch-core-plan.md` "핸들러 계약"): `isHandlable`은 부작용 없는 순수
-- 자신을 무르는 1-인자 retractor를 반환(생략 불가 — 정리할 게 없어도 `Void`). -- 판별(inst도 받음), `process`는 자기 자신을 무르는 1-인자 retractor를
-- retractor 인자는 `nil`(단순 철거)이거나 같은 핸들러가 처리할 새 값, 둘뿐. -- 반환(생략 불가 — 정리할 게 없어도 `Void`). retractor 인자는 `nil`(단순
-- 철거)이거나 같은 핸들러가 처리할 새 값, 둘뿐. `name`은 진단 전용
-- (`H-214` (a) 사용자 확정, 2026-08-31) — 동률 경고·`listHandlers`·
-- `quad-debug` 체인 덤프가 있으면 쓰고 없으면 priority만 보인다,
-- 스캔·매치엔 아무 영향 없음.
export type Handler = { export type Handler = {
name: string?,
isHandlable: (inst: any, key: any, value: any) -> boolean, isHandlable: (inst: any, key: any, value: any) -> boolean,
priority: number, priority: number,
process: (inst: any, key: any, value: any, index: number) -> (nextValue: any?) -> (), process: (inst: any, key: any, value: any, index: number) -> (nextValue: any?) -> (),