`qa-request/pre-implementation-handtrace-round7.md`의 `H-55`~`H-106`을
사용자와 대화형으로 처리하고 `base/`에 전량 반영했다. 결정의 소스는
`-followup.md`(개수·개별 항목은 여기서 세지 않는다).
## 처분
확정 39 / 무효·소멸 4(`H-73`~`H-76`) / 기각 1(`H-77`) / 범위 축소 2 /
다른 항목으로 흡수 6.
**부수로 `question.md` 최우선 절이 비었다** — 중간 State GC는 `_hold`
불변식(하류 → 상류 강함)으로, 동적 키 표면 위치는 `store:Of<<T>>(name)`
하나로 닫혔다. **M2 착수를 막는 항목이 없다.**
## 구조가 바뀐 것
- `Ref`가 `Epoch`를 만족(`.Revision` + `EpochBrand`) — 포탈 캐치업 비대칭과
같은 `Ref` 중복 dep이 같이 닫힘
- `Weak*` 등록 표면 신설(`Ref:WeakCallback` / `Observer:WeakSubscribe`) —
Weak 쪽이 프리미티브고 강한 쪽이 "GC 킵"을 얹은 것
- `Effect`: dep 등록이 생성자 한 곳으로, 강한 주인은 `_deps` 하나,
억제는 사적 `Blocker`, `bindLifetime`은 핸들 하나에만 적용.
`:Rerun()` 정의 신설(재진입은 지연 재실행), `_installed` 신설
- 전파 루프 의사코드 확정 — 구독자 집합의 원소는 Observer **값**이고
**자식 State 노드는 `canExecute`를 안 탄다**(그대로 짜면 파생 State
아래가 전부 침묵했다)
- `rawInvalid` → `cacheTargetCount`/`cacheCurrCount` 카운터 쌍
- `recompute` 재진입 차단 + `invalidAfter` 되감기, `gatedRecompute`가
인덱스를 캡처하지 않고 토큰으로 조회
- `emit(commit) -> boolean`, `EpochMap:Peek` — 정책이 흡수 집합을
버리지도 읽지도 못하던 것을 닫음
- error 계약(`level` 이분, 메시지는 영어)과 예외 계약(`pcall`로 안 감쌈)을
`architecture.md`에 신설
## Store — 같은 날 재설계하고 철회했다
`H-75`/`H-76`으로 `WrapStore`/`ProcessStoreType`이 폐기되자 그 자리를
"`store.key`는 값, `store:Of(k)`가 프리미티브"로 채웠다가 **같은 날
철회**했다(`archive/store-value-field-redesign-withdrawn.md`). 살아남은 건
**명시적 초기화** 하나다. 최종형은 타입 인자에 `Source<T>`를 직접 쓰고
`store.key`는 평범한 레코드 필드이며 **타입 함수를 안 쓴다**.
철회 이유 중 하나가 원칙으로 승격됐다 — **"타입 함수는 타입이 못 잡는
문제를 에러로 띄우는 정도 이상으로 가지 않는다"**(`typing-limits.md` §0).
`index<>`/`keyof<>`도 Luau가 predefine한 타입 함수라 같은 함정을 갖는다.
## 툴체인
**`luau` CLI가 심볼릭 링크를 못 탄다**(디렉토리·파일 둘 다)는 것이 최소
재현으로 밝혀졌다 — pesde 워크스페이스 링크가 전부 심볼릭이라 스모크 2개가
안 돌았고 `luau-analyze`는 **조용히 통과**했다("거짓 클린"). `scripts/relink.sh`
+ `scripts/test.sh` 신설, 이제 스모크 셋 전부 PASS.
## 검증
`/code-review high` 2회(12건 + 14건)와 감사 8패스(12/6/6/4/13/3/6/0건)를
각도를 바꿔가며 돌렸고 전부 반영했다 — 마지막 패스가 무발견으로 수렴.
각 패스의 각도와 대표 발견은 `-followup.md`의 검증 절이 소스.
`doc-check.py` ERROR 0.
Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_012oLwATeQdq9TCFdENPutFG
1245 lines
87 KiB
Markdown
1245 lines
87 KiB
Markdown
# 구현 전 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` — `:List` reconcile의 `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.md` 1순위였던
|
||
항목). 라이브 문서 19개 파일에 걸쳐 있고, **헤딩 1개와 그걸 절 인용하는
|
||
곳 1개가 짝으로 묶여** 있어 한쪽만 고치면 `doc-check.py` ERROR가 난다.
|
||
반영 대상 전수 목록이 그 항목에 표로 들어있다.
|
||
- `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`의 "`canBound` vs
|
||
`canExecute`" 절이 이 판정의 소스라고 선언돼 있으므로 거기가 진짜
|
||
진원지** — 정정 시 그 문서부터 볼 것.
|
||
- **문서가 주장하는 것**:
|
||
```lua
|
||
if canBound(self) then
|
||
error("이미 :Subscribe()로 전역 바인딩된 값" / "이미 다른 Instance에 바인딩된 값")
|
||
end
|
||
```
|
||
즉 `canBound == true`가 "이미 살아있게 묶여 있다 = 더 못 묶는다"는 뜻으로
|
||
쓰이고 있음.
|
||
- **사용자 판정**: 이름이 아니라 **호출부가 잘못됐다** — `canBound`는 이름
|
||
그대로 "지금 묶을 수 있는가"(true = 묶어도 됨)여야 하고, 게이트는
|
||
`if not canBound(value) then error(...) end` 형태가 돼야 한다.
|
||
- **연쇄로 같이 무너지는 서술 (정정 시 반드시 함께 볼 것)**:
|
||
1. **"`canBound`와 `canExecute`는 판정 로직이 같아서 값이 항상 같다"** —
|
||
`base/source-state-plan.md` "이중 바인딩 금지" 절과
|
||
`base/lifecycle-pattern.md`가 둘 다 "비공개 헬퍼 `isBoundAlive`를 그대로
|
||
부른다"고 서술함. 그런데 `canExecute`는 true=살아있음이고 `canBound`는
|
||
true=아직 안 묶임이므로, 올바른 관계는 **동치가 아니라 부정**
|
||
(`canBound(v) == not isBoundAlive(v)`)이다. "값은 항상 같지만 호출부의
|
||
질문이 달라 이름만 분리했다"는 근거 서술 자체가 성립하지 않게 됨 —
|
||
실제로는 **반대 방향을 묻는 두 predicate**임.
|
||
2. **`bindLifetime` 의사코드의 `if canBound(value) then error("이미
|
||
바인딩된 값") end`** — 같은 이유로 뒤집혀 있음. 이 줄엔
|
||
`[정정, 2026-08-14 열두 번째 세션]` 배너까지 붙어 "게이트는 canBound가
|
||
맞다"고 못박아 뒀는데, 정작 방향이 틀린 채로 확정된 것.
|
||
3. **"죽은 바인딩의 재사용은 허용 — `canBound`가 거짓이라 게이트를
|
||
통과함"** — 방향이 뒤집히면 이 문장도 뒤집혀야 함(죽은 바인딩은
|
||
`canBound`가 **참**이라 통과).
|
||
- **파급**: M8(`Ref`/`PreRef`/`PostRef`)과 M3(Observer/Effect) 양쪽의 진입
|
||
게이트가 전부 이 predicate를 쓴다. 지금 문서대로 구현하면 **정상적인 첫
|
||
바인드가 전부 에러나고 이중 바인드는 무사통과**하는, 정확히 반대로 도는
|
||
게이트가 된다.
|
||
- **정정해야 할 정확한 위치 (`base/lifecycle-pattern.md` 확인 후 확정)**:
|
||
1. **`canBound`의 구현 자체** — `(1)` 코드 블록:
|
||
```lua
|
||
function canBound(value)
|
||
return isBoundAlive(value) -- ← 이름대로면 not isBoundAlive(value) 여야 함
|
||
end
|
||
```
|
||
`canExecute`는 `return isBoundAlive(value)`가 맞으므로, **둘은 서로의
|
||
부정**이 된다. 같은 블록의 주석("어느 쪽 진입점에서 물어도 항상 같은
|
||
값이라")과 `(3)` 절의 "오늘 두 문맥의 판정값은 우연히 같다"도 함께
|
||
틀리게 됨.
|
||
2. **`bindLifetime`의 가드** — `if canBound(value) then error(...)`
|
||
(`(1)` 코드 블록).
|
||
3. **`Observer:Subscribe()`의 가드** — `if canBound(self) then error(...)`
|
||
(`(2)` 코드 블록).
|
||
4. **`(3)` 절 끝의 "죽은 바인딩 재사용" 서술** — "`canBound`가 **거짓**이라
|
||
게이트를 통과함" → 방향이 뒤집히면 "**참**이라 통과함"이 됨.
|
||
5. `base/source-state-plan.md`의 "이중 바인딩 금지" 절 게이트 스케치와
|
||
`bindLifetime` 의사코드(앞서 적은 것).
|
||
6. `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<T> 의 타입을 모르기도 하고, 우린 더이상 필요하지
|
||
> 않게 된 요소임.
|
||
- **문서가 주장하는 것**: 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<T> 에 대해 process 하면 후행 처리가
|
||
> 생길텐데, state 의 새로운 값이 나오면 get 을 통해 얻어진 것으로 다시
|
||
> process 가 수행됩니다. 여기서 이전의 프로세스 슬롯/인덱스와 비교해서
|
||
> 정확히 자신이 만들어낸 것이라면, 단순 retract 처리가 발생하고, 아니라면
|
||
> retractUnder 로 전부 제거되어 nil 이 됩니다. 즉 process 가 retract 를
|
||
> 담당한다가 성립하므로 위는 옳은 표현입니다. **다만, 처음부터 한 핸들러가
|
||
> 여러 값을 가질 수 있어 is 처리가 필요한건, 그 핸들러의 몫입니다.**
|
||
- **문서가 (너무 강하게) 주장하는 것** — 두 군데:
|
||
1. "핸들러 계약" 절: *"방어 가드를 남겨둬도 무해하지만 **죽은 코드**이고,
|
||
반대로 **그 가드가 있어야만 정확한 코드는 이제 없음**."*
|
||
2. "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> 등 일 수 있음.
|
||
> 최종 말단 요소가 이를 처리하는게 더 올바른것으로 보이는데, 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곳**:
|
||
1. `base/relate-plan.md`의 "대체하는 것" 절 — *"`bindLifetime`/`canExecute`
|
||
— gcconn/gchold를 `Relate`의 `SetStrong`으로 저장(둘 다 존재 이유가
|
||
'안 죽는 것'이므로 strong)"*. **괄호 안 근거까지 통째로 틀림** — 둘의
|
||
생존은 gcconn 클로저 upvalue와 `gchold[1]`이 이미 보장하므로, 같은
|
||
문서의 "다른 곳에서 안전하게 유지되는 것은 항상 `SetWeak`" 절 규칙에
|
||
따라 weak가 맞다.
|
||
2. `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.md` 8번 절 "구현 시 주의"**: *"`__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` 우회 기법을 쓸 이유 자체를 없앤다.
|
||
- **이 정정이 무효화하는 서술들 (같이 볼 것)**:
|
||
1. `.Value`의 `__index` 구현 근거 문단 전체(위 인용) — 대안에서는
|
||
hash 파트 충돌 자체가 안 생기므로 불필요해짐.
|
||
2. "구현 디테일" 문단의 **`for i, v in <배열> do` 단일 순회로 `type(v)`
|
||
분기**(thread=대기자 / function=콜백) — 별도 `.Callbacks` 테이블이
|
||
생기면 순회 대상이 `self`가 아니라 그 테이블이 된다. `type(v)` 분기
|
||
자체는 유지 가능하나 서술 위치가 바뀜.
|
||
3. 아래 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<number> 와 offset:
|
||
> state<number>|None 차이라고 생각함
|
||
- **틀린 위치**: `base/ref-plan.md`의 "왜 `None`이 아니라 `nil`인가" 절 결론
|
||
문장 — *"순서가 중요한 배열(**`PreRef` pre-pass 소진 슬롯**, Length/Offset
|
||
`sourceList`)은 계속 `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-part `None`-스킵 규칙**이 그대로 적용돼 아무 일도 안
|
||
일어남"*. 스킵 규칙이 사라지면 이 근거가 바뀐다(`NoneHandler`가 매치돼
|
||
`setLength(0)`/`setOffsetSource(None)`을 등록하는 경로가 됨).
|
||
**결론(`or None`을 쓰는 것)은 안 바뀜** — CC-3에서 관용구 자체는
|
||
"맞음"으로 확인됐으므로 근거 문장만 갱신.
|
||
- **사용자가 지시한 새 설계**:
|
||
1. **`Dispatch.drive`는 `v == None`을 확인하지 않고 전부 `Dispatch.process`에
|
||
태운다** — 특수 분기 제거. (이게 "가장 적절한 처리"라는 판단.)
|
||
2. **`NoneHandler`가 `k == number`인 경우를 스스로 처리**한다 — 실제 값
|
||
세팅은 하지 않고 넘어가되, 그 자리의 **`setLength(0)` /
|
||
`setOffsetSource(None)` 등록을 여기서** 한다.
|
||
3. `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처리를 먼저 해야하는 순서 의존이 있을 때에도 가능하게
|
||
> 하고자였음.
|
||
- **고쳐야 할 곳**:
|
||
1. **`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`에도 그대로 적용된다.
|
||
2. **`base/ref-plan.md`의 옛 `일반 Ref는 계속 Modifier/Store 어디든 자유롭게 들어감` 항목**(2026-08-18에 한정 서술 추가) — 이 문장이 "named 해시 키에 놓아도 leaf 바인딩이
|
||
된다"로 읽힌다. 실제 의미는 "Modifier 필드나 Store 값으로 **전달**될
|
||
수 있다"(=값으로서 어디든 흘러갈 수 있다)이지 "leaf 바인딩 자리가
|
||
아무 데나 된다"가 아니므로, 오해가 없게 다시 써야 한다.
|
||
3. **`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"*. 그리고 "자동 경로는 언마운트, 명시적으로
|
||
지우라고 한 것만 파괴"라는 일반 규칙.
|
||
- **어긋나는 지점**: 사용자는 `:List` reconcile의 `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`을 안 하면
|
||
안 지워지므로** 홀드만 해두면 살아있다.
|
||
- **결정해야 할 것**: (a) `:List` reconcile `nil` 리턴의 기본을 파괴로
|
||
되돌릴지, (b) `PopOnly`를 이번 설계에 넣을지 백로그로 뺄지, (c) 넣는다면
|
||
`updateFn` 반환 규약(현재는 "반환값을 해석하는 `:List` 내부 로직")이
|
||
어떻게 확장되는지.
|
||
|
||
---
|
||
|
||
## E. `base/effect-plan.md`
|
||
|
||
### E-2 — `:Unsubscribe()`가 leaf 바인딩에도 cleanup을 부르게 한 건 틀림 (`Subscribe`의 짝으로 좁혀야 함)
|
||
|
||
- **판정**: 아니오 — 계약을 **축소**해야 함.
|
||
- **사용자 답변 원문**:
|
||
> 표면적으로 맞아보이긴 하나, subscribe 한게 아니면 unsubscribe 는 지원하면
|
||
> 안 되거나, 적어도 리프 바운딩에선 그래선 안 됨. leaf 에 바운딩 된 경우,
|
||
> 특히 state<effect> 또는 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 ~= v` dedup). 그런데 `: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.md` 2-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`로 뒀다가 이후 삭제, 아래가 그 전문)**:
|
||
```luau
|
||
function Frame (prop: {MouseButton1Click: ((a: number)->())?})
|
||
end
|
||
|
||
Frame{
|
||
MouseButton1Click = function(a) -- a: number
|
||
end
|
||
}
|
||
```
|
||
props 테이블 **타입에 필드로 선언돼 있으면 콜백 파라미터가 그대로
|
||
추론된다** — 런타임 판별을 `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.md` 3번에 "사용자 확인 대기"로 열려 있던 항목의
|
||
답이며, **선택지 둘 중 하나가 아니라 제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.md` 3번에서 제거.
|
||
- **`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단계를 매번
|
||
손으로 써야 해서 편의성이 떨어진다.
|
||
- **후보 두 가지**:
|
||
1. `source:Apply(SetAndDispose(new))` — 콤비네이터. **단 여기서의
|
||
`Apply`는 `State`가 아니라 `Source`를 넘겨주는 함수여야 함**(사용자
|
||
명시) — 지금 확정된 `state:Apply(factory)`는 `factory(self)`에
|
||
`State`를 넘기므로, `Source` 전용 변형이 필요한지 같이 정해야 함.
|
||
2. `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.md` 9번)과 "사용자가 만든
|
||
`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.md` **1순위**로 열려 있던 항목.
|
||
- **확정 내용 두 갈래** (사용자 판정):
|
||
1. **네임스페이스/모듈 자체는 `DI` → `D`.** `D.Frame` / `D/init.luau` /
|
||
`D.InstSlot` / `D.FrameModifier`.
|
||
2. **"특수 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>> "Frame" {...} 로도 쓸 수 있고,
|
||
> D 에선 별다른 처리 없이 D.Frame = New<<Frame>> "Frame" :: (({...타입명시})
|
||
> -> Frame) 으로 쉽게 만들 수 있다는게 내 생각임.
|
||
- **제안 내용**:
|
||
1. **`New`는 커링** — `New(name)`이 생성자 함수를 반환하고, 그걸 다시
|
||
props 테이블로 호출: `New(name)({...})`.
|
||
2. **직접 사용도 같은 모양** — `New<<Frame>> "Frame" {...}`
|
||
(Lua 문법상 `New("Frame")({...})`).
|
||
3. **`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` 호출문까지 생성기가 찍는다.**
|
||
- **사용자 답변 원문**:
|
||
> 3. 코드로 자동 생성되는것이 맞는게, index 가지고만 하면
|
||
> MouseButton1Click 이 RBXScriptConnection 이 되어버림. 처리하기 힘들다
|
||
> 생각하는게, lsp 마다 Frame 타입을 어떻게 다루냐 다를 수 있음. 게다가
|
||
> T|State<T> 같은것 또한 처리해야하는데, 이걸 타입 함수로 다 처리하게
|
||
> 만드는것 보다 단순히 D 파일이 자동 생성되는게 좋다고 생각함. 그리고
|
||
> 코드 생성 산출물인가는 맞음, 전부 코드 생성이나, New 같은것도 생성기에서
|
||
> 같이 적어주어야할 부분.
|
||
- **⚠️ 이건 단순 확인이 아니라 `bind-system-plan.md`가 인용한 시그니처를
|
||
부정한다.** 그 문서는 PA님 코드를 그대로 인용해
|
||
`new<ClassName>(className): from<index<UIInstances, ClassName>>` —
|
||
즉 **타입 레벨 인덱싱**으로 클래스 타입을 뽑는 모양인데, 사용자가 든
|
||
세 가지 이유로 그 방식만으로는 부족하다:
|
||
1. **이벤트 필드가 콜백 타입이 안 된다.** Roblox 타입 정의에서
|
||
`MouseButton1Click`은 시그널 계열 타입이라, 인덱싱으로 뽑으면 그
|
||
타입이 그대로 나와버리고 quad가 원하는
|
||
`((...) -> ())?` 콜백 시그니처가 안 나옴. **이게 곧 `BS-2`가 요구한
|
||
"생성기가 이벤트 필드의 콜백 타입까지 만들어야 한다"의 직접적 근거** —
|
||
두 항목은 같은 문제의 양면이므로 반드시 같이 처리할 것.
|
||
2. **LSP마다 `Frame` 타입을 다루는 방식이 다를 수 있어** 타입 함수/인덱싱에
|
||
의존하는 게 위험하다.
|
||
3. **`T | State<T>`(그리고 `T | Tween<T>`, `None` 등)까지 타입 함수로
|
||
조립해야 하는데**, 그럴 바엔 `D` 파일을 통째로 생성하는 쪽이 단순하다.
|
||
- **따라서**: `D`는 타입뿐 아니라 **`D.Frame = New<<Frame>> "Frame" :: (...)`
|
||
라는 값 선언까지 생성기가 찍어내는 파일**이다. 손으로 쓰지 않는다.
|
||
- **반영**: `base/bind-system-plan.md`의 PA님 시그니처 인용을 이 결론에 맞게
|
||
다시 쓸 것 — "PA님 코드 그대로 채택"이라는 프레이밍 자체가
|
||
**타입 조립 방식에 한해서는 더 이상 정확하지 않음**(호출 모양은 그대로
|
||
채택, 타입은 생성으로 감).
|
||
|
||
**4. 생성 범위는 "GUI에 쓰이는 모든 인스턴스" — 전량은 부적합.**
|
||
- **사용자 답변 원문**:
|
||
> 4. 는 모든 인스턴스를 넣기는 부적합함. 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`의 마일스톤 분할이 이번 발견들과 맞는지.
|