직전 커밋에서 New()를 전부 Quad()로 치환했던 게 사용자 의도를 잘못 읽은 것이었음 — 사용자가 직접 바로잡음. 실제 설계: Quad(require의 반환값)는 이미 만들어진 기본 싱글톤 인스턴스이고, 그 안의 New 필드를 명시적으로 호출해야만 별도의 새 Quad 네임스페이스가 생긴다. "그냥 Quad()를 부르면 매번 새 인스턴스"였다면, 컴포넌트를 여러 모듈로 쪼갠 앱에서 각자 인스턴스를 "얻는" 것 자체가 "새로 만들기"가 되어 서로 다른 인스턴스가 생기는 사고로 이어졌을 것. architecture.md/dispatch-core-plan.md/bind-system-plan.md/ module-lifecycle-plan.md의 New() 서술을 되돌리고, qa-request round1의 A-3 절에 후속 정정 문단을 추가했다. quad-doc-auditor로 두 라운드 감사해 새 발견 0건까지 수렴시켰다. Co-authored-by: qwreey <me@qwreey.moe>
87 KiB
구현 전 QA 1라운드 — 사용자 심사에서 "아니오"가 나온 항목
상태: 1라운드 완료 + base/ 반영 완료(2026-08-18). 이 파일은
.claude/qa-request/에 라운드별로 쌓인다 — 2라운드는 새 파일
(pre-implementation-qa-round2.md)로 만들 것이고, 이 문서를 이어 쓰지 않는다.
2라운드 대상은 맨 아래 "진행 로그" 절의 "아직 안 본 것" 목록.
1라운드 범위(2026-08-18 세션에 시작). .claude/base/ 확정 문서를 의존성
순서로 훑으며 표면 타입계약 / 내부 구현 메커니즘 / 동작 원리·불변식 세
층위로 "예가 나와야 정상인 주장"을 사용자에게 확인받는 작업의 산출물.
이 문서의 용도: 사용자가 "아니오" 또는 "부분적으로 틀림"으로 판정한
항목만 모은다. [2026-08-18 갱신] 반영 완료 — 이 문서는 이제 "무엇이 왜
틀렸었나"의 근거 기록이고, 지금 유효한 설계는 항상 base/가 소스다.
같은 날 세션에서 아래 항목 전부를 base/(+ROADMAP.md/question.md/
archive/)에 반영했고, 반영 과정에서 판단이 갈리던 네 건(SL-3의 PopOnly,
D-7의 재역전 여부, N-4의 NoneHandler/NilHandler 역할 분담, ST-2의 동적
키 경로)은 그 자리에서 사용자에게 물어 확정했다 — 그 답변도 각 항목에
반영돼 있다.
아직 안 닫힌 것(결론 전에 해당 마일스톤 착수 금지)은 question.md 3번과
.claude/todos.md 00번이 소스 — 여기서 다시 나열하지 않는다.
표기: X-N의 X는 문서 코드(A=architecture, S=source-state, …),
N은 그 문서 안 질문 순번. 사용자 답변 원문은 그대로 인용한다(conventions.md의
"사용자 발언을 근거로 인용할 때" 관례).
진행 현황의 소스는 이 문서 맨 아래 "진행 로그" 절 — 어느 문서까지 심사했는지는 거기가 소스이고, 다른 곳에 개수를 적지 않는다.
먼저 볼 것 — 파급이 큰 순서
목록 전체를 순서대로 읽기 전에, 다른 항목의 전제가 되거나 코드가 반대로 도는 것부터 보는 게 효율적이다. 아래 분류는 심사한 에이전트의 판단이고 확정이 아니다 — 회신 시 우선순위가 다르면 그대로 알려주면 된다.
1. 구현하면 반대로 도는 것
S-1—canBound게이트 호출부 반전. 지금 문서대로 짜면 정상 첫 바인드가 전부 에러나고 이중 바인드는 통과한다.lifecycle-pattern.md가 진원지이고source-state-plan.md/ref-plan.md가 전부 이걸 인용한다.RE-1— gcconn/gchold를SetStrong으로 적은 두 곳. 그대로 구현하면 같은 문서가 경고하는 두-Relate상호 강참조 누수에 정확히 걸린다.
2. 설계 자체가 바뀌는 것
RF-4+N-4—drive의None스킵 제거,NoneHandler가k=number를 직접 처리,NilHandler신설.D-6의 열린 갭이 여기서 닫힌다.EV-1— 이벤트 disconnect 센티널false→None/nil.BS-2— "이벤트 콜백은 타입 검증 못 한다"가 거짓.onchange-plan.md의 근거까지 같이 무너진다.R-1—Ref의 내부 구조(.Callbacks분리 +.Value를 평범한 필드로).SL-3—:Listreconcile의nil리턴을 파괴로 되돌릴지 +PopOnly.
3. 아직 답이 없는 것 (착수 전 결론 필요)
S-12— 중간 State가 GC되는지 미검증. M3 착수 전 실측 필요.AT-1—Frame{a, a}(같은 그룹 객체 이중 배치)를 UB로 둘지 error로 잡을지.D-7— base Fallback Handler 등록 주체가 다시 뒤집힐 가능성.
4. 이름·표면 확정 (같이 처리하면 효율적)
N-8—DI→D리네임 확정(2026-08-18,question.md1순위였던 항목). 라이브 문서 19개 파일에 걸쳐 있고, 헤딩 1개와 그걸 절 인용하는 곳 1개가 짝으로 묶여 있어 한쪽만 고치면doc-check.pyERROR가 난다. 반영 대상 전수 목록이 그 항목에 표로 들어있다.N-9—New커링 +D는 전량 코드 생성 (2026-08-18에 열린 항목 전부 확정됨). N-8과 같은 줄들을 건드리므로 한 번에 처리할 것(architecture.md소스 트리 주석,ROADMAP.md체크박스).BS-2와는 같은 문제의 양면이라 (인덱싱으로는 이벤트 콜백 타입이 안 나온다는 것) 반드시 묶어서 볼 것. 부수적으로bind-system-plan.md의 PA님 시그니처 인용과 "알려진/모르는 타입을 전부 커버" 서술 두 곳이 더 이상 정확하지 않게 됨.
5. 나머지 — A-3, ST-2, D-1, D-5, M-3, R-3, B-1, SL-1,
E-2, 그리고 N-1~N-7(신규 요구사항).
A. base/architecture.md
A-3 — New() 도입 시 "자동으로 테이블별 스코핑"된다는 서술
- 판정: 부분적으로 틀림 (사용자 답변: "이해가 잘 안가나, …")
- 문서 위치:
base/architecture.md의 "확정된 결정" 13번 항목 (base/dispatch-core-plan.md의 "Dispatch는 프리미티브가 아니다" 절도 같은 주장을 근거로 인용함 — 같이 확인 필요). - 문서가 주장하는 것:
New()가 생기면 매번 새BaseModule테이블을 만들어 팩토리로 채우는 것뿐이고, 그러면 Dispatch 핸들러 레지스트리와_initializedBy마커 등 지금 module-level state로 사는 모든 것이 자동으로 테이블별 스코핑된다. - 사용자 답변 원문:
이해가 잘 안가나, 모듈이 하나의 인스턴스(dispatch 레지스트리 하나, canExecute 등 계약 필드 하나) 만 가지고 있다면 예. 단, 나중에 모듈이 여러 인스턴스를 지원해, quad-roblox, quad-mock 등을 같이 굴리고 싶어지는 시점이 올 때는 v1 처럼 Init() 수행 가능하도록 두는데, require 를 감싸지는 않고 단순히 InitModule(module) 등을 받도록 각 코드들을 약간 고쳐서 이것을 해결함. Quad() 하면 새로운 quad 가 나오는 식. 단 지금은 단순 싱글톤으로 구현해 Quad() 안하고 Quad.Dispatch 접근 가능.
- 어긋나는 지점: 문서는 "코드 변경 없이 자동으로 스코핑됨"을 주장하지만,
사용자는 "각 코드들을 약간 고쳐서" 해결한다고 봄 — 즉 module-level
state를 참조하는 코드들이
InitModule(module)같은 걸 받도록 손을 대야 한다는 것. 또 미래 API 이름도 문서의New()가 아니라Quad()로 언급됨(현재는 싱글톤이라Quad()없이Quad.Dispatch직접 접근). - 파급: 실제 코드 배치에 영향 — module-level upvalue로 레지스트리를 잡아두면 나중 다중 인스턴스화 때 전면 수정이 되므로, 지금부터 그 참조 형태를 정해둘지 여부가 M0 스캐폴딩 결정이 됨.
- ⚠️ [후속 정정, 2026-08-19] 위 "어긋나는 지점"의 이름 해석이 부정확했음
—
New()를 통째로Quad()로 바꾸는 건 사용자 의도가 아니었다. 이 판정을 그대로base/architecture.md에 반영했다가(2026-08-18) 여러 파일에New()→Quad()전면 치환이 퍼졌었는데, 사용자가 직접 바로잡음 — "New 와 Quad 가 이름 모순은 아닐꺼야... Quad 는 기본적으로 생성된걸 리턴하긴 하는데, Quad.New() 도 제공하는거 어떻냐는거였음, 즉 New() 는 존재하고 기본 리턴은 New() 해서 주는건데, 리턴 안에 New 필드가 있고 그 함수를 쓰면 하나의 새로운 Quad 네임스페이스가 만들어지는식." 실제 의미:Quad(require의 반환값)는 이미 만들어진 기본 인스턴스이고,New()는 그 안에 있는 명시적 opt-in 필드(호출하면 별도의 새 네임스페이스 생성) — "그냥Quad()를 부르면 매번 새 인스턴스"가 아님. 컴포넌트를 여러 모듈로 쪼갠 앱에서 각자Quad()를 불러 인스턴스를 "얻어야" 하는 모델이면 실수로 서로 다른 인스턴스가 생겨버리는 게 진짜 문제였음. 지금 유효한 서술은base/architecture.md"확정된 결정" 13번의 재정정이 소스.
S. base/source-state-plan.md
S-1 — canBound 게이트 호출부가 뒤집혀 있음 ⚠️ 파급 큼
- 판정: 아니오 — 호출부가 잘못됨(이름
canBound는 맞음). - 문서 위치:
base/source-state-plan.md의 "이중 바인딩 금지" 절의 게이트 스케치와, 같은 문서 "bindLifetime이 이 게이트의 두 번째" 절의bindLifetime의사코드.base/lifecycle-pattern.md의 "canBoundvscanExecute" 절이 이 판정의 소스라고 선언돼 있으므로 거기가 진짜 진원지 — 정정 시 그 문서부터 볼 것. - 문서가 주장하는 것:
즉if canBound(self) then error("이미 :Subscribe()로 전역 바인딩된 값" / "이미 다른 Instance에 바인딩된 값") endcanBound == true가 "이미 살아있게 묶여 있다 = 더 못 묶는다"는 뜻으로 쓰이고 있음. - 사용자 판정: 이름이 아니라 호출부가 잘못됐다 —
canBound는 이름 그대로 "지금 묶을 수 있는가"(true = 묶어도 됨)여야 하고, 게이트는if not canBound(value) then error(...) end형태가 돼야 한다. - 연쇄로 같이 무너지는 서술 (정정 시 반드시 함께 볼 것):
- "
canBound와canExecute는 판정 로직이 같아서 값이 항상 같다" —base/source-state-plan.md"이중 바인딩 금지" 절과base/lifecycle-pattern.md가 둘 다 "비공개 헬퍼isBoundAlive를 그대로 부른다"고 서술함. 그런데canExecute는 true=살아있음이고canBound는 true=아직 안 묶임이므로, 올바른 관계는 동치가 아니라 부정 (canBound(v) == not isBoundAlive(v))이다. "값은 항상 같지만 호출부의 질문이 달라 이름만 분리했다"는 근거 서술 자체가 성립하지 않게 됨 — 실제로는 반대 방향을 묻는 두 predicate임. bindLifetime의사코드의if canBound(value) then error("이미 바인딩된 값") end— 같은 이유로 뒤집혀 있음. 이 줄엔[정정, 2026-08-14 열두 번째 세션]배너까지 붙어 "게이트는 canBound가 맞다"고 못박아 뒀는데, 정작 방향이 틀린 채로 확정된 것.- "죽은 바인딩의 재사용은 허용 —
canBound가 거짓이라 게이트를 통과함" — 방향이 뒤집히면 이 문장도 뒤집혀야 함(죽은 바인딩은canBound가 참이라 통과).
- "
- 파급: M8(
Ref/PreRef/PostRef)과 M3(Observer/Effect) 양쪽의 진입 게이트가 전부 이 predicate를 쓴다. 지금 문서대로 구현하면 정상적인 첫 바인드가 전부 에러나고 이중 바인드는 무사통과하는, 정확히 반대로 도는 게이트가 된다. - 정정해야 할 정확한 위치 (
base/lifecycle-pattern.md확인 후 확정):canBound의 구현 자체 —(1)코드 블록:function canBound(value) return isBoundAlive(value) -- ← 이름대로면 not isBoundAlive(value) 여야 함 endcanExecute는return isBoundAlive(value)가 맞으므로, 둘은 서로의 부정이 된다. 같은 블록의 주석("어느 쪽 진입점에서 물어도 항상 같은 값이라")과(3)절의 "오늘 두 문맥의 판정값은 우연히 같다"도 함께 틀리게 됨.bindLifetime의 가드 —if canBound(value) then error(...)((1)코드 블록).Observer:Subscribe()의 가드 —if canBound(self) then error(...)((2)코드 블록).(3)절 끝의 "죽은 바인딩 재사용" 서술 — "canBound가 거짓이라 게이트를 통과함" → 방향이 뒤집히면 "참이라 통과함"이 됨.base/source-state-plan.md의 "이중 바인딩 금지" 절 게이트 스케치와bindLifetime의사코드(앞서 적은 것).base/ref-plan.md의 "이중 배치 방지" 절 — 같은 게이트를 쓴다고 선언돼 있으므로 같은 방향 오류가 있는지 확인 필요.
- ⚠️ 함께 재검토할 것: 이 정정이 들어가면
(3)절이 두 이름을 나눈 근거였던 *"판정 로직은 하나(isBoundAlive)를 공유하고 값도 항상 같다"*가 깨진다. 부정 관계라면 오히려 이름 분리의 명분은 더 강해지지만, 그 절의 서술은 통째로 다시 써야 함.
S-12 — 중간 State가 GC되지 않고 살아남는지 미검증 ⚠️ 미해결
- 판정: 결론 보류 — 사용자가 검증 필요로 지목(틀렸다고 확정한 게 아니라, 문서가 이 케이스를 다룬 적이 없음).
- 문서 위치:
base/source-state-plan.md의 "state:Observer(fn)" 절 (구독자를 weak로 담는 근거 서술), 같은 문서 "왜 State 체인을 Modifier처럼 플래튼하지 않는가" 절("각 노드가 자기 구독자 목록 + 자기 캐시만 가지면 된다"),base/lifecycle-pattern.md(gchold/gcconn). - 사용자 답변 원문:
확인해봐야 하는게 State -> State -> State -> Observer Leaf Bind 에서 중간 State 는 참조되지 않아도 사라지지 않음이 명확해야함. 물론 compute 등의 callback 상 가져서 안전할 수 있지만, With 등이 있는 경우 parent 와 연결된 상대를 자기 자신에 가지고 있어야 할것임. 이게 되는지는 더 알아봐야할 필요가 있음.
- 문제의 구조: 확정된 두 서술을 겹치면 체인이 끊길 수 있음 —
- State는 자기 구독자(하류)를 weak로 담는다(S-12에서 확인된 서술).
- Observer는
gchold(leaf) 또는 전역 레지스트리가 살려준다. - 그러면
A → B → C → Observer체인에서 중간 노드B/C를 강하게 붙잡는 주체가 문서 어디에도 명시돼 있지 않다. 아무도B/C를 로컬 변수로 안 들고 있으면(체이닝 한 줄로 쓰는 흔한 형태), 하류 weak 링크만 으로는 생존이 보장되지 않아 중간 State가 수거되고 전파가 조용히 끊길 수 있음.
- 사용자가 지목한 해법 방향: 각 노드가 자기 parent(상류)를 강참조로
들고 있어야 한다 —
:Compute는 콜백 클로저가 상류를 캡처해 우연히 안전할 수 있지만,:With가 만드는 pass-through 노드는 계산 함수가 없어서 그런 우연한 캡처가 없다. 즉 "구독 엣지는 하류로 weak, 상류로 strong"이라는 방향성이 명시돼야 함. - 해야 할 일: (a)
base/source-state-plan.md에 이 방향성(상류 strong / 하류 weak)을 불변식으로 명문화할지 결정, (b)luau-test에 실측 스파이크 추가(07이 연쇄 GC를 이미 다루므로 그 옆에). 미검증 상태로 M3에 착수하면 안 되는 항목.
ST. base/store-plan.md
ST-2 — store "key" 문자열 커링은 폐기됐는데 문서엔 "동적 키 폴백으로 유지"로 남아 있음
- 판정: 아니오 — 틀림. (사용자 재확인 완료)
- 사용자 답변 원문:
store "a" 식으로 문자열 호출하는것 또한 기각된 바임. 저러면 "a" 가 string 으로 들어가서, Source 의 타입을 모르기도 하고, 우린 더이상 필요하지 않게 된 요소임.
- 문서가 주장하는 것: dot-access를 1급으로 확정하면서
store "key"는 폐기가 아니라 동적 키용 미타입 폴백(Source<any>)으로 격하해 유지한다. - 실제로 맞는 것: 문자열 커링 호출 자체가 기각. 근거는 (a)
"a"가 그냥string으로 들어가Source<T>의T를 알 수 없고, (b) dot-access +type function타이핑(ST-4)이 자리잡아 더 이상 필요 없는 요소가 됨. - 정정해야 할 위치(grep 결과 — 전수):
base/store-plan.md:119— "…store "key"(문자열 커링)로state<T>를 오버로드 함수 타입으로 정확히 추론하려는 시도는 포기하고…"base/store-plan.md:128— "store "key"문자열 커링은 동적 키가 필요할 때 쓰는 미타입(Source<any>) 폴백으로 격하."base/store-plan.md"남는 것" 문단 — "myStore "key"(문자열 커링)는 … 그대로 유지"base/bind-system-plan.md:157— "store "key"(문자열 커링, 동적 키 폴백)는 이벤트와 달리 …"base/source-state-plan.md:101— "동적 키 폴백(store "key")은 이제State<any>가 아니라Source<any>를 반환"base/source-state-plan.md:465— 예시 코드 자체가 폐기된 문법으로 쓰여 있음:store "key1":With(store "key2"):Compute(...). 이건 서술이 아니라 예제라 그대로 베껴 쓰일 위험이 큼 —store.key1:With(store.key2)로 고쳐야 함.
- 파급: 폐기 사실이 반영되면 "동적 키를 어떻게 다루는가"라는 질문이 다시 열린다 — dot-access만 남으면 런타임에 키 이름이 정해지는 경우의 정식 경로가 없음. ST-1 답변("타입 시간에 Source가 없는 것으로 나와 타입 에러만 나면 됨")과 합치면 "동적 키는 아예 지원 안 함"이 의도로 보이나, 명시적으로 확인받지는 않았음.
D. base/dispatch-core-plan.md
D-1 — "isX(hint) 방어 가드는 죽은 코드"는 과장이다 (한 핸들러가 여러 값 모양을 받는 경우 여전히 필요)
- 판정: 큰 틀은 맞으나 일반화가 과함 — 사용자가 단서를 붙임.
- 사용자 답변 원문:
맞습니다. 더 정확히 표현하자면, State 에 대해 process 하면 후행 처리가 생길텐데, state 의 새로운 값이 나오면 get 을 통해 얻어진 것으로 다시 process 가 수행됩니다. 여기서 이전의 프로세스 슬롯/인덱스와 비교해서 정확히 자신이 만들어낸 것이라면, 단순 retract 처리가 발생하고, 아니라면 retractUnder 로 전부 제거되어 nil 이 됩니다. 즉 process 가 retract 를 담당한다가 성립하므로 위는 옳은 표현입니다. 다만, 처음부터 한 핸들러가 여러 값을 가질 수 있어 is 처리가 필요한건, 그 핸들러의 몫입니다.
- 문서가 (너무 강하게) 주장하는 것 — 두 군데:
- "핸들러 계약" 절: "방어 가드를 남겨둬도 무해하지만 죽은 코드이고, 반대로 그 가드가 있어야만 정확한 코드는 이제 없음."
- "Handler 작성 체크리스트" 3번: "
isTag(...)같은 방어 가드는 이제 불필요(넣어도 무해하지만 죽은 코드)."
- 왜 과한가: 하강 diff가 보장하는 것은 **"넘어온 값이 그 핸들러의
isHandlable을 만족한다"**까지다.isHandlable이 여러 모양의 값을 받아들이는 핸들러에서는, 그 안에서 어느 모양인지 가르는is판별이 여전히 필수다 — 보장은 "같은 핸들러"까지지 "같은 값 모양"까지가 아니기 때문. 실제 사례가 이미 코퍼스에 있음:PropertyHandler는 평범한 값과Tween<T>래퍼를 둘 다 받아isTween(realv)로 분기함 (base/tween-plan.md의 3-상태 릴레이션 슬롯). 즉 "그 가드가 있어야만 정확한 코드"가 지금도 존재한다. - 정정 방향: "옛 모델이 요구하던 타입 미보장을 메우는 방어 가드는 불필요"와 "한 핸들러가 여러 값 모양을 다루면 그 판별은 핸들러 자신의 책임"을 나눠 쓸 것. 지금 문장은 앞의 것만 말하면서 뒤의 것까지 부정한다.
- 파급: 체크리스트 3번은 "새 Handler 짜기 전에 훑을 목록"으로 지정된 곳이라, 이 상태로 두면 다중 모양 핸들러 작성자가 필요한 판별을 빼도록 유도한다.
D-5 — PreRef/PostRef는 "배열 먼저" 보장 위에 성립하는 게 아니라 더 위 루프에서 처리된다
- 판정: 구조는 맞으나 문서가 근거로 든 관계가 부정확 — 그 가정 위에 쓰인 서술은 재검토 필요.
- 사용자 답변 원문:
preref 랑 postref 는 정확히는 다른, 더 위에 있는 for 문에서 처리되고 flattened 에는 처리됨을 나타내는 값만 놔두는 구현. 이를 위해 Processed*Ref 와 ProcessedRefHandler(nop) 가 존재한다. PreRef 는 해당 보장 아래 성립하는게 아니라, 먼저 처리되는 것. 구조 자체는 틀린것이 없으나 가정을 그렇게 하였다면 재검토가 필요할 수 있음
- 문서가 주장하는 것:
dispatch-core-plan.md의 "props 순회 순서는 base 디스패치 드라이버가 명시적으로 두 단계로 고정한다" 절 끝 — "결과적으로 배열 슬롯에 놓인 어떤 값(Ref 포함)이든 모든 프로퍼티/이벤트 세팅보다 항상 먼저 처리된다는 게 base 자체의 보장이 됨 —ref-plan.md의 … "PreRef" 절이 이 보장 위에서 성립." - 실제로 맞는 것:
PreRef/PostRef는 두 패스 순회의 배열 파트 패스에 얹혀 있는 게 아니라, 그보다 위의 별도 pre-pass for 문에서 처리되고,flattened에는 소진 마커(ProcessedPreRef/ProcessedPostRef)만 남는다. 즉 "배열 먼저"라는 보장에 의존하지 않는다 — 독립적으로 더 먼저 돈다. - 정정 방향:
PreRef성립 근거를 "배열 파트 우선 보장"에서 떼어낼 것. 두 보장이 서로 독립임을 명시하고,ref-plan.md가 이 문장을 인용하고 있다면 거기도 같이 볼 것. - 부수: 사용자는 nop 핸들러를
ProcessedRefHandler로 통칭했는데 문서는ProcessedPreRefHandler/ProcessedPostRefHandler둘로 나눠 부름 — 하나로 합칠지 여부는 확인 안 됨.
D-6 — setLength/setOffsetSource 호출 책임자가 미결정이다 ⚠️ 열린 설계 질문
- 판정: 아니오 — "그 위치를 처음 매치한 Handler"라는 문서의 규정이 부정확하고, 아직 정해지지 않은 케이스가 있음.
- 사용자 답변 원문:
그 위치를 처음 매치한 핸들러는 약간 부정확함. State 등 일 수 있음. 최종 말단 요소가 이를 처리하는게 더 올바른것으로 보이는데, Slot 의 retract 처리가 비록 setOffset/Length 를 잘 stale 되지 않도록 처리는 해주지만, 처음부터 State<Slot|None> 에서 None 이 오는 경우는 생각이 필요한듯 보임. 0 으로 채워지는걸 누가 하냐를 지금 정해져있지 않을텐데. 어디 쪽에서 하는게 맞는지 확인해야함. None 처리자는 그 자체로 리프연산이라 볼 수 있느냐 하면 아니란것도 문제. 단순히 모든 핸들러가 k=number 일 때 처리하도록 두는게 맞는지 검토해보고 싶음
- 문서가 주장하는 것:
dispatch-core-plan.md의 "Length/Offset" 절 — "호출 책임은Slot자신의:List/CRUD가 아니라 그 위치를 처음 매치한 Handler(Dispatch/Slot.luau)", 그리고 "둘 다 array part의 모든 number 인덱스에 대해 반드시 호출 — 생략은 UB". - 문제 1 — "처음 매치한 Handler"는 중간 노드일 수 있다: 배열 위치에
State<Slot>이 오면 그 위치를 처음 매치하는 건StoreBind(중간 노드)다. 그런데 D-3에서 확정된 계약은 중간 노드가inst에 부작용을 가하지 않는다는 것 —setLength/setOffsetSource등록이 그 계약과 어떻게 양립하는지가 서술돼 있지 않다. 사용자 판단은 "최종 말단 요소가 처리하는 게 더 올바르다" 쪽. - 문제 2 —
State<Slot|None>에서None이 올 때 누가0을 채우는가가 미정:Slot의 retract는 stale을 막아주지만, 처음부터None이 흘러오는 경로가 열려 있다. 그리고None처리자(NoneHandler)는 중간 노드라 말단(leaf) 연산으로 볼 수도 없어서 "말단이 등록한다"는 규칙으로도 안 덮인다. - 사용자가 검토하고 싶어하는 대안: "단순히 모든 핸들러가
k=number일 때 처리하도록 두는 게 맞는지" — 즉 "위치를 매치한 특정 한 핸들러의 책임"이 아니라 "숫자 키를 다루는 모든 핸들러의 공통 의무"로 재규정하는 안. - 파급:
Length/Offset은 형제 순서 보장의 유일한 메커니즘이라, 이 갭이 남으면State<Slot|None>이 섞인 배열에서 형제 순서가 조용히 어긋난다(문서 스스로 이 상태를 UB로 규정). M3(Slot) 착수 전에 결론이 필요한 항목.
D-7 — base 소유 Fallback Handler의 등록 주체가 다시 뒤집힐 수 있음 ✅ 해소(같은 세션 내 재역전)
- 판정: 아니오(잠정) — 사용자는 quad-base 로드 시 등록이 맞다고 했었다고 기억하며, 문서의 현재 확정과 반대. 다만 사용자도 "더 확인이 필요"라고 함.
- 사용자 답변 원문:
fallback 들은 quad-base 로드 시가 맞다고 했었음. - 안 그러면 quad-roblox 를 로드하지 않았을 때 로드했는지 물어보는 요소가 처리가 안 된다고 했는데, 이것도 더 확인이 필요한 부분으로 보임.
- 문서의 현재 확정:
dispatch-core-plan.md의 "base가 소유하는 핸들러와 주입되는 엔진 op" 절 — [재정정, 2026-08-14 열두 번째 세션] 으로 "등록 주체는 quad-base 모듈 자체가 아니라 필요한 엔진(백엔드 팩토리)" 로 확정했고, 반대 모델("quad-base가 자기 모듈 로드 시점에 스스로 등록")을archive/tag-attribute-load-time-registration-reversed.md로 역전 처리까지 마쳤음. 근거는 "lifecycle-pattern.md가 이미 거부한InitNamespace류 top-level 부작용 패턴과 같은 클래스". - 사용자가 든 반대 근거: 백엔드 팩토리가 등록 주체라면, quad-roblox를 아예 로드하지 않은 상태에서는 그 Fallback Handler들도 없으므로 "provider가 초기화됐는지 물어보는" 안내 경로 자체가 동작하지 않는다.
- ⚠️ 이건 "역전을 다시 역전"하는 판단이라 특히 신중해야 함 — 정정
회신에 (a) 어느 쪽이 최종인지, (b) 최종이 "로드 시 등록"이면
archive/tag-attribute-load-time-registration-reversed.md를 되살릴지 아니면 새로 쓸지, (c)InitNamespace거부 원칙과 어떻게 양립시킬지를 같이 적어주시면 좋겠음. - 해소(같은 세션 내, 2026-08-18) — (a)/(b)/(c) 전부 답변됨:
(a) 최종은 로드 시 등록 — "등록 주체는 다시 quad-base 자신이다
(모듈이 자기 레지스트리를 구성하는 시점)"으로 재역전 확정. (b) 새로
안 쓰고 기존
archive/tag-attribute-load-time-registration-reversed.md에 재역전 배너만 추가(그 문서 자체가 "이번에 재역전됐다"는 걸 스스로 알림 — 새 archive 문서 생성 없음). (c)InitNamespace거부 원칙과의 양립: 그 원칙이 금지한 건 "사용자가 수동으로 init을 호출하게 만드는 것"과 "모듈 로드 시 남의 상태를 건드리는 것" 둘인데, base가 자기 모듈 안의 자기 레지스트리를 자기가 채우는 건 그 어느 쪽도 아니므로 충돌 없음. 지금 유효한 설계는base/dispatch-core-plan.md의 "base가 소유하는 핸들러와 주입되는 엔진 op" 절의 "[재역전, 2026-08-18 구현 전 QA — 사용자 확정]" 배너가 소스.
RE. base/relate-plan.md
RE-1 — gcconn/gchold 보관이 SetStrong이라고 적힌 두 곳이 틀림 (정답은 SetWeak)
- 판정: 아니오 —
SetWeak이 맞음(사용자 확정). - 틀린 위치 2곳:
base/relate-plan.md의 "대체하는 것" 절 — "bindLifetime/canExecute— gcconn/gchold를Relate의SetStrong으로 저장(둘 다 존재 이유가 '안 죽는 것'이므로 strong)". 괄호 안 근거까지 통째로 틀림 — 둘의 생존은 gcconn 클로저 upvalue와gchold[1]이 이미 보장하므로, 같은 문서의 "다른 곳에서 안전하게 유지되는 것은 항상SetWeak" 절 규칙에 따라 weak가 맞다.base/architecture.md소스 트리의quad-roblox/src/LifetimeHandle.luau행 주석 — "Relate:SetStrong으로 gcconn/gchold 저장".
- 맞는 서술(그대로 두면 됨):
relate-plan.md의 "다른 곳에서 안전하게 유지되는 것은 항상SetWeak" 절, 그리고base/lifecycle-pattern.md의 구현 스케치(InstData:SetWeak/BindData:SetWeak전부 weak). - 왜 그냥 오탈자가 아닌가: 1번은 근거 문장까지 딸려 있어 읽는 쪽이
"strong이어야 하는 이유가 있구나"로 납득하게 만든다. 실제로 이 서술을
따라
SetStrong으로 구현하면relate-plan.md가 경고하는 두-Relate상호 강참조 순환(RE-2, Luau에 ephemeron이 없어 실제 누수)에 정확히 걸린다 —gchold가value를 강하게 잡고BindData가value를 키로gchold를 강하게 잡는 모양이 되기 때문. 같은 문서가 "이 규칙을 지키면 그 위험이 구조적으로 안 생긴다"고 자랑하는 바로 그 사례를 반대로 적어둔 셈.
M. base/modifier-plan.md
M-3 — 예약 필드 이름이 Apply 하나가 아니라 셋이고, Overridden은 콜론 메소드로도 쓸 수 있다
- 판정: 아니오 — 문서 두 곳이 틀림.
- 사용자 답변 원문:
전부 가능한게 맞음. Overridden 도 편의 상 A: 체인으로 제공 가능함. 밖에서 직접 (A, B) 해주어도 좋고. 콜론과 닷 둘다 가능함
- 틀린 곳 1 —
modifier-plan.md8번 절 "구현 시 주의": "__index가 고정 메소드 테이블(현재는Apply하나)을 먼저 확인하고 … 따라서Apply는 Modifier 필드 이름으로 예약됨". → 9번 절이:Peek(key)를 추가했고Overridden도 콜론 호출을 지원하므로, 고정 메소드는Apply/Peek/Overridden셋이고 셋 다 필드 이름으로 예약된다. "현재는Apply하나"는Peek이 생기던 시점에 갱신되지 않은 stale. - 틀린 곳 2 —
base/architecture.md"코드 스타일 — 네이밍 케이싱" 절: 대문자 3번 항목이Modifier.Overridden을 "콜론 메서드는 아니지만 (여러 Modifier를 동등한 인자로 받아야 해서 self 하나로 안 됨)" 이라고 단정하며, 이를 "정적 결합 함수"라는 세 번째 하위 분류를 새로 만든 유일한 근거로 든다. → 실제로는 콜론 호출도 지원하므로 그 괄호 안 근거("self 하나로 안 됨")가 성립하지 않는다. 정정 시 이 세 번째 분류를 유지할지(다른 근거로) 아니면 2번 항목(콜론 메서드)에 흡수할지 판단 필요. - 파급: 예약 필드 이름은 Roblox 프로퍼티 이름과 충돌하면 안 되는
목록이라, 셋으로 늘어난 사실이
FrameModifier류 타입 생성 스크립트의 제외 목록에 반영돼야 함(M7).
R. base/ref-plan.md
R-1 — "Ref 객체 자신이 곧 콜백/대기자 배열" 구조를 재고할 것 (사용자: 별도 .Callbacks 테이블 + .Value는 평범한 해시 필드)
- 판정: 아니오 — 사용자가 더 단순한 대안을 제시.
- 사용자 답변 원문:
단순히 .Callbacks: {fun, thread} 등이 있는게 맞지 않나라는 생각임. .Value 는 단순 해시필드로 주는게 더 간단해보임. 엔지니어링 난이도구 단순 테이블 하나 더 만드는게 쉽고, 크게 비싸지도 않다고 생각됨.
- 문서가 주장하는 것(2026-08-09 열한 번째 세션 보강): Ref 객체 자신이
곧 콜백/대기자 배열(숫자 키 색인)이고, 그래서
.Value를self.Value = v로 얹으면T가 함수/스레드일 때for i,v in self do순회가 hash 파트까지 훑어 오분류되므로.Value를__index메타메소드로 구현해 저장 위치를 배열과 분리해야 한다. - 사용자 대안: 콜백/대기자를 별도 필드
.Callbacks에 담고,.Value는 그냥 평범한 해시 필드로 둔다. 근거는 "테이블 하나 더 만드는 게 쉽고 크게 비싸지도 않다" — 즉__index우회 기법을 쓸 이유 자체를 없앤다. - 이 정정이 무효화하는 서술들 (같이 볼 것):
.Value의__index구현 근거 문단 전체(위 인용) — 대안에서는 hash 파트 충돌 자체가 안 생기므로 불필요해짐.- "구현 디테일" 문단의
for i, v in <배열> do단일 순회로type(v)분기(thread=대기자 / function=콜백) — 별도.Callbacks테이블이 생기면 순회 대상이self가 아니라 그 테이블이 된다.type(v)분기 자체는 유지 가능하나 서술 위치가 바뀜. - 아래 R-3의
nil소진/빈 슬롯 재사용 결정 — 배열이 어디 있든 논리는 그대로 유효하지만, 서술이 "Ref 객체 자신"을 전제로 쓰여 있어 같이 고쳐야 함.
- 파급:
base/architecture.md소스 트리의Ref.luau주석 (범용 값 박스(.Value 읽기 + :Set()/:Callback()/:Wait() 셋))은 그대로 둬도 되지만, 구현 구조를 서술한 곳은 전부 갱신 필요.
R-3 — PreRef pre-pass 소진 슬롯을 None으로 채운다는 서술이 stale
- 판정: 아니오 — 틀림(이미 2026-08-14에 정정된 내용이 이 문서에 반영 안 됨).
- 사용자 답변 원문:
PreRef, PostRef 는 ProcessedPreRef 등의 nop 핸들러가 캐치하는걸로 자리를 채우지 None 으로 채우진 않았던것 같음 sourceList 는 정확. 단, length 는 0 을 넣는게 옳다. length: number|state 와 offset: state|None 차이라고 생각함
- 틀린 위치:
base/ref-plan.md의 "왜None이 아니라nil인가" 절 결론 문장 — "순서가 중요한 배열(PreRefpre-pass 소진 슬롯, Length/OffsetsourceList)은 계속None". - 실제로 맞는 것: pre-pass가 소진시킨 자리는
None이 아니라 전용 센티널ProcessedPreRef/ProcessedPostRef로 채워지고, 전용 nop 핸들러 (ProcessedPreRefHandler/ProcessedPostRefHandler)가 정상Dispatch.process경로에서 그걸 캐치한다.dispatch-core-plan.md는 이미 이렇게 정정돼 있음([정정, 2026-08-14 두 번째 세션]) —ref-plan.md만 갱신에서 빠짐.sourceList가None인 것은 맞음. - 부수 확인(정정 아님): 해제 시
setLength엔 숫자0,setOffsetSource엔None이 들어가는 비대칭이 의도된 것 — 타입이 각각number | State<number>와Source<number> | None이라서. 두 문서 모두 이미 이렇게 적혀 있어 고칠 것 없음.
RF-4 — drive가 None을 건너뛰는 특수 분기를 없애야 함 ⚠️ 설계 변경, D-6과 연결
- 판정: 아니오 — 전제("배열 파트의
None은Dispatch.process를 절대 안 탄다")가 거짓이고, 그 위에 세운 설계도 같이 바뀜. - 사용자 답변 원문:
직접적으로 Frame{None} 이면 맞긴한데, Frame{Store<Slot|None>} 이면 탈 수 있음. 무엇이냐 상관 없이 탈 수 있는게 맞긴 하고, 그 경우도 k=number 이면 단순 넘어가기를 해야함. 또한, 이 경우 setLength/OffsetSource 를 여기서 처리하는게 맞다고 보임. 즉, drive 는 v == None 인지 확인 안하고 그냥 프로세스 태우는게 가장 적절한 처리로 보임. NoneHandler 의 retract 는 Nop 일 수 있어보이나, process 자체가 이전걸 retract 하는건 필요(Tag -> None).
- 깨진 전제: 리터럴
Frame{None}만 생각하면 두 패스 루프가 걸러내면 그만이지만,Frame{ State<Slot|None> }처럼 반응형 값이None을 내놓으면 그None은StoreBind의 재귀를 타고Dispatch.process에 그대로 도착한다. 즉 "배열 파트의None은process를 안 탄다"는 보장이 애초에 성립하지 않는다. - 틀린 위치:
base/ref-plan.md의 옛명확화(2026-08-09 열한 번째 세션, 확인 질문에 답변)항목 전체(2026-08-18에 전면 정정됨) — "배열 파트의None은 애초에Dispatch.process자체를 절대 안 탄다", "k=number조합으로NoneHandler가 실제로 매치되는 경우는 없음".base/dispatch-core-plan.md의 "None센티널" 절 — "Dispatch.drive의 두 패스 루프 자신이NoneHandler/Dispatch.process를 거치지 않고 바로 건너뜀", 그리고 "정말 빈 자리인None만 두 패스 루프가 직접 건너뜀"이라는 후속 서술.- 같은 문서 "Length/Offset" 절이
None슬롯의 등록 책임을 서술한 부분. base/component-composition-plan.md의 "필수 관용구" 문단 — "props.Modifier or None이 최종적으로 배열 파트에None인 채로 남으면 두 패스 루프 자신의 array-partNone-스킵 규칙이 그대로 적용돼 아무 일도 안 일어남". 스킵 규칙이 사라지면 이 근거가 바뀐다(NoneHandler가 매치돼setLength(0)/setOffsetSource(None)을 등록하는 경로가 됨). 결론(or None을 쓰는 것)은 안 바뀜 — CC-3에서 관용구 자체는 "맞음"으로 확인됐으므로 근거 문장만 갱신.
- 사용자가 지시한 새 설계:
Dispatch.drive는v == None을 확인하지 않고 전부Dispatch.process에 태운다 — 특수 분기 제거. (이게 "가장 적절한 처리"라는 판단.)NoneHandler가k == number인 경우를 스스로 처리한다 — 실제 값 세팅은 하지 않고 넘어가되, 그 자리의setLength(0)/setOffsetSource(None)등록을 여기서 한다.NoneHandler가 반환하는 retractor는 no-op이어도 되지만,process자체는 이전 것을 retract시키는 역할을 해야 한다 —Tag→None전환에서 이전 Tag 기여가 실제로 걷혀야 하기 때문.
- ⚠️ D-6과의 관계: D-6이 "
State<Slot|None>에서None이 올 때 누가0을 채우는가가 미정, 그리고None처리자는 말단이 아니라 중간 노드라 '말단이 등록한다' 규칙으로도 안 덮인다" 고 남겨둔 갭의 답이 여기 나옴 —NoneHandler가k=number일 때 직접 등록한다. 다만 이러면NoneHandler가 "중간(래핑) 노드"이면서도 등록 책임을 지게 되므로, D-3의 "중간 노드는inst에 부작용을 가하지 않는다" 계약과의 관계를 정리해야 함 (setLength/setOffsetSource는inst의 프로퍼티를 건드리는 게 아니라 Dispatch 부기라 계약 위반이 아니라고 볼 여지가 크지만, 문서가 그렇게 명시하고 있지는 않음). - 정정 시 같이 확인할 것: 3번의 "
process가 이전 것을 retract"가 하강 diff 모델에서 자동으로 성립하는지(핸들러가TagHandler→NoneHandler로 바뀌므로 (B) 분기가retractFrom을 부름) 아니면 별도 코드가 필요한지. 자동이면 문서에 그 경로를 명시만 하면 되고, 아니라면NoneHandler.process에 명시적 정리가 들어가야 함.
B. base/brand-plan.md
B-1 — Brand.get이 None을 특수 분기하는 설계는 의존성을 만들므로 기각
- 판정: 부분적으로 틀림 — 포함 관계(
isRef(preRef)==true)와 테이블 아이덴티티 태그는 맞으나,None처리 방식이 틀림. - 사용자 답변 원문:
맞음. 그런데 Brand 는 None 을 참조할 필요는 없음. Brand 자체는 아에 의존성 없고, None 도 테깅되는건 맞으나, isNone 대신 필요한 곳에서 v == None 하면 되는 일, 혹은 isNone 구현 자체를 그렇게 해주면 되는 일.
- 문서가 주장하는 것:
brand-plan.md의 옛None은 이 레지스트리에 안 들어감문단(2026-08-18에 정정됨) — "Brand.get(x)가 … 범용 introspection 창구 역할까지 겸하게 하려면None도 빠지면 안 되므로,Brand.get이 내부적으로x == None을 먼저 확인하는 특수 분기를 하나 두고 그 뒤에 일반 레지스트리 조회로 폴백 —isNone은 바로 이 특수 분기의 실제 구현체가 됨". - 실제로 맞는 것:
Brand모듈은 아무 의존성도 갖지 않아야 한다 —None을 참조하는 특수 분기를 넣으면Brand → None의존이 생긴다.isNone은 필요한 곳에서v == None으로 하면 되고,isNone이라는 이름의 함수를 두더라도 그 구현이 그냥v == None이면 된다.None자체를 레지스트리에 태깅하는 것 자체는 무방. - 파급: "
Brand.get이 quad가 아는 모든 값을 답해주는 단일 introspection 창구"라는 서술도 같이 재검토 필요 —None이 그 창구에서 빠지는 걸 받아들일지, 아니면None도 평범하게 레지스트리에 등록해 특수 분기 없이 답이 나오게 할지(사용자 답변은 후자를 허용함).
AT. base/attribute-plan.md
AT-1 — 같은 그룹 객체를 두 위치에 놓는 경우(Frame{a, a})가 미검토 ⚠️ 열린 항목
-
판정: 확정 내용 자체는 맞음. 다만 사용자가 새 검토 항목을 제기.
-
사용자 답변 원문:
맞음. 이러면 a=Attribute() Frame{a,a} 가 되어도 이미 문제가 안 나는것으로 보이긴 하는데, UB로 둘지 에러를 쉽게 낼 수 있는지는 확인이 필요해보임.
-
왜 검토가 필요한가 (문서 의사코드 기준 손 트레이싱):
groupKey(v, name)이 그룹 값 객체별·이름별 메모이즈이므로, 같은 객체a가k=1과k=2에 놓이면 양쪽이 완전히 같은 키 객체로 위임한다. 그러면:nameClaims체크는cur == k라 통과한다(에러가 안 남).- 그런데 두 위치가
(inst, 같은 key)라는 하나의 체인을 공유하게 된다 —k=1의process가Dispatch.process(inst, key, source, 1)을 부르고,k=2도 같은 인자로 같은 체인을 다시 부른다. - 더 중요한 건 철거:
k=1이 retract되면 그 클로저가Dispatch.retractFrom(inst, key, 1)을 불러k=2가 아직 쓰고 있는 바인딩까지 통째로 철거한다. - 이건
Ref의 "이중 배치 방지"(base/ref-plan.md)가 막은 것과 정확히 같은 클래스의 문제로 보인다 — 다만Ref는bindLifetime게이트로 잡히는데 그룹Attribute엔 대응 게이트가 없다.
-
✅ 후속 답변으로 결론 나옴 (SL-1 답변에 덧붙여짐) — 위치별 claim을 하나 두는 쪽으로:
이전 답변에 대해 diff 를 보다 더 생각이 나서 말하자면, Attribute 는 bindLifetime 를 못함. Ref 와 다르게 여기저기서 사용 가능하기 때문. 한 곳에서 바운딩 했다고 다시 바운딩 못할 순 없음. 따라서 위치별 claim 을 하나 두어야한다고 생각함.
즉
Ref처럼bindLifetime을 재사용하는 방식은 쓸 수 없다 —Ref는 "한 곳에만 배치"가 규칙이지만Attribute그룹 값은 여러 곳에서 쓸 수 있어야 하므로, 한 번 바인딩했다고 다시 못 하게 만들면 안 된다. 대신 위치별 claim 레지스트리를 하나 추가해서 같은 그룹 객체가 같은 위치 집합을 이중 점유하는 것만 잡는다. -
정정 시 설계할 것: 그 위치별 claim이 무엇을 키로 하는지 (
(inst, groupValue) → k인지,groupKey단위인지), 그리고 기존nameClaims와 어떻게 공존하는지. -
같이 볼 것:
Tag는 같은 객체를 여러 위치에서 재사용하는 게 정상 관례로 확정돼 있고(T-1) 위치(k) 기준 참조 카운트로 안전하다 — 그룹Attribute만 왜 다른지(자원이 "값 하나"라 겹침=충돌)가 정정 시 같이 서술되면 좋겠음.
SL. base/slot-plan.md
SL-1 — Ref의 leaf 바인딩은 배열(숫자 키) 전용이고, RefLeafHandler에 k 체크가 빠져 있음
- 판정: 아니오 — "배열 전용"이 맞고, 이를 부정하는 서술과 구현이 틀림.
- 사용자 답변 원문:
배열 전용이 맞음. 컴포넌트 일 때는 함수의 인자에 맞게 위처럼 보낼 수 있겠으나, 기본 의도는 리프에선 숫자 바인딩임. 이유는 Ref끼리는 순서가 통하므로, 다른 Ref처리를 먼저 해야하는 순서 의존이 있을 때에도 가능하게 하고자였음.
- 고쳐야 할 곳:
base/ref-plan.md의RefLeafHandler.isHandlable—isRef(v) and not isPreRef(v) and not isPostRef(v)로만 적혀 있어type(k) == "number"체크가 빠졌다. 짝인ObserverEffectLeafHandler엔 그 체크가 필수라고 명시돼 있고 (S-8에서 확인), 빠지면 named 자리로 흘러온 값을 잡으려는 FALLBACK 가드가 죽은 코드가 된다 — 같은 이유가Ref에도 그대로 적용된다.base/ref-plan.md의 옛일반 Ref는 계속 Modifier/Store 어디든 자유롭게 들어감항목(2026-08-18에 한정 서술 추가) — 이 문장이 "named 해시 키에 놓아도 leaf 바인딩이 된다"로 읽힌다. 실제 의미는 "Modifier 필드나 Store 값으로 전달될 수 있다"(=값으로서 어디든 흘러갈 수 있다)이지 "leaf 바인딩 자리가 아무 데나 된다"가 아니므로, 오해가 없게 다시 써야 한다.base/slot-plan.md의 예시slot:Add(Frame { Ref = myRef })— 여기Frame이 컴포넌트 함수라서Ref가 그 함수의 named 파라미터인 경우에만 성립한다. 실제 Instance 리터럴로 읽히면 위 1번 규칙과 정면으로 어긋나므로, 예시에 그 전제를 명시해야 한다.
- 배열 전용인 이유(사용자 논거, 문서에 없던 것):
Ref끼리는 배열 index 순서가 통하므로, "다른 Ref 처리를 먼저 해야 하는 순서 의존"이 있을 때도 표현이 가능하게 하려는 것. (PreRef/PostRef의 "계열 안 순서 보장"과 같은 결의 근거인데, 일반Ref에 대해서는 어디에도 안 적혀 있었음.)
SL-3 — :List reconcile의 nil 리턴을 비파괴로 확정한 것을 재검토해야 함 ⚠️ 확정 뒤집기 후보
- 판정: 부분적으로 틀림 — 사용자는 파괴가 기본이 맞다고 보며, 현재 문서 확정(비파괴)과 어긋남.
- 사용자 답변 원문:
List reconcile 에서 nil 리턴으로 지워지길 요구하는 경우는 비파괴일지, 파괴일지 생각해보아야할 것이 많은듯. 기본적으로 파괴가 맞기는 한데, Instance.new Destroy 비용을 아끼고 싶은, filter 부분에 있어서는 단순 Parent = nil 로 두고 싶을수도 있음. 이를 위해 특정 리턴 등은 Parent = nil 로 만들고 홀드해둔 다음, 나중에 prev 상 이를 사용할 수 있게 두는 방법이 가능한가 모색해보고 싶음. 아마 ud 는 명시적으로 nil 안 하면 안 지워지니, PopOnly 등을 만들어 PopOnly, { old = ..., source... } 등을 하면 Parent 를 빼고 slot 에서 적절히 빼주는게 방법으로 보임.
- 문서의 현재 확정:
slot-plan.md의 "구현상 바뀌어야 하는 것" 절 2번 — ":List의reconcile— 교체/소멸 시rawRemove(파괴) 대신 같은 비파괴 경로. 데이터에서 빠진 아이템도 파괴되지 않고 언마운트만 되며, 아무도 안 들고 있으면 GC". 그리고 "자동 경로는 언마운트, 명시적으로 지우라고 한 것만 파괴"라는 일반 규칙. - 어긋나는 지점: 사용자는
:Listreconcile의nil리턴에 한해 파괴가 기본이라고 봄. 즉 "자동 경로 = 전부 언마운트"라는 일반화가:List에는 안 맞을 수 있다. (State<Slot>교체가 언마운트인 것은 SL-3 질문에서 별도로 부정되지 않았으므로 그대로 유효해 보임 — 두 경로를 분리해서 정할 필요.) - 사용자가 원하는 추가 기능 — 재사용을 위한
PopOnly(가칭):filter용도에서Instance.new/Destroy비용을 아끼려면, 파괴하지 않고Parent = nil로만 두고 홀드해뒀다가 나중에prev(:Compute의previous와 같은 자리)로 재사용할 수 있어야 함. 구체안:updateFn이PopOnly, { old = ..., source = ... }같은 걸 반환하면 reconcile이Parent만 빼고 Slot에서 적절히 제거하는 방식.- 참고로 사용자가 짚은 전제: userdata는 명시적으로
nil을 안 하면 안 지워지므로 홀드만 해두면 살아있다.
- 참고로 사용자가 짚은 전제: userdata는 명시적으로
- 결정해야 할 것: (a)
:Listreconcilenil리턴의 기본을 파괴로 되돌릴지, (b)PopOnly를 이번 설계에 넣을지 백로그로 뺄지, (c) 넣는다면updateFn반환 규약(현재는 "반환값을 해석하는:List내부 로직")이 어떻게 확장되는지.
E. base/effect-plan.md
E-2 — :Unsubscribe()가 leaf 바인딩에도 cleanup을 부르게 한 건 틀림 (Subscribe의 짝으로 좁혀야 함)
- 판정: 아니오 — 계약을 축소해야 함.
- 사용자 답변 원문:
표면적으로 맞아보이긴 하나, subscribe 한게 아니면 unsubscribe 는 지원하면 안 되거나, 적어도 리프 바운딩에선 그래선 안 됨. leaf 에 바운딩 된 경우, 특히 state 또는 observer 가 들어갈 때, 단순 emit 에서 최적화로 인해 이전과 동등이라 retract 가 아무 일을 하지 않음. (그런데, 별개로, retract 가 아무것도 안 하고 나서, process 쪽에서도 아무것도 안 하는지는 확인이 필요해보임) 그래서 다시 바운딩 안 먹는것이라 의도한 바가 아님. subscribe 는 unsubscribe 의 짝이라고 생각함.
- 문서가 주장하는 것:
effect-plan.md의 "EffectHandle:Subscribe()/:Unsubscribe()" 절 — ":Unsubscribe()는 Observer의 것을 그냥 위임하지 않는다 — Effect 계층에서 의미가 확장됨 … 직전(또는 유일한) cleanup을 정확히 1회 호출 — leaf가 죽을 때 하던 것과 정확히 같은 이벤트를 수동으로 앞당기는 것". - 실제로 맞는 것:
:Unsubscribe()는:Subscribe()의 짝이다 —:Subscribe()로 등록하지 않은 핸들(=leaf 바인딩된 핸들)에는 지원하지 않거나, 최소한 leaf 바인딩 경로에서는 cleanup을 앞당기면 안 된다. - 왜 위험한가 (사용자 논거): leaf 바인딩 +
State<Effect>/State<Observer>조합에서, 값이 실제로 안 바뀌면 dedup 최적화 때문에 retract가 아무 일도 안 한다(S-8의old ~= vdedup). 그런데:Unsubscribe()가 cleanup을 미리 실행해버리면, 뒤이은 재-dispatch에서 dedup 때문에 재바인딩이 안 일어나 그 Effect가 조용히 죽은 채로 남는다 — 의도한 동작이 아님. - ⚠️ 함께 확인해야 할 별건(사용자가 괄호로 남긴 것): dedup 경로에서
retract가 아무것도 안 한 뒤
process쪽도 정말 아무것도 안 하는지 대칭이 실제로 성립하는지 확인 필요.ObserverEffectLeafHandler의사코드 기준으론process의if old ~= v then bindLifetime(...) end와 클로저의if nextValue ~= v then unbindLifetime(...) end가 짝을 이루지만,EffectHandle은 내부 Observer로 cascade까지 해야 하므로(E-2의 (2)번, 그 자체는 "맞음"으로 확인됨) 그 cascade가 dedup 분기 안에 제대로 들어가 있는지가 별도 확인 대상. - 정정 범위:
base/effect-plan.md의 해당 절, 그리고 같은 논리가 적용되는base/source-state-plan.md의 Observer:Unsubscribe()서술(거기는 이미 "전역 경로 전용"으로 좁혀져 있어 괜찮아 보이나 같이 확인).
EV. base/event-plan.md
EV-1 — 이벤트 disconnect 센티널을 false에서 None/nil로 바꿀 것
- 판정: 아니오 — 센티널 선택이 이제 낡음.
- 사용자 답변 원문:
다만, 이젠 None 이 있어서 false 을 사용해야할 이유가 없어졌다고 봄. false 대신 None/nil 을 사용하지 말아야할 이유가 없다면 일관적이게 None/nil 을 주는게 맞다는 생각
- 문서가 주장하는 것:
event-plan.md의 "이벤트도 store-bind 가능" 절(제목의 센티널 표기는 2026-08-18에None/nil로 갱신됨) — "false로 disconnect,nil아님.nil은 Lua 테이블에서 '키가 아예 없음'과 구별이 안 됨 … 대신false(Luau에서 실재하는 싱글톤 타입)를 '연결 없음' 센티널로 씀". - 왜 낡았나: 그 결정(2026-08-06)은
None센티널이 확정되기 전에 "테이블에 실재하는 값으로 표현 가능한 것"이 필요해서false를 고른 것인데, 이후None이 정확히 그 역할로 도입됐다(base/modifier-plan.md2-1,base/dispatch-core-plan.md"None센티널"). 지금은 같은 문제를 푸는 센티널이 두 개 있는 셈이고, 이벤트만 다른 걸 쓸 이유가 없음. - 정정 시 같이 설계해야 할 것:
None으로 바꾸면NoneHandler가 매우 높은 우선순위로 먼저 매치해서nil로 재귀시킨다 — 그러면EventHandler가(k=이벤트키, v=nil)을 받아 disconnect로 처리할 수 있어야 한다.isHandlable이nil값에도 매치되도록 규정이 필요.- 문서의 기존 근거였던 "이벤트인지 여부는 값이 아니라 키(리플렉션)로
결정되므로 다른 boolean 프로퍼티 핸들러와
(k, false)매칭이 겹칠 위험 없음" 은false를 안 쓰면 아예 불필요해지므로 삭제 대상. - N-4(
NilHandler신설)와 상호작용 —k가 숫자가 아닌 이벤트 키에서nil이 어느 핸들러에 가야 하는지 같이 정할 것.
BS. base/bind-system-plan.md
BS-2 — "이벤트 콜백 시그니처는 Luau가 검증 못 한다"는 전제가 거짓 ⚠️ 두 문서의 근거가 무너짐
- 판정: 아니오 — 사용자가 반례 코드를 직접 작성해 제시.
- 사용자 답변 원문:
ReflectionService 사용은 맞음. 단 새로 프로젝트 루트에 생성된 test.luau 를 보면 알 수 있지만, 이벤트의 콜백 또한 타입을 지정해주는게 가능함. D가 만들어지는 제네레이터 상에서 타입을 적절히 제공하면 콜백 시그니처는 충분히 처리 가능한것으로 보임. 이는 OnChange 와 다르게, 필드이기 때문에 타이핑이 가능함.
- 반례 (2026-08-18 사용자가 직접 작성해 보여준 코드 — 당시 프로젝트 루트에
test.luau로 뒀다가 이후 삭제, 아래가 그 전문):
props 테이블 타입에 필드로 선언돼 있으면 콜백 파라미터가 그대로 추론된다 — 런타임 판별을function Frame (prop: {MouseButton1Click: ((a: number)->())?}) end Frame{ MouseButton1Click = function(a) -- a: number end }ReflectionService로 하는 것과 타입을 생성기가 제공하는 것은 완전히 별개 축인데, 문서가 둘을 묶어버렸다. - 틀린 위치 1 —
base/bind-system-plan.md의 "인스턴스 생성 / 이벤트 네이밍 인체공학" 절: "이건 타입 안전성을 어느 정도 포기하는 대가지만(콜백 시그니처까지 Luau가 검증 못 함 —apply<T,U>(instance: T, properties: U): T & U가 스키마 검증 없이 구조적으로만 merge), 이미 UB로 남긴 … 와 같은 급의 한계라 손해가 크지 않고". →D생성기가 클래스별 props 타입에 이벤트 필드까지 정확한 콜백 타입으로 포함시키면 검증된다. "감수하는 대가"가 아니라 생성기가 챙겨야 하는 구현 체크리스트 항목이다(ui-shorthand-plan.md가UICorner/UIPadding/UIScale을 Modifier 타입 메소드 목록에 끼워 넣으라고 한 것과 같은 성격). - 틀린 위치 2 —
base/onchange-plan.md의 "확정" 절:OnChange<<T>>제네릭을 안 만드는 근거로 "이미 확정된 '이벤트 바인딩은 콜백 시그니처를 Luau가 검증 못 하는 대가를 받아들인다'는 결정 … 과 같은 급의 트레이드오프라 새로 정당화할 것 없음" 을 듦. → 그 전제가 거짓이므로 이 근거는 통째로 무효. 다만 사용자 답변에 따르면 결론(제네릭 없음)은 유지되고 근거만 바뀐다 — 이벤트는 필드라 타이핑이 되지만OnChange(name)은 이름을 인자로 받는 팩토리라 그 경로가 없다는 것이 진짜 이유. - 파급:
base/architecture.md의 소스 트리DI/init.luau주석("제네릭 생성자 + ~25개 정적 필드")과D/DI생성기 설계에 **"이벤트 필드의 콜백 타입도 생성한다"**가 추가돼야 함.base/store-plan.md의 "타입 추론 문제" 절이 "이벤트는 이 관습의 유일한 예외" 라고 서술하는데, 예외의 성격이 바뀐다 — "타입을 포기하는 예외"가 아니라 "이름 지정 방식만 문자열 키인 예외"다.- 이벤트가 store-bind될 때(
State<function>/false→EV-1에 따라None/nil)의 타입은 어떻게 되는지도 같이 정해야 함 — 필드 타입이((a: number)->())?뿐이면State<...>나 센티널을 못 받는다.
추가 요구사항 — 심사 중 사용자가 새로 지시한 것
기존 서술이 틀려서가 아니라, 확인 과정에서 사용자가 새로 요구한 사항. 정정이 아니라 설계 추가라 위 결함 목록과 분리해 둔다.
N-1 — FALLBACK 가드의 에러 메시지에 실제 k 타입을 실어줄 것
- 출처: S-8 답변("정확함. 다만 …").
- 사용자 답변 원문:
정확함. 다만 Priority Fallback 이 type(k) == "string" 인 상황에서는 가장 위에 Ref/Observer binding should be array index item, but got typeof k 처럼 알려줄 필요는 있는듯 - 안 그러면 핸들러 미등록 이슈인지, MyRef = Ref 같이 아에 잘못 쓴 이슈인지 분간이 안가서 최종 유저에게 혼선을 줄 여지가 존재하는듯.
- 요구 내용:
HANDLER_PRIORITY_FALLBACK가드(Observer/Ref/PreRef/PostRef의 동적 경로 가드)가 에러를 낼 때, 단순히 "배열 리터럴에만 놓을 수 있음"이 아니라 실제로 들어온k의 타입을 메시지에 포함할 것 —Ref/Observer binding should be array index item, but got <typeof k>형태. - 근거(사용자 논거): 메시지에
k타입이 없으면 사용자가 두 원인을 구분할 수 없다 — (a) 핸들러가 등록이 안 된 것인지, (b)MyRef = Ref(...)처럼 named 자리에 잘못 쓴 것인지. 최종 사용자에게 혼선을 줌. - 반영할 곳:
base/source-state-plan.md"동적 경로 가드" 절의 가드 스케치,base/ref-plan.md의PreRef/PostRef동적 경로 가드, 그리고base/dispatch-core-plan.md의 "base가 소유하는 핸들러와 주입되는 엔진 op" 절(FALLBACK 자리 서술).
N-2 — 없는 Store 키는 "타입 에러"로 잡히면 충분 (구현 시 확인)
- 출처: ST-1 답변.
- 사용자 답변 원문:
네, 맞고, Store<{ field: type }> 상 없는 네임에는 타입 시간에 Source 가 없는것으로 나와 타입 에러만 나면 됩니다. 아마 지금 설계가 그럴것이예요
- 내용: lazy
__index생성이 오타/동적 키로 Source를 무한정 누적하는 트레이드오프는 그대로 수용. 방어선은 런타임이 아니라 타입 —Store<{field: T}>로 선언된 Store에 없는 이름을 쓰면type function이 합성한 결과 타입에 그 프로퍼티가 없어 타입 에러가 나야 한다. - 확인 필요: 사용자도 "아마 지금 설계가 그럴 것"이라며 단정하지 않았음 —
type function으로 합성한 테이블 타입이 미선언 프로퍼티 접근을 실제로 거부하는지(인덱서를 안 붙였을 때 Luau가 에러를 내는지) M0에서 확인할 것. ST-4의 스파이크(luau-test/done/16-...)에 이 음성 대조군이 있는지도 같이 볼 것.
N-3 — 동률 경고 print는 전역 디버그 플래그 Quad.debug로 게이팅
- 출처: D-8 답변.
- 사용자 답변 원문:
동률 print 는 라이브러리가 debug 모드일 때만. (Quad.debug: boolean = default false) 식이고, true 로 하면 디버깅 가능. 다른건 OK
- 내용:
dispatch-core-plan.md의 "디버그 모드 — 핸들러 등록/정렬 시점에 동률 감지 시 print 경고" 항목은 무조건 찍는 게 아니라 모듈 표면의 불리언 플래그Quad.debug(기본false)가true일 때만 찍는다. - 파급:
Quad.debug는 지금 어느 문서에도 없는 새 공개 API 표면이다 —base/module-lifecycle-plan.md(모듈 표면)와base/architecture.md의 소스 트리에 반영이 필요하고,New()/Quad()다중 인스턴스화(A-3) 시 이 플래그가 인스턴스별인지 전역인지도 같이 정해야 함.Dispatch.listHandlers()도 같은 디버그 표면에 속하는지 확인 필요.
N-4 — NilHandler 신설 요구 (State<Slot|nil>도 동작해야 함)
- 출처: B-1 답변에 덧붙인 별건("이 답과는 연관 없는 말을 하자면").
- 사용자 답변 원문:
State<Slot|None> 일 수도 있지만, State<Slot|nil> 이여도 작동은 함. 이것도 NoneHandler 유사하게 NilHandler 가 필요함. 오직 k=number v=nil 일 때만 받고, NoneHandler 와 유사하게 retract 처리를 담당. 단, 재귀만 안 할 뿐임. 혹은, NoneHandler 가 다시 NilHandler 가 불리도록 하는게 더 나을수도 있겠다는 생각(단순 재귀로 NoneHandler 는 재귀처리만 담당함).
- 요구 내용: 반응형 값이
None이 아니라 진짜nil을 내놓는 경우 (State<Slot|nil>)도 정상 동작해야 하므로,NilHandler를 신설한다.isHandlable:k == number이고v == nil일 때만 매치.- 역할:
NoneHandler와 마찬가지로 retract 처리 담당. 단 재귀는 하지 않음(NoneHandler는nil로 바꿔 재귀하는 게 일이지만,NilHandler는 이미nil이라 더 내려보낼 곳이 없음). - 사용자가 선호한 대안 구조:
NoneHandler는 재귀 처리만 담당하게 두고, 그 재귀가 결국NilHandler를 부르게 만드는 쪽이 더 깔끔할 수 있음 — 즉 실질 정리 로직을NilHandler한 곳에 모으는 구성.
- RF-4와 함께 봐야 함: RF-4가 "
drive는None스킵을 없애고 전부process에 태운다 +NoneHandler가k=number에서setLength(0)/setOffsetSource(None)을 등록한다"였는데, 여기에NilHandler가 들어오면 그 등록 책임이 둘 중 어디에 있는지를 같이 정해야 함(재귀 구조를 택하면 자연히NilHandler쪽으로 모임). - 반영할 곳:
base/dispatch-core-plan.md의 "None센티널" 절(핸들러 목록과 재귀 구조),base/architecture.md소스 트리(Dispatch/아래 파일 목록),base/dispatch-core-plan.md"Length/Offset" 절의 등록 책임 규정.
N-5 — Attribute.Merged와 Attribute.Overridden을 둘 다 제공 (열려 있던 결정 해소)
- 출처: AT-4.
question.md3번에 "사용자 확인 대기"로 열려 있던 항목의 답이며, 선택지 둘 중 하나가 아니라 제3안이 채택됨. - 사용자 답변 원문:
차라리 Merged, Overridden 을 제공하면 될것 같음. 전자는 에러를 내주고, 후자는 그냥 조용히 덮어써주는것. 사용자 의도에 따라 달라질 부분이라 분리해주는것이 이로워보임.
- 결정: 이름 겹침의 처리 방식을 API로 분리한다.
Attribute.Merged(a, b, ...)— 같은 이름이 겹치면 error.Attribute.Overridden(a, b, ...)— 겹치면 조용히 뒤가 이김(덮어쓰기).- 근거: 어느 쪽이 맞는지는 사용자 의도에 달린 문제라 프레임워크가 하나로 정하지 말고 골라 쓰게 한다.
- 파급 / 정리할 것:
base/attribute-plan.md의 "채택안 —Tag와 동형인 array-part 값 객체" API 목록에Attribute.Overridden추가, "열린 질문" 절의 해당 항목 해소 처리,.claude/question.md3번에서 제거.Merged/Overridden이라는 이름 쌍의 의미가 코퍼스 전체에서 재정렬됨 — 지금까지는Merged=무손실 합집합(Tag),Overridden=필드 단위 덮어쓰기(Modifier)로 연산의 종류를 가르는 이름이었는데,Attribute에선 충돌 시 정책(error냐 덮어쓰기냐)을 가르는 이름이 된다.base/tag-plan.md가Tag.Merged코드 주석에서Merged를 집합 합치기,Overridden을 이미 계산된 것 합치기로 대조해둔 서술과 같이 볼 것.Tag에도Overridden이 필요한가는 자동으로 따라오지 않음 —Tag는 합집합이라 애초에 충돌 개념이 없음. 확인 불필요해 보이나 정정 시 한 줄 명시해두면 좋겠음.
N-6 — SetAndDispose 류 편의 콤비네이터 검토 (백로그 후보)
- 출처: SL-2 답변.
- 사용자 답변 원문:
정확하나, source:apply(SetAndDispose( new )) 같은걸 구현해줄까는 생각해보았음(단 여기서의 apply 는 source 를 넘겨주는 함수가 되어야함.). Get해놓고 Set 이후 나중에 지우는게 편의성이 떨어지기 때문. 아니면 그냥 source 자체에 :콜론 메서드로 가능하게 하는걸 넣어줄까 생각은 하고 있음.
- 문제:
dispose는 "Set(언마운트) → 그 다음dispose" 순서를 요구하는데(SL-2), 그러려면 호출부가Get()으로 이전 값을 미리 잡아두고 →Set(new)→ 잡아둔 옛 값을dispose하는 3단계를 매번 손으로 써야 해서 편의성이 떨어진다. - 후보 두 가지:
source:Apply(SetAndDispose(new))— 콤비네이터. 단 여기서의Apply는State가 아니라Source를 넘겨주는 함수여야 함(사용자 명시) — 지금 확정된state:Apply(factory)는factory(self)에State를 넘기므로,Source전용 변형이 필요한지 같이 정해야 함.Source에 콜론 메서드로 직접 얹기(예:source:SetAndDispose(new)).
- 미결: 어느 쪽을 택할지, 그리고 애초에 이번 범위에 넣을지 백로그로
뺄지.
state:Apply의 시그니처((State<T>) -> U)에 영향이 갈 수 있으므로 M3 착수 전에 방향만이라도 정해두는 게 좋음.
N-7 — UI 숏핸드가 만든 자식을 FindFirstChild 대신 Relate로 기억할 것
- 출처: UI-1 답변.
- 사용자 답변 원문:
다만, FindFirstChild 는 비용이 ref 저장보단 비쌈. spring 등으로 움직일 수도 있다 생각하면 릴레이션으로 저장하는것도 좋은 생각. 각 숏핸드가 만들어낸 요소의 프로퍼티 세팅은 새로운 dispatch.process(target,k,v) 로 위임해 tween 등이 자연스럽게 가능.
- 문서가 주장하는 것:
base/ui-shorthand-plan.md는 재사용 대상을 **"quad가 이전에 만든 고정 이름(_quad_corner류) 자식"**으로 한정하는데, 그 "찾기"를 어떻게 하는지는 v1처럼 이름으로 조회(FindFirstChild)하는 것으로 읽힌다. - 요구 내용: 이름 조회 대신
Relate에(inst, 숏핸드키) → child로 저장해서 다시 찾을 것. 근거는 (a)FindFirstChild가 참조 저장보다 비싸고, (b) spring 등으로 자식이 계속 움직이는 상황이면 그 조회가 반복 비용이 됨. - 주의 — 고정 이름 규약을 없애자는 뜻은 아님: 이름(
_quad_corner류)은 디버깅 가시성(research/debug-tooling-plan.md9번)과 "사용자가 만든UICorner를 건드리지 않는다"는 판정에 여전히 필요해 보임.Relate는 조회 경로를 대체하는 것이고, 두 가지가 어떤 관계인지(이름은 표시용, 릴레이션은 조회용) 정정 시 명시할 것. - 주의 —
Relate키 전제:inst-키Relate는 gcconn 셋업 위에서만 성립하는데(RE-1/L-1), 숏핸드가 만드는 자식도 quad가 만든 Instance라 그 셋업을 거치는지 확인 필요.
N-8 — DI → D 리네임 확정 (2026-08-18) + 전수 반영 목록
- 출처: 사용자가 이 QA 라운드 중 직접 확정("이거 하면서 DI => D 확정하자").
2026-08-08 용어 정리 라운드부터
question.md1순위로 열려 있던 항목. - 확정 내용 두 갈래 (사용자 판정):
- 네임스페이스/모듈 자체는
DI→D.D.Frame/D/init.luau/D.InstSlot/D.FrameModifier. - "특수 DI 키"라는 설명용 표현은
D로 바꾸지 않고 "특수 키"로 단순화. (사용자 선택:"특수 키"로 단순화) — "특수 D 키"라고 쓰지 않는다. 수식어 자체를 빼도 문맥상 통한다는 판단.
- 네임스페이스/모듈 자체는
D로 가는 근거(이미 기록돼 있던 것): (1) "Instance" 전용 개념이 아니라 quad-* 전반의 declare 요소로 확장 가능한 이름, (2) 엔진 종속 없이 다른 백엔드에서도 재사용 가능, (3)D.FrameModifier류 타입 프리픽스가 짧아야 한다는 실용적 제약. 원래 이름DI의 문제는 "Dependency Injection"과 완전히 겹쳐 실제로 오해가 있었던 전례가 있다는 것.- ⚠️ 같이 정해야 할 것 — 한 글자 식별자의 검색성/자기설명력 보완책
(사용자 선택:
예, 같이 넣어둘 것). 2026-08-08에D를 확정 못 하고 미룬 유일한 사유가 이거였음 —D한 글자는 grep도 어렵고 이름만으로 뜻이 안 드러난다. 정정 시 아래를 같이 정할 것:- 문서에서
D가 처음 나올 때 항상 "Declarative"로 풀어쓰는 규약을 둘지(예: "D(Declarative) 네임스페이스"). conventions.md의 "문서 표기 규약"에 넣을지, 아니면base/architecture.md"코드 스타일 — 네이밍 케이싱" 절에 넣을지.
- 문서에서
반영 대상 — 전수 (2026-08-18 기준 grep, session/·session-summary.md·initreq/ 제외)
갈래 ① DI → D (네임스페이스/모듈)
| 파일 | 줄 | 무엇 |
|---|---|---|
base/architecture.md |
195 | 소스 트리의 DI/ 디렉토리 + 그 아래 init.luau 주석 |
base/slot-plan.md |
98 | DI.InstSlot = Slot<<Instance>> — "(DI 네임스페이스 이름 자체는 question.md 1번 용어정리 대기 중…)" 괄호도 삭제 |
base/bind-system-plan.md |
126 | ""DI"는 Dependency Injection이 아니라 "Declarative Instance"" — 이 문단이 개명의 근거 자체이므로 재작성(왜 D가 됐는지로) |
base/bind-system-plan.md |
130 | DI.Frame vs DI.New<<Frame>> "Frame" |
base/bind-system-plan.md |
199 | 열린 질문 "DI(또는 다른 이름) 등 정확한 모듈 이름" — 항목째 삭제(해소됨) |
base/modifier-plan.md |
313 | "DI 쪽 '제네릭 생성자 함수 하나 + …' 패턴 재사용" |
base/ui-shorthand-plan.md |
74 | "quad-roblox의 각종 타입(DI 인스턴스 타입, Modifier 타입 등)" |
base/attribute-plan.md |
66 | "이미 확정된 DI 인스턴스 생성 패턴" |
base/attribute-plan.md |
449 | 표의 백엔드(quad-roblox의 D/DI 층) — 이미 병기 중, D로 단일화 |
base/attribute-plan.md |
492 | "최종 이름은 다른 가칭들(DI→D/…)과 함께 대기열" — 목록에서 제거 |
base/dispatch-core-plan.md |
628 | 그 백엔드(quad-roblox의 D/DI 층) — 이미 병기 중, D로 단일화 |
ROADMAP.md |
333 | 체크박스 DI/init.luau(제네릭 생성자 + ~25개 정적 필드) |
ROADMAP.md |
429–430 | DI.InstSlot + "DI 네임스페이스 이름 자체는 question.md 1번" 대기 문구 |
ROADMAP.md |
726 | quad-roblox의 D/DI 층 — D로 단일화 |
ROADMAP.md |
825 | 용어 정리 스윕 체크박스의 State/DI/Slot 목록 — DI 제거 |
question.md |
43, 46, 47 | DI 항목 자체 — 해소 처리해 archive/question-resolved.md로 이전(Merge→Overridden 등 기존 해소 항목과 같은 방식). 46–47행의 파급(DI.FrameModifier류 타입 프리픽스)은 이번 리네임에 실제로 포함되므로 반영 목록에 흡수 |
question.md |
183 | "M3 Source/M5 DI 생성자" |
todos.md |
95 | 용어 정리 목록의 "DI→D(1순위)" — 해소로 제거 |
research/additional-primitives-plan.md |
21 | 프리미티브 나열 .../Slot/DI) |
research/debug-tooling-plan.md |
5, 126, 379, 460, 479 | "Source/DI 생성자", DI/init.luau, "DI 제네릭 생성자", "Dispatch/DI", "M5(quad-roblox DI 제네릭 생성자)" |
research/pre-implementation-audit.md |
537, 538, 541 | "DI 쪽 패턴 재사용", "DI 타입 생성 계층(M5)", "M5 DI 체크리스트" |
| 이 문서 자신 | BS-2의 파급 문단 | DI/init.luau 주석과 D/DI 생성기 언급 |
갈래 ② "DI 키" → "특수 키" (설명용 표현)
| 파일 | 줄 |
|---|---|
base/architecture.md |
37, 42, 191 |
base/onchange-plan.md |
20, 76(헤딩 — 아래 ⚠️) |
base/attribute-plan.md |
33, 74 |
base/lifecycle-hooks-plan.md |
28, 82, 83, 106, 111, 186, 294, 296, 300, 303 |
base/dispatch-core-plan.md |
315 |
base/bind-system-plan.md |
164 |
base/ref-plan.md |
637 |
ROADMAP.md |
135, 711 |
.claude/README.md |
64 |
luau-test/README.md |
81, 117 |
- ⚠️ 헤딩 1개 + 그 헤딩을 절 인용하는 곳 1개가 짝으로 묶여 있음 — 반드시
같은 커밋에서 함께 고칠 것.
base/onchange-plan.md:76의## 다른 특수 DI 키와의 대조(헤딩)base/lifecycle-hooks-plan.md:300이 그 절을"다른 특수 DI 키와의 대조"로 절 인용하고 있음.- 한쪽만 고치면
doc-check.py가 절 참조 불일치 ERROR로 잡아 커밋이 막힌다(conventions.md의 "절 인용 규약"이 ERROR로 승격돼 있음).
base/ref-plan.md:637/base/lifecycle-hooks-plan.md:106은 같은 문장의 원문·인용 관계 —lifecycle-hooks-plan.md가ref-plan.md의 문장을 blockquote로 그대로 옮겨 적었으므로 문구가 계속 일치해야 함.- archive/·session/·session-summary.md 는 건드리지 말 것 — 히스토리
문서라 당시 표기 그대로 두는 게
conventions.md관례 (archive/tag-hash-key-model-reversed.md제목의 "DI 키" 등).
반영 후 확인
python3 .claude/tools/doc-check.py→ ERROR 0(특히 위 절 인용 짝).grep -rn '\bDI\b' --include='*.md' . | grep -v '/initreq/\|/session/\|session-summary.md\|/archive/'→ 0건이어야 함.- 인덱스 3층 갱신:
.claude/README.md,question.md(항목 이전), 루트ROADMAP.md—conventions.md의 중대 변경 핸드오버 체크리스트 6번.
N-9 — New를 커링으로 명시하고 D를 "처리 없는 별칭 테이블"로 규정 (사용자 제안)
- 출처: 사용자 제안(2026-08-18, N-8 확정 직후).
- 사용자 발언 원문:
D.Frame 같은건 New 에서 커링되어, New 함수는 New(name)({}) 되는게 이롭다 생각하는데 어떰? 실제 사용 상 New<> "Frame" {...} 로도 쓸 수 있고, D 에선 별다른 처리 없이 D.Frame = New<> "Frame" :: (({...타입명시}) -> Frame) 으로 쉽게 만들 수 있다는게 내 생각임.
- 제안 내용:
New는 커링 —New(name)이 생성자 함수를 반환하고, 그걸 다시 props 테이블로 호출:New(name)({...}).- 직접 사용도 같은 모양 —
New<<Frame>> "Frame" {...}(Lua 문법상New("Frame")({...})). D는 별다른 처리가 없다 — 필드마다D.Frame = New<<Frame>> "Frame" :: (({...타입명시}) -> Frame)로 캐스팅만 얹은 순수 별칭 테이블.
- 지금 문서와의 관계 — 뒤집는 게 아니라 명시화:
base/bind-system-plan.md의 "인스턴스 생성 / 이벤트 네이밍 인체공학" 절이 인용한 PA님 패턴이 이미constructor.Frame = new("Frame")이라 사실상 커링이었음 — 다만 (a) "커링이다"라는 것과 (b) 2단계 호출 계약과 (c)New(name)({...})/New "Name" {...}라는 직접 호출 형태가 문서에 명시된 적이 없다. BS-1에서 "eager 정적 테이블"은 이미 확인됐으므로 이 제안은 그 위에 "그 정적 필드가 정확히 무엇인가"를 채우는 것. - BS-2와 정합적 — 필드별
::캐스트로 타입을 주는 방식은,D생성기가 어차피 클래스별 props 타입(이벤트 콜백 시그니처 포함)을 뽑아야 한다는 BS-2의 결론과 맞물린다. 기존에 인용돼 있던new<ClassName>(className): from<index<UIInstances, ClassName>>(타입 레벨 인덱싱)보다 생성기 입장에서 단순함. - 기각된 "2트랙"과 혼동하지 말 것 — 같은 절이 기각한 건
"필드=1급 타입 경로, 문자열=폴백" 이라는 능력 차이였지
New라는 이름이나 문자열 호출 자체가 아니었다. 이 제안은 오히려 두 형태가 완전히 같은 것(하나가 다른 하나의 미리 적용된 결과)임을 못박는다.
후속 확정 (2026-08-18, 같은 대화에서 1~4번 전부 확정)
2. 이름은 대문자 New로 확정. 사용자 답변: "2. New입니다."
지금 코퍼스는 둘 다 씀 — bind-system-plan.md가 PA님 코드를 인용할 땐
소문자 new(className), 같은 절의 (기각된) 2트랙 구상엔 대문자
DI.New<<Frame>> "Frame". 대문자로 통일하고, base/architecture.md의
"코드 스타일 — 네이밍 케이싱" 절에 D.New가 어느 부류로 들어가는지도
같이 적을 것.
3. D는 전부 코드 자동 생성이 맞음 — New 호출문까지 생성기가 찍는다.
- 사용자 답변 원문:
- 코드로 자동 생성되는것이 맞는게, index 가지고만 하면 MouseButton1Click 이 RBXScriptConnection 이 되어버림. 처리하기 힘들다 생각하는게, lsp 마다 Frame 타입을 어떻게 다루냐 다를 수 있음. 게다가 T|State 같은것 또한 처리해야하는데, 이걸 타입 함수로 다 처리하게 만드는것 보다 단순히 D 파일이 자동 생성되는게 좋다고 생각함. 그리고 코드 생성 산출물인가는 맞음, 전부 코드 생성이나, New 같은것도 생성기에서 같이 적어주어야할 부분.
- ⚠️ 이건 단순 확인이 아니라
bind-system-plan.md가 인용한 시그니처를 부정한다. 그 문서는 PA님 코드를 그대로 인용해new<ClassName>(className): from<index<UIInstances, ClassName>>— 즉 타입 레벨 인덱싱으로 클래스 타입을 뽑는 모양인데, 사용자가 든 세 가지 이유로 그 방식만으로는 부족하다:- 이벤트 필드가 콜백 타입이 안 된다. Roblox 타입 정의에서
MouseButton1Click은 시그널 계열 타입이라, 인덱싱으로 뽑으면 그 타입이 그대로 나와버리고 quad가 원하는((...) -> ())?콜백 시그니처가 안 나옴. 이게 곧BS-2가 요구한 "생성기가 이벤트 필드의 콜백 타입까지 만들어야 한다"의 직접적 근거 — 두 항목은 같은 문제의 양면이므로 반드시 같이 처리할 것. - LSP마다
Frame타입을 다루는 방식이 다를 수 있어 타입 함수/인덱싱에 의존하는 게 위험하다. T | State<T>(그리고T | Tween<T>,None등)까지 타입 함수로 조립해야 하는데, 그럴 바엔D파일을 통째로 생성하는 쪽이 단순하다.
- 이벤트 필드가 콜백 타입이 안 된다. Roblox 타입 정의에서
- 따라서:
D는 타입뿐 아니라D.Frame = New<<Frame>> "Frame" :: (...)라는 값 선언까지 생성기가 찍어내는 파일이다. 손으로 쓰지 않는다. - 반영:
base/bind-system-plan.md의 PA님 시그니처 인용을 이 결론에 맞게 다시 쓸 것 — "PA님 코드 그대로 채택"이라는 프레이밍 자체가 타입 조립 방식에 한해서는 더 이상 정확하지 않음(호출 모양은 그대로 채택, 타입은 생성으로 감).
4. 생성 범위는 "GUI에 쓰이는 모든 인스턴스" — 전량은 부적합.
- 사용자 답변 원문:
- 는 모든 인스턴스를 넣기는 부적합함. GUI에 쓰이는 모든 인스턴스를 자동 생성한다로 잡아줘도 좋을것으로 보임. 안 그럼 D 파일이 너무 커짐.
- 즉 기존 "자주 쓰는 ~25개" 도 아니고 Roblox 전체 클래스도 아닌,
"GUI에 쓰이는 것 전부" 가 기준. 근거는 파일 크기 — 전량 생성하면
D파일이 너무 커진다. - 반영:
base/bind-system-plan.md가 생성 대상을~25개(자주 쓰는Frame/TextButton/UICorner등,UIInstances타입 테이블 등록분)로 적어둔 서술과,base/architecture.md소스 트리의# 제네릭 생성자 + ~25개 정적 필드(UIInstances)주석을 이 기준으로 교체. "GUI에 쓰이는"의 정확한 판정 기준(Roblox API 덤프에서 무엇을 GUI로 볼 것인가 —GuiObject하위 +UIComponent하위 +LayerCollector류 등)은 생성기 구현 시점에 정할 것으로 남김.
1. D에 없는 클래스는 느슨하게 any — 필요하면 사용자가 직접 채운다.
- 사용자 답변 원문:
a. 느슨하게 any 로 하고, 필요하면 이를 직접 구현 가능하게 둡니다. cast 를 하든, 유저의 자유
- 내용:
D가 커버하는 범위(위 4번의 "GUI에 쓰이는 모든 인스턴스") 밖의 클래스를New<<X>> "X" {...}로 직접 쓰면 props 타입은 느슨하게any. quad는 그 자리에서 타입 안전성을 보장하지 않고, 필요하면 사용자가 직접 좁힌다 —::캐스트를 쓰든 자기 래퍼를 만들든 자유. - 왜 이게 자연스러운가:
D.Frame자체가 애초에New<<Frame>> "Frame" :: (({...}) -> Frame)— 캐스트 한 줄이다(위 N-9 본문). 즉 생성기 산출물이 특권적인 게 아니라, 사용자가 임의 클래스에 대해 똑같은 한 줄을 직접 쓸 수 있다. "직접 구현 가능하게 둔다"가 새 확장 지점을 만든다는 뜻이 아니라, 이미 있는 패턴을 그대로 쓰면 된다는 뜻. - 파급 — BS-1의 서술 수정 필요:
base/bind-system-plan.md가 확정해둔 "제네릭 생성자 함수 하나가 알려진 타입과 모르는 타입을 전부 커버" 는 이제 정확하지 않다. 런타임은 여전히 전부 커버하지만 타입은 아니다 —D범위 안은 생성된 정확한 타입, 밖은any. 그 문장을 이 구분이 드러나게 다시 쓸 것. - 이걸로 N-9의 열린 항목은 전부 닫힘 — [2026-08-18 기준] 반영도 같은 날 완료.
반영할 곳
base/bind-system-plan.md"인스턴스 생성 / 이벤트 네이밍 인체공학" 절 — 커링 계약과 두 호출 형태를 본문에 추가, PA님 시그니처 인용을 이 결론에 맞춰 갱신.base/architecture.md소스 트리의D/init.luau주석(현DI/init.luau # 제네릭 생성자 + ~25개 정적 필드(UIInstances)) — N-8과 같은 줄이라 한 번에 같이 고칠 것.ROADMAP.md의D/init.luau체크박스(현 333행) 및 M5 관련 항목.base/slot-plan.md의D.InstSlot = Slot<<Instance>>— 이것도 같은 "별칭 테이블" 패턴인지(즉D가 인스턴스 생성자 말고 타입 별칭도 담는지) 확인 필요.
부수 발견 — 오탈자/표기 불일치 (사용자 판정 불필요, 확인만 됨)
여기 있는 것은 설계 오류가 아니라 문서 표기 실수다. 위 절들과 달리 사용자 회신을 기다릴 필요 없이 정정 라운드 때 같이 고치면 된다.
architecture.md"Store/State/Source 온톨로지 — 확정됨" 절의.value(소문자) — A-6에서ref.Value(대문자)가 맞음으로 확인됨. 같은 문서 소스 트리 주석(Ref.luau행)은.Value로 맞게 적혀 있어 한 문서 안에서 두 표기가 섞여 있음. 다른 문서에도.value가 퍼져 있는지 정정 시 grep 필요.source-state-plan.md":Compute(fn)의 선택적 두 번째 인자" 절의fn(value, previous)— S-2에서 최종 시그니처가fn(self, previous?, ...deps)로 확인됨. 그 절만self가 lazy 핸들로 통일되기 전의 구 표기(value)로 남아 있어, 그 절만 읽으면 첫 인자가 raw 값인 줄 오해하게 됨.
진행 로그
1라운드(2026-08-18) 완료. base/ 25개 문서를 의존성 순서로 훑으며
AskUserQuestion으로 확인. 문서당 3~4개 주장씩, 총 22배치.
| 문서 | 결함/열린 항목 | 신규 요구 |
|---|---|---|
architecture.md |
A-3 | — |
source-state-plan.md |
S-1, S-12 | N-1 |
store-plan.md |
ST-2 | N-2 |
lifecycle-pattern.md |
(S-1의 진원지) | — |
dispatch-core-plan.md |
D-1, D-5, D-6, D-7 | N-3 |
relate-plan.md |
RE-1 | — |
modifier-plan.md |
M-3 | — |
ref-plan.md |
R-1, R-3, RF-4 | N-4 |
brand-plan.md |
B-1 | — |
attribute-plan.md |
AT-1 | N-5 |
slot-plan.md |
SL-1, SL-3 | N-6 |
effect-plan.md |
E-2 | — |
event-plan.md |
EV-1 | — |
bind-system-plan.md |
BS-2 | — |
ui-shorthand-plan.md |
— | N-7 |
| (전역 이름·표면) | — | N-8 DI→D, N-9 New 커링 |
blocker-plan.md / tag-plan.md / tween-plan.md / typing-limits.md / component-composition-plan.md / module-lifecycle-plan.md / onchange-plan.md / purity-and-effects-plan.md / fallback-plan.md / lifecycle-hooks-plan.md |
전부 통과 | — |
아직 안 본 것 (2라운드 대상 — 새 파일 pre-implementation-qa-round2.md에 쓸 것):
slot-plan.md의:List내부(reconcile구현,keyFn,userdata생명주기, 구독 시점, Slot-in-Slot 재귀)와dispatch-core-plan.md의recompute를 손으로 트레이싱하는 검증 — 이번 라운드는 "확정된 주장이 맞는가"를 물었지 의사코드를 실행해보진 않았다.reference/(v1 스냅샷, 프레임워크 비교) — 확정 문서가 아니라 제외했으나,base/가 근거로 인용하는 사실들이라 인용이 정확한지는 미검증.research/11개 — 확정 전이라 제외.- 루트
ROADMAP.md의 마일스톤 분할이 이번 발견들과 맞는지.