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:
parent
7f5868302e
commit
0ec22fbe73
21 changed files with 838 additions and 92 deletions
File diff suppressed because one or more lines are too long
|
|
@ -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` 위에 얹히는 **정책**, 바닥부터 짜지 않음
|
||||||
|
|
|
||||||
|
|
@ -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`의
|
||||||
|
|
|
||||||
|
|
@ -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` 방어가 이미 자리를 잡았고 "아직 아무 자리도 등록
|
||||||
|
|
|
||||||
|
|
@ -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` 진입 전 상태
|
||||||
self._installed = true -- cleanup 반환 여부와 무관하게 "설치됨"
|
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 반환 여부와 무관하게 "설치됨"
|
||||||
|
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`).
|
||||||
self:_consumeCleanup() -- 통과했을 때만: 직전 cleanup 정확히 1회, `_installed = false`
|
WeakSubscribed[self] = nil
|
||||||
|
self.Subscribed = 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 로 확인해내게
|
||||||
될것이므로 괜찮음"*). "전부 채워질 때까지 대기"는 안 한다.
|
될것이므로 괜찮음"*). "전부 채워질 때까지 대기"는 안 한다.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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`에 남던 버그도 같이 사라진다.
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()`를 동시에 쓰는 것도 안전"이라는 서술은 **이
|
||||||
규칙으로 대체(정정)** — 안전하게 지원하는 게 아니라 애초에 막아야
|
규칙으로 대체(정정)** — 안전하게 지원하는 게 아니라 애초에 막아야
|
||||||
하는 조합이었음.
|
하는 조합이었음.
|
||||||
|
|
|
||||||
|
|
@ -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`)이고, 타입별로 다른 후처리가 붙는 **본문**은 각자 가진다.
|
||||||
|
위 첫 원칙과 짝 — "구조를 늘리지 않는다"가 "본문을 억지로 합친다"로
|
||||||
|
읽히면 안 된다.
|
||||||
|
|
||||||
## 문서 표기 규약
|
## 문서 표기 규약
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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`가
|
||||||
|
|
|
||||||
169
.claude/qa-request/pre-implementation-handtrace-round10-brief.md
Normal file
169
.claude/qa-request/pre-implementation-handtrace-round10-brief.md
Normal 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 실행
|
||||||
|
기록**(어떤 스파이크를 어떻게 갱신했고 결과가 뭔지, 파일 경로).
|
||||||
99
.claude/qa-request/pre-implementation-handtrace-round10.md
Normal file
99
.claude/qa-request/pre-implementation-handtrace-round10.md
Normal 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/`.)
|
||||||
|
|
@ -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`가 소스.
|
||||||
|
|
|
||||||
|
|
@ -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`
|
||||||
|
|
|
||||||
|
|
@ -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`)의 결정
|
||||||
|
|
|
||||||
|
|
@ -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`
|
||||||
|
씨앗 + 광범위 탐사)로 이관, 배치 회신 대기.
|
||||||
|
|
|
||||||
75
.claude/session/2026-08-27-03-handtrace-round9-h143-h146.md
Normal file
75
.claude/session/2026-08-27-03-handtrace-round9-h143-h146.md
Normal 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` 신설,
|
||||||
|
커밋 후 탐사자 기동. 교훈: **대화형 처리는 발견 하나당 한 왕복이라 사람 쪽
|
||||||
|
대기 비용이 크다** — 라운드 단위 배치가 이 프로젝트의 기본 리듬이고, 이번처럼
|
||||||
|
반영분 리뷰에서 문항이 또 나오면 그 자리에서 다음 라운드로 넘긴다.
|
||||||
|
|
@ -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패스의 수정이 전부 그 커밋에 들어 있고 아무도
|
||||||
|
|
|
||||||
|
|
@ -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`.
|
||||||
|
|
||||||
|
|
|
||||||
30
ROADMAP.md
30
ROADMAP.md
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue