quad/.claude/qa-request/pre-implementation-handtrace-round7-verification.md
qwreey 8cc388cfab
qa: 7라운드 발견 52건 검증 패스 — 유효 49 / 범위 축소 2 (H-55~H-106)
발견을 더 찾는 패스가 아니라 이미 나온 것이 유효한지만 판정한다. 52건
전부를 한 세션이 한 컨텍스트에서 썼으므로 쓴 세션 자신은 자기가 뭘 잘못
봤는지 모른다(conventions.md가 반복 확인한 그 패턴). 판정 뒤 사용자가
batch로 결정하므로 무효 항목이 섞이면 그 비용을 그대로 문다.

**재실행이 아니라 대조로 판정했다.** audit/handtrace-round7-reference-impl/
README가 못박은 대로, 4·5·6차 패스의 발견은 스파이크를 다시 돌려서가 아니라
전사물(core/dispatch/chain.luau)과 base/ 확정 의사코드를 줄 단위로 맞춰
판정했다. Luau 언어 동작·저장소 상태를 주장하는 것(H-71/H-77/H-73/H-75/
H-76/H-78/H-83, 타입 스파이크 전량)만 직접 재실행해 교차 확인했다.

## 전사 대조 결과 — 전사 오류로 생긴 발견은 없다

State:_receive / _recompute / Get, GateNode _flush/_receive, Source:Set의
bit32.bnot(-rev), emitDown 스냅샷, getOffsetAt/recompute(에러 메시지까지)/
setLength/setOffsetSource, chain의 process/retractFrom — 전부 원문과 일치.

README가 신고한 의도적 차이 3개 외에 **미신고 차이 3개**를 찾았으나 셋 다
발견을 만들지 않는다: (1) Blocker:Policy가 2인자(죽은 인자, 아무도 안 넘김),
(2) Blocker.handles가 강한 배열(문서는 weak — H-63(2)를 오히려 가림),
(3) M.Observer가 즉시 1회 실행을 안 하고 self.owner 강참조를 둠(앞은
dispatch.luau가 명시 호출로 등가 보정, 뒤는 GC 문제를 가리는 방향).

## 판정

- 유효 49 / 범위 축소 2 / 무효 0 / 중복 0.
- **H-87 범위 축소** — "코퍼스가 error 경로를 어느 쪽도 안 다룬다"가 거짓.
  slot-plan.md가 같은 실패 모드를 명시하고 사용자가 2026-08-21에 "pcall로
  감싸지 않는다"로 이미 판단했다. 갈래 (a)가 든 두 자리 중 하나가 닫힌
  결정이라, 그대로 올리면 사용자가 자기 판단을 모른 채 재결정하게 된다.
  남는 진짜 질문은 그 판단을 Dispatch.drive(자가치유 없음)까지 확장할지.
- **H-105 범위 축소** — 개수를 직접 셌다. 영어 6은 맞지만 한국어는 17이
  아니라 약 23이고, 17+6이 애초에 "42곳"과 안 맞는다. 현상과 결정
  필요성은 유효.
- **H-88 정정** — 셋 문서에 error 0건은 참이나 "코퍼스에 한 줄도 없다"는
  과하다. 선례가 하나 있고 그건 갈래 (b) 쪽을 가리킨다.
- **H-104 정정** — "level 인자가 하나도 없다"는 전수 확인으로 참,
  "42곳"만 약 29곳으로 정정.

## batch용 요약

번호순 52건이 아니라 결정 단위 12묶음(A~L)으로 재편성했다. 같은 묶음은
결론이 서로를 규정하므로 따로 답하면 갈라진다. 몇 가지 눈에 띄는 것:

- H-55(버리기)와 H-86(읽기)은 emit(commit) -> boolean 하나로 같이 닫힌다.
- H-56/H-62/H-61은 "전파 루프 의사코드" 한 블록을 쓰면 같이 닫힌다.
- H-73을 탑레벨 함수로 닫으면 H-74가 통째로 소멸한다.
- H-58과 H-59는 Ref 콜백 등록을 공유 헬퍼로 빼는 같은 수정으로 닫히고,
  그 전에 H-60(Rerun 정의)이 있어야 나머지를 쓸 문장이 생긴다.
- H-71의 해법 (b)가 H-77엔 안 듣는다(실측) — Relate 슬롯 셋을 나눠 쓸 것.
- H-78이 제일 급하다: 지금 이 저장소에서 smoke 2개가 안 돌고
  luau-analyze가 거짓 클린을 준다(검증 세션에서 재현).
- H-66/H-67/H-82/H-91은 결정 불필요(근거·표기만 정정).

.claude/README.md의 qa-request 행에도 등재. doc-check.py ERROR 0.

Co-authored-by: qwreey <me@qwreey.moe>
2026-08-25 12:26:53 +09:00

33 KiB

7라운드 손 트레이싱 발견 검증 패스H-55~H-106 판정 (2026-08-25)

무엇인가: .claude/qa-request/pre-implementation-handtrace-round7.md의 발견 52건이 정말 유효한지만 판정한 결과. 새 발견을 찾는 패스가 아니다 — 원문에 없는 새 항목은 이 문서에 없다.

