qa: 9라운드 후속 H-143~H-146 확정·반영 + 감사 8라운드·code-review — 잔여 셋은 10라운드 문항지로

- H-143 Rerun 꼬리: 실행 중 사망(wasAlive and not canExecute)이면 cleanup 즉시 소진
  (처음 쓴 not canExecute 판정은 생성자 최초 설치를 죽여 감사 2라운드가 정정)
- H-144 재구독 꼬리(Refresh 먼저) + 진입점은 EffectHandle 자기 것 (b)
  (Observer 함수 배정은 콜론 위임으로 꼬리 2회 — 감사 4라운드, luau 재현)
  → conventions.md 설계 원칙 신설: 하나의 무언가가 두 일을 하지 않는가
- H-145 bk.indexOfElement weak-key / H-146 루트 .Parent는 사용자 몫, Mount 없음
- 감사 1→1→1→1→1→1→1→0, /code-review high 10건 중 7 반영
- H-147~H-149는 qa-request/pre-implementation-handtrace-round10.md §4로 (배치 회신)
- round10 지시서(-brief.md) 신설, 광범위 탐사 예정

Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01546hjsYNLSMZdHdPyTZaGb
This commit is contained in:
qwreey 2026-08-28 00:28:52 +09:00
parent 7f5868302e
commit 0ec22fbe73
Signed by: qwreey
GPG key ID: D28DB79297A214BD
21 changed files with 838 additions and 92 deletions

File diff suppressed because one or more lines are too long

View file

@ -262,7 +262,7 @@ quad/
│ └── src/ │ └── src/
│ ├── Source.luau # 값의 근원, 단일 지점. Source가 State를 구조적으로 만족(`__index` 델리게이션) │ ├── Source.luau # 값의 근원, 단일 지점. Source가 State를 구조적으로 만족(`__index` 델리게이션)
│ ├── State.luau # 캐시만 하는 non-owning 핸들, state(state) 분기, `:With`/`:Compute`/`:Observer`(등록 즉시 1회 실행)/`:Gate`(`GateNode`, `ComputeNode`와 같은 층위 — `base/gate-plan.md`) 전부 여기 소속 │ ├── State.luau # 캐시만 하는 non-owning 핸들, state(state) 분기, `:With`/`:Compute`/`:Observer`(등록 즉시 1회 실행)/`:Gate`(`GateNode`, `ComputeNode`와 같은 층위 — `base/gate-plan.md`) 전부 여기 소속
│ ├── Observer.luau # ⭐ [2026-08-25 신설, 7라운드 `H-99`] `Observer` 객체와 **`:Subscribe()`/`:WeakSubscribe()` 전역 레지스트리의 소유 모듈** — `EpochMap.luau`와 같은 이유로 `State.luau`에 묻지 않는다(`Effect`/`Gate`/leaf 핸들러가 전부 이 레지스트리를 본다) │ ├── Observer.luau # ⭐ [2026-08-25 신설, 7라운드 `H-99`] `Observer` 객체와 **`:Subscribe()`/`:WeakSubscribe()` 전역 레지스트리의 소유 모듈** — `EpochMap.luau`와 같은 이유로 `State.luau`에 묻지 않는다(`Effect`/`Gate`/leaf 핸들러가 전부 이 레지스트리를 본다**[2026-08-27 `H-144` (b)]** `Effect.luau`는 이 두 테이블과 `canBound`만 공유하고 네 진입점 본문은 자기 것)
│ ├── EpochMap.luau # 재사용 가능한 Epoch 부기 객체(`:Update`/`:Refresh`/`:Sync`/`:TrackFrom`) — `State.luau`에 묻지 않고 별도 모듈, `GateNode`/`State`/`Effect`가 전부 씀(`base/state-epoch-plan.md`) │ ├── EpochMap.luau # 재사용 가능한 Epoch 부기 객체(`:Update`/`:Refresh`/`:Sync`/`:TrackFrom`) — `State.luau`에 묻지 않고 별도 모듈, `GateNode`/`State`/`Effect`가 전부 씀(`base/state-epoch-plan.md`)
│ ├── Store.luau # source 집합체, dot-access로 Source 그대로 반환(**평범한 레코드 필드** — 타입 함수 안 씀). **[2026-08-25]** 생성은 **명시적 초기화**(타입 인자에 `Source<T>` 직접, `defaults`에도 `Source(v)` 직접 — 옛 lazy `__index` 폐기), 동적 키는 `:Of<<T>>(name)` 하나(옛 `GetDynamic` 흡수), `:Names()`, 예약 키 진단용 `CheckReservedKeys<keyof<T>>`(**[2026-08-26 `H-112`]** 옛 이름 `CheckReserved``T`를 통째로 받아 실사용 `T`에서 안 돌았다 — `base/store-plan.md`) │ ├── Store.luau # source 집합체, dot-access로 Source 그대로 반환(**평범한 레코드 필드** — 타입 함수 안 씀). **[2026-08-25]** 생성은 **명시적 초기화**(타입 인자에 `Source<T>` 직접, `defaults`에도 `Source(v)` 직접 — 옛 lazy `__index` 폐기), 동적 키는 `:Of<<T>>(name)` 하나(옛 `GetDynamic` 흡수), `:Names()`, 예약 키 진단용 `CheckReservedKeys<keyof<T>>`(**[2026-08-26 `H-112`]** 옛 이름 `CheckReserved``T`를 통째로 받아 실사용 `T`에서 안 돌았다 — `base/store-plan.md`)
│ ├── Blocker.luau # 값 기반 emit 지연/합치기(`base/blocker-plan.md`) — 위 `state:Gate``GateNode` 위에 얹히는 **정책**, 바닥부터 짜지 않음 │ ├── Blocker.luau # 값 기반 emit 지연/합치기(`base/blocker-plan.md`) — 위 `state:Gate``GateNode` 위에 얹히는 **정책**, 바닥부터 짜지 않음

View file

