- H-147 (A): fn/cleanup은 자기 구독을 못 바꾼다 — rawRerun(force)/Rerun 분리, 진입 canExecute 게이트, 네 진입점+_bindDestroying에 _running/_cleanupRunning 가드, H-143(원샷) 소멸, 자기 leaf 파괴 UB - H-148: 루트는 밖에서 .Parent=가 아니라 quad가 Claim으로 소유 → research/existing-mount-plan.md 신설, H-146 예외·전용 문구 폐기, archive 부활 배너 - H-149 Observer 진입점 인라인 / H-150 Effect._blocker 제거 / H-151 _epochs는 emit 때만(게이트는 emit 경로만 미룬다 계약) / H-152 GateNode StateBrand:register / H-153 Store 예약 이름 런타임 가드 + 그림자=store 자신 / H-154 InstanceChildHandler dedup / H-155~H-157 stale - 감사 3→5→2→3→1→0, /code-review high 10건 중 7 반영, 셋(H-159~H-161)은 -round10.md §4 문항으로 Co-authored-by: qwreey <me@qwreey.moe> Claude-Session: https://claude.ai/code/session_01546hjsYNLSMZdHdPyTZaGb
20 KiB
구현 전 손 트레이싱 10라운드 — 결정과 반영 (2026-08-28)
이 파일이 무엇인가:
-round10.md§4 문항 7건(+ 갈래 없는 넷)에 대한 사용자 결정과 근거, 그리고base/반영 결과. 결정의 소스는 이 파일이고 발견 원문은-round10.md. 사용자가 *"하나하나 같이 보자"*라고 해 이번 라운드는 대화형으로 처리한다(배치 회신 대신) — 반영은 전부 정한 뒤 한 번에.
진행 표 (상태의 소스)
| 문항 | 무엇 | 상태 |
|---|---|---|
H-147 |
죽은 핸들에서 Rerun() |
✅ 확정 — 문항의 전제를 뒤집음: fn/cleanup은 자기 생명주기를 못 바꾼다(A). H-143도 함께 소멸 |
H-148 |
Parent 거부 문구 |
✅ 전제 정정 — 문구가 아니라 루트 마운트 표면의 부재. research/existing-mount-plan.md 신설(Claim + D.Mapper), H-146 루트 예외 폐기, 전용 문구 철회 |
H-149 |
Observer Subscribe 위임과 level 2 |
✅ 확정 (a) — Subscribe/Unsubscribe도 게이트·등록을 인라인, 위임 없음 |
H-150 |
Effect._blocker 죽은 부품 |
✅ 확정 (a) — 제거, 억제는 Effect 핸들의 canExecute |
H-151 |
게이트 우회 계약 | ✅ 확정 — (a) 문서화 + Refresh 캐치업 폐기: Effect의 _epochs는 emit 수신 때만 갱신, 재바인드/재구독은 초기 설치와 같다 |
H-158 |
state:Block(blocker) 슈가 잔존 (이 대화에서 나옴) |
⏳ 권고: 폐기 → state:Apply(blocker) |
H-159~H-161 |
반영 뒤 /code-review high가 낸 새 메커니즘 셋 (-round10.md §4 하단) |
⏳ 판단 대기 — 바인드 전 emit 캐치업 / Destroying 경로 cleanup Rerun / M5 루트 부착·다중 스크립트 Claim |
H-153 |
Store 예약 이름 런타임 가드 | ✅ 확정 (a) — 생성자·Of(name)에 예약 이름 검사(level 2), 그림자 = store 자신 (I) |
H-154 |
InstanceChildHandler dedup |
✅ 확정 (a) — retractor 첫 줄 if nextValue == v then return end |
H-152/H-155~H-157 |
갈래 없음 | ✅ 반영(gate-plan.md 조립 첫 줄 StateBrand:register / ROADMAP.md M6×3·M11 / debounce-throttle-plan.md 7절 H-32 문단 / store-plan.md 빈 Store 실측 완료) |
H-147 — 전제 정정: fn/cleanup은 자기 구독을 바꿀 수 없다 (A) — H-143 소멸
문항은 "(a) UB / (b) _everAlive / (c) wasAlive 위치"였는데, 대화 중에 문제의
뿌리가 H-143의 허용 자체라는 것이 드러났다.
경위:
- 사용자가 *"canExecute 자체가 유저함수인 cleanup 아래 있으면"*을 제안 → 그러면
생성자 최초 실행이 죽는 문제(감사 2라운드와 같은 함정)를 짚었고,
wasAlive를 cleanup 앞에서 잡는 절충을 냈다. - 사용자: "처음부터 rerun 이 're'-run 인데도 초기 실행까지 담당하고 있잖아 …
rerun 자체에 인자로써 force: boolean? 처럼 주거나, rawRerun(force: boolean) 을
만들어 생성 시점과 실행 시점에서 이를 명시하는게 맞지 않아?" →
wasAlive는 호출자가 아는 사실("초기 설치냐")을 상태로 추론하던 편법이었음을 인정,rawRerun(self, force)분리 +not force and not canExecute게이트 제안. - 사용자가 그것도 기각: "not force 가지고 확인하면 안 될 부분같음. 초기 설치에서도 본인을 직접 죽이거나 바운딩을 걸거나 할 수 있잖아. … 유저 함수가 본인을 죽이고 살린다는점 자체가 모순이였다는 문제가 나와. 처음 실행해 unsub 했는데, 아래에서 sub 해버릴 수도 있지. 이건 의도 동작일까? 게다가 unbind/bind 는 본인이 못 해. 같은 계층으로 sub/unsub 가 본인이 할 수 있어야할 이유 제공 자체가 큰 그림에서 무언가 잘못된거 아닐까?"
- 갈래 (A)
fn/cleanup의 자기 구독 변경 금지(leaf와 대칭) / (B) 자기 해제만 허용 (비대칭 감수)을 올렸고 사용자 확정 (A): "그런것 같아. 나는 지원 안 할 이유가 안 보였었는데, 지금 보면 엄청난 모순이네. 나는 너의 권고처럼 A가 맞아보여."
확정된 것:
- Effect의 생애는 묶은 쪽이 소유한다 — leaf면 Instance,
:Subscribe()면 그 호출자.fn은 dep을 읽고 부작용을 내고 cleanup을 돌려주는 것까지. leaf가fn안에서 unbind/bind를 못 하는 것과 대칭. Subscribe/Unsubscribe/WeakSubscribe/WeakUnsubscribe는self._running또는self._cleanupRunning이면 error(level 2, "cannot change subscription from inside fn or cleanup"). [반영 뒤 감사 2라운드 정정] 처음엔_running하나로 적었는데 cleanup은rawRerun밖(Unsubscribe()·leafDestroying)에서도 돌아 그 안의self:Subscribe()가 가드를 지났다(재Unsubscribe만 레지스트리 가드에 걸렸다). 갈래 (a)_consumeCleanup이_running을 세움 / (b) UB 문서화 / (c) 별도 플래그 → 사용자 확정 (c)_cleanupRunning: "_running 으로 묶어 보는건 여전히 별로 괜찮은 이유가 없음. _cleanupRunning 같은걸 넣지 말아야할 이유가 없는것" — 한 플래그에 두 뜻을 얹지 않는다.fn안에서 자기 leafinst를 파괴하는 것은 UB(같은 감사 2라운드 미서술 항목):SignalBehavior = Immediate면Destroying콜백이fn도중 동기 발화해 cleanup이 영구 미소진. 사용자 확정: "bind/unbind 에 간접 영향을 주는건데, UB 인게 맞다는 생각." —effect-plan.md_bindDestroying아래 주석.H-143소멸 —fn안 자기 해제 지원 철회.Rerun본체는 원래 모양 (_consumeCleanup → fn → 저장),wasAlive도 사후 판정도 없다. 종료 신호는 다시 둘(강한Unsubscribe/ leaf 사망).rawRerun(self, force)/ 공개Rerun()분리 — "re"-run과 초기 설치는 다른 일. 공개Rerun은 진입에서canExecute게이트(죽은 핸들·안 묶인 핸들은 정의된 no-op,fire와 같은 규칙), 생성자는rawRerun(self, true)로 게이트만 건너뛴다. (A)에서는fn이 전이를 못 일으키므로force가 건너뛰는 것은 정확히 "아직 안 묶임" 하나. 사용자 확인: "unsub 뒤에 오는 rerun 은 그냥 실행 안 되는게 원래 정상적 형태" — 맞다.- 원샷("딱 한 번 처리하고 끝")은 지원 목록에서 빠진다 — 소유자가 밖에서
Unsubscribe하거나, 나중에Once류 슈가로 별도 결정. 코어에 넣지 않는다.
반영 대상: base/effect-plan.md(Rerun 의사코드 → rawRerun/Rerun, H-143
관련 서술 전부 — 꼬리 분기·"종료 신호 셋"·"fn 안 허용 호출" bullet·self를 주는
덕에 bullet, 네 진입점에 _running 가드, 실측 bullet), base/lifecycle-pattern.md
(포인터), ROADMAP.md M2 Effect 체크박스, conventions.md(원칙 사례로 추가 여부는
반영 시 판단), -round9-followup.md H-143 절에 소멸 배너, -round9.md 요약 표
H-143 행.
H-148 — 전제 정정: 문구 문제가 아니라 루트 마운트 표면의 부재 → Claim + D.Mapper (research 신설)
권고 (a)(전용 문구 철회)를 올렸더니 사용자가 더 큰 공백을 짚었다: "slot 은
물리 장치에 mount 할 방법이 거의 존재하지 않음. … PlayerGui 가 상위에 있고
거기에 GUI 를 여럿 바운딩 해야해서 Slot { Shop{} … } 하는게 안 될것 같은
느낌이 듦. 이건 Parent 이상의 문제인것 같아." → 이미 있는 트리를 quad가
소유하는 Claim(inst, D.Mapper.<Class> "Name" {…}) 제안(원문·확정·갈래 전량은
research/existing-mount-plan.md).
확정된 것: 방향 자체 / 디스크립터는 D.Mapper에(D.Frame에 직접 얹지 않음)
/ Claim DFS(내려가며 해석 → 자식부터 drive)는 derive 위의 한 겹 / 부기 대상
자식은 전부 매핑, 숏핸드(UI*)는 부기 밖 — quad가 직접 쓰거나 실제 객체로
매핑하거나 / 이름 중복·부재는 UB + debug 모드 seen 검사 / nativeFindChild
프로바이더 op, 순회는 quad-base / 다중 quad 한 트리 UB / M5 이후.
따름: 전용 문구 철회(일반 매치 실패 그대로), H-146 (a)의 "루트는 밖에서
.Parent =" 폐기(루트가 quad 소유). H-142 키 금지는 그대로.
미결 6개는 그 research 문서 §5 — 다음 배치 문항.
H-149 — Observer Subscribe/Unsubscribe도 인라인 (a)
사용자 확정: "a 로 가는게 맞는듯. weak 나 아닌거나 줄 차이가 그리 안 커서,
분리할 큰 이유가 없음." — Observer:Subscribe는 self:WeakSubscribe()에
위임하지 않고 게이트(canBound, error(…, 2))·플래그·약한 등록·강한 킵을 자기
안에 펼친다. Unsubscribe도 같은 이유로 self:WeakUnsubscribe() 위임을 풀어
양쪽 레지스트리 삭제를 직접 한다(콜론 위임 자체를 남기지 않는다 — H-144 (b)의
교훈). lifecycle-pattern.md의 "게이트는 한 번만 돈다 — Subscribe가
WeakSubscribe에 위임하므로" 문장은 "각 진입점이 자기 게이트를 한 번 돈다"로.
EffectHandle의 넷과 모양이 같아진다(거기에 H-147 (A)의 _running 가드가
하나 더 붙는 것만 다름 — Observer엔 _running이 없다).
반영 대상: base/lifecycle-pattern.md Observer 네 진입점 의사코드 + "게이트는
한 번만 돈다" bullet, base/effect-plan.md의 EffectHandle 블록 머리 주석(Observer
위임 서술 참조 부분).
H-150 — Effect._blocker 제거 (a)
사용자가 확인한 전제: "canExecute 와 별개로 처음 observer 생성에는 callback 이
실행되어야하는게 맞잖아. … 그러니까, Effect 의 canExecute 를 보겠다는거지? 그럼
그건 맞는것 같아." — 두 층이 다르다: 내부 Observer/Ref 콜백의 설치 발화는
그대로 일어나고(그 계약 불변), 그 콜백이 부르는 fire의 첫 줄
canExecute(self)의 self가 Effect 핸들이라 생성자 안(아직 안 묶임)에선
거기서 흡수된다. _blocker는 같은 흡수를 한 번 더 하려던 장치라 어떤 경로에서도
판정에 닿지 않는다(실측 t18). H-147 (A)로 fn이 생성자 안에서 자기를 묶을
수도 없어졌으니 "생성자 구간 = 안 묶임 = canExecute 거짓"은 불변식.
확정: _blocker 필드·On()/OffWithoutEmit() 제거. 생성자 주석은 "등록
즉시 1회는 Effect의 canExecute가 막는다"로. 7라운드 H-58의 지시("모든
옵저버와 callback 등록에 있어서 이를 수행해야할 것임")는 그 전제("등록 즉시
1회가 Rerun에 닿는다")가 성립하지 않았던 것으로 정정 배너.
반영 대상: base/effect-plan.md 생성자 의사코드·"확정 구조" 절·H-58 문단·
필드 목록, base/gate-plan.md 7번(_installing 잔재), ROADMAP.md M2 Effect
체크박스의 "선행: Blocker 기본 메커니즘" 문구(Effect 자체는 이제 Blocker 불필요
— Blocker.luau 선행 요구는 GateNode/Slot 쪽만).
H-151 — 게이트는 유보만 한다; Effect는 emit 받을 때만 _epochs를 갱신 (Refresh 캐치업 폐기)
사용자 확정: *"해당 우회는 더 크게 보면, 처음부터 Effect 가 Rerun 되는 경로라서 그건 맞아. 그리고 Observer 에서 받은 emit 의 epoch|{epoch:boolean} 를 받는 시점은 emit 되어야하는 시점이 맞기도 하지. 우린 애초에 Refersh 를 할 필요가 없는거야. 재진입은 초기 설정해주는 요소이고, 그건 처음 생성할때랑 같은거야. 사실 처음 생성할 때에도 Block 되어있던게 나중에 다시 들어오는 경로가 있어. 그 경우도 그냥 재실행 해주지. 또 observer 도 마찬가지야. block 은 단지 유보만 해줄뿐이라서.
- Effect 도 observer 랑 똑같게, 중간 state 랑 똑같게, emit 받을때에만 epoch 맵을 업데이트 하면 돼. 계약 추가로 끝나는 일로 보여"*
확정된 것:
- 계약(문항의 (a)): 게이트는 emit 경로만 미룬다. 재바인드/재구독 캐치업과
게이트 없는 형제 dep의 emit은 게이트를 거치지 않고, 그때
:Get()은 최신값. 유보됐던 emit이 나중에 풀려 들어오면 그냥 재실행 — 생성 직후 유보분이 들어오는 것과 같은 경로. _epochs는fire의Update(from)에서만 갱신 — Observer·중간 State와 같다._bindDestroying·resubscribeTail의_epochs:Refresh()와depsChanged는 폐기. 캐치업은if not self._installed then self:Rerun() end하나(초기 설치와 같은 뜻 — 소진돼 있으면 다시 설치).H-144의 "Refresh먼저" 하위 결정과 그 단축평가 캐비엇(ROADMAP.md·lifecycle-pattern.md·ref-plan.md에 오늘 넣은 두 줄 형태)은 전부 이 결정으로 소멸.- 따름: 죽어 있는 동안 떨어뜨린 emit은 다음 emit 때 리비전 차이로 잡힌다 —
Observer와 같은 정도의 "캐치업 없음"이고 계약으로 적는다. 재구독 뒤 게이트
flush가 같은 값으로 한 번 더 도는 것(
H-144재트레이싱의 케이스)은 허용 (유보가 풀리는 정상 재실행). EpochMap:Refresh는 State의rawInvalid == false경로(state-epoch-plan.md§4)에만 남는다 — Effect 소비자 삭제.
반영 대상: base/effect-plan.md(_bindDestroying·resubscribeTail·H-65
캐치업 서술·H-144 블록 주석), base/gate-plan.md(계약 문장),
base/debounce-throttle-plan.md 11절, base/lifecycle-pattern.md 357행 주석,
base/ref-plan.md 244·284·443행(Refresh 전제 서술 — 284행의 "다음 Refresh()
때에야" 추론은 재검토), ROADMAP.md M2·M6 캐치업 문구, -round9-followup.md
H-144 절에 소멸 배너.
H-158 — state:Block(blocker) 슈가 (이 대화에서 나옴, 미결)
사용자: ":Block 은 이제 없는거 아냐? Apply(Blocker) 이긴 할꺼야 (표면은
:Gate 만 남아 Blocker 는 Apply 슈거와 Policy 를 주는 프리미티브)" — 확인 결과
base/blocker-plan.md에 state:Block(blocker)가 state:Gate(function(emit) return b:Policy(emit) end) 위의 슈가로 아직 있다(60·94·163·178·207행).
갈래: (a) :Block 폐기, Blocker가 state:Apply(factory) 프로토콜을 만족해
state:Apply(blocker)로 / (b) :Block 유지. 권고 (a) — 동사 하나가 줄고
"Blocker = Policy를 주는 프리미티브 + Apply 슈가"로 뜻이 하나. 반영 시
blocker-plan.md·gate-plan.md·debounce-throttle-plan.md의 :Block 예시 전부.
H-153 — Store 예약 이름 런타임 가드 (a)
사용자 확정: "나도 a 동의." — 생성자의 isSource 순회에 if RESERVED[k] then error(…, 2) end, store:Of(name)에 같은 검사. H-122 화이트리스트와 같은
자리·같은 논거(조용히 받고 엉뚱한 자리에서 죽는 것을 fail-fast로). 부수로
스케치의 "그림자 테이블"은 store 자신(I)으로 못박는다 — store.key가 평범한
레코드 필드라는 계약과 맞고, 메소드는 __index에 있어 Names()가 안 센다.
RESERVED는 메소드 이름 집합(Of/Names/… — 구현 시 __index 테이블의 키에서
자동 도출하면 두 곳에 안 적어도 된다, 권고).
반영 대상: base/store-plan.md 구현 스케치·Of 절·__reservedCheck
주석(동적 키는 런타임 가드가 맡는다고 명시), ROADMAP.md M2 Store 체크박스.
H-154 — InstanceChildHandler retractor에 같은 값 dedup (a)
사용자 확정: "a 동의. … 간단한 dedup 이고 말단 핸들러가 v 를 정확히 알아서
retract 가 정확히 해소되는 부분이 맞네." — retractor 첫 줄에 if nextValue == v then return end(SlotHandler 동형). 같은 값 재발행에 Parent = nil → inst와
recompute 2회가 사라진다. 반영 대상: base/dispatch-core-plan.md H-134
문단, ROADMAP.md M5 InstanceChild.luau 체크박스.
반영 기록 (2026-08-28)
base/: effect-plan.md(rawRerun/Rerun 분리, _blocker 제거, Refresh 캐치업
폐기, 네 진입점 _running 가드, H-143 관련 서술 전부) / lifecycle-pattern.md
(Observer Subscribe/Unsubscribe 인라인, 캐치업 주석) / gate-plan.md(7번 정정,
조립 첫 줄 브랜드, "계약 — 게이트는 emit 경로만 미룬다" 절 신설) /
debounce-throttle-plan.md(7절 H-32 문단, 11절 Effect) / ref-plan.md(Refresh
전제 셋) / store-plan.md(그림자 = store 자신, 예약 이름 가드 둘, 빈 Store 실측) /
dispatch-core-plan.md(InstanceChildHandler dedup) / bind-system-plan.md(전용 문구
철회, 루트 예외 폐기 배너) / slot-plan.md(각주 둘). archive/existing-instance-bind-rejected.md
부활 배너. ROADMAP.md 배너·M2 Effect/GateNode/Store·M5·M6×3·M8·M11·백로그.
-round9-followup.md/-round9.md의 H-143/H-144/H-146 소멸·정정 배너.
남은 미결: H-158(state:Block 슈가 폐기 → state:Apply(blocker), 권고만) —
question.md에; research/existing-mount-plan.md §5 갈래 6개 — 다음 배치.
감사 루프 (2026-08-28, 10라운드 반영분)
quad-doc-auditor 한 턴에 하나, diff 범위, 각도 교체. 새 발견 3→5→2→3→1→0
(6라운드에서 수렴). 라운드별: 1 문구 잔존(ROADMAP 필드 목록 _blocker / 콜론 위임
현재형 둘 / rawRerun 선언 순서) · 2 의미론(README effect-plan.md 행 /
StateBrand:add→register / 예약 이름 도출 권고와 팬텀 필드 모순 /
_running 가드가 Unsubscribe·Destroying의 cleanup을 안 덮음 → 사용자 결정
_cleanupRunning / fn 안 자기 leaf 파괴 미서술 → UB) · 3 수정분 재검토(Store:Of
스니펫의 shadow 업밸류 / UB 괄호) · 4 전체 diff(gate-plan.md 새 절의 "4절"
인용에 문서명 / 인용문 한 구절 / README archive 행 부활 포인터) · 5 수렴
확인(CLAUDE.md 볼드 짝) · 6 형식·라벨 정합 0건.
/code-review high (2026-08-28, 10라운드 반영분 + 감사 6라운드 뒤)
10건. 일곱 반영, 셋은 새 메커니즘·기존 결정 변경이라 문항(-round10.md
H-159~H-161, question.md).
반영한 일곱:
bindLifetime경로에 (A)의 강제가 없었다 —fn안New "Frame" { self }로 자기를 leaf에 묶을 수 있었다._bindDestroying첫 줄에isRunning가드.guardNotRunning헬퍼 안의error(…, 2)가 헬퍼 호출 줄(quad 내부)을 가리킴 (H-104,H-149와 같은 이유) — 술어isRunning만 헬퍼로,error는 다섯 본문에 인라인(level 3선례를 만들지 않음).lifecycle-pattern.md의 헬퍼로 빼도 된다 문장에 단서.RESERVED가 어디에도 정의되지 않았고 예약 이름 셋이 네 곳에 리터럴 —Of절에local RESERVED = {…}단일 소스, 나머지는 가리키기만.H-154서술 정확화 — dedup이 없애는 건 물리 detach/attach와recompute1회 (2→1); process 쪽 skip은 옛 값을 몰라 안 둔다(SlotHandler의claimOwnerAt과 다른 점 명시).- 산문 ↔ 의사코드 모순 — 기존 플래그 재사용, 새 상태 없음 / 재
Unsubscribe는 레지스트리 가드 /fn안 직접 호출은 게이트하지 않는다 /lifecycle-pattern.md의_running가드만 — 전부_cleanupRunning·진입 게이트 반영. -round9-followup.md진행 표 행·H-146절에 2026-08-28 소멸 표시,todos.md의Refresh먼저·뒤집힌 것 둘→셋, 갈래 6개 다섯 곳 → 개수는 §5가 소스.- stale 문장 —
effect-plan.md의 게이트가 아니라Blocker를 쓰는 이유 / 포탈 근거를H-64캐치업으로 적은 것(재마운트는_installed참이라 캐치업이 아예 없다) /source-state-plan.md의 구현이 한 벌(위임 아님으로 정정).