# 구현 전 손 트레이싱 **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<keyof<T>>`.
- **부기 필드 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 <ref>:<경로>` /
   `git diff <ref> -- <경로>`.
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 남은 의심 / 못 본 것**

문서는 **한국어**로 쓴다(사용자가 읽는 문서). 코드·식별자는 원문 그대로.
