quad/ROADMAP.md
qwreey 8169b90a0e
decide(base): canBound 이름 확정, Compute previous 방어/스코핑 명확화, Modifier 핸들러값·State<Modifier> UB→error 전환
- Bound 플래그 → canBound(handle) 탑레벨 함수로 확정(canExecute와 같은 결)
- :Compute의 previous 인자 오버엔지니어링 의심 기각, 결과 노드 귀속으로 스코핑 명확화(self.Cache 안 씀 — 팬아웃 충돌 회피)
- Modifier 필드의 Ref/PreRef/Observer/Effect/Slot/Modifier 값: UB → isX predicate 기반 즉시 error
- State/Source가 Modifier를 값으로 담는 것(State<Modifier>)도 동일하게 error로 통일, Slot 등은 계속 허용
- Tween initValue/useTween 논의 신설(미확정)
- pre-implementation-audit.md 3-1/2-2/문서모순 절 해소 반영, question.md/ROADMAP.md 동기화

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYfAz3BsaTrmMM8hnzn6mj
2026-08-09 16:00:19 +09:00

19 KiB

ROADMAP.md

quad-v2 구현 단계 실행 계획. 설계 근거/아키텍처 자체는 여기 안 옮겨적음 — .claude/base/가 소스, 여긴 순서와 진행 상황만. 마일스톤 시작할 때 체크박스를 세분화해서 늘려도 되고, 끝나면 체크만 하면 됨 — 살아있는 문서.

2026-08-04 세션에 준비만 해둔 상태로 신설, 이후 여러 세션에 걸쳐 설계가 확정될 때마다 각 마일스톤 체크박스가 계속 갱신돼왔음 — 그래도 아직 M0 자체는 시작 안 함. 다음 세션은 바로 M0부터.

M0 — 스켈레톤 + 기술검증 (스파이크, "진짜" 마일스톤 아님)

최종 소스 트리를 그대로 만들기 전에, 지금까지 추론만으로 확정하고 실제 Luau 코드로 부딪혀본 적 없는 세 가지를 던지는 코드로 검증하는 단계 — .claude/base/ 감사에서 나온 결론(2026-08-04). 여기서 뭔가 어긋나면 architecture.md/bind-system-plan.md 등을 이 시점에 고치는 게 정상 — 실패가 아니라 이 단계의 목적.

  • Store/State push-invalidate → pull-recompute propagation을 실제로 짜보기(다이아몬드 의존성 케이스 포함 — 이미 invalid면 전파 중단되는지)
  • Source가 State를 구조적으로 만족하는 제네릭 타입(:Compute<U>(self: Source<T>, ...) -> State<U>류, self 타이핑 + State 참조 혼합)이 Luau 솔버에서 안전하게 추론되는지 확인(2026-08-06 후속 세션, base/store-semantics.md "Source가 State를 만족함" 절 — State<T>Source를 참조하지 않는 단방향 의존으로 두면 위험한 상호 재귀는 피할 수 있어 보이나 실제 검증 전엔 확정 아님)
  • process/retract 재귀 재-process 디스패치를 실제로 짜보기(store-bind 핸들러 하나 + isHandlable 우선순위 스캔 포함)
  • props 순회의 "배열 파트 먼저, 해시 파트 나중" 두 패스 계약이 실제 Luau 테이블에서 관찰한 대로 동작하는지 확인, PreRef pre-pass + 일반 Ref의 위치 기반 순서까지 최소 스파이크로 검증 (2026-08-07 세 번째 세션, base/bind-system-plan.md "phase 옵션 폐기 → 위치로 표현, PreRef 신설" 절) — PreRef pre-pass의 소진은 nil이 아니라 None으로(2026-08-07 열 번째 세션 정정, 사용자가 Luau REPL로 반례 제시 — 키가 듬성듬성해지면 순회가 index 순서를 전혀 안 지킴), 이 경로는 nil-hole 위험이 아예 없도록 설계됐으므로 "구멍 있는 테이블 순회" 자체를 검증할 필요는 없어짐(같은 절 "왜 nil이 아니라 None인가" 참고)
  • props.Modifier/props.Ref named-parameter로 받는 컴포넌트 하나 작성, export type Params = {...}로 타입 체크되는지 확인 (component-composition-plan.md 최종 결론 1번) — props.Modifier or None/props.Ref or None 관용구(2026-08-07 열 번째 세션 확정, component-composition-plan.md "필수 관용구" 절)로 nil-hole을 막는 케이스를 반드시 포함할 것 — caller가 Modifier/Ref를 안 넘겨도 or None이 항상 non-nil을 보장하므로 {nil, ref, child}류 리터럴 구멍 자체가 안 생김(research/pre-implementation-audit.md 1-5). M0에서 검증할 것은 "어떻게 막을지"가 아니라 이 관용구가 실제로 타입 체크/런타임 양쪽에서 문제없이 동작하는지
  • 위 과정에서 소스 트리/메커니즘 문서에 고칠 부분이 생기면 그 자리에서 .claude/base/ 갱신