@ -331,7 +331,28 @@ end
배선은 사용자 확정이 아니라 **규칙을 기존 계약에 얹은 제 선택**이다 — 배선은 사용자 확정이 아니라 **규칙을 기존 계약에 얹은 제 선택**이다 —
`H-142` 처방 후보 (a)/(b)/(c)가 전부 새 메커니즘이라 정하지 않았던 것을 `H-142` 처방 후보 (a)/(b)/(c)가 전부 새 메커니즘이라 정하지 않았던 것을
"키 금지"로 바꾸니 필요한 코드가 이 거부 한 줄뿐이다. 다른 모양이 낫다면 "키 금지"로 바꾸니 필요한 코드가 이 거부 한 줄뿐이다. 다른 모양이 낫다면
갈아끼울 것.) 순서 문제는 키가 없어지면서 소멸한다. 갈아끼울 것.) 순서 문제는 키가 없어지면서 소멸한다. **[2026-08-27 `H-146`]**
그 거부는 일반 매치 실패 문구(*"no handler … check quad-roblox provider"*)가
아니라 **전용 문구**를 낸다 — provider 설정을 의심하게 만들지 않도록
("`Parent` is not a prop: the parent attaches its children; a quad root is
parented by the caller" 취지, 정확한 문장은 구현 시).
- **⭐ [2026-08-27 확정, 9라운드 `H-146`] 루트는 이 금지의 범위 밖이다 —
quad 트리의 최상위를 quad 밖 부모에 붙이는 건 사용자가 밖에서 `.Parent =`
한다.** 위 인용문의 *"외부에서 직접 Parent 설정해주지 말것"*이 막는 것은
**quad가 관리하는 자식 자리**(Slot 요소·정적 자식)에 밖에서 끼우는 것이고
(`slot-plan.md`의 "동적 자식은 반드시" 절), 루트는 어느 `Length`/형제 순서
부기에도 속하지 않아 어긋날 부기가 없다(`gcconn`/`gchold`는 `Destroying`
기반이라 `Parent`와 무관). **`Mount(root, parent)`류 표면은 만들지 않는다**
— 사용자 확정: *"React 에서도 최상위 경로는 … root 를 돔 api 로 가져오고
거기에 바운딩 하는 처리를 해야하거든. … Quad 가 제공하는 마운트는 완전히
다른 성격이라서(부기에 대한 처리가 들어가는데 PlayerGUI 등 Quad 가 제공하지
않은 부기가 없는 객체에 대해서 Quad 의 객체를 주입하는 성격의 API 는
아니거든) 해당 부분을 해결하기 위해서 다른 API 를 제공 할 이유가 없기도 해.
해당 부분은 각 엔진을 사용하는 최종 사용자의 몫."* 즉 루트 부착은 엔진마다
다를 수 있는 최종 사용자 코드고, quad의 마운트(Slot)는 *부기가 있는* 자리에
넣는 별개 개념이다. 루트 전용 props 키(`H-142` 취지와 충돌)도 기각. 사용자
문서에 "루트는 직접 `.Parent =`, 그 아래는 절대 직접 하지 말 것"을 같이
적을 것(`research/documentation-content-map.md` 대상).
**이벤트 바인딩 — `On.EventName` 도트액세스 안 씀, PA님 방식(평범한 문자열 **이벤트 바인딩 — `On.EventName` 도트액세스 안 씀, PA님 방식(평범한 문자열
키 + 런타임 리플렉션)으로 전환**: `DeclarativeInstance.luau:13-91` 키 + 런타임 리플렉션)으로 전환**: `DeclarativeInstance.luau:13-91`

View file

@ -1502,8 +1502,9 @@ Dispatch.getOffsetAt(ownerKey, i): number -- [2026-08-21 5라운드] 그
(`nativeInsert` → `setLength``recompute`)과 같게. 옛 서술(`setLength` → (`nativeInsert` → `setLength``recompute`)과 같게. 옛 서술(`setLength` →
`Parent`)은 단건 경로에서 `recompute`가 부착 전에 돌았다. (3) **5번째 인자 `Parent`)은 단건 경로에서 `recompute`가 부착 전에 돌았다. (3) **5번째 인자
`element`는 안 넘긴다** — 상수 길이라 지속 클로저가 없어 Q3 계약상 생략 `element`는 안 넘긴다** — 상수 길이라 지속 클로저가 없어 Q3 계약상 생략
대상이고, 넘기면 `setLength(…, 0)` 해제가 `bk.indexOfElement`의 옛 키를 대상이다(**[2026-08-27 `H-145`]** 처음엔 "넘기면 해제가 옛 키를 안 지워
안 지워(해제 호출엔 요소가 없다) 교체마다 옛 자식이 강참조로 쌓인다. 대안 — `Dispatch.drive``type(k) == "number"` 분기에서 일괄 등록 — 강참조로 쌓인다"도 근거였으나 그 맵이 weak-key가 되며 그 근거는 사라졌다 —
남는 근거는 "지속 클로저가 없어 조회할 일이 없다" 하나). 대안 — `Dispatch.drive``type(k) == "number"` 분기에서 일괄 등록 —
은 아래 *"모든 핸들러가 `k=number`일 때 처리하도록 두는"*에서 **이미 기각된 은 아래 *"모든 핸들러가 `k=number`일 때 처리하도록 두는"*에서 **이미 기각된
안**이라 다시 열지 않는다. `ROADMAP.md` M5 체크박스에 같은 두 줄을 적었다. 안**이라 다시 열지 않는다. `ROADMAP.md` M5 체크박스에 같은 두 줄을 적었다.
@ -1548,8 +1549,29 @@ Dispatch.getOffsetAt(ownerKey, i): number -- [2026-08-21 5라운드] 그
같은 파일 안에서 어떤 자리는 가드하고 어떤 자리는 안 하는 불일치를 만든다. 같은 파일 안에서 어떤 자리는 가드하고 어떤 자리는 안 하는 불일치를 만든다.
**가드를 두지 말 것.** **가드를 두지 말 것.**
- **`getBookkeeping``bk`를 만들 때 `offsetCache = {}`, `offsetCacheValidUpTo = 0`, - **`getBookkeeping``bk`를 만들 때 `offsetCache = {}`, `offsetCacheValidUpTo = 0`,
`offsetSetUpTo = 0`, `recomputeBlocker = Blocker()`, **`indexOfElement = {}`** `offsetSetUpTo = 0`, `recomputeBlocker = Blocker()`,
(**[2026-08-27 Q3]**)으로 초기화한다.** **`indexOfElement = setmetatable({}, { __mode = "k" })`**(**[2026-08-27 Q3]**,
**[2026-08-27 weak-key 확정, 9라운드 `H-145`]**)으로 초기화한다.**
- **왜 weak-key인가**: 최상위 `SlotHandler` retractor의 해제 `setLength(inst,
k, 0)`엔 요소 인자가 없어 옛 Slot 키가 안 지워지고, `bk``Relate(inst)`
강한 키 맵이면 **`State<Slot>` 교체마다 옛 Slot(과 그 트리)이 부모가 죽을
때까지 산다.** 마운트 중엔 `_elements`/`bindLifetime`이 강하게 잡으므로
키가 빠질 일이 없고, 해제 뒤엔 저절로 빠진다 — "다른 곳에서 안전하게
유지되는 것은 항상 weak로 잡는다"(`base/lifecycle-pattern.md`) 그대로. 값이
정수라 Luau의 ephemeron 미지원(값이 키를 참조하면 안 빠지는 것)도 안
걸린다. 사용자 확정(*"컴퓨팅 비용이 더 안들고, 복잡하지 않고 깔끔한
경로"*) — 단점으로 짚은 "한 Slot이 포탈로 여러 `bk`에 들락거리면 여러
맵에 남을 수 있다"는 Slot 수·포탈 경로 수가 적어 문제 아님, 단 **요소가
마운트 중 GC되지 않아야 한다**는 조건 부착. 사용자는 그 보장자로 *"자신을
gchold 로 잡으니"*를 들었는데 **[2026-08-28 `/code-review` 정정]** gchold는
`inst → 값` 방향 홀더라 `inst` 자신을 잡지 않는다 — 실제 보장자는
**`slot._elements`(강한 배열)와 `gatedRecompute`의 요소 캡처**다. 이 둘을
약하게 바꾸면 이 맵의 키가 마운트 중 빠져 `bk.indexOfElement[element]`
`nil`이 된다 — 그 둘은 이 맵의 불변식을 지탱하는 자리로 표시할 것.
Instance를 `__mode = "k"` 키로 쓰는 경로 자체는
`audit/gcconn-trick-verification.md`가 **미확인**으로 남겨둔 항목이라 M3
구현 시 실측 대상. 해제에 요소를 넘겨 `setLength`가 지우게 하는 안(시그니처 의미 확장)과
inst 층 명시 삭제 API는 기각.
(**[2026-08-26]** `offsetCacheValidUpTo`은 같은 날 `offsetSetUpTo`에서 갈라져 나온 (**[2026-08-26]** `offsetCacheValidUpTo`은 같은 날 `offsetSetUpTo`에서 갈라져 나온
필드다 — 아래 "두 필드" 절.) (`bk.N`만 `nil` 시작을 필드다 — 아래 "두 필드" 절.) (`bk.N`만 `nil` 시작을
유지한다 — 그쪽은 `or 0` 방어가 이미 자리를 잡았고 "아직 아무 자리도 등록 유지한다 — 그쪽은 `or 0` 방어가 이미 자리를 잡았고 "아직 아무 자리도 등록

View file

@ -332,7 +332,10 @@ end
게 흔한 정상 용례) `_cleanup`이 **항상 `nil`**인 Effect가 존재한다. 그러면 게 흔한 정상 용례) `_cleanup`이 **항상 `nil`**인 Effect가 존재한다. 그러면
바인드/포탈 재마운트마다 조건이 참이 되어 `fn`이 다시 돌고 — 이 재설계가 바인드/포탈 재마운트마다 조건이 참이 되어 `fn`이 다시 돌고 — 이 재설계가
없애려던 `H-58`(바인드마다 `Rerun`)이 **그대로 되살아난다.** 없애려던 `H-58`(바인드마다 `Rerun`)이 **그대로 되살아난다.**
`_installed``Rerun`이 끝날 때 참, `_consumeCleanup`에서 거짓이 된다. `_installed``Rerun`이 끝날 때 참(**[2026-08-27 `H-143`]** 단 그 실행 중에
핸들이 죽었으면 — `wasAlive and not canExecute` — 세우지 않는다,
`_consumeCleanup`이 걸어둔 거짓이 그대로 남는다), `_consumeCleanup`에서 거짓이
된다.
**⭐ [2026-08-25 신설, 7라운드 `H-60`] `EffectHandle:Rerun()` 정의.** **⭐ [2026-08-25 신설, 7라운드 `H-60`] `EffectHandle:Rerun()` 정의.**
지금까지 호출부만 다섯 곳이고 정의가 없었다. 지금까지 호출부만 다섯 곳이고 정의가 없었다.
@ -347,8 +350,34 @@ function EffectHandle:Rerun() -- 공개 메소드, 무인자
repeat repeat
self._pending = false self._pending = false
self:_consumeCleanup() self:_consumeCleanup()
self._cleanup = self.fn(self) local wasAlive = canExecute(self) -- `fn` 진입 전 상태
local c = self.fn(self)
-- ⭐ [2026-08-27 확정, 9라운드 `H-143`] **이 실행 중에 죽었으면 cleanup만
-- 한다.** `fn` 안에서 `self:Unsubscribe()`(또는 `WeakUnsubscribe`)가
-- 통과했으면(강/약 구독 핸들 — leaf 바인딩 핸들은 자기 해제로 죽지
-- 않으므로 이 분기에 오지 않는다) 이 핸들은 더 이상 어느 레지스트리에도
-- 없고 leaf도 아니라 **아무도 이 cleanup을 소진할 수 없다** — 저장하지
-- 말고 즉시 소진한다.
-- 판정은 "살아 있다가 → 죽었다"(`wasAlive and not canExecute`)다.
-- `not canExecute` 하나로 하면 안 된다 — **생성자의 최초 `Rerun()`
-- 어떤 바인드·구독보다 먼저** 돌아 `canExecute`가 항상 거짓이라, 첫
-- 실행의 cleanup을 그 자리에서 소진하고 `_installed`를 거짓으로 남겨
-- 첫 바인드에서 `fn`이 또 돈다(감사 2라운드가 잡은 회귀 — `H-58`
-- 재현). 약한 해제도 같은 경로로 소진된다(죽은 핸들에 매달린 cleanup은
-- "영원히 안 불림"보다 "한 번 불림"이 계약에 가깝다). 사용자 확정:
-- *"특정 state 에 변경을 딱 한번만 처리하고 cleanup 되는걸 만드는 요구가
-- 존재하지 않을 이유가 딱히 없다"*, *"실행중 죽으면 클린업만 하기"*
-- `fn``Unsubscribe`는 지원 대상이다.
if wasAlive and not canExecute(self) then
if c then c() end -- `_installed`는 이미 `_consumeCleanup`이 거짓으로
self._pending = false -- 죽은 핸들의 재요청(`fn` 안 `self:Rerun()`)은 버린다
-- — `fire`가 죽은 핸들의 emit을 버리는 것과 같은 규칙.
-- 안 버리면 아래 `until`이 한 바퀴 더 돌아 죽은
-- 핸들에서 `fn`이 또 돌고 그 cleanup이 다시 고아가 된다.
else
self._cleanup = c
self._installed = true -- cleanup 반환 여부와 무관하게 "설치됨" self._installed = true -- cleanup 반환 여부와 무관하게 "설치됨"
end
until not self._pending -- 재요청이 또 오면 또 돈다 until not self._pending -- 재요청이 또 오면 또 돈다
self._running = false self._running = false
end end
@ -404,7 +433,8 @@ end
`fn|Observer`, **강참조**), `_epochs`(`EpochMap` — `Ref``Epoch`라 균일), `fn|Observer`, **강참조**), `_epochs`(`EpochMap` — `Ref``Epoch`라 균일),
`_blocker`(등록 구간 억제), `_cleanup`, **`_installed`**(설치 여부 — `_blocker`(등록 구간 억제), `_cleanup`, **`_installed`**(설치 여부 —
cleanup 반환이 선택이라 `_cleanup`으로는 판정 못 한다), cleanup 반환이 선택이라 `_cleanup`으로는 판정 못 한다),
`_running`/`_pending`(재진입). `_running`/`_pending`(재진입), **`.Subscribed`**(공개 플래그 — `canExecute`
읽는 그것, 네 진입점이 세우고 내린다, 아래 "`EffectHandle:Subscribe()`" 절).
**옛 `_refDeps`/`_refCallbacks`/`_observers`/`_installing`은 `_deps` 하나와 **옛 `_refDeps`/`_refCallbacks`/`_observers`/`_installing`은 `_deps` 하나와
`_blocker`로 대체됐다.** `_blocker`로 대체됐다.**
@ -497,37 +527,122 @@ quad의 반응형 그래프/cleanup 인체공학만 재사용하는 경우)로
**확정**: `EffectHandle`에도 `:Subscribe()`/`:Unsubscribe()` 추가, 둘 다 **확정**: `EffectHandle`에도 `:Subscribe()`/`:Unsubscribe()` 추가, 둘 다
`self` 반환(Observer와 동일한 fluent 대칭). `self` 반환(Observer와 동일한 fluent 대칭).
**⭐ [2026-08-27 확정, 9라운드 `H-127`] 의사코드 — 네 진입점은 Observer의 **⭐ [2026-08-27 확정, 9라운드 `H-127` → 같은 날 (b)로 정정] 의사코드 — 네
것을 재사용하고, `Unsubscribe`*통과한 뒤* cleanup을 덧붙인다.** 아래 진입점은 `EffectHandle` 자기 것**(Observer와 같은 레지스트리·같은 `canBound`
산문(번호 목록)은 이 블록을 풀어 쓴 것이고, **순서는 이 블록이 정본**이다. 게이트를 쓰되 함수 본문은 공유하지 않는다 — 아래 블록 머리 주석), **`Unsubscribe`
게이트를 *통과한 뒤* cleanup을 덧붙인다.** 아래 산문(번호 목록)은 이 블록을 풀어
쓴 것이고, **순서는 이 블록이 정본**이다.
산문의 번호 순서(플래그 → cleanup → fail-fast)대로 짜면 leaf 바인딩된 핸들에 산문의 번호 순서(플래그 → cleanup → fail-fast)대로 짜면 leaf 바인딩된 핸들에
`:Unsubscribe()`를 불렀을 때 **cleanup을 소진한 뒤에야 error**가 나서 `E-11` `:Unsubscribe()`를 불렀을 때 **cleanup을 소진한 뒤에야 error**가 나서 `E-11`
막으려던 피해(cleanup 앞당김, `_installed = false`)가 이미 일어난 뒤다 — 막으려던 피해(cleanup 앞당김, `_installed = false`)가 이미 일어난 뒤다 —
Observer 쪽 의사코드는 가드가 첫 줄이라 이 문제가 없었다. Observer 쪽 의사코드는 가드가 첫 줄이라 이 문제가 없었다.
```lua ```lua
-- 레지스트리(`Subscribed`/`WeakSubscribed`)·`canBound` 게이트·`.Subscribed` -- ⭐⭐ [2026-08-27 확정 (b), 9라운드 `H-144` 후속 — 감사 4라운드] **`EffectHandle`
-- 플래그 전부 `base/lifecycle-pattern.md` (2)의 Observer 네 진입점과 **같은 -- 네 진입점을 자기 것으로 가진다 — Observer의 함수 본문을 배정하지 않는다.**
-- 구현**이다 — 메소드 테이블에 그대로 배정한다. 등록되는 건 **핸들 자신뿐** -- 공유하는 건 **레지스트리 두 개**(`Subscribed`/`WeakSubscribed`, 소유 모듈은
-- (내부 Observer·`Ref` 콜백은 생성자에서 이미 `Weak*`로 걸려 있다, `H-59`). -- `Observer.luau``H-99`)와 **`canBound` 게이트**(`LifetimeHandle.luau`의
EffectHandle.Subscribe = Observer.Subscribe -- canBound 게이트 + 강한 킵 -- 탑레벨 함수)뿐이고, `.Subscribed` 플래그의 뜻도 같다. 여기 한때(Q4/`H-127`)
EffectHandle.WeakSubscribe = Observer.WeakSubscribe -- 게이트 + 약한 등록 + 플래그 -- `EffectHandle.Subscribe = Observer.Subscribe`처럼 **함수 객체를 그대로 배정**해
EffectHandle.WeakUnsubscribe = Observer.WeakUnsubscribe -- 관대(`H-133`) — cleanup 안 건드림 -- 뒀는데, `Observer:Subscribe`의 본문이 `self:WeakSubscribe()`로 **콜론 위임**하는
-- 탓에 `self``EffectHandle`이면 그 조회가 `EffectHandle`의 오버라이드로 가서
-- 재구독 꼬리가 두 번 돌고, 첫 번째는 강한 킵이 서기 **전에** `Rerun``fn`
-- `self:Unsubscribe()`(`H-143`이 지원하는 패턴)가 *"not subscribed strongly"*로
-- error했다(로컬 `luau`로 재현). **사용자 확정**: *"b가 맞아. 내 머리에서 나왔던
-- 처음 구조는 그것이였어. … '하나의 무언가가 두 일을 동작하지 않는가에
-- 유의하자' — 이것도 마찬가지야. 버그를 유발하기 좋은 포인트였고"* —
-- Observer와 Effect는 이질적 타입이라(생성 방법부터 다르다) 본문을 섞지 않는다
-- (`conventions.md`의 "설계 원칙" 절에 원칙으로 승격). 등록되는 건 **핸들
-- 자신뿐**(내부 Observer·`Ref` 콜백은 생성자에서 이미 `Weak*`로 걸려 있다,
-- `H-59`). 레지스트리 두 테이블을 `Effect.luau`가 어떻게 받는지(모듈 내부
-- export, 공개 API 아님)는 구현 세부 — 이름은 구현 시.
-- ⭐ [2026-08-27 확정, 9라운드 `H-144`] 구독 둘은 등록 뒤 leaf 재바인드
-- (`_bindDestroying`, `H-65`)와 **정확히 같은 꼬리**를 붙인다. `Unsubscribe`
-- cleanup을 소진한 핸들을 다시 `Subscribe`하면 `.Subscribed = true`만 서고 `fn`
-- 안 돌아, deps 없는 Effect는 레지스트리가 살려두는 죽은 핸들(누수)이 되고
-- deps 있는 것은 다음 emit까지 죽어 있었다. `Refresh()`**먼저** 부르는 이유는
-- 287행 캐비엇과 같다 — 해제 중 도착한 emit은 `fire``canExecute` 가드가
-- `_epochs:Update` 앞에서 버리므로 `_epochs`가 해제 전 리비전에 멈춰 있고, 안
-- 맞추면 재구독 뒤 첫 emit(예: 게이트 유보가 풀리며 오는 배치)에 같은 값으로
-- `fn`이 한 번 더 돈다. 사용자 확정: *"초기에 epoch 를 전부 잘 설정해주는게
-- 나을수도 … 처음 연산 기준에 있어서도 전부 값은 최신 값이거든. 따라서 다음
-- emit 을 받아야할 이유가 없을수도 있어."* Blocker는 안 쓴다 — dep을 다시
-- 등록하지 않으므로(생성자에서 한 번, `_deps` 강참조 유지) 억제할 발화가 없다.
local function resubscribeTail(self)
local depsChanged = self._epochs:Refresh()
if not self._installed or depsChanged then
self:Rerun()
end
end
-- 꼬리는 항상 **등록이 전부 끝난 뒤** 한 번 — `Subscribe``WeakSubscribe`
-- 부르지 않고 등록 세 줄을 자기 안에 펼쳐 쓴다. 위임하면(콜론이든 dot이든)
-- 꼬리가 강한 킵 **앞**에서 돌거나 두 번 돈다 — 감사 4라운드가 잡은 바로 그
-- 모양이다. 게이트·메시지 분기는 Observer의 것과 같다(`lifecycle-pattern.md` (2)).
function EffectHandle:WeakSubscribe()
if not canBound(self) then
error(if self.Subscribed then "already subscribed" else "already bound to an Instance", 2)
end
self.Subscribed = true
WeakSubscribed[self] = true
resubscribeTail(self)
return self
end
function EffectHandle:Subscribe()
if not canBound(self) then
error(if self.Subscribed then "already subscribed" else "already bound to an Instance", 2)
end
self.Subscribed = true
WeakSubscribed[self] = true
Subscribed[self] = true -- 강한 킵이 선 **뒤에** 꼬리 — `Rerun` 안의
resubscribeTail(self) -- `fn``self:Unsubscribe()`해도 가드 통과
return self
end
function EffectHandle:WeakUnsubscribe() -- 관대(`H-133`) — cleanup 안 건드림
if Subscribed[self] ~= nil then
error("subscribed strongly; use :Unsubscribe()", 2)
end
WeakSubscribed[self] = nil
self.Subscribed = false
return self
end
function EffectHandle:Unsubscribe() function EffectHandle:Unsubscribe()
Observer.Unsubscribe(self) -- ⭐ 게이트가 **먼저** — 강하게 구독된 적 없으면(leaf if Subscribed[self] == nil then -- ⭐ 게이트가 **먼저** — 강하게 구독된 적 없으면
-- 바인딩·약한 구독·미구독) 여기서 error, cleanup엔 error("not subscribed strongly; use :WeakUnsubscribe()", 2) -- (leaf 바인딩·약한
-- 손도 안 댄다(`E-11`). 통과하면 강한 킵 해제 + end -- 구독·미구독) 여기서 error, cleanup엔 손도
-- `.Subscribed = false`(향후 재실행 차단). Subscribed[self] = nil -- 안 댄다(`E-11`).
WeakSubscribed[self] = nil
self.Subscribed = false -- 향후 재실행 차단
self:_consumeCleanup() -- 통과했을 때만: 직전 cleanup 정확히 1회, `_installed = false` self:_consumeCleanup() -- 통과했을 때만: 직전 cleanup 정확히 1회, `_installed = false`
return self return self
end end
``` ```
- **`WeakUnsubscribe`는 cleanup을 소진하지 않는다** — 약한 구독은 "GC에 - **`WeakUnsubscribe` 자체는 cleanup을 소진하지 않는다** — 약한 구독은 "GC에
맡기는" 경로라 해제가 곧 종료 신호가 아니다. 종료 신호는 강한 구독의 맡기는" 경로라 해제가 곧 종료 신호가 아니다. 종료 신호는 **[2026-08-27 `H-143`
`Unsubscribe`와 leaf 사망(`unbindLifetime`의 훅) 둘뿐. 으로 셋]** — 강한 구독의 `Unsubscribe`, leaf 사망(`unbindLifetime`의 훅), 그리고
- 실측(9라운드 `core9.luau`, `t12` 매트릭스): 이 배정으로 leaf 바인딩된 **`fn`이 실행 중에 자기 핸들을 죽인 경우**(`fn` 안 `self:Unsubscribe()` /
`self:WeakUnsubscribe()``WeakUnsubscribe` 본문은 여전히 cleanup에 손대지
않지만, 그걸 감싼 `Rerun`의 죽음 판정 `wasAlive and not canExecute`가 그
실행이 돌려준 cleanup을 즉시 소진한다, 위 `Rerun` 의사코드). 여기 한때
"둘뿐"이라 적혀 있었다(감사 7라운드 정정).
- **`fn` 안에서 허용되는 핸들 호출은 `self:Rerun()`과 자기 해제다 — 단 자기
해제는 *건 경로로*: 강구독 핸들은 `self:Unsubscribe()`, 약구독 핸들은
`self:WeakUnsubscribe()`. leaf 바인딩된 핸들엔 자기 해제 경로가 없다**(종료는
leaf 사망뿐 — `Unsubscribe()`는 가드에서 error하고 그 error는 `Rerun`을 뚫고
나가 `_running`이 참으로 남는 UB, `WeakUnsubscribe()`는 관대 통과하지만
`isBoundAlive`가 그대로라 죽지 않는다; **[2026-08-28 `/code-review`]**). `fn`
재구독(`self:Subscribe()`/
`self:WeakSubscribe()`)은 **[2026-08-27 기준] 서술하지 않는다**(트레이싱상
`resubscribeTail``Rerun``_running` 재진입으로 `_pending`만 세워 크래시는
안 하지만 지원 목록이 아니다 — 필요가 관측되면 그때 정한다).
- 실측(9라운드 `core9.luau`, `t12` 매트릭스 — **당시 함수 배정 형태에서의 실측**이고
(b) 재작성 뒤 재실행하지는 않았다[2026-08-27 기준]; 게이트 순서는 그대로라
같은 결과가 나올 것으로 추정할 뿐, 확정 근거는 M2 구현 테스트가 될 것): leaf 바인딩된
핸들의 `:Unsubscribe()`가 cleanup을 건드리지 않고 error, `:Subscribe()` 핸들의 `:Unsubscribe()`가 cleanup을 건드리지 않고 error, `:Subscribe()`
`:Unsubscribe()`는 cleanup 1회 — 전부 기대대로. `:Unsubscribe()`는 cleanup 1회 — 전부 기대대로.
@ -542,10 +657,15 @@ end
등록하면 (a) `handle.Subscribed`가 안 세워져 `canExecute(handle)` 등록하면 (a) `handle.Subscribed`가 안 세워져 `canExecute(handle)`
영원히 거짓이고, (b) deps 없는 `Effect(fn):Subscribe()`는 등록할 게 영원히 거짓이고, (b) deps 없는 `Effect(fn):Subscribe()`는 등록할 게
아예 없어 핸들이 GC되고 cleanup이 유실된다. 아예 없어 핸들이 GC되고 cleanup이 유실된다.
- **`:Subscribe()`하는 일은 그것 하나뿐이다** — 내부 Observer와 `Ref` - **`:Subscribe()`등록하는 것은 그것 하나뿐이다** — 내부 Observer와 `Ref`
콜백은 **생성자에서 이미 `Weak*`로 걸려 있다**(위 "확정 구조" 절). 콜백은 **생성자에서 이미 `Weak*`로 걸려 있다**(위 "확정 구조" 절).
`Subscribed = true`가 서는 순간 `canExecute(handle)`이 참이 되어 그 `Subscribed = true`가 서는 순간 `canExecute(handle)`이 참이 되어 그
경로들이 살아난다. 경로들이 살아난다. **[2026-08-27 `H-144`]** 등록 뒤 꼬리로 `_epochs:Refresh()`
+ 조건부 `Rerun`이 붙는다(위 의사코드) — **그 사이 dep이 안 변했으면**
no-op이다. 첫 구독이라도 생성과 `Subscribe()` 사이에 emit이 있었으면
(`.Subscribed`가 거짓이라 `fire`가 첫 가드에서 버린다) `Refresh()`가 참을
돌려 캐치업으로 `fn`이 다시 돈다 — 바람직한 동작이고, "첫 구독은 재실행
안 한다"를 테스트에 인코딩하지 말 것(**[2026-08-28 `/code-review`]**).
- **⚠️ 용도는 완전히 top-level(모듈/스크립트 레벨, 어떤 Instance - **⚠️ 용도는 완전히 top-level(모듈/스크립트 레벨, 어떤 Instance
생명주기에도 안 묶인) 사이드 이펙트로 한정할 것 — 특정 `inst` 생명주기에도 안 묶인) 사이드 이펙트로 한정할 것 — 특정 `inst`
묶인 경우엔 leaf 부착(`bindLifetime`)을 쓰지 `:Subscribe()`를 쓰지 묶인 경우엔 leaf 부착(`bindLifetime`)을 쓰지 `:Subscribe()`를 쓰지
@ -606,8 +726,8 @@ end
이 요구는 성립하지 않는다 — **그대로 구현하면 `nil`을 순회한다.** 이 요구는 성립하지 않는다 — **그대로 구현하면 `nil`을 순회한다.**
- **[2026-08-21 기준]** 남은 건 **구현 시 회귀 확인**뿐이고, 설계상 열린 - **[2026-08-21 기준]** 남은 건 **구현 시 회귀 확인**뿐이고, 설계상 열린
항목이 아니다. 항목이 아니다.
- **`:Subscribe()`한 핸들에서는 `:Unsubscribe()`가 Observer의 것을 위임한 뒤 - **`:Subscribe()`한 핸들에서는 `:Unsubscribe()`가 Observer와 같은 게이트·레지스트리
cleanup 하나를 덧붙인다 — Effect 계층에서 의미가 확장됨.** Observer의 조작을 한 뒤 cleanup 하나를 덧붙인다 — Effect 계층에서 의미가 확장됨.** Observer의
`:Unsubscribe()`는 "미래 재실행만 `:Unsubscribe()`는 "미래 재실행만
끊는다"(Observer 자체엔 정리할 상태가 없음)로 충분하지만, Effect의 끊는다"(Observer 자체엔 정리할 상태가 없음)로 충분하지만, Effect의
계약은 "생애주기가 끝나는 시점에 마지막 cleanup이 정확히 1회 호출된다" 계약은 "생애주기가 끝나는 시점에 마지막 cleanup이 정확히 1회 호출된다"
@ -705,10 +825,12 @@ Effect의 의존성이 될 방법이 아예 없다.** 사용자 제기: *"Effect
인자마다 다른 규칙을 위치로 기억해야 했다. 아무것도 안 넘기므로 그 질문 인자마다 다른 규칙을 위치로 기억해야 했다. 아무것도 안 넘기므로 그 질문
자체가 없어지고, dep 값은 사용자가 클로저로 직접 읽는다. 자체가 없어지고, dep 값은 사용자가 클로저로 직접 읽는다.
- `self`를 주는 덕에 `fn` 안에서 `self:Rerun()`/`self:Unsubscribe()` 같은 - `self`를 주는 덕에 `fn` 안에서 `self:Rerun()`/`self:Unsubscribe()` 같은
핸들 표면에 바로 닿는다. **⚠️ [2026-08-27 `/code-review high`, 판단 대기 핸들 표면에 바로 닿는다. **[2026-08-27 확정, 9라운드 `H-143`]** `fn`
`H-143`]** `fn``Unsubscribe`는 그 실행이 돌려준 cleanup을 `Rerun` `Unsubscribe`는 **지원 대상**이다("이 dep의 변경을 딱 한 번만 처리하고
그대로 저장해 **아무도 소진 못 하게** 만든다 — 처방(`Rerun` 꼬리 분기 / 끝나는 Effect") — 그 실행이 돌려준 cleanup을 `Rerun`이 그대로 저장하면
금지 / 문서화)은 `question.md` 최우선 절. 아무도 소진 못 하므로, `Rerun` 꼬리가 "이 실행 중에 죽었다"(`wasAlive and
not canExecute`)를 보면 **즉시 소진**하고 재요청도 버린다(위 `Rerun`
의사코드).
- **최소 1회는 실행된다 — React `useEffect`와 동일.** 아직 안 채워진 `Ref` - **최소 1회는 실행된다 — React `useEffect`와 동일.** 아직 안 채워진 `Ref`
섞여 있어도 그대로 돈다(사용자: *"최초 1회에서 어차피 if 로 확인해내게 섞여 있어도 그대로 돈다(사용자: *"최초 1회에서 어차피 if 로 확인해내게
될것이므로 괜찮음"*). "전부 채워질 때까지 대기"는 안 한다. 될것이므로 괜찮음"*). "전부 채워질 때까지 대기"는 안 한다.

View file

@ -354,8 +354,8 @@ function bindLifetime(inst, value)
-- `base/effect-plan.md`의 "확정 구조" 절이 소스. -- `base/effect-plan.md`의 "확정 구조" 절이 소스.
if isEffect(value) then if isEffect(value) then
value:_bindDestroying(inst) -- Destroying 연결 + **조건부 캐치업 1회** value:_bindDestroying(inst) -- Destroying 연결 + **조건부 캐치업 1회**
-- (`if not self._installed or -- (`local depsChanged = self._epochs:Refresh()` 먼저,
-- self._epochs:Refresh() then self:Rerun() end`) -- `if not self._installed or depsChanged then self:Rerun() end`)
-- 의사코드는 `base/effect-plan.md`가 소스. -- 의사코드는 `base/effect-plan.md`가 소스.
-- 그 안에서 주입 op `onDestroying(inst, fn)` -- 그 안에서 주입 op `onDestroying(inst, fn)`
-- 부른다(base는 Instance를 모른다). -- 부른다(base는 Instance를 모른다).
@ -444,10 +444,17 @@ end
`:WeakSubscribe()`가 생기기 전(2026-08-25 이전) 서술이라 **약한 쪽이 어디서 `:WeakSubscribe()`가 생기기 전(2026-08-25 이전) 서술이라 **약한 쪽이 어디서
`.Subscribed`를 세우는지가 통째로 빠져 있었다.** 아래가 **Observer의** `.Subscribed`를 세우는지가 통째로 빠져 있었다.** 아래가 **Observer의**
진입점 전량이고 소스다(사용자 원문 *"구현이 한 벌"*). **[2026-08-27 9라운드 진입점 전량이고 소스다(사용자 원문 *"구현이 한 벌"*). **[2026-08-27 9라운드
`H-127`]** `EffectHandle`도 같은 넷을 **그대로 재사용**한다(같은 레지스트리, `H-127`, 같은 날 (b)로 정정]** `EffectHandle`은 **같은 레지스트리 둘과 같은
같은 게이트) — `Unsubscribe`만 이 게이트를 통과한 *뒤에* cleanup 소진을 `canBound` 게이트를 쓰되 네 진입점 본문은 자기 것**이다 — 한때 "같은 넷을
덧붙이며, 그 의사코드는 `base/effect-plan.md`의 "`EffectHandle:Subscribe()`" 그대로 재사용(함수 배정)"으로 적었는데, 아래 `Subscribe`/`Unsubscribe`가
절이 소스: `self:WeakSubscribe()`/`self:WeakUnsubscribe()`로 **콜론 위임**하므로 그 함수를
`EffectHandle`에 배정하면 위임이 `EffectHandle`의 오버라이드로 가서(재구독 꼬리
두 번, 첫 번째는 강한 킵 전) 깨진다(감사 4라운드, `luau` 재현). **사용자
확정**: *"observer 랑 effect 랑 헤테로지니어스한 타입인데 … '하나의 무언가가 두
일을 동작하지 않는가에 유의하자'"* — Observer 본문은 Observer만 쓴다. `Effect`
쪽 넷(`Unsubscribe`는 cleanup 소진, `Subscribe`/`WeakSubscribe`는
**[2026-08-27 `H-144`]** 등록 끝에 `_epochs:Refresh()` + 조건부 `Rerun`)은
`base/effect-plan.md`의 "`EffectHandle:Subscribe()`" 절이 소스:
```lua ```lua
local Subscribed = {} -- 강한 레지스트리(살려두는 게 목적) local Subscribed = {} -- 강한 레지스트리(살려두는 게 목적)
@ -458,7 +465,8 @@ function Observer:WeakSubscribe()
if not canBound(self) then -- bindLifetime과 정확히 같은 게이트(같은 isBoundAlive 공유) if not canBound(self) then -- bindLifetime과 정확히 같은 게이트(같은 isBoundAlive 공유)
error(if self.Subscribed error(if self.Subscribed
then "이미 구독된 값" -- 강/약 어느 쪽이든 이 분기 then "이미 구독된 값" -- 강/약 어느 쪽이든 이 분기
else "이미 Instance에 바인딩된 값") else "이미 Instance에 바인딩된 값", 2) -- [2026-08-27] `level 2` — 아래 둘과 같게
-- (자리표시자 문구, 실제 메시지는 영어)
end end
self.Subscribed = true -- ⭐ [H-111] 약한 쪽도 세운다 — 구독 경로 공용 플래그 self.Subscribed = true -- ⭐ [H-111] 약한 쪽도 세운다 — 구독 경로 공용 플래그
WeakSubscribed[self] = true WeakSubscribed[self] = true

