- Modifier 필드를 인라인 키/setter로 명시적으로 지우는 `None` 센티널 확정 — merge는 안 바뀌고, 디스패치 쪽 NoneHandler가 Tween store-bind와 같은 재귀 재디스패치로 처리(base 드라이버/개별 핸들러 시그니처 불변). - Dispatch.getHandler/process/addHandler/drive로 오케스트레이터 이름 공식화, isHandlable에 inst 추가, canExecute 시그니처를 (handle)->boolean 으로 정정(zero-arg 클로저 폐기). - isState를 Brand 공유 레지스트리로 일반화해 isObserver/isSource/isTag 등 10종 판별자로 확장, isSource 별도 필요하다고 정정. - Tag/Attribute retract 불필요함을 확인, 전용 문서(tag-plan.md/ attribute-plan.md) 신설. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
13 KiB
13 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 테이블에서 관찰한 대로 동작하는지 확인,
PreRefpre-pass + 일반Ref/CreatedRef의 위치 기반 순서까지 최소 스파이크로 검증 (2026-08-07 세 번째 세션,base/bind-system-plan.md"phase옵션 폐기 → 위치로 표현,PreRef신설" 절) props.Modifier/props.Refnamed-parameter로 받는 컴포넌트 하나 작성,export type Params = {...}로 타입 체크되는지 확인 (component-composition-plan.md최종 결론 1번)- 위 과정에서 소스 트리/메커니즘 문서에 고칠 부분이 생기면 그 자리에서
.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선례, 순수luauCLI,architecture.md"테스트 전략" 절 참고) - 이 시점부터
.claude/qa-request//.claude/archive/폴더 실사용 시작
M2 — 디스패치 엔진
Dispatch/init.luau—Dispatch.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.md의None센티널 절, 2026-08-07 여덟 번째 세션에 네이밍 확정). "이 키를 지금 누가 담당 중인가" bookkeeping은Dispatch.drive가 아니라Dispatch.process호출 자체 내부에서 갱신할 것(재귀 재-process 시에도 자연히 갱신되게 — 안 그러면 재귀 재디스패치를 쓰는 케이스(Tween store-bind,NoneHandler)에서 매 사이클 불필요한retract가 반복 호출될 위험)Handler.luau(핸들러 계약 타입:isHandlable(inst,k,v)/priority/process/retract—isHandlable도inst를 받도록 확정, 2026-08-07 여덟 번째 세션 정정)Brand.luau(공유 weak-key 레지스트리,Brand.set(x,tag)/Brand.get(x)—isState뿐 아니라isObserver/isEffect/isTag/isAttribute/isTween/isBlocker/isSource/isStore/isSlot전부의 기반.isNone만 예외로 레지스트리 없이x == None항등 비교 —bind-system-plan.md의Brand절, 2026-08-07 여덟 번째 세션 신설)LifetimeHandle.luau/PerInstanceState.luau인터페이스만(타입 계약, 실 구현 없음 — quad-roblox 실 구현은 M8) — 원래 M8에만 있었으나 M4(StoreBind의Connected확인)/M6(Slot의canExecute)이 이미 이 인터페이스를 전제로 서술돼 있어 로드맵 순서가 역전돼 있었음(pre-implementation-audit.md우선순위1-9,question.md2번 — 2026-08-07 세 번째 세션에 반영).canExecute는(handle: LifetimeHandle) -> boolean으로 확정(바인딩마다 클로저 만드는 zero-arg가 아니라, quad-roblox가 한 번만 주입하는 공유 함수 — "base 유틸은 인터페이스, 백엔드가 주입" 패턴과 맞춰야 해서.base/lifecycle-pattern.md의 gchold 스케치 절, 2026-08-07 여덟 번째 세션 정정)- mock 대상 테스트
M3 — Store/State/Source
Source.luau/State.luau/Store.luaustore.keydot-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 체이닝(ReactuseEffect동형). 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(재귀 재실행 로직, 엔진 무관)- mock 대상으로 "store 값 바꾸면
process가 다시 호출된다" 확인
M5 — quad-roblox 최소 프로바이더
RobloxFactory.luau(BaseModule 뮤테이션, 재호출 가드)DI/init.luau(제네릭 생성자 + ~25개 정적 필드)Handlers/Property.luau,Handlers/InstanceChild.luau- 실제 Roblox에서 첫
Frame{...}렌더 확인 — Studio 작업이라HUMAN_TODO.md1번(계정 분리) 먼저 되어야 진행 가능,SAFETY.md준수
M6 — Slot
- "여러 Slot이 형제로 섞일 때 순서 보장" 열린 질문 확인(
slot-plan.md) — Roblox 단일 백엔드로는 급하지 않으면 스킵하고 진행 가능 - base
Dispatch/Slot.luau(추상 재조정) + quad-robloxHandlers/Slot.luau(실제 Parent 조작)
M7 — Modifier
- flatten-before-dispatch, immutable
table.clone체이닝 Modifier.Override(mod1, mod2, ...)(가칭, 구Merge) — 필드별 raw 덮어쓰기, 특별한 State/함수 분기 불필요(modifier-plan.md9번)Override가 서브타입 관계인 서로 다른 Modifier 타입(예:FrameModifier/GuiObjectModifier)을 섞을 때의 타입 시그니처 실 Luau 테스트 (modifier-plan.md9-2번, 미검증 — 안 되면 일단Override(...: any): any로 느슨하게 열어두고 이 항목으로 되돌아올 것)State<Modifier>조합 타입 차단 확인(modifier-plan.md7번, UB 확정):Apply(factory)팩토리 함수 체이닝(modifier-plan.md8번, 예약 키Apply가 제네릭__index필드 setter와 안 겹치는지 확인):Peek<<T>>(key): T|State<T>|nil필드 읽기 접근자 +isState(x)/isSource(x): boolean(Brand공유 레지스트리 기반 —modifier-plan.md9번,bind-system-plan.md의Brand절, M2의Brand.luau에 이미 구현돼 있어야 함)- 인라인 키/setter로 modifier 필드를 명시적으로 지우는
None(가칭) 센티널(modifier-plan.md2-1번,Peek반환 타입에None추가) + 이를nil로 재디스패치하는 base 내장NoneHandler(bind-system-plan.md의None센티널 절, 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 모양" 절)CreatedRef메커니즘(숫자 슬롯 참가자)- Ref 콜백/대기자 실행 루프(
type(v)=="thread"면coroutine.resume(v, self)+소진, 함수면v(value)호출+유지 — 같은 배열 하나로 통합).:Wait(thread?)는thread가nil이면coroutine.running()캡처+yield, 있으면 등록만 하고 즉시self반환(남의 thread를 여기서 대신 정지시킬 수 없어서) LifetimeHandlequad-roblox 실제 구현(Instance 생존 확인, 인터페이스 자체는 M2로 이동됨)PerInstanceStatequad-roblox 실제 구현(weak-keyed table, 인터페이스 자체는 M2로 이동됨)
M9 — 컴포넌트 합성 레이어
- 플레인 함수 컴포넌트 관례 문서화/예제
props.Modifier/props.Ref전달 관례를 정식 컴포넌트로 검증(M0 스파이크를 정식화)
M10 — Event / Attribute / Tag
Handlers/Event.luau(ReflectionService기반 자동 판별)Handlers/Attribute.luau(base/attribute-plan.md— 메커니즘/None/retract불필요 확정, 타입 파라미터화 이름만 착수 전 확인)Handlers/Tag.luau(CollectionService,base/tag-plan.md— 전부 확정)
M11 — Tween
research/tween-plan.md남은 옵션 이름 확정(구조는 이미 확정)Handlers/Tween.luau(높은 우선순위 store-bind 핸들러, 기본 오버라이드 Cancel)
특정 마일스톤에 안 묶이고 병행 가능
- 용어 정리 스윕 —
State/DI/PerInstanceState/Slot등 (.claude/question.md1번), 최종 이름 확정되는 대로 아무 시점에나 - 각 마일스톤 완료 시
.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만이면 급하지 않음