왜 있나: 52건 전부를 한 세션이 한 컨텍스트에서 썼다. 쓴 세션은 자기가 뭘 잘못 봤는지 모른다는 게 이 코퍼스가 반복 확인한 사실이고 (.claude/conventions.md의 "작업 방식"), 판정이 끝나면 사용자가 batch로 결정을 내리므로 무효 항목이 섞여 있으면 그 비용을 그대로 문다.

⚠️ 이 패스가 지킨 규칙 — 재실행은 검증이 아니다. audit/handtrace-round7-reference-impl/README.md가 못박은 그대로, 4·5·6차 패스의 발견은 스파이크를 다시 돌려서 판정하지 않고 전사물(spikes/core.luau·dispatch.luau·chain.luau)과 base/의 확정 의사코드를 줄 단위로 대조해서 판정했다. Luau 언어 동작을 주장하는 부분(타입 스파이크, Relate GC)만 재실행으로 교차 확인했다 — 그쪽은 전사물이 아니라 언어 자체가 판정 대상이기 때문.


결론 요약

판정 건수 번호
유효 49 아래 표에서 "유효"인 전부
범위 축소 2 H-87, H-105
무효 0
중복 0

무효가 0건인 것 자체를 의심해봤다. 우선 의심 대상으로 지정된 네 축 (참조 구현 전사 오류 / 부정 주장 / 이미 닫힌 항목 / 트리거 과장)을 전부 개별로 훑었고, 실제로 걸린 것은 범위 축소 2건 + 부정·개수 주장 정정 2건 이다. 나머지가 살아남은 이유는 원문이 대체로 자기 주장의 근거를 base/ 원문 인용으로 달아뒀고, "이건 H-xx와 별개다"를 항목마다 직접 적어 중복을 미리 걷어냈기 때문이다.

다만 아래 넷은 배치 결정 전에 반드시 같이 읽을 것 — 판정은 유효지만 그대로 사용자에게 올리면 이미 내려진 판단을 다시 묻거나, 틀린 수치를 근거로 결정하게 된다:

  1. H-87 — 범위 축소. "코퍼스가 error 경로를 어느 쪽도 다루지 않는다"는 거짓이다. base/slot-plan.md가 같은 실패 모드를 명시하고 사용자가 2026-08-21에 "pcall로 감싸지 않는다"로 이미 판단했다.
  2. H-105 — 범위 축소. 개수를 직접 세보니 한국어 17이 아니라 약 23 이고, 17 + 6 = 23H-104의 "42곳"과 애초에 안 맞는다. 현상과 결정 필요성은 그대로 유효.
  3. H-88 — 유효, 부정 주장 1건 정정. "예외 안전성 계약이 코퍼스에 한 줄도 없다"는 과하다 — slot-plan.md선례가 하나 있고 그건 갈래 (b) 쪽을 가리킨다.
  4. H-104 — 유효, 개수 정정. "level 인자가 하나도 없다"는 전수 확인으로 참이지만, "42곳"은 산문 언급까지 센 수치로 보인다(실제 quad 자신의 error 코드 자리는 약 29).

참조 구현 대조 결과 (전사물 ↔ 원문)

README.md의도적 차이 3개(PeekDiffers / box.pos / 생명주기 게이팅 부재)를 미리 신고해뒀고, 그 셋은 차이로 세지 않았다. 나머지 차이를 전부 후보로 놓고 훑은 결과, 미신고 차이 3개를 찾았다 — 셋 다 발견을 만들어내지 않는다(전부 보수적 방향이거나 등가).

대조 항목 결과
EpochMap:Update/Refresh/Sync/TrackFrom state-epoch-plan.md §3과 일치
State:_receive(규칙 1~3) §4 의사코드와 축자 일치
State:_recomputerawInvalid = false 위치 §4 "재계산이 끝나면" 그대로 — H-85는 전사 오류가 아니다
State:Get(재계산 판정) §4 "재계산 판정" 절과 일치
Source:Setbit32.bnot(-self.Revision) §2와 일치(실측 표까지)
emitDown의 배열 스냅샷 source-state-plan.mdH-23 확정과 일치
GateNode:_flush(빈 배치 얼리리턴 → 스왑 → Sync → 전파) gate-plan.md 4·8번과 일치
GateNode:_receive(규칙 먼저 → unfold → 정책) gate-plan.md 4번과 일치
Dispatch.getOffsetAt(접두합 캐시) dispatch-core-plan.md축자 일치
recompute 에러 메시지 문자열까지 축자 일치
setLength/setOffsetSource 일치(아래 미신고 차이 3 참고)
chain.luauprocess/retractFrom 축자 일치(NOOP 마커 포함)