View file

@ -440,8 +440,9 @@ Instance를 직접 받으므로 — `base/dispatch-core-plan.md` "확정된 디
`Source`/`Ref`는 `:Sync`, `State``:TrackFrom`, `base/state-epoch-plan.md` `Source`/`Ref`는 `:Sync`, `State``:TrackFrom`, `base/state-epoch-plan.md`
§4·§8. 균일해지는 건 **판정 쪽**이다). §4·§8. 균일해지는 건 **판정 쪽**이다).
그래서 포탈 재마운트의 캐치업이 dep 종류에 따라 갈리던 것(`H-64`)이 그래서 포탈 재마운트의 캐치업이 dep 종류에 따라 갈리던 것(`H-64`)이
**대칭**이 되고, 판정이 `if self._epochs:Refresh() then self:Rerun() end` **대칭**이 되고, 판정이 `local depsChanged = self._epochs:Refresh()` +
한 줄이 된다. `if not self._installed or depsChanged then self:Rerun() end` 두 줄이 된다
(`Refresh()`는 항상 먼저 — `base/effect-plan.md``_bindDestroying` 캐비엇).
- 같은 `Ref`를 deps에 두 번 넣어도 `EpochMap`이 키로 dedup하므로 - 같은 `Ref`를 deps에 두 번 넣어도 `EpochMap`이 키로 dedup하므로
**공짜로** 처리된다(`H-70`) — 옛 `_refCallbacks[ref] = cb` 덮어쓰기로 **공짜로** 처리된다(`H-70`) — 옛 `_refCallbacks[ref] = cb` 덮어쓰기로
먼저 건 클로저가 `.Callbacks`에 남던 버그도 같이 사라진다. 먼저 건 클로저가 `.Callbacks`에 남던 버그도 같이 사라진다.

View file

@ -232,8 +232,9 @@ Slot에 들어간 요소는 **ownership이 귀속**되며 다른 곳에 마운
`isMounted`를 관리해서, **한 인스턴스에 대한 다중 마운팅이 라이브러리 차원에서 `isMounted`를 관리해서, **한 인스턴스에 대한 다중 마운팅이 라이브러리 차원에서
절대 일어나지 않도록 강제**하는 게 v1 대비 핵심 디자인 변화. v1의 `mount()` 절대 일어나지 않도록 강제**하는 게 v1 대비 핵심 디자인 변화. v1의 `mount()`
별다른 강제를 안 했지만(`reference/quad-v1-architecture.md`의 mount.lua 분석 참고 — 별다른 강제를 안 했지만(`reference/quad-v1-architecture.md`의 mount.lua 분석 참고 —
실제로는 부모/자식 부기까지 했지만 다중 마운트 방지는 없었음), v2는 mount 실제로는 부모/자식 부기까지 했지만 다중 마운트 방지는 없었음), v2는 **Slot의
함수 자체가 이 강제를 담당. 마운트 경로 자체**(`attachSlot` 분해분 — 별도 `Mount` 함수가 아니다, **[2026-08-27
`H-146`]** v2에 v1식 `Mount(parent, tree)` 표면은 없다)가 이 강제를 담당.
Fusion의 `Children` SpecialKey는 이걸 "특정 SpecialKey 하나의 내부 부기"로만 Fusion의 `Children` SpecialKey는 이걸 "특정 SpecialKey 하나의 내부 부기"로만
구현했고(재사용 가능한 1급 프리미티브가 아님), Vide는 아예 이 개념이 없어서 구현했고(재사용 가능한 1급 프리미티브가 아님), Vide는 아예 이 개념이 없어서
@ -2143,7 +2144,10 @@ UI에 직접 관측, (2) `Dispatch.setLength(inst, i, slot.Length)`가 형제
정확히 호출하는 유일한 정당 경로라, 이걸 우회해서(예: 외부 코드가 Slot이 정확히 호출하는 유일한 정당 경로라, 이걸 우회해서(예: 외부 코드가 Slot이
마운트해둔 부모 Instance에 직접 `.Parent = parentInst`로 자식을 끼워 마운트해둔 부모 Instance에 직접 `.Parent = parentInst`로 자식을 끼워
넣는 것) 자식을 추가/제거하면 `Length`/형제 순서 계산이 그 변화를 몰라 넣는 것) 자식을 추가/제거하면 `Length`/형제 순서 계산이 그 변화를 몰라
조용히 어긋남 — 별도 방어 로직 없음, 문서 경고로만 남김. 조용히 어긋남 — 별도 방어 로직 없음, 문서 경고로만 남김. **[2026-08-27 확정,
9라운드 `H-146`] 이 금지의 범위는 *quad가 관리하는 자식 자리*다 — quad 트리의
루트를 quad 밖 부모(`PlayerGui` 등)에 붙이는 `.Parent =`는 여기 해당하지 않고
사용자가 밖에서 한다**(`base/bind-system-plan.md`의 `H-142` 항목).
## `Slot:Single(state, updateFn?, opts?)` — 확정 (2026-08-11 세션, `:List` 위의 순수 sugar) ## `Slot:Single(state, updateFn?, opts?)` — 확정 (2026-08-11 세션, `:List` 위의 순수 sugar)

View file

@ -1570,9 +1570,12 @@ Observer가 **안 묶인 것으로 오판**됨"은 실제로는 안 일어남
`unbindLifetime`이 leaf 해제의 실제 통로). `unbindLifetime`이 leaf 해제의 실제 통로).
- **Effect도 동일 규칙 적용(사용자 확인)** — Effect가 `state` 인자로 - **Effect도 동일 규칙 적용(사용자 확인)** — Effect가 `state` 인자로
내부적으로 Observer를 조합하는 경우든, `state` 없는 경우든 같은 내부적으로 Observer를 조합하는 경우든, `state` 없는 경우든 같은
`canBound` 게이트를 그대로 재사용(`base/effect-plan.md`) — Effect `canBound` 게이트를 그대로 재사용(`base/effect-plan.md`) — **[2026-08-28
자신이 아니라 내부 Observer가 게이트를 갖고 있어서, Effect 구현이 정정]** 여기 한때 "Effect 자신이 아니라 내부 Observer가 게이트를 갖고 있어서
이 정정을 몰라도 자동으로 커버됨. 이전에 그 문서에 적어뒀던 "leaf 자동으로 커버됨"이라 적혀 있었는데 틀렸다: 내부 Observer는 생성자에서
`Weak*`로만 걸리고 발화는 `canExecute(handle)`이 막으므로(`H-58`/`H-59`) 그
게이트는 핸들의 이중 바인드를 막지 못한다. `EffectHandle`이 **자기 네
진입점에서 `canBound(self)`를 직접 돈다**(`H-144` (b)). 이전에 그 문서에 적어뒀던 "leaf
부착과 `:Subscribe()`를 동시에 쓰는 것도 안전"이라는 서술은 **이 부착과 `:Subscribe()`를 동시에 쓰는 것도 안전"이라는 서술은 **이
규칙으로 대체(정정)** — 안전하게 지원하는 게 아니라 애초에 막아야 규칙으로 대체(정정)** — 안전하게 지원하는 게 아니라 애초에 막아야
하는 조합이었음. 하는 조합이었음.

View file

