--!strict --[[ `Quad` 공개 타입 계약 — 구현 없음, 런타임 값은 없다시피 함(빈 테이블). `quad-base`가 이 타입을 구현하고, `quad-roblox`/향후 백엔드·플러그인 패키지는 무거운 `quad-base` 전체 대신 이 패키지만 의존한다. **왜 별도 패키지인가**: `QuadRoblox(Quad): QuadRoblox`처럼 quad-base 인스턴스를 **런타임에 주입받는** 패키지는, quad-base를 pesde `[dependencies]`로 선언할 필요가 없다 — 실제 값은 호출자가 넘겨준다. 그런데 `dev_dependencies`로만 선언하면, quad-roblox가 게시된 뒤 다른 사람이 그 패키지를 설치할 땐 dev dependency가 전파되지 않아 그 require(타입만 쓰려는 목적이어도 런타임에 실행됨, 실측 확인됨)가 그 자리에서 못 찾고 크래시한다. 그래서 "항상 안전하게 실 의존성으로 둘 수 있을 만큼 작은 것"이 따로 필요하고, 그게 이 패키지다. (`.claude/base/project-setup-plan.md`/`session/2026-08-19-07-*.md` 참고) ]] local TypeVersionCheck = require("./luau_packages/type_version_check") -- `Epoch` — 최소 인터페이스(`.claude/base/state-epoch-plan.md` §2). 판정은 -- identity + "직전과 다른 Revision"뿐, 순서 비교 없음. 런타임 판별은 `isEpoch`. export type Epoch = { Revision: number } export type EpochSet = { [Epoch]: true } -- 집합이지 배열이 아니다(게이트 배치가 그대로 넘어온다) -- `Relate` — inst를 weak 키로 하는 릴레이션(`.claude/base/relate-plan.md` "API"). -- `inst`는 항상 weak, `Weak`/`Strong`은 value의 보관 방식만 가리킨다. export type Relate = { SetStrong: (self: Relate, inst: any, key: any, value: any) -> (), GetStrong: (self: Relate, inst: any, key: any) -> any?, SetWeak: (self: Relate, inst: any, key: any, value: any) -> (), GetWeak: (self: Relate, inst: any, key: any) -> any?, } -- `Ref` 최소형(`.claude/base/ref-plan.md`, M2 `H-128`). 콜백은 `fn(value, 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, Revision: number, Callbacks: { [RefCallback | thread]: true }, -- 강한 해시맵 셋(+ M8 `:Wait` 대기자) WeakCallbacks: { [RefCallback]: true }, -- weak-key 테이블 Set: (self: Ref, value: T) -> Ref, Callback: (self: Ref, fn: RefCallback) -> Ref, WeakCallback: (self: Ref, fn: RefCallback) -> Ref, Uncallback: (self: Ref, fn: RefCallback) -> Ref, } -- ── 반응형 코어(M2 단위 2) ──────────────────────────────────────────── -- 선언 스타일은 `.claude/base/typing-limits.md` §1②의 "데이터부/메소드부" 쪼개기 — -- 콜백 파라미터 무주석 추론이 사는 유일한 스타일(`audit/handtrace-round10-reference-impl/ -- spikes/ty11_store_final.luau`가 최종형 실측). `Compute`의 `-> State` 반환은 §1①의 -- 알려진 한계라 **파생 State를 만드는 자리는 결과 타입을 명시 주석**할 것. -- `fn(self, previous?, ...deps)` — 전부 lazy 핸들, 값은 `:Get()`으로만 -- (`.claude/base/source-state-plan.md` "self 인자도 lazy 핸들로 통일"). export type StateData = { Get: (self: StateData) -> T } -- `Observer` — 값을 안 실어주는 leaf 구독(`source-state-plan.md` "`state:Observer(fn)`"). -- `fn(targetState, self, emitFrom?)`: 3번째가 출처(`Epoch` | 집합), 설치·캐치업 발화는 `nil`. -- 네 진입점은 `lifecycle-pattern.md` (2) — Weak가 프리미티브, `.Subscribed`는 강·약 공용. export type Observer = { Subscribed: boolean, Subscribe: (self: Observer) -> Observer, WeakSubscribe: (self: Observer) -> Observer, Unsubscribe: (self: Observer) -> Observer, WeakUnsubscribe: (self: Observer) -> Observer, } export type ObserverFn = (targetState: StateData, self: Observer, emitFrom: (Epoch | EpochSet)?) -> () -- `EffectHandle` — `Effect(fn, ...deps)`의 핸들(`effect-plan.md`). `fn(self) -> ...cleanup`. -- 네 진입점은 Observer와 같은 이름·같은 게이트이되 **본문은 자기 것**(`H-144`). export type EffectHandle = { Subscribed: boolean, Rerun: (self: EffectHandle) -> EffectHandle, Subscribe: (self: EffectHandle) -> EffectHandle, WeakSubscribe: (self: EffectHandle) -> EffectHandle, Unsubscribe: (self: EffectHandle) -> EffectHandle, WeakUnsubscribe: (self: EffectHandle) -> EffectHandle, } export type EffectFn = (self: EffectHandle) -> ...(() -> ()) -- `state:Gate(setup)`의 정책 계약(`gate-plan.md` 2번): 바깥은 생성 시 1회, 반환 클로저는 상류 -- emit마다. `emit()`/`emit(true)` = 흡수 집합 flush, `emit(false)` = 버리기, 반환 = 쌓인 게 있었나. export type GateEmit = (commit: boolean?) -> boolean export type GateSetup = (emit: GateEmit) -> () -> () -- `Blocker` — `GateNode` 위의 정책(`blocker-plan.md`). `state:Apply(blocker)`가 `__apply`로 배선. export type Blocker = { IsBlocked: boolean, IsOn: (self: Blocker) -> boolean, On: (self: Blocker) -> Blocker, Off: (self: Blocker) -> Blocker, OffWithoutEmit: (self: Blocker) -> Blocker, Policy: (self: Blocker, emit: GateEmit) -> () -> (), -- `state:Apply(b)`는 gated `State`(같은 T)를 돌려준다 — 호출부가 결과 타입을 명시 -- (`State.Apply`의 객체 오버로드 주석 참고). __apply: (self: any, state: any) -> any, } export type State = StateData & { -- deps는 `...any`다 — 타입팩 `D...`로 위치 인자를 좁히는 형태는 strict에서 콜백 dep -- 추론이 `{read Get: ...}`로 뒤틀려 정상 호출까지 막힌다(M2 단위 2 실측, 스파이크 15가 -- "미검증"으로 남겨둔 자리). 콜백 안에서 dep 파라미터에 주석을 달 것. Compute: (self: StateData, fn: (self: StateData, previous: U?, ...any) -> U, ...any) -> State, With: (self: StateData, ...any) -> State, -- 애플리커티브 팩토리는 함수이거나 메소드형 `__apply`를 가진 객체(`H-94`/`H-158`). -- **교집합 오버로드**로 선언한다(M2 단위 4 실측, `luau-test/done/26-*`): 유니온 하나로 -- 두면 `Blocker`처럼 필드가 더 있는 객체가 제네릭 `U` 자리에서 너비 서브타이핑을 못 -- 받아 `state:Apply(blocker)`가 strict에서 막힌다. 객체 쪽은 `any`를 돌려주므로 결과는 -- `local g: State = s:Apply(b)`로 명시(§1①의 파생 State 명시 주석 관례와 같다). Apply: ((self: StateData, factory: (State) -> U) -> U) & ((self: StateData, factory: { __apply: (self: any, state: any) -> any }) -> any), -- 등록 즉시 1회 실행. 무인자면 "항상 관측" 유틸. Observer: (self: StateData, fn: ObserverFn?) -> Observer, -- 전파 경로에 GateNode 하나(값은 안 가린다, 통지만 유보). 같은 T. Gate: (self: StateData, setup: GateSetup) -> State, } -- `Source`는 `State`를 구조적으로 만족하고 동시에 `Epoch`다(다중 태깅). export type Source = State & { Revision: number, Set: (self: Source, v: T) -> Source, Emit: (self: Source) -> Source, } -- 예약 키 진단 — `keyof`(키 싱글톤 유니온)만 받는다(`H-112`). `error()`는 못 쓴다 -- (타입 함수 자체가 실패로 판정) — `print` + `types.never`. 목록은 quad-base -- `Store.luau`의 `RESERVED` 테이블의 사본이라 이름을 바꿀 땐 둘을 같이. -- `.claude/base/typing-limits.md` §0: 타입 함수는 **진단까지만**. export type function CheckReservedKeys(keys: type) local list = if keys:is("union") then keys:components() else { keys } for _, k in list do if k:is("singleton") then local v = k:value() if v == "Of" or v == "Names" or v == "__reservedCheck" then print(`quad.Store: "{v}" is a reserved key`) return types.never end end end return types.singleton(true) end -- `Store` — `T`는 `{hp: Source, …}` 그대로(평범한 레코드, 타입 함수 없음). -- `__reservedCheck`는 팬텀(런타임 nil, 읽지 말 것, `Names()`에 안 들어감). export type Store = T & { Of: (self: any, name: string) -> Source, Names: (self: any) -> { string }, __reservedCheck: CheckReservedKeys>, } export type Quad = { Version: "0.0.0", -- quad-base/pesde.toml의 version과 항상 맞출 것 debug: boolean, New: () -> Quad, RunInit: (self: Quad, initFn: (Quad) -> any) -> (), AddPlugin: (self: Self, pluginFn: (Self) -> P) -> Self & P, -- M2 공통 기반이 얹는 탑레벨 값(`ROADMAP.md` M2 `H-80`). `Source`/`Store`/ -- `Effect`/`Blocker`는 각 단위에서 추가된다. Relate: () -> Relate, Void: (...any) -> (), Ref: (default: T) -> Ref, -- 단위 2. `State`는 런타임 생성자가 없다(파생 전용) — 타입만 위에서 export. Source: (v: T) -> Source, Store: (defaults: T?) -> Store, -- 단위 3. deps는 State/Source/Ref(`H-70`), `fn`엔 안 넘어간다. Effect: (fn: EffectFn, ...any) -> EffectHandle, -- 단위 4. Blocker: () -> Blocker, -- 생명주기 4종 — quad-base는 인터페이스(에러 스텁)만, 백엔드가 주입 -- (`.claude/base/lifecycle-pattern.md`). `canBound(v) == not canExecute(v)`. bindLifetime: (inst: any, value: any) -> (), unbindLifetime: (value: any) -> (), canBound: (value: any) -> boolean, canExecute: (value: any) -> boolean, -- 주입 엔진 op(`architecture.md` EngineOps) — `Effect._bindDestroying`이 부른다. 미주입이면 에러 스텁. onDestroying: (inst: any, fn: () -> ()) -> { Connected: boolean, Disconnect: (self: any) -> () }, -- 브랜드 술어(`.claude/base/brand-plan.md`). 나머지(`isTag`/`isAttribute*`/ -- `isTween`/`isSlot`)는 그 타입의 마일스톤에서. isEpoch: (x: any) -> boolean, isSource: (x: any) -> boolean, isState: (x: any) -> boolean, isStore: (x: any) -> boolean, isObserver: (x: any) -> boolean, isEffect: (x: any) -> boolean, isBlocker: (x: any) -> boolean, isModifier: (x: any) -> boolean, isRef: (x: any) -> boolean, isPreRef: (x: any) -> boolean, isPostRef: (x: any) -> boolean, } --[[ `CheckedQuad` — 주입된 값의 실제 타입 `T`가 `Pattern` (글롭/캐럿 문자열, `type-version-check` 패키지 참고 — `"*"`/`"N^"`/정확값)에 맞는 `Version`을 갖는지 컴파일 타임에 확인. quad-base/quad-roblox 자신은 항상 같은 모노레포에서 같이 개발되므로 지금은 정확히 일치하는 `"0.0.0"` 패턴으로만 쓰지만(아래 사용법), quad-spring-roblox류 **독립적으로 게시되는 백엔드 플러그인**은 `"0.*.*"`처럼 느슨한 패턴을 직접 골라 쓸 수 있다 — 그래서 패턴을 이 타입의 파라미터로 열어둠. **⚠️ 반드시 "가상 필드"로만 쓸 것 — `T`를 직접 패스스루하지 말 것.** `TypeVersionCheck.CheckVersion` 자체가 이미 이 규칙을 지키게 설계돼 있다(트리비얼한 `true`만 반환, `T`를 절대 참조/재구성 안 함) — `type function`을 거친 값은 패스스루라도 이후 `AddPlugin` 같은 제네릭 self 메소드 체이닝이 조용히 깨진다는 게 실측 확인됐기 때문(`typing-limits.md` §6, `type-version-check/src/init.luau` 참고). **사용법(필수 — 이 필드를 실제로 참조해야 체크가 평가된다)**: ```lua local checked: QuadTypes.CheckedQuad = injectedQuad :: any local _ = checked.__versionCheck -- ⚠️ 이 줄이 없으면 체크가 조용히 스킵됨(lazy 평가) -- 이후 checked는 injectedQuad와 완전히 같은 타입(원본 그대로) — -- AddPlugin 체이닝 등 뒤이은 제네릭 연산이 전부 안전하게 동작함 ``` ]] export type CheckedQuad = T & { __versionCheck: TypeVersionCheck.CheckVersion, Pattern> } return {}