미신고 차이 3개:

  1. Blocker:Policy(emit, discard)가 2인자다. 확정 시그니처는 blocker:Policy(emit) -> onUpstreamEmit(blocker-plan.md). 다만 어느 스파이크도 discard를 넘기지 않는다(죽은 인자) — 결과에 영향 없음. 아이러니하게도 이 인자가 존재한다는 사실 자체가 H-55가 필요하다고 말하는 그 통로다.
  2. Blocker.handles가 평범한 배열이다. 문서는 "onunblock 핸들을 blocker의 weak 배열에 등록". 전사물이 강하게 들었으므로 H-63(2) (핸들이 GC되면 Off()가 조용히 no-op)를 가려주는 방향이다 — 발견을 만들어내는 게 아니라 오히려 덜 나오게 한다.
  3. M.Observer가 "등록 즉시 1회 실행"을 안 하고, self.owner = state (상류 강참조)를 둔다. 앞의 것은 dispatch.luau가 그 자리에 명시 gatedRecompute()를 넣어 등가로 보정했다. 뒤의 것은 문서에 없는 필드인데 상류를 살려두는 방향이라 중간 State GC 문제를 가린다 — 그래서 H-98은 일부러 q.Observer를 안 쓰고 별도 Obs를 썼다(원문이 그 이유를 코드 주석으로 밝혀뒀다, 정직한 처리).

즉 전사 오류로 생긴 발견은 없다.


항목별 판정