@ -29,6 +29,26 @@ haiku, 일반 작업은 sonnet. 특히 소스코드를 많이 읽어야 하는
확인됨. 사용자가 세션 중 구두로 말한 게 옮겨적히지 않았을 가능성이 크다는 확인됨. 사용자가 세션 중 구두로 말한 게 옮겨적히지 않았을 가능성이 크다는
사용자 본인 추정에 따라 여기 정식 관례로 승격(선택지 (a) 채택). 이 누락이 사용자 본인 추정에 따라 여기 정식 관례로 승격(선택지 (a) 채택). 이 누락이
아래 "사용자 발언을 인용할 때" 관례가 생긴 계기이기도 함. 아래 "사용자 발언을 인용할 때" 관례가 생긴 계기이기도 함.
- **⭐ [2026-08-27 명문화] 하나의 무언가가 두 일을 하고 있지 않은지 유의한다 —
이질적인 타입끼리 구현 본문을 공유하지 않는다.** 사용자 원문: *"항상
언급되는 말이지만, '하나의 무언가가 두 일을 동작하지 않는가에 유의하자'"*
(`session/2026-08-27-03-handtrace-round9-h143-h146.md`). **첫 사례는 하루
전의 `invalidAfter`다** — 사용자 확인(2026-08-27): *"그게 정확히 invalid after
문제에서 났던 이야기였어"*. 한 필드가 "캐시가 여기까지 유효"와 "여기까지
`:Set`을 마쳤다"라는 **다른 목적의 값을 같이 쥐고** 있어서 `getOffsetAt`
꼬리가 splice의 되감기 신호를 지웠고(사용자 진단 *"Set을 해줬느냐와 캐시가
유효하지 않느냐라는 다른 목적의 값을 같은 값이 쥐고 있음"*),
`offsetCacheValidUpTo`/`offsetSetUpTo` 둘로 갈라졌다(`base/dispatch-core-plan.md`의
"두 필드" 절, `session/2026-08-26-01-handtrace-round8-resolution.md`가 *"두
뜻이 실제로 같다는 확정은 의심할 것"*으로 남긴 교훈). 이번 계기: `Observer`
네 진입점 함수 객체를 `EffectHandle` 메소드 테이블에 그대로 배정해 두 타입이
한 본문을 쓰게 했더니, 그 본문의 콜론 위임(`self:WeakSubscribe()`)이
`EffectHandle`의 오버라이드로 가서 재구독 꼬리가 두 번 돌고 강한 킵 전에
`Rerun`하는 결함이 났다(감사 4라운드, `base/effect-plan.md`
`EffectHandle` 블록). 공유해도 되는 건 **데이터**(레지스트리)와 **순수
술어**(`canBound`)이고, 타입별로 다른 후처리가 붙는 **본문**은 각자 가진다.
위 첫 원칙과 짝 — "구조를 늘리지 않는다"가 "본문을 억지로 합친다"로
읽히면 안 된다.
## 문서 표기 규약 ## 문서 표기 규약

View file

@ -27,7 +27,7 @@ M3=반응형)다. 그 교체의 부작용으로 한때 `question.md` 최우선
(소스는 `qa-request/pre-implementation-handtrace-round8-followup.md`), (소스는 `qa-request/pre-implementation-handtrace-round8-followup.md`),
`question.md` 최우선 절은 다시 비어 있다. **[2026-08-27] 9라운드**(그 커밋 `question.md` 최우선 절은 다시 비어 있다. **[2026-08-27] 9라운드**(그 커밋
`9dd8213`의 델타 재트레이싱, 발견 `H-124`~`H-141`)는 **Q1~Q3가 `base/` `9dd8213`의 델타 재트레이싱, 발견 `H-124`~`H-141`)는 **Q1~Q3가 `base/`
반영됐고**, 같은 날 Q4~Q10·`H-138`·`H-139`·`H-142`까지 **전량 반영** — 소스는 `-round9-followup.md`. 그 반영분에 `/code-review high`가 낸 **새 메커니즘 넷(`H-143`~`H-146`)이 `question.md` 최우선 절에 판단 대기**(M2 `Effect` 둘·M3·M5 하나씩). 게이트는 여전히 0. 저장소 루트에 반영됐고**, 같은 날 Q4~Q10·`H-138`·`H-139`·`H-142`까지 **전량 반영** — 소스는 `-round9-followup.md`. 그 반영분에 `/code-review high`가 낸 새 메커니즘 넷(`H-143`~`H-146`)도 **같은 날 `session/2026-08-27-03-handtrace-round9-h143-h146.md`에서 전부 권고 (a)로 확정·반영**돼 그 반영분의 `/code-review`가 낸 셋(`H-147`~`H-149`)은 **[2026-08-28] 10라운드 문항지**(`-round10.md`, 광범위 탐사 결과와 함께 배치 회신 대기)로. 게이트 0. 저장소 루트에
`quad-base/src/`(`New()`/`RunInit`/`AddPlugin`/`Relate`/`Debug`)/ `quad-base/src/`(`New()`/`RunInit`/`AddPlugin`/`Relate`/`Debug`)/
`quad-types/src/`/`type-version-check/src/`가 실제로 존재(`quad-roblox/src`는 `quad-types/src/`/`type-version-check/src/`가 실제로 존재(`quad-roblox/src`는
아직 빈 폴더 — M5에서 채워짐), 자세한 진행 상황은 루트 `ROADMAP.md` 아직 빈 폴더 — M5에서 채워짐), 자세한 진행 상황은 루트 `ROADMAP.md`

View file

@ -0,0 +1,169 @@
# 구현 전 손 트레이싱 **10라운드** 감사 지시서
> **이 파일이 무엇인가**: 10라운드 탐사자에게 그대로 주는 지시서다. 산출물은
> `pre-implementation-handtrace-round10.md`(§6). **[2026-08-28 신설]** 9라운드
> 지시서(`-round9-brief.md`)와 같은 관례.
>
> **왜 10라운드인가**: 9라운드 후속(`H-143`~`H-146`, 2026-08-27)을 반영하고
> 감사 8라운드 + `/code-review high`를 돌렸더니 **판단이 필요한 것 셋**이 또
> 나왔다(`-round10.md` §4의 `H-147`~`H-149`). 사용자 판단(2026-08-28): *"인간을
> 기다리는거 엄청 비효율이라서 … 늘 해오던것 처럼 batch 로 처리될 필요가 있는듯.
> 지금 결정해야할 분 10 으로 올리자. 커밋하고 정리한 다음 깔끔한 컨텍스트의
> fable 탐사자 하나 띄워서 광범위한 핸드트레이싱, 테스팅, 문제 검사를
> 하도록 해줘."* — 즉 이 라운드는 **한 번에 넓게 훑어 사용자가 한 자리에서
> 전부 결정할 문항지**를 만드는 것이 목적이다. 발견을 하나씩 물으러 오지
> 말 것.
당신은 Roblox 엔진용 DOMless UI 렌더러 **quad**의 설계 코퍼스를 감사한다.
저장소 루트가 작업 디렉토리다. **구현(M2 = 반응형 코어) 착수 직전**이고,
여기서 놓친 설계 결함은 구현 한참 뒤에 터져 M2/M3를 다시 짜는 비용이 된다.
당신은 신선한 컨텍스트에서 시작한다 — 앞선 세션의 가정을 물려받지 않는 것이
당신의 가치다.
---
## §0 먼저 읽을 것 (이 순서로)
1. `CLAUDE.md``.claude/conventions.md` / `.claude/project-context.md` /
`.claude/todos.md`. **특히 `conventions.md`의 두 규칙**: (1) "리뷰·감사가
내놓는 새 필드·인자·이름·메커니즘은 발견이지 결정이 아니다"(2026-08-27),
(2) "하나의 무언가가 두 일을 하고 있지 않은지 유의한다"(2026-08-27).
2. `.claude/qa-request/pre-implementation-handtrace-round9.md` — 9라운드 발견
원문(`H-124`~`H-146`). §5(이상 없음)·§6(남은 의심)은 다시 파지 않아도 되는
자리와 파야 할 자리다.
3. `.claude/qa-request/pre-implementation-handtrace-round9-followup.md`
**이 라운드의 출발점.** 9라운드 결정 전량과, `H-143`~`H-146` 반영 뒤의 감사
8라운드 표·`/code-review high` 기록. 거기 적힌 **실패 모드**(반영분 자체가
결함을 만든다 / 판정식을 단순화하다 생성자 케이스를 죽인다 / 함수 본문
공유 + 콜론 위임)가 당신의 사냥 목록이다(§3).
4. `.claude/qa-request/pre-implementation-handtrace-round10.md` — **이미 `H-147`
~`H-149`가 들어 있다.** 당신의 발견은 `H-150`부터 이어 매긴다. 그 셋을
재검토해도 좋다(전제가 틀렸으면 그렇게 적을 것).
5. `ROADMAP.md`(M2/M3 체크리스트), `.claude/question.md`,
`.claude/luau-test/STATUS.md`, `.claude/audit/handtrace-round7-reference-impl/README.md`.
6. `base/`는 **전량 완독**을 요구한다 — 이 라운드는 "델타"가 아니라 "광범위"가
전제다(사용자 요청). 먼저 `base/architecture.md`.
---
## §1 이 라운드의 전제
- **스코프는 광범위다.** 9라운드까지의 델타 스코프와 달리 M2~M8 전 표면을 본다.
다만 우선순위는 §2대로 — M2/M3가 먼저다.
- **한 번에 문항지를 만든다.** 사용자는 다음 세션에 이 파일 하나를 열어 §4의
표를 위에서 아래로 읽고 갈래만 골라 회신한다. 그러므로 모든 발견은
**갈래 + 권고 + 권고 근거**를 갖춰야 하고, 판단이 필요 없는 것(오타·stale·
개수)은 🟢로 분리해 §4에 넣지 않는다.
- **새 메커니즘은 승인이 아니다.** 당신이 처방으로 제안하는 필드·인자·이름·
표면은 전부 "권고"다. `base/`에 이미 기각된 모양인지(`archive/`, 각 문서의
"검토 후 안 만들기로 한 것"류 목록) 먼저 grep하고, 기각된 것을 다시 제안할
땐 그 사실을 적을 것.
- **실측이 가능한 주장은 실측한다.** 로컬에 `luau`/`luau-analyze`가 있다.
테스트는 반드시 `./scripts/test.sh`로(`luau` CLI가 심볼릭 링크를 못 타서
그냥 돌리면 거짓 클린이 난다 — `project-context.md`). 참조 구현
`audit/handtrace-round7-reference-impl/spikes/`는 **7라운드 시점 계약**이라
지금 계약(`H-107`~`H-149`)과 다르다 — 갱신해 돌리는 것이 레인 C다.
---
## §2 레인 — 우선순위 A > C > B > D
### 레인 A (최우선) — M2 반응형 코어 전체를 지금 계약으로 손 트레이싱
`base/source-state-plan.md` / `store-plan.md` / `state-epoch-plan.md` /
`effect-plan.md` / `gate-plan.md` / `blocker-plan.md` / `lifecycle-pattern.md` /
`ref-plan.md`(최소형) / `relate-plan.md` / `epoch-brand` 관련. 특히 **2026-08-27에
바뀐 것끼리 겹쳐서**:
- `EffectHandle` 네 진입점 자기 것(`H-144` (b)) × `Rerun` 꼬리 `wasAlive and
not canExecute`(`H-143`) × `_bindDestroying` 캐치업 × 생성자 `_blocker` ×
`fire` 가드 순서. 상태 기계 전체를 표로 그려 **모든 진입점 × 모든 상태**에서
`_installed`/`_cleanup`/`.Subscribed`/레지스트리 전이를 확인. `H-147`
"이미 죽은 핸들에서 `Rerun`"도 그 표 안에서 다시 보라.
- `state:Gate` 유보 × Effect 재구독 × `EpochMap:Refresh``H-144` 재트레이싱이
한 케이스만 봤다. 다이아몬드·게이트 배치·`Peek`/`Update` 조합을 넓혀라.
- Store 명시적 초기화 × `Of<<T>>` × `CheckReservedKeys<keyof<T>>` — 타입을
`luau-analyze`에 실제로 걸어라(레인 D와 겹침).
- `Source:Set` 동일값 emit(`H-68`) × `Blocker` × `_hold` 불변식 × 중간 State GC.
### 레인 C — 참조 구현을 지금 계약으로 갱신해 실제로 돌리기
`audit/handtrace-round7-reference-impl/spikes/`를 복사해
`audit/handtrace-round10-reference-impl/`를 만들고(원본은 건드리지 말 것 —
실측 시점 기록), `H-107`~`H-149`의 계약으로 갱신한 뒤 `luau`로 돌린다.
최소 목표: Effect 네 진입점 + `Rerun` 꼬리 + 재구독 + 게이트 유보 케이스,
`bk.indexOfElement` weak-key(Instance 키를 흉내낼 수 없으면 테이블 키로 —
그 한계를 적을 것), `reconcile` 배치 Blocker, `recompute` 되감기(`H-124`).
문서와 다르게 도는 자리가 발견이다. `README.md`에 무엇을 어떻게 갱신했는지
남길 것.
### 레인 B — M3 디스패치 / Slot 값 단위 트레이싱
`base/dispatch-core-plan.md` / `slot-plan.md` / `bind-system-plan.md`(`New`·
`drive` 파이프라인) / `handler-*`. 9라운드 레인 B의 연장 — `InstanceChildHandler`
부기, 숏핸드 우선순위, `H-142` `Parent` 금지와 `H-146` 루트 예외, `H-148`
에러 문구 자리, 최상위 Slot 교체 × weak-key, 포탈/`Detach`/`KeyGone` 조합.
### 레인 D — 타입 계약과 실제 커밋된 M1 코드
`quad-base/src/`·`quad-types/src/`·`type-version-check/src/`를 `./scripts/test.sh`
돌리고, `base/typing-limits.md`·`quad-types-plan.md`·`project-setup-plan.md`가
주장하는 것 중 실측 가능한 것을 `luau-analyze`로 확인. 확정 시그니처가 확정
관용구를 통과시키는가(7라운드 5차 패스의 재현, 지금 계약으로).
---
## §3 사냥 목록 — 이 코퍼스에서 **반복 관측된** 실패 모드
1. **반영분이 새 결함을 만든다** — 어제 하루에만 둘(`not canExecute` 판정식이
생성자 케이스를 죽임 / 함수 본문 공유 + 콜론 위임). 2026-08-27 diff
(`git log -p 7f58683..HEAD`)를 특히 의심하라.
2. **"두 뜻이 같다"는 확정** — `invalidAfter` 두 필드 분리의 교훈. 한 필드·한
함수·한 플래그가 두 목적을 쥐고 있는 자리.
3. **판정식 단순화가 경계 케이스(생성자·최초 설치·빈 배열·0번째 자리)를
죽인다.**
4. **의사코드 산문의 순서가 코드 순서와 다르다**(`H-127`형).
5. **인용문 옆에 인용이 승인하지 않은 메커니즘**(`H-141` `token`형).
6. **정정 배너는 달렸는데 본문 bullet·제목·색인이 옛 서술**(`H-130`형).
7. **개수·목록·상태를 두 곳에 값으로 적어 갈라진 것.**
8. **실측했다고 적혔는데 재작성 뒤 재실행 안 한 것.**
---
## §4 각도 (이전 라운드와 같은 문자 — 상호참조용)
A 겹침(결정 둘 이상이 만나는 자리) · B M5+ 값 단위 · C 참조 구현 실행 ·
D ROADMAP 시뮬레이션(체크박스 순서대로 짜면 막히는 자리) · E fail-fast 3종 ·
F 개수/목록 · G Luau 언어 사실 재확인 · H 타입.
---
## §5 규칙 (엄수)
- **`base/`·`research/`·`reference/`·`archive/`·인덱스 레이어를 고치지 말 것.**
당신의 출력은 `-round10.md``audit/handtrace-round10-reference-impl/`뿐이다.
(오타·stale은 발견으로 적되 고치지 않는다.)
- **`git stash`·`git commit`·`git checkout` 금지.** HEAD 대조는 `git show
HEAD:<경로>`/`git diff HEAD -- <경로>`.
- 발견 번호는 `H-150`부터 연속. 심각도 🔴(그대로 구현하면 크래시/누수/침묵)
🟡(계약 위반·불일치, 판단 필요) 🟢(정정만).
- 절 인용은 `conventions.md`의 "절 인용 규약"대로 원문에서 잘라 쓸 것 —
`python3 .claude/tools/doc-check.py`를 마지막에 돌려 **당신 파일이 ERROR를
만들지 않는지** 확인(기존 WARN 59건은 무시).
- 사용자 원문을 인용할 땐 `followup`/`session/`에 있는 것만, 자르지 말 것.
- 사용자에게 질문하지 말 것 — 당신은 답을 받을 수 없다. 갈래를 적고 넘어간다.
- 토큰·시간 제한은 없다고 생각하되, **레인 A와 C는 반드시 끝내고** B·D는 남은
만큼. 못 본 것은 §6에 정직하게.
---
## §6 산출물
`pre-implementation-handtrace-round10.md`에 이어 쓴다(이미 §0~§4 골격과
`H-147`~`H-149`가 있다):
- **요약 표**(번호·심각도·한 줄·주 대상·성격·실측) — 기존 표에 행 추가.
- **상세** — 발견마다 트레이스(실제 값으로), 왜 새 메커니즘인지 여부, 갈래,
권고, 권고 근거, 채택 시 고칠 자리.
- **§4 사용자 결정 표** — 판단 필요 항목만, 갈래를 한 셀에. 기존 `H-147`~`H-149`
행 아래에 이어서.
- **§5 이상 없다고 확인한 것** / **§6 남은 의심·못 본 것** / **§7 레인 C 실행
기록**(어떤 스파이크를 어떻게 갱신했고 결과가 뭔지, 파일 경로).

View file

@ -0,0 +1,99 @@
# 구현 전 손 트레이싱 **10라운드** — 발견 보고 + 배치 결정 문항지
> **이 파일이 무엇인가**: **[2026-08-28 신설]** 9라운드 후속(`H-143`~`H-146`)
> 반영 뒤 `/code-review high`가 낸 판단 대기 셋(`H-147`~`H-149`)을 씨앗으로,
> 신선한 탐사자가 **M2~M8 전 표면을 광범위하게** 손 트레이싱·실측한 결과를
> 여기 이어 쓴다(`H-150`~). 지시서는 `-round10-brief.md`. **사용자는 §4 표를
> 위에서 아래로 읽고 갈래만 회신한다**(배치). 결정이 나면 `-round10-followup.md`
> 새로 만들고 `base/`에 반영한다 — **이 파일은 발견 당시의 기록**이라 각 항목의
> "갈래"는 선택 전 목록이니 반영 뒤엔 그대로 믿지 말 것.
>
> 상태: **[2026-08-28 기준] 탐사 진행 중 / 회신 대기.** `H-147`~`H-149`는
> `H-143`~`H-146` 반영분에 `/code-review high`가 낸 10건 중 새 메커니즘·기존
> 결정 변경이라 문항으로 올린 셋(나머지 일곱은 반영 — `-round9-followup.md`
> 마지막 code-review 절).
## 요약 표
| 번호 | 심각도 | 한 줄 | 주 대상 | 성격 | 실측 |
|---|---|---|---|---|---|
| `H-147` | 🟡 | `Rerun` 꼬리 `wasAlive`가 루프 머리 `_consumeCleanup()` **뒤**에 잡히고 공개 `Rerun()`에 게이트가 없어 **진입 시점에 이미 죽은 핸들**(직전 cleanup이 `self:Unsubscribe()` / 해제 뒤 타이머의 `self:Rerun()`)은 else 분기 → 고아 cleanup + `_installed = true` | `effect-plan.md` `Rerun` | `/code-review` 발견, 처방은 규칙 또는 새 플래그 | — |
| `H-148` | 🟡 | `H-146`의 "`Parent` 거부는 **전용 문구**"가 새 메커니즘 — `isHandlable` 거부는 `Dispatch.process`**일반** 매치 실패 문구로 떨어지고 그 자리에 특수 분기는 두지 않기로 확정돼 있다. 사용자 회신에 문구 언급 없음(에이전트 선택이었음) | `bind-system-plan.md` `H-142` 항목, `ROADMAP.md` M5 | `/code-review` 발견, 처방은 새 핸들러 또는 철회 | — |
| `H-149` | 🟡 | `Observer:Subscribe``self:WeakSubscribe()`로 위임하므로 거기 붙인 `error(…, 2)``o:Subscribe()` 경로에선 사용자 호출부가 아니라 `Observer:Subscribe` 본문을 가리킴(`H-104` level 계약 위반). `EffectHandle`은 (b)로 인라인해 문제 없음 | `lifecycle-pattern.md` Observer 네 진입점 | `/code-review` 발견, 처방은 기존 결정("위임하므로 게이트 한 번") 변경 | — |
## 상세
### `H-147` 🟡 — 이미 죽은 핸들에서 `Rerun()`이 불리면 (`H-143` 잔여)
`effect-plan.md` `Rerun`: 루프 머리 `_consumeCleanup()` → `local wasAlive =
canExecute(self)` → `fn``if wasAlive and not canExecute(self)`. "실행 중에
죽은" 건 잡지만 **"진입 시점에 이미 죽은"** 건 못 잡는다:
1. 직전 cleanup이 `self:Unsubscribe()`를 부름 → 루프 머리 소진 안에서 죽음 →
`wasAlive` 거짓 → `fn` 실행 → else → 저장 + `_installed = true`. 고아 cleanup,
이후 재`Subscribe`의 `resubscribeTail``_installed` 참을 보고 재설치 생략.
2. `fn``task.delay(1, function() self:Rerun() end)`를 걸고 사용자가 그 전에
`Unsubscribe()`.
생성자 최초 `Rerun`("한 번도 산 적 없음")과 구분할 상태가 없다.
**갈래**: (a) "죽은 핸들에서의 `Rerun()`은 사용자 책임(UB)"으로 문서화 — 새
상태 없음; (1)은 "cleanup 안에서 자기 해제"를 지원 목록에서 빼는 것, (2)는
`Unsubscribe`가 도는 cleanup이 그 예약을 취소해야 하는 것(그게 cleanup의 일).
기존 "error 시 UB / 수렴 책임은 `fn`" 계약과 같은 결 / (b) `_everAlive` 플래그
하나(첫 `canExecute` 참 시점에 세움) → `Rerun` 진입에서 `everAlive and not
canExecute → return`. 필드 하나, 생성자 케이스도 가림 / (c) `wasAlive`
`_consumeCleanup()` **앞**에서 잡기 — (1)만 닫히고 (2)는 그대로.
**권고 (a)** — 두 시나리오 다 "죽은 뒤에도 자기를 부르는 코드"고, `Unsubscribe`
cleanup을 도는 이유가 바로 그런 예약을 거두라는 것.
### `H-148` 🟡 — `H-146`의 "전용 에러 문구"는 새 메커니즘이었다
`PropertyHandler.isHandlable``"Parent"`를 거부하면 나오는 건 `Dispatch.process`
**일반** 매치 실패 문구(*"no handler … check provider"*)이고, 그 자리에 특수
분기는 두지 않기로 확정돼 있다(`dispatch-core-plan.md`의 매치 실패 절). 전용
문구를 내려면 새 자리가 필요하다. 사용자 `H-146` 회신엔 문구 언급이 없었고
에이전트가 "배선 세부"라며 붙였다 — `conventions.md` 2026-08-27 규칙 위반.
**갈래**: (a) 전용 문구 **철회** — 일반 매치 실패 error 그대로, 오해는 사용자
문서("`Parent`는 props에 못 쓴다, 루트는 밖에서")로 흡수. 새 것 0 / (b) **가드
핸들러** — `k == "Parent"`에 매치해 `process`에서 전용 문구로 error. Observer/
Effect의 "동적 경로 가드"(`effect-plan.md`)와 같은 모양이라 새 *종류*는 아니나
핸들러 하나 + 숏핸드보다 위 우선순위 필요 / (c) `Dispatch.process` 매치 실패
문구에 키 이름을 싣기(일반 개선이지만 별도 결정).
**권고 (a)** — 원래 회신의 취지(새 API 없음, 최종 사용자 몫)와 같은 결. M5에서
실제로 밟아 헷갈리면 (b)를 그때 얹는 게 싸다.
### `H-149` 🟡 — `Observer:Subscribe`의 위임 때문에 `level 2`가 quad 내부 줄을 가리킨다
2026-08-27에 `Observer:WeakSubscribe``error``, 2`를 붙였는데(`H-104`
계약), `Subscribe``self:WeakSubscribe()`로 위임하므로 `o:Subscribe()` 경로의
level 2는 **`Observer:Subscribe` 본문**을 가리킨다. 가장 흔한 오용(leaf 바인딩된
Observer에 `o:Subscribe()`)이 quad 내부 줄을 블레임한다. `EffectHandle` 쪽은 (b)로
게이트를 인라인해 문제 없음.
**갈래**: (a) Observer도 `Subscribe`에 게이트·등록 세 줄을 **인라인** — Effect와
같은 모양, level 2 정확. "게이트는 한 번만 돈다 — `Subscribe``WeakSubscribe`
위임"이라는 기존 문장이 "각자 한 번"으로 바뀜. 중복 세 줄은 같은 타입 안이라
dot 호출 로컬 헬퍼로 빼도 됨(가상 디스패치 없음) / (b) 내부 헬퍼
`weakSubscribe(self, level)`로 위임하고 `Subscribe``level 3` — 코퍼스에
`level 3` 선례 없음 / (c) 그대로 두고 `Subscribe` 경로만 블레임이 한 단계 안쪽임을
문서화.
**권고 (a)** — "본문을 섞지 않는다"(2026-08-27 원칙)와 결이 같다.
## §4 ⭐ 사용자 결정이 필요한 것 (배치 회신용)
| 문항 | 무엇 | 선택지 | 권고 |
|---|---|---|---|
| **`H-147`** | 죽은 핸들에서 `Rerun()` | (a) UB로 문서화(cleanup 안 자기 해제는 지원 목록 밖, 타이머 취소는 cleanup의 일) / (b) `_everAlive` 플래그 + 진입 게이트 / (c) `wasAlive`를 소진 앞에서 | **(a)** — 새 상태 없이 기존 UB 계약과 같은 결 |
| **`H-148`** | `Parent` 거부 문구 | (a) 전용 문구 철회, 일반 매치 실패 그대로 / (b) `Parent` 가드 핸들러(동적 경로 가드형) / (c) 일반 문구에 키 이름 | **(a)** — 회신 취지(새 API 없음) 그대로, 필요하면 M5에서 (b) |
| **`H-149`** | Observer `Subscribe` 위임과 `level 2` | (a) `Subscribe`에 게이트·등록 인라인(Effect와 동형) / (b) `level 3` 위임 / (c) 문서화만 | **(a)** — 본문 안 섞기 원칙과 동형 |
(탐사자의 문항은 아래에 이어 붙는다.)
## §5 이상 없다고 확인한 것
(탐사자가 채운다.)
## §6 남은 의심 / 못 본 것
(탐사자가 채운다.)
## §7 레인 C 실행 기록
(탐사자가 채운다 — `audit/handtrace-round10-reference-impl/`.)

View file

@ -31,14 +31,14 @@
| — | `H-139` `New`/`D` 파이프라인 | ✅ **의사코드 신설** — 쓰면서 `H-142` 발견 | | — | `H-139` `New`/`D` 파이프라인 | ✅ **의사코드 신설** — 쓰면서 `H-142` 발견 |
| — | `H-132`/`H-137`/`H-140` | ✅ Q1~Q3 처리 때 닫힘 | | — | `H-132`/`H-137`/`H-140` | ✅ Q1~Q3 처리 때 닫힘 |
| — | `H-142` 해시 파트 `Parent` 순서 | ✅ **확정·반영** — props에 `Parent` 금지(순서 문제 소멸) | | — | `H-142` 해시 파트 `Parent` 순서 | ✅ **확정·반영** — props에 `Parent` 금지(순서 문제 소멸) |
| — | `H-143`~`H-146` (`/code-review high` 발견 중 새 메커니즘 넷) | **판단 대기**`fn``Unsubscribe` 고아 cleanup / 재`Subscribe` 재설치 / `indexOfElement` 잔존 / 루트 마운트 경로 | | — | `H-143`~`H-146` (`/code-review high` 발견 중 새 메커니즘 넷) | **확정·반영 (전부 권고 (a))**`Rerun` 꼬리 실행 중 사망이면 즉시 소진(`wasAlive`) / 재구독 꼬리(`Refresh` 먼저; 감사 4라운드로 진입점 소유권 (b) — `EffectHandle` 자기 것 — 추가 확정) / `indexOfElement` weak-key / 루트는 금지 범위 밖 + 전용 문구 |
**[2026-08-27] Q1~Q3는 `base/`·`ROADMAP.md`에 반영했다** — 반영 중 드러난 것과 **[2026-08-27] Q1~Q3는 `base/`·`ROADMAP.md`에 반영했다** — 반영 중 드러난 것과
열어둔 확인은 아래 "반영 기록" 절. **같은 날 이어서 Q4~Q8·Q10·`H-138`·`H-139`를 열어둔 확인은 아래 "반영 기록" 절. **같은 날 이어서 Q4~Q8·Q10·`H-138`·`H-139`를
확정·반영했다**(아래 Q4 이하 절). **[같은 날 마지막] Q9 확인과 `H-142` 판단도 확정·반영했다**(아래 Q4 이하 절). **[같은 날 마지막] Q9 확인과 `H-142` 판단도
닫혀 9라운드 발견은 전량 처리됐다** — 그 뒤 감사 8라운드와 `/code-review high` 닫혀 9라운드 발견은 전량 처리됐다** — 그 뒤 감사 8라운드와 `/code-review high`
돌렸고, **리뷰가 낸 새 메커니즘 넷(`H-143`~`H-146`)이 판단 대기**(아래 돌렸고, 리뷰가 낸 새 메커니즘 넷(`H-143`~`H-146`)**[2026-08-27 다음 세션]
"`/code-review high`" 절). 전량 확정·반영**(아래 "`H-143`~`H-146`" 절).
**사용자 회신 원문(Q4~Q10 일괄, 2026-08-27)**: *"Q5 까지는 권고에 전부 동의함. **사용자 회신 원문(Q4~Q10 일괄, 2026-08-27)**: *"Q5 까지는 권고에 전부 동의함.
WeakSubscribed 자체가 사라질 수 있는 요소라서, 그 사라지는걸 유저가 정하게 하는 WeakSubscribed 자체가 사라질 수 있는 요소라서, 그 사라지는걸 유저가 정하게 하는
@ -377,7 +377,10 @@ if slot._physicalTarget == nil then return end
`Subscribe`/`WeakSubscribe`/`WeakUnsubscribe`는 **Observer의 함수를 메소드 `Subscribe`/`WeakSubscribe`/`WeakUnsubscribe`는 **Observer의 함수를 메소드
테이블에 그대로 배정**(같은 레지스트리·같은 `canBound` 게이트·같은 테이블에 그대로 배정**(같은 레지스트리·같은 `canBound` 게이트·같은
`.Subscribed`), `Unsubscribe``Observer.Unsubscribe(self)` 통과 뒤 `.Subscribed`), `Unsubscribe``Observer.Unsubscribe(self)` 통과 뒤
`self:_consumeCleanup()`. `WeakUnsubscribe`는 cleanup을 안 건드린다(약한 `self:_consumeCleanup()`. **⚠️ [2026-08-27 같은 날 정정] 함수 배정은 (b)로
뒤집혔다** — 네 진입점은 `EffectHandle` 자기 것, 공유는 레지스트리·게이트뿐
(아래 `H-144` 절의 감사 4라운드 항목). 게이트가 먼저라는 이 문항의 결정
자체는 그대로다. `WeakUnsubscribe`는 cleanup을 안 건드린다(약한
구독은 GC에 맡기는 경로라 해제가 종료 신호가 아니다). 구독은 GC에 맡기는 경로라 해제가 종료 신호가 아니다).
- 산문의 번호 목록(플래그 → cleanup → fail-fast)은 **의미 목록이지 실행 순서가 - 산문의 번호 목록(플래그 → cleanup → fail-fast)은 **의미 목록이지 실행 순서가
아니라고** 그 자리에 적었다. `lifecycle-pattern.md`의 *"아래가 네 진입점 아니라고** 그 자리에 적었다. `lifecycle-pattern.md`의 *"아래가 네 진입점
@ -629,6 +632,121 @@ nil 로 지우는것만 안 한다면 맞아"*.
요청으로 신설한 블록. 캡에 밀린 하위 발견(`Slot:List` 잉여 블록 잔존 등)은 이미 요청으로 신설한 블록. 캡에 밀린 하위 발견(`Slot:List` 잉여 블록 잔존 등)은 이미
"잉여" 주석으로 처리된 것과 같은 항목. "잉여" 주석으로 처리된 것과 같은 항목.
## `H-143`~`H-146` — 전부 권고 (a) 확정 (2026-08-27)
문항·갈래 원문은 `-round9.md``H-143`~`H-146` 절. 반영 자리는 각 항목에.
**사용자 회신 원문**: *"H-143: 동의. 메니징된 effect 에서 해당 부분을 지원 안
해야할 이유가 딱히 존재하지 않아. 특정 state 에 변경을 딱 한번만 처리하고
cleanup 되는걸 만드는 요구가 존재하지 않을 이유가 딱히 없다고 봐. (a) 갈래를
택하는데 큰 문제가 없어보여. H-144: 재설치 처리는 동의해. 그리고 원래 처음
설치에서도 모든 옵저버와 ref 콜백이 하나하나 실행되지 않도록 blocker 를 통해서
전부 블로킹 하고 명시적으로 실행한다는 점을 생각해야해 이건 그 동작과 유사해. 재
sub 에서도 똑같게, 후행에서는 실행해야하는 부분이지. 그러고 나서는 사실 어떤
것이라도 가만히 둬도 좋아. 그 이후 받는 모든 emit 의 경우는 들어올 때 기준으로
항상 '다시 받는 emit' 에 속할것이거든. … 근데 blocker 로 블록된 다음 나중에
들어오는 것에 대해서 어떻게 처리할지는 갈리는 부분이야. 그걸 생각하면 초기에
epoch 를 전부 잘 설정해주는게 나을수도 있어. 왜냐하면 처음 연산 기준에 있어서도
전부 값은 최신 값이거든. 따라서 다음 emit 을 받아야할 이유가 없을수도 있어. 이건
중간 값이 emit 을 보류하는거랑 다른 이야기인듯. observer 에도 연관되는
이야기일텐데 한번 확인 해볼래? H-145: (A) 의 단점은 slot 자체가 여기저기에
마운트된다면 여러 bk 에서 남아있을 수 있다는건데, 그건 큰 문제가 안 돼. slot
자체가 소수의 생성이 되는 요소인데다가 포탈 경로가 많지 않거든. 아마 그래서
충분히 weak 로 두는건 괜찮은 발상이야. 컴퓨팅 비용이 더 안들고, 복잡하지 않고
깔끔한 경로거든. 다만 다른 inst 들이 삭제 되지 않는게 확실히 보장되어야해.
기본적으로 자신을 gchold 로 잡으니 괜찮아보여. H-146: 나도 (A) 갈래에 동의해.
왜냐하면, 실제로 React 에서도 최상위 경로는 보통 App.tsx 나 main 을 나누고 root
를 돔 api 로 가져오고 거기에 바운딩 하는 처리를 해야하거든. 유사하게 메인
컴포넌트에 대해서 생성 결과를 Parent 를 설정해주는건 옳을 수 있어. 우선 해당
방식은 엔진 마다 다를 수 있다는 점도 생각해야해. Quad 가 제공하는 마운트는
완전히 다른 성격이라서(부기에 대한 처리가 들어가는데 PlayerGUI 등 Quad 가
제공하지 않은 부기가 없는 객체에 대해서 Quad 의 객체를 주입하는 성격의 API 는
아니거든) 해당 부분을 해결하기 위해서 다른 API 를 제공 할 이유가 없기도 해. 해당
부분은 각 엔진을 사용하는 최종 사용자의 몫."*
### `H-143``Rerun` 꼬리에서 실행 중 사망(`wasAlive and not canExecute`)이면 반환 cleanup 즉시 소진 (a)
- 하위 결정: 판정은 **"이 실행 중에 죽었는가"(`wasAlive and not canExecute`)**
`fn``WeakUnsubscribe`도 같은 경로로 소진된다.
- ⚠️ 처음엔 `not canExecute` 하나로 썼는데 **감사 2라운드가 회귀를 잡았다**:
생성자의 최초 `Rerun()`은 어떤 바인드·구독보다 먼저 돌아 `canExecute`
항상 거짓 → 첫 실행 cleanup 즉시 소진 + `_installed` 거짓 → 첫 바인드에서
`fn`이 또 돈다(`H-58` 재현, "생성 즉시 1회 실행은 바인드로 미룰 수 없다"와
충돌). 진입 전 상태를 로컬로 잡아 "살아 있다가 죽었다"만 잡는 것으로 정정,
같은 자리에서 죽은 핸들의 `_pending` 재요청도 버리기로(안 버리면 `fn`
`Unsubscribe()` + `Rerun()` 조합이 죽은 핸들에서 `fn`을 한 번 더 돌린다 —
감사 2라운드 의심 항목). **사용자 확정**: *"오 그렇네. 실행중 죽으면
클린업만 하기, 해당 방식 맞아보여"*. `H-133`의 "약한 해제는 cleanup을 안 건드린다"는
*`WeakUnsubscribe` 자신*의 계약이고, 여기선 `Rerun`이 방금 받은 것을 죽은
핸들에 매다느냐의 문제라 행위자가 다르다. 강한 해제만 잡으려면 별도 판정이
필요해 더 든다(a2 기각).
- 반영: `base/effect-plan.md` `Rerun` 의사코드 + "`self`를 주는 덕에" bullet
(허용 문장이 확정으로), `ROADMAP.md` M2 `Effect` 체크박스.
### `H-144``Subscribe`/`WeakSubscribe` 등록 끝에 `Refresh()` 먼저, `not _installed or depsChanged → Rerun` (a) + 진입점은 `EffectHandle` 자기 것 (b, 감사 4라운드)
사용자가 요청한 **재트레이싱** 결과(회신의 두 질문):
1. *재구독 뒤 "다음 emit"이 오면 꼬여도 괜찮은가* — 괜찮다. `fire`의 가드
순서가 `canExecute → blocker:IsOn → _epochs:Update → Rerun`이라 해제 중
도착한 emit은 **첫 가드에서 버려져 `_epochs`를 안 건드린다.** 재구독 뒤 첫
emit은 리비전이 새로우니 정상 `Rerun`, 읽는 값은 항상 최신(State는 lazy,
수신 시점에 `valueEpochMap` 갱신). 다이아몬드 접힘도 그대로.
2. *재구독 시점에 epoch를 맞춰두는 게 나은가* — 낫고, **leaf 경로가 이미 그
모양**이다(`_bindDestroying`: `Refresh()` 먼저, 아니면 "다음 emit이 헛되이
한 번 더 돈다" 캐비엇). 구체 케이스: `state:Gate`가 유보 중일 때 재구독 →
`Rerun`이 읽는 값은 이미 새 리비전(게이트는 값은 수신 시점에 갱신하고 emit만
유보, `state-epoch-plan.md` §4 게이트 예외) → 유보가 풀려 `{A@r2}`가 오면
`Refresh` 안 했으면 같은 값으로 `fn`+cleanup이 한 번 더, 했으면 접힌다.
- Blocker는 재구독에 안 쓴다 — dep 재등록이 없으므로(생성자에서 한 번, `_deps`
강참조 유지) 억제할 발화가 없고 명시 `Rerun`만 하면 된다.
- Observer: 관련 없음 확인 — epoch가 없고(`H-107`) 구독 시 설치 발화도 없어
재`Subscribe`는 첫 구독과 동일. 고칠 자리 없음.
- 하위 결정: **`WeakSubscribe`도 같은 꼬리** — `WeakUnsubscribe`(cleanup 미소진,
`_installed` 유지) 뒤 재`WeakSubscribe`도 한 모양으로 맞는다(dep 안 변했으면
no-op, 변했으면 옛 cleanup 소진 후 재실행).
- ⚠️ **감사 4라운드가 배선 결함을 잡았다 → (b) 네 진입점을 `EffectHandle` 자기
것으로.** 처음엔 Q4(`H-127`)의 "Observer 함수를 그대로 배정" 위에 래퍼를
얹었는데, `Observer:Subscribe`의 본문이 `self:WeakSubscribe()`로 **콜론
위임**하므로 `self``EffectHandle`이면 그 조회가 새 `EffectHandle:WeakSubscribe`
래퍼로 가서 꼬리가 **두 번** 돌고, 첫 번째는 `Subscribed[self] = true`(강한 킵)
**전에** `Rerun``fn``self:Unsubscribe()`(`H-143`이 지원하는 패턴)가
*"not subscribed strongly"*로 error(로컬 `luau`로 재현: 콜론 위임은 꼬리
2회·첫 번째 킵 전, dot 위임은 1회·킵 뒤). 갈래 (a) Observer 본문의 내부 위임을
dot 호출로 / (b) 함수 공유를 접고 `EffectHandle`이 네 진입점을 따로(공유는
`Observer.luau`의 레지스트리 둘과 `canBound`뿐). **사용자 확정 (b)**: *"b가
맞아. 내 머리에서 나왔던 처음 구조는 그것이였어. 항상 언급되는 말이지만,
'하나의 무언가가 두 일을 동작하지 않는가에 유의하자' - 이것도 마찬가지야.
버그를 유발하기 좋은 포인트였고, 감사자의 좋은 지적이였어."* 앞서 *"애초에
observer 랑 effect 랑 헤테로지니어스한 타입인데. effect 가 observer 를
만족시키진 않아. 생성 방법도 다르고"*라 결함 자체가 진짜인지 물었고, 재현으로
확인. 원칙은 `conventions.md`의 "설계 원칙" 절로 승격. Q4의 "그대로 배정"은
이로써 **좁혀졌다**(레지스트리·게이트 공유만 남음).
`Subscribe``WeakSubscribe`를 부르지 않고 등록 세 줄을 펼쳐 쓴다 — 꼬리가
강한 킵 뒤에 정확히 한 번 돌아야 하므로.
- 반영: `base/effect-plan.md` `EffectHandle` 의사코드 블록(`resubscribeTail`) +
"`:Subscribe()`가 등록하는 것은" bullet, `base/lifecycle-pattern.md`의 (2) 절
머리 문단(`EffectHandle` 재사용 범위 정정), `ROADMAP.md` M2.
### `H-145``bk.indexOfElement`를 weak-key로 (a)
- 사용자가 붙인 조건: 요소 `inst`가 마운트 중 GC되지 않는 보장 — (0)의
gchold(`lifecycle-pattern.md`)가 그것. 값이 정수라 Luau ephemeron 미지원도
무관.
- (b) 해제에 요소를 넘겨 `setLength`가 지우기(시그니처 의미 확장) / (c) inst
층 명시 삭제 API 기각.
- 반영: `base/dispatch-core-plan.md` `getBookkeeping` 초기화 + `InstanceChildHandler`
5번째 인자 근거 정리(weak가 되며 "옛 키 잔존" 근거는 사라지고 "지속 클로저
없음"만 남음), `ROADMAP.md` M3 `getBookkeeping` 체크박스.
### `H-146` — 루트는 금지 범위 밖, `Mount` 표면 없음, 거부는 전용 문구 (a)
- 인용문 *"외부에서 직접 Parent 설정해주지 말것"*의 범위를 **quad가 관리하는
자식 자리**로 명문화. 루트 부착은 엔진마다 다른 최종 사용자 코드. (b)
`Mount(root, parent)` / (c) 루트 전용 props 키 기각.
- 반영: `base/bind-system-plan.md` `H-142` 항목에 루트 bullet + 전용 문구,
`base/slot-plan.md`의 "동적 자식은 반드시" 절 각주 + "v2는 mount 함수 자체가"
문구 정정(v1식 `Mount`는 v2에 없다), `ROADMAP.md` M5 `Property.luau`.
## 감사 루프 (2026-08-27, Q4~Q10 반영분) ## 감사 루프 (2026-08-27, Q4~Q10 반영분)
관례대로 `quad-doc-auditor` 한 턴에 하나, diff 범위, 라운드마다 각도 변경. 관례대로 `quad-doc-auditor` 한 턴에 하나, diff 범위, 라운드마다 각도 변경.
@ -644,3 +762,50 @@ nil 로 지우는것만 안 한다면 맞아"*.
| 7 | 6라운드 수정분 + 세션 기록 사실성 | 확실 3 · 의심 0 | README `bind-system-plan` 행이 "배치 닫는 자리 … `Parent` 순서 미정"으로 1라운드 정정·`H-142` 확정 이전 상태 / `todos.md` 00번에서 "`/code-review high`·커밋 남음" 액션이 사라짐 → 복원 / 이 파일의 "48줄" 개수 주장(실제 47) → 개수 삭제. 세션 파일·summary·todos 본문 서술은 전부 사실과 일치 | | 7 | 6라운드 수정분 + 세션 기록 사실성 | 확실 3 · 의심 0 | README `bind-system-plan` 행이 "배치 닫는 자리 … `Parent` 순서 미정"으로 1라운드 정정·`H-142` 확정 이전 상태 / `todos.md` 00번에서 "`/code-review high`·커밋 남음" 액션이 사라짐 → 복원 / 이 파일의 "48줄" 개수 주장(실제 47) → 개수 삭제. 세션 파일·summary·todos 본문 서술은 전부 사실과 일치 |
| 8 | 7라운드 수정분만(좁게) | 확실 1 | `project-context.md`의 볼드 마커가 홀수(이번 갱신이 닫는 `**`를 지움) → 짝 맞춤. 지정 세 자리는 회귀 없음. **여기서 멈춤** — 1라운드 이후 설계 내용 발견은 0이고 [2026-08-27 8라운드 시점] 남은 건 기록 문서의 표기뿐이라, 비용 대비 `/code-review high`로 넘어가는 게 낫다고 판단(추이 5→3→4→5→3→1→3→1, 단조 수렴은 아님) | | 8 | 7라운드 수정분만(좁게) | 확실 1 | `project-context.md`의 볼드 마커가 홀수(이번 갱신이 닫는 `**`를 지움) → 짝 맞춤. 지정 세 자리는 회귀 없음. **여기서 멈춤** — 1라운드 이후 설계 내용 발견은 0이고 [2026-08-27 8라운드 시점] 남은 건 기록 문서의 표기뿐이라, 비용 대비 `/code-review high`로 넘어가는 게 낫다고 판단(추이 5→3→4→5→3→1→3→1, 단조 수렴은 아님) |
## 감사 루프 (2026-08-27, `H-143`~`H-146` 반영분)
`quad-doc-auditor` 한 턴에 하나, diff 범위, 라운드마다 각도 교체. 새 발견
1→1→1→1→1→1→1→**0** (8라운드에서 수렴).
| 라운드 | 각도 | 발견 | 처리 |
|---|---|---|---|
| 1 | 인덱스·문구 잔존 | `effect-plan.md`*`_installed`는 `Rerun`이 끝날 때 참* 문장이 무조건 서술 | 조건 반영 |
| 2 | `base/` 의미론 | **`H-143` 판정식 `not canExecute`가 생성자 최초 설치를 죽임**(회귀) + `fn``Unsubscribe`+`Rerun` 재진입 미서술 | 사용자 결정 → `wasAlive and not canExecute` + `_pending` 버림 |
| 3 | audit/archive/reference | `session-summary.md`가 정정 전 판정식을 요약 | 정정 |
| 4 | 수정분 재검토 | **Observer 함수 배정 + 콜론 위임 → 재구독 꼬리 2회, 강한 킵 전 `Rerun`**(설계 결함) | 사용자 결정 → (b) 진입점 자기 것 + 원칙 승격 |
| 5 | (b) 수정분 재검토 | `README.md` 색인 행 미갱신 / Observer `WeakSubscribe` `error` `level` 누락 / 실측 문구 과장 | 셋 다 정정 |
| 6 | 인덱스 레이어 | followup `H-143` 헤딩·진행 표가 정정 전 표현 | 정정 |
| 7 | `effect-plan.md` 통독 | "종료 신호 둘뿐"이 `Rerun` 꼬리와 모순 / 필드 목록에 `.Subscribed` 누락 / `fn` 안 재구독 미서술 | 셋 → 정정·명시 |
| 8 | 전체 diff 재통독 | **0건** | — |
2·4라운드 발견은 둘 다 "권고 (a)의 의도는 맞는데 메인 세션이 고른 판정식·배선이
틀린" 종류 — 1라운드(문구 잔존) 각도로는 안 나왔고, 의미론·수정분 재검토
각도에서만 나왔다.
## `/code-review high` (2026-08-28, `H-143`~`H-146` 반영분 + 감사 8라운드 뒤)
10건(7 CONFIRMED 검증). **일곱은 기존 규칙 적용이라 반영**, **셋은 새 메커니즘·
기존 결정 변경이라 10라운드 문항으로**(`-round10.md` `H-147`~`H-149`).
반영한 일곱:
1. `effect-plan.md` "`EffectHandle:Subscribe()`" 절 머리 볼드가 여전히 "Observer의
것을 재사용" — (b)로 정정. 717행 "Observer의 것을 위임한 뒤"도.
2. `Refresh()` 먼저의 계약을 세웠는데 `ROADMAP.md`(M2·M6)·`lifecycle-pattern.md`·
`ref-plan.md`가 단축평가형 `if not _installed or Refresh()` 한 줄을 그대로 —
전부 두 줄 형태로.
3. "첫 구독에선 no-op"은 거짓(생성~첫 구독 사이 emit은 `fire`가 버려 `Refresh`
참) — "그 사이 dep이 안 변했으면 no-op"으로.
4. `fn` 안 자기 해제의 범위 — 건 경로로만(강구독 `Unsubscribe`, 약구독
`WeakUnsubscribe`), leaf 바인딩 핸들엔 자기 해제 경로 없음(종료는 leaf 사망).
`Rerun` 주석의 "leaf도 아니라"도 한정.
5. `source-state-plan.md`의 *내부 Observer가 게이트를 갖고 있어 Effect 구현이
몰라도 자동 커버* 서술 — 틀림(내부 Observer는 `Weak*`, 발화는 `canExecute(handle)`).
(b)대로 `EffectHandle`이 직접 `canBound`.
6. weak-key `indexOfElement`의 보장자 — 사용자가 든 gchold는 `inst → 값` 홀더라
`inst` 자신을 안 잡음. 실제 보장자는 `slot._elements` + `gatedRecompute` 캡처로
정정, Instance weak key 미확인(`audit/gcconn-trick-verification.md`) 포인터.
7. `-round9.md` `H-144` 셀 "둘 다 래퍼"·`ROADMAP.md` 배너 "재구독 래퍼" →
(b) 표현으로. 캡 아래 넷(`question.md`의 *판단 대기 없음* 과장, "다음 세션" →
파일 ID, 중복 세 줄, 한국어 자리표시자)도 앞 둘은 정정.
문항으로 올린 셋은 `-round10.md`가 소스.

View file

@ -57,10 +57,10 @@ high` 7패스의 수정)를 처음부터 다시 트레이싱한 결과. 발견
| `H-140` | 🟢 | `ROADMAP.md:1124`가 아직 *"해제 시 `slot.Offset = nil`"*`SL-75`/`D-60`이 전면 정정한 문장인데 **구현자가 실제로 보는 체크박스**에 살아남았다. 그대로 짜면 포탈 구독자가 영구히 끊긴다 | `ROADMAP.md` M6 | 사냥 #7 | — | | `H-140` | 🟢 | `ROADMAP.md:1124`가 아직 *"해제 시 `slot.Offset = nil`"*`SL-75`/`D-60`이 전면 정정한 문장인데 **구현자가 실제로 보는 체크박스**에 살아남았다. 그대로 짜면 포탈 구독자가 영구히 끊긴다 | `ROADMAP.md` M6 | 사냥 #7 | — |
| `H-139` | 🟢 | `New`가 둘(모듈 팩토리 `New(): Quad` / 인스턴스 생성자 `New "Frame" {…}`)이고, `D.Frame {…}`의 파이프라인(생성 → gcconn → flatten → drive)은 네 문서에 흩어져 있어 의사코드가 한 곳에 없다 | `module-lifecycle-plan.md` / `bind-system-plan.md` | 레인 B | — | | `H-139` | 🟢 | `New`가 둘(모듈 팩토리 `New(): Quad` / 인스턴스 생성자 `New "Frame" {…}`)이고, `D.Frame {…}`의 파이프라인(생성 → gcconn → flatten → drive)은 네 문서에 흩어져 있어 의사코드가 한 곳에 없다 | `module-lifecycle-plan.md` / `bind-system-plan.md` | 레인 B | — |
| `H-142` | 🟡→닫힘 | **[2026-08-27 `H-139` 의사코드에서 발견]** `Dispatch.drive`의 해시 파트 순서가 계약이 아니라 `Frame { Parent = x, Size = … }``Parent` 대입이 다른 프로퍼티보다 먼저 올 수 있다 — 사용자 확정은 순서가 아니라 **props에 `Parent` 금지** | `bind-system-plan.md` × `ROADMAP.md` M5 | 사냥 밖 — 의사코드 작성이 드러냄 | — | | `H-142` | 🟡→닫힘 | **[2026-08-27 `H-139` 의사코드에서 발견]** `Dispatch.drive`의 해시 파트 순서가 계약이 아니라 `Frame { Parent = x, Size = … }``Parent` 대입이 다른 프로퍼티보다 먼저 올 수 있다 — 사용자 확정은 순서가 아니라 **props에 `Parent` 금지** | `bind-system-plan.md` × `ROADMAP.md` M5 | 사냥 밖 — 의사코드 작성이 드러냄 | — |
| `H-143` | 🟡 | **[2026-08-27 `/code-review high`]** `fn` 안에서 `self:Unsubscribe()`를 부르면(문서가 허용하는 자리) `Rerun``fn`의 반환 cleanup을 그대로 `_cleanup`에 저장하고 `_installed = true`로 되돌려 **아무도 소진 못 하는 cleanup**이 남는다 — "마지막 cleanup 정확히 1회" 계약 위반 | `effect-plan.md` `Rerun` | 사냥 밖 — 리뷰 발견, 처방은 새 메커니즘 | — | | `H-143` | 🟡 | **[2026-08-27 `/code-review high`]** `fn` 안에서 `self:Unsubscribe()`를 부르면(문서가 허용하는 자리) `Rerun``fn`의 반환 cleanup을 그대로 `_cleanup`에 저장하고 `_installed = true`로 되돌려 **아무도 소진 못 하는 cleanup**이 남는다 — "마지막 cleanup 정확히 1회" 계약 위반 | `effect-plan.md` `Rerun` | 사냥 밖 — 리뷰 발견, 처방은 새 메커니즘. **[2026-08-27] 확정 (a)** | — |
| `H-144` | 🟡 | **[2026-08-27 `/code-review high`]** `EffectHandle.Subscribe = Observer.Subscribe` 배정이라 `Unsubscribe`로 cleanup을 소진한 핸들을 다시 `Subscribe`하면 `.Subscribed = true`만 서고 **재설치(`Rerun`)가 없다** — leaf 재바인드 경로(`_bindDestroying`의 `not _installed → Rerun`, `H-65`)와 비대칭 | `effect-plan.md` | 리뷰 발견, 처방은 새 메커니즘 | — | | `H-144` | 🟡 | **[2026-08-27 `/code-review high`]** `EffectHandle.Subscribe = Observer.Subscribe` 배정이라 `Unsubscribe`로 cleanup을 소진한 핸들을 다시 `Subscribe`하면 `.Subscribed = true`만 서고 **재설치(`Rerun`)가 없다** — leaf 재바인드 경로(`_bindDestroying`의 `not _installed → Rerun`, `H-65`)와 비대칭 | `effect-plan.md` | 리뷰 발견, 처방은 새 메커니즘. **[2026-08-27] 확정 (a) — `Refresh` 먼저, 두 구독 진입점 모두 꼬리; 진입점은 (b) `EffectHandle` 자기 것** | — |
| `H-145` | 🟡 | **[2026-08-27 `/code-review high`]** 최상위 `SlotHandler` retractor가 `setLength(inst, k, 0)`으로 해제할 때 `bk(inst).indexOfElement[slot]`**안 지워진다**(해제 호출엔 요소가 없고 `setLength``element ~= nil`일 때만 쓴다) — 교체마다 옛 Slot이 부모 `bk`에 강참조로 쌓여 부모가 죽을 때까지 산다(`InstanceChildHandler` 쪽은 5번째 인자를 안 넘기는 것으로 닫았지만 Slot은 `Length`가 State라 요소가 필요하다) | `dispatch-core-plan.md` `setLength` × `slot-plan.md` 494 | 리뷰 발견, 처방은 새 메커니즘 | — | | `H-145` | 🟡 | **[2026-08-27 `/code-review high`]** 최상위 `SlotHandler` retractor가 `setLength(inst, k, 0)`으로 해제할 때 `bk(inst).indexOfElement[slot]`**안 지워진다**(해제 호출엔 요소가 없고 `setLength``element ~= nil`일 때만 쓴다) — 교체마다 옛 Slot이 부모 `bk`에 강참조로 쌓여 부모가 죽을 때까지 산다(`InstanceChildHandler` 쪽은 5번째 인자를 안 넘기는 것으로 닫았지만 Slot은 `Length`가 State라 요소가 필요하다) | `dispatch-core-plan.md` `setLength` × `slot-plan.md` 494 | 리뷰 발견, 처방은 새 메커니즘. **[2026-08-27] 확정 (a) weak-key** | — |
| `H-146` | 🟡 | **[2026-08-27 `/code-review high`]** `H-142`(props에 `Parent` 금지, 대입은 자식을 받는 쪽만)를 닫고 나니 **루트**(`ScreenGui` → `PlayerGui`)를 붙이는 승인된 경로가 코퍼스 어디에도 없다 — 유일한 선례는 v1 `Mount(ScreenGui, …)`, `slot-plan.md` 233행의 *"v2는 mount 함수 자체가"*의 그 함수는 어느 표면에도 없다. 부수로 거부 배선의 에러가 일반 매치 실패 메시지(*"provider가 초기화됐는지 확인"*)라 오해를 부른다 | `bind-system-plan.md` `H-142` | 리뷰 발견, 처방은 새 표면 | — | | `H-146` | 🟡 | **[2026-08-27 `/code-review high`]** `H-142`(props에 `Parent` 금지, 대입은 자식을 받는 쪽만)를 닫고 나니 **루트**(`ScreenGui` → `PlayerGui`)를 붙이는 승인된 경로가 코퍼스 어디에도 없다 — 유일한 선례는 v1 `Mount(ScreenGui, …)`, `slot-plan.md` 233행의 *"v2는 mount 함수 자체가"*의 그 함수는 어느 표면에도 없다. 부수로 거부 배선의 에러가 일반 매치 실패 메시지(*"provider가 초기화됐는지 확인"*)라 오해를 부른다 | `bind-system-plan.md` `H-142` | 리뷰 발견, 처방은 새 표면. **[2026-08-27] 확정 (a) 루트 예외 + 전용 문구** | — |
--- ---
@ -617,7 +617,7 @@ Tween 절 스케치의 `hint == nil` 줄이 `v == nil` 규칙을 잘못 옮긴
--- ---
### `H-143`~`H-146` — 2026-08-27 `/code-review high`가 Q4~Q10 반영분에서 잡은 것 (사용자 판단) ### `H-143`~`H-146` — 2026-08-27 `/code-review high`가 Q4~Q10 반영분에서 잡은 것 (**[2026-08-27] 전량 확정 — 결정은 `-round9-followup.md`**)
반영 뒤 돌린 `/code-review high`(10건, 검증 12 CONFIRMED)가 낸 것 중 **처방이 새 반영 뒤 돌린 `/code-review high`(10건, 검증 12 CONFIRMED)가 낸 것 중 **처방이 새
메커니즘·표면인 넷**. 나머지 여섯(`InstanceChildHandler` retractor의 `Parent = nil` 메커니즘·표면인 넷**. 나머지 여섯(`InstanceChildHandler` retractor의 `Parent = nil`

View file

@ -12,24 +12,19 @@
--- ---
## ⭐ 최우선 — `/code-review`가 낸 새 메커니즘 넷 (2026-08-27 갱신) ## ⭐ 최우선 — 10라운드 문항지 (2026-08-28 갱신, 배치 회신 대기)
**[2026-08-27] 9라운드 손 트레이싱은 Q1~Q10·`H-138`·`H-139`·`H-142`까지 전량 **[2026-08-28] 10라운드 문항지가 `qa-request/pre-implementation-handtrace-round10.md`
처리·반영됐고**, 그 반영분에 `/code-review high`를 돌린 결과 **처방이 새 있습니다** — §4 표를 위에서 아래로 읽고 갈래만 회신하시면 됩니다. 씨앗은
메커니즘·표면인 넷**이 나와 판단을 기다립니다 — 갈래·권고는 `H-143`~`H-146` 반영분에 `/code-review high`가 낸 판단 대기 셋(`H-147` 죽은
`qa-request/pre-implementation-handtrace-round9.md``H-143`~`H-146` 절이 핸들에서 `Rerun` / `H-148` `Parent` 거부 전용 문구는 새 메커니즘 / `H-149`
소스(여기서 반복하지 않음). 한 줄씩: Observer `Subscribe` 위임과 `level 2`), 그 아래에 신선한 탐사자의 **광범위
- **`H-143`** `fn` 안에서 `self:Unsubscribe()`하면 그 실행이 돌려준 cleanup이 탐사**(M2~M8, 레인 A/C/B/D — 지시서 `-round10-brief.md`) 결과가 이어집니다.
영원히 안 불린다 — 권고 (a) `Rerun``canExecute` 거짓이면 즉시 소진. **M2.** **M2 착수 게이트는 아닙니다**(셋 다 구현 중 정해도 되는 크기). 사용자 판단:
- **`H-144`** `Unsubscribe`한 핸들을 다시 `Subscribe`해도 재설치가 없다 — 권고 *"인간을 기다리는거 엄청 비효율이라서 … batch 로 처리될 필요가 있는듯"*.
(a) `Subscribe` 래퍼가 `not _installed``Rerun`(leaf 경로 `H-65`와 대칭). **M2.**
- **`H-145`** 최상위 Slot 교체 시 부모 `bk.indexOfElement`에 옛 Slot이 강참조로 **[2026-08-27] 9라운드**(Q1~Q10·`H-138`·`H-139`·`H-142`·`H-143`~`H-146`)는
남는다 — 권고 (a) 그 맵을 weak-key로. **M3.** 전량 처리·반영됐습니다 — 소스는 `-round9-followup.md`.
- **`H-146`** props `Parent` 금지 뒤 루트(`ScreenGui` → `PlayerGui`)를 붙이는
승인 경로가 없다 — 권고 (a) 루트는 예외로 명문화(금지는 quad가 관리하는 자식
자리에 한함). **M5.**
`H-143`/`H-144`는 M2 `Effect` 표면이라 **M2 착수 전 답이 있는 게 좋지만** 구현
중 정해도 되는 크기입니다(게이트로 보지 않음).
**[2026-08-26] 8라운드 손 트레이싱이 처리 완료됐습니다 — M2(반응형 코어) **[2026-08-26] 8라운드 손 트레이싱이 처리 완료됐습니다 — M2(반응형 코어)
착수를 막는 항목이 하나도 없습니다.** 발견 17건(`H-107`~`H-123`)의 결정 착수를 막는 항목이 하나도 없습니다.** 발견 17건(`H-107`~`H-123`)의 결정

View file

@ -1961,3 +1961,21 @@ Q4(`EffectHandle` 네 진입점 의사코드 — Observer 것 재사용, `Unsubs
`question.md` 최우선 절). 결정의 소스는 `question.md` 최우선 절). 결정의 소스는
`qa-request/pre-implementation-handtrace-round9-followup.md`, 경위는 `qa-request/pre-implementation-handtrace-round9-followup.md`, 경위는
`session/2026-08-27-02-handtrace-round9-q4-q10.md`. `session/2026-08-27-02-handtrace-round9-q4-q10.md`.
- **`session/2026-08-27-03-handtrace-round9-h143-h146.md`** — 9라운드 후속:
`/code-review`가 낸 새 메커니즘 넷 **전부 권고 (a) 확정·반영**. `H-143`
`Rerun` 꼬리가 **실행 중에 죽었으면**(`wasAlive and not canExecute`) 반환
cleanup 즉시 소진 + 재요청 버림(`fn` 안 `self:Unsubscribe()` 지원 — 처음 쓴
`not canExecute` 하나짜리 판정은 생성자 최초 설치를 죽여 감사 2라운드가 정정) / `H-144` `Subscribe`·`WeakSubscribe` 등록 끝에
`_epochs:Refresh()` + `not _installed or depsChanged → Rerun` 꼬리(사용자
요청으로 재구독 뒤 emit·epoch·Blocker 상호작용을 재트레이싱, leaf
`_bindDestroying`과 동형) — **그리고 감사 4라운드가 Q4의 "Observer 함수
배정"이 콜론 위임 때문에 이 꼬리를 두 번 태우는 걸 잡아 (b) `EffectHandle`
네 진입점 자기 것으로 확정**(*"하나의 무언가가 두 일을 동작하지 않는가"* →
`conventions.md` 설계 원칙) / `H-145` `bk.indexOfElement` weak-key / `H-146`
루트 부착은 금지 범위 밖 — `Mount` 표면 없이 사용자 몫(*"각 엔진을 사용하는
최종 사용자의 몫"*), `Parent` 거부는 전용 문구. `question.md` 최우선 절 비움.
결정의 소스는 `qa-request/pre-implementation-handtrace-round9-followup.md`.
**[2026-08-28]** 감사 8라운드 수렴 → `/code-review high` 10건(일곱 반영, 셋은
판단 필요) → 사용자 판단으로 **10라운드 문항지**(`-round10.md`, `H-147`~`H-149`
씨앗 + 광범위 탐사)로 이관, 배치 회신 대기.

View file

@ -0,0 +1,75 @@
# 2026-08-27 — 9라운드 후속: `/code-review`가 낸 새 메커니즘 넷(`H-143`~`H-146`) 결정·반영
**무엇을 했나**: 앞 세션(`session/2026-08-27-02-handtrace-round9-q4-q10.md`)이
`question.md` 최우선 절에 올려둔 넷을 사용자에게 갈래·권고와 함께 제시하고 한 턴에
회신을 받아 `base/`·`ROADMAP.md`에 반영했다. 결정과 회신 원문은
`qa-request/pre-implementation-handtrace-round9-followup.md`의 "`H-143`~`H-146`"
절이 소스. 여기는 흐름과 문서에 안 들어간 것.
## 흐름
1. 컴팩트 직후 넷의 원문(`-round9.md` 620행 이하)과 관련 `base/` 서술(`Rerun`·
`_bindDestroying`·`Observer` 네 진입점·`EpochMap` API·`SlotHandler` retractor·
`H-142` 항목·`slot-plan.md`의 "동적 자식은 반드시" 절)을 읽고, 항목마다
**트레이스 → 왜 새 메커니즘인가 → 갈래와 결과 → 권고 → 채택 시 고칠 자리**로
정리해 올렸다. 하위 결정 둘을 같이 올렸다 — `H-143`의 판정 predicate
(`canExecute` 하나 vs 강한 해제만)와 `H-144``WeakSubscribe` 래퍼 여부.
2. 사용자가 넷 다 (a)에 동의하되 `H-144`에 **재트레이싱을 요청**했다 —
"재구독 뒤 emit이 꼬여도 괜찮은가"와 "재구독 시점에 epoch를 다 맞춰두는 게
낫지 않은가(Blocker로 막힌 뒤 늦게 오는 것 처리가 갈린다)", 그리고 Observer도
같은 문제가 있는지.
3. 재트레이싱 결과(followup에 기록): 첫 질문은 `fire`의 가드 순서
(`canExecute`가 `_epochs:Update` **앞**)로 안전, 둘째는 **leaf 경로
`_bindDestroying`이 이미 `Refresh()` 먼저**라 같은 모양으로 맞추면 된다 —
게이트 유보 중 재구독 케이스가 그 차이를 실제로 드러낸다. Observer는 epoch가
없고 설치 발화도 없어 무관. 래퍼 모양을 제시하고 반영으로 넘어갔다.
## 시행착오 / 다음 세션이 알아야 할 것
- **문서에 이미 있는 모양을 먼저 찾았다.** `H-144`의 래퍼는 새로 발명한 게
아니라 `_bindDestroying`(`H-65`)의 꼬리를 그대로 옮긴 것이다 — 앞 세션 교훈
("어디에도 없다"고 쓰기 전에 grep)의 반대 방향 적용: 새 메커니즘을 제안하기
전에 같은 문제를 이미 푼 자리가 있는지 grep했다. 그 덕에 사용자의 "epoch를
맞춰두자"가 곧 기존 캐비엇(287행)의 재발견이라는 걸 바로 연결할 수 있었다.
- `H-145` weak-key는 `InstanceChildHandler`가 5번째 인자를 안 넘기는 **근거 한
줄을 무효화**한다(옛 키 잔존) — 결정 하나가 다른 자리의 *근거*를 바꾸는
패턴이라 그 자리도 같이 고쳤다. 감사자가 잡을 종류의 stale을 반영 시점에
닫은 것.
- `slot-plan.md` 233행 *"v2는 mount 함수 자체가"*는 v1식 `Mount(parent, tree)`
표면이 v2에 있는 것처럼 읽혔다(`H-146` 발견의 부수) — Slot의 마운트 경로를
뜻하는 문장이라 그렇게 고쳤다.
## 감사 루프가 뒤집은 것 둘 (같은 날, 반영 뒤)
- **2라운드**: `H-143` 판정식 `not canExecute`가 **생성자 최초 설치를 죽였다**
(바인드·구독 전이라 항상 거짓 → 첫 cleanup 즉시 소진, `_installed` 거짓, 첫
바인드에서 `fn` 재실행 = `H-58` 재현). `wasAlive and not canExecute`("실행 중에
죽었는가") + 죽은 핸들의 `_pending` 버림으로 정정. 사용자: *"오 그렇네. 실행중
죽으면 클린업만 하기, 해당 방식 맞아보여"*.
- **4라운드**: `H-144` 래퍼가 Q4의 "Observer 함수 배정" 위에 얹히면서
`Observer:Subscribe`의 콜론 위임 `self:WeakSubscribe()``EffectHandle`
오버라이드로 가 꼬리 2회 + 강한 킵 전 `Rerun`. 사용자가 *"그게 진짜 날 수
있어?"*(Effect는 Observer가 아닌데)라 물어 로컬 `luau`로 재현해 보였고,
(a) dot 위임 / (b) 본문 공유 자체를 접기 중 **(b) 확정** — *"내 머리에서
나왔던 처음 구조는 그것이였어"*. 원칙 *"하나의 무언가가 두 일을 동작하지
않는가에 유의하자"*를 `conventions.md` "설계 원칙"으로 승격.
사용자가 이어서 *"그게 정확히 invalid after 문제에서 났던 이야기였어"* — 같은
원칙의 첫 사례가 8라운드의 `invalidAfter` 두 필드 분리였다는 출처를 원칙
항목에 붙였다(필드 하나가 두 뜻 / 함수 본문 하나가 두 타입).
- 교훈: **두 건 다 "반영분 자체가 만든 결함"**이고, 둘 다 "권고 (a)의 의도는
맞는데 내가 고른 판정식·배선이 틀린" 종류다. 감사 1라운드(문구 잔존)로는
못 잡고 2·4라운드(의미론·수정분 재검토) 각도에서만 나왔다 — 각도를 바꾸는
라운드가 실제로 값을 한다는 재확인.
## 마무리 (2026-08-28) — `/code-review high`와 10라운드로의 이관
감사 8라운드 수렴 뒤 `/code-review high` 10건 — 일곱 반영(`-round9-followup.md`
마지막 절), 셋은 판단 필요(`H-147` 죽은 핸들에서 `Rerun` / `H-148` `Parent` 전용
문구가 새 메커니즘 / `H-149` Observer 위임과 `level 2`). 사용자: *"너무 길어지고
있어서, round10 으로 옮기고 싶은데 … 인간을 기다리는거 엄청 비효율이라서 …
batch 로 처리될 필요가 있는듯. 지금 결정해야할 분 10 으로 올리자. 커밋하고
정리한 다음 깔끔한 컨텍스트의 fable 탐사자 하나 띄워서 광범위한 핸드트레이싱,
테스팅, 문제 검사를 하도록 해줘."* → `-round10-brief.md`/`-round10.md` 신설,
커밋 후 탐사자 기동. 교훈: **대화형 처리는 발견 하나당 한 왕복이라 사람 쪽
대기 비용이 크다** — 라운드 단위 배치가 이 프로젝트의 기본 리듬이고, 이번처럼
반영분 리뷰에서 문항이 또 나오면 그 자리에서 다음 라운드로 넘긴다.

View file

@ -47,8 +47,13 @@
`H-142` `Parent` 순서 발견 → props에 `Parent` **금지**로 확정)까지 반영. `H-142` `Parent` 순서 발견 → props에 `Parent` **금지**로 확정)까지 반영.
Q9는 문항 전제가 틀린 것(Tween 절 스케치 한 줄 복사 오류)으로 닫힘. Q9는 문항 전제가 틀린 것(Tween 절 스케치 한 줄 복사 오류)으로 닫힘.
**[2026-08-27 기준] 감사 8라운드·`/code-review high`까지 돌렸다 — 리뷰 10건 중 **[2026-08-27 기준] 감사 8라운드·`/code-review high`까지 돌렸다 — 리뷰 10건 중
여섯은 반영, 넷(`H-143`~`H-146`, 새 메커니즘)은 `question.md` 최우선 절에 여섯은 반영, 넷(`H-143`~`H-146`, 새 메커니즘)은 `session/2026-08-27-03-handtrace-round9-h143-h146.md`에서 사용자 결정으로
판단 대기. 남은 액션: 커밋, 그리고 그 넷의 결정.** 아래는 돌리기 전(2026-08-26) 서술: 전부 권고 (a) 확정·반영(`Rerun` 꼬리 즉시 소진 / 재구독 꼬리 `Refresh` 먼저(진입점은 `EffectHandle` 자기 것) /
`bk.indexOfElement` weak-key / 루트 부착은 금지 범위 밖). **[2026-08-28]** 그
반영분에 `/code-review high`가 또 셋(`H-147`~`H-149`)을 냈고, 사용자 판단으로
**10라운드 문항지**(`qa-request/pre-implementation-handtrace-round10.md`)로
올려 신선한 탐사자의 광범위 탐사(지시서 `-round10-brief.md`)와 함께 **배치
회신 대기**. 남은 액션: 그 회신 처리 → M2 착수.** 아래는 돌리기 전(2026-08-26) 서술:
지시서는 `qa-request/pre-implementation-handtrace-round9-brief.md`. 스코프는 지시서는 `qa-request/pre-implementation-handtrace-round9-brief.md`. 스코프는
**커밋 `9dd8213` 하나의 델타**다 — 8라운드 결정 반영과 그 뒤 **커밋 `9dd8213` 하나의 델타**다 — 8라운드 결정 반영과 그 뒤
`/code-review high` **7패스의 수정이 전부 그 커밋에 들어 있고 아무도 `/code-review high` **7패스의 수정이 전부 그 커밋에 들어 있고 아무도

View file

@ -13,9 +13,10 @@ M2(반응형 코어 — Source/State/Store)**. **⚠️ [2026-08-24] M2와 M3의
결정의 소스는 결정의 소스는
`.claude/qa-request/pre-implementation-handtrace-round8-followup.md` `.claude/qa-request/pre-implementation-handtrace-round8-followup.md`
(7라운드 몫은 `-round7-followup.md`; **[2026-08-27] 9라운드 몫은 (7라운드 몫은 `-round7-followup.md`; **[2026-08-27] 9라운드 몫은
`-round9-followup.md` — Q1~Q10·`H-138`·`H-139`·`H-142` 전량 반영 완료**)이고 `.claude/question.md` 최우선 `-round9-followup.md` — Q1~Q10·`H-138`·`H-139`·`H-142`, 그리고 `/code-review`
절엔 **그 반영분에 `/code-review`가 낸 새 메커니즘 넷(`H-143`~`H-146`)이 판단 `H-143`~`H-146`까지 전량 반영 완료**)이고, **[2026-08-28] `.claude/question.md`
대기**로 올라 있다(M2 착수 게이트는 아님). 같은 상태를 `.claude/project-context.md` 최우선 절엔 10라운드 문항지(`-round10.md` §4 — `H-147`~`H-149` + 광범위 탐사
결과)가 배치 회신 대기**로 올라 있다(M2 착수 게이트는 아님). 같은 상태를 `.claude/project-context.md`
서술하니 마일스톤이 넘어갈 때 두 곳을 같이 고칠 것. 진행 상황의 소스는 서술하니 마일스톤이 넘어갈 때 두 곳을 같이 고칠 것. 진행 상황의 소스는
항상 루트 `ROADMAP.md`. 항상 루트 `ROADMAP.md`.

View file

@ -25,7 +25,12 @@ quad-v2 구현 단계 실행 계획. 설계 근거/아키텍처 자체는 여기
> 순서 / Slot 생성자의 `Offset`·`_baseObserver`·`_destroyed` / `bk.indexOfElement` > 순서 / Slot 생성자의 `Offset`·`_baseObserver`·`_destroyed` / `bk.indexOfElement`
> 토큰 폐기)에 이어 같은 날 Q4~Q10·`H-138`·`H-139`·`H-142`까지 **전량** > 토큰 폐기)에 이어 같은 날 Q4~Q10·`H-138`·`H-139`·`H-142`까지 **전량**
> 반영됐습니다(`EffectHandle` 진입점 / M2에 `Ref` 최소형 / `InstanceChildHandler` > 반영됐습니다(`EffectHandle` 진입점 / M2에 `Ref` 최소형 / `InstanceChildHandler`
> 부기 / `reconcile` 배치 Blocker / `New`·`drive` 파이프라인 / props `Parent` 금지). > 부기 / `reconcile` 배치 Blocker / `New`·`drive` 파이프라인 / props `Parent` 금지),
> 그 반영분에 `/code-review`가 낸 `H-143`~`H-146`(`Rerun` 꼬리 실행 중 사망이면 즉시
> 소진 / 재구독 꼬리 + 진입점은 `EffectHandle` 자기 것 / `bk.indexOfElement`
> weak-key / 루트 부착은 사용자 몫)도 같은 날 확정·반영. **[2026-08-28]** 그 반영분의
> `/code-review`가 낸 셋(`H-147`~`H-149`)은 **10라운드 문항지**(`-round10.md`)에서
> 광범위 탐사 결과와 함께 배치 회신 대기(게이트 아님).
> **어느 체크박스가 바뀌었는지는 여기서 세지 않습니다** — 해당 체크박스에 > **어느 체크박스가 바뀌었는지는 여기서 세지 않습니다** — 해당 체크박스에
> 각각 `H-1xx` 표시가 붙어 있으니 그게 소스입니다. M2/M3 양쪽에 걸쳐 > 각각 `H-1xx` 표시가 붙어 있으니 그게 소스입니다. M2/M3 양쪽에 걸쳐
> 있습니다). M1까지의 산출물은 > 있습니다). M1까지의 산출물은
@ -493,6 +498,16 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
`EffectHandle:Subscribe()`/`:Unsubscribe()`도 추가(leaf 없이 쓰는 `EffectHandle:Subscribe()`/`:Unsubscribe()`도 추가(leaf 없이 쓰는
모듈/스크립트 레벨 Effect) — `:Unsubscribe()`는 Observer와 달리 모듈/스크립트 레벨 Effect) — `:Unsubscribe()`는 Observer와 달리
마지막 cleanup을 1회 트리거해야 함(2026-08-07 일곱 번째 세션). 마지막 cleanup을 1회 트리거해야 함(2026-08-07 일곱 번째 세션).
**[2026-08-27 9라운드 `H-143`/`H-144`]** `Rerun` 꼬리는 `fn` 실행 중에
핸들이 죽었으면(`wasAlive and not canExecute`) 반환 cleanup을 **즉시
소진**하고 `_pending`을 버린다(`fn` 안 `self:Unsubscribe()` 지원 —
`not canExecute` 하나로 판정하면 생성자 최초 설치가 죽는다), 네 진입점은
**`EffectHandle` 자기 것**(Observer 함수 본문을 배정하지 않는다 — 공유는
`Observer.luau`의 레지스트리 둘과 `canBound`뿐; 콜론 위임이 오버라이드를
타 꼬리가 두 번 도는 걸 감사가 잡아 사용자가 (b)로 확정), `Subscribe`/
`WeakSubscribe`는 등록 끝에 `_epochs:Refresh()` + `not _installed or
depsChanged → Rerun` 꼬리(재구독 재설치, leaf `_bindDestroying`과 동형) —
의사코드는 `base/effect-plan.md`.
**동적 경로 가드**도 Observer와 같은 패턴으로 등록(`base/effect-plan.md` **동적 경로 가드**도 Observer와 같은 패턴으로 등록(`base/effect-plan.md`
"동적 경로 가드" 절, 2026-08-14 열한 번째 세션) "동적 경로 가드" 절, 2026-08-14 열한 번째 세션)
**⚠️ [2026-08-24] 단 그 가드를 `Dispatch.addHandler`로 등록하는 것 **⚠️ [2026-08-24] 단 그 가드를 `Dispatch.addHandler`로 등록하는 것
@ -529,7 +544,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
`:WeakCallback()`으로 걸고, 바인드/언바인드는 dep을 아예 안 건드린다 `:WeakCallback()`으로 걸고, 바인드/언바인드는 dep을 아예 안 건드린다
(`H-58`/`H-59`). 발화 게이트는 전부 **`canExecute(handle)`** 하나다 (`H-58`/`H-59`). 발화 게이트는 전부 **`canExecute(handle)`** 하나다
(`H-7`). 캐치업은 바인드 직후 **조건부 최대 1회** (`H-7`). 캐치업은 바인드 직후 **조건부 최대 1회**
(`if not self._installed or self._epochs:Refresh() then self:Rerun() end` (`local depsChanged = self._epochs:Refresh()` **먼저**, 그다음 `if not self._installed or depsChanged then self:Rerun() end``or` 한 줄로 단축평가에 걸면 재설치 경로에서 `Refresh()`가 건너뛰어져 다음 emit이 헛돈다, `base/effect-plan.md` 캐비엇
`H-64`/`H-65`). 의사코드는 `base/effect-plan.md`가 소스 `H-64`/`H-65`). 의사코드는 `base/effect-plan.md`가 소스
- [ ] **[2026-08-24 `H-23`]** State 전파 루프는 구독자 집합을 **배열로 - [ ] **[2026-08-24 `H-23`]** State 전파 루프는 구독자 집합을 **배열로
스냅샷한 뒤** 돈다 — 순회 중 새 구독자 추가가 정상 경로인데 Lua에서 스냅샷한 뒤** 돈다 — 순회 중 새 구독자 추가가 정상 경로인데 Lua에서
@ -757,7 +772,9 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
옆으로만 틀려 알아채기 특히 어렵다): 옆으로만 틀려 알아채기 특히 어렵다):
(a) `getBookkeeping``bk.offsetCache = {}`, **`bk.offsetCacheValidUpTo = 0`, (a) `getBookkeeping``bk.offsetCache = {}`, **`bk.offsetCacheValidUpTo = 0`,
`bk.offsetSetUpTo = 0`**, `bk.recomputeBlocker = Blocker()`, `bk.offsetSetUpTo = 0`**, `bk.recomputeBlocker = Blocker()`,
`bk.indexOfElement = {}`(**[2026-08-27 Q3]**)으로 `bk.indexOfElement = setmetatable({}, { __mode = "k" })`(**[2026-08-27
Q3]**, weak-key는 **[2026-08-27 `H-145`]** — 최상위 Slot 교체 시 해제
`setLength(…, 0)`이 옛 키를 못 지우므로)으로
초기화(`nil` 시작이면 첫 `setOffsetSource``nil` 비교에서 죽는다), 초기화(`nil` 시작이면 첫 `setOffsetSource``nil` 비교에서 죽는다),
(b) **캐시를 앞으로 당기는 자리**(개수는 `base/dispatch-core-plan.md` (b) **캐시를 앞으로 당기는 자리**(개수는 `base/dispatch-core-plan.md`
무효화 표가 소스) — `setLength` 본문과 그 자리 length가 무효화 표가 소스) — `setLength` 본문과 그 자리 length가
@ -933,7 +950,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
`QuadRoblox(Quad): QuadRoblox``QuadTypes.CheckedQuad<T, Pattern>`으로 `QuadRoblox(Quad): QuadRoblox``QuadTypes.CheckedQuad<T, Pattern>`으로
주입받은 quad-base 버전을 확인(`base/quad-types-plan.md` 참고) 주입받은 quad-base 버전을 확인(`base/quad-types-plan.md` 참고)
- [ ] `D/init.luau`(제네릭 생성자 `New` + 생성기가 찍는 정적 별칭 필드 — **[2026-08-18]** 범위는 "GUI에 쓰이는 모든 인스턴스", 이벤트 필드의 콜백 타입까지 생성, `base/bind-system-plan.md`의 "인스턴스 생성 / 이벤트 네이밍 인체공학" 절). **[2026-08-27 9라운드 `H-142`] 생성되는 props 타입에서 `Parent`를 제외할 것** — props에 `Parent`는 올 수 없다(부모가 하는 일, 같은 문서의 파이프라인 절). `New` ①~④ 순서는 그 절의 의사코드가 소스 - [ ] `D/init.luau`(제네릭 생성자 `New` + 생성기가 찍는 정적 별칭 필드 — **[2026-08-18]** 범위는 "GUI에 쓰이는 모든 인스턴스", 이벤트 필드의 콜백 타입까지 생성, `base/bind-system-plan.md`의 "인스턴스 생성 / 이벤트 네이밍 인체공학" 절). **[2026-08-27 9라운드 `H-142`] 생성되는 props 타입에서 `Parent`를 제외할 것** — props에 `Parent`는 올 수 없다(부모가 하는 일, 같은 문서의 파이프라인 절). `New` ①~④ 순서는 그 절의 의사코드가 소스
- [ ] `Handlers/Property.luau`(**[2026-08-27 9라운드 `H-142`]** `isHandlable``"Parent"` 키를 **거부**한다 — 매치 핸들러가 없어지면 `Dispatch.process`의 "매치 핸들러 없음 → 즉시 error"에 걸리는 것으로 런타임 가드가 공짜로 생긴다, 새 메커니즘 없음. **사용자 확정은 "props에 `Parent` 금지"라는 규칙이고, 이 거부 배선은 에이전트 선택**`base/bind-system-plan.md``H-142` 항목이 그렇게 갈라 적음), `Handlers/InstanceChild.luau` - [ ] `Handlers/Property.luau`(**[2026-08-27 9라운드 `H-142`]** `isHandlable``"Parent"` 키를 **거부**한다 — 매치 핸들러가 없어지면 `Dispatch.process`의 "매치 핸들러 없음 → 즉시 error"에 걸리는 것으로 런타임 가드가 공짜로 생긴다, 새 메커니즘 없음. **사용자 확정은 "props에 `Parent` 금지"라는 규칙이고, 이 거부 배선은 에이전트 선택**`base/bind-system-plan.md``H-142` 항목이 그렇게 갈라 적음; **[2026-08-27 `H-146`]** 그 거부는 **전용 에러 문구** — "`Parent` is not a prop" 취지 — 를 내고, 루트를 quad 밖 부모에 붙이는 건 사용자가 밖에서 `.Parent =`로 한다(`Mount` 표면 없음, 같은 항목)), `Handlers/InstanceChild.luau`
**⭐ [2026-08-27 9라운드 `H-134`] `InstanceChildHandler`도 말단이라 **⭐ [2026-08-27 9라운드 `H-134`] `InstanceChildHandler`도 말단이라
부기를 등록한다**: `process`에서 `setOffsetSource(inst, k, None)` 부기를 등록한다**: `process`에서 `setOffsetSource(inst, k, None)`
`v.Parent = inst``setLength(inst, k, 1, inst)`(정적 단일 자식은 상수 `v.Parent = inst``setLength(inst, k, 1, inst)`(정적 단일 자식은 상수
@ -1484,8 +1501,9 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
않는다** — 그 필드 자체가 폐기됐다(`_deps` 하나로 통합). 그리고 않는다** — 그 필드 자체가 폐기됐다(`_deps` 하나로 통합). 그리고
`_bindDestroying`**`Ref` dep 콜백을 (재)등록하지 않는다** — `_bindDestroying`**`Ref` dep 콜백을 (재)등록하지 않는다** —
dep 등록은 생성자에서 끝나고, 여기서 하는 건 `Destroying` 연결과 dep 등록은 생성자에서 끝나고, 여기서 하는 건 `Destroying` 연결과
**조건부 캐치업 한 줄**(`if not self._installed or **조건부 캐치업 두 줄**(`local depsChanged = self._epochs:Refresh()` 뒤
self._epochs:Refresh() then self:Rerun() end`)뿐이다. **이게 `Effect`의 leaf 사망 cleanup을 실제로 `if not self._installed or depsChanged then self:Rerun() end``Refresh()`
`or` 뒤에 두면 단축평가로 건너뛰어진다)뿐이다. **이게 `Effect`의 leaf 사망 cleanup을 실제로
발화시키는 유일한 배선**이고, M6의 `_detached` 정리가 여기 의존한다 발화시키는 유일한 배선**이고, M6의 `_detached` 정리가 여기 의존한다
(그 항목의 `H-50` 각주 참고). 의사코드는 `base/lifecycle-pattern.md` (그 항목의 `H-50` 각주 참고). 의사코드는 `base/lifecycle-pattern.md`
`base/effect-plan.md`가 소스. **[2026-08-14 `base/effect-plan.md`가 소스. **[2026-08-14