qa: 9라운드 Q4~Q10 + H-138/H-139/H-142 확정·반영, 감사 8라운드, code-review 반영 — H-143~H-146 판단 대기
Q4 EffectHandle 네 진입점 의사코드(Observer 것 재사용, Unsubscribe만 게이트 통과 뒤 cleanup) / Q5 M2 공통 기반에 Ref 최소형(+ Quad.Ref 필드) / Q6 WeakUnsubscribe 관대 명문화 / Q7 폐기 블록 archive/effect-internal-observer-cascade-reversed.md / Q8 InstanceChildHandler 부기(Parent → setLength(1), retractor는 Parent=nil → 해제) / Q9 문항 전제 정정 — Tween 절 스케치 hint==nil 줄은 복사 오류, retractor는 function() end / Q10 :List reconcile 재실행도 배치 Blocker(ownsGate — 네스팅 불가) / H-138 숏핸드 우선순위 > PropertyHandler, 충돌 방지는 UI 접두어 / H-139 New(name)(props)·Dispatch.drive 파이프라인 의사코드(bind-system-plan) — 쓰면서 빈 배열 파트 가드와 H-142 발견 / H-142 props에 Parent 금지(부모가 하는 일) — 순서 문제 소멸, D·Modifier 타입 제외 + PropertyHandler 거부(배선은 에이전트 선택으로 갈라 적음). H-129/H-131 정정. 감사 8라운드(5→3→4→5→3→1→3→1) — 1라운드가 drive 의사코드의 H-17 위반(post-pass 포함 전체 감쌈)을 잡음, 이후는 기록 문서 표기. /code-review high 10건 — 여섯 반영(그중 셋이 이 세션의 H-134 반영이 만든 것), 넷은 새 메커니즘이라 문항 H-143~H-146으로(question.md 최우선 절). doc-check ERROR 0. Claude-Session: https://claude.ai/code/session_01546hjsYNLSMZdHdPyTZaGb Co-authored-by: qwreey <me@qwreey.moe>
This commit is contained in:
parent
031495cc0b
commit
7f5868302e
20 changed files with 961 additions and 129 deletions
File diff suppressed because one or more lines are too long
72
.claude/archive/effect-internal-observer-cascade-reversed.md
Normal file
72
.claude/archive/effect-internal-observer-cascade-reversed.md
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# [역전됨] `EffectHandle`의 내부 Observer 바인딩 세부 — `_observers` 배열 + `bindLifetime`/`:Subscribe()` cascade
|
||||
|
||||
> **역전일**: 2026-08-25(7라운드 `H-58`/`H-59`). **대체된 곳**:
|
||||
> `base/effect-plan.md`의 "확정 구조 — 강한 주인은 항상 `Effect`" 절과
|
||||
> "의사코드 — 생성자 / `bindLifetime`이 부르는 두 훅 / `Rerun`" 절 —
|
||||
> `bindLifetime`/`unbindLifetime`은 **`Effect` 핸들 하나에만** 적용되고 내부
|
||||
> Observer로 cascade하지 않는다. dep 등록은 생성자에서 한 번만
|
||||
> `WeakSubscribe`/`WeakCallback`으로 하고, 발화 여부는 `canExecute(handle)`이
|
||||
> 전담한다. 아래가 서술하는 `_observers` 배열/cascade/`Subscribe` 순회는
|
||||
> 전부 `_deps` 하나와 `_blocker`로 대체됐다. **이 문단대로 짜면 `H-58`의
|
||||
> 중복 `Rerun`이 되살아난다.**
|
||||
>
|
||||
> **왜 `archive/`로 왔나 (2026-08-27, 9라운드 `H-130`, 사용자 결정 Q7-(b))**:
|
||||
> 이 블록은 `base/effect-plan.md` 안에 ⛔⛔ 폐기 배너를 단 채 남아 있었는데,
|
||||
> 배너 아래 죽은 문단을 **두 번**이나 살아 있는 문장처럼 편집하는 사고가
|
||||
> 났다(8라운드 2차 #1, 그리고 커밋 `9dd8213`이 아래 40번째 줄 근처의
|
||||
> *"`.Subscribed`는 구독 경로 전용"*을 날짜 마커 없이 고친 것 — 옛 표기는
|
||||
> *"전역 `:Subscribe()` 전용"*). `conventions.md`의 핸드오버 체크리스트 3번
|
||||
> (*"뒤집힌 원문은 `archive/`로 옮기고 포인터만 남길 것"*)이 정확히 이
|
||||
> 실패 모드를 규정하고 있어 그대로 따랐다. 아래는 **옮기는 시점의 원문
|
||||
> 그대로**(그 편집 흔적 포함)이고, 더 이상 갱신하지 않는다.
|
||||
|
||||
## 옮겨온 원문 (`base/effect-plan.md`, 2026-08-27 시점)
|
||||
|
||||
**보강 — `EffectHandle`의 내부 Observer 바인딩 세부(2026-08-09 열한 번째
|
||||
세션, 재확인 후 명시화)**:
|
||||
|
||||
> **⛔⛔ [2026-08-25 폐기, 7라운드 `H-58`/`H-59`] 이 문단 전체는 옛 모델이다.**
|
||||
> 위 "확정 구조 — 강한 주인은 항상 `Effect`" 절이 **정반대로** 확정했다 —
|
||||
> **`bindLifetime`/`unbindLifetime`은 `Effect` 핸들 하나에만 적용되고**
|
||||
> 내부 Observer로 cascade하지 않는다. dep 등록은 **생성자에서 한 번만**
|
||||
> `WeakSubscribe`/`WeakCallback`으로 하고, 발화 여부는 `canExecute(handle)`이
|
||||
> 전담한다. 아래가 서술하는 `_observers` 배열/cascade/`Subscribe` 순회는
|
||||
> **전부 `_deps` 하나와 `_blocker`로 대체됐다**(위 "필드 목록").
|
||||
> 아래는 히스토리로만 읽을 것 — **이 문단대로 짜면 `H-58`의 중복 `Rerun`이
|
||||
> 되살아난다.**
|
||||
|
||||
**⚠️ [2026-08-24 6라운드 손 트레이싱 `H-8`, 2026-08-25 폐기] 이 문단 전체가 아직 "Observer 하나"
|
||||
전제로 쓰여 있었다 — `_observer`(단수)를 `_observers`(배열)로 읽을 것.**
|
||||
아래 절이 확정한 `Effect(fn, ...deps)`(N-deps)와 정면으로 어긋났고, 그대로
|
||||
구현하면 **2번째 이후 dep의 Observer엔 `canExecute` 판정 근거가 아예 안 실려**
|
||||
그 Observer의 재실행이 통째로 죽는다 — 바로 이 문단 자신이 경고하는 실패
|
||||
모드다. 필드를 배열로 바꾸고 cascade/`Subscribe`/`Unsubscribe`를 전부 순회로
|
||||
고친다(새 결정 없음, 반영 누락). `Ref` dep은 Observer가 아니라 콜백이라 이
|
||||
배열에 안 들어간다 — 그쪽 해제는 아래 `H-7` 문단이 소스.
|
||||
|
||||
- **`EffectHandle`은 내부 Observer를 필드로 강참조** — `handle._observers[i] =
|
||||
observer`(dep이 State/Source인 경우만 존재). 이건 GC 방지가 목적이 아니라
|
||||
(그건 아래 `bindLifetime`/`gchold`가 담당) `:Unsubscribe()`/`bindLifetime`
|
||||
cascade가 이 필드를 통해 내부 Observer에 접근하기 위한 것.
|
||||
- **`bindLifetime(inst, handle)`은 `state`가 있는 경우 내부 Observer도
|
||||
같은 `inst`로 `handle._observers` **전부**에 대해
|
||||
`bindLifetime(inst, observer)`를 cascade해야 함** — `Dispatch/Leaf.luau`가 children 배열의 `EffectHandle`을 매치해
|
||||
`bindLifetime(inst, handle)`을 부르는 시점(leaf 부착)과, `:Subscribe()`가
|
||||
`handle`을 전역 레지스트리에 등록하는 시점(아래) 둘 다 해당. 이유:
|
||||
`canExecute(observer)`가 보는 gcconn 참조는 **그 Observer 자신이
|
||||
`bindLifetime(inst, observer)`될 때 그 Observer 쪽 릴레이션에
|
||||
복사되는 것**이라, `EffectHandle`만 바인드하고 내부 Observer는 안 하면
|
||||
그 Observer에겐 판정 근거가 아예 없어서 `canExecute`가 항상 거짓이 됨
|
||||
(=재실행이 통째로 죽음). 같은 이유로 `unbindLifetime(handle)`도 내부
|
||||
Observer까지 같이 풀어야 대칭이 맞음.
|
||||
**[정정, 2026-08-14 다섯 번째 세션]** 이 항목이 원래 근거로 든
|
||||
"`canExecute`가 `Subscribed` 필드 + `inst`의 gcconn을 함께 본다"는
|
||||
틀렸음 — `.Subscribed`는 구독 경로 전용이고 leaf 경로와
|
||||
무관(`archive/canexecute-inst-arg-reversed.md`). cascade가 필요하다는
|
||||
결론은 그대로이고 오히려 근거가 더 직접적이 됨.
|
||||
- **`:Subscribe()`도 마찬가지로 `state`가 있으면 내부 Observer를 같은
|
||||
전역 강참조 레지스트리에 같이 등록**(`handle` 자신 + `handle._observers`
|
||||
전부, 또는 `handle._observers`만으로 충분한지는 구현 세부 — 어느 쪽이든
|
||||
"`EffectHandle`은 등록됐는데 내부 Observer는 등록 안 됨" 상태가 생기면
|
||||
안 됨).
|
||||
|
||||
|
|
@ -287,7 +287,7 @@ quad/
|
|||
│ │ └── Modifier.luau # [2026-08-24 `H-35`] ProcessedModifierHandler — flatten이 소진한 자리를 캐치해 `setOffsetSource(None)`/`setLength(0)`만 등록하는 nop 핸들러(`base/modifier-plan.md`)
|
||||
│ ├── 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`)
|
||||
│ ├── Ref.luau # 범용 값 박스(.Value 읽기 + :Set()/:Callback()/:Wait() 셋), `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 여섯 번째 세션에서 분리)
|
||||
│ ├── 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 개념 없음
|
||||
|
|
|
|||
|
|
@ -200,6 +200,139 @@ D.Frame = New<<Frame>> "Frame" :: (({ ...타입명시 }) -> Frame)
|
|||
커버"라는 옛 서술은 런타임에 대해서만 맞다** — 타입은 `D` 범위 안만
|
||||
정확하고 밖은 `any`다.
|
||||
|
||||
**⭐ [2026-08-27 신설, 9라운드 `H-139`] `New(name)(props)` 파이프라인 의사코드 —
|
||||
네 문서에 흩어져 있던 순서를 한 자리에.** 사용자 지시: *"실 구현 전에
|
||||
의사코드를 써보자. 그걸로 인해서 감추어졌던 설계 결함이나 폭탄이 발견된 경우가
|
||||
많아서, 커지기 전에 확인해볼 필요가 있음. 중요 계층이라서"*. 각 단계의 소스는
|
||||
주석의 문서이고 **여기는 순서만 확정**한다 — 단계 안의 규칙은 그 문서가 정본.
|
||||
(이름 주의: 여기 `New`는 quad-roblox `D/init.luau`의 **인스턴스 생성자**이고,
|
||||
`module-lifecycle-plan.md`의 `New(): Quad`는 quad-base의 **모듈 팩토리**다.
|
||||
패키지가 달라 런타임 충돌은 없지만 산문에서 섞이니, 생성자는 항상
|
||||
`New "Frame"` 꼴로, 팩토리는 `New()` 꼴로 쓸 것.)
|
||||
|
||||
```lua
|
||||
-- quad-roblox/src/D/init.luau — 생성기가 찍는 커링 생성자. 아래 ①~④ 순서가 계약.
|
||||
local function New(className: string)
|
||||
return function(props)
|
||||
-- ① 물리 생성 — 백엔드의 일. base는 `Instance`를 모른다
|
||||
-- (`dispatch-core-plan.md`의 "base가 소유하는 핸들러와 주입되는 엔진 op").
|
||||
local inst = Instance.new(className)
|
||||
|
||||
-- ② gcconn/gchold — 생성 **직후, 무조건**(핸들러/바인딩 유무와 무관).
|
||||
-- `lifecycle-pattern.md` (0)의 코드 그대로: 클로저가 `gchold`와 `inst`를
|
||||
-- 같이 캡처해 userdata 동일성을 고정하고 `InstData:SetWeak(inst, …)`.
|
||||
-- ③④보다 앞인 이유 — 거기서부터 `inst`를 키로 쓰는 `Relate`
|
||||
-- (`elementOwner`/`nameClaims`/`bk`/`chains`)가 생기는데, 키의 동일성
|
||||
-- 고정이 그보다 먼저여야 한다.
|
||||
-- (lifecycle-pattern.md (0)의 인라인 코드 — 헬퍼 이름은 구현 시)
|
||||
|
||||
-- ③ flatten — Modifier 항목을 제자리에서 `ProcessedModifier`로 소진하고
|
||||
-- 필드를 해시 파트로 merge. 새 테이블 없음, `inst`를 안 받는 순수 변환
|
||||
-- (`modifier-plan.md`의 "flatten의 정확한 형태"). `PreRef`/`PostRef`가
|
||||
-- Modifier 필드에 오는 건 타입으로 차단돼 있어 여기선 안 다룬다.
|
||||
local flattened = flatten(props)
|
||||
|
||||
-- ④ 디스패치 — pre-pass → 본체 → `postRefList`. 전부 `Dispatch.drive`가 소유.
|
||||
Dispatch.drive(inst, flattened)
|
||||
|
||||
return inst -- `D.Frame`은 이 함수에 캐스트만 얹은 별칭(위 확정)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```lua
|
||||
-- quad-base: Dispatch/init.luau
|
||||
function Dispatch.drive(inst, flattened)
|
||||
-- ⓪ 배치 Blocker — **진입 직후** 켜고 `drive`가 할 일을 전부 마친 뒤(post-pass
|
||||
-- 포함) 끈다: `dispatch-core-plan.md`의 `H-17` 계약(*"`drive` 전체를 `inst`
|
||||
-- 전용 `Blocker`로 감싼다"* / *"`PostRef` 콜백은 게이트가 켜진 채로 실행된다"*).
|
||||
-- ⚠️ 배열 파트가 비어 있으면 열지 않는다 — 안 그러면 `Frame { Size = … }`
|
||||
-- 처럼 자식 없는 모든 Instance마다 Blocker + `bk`가 eager 생성된다
|
||||
-- (9라운드 Q2/Q3가 Slot 쪽에서 막은 것과 같은 부류). pre-pass는 자리를
|
||||
-- 센티널로 바꿀 뿐 비우지 않으므로 이 판정은 pre-pass 앞뒤가 같다.
|
||||
local batching = flattened[1] ~= nil
|
||||
local blocker = if batching then getBlocker(inst) else nil
|
||||
if batching then blocker:On() end
|
||||
|
||||
-- (a) pre-pass — 배열 파트만, index 순서(`ref-plan.md`의 "메커니즘 — pre-pass 한 스윕" 절).
|
||||
-- `PreRef`는 그 자리에서 fire(`v:Set(inst)`) + 소진,
|
||||
-- `PostRef`는 수집 + 소진. `_fired` 1회용 가드는 둘 다 **여기서** 선다.
|
||||
local postRefList = {} -- 이 호출에만 로컬 — Relate 아님
|
||||
for i, v in ipairs(flattened) do
|
||||
if isPreRef(v) then
|
||||
if v._fired then error("PreRef instance reused", 2) end
|
||||
v._fired = true
|
||||
v:Set(inst) -- 콜백 fire — 아직 자식도 프로퍼티도 없다
|
||||
flattened[i] = ProcessedPreRef
|
||||
elseif isPostRef(v) then
|
||||
if v._fired then error("PostRef instance reused", 2) end
|
||||
v._fired = true
|
||||
table.insert(postRefList, v)
|
||||
flattened[i] = ProcessedPostRef
|
||||
end
|
||||
end
|
||||
|
||||
-- (b) 본체 — **단일 일반화 `for`**(`F-4-1`). 배열 → 해시 순서는 Luau 순회에
|
||||
-- 기댄다. 배열 파트가 Length/Offset **배치 등록 구간**이고 해시 파트는
|
||||
-- 부기를 안 만진다(`dispatch-core-plan.md`의 "해법의 핵심" 1번·4번).
|
||||
for k, v in flattened do
|
||||
Dispatch.process(inst, k, v, 1) -- 체인 index는 항상 1부터
|
||||
end
|
||||
|
||||
-- (c) `postRefList` — push 순서 = index 순서. 이 시점에 끝나 있는 것/아닌 것은
|
||||
-- `ref-plan.md`의 "`PostRef`" 절(자기 서브트리와 프로퍼티는 완성,
|
||||
-- **자기 `.Parent`는 아직**일 수 있다). **게이트가 켜진 채 돈다** — 콜백이
|
||||
-- `slot:Add(…)`로 이 `inst`에 emit을 올려도 `gatedRecompute`는 스킵되고
|
||||
-- 정합성은 아래 마지막 `recompute` 한 번에 의존한다(`H-17`).
|
||||
for _, v in ipairs(postRefList) do v:Set(inst) end
|
||||
|
||||
-- ⓪' 배치 닫기 — `drive`가 할 일을 전부 마친 뒤 딱 한 번.
|
||||
if batching then
|
||||
blocker:OffWithoutEmit()
|
||||
local bk = getBookkeeping(inst) -- 배열 파트가 있었으니 이미 존재
|
||||
if not bk.recomputeBlocker:IsOn() then recompute(inst, bk) end -- `H-119`
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**이 의사코드를 쓰면서 드러난 것**(결정은 `qa-request/pre-implementation-handtrace-round9-followup.md`의
|
||||
`H-139` 절 — 여기선 목록만):
|
||||
- **배치를 닫는 자리** — 처음엔 *"어디에도 안 적혀 있다"*고 보고 해시 파트
|
||||
앞에서 닫는 모양으로 썼는데, **틀렸다**(감사 1라운드가 잡음):
|
||||
`dispatch-core-plan.md`의 `H-17` 절이 이미 *"`drive` 전체(post-pass 포함)를
|
||||
감싼다, `PostRef` 콜백은 게이트가 켜진 채 실행된다"*로 정해뒀고 그 이유(단일
|
||||
루프에선 배열 파트의 끝이 루프 밖에서 관측되지 않는다 / `postRefList`는 해시
|
||||
파트보다 뒤다)까지 적혀 있었다. 위 의사코드는 그 계약대로 고쳤다. 실제로
|
||||
드러난 건 그 절의 아래쪽 "해법의 핵심" 4번이 옛 문구(*"배열 파트 순회
|
||||
전체"*)로 남아 있던 것 하나.
|
||||
- **자식 없는 Instance에도 Blocker/`bk`가 생기는 경로** — `getBlocker(inst)`를
|
||||
무조건 부르면 그렇게 된다. `flattened[1] ~= nil` 가드로 막았다.
|
||||
- **⭐ [2026-08-27 확정, 9라운드 `H-142`] props에 `Parent`는 올 수 없다 — 그건
|
||||
부모가 하는 일이다.** 의사코드를 쓰다 "해시 파트 안의 `Parent` 대입 순서가
|
||||
미정"이 드러났는데, 사용자는 순서를 정하는 대신 **키 자체를 금지**했다:
|
||||
*"Parent대입 자체가 오면 안 돼. 그건 부모에서 할 일이거든. 자신이 바로 하는
|
||||
경우는 없어. 그걸 허용해준다는것 자체가, '외부에서 직접 Parent 설정해주지
|
||||
말것' 을 해치는 요인이 되기도 해.(암묵적으고 가능하도록 둬버려서)"* —
|
||||
`slot-plan.md`가 *"동적 자식은 반드시 `Slot` 또는 `state<Frame>`류 store-bind를
|
||||
통해서만"* 이라고 세운 원칙(외부 코드가 `.Parent`로 자식을 끼우면 `Length`/
|
||||
형제 순서가 조용히 어긋난다)의 **정적 리터럴 판**이다. `.Parent` 대입은
|
||||
자식을 받는 쪽 — `InstanceChildHandler`(정적 자식, `H-134`)와 Slot의
|
||||
`native*` 주입 op — 만 한다.
|
||||
- **타입**: `D` 생성기가 각 클래스의 props 타입에서 `Parent`를 **제외**한다
|
||||
(`ROADMAP.md` M5 `D/init.luau` 체크박스) — **그리고 `FrameModifier`류
|
||||
메소드 목록에서도**(`ROADMAP.md` M7; **[2026-08-27 `/code-review`]** 두
|
||||
목록이 같은 API 덤프에서 따로 생성되는데 한쪽만 빼면
|
||||
`Modifier():Parent(x)`가 타입을 통과하고 `flatten`이 해시 파트로 merge한다 —
|
||||
`PreRef`/`PostRef`를 Modifier 타입으로 차단하는 것과 같은 자리). 범위 밖 클래스의 `New<<X>> "X"`는
|
||||
`any`라 타입으로 못 막고 아래 런타임 가드가 잡는다.
|
||||
- **런타임**: 새 메커니즘 없이 기존 계약으로 — `PropertyHandler.isHandlable`이
|
||||
`"Parent"`를 거부하면 그 키에 매치되는 핸들러가 없어 `Dispatch.process`의
|
||||
*"매치 핸들러 없음 → 즉시 error"* 계약(`ROADMAP.md` M3)에 걸린다. (이
|
||||
배선은 사용자 확정이 아니라 **규칙을 기존 계약에 얹은 제 선택**이다 —
|
||||
`H-142` 처방 후보 (a)/(b)/(c)가 전부 새 메커니즘이라 정하지 않았던 것을
|
||||
"키 금지"로 바꾸니 필요한 코드가 이 거부 한 줄뿐이다. 다른 모양이 낫다면
|
||||
갈아끼울 것.) 순서 문제는 키가 없어지면서 소멸한다.
|
||||
|
||||
**이벤트 바인딩 — `On.EventName` 도트액세스 안 씀, PA님 방식(평범한 문자열
|
||||
키 + 런타임 리플렉션)으로 전환**: `DeclarativeInstance.luau:13-91`의
|
||||
`assign(instance, key, value)`가 `ReflectionService:GetPropertiesOfClass`/
|
||||
|
|
|
|||
|
|
@ -245,6 +245,12 @@ blocker:Off() -- onunblock 핸들 실행 → HasBlockedEmit 확인 → 딱 한
|
|||
`Off()`는 스태킹 없이 즉시 그 자리에서 꺼진다. **이 제약은 반드시 사용자
|
||||
문서(API 레퍼런스 수준)에 명시적으로 강조할 것** — 네스팅을 시도하면
|
||||
조용히 잘못된 시점에 조기 해제되는, 원인 추적이 어려운 버그로 이어짐.
|
||||
**[2026-08-27 9라운드 `H-136`] 이 규칙의 예외가 아닌 것 하나** — 같은
|
||||
Blocker를 소유한 호출부가 `IsOn()`으로 "이미 바깥이 켜뒀다"를 알아보고 자기
|
||||
`On()`/`Off()`를 건너뛰는 것(`base/slot-plan.md`의 `reconcile` — 최초
|
||||
population은 바깥 배치 안에서, 재실행은 밖에서 같은 클로저로 불린다).
|
||||
Blocker 자신은 여전히 단순 불리언이고 두 번 켜지지 않는다 — 카운팅이
|
||||
아니라 소유권 판정이다.
|
||||
|
||||
**base 내부 용례에도 이 규칙이 그대로 적용된 실제 사례(2026-08-18)** —
|
||||
위 "`state:Block()` 없이 직접 쓰는 두 번째 용례" 절의 Length/Offset
|
||||
|
|
|
|||
|
|
@ -581,7 +581,11 @@ end
|
|||
`base/ref-plan.md`의 "`PostRef`" 절.
|
||||
**[2026-08-18 구현 전 QA 2라운드 후속, `RC-1` 해결 / 범위 정정
|
||||
2026-08-24 `H-17`] `drive` 전체를 `inst` 전용 `Blocker`로 감싼다** —
|
||||
진입 직후 `Relate(inst)`에 lazy 생성한 Blocker를 `:On()`하고,
|
||||
진입 직후 `Relate(inst)`에 lazy 생성한 Blocker를 `:On()`하고
|
||||
(**[2026-08-27 9라운드 `H-139`, 사용자 확인]** 단, **배열 파트가 비어
|
||||
있으면(`flattened[1] == nil`) 열지 않는다** — 안 그러면 자식 없는 모든
|
||||
Instance마다 Blocker + `bk`가 eager 생성된다. pre-pass는 자리를 센티널로
|
||||
바꿀 뿐 비우지 않아 이 판정은 진입 시점에 해도 같다),
|
||||
**`drive`가 할 일을 전부 마치면**(단일 일반화 순회 + post-pass 포함)
|
||||
`:OffWithoutEmit()` 한 뒤 `recompute(inst, bk)`를 명시적으로 1회 호출
|
||||
(**⭐ [2026-08-26, `/code-review high` 4차] 이 호출도 `H-119`의 재진입
|
||||
|
|
@ -1068,6 +1072,7 @@ end
|
|||
| `NoneHandler` | 중간 | 없음(재위임만) |
|
||||
| `NilHandler` | 말단 | 없음(`setLength`/`setOffsetSource` 부기만 — 2026-08-18 신설) |
|
||||
| `PropertyHandler` | 말단 | 프로퍼티 세팅 |
|
||||
| `InstanceChildHandler` | 말단 | `Parent` 대입 (+ 부기 — `H-134`) |
|
||||
| `TagHandler` | 말단 | `addTag`/`removeTag` (+ 부기 — `H-39`) |
|
||||
| `AttributeKeyHandler` | 말단 | `setAttribute` |
|
||||
| `AttributeGroupHandler` | 자기 체인에선 말단 | 다른 키로 위임 (+ 부기 — `H-39`) |
|
||||
|
|
@ -1472,6 +1477,36 @@ Dispatch.getOffsetAt(ownerKey, i): number -- [2026-08-21 5라운드] 그
|
|||
"Length/Offset에 참여하지 않는 별도 카테고리"로 재정의하면 `bk.N`의 의미가
|
||||
바뀌어 파급이 크다(사용자 확정, 2026-08-24).
|
||||
|
||||
**⭐⭐ [2026-08-27 9라운드 `H-134`] 다섯째 — `InstanceChildHandler`.** 위
|
||||
전수 grep은 **의사코드가 있는 핸들러만** 잡을 수 있었는데, 정적 자식
|
||||
Instance를 받는 이 핸들러(`quad-roblox/src/Handlers/InstanceChild.luau`,
|
||||
`k:number, v:Instance` — `architecture.md`의 소스 트리)는 코퍼스 어디에도
|
||||
의사코드가 없었고 위 표에도 행이 없었다. 아래 Length/Offset 절 머리가
|
||||
*"정적 단일 자식은 상수 `1`"*이라고만 하고 **누가** 등록하는지는 안 적어서,
|
||||
`Frame { Frame{}, Slot() }`처럼 **정적 자식 뒤에 Slot이 오는 가장 흔한
|
||||
배치가 첫 마운트에서 죽는다** — Slot의 `setOffsetSource(inst, 2, …)` →
|
||||
`getOffsetAt(inst, 2)`가 `lengthList[1] == nil`을 만나 `H-106` 가드로 error
|
||||
(순서를 뒤집으면 `bk.N`이 1에서 멈춰 안 터진다 — 위와 같은 "가끔" 모양).
|
||||
확정(사용자, 갈래 없음 — `H-39`의 "예외 없이"에 이 핸들러를 넣는 것뿐):
|
||||
`process`에서 `setOffsetSource(inst, k, None)` → **`v.Parent = inst`** →
|
||||
**`setLength(inst, k, 1, inst)`**(상수 `1`). 반환 클로저는 (A)/(B) 분기·단순
|
||||
철거에서 **`v.Parent = nil`** → `setOffsetSource(inst, k, None)` →
|
||||
`setLength(inst, k, 0)`(`SlotHandler`의 retractor가 `unmountSlotTree`를 먼저
|
||||
부르는 것과 같은 모양, 부기 둘의 순서 근거는 아래 "해제" 문단).
|
||||
**[2026-08-27 `/code-review high` 정정 셋]** — (1) 옛 자식은 **내린다**(파괴
|
||||
아님): `store.child:Set(otherFrame)`이 이전 `Frame`을 `Destroy`하지 않고
|
||||
트리에서 내리기만 한다는 것이 `base/slot-plan.md`의 "`State<Slot>` 교체는
|
||||
파괴가 아니라 언마운트" 절의 근거 1이라, retractor가 `Parent = nil`을 안
|
||||
하면 옛 자식이 물리 자식으로 남은 채 `lengthList[k] == 1`이 된다. (2) 순서는
|
||||
**물리 먼저, `setLength` 나중** — 아래 "일반 계약 — 물리와 부기의 순서" 3번
|
||||
(`nativeInsert` → `setLength` → `recompute`)과 같게. 옛 서술(`setLength` →
|
||||
`Parent`)은 단건 경로에서 `recompute`가 부착 전에 돌았다. (3) **5번째 인자
|
||||
`element`는 안 넘긴다** — 상수 길이라 지속 클로저가 없어 Q3 계약상 생략
|
||||
대상이고, 넘기면 `setLength(…, 0)` 해제가 `bk.indexOfElement`의 옛 키를
|
||||
안 지워(해제 호출엔 요소가 없다) 교체마다 옛 자식이 강참조로 쌓인다. 대안 — `Dispatch.drive`가 `type(k) == "number"` 분기에서 일괄 등록 —
|
||||
은 아래 *"모든 핸들러가 `k=number`일 때 처리하도록 두는"*에서 **이미 기각된
|
||||
안**이라 다시 열지 않는다. `ROADMAP.md` M5 체크박스에 같은 두 줄을 적었다.
|
||||
|
||||
**해제(그 자리가 더 이상 기여하지 않게 될 때)는 `setOffsetSource(...,None)`
|
||||
→ `setLength(...,0)` 순서로 (2026-08-13 여섯 번째 세션, 사용자 지적).**
|
||||
별도 unregister API는 없고 `0`/`None` 재등록이 곧 해제인데, **순서가
|
||||
|
|
@ -2208,7 +2243,8 @@ Slot 이 effect 나 다른 요소들을 소유할 수가 없다 … 실제 obser
|
|||
1. **배치를 여는 쪽(`Dispatch.drive` 최상위, 또는 `materializeSlotTree`가
|
||||
자기 자신의 `_elements`를 등록하는 자리 — 아래 "적용 지점" 참고)이 그
|
||||
owner 전용 `Blocker`를 `Relate(ownerKey)`에 lazy 생성하고 배치 시작
|
||||
전에 `:On()`한다.** 이 Blocker는 `state:Block()`을 거치지 않고
|
||||
전에 `:On()`한다**(`Dispatch.drive`는 배열 파트가 있을 때만 — 위 `H-17`
|
||||
절의 2026-08-27 가드). 이 Blocker는 `state:Block()`을 거치지 않고
|
||||
**직접** 쓰인다 — `base/blocker-plan.md`의 "`state:Block()` 없이
|
||||
직접 쓰는 두 번째 용례" 절 참고.
|
||||
2. 배치가 도는 동안, 각 position의 `setLength`가 트리거하는
|
||||
|
|
@ -2223,8 +2259,10 @@ Slot 이 effect 나 다른 요소들을 소유할 수가 없다 … 실제 obser
|
|||
미루면 초기 레이아웃이 이상해진다"는 우려를 없앤다** — `:List`가
|
||||
실체화되며 `Slot.Offset`을 곧바로 읽어 쓰는 자리(`activateList`)가
|
||||
배치 중이라도 항상 최신값을 보게 됨.
|
||||
4. 배치가 끝나면(`Dispatch.drive`의 배열 파트 순회 전체, 또는
|
||||
`materializeSlotTree`의 등록 루프 전체가 끝나면) `blocker:OffWithoutEmit()`을
|
||||
4. 배치가 끝나면(`Dispatch.drive`가 할 일을 **전부** 마치면 — post-pass 포함,
|
||||
위 `H-17` 절 / 또는 `materializeSlotTree`의 등록 루프 전체가 끝나면;
|
||||
**[2026-08-27 정정]** 여기 *"배열 파트 순회 전체"*라고 남아 있던 건 `H-17`이
|
||||
범위를 넓히기 전 문구다) `blocker:OffWithoutEmit()`을
|
||||
부르고, **그 직후 딱 한 번** `recompute(ownerKey, bk)`를 명시적으로
|
||||
호출한다(**[2026-08-26]** 이 명시 호출도 `bk.recomputeBlocker:IsOn()`이면
|
||||
건너뛴다 — `H-119`). 이 시점엔 `bk.N`개 position이 전부 등록돼 있어 안전하고,
|
||||
|
|
@ -2378,7 +2416,17 @@ yield 금지(2026-08-18 신설, 사용자 확정).** 이 배치 게이팅 전체
|
|||
|
||||
**`:List` reconcile에서 `Length` 갱신 시점**: 한 사이클(여러 항목이
|
||||
한꺼번에 추가/제거되는 경우 포함) 전체가 끝난 뒤 **한 번만** — 사이클
|
||||
도중 항목마다 갱신하면 캐스케이드가 그만큼 반복됨.
|
||||
도중 항목마다 갱신하면 캐스케이드가 그만큼 반복됨. **⭐ [2026-08-27 확정,
|
||||
9라운드 `H-136`] 재실행 사이클도 포함한다** — 의사코드는 최초 population만
|
||||
Blocker로 감싸고 `data:Set(…)`으로 `reconcile`이 다시 돌 땐 raw op마다
|
||||
`recompute`가 완주해(O(n²) + 부모 캐스케이드 n회) 이 문장과 어긋나 있었다.
|
||||
이제 `reconcile` 본문 자체가 게이트를 쥔다(`base/slot-plan.md`의
|
||||
`reconcile` 머리·꼬리). 사용자 논거: *"이전에는 native* 가 없어 모두 dom 식
|
||||
elem 입출력이 강제가 아니였는데, 이젠 그렇기 때문에 최초 방식으로 recompute
|
||||
되는것 처럼 처리되어도 되는 지점 … 이미 우린 set upto 와 valid upto 가 나뉜
|
||||
지점이고, 싱크라서 괜찮아보임"* — 배치 중엔 offset이 뒤늦게 한 번에 잡히지만
|
||||
`native*` 계층이 물리 위치를 부기와 무관하게 정확히 넣고, 무효화 커서 둘이
|
||||
분리돼 있어 마지막 `recompute` 한 번이 전부 따라잡는다.
|
||||
|
||||
**웹 백엔드(quad-web, 아직 없음) — 같은 `lengthList`/`sourceList`/
|
||||
`recompute`를 그대로 재사용, 다른 건 "offset 변경 시 무엇을 하는가"뿐**:
|
||||
|
|
|
|||
|
|
@ -242,8 +242,10 @@ function Effect(fn, ...)
|
|||
end
|
||||
local function onRefFire(_, ref) fire(ref) end -- Ref: 2번째가 출처
|
||||
local function onStateFire(_, _, from) fire(from) end -- Observer: 3번째가 출처
|
||||
for d in pairs(seen) do -- [2026-08-26 `/code-review high` 7차] `for d in seen`은
|
||||
-- 테이블을 호출하려 들어 죽는다
|
||||
for d in pairs(seen) do -- 스타일 통일(`pairs` 명시). **[2026-08-27 9라운드
|
||||
-- `H-131`]** 옛 근거 *"`for d in seen`은 테이블을
|
||||
-- 호출하려 들어 죽는다"*는 **거짓** — Luau의 일반화
|
||||
-- 반복은 런타임·`--!strict` 둘 다 통과한다(실측).
|
||||
if isRef(d) then
|
||||
self._deps[d] = onRefFire -- ⭐ 강한 주인 = Effect
|
||||
d:WeakCallback(onRefFire) -- Ref 쪽은 약함
|
||||
|
|
@ -465,53 +467,12 @@ got {typeof(k)}`) end }`(**[2026-08-18]** 에러 메시지에 실제 `k` 타입
|
|||
named 자리 바인드 같은 실제 기능이 확정되면 평범한 우선순위의 Handler로
|
||||
값싸게 override 가능한 자리로 열어둠.
|
||||
|
||||
**보강 — `EffectHandle`의 내부 Observer 바인딩 세부(2026-08-09 열한 번째
|
||||
세션, 재확인 후 명시화)**:
|
||||
|
||||
> **⛔⛔ [2026-08-25 폐기, 7라운드 `H-58`/`H-59`] 이 문단 전체는 옛 모델이다.**
|
||||
> 위 "확정 구조 — 강한 주인은 항상 `Effect`" 절이 **정반대로** 확정했다 —
|
||||
> **`bindLifetime`/`unbindLifetime`은 `Effect` 핸들 하나에만 적용되고**
|
||||
> 내부 Observer로 cascade하지 않는다. dep 등록은 **생성자에서 한 번만**
|
||||
> `WeakSubscribe`/`WeakCallback`으로 하고, 발화 여부는 `canExecute(handle)`이
|
||||
> 전담한다. 아래가 서술하는 `_observers` 배열/cascade/`Subscribe` 순회는
|
||||
> **전부 `_deps` 하나와 `_blocker`로 대체됐다**(위 "필드 목록").
|
||||
> 아래는 히스토리로만 읽을 것 — **이 문단대로 짜면 `H-58`의 중복 `Rerun`이
|
||||
> 되살아난다.**
|
||||
|
||||
**⚠️ [2026-08-24 6라운드 손 트레이싱 `H-8`, 2026-08-25 폐기] 이 문단 전체가 아직 "Observer 하나"
|
||||
전제로 쓰여 있었다 — `_observer`(단수)를 `_observers`(배열)로 읽을 것.**
|
||||
아래 절이 확정한 `Effect(fn, ...deps)`(N-deps)와 정면으로 어긋났고, 그대로
|
||||
구현하면 **2번째 이후 dep의 Observer엔 `canExecute` 판정 근거가 아예 안 실려**
|
||||
그 Observer의 재실행이 통째로 죽는다 — 바로 이 문단 자신이 경고하는 실패
|
||||
모드다. 필드를 배열로 바꾸고 cascade/`Subscribe`/`Unsubscribe`를 전부 순회로
|
||||
고친다(새 결정 없음, 반영 누락). `Ref` dep은 Observer가 아니라 콜백이라 이
|
||||
배열에 안 들어간다 — 그쪽 해제는 아래 `H-7` 문단이 소스.
|
||||
|
||||
- **`EffectHandle`은 내부 Observer를 필드로 강참조** — `handle._observers[i] =
|
||||
observer`(dep이 State/Source인 경우만 존재). 이건 GC 방지가 목적이 아니라
|
||||
(그건 아래 `bindLifetime`/`gchold`가 담당) `:Unsubscribe()`/`bindLifetime`
|
||||
cascade가 이 필드를 통해 내부 Observer에 접근하기 위한 것.
|
||||
- **`bindLifetime(inst, handle)`은 `state`가 있는 경우 내부 Observer도
|
||||
같은 `inst`로 `handle._observers` **전부**에 대해
|
||||
`bindLifetime(inst, observer)`를 cascade해야 함** — `Dispatch/Leaf.luau`가 children 배열의 `EffectHandle`을 매치해
|
||||
`bindLifetime(inst, handle)`을 부르는 시점(leaf 부착)과, `:Subscribe()`가
|
||||
`handle`을 전역 레지스트리에 등록하는 시점(아래) 둘 다 해당. 이유:
|
||||
`canExecute(observer)`가 보는 gcconn 참조는 **그 Observer 자신이
|
||||
`bindLifetime(inst, observer)`될 때 그 Observer 쪽 릴레이션에
|
||||
복사되는 것**이라, `EffectHandle`만 바인드하고 내부 Observer는 안 하면
|
||||
그 Observer에겐 판정 근거가 아예 없어서 `canExecute`가 항상 거짓이 됨
|
||||
(=재실행이 통째로 죽음). 같은 이유로 `unbindLifetime(handle)`도 내부
|
||||
Observer까지 같이 풀어야 대칭이 맞음.
|
||||
**[정정, 2026-08-14 다섯 번째 세션]** 이 항목이 원래 근거로 든
|
||||
"`canExecute`가 `Subscribed` 필드 + `inst`의 gcconn을 함께 본다"는
|
||||
틀렸음 — `.Subscribed`는 구독 경로 전용이고 leaf 경로와
|
||||
무관(`archive/canexecute-inst-arg-reversed.md`). cascade가 필요하다는
|
||||
결론은 그대로이고 오히려 근거가 더 직접적이 됨.
|
||||
- **`:Subscribe()`도 마찬가지로 `state`가 있으면 내부 Observer를 같은
|
||||
전역 강참조 레지스트리에 같이 등록**(`handle` 자신 + `handle._observers`
|
||||
전부, 또는 `handle._observers`만으로 충분한지는 구현 세부 — 어느 쪽이든
|
||||
"`EffectHandle`은 등록됐는데 내부 Observer는 등록 안 됨" 상태가 생기면
|
||||
안 됨).
|
||||
**보강 — `EffectHandle`의 내부 Observer 바인딩 세부** — **⛔ [2026-08-25
|
||||
폐기, 7라운드 `H-58`/`H-59`; 2026-08-27 `archive/`로 이전, 9라운드 `H-130`]**
|
||||
옛 모델(`_observers` 배열 + `bindLifetime`/`:Subscribe()` cascade)의 원문은
|
||||
`archive/effect-internal-observer-cascade-reversed.md`. 위 "확정 구조 — 강한
|
||||
주인은 항상 `Effect`" 절이 정반대로 확정했고, 배너 아래 죽은 문단이 두 번
|
||||
살아 있는 문장처럼 편집되는 사고가 나서(그 파일 머리) 본문에서 뺐다.
|
||||
|
||||
**Observer 자체에 cleanup 반환 계약을 추가하는 안은 여전히 기각** — React
|
||||
`useEffect`식으로 `fn`의 반환값을 자동으로 배선해주는 안을 검토했으나,
|
||||
|
|
@ -536,6 +497,40 @@ quad의 반응형 그래프/cleanup 인체공학만 재사용하는 경우)로
|
|||
**확정**: `EffectHandle`에도 `:Subscribe()`/`:Unsubscribe()` 추가, 둘 다
|
||||
`self` 반환(Observer와 동일한 fluent 대칭).
|
||||
|
||||
**⭐ [2026-08-27 확정, 9라운드 `H-127`] 의사코드 — 네 진입점은 Observer의
|
||||
것을 재사용하고, `Unsubscribe`만 *통과한 뒤* cleanup을 덧붙인다.** 아래
|
||||
산문(번호 목록)은 이 블록을 풀어 쓴 것이고, **순서는 이 블록이 정본**이다.
|
||||
산문의 번호 순서(플래그 → cleanup → fail-fast)대로 짜면 leaf 바인딩된 핸들에
|
||||
`:Unsubscribe()`를 불렀을 때 **cleanup을 소진한 뒤에야 error**가 나서 `E-11`이
|
||||
막으려던 피해(cleanup 앞당김, `_installed = false`)가 이미 일어난 뒤다 —
|
||||
Observer 쪽 의사코드는 가드가 첫 줄이라 이 문제가 없었다.
|
||||
|
||||
```lua
|
||||
-- 레지스트리(`Subscribed`/`WeakSubscribed`)·`canBound` 게이트·`.Subscribed`
|
||||
-- 플래그 전부 `base/lifecycle-pattern.md` (2)의 Observer 네 진입점과 **같은
|
||||
-- 구현**이다 — 메소드 테이블에 그대로 배정한다. 등록되는 건 **핸들 자신뿐**
|
||||
-- (내부 Observer·`Ref` 콜백은 생성자에서 이미 `Weak*`로 걸려 있다, `H-59`).
|
||||
EffectHandle.Subscribe = Observer.Subscribe -- canBound 게이트 + 강한 킵
|
||||
EffectHandle.WeakSubscribe = Observer.WeakSubscribe -- 게이트 + 약한 등록 + 플래그
|
||||
EffectHandle.WeakUnsubscribe = Observer.WeakUnsubscribe -- 관대(`H-133`) — cleanup 안 건드림
|
||||
|
||||
function EffectHandle:Unsubscribe()
|
||||
Observer.Unsubscribe(self) -- ⭐ 게이트가 **먼저** — 강하게 구독된 적 없으면(leaf
|
||||
-- 바인딩·약한 구독·미구독) 여기서 error, cleanup엔
|
||||
-- 손도 안 댄다(`E-11`). 통과하면 강한 킵 해제 +
|
||||
-- `.Subscribed = false`(향후 재실행 차단).
|
||||
self:_consumeCleanup() -- 통과했을 때만: 직전 cleanup 정확히 1회, `_installed = false`
|
||||
return self
|
||||
end
|
||||
```
|
||||
|
||||
- **`WeakUnsubscribe`는 cleanup을 소진하지 않는다** — 약한 구독은 "GC에
|
||||
맡기는" 경로라 해제가 곧 종료 신호가 아니다. 종료 신호는 강한 구독의
|
||||
`Unsubscribe`와 leaf 사망(`unbindLifetime`의 훅) 둘뿐.
|
||||
- 실측(9라운드 `core9.luau`, `t12` 매트릭스): 이 배정으로 leaf 바인딩된
|
||||
핸들의 `:Unsubscribe()`가 cleanup을 건드리지 않고 error, `:Subscribe()`
|
||||
후 `:Unsubscribe()`는 cleanup 1회 — 전부 기대대로.
|
||||
|
||||
- **`:Subscribe()`** — Observer가 쓰는 것과 같은 강참조 레지스트리에
|
||||
**핸들 자신**을 등록 — 새 메커니즘 아님, 기존 레지스트리 재사용. 이후
|
||||
로컬 변수로 참조를 안 들고 있어도 계속 살아있음(Observer와 동일 관용구).
|
||||
|
|
@ -611,31 +606,33 @@ quad의 반응형 그래프/cleanup 인체공학만 재사용하는 경우)로
|
|||
이 요구는 성립하지 않는다 — **그대로 구현하면 `nil`을 순회한다.**
|
||||
- **[2026-08-21 기준]** 남은 건 **구현 시 회귀 확인**뿐이고, 설계상 열린
|
||||
항목이 아니다.
|
||||
- **`:Subscribe()`한 핸들에서는 `:Unsubscribe()`가 Observer의 것을 그냥
|
||||
위임하지 않는다 — Effect 계층에서 의미가 확장됨.** Observer의
|
||||
- **`:Subscribe()`한 핸들에서는 `:Unsubscribe()`가 Observer의 것을 위임한 뒤
|
||||
cleanup 하나를 덧붙인다 — Effect 계층에서 의미가 확장됨.** Observer의
|
||||
`:Unsubscribe()`는 "미래 재실행만
|
||||
끊는다"(Observer 자체엔 정리할 상태가 없음)로 충분하지만, Effect의
|
||||
계약은 "생애주기가 끝나는 시점에 마지막 cleanup이 정확히 1회 호출된다"
|
||||
이고 leaf 사망은 그 "끝"의 신호 중 하나일 뿐이라, `:Unsubscribe()`도
|
||||
동일하게 "지금 끝났다"는 신호로 취급해야 계약이 일관됨:
|
||||
1. **⚠️ [2026-08-25 정정, 7라운드 `H-58`/`H-59`]** 여기 원래 *"`state`가
|
||||
있으면 내부 Observer도 `:Unsubscribe()`해서 향후 재실행을 끊고"*라
|
||||
적혀 있었는데, 내부 Observer는 생성자에서 `:WeakSubscribe()`로 걸리고
|
||||
**해제하지 않는다** — 발화는 `canExecute(handle)`이 막는다(위
|
||||
"확정 구조" 절). `:Unsubscribe()`가 `handle.Subscribed = false`로
|
||||
만들면 그 게이트가 곧바로 거짓이 되므로 **향후 재실행은 그것만으로
|
||||
끊긴다.** 단수 `state` 전제도 이미 `...deps`로 대체됐다.
|
||||
2. **직전(또는 유일한) cleanup을 정확히 1회 호출** — leaf가 죽을 때
|
||||
하던 것과 정확히 같은 이벤트를 수동으로 앞당기는 것.
|
||||
3. **⚠️ [2026-08-26 정정, `/code-review high` 7차] "idempotent"는 폐기됐다** —
|
||||
`Unsubscribe`는 이제 **fail-fast**다(약하게만 구독된 값이면 error,
|
||||
`base/lifecycle-pattern.md`의 "(2) 전역 경로" 절. 6차가 같은 문장을
|
||||
두 문서에서 지웠는데 **이 세 번째 사본을 놓쳤다**). 살아 있는 요구는
|
||||
뒷부분뿐이다 — **이후 leaf가 실제로 죽어도 cleanup이 중복
|
||||
호출되면 안 됨** — 새 메커니즘 불필요, Observer가 이미 확정해둔
|
||||
`canExecute(value)` liveness 체크가 자동(리프=gcconn 참조)/수동
|
||||
(전역=`Subscribed` 필드) 두 경로를 하나의 게이트로 OR 묶어주므로
|
||||
여기 그대로 얹힘.
|
||||
동일하게 "지금 끝났다"는 신호로 취급해야 계약이 일관됨. **순서는 위
|
||||
의사코드가 정본이고 아래 번호는 그 순서 그대로다**(**[2026-08-27
|
||||
`/code-review high` 재정렬]** 옛 목록은 플래그 → cleanup → fail-fast 순이라
|
||||
leaf 바인딩된 핸들에서 cleanup을 소진한 뒤 error가 났다):
|
||||
1. **게이트 — fail-fast.** 강하게 구독된 적 없는 값(leaf 바인딩·약한
|
||||
구독·미구독)이면 여기서 error, cleanup엔 손도 안 댄다(`E-11`).
|
||||
**[2026-08-26 정정, `/code-review high` 7차]** 옛 "idempotent"는 폐기됐다
|
||||
(`base/lifecycle-pattern.md`의 "(2) 전역 경로" 절 — 6차가 같은 문장을 두
|
||||
문서에서 지웠는데 이 세 번째 사본을 놓쳤었다).
|
||||
2. **강한 킵 해제 + `handle.Subscribed = false`.** 이것만으로 향후 재실행이
|
||||
끊긴다 — `canExecute(handle)` 게이트가 곧바로 거짓이 되므로. **[2026-08-25
|
||||
정정, 7라운드 `H-58`/`H-59`]** 옛 문장 *"`state`가 있으면 내부 Observer도
|
||||
`:Unsubscribe()`해서"*는 틀렸다 — 내부 Observer는 생성자에서
|
||||
`:WeakSubscribe()`로 걸리고 **해제하지 않는다**(위 "확정 구조" 절), 단수
|
||||
`state` 전제도 `...deps`로 대체됐다.
|
||||
3. **직전(또는 유일한) cleanup을 정확히 1회 호출** — leaf가 죽을 때
|
||||
하던 것과 정확히 같은 이벤트를 수동으로 앞당기는 것. **이후 leaf가 실제로
|
||||
죽어도 cleanup이 중복 호출되면 안 됨** — 새 메커니즘 불필요,
|
||||
`canExecute(value)` liveness 체크가 자동(리프=gcconn 참조)/수동(전역=
|
||||
`Subscribed` 필드) 두 경로를 하나의 게이트로 OR 묶어주므로 여기 그대로
|
||||
얹힘.
|
||||
- **`state` 없는 mount-only Effect엔 특별한 분기 불필요** — install은 이미
|
||||
`Effect(fn)` 호출 시점에 끝나 있으므로, `:Unsubscribe()`는 그냥 "지금
|
||||
leaf-사망 cleanup을 수동으로 트리거"하는 것과 완전히 동치.
|
||||
|
|
@ -708,7 +705,10 @@ Effect의 의존성이 될 방법이 아예 없다.** 사용자 제기: *"Effect
|
|||
인자마다 다른 규칙을 위치로 기억해야 했다. 아무것도 안 넘기므로 그 질문
|
||||
자체가 없어지고, dep 값은 사용자가 클로저로 직접 읽는다.
|
||||
- `self`를 주는 덕에 `fn` 안에서 `self:Rerun()`/`self:Unsubscribe()` 같은
|
||||
핸들 표면에 바로 닿는다.
|
||||
핸들 표면에 바로 닿는다. **⚠️ [2026-08-27 `/code-review high`, 판단 대기
|
||||
`H-143`]** `fn` 안 `Unsubscribe`는 그 실행이 돌려준 cleanup을 `Rerun`이
|
||||
그대로 저장해 **아무도 소진 못 하게** 만든다 — 처방(`Rerun` 꼬리 분기 /
|
||||
금지 / 문서화)은 `question.md` 최우선 절.
|
||||
- **최소 1회는 실행된다 — React `useEffect`와 동일.** 아직 안 채워진 `Ref`가
|
||||
섞여 있어도 그대로 돈다(사용자: *"최초 1회에서 어차피 if 로 확인해내게
|
||||
될것이므로 괜찮음"*). "전부 채워질 때까지 대기"는 안 한다.
|
||||
|
|
|
|||
|
|
@ -442,8 +442,12 @@ end
|
|||
**⭐ [2026-08-26 재작성, 8라운드 `H-111`] 프리미티브는 `WeakSubscribe` 쪽이다** —
|
||||
여기 한때 `Subscribe`/`Unsubscribe` 둘만 있는 블록이 있었는데, 그건
|
||||
`:WeakSubscribe()`가 생기기 전(2026-08-25 이전) 서술이라 **약한 쪽이 어디서
|
||||
`.Subscribed`를 세우는지가 통째로 빠져 있었다.** 아래가 네 진입점 전량이고
|
||||
소스다(사용자 원문 *"구현이 한 벌"*):
|
||||
`.Subscribed`를 세우는지가 통째로 빠져 있었다.** 아래가 **Observer의** 네
|
||||
진입점 전량이고 소스다(사용자 원문 *"구현이 한 벌"*). **[2026-08-27 9라운드
|
||||
`H-127`]** `EffectHandle`도 같은 넷을 **그대로 재사용**한다(같은 레지스트리,
|
||||
같은 게이트) — `Unsubscribe`만 이 게이트를 통과한 *뒤에* cleanup 소진을
|
||||
덧붙이며, 그 의사코드는 `base/effect-plan.md`의 "`EffectHandle:Subscribe()`"
|
||||
절이 소스:
|
||||
|
||||
```lua
|
||||
local Subscribed = {} -- 강한 레지스트리(살려두는 게 목적)
|
||||
|
|
@ -472,6 +476,9 @@ function Observer:WeakUnsubscribe()
|
|||
if Subscribed[self] ~= nil then
|
||||
error("...: subscribed strongly; use :Unsubscribe()", 2)
|
||||
end
|
||||
-- ⭐ [2026-08-27 확정, 9라운드 `H-133`] 여기까지가 가드 전부다 — 구독한 적
|
||||
-- 없는 값·이미 약하게 풀린 값은 **조용히 통과**(아래 두 줄이 no-op).
|
||||
-- 의도된 관대함이지 누락이 아니다(아래 산문).
|
||||
WeakSubscribed[self] = nil
|
||||
self.Subscribed = false
|
||||
return self
|
||||
|
|
@ -510,6 +517,20 @@ end
|
|||
State dep 전량이 침묵한다. **"둘 중 뭐든 풀어주는" 범용 해제는 없다** —
|
||||
필요하면 호출부가 `.Subscribed`가 아니라 어느 경로로 걸었는지를 알고 있어야
|
||||
한다(핸들을 만든 쪽이 안다).
|
||||
- **⭐ [2026-08-27 확정, 9라운드 `H-133`] 그 대칭은 *경로 교차*만 막는다 —
|
||||
`WeakUnsubscribe`는 관대하고 `Unsubscribe`는 엄격하다.** 구독한 적 없는
|
||||
값·이미 약하게 풀린 값에 `WeakUnsubscribe`를 부르면 error 없이 지나간다
|
||||
(`WeakSubscribed[self] = nil; .Subscribed = false`가 그대로 no-op — 실측
|
||||
`t12_subscribe_failfast.luau`). 같은 값에 `Unsubscribe`는 error다. 사용자
|
||||
논거: *"WeakSubscribed 자체가 사라질 수 있는 요소라서, 그 사라지는걸 유저가
|
||||
정하게 하는 요소라서 에러를 내야할지 말아야할지 애매한 부분 … 다만 weak 에
|
||||
대한 홀드를 유저가 유지하는게 강제라면 b가 되긴 해야하나, 그럴 이유가 없어서
|
||||
a가 되는게 맞는듯"* — 약한 등록의 생존은 사용자가 쥔 참조에 달린 것이고
|
||||
quad는 그 홀드를 강제하지 않으므로, "없는 항목의 약한 해제"를 오류로 볼
|
||||
근거가 없다. 강한 등록은 quad가 살려두는 것이라 "없음"은 반드시 호출부
|
||||
실수다. **leaf 바인딩된 값에 `WeakUnsubscribe`를 불러도 같은 이유로
|
||||
통과**한다 — `.Subscribed = false` 대입만 하고 gcconn 경로는 안 건드려
|
||||
무해하다.
|
||||
- **⚠️ `:Subscribe()`는 idempotent가 아니다.** 이미 구독됐거나 leaf에
|
||||
바인드된 값에 다시 부르면 `canBound` 게이트에 걸려 **error**다
|
||||
(**[2026-08-26 확정, `/code-review high`]** `base/source-state-plan.md`가
|
||||
|
|
|
|||
|
|
@ -1367,6 +1367,12 @@ function Slot:List(data, updateFn, keyFn, opts)
|
|||
if self._physicalTarget then
|
||||
-- 초기 population도 `materializeSlotTree`와 같은 이유로 게이팅한다 —
|
||||
-- 안 그러면 아이템마다 `recompute`가 돌아 O(n²)(그 절의 `blocker:On()` 문단).
|
||||
-- [2026-08-27, `H-136` 후속] `reconcile`이 이제 자기 게이트를 쥐므로(`ownsGate`)
|
||||
-- **이 `if` 블록 전체가 잉여**다 — `On()`/`OffWithoutEmit()`뿐 아니라 아래
|
||||
-- `recomputeBlocker` 확인 + `recompute` 호출까지 `reconcile` 꼬리가 똑같이
|
||||
-- 한다. 안쪽에선 `ownsGate = false`로 걸러져 동작은 같다.
|
||||
-- `materializeSlotTree`와 모양을 맞추려 두지만, 구현 시 블록째 지우고
|
||||
-- `activateList(self, self._physicalTarget)` 한 줄만 남겨도 무방하다.
|
||||
local blocker = getBlocker(self)
|
||||
blocker:On()
|
||||
activateList(self, self._physicalTarget)
|
||||
|
|
@ -1541,6 +1547,21 @@ function activateList(self, physicalTarget)
|
|||
keys[i] = key
|
||||
end
|
||||
|
||||
-- ⭐ [2026-08-27 확정, 9라운드 `H-136`] **한 사이클은 한 배치다** — 재실행
|
||||
-- (`data:Set(…)` → 이 함수)도 최초 population과 똑같이 Blocker로 감싼다.
|
||||
-- 안 그러면 raw op마다 `recompute`가 완주해 O(n²) + 부모 캐스케이드가
|
||||
-- 아이템 수만큼 나고, `dispatch-core-plan.md`의 *"한 사이클 … 한 번만"*이
|
||||
-- 거짓이 된다. `raw*`의 명시 호출은 이미 `getBlocker(self):IsOn()`을
|
||||
-- 보므로(`H-119`) 추가 배선은 없다.
|
||||
-- **Blocker는 네스팅이 안 된다**(불리언, `base/blocker-plan.md`) — 최초
|
||||
-- population은 `Slot:List`/`materializeSlotTree`가 이미 켜둔 **바깥 배치
|
||||
-- 안에서** 여기 오므로, 이미 켜져 있으면 손대지 않고 바깥이 끄게 둔다.
|
||||
-- `updateFn`이 도중에 던지면 켜진 채 남는 것은 `materializeSlotTree`가
|
||||
-- 이미 감수하는 것과 같은 부류(`pcall` 안 씀 — error 계약).
|
||||
local blocker = getBlocker(self)
|
||||
local ownsGate = not blocker:IsOn()
|
||||
if ownsGate then blocker:On() end
|
||||
|
||||
local slotPos = 0 -- `_elements` 자리 카운터 — 생존 아이템마다 정확히 +1
|
||||
|
||||
for i, item in ipairs(items) do
|
||||
|
|
@ -1607,6 +1628,15 @@ function activateList(self, physicalTarget)
|
|||
-- **다음 사이클엔 다시 안 묻는다**
|
||||
end
|
||||
end
|
||||
|
||||
-- ⭐ [2026-08-27, `H-136`] 배치 닫기 — `Slot:List`의 `_physicalTarget` 분기와
|
||||
-- 같은 꼬리. `recomputeBlocker`는 따로 본다(사용자 코드가 바깥 `recompute`
|
||||
-- 안에서 이 사이클을 일으킨 재진입이면 바깥 루프가 되감아 따라잡는다).
|
||||
if ownsGate then
|
||||
blocker:OffWithoutEmit()
|
||||
local bk = getBookkeeping(self)
|
||||
if not bk.recomputeBlocker:IsOn() then recompute(self, bk) end
|
||||
end
|
||||
end
|
||||
|
||||
-- [이관, 2026-08-21] `_detached` 정리용 Effect는 원래 `mountSlotTree`가
|
||||
|
|
|
|||
|
|
@ -1421,7 +1421,11 @@ no-op. 한때 검토했던 "`isInit=false`면 허용, `isInit=true`+생존확인
|
|||
- **⚠️ [2026-08-26 재정정, `/code-review high` 6차] `:Unsubscribe()`도
|
||||
idempotent가 아니다.** 여기 한때 *"게이트가 없어 … 비대칭이 의도된 것"*
|
||||
이라고 적혀 있었는데, 같은 날 **대칭 가드**가 들어오며 거짓이 됐다(위 항목).
|
||||
지금 계약은 **해제는 건 경로로 푼다** 하나다.
|
||||
지금 계약은 **해제는 건 경로로 푼다** 하나다. **[2026-08-27 9라운드
|
||||
`H-133`]** 그 대칭 가드는 *경로 교차*(강↔약)만 막는다 — 구독한 적 없는
|
||||
값·이미 약하게 풀린 값에 `WeakUnsubscribe`는 **조용히 통과**(의도된 관대함,
|
||||
사용자 논거와 함께 `base/lifecycle-pattern.md` (2)가 소스), 같은 값에
|
||||
`Unsubscribe`는 error.
|
||||
- **[정정, 2026-08-09 여섯 번째 세션] "`:Unsubscribe()`는 자동(리프)
|
||||
케이스에도 동일하게 씀"은 틀림 — 리프/`bindLifetime` 경로의 조기
|
||||
해제는 `unbindLifetime(value)`가 담당, `:Unsubscribe()`는
|
||||
|
|
|
|||
|
|
@ -48,6 +48,10 @@ Roblox Instance 이름과 맞춘 `UICorner`/`UIPadding`(+`UIPaddingOffset`)/
|
|||
비슷한 이름의 부가 Modifier 필드인지 구분이 안 됨(사용자 지적). 접두어
|
||||
`UI`를 붙이면 실제 대응하는 Roblox Instance 클래스 이름과 1:1로 읽혀서
|
||||
이 모호함 자체가 사라짐 — `Frame { UICorner = 8 }`, `mod:UICorner(8)`.
|
||||
**[2026-08-27 추가, 9라운드 `H-138`]** 접두어의 두 번째 근거 — Roblox 프로퍼티
|
||||
이름엔 `UI` 접두어가 없어서 숏핸드 키가 실제 프로퍼티와 **우연히 겹치는 일을
|
||||
구조적으로 막는다**(아래 "메커니즘" 절의 우선순위 문단 — 충돌 방지는 접두어,
|
||||
선택은 우선순위로 역할이 갈린다).
|
||||
|
||||
## 메커니즘 — 새 아키텍처 개념 불필요
|
||||
|
||||
|
|
@ -67,6 +71,19 @@ Roblox Instance 이름과 맞춘 `UICorner`/`UIPadding`(+`UIPaddingOffset`)/
|
|||
자동 생성된 자식은 기존 관례대로 `_`/`QUAD_` 접두어 네이밍
|
||||
(`research/debug-tooling-plan.md` 9번, v1의 `_quad_round`류 그대로 재사용).
|
||||
|
||||
**⭐ [2026-08-27 확정, 9라운드 `H-138`] 매치 우선순위 — 숏핸드 핸들러가
|
||||
`PropertyHandler`보다 높다.** `Frame { UICorner = 8 }`에서
|
||||
`getHandler(inst, "UICorner", 8)`이 `UICornerHandler`를 고르는 근거는
|
||||
`PropertyHandler`가 리플렉션으로 그 키를 거부해서가 **아니라** `priority`다
|
||||
(구체 상수는 구현 시 — `PropertyHandler`보다 높은 밴드면 된다). 사용자 논거:
|
||||
*"당연히 숏핸드 우선순위가 높음. 안 그러면 프로퍼티 핸들러가 숏핸드 계층을
|
||||
인지하고 준비한다는 말이 돼"* — 거부에 기대면 하위 계층(프로퍼티)이 상위
|
||||
계층(숏핸드)의 키 집합을 알아야 하는 역방향 의존이 생긴다. 이름 충돌 방지도
|
||||
우선순위가 아니라 **`UI` 접두어**가 맡는다: *"프로퍼티에 UI를 붙인 이유도
|
||||
우연히 겹치는걸 막기 위함임. UI 프리픽스는 로블록스 프로퍼티에 발견되진
|
||||
않거든"*(위 "결론" 절의 접두어 확정에 이 근거가 하나 더 붙는다 — 그 절은
|
||||
Modifier 메소드와의 모호함만 들었다).
|
||||
|
||||
**[보강, 2026-08-09 열한 번째 세션] `mod:UICorner(8)`류 체이닝이 실제로
|
||||
타입체크되려면, 생성되는 `FrameModifier`류 정적 타입의 메소드 목록에
|
||||
`UICorner`/`UIPadding`/`UIScale`이 (진짜 프로퍼티들과 나란히) 포함돼
|
||||
|
|
@ -165,12 +182,24 @@ function UICornerHandler.process(inst, k, v, index)
|
|||
end
|
||||
local child = ensureManagedChild(inst, k) -- 없으면 Instance.new + Parent, 있으면 재사용
|
||||
Dispatch.process(child, "CornerRadius", mapTweenValue(v, toUDim), 1)
|
||||
return function(hint)
|
||||
if hint == nil then destroyManagedChild(inst, k) end
|
||||
end
|
||||
return function() end -- ⭐ [2026-08-27 정정, 9라운드 `H-135`] no-op — 아래 참고
|
||||
end
|
||||
```
|
||||
|
||||
- **⭐ [2026-08-27 정정, 9라운드 `H-135`] 반환 클로저는 `function() end`다 —
|
||||
여기 한때 `function(hint) if hint == nil then destroyManagedChild(inst, k) end
|
||||
end`가 적혀 있었는데, 그건 위 "`v`가 `nil`인 경우" 절의 `v == nil`(값) 규칙을
|
||||
`hint == nil`(retractor 인자)로 잘못 옮긴 **복사 오류**였다.** 이 절의 주제는
|
||||
자식 프로퍼티 세팅을 `Dispatch.process`로 되돌려주는 것뿐이고 관리 자식의
|
||||
생사는 그 절이 정한다. **사용자 확정**: *"v == nil 로 신규 들어오면 파괴가
|
||||
맞고 retract 는 nop 맞아. 파괴 자체가 사실 inst 바꿔서 넣은 process 를 처리할
|
||||
필요 없게 만들어버리고, 우린 파괴에 대해서 retract 안하던게 맞아서, Tween 과
|
||||
무관한 것도 맞지."* — 파괴 경로는 `process(inst, k, nil)` **하나**이고,
|
||||
자식이 파괴되면 그 위에 위임됐던 `(child, "CornerRadius")` 체인·트윈 문맥은
|
||||
Instance와 함께 사라지므로 retractor가 되돌릴 것이 없다. 옛 줄대로 짜면 (A)
|
||||
분기에서 `R(nil)` + `process(nil)`이 같은 자식을 두 번 파괴하고, (B) 분기
|
||||
(`State<State<…>>`의 안쪽이 값↔State로 바뀔 때)에서 자식이 파괴·재생성됐다.
|
||||
|
||||
- **`process` 도중에 대상 `inst`를 바꾸는 것은 UB가 아님(사용자 확정)** —
|
||||
키가 바뀔 수 있는 것과 정확히 같음. `chains`가 `(inst,k)` 쌍으로
|
||||
인덱싱되므로 `(inst, "UICorner")` → `(child, "CornerRadius")` 위임은
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ M3=반응형)다. 그 교체의 부작용으로 한때 `question.md` 최우선
|
|||
(소스는 `qa-request/pre-implementation-handtrace-round8-followup.md`),
|
||||
`question.md` 최우선 절은 다시 비어 있다. **[2026-08-27] 9라운드**(그 커밋
|
||||
`9dd8213`의 델타 재트레이싱, 발견 `H-124`~`H-141`)는 **Q1~Q3가 `base/`에
|
||||
반영됐고 Q4~Q10 대기** — 소스는 `-round9-followup.md`. 게이트는 여전히 0. 저장소 루트에
|
||||
반영됐고**, 같은 날 Q4~Q10·`H-138`·`H-139`·`H-142`까지 **전량 반영** — 소스는 `-round9-followup.md`. 그 반영분에 `/code-review high`가 낸 **새 메커니즘 넷(`H-143`~`H-146`)이 `question.md` 최우선 절에 판단 대기**(M2 `Effect` 둘·M3·M5 하나씩). 게이트는 여전히 0. 저장소 루트에
|
||||
`quad-base/src/`(`New()`/`RunInit`/`AddPlugin`/`Relate`/`Debug`)/
|
||||
`quad-types/src/`/`type-version-check/src/`가 실제로 존재(`quad-roblox/src`는
|
||||
아직 빈 폴더 — M5에서 채워짐), 자세한 진행 상황은 루트 `ROADMAP.md`가
|
||||
|
|
|
|||
|
|
@ -8,21 +8,56 @@
|
|||
**진행 방식**: 그 문서 §4가 배치 회신용으로 묶어둔 **결정 문항 Q1~Q10** 순서를
|
||||
따른다. 8라운드와 같다.
|
||||
|
||||
**⚠️ [2026-08-27 기준] 진행 중이다.** 아래 표가 어디까지 왔는지의 소스다.
|
||||
처음엔 "문항을 다 처리한 뒤 일괄 반영"으로 잡았으나, Q1~Q3가 같은 `slot-plan.md`
|
||||
구간에 몰려 있고 서로 얽혀(Q2의 생성자 이동이 Q3의 `_elemIndex` 삭제와 같은
|
||||
줄) 사용자 지시로 **Q3까지 먼저 반영**했다. Q4 이후는 결정 뒤 반영한다.
|
||||
**✅ [2026-08-27] Q1~Q10·`H-138`·`H-139`·`H-142`까지 전량 처리·반영됐다.** 아래
|
||||
표가 항목별 상태의 소스다. (경위: 처음엔 "문항을 다 처리한 뒤 일괄 반영"으로
|
||||
잡았으나, Q1~Q3가 같은 `slot-plan.md` 구간에 몰려 있고 서로 얽혀(Q2의 생성자
|
||||
이동이 Q3의 `_elemIndex` 삭제와 같은 줄) 사용자 지시로 Q3까지 먼저 반영하고
|
||||
체크포인트 커밋했고, Q4 이후는 같은 날 이어진 세션이 결정 뒤 반영했다.)
|
||||
|
||||
| 문항 | 발견 | 상태 |
|
||||
|---|---|---|
|
||||
| Q1 | `H-124` `recompute` 루프 순서 | ✅ **확정** — (a), `continue` 형태 |
|
||||
| Q2 | `H-125` 재마운트 캐시 | ✅ **확정** — 생성자 이동 + (c) 순서 + `_destroyed` |
|
||||
| Q3 | `H-126` splice 빈자리 (+ `H-137` 소멸, `H-141` 신설) | ✅ **확정** — `element → index`는 `bk`가 소유, 토큰 폐기 |
|
||||
| Q4~Q10 | `H-127`~`H-133` | ⏳ 대기 |
|
||||
| — | `H-134`~`H-140` | ⏳ 대기(레인 B·부수 발견) |
|
||||
| Q4 | `H-127` `EffectHandle:Unsubscribe` 순서 | ✅ **확정·반영** — (a) 의사코드, Observer 게이트 먼저 |
|
||||
| Q5 | `H-128` M2×M8 `Ref` | ✅ **확정·반영** — (a) M2 공통 기반에 `Ref` 최소형 |
|
||||
| Q6 | `H-133` `WeakUnsubscribe` 비대칭 | ✅ **확정·반영** — (a) 의도된 관대함 |
|
||||
| Q7 | `H-130` 폐기 블록 편집 | ✅ **확정·반영** — (b) `archive/`로 이전 |
|
||||
| Q8 | `H-134` `InstanceChildHandler` 부기 | ✅ **확정·반영** — (a) |
|
||||
| Q9 | `H-135` 숏핸드 retractor | ✅ **확정·반영** — 문항 전제 정정, Tween 절 스케치 한 줄이 복사 오류(🟡→🟢) |
|
||||
| Q10 | `H-136` reconcile 배치 Blocker | ✅ **확정·반영** — (a) |
|
||||
| — | `H-129`/`H-131` | ✅ 정정 반영(판단 불필요) |
|
||||
| — | `H-138` 숏핸드 우선순위 | ✅ **확정·반영** — 숏핸드가 높다, 충돌 방지는 `UI` 접두어 |
|
||||
| — | `H-139` `New`/`D` 파이프라인 | ✅ **의사코드 신설** — 쓰면서 `H-142` 발견 |
|
||||
| — | `H-132`/`H-137`/`H-140` | ✅ Q1~Q3 처리 때 닫힘 |
|
||||
| — | `H-142` 해시 파트 `Parent` 순서 | ✅ **확정·반영** — props에 `Parent` 금지(순서 문제 소멸) |
|
||||
| — | `H-143`~`H-146` (`/code-review high` 발견 중 새 메커니즘 넷) | ⏳ **판단 대기** — `fn` 안 `Unsubscribe` 고아 cleanup / 재`Subscribe` 재설치 / `indexOfElement` 잔존 / 루트 마운트 경로 |
|
||||
|
||||
**[2026-08-27] Q1~Q3는 `base/`·`ROADMAP.md`에 반영했다** — 반영 중 드러난 것과
|
||||
열어둔 확인은 아래 "반영 기록" 절.
|
||||
열어둔 확인은 아래 "반영 기록" 절. **같은 날 이어서 Q4~Q8·Q10·`H-138`·`H-139`를
|
||||
확정·반영했다**(아래 Q4 이하 절). **[같은 날 마지막] Q9 확인과 `H-142` 판단도
|
||||
닫혀 9라운드 발견은 전량 처리됐다** — 그 뒤 감사 8라운드와 `/code-review high`를
|
||||
돌렸고, **리뷰가 낸 새 메커니즘 넷(`H-143`~`H-146`)이 판단 대기**(아래
|
||||
"`/code-review high`" 절).
|
||||
|
||||
**사용자 회신 원문(Q4~Q10 일괄, 2026-08-27)**: *"Q5 까지는 권고에 전부 동의함.
|
||||
WeakSubscribed 자체가 사라질 수 있는 요소라서, 그 사라지는걸 유저가 정하게 하는
|
||||
요소라서 에러를 내야할지 말아야할지 애매한 부분이긴 한듯. 다만 weak 에 대한
|
||||
홀드를 유저가 유지하는게 강제라면 b가 되긴 해야하나, 그럴 이유가 없어서 a가
|
||||
되는게 맞는듯. 7번은 프로젝트 컨벤션대로, 권고 b적용. 8번도 권고대로. 9는 뭔가
|
||||
이상한데? nil은 파괴가 맞고 파괴 자체가 트윈을 지움. 메니지드를 지우는 스케치가
|
||||
왜 트윈에 있는지도 모르겠는 부분..? 트윈은 inst 를 바꾸어 재프로세싱 하는거라
|
||||
연관이 없는 부분인데. 다시 생각하고 말해볼래? Q10 은 이제 그래도 되어보임.
|
||||
이전에는 native* 가 없어 모두 dom 식 elem 입출력이 강제가 아니였는데, 이젠
|
||||
그렇기 때문에 최초 방식으로 recompute 되는것 처럼 처리되어도 되는 지점.
|
||||
부작용으론, offset 이 먼저 설정되어진 다음 후행 요소들이 밀리거나 당겨진 다음
|
||||
후행으로 넘어가느냐가 차이가 나는데, 이미 우린 set upto 와 valid upto 가 나뉜
|
||||
지점이고, 싱크라서 괜찮아보임. 권고대로 진입해도 된다 보는데 어떻게 생각하는지?
|
||||
H138은 당연히 숏핸드 우선순위가 높음. 안 그러면 프로퍼티 핸들러가 숏핸드 계층을
|
||||
인지하고 준비한다는 말이 돼. 그리고 프로퍼티에 UI를 붙인 이유도 우연히 겹치는걸
|
||||
막기 위함임. UI 프리픽스는 로블록스 프로퍼티에 발견되진 않거든. H139 은 실 구현
|
||||
전에 의사코드를 써보자. 그걸로 인해서 감추어졌던 설계 결함이나 폭탄이 발견된
|
||||
경우가 많아서, 커지기 전에 확인해볼 필요가 있음. 중요 계층이라서"*
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -336,6 +371,165 @@ if slot._physicalTarget == nil then return end
|
|||
|
||||
---
|
||||
|
||||
## Q4 — `EffectHandle:Unsubscribe()`는 Observer 게이트를 **먼저** 통과한 뒤 cleanup (`H-127`, 확정 (a))
|
||||
|
||||
- `effect-plan.md`의 "`EffectHandle:Subscribe()`" 절에 의사코드 한 블록 신설 —
|
||||
`Subscribe`/`WeakSubscribe`/`WeakUnsubscribe`는 **Observer의 함수를 메소드
|
||||
테이블에 그대로 배정**(같은 레지스트리·같은 `canBound` 게이트·같은
|
||||
`.Subscribed`), `Unsubscribe`만 `Observer.Unsubscribe(self)` 통과 뒤
|
||||
`self:_consumeCleanup()`. `WeakUnsubscribe`는 cleanup을 안 건드린다(약한
|
||||
구독은 GC에 맡기는 경로라 해제가 종료 신호가 아니다).
|
||||
- 산문의 번호 목록(플래그 → cleanup → fail-fast)은 **의미 목록이지 실행 순서가
|
||||
아니라고** 그 자리에 적었다. `lifecycle-pattern.md`의 *"아래가 네 진입점
|
||||
전량이고 소스다"*는 *"Observer의 네 진입점 … `EffectHandle`은 같은 넷을 그대로
|
||||
재사용"*으로.
|
||||
|
||||
## Q5 — M2 공통 기반에 `Ref` 최소형 체크박스 (`H-128`, 확정 (a))
|
||||
|
||||
- `ROADMAP.md` M2 "공통 기반" 절에 체크박스 신설 — `.Value`/`.Revision`/`:Set`/
|
||||
`:WeakCallback`/`:Callback`/`:Uncallback`/`isRef` + `EpochBrand:register`.
|
||||
M8 `Ref.luau` 체크박스는 *"최소형은 M2로 앞당겨졌다 — 여기 남는 건 `:Wait`,
|
||||
`PreRef`/`PostRef`, 디스패치 핸들러"*로. 절 머리의 *"셋 다 State-free"*는
|
||||
개수 없이 *"여기 있는 것 전부"*로.
|
||||
|
||||
## Q6 — `WeakUnsubscribe`의 관대함은 의도된 것 (`H-133`, 확정 (a))
|
||||
|
||||
- `lifecycle-pattern.md` (2) 의사코드 주석 + 산문 bullet 신설. **사용자 논거**
|
||||
(위 원문): 약한 등록의 생존은 사용자가 쥔 참조에 달린 것이고 quad가 그 홀드를
|
||||
강제하지 않으므로 "없는 항목의 약한 해제"를 오류로 볼 근거가 없다 — 홀드
|
||||
유지가 강제였다면 (b)여야 했다. 강한 등록은 quad가 살려두는 것이라 "없음"이
|
||||
곧 호출부 실수라 엄격. leaf 바인딩된 값에 `WeakUnsubscribe`도 같은 이유로
|
||||
통과(무해).
|
||||
|
||||
## Q7 — 폐기 블록을 `archive/`로 (`H-130`, 확정 (b))
|
||||
|
||||
- `effect-plan.md`의 ⛔⛔ 배너 아래 블록(`_observers` 배열 + cascade)을
|
||||
`archive/effect-internal-observer-cascade-reversed.md`로 옮기고 포인터 한
|
||||
문단만 남겼다. 옮긴 원문은 **`9dd8213`이 배너 아래를 편집한 흔적 그대로**이고
|
||||
파일 머리에 그 경위를 적었다. 새 규칙은 안 만들었다 — `conventions.md`
|
||||
핸드오버 체크리스트 3번이 이미 이 실패 모드를 규정한다(사용자: *"프로젝트
|
||||
컨벤션대로"*).
|
||||
|
||||
## Q8 — `InstanceChildHandler`도 부기를 등록한다 (`H-134`, 확정 (a))
|
||||
|
||||
- `dispatch-core-plan.md` 말단 표에 행 신설 + `H-39` 블록에 "다섯째" 문단:
|
||||
`process` 맨 앞에서 `setOffsetSource(inst, k, None)` →
|
||||
**`setLength(inst, k, 1, inst, v)`** → `v.Parent = inst`; 반환 클로저는
|
||||
`None` → `0` 순서로 해제. `ROADMAP.md` M5 체크박스에 같은 두 줄. 5번째
|
||||
인자(요소)는 Q3의 `setLength` 시그니처를 따른 것 — 상수 길이라 지속 클로저는
|
||||
안 생기지만 등록 모양을 다른 말단과 맞췄다.
|
||||
|
||||
## Q9 — 재고: 두 "확정"이 아니라 Tween 절 스케치의 **복사 오류** (`H-135`, 확정)
|
||||
|
||||
사용자가 문항의 전제를 정정했다(*"nil은 파괴가 맞고 파괴 자체가 트윈을 지움.
|
||||
메니지드를 지우는 스케치가 왜 트윈에 있는지도 모르겠는 부분..? 트윈은 inst 를
|
||||
바꾸어 재프로세싱 하는거라 연관이 없는 부분인데"*). 다시 본 결과:
|
||||
|
||||
- `v == nil` → `process`가 자식을 파괴하는 규칙은 두 절이 **같다**. 이상한 건
|
||||
Tween 절 스케치의 마지막 줄 `return function(hint) if hint == nil then
|
||||
destroyManagedChild(inst,k) end end` 하나 — 그 절의 주제는 *자식 프로퍼티
|
||||
세팅을 `Dispatch.process(child, "CornerRadius", …)`로 되돌려준다*뿐이라
|
||||
관리 자식의 생사를 정할 자리가 아닌데, `v == nil`(값)과 `hint == nil`
|
||||
(retractor 인자)을 겹쳐 적은 **복사 오류**로 보인다.
|
||||
- 제가 (B) 분기에서 *"트윈이 끊긴다"*를 피해로 든 것은 틀렸다 — 자식이
|
||||
파괴되면 그 위의 트윈이 사라지는 건 당연한 결과지 결함이 아니다. 그 줄을
|
||||
지워야 하는 이유는 **파괴 경로가 하나여야 한다**(`process(inst,k,nil)`)는 것
|
||||
하나이고, 이중 파괴는 그 중복의 증상이다.
|
||||
- 트레이스: `(inst,"UICorner")` 체인은 어떤 값이든 `UICornerHandler.process`에
|
||||
`number|Tween|nil`로 닿고(`StoreBind`가 State를 풀고 `NoneHandler`가 `nil`로),
|
||||
키 자체가 사라지는 건 인스턴스 teardown뿐이라 그때는 자식이 부모와 같이
|
||||
죽는다 — retractor가 할 일이 없다.
|
||||
- **✅ [2026-08-27 확정]** 사용자: *"9 맞음. v == nil 로 신규 들어오면 파괴가 맞고
|
||||
retract 는 nop 맞아. 파괴 자체가 사실 inst 바꿔서 넣은 process 를 처리할 필요
|
||||
없게 만들어버리고, 우린 파괴에 대해서 retract 안하던게 맞아서, Tween 과 무관한
|
||||
것도 맞지."* — `ui-shorthand-plan.md` Tween 절 스케치의 그 줄을
|
||||
`return function() end`로 고치고 정정 bullet을 달았다. `H-135`는 🟡→🟢.
|
||||
|
||||
## Q10 — `reconcile` 재실행도 배치 Blocker로 (`H-136`, 확정 (a))
|
||||
|
||||
- `slot-plan.md`의 `reconcile` 머리(중복 키 선행 패스 뒤)에 `getBlocker(self)`
|
||||
획득, 꼬리에 `OffWithoutEmit()` + `recomputeBlocker` 확인 후 `recompute` 1회.
|
||||
**Blocker가 네스팅이 안 되므로**(불리언, `blocker-plan.md`) 최초 population
|
||||
처럼 바깥 배치 안에서 오면 `IsOn()`으로 알아보고 손대지 않는다(`ownsGate` —
|
||||
이 판정은 사용자가 승인한 (a)의 문구 밖에 있는 **구현 세부**다. 같은
|
||||
`reconcile` 클로저가 바깥 배치 안(최초 population)과 밖(재실행) 양쪽에서
|
||||
불리는데 Blocker에 네스팅이 없어서 필요한 것이고, 함수 지역 변수라 표면엔
|
||||
안 드러난다).
|
||||
- **사용자 논거**(위 원문): `native*` 계층이 생기기 전엔 DOM식 요소 입출력이
|
||||
강제가 아니라 raw op마다 `recompute`가 돌아야 했지만, 이제는 물리 위치를
|
||||
`native*`가 부기와 무관하게 정확히 넣으므로 최초 population처럼 한 번에
|
||||
따라잡아도 된다. 부작용으로 "offset이 먼저 잡힌 뒤 후행 요소가 밀리느냐,
|
||||
밀린 뒤 넘어가느냐"가 갈리지만 `offsetSetUpTo`/`offsetCacheValidUpTo`가
|
||||
분리돼 있고 동기라 괜찮다. `dispatch-core-plan.md`의 *"한 사이클 … 한 번만"*
|
||||
문단에 같은 근거를 적었다.
|
||||
- 제 판단: 동의 — `raw*`의 명시 호출이 이미 `getBlocker(self):IsOn()`을 보므로
|
||||
(`H-119`) 추가 배선이 없고, `getOffsetAt`/`physIndex`는 Blocker와 무관하게
|
||||
정확하다(최초 population과 같은 논증). `updateFn`이 던지면 Blocker가 켜진 채
|
||||
남는 것도 `materializeSlotTree`와 같은 부류다.
|
||||
|
||||
## `H-138` — 숏핸드 핸들러가 `PropertyHandler`보다 우선순위가 높다 (확정)
|
||||
|
||||
- `ui-shorthand-plan.md` "메커니즘" 절에 문단 신설, "결론" 절의 접두어 확정에
|
||||
두 번째 근거 추가. **사용자 논거**(위 원문): 리플렉션 거부에 기대면 하위
|
||||
계층(프로퍼티)이 상위 계층(숏핸드)의 키 집합을 알아야 하는 역방향 의존이
|
||||
생긴다; 충돌 방지는 우선순위가 아니라 `UI` 접두어의 몫(Roblox 프로퍼티
|
||||
이름엔 `UI` 접두어가 없다). 구체 상수는 구현 시.
|
||||
|
||||
## `H-139` — `New(name)(props)` 파이프라인 의사코드 (신설) + 거기서 나온 것
|
||||
|
||||
- `bind-system-plan.md` "인스턴스 생성 / 이벤트 네이밍 인체공학" 절에
|
||||
`New` ①~④(물리 생성 → gcconn/gchold → `flatten` → `Dispatch.drive`)와
|
||||
`drive` (a)~(c)(pre-pass → 단일 일반화 `for` 본체 + 배치 Blocker →
|
||||
`postRefList`) 의사코드 신설. 단계 안의 규칙은 각 소스 문서가 정본이고 여기는
|
||||
**순서**만 확정. `ROADMAP.md` M3 `Dispatch.drive` 항목에서 가리킨다.
|
||||
- 이름 충돌(모듈 팩토리 `New()` vs 생성자 `New "Frame"`)은 산문 표기 규칙
|
||||
한 줄로 닫았다(패키지가 달라 런타임 충돌은 없다).
|
||||
- **쓰면서 드러난 것 셋** — 사용자가 예상한 그대로:
|
||||
1. ~~**배치를 닫는 자리가 어디에도 없었다.**~~ **⚠️ 이 주장은 틀렸다**(감사
|
||||
1라운드) — `dispatch-core-plan.md`의 `H-17` 절이 이미 *"`drive` 전체
|
||||
(post-pass 포함)를 감싼다, `PostRef` 콜백은 게이트가 켜진 채 실행된다"*로
|
||||
정해뒀는데 제가 그 절을 안 보고 "해시 파트 앞에서 닫는" 의사코드를 썼고,
|
||||
사용자 확인 1번은 **그 틀린 전제 위에서** 받은 것이다. 의사코드를 `H-17`대로
|
||||
(진입 직후 On, `postRefList` 뒤 Off + `recompute`) 고쳤다 — 확인 1번은
|
||||
무효, 계약은 원래 것 그대로. 실제로 낡아 있던 건 같은 문서 "해법의 핵심"
|
||||
4번의 옛 문구(*"배열 파트 순회 전체"*) 하나라 같이 정정.
|
||||
2. **자식 없는 Instance에도 Blocker/`bk`가 생기는 경로.** `getBlocker(inst)`를
|
||||
무조건 부르면 `Frame { Size = … }`마다 Blocker + `bk` + `Relate` 항목이
|
||||
eager 생성된다 — Q2/Q3가 Slot 쪽에서 막은 것과 같은 부류.
|
||||
`flattened[1] ~= nil` 가드로 막았다. 확인 요청.
|
||||
3. **`H-142` 🟡 신설 — 해시 파트 안의 `Parent` 순서가 미정.** `Frame { Parent
|
||||
= x, Size = … }`에서 `Parent` 대입이 다른 프로퍼티보다 먼저 올 수 있다
|
||||
(Luau 해시 순회 순서는 계약이 아니다). 처방 후보(순서 미루기/문서화/무시)가
|
||||
전부 새 메커니즘이라 정하지 않고 올렸다.
|
||||
**✅ [2026-08-27 확정 — 순서가 아니라 키 금지]** 사용자: *"Parent대입 자체가
|
||||
오면 안 돼. 그건 부모에서 할 일이거든. 자신이 바로 하는 경우는 없어. 그걸
|
||||
허용해준다는것 자체가, '외부에서 직접 Parent 설정해주지 말것' 을 해치는
|
||||
요인이 되기도 해.(암묵적으고 가능하도록 둬버려서)"* — `slot-plan.md`의
|
||||
"동적 자식은 반드시 `Slot` 또는 `state<Frame>`류 store-bind를 통해서만" 원칙의
|
||||
정적 리터럴 판. 반영: `bind-system-plan.md` 파이프라인 절에 규칙 + `ROADMAP.md` M5
|
||||
(`D` 생성기가 props 타입에서 `Parent` 제외 / `PropertyHandler.isHandlable`이
|
||||
`"Parent"` 거부 → 기존 "매치 핸들러 없음 → 즉시 error"에 걸림). **런타임
|
||||
배선은 제 선택**(새 메커니즘 없이 기존 계약 재사용)이라 문서에 그렇게
|
||||
갈라 적었다.
|
||||
- **1·2 확인 완료**(사용자: *"1과 2는 확인했어. 2는 특히 생성 이후 다시 process
|
||||
를 주는걸 우린 안 하기로 해서 충분히 가능한 일"*). 2에 대해 사용자가 남긴
|
||||
의심 — *"Frame{} 으로 나온 bk없는게 Slot 안에 멀쩡히 들어가는데 문제
|
||||
없을까"* — 를 코퍼스의 호출부 전수로 확인했다: **문제 없다.**
|
||||
- `getBookkeeping`/`getOffsetAt`/`setLength`/`setOffsetSource`의 첫 인자
|
||||
(`ownerKey`)는 코퍼스 전체에서 **Slot 자신(`self`/`slot`) 또는 `drive`를
|
||||
도는 최상위 `inst`뿐**이다. Slot에 들어간 `Frame{}`은 언제나 **요소**
|
||||
(`setLength`의 5번째)나 **앵커/물리 target**(4번째, `bindLifetime`·
|
||||
`nativeInsert`의 첫 인자)으로만 나온다 — 앵커는 gchold(②에서 무조건
|
||||
생성)를 쓰지 `bk`를 안 본다.
|
||||
- 그 Frame이 owner가 되는 경우는 자기 props에 배열 파트가 있을 때뿐이고,
|
||||
그땐 `flattened[1] ~= nil`이라 가드가 안 걸린다. `outerSlot:Add(innerSlot)`
|
||||
처럼 나중에 중첩 Slot이 그 Frame을 물리 target으로 써도 owner는
|
||||
`outerSlot`이다.
|
||||
- "`bk.base`"는 이미 걷어낸 필드다(`dispatch-core-plan.md` 3번, 사용자
|
||||
지적으로 — 최상위 `inst`의 베이스는 항상 0이라 저장할 게 없다). 설령
|
||||
예상 못 한 경로가 `getBookkeeping(frame)`을 불러도 **lazy 생성**이라
|
||||
크래시가 아니라 그때 만들어질 뿐이다.
|
||||
|
||||
## 반영 기록 — Q1~Q3 (2026-08-27)
|
||||
|
||||
**바뀐 파일**: `base/dispatch-core-plan.md`(`recompute` 루프 재배치 / `setLength`
|
||||
|
|
@ -379,7 +573,10 @@ elem->index 를 위치를 재설정 해준다면 괜찮아"* — `rawReplace`의
|
|||
맞아. 그래야 인덱싱 매핑을 만드니까"*. (4) *"slot._baseObserver 는 unbind 만 했고,
|
||||
nil 로 지우는것만 안 한다면 맞아"*.
|
||||
|
||||
**아직 안 한 것**: `/code-review high`, 커밋 — Q4~Q10 반영 뒤 한 번에. README
|
||||
**아직 안 한 것**(Q1~Q3 체크포인트 시점 문장 — **[2026-08-27 후속] Q4~Q10도
|
||||
전량 반영됐고**, 그 반영분의 감사 루프는 아래 "감사 루프 (2026-08-27, Q4~Q10
|
||||
반영분)" 절; 남은 건 라운드 전체에 대한 `/code-review high`와 커밋뿐):
|
||||
`/code-review high`, 커밋 — Q4~Q10 반영 뒤 한 번에. README
|
||||
색인과 `todos.md` 00번 갱신은 감사 1라운드 지적으로 그 자리에서 했다. 감사
|
||||
루프는 Q1~Q3 반영분에 대해 돌았고 6라운드에서 수렴했다(아래 "감사 루프" 절).
|
||||
|
||||
|
|
@ -396,3 +593,54 @@ nil 로 지우는것만 안 한다면 맞아"*.
|
|||
| 4 | 앞 라운드 수정분 자체 + followup/발견 문서 내부 정합 | 확실 1 | `CLAUDE.md`·`project-context.md`(둘 다 `@import`)가 결정 소스로 8라운드까지만 나열 — 9라운드 한 줄 추가. 그 외 앞 수정분·두 문서 내부 정합은 줄 단위 대조로 이상 없음 |
|
||||
| 5 | 4라운드 수정분 + 델타 밖 `base/` 문서 | 확실 1 · 판단 1 | `README.md`의 `dispatch-core`/`slot-plan` 색인 행에 Q1/Q2 요약 추가 / `architecture.md`의 `Slot.luau` 한 줄에 생성자 필드·`_destroyed` 표기(판단 항목 — 8라운드 행들이 같은 밀도라 넣음) |
|
||||
| 6 | 5라운드 수정분 + 신설 규칙 문단 | **확실 0** · 의심 1 | *"`Owned=false`는 `_destroyed`가 안 선다"*가 README 요약과 followup에만 있고 `slot-plan.md` 산문엔 없던 것 — 사용자 확정 인용과 함께 명문화. **수렴**(확실 0) |
|
||||
|
||||
## `/code-review high` (2026-08-27, Q4~Q10 반영분 + 감사 8라운드 뒤)
|
||||
|
||||
10건(검증 12 CONFIRMED · 5 PLAUSIBLE · 1 REFUTED). **여섯은 기존 규칙 적용이라
|
||||
그 자리에서 반영**, **넷은 처방이 새 메커니즘·표면이라 문항으로**(`-round9.md`의
|
||||
`H-143`~`H-146`, `question.md` 최우선 절).
|
||||
|
||||
반영한 여섯:
|
||||
1. **`InstanceChildHandler` retractor가 옛 자식을 안 내렸다** — `Parent = nil`
|
||||
추가(파괴 아님 — `slot-plan.md`의 "`State<Slot>` 교체는 파괴가 아니라
|
||||
언마운트" 절 근거 1이 바로 `state<Frame>`의 이 동작). `state:Set(B)`에서 A가
|
||||
물리 자식으로 남은 채 `lengthList[k] == 1`이 되던 것.
|
||||
2. **같은 핸들러의 순서** — `setLength` → `Parent`였던 것을 `Parent` →
|
||||
`setLength`로("일반 계약 — 물리와 부기의 순서" 3번과 같게). 단건 경로에서
|
||||
`recompute`가 부착 전에 돌았다.
|
||||
3. **같은 핸들러의 5번째 인자 `v`** — 제가 "모양을 맞추려" 넣은 것인데, 해제
|
||||
`setLength(…, 0)`이 옛 키를 안 지워 교체마다 `bk.indexOfElement`에 옛 자식이
|
||||
강참조로 쌓였다. Q3 계약대로 상수 길이는 **생략**. (Slot 쪽 같은 구멍은
|
||||
`H-145`로.)
|
||||
4. **`H-17`에 빈 배열 파트 가드가 없었다** — 사용자 확인 2번이 스케치에만 적혀
|
||||
정본(`dispatch-core-plan.md`)은 여전히 "무조건 On"이었다. 정본과 "해법의
|
||||
핵심" 1번에 반영.
|
||||
5. **`quad-types`의 `Quad`에 `Ref` 필드** — `H-128`로 최소형이 M2에 왔는데 그
|
||||
필드 체크박스는 M8에 남아 `H-25` 공백을 다시 열었다. M2 `H-80` 목록에 `Ref`
|
||||
추가, M8 항목은 흡수 표기.
|
||||
6. **Modifier 메소드 목록의 `Parent`** — props 타입에서만 빼면
|
||||
`Modifier():Parent(x)`가 타입을 통과한다. `bind-system-plan.md` `H-142` 타입
|
||||
항목 + ROADMAP M7 체크박스.
|
||||
7. **`effect-plan.md` 번호 목록 재정렬** — "실행 순서가 아니다" 괄호만 달고 옛
|
||||
순서를 남겨둔 것이 `H-130`과 같은 모양이라 게이트 → 플래그 → cleanup으로
|
||||
다시 씀.
|
||||
|
||||
기각 1(REFUTED): "`drive` 의사코드가 `dispatch-core-plan.md`를 중복한다" — 사용자
|
||||
요청으로 신설한 블록. 캡에 밀린 하위 발견(`Slot:List` 잉여 블록 잔존 등)은 이미
|
||||
"잉여" 주석으로 처리된 것과 같은 항목.
|
||||
|
||||
## 감사 루프 (2026-08-27, Q4~Q10 반영분)
|
||||
|
||||
관례대로 `quad-doc-auditor` 한 턴에 하나, diff 범위, 라운드마다 각도 변경.
|
||||
|
||||
| 라운드 | 각도 | 새 발견 | 처분 |
|
||||
|---|---|---|---|
|
||||
| 1 | `base/` 정합성 | 확실 3 · 판단 1 · 의심 1 | ⭐ `drive` 의사코드가 `H-17`(*"`drive` 전체를 감싼다, `PostRef` 콜백은 게이트 안"*)을 어김 — 제 *"닫는 자리가 없다"* 주장이 틀렸고 사용자 확인 1번은 무효(`H-139` 절) / `question.md` 최우선 절 "Q4~Q10 대기" 잔재 / README `effect-plan` 행 `:Callback` 잔재 / ROADMAP `Property.luau`에 "거부 배선은 에이전트 선택" caveat / Blocker 시작점 "진입 직후"로 |
|
||||
| 2 | 인덱스 레이어 + 새 문단 자기모순 | 확실 1 · 의심 2 | ROADMAP M8 `Ref.luau` 체크박스가 자기 `H-128` 노트와 모순 → "나머지(`:Wait`)"로 재작성 / Q9 헤더 "확인 대기" 잔재 / `Slot:List` wrap이 `reconcile` 게이트와 중복 → "잉여" 주석 |
|
||||
| 3 | 주변 폴더 인용처 + 인용문 역방향 + 수정분 | 확실 2 · 의심 2 | ROADMAP 머리 배너 "Q4~Q10 대기" 잔재 / 이 파일 "아직 안 한 것" 문장 / (의심) `ownsGate`는 승인 문구 밖의 구현 세부 — 지역 변수라 그대로, `H-136` 절에 그렇게 표기 / (의심) M8 체크박스의 *"서술도 같이 갔다"*가 과장 → 문구 정정. 인용문 역방향은 전부 일치 |
|
||||
| 4 | 1~3라운드 수정분 + 발견/결정 문서 내부 정합 | 확실 3 · 의심 2 | 발견 문서 요약 표에 `H-142` 행 없음 / `H-135` 심각도 표 vs 헤더 / §4 아래 Q9 "확인 대기" 잔재 / (의심) `Slot:List` "잉여" 주석 범위를 블록 전체로 / (의심) M8 체크박스의 표면 목록 반복 → M2 참조로 |
|
||||
| 5 | 4라운드 수정분 + 델타 밖 `base/` | 확실 1 · 의심 2 | `source-state-plan.md`의 *둘 다 error · 계약은 하나* 문장에 `H-133` 캐비엇 / `blocker-plan.md` 재진입 절에 "`IsOn()` 소유권 판정은 네스팅이 아니다" 한 줄 / `architecture.md` `Ref.luau` 한 줄에 `Epoch` 표면·M2 최소형 |
|
||||
| 6 | 5라운드 수정분 + 신설 규칙 vs 기존 규정 | 확실 1 · 의심 0 | 이 파일 머리 배너 *"진행 중이다"* 잔재(5라운드 동안 빠졌던 것) → "전량 처리"로. 신설 규칙 여섯 자리(`Parent` 금지 / gcconn 시점 / `New` 표기 / `WeakUnsubscribe` 비소진 / 숏핸드 우선순위 / 다섯째 말단) 전부 기존 규정과 충돌 없음 |
|
||||
| 7 | 6라운드 수정분 + 세션 기록 사실성 | 확실 3 · 의심 0 | README `bind-system-plan` 행이 "배치 닫는 자리 … `Parent` 순서 미정"으로 1라운드 정정·`H-142` 확정 이전 상태 / `todos.md` 00번에서 "`/code-review high`·커밋 남음" 액션이 사라짐 → 복원 / 이 파일의 "48줄" 개수 주장(실제 47) → 개수 삭제. 세션 파일·summary·todos 본문 서술은 전부 사실과 일치 |
|
||||
| 8 | 7라운드 수정분만(좁게) | 확실 1 | `project-context.md`의 볼드 마커가 홀수(이번 갱신이 닫는 `**`를 지움) → 짝 맞춤. 지정 세 자리는 회귀 없음. **여기서 멈춤** — 1라운드 이후 설계 내용 발견은 0이고 [2026-08-27 8라운드 시점] 남은 건 기록 문서의 표기뿐이라, 비용 대비 `/code-review high`로 넘어가는 게 낫다고 판단(추이 5→3→4→5→3→1→3→1, 단조 수렴은 아님) |
|
||||
|
||||
|
|
|
|||
|
|
@ -49,13 +49,18 @@ high` 7패스의 수정)를 처음부터 다시 트레이싱한 결과. 발견
|
|||
| `H-132` | 🟢 | `slot-plan.md` 2900행의 *"…해야 하는 일이 **셋** 늘었다"* 헤딩 아래 bullet이 넷(`H-102`가 넷째를 더함) | `slot-plan.md` 2900 | 사냥 #3 | — |
|
||||
| `H-133` | 🟢 | `WeakUnsubscribe`는 **구독한 적 없는 값**에 조용히 통과한다(가드가 "강한 킵 있음"만 본다) — *"해제는 건 경로로 푼다 … 양방향 fail-fast"* 산문과 비대칭. 의도라면 명문화 | `lifecycle-pattern.md` (2) | 계약 정합 (E) | ✅ 실측 |
|
||||
| `H-134` | 🟡 | `InstanceChildHandler`(`Frame { Frame {} }`의 정적 자식)의 배열 자리 부기(`setOffsetSource`/`setLength(…, 1)`)가 **어디에도 명세돼 있지 않다** — 말단 표에 행이 없고 의사코드도 없어 `H-39`의 "전수"가 못 잡았다. `Frame { Frame{}, Slot() }`이 첫 마운트에 `H-106` 가드로 즉사 | `dispatch-core-plan.md` 말단 표 × `architecture.md` × `ROADMAP.md` M5 | 레인 B (M5) | 값 트레이스 |
|
||||
| `H-135` | 🟡 | `ui-shorthand-plan.md` 안에 숏핸드 retractor 모양이 **둘** — *"`function() end`이면 충분"*(136행) vs Tween 절 스케치 `function(hint) if hint == nil then destroyManagedChild end`(169행). 후자면 (A)-`nil`에서 이중 파괴, (B)에서 자식 파괴·재생성으로 트윈 스냅 | `ui-shorthand-plan.md` | 레인 B (M5) | 값 트레이스 |
|
||||
| `H-135` | 🟡→🟢 | `ui-shorthand-plan.md` 안에 숏핸드 retractor 모양이 **둘** — *"`function() end`이면 충분"*(136행) vs Tween 절 스케치 `function(hint) if hint == nil then destroyManagedChild end`(169행). 후자면 (A)-`nil`에서 이중 파괴, (B)에서 자식 파괴·재생성으로 트윈 스냅 | `ui-shorthand-plan.md` | 레인 B (M5) | 값 트레이스 |
|
||||
| `H-136` | 🟡 | `:List` **재실행** reconcile은 배치 Blocker 없이 돌아 raw op마다 `recompute` + `Length:Set`이 난다 — *"한 사이클 전체가 끝난 뒤 한 번만"*(`dispatch-core-plan.md` 2344) 서술과 모순, 최초 population만 감싼 O(n²) 논거가 재실행엔 빠져 있다 | `slot-plan.md` `activateList`/`reconcile` × `dispatch-core-plan.md` | 레인 B (M6) | 값 트레이스 |
|
||||
| `H-137` | 🟢 | `rawMove`/`rawSwap` `H-29` 규약 1의 치환 목록에 `bk.tokens`/`bk.indexOfToken`이 없다(`H-102`가 splice에만 요구) — 규약 4(이동 구간 **전부** `setLength` 재등록)에 암묵 의존, 토큰은 자리 귀속이라 치환하면 안 된다는 것도 미명시 | `slot-plan.md` `H-29` 규약 | 레인 B | — |
|
||||
| `H-138` | 🟢 | `UICorner`류 숏핸드 핸들러와 `PropertyHandler`의 매치 구분(리플렉션 거부? 우선순위?)이 명세에 없다 — 이름이 우연히 프로퍼티와 겹치면 조용히 `PropertyHandler`가 먹는다 | `ui-shorthand-plan.md` × `bind-system-plan.md` | 레인 B | — |
|
||||
| `H-141` | 🟡 | **확정의 근거로 인용된 사용자 발언이 실제로는 다른 것을 승인한 것** — `H-102`의 *"dispatch 로 격상"* 인용 옆에 사용자가 정한 적 없는 `bk.tokens`/`indexOfToken`(`token = {}`, 2026-08-25 `/code-review`가 발명)이 확정 메커니즘처럼 앉아 있었고, 같은 문단의 *"splice 요구 목록에 항목이 늘지 않는다"*는 구현이 스스로 깼다(둘 늘렸다). 같은 뜻의 맵(`slot._elemIndex`)이 Slot 층에 따로 살아 두 층 이원화 | `dispatch-core-plan.md` `H-102` 문단 × `slot-plan.md` | 사냥 #1(인용문 판) — Q3 처리 중 발견 | — |
|
||||
| `H-140` | 🟢 | `ROADMAP.md:1124`가 아직 *"해제 시 `slot.Offset = nil`"* — `SL-75`/`D-60`이 전면 정정한 문장인데 **구현자가 실제로 보는 체크박스**에 살아남았다. 그대로 짜면 포탈 구독자가 영구히 끊긴다 | `ROADMAP.md` M6 | 사냥 #7 | — |
|
||||
| `H-139` | 🟢 | `New`가 둘(모듈 팩토리 `New(): Quad` / 인스턴스 생성자 `New "Frame" {…}`)이고, `D.Frame {…}`의 파이프라인(생성 → gcconn → flatten → drive)은 네 문서에 흩어져 있어 의사코드가 한 곳에 없다 | `module-lifecycle-plan.md` / `bind-system-plan.md` | 레인 B | — |
|
||||
| `H-142` | 🟡→닫힘 | **[2026-08-27 `H-139` 의사코드에서 발견]** `Dispatch.drive`의 해시 파트 순서가 계약이 아니라 `Frame { Parent = x, Size = … }`의 `Parent` 대입이 다른 프로퍼티보다 먼저 올 수 있다 — 사용자 확정은 순서가 아니라 **props에 `Parent` 금지** | `bind-system-plan.md` × `ROADMAP.md` M5 | 사냥 밖 — 의사코드 작성이 드러냄 | — |
|
||||
| `H-143` | 🟡 | **[2026-08-27 `/code-review high`]** `fn` 안에서 `self:Unsubscribe()`를 부르면(문서가 허용하는 자리) `Rerun`이 `fn`의 반환 cleanup을 그대로 `_cleanup`에 저장하고 `_installed = true`로 되돌려 **아무도 소진 못 하는 cleanup**이 남는다 — "마지막 cleanup 정확히 1회" 계약 위반 | `effect-plan.md` `Rerun` | 사냥 밖 — 리뷰 발견, 처방은 새 메커니즘 | — |
|
||||
| `H-144` | 🟡 | **[2026-08-27 `/code-review high`]** `EffectHandle.Subscribe = Observer.Subscribe` 배정이라 `Unsubscribe`로 cleanup을 소진한 핸들을 다시 `Subscribe`하면 `.Subscribed = true`만 서고 **재설치(`Rerun`)가 없다** — leaf 재바인드 경로(`_bindDestroying`의 `not _installed → Rerun`, `H-65`)와 비대칭 | `effect-plan.md` | 리뷰 발견, 처방은 새 메커니즘 | — |
|
||||
| `H-145` | 🟡 | **[2026-08-27 `/code-review high`]** 최상위 `SlotHandler` retractor가 `setLength(inst, k, 0)`으로 해제할 때 `bk(inst).indexOfElement[slot]`이 **안 지워진다**(해제 호출엔 요소가 없고 `setLength`는 `element ~= nil`일 때만 쓴다) — 교체마다 옛 Slot이 부모 `bk`에 강참조로 쌓여 부모가 죽을 때까지 산다(`InstanceChildHandler` 쪽은 5번째 인자를 안 넘기는 것으로 닫았지만 Slot은 `Length`가 State라 요소가 필요하다) | `dispatch-core-plan.md` `setLength` × `slot-plan.md` 494 | 리뷰 발견, 처방은 새 메커니즘 | — |
|
||||
| `H-146` | 🟡 | **[2026-08-27 `/code-review high`]** `H-142`(props에 `Parent` 금지, 대입은 자식을 받는 쪽만)를 닫고 나니 **루트**(`ScreenGui` → `PlayerGui`)를 붙이는 승인된 경로가 코퍼스 어디에도 없다 — 유일한 선례는 v1 `Mount(ScreenGui, …)`, `slot-plan.md` 233행의 *"v2는 mount 함수 자체가"*의 그 함수는 어느 표면에도 없다. 부수로 거부 배선의 에러가 일반 매치 실패 메시지(*"provider가 초기화됐는지 확인"*)라 오해를 부른다 | `bind-system-plan.md` `H-142` | 리뷰 발견, 처방은 새 표면 | — |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -433,7 +438,7 @@ Length/Offset 절 머리(1365행)가 *"정적 단일 자식은 상수 `1`"*이
|
|||
등록)은 이미 기각된 안(*"모든 핸들러가 `k=number`일 때 처리하도록 두는"*,
|
||||
1387행)이라 비권고. 갈래는 사실상 없다 — 확인만.
|
||||
|
||||
### `H-135` 🟡 — 숏핸드 retractor의 두 모양
|
||||
### `H-135` 🟢 — 숏핸드 retractor의 두 모양 (**[2026-08-27] 🟡→🟢 강등 — 문항의 전제가 틀렸다**, 아래 처방 갈래가 아니라 Tween 절 스케치의 복사 오류 한 줄; `-round9-followup.md` Q9)
|
||||
|
||||
**무엇이 문제인가.** `ui-shorthand-plan.md`의 *"`v`가 `nil`인 경우"* 절(136행)은
|
||||
*"반환 클로저가 할 일이 없어 `function() end`이면 충분"*이라 확정하는데, 같은
|
||||
|
|
@ -584,8 +589,92 @@ slotPos, S.Offset)`이 `S.Offset:Set(newAbs)`를 내는데, 그 시점 `S._baseO
|
|||
맵을 `bk.indexOfElement` 하나로 통일하고 `token`을 폐기**하는 형태로 닫혔다 —
|
||||
그 결과 **`H-137`은 소멸**했다(토큰이 없어졌으므로).
|
||||
|
||||
**⚠️ [2026-08-27 같은 날 후속] Q4~Q8·Q10과 `H-138`·`H-139`도 확정·반영됐다** —
|
||||
Q4/Q5/Q8/Q10은 권고 (a), Q6은 (a)(의도된 관대함), Q7은 (b)(`archive/`로).
|
||||
**Q9는 사용자가 문항의 전제를 정정**했다 — 두 "확정"이 경쟁하는 게 아니라
|
||||
Tween 절 스케치의 `hint == nil` 줄이 `v == nil` 규칙을 잘못 옮긴 복사 오류이고,
|
||||
(B) 분기의 "트윈이 끊긴다"는 피해가 아니다(자식 파괴가 트윈을 지우는 건 당연).
|
||||
**[같은 날 확정]** 사용자: *"9 맞음 … retract 는 nop 맞아"* — `H-135`는 🟢로.
|
||||
`H-139` 의사코드를 쓰면서 **`H-142`**가 나왔다(아래). 결정의 소스는
|
||||
`-round9-followup.md`.
|
||||
|
||||
### `H-142` 🟡 — 해시 파트 안의 `Parent` 순서가 미정 (`H-139` 의사코드에서 발견)
|
||||
|
||||
`Dispatch.drive`의 본체가 단일 일반화 `for`(`F-4-1`)라 해시 파트 순서는 Luau의
|
||||
내부 순회 순서다 — 계약이 아니다. `Frame { Parent = x, Size = …, Position = … }`
|
||||
에서 `Parent` 대입이 다른 프로퍼티보다 **먼저** 올 수 있고, 그러면 부모에 붙은
|
||||
뒤에 프로퍼티가 바뀐다. 프레임 경계가 안 끼므로 사용자에게 보이는 중간 상태는
|
||||
없지만 엔진 쪽 레이아웃 재계산이 프로퍼티 수만큼 붙는다. 코퍼스 어디에도
|
||||
`Parent`의 처리 순서 서술이 없다(`PostRef` 절은 *"자기 `.Parent`는 아직일 수
|
||||
있다"*라고 부모 쪽 시점만 말한다). 처방 후보는 전부 새 메커니즘이라 정하지
|
||||
않았다 — (a) `drive`가 `Parent` 키를 루프 뒤로 미룸, (b) 문서화만(사용자가
|
||||
`Parent`를 별도로 대입), (c) 무시. **사용자 판단.**
|
||||
|
||||
**[2026-08-27 확정 — 셋 다 아님, 키 금지]** 사용자: *"Parent대입 자체가 오면 안
|
||||
돼. 그건 부모에서 할 일이거든."* props에 `Parent`는 올 수 없고(타입 제외 +
|
||||
`PropertyHandler` 거부 → 기존 no-handler error), 순서 문제는 소멸. 소스는
|
||||
`-round9-followup.md`의 `H-142` 항목.
|
||||
|
||||
---
|
||||
|
||||
### `H-143`~`H-146` — 2026-08-27 `/code-review high`가 Q4~Q10 반영분에서 잡은 것 (사용자 판단)
|
||||
|
||||
반영 뒤 돌린 `/code-review high`(10건, 검증 12 CONFIRMED)가 낸 것 중 **처방이 새
|
||||
메커니즘·표면인 넷**. 나머지 여섯(`InstanceChildHandler` retractor의 `Parent = nil`
|
||||
누락·순서·5번째 인자 / `H-17`에 빈 배열 파트 가드 / `Quad.Ref` 필드 마일스톤 /
|
||||
Modifier 메소드 목록의 `Parent` 제외 / `EffectHandle` 산문 순서)은 기존 규칙
|
||||
적용이라 그 자리에서 반영했다(`-round9-followup.md`의 code-review 절).
|
||||
|
||||
**`H-143` 🟡 — `fn` 안 `self:Unsubscribe()` 뒤 반환 cleanup이 고아.**
|
||||
`effect-plan.md`가 *"`fn` 안에서 `self:Rerun()`/`self:Unsubscribe()` 같은 핸들
|
||||
표면에 바로 닿는다"*고 허용하는데, `Rerun`은 `_consumeCleanup()` → `fn` → 반환값을
|
||||
`_cleanup`에 저장 + `_installed = true`다. `fn` 안에서 `Unsubscribe`가 통과하면
|
||||
(레지스트리 제거, `_consumeCleanup` no-op — 이미 nil) 그 뒤 `fn`이 돌려준 새
|
||||
cleanup이 저장되는데, 핸들은 이제 어느 레지스트리에도 없고 leaf도 아니라 재
|
||||
`Unsubscribe`는 error, `WeakUnsubscribe`는 소진 안 함, dep도 못 깨운다 → 영원히
|
||||
안 불린다(예: 타이머 정지 cleanup). **갈래**: (a) `Rerun`이 `fn` 반환 뒤
|
||||
`canExecute(self)`가 거짓이면 반환 cleanup을 **즉시 소진**(저장 안 함) / (b) `fn`
|
||||
안 `Unsubscribe`를 금지하고 그 문장을 지움 / (c) 허용하되 "그 실행의 cleanup은
|
||||
사용자 책임"으로 문서화. **권고 (a)** — 계약(*"끝나는 시점에 마지막 cleanup 정확히
|
||||
1회"*)을 코드가 지키는 유일한 갈래. 다만 `Rerun` 꼬리에 분기 하나가 는다.
|
||||
|
||||
**`H-144` 🟡 — `Unsubscribe` 후 재`Subscribe`에 재설치가 없다.**
|
||||
`Observer.Subscribe`를 그대로 배정해서 `canBound` 게이트를 통과하면
|
||||
`.Subscribed = true`만 서고 `fn`은 안 돈다. deps 없는 Effect는 아무도 `Rerun`을
|
||||
못 불러 레지스트리가 살려두는 죽은 핸들(누수), deps 있으면 다음 emit까지 죽어
|
||||
있다가 조용히 재설치. leaf 재바인드는 `_bindDestroying`이 `not _installed`면
|
||||
`Rerun`한다(`H-65`)라 비대칭. **갈래**: (a) `EffectHandle:Subscribe` 래퍼 —
|
||||
`Observer.Subscribe(self)` 뒤 `if not self._installed then self:Rerun() end`
|
||||
(leaf 경로와 대칭) / (b) 소진된 핸들의 재구독을 error / (c) 허용하되 재설치 없음을
|
||||
문서화. **권고 (a)** — `H-65`가 leaf 쪽에 이미 세운 규칙과 같은 모양.
|
||||
|
||||
**`H-145` 🟡 — 최상위 Slot 교체 시 `bk(inst).indexOfElement[oldSlot]` 잔존.**
|
||||
`setLength(inst, k, 0)` 해제엔 요소가 없어 옛 키가 안 지워지고(`setLength`는
|
||||
`element ~= nil`일 때만 쓴다), `indexOfElement`는 강한 키 맵이라 `Relate(inst)`
|
||||
부기가 옛 Slot을 부모가 죽을 때까지 붙든다 — Q3가 세운 맵의 부작용이고
|
||||
`rawReplace`(Slot 층)는 옛 키를 지우지만 inst 층 핸들러는 `bk`에 못 닿는다.
|
||||
`InstanceChildHandler`는 5번째 인자를 안 넘기는 것으로 닫았지만 Slot은 `Length`가
|
||||
State라 요소가 필요하다. **갈래**: (a) `indexOfElement`를 **weak-key**로(마운트
|
||||
중엔 `_elements`/`Relate`가 강하게 잡으므로 사라질 일 없고, 해제 뒤엔 저절로
|
||||
빠진다) / (b) 해제 호출 `setLength(inst, k, 0, inst, oldSlot)`에도 요소를 넘기면
|
||||
`setLength`가 `len == 0`일 때 그 키를 지운다(시그니처 의미 확장) / (c) Slot 층처럼
|
||||
inst 층에도 명시 삭제 API. **권고 (a)** — 코드 한 단어이고 "다른 곳에서 안전하게
|
||||
유지되는 것은 weak로"(`lifecycle-pattern.md` (0)) 규칙 그대로.
|
||||
|
||||
**`H-146` 🟡 — 루트를 붙이는 승인된 경로가 없다.**
|
||||
`H-142`대로 `.Parent` 대입은 자식을 받는 쪽(`InstanceChildHandler`/Slot `native*`)만
|
||||
하는데, 부모가 quad 밖인 루트(`ScreenGui` → `PlayerGui`)는 그 어느 쪽도 아니다.
|
||||
사용자가 `D.ScreenGui { … }`를 만든 뒤 props `Parent`는 error, `Mount`류 API는
|
||||
없음, 밖에서 `gui.Parent = PlayerGui`는 인용문(*"외부에서 직접 Parent 설정해주지
|
||||
말것"*)상 금지처럼 읽힌다. 부수로 거부 배선의 에러가 *"no handler … check
|
||||
quad-roblox provider"*라 provider 설정을 의심하게 만든다. **갈래**: (a) **루트는
|
||||
예외** — quad가 만든 트리의 최상위를 quad 밖 부모에 붙이는 건 사용자가 밖에서
|
||||
`.Parent =`로 한다고 명문화(금지되는 건 *quad가 관리하는 자식 자리*에 끼우는 것) /
|
||||
(b) `Mount(root, parent)`류 표면 신설 / (c) 루트 전용 props 키. 에러 메시지는 어느
|
||||
갈래든 `PropertyHandler`가 `"Parent"`를 거부할 때 전용 문구를 내는 게 낫다(그건
|
||||
배선 세부라 갈래와 무관). **권고 (a)** — 새 표면 없이 인용문의 범위만 정확히
|
||||
적는 것.
|
||||
|
||||
## §5 이상 없다고 확인한 것 (다음 라운드가 다시 파지 않도록)
|
||||
|
||||
**A각도 — 겹쳐 읽어 정합했던 조합**:
|
||||
|
|
|
|||
|
|
@ -12,13 +12,24 @@
|
|||
|
||||
---
|
||||
|
||||
## ⭐ 최우선 — **비어 있음** (2026-08-27 갱신)
|
||||
## ⭐ 최우선 — `/code-review`가 낸 새 메커니즘 넷 (2026-08-27 갱신)
|
||||
|
||||
**[2026-08-27] 9라운드 손 트레이싱의 결정 문항 Q4~Q10이 회신 대기입니다 —
|
||||
M2 착수 게이트는 아닙니다**(🔴 둘은 Q1/Q2로 이미 닫혔고 남은 건 🟡/🟢). 문항과
|
||||
권고는 `qa-request/pre-implementation-handtrace-round9.md` §4 표, 진행 상태는
|
||||
`-round9-followup.md`의 진행 표가 소스(여기서 문항을 반복하지 않는다). 다음
|
||||
세션이 위에서부터 처리한다.
|
||||
**[2026-08-27] 9라운드 손 트레이싱은 Q1~Q10·`H-138`·`H-139`·`H-142`까지 전량
|
||||
처리·반영됐고**, 그 반영분에 `/code-review high`를 돌린 결과 **처방이 새
|
||||
메커니즘·표면인 넷**이 나와 판단을 기다립니다 — 갈래·권고는
|
||||
`qa-request/pre-implementation-handtrace-round9.md`의 `H-143`~`H-146` 절이
|
||||
소스(여기서 반복하지 않음). 한 줄씩:
|
||||
- **`H-143`** `fn` 안에서 `self:Unsubscribe()`하면 그 실행이 돌려준 cleanup이
|
||||
영원히 안 불린다 — 권고 (a) `Rerun`이 `canExecute` 거짓이면 즉시 소진. **M2.**
|
||||
- **`H-144`** `Unsubscribe`한 핸들을 다시 `Subscribe`해도 재설치가 없다 — 권고
|
||||
(a) `Subscribe` 래퍼가 `not _installed`면 `Rerun`(leaf 경로 `H-65`와 대칭). **M2.**
|
||||
- **`H-145`** 최상위 Slot 교체 시 부모 `bk.indexOfElement`에 옛 Slot이 강참조로
|
||||
남는다 — 권고 (a) 그 맵을 weak-key로. **M3.**
|
||||
- **`H-146`** props `Parent` 금지 뒤 루트(`ScreenGui` → `PlayerGui`)를 붙이는
|
||||
승인 경로가 없다 — 권고 (a) 루트는 예외로 명문화(금지는 quad가 관리하는 자식
|
||||
자리에 한함). **M5.**
|
||||
`H-143`/`H-144`는 M2 `Effect` 표면이라 **M2 착수 전 답이 있는 게 좋지만** 구현
|
||||
중 정해도 되는 크기입니다(게이트로 보지 않음).
|
||||
|
||||
**[2026-08-26] 8라운드 손 트레이싱이 처리 완료됐습니다 — M2(반응형 코어)
|
||||
착수를 막는 항목이 하나도 없습니다.** 발견 17건(`H-107`~`H-123`)의 결정
|
||||
|
|
|
|||
|
|
@ -1943,3 +1943,21 @@ ref 는 그 자체로 epoch임"*), `H-118`은 소유권 문제가 아니라 `gat
|
|||
high`는 Q4~Q10 반영 뒤로. 결정의 소스는
|
||||
`qa-request/pre-implementation-handtrace-round9-followup.md`, 경위는
|
||||
`session/2026-08-27-01-handtrace-round9-q1-q3.md`. **Q4~Q10은 다음 세션.**
|
||||
|
||||
## 2026-08-27-02 — 9라운드 Q4~Q10 결정·반영 + `H-138`/`H-139`/`H-142` (전량 처리)
|
||||
|
||||
Q4(`EffectHandle` 네 진입점 의사코드 — Observer 것 재사용, `Unsubscribe`만 게이트
|
||||
통과 뒤 cleanup) / Q5(M2 공통 기반에 `Ref` 최소형) / Q6(`WeakUnsubscribe` 관대 —
|
||||
약한 홀드를 강제하지 않으므로) / Q7(폐기 블록 `archive/effect-internal-observer-cascade-reversed.md`)
|
||||
/ Q8(`InstanceChildHandler` 부기) / Q10(`reconcile` 재실행도 배치 Blocker,
|
||||
네스팅 불가라 `ownsGate`). **Q9는 문항의 전제가 틀렸다** — Tween 절 스케치의
|
||||
`hint == nil` 줄이 `v == nil` 규칙의 복사 오류, 파괴 경로는 `process(nil)` 하나.
|
||||
`H-138` 숏핸드 우선순위 > `PropertyHandler`(충돌 방지는 `UI` 접두어). **`H-139`
|
||||
파이프라인 의사코드**(`New` ①~④ + `drive` (a)~(c), `bind-system-plan.md`)를
|
||||
쓰면서 배치 닫는 자리·빈 배열 파트 가드·`Parent` 순서가 드러났고, 마지막은
|
||||
**`H-142` — props에 `Parent` 금지**(*"그건 부모에서 할 일"*)로 순서 문제 자체가
|
||||
소멸. 감사 8라운드 뒤 `/code-review high` 10건 — 여섯 반영(그중 셋이 이 세션의
|
||||
`H-134` 반영분이 만든 것), **넷은 새 메커니즘이라 문항으로**(`H-143`~`H-146`,
|
||||
`question.md` 최우선 절). 결정의 소스는
|
||||
`qa-request/pre-implementation-handtrace-round9-followup.md`, 경위는
|
||||
`session/2026-08-27-02-handtrace-round9-q4-q10.md`.
|
||||
|
|
|
|||
73
.claude/session/2026-08-27-02-handtrace-round9-q4-q10.md
Normal file
73
.claude/session/2026-08-27-02-handtrace-round9-q4-q10.md
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# 2026-08-27 — 9라운드 Q4~Q10 결정·반영 + `H-138`/`H-139`/`H-142`
|
||||
|
||||
**무엇을 했나**: 앞 세션(`session/2026-08-27-01-handtrace-round9-q1-q3.md`)이
|
||||
Q1~Q3까지 반영하고 남긴 Q4~Q10을 사용자와 두 턴으로 처리해 `base/`·`ROADMAP.md`에
|
||||
반영했다. 결정의 소스는 `qa-request/pre-implementation-handtrace-round9-followup.md`
|
||||
(사용자 회신 원문 두 개도 거기 인용). 여기는 흐름과, 문서에 안 들어간 시행착오.
|
||||
|
||||
## 흐름
|
||||
|
||||
1. 판단 불필요한 `H-129`(ROADMAP `:Callback` 잔재)·`H-131`(*"`for d in seen`은
|
||||
죽는다"*는 거짓)을 먼저 닫고 Q4~Q10 + 🟢 둘(`H-138`/`H-139`)을 갈래·권고와 함께
|
||||
한 번에 올렸다.
|
||||
2. 사용자 1차 회신 — Q4/Q5/Q8/Q10 권고대로, Q6 (a)(약한 홀드를 강제하지 않으므로),
|
||||
Q7 (b)(컨벤션대로 `archive/`), **Q9는 "뭔가 이상한데?"** — 문항의 전제를
|
||||
정정. `H-138`은 숏핸드 우선순위가 높다(역방향 의존 논거 + `UI` 접두어의 역할),
|
||||
`H-139`는 *"실 구현 전에 의사코드를 써보자 … 감추어졌던 설계 결함이나 폭탄이
|
||||
발견된 경우가 많아서"*.
|
||||
3. Q9 재고: 두 "확정"이 경쟁하는 게 아니라 Tween 절 스케치의 `hint == nil` 줄이
|
||||
`v == nil` 규칙의 **복사 오류**였고, 내가 (B) 분기의 "트윈이 끊긴다"를 피해로 든
|
||||
것이 틀렸다(자식 파괴가 트윈을 지우는 건 당연). 파괴 경로는 `process(nil)`
|
||||
하나. 사용자 확인(*"9 맞음 … 우린 파괴에 대해서 retract 안하던게 맞아서"*).
|
||||
4. `H-139` 파이프라인 의사코드(`New` ①~④ + `drive` (a)~(c))를 쓰면서 셋이
|
||||
드러났다 — 배치 닫는 자리 미정(루프 뒤로), 자식 없는 Instance의 eager
|
||||
Blocker/`bk`(`flattened[1] ~= nil` 가드), 해시 파트 `Parent` 순서(`H-142`).
|
||||
사용자 2차 회신: 1·2 확인, **`H-142`는 순서가 아니라 키 금지**(*"Parent대입
|
||||
자체가 오면 안 돼. 그건 부모에서 할 일이거든 … '외부에서 직접 Parent 설정해주지
|
||||
말것' 을 해치는 요인"*). 2에 대한 의심(*"bk없는게 Slot 안에 멀쩡히 들어가는데
|
||||
문제 없을까"*)은 호출부 전수로 확인 — owner 키는 코퍼스 전체에서 Slot 자신 또는
|
||||
최상위 `inst`뿐이고 요소 Frame은 앵커(gchold)/요소로만 나온다, `bk.base`는
|
||||
이미 걷어낸 필드, `getBookkeeping`은 lazy.
|
||||
|
||||
## 시행착오 (문서엔 결론만)
|
||||
|
||||
- **Q9의 문항 자체가 잘못 세워져 있었다.** `H-135`는 "같은 문서의 두 확정이
|
||||
갈린다"로 썼는데 실제론 한쪽이 그 절의 주제 밖 스케치 한 줄이었다. 그 줄을
|
||||
"확정"으로 격상해 갈래를 만든 건 나였다 — 스케치 안의 코드 한 줄을 산문의
|
||||
확정과 같은 무게로 읽지 말 것.
|
||||
- **`H-142`에서 제시한 세 갈래가 전부 틀린 축이었다.** 순서 (a)/(b)/(c)를 물었는데
|
||||
답은 "그 키가 존재하면 안 된다"였다 — 증상(순서)에서 처방을 찾지 말고 그 키가
|
||||
거기 있어야 하는지부터 물었어야 했다. 반영한 런타임 배선(`PropertyHandler`가
|
||||
`"Parent"` 거부 → 기존 no-handler error)은 내 선택이라 문서에 갈라 적었다.
|
||||
- **Blocker 네스팅.** Q10을 반영하며 `reconcile`에 `blocker:On()`을 넣으려다
|
||||
`blocker-plan.md`의 "네스팅 미지원(불리언)"에 걸렸다 — 최초 population은 바깥
|
||||
배치 안에서 `reconcile`에 오므로 무조건 `On()`이면 안쪽이 먼저 꺼서 바깥 등록이
|
||||
게이팅을 잃는다. `ownsGate = not blocker:IsOn()`으로 갈랐다.
|
||||
- **`H-17`을 못 보고 "배치 닫는 자리가 미정"이라고 써서 사용자 확인까지 받았다.**
|
||||
감사 1라운드가 잡음 — `dispatch-core-plan.md`가 `drive` 전체(post-pass 포함)를
|
||||
감싸고 `PostRef` 콜백이 게이트 안에서 돈다고 이미 계약해뒀다. 의사코드를
|
||||
그대로 고쳤고 사용자 확인 1번은 무효로 표기. "어디에도 없다"는 주장은 grep
|
||||
한 번이면 반증됐을 것 — 없다고 쓰기 전에 그 단어(`post-pass`)로 찾을 것.
|
||||
- 절 인용이 Lua 주석 두 줄에 걸쳐 `--` 마커가 인용문 안으로 들어가 `doc-check`
|
||||
ERROR — 한 줄로 합쳤다(`conventions.md`의 blockquote 주의와 같은 부류, 코드
|
||||
주석도 마찬가지).
|
||||
|
||||
## 감사 루프와 code-review
|
||||
|
||||
- `quad-doc-auditor` 8라운드(5→3→4→5→3→1→3→1, 각도: `base/` 정합성 → 인덱스
|
||||
레이어 → 주변 폴더·인용문 역방향 → 수정분·발견/결정 문서 내부 → 델타 밖
|
||||
`base/` → 신설 규칙 vs 기존 규정 → 세션 기록 사실성 → 7라운드 수정분). 단조
|
||||
수렴은 아니었지만 1라운드(`H-17`) 이후 설계 내용 발견은 0이고 나머지는 기록
|
||||
문서 표기라 8라운드에서 멈추고 code-review로 넘어갔다(처분 표는 followup 끝).
|
||||
- `/code-review high` 10건 — 여섯 반영(`InstanceChildHandler` retractor의
|
||||
`Parent = nil`·순서·5번째 인자 / `H-17` 빈 배열 가드 / `Quad.Ref` 필드 M2 /
|
||||
Modifier 메소드 목록 `Parent` 제외 / `effect-plan` 목록 재정렬), **넷은 새
|
||||
메커니즘이라 문항으로**(`H-143`~`H-146` — `fn` 안 `Unsubscribe` 고아 cleanup /
|
||||
재`Subscribe` 재설치 / `indexOfElement` 잔존 / 루트 마운트 경로). 이번에도
|
||||
**반영분 자체가 새 결함을 만든 것**(`H-134`의 순서와 5번째 인자 둘 다 내가 넣은
|
||||
것)이 반복됐다 — `conventions.md`의 2026-08-26 실측 그대로.
|
||||
|
||||
## 남은 것
|
||||
|
||||
커밋, 그리고 `question.md` 최우선 절의 `H-143`~`H-146` 결정. M2 착수 게이트는
|
||||
여전히 0.
|
||||
|
|
@ -34,14 +34,21 @@
|
|||
검증(`error` level 2), `isModifier` 가드를 `Source` 생성자로 이동.
|
||||
- **문서화 대상 등록**: quad 두 벌 공존 시 `Brand`/`None`/`Subscribed`가
|
||||
사본마다 분리된다는 사실(`research/documentation-content-map.md` §4).
|
||||
- **⭐ [2026-08-27] 9라운드를 돌렸고 Q1~Q3까지 확정·반영했다 — Q4~Q10 대기.**
|
||||
- **⭐ [2026-08-27] 9라운드를 돌렸고 Q1~Q10·`H-138`·`H-139`·`H-142` 전량 확정·반영했다.**
|
||||
발견은 `qa-request/pre-implementation-handtrace-round9.md`(`H-124`~`H-141`,
|
||||
🔴 둘 다 실측 재현), **결정의 소스는 `-round9-followup.md`**(진행 표가
|
||||
상태의 소스). 반영된 셋: `recompute` 되감기 판정을 `lengthList[i]` 읽기
|
||||
앞으로 / `Offset`·`_baseObserver`를 Slot 생성자로 + `materializeSlotTree`
|
||||
순서 + `_destroyed` / `element → index`를 `bk.indexOfElement` 하나로(사용자가
|
||||
정한 적 없는 `token` 폐기). README 색인은 했고 `/code-review high`·커밋은
|
||||
Q4~Q10 뒤. 아래는 돌리기 전(2026-08-26) 서술:
|
||||
정한 적 없는 `token` 폐기). 같은 날 후속으로 Q4~Q8·Q10(`EffectHandle` 네
|
||||
진입점 의사코드 / M2에 `Ref` 최소형 / `WeakUnsubscribe` 관대 / 폐기 블록
|
||||
`archive/` / `InstanceChildHandler` 부기 / `reconcile` 배치 Blocker)과
|
||||
`H-138`(숏핸드 우선순위)·`H-139`(`New`/`drive` 파이프라인 의사코드 — 거기서
|
||||
`H-142` `Parent` 순서 발견 → props에 `Parent` **금지**로 확정)까지 반영.
|
||||
Q9는 문항 전제가 틀린 것(Tween 절 스케치 한 줄 복사 오류)으로 닫힘.
|
||||
**[2026-08-27 기준] 감사 8라운드·`/code-review high`까지 돌렸다 — 리뷰 10건 중
|
||||
여섯은 반영, 넷(`H-143`~`H-146`, 새 메커니즘)은 `question.md` 최우선 절에
|
||||
판단 대기. 남은 액션: 커밋, 그리고 그 넷의 결정.** 아래는 돌리기 전(2026-08-26) 서술:
|
||||
지시서는 `qa-request/pre-implementation-handtrace-round9-brief.md`. 스코프는
|
||||
**커밋 `9dd8213` 하나의 델타**다 — 8라운드 결정 반영과 그 뒤
|
||||
`/code-review high` **7패스의 수정이 전부 그 커밋에 들어 있고 아무도
|
||||
|
|
|
|||
|
|
@ -13,8 +13,9 @@ M2(반응형 코어 — Source/State/Store)**. **⚠️ [2026-08-24] M2와 M3의
|
|||
결정의 소스는
|
||||
`.claude/qa-request/pre-implementation-handtrace-round8-followup.md`
|
||||
(7라운드 몫은 `-round7-followup.md`; **[2026-08-27] 9라운드 몫은
|
||||
`-round9-followup.md` — Q1~Q3 반영 완료, Q4~Q10 대기**)이고 `.claude/question.md` 최우선
|
||||
절은 비어 있다. 같은 상태를 `.claude/project-context.md`도
|
||||
`-round9-followup.md` — Q1~Q10·`H-138`·`H-139`·`H-142` 전량 반영 완료**)이고 `.claude/question.md` 최우선
|
||||
절엔 **그 반영분에 `/code-review`가 낸 새 메커니즘 넷(`H-143`~`H-146`)이 판단
|
||||
대기**로 올라 있다(M2 착수 게이트는 아님). 같은 상태를 `.claude/project-context.md`도
|
||||
서술하니 마일스톤이 넘어갈 때 두 곳을 같이 고칠 것. 진행 상황의 소스는
|
||||
항상 루트 `ROADMAP.md`.
|
||||
|
||||
|
|
|
|||
79
ROADMAP.md
79
ROADMAP.md
|
|
@ -23,7 +23,9 @@ quad-v2 구현 단계 실행 계획. 설계 근거/아키텍처 자체는 여기
|
|||
> 반영분을 겹쳐 재트레이싱한 발견 17건 — 역전 없이 누락·충돌만 닫았습니다.
|
||||
> **[2026-08-27] 9라운드 몫은 `-round9-followup.md`** — Q1~Q3(`recompute` 되감기
|
||||
> 순서 / Slot 생성자의 `Offset`·`_baseObserver`·`_destroyed` / `bk.indexOfElement`로
|
||||
> 토큰 폐기)까지 반영됐고 Q4~Q10은 대기 중입니다.
|
||||
> 토큰 폐기)에 이어 같은 날 Q4~Q10·`H-138`·`H-139`·`H-142`까지 **전량**
|
||||
> 반영됐습니다(`EffectHandle` 진입점 / M2에 `Ref` 최소형 / `InstanceChildHandler`
|
||||
> 부기 / `reconcile` 배치 Blocker / `New`·`drive` 파이프라인 / props `Parent` 금지).
|
||||
> **어느 체크박스가 바뀌었는지는 여기서 세지 않습니다** — 해당 체크박스에
|
||||
> 각각 `H-1xx` 표시가 붙어 있으니 그게 소스입니다. M2/M3 양쪽에 걸쳐
|
||||
> 있습니다). M1까지의 산출물은
|
||||
|
|
@ -249,8 +251,8 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
|
||||
### 공통 기반 — 반응형보다 먼저 (구 M2, 지금의 M3에서 이동)
|
||||
|
||||
> 셋 다 State-free이자 dispatch-free라 어느 쪽에도 안 걸립니다. 이 절이
|
||||
> 끝나야 아래 반응형 본체를 짤 수 있습니다.
|
||||
> 여기 있는 것 전부 State-free이자 dispatch-free라 어느 쪽에도 안 걸립니다.
|
||||
> 이 절이 끝나야 아래 반응형 본체를 짤 수 있습니다.
|
||||
|
||||
- [ ] `Brand.luau`(**[2026-08-21 재작성]** 인스턴스 브랜드 — `Brand()`가
|
||||
브랜드마다 weak-key 집합 하나를 들고 `:register(x)`/`:is(x)`,
|
||||
|
|
@ -325,6 +327,22 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
다시 갈라짐, 판정 로직은 공유하는 비공개 헬퍼 하나 — M2 체크박스
|
||||
참고**), children 배열 leaf 부착이 실제로는 `bindLifetime` 호출이라
|
||||
이 게이트를 그대로 탐
|
||||
- [ ] **⭐ [2026-08-27 9라운드 `H-128` 신설] `Ref.luau` 최소형** — 아래
|
||||
`Effect(fn, ...deps)`의 `Ref` dep 분기(`isRef(d)` →
|
||||
`d:WeakCallback(onRefFire)` → `self._epochs:Sync(d)`)가 **M2 안에서
|
||||
실제로 돌려면** 필요한 표면만: `.Value`/`.Revision`/`:Set(value)`/
|
||||
`:WeakCallback(fn)`/`:Callback(fn)`/`:Uncallback(fn)`/`isRef` +
|
||||
`EpochBrand:register(self)`(`Epoch`를 만족하는 데 필요한 것 전부).
|
||||
`PreRef`/`PostRef`/`:Wait`/디스패치 핸들러(`(v=Ref)` 매치, `Processed*`)는
|
||||
**M8 그대로**. 근거: `Ref`가 `Epoch`인 것 자체가 M2의 결정
|
||||
(`H-58`/`H-64`/`H-70`)이고, `Effect`의 `isEpoch` 분기를 M2의 mock
|
||||
테스트가 한 번은 실제로 태워야 한다 — 그 전엔 M8 머리가 *"M2가 이미 이
|
||||
표면을 전제한다"*고 **인정만** 하고 M2 쪽엔 앞으로 참조가 없어, 구현자가
|
||||
`isRef` 스텁으로 비워두기와 M8 절반 앞당기기 중 임의로 고르게 돼 있었다.
|
||||
소스는 `base/ref-plan.md`의 "`Ref`는 `Epoch`를 만족한다" 절.
|
||||
**[2026-08-27 `/code-review`]** 아래 `H-80` 탑레벨 목록의 규칙(*"이
|
||||
마일스톤이 얹는 탑레벨 값 전부"*)대로 **`quad-types`의 `Quad`에 `Ref`
|
||||
생성자 필드도 여기서** 추가한다 — M8의 `H-25` 체크박스는 이걸로 흡수.
|
||||
|
||||
### 반응형 본체
|
||||
|
||||
|
|
@ -465,7 +483,9 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
뒤에. (`base/effect-plan.md`, **[2026-08-21 5라운드
|
||||
`C-6`]** 옛 시그니처는 `Effect(fn, state?)`) — deps 생략 시 설치
|
||||
1회+leaf 사망 시 확정 정리, deps 지정 시 **각각에 맞는 구독**
|
||||
(State/Source는 `Observer`, `Ref`는 `:Callback`)을 걸어
|
||||
(State/Source는 `Observer`, `Ref`는 `:WeakCallback` — **[2026-08-27
|
||||
9라운드 `H-129`]** 옛 `:Callback` 표기는 `H-58`이 정정한 것의 잔재로,
|
||||
강한 셋에 걸면 `Ref`가 `Effect`를 영원히 붙든다)을 걸어
|
||||
재실행+cleanup 체이닝(React `useEffect` 동형). **`EffectHandle`이
|
||||
`EpochMap`을 하나 들어** 공통 상류로 인한 중복 발화를 접고, 설치 구간
|
||||
억제 플래그가 그 `Update`보다 먼저 와야 함
|
||||
|
|
@ -572,7 +592,8 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
쪽 헬퍼다) **최소한 그 셋이 도는 형태까지는 M3(디스패치)가 요구**
|
||||
- [ ] **[2026-08-24 `H-25` 파생, 2026-08-25 `H-80`으로 목록 확장]**
|
||||
`quad-types`의 `Quad`에 **이 마일스톤이 얹는 탑레벨 값 전부** 추가 —
|
||||
`Source` / `Store` / `Effect` / `Blocker` / `Relate` /
|
||||
`Source` / `Store` / `Effect` / `Blocker` / `Relate` / **`Ref`**(최소형,
|
||||
2026-08-27 `H-128`) /
|
||||
`is*` 전량(`isState`/`isSource`/`isStore`/`isRef`/`isObserver`/
|
||||
`isEffect`/`isEpoch`/`isModifier` …) / `bindLifetime`·`unbindLifetime`·
|
||||
`canBound`·`canExecute` 4종. **⚠️ `State`는 런타임 생성자가 없다** —
|
||||
|
|
@ -658,6 +679,10 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
flattened)`(**일반화 `for` 한 번**으로 순회하며 각 `(k,v)`에
|
||||
`Dispatch.process(inst,k,v,1)` 호출 — `dispatch-core-plan.md`의 `None`
|
||||
센티널 절, 2026-08-07 여덟 번째 세션에 네이밍 확정).
|
||||
**[2026-08-27 9라운드 `H-139`]** `New` ①~④ + `drive` (a)~(c) 전체
|
||||
파이프라인 의사코드는 `base/bind-system-plan.md`의 "`New(name)(props)`
|
||||
파이프라인 의사코드" 절 — 배치 Blocker를 여닫는 자리와 빈 배열 파트
|
||||
가드도 거기.
|
||||
**[2026-08-21 구현 전 QA 4라운드 `F-4-1`] 순회는 두 패스가 아니라
|
||||
단일 일반화 `for`다** — "배열 파트 전체가 해시 파트보다 먼저"는 여전히
|
||||
base가 보장하는 **계약**이지만, `flattened`가 항상 평범한 Luau
|
||||
|
|
@ -907,8 +932,18 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
- [ ] `RobloxFactory.luau`(BaseModule 뮤테이션, 재호출 가드) — 진입점
|
||||
`QuadRoblox(Quad): QuadRoblox`가 `QuadTypes.CheckedQuad<T, Pattern>`으로
|
||||
주입받은 quad-base 버전을 확인(`base/quad-types-plan.md` 참고)
|
||||
- [ ] `D/init.luau`(제네릭 생성자 `New` + 생성기가 찍는 정적 별칭 필드 — **[2026-08-18]** 범위는 "GUI에 쓰이는 모든 인스턴스", 이벤트 필드의 콜백 타입까지 생성, `base/bind-system-plan.md`의 "인스턴스 생성 / 이벤트 네이밍 인체공학" 절)
|
||||
- [ ] `Handlers/Property.luau`, `Handlers/InstanceChild.luau`
|
||||
- [ ] `D/init.luau`(제네릭 생성자 `New` + 생성기가 찍는 정적 별칭 필드 — **[2026-08-18]** 범위는 "GUI에 쓰이는 모든 인스턴스", 이벤트 필드의 콜백 타입까지 생성, `base/bind-system-plan.md`의 "인스턴스 생성 / 이벤트 네이밍 인체공학" 절). **[2026-08-27 9라운드 `H-142`] 생성되는 props 타입에서 `Parent`를 제외할 것** — props에 `Parent`는 올 수 없다(부모가 하는 일, 같은 문서의 파이프라인 절). `New` ①~④ 순서는 그 절의 의사코드가 소스
|
||||
- [ ] `Handlers/Property.luau`(**[2026-08-27 9라운드 `H-142`]** `isHandlable`이 `"Parent"` 키를 **거부**한다 — 매치 핸들러가 없어지면 `Dispatch.process`의 "매치 핸들러 없음 → 즉시 error"에 걸리는 것으로 런타임 가드가 공짜로 생긴다, 새 메커니즘 없음. **사용자 확정은 "props에 `Parent` 금지"라는 규칙이고, 이 거부 배선은 에이전트 선택** — `base/bind-system-plan.md`의 `H-142` 항목이 그렇게 갈라 적음), `Handlers/InstanceChild.luau` —
|
||||
**⭐ [2026-08-27 9라운드 `H-134`] `InstanceChildHandler`도 말단이라
|
||||
부기를 등록한다**: `process`에서 `setOffsetSource(inst, k, None)` →
|
||||
`v.Parent = inst` → `setLength(inst, k, 1, inst)`(정적 단일 자식은 상수
|
||||
`1`, 5번째 인자 없음). 반환 클로저는 `v.Parent = nil`(내리기만, 파괴
|
||||
아님) → `setOffsetSource(inst, k, None)` → `setLength(inst, k, 0)`.
|
||||
**[2026-08-27 `/code-review` 정정]** 옛 순서(`setLength` → `Parent`)와
|
||||
"5번째 인자 `v`"는 틀렸었다 — 근거는 `base/dispatch-core-plan.md`의 그
|
||||
문단. 빠뜨리면
|
||||
`Frame { Frame{}, Slot() }`이 첫 마운트에서 죽는다 —
|
||||
`base/dispatch-core-plan.md`의 `H-39` 블록(그 다섯째 항목)이 소스.
|
||||
- [ ] **Instance 생성 시점의 gcconn/gchold 셋업**(2026-08-14 다섯 번째 세션
|
||||
확정, 옛 "`bindLifetime` 첫 호출에서 lazy 생성"에서 전환 — `base/
|
||||
lifecycle-pattern.md`의 "(0) gcconn/gchold는 Instance 생성 시점에
|
||||
|
|
@ -1262,6 +1297,13 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
`SetSiblingIndex` 또는 `LayoutOrder` 기반이면 no-op, 구현 선택)
|
||||
## M7 — Modifier
|
||||
|
||||
- [ ] **[2026-08-27 9라운드 `H-142` 후속, `/code-review`]** 생성기가 찍는
|
||||
`FrameModifier`류 메소드 목록에서도 **`Parent`를 제외**할 것 — props
|
||||
타입에서만 빼면 `Modifier():Parent(x)`가 타입을 통과하고 `flatten`이
|
||||
`Parent`를 해시 파트로 merge해 런타임에서야 죽는다. `PreRef`/`PostRef`가
|
||||
Modifier 타입으로 차단되는 것과 같은 자리(`base/bind-system-plan.md`의
|
||||
`H-142` 항목).
|
||||
|
||||
- [ ] `Modifier()`(빈 인스턴스 바닥 생성자, 2026-08-07 열 번째 세션
|
||||
명시 — `Source(default)`/`Ref(default)`/`Store({defaults})`와 같은
|
||||
`Type(args)` 팩토리 관습, `modifier-plan.md` 3번)
|
||||
|
|
@ -1319,18 +1361,17 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
|
||||
## M8 — Ref
|
||||
|
||||
- [ ] **[2026-08-24 `H-25` 파생]** `quad-types`의 `Quad`에 `Ref` 필드 추가
|
||||
(위 M3 항목의 "마일스톤마다" 규칙)
|
||||
- [ ] `Ref.luau`(`.Value` 읽기 전용 필드 + `:Set(value)`/`:Callback(fn)`/
|
||||
`:Wait(thread?)`, 전부 self 반환).
|
||||
**⭐ [2026-08-25 추가, 7라운드 `H-58`/`H-64`/`H-70`] `Ref`가 `Epoch`를
|
||||
만족하게 됐다** — 공개 필드 **`.Revision`**(`:Set()`이 `Source`와 같은
|
||||
`bit32.bnot(-rev)`로 갱신) + **`EpochBrand:register(self)`**, 그리고
|
||||
약하게 등록하는 **`:WeakCallback(fn)`**(weak-키 별도 테이블, `Weak` 쪽이
|
||||
프리미티브이고 `:Callback`이 그 위에 "GC 킵"을 얹은 것).
|
||||
**M2가 이미 이 표면을 전제한다** — `Effect` 생성자가 `Ref` dep을
|
||||
`self._epochs:Sync(d)`로 `EpochMap`에 태운다(`base/effect-plan.md`).
|
||||
`base/ref-plan.md`의 "`Ref`는 `Epoch`를 만족한다" 절이 소스 + `PreRef.luau`/`PostRef.luau`(별도 파일, Ref
|
||||
- [x] ~~**[2026-08-24 `H-25` 파생]** `quad-types`의 `Quad`에 `Ref` 필드 추가~~
|
||||
— **[2026-08-27 `H-128` 후속]** `Ref` 최소형과 함께 M2 공통 기반으로
|
||||
이동(그 체크박스). `PreRef`/`PostRef` 필드는 아래 항목이 얹는다
|
||||
- [ ] `Ref.luau`의 **나머지** — `:Wait(thread?)`(self 반환). **[2026-08-27
|
||||
9라운드 `H-128`] 최소형은 M2 "공통 기반" 절로 앞당겨졌다**(표면 목록은
|
||||
그 체크박스가 소스 — 여기 반복하지 않는다) — `Ref`가 `Epoch`를 만족한다는 2026-08-25 확정
|
||||
(7라운드 `H-58`/`H-64`/`H-70`: `.Revision`은 `:Set()`이 `Source`와 같은
|
||||
`bit32.bnot(-rev)`로 갱신, `Weak` 쪽이 프리미티브이고 `:Callback`이 그
|
||||
위에 "GC 킵"을 얹은 것, `Effect` 생성자가 `self._epochs:Sync(d)`로 태움)은
|
||||
그 표면의 일부라 M2 몫이다 — 세부는 체크박스에 재서술하지 않고
|
||||
`base/ref-plan.md`의 "`Ref`는 `Epoch`를 만족한다" 절이 소스. + `PreRef.luau`/`PostRef.luau`(별도 파일, Ref
|
||||
런타임 재사용 + children 배열 전용, Modifier/Store 타입 차단,
|
||||
위치 무관 호이스팅 pre-pass — `base/ref-plan.md` "`phase`
|
||||
옵션 폐기 → 위치로 표현, `PreRef` 신설" 절 + "API 모양" 절)
|
||||
|
|
|
|||
Loading…
Reference in a new issue