# 구현 전 손 트레이싱 **9라운드** 감사 지시서 > **이 파일이 무엇인가**: 9라운드 감사자에게 그대로 주는 지시서다. 발견 > 보고가 아니라 그 **앞단**이고, 산출물은 별도 파일이다(§6). > **[2026-08-26] 지시서를 저장소에 남기는 건 이번이 처음이다** — 7·8라운드 > 지시서는 대화에만 있었고 저장되지 않았다(8라운드 본문이 인용하는 > "감사 지시서 §2"가 코퍼스 어디에도 없는 이유). 앞으로 라운드마다 > `-roundN-brief.md`를 같이 남긴다. 당신은 Roblox 엔진용 DOMless UI 렌더러 **quad**의 설계 코퍼스를 감사한다. 저장소 루트는 이 세션의 작업 디렉토리다. **구현(M2 = 반응형 코어) 착수 직전**이고, 여기서 놓친 설계 결함은 구현 한참 뒤에 터져 M2/M3를 다시 짜는 비용이 된다. --- ## §0 먼저 읽을 것 (이 순서로) 1. `CLAUDE.md` → `.claude/conventions.md` / `.claude/project-context.md` / `.claude/todos.md` — 프로젝트 관례와 현재 상태. 2. `.claude/qa-request/pre-implementation-handtrace-round8.md` — 8라운드 발견 원문(`H-107`~`H-123`). 특히 **§5(이상 없다고 확인한 것)** 와 **§6(남은 의심 / 못 본 것)**. 3. `.claude/qa-request/pre-implementation-handtrace-round8-followup.md` — **이 라운드의 출발점.** 8라운드 결정 Q1~Q10과, 그 반영 뒤 돌린 `quad-doc-auditor` 11라운드 + `/code-review high` **7패스**의 기록. `/code-review high`의 차수별 절(1차부터 7차까지)을 반드시 정독할 것 — 거기 적힌 **반복 실패 모드**가 당신의 사냥 목록이다(§3). 4. `ROADMAP.md`(M2/M3 체크리스트) / `.claude/question.md` / `.claude/luau-test/STATUS.md`. 5. `base/`는 레인별 필요 범위만(§2). 전량 완독은 요구하지 않는다 — 8라운드 3차 패스가 이미 전량 완독했고 🔴가 0건이었다. **이전 라운드**(1~7)는 필요할 때만 인용 자리를 부분 확인하라. 재심사 대상이 아니다. --- ## §1 이 라운드의 전제 8라운드 결정 반영과 그 뒤의 code-review 7패스 수정이 **전부 단일 커밋 `9dd8213`에 들어 있고, 그 결과물을 아무도 처음부터 트레이싱한 적이 없다.** ``` git show 9dd8213 --stat git show 9dd8213 -- .claude/base/ ROADMAP.md ``` 이게 이 라운드가 볼 델타의 **완전한 정의**다. `base/` 주요 증분: `dispatch-core-plan +250` / `source-state-plan +160` / `slot-plan +139` / `lifecycle-pattern +124` / `store-plan +117` / `effect-plan +103`. **왜 이 델타가 특히 위험한가** — 같은 프로젝트가 낸 실측이 셋 있다. - 8라운드의 🔴 다섯(`H-107`/`108`/`109`/`112`/`119`)은 **전부** "직전 라운드(7라운드)의 반영분이 서로 겹치는 자리"에서 나왔다. 개별 함수의 버그가 아니라, 하루 차로 확정된 결정들이 서로를 못 본 자리다. - code-review 7패스의 HIGH 추이는 `1 → 1 → 0 → 1(설계 역전) → 0 → 3 → 0` 이다. **5차가 HIGH 0인데 6차에서 HIGH 3이 나왔고 전부 5차 수정이 만든 것**이었다. "직전 패스가 조용했다"는 수렴의 증거가 아니다. 7차 수정분은 **아무도 안 봤다.** - 4차에서 설계 역전이 있었다 — `H-101`의 "새 필드를 안 만든다"가 뒤집혀 `bk.invalidAfter`가 `bk.offsetCacheValidUpTo` / `bk.offsetSetUpTo` **둘로 분리**됐고 40곳 넘게 치환됐다. 감사 사이클 **끝머리**에 들어온 구조 변경이라 가장 덜 검증됐다. --- ## §2 레인 — 우선순위 A > C > B 리포트를 레인별로 독립적으로 쓸 것. 예산이 모자라 B를 못 해도 A/C 결과가 그대로 쓸 수 있어야 한다. ### 레인 A (최우선) — `9dd8213` 델타의 상호 간섭 이번에 바뀐 계약들을 **서로 겹쳐서** 읽는다. 개별 문서가 자기 안에서 일관된지가 아니라, **두 결정이 만나는 자리에서 성립하는지**를 본다. 이번 라운드가 바꾼 계약(전량은 followup이 소스, 여기 나열은 진입점): - `Ref` 콜백이 **`fn(value, ref)`** — 2번째 인자가 곧 출처 `Epoch`. 훅 슈가의 `guard(fn)`도 2-인자가 됐다. - Observer `fn`이 **세 자리** `fn(targetState, self, emitFrom)` + `observer._state` **강참조**. - `WeakSubscribe`도 `.Subscribed = true`를 세운다. - `Subscribe`/`WeakUnsubscribe`/`Unsubscribe`가 **전부 fail-fast** ("해제는 건 경로로 푼다"). *idempotent* 서술은 세 번에 걸쳐 지워졌다 — **네 번째 사본이 남아 있는지 전수하라.** - 예약 키 진단 타입 함수가 `CheckReservedKeys>`. - **부기 필드 2분할** — `offsetCacheValidUpTo`(올리는 쪽: `getOffsetAt`, 어디서 불리든) / `offsetSetUpTo`(올리는 쪽: `recompute`만). 무효화는 둘 다 내린다. 옛 이름 `invalidAfter`는 **의도적으로 전멸**시켰다(단, **인용문· 절 제목·정정 배너 안에서는 옛 이름이 정본**이다). - 무효화 인덱스가 **세 가지 → 네 가지**(splice 계열은 `j - 1`, `rawMove`/`rawSwap` 계열 행 신설, `rawExtract`는 **조건부**). - `recompute` 재진입 게이트가 **명시 호출부 전부**로 확대(`raw*` 3형제, `_baseObserver`, `:List` 활성화 꼬리, `mountSlotTree` 꼬리, `Dispatch.drive` 꼬리, 일반 배치 계약). - Store 생성자의 `defaults` `isSource` 화이트리스트 검증(`error` level 2), `isModifier` 가드가 `Source` 생성자로 이동. **특히 겹쳐 볼 것**(과거에 실제로 여기서 🔴가 나왔다): - `H-113`(splice `-1`) × `H-119`(`_baseObserver`가 마커를 0으로) — 이 둘이 겹쳐 `math.max(…, 1)` 클램프가 필요해졌다. **같은 종류의 0-하한/끝-초과 경로가 다른 조합에 또 있는가.** - 부기 필드 분할 × 무효화 4규칙 × `bk.N` 예외(`rawSplice`/`rawClear`/ 조건부 `rawExtract`) — 세 개를 동시에 만족하는 CRUD 시퀀스를 **실제 값으로** 돌려라. 특히 **한 콜백에서 CRUD를 두 번**(예: `Remove` 뒤 `Add`) 하는 2-연산 경로. 4차 HIGH가 정확히 그 형태였다. - `Ref` 2-인자 × 훅 슈가 × `Effect`의 dep 종류별 클로저 둘. - fail-fast 3종 × `Effect` cleanup × leaf 사망 경로. ### 레인 C — 참조 구현을 갱신해 **실제로 돌리기** `.claude/audit/handtrace-round7-reference-impl/`에 7라운드가 만든 M2 코어 + M2→M3 경계 참조 구현과 스파이크(`spikes/`)가 있다. **그 계약은 8라운드 이후로 낡았다.** 1. 그 README의 원문 대조표를 따라 **지금의 `base/` 서술로 갱신**하라 (계약 4변경 + 부기 필드 분할 + 무효화 4규칙 + 재진입 게이트 확대). 2. `luau` / `luau-analyze`로 돌려라. 3. **⚠️ 참조 구현은 확정 의사코드의 *전사물*이라 그 자체가 틀렸을 수 있다.** 재실행은 검증이 아니다 — 발견을 판정할 땐 `base/` 원문과 줄 단위로 대조하라. 반대로 **참조 구현이 안 돌아가면 그건 거의 항상 문서의 결함**이다(7차 code-review가 `for d in seen do`가 유효한 Luau가 아니라는 걸 이렇게 잡았다). 실행 환경: - 저장소 테스트는 **반드시 `./scripts/test.sh`**. `luau` CLI가 심볼릭 링크를 못 타는데 pesde 워크스페이스 링크가 전부 심볼릭이라, 그냥 `luau`로 돌리면 스모크가 죽고 `luau-analyze`는 **조용히 통과**한다(거짓 클린). - 독립 스파이크는 스크래치패드에 만들어 `luau <파일>` / `luau-analyze <파일>`로 돌려라. ### 레인 B — M5+ 값 단위 트레이싱 (첫 시도) 8라운드 §6이 남긴 유일한 실질 공백이다. **문서 정독은 됐지만 의사코드를 실제 값으로 돌려본 적이 한 번도 없다**: - 그룹 `Attribute` 위임 체인(`attribute-plan.md`) - `D` 생성자 (`ui-shorthand-plan.md`) - 숏핸드 → `PropertyHandler` 위임 (`ui-shorthand-plan.md` × `dispatch-core-plan.md`) - `:List` reconcile의 **실제 값 대입** (`slot-plan.md`) 구체적인 인스턴스 트리와 구체적인 값으로 한 사이클을 손으로 돌려라 ("계약이 이어진다"는 수준이 아니라 변수마다 값을 적으면서). --- ## §3 사냥 목록 — 이 코퍼스에서 **반복 관측된** 실패 모드 followup의 code-review 절이 명시적으로 남긴 것들이다. 델타를 읽을 때 이 패턴을 **능동적으로** 찾아라. 1. **⭐ 토큰만 바꾸고 그 토큰이 든 문장을 안 읽음** — 이 세션에서만 **네 번** 반복됐다(`_observers`→`_deps`, `CheckReserved`→`CheckReservedKeys`, `invalidAfter`→`offsetSetUpTo`, 그리고 역사 인용문 오염). 이름이 바뀌었는데 **그 이름이 서술하던 동작·근거·불변식이 옛것 그대로**인 자리를 찾아라. 2. **전역 치환이 인용문·절 제목·정정 배너를 오염** — 그 셋은 *과거에 무엇이라 적혀 있었는가*를 보존하는 게 목적이다. `H-101` 인용문이 새 필드 이름으로 바뀌어 "새 필드를 안 만든다"와 자기모순이 된 사례가 실제로 있었다. **반대 방향도 보라** — 되돌리다 살아 있는 서술까지 옛 이름으로 돌린 자리. 3. **표에 행만 넣고 헤딩/산문/개수는 그대로** — "세 가지"라고 쓰인 채 표엔 네 행. 두 번 반복됐고(2차/3차), `ROADMAP` 체크리스트에도 번졌다. 4. **폐기 블록을 만지면 오히려 해로움** — 죽은 문단에 이름만 고치거나 날짜 마커를 찍으면 **갓 정비된 것처럼** 보여 구현자가 더 믿는다. 폐기 배너 아래 본문이 최근에 수정된 흔적이 있는지 보라. 5. **한 곳에서 고친 거짓 전제가 새로 쓰는 글에서 되살아남** — *"Store는 `Source`를 만들지 않는다"*가 3차에 세 곳에서 고쳐졌는데 5차에 새 블록에 다시 들어갔다. **이미 거짓으로 판정된 문장의 새 사본**을 전수하라. 6. **같은 계약의 N번째 사본** — *idempotent* 서술이 1차·6차·7차에 걸쳐 세 번 지워졌다. 하나를 고쳤으면 `grep`으로 전수하라. 7. **`ROADMAP.md` 체크박스가 `base/`보다 낡음** — 7차의 2번이 그랬다. **구현자가 실제로 보는 자리**라 심각도가 높다. --- ## §4 각도 (8라운드와 같은 문자를 유지 — 상호참조용) - **A. 반영분 재트레이싱** (최우선, 레인 A) - **B. 엔드투엔드 체인** — Store→State→Observer/Effect→Gate/Blocker→ `setLength`/`recompute`, 그리고 **M5+**(레인 B) - **C. 확정 의사코드/타입 실측** (레인 C) - **D. 구현 순서 시뮬레이션** — `ROADMAP.md` M2 체크박스를 위에서부터 실제로 짜는 시뮬레이션. **체크박스만 보고 짰을 때 성립하는가**(7차 2번이 여기서 나왔다) - **E. 공개 API 오용 진단** — 사용자가 틀리게 썼을 때 조용히 통과하는가 - **F. 코퍼스 미다룸 영역** - **G. Luau 사실 주장 재확인** — 문서가 "확인했다"고 적은 언어 동작을 실제로 걸어보기 - **H. 비용 서술 점검** --- ## §5 규칙 (엄수) 1. **저장소를 수정하지 마라.** 산출물은 리포트 파일 **하나**뿐이다. 스파이크·참조 구현 갱신본은 스크래치패드에 만들고, 발견의 근거는 리포트 항목 안에 **인라인으로 전사**하라(파일이 유실돼도 재현 가능하게). 2. **`git stash`를 절대 쓰지 마라.** 과거 실동에서 메인 세션의 스테이지가 반복적으로 풀렸다. 과거 상태가 필요하면 `git show :<경로>` / `git diff -- <경로>`. 3. **아무것도 반영하지 마라.** 판정은 사용자가 이 리포트를 보고 한다. 4. **8라운드까지의 결정은 뒤집지 않는 것을 기본으로 하라.** 뒤집어야 한다고 보면, **그 결정의 어느 추론이 틀렸는지**를 항목 안에서 지목하라 (근거 없이 "다시 생각해보니"는 안 된다). 5. **발견 번호는 `H-124`부터** 이어서 매긴다(라운드를 가로질러 연속). 심각도는 🔴(M2 착수 전 결정 필요) / 🟡(계약 결정) / 🟢(문서 정합·문서화). 6. `.claude/audit/`의 참조 구현은 **참고용**이다 — 재실행이 곧 검증이 아니다 (§2 레인 C의 ⚠️). 7. **확신이 안 서면 발견으로 올리지 말고 §6 "남은 의심"에 적어라.** 반대로, 확인해서 **이상 없었던 것**은 §5에 적어라 — 다음 라운드가 같은 자리를 다시 파지 않도록. 8. **못 본 범위를 §6에 정직하게 선언하라.** 리포트가 "감사 통과"로 읽히면 안 된다. --- ## §6 산출물 `.claude/qa-request/pre-implementation-handtrace-round9.md` 하나. 구성: - **머리말** — 무엇인가 / 쓴 각도 / 실제로 본 범위 / 읽는 순서 - **요약 표** — `| 번호 | 심각도 | 한 줄 | 주 대상 | 성격 | 실측 |` - **상세** — 항목마다: 무엇이 문제인가 / **어떻게 재현·트레이싱했는가** (값 단위로) / 어느 문서 어느 줄이 근거인가 / 그대로 구현하면 무슨 일이 일어나는가 / (있으면) 실측 출력 전사 - **§4 사용자 결정이 필요한 것** — 배치 회신용. 문항마다 선택지와 **당신의 권고**를 달아라. 사용자는 이걸 위에서부터 답한다 - **§5 이상 없다고 확인한 것** - **§6 남은 의심 / 못 본 것** 문서는 **한국어**로 쓴다(사용자가 읽는 문서). 코드·식별자는 원문 그대로.