diff --git a/.claude/base/architecture.md b/.claude/base/architecture.md index 718d4e1..3781581 100644 --- a/.claude/base/architecture.md +++ b/.claude/base/architecture.md @@ -286,8 +286,9 @@ quad/ │ │ ├── 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`) │ ├── Void.luau # **[2026-08-28 `H-162`]** `return function() end` 한 줄 — 단일 no-op. 의존 없는 잎(`None`/`Brand`/`Relate`와 같은 급), `Dispatch/*`·핸들러·최상위 `init.luau`가 require +│ ├── Brand.luau # **[2026-08-28 M2 첫 단위]** `Brand()` 생성자 + **브랜드 인스턴스 전부**(`EpochBrand`를 `Source`/`Ref`/`GateNode`가 공유하므로 타입 모듈마다 두면 순환 require) + `is*` 술어(타입이 생길 때 그 술어를 여기 추가, 최상위 `init.luau`가 재export). 의존 없는 잎(`base/brand-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`) +│ ├── LifetimeHandle.luau # `bindLifetime(inst,value)`/`unbindLifetime(value)`/`canBound(value)`/`canExecute(value)` 탑레벨 함수 "인터페이스"(타입/계약만) — **[2026-08-28 M2 첫 단위]** `InitLifetimeHandle(module)`이 모듈 인스턴스에 영어 `level 2` 에러 스텁 4종을 설치하고 백엔드가 덮어쓴다, `Relate`는 안 쓴다(그건 아래 quad-roblox 실 구현 몫 — `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 래퍼 없음 │ ├── 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 아홉 번째 세션 확정) diff --git a/.claude/qa-request/pre-implementation-handtrace-round11.md b/.claude/qa-request/pre-implementation-handtrace-round11.md index ecdbc02..05495c9 100644 --- a/.claude/qa-request/pre-implementation-handtrace-round11.md +++ b/.claude/qa-request/pre-implementation-handtrace-round11.md @@ -16,6 +16,7 @@ |---|---|---|---|---|---| | `H-165` | ① | 1 | 🟡 | `quad-types`에 `export type`을 더하면 pesde shim이 그걸 모른다 — `pesde install` 재실행 없이는 `QuadTypes.Ref`가 Unknown type | ✅ 반영(`project-setup-plan.md`) | | `H-166` | ① | 1 | 🟢 | `Ref.Revision` 초기값을 어느 문서도 안 정했다 | ✅ 반영(`ref-plan.md`: `0`) | +| `H-167` | ① | 1 | 🟡 | 옮기며 `Ref(default: T?)`/`.Value: T?`로 바꿔 놓았다 — 문서는 `Ref(T)` 단일 파라미터, nil은 `Ref<>(nil)`로 | ✅ 코드를 문서에 맞춤(감사 2라운드) | ## 상세 @@ -43,6 +44,16 @@ - **무엇이**: 갱신식(`bit32.bnot(-rev)`)과 표만 있고 시작값이 없다. - **처리**: `0`으로 구현하고 그 절에 한 줄 추가. 계약이 `==`/`~=`뿐이라 값은 무관. +### `H-167` 🟡 — 구현이 `Ref` 시그니처를 `T?`로 바꿔 놓았다 (자기 실수) + +- **어디서**: `quad-types/src/init.luau`·`quad-base/src/Ref.luau` vs `base/ref-plan.md` + "제네릭 시그니처(2026-08-07 확정)". +- **무엇이**: 문서는 `Ref(T) -> Ref`, `.Value: T` — nil이 올 수 있는 자리는 호출자가 + `Ref<>(nil)`로 넓힌다(그 문서의 언바인딩 절도 같은 전제). 옮기면서 `Ref()` 관용구를 + 타입에서 받으려고 `default: T?`/`Value: T?`로 적었는데 그러면 `Ref(5).Value`까지 + nil 검사를 강요한다 — 문서가 이미 기각한 모양. 감사 2라운드가 발견. +- **처리**: 코드를 문서대로 되돌림. 테스트의 `Ref()`는 `Ref<>(nil)`로. + ## §4 ⭐ 사용자 결정이 필요한 것 (배치 회신용) | 문항 | 무엇 | 선택지 | 권고 | 권고 근거 | 옛 메커니즘 복원? | diff --git a/quad-base/src/Ref.luau b/quad-base/src/Ref.luau index 147253e..6a3c2b0 100644 --- a/quad-base/src/Ref.luau +++ b/quad-base/src/Ref.luau @@ -15,6 +15,9 @@ - `.claude/base/state-epoch-plan.md` §2: revision bump is the uint32 wrap-around decrement `bit32.bnot(-rev)`; only `==`/`~=` are contract. + Single type parameter, `Ref(T) -> Ref` (`ref-plan.md` "제네릭 + 시그니처"): a nil-able slot is widened by the caller, `Ref<>(nil)`. + Callback signature is `fn(value, ref)` — the second argument is the Ref itself, i.e. the `Epoch` a consumer (`Effect`) feeds to `EpochMap:Update`. Plain user callbacks just ignore it. @@ -37,7 +40,7 @@ local WEAK_KEY_MT = { __mode = "k" } local RefImpl = {} RefImpl.__index = RefImpl -function RefImpl.Set(self: Ref, value: T?): Ref +function RefImpl.Set(self: Ref, value: T): Ref self.Value = value -- (1) settle the value first self.Revision = bit32.bnot(-self.Revision) -- (2) before callbacks (`H-108`) @@ -98,7 +101,7 @@ function RefImpl.Uncallback(self: Ref, fn: Callback): Ref return self end -local function Ref(default: T?): Ref +local function Ref(default: T): Ref local self = setmetatable({ Value = default, Revision = 0, diff --git a/quad-base/test/mock.luau b/quad-base/test/mock.luau index 3904c80..15abe14 100644 --- a/quad-base/test/mock.luau +++ b/quad-base/test/mock.luau @@ -7,9 +7,11 @@ 않으므로 mock이 실제 Roblox 충실도를 가질 이유가 없다(같은 절). Vide의 mock과 달리 GC-독립 userdata 프록시 트릭을 안 씀 — 그건 Roblox - 엔진 userdata의 GC 동일성을 흉내내려는 것이고, 그 동일성 문제는 - quad-roblox의 LifetimeHandle(gcconn 트릭)이 다루는 자리라 quad-base - 정적 스냅샷 테스트 범위 밖. + 엔진 userdata가 Lua 참조와 무관하게 회수·재생성되는 **엔진 GC 동일성**을 + 흉내내려는 것이고, 그건 quad-roblox의 `nativeClaim`이 실기기에서 다루는 + 자리라 mock 범위 밖. 반면 **생명주기 계약 로직**(`bindLifetime`/`canBound`/ + `canExecute`/`unbindLifetime`이 gcconn의 `.Connected`로 판정하는 것)은 + mock 시그널 위에서 그대로 재현한다 — 아래 `installLifetime`. **[2026-08-28 M2, ROADMAP `H-97`] `installLifetime(quad)`** — 생명주기 4종 (`bindLifetime`/`unbindLifetime`/`canBound`/`canExecute`)의 mock 백엔드. diff --git a/quad-base/test/spec.ref.luau b/quad-base/test/spec.ref.luau index c29213f..e36cc56 100644 --- a/quad-base/test/spec.ref.luau +++ b/quad-base/test/spec.ref.luau @@ -19,8 +19,8 @@ do assert(not Quad.isPreRef(r) and not Quad.isPostRef(r) and not Quad.isState(r), "plain Ref only") assert((getmetatable(r.WeakCallbacks :: any) :: any).__mode == "k", "WeakCallbacks is weak-keyed") assert(getmetatable(r.Callbacks) == nil, "Callbacks is a plain strong set") - local empty = Ref() - assert(empty.Value == nil, "Ref() has no value") + local empty = Ref(nil :: number?) -- nil이 올 수 있는 자리는 호출자가 `T?`로 넓힌다(`ref-plan.md` "제네릭 시그니처") + assert(empty.Value == nil, "Ref(nil) has no value") print("PASS") end @@ -45,7 +45,7 @@ end print() print("=== 3. 리비전 — bit32.bnot(-rev) 랩어라운드 감소: 0 → 4294967295 → 4294967294, 매번 다름 ===") do - local r = Ref() + local r = Ref(nil :: number?) r:Set(1) assert(r.Revision == 4294967295, "0 wraps to 4294967295, got " .. r.Revision) r:Set(2) @@ -62,7 +62,7 @@ end print() print("=== 4. 즉시 1회 호출 — nil/미설정이어도 그 상태 그대로 (H-120), Weak도 동일 ===") do - local r = Ref() + local r = Ref(nil :: number?) local calls = 0 local weakCalls = 0 r:Callback(function(value) diff --git a/quad-types/src/init.luau b/quad-types/src/init.luau index 8608239..926aa23 100644 --- a/quad-types/src/init.luau +++ b/quad-types/src/init.luau @@ -31,14 +31,16 @@ export type Relate = { } -- `Ref` 최소형(`.claude/base/ref-plan.md`, M2 `H-128`). 콜백은 `fn(value, ref)` — --- 두 번째 인자가 Ref 자신(= `Epoch`). `:Wait`는 M8. -export type RefCallback = (value: T?, ref: Ref) -> () +-- 두 번째 인자가 Ref 자신(= `Epoch`). `:Wait`는 M8. **단일 타입 파라미터 +-- `Ref(T)`** — nil이 들어올 수 있는 자리는 호출자가 `Ref<>(nil)`로 넓힌다 +-- (`ref-plan.md` "제네릭 시그니처"). +export type RefCallback = (value: T, ref: Ref) -> () export type Ref = { - Value: T?, + Value: T, Revision: number, Callbacks: { [RefCallback | thread]: true }, -- 강한 해시맵 셋(+ M8 `:Wait` 대기자) WeakCallbacks: { [RefCallback]: true }, -- weak-key 테이블 - Set: (self: Ref, value: T?) -> Ref, + Set: (self: Ref, value: T) -> Ref, Callback: (self: Ref, fn: RefCallback) -> Ref, WeakCallback: (self: Ref, fn: RefCallback) -> Ref, Uncallback: (self: Ref, fn: RefCallback) -> Ref, @@ -55,7 +57,7 @@ export type Quad = { -- `Effect`/`Blocker`는 각 단위에서 추가된다. Relate: () -> Relate, Void: (...any) -> (), - Ref: (default: T?) -> Ref, + Ref: (default: T) -> Ref, -- 생명주기 4종 — quad-base는 인터페이스(에러 스텁)만, 백엔드가 주입 -- (`.claude/base/lifecycle-pattern.md`). `canBound(v) == not canExecute(v)`.