From 8cc388cfab9d1b48ea4c6e3c414a670b6ff9c2b2 Mon Sep 17 00:00:00 2001 From: qwreey Date: Tue, 25 Aug 2026 12:26:53 +0900 Subject: [PATCH] =?UTF-8?q?qa:=207=EB=9D=BC=EC=9A=B4=EB=93=9C=20=EB=B0=9C?= =?UTF-8?q?=EA=B2=AC=2052=EA=B1=B4=20=EA=B2=80=EC=A6=9D=20=ED=8C=A8?= =?UTF-8?q?=EC=8A=A4=20=E2=80=94=20=EC=9C=A0=ED=9A=A8=2049=20/=20=EB=B2=94?= =?UTF-8?q?=EC=9C=84=20=EC=B6=95=EC=86=8C=202=20(H-55~H-106)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 발견을 더 찾는 패스가 아니라 이미 나온 것이 유효한지만 판정한다. 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 --- .claude/README.md | 2 +- ...mentation-handtrace-round7-verification.md | 405 ++++++++++++++++++ 2 files changed, 406 insertions(+), 1 deletion(-) create mode 100644 .claude/qa-request/pre-implementation-handtrace-round7-verification.md diff --git a/.claude/README.md b/.claude/README.md index 39f1106..01e07ee 100644 --- a/.claude/README.md +++ b/.claude/README.md @@ -24,7 +24,7 @@ | `base/` | 결정 완료 + 프로젝트 전체에 걸치는 컨텍스트 — plan/done 개념 없음, 계속 참조되는 배경지식. **항상 읽어야 하는** 배경지식만 여기 둠(다른 문서를 이해하는 데 전제되는 것) | | `reference/` | **[2026-08-07 신설]** 결정 자체가 아니라 다른 문서가 근거로 인용하는 온디맨드 참고 자료(v1 스냅샷, 프레임워크 비교 리서치) — "완료" 개념 없는 건 `base/`와 같지만, 항상 읽을 필요는 없고 해당 문서가 인용될 때만 열어보면 됨. `quadnomicon` 소재 후보가 많음. **[2026-08-21 확장] 확정된 결정의 "왜 그렇게 정했나" 근거 기록도 여기 둔다** — `research/`(아직 상의 필요)도 `archive/`(뒤집혔거나 기각됨)도 아니고, `base/`가 근거로 인용하는 온디맨드 자료라는 이 폴더의 기준에 정확히 맞기 때문(`slot-attach-decomposition.md`/`epoch-brand-composition.md`가 그렇게 들어옴) | | `research/` | 아직 착수 전, 사용자와 스코프/설계를 더 상의해야 함 | -| `qa-request/` | 원래 용도는 "구현 완료(코드/에이전트 검증까지 끝남) + 사용자 본인의 실기기(Roblox Studio) QA만 남음". **[2026-08-18 확장]** 구현 전에도 **사용자 심사 라운드의 산출물**을 여기 둠 — `pre-implementation-qa-round1.md`(1라운드: `base/` 확정 문서 전체를 문항으로 재확인받아 **"아니오"가 나온 항목만** 모은 결함 목록 + 신규 요구사항(`N-n`) + 부수 오탈자. **같은 날 전부 `base/`에 반영 완료**라 지금은 "무엇이 왜 틀렸었나"의 근거 기록이고, 지금 유효한 설계는 항상 `base/`가 소스. 아직 안 닫힌 것은 `question.md` 최우선 절과 `.claude/todos.md` 00번이 소스), `pre-implementation-qa-round2.md`(2라운드: 확정 의사코드를 실제로 손으로 실행해보는 트레이싱 — **완료**, 발견된 크래시 `RC-1`(`recompute` 트리거 모델)도 같은 날 후속 세션에서 Blocker 게이팅 설계로 해결·반영까지 끝남, `archive/question-resolved.md`에 논의 요지 정리), `pre-implementation-qa-round3.md`(3라운드: `RC-1` 해법(Blocker 게이팅)이 실제로 `attachSlot`/`recompute`에 반영된 걸 손으로 트레이싱 — **완료**, `RC-3`/`RC-4`(`activateList`가 자기 Slot의 Blocker보다 먼저 실행되는 순서 문제)와 `bk.N` 수명주기 미정을 발견했다가 같은 세션에 사용자가 최초 분석 오류를 직접 정정하며 전부 해결·`base/` 반영까지 끝남, `archive/question-resolved.md`에 논의 요지 정리. `ROADMAP.md` M3가 M2의 `Blocker.luau`에 의존하게 된 마일스톤 순서 불일치는 각주로 반영 — **[2026-08-24]** 그때 열어뒀던 마일스톤 재편은 M2/M3 순서 교체로 닫혔다). `pre-implementation-qa-round4.md`(4라운드: 사용자 요청으로 **`base/` 확정 전체를 "예가 나와야 정상인 문항"으로 다시 뽑은 전수 문항지** — **[2026-08-21] 완료·종결**. 1~3라운드와 달리 이 파일은 **문항지 원본 그대로 남긴다** — 처리 결과 전량이 `-followup.md`에 쌓였으므로 "아니오만 남기는 재편"은 안 하기로 함(같은 정보가 두 곳에 갈라지는 걸 피함). 문항 수/문서별 분포는 그 파일 자신이 소스). `pre-implementation-qa-round4-response.md`(사용자 회신 원문 — 이 라운드는 회신을 **별도 파일**로 받았다, 1~3라운드와 다른 점), `pre-implementation-qa-round4-followup.md`(**[2026-08-20 시작, 2026-08-21 종결]** 그 회신 처리 결과 — A~H 8개 절이 4차에 걸쳐 시간순으로 쌓였고 **마지막 H절이 최신이자 소스**. B·C절의 재질문/판단 대기 항목은 F·G절을 거쳐 **H절에서 전량 닫혔다**(`Detach` 보존 주체, `KeyGone`, `Owned`, `attachSlot` 분해). **열린 질문 없음**. **[2026-08-21 정정]** 여기 적혀 있던 "5라운드 문항지는 만들지 않는다"는 뒤집혔다 — 같은 날 사용자 요청으로 5라운드를 만들었다), `pre-implementation-qa-round5.md`(**[2026-08-21 신설·처리 완료]** 5라운드: 4라운드에서 "예"로 넘어간 자리는 건너뛰고 **(1) 4라운드에 문항이 아예 없던 영역**(`project-setup-plan.md`/`quad-types-plan.md`, 그리고 **문서가 아니라 실제 커밋된 M1 코드**), **(2) 4라운드 회신 이후 새로 확정된 것**(`Detach`/`_detached`/`KeyGone`/`Owned`/`attachSlot` 분해 등), **(3) 큰 문서의 심화**(예: `debounce-throttle-plan.md`)만 묻는다. 문항 수는 그 문서 자신이 소스), `pre-implementation-qa-round5-response.md`(사용자 회신 원문 — 4라운드와 같이 별도 파일), `pre-implementation-qa-round5-followup.md`(**[2026-08-21]** 그 회신 처리 결과 — 즉시 반영분 / 재질문 / 사용자 판단 필요 / 새로 만든 문서 둘(`gate-plan.md`·`state-epoch-plan.md` — 같은 날 확정되며 `base/`로 승격)까지. **처리 결과의 소스는 이 파일**). `pre-implementation-handtrace-round6.md`(**[2026-08-22 신설]** 6라운드: 문항지가 아니라 **손 트레이싱**이다(2·3라운드와 같은 성격) — 사용자가 지목한 최근 확정 5개 영역(`Effect(fn, ...deps)`/`Gate`·`Blocker`/State 전파(`rawInvalid`·emit 지연)/Slot의 `native*`·offset·length·mount/`Brand`·`Epoch`·`EpochMap`)을 실제 값으로 돌려본 결과. 발견 번호는 `H-n`. **[2026-08-23] 2차 패스** — 1차가 안 본 영역(디스패치 코어 전체/라이프타임 유틸/Ref·Tag·Attribute·UI 숏핸드 핸들러/Slot의 `raw*` 계층). **[2026-08-23] 3차 패스** — 1·2차가 한 번도 안 연 문서 전체(Store/State/Source 코어, Modifier·컴포넌트 합성, 이벤트·라이프사이클·에러 격리, Tween·시간 게이트, 타입 계약과 실제 커밋된 M1 코드) + 통합 시나리오. **[2026-08-24] 4차 패스** — 문서 단위가 아니라 **축을 바꿔서**(핸들러 레지스트리 전수/두 대형 핸들러 문서 심층/`luau-test` 스파이크 실제 재실행/프리미티브 조합 매트릭스/`reference`·`archive`·로드맵 M2~M9/엔진·언어 사실 주장 전수 검증). 3·4차는 추론으로 끝내지 않고 로컬 `luau`/`luau-analyze`와 공식 문서로 **직접 재현·교차검증**했고, 그 부수로 기존 `H-2`의 크래시 주장이 틀렸음도 드러났다(3차 패스 머리의 정정 절). 발견 번호는 패스를 가로질러 이어서 매긴다. 발견 수/심각도 분포는 그 문서 자신이 소스. 각 패스의 "확인만 하고 문제 없었던 것" 절은 다시 트레이싱할 필요 없는 자리를 적어둔 것. **⭐ [2026-08-24] 전량 처리·반영 완료** — 그 문서는 이제 **발견 당시의 기록**이라 각 항목의 "갈래"는 선택 전 목록이니 그대로 믿지 말 것), `pre-implementation-handtrace-round6-followup.md`(**[2026-08-24 신설] 6라운드의 결정과 근거가 여기 소스다** — `H-1`~`H-54`를 사용자와 대화형으로 하나씩 결정한 기록이고, 반영 후 `/code-review high`가 잡은 7건(그중 셋이 이번 반영이 만든 회귀)도 D절에 있다. 진행 경위와 사용자 발언 원문은 `session/2026-08-24-01-handtrace-round6-resolution.md`). `pre-implementation-handtrace-round7.md`(**[2026-08-25 신설, 회신 대기]** 7라운드: 6라운드와 같은 손 트레이싱이되 범위가 **M2(반응형 코어)와 M2→M3 경계**다. 패스 6개가 각기 다른 각도를 쓴다 — 1차는 프리미티브 사이의 *호출 순서*를 시간축으로 겹쳐 보기, 2차는 문서가 "확인했다"고 적은 런타임/타입 주장을 실제로 `luau`/`luau-analyze`에 걸어보기, 3차는 **커밋된 M1 코드·툴체인을 이 저장소에서 그대로 돌려보기** + 1·2차가 뺐던 "M2를 *소비하는* 문서", 4차는 **M2 코어를 문서 그대로 옮긴 참조 구현을 돌려보기** + 아무 문서도 안 정한 **예외 경로**, 5차는 **확정된 M2 표면을 실제 Luau 타입으로 선언해 `luau-analyze`에 걸어보기**(확정 시그니처가 확정 관용구를 통과시키는가), 6차는 **그 참조 구현을 M2→M3 경계(Length/Offset 부기·Dispatch 체인)까지 이어 붙여 돌려보기** + **quad 자신이 던지기로 확정한 error 42곳의 계약 감사**. 발견 번호는 6라운드에서 이어서 `H-55`부터, 패스를 가로질러 연속으로 매긴다. 발견 수/심각도 분포는 그 문서 자신이 소스. 각 패스의 "문제가 없던 것" 부록은 다시 파지 않아도 되는 자리를 적어둔 것. **아직 아무것도 `base/`에 반영하지 않았다** — 판정은 사용자가 하고, 결정이 나면 6라운드처럼 `-followup.md`를 새로 만든다). 다음 라운드가 필요해지면 라운드마다 파일을 새로 만들고 이름에 라운드 번호를 넣을 것 | +| `qa-request/` | 원래 용도는 "구현 완료(코드/에이전트 검증까지 끝남) + 사용자 본인의 실기기(Roblox Studio) QA만 남음". **[2026-08-18 확장]** 구현 전에도 **사용자 심사 라운드의 산출물**을 여기 둠 — `pre-implementation-qa-round1.md`(1라운드: `base/` 확정 문서 전체를 문항으로 재확인받아 **"아니오"가 나온 항목만** 모은 결함 목록 + 신규 요구사항(`N-n`) + 부수 오탈자. **같은 날 전부 `base/`에 반영 완료**라 지금은 "무엇이 왜 틀렸었나"의 근거 기록이고, 지금 유효한 설계는 항상 `base/`가 소스. 아직 안 닫힌 것은 `question.md` 최우선 절과 `.claude/todos.md` 00번이 소스), `pre-implementation-qa-round2.md`(2라운드: 확정 의사코드를 실제로 손으로 실행해보는 트레이싱 — **완료**, 발견된 크래시 `RC-1`(`recompute` 트리거 모델)도 같은 날 후속 세션에서 Blocker 게이팅 설계로 해결·반영까지 끝남, `archive/question-resolved.md`에 논의 요지 정리), `pre-implementation-qa-round3.md`(3라운드: `RC-1` 해법(Blocker 게이팅)이 실제로 `attachSlot`/`recompute`에 반영된 걸 손으로 트레이싱 — **완료**, `RC-3`/`RC-4`(`activateList`가 자기 Slot의 Blocker보다 먼저 실행되는 순서 문제)와 `bk.N` 수명주기 미정을 발견했다가 같은 세션에 사용자가 최초 분석 오류를 직접 정정하며 전부 해결·`base/` 반영까지 끝남, `archive/question-resolved.md`에 논의 요지 정리. `ROADMAP.md` M3가 M2의 `Blocker.luau`에 의존하게 된 마일스톤 순서 불일치는 각주로 반영 — **[2026-08-24]** 그때 열어뒀던 마일스톤 재편은 M2/M3 순서 교체로 닫혔다). `pre-implementation-qa-round4.md`(4라운드: 사용자 요청으로 **`base/` 확정 전체를 "예가 나와야 정상인 문항"으로 다시 뽑은 전수 문항지** — **[2026-08-21] 완료·종결**. 1~3라운드와 달리 이 파일은 **문항지 원본 그대로 남긴다** — 처리 결과 전량이 `-followup.md`에 쌓였으므로 "아니오만 남기는 재편"은 안 하기로 함(같은 정보가 두 곳에 갈라지는 걸 피함). 문항 수/문서별 분포는 그 파일 자신이 소스). `pre-implementation-qa-round4-response.md`(사용자 회신 원문 — 이 라운드는 회신을 **별도 파일**로 받았다, 1~3라운드와 다른 점), `pre-implementation-qa-round4-followup.md`(**[2026-08-20 시작, 2026-08-21 종결]** 그 회신 처리 결과 — A~H 8개 절이 4차에 걸쳐 시간순으로 쌓였고 **마지막 H절이 최신이자 소스**. B·C절의 재질문/판단 대기 항목은 F·G절을 거쳐 **H절에서 전량 닫혔다**(`Detach` 보존 주체, `KeyGone`, `Owned`, `attachSlot` 분해). **열린 질문 없음**. **[2026-08-21 정정]** 여기 적혀 있던 "5라운드 문항지는 만들지 않는다"는 뒤집혔다 — 같은 날 사용자 요청으로 5라운드를 만들었다), `pre-implementation-qa-round5.md`(**[2026-08-21 신설·처리 완료]** 5라운드: 4라운드에서 "예"로 넘어간 자리는 건너뛰고 **(1) 4라운드에 문항이 아예 없던 영역**(`project-setup-plan.md`/`quad-types-plan.md`, 그리고 **문서가 아니라 실제 커밋된 M1 코드**), **(2) 4라운드 회신 이후 새로 확정된 것**(`Detach`/`_detached`/`KeyGone`/`Owned`/`attachSlot` 분해 등), **(3) 큰 문서의 심화**(예: `debounce-throttle-plan.md`)만 묻는다. 문항 수는 그 문서 자신이 소스), `pre-implementation-qa-round5-response.md`(사용자 회신 원문 — 4라운드와 같이 별도 파일), `pre-implementation-qa-round5-followup.md`(**[2026-08-21]** 그 회신 처리 결과 — 즉시 반영분 / 재질문 / 사용자 판단 필요 / 새로 만든 문서 둘(`gate-plan.md`·`state-epoch-plan.md` — 같은 날 확정되며 `base/`로 승격)까지. **처리 결과의 소스는 이 파일**). `pre-implementation-handtrace-round6.md`(**[2026-08-22 신설]** 6라운드: 문항지가 아니라 **손 트레이싱**이다(2·3라운드와 같은 성격) — 사용자가 지목한 최근 확정 5개 영역(`Effect(fn, ...deps)`/`Gate`·`Blocker`/State 전파(`rawInvalid`·emit 지연)/Slot의 `native*`·offset·length·mount/`Brand`·`Epoch`·`EpochMap`)을 실제 값으로 돌려본 결과. 발견 번호는 `H-n`. **[2026-08-23] 2차 패스** — 1차가 안 본 영역(디스패치 코어 전체/라이프타임 유틸/Ref·Tag·Attribute·UI 숏핸드 핸들러/Slot의 `raw*` 계층). **[2026-08-23] 3차 패스** — 1·2차가 한 번도 안 연 문서 전체(Store/State/Source 코어, Modifier·컴포넌트 합성, 이벤트·라이프사이클·에러 격리, Tween·시간 게이트, 타입 계약과 실제 커밋된 M1 코드) + 통합 시나리오. **[2026-08-24] 4차 패스** — 문서 단위가 아니라 **축을 바꿔서**(핸들러 레지스트리 전수/두 대형 핸들러 문서 심층/`luau-test` 스파이크 실제 재실행/프리미티브 조합 매트릭스/`reference`·`archive`·로드맵 M2~M9/엔진·언어 사실 주장 전수 검증). 3·4차는 추론으로 끝내지 않고 로컬 `luau`/`luau-analyze`와 공식 문서로 **직접 재현·교차검증**했고, 그 부수로 기존 `H-2`의 크래시 주장이 틀렸음도 드러났다(3차 패스 머리의 정정 절). 발견 번호는 패스를 가로질러 이어서 매긴다. 발견 수/심각도 분포는 그 문서 자신이 소스. 각 패스의 "확인만 하고 문제 없었던 것" 절은 다시 트레이싱할 필요 없는 자리를 적어둔 것. **⭐ [2026-08-24] 전량 처리·반영 완료** — 그 문서는 이제 **발견 당시의 기록**이라 각 항목의 "갈래"는 선택 전 목록이니 그대로 믿지 말 것), `pre-implementation-handtrace-round6-followup.md`(**[2026-08-24 신설] 6라운드의 결정과 근거가 여기 소스다** — `H-1`~`H-54`를 사용자와 대화형으로 하나씩 결정한 기록이고, 반영 후 `/code-review high`가 잡은 7건(그중 셋이 이번 반영이 만든 회귀)도 D절에 있다. 진행 경위와 사용자 발언 원문은 `session/2026-08-24-01-handtrace-round6-resolution.md`). `pre-implementation-handtrace-round7.md`(**[2026-08-25 신설, 회신 대기]** 7라운드: 6라운드와 같은 손 트레이싱이되 범위가 **M2(반응형 코어)와 M2→M3 경계**다. 패스 6개가 각기 다른 각도를 쓴다 — 1차는 프리미티브 사이의 *호출 순서*를 시간축으로 겹쳐 보기, 2차는 문서가 "확인했다"고 적은 런타임/타입 주장을 실제로 `luau`/`luau-analyze`에 걸어보기, 3차는 **커밋된 M1 코드·툴체인을 이 저장소에서 그대로 돌려보기** + 1·2차가 뺐던 "M2를 *소비하는* 문서", 4차는 **M2 코어를 문서 그대로 옮긴 참조 구현을 돌려보기** + 아무 문서도 안 정한 **예외 경로**, 5차는 **확정된 M2 표면을 실제 Luau 타입으로 선언해 `luau-analyze`에 걸어보기**(확정 시그니처가 확정 관용구를 통과시키는가), 6차는 **그 참조 구현을 M2→M3 경계(Length/Offset 부기·Dispatch 체인)까지 이어 붙여 돌려보기** + **quad 자신이 던지기로 확정한 error 42곳의 계약 감사**. 발견 번호는 6라운드에서 이어서 `H-55`부터, 패스를 가로질러 연속으로 매긴다. 발견 수/심각도 분포는 그 문서 자신이 소스. 각 패스의 "문제가 없던 것" 부록은 다시 파지 않아도 되는 자리를 적어둔 것. **아직 아무것도 `base/`에 반영하지 않았다** — 판정은 사용자가 하고, 결정이 나면 6라운드처럼 `-followup.md`를 새로 만든다), `pre-implementation-handtrace-round7-verification.md`(**[2026-08-25 신설]** 그 52건이 **정말 유효한지만** 판정한 검증 패스 — 새 발견은 없다. ⚠️ **재실행이 아니라 대조로 판정했다**: 4·5·6차 패스가 쓴 전사물(`audit/handtrace-round7-reference-impl/spikes/`)과 `base/` 확정 의사코드를 줄 단위로 맞춰봤고(전사 오류로 생긴 발견은 **없었다**, 미신고 차이 3개는 전부 발견을 만들지 않는 방향), Luau 언어 동작·저장소 상태를 주장하는 것만 직접 재실행했다. 판정 분포와 항목별 근거는 그 문서 자신이 소스 — 여기서 세지 않는다. **결정 전에 반드시 읽을 것**: 부정 주장·개수 주장 몇 건이 정정됐고, 특히 **이미 사용자가 판단한 항목을 다시 묻게 되는 자리**가 하나 있다. 마지막 절의 **batch용 요약**이 유효 항목을 "한 결정으로 닫히는 묶음"으로 재편성해뒀으니 회신은 번호순이 아니라 그 묶음 단위로 하는 게 싸다). 다음 라운드가 필요해지면 라운드마다 파일을 새로 만들고 이름에 라운드 번호를 넣을 것 | | `archive/` | 완료 + 사용자가 실사용/실기기로 직접 검증까지 마침 (구현 대상). **[2026-08-06 확장]** 완전히 뒤집힌 설계 결정을 원문+역전 이유+diff와 함께 보존하는 용도로도 사용(제목 `[역전됨]` — 한 번 확정했다가 뒤집힌 것) — 더 이상 능동적으로 참고 안 해도 되지만(토큰 낭비 방지 위해 `base/`/`research/`에서 뺌) `quadnomicon` 소재로는 나중에 쓸 수 있음. **[2026-08-07 확장]** 후보였다가 채택 안 된 것(확정한 적 없이 검토 후 기각)도 같은 방식으로 보존, 제목은 구분을 위해 `[기각됨]` — `[역전됨]`과 의미가 다르므로 혼동하지 말 것. **[2026-08-07 세 번째 확장]** 설계 반전/기각과 별개로, 에이전트가 문서 작성 중 스스로 낸 개념 혼동을 정정한 이력은 `[에이전트 실수]` 태그로 `agent-mistake.md` 하나에 모음(`.claude/session-summary.md`/`session/` 로그와의 중복 방지) | | `feedback/` | 실사용 피드백을 정리한 긴 로그 — **[2026-08-19 기준] 폴더 자체가 아직 없음**(M0/M1 스캐폴딩만으론 안 생기고 실제로 렌더링해보고 쓰는 단계부터, 첫 피드백이 생길 때 만들면 됨). `qa-request/`는 **[2026-08-18] 더 이상 비어 있지 않음**(구현 전 QA 1라운드 산출물이 들어감) — 여긴 아직 폴더도 없음 | | `luau-test/` | **[2026-08-09 신설]** `base/` 확정 사항 중 "추론만으로 확정하고 실제 Luau로 부딪혀본 적 없는 것"(M0 스파이크 대상)을 `luau`/`luau-analyze`/`luau-lsp`/Roblox Studio로 사용자가 직접 돌려볼 독립 실행 스크립트 모음. **[2026-08-13 여섯 번째 세션, 첫 실측]** `luau`/`luau-analyze` 바이너리가 생겨 처음으로 실제 실행 — **런타임 12개 전원 통과**, 타입 쪽에서 `:Compute(fn)` lazy 핸들 계약이 Luau 추론과 충돌하는 게 드러남(당시 `question.md` 0-Y). **[2026-08-13 열세 번째 세션]** 그 0-Y가 해소되며 `review-required/`가 **비었음** — 계약은 유지 확정, 남은 건 Luau 자체 한계라 `base/typing-limits.md`가 담당. **`STATUS.md`가 상태의 소스**(pass / 사람 결정 필요 / 스파이크 깨짐 / 미실행 분류 — 사람이 먼저 볼 것만 위에), `luau-test/README.md`는 각 파일의 검증 의도·배경, 실행 결과 상세는 `audit/luau-test-first-run-2026-08-13.md` | diff --git a/.claude/qa-request/pre-implementation-handtrace-round7-verification.md b/.claude/qa-request/pre-implementation-handtrace-round7-verification.md new file mode 100644 index 0000000..8774fc7 --- /dev/null +++ b/.claude/qa-request/pre-implementation-handtrace-round7-verification.md @@ -0,0 +1,405 @@ +# 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 = 23`이 `H-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:_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개**: + +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` | 유효 | `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(store,name): Source` 재현 — `Source`로 떨어짐. `store-plan.md`가 확정한 표기는 `store:GetDynamic<>(name): Source` | +| `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` 자리에 대입 가능"*은 검증된 적 없음 | +| `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}` 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]: true }`가 `Epoch | EpochSet` 자리에서 거부. `Source`가 `Epoch`를 구조적으로 만족한다는 §2 확정은 그대로 성립 | +| `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.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 = 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`을 +탑레벨 함수로 옮기면 예약 키 자체가 안 생긴다. 그러니 **순서가 있다**: +1. `H-73` — `question.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` 선언 모양**이다. `WrapStore`가 ②쪼개기를 안 하면 +`store.key:Compute(무주석)`이 깨지고(`H-75`), `Revision` 하나만 어긋나도 +`store.key`가 `Source` 자리에 못 들어간다(`H-76`). **M2에서 +`Source`/`State` 타입을 쓰기 전에 정해야 하는 것**이라 🅗보다 먼저다. + +### 🅗 확정 시그니처 vs 확정 관용구 — `H-94` · `H-95` · `H-96` · `H-100` +전부 같은 모양이다 — **문서가 확정한 타입이 문서가 확정한 관용구를 통과시키지 +않는다.** 넷 다 실측으로 해법까지 대조돼 있어 결정은 "시그니처를 고칠까 +관용구를 고칠까" 하나다. +- `H-94`: `:Apply`가 `__call` 팩토리를 못 받음 → 유니온으로 열기 vs + `Debounce`가 함수만 반환. +- `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/` 원문 대조로 판정했다.