통과 기준: 세 개 다 Luau에서 자연스럽게 짜이는 게 확인되면 M1 진행. 안 되면 여기서 관련 base/ 문서부터 고치고 재시도.

M1 — 실제 스캐폴딩

  • quad-base/, quad-roblox/ 폴더 + 각 wally.toml
  • 루트 default.project.json, .luaurc(architecture.md "구현 착수: 소스 트리 구조 확정" 절 그대로)
  • quad-base용 최소 mock 테스트 하네스(Vide test/mock.luau 선례, 순수 luau CLI, architecture.md "테스트 전략" 절 참고)
  • 이 시점부터 .claude/qa-request//.claude/archive/ 폴더 실사용 시작

M2 — 디스패치 엔진

  • Dispatch/init.luauDispatch.getHandler(inst,k,v): Handler?(순수 스캔, isHandlable+priority) / Dispatch.process(inst,k,v)(오케 스트레이터: getHandler → 이전 담당자와 다르면 그 retract → 새 핸들러의 .process) / Dispatch.addHandler(handler)(레지스트리 등록, quad-roblox가 팩토리 뮤테이션 시점에 호출) / Dispatch.drive(inst, flattened)(배열→해시 두 패스 순회하며 각 (k,v)process 호출 — bind-system-plan.mdNone 센티널 절, 2026-08-07 여덟 번째 세션에 네이밍 확정). "이 키를 지금 누가 담당 중인가" bookkeeping은 Dispatch.drive가 아니라 Dispatch.process 호출 자체 내부에서 갱신할 것(재귀 재-process 시에도 자연히 갱신되게 — 안 그러면 재귀 재디스패치를 쓰는 케이스(Tween store-bind, NoneHandler)에서 매 사이클 불필요한 retract가 반복 호출될 위험)
  • Handler.luau(핸들러 계약 타입: isHandlable(inst,k,v)/priority/ process/retractisHandlableinst를 받도록 확정, 2026-08-07 여덟 번째 세션 정정)
  • Brand.luau(공유 weak-key 레지스트리, Brand.set(x,tag)/ Brand.get(x)isState뿐 아니라 isObserver/isEffect/isTag/ isAttribute/isTween/isBlocker/isSource/isStore/isSlot/ isRef/isPreRef/isModifier(2026-08-07 열 번째 세션 추가 — 원래 태그 목록에서 빠져있었음, isRef/isPreRef는 단순 항등이지 isState처럼 집합 멤버십 아님) 전부의 기반. isNone만 예외로 레지스트리 없이 x == None 항등 비교 — bind-system-plan.mdBrand 절, 2026-08-07 여덟 번째 세션 신설)
  • Relate.luau(전체가 quad-base, 순수 Lua — base/relate-plan.md) — Relate() 비싱글톤 생성자, :SetWeak/:GetWeak/:SetStrong/:GetStrong. inst(첫 인자)는 항상 weak, StrongMap/WeakMap 서브테이블은 lazy 생성(첫 Set 호출 시에만), WeakMap은 공유 메타테이블({__mode="v"}) 재사용 — 구 base.perInstanceState(inst)/PerInstanceState.luau를 대체(2026-08-08 세션 신설).
  • LifetimeHandle.luau 인터페이스만(bindLifetime(inst,value)/ canExecute(inst,value) 탑레벨 함수 타입 계약, 실 구현 없음 — quad-roblox 실 구현은 M8) — 원래 M8에만 있었으나 M4(StoreBind의 Connected 확인)/M6(Slot의 canExecute)이 이미 이 인터페이스를 전제로 서술돼 있어 로드맵 순서가 역전돼 있었음(pre-implementation-audit.md 우선순위1-9, question.md 2번 — 2026-08-07 세 번째 세션에 반영). canExecute(inst, value) -> boolean으로 재확정(2026-08-08 세션, (handle) 단일 인자 서술을 대체) — Observer/Effect는 자기 Subscribed 상태를 먼저 확인, 그 다음 inst의 공유 gcconn(Relate로 저장)의 .Connected를 봄. bindLifetime/canExecute 둘 다 네임스페이스 없이 탑레벨 함수로 export(Dispatch.xxx류 시스템 네임싱과 구분, isState/isObserver와 같은 1급 프리미티브 취급) — base/ lifecycle-pattern.md의 "bindLifetime/canExecute — 확정" 절 참고
  • 핸들러 계약 검증: retract 필드가 없는 핸들러를 등록하면 리뷰/린트에서 걸러내기(no-op이라도 필드 자체는 항상 정의 — Dispatch.process가 핸들러 교체 시 nil 체크 없이 호출, base/bind-system-plan.md "핸들러 계약" 절, 2026-08-08 세션)
  • Dispatch/Leaf.luau(i:number, v=Ref/Observer/PreRef) children-array leaf 매칭 Handler, StoreBind.luau와 같은 층위(범용/엔진무관) — quad-base 소속으로 확정(2026-08-08 두 번째 세션, base/ bind-system-plan.md "Dispatch는 프리미티브가 아니다" 절)
  • chains(Relate 기반, {[inst(weak)]={[k]={handler,handler,...} (strong 순서 배열)}}) + Dispatch.retractUnder(inst,k,keep,v) — 재귀 재-dispatch(StoreBind/Tween/NoneHandler)의 retract를 다단 체인까지 정확히 전파(2026-08-08 세 번째 세션, base/ bind-system-plan.md "Dispatch 체인" 절 — pre-implementation-audit.md 1-2번 "이전 핸들러 추적" 항목 해소). Dispatch.process가 매치될 때마다 체인에 push하는 것도 이 항목에 포함
  • mock 대상 테스트

