quad/.claude/qa-request/pre-implementation-handtrace-round9-brief.md
qwreey 031495cc0b
qa: 9라운드 손 트레이싱 실행 + Q1~Q3 확정·반영 — 체크포인트 (Q4~Q10 다음 세션)
8라운드가 써둔 지시서(-round9-brief.md)대로 커밋 9dd8213의 델타를 재트레이싱해
발견 18건(H-124~H-141)을 냈고(qa-request/pre-implementation-handtrace-round9.md),
§4 문항 중 Q1~Q3를 사용자와 확정해 base/·ROADMAP에 반영했다. 결정의 소스는
-round9-followup.md(진행 표가 상태의 소스). 🔴 둘 다 luau 실측 재현.

Q1 H-124 — recompute가 lengthList[i]를 되감기 판정보다 먼저 읽어, offset:Set 안의
  사용자 코드가 커서 뒤 자리 수를 줄이면 sum += nil로 죽고 recomputeBlocker가
  영구 On. 되감기 판정을 앞으로(continue), 읽기·누적은 되감지 않을 때만.
Q2 H-125 — 재마운트 시 setOffsetSource가 slot.Offset을 바꾸는 순간 _baseObserver가
  unbind 상태라 두 필드가 0으로 안 내려가 옛 베이스의 offsetCache[1]을 씀.
  사용자 확정: Offset·_baseObserver를 Slot 생성자로(첫 마운트/재마운트 분기
  소멸), materializeSlotTree는 blocker:On → bindLifetime → setOffsetSource,
  파괴는 _destroyed 플래그 하나(핸들은 unbind만, mutate CRUD·:List·마운트 진입
  error, Owned=false는 안 섬, 이중 dispose no-op).
Q3 H-126/H-137/H-141 — element→index 맵이 Slot 층(slot._elemIndex)과 Dispatch
  층(bk.tokens/indexOfToken)에 두 벌 있었고, 후자의 token은 사용자가 정한 적
  없는 것(2026-08-25 /code-review가 발명해 사용자 인용문 옆에 앉아 있던 것).
  bk.indexOfElement 하나로 통일, setLength 5번째 인자 element, splice가 비운
  자리는 세 배열 전부 처리. H-137 소멸.

부수: H-140(ROADMAP의 폐기된 "해제 시 slot.Offset = nil" 잔존) 정정,
H-125 피해 범위를 "중첩 Slot의 Offset"으로 축소(유저 체인은 요소 인스턴스에
바인드돼 전파됨 — 실측), G각도로 "for d in seen do는 유효한 Luau가 아니다"가
거짓임을 확인, keyof<{}> 빈 Store 실측 클린.

conventions.md 신설: "/code-review(그리고 메인 세션)가 내놓는 새 필드·인자·
이름·메커니즘은 발견이지 결정이 아니다" — 이 세션에서 메인 세션도 같은 실수를
세 번 했다(subject 인자 / Observer 위치 필드 = 기각된 Effect userdata 재개방 /
조회 클로저).

검증: quad-doc-auditor 6라운드(확실 1→1→3→1→1→0, base/ 본문 결함은 1라운드
이후 0건 — 나머지는 인덱스 레이어·인용처), doc-check ERROR 0. /code-review high는
Q4~Q10 반영 뒤 한 번에(같은 파일을 또 건드려 diff가 섞이므로 지금 체크포인트).

Claude-Session: https://claude.ai/code/session_01F9zgJ4c4kDitAoQMm9qxKn
Co-authored-by: qwreey <me@qwreey.moe>
2026-08-27 01:30:48 +09:00

235 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 구현 전 손 트레이싱 **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 남은 의심 / 못 본 것**
문서는 **한국어**로 쓴다(사용자가 읽는 문서). 코드·식별자는 원문 그대로.