번호 판정 한 줄 근거
H-55 유효 gate-plan.md 4번 "emit 없이 푸는 경로는 집합을 버려야 한다" + setup: (emit: () -> ()) -> (onUpstreamEmit: () -> ()) — 정책이 withheld에 닿는 통로가 실제로 없다
H-56 유효 lifecycle-pattern.md (4) "발화 시 각 구독자에 대해 canExecute(observer)를 확인하고, 거짓이면 그 구독자만 조용히 건너뜀" + isBoundAlive가 State 노드엔 false
H-57 유효 effect-plan.md 2번 "unbindLifetime은 cleanup을 부르지 않는다" + source-state-plan.md의 retract 클로저가 unbindLifetime(v)만 부름
H-58 유효 bindLifetimeBindData:SetWeak(value,"gcconn",…) 뒤에 _bindDestroying을 부르고, ref-plan.md가 *"등록 즉시 그 값으로 1회 호출됨"*을 확정
H-59 유효 Ref 콜백을 거는 코드가 _bindDestroying(inst)뿐이고 :Subscribe()inst가 없음. ref-plan.md는 *":Unsubscribe()에서 :Uncallback한다"*고 적음
H-60 유효 코퍼스 전체 grep — Rerun은 호출 5곳뿐, 정의 0곳. ROADMAP.md M2의 "Effect 구현 시 같이 만들 것" 목록에도 없음
H-61 유효 source-state-plan.md "그냥 이 State가 계속 재계산되게만 강제하고 싶을 때 씀" vs 같은 문서 "값을 안 실어줌 — 반드시 Get()을 다시 해야 함"
H-62 유효 "중간에 만들어놓고 아무도 안 보는 State는 구독 등록 자체가 안 일어남"(lazy) vs "이 노드는 Observer와 같은 패턴(외부 weak table)으로 상위 노드의 구독자 목록에 등록됨"(eager) + §4 시딩
H-63 유효 blocker-plan.md "onunblock 핸들을 blocker의 weak 배열에 등록" — 구멍/강참조 주체/순회 중 등록 셋 다 미정. (2)가 GC 타이밍 의존이라 제일 아프다
H-64 유효(미확정) effect-plan.md "bind/unbind가 대칭이라 포탈이 자연히 성립한다" — State dep 캐치업 경로가 실제로 없음. H-58과 같이 정해야 하는 건 원문 서술 그대로
H-65 유효(미확정) lifecycle-pattern.md "바인딩이 죽은 뒤의 재사용은 허용" + Destroying 클로저의 self._cleanup = nil 소진
H-66 유효 typing-limits.md:245EffectHandle 반환, source-state-plan.md:169state:Observer(fn) → Observer
H-67 유효 blocker-plan.md의 두 번째 용례 "이 용례는 state:Block()을 전혀 호출하지 않으므로 gated state도 … 생기지 않는다" — 결론은 안 바뀐다는 원문 스코핑도 정확
H-68 유효 source-state-plan.md/state-epoch-plan.md/store-plan.md 전수 grep — 동일값 Set의 리비전 처리 서술 0건
H-69 유효 gate-plan.md 4번의 무조건 삽입 + flush 진입 시 newWeakK() 스왑. 정확성 아님(원문도 그렇게 적음)
H-70 유효 effect-plan.md의 deps 절에 nil 구멍·타입 검증·같은 Ref 중복 서술 0건. _refCallbacks[ref] = cb 덮어쓰기도 실재
H-71 유효 커밋된 quad-base/src/Relate.luau(buckets __mode="k" + 평범한 StrongMap)를 그대로 돌려 대조군 0 / 케이스1 50 / SetWeak 0 재현. relate-plan.md가 *"단일 Relate 안에서 일어나는 한 안전함"*이라 단언
H-72 유효 state-epoch-plan.md §3의 4개 연산이 전부 쓰기를 함(Update = 읽고 덮음). §4 게이트 예외는 "판정(규칙 1~3)은 똑같이 먼저 돈다" + "emitEpochMap:Update를 수신 시점에 부르지 않으므로" — 둘을 동시에 만족할 연산이 없다
H-73 유효 getDynamic<T>(store,name): Source<T> 재현 — Source<unknown>로 떨어짐. store-plan.md가 확정한 표기는 store:GetDynamic<<T>>(name): Source<T>
H-74 유효 store-plan.md의 확정 스케치가 table.clone(defaults) 결과에 Source직접 써넣음 → raw 키라 __index 미실행. "고정 메소드 테이블을 먼저 확인" 방어가 eager 경로엔 없음
H-75 유효 재현 — 평평한 선언은 실패(Expected type table, got 'T'), ②쪼개기는 진단 0건. store-plan.mdWrapStore 스케치는 평평하고 typing-limits.md §5는 그걸 *" 검증 완료"*라 적음
H-76 유효 재현 — type function이 바깥 별칭을 반환하면 returned a non-type value. store-plan.md의 *"문제없이 Source<string> 자리에 대입 가능"*은 검증된 적 없음
H-77 유효 커밋된 Relate.luauWEAK_VALUE_MT = { __mode = "v" }키는 항상 강함. 재현: 대조군 0 / SetStrong 50 / SetWeak도 50. 커밋된 init.luau:23"weak-키잉되므로 별도 정리 불필요" 주석이 거짓이 됨
H-78 유효 이 검증 세션에서 그대로 재현 — smoke.init/smoke.plugincould not resolve child component "src"로 죽고, 스파이크 23은 링크 실패 진단 2건만 내고 의도한 음성 대조군이 안 뜬다. ROADMAP.md:198의 *"전부 PASS"*엔 날짜도 전제조건도 없음
H-79 유효 attribute-plan.mdattr:NameMap(): {[string]: Source<any>} vs store-plan.md에 열거 표면 0건. eager/lazy 이중 모델이라 키 집합이 접근 이력에 좌우됨
H-80 유효 ROADMAP.md:447 체크박스가 Source/State/Store 셋뿐. 같은 M2가 Effect/is* 전량/bindLifetime 4종/Blocker/Relate를 탑레벨로 얹음. State는 런타임 생성자가 코퍼스에 없음
H-81 유효 isModifier 게이트 체크박스는 ROADMAP.md:1076(M7)에만 있고 M2엔 0건. 적용 지점이 전부 M2 파일. source-state-plan.md가 독립 Source(someModifier)를 추가로 요구하는데 modifier-plan.md 7번 목록엔 없음
H-82 유효 근거 2번은 *"w의 캐시를 c1/c2가 공유"*인데 같은 절이 :With를 *"계산 함수는 없고 값은 self를 그대로 통과(pass-through)"*로 확정 — 공유될 계산이 없다. 결론 불변이라는 스코핑도 정확
H-83 유효 luau로 확인 — table.clone(nil)table expected, got nil. 같은 절이 *"defaults는 선택"*을 확정
H-84 유효 ROADMAP.md M2 절 grep — :With/state:Block/Source:Emit 개별 체크박스 0건
H-85 유효 전사 대조 통과 — §4 *"재계산이 끝나면 … rawInvalid = false"*가 원문이고 전사물이 그 순서다. 코퍼스에 rawInvalidfn 앞에 내리라는 서술 0건
H-86 유효 gate-plan.md 5번 "pending 같은 정책 상태는 HasBlockedEmit으로 흡수한다" + blocker-plan.mdHasBlockedEmitgated state의 필드로 확정 + "구현 시 두 개를 따로 들지 말 것" → 정책이 읽을 통로가 없다. debounce-throttle-plan.md가 그 값을 두 자리에서 요구
H-87 범위 축소 아래 상세
H-88 유효(부정 주장 1건 정정) state-epoch-plan/gate-plan/blocker-plan 셋에 error 0건 확인. 다만 *"코퍼스에 한 줄도 없다"*는 과함 — 아래 상세
H-89 유효 gate-plan.md 4번이 :Sync(batch)를 *"실제로 전파할 때"*라고만 하고 앞/뒤를 안 정함. blocker-plan.mdOff()를 *"IsBlocked = false로 먼저 설정, 그 다음 등록된 onunblock 핸들 전부 실행"*으로 확정 → 중간 실패 시 "안 막혔는데 안 풀린" 상태
H-90 유효 effect-plan.md의 확정 의사코드가 from(루트 Epoch)으로만 접음 + gate-plan.md 3번 "게이트를 통과하지 않은 값도 :Get()으로는 보인다". 전사물이 그 의사코드 그대로
H-91 유효 §8 인용 "이제 정말로 항상 state 는 get 이 최신을 던지는게 맞다" vs tween-plan.mdAnimate"info.Style이 State여도 이 내부 :Compute의 trailing deps로 안 넘어가므로 구독 목록에 안 걸림" — 미선언 읽기를 설계로
H-92 유효 §2가 테이블 리비전을 기각한 근거가 *"Set 한 번마다 테이블 하나를 할당"*인데 H-23 스냅샷은 파동이 지나는 노드 수만큼 할당. §7 비용 절이 이걸 안 셈
H-93 유효(미해결의 파생) §3 "키는 weak다. epoch가 죽으면 항목이 사라진다" + §4 순회가 *"자기가 이미 들고 있는 키 전부"*만 봄 → §4가 시딩에 대해 경고한 *"비어 있으면 '훑을 게 없으니 유효하다'로 오판한다"*가 수거로 재발
H-94 유효 luau-analyze 재현 — GateFactory가 함수 타입 자리에서 거부(제네릭·비제네릭 양쪽). gate-plan.md 2번이 *"확인할 필요도 없어졌다"*고 접었는데 같은 항목이 *"Debounce/Throttle:Apply 그대로 둔다"*를 확정
H-95 유효 재현 — Not all codepaths in this function return '(() -> ())?' / Expected 'El?, number?', but got 'El?'. 유니온 해법은 통과하고 음성 대조군만 잡힘(ty7_fix 진단 1건)
H-96 유효 재현 — deps 0개는 통과, deps가 붙으면 Consider annotating the return + dep 파라미터 미해소. 2차 패스 부록의 자기 정정도 정확(그 코드엔 dep 주석이 달려 있었음)
H-97 유효 ROADMAP.md:274 "인터페이스만 … 실 구현 없음 — quad-roblox 실 구현은 M8" + module-lifecycle-plan.md:343 "명시적으로 에러내는 스텁" + architecture.mdbindLifetime/canBound/canExecute도 주입 대상으로 명시. 커밋된 mock에 signal/Connection이 이미 있다는 관찰도 확인
H-98 유효(미해결의 파생) source-state-plan.md:1178 "참조를 아무 데도 안 담아도 정상" / :1185 "GC되지 않고 영원히 계속 실행됨". 전사물이 상류 되참조를 일부러 뺀 이유를 코드에 밝혀둠
H-99 유효 architecture.md 소스 트리 전수 — Observer.luau 0건, 레지스트리(Subscribed) 소유 모듈 미지정. EpochMap.luau는 *"State.luau에 묻지 않고 별도 모듈"*로 명시돼 대비가 성립
H-100 유효 재현 — { [Source<number>]: true }가 `Epoch
H-101 유효 전사 대조 통과recomputesum 누적/꼬리 Length:Set(sum)/for i = 1, bk.N이 전부 원문 축자. gate-plan.md 6번이 *"유한한 재진입은 지원한다"*고 확정한 경로
H-102 유효 dispatch-core-plan.mdgatedRecomputei를 캡처(bk.invalidAfter = math.min(bk.invalidAfter, i)). slot-plan.md의 splice 요구 목록(observers 당김 / bk.N 감소 / invalidAfter 당김) 셋 어디에도 옮겨진 클로저의 인덱스가 없다
H-103 유효 전사 대조 통과list[index] = { handler = h, retractor = NOOP } 마커가 원문 그대로. attribute-plan.mdAttributeKeyHandler.processnameClaims:SetStrong 뒤에 setAttribute를 부르는 것도 원문 확인
H-104 유효(개수 정정) 전수 grep — error(x, N) 형태는 fallback-plan.md:85산문 설명 1건뿐, quad 자신의 error 코드 자리엔 0건. "42곳"만 정정 필요(아래 상세)
H-105 범위 축소 아래 상세
H-106 유효 getOffsetAtcur += contribution(bk, i)nil 가드 없음(원문 확인). recomputeC-6 메시지를 갖고, setOffsetSource가 등록 시점마다 getOffsetAt을 부름

상세 — 범위 축소 2건

🔻 H-87 — 범위 축소: 절반은 이미 사용자가 판단한 항목이다

현상은 맞다. dispatch-core-plan.md의 배치 게이팅이 On() … 사용자 코드 … OffWithoutEmit() 모양이고, 중간에서 던지면 그 owner의 Blocker가 영구 On으로 남는다 — 원문 인용도 정확하고 재현도 타당하다.

틀린 것은 부정 주장이다. 원문은 "문서는 yield만 UB로 못박고 error는 안 다룬다", *"error 경로는 그 실패 모드에 정확히 해당하는데 어느 쪽도 다루지 않는다"*라고 적는다. base/slot-plan.mdmaterializeSlotTree 끝(blocker:OffWithoutEmit() 직전)에 정확히 그 내용이 이미 적혀 있고, 사용자 판단까지 붙어 있다:

[2026-08-21 감사] 위 재귀가 **예외를 던지면 이 줄에 도달하지 못해 Blocker가 켜진 채 남는다** … pcall로 감싸지 않는 것이 **사용자 판단(2026-08-21)**: 마운트 도중 예외는 quad가 복구를 보장하지 않는 상태이고 … 아직 실제로 밟은 적 없는 경로다 — conventions.md의 "드문 오용이나 가상의 미래 요구까지" 절이 세운 원칙 그대로. 실제로 물리면 그때 넣는다.

그래서 무엇이 바뀌나:

  • 원문의 갈래 (a)는 *"감싸는 자리는 정확히 둘 — Dispatch.drivematerializeSlotTree"*라고 적는데, 뒤쪽은 닫힌 결정이다. 그대로 올리면 사용자가 2026-08-21의 자기 판단을 모른 채 다시 결정하게 된다.
  • 남는 진짜 질문은 하나: 그 판단을 Dispatch.drive까지 확장할 것인가. 확장을 고민할 값이 있는 이유는 원문이 짚은 비대칭이다 — materializeSlotTree는 같은 Slot에 재마운트가 일어나면 자가치유되지만, Dispatch.drive의 owner는 inst이고 배열 파트 배치는 생성 시 한 번뿐이라 사실상 영구다.
  • 심각도: 🔴🟡로 읽는 게 맞다. 새 결함 보고가 아니라 기존 판단의 적용 범위 문제이고, 같은 원칙("실제로 물리면 그때")을 적용하면 지금은 아무것도 안 하는 게 답일 수 있다.

🔻 H-105 — 범위 축소: 개수가 안 맞는다

현상과 결정 필요성은 유효하다. error 메시지 언어가 갈려 있고, 갈리는 경계도 원문 서술대로다 — 동적 경로 가드 4형제 + 모듈 초기화 + attribute 이름 충돌만 영어, 나머지는 한국어. conventions.md의 *"사용자가 보게 될 것은 한국어"*가 quad 라이브러리 사용자에게도 적용되는지는 실제로 정해진 적이 없고, 이건 에이전트가 임의로 정할 수 없는 사용자 결정이라는 원문 판단도 정확하다.

틀린 것은 수치다. 직접 세었다(.claude/base/*.md 전수, 한글 포함 여부로 분류):

원문 주장 실제
error( 등장 총계 42 46(산문 언급 포함)
그중 실제 error 코드 자리 (=42로 읽힘) 약 29
영어 메시지 6 6 (binding 가드 4 + Quad module already initialized + attribute … already bound)
한국어 메시지 17 약 23

17 + 6 = 23은 원문 자신이 든 "42곳"과 애초에 안 맞는다. 결정에 영향은 없지만(어느 쪽이든 "지금 정해두면 한 번에 맞출 수 있다"는 논거는 그대로), H-104가 같은 42를 쓰므로 두 항목의 수치를 같이 고쳐야 한다.


상세 — 정정이 필요한 유효 항목 2건

H-88 — 부정 주장 1건 정정

"state-epoch-plan.md/gate-plan.md/blocker-plan.md 셋에 'error'라는 단어가 0건"확인, 참(각 0건).

"코퍼스에 예외 안전성 계약이 한 줄도 없다"과하다.H-87이 인용한 slot-plan.md의 문단이 유일한 선례이고, 그 선례가 고른 방향은 원문의 갈래 (b)("던지면 UB로 명문화, 실제로 물리면 그때 넣는다")다. 전파 루프에 대한 결정이 선례 없이 백지에서 시작하는 게 아니라 그 선례를 따를지 갈릴지의 문제라는 걸 사용자에게 같이 보여야 한다.

H-104 — 개수 정정

"level 인자가 하나도 없다"전수 확인으로 참. grep -rn "error([^)]*,\s*[0-9]"가 잡는 건 fallback-plan.md:85산문 설명(사용자 코드가 던지는 쪽) 1건뿐이고, quad 자신의 error 코드 자리엔 level 인자가 0건이다.

"42곳"약 29곳으로 정정(위 H-105 표). "사용자 입력 검증"과 "내부 불변식 위반"을 나누자는 제안 자체는 그대로 유효하다.


batch용 요약 — 한 결정으로 닫히는 묶음

번호순 52건이 아니라 결정 단위 12묶음으로 재편성했다. 같은 묶음 안의 항목은 결론이 서로를 규정하므로 따로 답하면 갈라진다.

🅐 게이트 정책의 상태 접근 통로 — H-55 · H-86 · H-72 (+ 종속 H-63)

정책이 손에 쥔 게 emit 하나뿐이라 버릴 수도 없고(H-55) 읽을 수도 없다(H-86). 게다가 게이트 노드 자신도 emitEpochMap갱신 없이 비교할 연산이 없다(H-72). 셋 다 "Epoch 일반화 때 게이트 쪽 요구가 표면에 덜 반영됐다" 하나에서 나온다.

  • 한 번에 닫히는 형태가 있다: emit에 인자와 반환값을 준다 — emit(commit: boolean?) -> boolean(H-55의 갈래 (b) + H-86의 갈래 (a)가 그대로 합성). H-49의 *"setup 시그니처는 안 바뀐다"*를 인자 목록은 유지한 채 최소로만 되짚는다.
  • H-72EpochMap:Peek(...) 한 줄 추가로 독립적으로 닫힌다.
  • H-63(2)(정책 클로저가 onunblock 핸들을 강하게 잡아야 한다)는 이 묶음의 결론이 정해져야 문장을 쓸 수 있다.
  • 같이 움직일 문서: gate-plan.md 4·5번, blocker-plan.md, debounce-throttle-plan.md 7절, state-epoch-plan.md §3.

🅑 전파 루프를 코드로 확정 — H-56 · H-62 · H-61 (+ H-92 · H-69)

지금 전파 루프는 산문만 있고 의사코드가 없다. 그 공백에서 셋이 동시에 나온다 — 구독자가 State 노드면 canExecute에 걸려 죽고(H-56), 엣지 등록 시점이 lazy/eager로 갈려 있고(H-62), 인자 없는 Observer()Get()을 안 불러 자기 용도를 못 한다(H-61).

  • 한 블록을 쓰면 셋이 같이 닫힌다: 구독자 종류별 분기 + 등록 시점 + H-23 스냅샷 + from 전달 + 재진입.
  • 그 블록을 쓸 때 스냅샷 할당 비용(H-92)과 게이트 통과 모드 할당(H-69)도 같은 자리에서 정하면 된다(정확성 아님 — §2/§7 서술만 실제와 맞추면 됨).
  • H-84가 이 묶음의 로드맵 각주다(:With/state:Block/Source:Emit 체크박스 부재).

🅒 예외 안전성 — 하나의 질문, 네 자리 — H-88 · H-89 · H-87 · H-103

질문은 하나다: "예외가 나면 부기를 어디까지 되돌리는가."

  • 자리는 넷 — 전파 루프(H-88), 게이트 flush + Blocker:Off() 순회(H-89), 배치 게이팅(H-87), Dispatch 체인 슬롯(H-103).
  • 선례가 이미 하나 있다(slot-plan.md, 2026-08-21 사용자 판단: 감싸지 않는다). 그래서 실질 선택은 "그 선례를 전 자리에 확장한다" 대 **"전파/체인만 예외로 감싼다"**다.
  • 비용 축이 자리마다 다르다는 게 결정 재료다 — 배치당 1회(H-87) / 구독자당(H-88, hot path) / 자리당(H-103, hot path) / flush당(H-89).

🅓 "긴 연산의 꼬리가 도중 변경을 덮어쓴다" — H-85 · H-101

같은 모양이 두 자리에서 나왔다 — 재계산 꼬리의 rawInvalid = false(H-85)와 recompute 꼬리의 Length:Set(sum)(H-101). 원칙 하나로 같이 정하는 게 자연스럽다: "긴 연산의 마지막 줄은 그 연산 도중 갱신된 값을 무조건 덮어쓰지 않는다."

  • H-85는 한 줄 이동(rawInvalid = falsefn 앞으로), H-101은 재진입 가드 또는 세대 번호. 둘 다 원문이 대조군까지 확인해뒀다.

🅔 Effect 배선 — H-57 · H-58 · H-59 · H-60 (+ H-64 · H-65 · H-70)

전부 effect-plan.md 한 문서이고 서로 얽혀 있다:

  • Ref 콜백 등록 자리를 _bindDestroying에서 떼어 공유 헬퍼로 만들면 H-58(바인드마다 Rerun)과 H-59(Subscribe가 콜백을 안 검)이 같은 수정으로 닫힌다 — 원문도 *"H-58의 갈래 (a)와 같은 자리"*라고 적는다.
  • 그 결정이 H-64(포탈 캐치업)를 규정한다 — 즉시 호출을 없애면 캐치업 Rerun이 필요해진다.
  • H-57(값 교체 cleanup)과 H-60(Rerun 정의)은 그 다음에 쓰면 된다 — H-60이 정의되지 않으면 나머지 셋을 쓸 문장이 없다. H-60부터.
  • H-65(죽은 바인딩 재사용 뒤 inert)와 H-70(deps 세부)은 독립이지만 같은 문서라 한 번에 처리하는 게 싸다.

🅕 Relate 되참조 규칙 재작성 — H-71 · H-77

relate-plan.md의 "위험한 패턴" 절 하나를 다시 쓰면서 둘 다 닫힌다. 지금 그 절은 위험을 "값" 기준으로만 서술하는데, 실제 슬롯은 셋이다:

슬롯 SetStrong SetWeak
바깥 키(inst) weak weak
내부 키(key) 강함 강함H-77, 규칙 자체가 없었다
값(value) 강함 ← H-71 weak
  • H-71의 해법 (b)(SetWeak으로 낮춤)가 H-77엔 안 듣는다는 게 실측으로 확인됐으므로 두 슬롯을 따로 정해야 한다.
  • 부수로 반드시 같이 고칠 것: 커밋된 quad-base/src/init.luau:23의 주석 ("weak-키잉되므로 별도 정리 불필요")이 지금 거짓이다. luau-test/done/07에 되참조 음성 대조군 추가도 원문 제안대로.

🅖 Store 표면 — H-73 · H-74 · H-79 · H-83 (+ H-75 · H-76)

H-73을 먼저 정하면 H-74가 통째로 사라진다(원문 갈래 (c)) — GetDynamic을 탑레벨 함수로 옮기면 예약 키 자체가 안 생긴다. 그러니 순서가 있다:

  1. H-73question.md 최우선 항목. 실측 결론은 *"어느 표면이든 T를 못 묶으므로 비제네릭 + 호출부 캐스트"*이고, 그러면 탑레벨 쪽이 명확히 유리하다.
  2. H-74는 1의 결론이 (c)면 소멸, 아니면 (a)(생성 시 예약 키 대조 error).
  3. H-79(열거 표면)는 독립 — 그룹 Attribute작동하려면 필요하다.
  4. H-83은 한 글자(table.clone(defaults or {})).

H-75/H-76은 같은 Store지만 축이 다르다 — 런타임이 아니라 정본 Source<T> 선언 모양이다. WrapStore가 ②쪼개기를 안 하면 store.key:Compute(무주석)이 깨지고(H-75), Revision 하나만 어긋나도 store.keySource<number> 자리에 못 들어간다(H-76). M2에서 Source/State 타입을 쓰기 전에 정해야 하는 것이라 🅗보다 먼저다.

🅗 확정 시그니처 vs 확정 관용구 — H-94 · H-95 · H-96 · H-100

전부 같은 모양이다 — 문서가 확정한 타입이 문서가 확정한 관용구를 통과시키지 않는다. 넷 다 실측으로 해법까지 대조돼 있어 결정은 "시그니처를 고칠까 관용구를 고칠까" 하나다.

  • H-94: :Apply__call 팩토리를 못 받음 → 유니온으로 열기 vs Debounce가 함수만 반환.
  • H-95: Effectfn:ListupdateFn → 가변 반환 팩/유니온 vs "항상 명시적으로 반환하라" 계약.
  • H-96: 결정이 아니라 문서에 경계 한 줄(§7에 "단 trailing deps가 붙으면 dep 파라미터엔 주석이 필요하다").
  • H-100: EpochSet 리터럴에 캐스트 필요 — 구현 시 정하면 됨.

🅘 부기(Length/Offset) 결함 — H-102 · H-106

  • H-102실제 오작동이다(Slot:Remove 뒤 다른 항목 길이 변경 → 뒤 형제 offset이 낡음). 규칙 셋이 각각 다른 날 추가되며 "옮겨진 클로저 안의 인덱스"가 어느 목록에도 안 들어간 자리다.
  • H-106은 한 줄 가드(contributionnil 검사) — C-6이 승격시킨 진단이 새 경로에서 반쯤 무효화된 것.

🅙 error 계약 — H-104 · H-105

42곳(실제 ~29곳)을 쓰기 전에 정하면 한 번에 맞고, 나중에 바꾸면 전수를 다시 훑어야 한다.

  • H-104: 사용자 입력 검증(level 2) / 내부 불변식(level 1) 이분.
  • H-105: 메시지 언어. 사용자만 정할 수 있다conventions.md의 한국어 규칙이 quad 라이브러리 사용자에게도 적용되는지가 정해진 적 없다.
  • ⚠️ 두 항목의 수치를 위 정정대로 같이 고칠 것.

🅚 "중간 State GC" 미해결에 딸린 것 — H-93 · H-98 (+ H-62의 eager 갈래)

셋 다 question.md 최우선 항목의 파생이라 그 항목을 닫을 때 같이 봐야 한다는 게 결론이다. 지금 그 항목은 실패 모드를 하나로만 서술한다:

  • (기존) 중간 State가 수거되어 전파가 끊긴다.
  • (H-93) 루트 Epoch가 수거되면 :Refresh()false를 줘 낡은 값을 최신이라고 확신하고 반환한다 — 관측이 훨씬 늦다. 예고된 스파이크에 이 확인을 하나 더 넣어야 한다.
  • (H-98) :Subscribe()공개 계약 문장 두 개가 이 결과에 종속돼 있다 — 나쁜 쪽으로 닫히면 *"GC도 안 되고 발화도 안 한다"*가 된다.
  • (H-62) eager로 확정하면 이 미해결이 더 절실해진다(원문 지적 그대로).

🅛 저장소·로드맵 정합 — H-78 · H-80 · H-81 · H-97 · H-99

설계 결정이 아니라 **"M2를 짜기 시작하면 바로 막히는 것"**들이다.

  • H-78이 제일 급하다 — 지금 이 저장소에서 smoke.init/smoke.plugin이 안 돌고 luau-analyze거짓 클린을 준다(검증 세션에서 재현). M2가 Quad에 필드를 추가하는 첫 마일스톤인데, 그게 먹었는지 확인하는 유일한 경로가 정확히 그 망가진 경로다.
  • H-97(mock에 생명주기 배선이 없어 전파 테스트가 아예 못 돎)이 그 바로 옆.
  • H-80/H-81/H-99는 체크박스·소스 트리 갱신.

🅜 문서 정합 — 결정 불필요 — H-66 · H-67 · H-82 · H-91

전부 결론은 안 바뀌고 근거·표기만 고치면 되는 것이다. 사용자 결정을 기다릴 이유가 없어 반영 시점에 그냥 고치면 된다.

  • H-66: 표의 EffectHandle 반환Observer 반환.
  • H-67: OffWithoutEmit 근거가 드는 용례를 gated state 쓰는 것으로 교체.
  • H-82: :With 근거 2번을 "중복 계산"에서 **"엣지 수와 에포크 부기"**로.
  • H-91: §8 인용을 *"선언한 의존성에 대해서는 항상 최신"*으로 좁히고 Animate를 의도적 예외로 가리키기.

이 검증 패스가 안 한 것

  • 원문에 없는 새 발견을 찾지 않았다. 대조 중에 눈에 걸린 것도 이 문서에 안 적었다(이 패스의 정의가 판정이므로).
  • base/를 하나도 안 고쳤다. 반영은 사용자 결정 뒤 -followup.md에서.
  • 1~3차 패스의 인라인 재현 코드를 전부 재실행하진 않았다H-71/H-77/ H-73/H-75/H-76/H-78/H-83(즉 Luau 언어 동작·저장소 상태를 주장하는 것)만 직접 돌렸고, 나머지는 base/ 원문 대조로 판정했다.