M3 — Store/State/Source

  • Source.luau/State.luau/Store.luau
  • store.key dot-access 타입 추론 확인
  • Blocker.luau(base/blocker-plan.md 참고 — 여러 Source를 한꺼번에 바꿔도 파생값 재계산/재대입이 한 번만 되게 하는 primitive, State와 밀접히 연관돼 있어 같은 마일스톤에서 개발)
  • state:Apply(factory)(base/bind-system-plan.md "state:Apply(factory)" 절, 2026-08-07 일곱 번째 세션) — factory(self)를 체이닝 문법으로 부르는 순수 설탕, factory: (State<T>) -> U): U로 열린 타입. Source도 기존 :With/:Compute 델리게이션에 얹혀 자동 포함
  • state:Observer(fn) — children 배열 leaf 참가자, 등록 즉시 1회 실행 확정(base/bind-system-plan.md의 Observer 절), isObserver 판별자, canExecute 게이팅, :Subscribe()/:Unsubscribe()
  • Effect(fn, state?)(base/effect-plan.md) — state 생략 시 설치 1회+leaf 사망 시 확정 정리, state 지정 시 내부적으로 state:Observer(...)를 조합해 재실행+cleanup 체이닝(React useEffect 동형). Observer 구현 이후에 착수(의존 관계). EffectHandle:Subscribe()/:Unsubscribe()도 추가(leaf 없이 쓰는 모듈/스크립트 레벨 Effect) — :Unsubscribe()는 Observer와 달리 마지막 cleanup을 1회 트리거해야 함(2026-08-07 일곱 번째 세션)
  • Observer/Effect 이중 바인딩 금지 — Bound(가칭) 플래그로 leaf 부착과 :Subscribe()가 동시에 걸리면 즉시 error(base/bind-system-plan.md "이중 바인딩 금지" 절, 2026-08-07 일곱 번째 세션)
  • mock 대상 테스트

M4 — 첫 end-to-end 반응형 업데이트

  • Dispatch/StoreBind.luau(재귀 재실행 로직, 엔진 무관 — 재-dispatch 전 Dispatch.retractUnder(inst,k,self,realv) 호출 필수, base/ bind-system-plan.md "Dispatch 체인" 절)
  • mock 대상으로 "store 값 바꾸면 process가 다시 호출된다" + "이전 값이 다른 타입이면 이전 핸들러의 retract가 정확히 불린다" 확인

