발견 17건(H-107~H-123)의 사용자 결정을 base/ 전체에 반영. 7라운드 확정 중 뒤집힌 건 없고, 고친 건 전부 7라운드가 base/에 내려앉을 때 생긴 누락·충돌 (하루 차로 확정된 결정들이 서로를 못 본 자리)이다. 결정의 소스는 qa-request/pre-implementation-handtrace-round8-followup.md. 계약 변경 넷: - Ref 콜백이 fn(value, ref) — 2번째가 곧 출처 Epoch. Effect가 Update(from)에 넘길 유일한 통로였다(k(value)뿐이면 Update(nil) 크래시, 실측 재현). - Observer fn이 세 자리 fn(targetState, self, emitFrom) + observer._state 강참조. 옛 2-인자는 "self는 리시버" 계약과 정면 충돌해 무인자 state:Observer()의 내부 콜백이 즉사했다. - WeakSubscribe도 .Subscribed를 세운다. 안 그러면 Effect의 State dep 전량이 조용히 침묵. 해제는 "건 경로로 푼다"(양방향 fail-fast). - 예약 키 진단이 CheckReservedKeys<keyof<T>> — T를 통째로 넘기는 배선은 실사용 T에서 아예 안 돈다(Source<T>가 *error-type*을 품어 유효한 Store 전부에 스퓨리어스 에러). 사용자가 문항의 전제를 두 번 정정: Ref 콜백과 Observer 콜백은 이질적이라 애초에 통합 대상이 아니었고(Observer엔 자기 epoch가 없다), H-118은 소유권 문제가 아니라 gate-plan 5번의 문장이 틀린 것이었다(🟡→🟢). 커밋 전 검증 — 감사 11라운드(44건, 0건으로 수렴) + /code-review high 7라운드(42건) = 86건. 감사가 0으로 수렴한 직후 code-review가 42건을 냈고, 그중 하나가 H-101의 "새 필드를 안 만든다"를 역전시켰다: getOffsetAt의 부수효과가 splice의 되감기 신호를 지우는 경로가 실재해, 부기 필드를 offsetCacheValidUpTo(캐시)와 offsetSetUpTo(:Set 완료) 둘로 분리했다. "Set을 해줬느냐"와 "캐시가 유효하냐"를 한 값이 쥔 게 원인이었다. 그 외: splice 무효화 i-1, 명시 recompute 호출부 전부 재진입 게이트, recompute 되감기 클램프, Store defaults isSource 검증, isModifier 가드를 Source 생성자로, 훅 슈가 nil 가드, pesde.lock 커밋 확정, :Single 3-인자. 2026-08-25 session/ 원문 공백은 2026-08-19 선례대로 재구성 없이 기록만. doc-check ERROR 0 / WARN 기준선 유지. Claude-Session: https://claude.ai/code/session_01F9zgJ4c4kDitAoQMm9qxKn Co-authored-by: qwreey <me@qwreey.moe>
19 KiB
2026-08-26 — 8라운드 손 트레이싱 처리 (Q1~Q10)
무엇을 했나: qa-request/pre-implementation-handtrace-round8.md의 발견
17건(H-107~H-123)을 사용자와 대화형으로 처리하고 base/에 전량 반영,
-round8-followup.md를 신설했다. 결정과 근거의 소스는 그 followup 파일
이고 여기선 진행 방식과 그 과정에서 드러난 것만 남긴다.
진행 방식
- 라운드 문서를 전량 읽고, 물어보기 전에 🔴 다섯의 핵심 주장을
base/에서 직접 재확인했다 —Ref:SetH-53 블록에Revision갱신·Weak 순회가 없는 것, 전파 루프가sub.fn(sub, from)인 것,CheckReserved가T를 통째로 받는 서술. 전부 그대로였다. - §4의 문항 Q1~Q10을 세 배치로 물었다(🔴 넷 → M3/백로그 넷 → 나머지).
- 사용자 회신을 받는 즉시
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.pyERROR 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 대조군,CheckedQuadM2 후 재실측).