qa: 10라운드 후속 H-158~H-162 반영 — :Block 폐기(__apply), _rerunRequired 홀드(_installed 흡수), Claim M5, Void export
- H-158 state:Block → state:Apply(blocker), Blocker:__apply 메소드형(호출 규약 명시) - H-159 사용자 제안 _rerunRequired 홀드: fire=Update→Rerun, rawRerun이 실행 불가 상태의 요청을 홀드, _installed 폐기, Observer 대칭(전파 루프 else + bind/subscribe 1회 발화, 생성자 순서 fn→_subs) - H-160 홀드로 정정 + "error 나면 그 Effect는 죽는다" 계약 / H-161 Claim M5 스코프 / H-162 Void.luau 잎 모듈 - 감사 7→6→1→0, /code-review high 10건 중 8 반영, 둘(H-163 Slot 내부 Observer×홀드, H-164 emitFrom nil)은 문항으로 Co-authored-by: qwreey <me@qwreey.moe> Claude-Session: https://claude.ai/code/session_01546hjsYNLSMZdHdPyTZaGb
This commit is contained in:
parent
ae34cfa316
commit
ecc6b0e538
27 changed files with 415 additions and 189 deletions
File diff suppressed because one or more lines are too long
|
|
@ -278,20 +278,21 @@ quad/
|
||||||
│ │ ├── init.luau # process 엔진 — `chains`(inst,k별 인덱스 배열, 슬롯마다 {handler, retractor}) + 하강 diff(핸들러가 같으면 그 자리 클로저에 새 값을 넘기고 재process, 다르면 그 자리부터 retractFrom) + 3-인자 `retractFrom(inst,k,index)` (`dispatch-core-plan.md` "Dispatch 체인" 절, 2026-08-08 신설 → 2026-08-13 다섯 번째 세션 인덱스화 → 같은 날 열네 번째 세션 하강 diff)
|
│ │ ├── init.luau # process 엔진 — `chains`(inst,k별 인덱스 배열, 슬롯마다 {handler, retractor}) + 하강 diff(핸들러가 같으면 그 자리 클로저에 새 값을 넘기고 재process, 다르면 그 자리부터 retractFrom) + 3-인자 `retractFrom(inst,k,index)` (`dispatch-core-plan.md` "Dispatch 체인" 절, 2026-08-08 신설 → 2026-08-13 다섯 번째 세션 인덱스화 → 같은 날 열네 번째 세션 하강 diff)
|
||||||
│ │ ├── Handler.luau # 핸들러 계약 타입(isHandlable/priority/process — process가 자기 retract 클로저를 반환)
|
│ │ ├── Handler.luau # 핸들러 계약 타입(isHandlable/priority/process — process가 자기 retract 클로저를 반환)
|
||||||
│ │ ├── StoreBind.luau # store 값 재귀 재실행 로직(범용, 엔진 무관)
|
│ │ ├── StoreBind.luau # store 값 재귀 재실행 로직(범용, 엔진 무관)
|
||||||
│ │ ├── None.luau # NoneHandler(`v==None`을 `nil`로 바꿔 재귀만 — 배열/해시 구분 없음) + NilHandler(`k=number and v==nil` 전용 말단, `setLength(0)`/`setOffsetSource(None)` 등록) (`base/dispatch-core-plan.md`의 "`None` 센티널"/"`NilHandler`" 절, 2026-08-18 재설계 — `drive`의 `None` 스킵 분기 폐기)
|
│ │ ├── None.luau # (**[2026-08-28 `H-162`]** 센티널 `None`과 같은 급으로 quad-base가 export하는 단일 no-op 함수 **`Void`**는 의존 없는 잎 모듈 `Void.luau`(아래)에 정의하고 최상위 `init.luau`가 재export한다 — `Dispatch/init.luau`가 파일 스코프 `local NOOP = Void`로 쓰므로 최상위에 두면 순환 require(`/code-review` 지적); 핸들러 retractor·cleanup 자리의 `function() end`를 전부 대체) NoneHandler(`v==None`을 `nil`로 바꿔 재귀만 — 배열/해시 구분 없음) + NilHandler(`k=number and v==nil` 전용 말단, `setLength(0)`/`setOffsetSource(None)` 등록) (`base/dispatch-core-plan.md`의 "`None` 센티널"/"`NilHandler`" 절, 2026-08-18 재설계 — `drive`의 `None` 스킵 분기 폐기)
|
||||||
│ │ ├── Leaf.luau # (i:number, v=Ref/Observer/Effect/PreRef/PostRef) children-array leaf 매칭 Handler(일반 Ref 매치는 `isRef(v) and not isPreRef(v) and not isPostRef(v)`, Observer/Effect는 `ObserverEffectLeafHandler` 하나가 `type(k)=="number" and (isObserver(v) or isEffect(v))`로 같이 매치 — `base/source-state-plan.md` "Observer/Effect Leaf dedup" 절, 2026-08-14 열두 번째 세션), StoreBind와 같은 층위(범용/엔진무관, 2026-08-08 두 번째 세션 확정)
|
│ │ ├── Leaf.luau # (i:number, v=Ref/Observer/Effect/PreRef/PostRef) children-array leaf 매칭 Handler(일반 Ref 매치는 `isRef(v) and not isPreRef(v) and not isPostRef(v)`, Observer/Effect는 `ObserverEffectLeafHandler` 하나가 `type(k)=="number" and (isObserver(v) or isEffect(v))`로 같이 매치 — `base/source-state-plan.md` "Observer/Effect Leaf dedup" 절, 2026-08-14 열두 번째 세션), StoreBind와 같은 층위(범용/엔진무관, 2026-08-08 두 번째 세션 확정)
|
||||||
│ │ ├── Tag.luau # TagHandler — 이름별 참조 카운트(`tagNameMap`), 실제 호출은 주입된 addTag/removeTag(inst, {string}). `HANDLER_PRIORITY_FALLBACK`에는 이걸 감싸는 `TagFallbackHandler`가 quad-base 자신에 의해 등록됨(**[재역전, 2026-08-18]** 백엔드 팩토리가 아님)(`base/tag-plan.md`, 2026-08-13 열네 번째 세션 base로 이동)
|
│ │ ├── Tag.luau # TagHandler — 이름별 참조 카운트(`tagNameMap`), 실제 호출은 주입된 addTag/removeTag(inst, {string}). `HANDLER_PRIORITY_FALLBACK`에는 이걸 감싸는 `TagFallbackHandler`가 quad-base 자신에 의해 등록됨(**[재역전, 2026-08-18]** 백엔드 팩토리가 아님)(`base/tag-plan.md`, 2026-08-13 열네 번째 세션 base로 이동)
|
||||||
│ │ ├── AttributeKey.luau # AttributeKeyHandler — 이름 claim(`nameClaims`, 소유권 충돌 즉시 error) + 주입된 setAttribute(inst,name,v) 호출, `None`→nil은 재디스패치로 자동(`base/attribute-plan.md` "이름 소유권" 절). `HANDLER_PRIORITY_FALLBACK`에는 이걸 감싸는 `AttributeKeyFallbackHandler`가 quad-base 자신에 의해 등록됨(**[재역전, 2026-08-18]**)
|
│ │ ├── AttributeKey.luau # AttributeKeyHandler — 이름 claim(`nameClaims`, 소유권 충돌 즉시 error) + 주입된 setAttribute(inst,name,v) 호출, `None`→nil은 재디스패치로 자동(`base/attribute-plan.md` "이름 소유권" 절). `HANDLER_PRIORITY_FALLBACK`에는 이걸 감싸는 `AttributeKeyFallbackHandler`가 quad-base 자신에 의해 등록됨(**[재역전, 2026-08-18]**)
|
||||||
│ │ ├── Attribute.luau # AttributeGroupHandler — 그룹 전용 키(비공개 GetKey)로 이름마다 AttributeKey 경로에 인덱스 1 위임, 클로저가 자기 키 전부 retractFrom(`base/attribute-plan.md` "메커니즘" 절). `AttributeGroupFallbackHandler`가 같은 방식으로 감쌈
|
│ │ ├── Attribute.luau # AttributeGroupHandler — 그룹 전용 키(비공개 GetKey)로 이름마다 AttributeKey 경로에 인덱스 1 위임, 클로저가 자기 키 전부 retractFrom(`base/attribute-plan.md` "메커니즘" 절). `AttributeGroupFallbackHandler`가 같은 방식으로 감쌈
|
||||||
│ │ ├── Slot.luau # SlotHandler — 마운트/언마운트 + 그 자리 Length/Offset 부기(값 타입 본체는 위 top-level `Slot.luau`)
|
│ │ ├── Slot.luau # SlotHandler — 마운트/언마운트 + 그 자리 Length/Offset 부기(값 타입 본체는 위 top-level `Slot.luau`)
|
||||||
│ │ └── Modifier.luau # [2026-08-24 `H-35`] ProcessedModifierHandler — flatten이 소진한 자리를 캐치해 `setOffsetSource(None)`/`setLength(0)`만 등록하는 nop 핸들러(`base/modifier-plan.md`)
|
│ │ └── Modifier.luau # [2026-08-24 `H-35`] ProcessedModifierHandler — flatten이 소진한 자리를 캐치해 `setOffsetSource(None)`/`setLength(0)`만 등록하는 nop 핸들러(`base/modifier-plan.md`)
|
||||||
|
│ ├── Void.luau # **[2026-08-28 `H-162`]** `return function() end` 한 줄 — 단일 no-op. 의존 없는 잎(`None`/`Brand`/`Relate`와 같은 급), `Dispatch/*`·핸들러·최상위 `init.luau`가 require
|
||||||
│ ├── Relate.luau # inst를 weak 키로 하는 범용 릴레이션(`SetWeak`/`GetWeak`/`SetStrong`/`GetStrong`), 비싱글톤 생성자(`base/relate-plan.md`) — 구 PerInstanceState/perInstanceState 대체
|
│ ├── Relate.luau # inst를 weak 키로 하는 범용 릴레이션(`SetWeak`/`GetWeak`/`SetStrong`/`GetStrong`), 비싱글톤 생성자(`base/relate-plan.md`) — 구 PerInstanceState/perInstanceState 대체
|
||||||
│ ├── LifetimeHandle.luau # `bindLifetime(inst,value)`/`unbindLifetime(value)`/`canBound(value)`/`canExecute(value)` 탑레벨 함수 "인터페이스"(타입/계약만), 내부는 Relate 사용(`base/lifecycle-pattern.md`)
|
│ ├── LifetimeHandle.luau # `bindLifetime(inst,value)`/`unbindLifetime(value)`/`canBound(value)`/`canExecute(value)` 탑레벨 함수 "인터페이스"(타입/계약만), 내부는 Relate 사용(`base/lifecycle-pattern.md`)
|
||||||
│ ├── Ref.luau # 범용 값 박스(.Value/.Revision 읽기 + :Set()/:WeakCallback()/:Callback()/:Uncallback()/:Wait(); `Epoch`를 만족 — `base/ref-plan.md`. **[2026-08-27 `H-128`]** `:Wait`·핸들러 뺀 최소형은 M2 공통 기반), `Ref(default)`를 children 배열 숫자 슬롯에 직접 놓으면 (v=Ref) 매치 핸들러가 바인드 — 별도 CreatedRef 래퍼 없음
|
│ ├── Ref.luau # 범용 값 박스(.Value/.Revision 읽기 + :Set()/:WeakCallback()/:Callback()/:Uncallback()/:Wait(); `Epoch`를 만족 — `base/ref-plan.md`. **[2026-08-27 `H-128`]** `:Wait`·핸들러 뺀 최소형은 M2 공통 기반), `Ref(default)`를 children 배열 숫자 슬롯에 직접 놓으면 (v=Ref) 매치 핸들러가 바인드 — 별도 CreatedRef 래퍼 없음
|
||||||
│ ├── PreRef.luau # Ref 런타임 재사용 + children 배열 전용, Modifier/Store 타입 차단, 호이스팅되는 pre-pass 특수화(별도 파일, `ref-plan.md` "PreRef 신설" 절, 2026-08-07 여섯 번째 세션에서 분리)
|
│ ├── PreRef.luau # Ref 런타임 재사용 + children 배열 전용, Modifier/Store 타입 차단, 호이스팅되는 pre-pass 특수화(별도 파일, `ref-plan.md` "PreRef 신설" 절, 2026-08-07 여섯 번째 세션에서 분리)
|
||||||
│ ├── PostRef.luau # PreRef의 거울상 — 같은 Ref 런타임/제약, 같은 pre-pass가 수집만 하고 두 패스가 전부 끝난 뒤 fire(`ref-plan.md` "`PostRef`" 절, 2026-08-14 아홉 번째 세션 확정)
|
│ ├── PostRef.luau # PreRef의 거울상 — 같은 Ref 런타임/제약, 같은 pre-pass가 수집만 하고 두 패스가 전부 끝난 뒤 fire(`ref-plan.md` "`PostRef`" 절, 2026-08-14 아홉 번째 세션 확정)
|
||||||
│ ├── LifecycleHooks.luau # OnCreated/OnRendered/OnDestroyed — PreRef/PostRef/Effect를 반환하는 순수 팩토리 슈가(`base/lifecycle-hooks-plan.md`), 새 타입/Dispatch 개념 없음
|
│ ├── LifecycleHooks.luau # OnCreated/OnRendered/OnDestroyed — PreRef/PostRef/Effect를 반환하는 순수 팩토리 슈가(`base/lifecycle-hooks-plan.md`), 새 타입/Dispatch 개념 없음
|
||||||
│ └── init.luau
|
│ └── init.luau # 패키지 최상위 export — `Quad` 값 테이블(`New`/`Source`/…/`None`/**`Void`**(재export — 정의는 위 `Void.luau`, `H-162`))
|
||||||
└── quad-roblox/
|
└── quad-roblox/
|
||||||
├── pesde.toml # quad-base가 아니라 quad-types에만 workspace 의존
|
├── pesde.toml # quad-base가 아니라 quad-types에만 workspace 의존
|
||||||
└── src/
|
└── src/
|
||||||
|
|
|
||||||
|
|
@ -342,7 +342,7 @@ end
|
||||||
상위에 있고 거기에 GUI 를 여럿 바운딩 해야해서 `Slot { Shop{} … }` 하는게 안
|
상위에 있고 거기에 GUI 를 여럿 바운딩 해야해서 `Slot { Shop{} … }` 하는게 안
|
||||||
될것 같은 느낌이 듦. 이건 Parent 이상의 문제인것 같아."* 루트는 밖에서
|
될것 같은 느낌이 듦. 이건 Parent 이상의 문제인것 같아."* 루트는 밖에서
|
||||||
`Parent`를 만지는 게 아니라 **quad가 `Claim`으로 소유**한다(PlayerGui·`Clone()`
|
`Parent`를 만지는 게 아니라 **quad가 `Claim`으로 소유**한다(PlayerGui·`Clone()`
|
||||||
사본·Studio GUI — 그 research 문서가 소스, M5 이후). 그러면 `.Parent =`를
|
사본·Studio GUI — 그 research 문서가 소스, **M5 스코프**(`H-161`)). 그러면 `.Parent =`를
|
||||||
사용자가 쓸 자리 자체가 없어진다. 아래는 폐기 전 서술:
|
사용자가 쓸 자리 자체가 없어진다. 아래는 폐기 전 서술:
|
||||||
**[2026-08-27 확정, 9라운드 `H-146`] 루트는 이 금지의 범위 밖이다 —
|
**[2026-08-27 확정, 9라운드 `H-146`] 루트는 이 금지의 범위 밖이다 —
|
||||||
quad 트리의 최상위를 quad 밖 부모에 붙이는 건 사용자가 밖에서 `.Parent =`로
|
quad 트리의 최상위를 quad 밖 부모에 붙이는 건 사용자가 밖에서 `.Parent =`로
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,20 @@
|
||||||
문제를 콜스택/코루틴이 아니라 사용자가 들고 있는 "값"으로 표현**해서 이
|
문제를 콜스택/코루틴이 아니라 사용자가 들고 있는 "값"으로 표현**해서 이
|
||||||
위험을 구조적으로 우회한다.
|
위험을 구조적으로 우회한다.
|
||||||
|
|
||||||
**store 개발(M2)과 밀접하게 연관됨** — `state:Block(blocker)`가 State
|
**⭐ [2026-08-28 확정, 10라운드 `H-158`] `state:Block(blocker)` 동사는 폐기 — 배선은
|
||||||
|
`state:Apply(blocker)`다.** 사용자: *"이제 Compute 와 유사하게 Gate 만 놓일 뿐, Apply 로
|
||||||
|
Blocker 처리가 가능함. 왜냐면 펑터와 적용성펑터를 전부 허용하니까 ( map|{...map} ) 키는
|
||||||
|
`__apply` 로 하기로 했던거로 기억중임."* — `source-state-plan.md`의 "`state:Apply(factory)`"
|
||||||
|
절이 애플리커티브 팩토리를 **지정된 필드**로 받기로 확정해뒀고(필드 이름은 "구현 시"로
|
||||||
|
열려 있었다 — 이 발언으로 **`__apply`**), `Blocker`가 그 필드를 **메소드**로 노출한다 — `function Blocker:__apply(state)
|
||||||
|
return state:Gate(function(emit) return self:Policy(emit) end) end`(`self` = 이 blocker
|
||||||
|
인스턴스; 호출 규약은 `source-state-plan.md`의 "`state:Apply(factory)`" 절). 아래 본문의
|
||||||
|
`state:Apply(blocker)`는 전부 옛 `state:Block(blocker)`를 기계 치환한 것 — 의미는 같다
|
||||||
|
(2026-08-21에 *"`Debounce`/`Throttle`/`Blocker`가 전부 같은 계약을 만족하게 된다"*고
|
||||||
|
예고한 그것). 이 문서는 **`Blocker` = `Policy`를 주는 프리미티브 + `__apply` 슈가**를
|
||||||
|
서술한다.
|
||||||
|
|
||||||
|
**store 개발(M2)과 밀접하게 연관됨** — `state:Apply(blocker)`가 State
|
||||||
위에 얹히는 메소드이므로 `base/source-state-plan.md`의 Source/State
|
위에 얹히는 메소드이므로 `base/source-state-plan.md`의 Source/State
|
||||||
온톨로지, 특히 push-invalidate/pull-recompute 전파 모델(`base/source-state-plan.md` "전파 모델 확정" 절)을 전제로 함.
|
온톨로지, 특히 push-invalidate/pull-recompute 전파 모델(`base/source-state-plan.md` "전파 모델 확정" 절)을 전제로 함.
|
||||||
**[2026-08-24 재확정] 구현 마일스톤은 다시 M2다 — "State와 같은
|
**[2026-08-24 재확정] 구현 마일스톤은 다시 M2다 — "State와 같은
|
||||||
|
|
@ -57,14 +70,15 @@ blocker:IsOn() -> boolean -- [2026-08-18 신설] `self.IsBlocked`를
|
||||||
-- 얇은 조회 메소드 — 필드 `IsBlocked`는 그대로 유지(아래
|
-- 얇은 조회 메소드 — 필드 `IsBlocked`는 그대로 유지(아래
|
||||||
-- "이름 확정" 참고), 호출부 가독성만을 위한 추가.
|
-- "이름 확정" 참고), 호출부 가독성만을 위한 추가.
|
||||||
|
|
||||||
state:Block(blocker) -> state -- 새 gated state 반환. **호출되는 즉시**(나중에
|
state:Apply(blocker) -> state -- [2026-08-28 `H-158`] `Blocker.__apply`를 통한 `state:Apply` —
|
||||||
|
-- 옛 `state:Block(blocker)`. 새 gated state 반환. **호출되는 즉시**(나중에
|
||||||
-- 처음 블록될 때가 아니라) onunblock 핸들을
|
-- 처음 블록될 때가 아니라) onunblock 핸들을
|
||||||
-- blocker의 weak-키 셋에 등록(아래 "onunblock 핸들 보관").
|
-- blocker의 weak-키 셋에 등록(아래 "onunblock 핸들 보관").
|
||||||
blocker:Policy(emit) -> onUpstreamEmit
|
blocker:Policy(emit) -> onUpstreamEmit
|
||||||
-- [2026-08-24 신설] 이 blocker의 게이트 정책을
|
-- [2026-08-24 신설] 이 blocker의 게이트 정책을
|
||||||
-- **값으로** 돌려준다. `state:Block(b)`가
|
-- **값으로** 돌려준다. `state:Apply(b)`가
|
||||||
-- 내부에서 쓰는 바로 그것:
|
-- 내부에서 쓰는 바로 그것:
|
||||||
-- state:Block(b)
|
-- state:Apply(b)
|
||||||
-- == state:Gate(function(emit) return b:Policy(emit) end)
|
-- == state:Gate(function(emit) return b:Policy(emit) end)
|
||||||
-- **[2026-08-24 정정]** 여기 `state:Gate(b.Policy)`라
|
-- **[2026-08-24 정정]** 여기 `state:Gate(b.Policy)`라
|
||||||
-- 적었었는데 그건 언바운드 메소드라 `emit`이 `self`
|
-- 적었었는데 그건 언바운드 메소드라 `emit`이 `self`
|
||||||
|
|
@ -91,10 +105,10 @@ gated state의 동작:
|
||||||
안 한다"**로 일반화된다(`base/gate-plan.md`의 8번). 구현 시 두 개를 따로
|
안 한다"**로 일반화된다(`base/gate-plan.md`의 8번). 구현 시 두 개를 따로
|
||||||
들지 말 것.
|
들지 말 것.
|
||||||
|
|
||||||
**⭐ [2026-08-21 신설] `state:Block(blocker)`는 `state:Gate(setup)` 위에
|
**⭐ [2026-08-21 신설] `state:Apply(blocker)`는 `state:Gate(setup)` 위에
|
||||||
얹힌다.** 위 "gated state의 동작"은 `Blocker`만의 특수 노드가 아니라
|
얹힌다.** 위 "gated state의 동작"은 `Blocker`만의 특수 노드가 아니라
|
||||||
`base/gate-plan.md`가 확정한 **`GateNode`**(`ComputeNode`와 같은 층위)의
|
`base/gate-plan.md`가 확정한 **`GateNode`**(`ComputeNode`와 같은 층위)의
|
||||||
정책 하나다 — `Block`이 내부에서 `self:Gate(policy)`를 부르고, 그 `policy`가
|
정책 하나다 — `Blocker.__apply`(옛 `Block`)가 내부에서 `state:Gate(policy)`를 부르고, 그 `policy`가
|
||||||
`blocker.IsBlocked`를 보고 `emit()`을 부를지 `HasBlockedEmit`만 세울지
|
`blocker.IsBlocked`를 보고 `emit()`을 부를지 `HasBlockedEmit`만 세울지
|
||||||
정한다. `Debounce`/`Throttle`도 같은 자리에 다른 정책으로 들어간다.
|
정한다. `Debounce`/`Throttle`도 같은 자리에 다른 정책으로 들어간다.
|
||||||
|
|
||||||
|
|
@ -118,20 +132,20 @@ gated state의 동작:
|
||||||
걸렸던 **모든 gated state와 그 상류 체인을 영원히 살려둔다**(`:List`
|
걸렸던 **모든 gated state와 그 상류 체인을 영원히 살려둔다**(`:List`
|
||||||
항목마다 게이트를 무는 패턴에서 직행 누수).
|
항목마다 게이트를 무는 패턴에서 직행 누수).
|
||||||
3. **`Off()`/`OffWithoutEmit()`은 스냅샷을 뜬 뒤 순회한다** — 순회 중
|
3. **`Off()`/`OffWithoutEmit()`은 스냅샷을 뜬 뒤 순회한다** — 순회 중
|
||||||
새 등록(핸들 → flush → 하류 Observer가 `state:Block(b)`를 새로 만듦)이
|
새 등록(핸들 → flush → 하류 Observer가 `state:Apply(b)`를 새로 만듦)이
|
||||||
`pairs`에서 미정의이기 때문. `Ref.Callbacks`/State 구독자 집합과 같은
|
`pairs`에서 미정의이기 때문. `Ref.Callbacks`/State 구독자 집합과 같은
|
||||||
처방(`base/source-state-plan.md`의 `H-23` 확정).
|
처방(`base/source-state-plan.md`의 `H-23` 확정).
|
||||||
|
|
||||||
**⭐ [2026-08-24 신설, 6라운드 손 트레이싱 `H-33`/`H-49`] 그 정책을 값으로
|
**⭐ [2026-08-24 신설, 6라운드 손 트레이싱 `H-33`/`H-49`] 그 정책을 값으로
|
||||||
꺼내는 표면 `blocker:Policy(emit)`을 추가한다** — 표면이 하나 늘고,
|
꺼내는 표면 `blocker:Policy(emit)`을 추가한다** — 표면이 하나 늘고,
|
||||||
`Blocker()` 생성자와 `state:Block`은 그대로다.
|
`Blocker()` 생성자와 배선(`state:Apply(blocker)` — 옛 `state:Block`)은 그대로다.
|
||||||
|
|
||||||
- **왜 필요한가**: `Debounce`/`Throttle`이 "언제 통과시킬지"를 정하면서
|
- **왜 필요한가**: `Debounce`/`Throttle`이 "언제 통과시킬지"를 정하면서
|
||||||
실제 emit/보류 배선은 Blocker에 위임할 수 있어야 한다. 정책을 값으로 낼 수
|
실제 emit/보류 배선은 Blocker에 위임할 수 있어야 한다. 정책을 값으로 낼 수
|
||||||
있으면 그게 그냥 함수 합성이 된다 — `setup`이 곧
|
있으면 그게 그냥 함수 합성이 된다 — `setup`이 곧
|
||||||
`(emit) -> onUpstreamEmit`이라 타입이 이미 맞는다.
|
`(emit) -> onUpstreamEmit`이라 타입이 이미 맞는다.
|
||||||
- **정책이 하는 일은 안 바뀐다** — `Policy(emit)`을 부르는 시점에 onunblock
|
- **정책이 하는 일은 안 바뀐다** — `Policy(emit)`을 부르는 시점에 onunblock
|
||||||
핸들이 등록되므로(지금 `state:Block`이 하던 것과 같은 자리), `Off()`가
|
핸들이 등록되므로(지금 `state:Apply(blocker)`가 하던 것과 같은 자리), `Off()`가
|
||||||
풀 때 그 `emit`이 정확히 1회 불린다.
|
풀 때 그 `emit`이 정확히 1회 불린다.
|
||||||
- **`Debounce`/`Throttle`은 Blocker를 사적으로 하나 갖는다** — 적용 핸들당
|
- **`Debounce`/`Throttle`은 Blocker를 사적으로 하나 갖는다** — 적용 핸들당
|
||||||
하나(커링 결과가 여러 곳에 적용될 수 있으므로 `Apply` 시점 생성).
|
하나(커링 결과가 여러 곳에 적용될 수 있으므로 `Apply` 시점 생성).
|
||||||
|
|
@ -160,7 +174,7 @@ gated state의 동작:
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
local blocker = Blocker()
|
local blocker = Blocker()
|
||||||
local gated3 = state3:Block(blocker) -- 소비자는 gated3를 구독
|
local gated3 = state3:Apply(blocker) -- 소비자는 gated3를 구독
|
||||||
|
|
||||||
blocker:On()
|
blocker:On()
|
||||||
state1:Set(1) -- state3 무효화 → gated3로 전파 시도 → 블록됨 → HasBlockedEmit=true
|
state1:Set(1) -- state3 무효화 → gated3로 전파 시도 → 블록됨 → HasBlockedEmit=true
|
||||||
|
|
@ -173,9 +187,9 @@ blocker:Off() -- onunblock 핸들 실행 → HasBlockedEmit 확인 → 딱 한
|
||||||
가까운 지점)에 거는 게 원칙 — 소스가 여러 개든, 하나가 한 주기에 여러 번
|
가까운 지점)에 거는 게 원칙 — 소스가 여러 개든, 하나가 한 주기에 여러 번
|
||||||
바뀌든 상관없이 이 지점 하나만 지키면 됨. 소스 쪽에 각각 거는 게 아니다.
|
바뀌든 상관없이 이 지점 하나만 지키면 됨. 소스 쪽에 각각 거는 게 아니다.
|
||||||
|
|
||||||
## `state:Block()` 없이 직접 쓰는 두 번째 용례 — base 내부 부기 게이팅 (2026-08-18 신설)
|
## `state:Apply(blocker)` 없이 직접 쓰는 두 번째 용례 — base 내부 부기 게이팅 (2026-08-18 신설)
|
||||||
|
|
||||||
지금까지 위 예시는 전부 `state:Block(blocker)`로 만든 **gated state**를
|
지금까지 위 예시는 전부 `state:Apply(blocker)`로 만든 **gated state**를
|
||||||
경유하는 사용자 대상 패턴이었다. `base/dispatch-core-plan.md`의
|
경유하는 사용자 대상 패턴이었다. `base/dispatch-core-plan.md`의
|
||||||
"Length/Offset" 절이 `recompute`의 크래시(`RC-1`, 배열 위치가 하나씩
|
"Length/Offset" 절이 `recompute`의 크래시(`RC-1`, 배열 위치가 하나씩
|
||||||
순차 등록되는 동안 아직 등록 안 된 자리를 읽어 산술 에러가 나는 경로)를
|
순차 등록되는 동안 아직 등록 안 된 자리를 읽어 산술 에러가 나는 경로)를
|
||||||
|
|
@ -186,7 +200,7 @@ blocker:Off() -- onunblock 핸들 실행 → HasBlockedEmit 확인 → 딱 한
|
||||||
배치 등록 비용(O(N²)→O(N)) 절감으로 바뀌었을 뿐. 콜백 안에서
|
배치 등록 비용(O(N²)→O(N)) 절감으로 바뀌었을 뿐. 콜백 안에서
|
||||||
`blocker:IsOn()`을 직접 확인하고 스스로 전파를 건너뛰는
|
`blocker:IsOn()`을 직접 확인하고 스스로 전파를 건너뛰는
|
||||||
방식(`Length` State의 Observer가 `if not blocker:IsOn() then recompute(...) end`
|
방식(`Length` State의 Observer가 `if not blocker:IsOn() then recompute(...) end`
|
||||||
형태로 자기 자신을 게이팅). 이 용례는 `state:Block()`을 전혀 호출하지
|
형태로 자기 자신을 게이팅). 이 용례는 `state:Apply(blocker)`를 전혀 호출하지
|
||||||
않으므로 gated state도, 그 위에 걸리는 onunblock 핸들도 생기지 않는다 —
|
않으므로 gated state도, 그 위에 걸리는 onunblock 핸들도 생기지 않는다 —
|
||||||
`blocker:Off()`/`:OffWithoutEmit()`을 불러도 실행할 핸들이 없어 두
|
`blocker:Off()`/`:OffWithoutEmit()`을 불러도 실행할 핸들이 없어 두
|
||||||
메소드가 이 용례에서는 사실상 동일하게 동작하지만, **의도를 코드에 남기기
|
메소드가 이 용례에서는 사실상 동일하게 동작하지만, **의도를 코드에 남기기
|
||||||
|
|
@ -204,13 +218,12 @@ blocker:Off() -- onunblock 핸들 실행 → HasBlockedEmit 확인 → 딱 한
|
||||||
- 클래스: `Blocker` — `Observer`/`Modifier`/`Ref`와 같은 명사-행위자
|
- 클래스: `Blocker` — `Observer`/`Modifier`/`Ref`와 같은 명사-행위자
|
||||||
네이밍 관례와 일치.
|
네이밍 관례와 일치.
|
||||||
- Blocker 자신의 토글: **`On()`/`Off()` -> self** (`Block()`/`Unblock()`
|
- Blocker 자신의 토글: **`On()`/`Off()` -> self** (`Block()`/`Unblock()`
|
||||||
아님) — `state:Block(blocker)`가 이미 "배선(wiring)" 동작의 동사로
|
아님) — 원래 근거는 배선 동사 `state:Block(blocker)`와의 충돌이었고,
|
||||||
"Block"을 쓰고 있어서, Blocker 자신의 토글까지 같은 단어를 쓰면
|
**[2026-08-28 `H-158`]** 그 동사가 `state:Apply(blocker)`로 바뀐 뒤에도 이름은
|
||||||
`blocker:Block()`(블로커를 켠다)과 `state:Block(blocker)`(state를 이
|
유지한다(`On`/`Off`가 `IsOn`/`IsBlocked`와 이미 짝이고, 바꿀 이유가 없다).
|
||||||
블로커에 배선한다)가 같은 단어로 다른 두 동작을 가리키게 됨.
|
|
||||||
- 필드: **`IsBlocked`**(Blocker 자신의 On/Off 상태), **`HasBlockedEmit`**
|
- 필드: **`IsBlocked`**(Blocker 자신의 On/Off 상태), **`HasBlockedEmit`**
|
||||||
(gated state의 대기 플래그, `Is`/`Has` 접두어로 불리언임을 바로 알려줌).
|
(gated state의 대기 플래그, `Is`/`Has` 접두어로 불리언임을 바로 알려줌).
|
||||||
- 메소드: `state:Block(blocker) -> state`.
|
- 메소드: `state:Apply(blocker) -> state`(**[2026-08-28 `H-158`]** 옛 `state:Block(blocker)` — 별도 메소드가 아니라 `Blocker.__apply`).
|
||||||
- **[2026-08-18 신설] `IsOn() -> boolean`**(`IsBlocked` 필드를 그대로 읽는
|
- **[2026-08-18 신설] `IsOn() -> boolean`**(`IsBlocked` 필드를 그대로 읽는
|
||||||
얇은 조회 메소드), **`OffWithoutEmit() -> self`**(위 "onunblock 핸들"
|
얇은 조회 메소드), **`OffWithoutEmit() -> self`**(위 "onunblock 핸들"
|
||||||
참고) — 사용자 확정: *"IsBlocked가 있다면 그냥 두어도 될듯 함.
|
참고) — 사용자 확정: *"IsBlocked가 있다면 그냥 두어도 될듯 함.
|
||||||
|
|
@ -253,7 +266,7 @@ Blocker 자신은 여전히 단순 불리언이고 두 번 켜지지 않는다
|
||||||
아니라 소유권 판정이다.
|
아니라 소유권 판정이다.
|
||||||
|
|
||||||
**base 내부 용례에도 이 규칙이 그대로 적용된 실제 사례(2026-08-18)** —
|
**base 내부 용례에도 이 규칙이 그대로 적용된 실제 사례(2026-08-18)** —
|
||||||
위 "`state:Block()` 없이 직접 쓰는 두 번째 용례" 절의 Length/Offset
|
위 "`state:Apply(blocker)` 없이 직접 쓰는 두 번째 용례" 절의 Length/Offset
|
||||||
배치 게이팅에서, 중첩된 Slot(부모 Slot 안의 자식 Slot)이 `attachSlot`을
|
배치 게이팅에서, 중첩된 Slot(부모 Slot 안의 자식 Slot)이 `attachSlot`을
|
||||||
재귀할 때마다(**[2026-08-21] 분해 후 정확히는 그 안의
|
재귀할 때마다(**[2026-08-21] 분해 후 정확히는 그 안의
|
||||||
`materializeSlotTree`** — 물리 마운트 쪽은 Blocker가 필요 없다)
|
`materializeSlotTree`** — 물리 마운트 쪽은 Blocker가 필요 없다)
|
||||||
|
|
@ -267,7 +280,7 @@ Blocker (권장)"*). 부모/자식이 같은 Blocker를 공유했다면, 자식
|
||||||
## 상태: 핵심 메커니즘+이름 확정. [2026-08-18 기준] 남은 건 문서화뿐
|
## 상태: 핵심 메커니즘+이름 확정. [2026-08-18 기준] 남은 건 문서화뿐
|
||||||
|
|
||||||
**[2026-08-18 갱신]** `IsOn()`/`OffWithoutEmit()`(위 "메커니즘" 절)과
|
**[2026-08-18 갱신]** `IsOn()`/`OffWithoutEmit()`(위 "메커니즘" 절)과
|
||||||
`state:Block()` 없이 직접 쓰는 두 번째 용례는 이 날짜에 추가된 실제 API
|
`state:Apply(blocker)` 없이 직접 쓰는 두 번째 용례는 이 날짜에 추가된 실제 API
|
||||||
확장 — "남은 건 문서화뿐"이라는 결론 자체는 안 바뀌었지만(API 표면과
|
확장 — "남은 건 문서화뿐"이라는 결론 자체는 안 바뀌었지만(API 표면과
|
||||||
메커니즘은 이 확장을 포함해 다시 확정 완료), 기준 날짜만 갱신.
|
메커니즘은 이 확장을 포함해 다시 확정 완료), 기준 날짜만 갱신.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -932,7 +932,8 @@ local function makeGate(reset: boolean, opts)
|
||||||
-- 지정된 필드**로 자기를 노출하는 것이고, `Debounce`/`Throttle`/
|
-- 지정된 필드**로 자기를 노출하는 것이고, `Debounce`/`Throttle`/
|
||||||
-- `Blocker`가 전부 같은 계약을 만족한다 —
|
-- `Blocker`가 전부 같은 계약을 만족한다 —
|
||||||
-- `base/source-state-plan.md`의 "`state:Apply(factory)`" 절이 소스
|
-- `base/source-state-plan.md`의 "`state:Apply(factory)`" 절이 소스
|
||||||
-- (필드 이름과 정확한 시그니처는 구현 시 정한다).
|
-- (필드 이름은 `__apply`, **메소드형** `obj:__apply(state) -> State` — [2026-08-28 `H-158`],
|
||||||
|
-- 호출 규약은 그 절이 소스; 아래 `__call = function(_, self)` 자리 수는 자리표시자일 뿐).
|
||||||
-- 아래 `__call` 표기는 **창/타이머 정책 본문을 읽기 위한 자리표시자**로만
|
-- 아래 `__call` 표기는 **창/타이머 정책 본문을 읽기 위한 자리표시자**로만
|
||||||
-- 볼 것 — 위 7절 배너와 같은 취급이다.
|
-- 볼 것 — 위 7절 배너와 같은 취급이다.
|
||||||
local factory = setmetatable({}, {
|
local factory = setmetatable({}, {
|
||||||
|
|
@ -1138,7 +1139,7 @@ Roblox 관용 "debounce"와 다르다는 걸 못박기**. 업계 표준 이름
|
||||||
평소처럼 무효화를 받아 `:Get()`할 뿐.
|
평소처럼 무효화를 받아 `:Get()`할 뿐.
|
||||||
- **`Tween`**: 직교. `debounced:Apply(Animate{...})`처럼 겹쳐 쓸 수 있고,
|
- **`Tween`**: 직교. `debounced:Apply(Animate{...})`처럼 겹쳐 쓸 수 있고,
|
||||||
둘 다 시간을 다루지만 층이 다름(하나는 값 보간, 하나는 전파 타이밍).
|
둘 다 시간을 다루지만 층이 다름(하나는 값 보간, 하나는 전파 타이밍).
|
||||||
- **`Blocker`**: 직교하게 겹쳐 쓸 수 있음(`state:Apply(Debounce{...}):Block(b)`).
|
- **`Blocker`**: 직교하게 겹쳐 쓸 수 있음(`state:Apply(Debounce{...}):Apply(b)`).
|
||||||
실사용 사례는 잘 안 떠오르지만 구조적으로 막을 이유도 없음.
|
실사용 사례는 잘 안 떠오르지만 구조적으로 막을 이유도 없음.
|
||||||
- **`Effect`/`Observer`**: 게이트 아래에 붙으면 자동으로 debounce된
|
- **`Effect`/`Observer`**: 게이트 아래에 붙으면 자동으로 debounce된
|
||||||
빈도로 재실행됨 — 별도 장치 불필요. **[2026-08-28 `H-151`]** 단 게이트는
|
빈도로 재실행됨 — 별도 장치 불필요. **[2026-08-28 `H-151`]** 단 게이트는
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,11 @@ v1의 `ProcessQuadProperty`(`.claude/initreq/quad/src/class.lua:134-214`)는
|
||||||
전반에서 이 인자를 `hintValue`라고 부르는데 이는 타입이 보장되지 않던
|
전반에서 이 인자를 `hintValue`라고 부르는데 이는 타입이 보장되지 않던
|
||||||
옛 모델에서 온 이름이고, 지금은 "힌트"가 아니라 보장된 값임에 유의
|
옛 모델에서 온 이름이고, 지금은 "힌트"가 아니라 보장된 값임에 유의
|
||||||
(이름 자체는 `question.md` 용어 정리 대기열). **반환값 생략 불가 —
|
(이름 자체는 `question.md` 용어 정리 대기열). **반환값 생략 불가 —
|
||||||
정리할 게 없는 핸들러도 항상 `function() end`(no-op) 형태로 반환할 것** —
|
정리할 게 없는 핸들러도 항상 no-op 클로저를 반환할 것 — [2026-08-28 `H-162`]
|
||||||
|
새 클로저 `function() end`가 아니라 quad-base가 export하는 단일 `Void`를
|
||||||
|
반환한다**(사용자: *"의도적으로 클린업이 없는 Void 함수를 많이 만들게 될 것 …
|
||||||
|
quad-base 에서 Void = function()end 를 제공하는게 편해보임"* — 할당 없음, 신원
|
||||||
|
비교 가능) —
|
||||||
`Dispatch.process`(아래 절)가 이 반환값을 `chains`에 저장해뒀다가
|
`Dispatch.process`(아래 절)가 이 반환값을 `chains`에 저장해뒀다가
|
||||||
나중에 정확히 이 클로저 하나만 호출해서 정리하므로(예전 "retract 필드
|
나중에 정확히 이 클로저 하나만 호출해서 정리하므로(예전 "retract 필드
|
||||||
생략 불가" 규칙과 같은 이유, 자리만 옮겨옴). **생략했을 때 실제로
|
생략 불가" 규칙과 같은 이유, 자리만 옮겨옴). **생략했을 때 실제로
|
||||||
|
|
@ -278,8 +282,8 @@ retract 클로저를 반환하는 1-메소드 계약으로 합쳐짐 — 이 절
|
||||||
Destroy될 때는 이 클로저가 호출되지 않음(`base/lifecycle-pattern.md`의
|
Destroy될 때는 이 클로저가 호출되지 않음(`base/lifecycle-pattern.md`의
|
||||||
"quad는 자신이 만든 Instance의 라이프사이클" 절의 원칙 참고).
|
"quad는 자신이 만든 Instance의 라이프사이클" 절의 원칙 참고).
|
||||||
- 일반 프로퍼티는 애초에 "unset" 개념이 없음(`nil`로 셋하는 것도 그냥 셋
|
- 일반 프로퍼티는 애초에 "unset" 개념이 없음(`nil`로 셋하는 것도 그냥 셋
|
||||||
동작) — 그래서 프로퍼티 핸들러는 보통 no-op 클로저(`function() end`)만
|
동작) — 그래서 프로퍼티 핸들러는 보통 no-op 클로저(`Void`, **[2026-08-28
|
||||||
반환하면 됨.
|
`H-162`]**)만 반환하면 됨.
|
||||||
- **[정정 이력, 2026-08-12 열한 번째 → 2026-08-13 다섯 번째 → 열네 번째
|
- **[정정 이력, 2026-08-12 열한 번째 → 2026-08-13 다섯 번째 → 열네 번째
|
||||||
세션] 이 클로저는 "핸들러 타입이 바뀔 때만" 불리는 게 아니라, store
|
세션] 이 클로저는 "핸들러 타입이 바뀔 때만" 불리는 게 아니라, store
|
||||||
바인드가 재발행될 때마다(값이 뭐로 바뀌든) 항상 불림** — 다만 **누가
|
바인드가 재발행될 때마다(값이 뭐로 바뀌든) 항상 불림** — 다만 **누가
|
||||||
|
|
@ -478,7 +482,7 @@ NoneHandler.priority = <매우 높음>
|
||||||
NoneHandler.isHandlable(inst, k, v) = (v == None)
|
NoneHandler.isHandlable(inst, k, v) = (v == None)
|
||||||
function NoneHandler.process(inst, k, v, index)
|
function NoneHandler.process(inst, k, v, index)
|
||||||
Dispatch.process(inst, k, nil, index + 1) -- 재귀 재호출, 별개 인덱스
|
Dispatch.process(inst, k, nil, index + 1) -- 재귀 재호출, 별개 인덱스
|
||||||
return function() end -- 자기 자신은 아무 상태도 없어 no-op
|
return Void -- 자기 자신은 아무 상태도 없어 no-op ([2026-08-28 `H-162`] 단일 `Void`)
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -638,7 +642,7 @@ end
|
||||||
- **반환하는 retractor는 여기서 할 일이 없음** — `NoneHandler`는 `v==None`을
|
- **반환하는 retractor는 여기서 할 일이 없음** — `NoneHandler`는 `v==None`을
|
||||||
매치했을 때 재귀 호출로 곧바로 `Dispatch.process(inst,k,nil,index+1)`을
|
매치했을 때 재귀 호출로 곧바로 `Dispatch.process(inst,k,nil,index+1)`을
|
||||||
부르는 게 전부고 자기 자신이 들고 있는 별도 상태가 없어서(`Relate` 등
|
부르는 게 전부고 자기 자신이 들고 있는 별도 상태가 없어서(`Relate` 등
|
||||||
전혀 안 씀) `function() end`(no-op)만 반환하면 됨 — 일반 프로퍼티
|
전혀 안 씀) `Void`(no-op, **[2026-08-28 `H-162`]**)만 반환하면 됨 — 일반 프로퍼티
|
||||||
핸들러가 no-op 클로저를 반환하는 것과 같은 이유. 자기 아래(index+1)에
|
핸들러가 no-op 클로저를 반환하는 것과 같은 이유. 자기 아래(index+1)에
|
||||||
쌓인 것의 정리는 `Dispatch.retractFrom`의 순회 구조가 대신해줌(위
|
쌓인 것의 정리는 `Dispatch.retractFrom`의 순회 구조가 대신해줌(위
|
||||||
"핸들러 계약" 절 참고), `NoneHandler` 자신이 손댈 필요 없음.
|
"핸들러 계약" 절 참고), `NoneHandler` 자신이 손댈 필요 없음.
|
||||||
|
|
@ -669,7 +673,7 @@ function NilHandler.process(inst, k, v, index)
|
||||||
-- [2026-08-18 감사에서 순서 정정]
|
-- [2026-08-18 감사에서 순서 정정]
|
||||||
Dispatch.setOffsetSource(inst, k, None)
|
Dispatch.setOffsetSource(inst, k, None)
|
||||||
Dispatch.setLength(inst, k, 0)
|
Dispatch.setLength(inst, k, 0)
|
||||||
return function() end
|
return Void -- [2026-08-28 `H-162`] no-op은 단일 `Void`
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -949,7 +953,7 @@ Observer 구독)와, A가 재귀로 위임한 핸들러 B의 생명주기가 **
|
||||||
```lua
|
```lua
|
||||||
-- Dispatch/init.luau
|
-- Dispatch/init.luau
|
||||||
local chains = Relate() -- {[inst(weak)] = {[k] = {[index] = {handler, retractor}}(strong)}}
|
local chains = Relate() -- {[inst(weak)] = {[k] = {[index] = {handler, retractor}}(strong)}}
|
||||||
local NOOP = function() end
|
local NOOP = Void -- [2026-08-28 `H-162`] 새 클로저가 아니라 export된 단일 no-op
|
||||||
|
|
||||||
function Dispatch.process(inst, k, v, index)
|
function Dispatch.process(inst, k, v, index)
|
||||||
-- [순서 주의] list 확보 + chains 등록은 반드시 h.process 호출 *전에* 끝나야 함 —
|
-- [순서 주의] list 확보 + chains 등록은 반드시 h.process 호출 *전에* 끝나야 함 —
|
||||||
|
|
@ -1110,7 +1114,7 @@ end
|
||||||
Dispatch.process(inst, k, v.inner, index + 1) -- 재위임함
|
Dispatch.process(inst, k, v.inner, index + 1) -- 재위임함
|
||||||
end
|
end
|
||||||
-- v.enabled가 false면 아무것도 안 함 ← 여기가 문제
|
-- v.enabled가 false면 아무것도 안 함 ← 여기가 문제
|
||||||
return function() end
|
return Void -- [2026-08-28 `H-162`] no-op은 단일 `Void`
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -1306,7 +1310,7 @@ end
|
||||||
이 `index`는 완전히 다른 것 — `AttributeGroupHandler`가 배열 위치를
|
이 `index`는 완전히 다른 것 — `AttributeGroupHandler`가 배열 위치를
|
||||||
`index`라고 이름 붙였다가 시그니처 자체가 계약과 어긋난 전례가 있음.
|
`index`라고 이름 붙였다가 시그니처 자체가 계약과 어긋난 전례가 있음.
|
||||||
|
|
||||||
**7. 반환 생략 금지.** 정리할 게 없어도 `function() end`. `nil`을
|
**7. 반환 생략 금지.** 정리할 게 없어도 `Void`(**[2026-08-28 `H-162`]** 단일 no-op export). `nil`을
|
||||||
반환하면 그 자리 슬롯이 완성되지 못해 `#list`가 정의되지 않게 되고
|
반환하면 그 자리 슬롯이 완성되지 못해 `#list`가 정의되지 않게 되고
|
||||||
(`retractFrom` 순회 시작점이 어긋남) 체인 추적 자체가 깨짐 —
|
(`retractFrom` 순회 시작점이 어긋남) 체인 추적 자체가 깨짐 —
|
||||||
`Dispatch.process`가 (A)/(B) 양쪽에서 즉시 error를 냄. **[정정, 2026-08-13
|
`Dispatch.process`가 (A)/(B) 양쪽에서 즉시 error를 냄. **[정정, 2026-08-13
|
||||||
|
|
@ -2275,8 +2279,8 @@ Slot 이 effect 나 다른 요소들을 소유할 수가 없다 … 실제 obser
|
||||||
자기 자신의 `_elements`를 등록하는 자리 — 아래 "적용 지점" 참고)이 그
|
자기 자신의 `_elements`를 등록하는 자리 — 아래 "적용 지점" 참고)이 그
|
||||||
owner 전용 `Blocker`를 `Relate(ownerKey)`에 lazy 생성하고 배치 시작
|
owner 전용 `Blocker`를 `Relate(ownerKey)`에 lazy 생성하고 배치 시작
|
||||||
전에 `:On()`한다**(`Dispatch.drive`는 배열 파트가 있을 때만 — 위 `H-17`
|
전에 `:On()`한다**(`Dispatch.drive`는 배열 파트가 있을 때만 — 위 `H-17`
|
||||||
절의 2026-08-27 가드). 이 Blocker는 `state:Block()`을 거치지 않고
|
절의 2026-08-27 가드). 이 Blocker는 `state:Apply(blocker)`를 거치지 않고
|
||||||
**직접** 쓰인다 — `base/blocker-plan.md`의 "`state:Block()` 없이
|
**직접** 쓰인다 — `base/blocker-plan.md`의 "`state:Apply(blocker)` 없이
|
||||||
직접 쓰는 두 번째 용례" 절 참고.
|
직접 쓰는 두 번째 용례" 절 참고.
|
||||||
2. 배치가 도는 동안, 각 position의 `setLength`가 트리거하는
|
2. 배치가 도는 동안, 각 position의 `setLength`가 트리거하는
|
||||||
`gatedRecompute`(위)는 `blocker:IsOn()`이 참이라 전부 스킵된다 — 즉
|
`gatedRecompute`(위)는 `blocker:IsOn()`이 참이라 전부 스킵된다 — 즉
|
||||||
|
|
|
||||||
|
|
@ -153,9 +153,9 @@ gcconn/gchold 복사가 전부다 — **`Destroying`도, cleanup 저장도, 그
|
||||||
`H-114`]** 위 배너대로 `H-58`이 뒤집었다. 포탈이 성립하는 실제 근거는
|
`H-114`]** 위 배너대로 `H-58`이 뒤집었다. 포탈이 성립하는 실제 근거는
|
||||||
**dep 등록이 생성자에 고정돼 언바인드가 아무것도 안 뗀다**는 것 — 재마운트는
|
**dep 등록이 생성자에 고정돼 언바인드가 아무것도 안 뗀다**는 것 — 재마운트는
|
||||||
`Destroying` 연결만 다시 건다(`_bindDestroying`). 포탈 사이에 놓친 emit의
|
`Destroying` 연결만 다시 건다(`_bindDestroying`). 포탈 사이에 놓친 emit의
|
||||||
캐치업은 없다(**[2026-08-28 `H-151`]** 옛 `_epochs:Refresh()` 폐기 — `_installed`가
|
캐치업은 **홀드 플래그로**(**[2026-08-28 `H-151`/`H-159`]** 옛 `_epochs:Refresh()`
|
||||||
참인 채 재마운트되므로 `not _installed → Rerun`도 안 걸린다; 다음 emit이
|
폐기 — 대신 언마운트~재마운트 사이에 온 emit은 `rawRerun`이 `_rerunRequired`로
|
||||||
리비전 차이로 잡는다).
|
잡아 두고 재마운트의 `_bindDestroying`이 1회 돌린다).
|
||||||
3. **cleanup은 `handle._cleanup` 필드에 보관한다.** `Rerun`이 이미 직전
|
3. **cleanup은 `handle._cleanup` 필드에 보관한다.** `Rerun`이 이미 직전
|
||||||
cleanup을 필요로 하므로 필드 쪽이 자연스럽고, `Destroying` 클로저와
|
cleanup을 필요로 하므로 필드 쪽이 자연스럽고, `Destroying` 클로저와
|
||||||
`Rerun`이 같은 자리를 읽게 된다.
|
`Rerun`이 같은 자리를 읽게 된다.
|
||||||
|
|
@ -220,7 +220,8 @@ function Effect(fn, ...)
|
||||||
if not seen[d] then seen[d] = true end -- 중복 dep은 조용히 무시(error 아님)
|
if not seen[d] then seen[d] = true end -- 중복 dep은 조용히 무시(error 아님)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- (1) dep 등록 — **여기서 한 번만**. 즉시-1회 호출은 Blocker로 억제한다.
|
-- (1) dep 등록 — **여기서 한 번만**. 즉시-1회 호출(설치 발화)은 `fire`의 `from == nil`
|
||||||
|
-- 가드가 거른다(**[2026-08-28]** 옛 `Blocker`/`canExecute` 억제 서술은 폐기).
|
||||||
-- ⭐⭐ [2026-08-26 재확정, 8라운드 `H-107`] dep 종류마다 **클로저를
|
-- ⭐⭐ [2026-08-26 재확정, 8라운드 `H-107`] dep 종류마다 **클로저를
|
||||||
-- 따로** 단다. 여기 한때 "클로저는 하나로 통일한다"고 적혀 있었으나,
|
-- 따로** 단다. 여기 한때 "클로저는 하나로 통일한다"고 적혀 있었으나,
|
||||||
-- 그 통일을 시도할 근거 자체가 없었다 — **사용자 확정**:
|
-- 그 통일을 시도할 근거 자체가 없었다 — **사용자 확정**:
|
||||||
|
|
@ -235,18 +236,21 @@ function Effect(fn, ...)
|
||||||
-- "공통 상류를 공유해도 한 파동에 fn은 한 번만"이 그대로 성립한다
|
-- "공통 상류를 공유해도 한 파동에 fn은 한 번만"이 그대로 성립한다
|
||||||
-- (그게 아니었으면 `A → b`, `A → c`, `Effect(fn, b, c)`에서
|
-- (그게 아니었으면 `A → b`, `A → c`, `Effect(fn, b, c)`에서
|
||||||
-- `A:Set()` 한 번에 `fn`이 두 번 돈다 — 2026-08-21에 닫은 그 버그).
|
-- `A:Set()` 한 번에 `fn`이 두 번 돈다 — 2026-08-21에 닫은 그 버그).
|
||||||
-- ⭐ [2026-08-28 확정, 10라운드 `H-150`] 등록 즉시 1회 발화(내부 Observer·`Ref`
|
-- ⭐ [2026-08-28 확정, 10라운드 `H-150`] 사적 `Blocker`(`_blocker:On()` …
|
||||||
-- 콜백의 설치 발화는 **그대로 일어난다**)는 아래 `fire`의 첫 줄 — **Effect
|
-- `OffWithoutEmit()`)는 **제거** — 실측(10라운드 `t18`)상 어떤 경로에서도 판정에
|
||||||
-- 핸들의** `canExecute` — 이 흡수한다: 생성자 안에선 아직 어디에도 안 묶여
|
-- 닿지 않는 죽은 부품이었다(사용자 확정: *"Effect 의 canExecute 를 보겠다는거지?
|
||||||
-- 있어 항상 거짓이다. 한때 여기 사적 `Blocker`(`_blocker:On()` … `OffWithoutEmit()`)
|
-- 그럼 그건 맞는것 같아"*). 설치 발화의 억제는 아래 `from == nil` 가드가, 실행
|
||||||
-- 가 같은 억제를 한 번 더 하려고 있었는데, 실측(10라운드 `t18`)상 어떤 경로에서도
|
-- 가능 여부는 `rawRerun`이 한 곳에서 본다(**[같은 날 `H-159`]** `fire` 자신은
|
||||||
-- 판정에 닿지 않는 죽은 부품이라 **제거**(사용자 확정: *"Effect 의 canExecute 를
|
-- 상태를 판정하지 않는다 — 사용자: *"fire 는 그냥 rerun 을 호출해도 될것"*).
|
||||||
-- 보겠다는거지? 그럼 그건 맞는것 같아"*). `H-147`로 `fn`이 생성자 안에서 자기를
|
|
||||||
-- 묶을 수도 없으므로 "생성자 구간 = 안 묶임 = `canExecute` 거짓"은 불변식이다.
|
|
||||||
local function fire(from) -- 공통 본문
|
local function fire(from) -- 공통 본문
|
||||||
if not canExecute(self) then return end -- 발화 게이트 — `Update`보다 먼저(아래 ⚠️)
|
if from == nil then return end -- 내부 Observer의 **설치 발화**(등록 즉시 1회,
|
||||||
if self._epochs:Update(from) then -- ⭐ [`H-151`] `_epochs`가 갱신되는 **유일한** 자리
|
-- `emitFrom == nil` — `source-state-plan.md`)는
|
||||||
self:Rerun()
|
-- 출처가 없어 `Update(nil)`을 못 한다. ⚠️ `Ref`
|
||||||
|
-- 값이 `nil`인 것과 무관 — `Ref` 경로의 `from`은
|
||||||
|
-- 항상 그 `ref` 객체다(`onRefFire`).
|
||||||
|
if self._epochs:Update(from) then -- ⭐ [`H-151`] `_epochs`가 갱신되는 **유일한** 자리 —
|
||||||
|
self:Rerun() -- 묶여 있든 아니든 항상. 실행 불가면 `rawRerun`이
|
||||||
|
-- `_rerunRequired`로 홀드한다(`H-159`).
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
local function onRefFire(_, ref) fire(ref) end -- Ref: 2번째가 출처
|
local function onRefFire(_, ref) fire(ref) end -- Ref: 2번째가 출처
|
||||||
|
|
@ -275,9 +279,12 @@ function Effect(fn, ...)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- (2) 설치 — 생성 즉시 1회. **바인드로 미룰 수 없다**(아래 캐비엇).
|
-- (2) 설치 — 생성 즉시 1회. **바인드로 미룰 수 없다**(아래 캐비엇).
|
||||||
-- ⭐ [2026-08-28 `H-147`] 공개 `Rerun()`이 아니라 본체를 `force`로 부른다 —
|
-- ⭐ [2026-08-28 `H-147`/`H-159`] "한 번도 안 돌았다"는 `_rerunRequired`로
|
||||||
-- 초기 설치는 "re"-run이 아니고, 아직 안 묶여 있어 공개 진입의
|
-- 표시하고(초기 실행과 "실행 못 하던 중의 변경"은 같은 요구 — 사용자),
|
||||||
-- `canExecute` 게이트를 통과하지 못한다.
|
-- 본체를 `force`로 부른다. `force`의 뜻은 **하나** — *"canExecute 를 무시하고도
|
||||||
|
-- 호출할 수 있냐. 오직 그게 전부야."*(사용자). 초기 실행을 바인드로 미룰 수
|
||||||
|
-- 없다는 결정(순차 처리) 때문에 이 시점 예외 하나가 남는다.
|
||||||
|
self._rerunRequired = true
|
||||||
rawRerun(self, true)
|
rawRerun(self, true)
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
@ -285,8 +292,8 @@ end
|
||||||
|
|
||||||
- **`_installing` 플래그도, 그 뒤를 이은 사적 `_blocker`도 폐기됐다** —
|
- **`_installing` 플래그도, 그 뒤를 이은 사적 `_blocker`도 폐기됐다** —
|
||||||
`_installing`은 생성자 구간만 덮어 바인드 구간을 놓쳤고(7라운드 `H-58`),
|
`_installing`은 생성자 구간만 덮어 바인드 구간을 놓쳤고(7라운드 `H-58`),
|
||||||
`_blocker`는 그 자리에 들어왔지만 **[2026-08-28 10라운드 `H-150`]** `fire`의
|
`_blocker`는 그 자리에 들어왔지만 **[2026-08-28 10라운드 `H-150`]** `canExecute`(당시
|
||||||
첫 줄 `canExecute`가 이미 같은 억제를 하고 있어 한 번도 판정에 닿지 않았다
|
`fire` 첫 줄, `H-159` 뒤엔 `rawRerun` 진입)가 이미 같은 억제를 하고 있어 한 번도 판정에 닿지 않았다
|
||||||
(실측 `t18`: `drop:canExecute` 3 / `drop:blocker` 0). `H-58`의 사용자 지시
|
(실측 `t18`: `drop:canExecute` 3 / `drop:blocker` 0). `H-58`의 사용자 지시
|
||||||
(*"해당 맥락의 도구인 Blocker 가 존재함 … 모든 옵저버와 callback 등록에 있어서
|
(*"해당 맥락의 도구인 Blocker 가 존재함 … 모든 옵저버와 callback 등록에 있어서
|
||||||
이를 수행해야할 것임."*)는 그 전제("등록 즉시 1회가 `Rerun`에 닿는다")가
|
이를 수행해야할 것임."*)는 그 전제("등록 즉시 1회가 `Rerun`에 닿는다")가
|
||||||
|
|
@ -313,17 +320,17 @@ function EffectHandle:_bindDestroying(inst)
|
||||||
self:_consumeCleanup()
|
self:_consumeCleanup()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
-- (2) 캐치업 — **재설치 1회뿐**. dep 등록은 이미 생성자에서 끝났다.
|
-- (2) 캐치업 — **`_rerunRequired`가 서 있으면 1회**. dep 등록은 이미 생성자에서
|
||||||
-- ⭐ [2026-08-28 확정, 10라운드 `H-151`] 여기 한때 `_epochs:Refresh()`로
|
-- 끝났다. ⭐ [2026-08-28 `H-151`→`H-159`] `_epochs:Refresh()`는 폐기(`_epochs`는
|
||||||
-- "dep이 변했으면 재실행"까지 했는데 **폐기** — `_epochs`는 `fire`의
|
-- `fire`의 `Update(from)`에서만 갱신 — 사용자: *"우린 애초에 Refersh 를 할
|
||||||
-- `Update(from)`에서만, 즉 **emit을 받을 때만** 갱신한다(Observer·중간
|
-- 필요가 없는거야"*), 대신 **실행 불가 상태(안 묶임·죽음·cleanup 중)에 온
|
||||||
-- State와 같다). 재바인드는 초기 설치와 같은 뜻이라 소진돼 있으면 다시
|
-- 변경은 `rawRerun`이 `_rerunRequired`로 홀드**해 두고 여기서 한 번 돌린다
|
||||||
-- 설치할 뿐이고, 죽어 있는 동안 떨어뜨린 emit은 다음 emit의 리비전 차이로
|
-- (Gate의 유보와 같은 그림 — Effect는 "한 번 다시 돌면 된다"라 불리언 하나).
|
||||||
-- 잡힌다(Observer와 같은 정도의 캐치업 없음 — 계약). 사용자: *"우린 애초에
|
-- 소진된 뒤의 재바인드도 같은 플래그(`_consumeCleanup`이 세운다). 사용자:
|
||||||
-- Refersh 를 할 필요가 없는거야. 재진입은 초기 설정해주는 요소이고, 그건
|
-- *"'초기실행' 과 '실행 안하던 중에 바뀐것' 이 사실 같은 요소"* — 옛
|
||||||
-- 처음 생성할때랑 같은거야."* gcconn 연결 **뒤**라 공개 `Rerun`의 게이트를
|
-- `_installed`는 이 플래그의 부정형이라 통합했다. gcconn 연결 **뒤**라 공개
|
||||||
-- 통과한다.
|
-- `Rerun`의 게이트를 통과한다.
|
||||||
if not self._installed then
|
if self._rerunRequired then
|
||||||
self:Rerun()
|
self:Rerun()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -350,7 +357,7 @@ end
|
||||||
function EffectHandle:_consumeCleanup()
|
function EffectHandle:_consumeCleanup()
|
||||||
local c = self._cleanup
|
local c = self._cleanup
|
||||||
self._cleanup = nil
|
self._cleanup = nil
|
||||||
self._installed = false -- ⭐ 아래 캐비엇 참고 — cleanup 유무로는 판정 못 한다
|
self._rerunRequired = true -- ⭐ 소진됐다 = 다음 기회에 다시 설치해야 한다(아래 캐비엇)
|
||||||
if c then
|
if c then
|
||||||
-- ⭐ [2026-08-28 확정, 10라운드 감사 2라운드] cleanup은 **세 자리**에서 돈다 —
|
-- ⭐ [2026-08-28 확정, 10라운드 감사 2라운드] cleanup은 **세 자리**에서 돈다 —
|
||||||
-- `rawRerun` 루프 머리 / `Unsubscribe()` / leaf `Destroying` 콜백. 뒤의 둘은
|
-- `rawRerun` 루프 머리 / `Unsubscribe()` / leaf `Destroying` 콜백. 뒤의 둘은
|
||||||
|
|
@ -366,16 +373,17 @@ function EffectHandle:_consumeCleanup()
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
**⚠️ [2026-08-25 `/code-review high` 정정] "설치돼 있는가"를 `_cleanup`의
|
**⚠️ [2026-08-25 `/code-review high` 정정; 2026-08-28 `H-159`로 플래그 통합] "설치돼
|
||||||
유무로 판정하면 안 된다 — 별도 `_installed` 플래그가 필요하다.** 여기 한때
|
있는가"를 `_cleanup`의 유무로 판정하면 안 된다 — 별도 플래그가 필요하다.** 여기
|
||||||
`if self._cleanup == nil or ...`라고 적어뒀는데, **`fn`의 cleanup 반환은
|
한때 `if self._cleanup == nil or ...`라고 적어뒀는데, **`fn`의 cleanup 반환은
|
||||||
선택**이라(`Effect(function() print("x") end, s)`처럼 아무것도 안 돌려주는
|
선택**이라(`Effect(function() print("x") end, s)`처럼 아무것도 안 돌려주는
|
||||||
게 흔한 정상 용례) `_cleanup`이 **항상 `nil`**인 Effect가 존재한다. 그러면
|
게 흔한 정상 용례) `_cleanup`이 **항상 `nil`**인 Effect가 존재한다. 그러면
|
||||||
바인드/포탈 재마운트마다 조건이 참이 되어 `fn`이 다시 돌고 — 이 재설계가
|
바인드/포탈 재마운트마다 조건이 참이 되어 `fn`이 다시 돌고 — 이 재설계가
|
||||||
없애려던 `H-58`(바인드마다 `Rerun`)이 **그대로 되살아난다.**
|
없애려던 `H-58`(바인드마다 `Rerun`)이 **그대로 되살아난다.** 그 플래그는
|
||||||
`_installed`는 `rawRerun`이 `fn`을 돌리고 끝날 때 참, `_consumeCleanup`에서
|
2026-08-25~28엔 `_installed`(설치됨)였고, **[2026-08-28 `H-159`]** 지금은
|
||||||
거짓이 된다(**[2026-08-28 `H-147`]** `fn` 실행 중에 핸들이 죽는 경로는 더 이상
|
**`_rerunRequired`**("`fn`이 돌아야 하는데 아직 안 돌았다") 하나다 — 세워지는 곳은
|
||||||
없다 — 아래 `Rerun` 정의).
|
생성자·`_consumeCleanup`·`rawRerun`의 홀드 셋, 내려가는 곳은 `rawRerun`이 `fn`을
|
||||||
|
실제로 돌리는 자리 하나. `_installed`는 이 플래그의 부정형이라 통합했다.
|
||||||
|
|
||||||
**⭐ [2026-08-25 신설, 7라운드 `H-60`; 2026-08-28 10라운드 `H-147`로 재정의]
|
**⭐ [2026-08-25 신설, 7라운드 `H-60`; 2026-08-28 10라운드 `H-147`로 재정의]
|
||||||
`rawRerun(self, force)` 본체 + 공개 `EffectHandle:Rerun()`.**
|
`rawRerun(self, force)` 본체 + 공개 `EffectHandle:Rerun()`.**
|
||||||
|
|
@ -396,18 +404,22 @@ local function rawRerun(self, force: boolean)
|
||||||
self._pending = true -- 실행 중 재진입 → 지연
|
self._pending = true -- 실행 중 재진입 → 지연
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if not force and not canExecute(self) then
|
if self._cleanupRunning or (not force and not canExecute(self)) then
|
||||||
return -- ⭐ 죽은 핸들·안 묶인 핸들의 재실행 요청은 **정의된
|
self._rerunRequired = true -- ⭐ [2026-08-28 `H-159`/`H-160`] 실행 불가 상태(cleanup 중 /
|
||||||
end -- no-op** — `fire`가 죽은 핸들의 emit을 버리는 것과
|
return -- 안 묶임 / 죽음)에 온 요청은 **버리지 않고 홀드** — 다음에
|
||||||
-- 같은 규칙(`H-147`: `Unsubscribe` 뒤 늦게 오는
|
end -- 묶이는 순간 1회 돈다. 버리면 "변경을 아예 보고 안 함" 경로가
|
||||||
-- 타이머의 `Rerun()`, 해제 뒤 cleanup의 재요청 등).
|
-- 생긴다(사용자: `State<Effect>` 포탈의 언마운트 cleanup 도중
|
||||||
|
-- dep이 바뀌면 재마운트가 최신값을 못 본다). leaf `Destroying`
|
||||||
|
-- cleanup 안의 `self:Rerun()`/`dep:Set()`도 여기 — gcconn이 아직
|
||||||
|
-- 연결돼 `canExecute`만으론 못 막는다(`H-160`). `Unsubscribe` 뒤
|
||||||
|
-- 늦게 오는 타이머의 `Rerun()`도 홀드(재구독하면 돈다).
|
||||||
self._running = true
|
self._running = true
|
||||||
repeat
|
repeat
|
||||||
self._pending = false
|
self._pending = false
|
||||||
self:_consumeCleanup()
|
self:_consumeCleanup() -- 안에서 `_rerunRequired = true`
|
||||||
|
self._rerunRequired = false -- ⭐ 실제로 돈다 — 이 플래그가 내려가는 **유일한** 자리
|
||||||
self._cleanup = self.fn(self)
|
self._cleanup = self.fn(self)
|
||||||
self._installed = true -- cleanup 반환 여부와 무관하게 "설치됨"
|
until not self._pending -- 재요청이 또 오면 또 돈다(`_pending` = 실행 **중**에 온 요청)
|
||||||
until not self._pending -- 재요청이 또 오면 또 돈다
|
|
||||||
self._running = false
|
self._running = false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -437,18 +449,21 @@ end
|
||||||
뭔가 수행되어 rerun 해야할 상황이 발생하면, 지연해 두었다 나중에 재실행
|
뭔가 수행되어 rerun 해야할 상황이 발생하면, 지연해 두었다 나중에 재실행
|
||||||
하는건 어떤지(실행이 끝나고 나서). 실제로 Effect 안에서 state 등을 바꾸는
|
하는건 어떤지(실행이 끝나고 나서). 실제로 Effect 안에서 state 등을 바꾸는
|
||||||
상황은 react 등지에서 흔함."*
|
상황은 react 등지에서 흔함."*
|
||||||
- **`canExecute` 확인은 진입에서 한 번** — `fire`(`Ref` 콜백·전파 루프 경유)가
|
- **`canExecute` 확인은 `rawRerun` 진입에서 한 번** — **[2026-08-28 `H-159`]** `fire`는
|
||||||
첫 줄에서 보고, 공개 `Rerun()`도 **[2026-08-28 `H-147`]** 진입에서 본다(죽은·안
|
판정하지 않고 `Update → Rerun`만 하며, 실행 불가(안 묶임·죽음·cleanup 중)면
|
||||||
묶인 핸들은 no-op). 그래서 `rawRerun` 루프 안엔 판정이 없다. (한때 "사용자가
|
`rawRerun`이 `_rerunRequired`로 **홀드**한다(no-op이 아니다 — 다음 바인드에서 1회).
|
||||||
`fn` 안에서 직접 부르는 경로는 게이트하지 않는다"였는데, 그 문장은 `Rerun`에
|
그래서 루프 안엔 판정이 없다. (한때 "`fire` 첫 줄에서 보고 죽은 핸들은 no-op"
|
||||||
게이트가 없던 시절 것.)
|
이었는데 그 문장은 `H-159` 이전 것.)
|
||||||
- **error 시 UB** — 전파되고 복구하지 않는다(`_running`/`_cleanupRunning`이 참으로
|
- **error 시 UB — 그 Effect는 죽는다.** 전파되고 복구하지 않는다: `fn`이 error하면
|
||||||
남는 것 포함). *"에러가 난 이후 데이터의 무결이 깨져도 별 책임 안 진다는 quad의
|
`_running`이, cleanup이 error하면 `_cleanupRunning`이 참으로 남아 **이후 모든
|
||||||
|
재진입(`Rerun`·네 진입점·재바인드)이 막힌다**. **[2026-08-28 `H-160` 사용자 확정]**
|
||||||
|
*"한번 죽는게 나오면 Effect 가 전부 죽는다가 계약으로 상향되어도 문제는 없는듯.
|
||||||
|
이미 _running 도 그러한 제약을 받으니까."* — 계약으로 명문화. *"에러가 난 이후 데이터의 무결이 깨져도 별 책임 안 진다는 quad의
|
||||||
일반 동작"*(사용자). 수렴 책임은 사용자 `fn`에 있고 무한 루프도 UB다.
|
일반 동작"*(사용자). 수렴 책임은 사용자 `fn`에 있고 무한 루프도 UB다.
|
||||||
|
|
||||||
**⭐ [2026-08-25 신설, 7라운드 `H-65`] 재바인드는 재설치, 재사용은 팩토리
|
**⭐ [2026-08-25 신설, 7라운드 `H-65`] 재바인드는 재설치, 재사용은 팩토리
|
||||||
패턴.** 파괴로 cleanup이 소진된 `Effect`를 다시 바인드하면 위 (2)의
|
패턴.** 파괴로 cleanup이 소진된 `Effect`를 다시 바인드하면 위 (2)의
|
||||||
`not self._installed`가 참이라 **재설치**된다. 죽음을 표시하는 별도 부기는
|
`_rerunRequired`가 참이라(**[2026-08-28 `H-159`]** 옛 `not _installed`) **재설치**된다. 죽음을 표시하는 별도 부기는
|
||||||
만들지 않는다 — **사용자 지적**: *"파괴 클린업은 결국 inst.Destroying 에
|
만들지 않는다 — **사용자 지적**: *"파괴 클린업은 결국 inst.Destroying 에
|
||||||
이벤트 바인딩인데 이 바인딩도 파괴 이후 자동 삭제된다 … gchold 나 gcconn 도
|
이벤트 바인딩인데 이 바인딩도 파괴 이후 자동 삭제된다 … gchold 나 gcconn 도
|
||||||
알아서 잘 풀린 상태라, 그냥 가만히 두면 삭제 이후 다시 사용에 있어 다시
|
알아서 잘 풀린 상태라, 그냥 가만히 두면 삭제 이후 다시 사용에 있어 다시
|
||||||
|
|
@ -484,9 +499,11 @@ end
|
||||||
`base/architecture.md`의 `EngineOps.luau` 줄이다.
|
`base/architecture.md`의 `EngineOps.luau` 줄이다.
|
||||||
- **필드 목록**: `_destroyConn`(연결 핸들), **`_deps`**(`Ref|State` → 내가 건
|
- **필드 목록**: `_destroyConn`(연결 핸들), **`_deps`**(`Ref|State` → 내가 건
|
||||||
`fn|Observer`, **강참조**), `_epochs`(`EpochMap` — `Ref`도 `Epoch`라 균일),
|
`fn|Observer`, **강참조**), `_epochs`(`EpochMap` — `Ref`도 `Epoch`라 균일),
|
||||||
`_cleanup`, **`_installed`**(설치 여부 —
|
`_cleanup`, **`_rerunRequired`**(`fn`이 돌아야 하는데 아직 안 돌았다 — 생성 직후 /
|
||||||
cleanup 반환이 선택이라 `_cleanup`으로는 판정 못 한다),
|
소진 뒤 / 실행 불가 상태에 온 변경. cleanup 반환이 선택이라 `_cleanup`으로는 판정
|
||||||
`_running`/`_pending`(재진입), **`_cleanupRunning`**(cleanup 실행 중 — `_running`과
|
못 한다; **[2026-08-28 `H-159`]** 옛 `_installed`의 부정형을 흡수),
|
||||||
|
`_running`/`_pending`(재진입 — `_pending`은 실행 **중**에 온 요청, `_rerunRequired`는
|
||||||
|
실행 **불가 상태**에 온 요청), **`_cleanupRunning`**(cleanup 실행 중 — `_running`과
|
||||||
별개, 네 진입점 가드가 둘 다 본다, **[2026-08-28]**), **`.Subscribed`**(공개 플래그 — `canExecute`가
|
별개, 네 진입점 가드가 둘 다 본다, **[2026-08-28]**), **`.Subscribed`**(공개 플래그 — `canExecute`가
|
||||||
읽는 그것, 네 진입점이 세우고 내린다, 아래 "`EffectHandle:Subscribe()`" 절).
|
읽는 그것, 네 진입점이 세우고 내린다, 아래 "`EffectHandle:Subscribe()`" 절).
|
||||||
**옛 `_refDeps`/`_refCallbacks`/`_observers`/`_installing`은 `_deps` 하나로
|
**옛 `_refDeps`/`_refCallbacks`/`_observers`/`_installing`은 `_deps` 하나로
|
||||||
|
|
@ -589,7 +606,7 @@ quad의 반응형 그래프/cleanup 인체공학만 재사용하는 경우)로
|
||||||
쓴 것이고, **순서는 이 블록이 정본**이다.
|
쓴 것이고, **순서는 이 블록이 정본**이다.
|
||||||
산문의 번호 순서(플래그 → cleanup → fail-fast)대로 짜면 leaf 바인딩된 핸들에
|
산문의 번호 순서(플래그 → cleanup → fail-fast)대로 짜면 leaf 바인딩된 핸들에
|
||||||
`:Unsubscribe()`를 불렀을 때 **cleanup을 소진한 뒤에야 error**가 나서 `E-11`이
|
`:Unsubscribe()`를 불렀을 때 **cleanup을 소진한 뒤에야 error**가 나서 `E-11`이
|
||||||
막으려던 피해(cleanup 앞당김, `_installed = false`)가 이미 일어난 뒤다 —
|
막으려던 피해(cleanup 앞당김, `_rerunRequired = true`)가 이미 일어난 뒤다 —
|
||||||
Observer 쪽 의사코드는 가드가 첫 줄이라 이 문제가 없었다.
|
Observer 쪽 의사코드는 가드가 첫 줄이라 이 문제가 없었다.
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
|
|
@ -624,8 +641,8 @@ Observer 쪽 의사코드는 가드가 첫 줄이라 이 문제가 없었다.
|
||||||
-- 있어. 그 경우도 그냥 재실행 해주지."*). Blocker는 안 쓴다 — dep을 다시
|
-- 있어. 그 경우도 그냥 재실행 해주지."*). Blocker는 안 쓴다 — dep을 다시
|
||||||
-- 등록하지 않으므로(생성자에서 한 번, `_deps` 강참조 유지) 억제할 발화가 없다.
|
-- 등록하지 않으므로(생성자에서 한 번, `_deps` 강참조 유지) 억제할 발화가 없다.
|
||||||
local function resubscribeTail(self)
|
local function resubscribeTail(self)
|
||||||
if not self._installed then -- 등록 뒤라 공개 `Rerun`의 게이트를 통과한다
|
if self._rerunRequired then -- 소진됐거나 죽어 있는 동안 변경이 홀드됐으면 1회(`H-159`).
|
||||||
self:Rerun()
|
self:Rerun() -- 등록 뒤라 공개 `Rerun`의 게이트를 통과한다
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -685,7 +702,7 @@ function EffectHandle:Unsubscribe()
|
||||||
Subscribed[self] = nil -- 안 댄다(`E-11`).
|
Subscribed[self] = nil -- 안 댄다(`E-11`).
|
||||||
WeakSubscribed[self] = nil
|
WeakSubscribed[self] = nil
|
||||||
self.Subscribed = false -- 향후 재실행 차단
|
self.Subscribed = false -- 향후 재실행 차단
|
||||||
self:_consumeCleanup() -- 통과했을 때만: 직전 cleanup 정확히 1회, `_installed = false`
|
self:_consumeCleanup() -- 통과했을 때만: 직전 cleanup 정확히 1회, `_rerunRequired = true`
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
@ -720,11 +737,10 @@ end
|
||||||
- **`:Subscribe()`가 등록하는 것은 그것 하나뿐이다** — 내부 Observer와 `Ref`
|
- **`:Subscribe()`가 등록하는 것은 그것 하나뿐이다** — 내부 Observer와 `Ref`
|
||||||
콜백은 **생성자에서 이미 `Weak*`로 걸려 있다**(위 "확정 구조" 절).
|
콜백은 **생성자에서 이미 `Weak*`로 걸려 있다**(위 "확정 구조" 절).
|
||||||
`Subscribed = true`가 서는 순간 `canExecute(handle)`이 참이 되어 그
|
`Subscribed = true`가 서는 순간 `canExecute(handle)`이 참이 되어 그
|
||||||
경로들이 살아난다. **[2026-08-27 `H-144`]** 등록 뒤 꼬리로 `not _installed →
|
경로들이 살아난다. **[2026-08-27 `H-144`]** 등록 뒤 꼬리로 `_rerunRequired →
|
||||||
Rerun`이 붙는다(위 의사코드) — 첫 구독은 설치돼 있으니 no-op, **소진된 뒤의
|
Rerun`이 붙는다(위 의사코드) — 소진된 뒤의 재구독은 재설치, **[2026-08-28
|
||||||
재구독**만 재설치. **[2026-08-28 `H-151`]** 생성과 `Subscribe()` 사이에 온
|
`H-159`]** 생성과 `Subscribe()` 사이에 온 변경도 `rawRerun`이 홀드해 뒀다가
|
||||||
emit은 `fire`가 버렸고 여기서 따라잡지 않는다 — 다음 emit의 리비전 차이로
|
여기서 1회 따라잡는다(첫 구독이라도 그 사이 변경이 없었으면 no-op).
|
||||||
잡힌다(Observer와 같은 정도의 캐치업 없음).
|
|
||||||
- **⚠️ 용도는 완전히 top-level(모듈/스크립트 레벨, 어떤 Instance
|
- **⚠️ 용도는 완전히 top-level(모듈/스크립트 레벨, 어떤 Instance
|
||||||
생명주기에도 안 묶인) 사이드 이펙트로 한정할 것 — 특정 `inst`에
|
생명주기에도 안 묶인) 사이드 이펙트로 한정할 것 — 특정 `inst`에
|
||||||
묶인 경우엔 leaf 부착(`bindLifetime`)을 쓰지 `:Subscribe()`를 쓰지
|
묶인 경우엔 leaf 부착(`bindLifetime`)을 쓰지 `:Subscribe()`를 쓰지
|
||||||
|
|
@ -900,7 +916,7 @@ Effect의 의존성이 될 방법이 아예 없다.** 사용자 제기: *"Effect
|
||||||
핸들의 `canExecute`가 한다.** 여기 한때 *"[2026-08-21 확정] 이건 `Effect` 내부
|
핸들의 `canExecute`가 한다.** 여기 한때 *"[2026-08-21 확정] 이건 `Effect` 내부
|
||||||
플래그로 한다"*고 적혀 있었고 그 플래그는 생성자 구간만 덮어 바인드 구간을
|
플래그로 한다"*고 적혀 있었고 그 플래그는 생성자 구간만 덮어 바인드 구간을
|
||||||
놓쳤다(`H-58`). 그 자리에 `self._blocker:On()` … `:OffWithoutEmit()`이 들어왔는데,
|
놓쳤다(`H-58`). 그 자리에 `self._blocker:On()` … `:OffWithoutEmit()`이 들어왔는데,
|
||||||
생성자 안의 핸들은 아직 어디에도 안 묶여 있어 `fire`의 첫 줄 `canExecute`가
|
생성자 안의 핸들은 아직 어디에도 안 묶여 있어 `canExecute`(**[`H-159`]** 지금은 `fire`가 아니라 `rawRerun` 진입에서 본다)가
|
||||||
설치 발화를 전부 떨어뜨리므로 `_blocker`는 **한 번도 판정에 닿지 않았다**
|
설치 발화를 전부 떨어뜨리므로 `_blocker`는 **한 번도 판정에 닿지 않았다**
|
||||||
(실측 `t18`). 위 생성자 의사코드가 소스다. **`_installing`도 `_blocker`도 폐기된
|
(실측 `t18`). 위 생성자 의사코드가 소스다. **`_installing`도 `_blocker`도 폐기된
|
||||||
필드다.**
|
필드다.**
|
||||||
|
|
@ -943,8 +959,8 @@ Effect의 의존성이 될 방법이 아예 없다.** 사용자 제기: *"Effect
|
||||||
설치 발화가 맵을 건드려 **그 파동의 첫 진짜 emit이 접힐** 수 있다
|
설치 발화가 맵을 건드려 **그 파동의 첫 진짜 emit이 접힐** 수 있다
|
||||||
(2026-08-21 커밋 전 `/code-review high` 발견). **[2026-08-25]** 플래그가
|
(2026-08-21 커밋 전 `/code-review high` 발견). **[2026-08-25]** 플래그가
|
||||||
`_blocker:IsOn()`으로 바뀌었을 뿐 순서 제약은 그대로였고, **[2026-08-28
|
`_blocker:IsOn()`으로 바뀌었을 뿐 순서 제약은 그대로였고, **[2026-08-28
|
||||||
`H-150`]** 그 억제 주체가 `canExecute`가 된 지금도 같다 — `canExecute`가
|
`H-150`→`H-159`]** 지금은 `fire`가 `Update`를 **먼저** 하므로 설치 발화는
|
||||||
`fire`의 첫 줄, `Update`는 그 뒤.
|
명시적 `from == nil` 가드가 `Update` 앞에서 거른다(같은 제약, 자리만 바뀜).
|
||||||
- **⭐ [2026-08-25 정정, 7라운드 `H-58`] `Ref` 의존성도 이 맵에 낀다** —
|
- **⭐ [2026-08-25 정정, 7라운드 `H-58`] `Ref` 의존성도 이 맵에 낀다** —
|
||||||
여기 한때 *"`Ref`는 `Epoch`가 아니고 `:Callback`으로 발화하므로 `from`이
|
여기 한때 *"`Ref`는 `Epoch`가 아니고 `:Callback`으로 발화하므로 `from`이
|
||||||
없다"*고 적혀 있었는데, **`Ref`가 `Epoch`로 승격**되며(`base/ref-plan.md`)
|
없다"*고 적혀 있었는데, **`Ref`가 `Epoch`로 승격**되며(`base/ref-plan.md`)
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@ end)
|
||||||
`pending`을 다시 드는 것(`H-32`를 손으로 다시 막아야 하고 버리기는
|
`pending`을 다시 드는 것(`H-32`를 손으로 다시 막아야 하고 버리기는
|
||||||
여전히 안 닫힌다).
|
여전히 안 닫힌다).
|
||||||
|
|
||||||
`Blocker`는 **그 위에 얹히는 별개 프리미티브**로, `state:Block(blocker)`가
|
`Blocker`는 **그 위에 얹히는 별개 프리미티브**로, `state:Apply(blocker)`가
|
||||||
내부에서 이 배선을 그대로 쓴다. 탑레벨 `Gate(...)` 생성자는 **안 만든다.**
|
내부에서 이 배선을 그대로 쓴다. 탑레벨 `Gate(...)` 생성자는 **안 만든다.**
|
||||||
`:Gate`가 메소드라고 `:Apply`와 배타적인 것도 아니다 — 사용자 지적대로
|
`:Gate`가 메소드라고 `:Apply`와 배타적인 것도 아니다 — 사용자 지적대로
|
||||||
*"`:Gate` 는 또 Apply 에서 쓸만한 표면을 주기도"* 하므로,
|
*"`:Gate` 는 또 Apply 에서 쓸만한 표면을 주기도"* 하므로,
|
||||||
|
|
@ -149,7 +149,9 @@ end)
|
||||||
- **`Blocker` 배선 문제도 같이 사라진다.** `base/blocker-plan.md`가 이미
|
- **`Blocker` 배선 문제도 같이 사라진다.** `base/blocker-plan.md`가 이미
|
||||||
**`state:Block(blocker) -> state`(새 gated state 반환)** 라는 **메소드**로
|
**`state:Block(blocker) -> state`(새 gated state 반환)** 라는 **메소드**로
|
||||||
확정해뒀으므로, `Block`이 내부에서 `self:Gate(blocker의 정책)`을 부르는
|
확정해뒀으므로, `Block`이 내부에서 `self:Gate(blocker의 정책)`을 부르는
|
||||||
것으로 끝난다 — `blocker` 객체를 `Apply`에 넘길 일이 없다.
|
것으로 끝난다 — **[2026-08-28 `H-158` 정정]** 그 메소드는 폐기됐고 지금은
|
||||||
|
정확히 반대로 `blocker` 객체를 `state:Apply(blocker)`에 넘긴다(`blocker:__apply(state)`가
|
||||||
|
`state:Gate(...)`를 감싼다 — 메소드형, `self`는 blocker). 결과 노드가 `GateNode`인 건 같다.
|
||||||
- **`__call`은 안 쓴다.** 사용자도 *"이상적이여 보이지는 않음"*이라 했고,
|
- **`__call`은 안 쓴다.** 사용자도 *"이상적이여 보이지는 않음"*이라 했고,
|
||||||
타입 쪽 근거가 하나 더 있다 — `__call` 테이블이 Luau에서 `(State<T>) -> U`
|
타입 쪽 근거가 하나 더 있다 — `__call` 테이블이 Luau에서 `(State<T>) -> U`
|
||||||
함수 타입 자리에 그대로 들어가는지가 불확실하다(들어가지 않는 쪽이 유력).
|
함수 타입 자리에 그대로 들어가는지가 불확실하다(들어가지 않는 쪽이 유력).
|
||||||
|
|
@ -281,10 +283,10 @@ end)
|
||||||
했던 옛 원천들이 같이 실려 나가** 하류가 폐기된 통지로 무효화된다.
|
했던 옛 원천들이 같이 실려 나가** 하류가 폐기된 통지로 무효화된다.
|
||||||
- **⭐⭐ [2026-08-25 정정, 7라운드 `H-67`] 여기 근거로 든 용례가
|
- **⭐⭐ [2026-08-25 정정, 7라운드 `H-67`] 여기 근거로 든 용례가
|
||||||
틀렸다.** `Dispatch.drive`의 배치 게이팅은 `base/blocker-plan.md`가
|
틀렸다.** `Dispatch.drive`의 배치 게이팅은 `base/blocker-plan.md`가
|
||||||
*"이 용례는 `state:Block()`을 전혀 호출하지 않으므로 gated state도 …
|
*"이 용례는 `state:Apply(blocker)`를 전혀 호출하지 않으므로 gated state도 …
|
||||||
생기지 않는다"*고 명시한 경로라 **애초에 `withheld` 집합이 없다.**
|
생기지 않는다"*고 명시한 경로라 **애초에 `withheld` 집합이 없다.**
|
||||||
결론(비워야 한다)은 그대로 유효하고 **근거가 될 용례만 바꾼다** —
|
결론(비워야 한다)은 그대로 유효하고 **근거가 될 용례만 바꾼다** —
|
||||||
`state:Block(b)`로 만든 gated state에 `b:OffWithoutEmit()`을 반복해
|
`state:Apply(b)`로 만든 gated state에 `b:OffWithoutEmit()`을 반복해
|
||||||
거는 경우, 또는 `Throttle{Trailing = false}`가 창마다 버리는 경우가
|
거는 경우, 또는 `Throttle{Trailing = false}`가 창마다 버리는 경우가
|
||||||
실제로 집합이 단조 증가하는 자리다.
|
실제로 집합이 단조 증가하는 자리다.
|
||||||
- **⭐⭐ [2026-08-25] 정책은 이걸 스스로 할 수 없다** — 위 2번의
|
- **⭐⭐ [2026-08-25] 정책은 이걸 스스로 할 수 없다** — 위 2번의
|
||||||
|
|
@ -375,14 +377,14 @@ end
|
||||||
방향이 반대로 잡혔다:
|
방향이 반대로 잡혔다:
|
||||||
|
|
||||||
- **`Blocker`가 자기 정책을 값으로 낸다 — `blocker:Policy(emit) -> onUpstreamEmit`.**
|
- **`Blocker`가 자기 정책을 값으로 낸다 — `blocker:Policy(emit) -> onUpstreamEmit`.**
|
||||||
`state:Block(b)`는 그 위의 얇은 래퍼가 된다. 노출되는 새 표면은 이 하나뿐이다.
|
`state:Apply(b)`는 그 위의 얇은 래퍼가 된다. 노출되는 새 표면은 이 하나뿐이다.
|
||||||
**⚠️ [2026-08-24 표기 정정, `/code-review high` 지적]** 여기 한때
|
**⚠️ [2026-08-24 표기 정정, `/code-review high` 지적]** 여기 한때
|
||||||
`state:Gate(b:Policy)`라고 적었는데 **그건 문법 오류다**(인자 목록 없는
|
`state:Gate(b:Policy)`라고 적었는데 **그건 문법 오류다**(인자 목록 없는
|
||||||
`b:Policy`). `b.Policy`로 고쳐도 **언바운드 메소드**라 `Gate`가
|
`b:Policy`). `b.Policy`로 고쳐도 **언바운드 메소드**라 `Gate`가
|
||||||
`setup(emit)`으로 부르면 `emit`이 `self` 자리에 들어가 정책의 `emit`이
|
`setup(emit)`으로 부르면 `emit`이 `self` 자리에 들어가 정책의 `emit`이
|
||||||
`nil`이 된다. 정확한 형태는 클로저로 묶는 것이다:
|
`nil`이 된다. 정확한 형태는 클로저로 묶는 것이다:
|
||||||
```lua
|
```lua
|
||||||
state:Block(b) == state:Gate(function(emit) return b:Policy(emit) end)
|
state:Apply(b) == state:Gate(function(emit) return b:Policy(emit) end)
|
||||||
```
|
```
|
||||||
- **`Debounce`/`Throttle`은 보류 판정·`pending` 부기를 직접 구현하지
|
- **`Debounce`/`Throttle`은 보류 판정·`pending` 부기를 직접 구현하지
|
||||||
않는다 — `Blocker`에 위임한다.**
|
않는다 — `Blocker`에 위임한다.**
|
||||||
|
|
@ -503,7 +505,7 @@ end
|
||||||
## 계약 — 게이트는 emit 경로만 미룬다 (2026-08-28 확정, 10라운드 `H-151`)
|
## 계약 — 게이트는 emit 경로만 미룬다 (2026-08-28 확정, 10라운드 `H-151`)
|
||||||
|
|
||||||
게이트가 하는 일은 **다운스트림 통지의 유보**뿐이고, 값은 안 가린다
|
게이트가 하는 일은 **다운스트림 통지의 유보**뿐이고, 값은 안 가린다
|
||||||
(`base/debounce-throttle-plan.md` 4절이 확정한 (A) emit-gate). 그래서 통지가 emit이 아닌 경로로 오면 게이트를 **거치지 않는다**:
|
(`base/debounce-throttle-plan.md` 4절이 확정한 (A) emit-gate). 그래서 통지가 emit이 아닌 경로로 오면 게이트를 **거치지 않는다**(**[같은 날 `H-159`]** 그 캐치업의 메커니즘은 `_epochs:Refresh()`가 아니라 `rawRerun`이 세우는 `_rerunRequired` 홀드 플래그 — `base/effect-plan.md`. 재구독 시 소진돼 있었으면(`_consumeCleanup`이 플래그를 세운다) 게이트 상태와 무관하게 재설치로 한 번 돌고, 유보분은 flush 때 `Update`로 또 한 번 — 둘 다 정상):
|
||||||
- **`Effect`의 재바인드/재구독 캐치업** — 소진된 핸들이 다시 묶이면 초기 설치와
|
- **`Effect`의 재바인드/재구독 캐치업** — 소진된 핸들이 다시 묶이면 초기 설치와
|
||||||
같은 뜻으로 `fn`이 돈다(`base/effect-plan.md` `_bindDestroying`). 유보 중이어도
|
같은 뜻으로 `fn`이 돈다(`base/effect-plan.md` `_bindDestroying`). 유보 중이어도
|
||||||
돈다.
|
돈다.
|
||||||
|
|
|
||||||
|
|
@ -352,10 +352,19 @@ function bindLifetime(inst, value)
|
||||||
-- 순회에서 죽고, 피해 가도 **바인드마다 `Rerun`이 도는 `H-58`이
|
-- 순회에서 죽고, 피해 가도 **바인드마다 `Rerun`이 도는 `H-58`이
|
||||||
-- 되살아난다.** 발화 게이팅은 전부 `canExecute(handle)` 하나가 맡는다 —
|
-- 되살아난다.** 발화 게이팅은 전부 `canExecute(handle)` 하나가 맡는다 —
|
||||||
-- `base/effect-plan.md`의 "확정 구조" 절이 소스.
|
-- `base/effect-plan.md`의 "확정 구조" 절이 소스.
|
||||||
|
if isObserver(value) and value._rerunRequired then
|
||||||
|
-- ⭐ [2026-08-28 `H-159`] Observer도 대칭 — 묶이기 전(생성~바인드 사이)에 온 emit은
|
||||||
|
-- 전파 루프가 `_rerunRequired`로 홀드해 두고(`source-state-plan.md` 전파 루프),
|
||||||
|
-- 묶이는 순간 1회 발화(출처 없음 — 설치 발화와 같은 모양). Observer엔 epoch가
|
||||||
|
-- 없으니 dedup은 없고 "놓친 게 있었다"만 기록된다.
|
||||||
|
value._rerunRequired = false
|
||||||
|
value.fn(value._state, value, nil)
|
||||||
|
end
|
||||||
if isEffect(value) then
|
if isEffect(value) then
|
||||||
value:_bindDestroying(inst) -- Destroying 연결 + **조건부 캐치업 1회**
|
value:_bindDestroying(inst) -- Destroying 연결 + **홀드된 변경 캐치업 1회**
|
||||||
-- (`if not self._installed then self:Rerun() end` —
|
-- (`if self._rerunRequired then self:Rerun() end` —
|
||||||
-- [2026-08-28 `H-151`] 옛 `_epochs:Refresh()` 캐치업은 폐기)
|
-- [2026-08-28 `H-151`/`H-159`] 옛 `_epochs:Refresh()`는 폐기,
|
||||||
|
-- 실행 불가 상태에 온 변경은 홀드됐다가 여기서 1회)
|
||||||
-- 의사코드는 `base/effect-plan.md`가 소스.
|
-- 의사코드는 `base/effect-plan.md`가 소스.
|
||||||
-- 그 안에서 주입 op `onDestroying(inst, fn)`을
|
-- 그 안에서 주입 op `onDestroying(inst, fn)`을
|
||||||
-- 부른다(base는 Instance를 모른다).
|
-- 부른다(base는 Instance를 모른다).
|
||||||
|
|
@ -454,7 +463,7 @@ end
|
||||||
확정**: *"observer 랑 effect 랑 헤테로지니어스한 타입인데 … '하나의 무언가가 두
|
확정**: *"observer 랑 effect 랑 헤테로지니어스한 타입인데 … '하나의 무언가가 두
|
||||||
일을 동작하지 않는가에 유의하자'"* — Observer 본문은 Observer만 쓴다. `Effect`
|
일을 동작하지 않는가에 유의하자'"* — Observer 본문은 Observer만 쓴다. `Effect`
|
||||||
쪽 넷(`Unsubscribe`는 cleanup 소진, `Subscribe`/`WeakSubscribe`는
|
쪽 넷(`Unsubscribe`는 cleanup 소진, `Subscribe`/`WeakSubscribe`는
|
||||||
**[2026-08-27 `H-144`]** 등록 끝에 `not _installed → Rerun`, 넷 다 첫 줄에
|
**[2026-08-27 `H-144`]** 등록 끝에 `_rerunRequired → Rerun`, 넷 다 첫 줄에
|
||||||
**[2026-08-28 `H-147`]** `_running`/`_cleanupRunning` 가드 — `fn`/cleanup은 자기 구독을
|
**[2026-08-28 `H-147`]** `_running`/`_cleanupRunning` 가드 — `fn`/cleanup은 자기 구독을
|
||||||
못 바꾼다)은
|
못 바꾼다)은
|
||||||
`base/effect-plan.md`의 "`EffectHandle:Subscribe()`" 절이 소스:
|
`base/effect-plan.md`의 "`EffectHandle:Subscribe()`" 절이 소스:
|
||||||
|
|
@ -473,6 +482,10 @@ function Observer:WeakSubscribe()
|
||||||
end
|
end
|
||||||
self.Subscribed = true -- ⭐ [H-111] 약한 쪽도 세운다 — 구독 경로 공용 플래그
|
self.Subscribed = true -- ⭐ [H-111] 약한 쪽도 세운다 — 구독 경로 공용 플래그
|
||||||
WeakSubscribed[self] = true
|
WeakSubscribed[self] = true
|
||||||
|
if self._rerunRequired then -- [2026-08-28 `H-159`] 구독 전에 홀드된 변경 1회(바인드와 대칭)
|
||||||
|
self._rerunRequired = false
|
||||||
|
self.fn(self._state, self, nil)
|
||||||
|
end
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -510,6 +523,10 @@ function Observer:Subscribe()
|
||||||
self.Subscribed = true
|
self.Subscribed = true
|
||||||
WeakSubscribed[self] = true
|
WeakSubscribed[self] = true
|
||||||
Subscribed[self] = true -- 강한 킵 하나만 더
|
Subscribed[self] = true -- 강한 킵 하나만 더
|
||||||
|
if self._rerunRequired then -- [2026-08-28 `H-159`] 위 `WeakSubscribe`와 같은 꼬리
|
||||||
|
self._rerunRequired = false
|
||||||
|
self.fn(self._state, self, nil)
|
||||||
|
end
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -595,6 +612,16 @@ leaf냐"를 가르는 판별자.
|
||||||
| `:WeakUnsubscribe()` | `false` | — | 제거 |
|
| `:WeakUnsubscribe()` | `false` | — | 제거 |
|
||||||
| `:Unsubscribe()` | `false` | 제거 | 제거 |
|
| `:Unsubscribe()` | `false` | 제거 | 제거 |
|
||||||
|
|
||||||
|
**Observer 인스턴스 필드 목록 (2026-08-28 명문화)** — `fn`(콜백, `fn(targetState, self,
|
||||||
|
emitFrom)`), `_state`(리시버 State — `_hold`로 강참조, `source-state-plan.md`),
|
||||||
|
**`.Subscribed`**(공개 플래그, 위 표), **`_rerunRequired`**(**[2026-08-28 10라운드
|
||||||
|
`H-159`]** 묶이기 전에 온 emit을 전파 루프가 홀드 — `bindLifetime`/`Subscribe`/
|
||||||
|
`WeakSubscribe`가 1회 발화. **거짓으로 시작**한다: `state:Observer(fn)`의 "등록 시점
|
||||||
|
즉시 1회 실행"(`source-state-plan.md`)은 생성자가 **무조건** 하는 것이라 이 플래그와
|
||||||
|
무관하고, 플래그는 그 뒤 ~ 묶이기 전 사이에 온 변경만 기록한다 — 감사 3라운드가
|
||||||
|
"초기화가 없어 한 번도 안 돈다"로 오독할 수 있음을 짚어 명시). 레지스트리 두 테이블은 인스턴스 필드가 아니라
|
||||||
|
`Observer.luau`의 모듈 로컬. Effect와 달리 epoch 맵·cleanup·재진입 플래그는 없다.
|
||||||
|
|
||||||
**여전히 참인 것**: 자기 짝은 반드시 같이 지운다 — `:Unsubscribe()`가
|
**여전히 참인 것**: 자기 짝은 반드시 같이 지운다 — `:Unsubscribe()`가
|
||||||
강한 테이블만 비우고 `WeakUnsubscribe`에 위임하지 않으면(또는 필드만
|
강한 테이블만 비우고 `WeakUnsubscribe`에 위임하지 않으면(또는 필드만
|
||||||
내리면) 그게 반쪽짜리 해제다.
|
내리면) 그게 반쪽짜리 해제다.
|
||||||
|
|
@ -666,7 +693,8 @@ Destroy됐거나 `unbindLifetime`된 `value`는 `canBound`가 **참**이라
|
||||||
Observer **값**을 키로 `BindData`에 gcconn을 복사하므로, 집합에 클로저를
|
Observer **값**을 키로 `BindData`에 gcconn을 복사하므로, 집합에 클로저를
|
||||||
담으면 identity가 달라 `canExecute`가 **항상 거짓**이 된다.
|
담으면 identity가 달라 `canExecute`가 **항상 거짓**이 된다.
|
||||||
- 발화 시 **Observer/Effect 구독자에 대해서만** `canExecute(observer)`를
|
- 발화 시 **Observer/Effect 구독자에 대해서만** `canExecute(observer)`를
|
||||||
확인하고, 거짓이면 **그 구독자만 조용히 건너뜀**(no-op) — 죽은 `inst`를
|
확인하고, 거짓이면 **그 구독자에게 `_rerunRequired`만 세우고 건너뜀**(**[2026-08-28
|
||||||
|
`H-159`]** 옛 "조용히 건너뜀" — 이제 묶일 때 1회 따라잡는다) — 죽은 `inst`를
|
||||||
건드리는 시도가 일어나지 않게 막는 위 "해야 할 일은 딱 하나" 원칙의
|
건드리는 시도가 일어나지 않게 막는 위 "해야 할 일은 딱 하나" 원칙의
|
||||||
실제 구현 지점.
|
실제 구현 지점.
|
||||||
**⭐⭐ [2026-08-25 정정, 7라운드 `H-56`] 자식 State 노드는 이 게이트를
|
**⭐⭐ [2026-08-25 정정, 7라운드 `H-56`] 자식 State 노드는 이 게이트를
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,7 @@ end
|
||||||
-- 순서는 늘 offsetSource 먼저(setLength가 끝에서 recompute를 태우므로)
|
-- 순서는 늘 offsetSource 먼저(setLength가 끝에서 recompute를 태우므로)
|
||||||
Dispatch.setOffsetSource(inst, k, None)
|
Dispatch.setOffsetSource(inst, k, None)
|
||||||
Dispatch.setLength(inst, k, 0, inst)
|
Dispatch.setLength(inst, k, 0, inst)
|
||||||
return function() end -- no-op retract — flatten이 이미 끝나 되돌릴 상태가 없음
|
return Void -- no-op retract — flatten이 이미 끝나 되돌릴 상태가 없음 ([2026-08-28 `H-162`] 단일 `Void`)
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -288,7 +288,7 @@ print**(`base/dispatch-core-plan.md`의 "핸들러 계약" 절)이고, 앞으로
|
||||||
`priority`/`process(inst,key,value,index)` **3종**(**[정정, 2026-08-13
|
`priority`/`process(inst,key,value,index)` **3종**(**[정정, 2026-08-13
|
||||||
다섯 번째 세션]** 원래 별도 `retract(inst,key,value)` 필드가 있던 4종
|
다섯 번째 세션]** 원래 별도 `retract(inst,key,value)` 필드가 있던 4종
|
||||||
계약이었으나, `process`가 자기 retract 클로저 `(nextValue: any?) -> ()`를
|
계약이었으나, `process`가 자기 retract 클로저 `(nextValue: any?) -> ()`를
|
||||||
반환하는 1-메소드로 합쳐짐), 정리할 게 없어도 `function() end`
|
반환하는 1-메소드로 합쳐짐), 정리할 게 없어도 `Void`(**[2026-08-28 `H-162`]** 단일 no-op)
|
||||||
반환 생략 불가까지 확정. `base/dispatch-core-plan.md` "핸들러 계약" 절.
|
반환 생략 불가까지 확정. `base/dispatch-core-plan.md` "핸들러 계약" 절.
|
||||||
- ~~**네이밍 미정(2026-08-04 보강)**: "프로바이더"라고 불러온 개념을 정확히
|
- ~~**네이밍 미정(2026-08-04 보강)**: "프로바이더"라고 불러온 개념을 정확히
|
||||||
뭐라고 부를지("provider" vs "processor" vs 그냥 "plug") 아직 안 정함~~
|
뭐라고 부를지("provider" vs "processor" vs 그냥 "plug") 아직 안 정함~~
|
||||||
|
|
|
||||||
|
|
@ -803,7 +803,7 @@ flatten된 값은 해시 파트(프로퍼티 키)로 존재하게 되고, Store
|
||||||
-- (`base/dispatch-core-plan.md`의 해제 순서 계약)
|
-- (`base/dispatch-core-plan.md`의 해제 순서 계약)
|
||||||
Dispatch.setOffsetSource(inst, k, None)
|
Dispatch.setOffsetSource(inst, k, None)
|
||||||
Dispatch.setLength(inst, k, 0, inst)
|
Dispatch.setLength(inst, k, 0, inst)
|
||||||
return function() end -- no-op retract, 이 자리는 fire가 끝나
|
return Void -- no-op retract([2026-08-28 `H-162`]), 이 자리는 fire가 끝나
|
||||||
-- 되돌릴 상태 자체가 없음
|
-- 되돌릴 상태 자체가 없음
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
@ -1080,7 +1080,7 @@ function ProcessedPostRefHandler.process(inst, k, v, index)
|
||||||
-- [순서 정정, 2026-08-18 감사] setOffsetSource가 먼저(위 ProcessedPreRefHandler와 동일 이유)
|
-- [순서 정정, 2026-08-18 감사] setOffsetSource가 먼저(위 ProcessedPreRefHandler와 동일 이유)
|
||||||
Dispatch.setOffsetSource(inst, k, None)
|
Dispatch.setOffsetSource(inst, k, None)
|
||||||
Dispatch.setLength(inst, k, 0, inst)
|
Dispatch.setLength(inst, k, 0, inst)
|
||||||
return function() end -- no-op retract, PreRef와 같은 이유(되돌릴 상태가 없음)
|
return Void -- no-op retract([2026-08-28 `H-162`]), PreRef와 같은 이유(되돌릴 상태가 없음)
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2148,7 +2148,7 @@ UI에 직접 관측, (2) `Dispatch.setLength(inst, i, slot.Length)`가 형제
|
||||||
조용히 어긋남 — 별도 방어 로직 없음, 문서 경고로만 남김. **[2026-08-28 10라운드
|
조용히 어긋남 — 별도 방어 로직 없음, 문서 경고로만 남김. **[2026-08-28 10라운드
|
||||||
`H-148`]** 루트(`PlayerGui` 등 quad 밖 부모)는 사용자가 `.Parent =`로 붙이는 게
|
`H-148`]** 루트(`PlayerGui` 등 quad 밖 부모)는 사용자가 `.Parent =`로 붙이는 게
|
||||||
아니라 **quad가 `Claim`으로 소유**하는 쪽으로 방향이 확정됐다
|
아니라 **quad가 `Claim`으로 소유**하는 쪽으로 방향이 확정됐다
|
||||||
(`research/existing-mount-plan.md`, M5 이후) — 그래서 이 금지에 예외가 없어진다.
|
(`research/existing-mount-plan.md`, M5 스코프 — `H-161`) — 그래서 이 금지에 예외가 없어진다.
|
||||||
(2026-08-27에 하루 있었던 "루트는 밖에서" 예외는 폐기.)
|
(2026-08-27에 하루 있었던 "루트는 밖에서" 예외는 폐기.)
|
||||||
|
|
||||||
## `Slot:Single(state, updateFn?, opts?)` — 확정 (2026-08-11 세션, `:List` 위의 순수 sugar)
|
## `Slot:Single(state, updateFn?, opts?)` — 확정 (2026-08-11 세션, `:List` 위의 순수 sugar)
|
||||||
|
|
|
||||||
|
|
@ -255,7 +255,11 @@ function State:_emitDown(from)
|
||||||
sub:_receive(from) -- state-epoch-plan.md §4 규칙 1~3
|
sub:_receive(from) -- state-epoch-plan.md §4 규칙 1~3
|
||||||
elseif canExecute(sub) then -- Observer (Effect는 자기 내부 Observer로 여기 온다)
|
elseif canExecute(sub) then -- Observer (Effect는 자기 내부 Observer로 여기 온다)
|
||||||
sub.fn(sub._state, sub, from) -- ⭐ (리시버 State, Observer 자신, 출처)
|
sub.fn(sub._state, sub, from) -- ⭐ (리시버 State, Observer 자신, 출처)
|
||||||
end -- 거짓이면 조용히 건너뜀
|
else
|
||||||
|
sub._rerunRequired = true -- ⭐ [2026-08-28 `H-159`] 묶이기 전의 변경은 버리지 않고 홀드 —
|
||||||
|
end -- `bindLifetime`/`Subscribe`가 1회 발화(`lifecycle-pattern.md`).
|
||||||
|
-- (Effect의 내부 Observer는 `WeakSubscribe`돼 있어 여기 안 옴 —
|
||||||
|
-- Effect 쪽 홀드는 `rawRerun`이 한다)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
@ -1029,7 +1033,13 @@ Source가 State 계약을 만족하는 이상 같은 이유(Modifier용 processo
|
||||||
State/Source도 `:With`/`:Compute`마다 새 노드가 나오는 같은 모양이라 —
|
State/Source도 `:With`/`:Compute`마다 새 노드가 나오는 같은 모양이라 —
|
||||||
`state:Apply(factory)`는 그냥 `factory(state)`를 메소드 체이닝 문법으로
|
`state:Apply(factory)`는 그냥 `factory(state)`를 메소드 체이닝 문법으로
|
||||||
쓴 것뿐이고 그 이상의 계약은 없음(`Modifier:Apply`와 완전히 동일한
|
쓴 것뿐이고 그 이상의 계약은 없음(`Modifier:Apply`와 완전히 동일한
|
||||||
정의: `function(self, factory) return factory(self) end`).
|
정의: `function(self, factory) return factory(self) end`). **[2026-08-28 `H-158`
|
||||||
|
호출 규약 — `/code-review` 지적으로 명시, 에이전트 배선]** 애플리커티브 팩토리는
|
||||||
|
**메소드형** `__apply`다: `function(self, factory) if type(factory) == "function"
|
||||||
|
then return factory(self) else return factory:__apply(self) end end` — 즉
|
||||||
|
`Blocker`/`Debounce`는 `function Blocker:__apply(state) … end`로 정의하고 `self`는
|
||||||
|
그 인스턴스다(클래스 테이블의 `__apply`를 `self` 없이 부르면 첫 emit에서 `nil`
|
||||||
|
인덱스로 죽는다). 세 소비자(`Blocker`·`Debounce`·`Throttle`)가 같은 자리 수.
|
||||||
|
|
||||||
- **동기**: 커링 팩토리 두 개 이상을 이미 있는 문법만으로 이으면 바깥에서
|
- **동기**: 커링 팩토리 두 개 이상을 이미 있는 문법만으로 이으면 바깥에서
|
||||||
안으로 겹쳐 읽어야 하는 중첩 호출이 됨 — 실제 형태로 예를 들면,
|
안으로 겹쳐 읽어야 하는 중첩 호출이 됨 — 실제 형태로 예를 들면,
|
||||||
|
|
@ -1064,8 +1074,9 @@ State/Source도 `:With`/`:Compute`마다 새 노드가 나오는 같은 모양
|
||||||
- **부수 효과**: `Blocker`를 슈가로 못 두던 이유도 같이 풀린다 —
|
- **부수 효과**: `Blocker`를 슈가로 못 두던 이유도 같이 풀린다 —
|
||||||
`Debounce`/`Throttle`/`Blocker`가 전부 같은 계약을 만족하게 된다.
|
`Debounce`/`Throttle`/`Blocker`가 전부 같은 계약을 만족하게 된다.
|
||||||
- **함수와 콜러블의 유니온으로 여는 안은 기각** — 필드로 받으면
|
- **함수와 콜러블의 유니온으로 여는 안은 기각** — 필드로 받으면
|
||||||
유니온도 캐스트도 필요 없다. 필드 이름과 정확한 시그니처는 구현 시
|
유니온도 캐스트도 필요 없다. 필드 이름은 **`__apply`**(**[2026-08-28 10라운드
|
||||||
정한다.
|
`H-158` 사용자 확정]** *"키는 __apply 로 하기로 했던거로 기억중임"* —
|
||||||
|
`base/blocker-plan.md` 배너), 정확한 시그니처는 구현 시 정한다.
|
||||||
- **구현 비용 거의 0**: Modifier와 달리 State/Source는 제네릭 `__index`로
|
- **구현 비용 거의 0**: Modifier와 달리 State/Source는 제네릭 `__index`로
|
||||||
필드 setter를 즉석 합성하는 메커니즘이 없어서(고정된 메소드 표면만
|
필드 setter를 즉석 합성하는 메커니즘이 없어서(고정된 메소드 표면만
|
||||||
존재), Modifier의 `Apply`처럼 "필드 이름으로 예약해야 하는" 충돌
|
존재), Modifier의 `Apply`처럼 "필드 이름으로 예약해야 하는" 충돌
|
||||||
|
|
@ -1075,7 +1086,7 @@ State/Source도 `:With`/`:Compute`마다 새 노드가 나오는 같은 모양
|
||||||
**함수만** 받으므로 위 `H-94` 항목이 확정한 "지정된 필드를 가진 객체"
|
**함수만** 받으므로 위 `H-94` 항목이 확정한 "지정된 필드를 가진 객체"
|
||||||
형태를 **거부한다** — `state:Apply(Debounce{...})`가 그대로 타입에러다.
|
형태를 **거부한다** — `state:Apply(Debounce{...})`가 그대로 타입에러다.
|
||||||
파라미터는 **함수 또는 그 필드를 가진 객체**를 받고 반환 `U`만 열어둔다
|
파라미터는 **함수 또는 그 필드를 가진 객체**를 받고 반환 `U`만 열어둔다
|
||||||
(정확한 필드 이름과 시그니처는 구현 시 정한다). 아래 논거는 **반환 쪽**에
|
(필드 이름은 `__apply` — **[2026-08-28 `H-158`]**; 시그니처는 구현 시 정한다). 아래 논거는 **반환 쪽**에
|
||||||
대한 것이라 그대로 유효하다 — Modifier의
|
대한 것이라 그대로 유효하다 — Modifier의
|
||||||
`Apply`는 `factory: (M) -> M`으로 같은 타입을 유지해야 체이닝이
|
`Apply`는 `factory: (M) -> M`으로 같은 타입을 유지해야 체이닝이
|
||||||
이어지지만, State의 `:Apply`는 팩토리가 State가 아닌 값(예: 최종
|
이어지지만, State의 `:Apply`는 팩토리가 State가 아닌 값(예: 최종
|
||||||
|
|
@ -1131,7 +1142,13 @@ Frame {
|
||||||
retract/Destroy되면 자동으로 정리됨.
|
retract/Destroy되면 자동으로 정리됨.
|
||||||
|
|
||||||
- **`fn`은 등록 시점에 즉시 1회 실행된다(2026-08-07 여섯 번째 세션,
|
- **`fn`은 등록 시점에 즉시 1회 실행된다(2026-08-07 여섯 번째 세션,
|
||||||
사용자 확정 — 이전까지 미명시였던 항목).** 근거: (1) 이미 채워진
|
사용자 확정 — 이전까지 미명시였던 항목).** (**[2026-08-28 `H-159`]** 이 1회는
|
||||||
|
`state:Observer(fn)` 생성자가 무조건 하는 것이고, 같은 날 신설된 `_rerunRequired`
|
||||||
|
홀드는 그 **뒤** ~ 묶이기 전 사이의 변경만 다룬다 — 별개다. **생성자 순서는 `fn`
|
||||||
|
1회 실행 → `_subs` 삽입**(`/code-review` 지적으로 고정): 반대면 설치 발화가 자기
|
||||||
|
State를 `Set`할 때 그 emit이 아직 안 묶인 자신에게 와 `_rerunRequired`가 서고 첫
|
||||||
|
바인드에서 `fn`이 한 번 더 돈다. Effect의 내부
|
||||||
|
Observer가 이 설치 발화를 `from == nil`로 거르는 이유이기도 하다.) 근거: (1) 이미 채워진
|
||||||
State를 나중에 구독하면 그 값을 반영하는 연산이 아예 한 번도 안
|
State를 나중에 구독하면 그 값을 반영하는 연산이 아예 한 번도 안
|
||||||
일어나는 문제가 생겨 초기화 순서에 디버깅 부담이 생김. (2) 초회
|
일어나는 문제가 생겨 초기화 순서에 디버깅 부담이 생김. (2) 초회
|
||||||
실행을 하지 말아야 할 구체적 근거가 약함. (3) **이 결정 덕에
|
실행을 하지 말아야 할 구체적 근거가 약함. (3) **이 결정 덕에
|
||||||
|
|
@ -1162,7 +1179,9 @@ retract/Destroy되면 자동으로 정리됨.
|
||||||
그건 통지가 아니라 설치라 출처가 존재하지 않는다. 그래서 `emitFrom`은
|
그건 통지가 아니라 설치라 출처가 존재하지 않는다. 그래서 `emitFrom`은
|
||||||
**옵셔널**이고, 이때만 `nil`이다(2026-08-21 커밋 전 `/code-review high`
|
**옵셔널**이고, 이때만 `nil`이다(2026-08-21 커밋 전 `/code-review high`
|
||||||
발견 — 한때 non-optional로 적혀 있었다). `fn`이 `emitFrom`을 실제로 쓰는
|
발견 — 한때 non-optional로 적혀 있었다). `fn`이 `emitFrom`을 실제로 쓰는
|
||||||
소비자라면 `nil`을 "설치 발화"로 분기해야 한다.
|
소비자라면 `nil`을 "설치 발화"로 분기해야 한다. **⚠️ [2026-08-28 판단 대기,
|
||||||
|
`H-164`]** `H-159`의 Observer 홀드 발화(묶일 때 1회)도 지금 `nil`을 넘겨 이
|
||||||
|
분기와 구분이 안 된다 — 갈래는 `-round10.md` §4.
|
||||||
- **이건 "값을 안 실어주는 구독" 계약을 안 깬다** — 넘기는 건 값이 아니라
|
- **이건 "값을 안 실어주는 구독" 계약을 안 깬다** — 넘기는 건 값이 아니라
|
||||||
**핸들과 메타데이터**뿐이다.
|
**핸들과 메타데이터**뿐이다.
|
||||||
- 인자 없는 `state:Observer()`(항상 관측 유틸)도 그대로 성립한다 — 넘겨줄
|
- 인자 없는 `state:Observer()`(항상 관측 유틸)도 그대로 성립한다 — 넘겨줄
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ Lua 문법상 `Tween{Value=target, Time=0.3}`처럼 괄호를 생략해 호출.
|
||||||
|
|
||||||
**PropertyHandler.process(inst,k,realv,index)의 새 로직** — `realv`는 이미
|
**PropertyHandler.process(inst,k,realv,index)의 새 로직** — `realv`는 이미
|
||||||
StoreBind가 State/Source 레이어를 전부 풀어낸 뒤의 값(그리고 이 함수는
|
StoreBind가 State/Source 레이어를 전부 풀어낸 뒤의 값(그리고 이 함수는
|
||||||
계약대로 마지막에 no-op 클로저 `function() end`을 반환 — 아래 "왜
|
계약대로 마지막에 no-op 클로저 `Void`(**[2026-08-28 `H-162`]**)를 반환 — 아래 "왜
|
||||||
`retract`가 더 이상 필요 없는가" 절):
|
`retract`가 더 이상 필요 없는가" 절):
|
||||||
|
|
||||||
1. `isTween(realv)`가 거짓이면 — 기존과 동일하게(아래 "3-상태 저장" 참고,
|
1. `isTween(realv)`가 거짓이면 — 기존과 동일하게(아래 "3-상태 저장" 참고,
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ ref 저장보단 비쌈. spring 등으로 움직일 수도 있다 생각하면
|
||||||
바뀌어도, `dispatch-core-plan.md` 일반 retract 계약 절 정정분 참고), 이
|
바뀌어도, `dispatch-core-plan.md` 일반 retract 계약 절 정정분 참고), 이
|
||||||
Handler는 `process(inst,k,v,index)` 자체가 `v`가 `nil`이든 숫자든
|
Handler는 `process(inst,k,v,index)` 자체가 `v`가 `nil`이든 숫자든
|
||||||
전부 완결적으로 처리하므로(있으면 지우거나 만들거나) 반환 클로저가 할 일이
|
전부 완결적으로 처리하므로(있으면 지우거나 만들거나) 반환 클로저가 할 일이
|
||||||
없어 `function() end`이면 충분 — 일반 프로퍼티 핸들러가 no-op 클로저를
|
없어 `Void`(**[2026-08-28 `H-162`]**)면 충분 — 일반 프로퍼티 핸들러가 no-op 클로저를
|
||||||
반환하는 것과 같은 이유. 값이 나중에 다시 숫자로(`2`→`nil`→`3`처럼)
|
반환하는 것과 같은 이유. 값이 나중에 다시 숫자로(`2`→`nil`→`3`처럼)
|
||||||
바뀌면 `process`가 다시 자식을 만들면 그만이라 클로저 쪽에 별도로
|
바뀌면 `process`가 다시 자식을 만들면 그만이라 클로저 쪽에 별도로
|
||||||
구현할 게 없음.
|
구현할 게 없음.
|
||||||
|
|
@ -178,15 +178,15 @@ function UICornerHandler.process(inst, k, v, index)
|
||||||
if v == nil then
|
if v == nil then
|
||||||
-- 기존 규칙 그대로: 만들어둔 자식이 있으면 지움(아래 "v가 nil인 경우" 절)
|
-- 기존 규칙 그대로: 만들어둔 자식이 있으면 지움(아래 "v가 nil인 경우" 절)
|
||||||
destroyManagedChild(inst, k)
|
destroyManagedChild(inst, k)
|
||||||
return function() end
|
return Void -- [2026-08-28 `H-162`]
|
||||||
end
|
end
|
||||||
local child = ensureManagedChild(inst, k) -- 없으면 Instance.new + Parent, 있으면 재사용
|
local child = ensureManagedChild(inst, k) -- 없으면 Instance.new + Parent, 있으면 재사용
|
||||||
Dispatch.process(child, "CornerRadius", mapTweenValue(v, toUDim), 1)
|
Dispatch.process(child, "CornerRadius", mapTweenValue(v, toUDim), 1)
|
||||||
return function() end -- ⭐ [2026-08-27 정정, 9라운드 `H-135`] no-op — 아래 참고
|
return Void -- ⭐ [2026-08-27 정정, 9라운드 `H-135`] no-op — 아래 참고. [2026-08-28 `H-162`] `Void` export
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
- **⭐ [2026-08-27 정정, 9라운드 `H-135`] 반환 클로저는 `function() end`다 —
|
- **⭐ [2026-08-27 정정, 9라운드 `H-135`] 반환 클로저는 no-op(`Void`, **[2026-08-28 `H-162`]** — 옛 표기 `function() end`)다 —
|
||||||
여기 한때 `function(hint) if hint == nil then destroyManagedChild(inst, k) end
|
여기 한때 `function(hint) if hint == nil then destroyManagedChild(inst, k) end
|
||||||
end`가 적혀 있었는데, 그건 위 "`v`가 `nil`인 경우" 절의 `v == nil`(값) 규칙을
|
end`가 적혀 있었는데, 그건 위 "`v`가 `nil`인 경우" 절의 `v == nil`(값) 규칙을
|
||||||
`hint == nil`(retractor 인자)로 잘못 옮긴 **복사 오류**였다.** 이 절의 주제는
|
`hint == nil`(retractor 인자)로 잘못 옮긴 **복사 오류**였다.** 이 절의 주제는
|
||||||
|
|
|
||||||
|
|
@ -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`)도 **같은 날 `session/2026-08-27-03-handtrace-round9-h143-h146.md`에서 전부 권고 (a)로 확정·반영**돼 **[2026-08-28] 10라운드**(`-round10.md`, 광범위 탐사 `H-150`~`H-157` 포함)도 같은 날 전량 결정·반영(소스 `-round10-followup.md`) — 둘이 뒤집혔다(`fn`은 자기 구독을 못 바꿈 / 루트는 `Claim`으로 quad 소유, `research/existing-mount-plan.md`). 남은 미결은 `question.md` 최우선 절 둘(게이트 아님). 저장소 루트에
|
반영됐고**, 같은 날 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)로 확정·반영**돼 **[2026-08-28] 10라운드**(`-round10.md`, 광범위 탐사 `H-150`~`H-157` 포함)도 같은 날 전량 결정·반영(소스 `-round10-followup.md`) — 둘이 뒤집혔다(`fn`은 자기 구독을 못 바꿈 / 루트는 `Claim`으로 quad 소유, `research/existing-mount-plan.md`). 같은 날 후속 `H-158`~`H-162`까지 반영. 남은 미결은 `question.md` 최우선 절의 `Claim` 갈래 하나(게이트 아님). 저장소 루트에
|
||||||
`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`가
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,17 @@
|
||||||
| `H-149` | Observer `Subscribe` 위임과 `level 2` | ✅ **확정 (a)** — `Subscribe`/`Unsubscribe`도 게이트·등록을 인라인, 위임 없음 |
|
| `H-149` | Observer `Subscribe` 위임과 `level 2` | ✅ **확정 (a)** — `Subscribe`/`Unsubscribe`도 게이트·등록을 인라인, 위임 없음 |
|
||||||
| `H-150` | `Effect._blocker` 죽은 부품 | ✅ **확정 (a)** — 제거, 억제는 Effect 핸들의 `canExecute` |
|
| `H-150` | `Effect._blocker` 죽은 부품 | ✅ **확정 (a)** — 제거, 억제는 Effect 핸들의 `canExecute` |
|
||||||
| `H-151` | 게이트 우회 계약 | ✅ **확정 — (a) 문서화 + `Refresh` 캐치업 폐기**: Effect의 `_epochs`는 emit 수신 때만 갱신, 재바인드/재구독은 초기 설치와 같다 |
|
| `H-151` | 게이트 우회 계약 | ✅ **확정 — (a) 문서화 + `Refresh` 캐치업 폐기**: Effect의 `_epochs`는 emit 수신 때만 갱신, 재바인드/재구독은 초기 설치와 같다 |
|
||||||
| `H-158` | `state:Block(blocker)` 슈가 잔존 (이 대화에서 나옴) | ⏳ 권고: 폐기 → `state:Apply(blocker)` |
|
| `H-158` | `state:Block(blocker)` 슈가 잔존 (이 대화에서 나옴) | ✅ **확정 폐기** → `state:Apply(blocker)`, 필드 `__apply` |
|
||||||
| `H-159`~`H-161` | 반영 뒤 `/code-review high`가 낸 새 메커니즘 셋 (`-round10.md` §4 하단) | ⏳ 판단 대기 — 바인드 전 emit 캐치업 / `Destroying` 경로 cleanup `Rerun` / M5 루트 부착·다중 스크립트 `Claim` |
|
| `H-159` | 바인드 전 emit 캐치업 | ✅ **확정 — 사용자 제안 `_rerunRequired`(Gate식 홀드)**, `_installed` 흡수, Observer 대칭, `fire`는 `Update → Rerun`만 |
|
||||||
|
| `H-162` | `Void` no-op export (이 대화에서 나옴) | ✅ **확정** — quad-base export(잎 모듈 `Void.luau`), no-op 클로저 자리는 전부 `Void` |
|
||||||
|
| `H-163`/`H-164` | `H-159` 반영분에 `/code-review high`가 낸 둘 | ⏳ 판단 대기 — Slot 내부 Observer × 홀드 발화 / 홀드 발화의 `emitFrom == nil` |
|
||||||
|
| `H-160` | `Destroying` 경로 cleanup `Rerun` | ✅ **확정 (a) → `H-159`로 정정**: `rawRerun`이 `_cleanupRunning`이면 **버리지 않고 `_rerunRequired`로 홀드** + "error 나면 그 Effect는 죽는다" 계약 |
|
||||||
|
| `H-161` | M5 루트 부착·다중 스크립트 `Claim` | ✅ **확정 (a)** `Claim`을 M5 스코프로; §5-7 다중 스크립트는 미결 |
|
||||||
| `H-153` | Store 예약 이름 런타임 가드 | ✅ **확정 (a)** — 생성자·`Of(name)`에 예약 이름 검사(level 2), 그림자 = store 자신 (I) |
|
| `H-153` | Store 예약 이름 런타임 가드 | ✅ **확정 (a)** — 생성자·`Of(name)`에 예약 이름 검사(level 2), 그림자 = store 자신 (I) |
|
||||||
| `H-154` | `InstanceChildHandler` dedup | ✅ **확정 (a)** — retractor 첫 줄 `if nextValue == v then return end` |
|
| `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-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` 소멸
|
## `H-147` — 전제 정정: `fn`/cleanup은 자기 구독을 바꿀 수 없다 (A) — `H-143` 소멸 (`Rerun` 모양은 이후 `H-159`로 다시 바뀜 — 아래)
|
||||||
|
|
||||||
문항은 "(a) UB / (b) `_everAlive` / (c) `wasAlive` 위치"였는데, 대화 중에 **문제의
|
문항은 "(a) UB / (b) `_everAlive` / (c) `wasAlive` 위치"였는데, 대화 중에 **문제의
|
||||||
뿌리가 `H-143`의 허용 자체**라는 것이 드러났다.
|
뿌리가 `H-143`의 허용 자체**라는 것이 드러났다.
|
||||||
|
|
@ -93,10 +97,10 @@
|
||||||
/ `Claim` DFS(내려가며 해석 → 자식부터 `drive`)는 derive 위의 한 겹 / 부기 대상
|
/ `Claim` DFS(내려가며 해석 → 자식부터 `drive`)는 derive 위의 한 겹 / 부기 대상
|
||||||
자식은 전부 매핑, 숏핸드(`UI*`)는 부기 밖 — quad가 직접 쓰거나 실제 객체로
|
자식은 전부 매핑, 숏핸드(`UI*`)는 부기 밖 — quad가 직접 쓰거나 실제 객체로
|
||||||
매핑하거나 / 이름 중복·부재는 UB + debug 모드 `seen` 검사 / `nativeFindChild`
|
매핑하거나 / 이름 중복·부재는 UB + debug 모드 `seen` 검사 / `nativeFindChild`
|
||||||
프로바이더 op, 순회는 quad-base / 다중 quad 한 트리 UB / M5 이후.
|
프로바이더 op, 순회는 quad-base / 다중 quad 한 트리 UB / M5 스코프(`H-161`로 당김).
|
||||||
**따름**: 전용 문구 **철회**(일반 매치 실패 그대로), `H-146` (a)의 "루트는 밖에서
|
**따름**: 전용 문구 **철회**(일반 매치 실패 그대로), `H-146` (a)의 "루트는 밖에서
|
||||||
`.Parent =`" **폐기**(루트가 quad 소유). `H-142` 키 금지는 그대로.
|
`.Parent =`" **폐기**(루트가 quad 소유). `H-142` 키 금지는 그대로.
|
||||||
**미결 6개**는 그 research 문서 §5 — 다음 배치 문항.
|
**미결**(개수는 그 research 문서 §5가 소스)은 다음 배치 문항.
|
||||||
|
|
||||||
## `H-149` — Observer `Subscribe`/`Unsubscribe`도 인라인 (a)
|
## `H-149` — Observer `Subscribe`/`Unsubscribe`도 인라인 (a)
|
||||||
|
|
||||||
|
|
@ -134,7 +138,7 @@
|
||||||
체크박스의 "선행: `Blocker` 기본 메커니즘" 문구(Effect 자체는 이제 Blocker 불필요
|
체크박스의 "선행: `Blocker` 기본 메커니즘" 문구(Effect 자체는 이제 Blocker 불필요
|
||||||
— `Blocker.luau` 선행 요구는 `GateNode`/Slot 쪽만).
|
— `Blocker.luau` 선행 요구는 `GateNode`/Slot 쪽만).
|
||||||
|
|
||||||
## `H-151` — 게이트는 유보만 한다; Effect는 emit 받을 때만 `_epochs`를 갱신 (`Refresh` 캐치업 폐기)
|
## `H-151` — 게이트는 유보만 한다; Effect는 emit 받을 때만 `_epochs`를 갱신 (`Refresh` 캐치업 폐기 — "캐치업 없음"은 이후 `H-159`의 홀드로 대체, 아래)
|
||||||
|
|
||||||
**사용자 확정**: *"해당 우회는 더 크게 보면, 처음부터 Effect 가 Rerun 되는
|
**사용자 확정**: *"해당 우회는 더 크게 보면, 처음부터 Effect 가 Rerun 되는
|
||||||
경로라서 그건 맞아. 그리고 Observer 에서 받은 emit 의 epoch|{epoch:boolean} 를
|
경로라서 그건 맞아. 그리고 Observer 에서 받은 emit 의 epoch|{epoch:boolean} 를
|
||||||
|
|
@ -216,7 +220,7 @@ then return end`(`SlotHandler` 동형). 같은 값 재발행에 `Parent = nil
|
||||||
`-round9-followup.md`/`-round9.md`의 `H-143`/`H-144`/`H-146` 소멸·정정 배너.
|
`-round9-followup.md`/`-round9.md`의 `H-143`/`H-144`/`H-146` 소멸·정정 배너.
|
||||||
|
|
||||||
**남은 미결**: `H-158`(`state:Block` 슈가 폐기 → `state:Apply(blocker)`, 권고만) —
|
**남은 미결**: `H-158`(`state:Block` 슈가 폐기 → `state:Apply(blocker)`, 권고만) —
|
||||||
`question.md`에; `research/existing-mount-plan.md` §5 갈래 6개 — 다음 배치.
|
`question.md`에; `research/existing-mount-plan.md` §5 갈래 — 다음 배치. (이후 `H-158`은 확정, 아래 절.)
|
||||||
|
|
||||||
## 감사 루프 (2026-08-28, 10라운드 반영분)
|
## 감사 루프 (2026-08-28, 10라운드 반영분)
|
||||||
|
|
||||||
|
|
@ -255,3 +259,109 @@ then return end`(`SlotHandler` 동형). 같은 값 재발행에 `Parent = nil
|
||||||
7. stale 문장 — `effect-plan.md`의 *게이트가 아니라 `Blocker`를 쓰는 이유* / 포탈
|
7. stale 문장 — `effect-plan.md`의 *게이트가 아니라 `Blocker`를 쓰는 이유* / 포탈
|
||||||
근거를 *`H-64` 캐치업*으로 적은 것(재마운트는 `_installed` 참이라 캐치업이 아예
|
근거를 *`H-64` 캐치업*으로 적은 것(재마운트는 `_installed` 참이라 캐치업이 아예
|
||||||
없다) / `source-state-plan.md`의 *구현이 한 벌*(위임 아님으로 정정).
|
없다) / `source-state-plan.md`의 *구현이 한 벌*(위임 아님으로 정정).
|
||||||
|
|
||||||
|
## `H-158` — `state:Block(blocker)` 폐기 → `state:Apply(blocker)` (확정)
|
||||||
|
|
||||||
|
**사용자 확정**: *"H158은 폐기로 하자. 내가 이미 그렇게 정했었는데, 전파가 안 된
|
||||||
|
부분이라서. 이제 Compute 와 유사하게 Gate 만 놓일 뿐, Apply 로 Blocker 처리가 가능함.
|
||||||
|
왜냐면 펑터와 적용성펑터를 전부 허용하니까 ( map|{...map} ) 키는 __apply 로 하기로
|
||||||
|
했던거로 기억중임."* — `source-state-plan.md`의 "`state:Apply(factory)`" 절은
|
||||||
|
애플리커티브 팩토리를 "지정된 필드"로 받되 이름은 "구현 시"로 열어뒀었다 → 이
|
||||||
|
발언으로 **`__apply`**. `Blocker.__apply = function(state) return state:Gate(function(emit)
|
||||||
|
return self:Policy(emit) end) end`. 반영: `blocker-plan.md` 배너·API·이름 절,
|
||||||
|
`gate-plan.md`·`dispatch-core-plan.md`·`debounce-throttle-plan.md`·`ROADMAP.md`·
|
||||||
|
`README.md`의 `state:Block` 전부 치환, `qa-round2.md`의 절 인용.
|
||||||
|
|
||||||
|
## `H-160` — `rawRerun`은 `_cleanupRunning`이면 버린다 (a) + error 계약 상향 (→ `H-159`로 "홀드"로 정정. 문별 동작: `Rerun`/`fire` 경로는 조용히 홀드, 네 진입점·`_bindDestroying`은 error)
|
||||||
|
|
||||||
|
**사용자 확정**: *"H-160 는 a 동의. 그런데 이로 인해 cleanup 이 error 를 내면
|
||||||
|
cleanupRunning 플래그가 풀리지 않는 문제가 날듯. 모든 재 진입이 막히는건데, 문제 될
|
||||||
|
것은 없어보이나 문서화가 필요한것 같아. 한번 죽는게 나오면 Effect 가 전부 죽는다가
|
||||||
|
계약으로 상향되어도 문제는 없는듯. 이미 _running 도 그러한 제약을 받으니까."* —
|
||||||
|
`effect-plan.md` "error 시 UB" bullet을 **"그 Effect는 죽는다"** 계약으로.
|
||||||
|
|
||||||
|
## `H-161` — `Claim`을 M5 스코프로 (a); §5-7은 미결
|
||||||
|
|
||||||
|
**사용자 확정**: *"H-161 는 확인. M5 스코프로 올라가도 될것으로 보임."* — `ROADMAP.md`
|
||||||
|
M5 체크박스·`research/existing-mount-plan.md` 헤더·§5-6. 다중 스크립트/루트 컨테이너
|
||||||
|
(§5-7)는 답이 없어 미결 유지.
|
||||||
|
|
||||||
|
## `H-159` — `_rerunRequired` 홀드 플래그 (사용자 제안, 확정) — `_installed` 흡수, `H-160` 정정
|
||||||
|
|
||||||
|
`/code-review`의 권고 (a)("묶이는 시점 1회 `Refresh` 복원")를 올렸더니 사용자가
|
||||||
|
다른 모양을 냈다: *"observrer 는 생성과 동시에 바운딩 처리가 되는게 일반적임. 그리고
|
||||||
|
이건 또한 observer 에도 유사한 문제가 있는 부분으로 보임 - 바운딩 전에 바뀌다가,
|
||||||
|
바운딩 되면 그대로 다 씹힘. 따라서 Gate 와 유사하게 _rerunRequired 정도가 필요한듯.
|
||||||
|
언바운딩 상태에서 이것을 true 로 만들지 관리. canExecute 가 거짓이면 모두 홀드하는게
|
||||||
|
맞아보임. 각 둘의 정의는 '초기에 한번은 불러주고, 각 변경에 불러주겠다' 인데(바인드
|
||||||
|
빼고 보면) 바인드가 들어오면서 각 변경에 아에 스킵하는 경우가 생겨났다는 의미."*
|
||||||
|
|
||||||
|
**메인 세션 판단**: `Refresh` 복원보다 낫다 — 갱신 경로는 `fire`의 `Update` 하나로
|
||||||
|
유지(`H-151` 계약 그대로)하고, 실행 불가일 때 *버리는 대신 홀드*한다. Gate의 유보와
|
||||||
|
같은 그림이되 Effect는 "한 번 다시 돌면 된다"라 불리언 하나.
|
||||||
|
|
||||||
|
**대화로 다듬어진 것 넷**:
|
||||||
|
1. **`_cleanupRunning` 중의 변경도 홀드** — 처음 메인 세션이 "cleanup 중 `dep:Set()`은
|
||||||
|
`_epochs`에 반영되니 버려도 된다"고 했는데 사용자가 정정: *"변경을 '아에 보고
|
||||||
|
안함' 이라는 경로가 생김. State<Effect> 형태로 포탈을 만들었다고 가정하면, cleanup
|
||||||
|
도중에도 여전히 _rerunRequired 는 셋업되어야함. 안 그러면 다음 바운딩에 최신값
|
||||||
|
측정이라는 목표를 잃거든."* → `H-160`의 "버림"도 홀드로 정정. 세 상황이 갈린다:
|
||||||
|
`fn` 실행 중 → `_pending`(같은 루프) / 실행 불가(안 묶임·죽음·cleanup 중) →
|
||||||
|
`_rerunRequired`(다음 바인드) / 그 외 → 즉시. `_pending`과 `_rerunRequired`의 분리는
|
||||||
|
사용자 *"완전 동의"*.
|
||||||
|
2. **`fire`는 `Update → Rerun`만** — 사용자: *"self._cleanupRunning 확인이 아래에
|
||||||
|
있다면 … fire 는 그냥 rerun 을 호출해도 될것"*. 상태 판정은 `rawRerun` 한 곳.
|
||||||
|
3. **`_installed` 폐기 → `_rerunRequired`로 통합** — 사용자: *"rerun 의 force 가
|
||||||
|
self._rerunRequired = true 하는것과 같은 동작을 낼것으로 보임. … '초기실행' 과
|
||||||
|
'실행 안하던 중에 바뀐것' 이 사실 같은 요소"*. `_installed`는 그 플래그의
|
||||||
|
부정형이었다. 세워지는 곳: 생성자·`_consumeCleanup`·`rawRerun` 홀드 / 내려가는 곳:
|
||||||
|
`rawRerun`이 `fn`을 실제로 돌리는 자리 하나. **`force`는 시점 예외 하나만** —
|
||||||
|
사용자: *"force 는 딱 하나의 역할을 해. canExecute 를 무시하고도 호출할 수 있냐.
|
||||||
|
오직 그게 전부야."* 초기 실행을 바인드로 미룰 수 없다는 결정(순차 처리)은 유지.
|
||||||
|
4. **`fire`의 `from == nil` 가드는 유지** — 사용자가 *"ref<...> 가 Frame->nil 가는
|
||||||
|
경로를 막을 이유가 없지 않나?"*라 물었는데 그건 오해: `from`은 값이 아니라 출처
|
||||||
|
`Epoch`라 `Ref` 경로에선 항상 `ref` 객체이고, `nil`은 내부 Observer의 설치 발화
|
||||||
|
(`emitFrom == nil`)뿐 — `Update(nil)`이 정의돼 있지 않아 걸러야 한다(2026-08-21
|
||||||
|
`/code-review`가 잡았던 자리). 주석에 그 구분을 명시.
|
||||||
|
|
||||||
|
**Observer 대칭**: 전파 루프가 `canExecute(observer)` 거짓이면 `_rerunRequired`를 세우고,
|
||||||
|
`bindLifetime`·`Subscribe`·`WeakSubscribe`가 그 플래그를 보면 1회 발화(`emitFrom = nil`,
|
||||||
|
설치 발화와 같은 모양). 실효 범위는 "구독자 집합엔 있지만 아직 안 묶인" 창.
|
||||||
|
**[반영 뒤 감사 3라운드]** Observer의 "등록 시점 즉시 1회 실행"(2026-08-07 확정)은
|
||||||
|
생성자가 무조건 하는 것이라 이 플래그와 **별개** — Observer의 `_rerunRequired`는
|
||||||
|
거짓으로 시작한다(Effect는 생성자가 참으로 세우고 `force`로 즉시 돌리는 것과 대비).
|
||||||
|
감사자가 "초기화가 없어 한 번도 안 돈다"로 읽을 수 있음을 짚어 두 문서에 명시.
|
||||||
|
|
||||||
|
**반영**: `effect-plan.md`(`fire`·생성자·`rawRerun`·`_consumeCleanup`·`_bindDestroying`·
|
||||||
|
`resubscribeTail`·필드 목록·캐비엇·포탈 근거·`H-65` 문단), `lifecycle-pattern.md`
|
||||||
|
(bindLifetime Observer 분기·Observer `Subscribe`/`WeakSubscribe` 꼬리·주석),
|
||||||
|
`source-state-plan.md` 전파 루프, `ROADMAP.md` M2 넷·M6, `README.md`.
|
||||||
|
|
||||||
|
## `H-162` — `Void` no-op export (확정)
|
||||||
|
|
||||||
|
사용자: *"지금 상황에서 의도적으로 클린업이 없는 Void 함수를 많이 만들게 될 것으로
|
||||||
|
보이는데, 이걸 quad-base 에서 Void = function()end 를 제공하는게 편해보임."* —
|
||||||
|
quad-base가 단일 no-op 함수 `Void`를 export(quad-roblox 핸들러도 쓰므로 공개),
|
||||||
|
no-op 클로저를 돌려주는 자리(숏핸드 retractor 등)는 새 클로저 대신 `Void`. 사용자
|
||||||
|
Effect `fn`이 `return Void`로 "cleanup 없음"을 명시하는 것도 자연히 허용(반환 안 하는
|
||||||
|
것과 동일 취급). 반영: `dispatch-core-plan.md` 반환값 규칙, `ui-shorthand-plan.md` 둘,
|
||||||
|
`ROADMAP.md` M2 공통 기반.
|
||||||
|
|
||||||
|
## 감사 루프 (2026-08-28, `H-158`~`H-162` 반영분)
|
||||||
|
|
||||||
|
새 발견 7→6→1→**0**(4라운드에서 수렴). 1 문구 잔존(`Block` 현재형 / `__apply` 전파 /
|
||||||
|
`Void` 9곳 / "`fire`의 첫 줄 `canExecute`" stale 4곳 / `architecture.md` `Void` 자리 /
|
||||||
|
session 후속 절) · 2 의미론(**`_rerunRequired` 상태 기계 자체는 모순 없음**; debounce
|
||||||
|
`__apply` / `Void` 5곳 더 / session-summary 후속 / `init.luau` 모호 / gate 계약 절
|
||||||
|
포인터 / Observer 필드 목록 신설) · 3 수정분 재검토(Observer 생성자 1회 실행이
|
||||||
|
플래그와 별개임을 명시 — 감사자의 오독 가능성) · 4 수렴 **0건**.
|
||||||
|
|
||||||
|
## `/code-review high` (2026-08-28, `H-158`~`H-162` 반영분 + 감사 4라운드 뒤)
|
||||||
|
|
||||||
|
10건. **여덟 반영**, **둘 문항**(`H-163`/`H-164`, `-round10.md` §4).
|
||||||
|
반영: `__apply` 호출 규약(메소드형 `factory:__apply(state)`, 에이전트 배선) /
|
||||||
|
`Void`는 잎 모듈 `Void.luau`(최상위 `init.luau`에 두면 순환 require) / Observer 생성자
|
||||||
|
순서 `fn` 1회 → `_subs` 삽입 / `gate-plan.md` 계약 절 괄호 정정 / `effect-plan.md`
|
||||||
|
stale 셋(`Blocker` 억제·"`fire` 첫 줄"·"죽은 핸들은 no-op") / `ROADMAP.md` M2 `Rerun`
|
||||||
|
no-op → 홀드 / `lifecycle-pattern.md`의 *조용히 건너뜀* → 홀드 / "M5 이후" 잔존 셋 /
|
||||||
|
followup 절 제목 포인터·개수.
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
> 새로 만들고 `base/`에 반영한다 — **이 파일은 발견 당시의 기록**이라 각 항목의
|
> 새로 만들고 `base/`에 반영한다 — **이 파일은 발견 당시의 기록**이라 각 항목의
|
||||||
> "갈래"는 선택 전 목록이니 반영 뒤엔 그대로 믿지 말 것.
|
> "갈래"는 선택 전 목록이니 반영 뒤엔 그대로 믿지 말 것.
|
||||||
>
|
>
|
||||||
> 상태: **[2026-08-28] 탐사 완료(발견 `H-150`~`H-157`, 🔴 0 · 🟡 5 · 🟢 3, §4 문항 7) → 같은 날 사용자와 대화형으로 전량 결정·반영 — 결정의 소스는 `-round10-followup.md`.** 미결로 남은 것은 거기서 새로 생긴 `H-158`(`:Block` 슈가)과 `research/existing-mount-plan.md` §5의 갈래들뿐(개수는 거기가 소스). `H-147`~`H-149`는
|
> 상태: **[2026-08-28] 탐사 완료(발견 `H-150`~`H-157`, 🔴 0 · 🟡 5 · 🟢 3, §4 문항 7) → 같은 날 사용자와 대화형으로 전량 결정·반영 — 결정의 소스는 `-round10-followup.md`.** 후속 `H-158`~`H-162`도 같은 날 확정(`H-159`는 `/code-review` 권고 (a) `Refresh` 복원이 아니라 사용자 제안 **`_rerunRequired` 홀드**로). 미결은 `research/existing-mount-plan.md` §5의 갈래들뿐(개수는 거기가 소스). `H-147`~`H-149`는
|
||||||
> `H-143`~`H-146` 반영분에 `/code-review high`가 낸 10건 중 새 메커니즘·기존
|
> `H-143`~`H-146` 반영분에 `/code-review high`가 낸 10건 중 새 메커니즘·기존
|
||||||
> 결정 변경이라 문항으로 올린 셋(나머지 일곱은 반영 — `-round9-followup.md`의
|
> 결정 변경이라 문항으로 올린 셋(나머지 일곱은 반영 — `-round9-followup.md`의
|
||||||
> 마지막 code-review 절).
|
> 마지막 code-review 절).
|
||||||
|
|
@ -350,6 +350,8 @@ spurious 재발행은 `Source:Set`이 같은 값도 emit한다는 확정(`t18`
|
||||||
| **`H-159`** | **[2026-08-28 `/code-review`, 반영 뒤]** `H-151`이 잃은 캐치업 — 바인드 **전**에 온 emit(특히 `Ref`)은 다시 안 온다 | (a) `_bindDestroying`/`resubscribeTail`에 **"묶이는 시점 1회 `Refresh`"**만 되살림(emit 경로의 `_epochs` 갱신은 `H-151`대로 `Update`만) / (b) `Ref` dep만 바인드 시 `.Revision` 대조 / (c) 계약으로 두고 사용자에게 "`Ref`를 dep으로 쓰는 Effect는 그 leaf 뒤에 두라" 문서화 | **(a)** — `H-151`의 근거("다음 emit이 잡는다")가 `Ref`엔 성립하지 않는다; (a)는 `H-151`을 되돌리는 게 아니라 "emit 경로만 미룬다"는 계약과 양립(바인드는 emit 경로가 아님) |
|
| **`H-159`** | **[2026-08-28 `/code-review`, 반영 뒤]** `H-151`이 잃은 캐치업 — 바인드 **전**에 온 emit(특히 `Ref`)은 다시 안 온다 | (a) `_bindDestroying`/`resubscribeTail`에 **"묶이는 시점 1회 `Refresh`"**만 되살림(emit 경로의 `_epochs` 갱신은 `H-151`대로 `Update`만) / (b) `Ref` dep만 바인드 시 `.Revision` 대조 / (c) 계약으로 두고 사용자에게 "`Ref`를 dep으로 쓰는 Effect는 그 leaf 뒤에 두라" 문서화 | **(a)** — `H-151`의 근거("다음 emit이 잡는다")가 `Ref`엔 성립하지 않는다; (a)는 `H-151`을 되돌리는 게 아니라 "emit 경로만 미룬다"는 계약과 양립(바인드는 emit 경로가 아님) |
|
||||||
| **`H-160`** | leaf `Destroying` 콜백이 도는 cleanup 안의 `self:Rerun()`/`dep:Set()` — `canExecute`가 아직 참이라 죽는 inst에서 `fn`이 돌고 새 cleanup이 영구 고아 | (a) `rawRerun` 진입에서 `_cleanupRunning`이면 **버린다**(no-op) — "cleanup은 자기 생명주기를 못 바꾼다"의 `Rerun`판 / (b) `Destroying` 콜백이 `_consumeCleanup` **전에** `.Subscribed`류 표식으로 죽음을 먼저 세움(새 상태) / (c) UB 문서화 | **(a)** — 새 상태 없이 기존 플래그 하나로, `Unsubscribe` 경로와 같은 결과 |
|
| **`H-160`** | leaf `Destroying` 콜백이 도는 cleanup 안의 `self:Rerun()`/`dep:Set()` — `canExecute`가 아직 참이라 죽는 inst에서 `fn`이 돌고 새 cleanup이 영구 고아 | (a) `rawRerun` 진입에서 `_cleanupRunning`이면 **버린다**(no-op) — "cleanup은 자기 생명주기를 못 바꾼다"의 `Rerun`판 / (b) `Destroying` 콜백이 `_consumeCleanup` **전에** `.Subscribed`류 표식으로 죽음을 먼저 세움(새 상태) / (c) UB 문서화 | **(a)** — 새 상태 없이 기존 플래그 하나로, `Unsubscribe` 경로와 같은 결과 |
|
||||||
| **`H-161`** | `H-148` 이후 **M5에 승인된 루트 부착 경로가 없다** + 여러 스크립트가 같은 `PlayerGui`를 `Claim`하면 이중 claim error / 다중 quad UB라 `Claim`이 자기 동기 사례를 막는다 | (a) `Claim`을 **M5 스코프**로 당기고(프로바이더 마일스톤이라 자연스러움) `research/existing-mount-plan.md` §5-7·8 갈래를 같이 정한다 / (b) `Claim` 전까지 임시로 `H-146` 루트 예외(밖에서 `.Parent =`)를 M5 한정으로 되살림 / (c) 루트 컨테이너(부기 대상 아님)는 claim 없이 자식만 붙이는 얇은 표면 신설 | **(a)** — 임시 예외는 하루 만에 뒤집힌 것을 되살리는 것이고, (c)는 `Mount` 기각의 재개방. §5-7(다중 스크립트)은 `Claim`의 "전부 매핑" 계약이 **루트 컨테이너에는 안 맞는다**는 신호라 갈래를 그 문서에 적었다 |
|
| **`H-161`** | `H-148` 이후 **M5에 승인된 루트 부착 경로가 없다** + 여러 스크립트가 같은 `PlayerGui`를 `Claim`하면 이중 claim error / 다중 quad UB라 `Claim`이 자기 동기 사례를 막는다 | (a) `Claim`을 **M5 스코프**로 당기고(프로바이더 마일스톤이라 자연스러움) `research/existing-mount-plan.md` §5-7·8 갈래를 같이 정한다 / (b) `Claim` 전까지 임시로 `H-146` 루트 예외(밖에서 `.Parent =`)를 M5 한정으로 되살림 / (c) 루트 컨테이너(부기 대상 아님)는 claim 없이 자식만 붙이는 얇은 표면 신설 | **(a)** — 임시 예외는 하루 만에 뒤집힌 것을 되살리는 것이고, (c)는 `Mount` 기각의 재개방. §5-7(다중 스크립트)은 `Claim`의 "전부 매핑" 계약이 **루트 컨테이너에는 안 맞는다**는 신호라 갈래를 그 문서에 적었다 |
|
||||||
|
| **`H-163`** | **[2026-08-28 `/code-review`, `H-159` 반영 뒤]** Slot 내부 Observer(`_listObserver`·`_baseObserver`)에도 홀드 발화가 걸려 재마운트의 `bindLifetime`이 `materializeSlotTree` **도중** `reconcile`을 동기 실행 → 자리 이중 등록, 중첩 Slot이면 `canBound` error | (a) Slot이 자기 내부 Observer를 다시 묶기 전에 `_rerunRequired`를 **지운다**(재마운트 캐치업은 `activateList`가 이미 명시적으로 한다 — 이중) / (b) 홀드 발화를 사용자 Observer에만(내부 Observer는 브랜드로 구분 — 새 구분) / (c) 홀드 발화를 `bindLifetime` 안이 아니라 `materializeSlotTree` 끝(`blocker:OffWithoutEmit()` 뒤)으로 미룸 | **(a)** — 새 구분 없이 한 줄, "재마운트 캐치업의 주체는 Slot"이라는 기존 계약 그대로 |
|
||||||
|
| **`H-164`** | Observer 홀드 발화가 `emitFrom = nil`로 오면 계약("`nil` = 설치 발화")과 구분 불가 — `if emitFrom == nil then initOnly()`로 짠 소비자가 변경을 놓침 | (a) 홀드 시 **마지막 `from`을 보관**(`_rerunRequired = from`, 진리값으로 플래그 겸용)해 그것을 넘김 / (b) 전용 센티널(`HeldEmit`) / (c) 계약 문구만 "`nil` = 설치 **또는** 묶일 때 캐치업" | **(a)** — Observer는 dedup이 없어 "마지막 출처"가 곧 홀드의 내용; 단 필드가 불리언과 출처를 겸하는 게 원칙(한 필드 두 뜻)에 걸리면 (b) |
|
||||||
|
|
||||||
갈래 없는 것(회신 불필요, 반영만): `H-152`(브랜드 등록 한 줄), `H-155`(ROADMAP 넷),
|
갈래 없는 것(회신 불필요, 반영만): `H-152`(브랜드 등록 한 줄), `H-155`(ROADMAP 넷),
|
||||||
`H-156`(`H-32` 문단), `H-157`(실측 완료 표기).
|
`H-156`(`H-32` 문단), `H-157`(실측 완료 표기).
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
설계로 확정**됐다(아래 "해결 — Blocker 게이팅" 절). 해법이 실제
|
설계로 확정**됐다(아래 "해결 — Blocker 게이팅" 절). 해법이 실제
|
||||||
반영된 곳은 `base/dispatch-core-plan.md`의 "배치 등록을 안전하게 만드는
|
반영된 곳은 `base/dispatch-core-plan.md`의 "배치 등록을 안전하게 만드는
|
||||||
Blocker 게이팅" 절, `base/slot-plan.md`의 "재귀 메커니즘" 절,
|
Blocker 게이팅" 절, `base/slot-plan.md`의 "재귀 메커니즘" 절,
|
||||||
`base/blocker-plan.md`의 "`state:Block()` 없이 직접 쓰는 두 번째 용례"
|
`base/blocker-plan.md`의 "`state:Apply(blocker)` 없이 직접 쓰는 두 번째 용례"(2026-08-28 `H-158`로 개명 — 당시 이름은 `state:Block()`)
|
||||||
절 — 이 문서는 그 결론에 이르는 논의 원문만 보존한다.
|
절 — 이 문서는 그 결론에 이르는 논의 원문만 보존한다.
|
||||||
|
|
||||||
### 발견한 크래시 경로
|
### 발견한 크래시 경로
|
||||||
|
|
|
||||||
|
|
@ -12,23 +12,22 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ⭐ 최우선 — 10라운드 후속 둘 (2026-08-28 갱신)
|
## ⭐ 최우선 — `Claim` 갈래 (2026-08-28 갱신)
|
||||||
|
|
||||||
**[2026-08-28] 10라운드 §4 문항 7건은 사용자와 대화형으로 전량 결정·반영됐습니다**
|
**[2026-08-28] 10라운드 §4 문항 7건과 그 반영분의 후속(`H-158`~`H-162`)까지
|
||||||
(소스는 `qa-request/pre-implementation-handtrace-round10-followup.md`). 그 대화에서
|
사용자와 대화형으로 전량 결정·반영됐습니다**(소스는
|
||||||
새로 생긴 것만 남아 있습니다 — **둘 다 M2 게이트 아님**:
|
`qa-request/pre-implementation-handtrace-round10-followup.md`). 남은 건 하나 —
|
||||||
- **`H-158`** `state:Block(blocker)` 슈가가 `blocker-plan.md`에 아직 있다 — 사용자
|
**M2 게이트 아님**:
|
||||||
언급(*"Apply(Blocker) 이긴 할꺼야 (표면은 :Gate 만 남아 …)"*)대로 **폐기하고
|
- **`H-163`/`H-164`** (`-round10.md` §4 표 마지막 둘, `H-159` 반영분에
|
||||||
`state:Apply(blocker)`로** 갈지 확인(권고 (a) 폐기).
|
`/code-review high`가 낸 것) — Slot 내부 Observer × 홀드 발화(권고 (a) Slot이
|
||||||
- **`H-159`~`H-161`** (`-round10.md` §4 표 아래 셋, 10라운드 반영분에
|
재바인드 전에 플래그를 지움) / Observer 홀드 발화의 `emitFrom == nil` 모호(권고
|
||||||
`/code-review high`가 낸 것) — `H-151`이 잃은 "바인드 전 emit(특히 `Ref`)"
|
(a) 마지막 `from`을 넘김, 한 필드 두 뜻이 걸리면 (b) 센티널).
|
||||||
캐치업(권고 (a) 묶이는 시점 1회 `Refresh`만 복원) / leaf `Destroying` 경로의
|
|
||||||
cleanup 안 `Rerun`(권고 (a) `_cleanupRunning`이면 `rawRerun` no-op) / **M5
|
|
||||||
루트 부착 경로 부재 + 다중 스크립트 `Claim`**(권고 (a) `Claim`을 M5로, 다중
|
|
||||||
스크립트는 권고 없음).
|
|
||||||
- **`research/existing-mount-plan.md` §5** — 루트/템플릿을 quad가 소유하는
|
- **`research/existing-mount-plan.md` §5** — 루트/템플릿을 quad가 소유하는
|
||||||
`Claim` + `D.Mapper`의 갈래들(루트 디스크립터 이름·물리 순서 계약·비루트
|
`Claim` + `D.Mapper`의 갈래들(루트 디스크립터 이름·물리 순서 계약·비루트
|
||||||
사용·debug 검사 범위·표면 이름·마일스톤 … — 개수는 그 문서 §5가 소스). 방향은 확정, M5 이후.
|
사용·debug 검사 범위·표면 이름 … — 개수는 그 문서 §5가 소스). 방향은 확정, M5
|
||||||
|
스코프(`H-161`). **특히 §5-7**(여러 스크립트/여러 quad가 같은 `PlayerGui`를
|
||||||
|
쓰는 경우 — "전부 매핑" 계약이 루트 컨테이너엔 안 맞는다는 신호, 권고 없음)이
|
||||||
|
`Claim` 설계의 핵심 미결입니다.
|
||||||
|
|
||||||
**[2026-08-27] 9라운드**(Q1~Q10·`H-138`·`H-139`·`H-142`·`H-143`~`H-146`)는
|
**[2026-08-27] 9라운드**(Q1~Q10·`H-138`·`H-139`·`H-142`·`H-143`~`H-146`)는
|
||||||
전량 처리·반영됐습니다 — 소스는 `-round9-followup.md`.
|
전량 처리·반영됐습니다 — 소스는 `-round9-followup.md`.
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,10 @@
|
||||||
|
|
||||||
> **[2026-08-28 신설, 사용자 발의]** 10라운드 `H-148`(`Parent` 거부 문구)을
|
> **[2026-08-28 신설, 사용자 발의]** 10라운드 `H-148`(`Parent` 거부 문구)을
|
||||||
> 논의하다 **더 큰 표면의 공백**이 드러나 만든 문서. 상태: **설계 논의 중 —
|
> 논의하다 **더 큰 표면의 공백**이 드러나 만든 문서. 상태: **설계 논의 중 —
|
||||||
> 방향은 사용자 확정, 갈래 몇 개 미결(§5)**. M2 착수 게이트 아님, **M5 이후**
|
> 방향은 사용자 확정, 갈래 몇 개 미결(§5)**. M2 착수 게이트 아님, **M5 스코프**
|
||||||
> (프로바이더 op가 필요). 결정이 나면 `base/`로 승격한다.
|
> (**[2026-08-28 `H-161`]** "M5 이후"에서 당김 — 루트 예외 폐기 뒤 M5의 유일한 루트
|
||||||
|
> 부착 경로; 프로바이더 op가 필요하니 M5가 자연스러운 자리). 결정이 나면 `base/`로
|
||||||
|
> 승격한다.
|
||||||
>
|
>
|
||||||
> **`archive/existing-instance-bind-rejected.md`(2026-08-14 기각)와의 관계**:
|
> **`archive/existing-instance-bind-rejected.md`(2026-08-14 기각)와의 관계**:
|
||||||
> 그 기각은 *"이미 있는 Instance에 나중에 새 props를 다시 바인드"*였고 사유는
|
> 그 기각은 *"이미 있는 Instance에 나중에 새 props를 다시 바인드"*였고 사유는
|
||||||
|
|
@ -131,7 +133,8 @@ local cloned = Claim(template:Clone(), M.Frame "root" { -- ← 루트 이름
|
||||||
백로그에 포인터만. **[2026-08-28 `/code-review`, `H-161`]** 단 `H-146` 루트
|
백로그에 포인터만. **[2026-08-28 `/code-review`, `H-161`]** 단 `H-146` 루트
|
||||||
예외를 폐기한 지금 **M5에 승인된 루트 부착 경로가 없다** — (a) `Claim`을 M5
|
예외를 폐기한 지금 **M5에 승인된 루트 부착 경로가 없다** — (a) `Claim`을 M5
|
||||||
스코프로 당김 / (b) `Claim` 전까지 M5 한정 임시 예외 / (c) 루트 컨테이너용
|
스코프로 당김 / (b) `Claim` 전까지 M5 한정 임시 예외 / (c) 루트 컨테이너용
|
||||||
얇은 표면. 권고 (a).
|
얇은 표면. **사용자 확정 (a)**(*"M5 스코프로 올라가도 될것으로 보임"*) — 헤더와
|
||||||
|
`ROADMAP.md` M5 체크박스에 반영. §5-7(다중 스크립트)은 여전히 미결.
|
||||||
7. **[2026-08-28 `/code-review`, `H-161`] 여러 스크립트/여러 quad가 같은 루트
|
7. **[2026-08-28 `/code-review`, `H-161`] 여러 스크립트/여러 quad가 같은 루트
|
||||||
컨테이너를 쓰는 경우** — 위 "이중 claim error / 다중 quad UB / 부기 대상 자식
|
컨테이너를 쓰는 경우** — 위 "이중 claim error / 다중 quad UB / 부기 대상 자식
|
||||||
전부 매핑"을 그대로 두면 `Shop.client.luau`와 `Inventory.client.luau`가 각각
|
전부 매핑"을 그대로 두면 `Shop.client.luau`와 `Inventory.client.luau`가 각각
|
||||||
|
|
|
||||||
|
|
@ -1988,5 +1988,8 @@ Q4(`EffectHandle` 네 진입점 의사코드 — Observer 것 재사용, `Unsubs
|
||||||
`_epochs`는 emit 때만 갱신, `Refresh` 캐치업 폐기 + "게이트는 emit 경로만 미룬다"
|
`_epochs`는 emit 때만 갱신, `Refresh` 캐치업 폐기 + "게이트는 emit 경로만 미룬다"
|
||||||
계약(`H-151`) / `GateNode` 브랜드 등록(`H-152`) / Store 예약 이름 런타임
|
계약(`H-151`) / `GateNode` 브랜드 등록(`H-152`) / Store 예약 이름 런타임
|
||||||
가드(`H-153`) / `InstanceChildHandler` dedup(`H-154`) / ROADMAP·debounce·store
|
가드(`H-153`) / `InstanceChildHandler` dedup(`H-154`) / ROADMAP·debounce·store
|
||||||
stale(`H-155`~`H-157`). 미결 `H-158`(`:Block` 슈가). 소스는
|
stale(`H-155`~`H-157`). **같은 세션 후속**으로 `/code-review` 3건 + `H-158`~`H-162`도
|
||||||
|
확정 — `:Block` 폐기(`__apply`) / **`_rerunRequired` 홀드 플래그**(사용자 제안,
|
||||||
|
`_installed` 흡수, Observer 대칭) / `Claim` M5 스코프 / `Void` export. 미결은
|
||||||
|
`Claim` 갈래(특히 다중 스크립트)뿐. 소스는
|
||||||
`qa-request/pre-implementation-handtrace-round10-followup.md`.
|
`qa-request/pre-implementation-handtrace-round10-followup.md`.
|
||||||
|
|
|
||||||
|
|
@ -34,3 +34,23 @@
|
||||||
- 사용자가 결정 직전에 전제를 묻는 패턴(*"그게 진짜 날 수 있어?"*, *"canExecute 가
|
- 사용자가 결정 직전에 전제를 묻는 패턴(*"그게 진짜 날 수 있어?"*, *"canExecute 가
|
||||||
막는다가 말이 맞아?"*)이 두 번 다 유효한 정정으로 이어졌다 — 그때 "맞다"로
|
막는다가 말이 맞아?"*)이 두 번 다 유효한 정정으로 이어졌다 — 그때 "맞다"로
|
||||||
넘기지 말고 층을 갈라 답할 것.
|
넘기지 말고 층을 갈라 답할 것.
|
||||||
|
|
||||||
|
## 후속 (같은 세션) — `/code-review` 3건과 `H-158`~`H-162`
|
||||||
|
|
||||||
|
반영 뒤 감사 6라운드 수렴 → `/code-review high` 10건 중 셋이 판단 필요라 커밋
|
||||||
|
`ae34cfa` 뒤 사용자와 이어서 처리:
|
||||||
|
- **`H-158`** `:Block` 폐기 확정(*"내가 이미 그렇게 정했었는데, 전파가 안 된
|
||||||
|
부분"*) + `__apply` 필드명(코퍼스는 "구현 시"로 열어뒀었다).
|
||||||
|
- **`H-160`** (a) + *"한번 죽는게 나오면 Effect 가 전부 죽는다"* 계약 상향.
|
||||||
|
- **`H-161`** `Claim` M5 스코프.
|
||||||
|
- **`H-159`** — 리뷰 권고((a) `Refresh` 복원)를 사용자가 **다른 모양**으로 뒤집음:
|
||||||
|
`_rerunRequired` 홀드. 대화 네 턴: (1) 홀드 제안 → 메인이 `fire` 분기 모양 제시
|
||||||
|
→ (2) 사용자: cleanup 중 변경도 홀드해야(포탈), `fire`는 그냥 `Rerun`, `force` ≈
|
||||||
|
`_rerunRequired` → (3) 메인: `_installed`를 `_rerunRequired`로 통합 제안 → (4)
|
||||||
|
사용자: `from == nil` 가드 의문(→ 출처 `Epoch`와 값 `nil`의 혼동을 갈라 답함),
|
||||||
|
*"force 는 딱 하나의 역할"*. 결정의 소스는 followup `H-159` 절.
|
||||||
|
- **`H-162`** `Void` export(사용자 발의).
|
||||||
|
교훈: **리뷰 권고 (a)가 "옛 메커니즘 복원"일 때는 사용자가 더 단순한 새 모양을
|
||||||
|
갖고 있을 가능성이 높다** — `Refresh`(두 번째 갱신 경로) 대신 홀드 플래그(갱신
|
||||||
|
경로는 하나, 실행만 미룸). 이번 세션에서 `_installed`가 사라진 것처럼 통합 기회도
|
||||||
|
같이 온다.
|
||||||
|
|
|
||||||
|
|
@ -56,8 +56,10 @@
|
||||||
와 함께 **같은 날 대화형으로 전량 결정·반영**(소스 `-round10-followup.md`).
|
와 함께 **같은 날 대화형으로 전량 결정·반영**(소스 `-round10-followup.md`).
|
||||||
**어제 결정 중 뒤집힌 것 셋**: `fn`/cleanup은 자기 구독을 못 바꾼다(`H-147`, `H-143` 소멸) /
|
**어제 결정 중 뒤집힌 것 셋**: `fn`/cleanup은 자기 구독을 못 바꾼다(`H-147`, `H-143` 소멸) /
|
||||||
재구독·재바인드의 `Refresh` 캐치업 폐기(`H-151`) / 루트는 밖에서 `.Parent =`가
|
재구독·재바인드의 `Refresh` 캐치업 폐기(`H-151`) / 루트는 밖에서 `.Parent =`가
|
||||||
아니라 quad가 `Claim`으로 소유(`H-148`, `research/existing-mount-plan.md`). 남은 미결은 `question.md` 최우선 절의 둘
|
아니라 quad가 `Claim`으로 소유(`H-148`, `research/existing-mount-plan.md`). **같은 날 후속** `H-158`~`H-162`(`:Block` 폐기 → `state:Apply(blocker)` /
|
||||||
(`H-158` `:Block` 슈가, `Claim` 갈래 — 개수는 `research/existing-mount-plan.md` §5가 소스) — 게이트 아님. 남은 액션: M2 착수.**
|
`_rerunRequired` 홀드 플래그가 `_installed`를 흡수 / `Claim` M5 스코프 / `Void` export)도
|
||||||
|
확정·반영. 남은 미결은 `question.md` 최우선 절의 `Claim` 갈래(특히 §5-7 다중
|
||||||
|
스크립트) 하나 — 게이트 아님. 남은 액션: M2 착수.**
|
||||||
아래는 돌리기 전(2026-08-26) 서술:
|
아래는 돌리기 전(2026-08-26) 서술:
|
||||||
지시서는 `qa-request/pre-implementation-handtrace-round9-brief.md`. 스코프는
|
지시서는 `qa-request/pre-implementation-handtrace-round9-brief.md`. 스코프는
|
||||||
**커밋 `9dd8213` 하나의 델타**다 — 8라운드 결정 반영과 그 뒤
|
**커밋 `9dd8213` 하나의 델타**다 — 8라운드 결정 반영과 그 뒤
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ M2(반응형 코어 — Source/State/Store)**. **⚠️ [2026-08-24] M2와 M3의
|
||||||
낸 `H-143`~`H-146`까지 전량 반영 완료**; **[2026-08-28] 10라운드 몫은
|
낸 `H-143`~`H-146`까지 전량 반영 완료**; **[2026-08-28] 10라운드 몫은
|
||||||
`-round10-followup.md`** — 광범위 탐사 `H-150`~`H-157`까지 전량 결정·반영, 그중
|
`-round10-followup.md`** — 광범위 탐사 `H-150`~`H-157`까지 전량 결정·반영, 그중
|
||||||
`H-143`과 `H-146` 루트 예외는 하루 만에 뒤집힘)이고, `.claude/question.md` 최우선
|
`H-143`과 `H-146` 루트 예외는 하루 만에 뒤집힘)이고, `.claude/question.md` 최우선
|
||||||
절엔 그 대화에서 새로 생긴 둘(`H-158` `:Block` 슈가 / `Claim` 갈래 —
|
절엔 `Claim` 갈래(`research/existing-mount-plan.md` §5, 특히 다중 스크립트)만
|
||||||
`research/existing-mount-plan.md` §5)만 남아 있다(M2 착수 게이트는 아님). 같은 상태를 `.claude/project-context.md`도
|
남아 있다(M2 착수 게이트는 아님). 같은 상태를 `.claude/project-context.md`도
|
||||||
서술하니 마일스톤이 넘어갈 때 두 곳을 같이 고칠 것. 진행 상황의 소스는
|
서술하니 마일스톤이 넘어갈 때 두 곳을 같이 고칠 것. 진행 상황의 소스는
|
||||||
항상 루트 `ROADMAP.md`.
|
항상 루트 `ROADMAP.md`.
|
||||||
|
|
||||||
|
|
|
||||||
39
ROADMAP.md
39
ROADMAP.md
|
|
@ -32,7 +32,7 @@ quad-v2 구현 단계 실행 계획. 설계 근거/아키텍처 자체는 여기
|
||||||
> (`-round10-followup.md`가 소스) — 그중 둘은 하루 만에 다시 뒤집혔다: `fn`/cleanup은
|
> (`-round10-followup.md`가 소스) — 그중 둘은 하루 만에 다시 뒤집혔다: `fn`/cleanup은
|
||||||
> 자기 구독을 못 바꾼다(`H-147`, `H-143` 소멸 · `rawRerun(force)`/`Rerun` 분리) /
|
> 자기 구독을 못 바꾼다(`H-147`, `H-143` 소멸 · `rawRerun(force)`/`Rerun` 분리) /
|
||||||
> 루트는 밖에서 `.Parent =`가 아니라 **quad가 `Claim`으로 소유**(`H-148`,
|
> 루트는 밖에서 `.Parent =`가 아니라 **quad가 `Claim`으로 소유**(`H-148`,
|
||||||
> `research/existing-mount-plan.md`, M5 이후). 그 밖에 `_epochs`는 emit 때만 갱신
|
> `research/existing-mount-plan.md`, **M5 스코프** — `H-161`). 그 밖에 `_epochs`는 emit 때만 갱신
|
||||||
> (`Refresh` 캐치업 폐기, `H-151`) / `Effect._blocker` 제거(`H-150`) / Observer
|
> (`Refresh` 캐치업 폐기, `H-151`) / `Effect._blocker` 제거(`H-150`) / Observer
|
||||||
> 진입점 인라인(`H-149`) / `GateNode` 브랜드 등록(`H-152`) / Store 예약 이름
|
> 진입점 인라인(`H-149`) / `GateNode` 브랜드 등록(`H-152`) / Store 예약 이름
|
||||||
> 런타임 가드(`H-153`) / `InstanceChildHandler` dedup(`H-154`).
|
> 런타임 가드(`H-153`) / `InstanceChildHandler` dedup(`H-154`).
|
||||||
|
|
@ -430,6 +430,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
||||||
if isState(sub) then sub:_receive(from) -- 자식 노드: 게이트 없음
|
if isState(sub) then sub:_receive(from) -- 자식 노드: 게이트 없음
|
||||||
elseif canExecute(sub) then -- Observer만 (Effect는 자기 내부 Observer로 온다)
|
elseif canExecute(sub) then -- Observer만 (Effect는 자기 내부 Observer로 온다)
|
||||||
sub.fn(sub._state, sub, from) -- [H-109] (리시버 State, Observer 자신, 출처)
|
sub.fn(sub._state, sub, from) -- [H-109] (리시버 State, Observer 자신, 출처)
|
||||||
|
else sub._rerunRequired = true -- [2026-08-28 `H-159`] 묶이기 전의 변경은 홀드 → 바인드/구독 시 1회
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
`canExecute`가 `inst`를 인자로 받을 수 없는 이유는 그대로다(State는
|
`canExecute`가 `inst`를 인자로 받을 수 없는 이유는 그대로다(State는
|
||||||
|
|
@ -489,7 +490,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
||||||
자체는 M3다** — 레지스트리가 거기서 생긴다(M3의 그 항목).
|
자체는 M3다** — 레지스트리가 거기서 생긴다(M3의 그 항목).
|
||||||
- [ ] `Effect(fn, ...deps)` — ~~**⚠️ 선행: `Blocker`의 기본 메커니즘**~~
|
- [ ] `Effect(fn, ...deps)` — ~~**⚠️ 선행: `Blocker`의 기본 메커니즘**~~
|
||||||
(**[2026-08-28 10라운드 `H-150`]** 선행 요구 **해소** — 생성자의 사적
|
(**[2026-08-28 10라운드 `H-150`]** 선행 요구 **해소** — 생성자의 사적
|
||||||
`Blocker`는 `fire` 첫 줄의 `canExecute`가 이미 같은 억제를 해서 한 번도
|
`Blocker`는 `canExecute`(지금은 `rawRerun` 진입, `H-159`)가 이미 같은 억제를 해서 한 번도
|
||||||
판정에 닿지 않는 죽은 부품이라 제거됐다. `Blocker.luau`는 이제 `GateNode`/
|
판정에 닿지 않는 죽은 부품이라 제거됐다. `Blocker.luau`는 이제 `GateNode`/
|
||||||
Slot 쪽 요구뿐.) (`base/effect-plan.md`, **[2026-08-21 5라운드
|
Slot 쪽 요구뿐.) (`base/effect-plan.md`, **[2026-08-21 5라운드
|
||||||
`C-6`]** 옛 시그니처는 `Effect(fn, state?)`) — deps 생략 시 설치
|
`C-6`]** 옛 시그니처는 `Effect(fn, state?)`) — deps 생략 시 설치
|
||||||
|
|
@ -505,14 +506,15 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
||||||
모듈/스크립트 레벨 Effect) — `:Unsubscribe()`는 Observer와 달리
|
모듈/스크립트 레벨 Effect) — `:Unsubscribe()`는 Observer와 달리
|
||||||
마지막 cleanup을 1회 트리거해야 함(2026-08-07 일곱 번째 세션).
|
마지막 cleanup을 1회 트리거해야 함(2026-08-07 일곱 번째 세션).
|
||||||
**[2026-08-28 10라운드 `H-147`]** `rawRerun(self, force)` 본체 + 공개
|
**[2026-08-28 10라운드 `H-147`]** `rawRerun(self, force)` 본체 + 공개
|
||||||
`Rerun()`(진입에서 `canExecute` 게이트 — 죽은·안 묶인 핸들은 정의된 no-op),
|
`Rerun()`(진입에서 `canExecute` 게이트 — 죽은·안 묶인 핸들의 요청은 **[`H-159`]** `_rerunRequired`로 홀드),
|
||||||
생성자는 `rawRerun(self, true)`. **`fn`/cleanup은 자기 구독을 못 바꾼다** —
|
생성자는 `rawRerun(self, true)`. **`fn`/cleanup은 자기 구독을 못 바꾼다** —
|
||||||
네 진입점 첫 줄에 `_running` 가드(2026-08-27의 "`fn` 안 `Unsubscribe` 지원"
|
네 진입점 첫 줄에 `_running` 가드(2026-08-27의 "`fn` 안 `Unsubscribe` 지원"
|
||||||
`H-143`과 `wasAlive` 꼬리는 소멸). 네 진입점은 **`EffectHandle` 자기 것**
|
`H-143`과 `wasAlive` 꼬리는 소멸). 네 진입점은 **`EffectHandle` 자기 것**
|
||||||
(`H-144` (b) — 공유는 `Observer.luau`의 레지스트리 둘과 `canBound`뿐),
|
(`H-144` (b) — 공유는 `Observer.luau`의 레지스트리 둘과 `canBound`뿐),
|
||||||
`Subscribe`/`WeakSubscribe`는 등록 끝에 `not _installed → Rerun`(재구독
|
`Subscribe`/`WeakSubscribe`는 등록 끝에 `_rerunRequired → Rerun`(재구독
|
||||||
재설치; **[`H-151`]** `_epochs:Refresh()` 캐치업은 폐기 — `_epochs`는
|
재설치 + 홀드된 변경; **[`H-151`/`H-159`]** `_epochs:Refresh()` 캐치업은 폐기 —
|
||||||
`fire`의 `Update`에서만 갱신) — 의사코드는 `base/effect-plan.md`.
|
`_epochs`는 `fire`의 `Update`에서만 갱신하고, 실행 불가 상태에 온 변경은
|
||||||
|
`rawRerun`이 `_rerunRequired`로 홀드) — 의사코드는 `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`로 등록하는 것
|
||||||
|
|
@ -531,11 +533,12 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
||||||
Observer엔 자기 epoch가 없지만 `Ref`는 그 자체가 epoch다). dedup은
|
Observer엔 자기 epoch가 없지만 `Ref`는 그 자체가 epoch다). dedup은
|
||||||
클로저 identity가 아니라 `_deps`/`_epochs` 맵이 한다 ·
|
클로저 identity가 아니라 `_deps`/`_epochs` 맵이 한다 ·
|
||||||
~~**`handle._blocker`**~~(**[2026-08-28 `H-150`]** 제거 — 등록 구간의
|
~~**`handle._blocker`**~~(**[2026-08-28 `H-150`]** 제거 — 등록 구간의
|
||||||
즉시-1회 호출은 별도 필드 없이 `fire` 첫 줄의 `canExecute`가 억제한다; 옛
|
즉시-1회 호출은 별도 필드 없이 `fire`의 `from == nil` 가드와 `rawRerun`의 `canExecute`가 억제한다(`H-159`); 옛
|
||||||
`_installing` 플래그도 생성자 구간만 덮어 폐기됐었다) ·
|
`_installing` 플래그도 생성자 구간만 덮어 폐기됐었다) ·
|
||||||
**`handle._cleanup`**(직전 cleanup 보관, `Rerun`과 `Destroying` 클로저가
|
**`handle._cleanup`**(직전 cleanup 보관, `Rerun`과 `Destroying` 클로저가
|
||||||
같은 자리를 읽는다) · **`handle._installed`**(설치 여부 — `fn`의 cleanup
|
같은 자리를 읽는다) · **`handle._rerunRequired`**(`fn`이 돌아야 하는데 아직 안 돌았다 — 생성 직후 /
|
||||||
반환이 **선택**이라 `_cleanup`의 유무로는 판정할 수 없다) ·
|
소진 뒤 / 실행 불가 상태에 온 변경; **[2026-08-28 `H-159`]** 옛 `_installed`를 흡수.
|
||||||
|
`fn`의 cleanup 반환이 **선택**이라 `_cleanup`의 유무로는 판정할 수 없다) ·
|
||||||
**`handle._running`/`_pending`**(`Rerun` 재진입
|
**`handle._running`/`_pending`**(`Rerun` 재진입
|
||||||
지연) · **`handle._destroyConn`** ·
|
지연) · **`handle._destroyConn`** ·
|
||||||
**`:_bindDestroying(inst)`/`:_unbindDestroying()`**
|
**`:_bindDestroying(inst)`/`:_unbindDestroying()`**
|
||||||
|
|
@ -549,9 +552,9 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
||||||
**⭐ dep 등록은 생성자에서 한 번만** — `:WeakSubscribe()`/
|
**⭐ dep 등록은 생성자에서 한 번만** — `:WeakSubscribe()`/
|
||||||
`:WeakCallback()`으로 걸고, 바인드/언바인드는 dep을 아예 안 건드린다
|
`:WeakCallback()`으로 걸고, 바인드/언바인드는 dep을 아예 안 건드린다
|
||||||
(`H-58`/`H-59`). 발화 게이트는 전부 **`canExecute(handle)`** 하나다
|
(`H-58`/`H-59`). 발화 게이트는 전부 **`canExecute(handle)`** 하나다
|
||||||
(`H-7`). 캐치업은 바인드 직후 **재설치 1회뿐**(`if not self._installed then
|
(`H-7`). 캐치업은 바인드 직후 **`_rerunRequired`면 1회**(`if self._rerunRequired then
|
||||||
self:Rerun() end` — **[2026-08-28 `H-151`]** 옛 `_epochs:Refresh()`는 폐기,
|
self:Rerun() end` — **[2026-08-28 `H-151`/`H-159`]** 옛 `_epochs:Refresh()`는 폐기,
|
||||||
`_epochs`는 emit 받을 때만 갱신 — `H-64`/`H-65`). 의사코드는 `base/effect-plan.md`가 소스
|
`_epochs`는 emit 받을 때만 갱신하되 실행 불가 상태의 변경은 홀드 — `H-64`/`H-65`). 의사코드는 `base/effect-plan.md`가 소스
|
||||||
- [ ] **[2026-08-24 `H-23`]** State 전파 루프는 구독자 집합을 **배열로
|
- [ ] **[2026-08-24 `H-23`]** State 전파 루프는 구독자 집합을 **배열로
|
||||||
스냅샷한 뒤** 돈다 — 순회 중 새 구독자 추가가 정상 경로인데 Lua에서
|
스냅샷한 뒤** 돈다 — 순회 중 새 구독자 추가가 정상 경로인데 Lua에서
|
||||||
미정의라, 실측에서 실행마다 결과가 달라지고 한 Observer가 통째로
|
미정의라, 실측에서 실행마다 결과가 달라지고 한 Observer가 통째로
|
||||||
|
|
@ -616,7 +619,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
||||||
쪽 헬퍼다) **최소한 그 셋이 도는 형태까지는 M3(디스패치)가 요구**
|
쪽 헬퍼다) **최소한 그 셋이 도는 형태까지는 M3(디스패치)가 요구**
|
||||||
- [ ] **[2026-08-24 `H-25` 파생, 2026-08-25 `H-80`으로 목록 확장]**
|
- [ ] **[2026-08-24 `H-25` 파생, 2026-08-25 `H-80`으로 목록 확장]**
|
||||||
`quad-types`의 `Quad`에 **이 마일스톤이 얹는 탑레벨 값 전부** 추가 —
|
`quad-types`의 `Quad`에 **이 마일스톤이 얹는 탑레벨 값 전부** 추가 —
|
||||||
`Source` / `Store` / `Effect` / `Blocker` / `Relate` / **`Ref`**(최소형,
|
`Source` / `Store` / `Effect` / `Blocker` / `Relate` / **`Void`**(단일 no-op 함수 export — no-op 클로저를 돌려주는 자리는 새 클로저 대신 이것, **[2026-08-28 `H-162`]**) / **`Ref`**(최소형,
|
||||||
2026-08-27 `H-128`) /
|
2026-08-27 `H-128`) /
|
||||||
`is*` 전량(`isState`/`isSource`/`isStore`/`isRef`/`isObserver`/
|
`is*` 전량(`isState`/`isSource`/`isStore`/`isRef`/`isObserver`/
|
||||||
`isEffect`/`isEpoch`/`isModifier` …) / `bindLifetime`·`unbindLifetime`·
|
`isEffect`/`isEpoch`/`isModifier` …) / `bindLifetime`·`unbindLifetime`·
|
||||||
|
|
@ -643,7 +646,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
||||||
`luau-test`의 `15-type-compute-trailing-deps-typepack.luau`로
|
`luau-test`의 `15-type-compute-trailing-deps-typepack.luau`로
|
||||||
이형 다중 deps를 제네릭 타입 팩으로 표현 가능한지만 실측 필요(안
|
이형 다중 deps를 제네릭 타입 팩으로 표현 가능한지만 실측 필요(안
|
||||||
되면 동종 타입 dep 1개로 한정)
|
되면 동종 타입 dep 1개로 한정)
|
||||||
- [ ] **[2026-08-25 신설, `H-84`]** `:With(...)` / `state:Block(blocker)` /
|
- [ ] **[2026-08-25 신설, `H-84`]** `:With(...)` / `state:Apply(blocker)` /
|
||||||
`Source:Emit()` — `:Compute`/`:Apply`/`:Observer`는 각각 체크박스가
|
`Source:Emit()` — `:Compute`/`:Apply`/`:Observer`는 각각 체크박스가
|
||||||
있는데 이 셋만 빠져 있었다
|
있는데 이 셋만 빠져 있었다
|
||||||
- [ ] **[2026-08-25 신설, `H-81`; 2026-08-26 자리 정정 `H-122`]**
|
- [ ] **[2026-08-25 신설, `H-81`; 2026-08-26 자리 정정 `H-122`]**
|
||||||
|
|
@ -844,7 +847,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
||||||
`base/gate-plan.md`가 소스 — 여기서 반복하지 않는다.
|
`base/gate-plan.md`가 소스 — 여기서 반복하지 않는다.
|
||||||
- [ ] 핸들러 계약 검증: `process`가 retractor 클로저를 **반환하지 않는**
|
- [ ] 핸들러 계약 검증: `process`가 retractor 클로저를 **반환하지 않는**
|
||||||
핸들러를 등록하면 리뷰/린트에서 걸러내기(정리할 게 없어도 항상
|
핸들러를 등록하면 리뷰/린트에서 걸러내기(정리할 게 없어도 항상
|
||||||
`function() end`를 반환 — `Dispatch.retractFrom`이 nil 체크 없이
|
`Void`(**[2026-08-28 `H-162`]** 단일 no-op)를 반환 — `Dispatch.retractFrom`이 nil 체크 없이
|
||||||
호출, `base/dispatch-core-plan.md` "핸들러 계약" 절, 2026-08-08 세션
|
호출, `base/dispatch-core-plan.md` "핸들러 계약" 절, 2026-08-08 세션
|
||||||
/ **2026-08-13 다섯 번째 세션에 별도 `retract` 필드가 `process`
|
/ **2026-08-13 다섯 번째 세션에 별도 `retract` 필드가 `process`
|
||||||
반환값으로 합쳐지며 대상만 바뀜**)
|
반환값으로 합쳐지며 대상만 바뀜**)
|
||||||
|
|
@ -970,7 +973,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
||||||
문단. 빠뜨리면
|
문단. 빠뜨리면
|
||||||
`Frame { Frame{}, Slot() }`이 첫 마운트에서 죽는다 —
|
`Frame { Frame{}, Slot() }`이 첫 마운트에서 죽는다 —
|
||||||
`base/dispatch-core-plan.md`의 `H-39` 블록(그 다섯째 항목)이 소스.
|
`base/dispatch-core-plan.md`의 `H-39` 블록(그 다섯째 항목)이 소스.
|
||||||
- [ ] **[2026-08-28 백로그, M5 이후]** `Claim(inst, D.Mapper.<Class> "Name" {…})` — 이미 있는 트리(PlayerGui·`Clone()` 사본)를 quad가 소유. 프로바이더 op `nativeFindChild` 필요. 갈래 미결 — `research/existing-mount-plan.md` §5가 소스(개수도), 다음 배치 문항.
|
- [ ] **[2026-08-28 M5 스코프, `H-161`]** `Claim(inst, D.Mapper.<Class> "Name" {…})` — 이미 있는 트리(PlayerGui·`Clone()` 사본)를 quad가 소유. `H-146` 루트 예외를 폐기한 뒤 M5에 승인된 루트 부착 경로가 이것뿐이라 **백로그가 아니라 M5 안**(사용자 확정). 프로바이더 op `nativeFindChild` 필요. 갈래 미결(특히 §5-7 다중 스크립트/루트 컨테이너) — `research/existing-mount-plan.md` §5가 소스(개수도), 다음 배치 문항.
|
||||||
- [ ] **Instance 생성 시점의 gcconn/gchold 셋업**(2026-08-14 다섯 번째 세션
|
- [ ] **Instance 생성 시점의 gcconn/gchold 셋업**(2026-08-14 다섯 번째 세션
|
||||||
확정, 옛 "`bindLifetime` 첫 호출에서 lazy 생성"에서 전환 — `base/
|
확정, 옛 "`bindLifetime` 첫 호출에서 lazy 생성"에서 전환 — `base/
|
||||||
lifecycle-pattern.md`의 "(0) gcconn/gchold는 Instance 생성 시점에
|
lifecycle-pattern.md`의 "(0) gcconn/gchold는 Instance 생성 시점에
|
||||||
|
|
@ -1514,8 +1517,8 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
||||||
않는다** — 그 필드 자체가 폐기됐다(`_deps` 하나로 통합). 그리고
|
않는다** — 그 필드 자체가 폐기됐다(`_deps` 하나로 통합). 그리고
|
||||||
`_bindDestroying`은 **`Ref` dep 콜백을 (재)등록하지 않는다** —
|
`_bindDestroying`은 **`Ref` dep 콜백을 (재)등록하지 않는다** —
|
||||||
dep 등록은 생성자에서 끝나고, 여기서 하는 건 `Destroying` 연결과
|
dep 등록은 생성자에서 끝나고, 여기서 하는 건 `Destroying` 연결과
|
||||||
**재설치 캐치업 한 줄**(`if not self._installed then self:Rerun() end` —
|
**홀드 캐치업 한 줄**(`if self._rerunRequired then self:Rerun() end` —
|
||||||
**[2026-08-28 `H-151`]** 옛 `Refresh()` 판정은 폐기)뿐이다. **이게 `Effect`의 leaf 사망 cleanup을 실제로
|
**[2026-08-28 `H-151`/`H-159`]** 옛 `Refresh()` 판정은 폐기)뿐이다. **이게 `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