M5 — quad-roblox 최소 프로바이더

  • RobloxFactory.luau(BaseModule 뮤테이션, 재호출 가드)
  • DI/init.luau(제네릭 생성자 + ~25개 정적 필드)
  • Handlers/Property.luau, Handlers/InstanceChild.luau
  • 실제 Roblox에서 첫 Frame{...} 렌더 확인 — Studio 작업이라 HUMAN_TODO.md 1번(계정 분리) 먼저 되어야 진행 가능, SAFETY.md 준수

M6 — Slot

  • "여러 Slot이 형제로 섞일 때 순서 보장" 열린 질문 확인(slot-plan.md) — Roblox 단일 백엔드로는 급하지 않으면 스킵하고 진행 가능
  • base Dispatch/Slot.luau(추상 재조정) + quad-roblox Handlers/Slot.luau (실제 Parent 조작)

M7 — Modifier

  • Modifier()(빈 인스턴스 바닥 생성자, 2026-08-07 열 번째 세션 명시 — Source(default)/Ref(default)/Store({defaults})와 같은 Type(args) 팩토리 관습, modifier-plan.md 3번)
  • flatten-before-dispatch(isModifier(v)로 배열 항목 중 Modifier만 판별해 필드 merge, 나머지는 안 건드리고 통과 — 2026-08-07 열 번째 세션 명시, modifier-plan.md 1번), immutable table.clone 체이닝
  • Modifier.Overridden(mod1, mod2, ...)(이름 확정, 구 MergeOverride, 2026-08-08 세션) — 필드별 raw 덮어쓰기, 특별한 State/함수 분기 불필요(modifier-plan.md 9번)
  • Overridden가 서브타입 관계인 서로 다른 Modifier 타입(예: FrameModifier/ GuiObjectModifier)을 섞을 때의 타입 시그니처 실 Luau 테스트 (modifier-plan.md 9-2번, 미검증 — 안 되면 일단 Overridden(...: any): any로 느슨하게 열어두고 이 항목으로 되돌아올 것)
  • State<Modifier> 조합에 isModifier 기반 명시적 error 적용 (modifier-plan.md 7번, 2026-08-09 세션 확정) — 타입 차단은 되면 좋은 보너스로 선택 검증(필수 아님)
  • :Apply(factory) 팩토리 함수 체이닝(modifier-plan.md 8번, 예약 키 Apply가 제네릭 __index 필드 setter와 안 겹치는지 확인)
  • :Peek<<T>>(key): T|State<T>|nil 필드 읽기 접근자 + isState(x)/isSource(x): boolean(Brand 공유 레지스트리 기반 — modifier-plan.md 9번, bind-system-plan.mdBrand 절, M2의 Brand.luau에 이미 구현돼 있어야 함)
  • 인라인 키/setter로 modifier 필드를 명시적으로 지우는 None(가칭) 센티널(modifier-plan.md 2-1번, Peek 반환 타입에 None 추가) + 이를 nil로 재디스패치하는 base 내장 NoneHandler (bind-system-plan.mdNone 센티널 절, M2 dispatch 엔진의 "이전 매치 핸들러 추적" 항목과 함께 구현 — Tween store-bind 핸들러와 동일한 재귀 재디스패치 패턴이라 새 메커니즘 아님) — 확정 완료

