diff --git a/.claude/base/attribute-plan.md b/.claude/base/attribute-plan.md index 9b8f9c6..b3cd152 100644 --- a/.claude/base/attribute-plan.md +++ b/.claude/base/attribute-plan.md @@ -459,7 +459,7 @@ function AttributeGroupHandler.process(inst, k, v, index) -- 기록되는 중간 상태가 안 생긴다 — 위 그 항목이 소스). local claimed = groupClaimKeys:GetStrong(inst, v) 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 groupClaimKeys:SetStrong(inst, v, k) diff --git a/.claude/base/debounce-throttle-plan.md b/.claude/base/debounce-throttle-plan.md index 31d042c..e0c450f 100644 --- a/.claude/base/debounce-throttle-plan.md +++ b/.claude/base/debounce-throttle-plan.md @@ -909,7 +909,7 @@ end local function makeGate(reset: boolean, opts) 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 local leading = if reset then opts.Leading == true else opts.Leading ~= false local trailing = opts.Trailing ~= false diff --git a/.claude/base/dispatch-core-plan.md b/.claude/base/dispatch-core-plan.md index 58ba3f4..8cb8bed 100644 --- a/.claude/base/dispatch-core-plan.md +++ b/.claude/base/dispatch-core-plan.md @@ -129,6 +129,19 @@ v1의 `ProcessQuadProperty`(`.claude/initreq/quad/src/class.lua:134-214`)는 > 이야기**다 — (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`을 호출, 첫 매치가 처리(Fusion의 SpecialKey 우선순위 스캔과 유사하되 4단계 고정이 아니라 열린 레지스트리). tbox의 `TUnion` 런타임 체커가 이미 이 "순서대로 스캔, 첫 매치 @@ -877,9 +890,13 @@ Fallback Handler들도 존재하지 않아**, 위 "매치 실패는 즉시 `erro - **아직 아무 팩토리도 채우지 않은 슬롯의 기본값은 quad-base가 준다 — 단 "동작하는 구현을 추측"하지 않고 명시적으로 에러내는 스텁으로.** - `BaseModule.addTag = function() error("addTag가 구현되지 않음 — - provider가 초기화됐는지, 이 백엔드가 Tag를 지원하는지 확인하라") end` - 류. base가 "그럴듯한 기본 동작"(예: 조용한 no-op)을 대신 만들어주는 + `BaseModule.addTag = function() error("quad: addTag is not available — + check that the provider is initialized and that this backend supports + Tag") end` 류(**[2026-08-31 `H-212` 확장]** 예시 메시지를 영어로 — + M2가 커밋한 `LifetimeHandle.luau`의 미주입 스텁과 같은 어조. `level` + 숫자는 이 스텁이 quad 내부 프레임(`TagHandler.process`)을 거쳐 불려 + "사용자 호출부"까지의 프레임 수를 문서 시점엔 못 세므로 여기 안 박는다 — + M5/M10 구현 시점에 도착지 계약(`architecture.md`)대로 정할 것). base가 "그럴듯한 기본 동작"(예: 조용한 no-op)을 대신 만들어주는 건 기각 — 임의의 엔진에 뭐가 맞는 기본값인지 base는 알 수 없고, 조용한 no-op은 실수(provider 초기화를 잊음)를 가려버림. 명시적 에러가 유일하게 안전한 기본값. @@ -893,7 +910,7 @@ Fallback Handler들도 존재하지 않아**, 위 "매치 실패는 즉시 `erro ```lua { priority = HANDLER_PRIORITY_FALLBACK + 1, 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`보다 한 단계 높아 스캔에서 먼저 매치되고(2026-08-14 열두 번째 세션 정정 — `TagHandler` @@ -1783,7 +1800,7 @@ function Dispatch.getOffsetAt(ownerKey, at) -- ⭐ [2026-08-25, 7라운드 `H-106`] `nil` 가드 — `recompute`만 갖고 있던 -- `C-6` 진단이 이 경로에선 우회돼 익명 산술 에러로 먼저 터졌다. 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 cur += contribution(bk, i) -- lengthList[i](State면 :Get()) bk.offsetCache[i + 1] = cur -- **지금 자리의 길이가 다음 자리의 offset을 정한다** @@ -1960,7 +1977,7 @@ local function recompute(ownerKey, bk) -- 부기가 깨진 것 — 조용히 건너뛰면 위치 하나가 순서 계산에서 빠지는 -- 추적 어려운 오작동이 된다. 상세는 base/slot-plan.md의 "추가 방어 조치". 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 local abs = Dispatch.getOffsetAt(ownerKey, i) -- 절대 offset(캐시 경유) bk.offsetSetUpTo = i -- 여기까지 Set 완료 diff --git a/.claude/base/quad-types-plan.md b/.claude/base/quad-types-plan.md index e0be469..d4a4b09 100644 --- a/.claude/base/quad-types-plan.md +++ b/.claude/base/quad-types-plan.md @@ -107,6 +107,8 @@ quad-roblox는 `quad-types`의 좁은 `Quad`만 본다. - 규칙이 쓰인 계기는 `Dispatch`이고, **M3**가 `Dispatch: Dispatch` 필드와 그 타입 재수출을 여기 추가한다 — `ROADMAP.md` M3 체크리스트에 **항목으로 명시**한다(지금까지 아무도 이 필요성을 항목화해두지 않았다). + **[2026-08-31] M3 단위 1로 완료** — `Handler`/`Dispatch` export type과 + `Quad.Dispatch` 필드가 실제로 추가됐다(그 체크박스는 `[x]`). - **[2026-08-24 정정] 다만 규칙이 *처음 적용된* 마일스톤은 M2다** — 마일스톤 순서 교체로 반응형 코어가 앞에 오면서, `Source`/`State`/`Store` 필드 추가가 `Dispatch`보다 먼저 왔다(`ROADMAP.md` M2의 `H-25` 파생 항목 — diff --git a/.claude/base/ref-plan.md b/.claude/base/ref-plan.md index e05baca..1708db3 100644 --- a/.claude/base/ref-plan.md +++ b/.claude/base/ref-plan.md @@ -975,8 +975,9 @@ flatten된 값은 해시 파트(프로퍼티 키)로 존재하게 되고, Store - **구현**: pre-pass가 첫 fire 때 해당 `PreRef` 객체에 내부 플래그 (`_fired = true`)를 세팅. pre-pass가 배열을 훑다 `isPreRef(v)`인 슬롯을 만났는데 그 객체가 이미 `_fired`면, fire하지 않고 그 자리에서 - 즉시 `error("PreRef는 1회용 — 이미 다른 construction에 쓰인 - PreRef를 재사용할 수 없음, 매번 새로 만들 것")`. 위 "동적 경로 가드" + 즉시 `error("PreRef instance reused", 2)`(문구는 + `bind-system-plan.md`의 파이프라인 의사코드와 같은 리터럴 — 1회용이라 + 매번 새로 만들라는 뜻). 위 "동적 경로 가드" Handler(정상 본체 루프에서 매치)와는 별개 코드 경로 — 이 가드는 pre-pass 자신 안에, `_fired`가 아닌 정상 fire는 그대로 통과. - **관용구**: `Slot:List`의 `updateFn`처럼 반복 호출되는 자리에서 diff --git a/.claude/base/slot-plan.md b/.claude/base/slot-plan.md index 03839b6..67dd0d2 100644 --- a/.claude/base/slot-plan.md +++ b/.claude/base/slot-plan.md @@ -430,7 +430,7 @@ local function claimOwner(element, ownerKey, fromDetached) -- `fromDetached` 없이 같은 owner라는 것만으로 통과시키면 -- `Slot{a, a}`가 다시 조용히 새어나간다(2026-08-13 감사가 막은 것). if not (fromDetached and cur == ownerKey) then - error("이 요소는 이미 마운트돼 있음 — 다중 마운트 금지") + error("Slot: this element is already mounted — multiple mounts are not allowed", 2) end return -- 이미 내 것이므로 SetWeak도 불필요 end @@ -445,7 +445,7 @@ local function claimOwnerAt(element, inst, k) return false -- 정확히 이 자리가 이미 들고 있음 — 재확인만, no-op end if current ~= nil then - error("이 요소는 이미 다른 곳에 마운트돼 있음 — 다중 마운트 금지") + error("Slot: this element is already mounted elsewhere — multiple mounts are not allowed", 2) end elementOwner:SetWeak(element, OWNER, inst) elementOwner:SetWeak(element, OWNER_POS, k) @@ -460,7 +460,7 @@ local function releaseOwner(element, ownerKey) -- 무시 없이 즉시 error" 원칙과 같은 결로 즉시 error. local current = elementOwner:GetWeak(element, OWNER) if current ~= ownerKey then - error("releaseOwner: 이 element는 이 ownerKey가 소유하고 있지 않음 — 호출측 소유권 추적이 깨졌음") + error("releaseOwner: this element is not owned by this ownerKey — ownership tracking is broken", 1) end elementOwner:SetWeak(element, OWNER, nil) elementOwner:SetWeak(element, OWNER_POS, nil) @@ -1554,7 +1554,7 @@ function activateList(self, physicalTarget) for i, item in ipairs(items) do local key = keyFn(item, i) -- keyFn은 raw i를 받음(:List 파라미터 설명 참고) if seen[key] then - error("Slot:List — duplicate key: " .. tostring(key)) + error("Slot:List — duplicate key: " .. tostring(key), 2) end seen[key] = true keys[i] = key @@ -1632,8 +1632,8 @@ function activateList(self, physicalTarget) -- KeyGone을 받은 자리는 "데이터가 다시 나타날 때를 위한 캐싱" -- (= Detach) 아니면 파괴뿐이고, **새 마운트/생성은 거부**한다. if result ~= nil then - error("Slot:List — KeyGone에는 nil/None(파괴) 또는 Detach(홀드)만 반환할 수 있음 " - .. "(자리가 없어진 키에 새 요소를 마운트할 수 없고, prev 유지도 모순)") + error("Slot:List — KeyGone accepts only nil/None (destroy) or Detach (hold) " + .. "(cannot mount a new element at a key whose slot is gone; keeping prev is contradictory)", 2) end settle(key, result, detach, 0) -- slotPos는 의미 없음(자리를 안 차지함) userdata[key] = ud -- 유저가 nil을 반환해야 지워짐 @@ -3271,7 +3271,7 @@ function Slot:Add(element, index) assert(not self._listed, "Slot: :List가 설치된 Slot엔 수동 CRUD를 쓸 수 없음") -- (2) index 범위 검증 — **clamp 안 함**, 범위 밖이면 error 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 -- (3) 요소 타입 검증은 `wrapElement`가 한다(위 그 함수 — `isSlot`/`isState`/`isInst`) local wrapped = wrapElement(element) @@ -3316,13 +3316,13 @@ local function wrapElement(v) if not isState(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 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 if not isInst(v) then -- 백엔드 주입 술어(위 `native*` 절) - error("Slot: 이 백엔드가 마운트할 수 없는 값") + error("Slot: this backend cannot mount this value", 2) end return v end @@ -3608,7 +3608,7 @@ function dispose(value) -- 안 들어간다(`Relate`는 항상 3-인자 `SetWeak`/2-인자 `GetWeak`, 409행). -- `H-71`로 dedup 기록까지 `SetWeak`이 되며 강/약 짝맞춤이 더 중요해졌다. 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 -- (2) [`H-43`] Slot도 Instance도 아닌 값이 백엔드로 그냥 흘러가지 않게 if isSlot(value) then @@ -3616,7 +3616,7 @@ function dispose(value) elseif isInst(value) then nativeDispose(value) -- 아래 주입 op else - error("dispose: 이 백엔드가 파괴할 수 없는 값") + error("dispose: this backend cannot dispose this value", 2) end end ``` diff --git a/.claude/base/source-state-plan.md b/.claude/base/source-state-plan.md index a43505a..8056089 100644 --- a/.claude/base/source-state-plan.md +++ b/.claude/base/source-state-plan.md @@ -1691,7 +1691,7 @@ function bindLifetime(inst, value) -- bindLifetime의 게이트는 canBound, canExecute 아님 -- [정정, 2026-08-18 구현 전 QA] 방향이 뒤집혀 있었음 — -- canBound 참 = 묶어도 됨이라 에러는 not 쪽 - error("이미 바인딩된 값") -- 메시지 분기는 위 게이트 스케치 참고 + error("bindLifetime: value is already bound", 2) -- 실제 메시지 분기는 위 게이트 스케치 참고 end ... -- gchold 등록 + gcconn 참조 복사(base/lifecycle-pattern.md) end diff --git a/.claude/luau-test/README.md b/.claude/luau-test/README.md index e12fa6a..4d422f5 100644 --- a/.claude/luau-test/README.md +++ b/.claude/luau-test/README.md @@ -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 | | `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>` **정상 동작**, 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>` **정상 동작**, 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 | | `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`에서 사라짐 | diff --git a/.claude/luau-test/STATUS.md b/.claude/luau-test/STATUS.md index 243fccf..6437c19 100644 --- a/.claude/luau-test/STATUS.md +++ b/.claude/luau-test/STATUS.md @@ -3,7 +3,9 @@ > 마지막 갱신: **2026-08-31** — M3 단위 1이 `01`을 닫음: 재작성이 물어야 했던 > 언어 동작(일반화 `for`의 배열→해시 순서)을 `quad-base/test/spec.drive.luau` > 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` 기각 실측) **폐기 → > `done/`**, 신규 `26`(`:Apply` 교집합 오버로드, `H-179`) `done/` 직행, "만들어야 할 > 스파이크"의 중간 State GC(`spec.state` 11번)·`CheckedQuad` 재실행(`23`) 닫힘 — @@ -54,9 +56,9 @@ | 폴더 | 뜻 | 개수 | 누가 처리 | |---|---|---|---| | `review-required/` | **설계가 걸림 — 사람 결정 필요** | **0** | ⭐ 사용자 | -| `rewrite-required/` | 스파이크가 낡음(코드가 깨졌거나, 설계가 바뀌어 옛 모델을 검증 중) | 8 | 에이전트 | +| `rewrite-required/` | 스파이크가 낡음(코드가 깨졌거나, 설계가 바뀌어 옛 모델을 검증 중) | 6 | 에이전트 | | `not-run/` | 이 환경에서 못 돌림(Studio 전용) | 0(+헬퍼 1) | 사용자 or MCP 연결 후 에이전트 | -| `done/` | 통과 or 판정 끝, 더 할 일 없음 | 16 | — | +| `done/` | 통과 or 판정 끝, 더 할 일 없음 | 18 | — | **⚠️ [2026-08-25 신설] 타입 스파이크는 `./scripts/test.sh`가 하는 리링크를 먼저 거쳐야 한다.** `luau` CLI가 심볼릭 링크를 못 타는데(디렉토리·파일 둘 @@ -107,9 +109,12 @@ `10`은 **Studio 전용이라 재작성해도 이 환경에서는 못 돌린다** — 재작성 후 다시 `not-run/`으로 내려가 사용자/MCP를 기다리는 자리다. -**[2026-08-21] `01`과 `05`가 합류** — 둘 다 같은 "설계가 바뀐" 유형이다. -통과 상태로 `done/`에 두면 `01`은 구현이 안 하는 두 루프 순회를, `05`는 -**이제 접히는 중복 통지가 안 접힌다는 것**을 "검증됨"으로 오독하게 된다. +**[2026-08-21 신설 — 2026-08-29·2026-08-31에 해소됨]** `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`가 합류했다** — 그 파일의 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는 손댈 것 없음 | | `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>`가 `types.never`로 무너뜨리는지 — **[2026-08-26 `H-112`]** 인자가 `T`가 아니라 `keyof`다)과 **없는 키 접근**을 포함할 것 | @@ -173,8 +177,11 @@ (개수는 위 표와 폴더가 소스 — 여기서 다시 세지 않는다.) -**지금 `done/`에 있는 런타임 스파이크는 `02`/`03`/`06`/`07`/`11`/`17`/`18`/`20`, -전원 통과**(crash 0 / FAIL 0). 나머지는 타입 스파이크다. +**[2026-08-31 기준] 지금 `done/`에 있는 런타임(비타입) 스파이크는 +`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` 인스턴스 브랜드 재작성으로 파일이 쓰는 `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.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 순회 순서" 절) 언어 동작 자체를 묻도록 재작성 대기였음 | | `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`로 걸려 **파일 전체가 아무것도 검증 못 함** — 대조군을 별도 파일/블록으로 격리 | @@ -213,8 +221,9 @@ ### 특별히 중요한 통과 3건 -**`04` — 직전 감사가 찾은 버그가 음성 대조군으로 재현됨**(파일은 지금 -`rewrite-required/`에 있음 — 아래 관측 자체는 새 모델에서도 유효) +**`04` — 직전 감사가 찾은 버그가 음성 대조군으로 재현됨**(**[2026-08-31]** +파일은 폐기돼 `done/`에 있음(`H-215` (a)) — 아래 관측 자체는 새 모델에서도 +유효하고, 같은 음성 대조가 `spec.dispatch.luau` 6번에 상시 회귀로 산다) | 관측 지점 | 정상(수정본) | 대조군(버그) | |---|---|---| diff --git a/.claude/luau-test/rewrite-required/04-dispatch-chain-retractFrom.luau b/.claude/luau-test/done/04-dispatch-chain-retractFrom.luau similarity index 100% rename from .claude/luau-test/rewrite-required/04-dispatch-chain-retractFrom.luau rename to .claude/luau-test/done/04-dispatch-chain-retractFrom.luau diff --git a/.claude/qa-request/m3-implementation-round12.md b/.claude/qa-request/m3-implementation-round12.md index 7b25683..a6a83fe 100644 --- a/.claude/qa-request/m3-implementation-round12.md +++ b/.claude/qa-request/m3-implementation-round12.md @@ -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-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 계약 이전 표기로 남아 있었다 (①) @@ -43,8 +46,11 @@ ### `H-214` — `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종으로 못 박혀** 있고(같은 문서 "핸들러 계약" 절: *"다음 3개를 제공하는"*), `name`을 붙이는 건 **새 필드**라 규약 §2의 ② 갈래다. 기각 이력 grep: Handler에 이름 필드를 검토·기각한 기록 @@ -54,11 +60,22 @@ 동률 경고 print는 priority 값만 찍는다. 코드 마커 `TODO(H-214)` 1곳 (`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 배치 문항지 (사용자가 읽을 유일한 자리) +**⭐ [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 이상 없음 확인 (탐사자·구현이 확인만 하고 문제 없었던 자리) diff --git a/.claude/session-summary.md b/.claude/session-summary.md index ec2656e..95b3833 100644 --- a/.claude/session-summary.md +++ b/.claude/session-summary.md @@ -2061,3 +2061,17 @@ Q4(`EffectHandle` 네 진입점 의사코드 — Observer 것 재사용, `Unsubs `mN-implementation-roundNN`, 라운드 번호는 마일스톤 가로질러 단순 증가 — `README.md` `qa-request/` 행이 소스), 툴링 픽스 둘(`H-210` rojo 트리 / `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. diff --git a/.claude/session/2026-08-31-02-m3-kickoff-unit1-dispatch-core.md b/.claude/session/2026-08-31-02-m3-kickoff-unit1-dispatch-core.md new file mode 100644 index 0000000..708a882 --- /dev/null +++ b/.claude/session/2026-08-31-02-m3-kickoff-unit1-dispatch-core.md @@ -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를 보라" 한 줄. diff --git a/CLAUDE.md b/CLAUDE.md index e093503..af2cd08 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ Roblox 엔진용 DOMless UI 렌더러 **quad**를 처음부터 다시 짜는 프로젝트. **⭐ [2026-08-31 기준] M0(스파이크 검증)/M1(스캐폴딩)/M2(반응형 코어 — -Source/State/Store)까지 완료, **M3(디스패치 엔진) 진행 중**** — M2는 자율 구현 +Source/State/Store)까지 완료, M3(디스패치 엔진) 진행 중** — M2는 자율 구현 구간(규약 `.claude/qa-request/m2-implementation-round11-brief.md`, 발견·배치 문항 `-round11.md`)으로 돌아 2026-08-28 착수~08-31 종결: 단위 넷 구현·감사· 리뷰·탐사 완료, `ROADMAP.md` M2 체크박스 전부 `[x]`, §4 문항·코드 마커 0. diff --git a/ROADMAP.md b/ROADMAP.md index 7b53531..590d54c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -176,8 +176,13 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 구현의 `spec.state.luau` 3번(다이아몬드 규칙 3, 조인 1회)· `spec.effect.luau` 3번이 실제 구현에서 같은 것을 고정해 스파이크는 폐기, `done/`으로 이동(`luau-test/STATUS.md`의 그 행이 소스) -- [ ] **`04`(Dispatch 체인 retractFrom)** — 하강 diff 확정으로 무효화. - **M3 착수 시 같이 처리**하는 게 자연스러움 +- [x] **`04`(Dispatch 체인 retractFrom)** — **[2026-08-31 닫힘, 재작성 안 + 함 — `H-215` (a) 사용자 확정]** M3 단위 1의 `spec.dispatch.luau`가 + 검증 대상 대부분(체인 깊이·레벨별 힌트·3-인자 `retractFrom`·`SetStrong` + 음성 대조군)을 실제 구현에 대고 실측 — 스파이크는 폐기, `done/`으로 + 이동. 잔여 몫(실제 `StoreBind` 경유 재귀 재발행 경로 — spec은 로컬 + 핸들러 근사)은 **M4 mock 테스트 항목이 명시적으로 진다**(그 체크박스에 + 적어둠) - [ ] **`19`(소유권/참조카운트 Relate 패턴)** — **B 섹션만** 낡음 (공개 `AttributeKey(name)` + 인덱스 1 점유 체크가 폐기되고 그룹 전용 키 + `AttributeKeyHandler`의 이름 claim으로 바뀜, `0-Z` 확정). @@ -967,7 +972,11 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 인덱스 N/N+1로 안 겹치고 정상 동작하는지**(2026-08-13 다섯 번째 세션에 UB→정상 지원으로 재정정) + **최초 마운트 직후 첫 재발행에서 인덱스 2의 retractor가 실제로 불리는지**(위 M3의 `SetStrong` 순서 - 버그가 정확히 여기서 증상으로 나타남) + 버그가 정확히 여기서 증상으로 나타남). + **[2026-08-31 `H-215` (a)]** 이 항목이 폐기된 스파이크 `04`의 잔여 + 몫도 진다 — M3 단위 1 `spec.dispatch.luau`는 재귀 재발행을 로컬 + wrapping 핸들러로 근사했으므로, **실제 `StoreBind` 경유** 재발행에서 + 깊은 인덱스부터 정리되는 것까지 여기서 실측할 것 ## M5 — quad-roblox 최소 프로바이더 diff --git a/quad-base/src/Dispatch/init.luau b/quad-base/src/Dispatch/init.luau index bd2692d..76c4f69 100644 --- a/quad-base/src/Dispatch/init.luau +++ b/quad-base/src/Dispatch/init.luau @@ -179,8 +179,11 @@ local function Init(module: any) if module.debug then for _, existing in handlers do 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( `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" ) break @@ -196,10 +199,9 @@ local function Init(module: any) function Dispatch.listHandlers(): { Handler } -- Pure query — returns the registered handlers in scan order, prints -- nothing (`D-8`); whether to display them is the caller's business. - -- A copy, so callers can't disturb the scan order. - -- TODO(H-214): the Handler contract has no `name` field, but this doc'd - -- surface ("이름/priority") and the tie warning/`quad-debug` chain dump - -- want one — pending §4 batch decision. + -- A copy, so callers can't disturb the scan order. Each handler carries + -- the doc'd name/priority via the optional `name` contract field + -- (`H-214` (a) — absent name just shows as nil). return table.clone(handlers) end diff --git a/quad-base/test/spec.dispatch.luau b/quad-base/test/spec.dispatch.luau index 75ede1b..92a20ad 100644 --- a/quad-base/test/spec.dispatch.luau +++ b/quad-base/test/spec.dispatch.luau @@ -301,9 +301,11 @@ do q.Dispatch.addHandler(low) q.Dispatch.addHandler(fallback) q.Dispatch.addHandler(high) + high.name = "HighLeaf" -- 선택 진단 필드(`H-214` (a)) — 매치·순서엔 무영향 local listed = q.Dispatch.listHandlers() 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].name == "HighLeaf" and listed[2].name == nil, "optional name rides along; absent stays nil") table.clear(listed) -- 사본이라 레지스트리에 영향 없음 assert(q.Dispatch.getHandler({}, "k", 5) == high, "mutating the returned list does not disturb the registry") print("PASS") diff --git a/quad-types/src/init.luau b/quad-types/src/init.luau index 49d0ad5..f1f091d 100644 --- a/quad-types/src/init.luau +++ b/quad-types/src/init.luau @@ -127,11 +127,16 @@ export type Source = State & { } -- ── 디스패치 엔진(M3 단위 1) ────────────────────────────────────────── --- `Handler` — 핸들러 계약 **3종**(`.claude/base/dispatch-core-plan.md` "핸들러 --- 계약"): `isHandlable`은 부작용 없는 순수 판별(inst도 받음), `process`는 자기 --- 자신을 무르는 1-인자 retractor를 반환(생략 불가 — 정리할 게 없어도 `Void`). --- retractor 인자는 `nil`(단순 철거)이거나 같은 핸들러가 처리할 새 값, 둘뿐. +-- `Handler` — 핸들러 계약 **필수 3종 + 선택 `name`**(`.claude/base/ +-- dispatch-core-plan.md` "핸들러 계약"): `isHandlable`은 부작용 없는 순수 +-- 판별(inst도 받음), `process`는 자기 자신을 무르는 1-인자 retractor를 +-- 반환(생략 불가 — 정리할 게 없어도 `Void`). retractor 인자는 `nil`(단순 +-- 철거)이거나 같은 핸들러가 처리할 새 값, 둘뿐. `name`은 진단 전용 +-- (`H-214` (a) 사용자 확정, 2026-08-31) — 동률 경고·`listHandlers`· +-- `quad-debug` 체인 덤프가 있으면 쓰고 없으면 priority만 보인다, +-- 스캔·매치엔 아무 영향 없음. export type Handler = { + name: string?, isHandlable: (inst: any, key: any, value: any) -> boolean, priority: number, process: (inst: any, key: any, value: any, index: number) -> (nextValue: any?) -> (),