발견을 더 찾는 패스가 아니라 이미 나온 것이 유효한지만 판정한다. 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>
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와 별개다"를 항목마다 직접
적어 중복을 미리 걷어냈기 때문이다.
다만 아래 넷은 배치 결정 전에 반드시 같이 읽을 것 — 판정은 유효지만 그대로 사용자에게 올리면 이미 내려진 판단을 다시 묻거나, 틀린 수치를 근거로 결정하게 된다:
H-87— 범위 축소. "코퍼스가 error 경로를 어느 쪽도 다루지 않는다"는 거짓이다.base/slot-plan.md가 같은 실패 모드를 명시하고 사용자가 2026-08-21에 "pcall로 감싸지 않는다"로 이미 판단했다.H-105— 범위 축소. 개수를 직접 세보니 한국어 17이 아니라 약 23 이고,17 + 6 = 23이H-104의 "42곳"과 애초에 안 맞는다. 현상과 결정 필요성은 그대로 유효.H-88— 유효, 부정 주장 1건 정정. "예외 안전성 계약이 코퍼스에 한 줄도 없다"는 과하다 —slot-plan.md에 선례가 하나 있고 그건 갈래 (b) 쪽을 가리킨다.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:_recompute의 rawInvalid = false 위치 |
§4 "재계산이 끝나면" 그대로 — H-85는 전사 오류가 아니다 |
State:Get(재계산 판정) |
§4 "재계산 판정" 절과 일치 |
Source:Set의 bit32.bnot(-self.Revision) |
§2와 일치(실측 표까지) |
emitDown의 배열 스냅샷 |
source-state-plan.md의 H-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.luau의 process/retractFrom |
축자 일치(NOOP 마커 포함) |
미신고 차이 3개:
Blocker:Policy(emit, discard)가 2인자다. 확정 시그니처는blocker:Policy(emit) -> onUpstreamEmit(blocker-plan.md). 다만 어느 스파이크도discard를 넘기지 않는다(죽은 인자) — 결과에 영향 없음. 아이러니하게도 이 인자가 존재한다는 사실 자체가H-55가 필요하다고 말하는 그 통로다.Blocker.handles가 평범한 배열이다. 문서는 "onunblock 핸들을 blocker의 weak 배열에 등록". 전사물이 강하게 들었으므로H-63(2) (핸들이 GC되면Off()가 조용히 no-op)를 가려주는 방향이다 — 발견을 만들어내는 게 아니라 오히려 덜 나오게 한다.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 |
유효 | bindLifetime이 BindData: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:245가 EffectHandle 반환, source-state-plan.md:169는 state: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.md의 WrapStore 스케치는 평평하고 typing-limits.md §5는 그걸 *"✅ 검증 완료"*라 적음 |
H-76 |
유효 | 재현 — type function이 바깥 별칭을 반환하면 returned a non-type value. store-plan.md의 *"문제없이 Source<string> 자리에 대입 가능"*은 검증된 적 없음 |
H-77 |
유효 | 커밋된 Relate.luau의 WEAK_VALUE_MT = { __mode = "v" } — 키는 항상 강함. 재현: 대조군 0 / SetStrong 50 / SetWeak도 50. 커밋된 init.luau:23의 "weak-키잉되므로 별도 정리 불필요" 주석이 거짓이 됨 |
H-78 |
유효 | 이 검증 세션에서 그대로 재현 — smoke.init/smoke.plugin이 could not resolve child component "src"로 죽고, 스파이크 23은 링크 실패 진단 2건만 내고 의도한 음성 대조군이 안 뜬다. ROADMAP.md:198의 *"전부 PASS"*엔 날짜도 전제조건도 없음 |
H-79 |
유효 | attribute-plan.md의 attr: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"*가 원문이고 전사물이 그 순서다. 코퍼스에 rawInvalid를 fn 앞에 내리라는 서술 0건 |
H-86 |
유효 | gate-plan.md 5번 "pending 같은 정책 상태는 HasBlockedEmit으로 흡수한다" + blocker-plan.md가 HasBlockedEmit을 gated 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.md가 Off()를 *"IsBlocked = false로 먼저 설정, 그 다음 등록된 onunblock 핸들 전부 실행"*으로 확정 → 중간 실패 시 "안 막혔는데 안 풀린" 상태 |
H-90 |
유효 | effect-plan.md의 확정 의사코드가 from(루트 Epoch)으로만 접음 + gate-plan.md 3번 "게이트를 통과하지 않은 값도 :Get()으로는 보인다". 전사물이 그 의사코드 그대로 |
H-91 |
유효 | §8 인용 "이제 정말로 항상 state 는 get 이 최신을 던지는게 맞다" vs tween-plan.md의 Animate가 "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.md가 bindLifetime/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 |
유효 | 전사 대조 통과 — recompute의 sum 누적/꼬리 Length:Set(sum)/for i = 1, bk.N이 전부 원문 축자. gate-plan.md 6번이 *"유한한 재진입은 지원한다"*고 확정한 경로 |
H-102 |
유효 | dispatch-core-plan.md의 gatedRecompute가 i를 캡처(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.md의 AttributeKeyHandler.process가 nameClaims:SetStrong 뒤에 setAttribute를 부르는 것도 원문 확인 |
H-104 |
유효(개수 정정) | 전수 grep — error(x, N) 형태는 fallback-plan.md:85의 산문 설명 1건뿐, quad 자신의 error 코드 자리엔 0건. "42곳"만 정정 필요(아래 상세) |
H-105 |
범위 축소 | 아래 상세 |
H-106 |
유효 | getOffsetAt의 cur += contribution(bk, i)에 nil 가드 없음(원문 확인). recompute만 C-6 메시지를 갖고, setOffsetSource가 등록 시점마다 getOffsetAt을 부름 |
상세 — 범위 축소 2건
🔻 H-87 — 범위 축소: 절반은 이미 사용자가 판단한 항목이다
현상은 맞다. dispatch-core-plan.md의 배치 게이팅이
On() … 사용자 코드 … OffWithoutEmit() 모양이고, 중간에서 던지면 그
owner의 Blocker가 영구 On으로 남는다 — 원문 인용도 정확하고 재현도
타당하다.
틀린 것은 부정 주장이다. 원문은 "문서는 yield만 UB로 못박고 error는
안 다룬다", *"error 경로는 그 실패 모드에 정확히 해당하는데 어느 쪽도
다루지 않는다"*라고 적는다. base/slot-plan.md의 materializeSlotTree
끝(blocker:OffWithoutEmit() 직전)에 정확히 그 내용이 이미 적혀 있고,
사용자 판단까지 붙어 있다:
[2026-08-21 감사] 위 재귀가 **예외를 던지면 이 줄에 도달하지 못해 Blocker가 켜진 채 남는다** …pcall로 감싸지 않는 것이 **사용자 판단(2026-08-21)**: 마운트 도중 예외는 quad가 복구를 보장하지 않는 상태이고 … 아직 실제로 밟은 적 없는 경로다 —conventions.md의 "드문 오용이나 가상의 미래 요구까지" 절이 세운 원칙 그대로. 실제로 물리면 그때 넣는다.
그래서 무엇이 바뀌나:
- 원문의 갈래 (a)는 *"감싸는 자리는 정확히 둘 —
Dispatch.drive와materializeSlotTree"*라고 적는데, 뒤쪽은 닫힌 결정이다. 그대로 올리면 사용자가 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-72는EpochMap:Peek(...)한 줄 추가로 독립적으로 닫힌다.H-63(2)(정책 클로저가 onunblock 핸들을 강하게 잡아야 한다)는 이 묶음의 결론이 정해져야 문장을 쓸 수 있다.- 같이 움직일 문서:
gate-plan.md4·5번,blocker-plan.md,debounce-throttle-plan.md7절,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 = false를fn앞으로),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을
탑레벨 함수로 옮기면 예약 키 자체가 안 생긴다. 그러니 순서가 있다:
H-73—question.md최우선 항목. 실측 결론은 *"어느 표면이든T를 못 묶으므로 비제네릭 + 호출부 캐스트"*이고, 그러면 탑레벨 쪽이 명확히 유리하다.H-74는 1의 결론이 (c)면 소멸, 아니면 (a)(생성 시 예약 키 대조 error).H-79(열거 표면)는 독립 — 그룹Attribute가 작동하려면 필요하다.H-83은 한 글자(table.clone(defaults or {})).
H-75/H-76은 같은 Store지만 축이 다르다 — 런타임이 아니라 정본
Source<T> 선언 모양이다. WrapStore가 ②쪼개기를 안 하면
store.key:Compute(무주석)이 깨지고(H-75), Revision 하나만 어긋나도
store.key가 Source<number> 자리에 못 들어간다(H-76). M2에서
Source/State 타입을 쓰기 전에 정해야 하는 것이라 🅗보다 먼저다.
🅗 확정 시그니처 vs 확정 관용구 — H-94 · H-95 · H-96 · H-100
전부 같은 모양이다 — 문서가 확정한 타입이 문서가 확정한 관용구를 통과시키지 않는다. 넷 다 실측으로 해법까지 대조돼 있어 결정은 "시그니처를 고칠까 관용구를 고칠까" 하나다.
H-94::Apply가__call팩토리를 못 받음 → 유니온으로 열기 vsDebounce가 함수만 반환.H-95:Effect의fn과:List의updateFn→ 가변 반환 팩/유니온 vs "항상 명시적으로 반환하라" 계약.H-96: 결정이 아니라 문서에 경계 한 줄(§7에 "단 trailing deps가 붙으면 dep 파라미터엔 주석이 필요하다").H-100:EpochSet리터럴에 캐스트 필요 — 구현 시 정하면 됨.
🅘 부기(Length/Offset) 결함 — H-102 · H-106
H-102는 실제 오작동이다(Slot:Remove뒤 다른 항목 길이 변경 → 뒤 형제 offset이 낡음). 규칙 셋이 각각 다른 날 추가되며 "옮겨진 클로저 안의 인덱스"가 어느 목록에도 안 들어간 자리다.H-106은 한 줄 가드(contribution에nil검사) —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/원문 대조로 판정했다.