# 2026-08-26 — 8라운드 손 트레이싱 처리 (Q1~Q10) **무엇을 했나**: `qa-request/pre-implementation-handtrace-round8.md`의 발견 17건(`H-107`~`H-123`)을 사용자와 대화형으로 처리하고 `base/`에 전량 반영, `-round8-followup.md`를 신설했다. **결정과 근거의 소스는 그 followup 파일** 이고 여기선 진행 방식과 그 과정에서 드러난 것만 남긴다. ## 진행 방식 1. 라운드 문서를 전량 읽고, **물어보기 전에 🔴 다섯의 핵심 주장을 `base/`에서 직접 재확인**했다 — `Ref:Set` H-53 블록에 `Revision` 갱신·Weak 순회가 없는 것, 전파 루프가 `sub.fn(sub, from)`인 것, `CheckReserved`가 `T`를 통째로 받는 서술. 전부 그대로였다. 2. §4의 문항 Q1~Q10을 세 배치로 물었다(🔴 넷 → M3/백로그 넷 → 나머지). 3. 사용자 회신을 받는 즉시 `base/`에 반영하고, 인덱스 레이어 (`question.md`/`todos.md`/`README.md`/`ROADMAP.md`/`CLAUDE.md`/ `project-context.md`)도 갱신했다. **다만 첫 패스에서 인덱스 쪽이 덜 닫혔다** — 아래 "감사 루프" 절 참고. ## ⭐ 이 세션의 실질적 소득 — 사용자가 **문항의 전제를 두 번 정정했다** 권고안이 그대로 채택된 항목이 대부분이었지만, 배울 게 있었던 건 그 둘이 아니다. ### (1) Q2-후속 — "클로저를 통일한다"는 목표 자체가 없었다 Q1(a)로 `Ref` 콜백의 출처가 2번째 자리, Q2로 Observer의 출처가 3번째 자리가 되면서 `Effect`의 단일 `onDepFire`가 성립하지 않게 됐다. 나는 이걸 **"자리를 어떻게 맞출까"**라는 문항으로 냈고, (a) `Ref`도 3슬롯으로 (`k(value, self, self)`), (b) `or`로 흡수, (c) 클로저 2개를 갈래로 세웠다. 사용자 회신: > *"애초에 둘을 같게 두려는 목적 자체가 없었음. Ref 의 callback 과 observer 의 > 콜백이 아주 헤테로지니어스한 개념이라, 둘을 전혀 합치고자 한 적 없고, 원 > 아이디어는 달랐음. 아주 중요한 부분이 있는데, observer 에는 epoch 란게 > 존재하지 않음. emit 으로 온 epoch 를 넘겨줄 뿐, 그러나 ref 는 그 자체로 > epoch임. 처음부터 둘 처리를 묶어보는 시각 자체가 잘못되었는것."* 즉 `effect-plan.md`에 있던 *"⭐ 클로저는 **하나**로 통일한다"* 주석이 애초에 근거 없는 서술이었고, 8라운드가 그걸 "확정"으로 읽고 갈래를 그 위에 세운 것이다. **문서에 적힌 "⭐ 확정"이라도 그 근거가 문서 안에 없으면 확정이 아닐 수 있다** — 라운드 문서가 `H-107` (b)를 *"'클로저는 하나로 통일' 주석과 표면상 어긋난다"*는 이유로 낮춰 본 것도 같은 함정이었다(실제로는 dedup을 클로저 identity가 하는 게 아니라 `_deps`/`_epochs` 맵이 한다는 걸 그 문서 자신이 괄호로 적어놓고도). ### (2) Q7 — 문항이 없는 대립을 세웠다 `H-118`은 *"`Debounce`/`Throttle`은 `emit`을 아예 안 쥔다"*(gate-plan 5번)와 *"정책이 `emit()` 반환값과 `emit(false)`를 직접 쓴다"*(2번, `H-55`/`H-86`)를 **미조정 충돌**로 보고 "누가 쥐는가"를 갈래로 냈다. 사용자 회신: > *"둘다 쥔다는 의미를 모르겠음. epoch|{epoch} 를 모아두는 부분은 gate 쪽이긴 > 한데(각각 본인껀 본인이 모아야하니까), emit 을 blocker 가 쥔다는건 > 정확하게는, 'emit 된 적 있던가?' 를 저장하기 위함 아님? 그 구현을 나눠 쓰지 > 않기 위함일 뿐 아녔음?"* 원문을 다시 읽으니 그대로였다 — `setup(emit)`이 계약이므로 `emit`은 정의상 정책 손에 있고, `blocker:Policy(emit)`이 위임받는 건 **보류 부기**뿐이다. 설계는 아무것도 안 바뀌고 **5번의 머리 문장만 틀렸다.** `H-118`은 🟡 계약 결정에서 🟢 문서 정합으로 강등됐다. **교훈**: "두 확정이 충돌한다"는 발견을 낼 때, **둘 중 하나가 그냥 틀리게 쓰인 문장일 가능성**을 갈래에 넣어야 한다. 8라운드는 두 문장을 각각 유효한 설계 입장으로 대우해 없는 선택을 만들었다. ## 반영 규모 - `base/` **14개 파일**(위 12개 — `ref-plan`/`source-state-plan`/ `lifecycle-pattern`/`effect-plan`/`store-plan`/`state-epoch-plan`/ `dispatch-core-plan`/`slot-plan`/`gate-plan`/`modifier-plan`/ `lifecycle-hooks-plan`/`project-setup-plan` — 에 감사 루프가 더한 `architecture.md`/`typing-limits.md`), `ROADMAP.md`, `luau-test/STATUS.md`, `research/documentation-content-map.md`, `audit/type-recursion-issue/spikes/` 2개(배너), 인덱스 레이어 6개. - `doc-check.py` ERROR 0. ## 안 한 것 - **8라운드 §6의 "못 본 것"은 그대로 유효**하다 — 특히 M5+ 구간이 문서 정독 수준이고 값 단위 트레이싱을 안 했다. - 남은 의심 하나(게이트 `emit(false)` 직후 다이아몬드 두 번째 경로 도착)는 8라운드 스스로 "판단이 안 서서 발견으로 안 올렸다"고 적은 것이라 여기서도 열지 않았다. ## 검증 (커밋 전) — 감사 11라운드 + `/code-review high` 7라운드 **합계: 감사 44건 + code-review 42건 = 86건.** 감사는 0건으로 수렴했고, code-review는 마지막 라운드 HIGH 0으로 마쳤다(항목별 처분은 followup이 소스). **⚠️ 감사가 0건으로 수렴한 *직후* code-review가 42건을 냈다.** 두 도구는 대체 관계가 아니고, 그 실측이 `conventions.md`에 반영됐다. ### 감사 루프 — 11라운드, 발견 44건, 마지막 0건으로 수렴 `quad-doc-auditor`를 한 턴에 하나씩(병렬 금지), 라운드마다 각도를 바꿔 돌렸다. 새 발견 추이: **9 → 10 → 7 → 4 → 2 → 2 → 2 → 1 → 4 → 3 → 0.** | 라운드 | 각도 | 발견 | |---|---|---| | 1 | `base/` 정합성 | 9 | | 2 | 인덱스 레이어(`README`/`ROADMAP`/`architecture` 요약) | 10 | | 3 | 스파이크·실측 기록(`audit/`·`luau-test/`) + 1·2라운드 수정분 재검 | 7 | | 4 | **오늘 쓴 문장 자체의 정확성**(누락이 아니라 오기·과장·의사코드 결함) | 4 | | 5 | 오늘 *안 만진* `base/` 문서가 바뀐 계약을 전제하는가 | 2 | | 6 | "한쪽만 고치고 짝을 안 고친" 쌍 | 2 | | 7 | 오늘 바뀐 `base/` 파일 **전량 정독**(diff 아님) | 2 (`base/` 안쪽 0) | | 8 | 미사용 문서군(`conventions`/`agents`/`research`/`HUMAN_TODO`) + 수정분 재검 | 1 | | 9 | **`H-111` 하나를 토큰 전수 추적** | 4 | | 10 | 오늘 확정 12개 계약 전부를 토큰 전수 추적 | 3 | | 11 | "확정/새 모델/해법" 권위 표지가 붙은 폐기 블록 | **0** | ### 이 루프가 알려준 것 셋 **1. 계약을 고칠 때 가장 새기 쉬운 자리는 `base/`가 아니라 그걸 요약·복사해 든 곳이다.** 1·2라운드 발견 19건 중 15건이 `ROADMAP.md`의 체크박스 인라인 사본, `README.md`의 `base/` 표, `architecture.md`의 소스 트리 주석이었다. 그중 몇은 **그대로 구현하면 그날 고친 버그를 재현**했다(전파 루프 2-인자, `Ref:Set`의 `k(value)`, 훅의 `Callback(fn)`). `base/`를 고칠 때 그 세 곳을 같은 배치에 넣는 걸 기본으로 할 것. **2. ⭐ 문서군 각도로는 구조적으로 안 잡히는 잔재가 있다 — 토큰으로 범위를 잡아야 한다.** `H-111`(`WeakSubscribe`도 `.Subscribed`를 세운다)이 6·8라운드에 연속으로 새 잔재를 냈고, 그때마다 "그 문서는 고쳤다"고 넘어갔다. 9라운드가 문서가 아니라 **토큰**(`Subscribed`/`isBoundAlive`/`canBound` 계열)으로 전수 grep하자 **4건이 한 번에** 나왔다 — 전부 *"`.Subscribed`는 전역 `:Subscribe()` 전용"*이라는 **똑같은 어구**였고, 앞의 여러 라운드가 각자 다른 문서를 보느라 계속 지나쳤던 자리다. 10라운드가 같은 방법을 나머지 계약에 적용해 3건을 더 냈다. **넓게 퍼진 계약을 바꿀 땐 옛 표기의 토큰을 정해 전수 grep할 것.** **3. 고치는 과정이 새 결함을 만든다 — 수정분도 감사 대상이다.** 이 루프에서 메인 세션이 **회귀를 6건 만들었다**: `reference/`의 blockquote가 기존 문장을 반토막 냄, `lifecycle-pattern.md`에 `Subscribe`가 서로 다른 구현으로 **두 번 정의**됨(+ 산문이 약속한 `WeakUnsubscribe` 코드 부재, `Unsubscribe`가 약한 레지스트리를 안 지우는 반쪽 해제), 새 의사코드 아래 결론 문단이 옛 주장을 유지, 정정 배너 3건이 **엉뚱한 문단**에 붙음(문단 끝 자동 탐색이 다음 bullet으로 넘어감). 4·8·9·11라운드가 이걸 잡았다 — **"수정분 재검"을 별도 각도로 두는 게 값을 했다.** ### 부수로 드러난 기존 부채 둘 (오늘 작업과 무관) - **`HUMAN_TODO.md`가 이미 닫힌 게이트를 열려 있다고 서술**(2자리) — 관례가 요구하는 인덱스 4개 층 중 거기만 2026-08-25 갱신을 안 받았다. - **2026-08-25의 `session/` 원문이 통째로 없다** — 그날 커밋 9개로 지금 M2 계약의 상당 부분이 확정됐는데 로그가 없다. **그 규칙 자체가 2026-08-18/19의 같은 공백에서 만들어진 것이라 재발이다.** 2026-08-19 선례대로 재구성하지 않고 `session-summary.md`에 공백을 명시했다(결정 내용 자체는 followup·발견 문서·참조 구현에 남아 있어 유실이 아니고, 없는 건 논의 과정뿐이다). `doc-check.py` ERROR 0 / WARN 43(전부 기존 부채, 이번 작업이 늘린 것 없음). ### `/code-review high` 1차 — 감사 11라운드가 못 본 7건 사용자가 직접 호출(이 명령은 세션이 못 부른다). **7건 전부 유효.** 항목별 처분은 followup 문서의 "반영 후 검증" 절이 소스 — 여기선 교훈만. **⭐ 감사자와 code-review는 대체 관계가 아니다(2026-08-18 실측의 재확인).** 감사 11라운드가 44건을 잡고 0건으로 수렴한 **직후에** 7건이 더 나왔고, 그중 셋은 감사 각도 어디에서도 안 나오는 종류였다. 축이 다르기 때문이다 — 감사자는 **코퍼스 전체의 의미론적 정합성**(A 문서의 결정과 B 문서의 서술이 어긋나는가), code-review는 **diff 자체의 결함**(새로 쓴 서술 안의 모순, 새 계약이 기존 계약과 충돌하는가). **⭐⭐ 가장 무거운 발견(HIGH)은 이 라운드의 *처방들이 겹쳐서* 생긴 것이다.** `H-113`(splice 무효화를 `index - 1`로)과 `H-119`(`_baseObserver`에 `bk.invalidAfter = 0`)는 각각 독립적으로 옳은데, 둘이 겹치면서 **`invalidAfter`가 0이 될 수 있는 경로가 둘** 생겼고 `recompute`의 되감기 블록엔 클램프가 없었다 — `sum = prefix[0]`(nil) → `sourceList[0]`이 nil → **부기가 멀쩡한데 "부기가 깨졌음"이라는 error로 죽는다.** 8라운드 손 트레이싱 자체가 *"반영된 결정들이 서로 겹칠 때 성립하는가"*를 보는 라운드였는데, **그 라운드의 처방들이 겹쳐 같은 종류를 하나 더 만들었다.** 교훈: 한 라운드 안에서 여러 결정이 **같은 변수**를 건드리면(여기선 `bk.invalidAfter`), 각 결정을 개별로 검증하는 것으로 부족하다 — 그 변수의 **값 범위가 어떻게 넓어졌는지**를 따로 봐야 한다. **계약 결정 둘은 사용자에게 물었다** — `Subscribe`는 idempotent가 아니고 (`canBound` 게이트에 걸려 error), `WeakUnsubscribe`는 강한 킵이 남아 있으면 error. 둘 다 fail-fast 쪽. `Unsubscribe`만 idempotent이고 **이 비대칭이 의도된 것**이라는 것도 같이 명문화했다. ### 2차 `/code-review high` — 또 7건, 그중 넷이 1차 수정의 산물 1차 반영 **직후** 다시 돌렸더니 7건이 더 나왔다. **폐기된 서술을 만지는 방식**에 대한 교훈이 둘 나왔다: - **이름만 고치면 죽은 주장이 갓 정비된 것처럼 보인다.** `H-114` 반영 때 `handle._observers` → `_deps`로 필드명만 바꿨는데, 그 문장이 서술하던 **동작**(`bindLifetime`이 내부 Observer로 cascade한다)은 `H-58`이 이미 폐기한 것이었다. 이름이 최신이라 배너도 안 달렸고, 그 결과 살아 있는 확정 절 안에 **`H-58`이 막은 버그(바인드마다 `Rerun`)를 되살리는 안내**가 남았다. - **폐기 블록에 날짜 마커를 찍지 말 것.** `⛔` 배너 아래 죽은 문단에 `H-111` 정정 마커를 찍었더니 그 문단만 최신처럼 보였다. 배너만 달고 본문은 건드리지 않는 게 낫다. 나머지 다섯도 전부 "한 곳은 고쳤는데 그걸 요약·인용하는 곳이 안 따라옴" 계열이다(무효화 절의 머리 문장 vs 표, `rawSwap` 규칙이 표에 없음, 전파 루프 주석의 `Effect`, 예약 키 개수 2 vs 3, `luau-test` README의 옛 이름). ### 3차 `/code-review high` — 7건 더, 그리고 총평 **세 차례에서 21건**이 나왔고 **절반 이상이 직전 수정의 산물**이었다. (3차는 세션이 직접 호출했다 — `conventions.md`가 *"`/code-review`는 사용자만 호출할 수 있다"*고 적어뒀는데 **지금은 세션의 skill 목록에 있다.** 그 관례 문장은 실태와 다르다.) **반복된 실패 모드는 하나다 — 한 자리를 고치면서 그 자리를 요약·인용·정당화하는 이웃 문장을 같이 안 고침.** - 표에 행만 넣고 헤딩의 개수("인덱스는 셋")와 배치 목록은 그대로 → 새 규칙이 *"표는 산문뿐이고 코드 경로가 없다"*(`H-3`)는 원래 상태로 되돌아감. - 필드 이름만 바꾸고 그 이름이 서술하던 **동작**은 그대로 → 폐기된 cascade 주장이 갓 정비된 것처럼 보임. - 새 dedup의 **근거**를 잘못 적어(*"thread를 두 번 resume"*) 그 근거가 "thread가 양쪽 테이블에 산다"를 함의하게 됨 → 그 함의를 따라가면 죽은 코루틴을 영원히 조용히 `resume`하는 경로가 열린다. **감사자는 이 클래스를 구조적으로 못 잡는다.** 코퍼스 정합성(A 문서 vs B 문서)이 아니라 **방금 쓴 문단 안의 논리**이기 때문이다. 감사 11라운드가 0건으로 수렴한 뒤에도 code-review가 21건을 낸 이유가 이거다. ### 4차 `/code-review high` — 설계 결함 하나가 `H-101`을 역전시켰다 **부기 필드가 하나에서 둘로 갈라졌다** — `bk.offsetCacheValidUpTo`(캐시)와 `bk.offsetSetUpTo`(`:Set` 완료). 옛 단일 `invalidAfter`는 이름째 없앴다. 결정과 표는 followup의 "`H-101`의 '새 필드를 안 만든다'가 역전됐다" 절이 소스. **이 세션에서 가장 값이 컸던 순간은 사용자가 리뷰를 되물은 지점이다.** 리뷰가 낸 건 *"`getOffsetAt`이 되감기 신호를 지운다"*였고 나는 그걸 그대로 옮겨 "(a) recompute 중엔 안 올린다 / (b) 필드 분리" 두 갈래로 물었다. 사용자 반응: *"그럴리가. getOffsetAt 전부 끝나고 나서야 Set 이 일어나고 … 순서가 섞일 일이 없어서 되감기 신호가 지워질 일이 안 보이는듯 한데..? 다시 볼래?"* — **그 지적의 절반이 맞았다.** 한 프리미티브 안에서는 정말 안전하다 (`rawRemove`가 `getOffsetAt`을 splice **앞**에서 부른다). 다시 트레이싱해서 **한 콜백에 CRUD 두 번**(`Remove` 뒤 `Add` → `setOffsetSource` → `getOffsetAt`) 이라는 실제 경로를 찾아내 보이자, 사용자가 **패치가 아니라 근본 원인**을 짚었다: *"Set을 해줬느냐와 캐시가 유효하지 않느냐라는 다른 목적의 값을 같은 값이 쥐고 있음. 그것 자체가 문제였는듯."* 교훈 셋: - **리뷰 결과를 그대로 사용자에게 넘기지 말 것.** 내가 먼저 트레이싱해서 "어느 경로로 도달하는가"를 확정했어야 했다. 실제로 같은 라운드의 다른 발견(materialize 꼬리 게이트가 사후조건을 깬다)은 **직접 확인해보니 틀렸다** — `bk`가 그 Slot 자신의 부기라 게이트가 발화하면 바깥 루프가 어차피 확정한다. - **"두 뜻이 실제로 같다"는 확정은 의심할 것.** `H-101`이 그렇게 적고 새 필드를 거부했는데, 그 통합이 정확히 버그의 원인이었다. 같은 값이 두 질문에 답하고 있으면 **언젠가 한쪽만 갱신되는 경로가 나온다.** - **이름이 뜻을 겸하면 그 이름부터 없앨 것.** 사용자 지적으로 `invalidAfter`를 남기지 않고 완전히 치환했다 — 남겨두면 읽는 쪽이 옛 의미를 그대로 가져온다. ### 5~7차, 그리고 멈춘 이유 5차 6건 / 6차 8건(**HIGH 3, 전부 5차 수정의 산물**) / 7차 5건(**HIGH 0**). 항목은 followup이 소스. 6차가 정점이었고, 거기서 나온 규칙 하나를 명문화한 뒤 7차는 HIGH가 없었다. **⭐⭐ 이 세션 최대의 교훈 — 나는 같은 실수를 네 번 했다.** `_observers` → `_deps`(1차), `CheckReservedKeys`(3차), `invalidAfter` → `offsetSetUpTo`(5차), 그리고 6차의 인용문 오염. **전부 "토큰을 바꾸고 그 토큰이 든 문장은 안 읽음"**이다. 6차 건은 한 걸음 더 나아가 **역사 기록까지 오염**시켰다 — *"옛 이름을 완전히 없앴다"*는 내 선언이 정정 배너 **안의 인용문**에까지 적용돼, 폐기를 서술하는 문장이 자기가 폐기한 것의 새 이름을 쓰게 됐다. > **규칙**: 전역 치환은 **인용문·절 제목·정정 배너를 건드리면 안 된다.** > 그 셋은 *과거에 무엇이라고 적혀 있었는가*를 보존하는 게 목적이라, 새 > 이름으로 바꾸는 순간 목적이 무너진다. 치환 전에 제외 목록에 넣을 것. > 그리고 치환 뒤에는 **바뀐 토큰이 든 문장 전체**를 다시 읽을 것 — 토큰만 > 맞추면 그 문장이 설명하던 불변식이 바뀐 걸 못 본다. **7차에서 멈춘 판단**: HIGH가 나온 라운드는 **전부 직전에 구조를 바꾼 뒤**였다(필드 분리, 새 가드, 게이트 추가). 7차 수정 5건은 문장/줄 단위 국소 정정이고 새 계약도 대규모 치환도 없어, 반복된 실패 모드가 적용될 표면이 없다. 커밋 안 된 36파일 diff 자체도 리스크라 여기서 체크포인트를 만든다 — 이후 검토는 커밋 대상으로 언제든 가능하다. ### 남은 미검증 - 7차 수정분 자체(위 판단으로 감수). - 8라운드 §6의 "못 본 것"은 그대로 — 특히 **M5+ 구간은 문서 정독 수준**이고 값 단위 트레이싱을 안 했다. 다음 라운드가 있다면 거기가 최우선. - 실측 스파이크는 `luau-test/STATUS.md`가 소스. 이 세션이 항목 넷을 더했다 (`11` 재작성, `16`/`21` 최종형 + 빈 Store 대조군, `CheckedQuad` M2 후 재실측).