M8 — Ref

  • Ref.luau(.Value 읽기 전용 필드 + :Set(value)/:Callback(fn)/ :Wait(thread?), 전부 self 반환) + PreRef.luau(별도 파일, Ref 런타임 재사용 + children 배열 전용, Modifier/Store 타입 차단, 위치 무관 호이스팅 pre-pass — base/bind-system-plan.md "phase 옵션 폐기 → 위치로 표현, PreRef 신설" 절 + "API 모양" 절)
  • (v=Ref) 매치 핸들러 — children 배열의 숫자 슬롯에 놓인 Ref(default) 인스턴스를 인식해 바인드(별도 CreatedRef 래퍼 없음 — 이름 자체가 폐기됨, 아래 참고)
  • PreRef pre-pass — 새 Dispatch.* 함수 없이 Dispatch.drive(inst, flattened) 자신이 두 패스(배열→해시) 루프 전에 배열 파트를 훑어 PreRef 항목만 fire(Dispatch.process/getHandler 우회하는 raw 루프, flatten 함수에는 얹지 않음 — 재바인드 시 flatten 재호출 가능성과 충돌하므로 기각). 복수 PreRef는 배열 index 순서 그대로(별도 규칙 없음). fire된 슬롯은 그 자리에서 소진(None 처리, nil 아님 — 2026-08-07 열 번째 세션 정정)해 이어지는 정상 두 패스에 다시 노출 안 되게 함 — base/bind-system-plan.md "PreRef" 절
  • PreRef 동적 경로 가드 Handler — {isHandlable = v is PreRef, process = error(...)} 형태로 정상 우선순위 레지스트리에 등록, NoneHandler와 같은 "한 값 종류 전담" 패턴. 리터럴 배열 경로는 pre-pass가 이미 소진시키므로 이 Handler가 매치되면 곧 타입 차단을 우회한 버그라는 뜻 — 같은 절 참고
  • Ref 콜백/대기자 실행 루프(type(v)=="thread"coroutine.resume(v, self)+None으로 소진(nil 아님 — 2026-08-07 열 번째 세션 정정, #t/table.insert 안전성), 함수면 v(value) 호출+유지 — 같은 배열 하나로 통합). :Wait(thread?)threadnil이면 coroutine.running() 캡처+yield, 있으면 등록만 하고 즉시 self 반환(남의 thread를 여기서 대신 정지시킬 수 없어서)
  • LifetimeHandle quad-roblox 실제 구현 — bindLifetime/canExecute 본체(GetPropertyChangedSignal("ClassName") 연결 트릭으로 gcconn 확보, Relate:SetStrong으로 gcconn/gchold 저장 — 인터페이스 자체는 M2로 이동됨, Relate 자체는 quad-base라 quad-roblox 쪽 재구현 없음)

M9 — 컴포넌트 합성 레이어

  • 플레인 함수 컴포넌트 관례 문서화/예제
  • props.Modifier/props.Ref 전달 관례를 정식 컴포넌트로 검증(M0 스파이크를 정식화)

M10 — Event / Attribute / Tag

  • Handlers/Event.luau(ReflectionService 기반 자동 판별)
  • Handlers/Attribute.luau(base/attribute-plan.md — 메커니즘/None/ retract 불필요 확정, 타입 파라미터화 이름만 착수 전 확인)
  • Tag.luau(quad-base — 값 타입+immutable clone 체이닝: Tag(...)/ :Added/:Removed/:Contains/:Apply/Merged, base/tag-plan.md — 2026-08-08 세 번째 세션 array-part 값 객체로 재설계, 구 해시 파트 모델은 archive/tag-hash-key-model-reversed.md)
  • Handlers/Tag.luau(quad-roblox — CollectionService process/retract 글루만, isHandlableisTag(v). retract는 이제 의미 있음(값이 Tag가 아니게 되면 전체 삭제), 같은 Tag끼리 바뀌는 diff는 process가 자기 Relate 저장분과 비교해서 처리 — 전체 삭제 후 재생성 금지(랙 유발), base/tag-plan.md 참고)

M11 — Tween

  • research/tween-plan.md 남은 옵션 이름 확정(구조는 이미 확정)
  • Handlers/Tween.luau(높은 우선순위 store-bind 핸들러, 기본 오버라이드 Cancel)

특정 마일스톤에 안 묶이고 병행 가능

  • 용어 정리 스윕 — State/DI/Slot 등(PerInstanceStateRelate로 대체·해소됨) — .claude/question.md 1번, 최종 이름 확정되는 대로 아무 시점에나
  • 각 마일스톤 완료 시 .claude/qa-request//.claude/archive/에 기록, 필요하면 CLAUDE.md "최근 세션 요약"도 갱신

백로그 (스코프 밖 — 필요성이 실제로 드러나면 그때 설계)

  • research/existing-instance-bind-plan.md — Modifier 정적 flatten과 긴장 관계 있음, 재검토 시 그 문서부터 다시 볼 것
  • 범용 렌더 디버깅 도구로서의 quad-mock(Tween mock 등 동적 동작 포함, M1의 quad-base 테스트용 mock과는 별개)
  • quad-debug/quad-debug-roblox-plugin — 실물 Instance→코드 위치 역추적 Studio 플러그인(research/debug-tooling-plan.md). 위 quad-mock과 목적이 다름(오프라인 검증 vs 실시간 라이브 관찰) — 단 trace 이벤트 스키마를 공유할 여지는 있음, 그 문서 참고. M2/M3/M5 구현 시 훅 확장 지점만 고려해두면 이 항목 자체는 지금 착수 불필요.
  • v1 마이그레이션 가이드 + objectListClass.__newIndex 오타 기능 재현 테스트
  • Slot 형제 순서 보장(다중 백엔드 관점) — Roblox만이면 급하지 않음