decide(base): CreatedRef 폐기, PreRef pre-pass 확정, None 소진 정정, nil-hole 방지 관용구 확정

CreatedRef 이름 완전 폐기 — Ref(default)/PreRef(default)가 이미 Compose식
Type(args) 팩토리 생성자로 확정돼 있어 별도 래퍼가 불필요했음(2026-08-04
Ref 일반화 이전 시절의 잔재). bind-system-plan.md/ROADMAP.md/question.md/
architecture.md/documentation-content-map.md 전체 동기화.

PreRef pre-pass 구현 위치 확정 — 새 Dispatch.* 함수나 flatten에 얹지 않고
이미 확정된 Dispatch.drive(inst, flattened) 자신이 두 번 순회(pre-pass +
정상 두 패스)하는 것으로 확정. flatten에 얹는 안은 재바인드 시 flatten
재호출 가능성과 충돌해 기각. 복수 PreRef는 배열 index 순서, 동적 경로로
도착한 PreRef는 전용 Handler가 즉시 error.

소진 슬롯을 nil이 아니라 None으로 — 사용자가 Luau REPL 반례로 직접 발견:
키가 촘촘한 저범위 정수에서 벗어나면(nil 구멍 포함) 순회 순서가 index
오름차순을 안 지킴, table.insert가 쓰는 #t도 구멍 있는 테이블에서 정의
안 됨. Ref 콜백/대기자 배열과 PreRef pre-pass 둘 다 None 소진으로 정정.
배열 파트 None(순수 스킵)과 해시 파트 None(NoneHandler 경유)이 다른
경로임을 명시.

props.Modifier/props.Ref forwarding에 `or None` 필수 관용구 확정 —
nil-hole 위험도가 국소적이지 않고 테이블 전체에 영향을 준다는 게 이번
실측으로 드러나 방어 필요, 기존 None 스킵 메커니즘 재사용이라 새 코드
불필요.

부수 발견/보강: Modifier() 바닥 생성자가 문서에 없던 갭 보강, Brand 태그
목록에 RefTag/PreRefTag/ModifierTag가 빠져있던 갭 보강(isRef/isPreRef는
isState와 달리 단순 항등 — PreRef가 일반 Ref 핸들러에 안 잡히려면 필수).

archive/agent-mistake.md 관례에 따라 이번 세션의 실수(Modifier.Rounded(8)
stale 치환 시 잘못된 예시로 대체)는 CLAUDE.md 세션 로그에 경위 그대로
남김.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
qwreey 2026-08-07 20:29:36 +09:00
parent 98bd46af09
commit 33790df6ee
Signed by: qwreey
GPG key ID: D28DB79297A214BD
10 changed files with 531 additions and 77 deletions

View file

@ -135,7 +135,7 @@ quad/
│ │ └── Slot.luau # add/remove/clear 재조정 로직(추상 자식 참조 기준) │ │ └── Slot.luau # add/remove/clear 재조정 로직(추상 자식 참조 기준)
│ ├── LifetimeHandle.luau # Connected 계산 속성 "인터페이스"(타입/계약만) │ ├── LifetimeHandle.luau # Connected 계산 속성 "인터페이스"(타입/계약만)
│ ├── PerInstanceState.luau # per-instance 상태 저장 "인터페이스" │ ├── PerInstanceState.luau # per-instance 상태 저장 "인터페이스"
│ ├── Ref.luau # 범용 값 박스(.Value 읽기 + :Set()/:Callback()/:Wait() 셋)와 그 위에 얹힌 CreatedRef(숫자 슬롯 참가자) 특수화 │ ├── Ref.luau # 범용 값 박스(.Value 읽기 + :Set()/:Callback()/:Wait() 셋), `Ref(default)`를 children 배열 숫자 슬롯에 직접 놓으면 (v=Ref) 매치 핸들러가 바인드 — 별도 CreatedRef 래퍼 없음
│ ├── PreRef.luau # Ref 런타임 재사용 + children 배열 전용, Modifier/Store 타입 차단, 호이스팅되는 pre-pass 특수화(별도 파일, `bind-system-plan.md` "PreRef 신설" 절, 2026-08-07 여섯 번째 세션에서 분리) │ ├── PreRef.luau # Ref 런타임 재사용 + children 배열 전용, Modifier/Store 타입 차단, 호이스팅되는 pre-pass 특수화(별도 파일, `bind-system-plan.md` "PreRef 신설" 절, 2026-08-07 여섯 번째 세션에서 분리)
│ └── init.luau │ └── init.luau
└── quad-roblox/ └── quad-roblox/

View file

@ -5,7 +5,7 @@ Signal 미채택, Ref 역할)과 소스 트리 상 패키지 경계(디스패치
`quad-base`가 인터페이스로 소유, `quad-roblox`는 실제 구현만)까지 전부 `quad-base`가 인터페이스로 소유, `quad-roblox`는 실제 구현만)까지 전부
2026-08-04 세션에서 확정되어 `research/`에서 승격됨(`base/architecture.md`의 2026-08-04 세션에서 확정되어 `research/`에서 승격됨(`base/architecture.md`의
"구현 착수: 소스 트리 구조 확정" 절 참고). 남은 건 세부 시그니처(dependency "구현 착수: 소스 트리 구조 확정" 절 참고). 남은 건 세부 시그니처(dependency
array API, `CreatedRef` 모양) 뿐 — 구현 단계에서 자연히 정리됨. 원본: array API) 뿐 — 구현 단계에서 자연히 정리됨. 원본:
`.claude/initreq/raw-userinput.md` `.claude/initreq/raw-userinput.md`
"key와 value에 대한 바인드 연산은 pluggable 하도록 구성하기" / "스토어는 스토어를 "key와 value에 대한 바인드 연산은 pluggable 하도록 구성하기" / "스토어는 스토어를
저장 가능한가" / "Ref는 고민중" 절. v1의 문제점은 `reference/quad-v1-architecture.md` 저장 가능한가" / "Ref는 고민중" 절. v1의 문제점은 `reference/quad-v1-architecture.md`
@ -177,6 +177,13 @@ NoneHandler.process(inst, k, v) = process(inst, k, nil) -- 재귀 재호출
바인딩/등록 하나가 "지금 살아있어서 실행돼도 되는가"만 보는 별개의 바인딩/등록 하나가 "지금 살아있어서 실행돼도 되는가"만 보는 별개의
라이프타임 게이트(`base/lifecycle-pattern.md` "생명 바인드 유틸" 절) — 라이프타임 게이트(`base/lifecycle-pattern.md` "생명 바인드 유틸" 절) —
KV 매치와 무관. KV 매치와 무관.
**이 `NoneHandler`는 해시 파트(프로퍼티/이벤트) 전용 — 배열 파트에서
`None`을 만나는 건 완전히 다른 규칙(2026-08-07 열 번째 세션, "PreRef"
절 "호이스팅의 실제 구현" 참고).** 배열 파트의 `None`은 "빈 슬롯"
표시일 뿐 처리할 핸들러 자체가 없으므로, `Dispatch.drive`의 두 패스
루프 자신이 `NoneHandler`/`Dispatch.process`를 거치지 않고 바로
건너뜀 — 같은 센티널 값이지만 배열 파트냐 해시 파트냐에 따라 처리
경로가 다르다는 점에 유의.
`NoneHandler.isHandlable``v == None`(센티널 자체)을 잡는 것이지 `NoneHandler.isHandlable``v == None`(센티널 자체)을 잡는 것이지
`v == nil`이 아님 — 진짜 `nil`은 애초에 테이블 순회로 나올 수 없다는 게 `v == nil`이 아님 — 진짜 `nil`은 애초에 테이블 순회로 나올 수 없다는 게
이 문제의 출발점이었으므로, 매치 대상은 항상 `None` 마커. 이 문제의 출발점이었으므로, 매치 대상은 항상 `None` 마커.
@ -291,15 +298,21 @@ ref 타입처럼 생각하는 게 맞는 거 같음 — 그걸 처리하는 플
- Store는 이미 바깥에서 옵저빙 가능한 존재라 별도 취급 불필요 — Ref는 그와 - Store는 이미 바깥에서 옵저빙 가능한 존재라 별도 취급 불필요 — Ref는 그와
달리 "원하는 객체 자체를 직접 얻어오는" 경로. **얻어진 뒤에 그 참조를 어디에 달리 "원하는 객체 자체를 직접 얻어오는" 경로. **얻어진 뒤에 그 참조를 어디에
저장하고 어떻게 쓰는지는 라이브러리 책임 범위 밖**(사용자 자유). 저장하고 어떻게 쓰는지는 라이브러리 책임 범위 밖**(사용자 자유).
- **바인드 방법**: children을 배열 아이템으로 넣듯 `CreatedRef` 같은 - **바인드 방법**: children을 배열 아이템으로 넣듯 `Ref(default)`(또는
값을 숫자 키 슬롯에 넣는 방식(정확한 이름/시그니처는 미정, 예: `:Callback(fn)`을 미리 걸어둔 `Ref(default):Callback(fn)`) 인스턴스
`[1] = CreatedRef(function(inst) ... end)`) — `(v=Ref)` 매치 핸들러가 자체를 숫자 키 슬롯에 그대로 넣는 방식 — `(v=Ref)` 매치 핸들러가 이걸
이걸 처리함. **[정정, 2026-08-07 세 번째 세션]** 정확한 순서 보장(자식 처리함. **별도 `CreatedRef` 래퍼 함수는 없음(2026-08-07 아홉 번째
마운트 전/후, 프로퍼티보다 먼저)은 위치와 `PreRef` 타입으로 갈렸음 — 세션, 사용자 확인) — `Ref`/`PreRef`가 이미 Compose식 `Type(default)`
아래 "`phase` 옵션 폐기 → 위치로 표현, `PreRef` 신설" 절이 최신, 팩토리 생성자로 확정됐으므로("생성자 스타일 확정" 절), 그 결과를 그대로
원래 있던 "옵션(`{phase=...}`)으로 두 타이밍을 고른다"/"특수 처리 children 배열에 놓는 것 자체가 바인드 관용구.** 원래 "`CreatedRef` 같은
없는 평범한 참가자" 서술은 `archive/ref-phase-option-reversed.md` 이름 미정의 래퍼"로 서술했던 것은 Ref가 아직 "instance 얻는 통로"로
옮김. 좁게 정의됐던 시절(2026-08-04)의 잔재였고, 2026-08-06 Ref 일반화 이후
래퍼 자체가 불필요해졌는데 이름만 남아있던 것을 이번에 정리함.
**[정정, 2026-08-07 세 번째 세션]** 정확한 순서 보장(자식 마운트
전/후, 프로퍼티보다 먼저)은 위치와 `PreRef` 타입으로 갈렸음 — 아래
"`phase` 옵션 폐기 → 위치로 표현, `PreRef` 신설" 절이 최신, 원래 있던
"옵션(`{phase=...}`)으로 두 타이밍을 고른다"/"특수 처리 없는 평범한
참가자" 서술은 `archive/ref-phase-option-reversed.md`로 옮김.
- **왜 값이 아니라 콜백인가**: quad는 React처럼 렌더 함수가 계속 - **왜 값이 아니라 콜백인가**: quad는 React처럼 렌더 함수가 계속
재실행되지 않음(플레인 함수를 한 번 호출해 트리를 만들고 끝) — 그래서 재실행되지 않음(플레인 함수를 한 번 호출해 트리를 만들고 끝) — 그래서
"채워졌는지 매 렌더마다 다시 확인"하는 모델 자체가 없고, `useEffect` "채워졌는지 매 렌더마다 다시 확인"하는 모델 자체가 없고, `useEffect`
@ -348,8 +361,8 @@ ref 타입처럼 생각하는 게 맞는 거 같음 — 그걸 처리하는 플
존재 여부부터 체크하는 것과 같은 이유, Ref가 자식으로 전달되는 경우 존재 여부부터 체크하는 것과 같은 이유, Ref가 자식으로 전달되는 경우
채워지는 시점이 더 늦어질 수 있어서 "이미 채워졌는지" 확인이 항상 채워지는 시점이 더 늦어질 수 있어서 "이미 채워졌는지" 확인이 항상
필요함. `:Wait()`의 대기자 리스트와 콜백 리스트는 같은 구조 재사용 필요함. `:Wait()`의 대기자 리스트와 콜백 리스트는 같은 구조 재사용
가능(발화 후 해당 인덱스만 nil 처리, Luau의 일반화 for는 성긴 배열도 가능(발화 후 해당 인덱스만 **`None`으로 소진** — 아래 구현 디테일의
잘 순회함). 2026-08-07 열 번째 세션 정정 참고, 단순 `nil` 처리는 아님).
- **`:Wait(thread?)``thread` 인자(2026-08-07 여섯 번째 세션, 사용자 - **`:Wait(thread?)``thread` 인자(2026-08-07 여섯 번째 세션, 사용자
제안, 확정)**: 생략(`nil`)하면 `coroutine.running()`으로 호출 중인 제안, 확정)**: 생략(`nil`)하면 `coroutine.running()`으로 호출 중인
코루틴 자신을 캡처해 대기자로 등록하고 그 자리에서 `coroutine.yield()` 코루틴 자신을 캡처해 대기자로 등록하고 그 자리에서 `coroutine.yield()`
@ -363,20 +376,38 @@ ref 타입처럼 생각하는 게 맞는 거 같음 — 그걸 처리하는 플
블록되지 않고 계속 진행하고 싶은 경우. 구현은 정말 단순함 — `thread` 블록되지 않고 계속 진행하고 싶은 경우. 구현은 정말 단순함 — `thread`
`nil`이면 yield, 있으면 yield 안 함. `nil`이면 yield, 있으면 yield 안 함.
- **구현 디테일(2026-08-07 세 번째 세션 제안, 여섯 번째 세션에서 resume - **구현 디테일(2026-08-07 세 번째 세션 제안, 여섯 번째 세션에서 resume
payload 정정)**: 값이 새로 `:Set()`될 때, 같은 배열 하나를 payload 정정, 열 번째 세션에서 소진 방식 정정)**: 값이 새로 `:Set()`
`for i, v in <배열> do ... end`로 한 번만 순회하면서 `type(v) == 때, 같은 배열 하나를 `for i, v in <배열> do ... end`로 한 번만
"thread"`면 `:Wait()`가 만든 대기자로 보고 **`coroutine.resume(v, self)`** 순회하면서 `type(v) == "thread"``:Wait()`가 만든 대기자로 보고
(즉 값이 아니라 **Ref 자기 자신**을 resume 인자로 넘김 — 위 self-반환 **`coroutine.resume(v, self)`** (즉 값이 아니라 **Ref 자기 자신**을
관용구가 `:Wait()`의 yield 경로에서도 그대로 성립하게 하기 위해, resume 인자로 넘김 — 위 self-반환 관용구가 `:Wait()`의 yield
`coroutine.yield()`의 리턴값이 곧 `self`가 되도록 정정. 세 번째 경로에서도 그대로 성립하게 하기 위해, `coroutine.yield()`
세션 원안은 `value`를 넘기는 것으로 적혀 있었으나 이러면 리턴값이 곧 `self`가 되도록 정정. 세 번째 세션 원안은 `value`
`ref:Wait().Value`가 안 풀려서 이번 세션에 정정) 후 `[i] = nil` 넘기는 것으로 적혀 있었으나 이러면 `ref:Wait().Value`가 안 풀려서
(1회성 소진), 아니면 일반 콜백 함수로 보고 그냥 `v(value)`(콜백은 이번 세션에 정정) 후 **`[i] = None`**(**`nil`이 아님** — 아래
여전히 원래 값을 직접 받음, 소진 안 함, 계속 유지)로 분기하면 됨 — "왜 `nil`이 아니라 `None`인가" 참고), 아니면 일반 콜백 함수로 보고
대기자/콜백을 서로 다른 배열로 나눌 필요 없이 값 타입 하나로 분기 그냥 `v(value)`(콜백은 여전히 원래 값을 직접 받음, 소진 안 함, 계속
가능. 새 콜백/대기자 등록은 `table.insert`로 끝(빈 인덱스가 있어도 유지)로 분기하면 됨 — 대기자/콜백을 서로 다른 배열로 나눌 필요 없이
없어도 상관없이 다음 슬롯에 들어감, 성긴 배열이어도 일반화 `for` 값 타입 하나로 분기 가능(`type(v) == "thread"` → 대기자,
계속 잘 순회하므로 압축(compact)할 필요도 없음). `type(v) == "function"` → 콜백, 그 외/`None` → 빈 슬롯이라 스킵).
새 콜백/대기자 등록은 `table.insert`로 끝.
- **왜 `nil`이 아니라 `None`인가(2026-08-07 열 번째 세션, 사용자가 실제
Luau REPL로 반례 제시 후 정정) — 이전 서술("성긴 배열이어도 일반화
`for`가 계속 잘 순회하므로 압축 불필요")은 절반만 맞았음.** 대기자/콜백
자체는 순서가 안 중요해서(어느 게 먼저 fire되든 상관없이 전부 fire되기만
하면 됨) "잘 순회함"까지는 맞았지만, 두 가지를 놓쳤음: (1) 키가 촘촘한
저범위 정수(1,2,3,...)에서 벗어나 듬성듬성해지면(`nil`로 지운 슬롯도
포함) Luau/Lua 테이블이 그 키들을 해시 파트로 취급해 순회 순서가 해시
버킷 순서가 되어버림(사용자가 `{[1]=1,[2222]=2222,[211]=211,...}`
REPL 실측으로 확인 — 대기자/콜백 리스트 자체는 이 순서 소실이 문제
안 되지만, 순서가 실제로 중요한 다른 배열(`PreRef` pre-pass 등)엔
치명적). (2) `table.insert`가 내부적으로 쓰는 `#t`(length 연산자)는
Lua 명세상 구멍이 있는 테이블에서 **정의되지 않은 동작**이라, 다음
콜백/대기자 등록이 엉뚱한 인덱스에 들어가 기존 항목을 덮어쓸 위험이
있음 — 이건 대기자/콜백 리스트에도 실제로 해당하는 진짜 버그.
`None``nil`이 아닌 **실재하는 값**이라 그 슬롯이 "차 있다"는 사실
자체는 안 바뀌므로 두 문제 다 피함 — 소진된 슬롯도 여전히 non-nil
값을 갖고 있어 테이블이 "구멍 없는 시퀀스"라는 불변식이 깨지지 않음.
- **주의(문서화 대상, 방어 로직 없음)**: 이미 죽은(완료/에러난) thread를 - **주의(문서화 대상, 방어 로직 없음)**: 이미 죽은(완료/에러난) thread를
`:Wait(thread)`에 넘기면 나중에 `coroutine.resume`이 에러남 — 이건 `:Wait(thread)`에 넘기면 나중에 `coroutine.resume`이 에러남 — 이건
다른 UB 케이스들과 같은 결로 라이브러리가 방어하지 않고 호출부 책임으로 다른 UB 케이스들과 같은 결로 라이브러리가 방어하지 않고 호출부 책임으로
@ -397,12 +428,12 @@ ref 타입처럼 생각하는 게 맞는 거 같음 — 그걸 처리하는 플
풀면 됨 — React `useRef<HTMLDivElement>(null)`도 명시적 타입 인자 없이는 풀면 됨 — React `useRef<HTMLDivElement>(null)`도 명시적 타입 인자 없이는
같은 문제를 겪으므로 이미 널리 받아들여진 UX, quad가 새로 감수하는 같은 문제를 겪으므로 이미 널리 받아들여진 UX, quad가 새로 감수하는
트레이드오프 아님. 트레이드오프 아님.
- **`CreatedRef`와의 관계**: 둘은 상충하지 않음 — 이 절의 Ref가 범용 - **children 배열에 넣으면 dispatch가 자동으로 채워주는 것과의 관계**:
프리미티브, `CreatedRef(fn)`는 그 위에 얹힌 "children 배열에 넣으면 이 절의 Ref가 그 범용 프리미티브 자체 — 위 "바인드 방법" 절대로 `Ref`
dispatch가 자동으로 채워주는" 특수 편의 패턴(quad가 만든 instance에 인스턴스를 children 배열 숫자 슬롯에 그대로 놓으면 됨(quad가 만든
한정된 경우). 정확한 타이밍 보장은 옵션 값이 아니라 위치 기반 + `PreRef` instance에 한정된 경우). 정확한 타이밍 보장은 옵션 값이 아니라 위치
타입으로 표현됨 — 아래 "`phase` 옵션 폐기 → 위치로 표현, `PreRef` 신설" 기반 + `PreRef` 타입으로 표현됨 — 아래 "`phase` 옵션 폐기 → 위치로
절이 최신. 표현, `PreRef` 신설" 절이 최신.
- **해소됨 — 반복 재설정 가능(one-shot 아님), 사용자 확정.** React에서도 - **해소됨 — 반복 재설정 가능(one-shot 아님), 사용자 확정.** React에서도
자식이 재생성되는 경우 같은 방식(ref가 다시 채워짐)을 씀 — 예: 마우스 자식이 재생성되는 경우 같은 방식(ref가 다시 채워짐)을 씀 — 예: 마우스
호버/무브 시 `current` 확인 후 라벨 위치를 결정하는 라벨 컨테이너 호버/무브 시 `current` 확인 후 라벨 위치를 결정하는 라벨 컨테이너
@ -422,12 +453,15 @@ ref 타입처럼 생각하는 게 맞는 거 같음 — 그걸 처리하는 플
"범용 값 박스"로 넓어졌으므로, 진행 중인 용어 정리(`question.md` 1번) "범용 값 박스"로 넓어졌으므로, 진행 중인 용어 정리(`question.md` 1번)
때 이름이 여전히 맞는지 같이 재검토할 것. 때 이름이 여전히 맞는지 같이 재검토할 것.
### `phase` 옵션 폐기 → 위치로 표현, `PreRef` 신설 (2026-08-07 세 번째 세션) ### `phase` 옵션 폐기 → 위치로 표현, `PreRef` 신설 (2026-08-07 세 번째
세션 — 이 절이 당시 쓰던 `CreatedRef(fn, ...)` 래퍼 이름 자체도 이후
아홉 번째 세션에서 폐기됨, 위 "바인드 방법" 절 참고)
**`CreatedRef(fn, {phase="created"|"mounted"})`의 `phase` 옵션 자체를 **children 배열에 놓는 Ref에 `{phase="created"|"mounted"}` 옵션으로 두
없앤다.** 위 "확정된 디스패치 모델" 절에 새로 추가된 두 패스 보장(배열 타이밍을 고르게 하던 것 자체를 없앤다.** 위 "확정된 디스패치 모델" 절에
파트는 index 순서대로, 그 다음 해시 파트) 덕분에, 같은 인스턴스 안에서 새로 추가된 두 패스 보장(배열 파트는 index 순서대로, 그 다음 해시 파트)
**일반 `Ref`/`CreatedRef`를** 다른 children보다 앞/뒤 어디에 놓느냐가 덕분에, 같은 인스턴스 안에서 **일반 `Ref`를** 다른 children보다 앞/뒤
어디에 놓느냐가
이미 "그 형제가 마운트되기 전/후"를 그대로 결정함 — 각 자식은 자기 이미 "그 형제가 마운트되기 전/후"를 그대로 결정함 — 각 자식은 자기
서브트리까지 전부 동기적으로 마운트를 끝내야 다음 형제로 넘어가므로, 서브트리까지 전부 동기적으로 마운트를 끝내야 다음 형제로 넘어가므로,
"마지막에 놓기"만으로 "모든 자식 마운트 후" 의미가 공짜로 나옴. 별도 "마지막에 놓기"만으로 "모든 자식 마운트 후" 의미가 공짜로 나옴. 별도
@ -472,7 +506,7 @@ flatten된 값은 해시 파트(프로퍼티 키)로 존재하게 되고, Store
없음 — `State<Modifier>`를 UB로 보고 타입으로 막기로 한 것과 정확히 없음 — `State<Modifier>`를 UB로 보고 타입으로 막기로 한 것과 정확히
같은 원칙의 재적용. 같은 원칙의 재적용.
- **`PreRef`는 배열 안 위치의 영향을 안 받는다 — 호이스팅.** 일반 - **`PreRef`는 배열 안 위치의 영향을 안 받는다 — 호이스팅.** 일반
`Ref`/`CreatedRef`와 달리, 같은 인스턴스의 배열 파트 안에서 다른 `Ref`와 달리, 같은 인스턴스의 배열 파트 안에서 다른
children/`Ref`보다 뒤에 적었어도 그것들보다 먼저 fire됨(자바스크립트 children/`Ref`보다 뒤에 적었어도 그것들보다 먼저 fire됨(자바스크립트
함수 선언 호이스팅과 같은 느낌으로 문서화). 이유: PreRef의 존재 함수 선언 호이스팅과 같은 느낌으로 문서화). 이유: PreRef의 존재
목적 자체가 "이 인스턴스에 뭐가 됐든 일어나기 전에" 채워지는 것인데, 목적 자체가 "이 인스턴스에 뭐가 됐든 일어나기 전에" 채워지는 것인데,
@ -485,7 +519,96 @@ flatten된 값은 해시 파트(프로퍼티 키)로 존재하게 되고, Store
패스로 처리하면 됨 — 이 pre-pass는 오직 `PreRef` 타입만 골라내므로 패스로 처리하면 됨 — 이 pre-pass는 오직 `PreRef` 타입만 골라내므로
범위가 좁고, "확정된 디스패치 모델" 절의 두 패스 계약과 별개로 그 범위가 좁고, "확정된 디스패치 모델" 절의 두 패스 계약과 별개로 그
앞에 얹히는 것. 앞에 얹히는 것.
- **일반 `Ref`/`CreatedRef`는 계속 Modifier/Store 어디든 자유롭게 - **복수 `PreRef` 간 순서(2026-08-07 아홉 번째 세션, 사용자 확인) —
새 규칙 불필요, 배열 index 순서 그대로.** 같은 인스턴스에 `PreRef`
여럿 있으면, 이 pre-pass는 위 "props 순회 순서" 절이 이미 확정해둔
"배열 파트는 index 순서대로" 계약을 그대로 재사용해 리터럴 순서대로
fire하면 됨 — 서로 다른 우선순위/순서 개념을 별도로 만들 필요 없음
(호이스팅은 "PreRef 전체 대 나머지"에만 적용되는 규칙이지, "PreRef끼리"
에는 적용될 게 없음 — PreRef끼리는 그냥 평범한 배열 순회).
- **호이스팅의 실제 구현 = "물리적 재배치"가 아니라 "완전히 별도의
선행 스캔"(2026-08-07 아홉 번째 세션 후속, 사용자 질문에 답변).**
`Dispatch.drive(inst, flattened)`는 같은 `flattened` 배열을 **두 번
순회**한다 — (1) pre-pass: 배열 파트 전체를 index 순서대로 훑으며
`isPreRef(v)`인 슬롯을 찾아 그 자리에서 fire하고 즉시 **`flattened[i]
= None`**으로 소진(`nil`이 아님 — 위 "왜 `nil`이 아니라 `None`인가"
절과 같은 이유, 2026-08-07 열 번째 세션 정정: `nil`로 지우면 그
순간 테이블이 "구멍 있는" 상태가 되어 이어지는 (2)의 순회 순서
보장 자체가 깨질 위험이 있음 — 정확히 이 pre-pass가 의존하는 바로 그
보장이라 치명적). (2) 그 다음에야 비로소 평소의 배열→해시 두 패스가
**같은 테이블**을 다시 순회 — 이때 `None`으로 소진된 슬롯은 **정상
`Dispatch.process`/`NoneHandler` 경로를 안 타고 두 패스 루프 자신이
직접 건너뜀**(`if v == None then continue end`, 배열 파트 전용
특수 취급). **해시 파트의 `None`(Modifier 필드 명시적 지우기)과는
의미가 다름** — 해시 파트 `None`은 "이 프로퍼티 핸들러야, 방금
지워진 걸 알고 알아서 처리해"라는 의미 있는 재디스패치(`NoneHandler`
경유)지만, 배열 파트 `None`은 그냥 "여기 원래 아무것도 없었던 것과
같다"는 순수 빈 슬롯 표시라 처리할 핸들러 자체가 없음 — 재디스패치
없이 루프가 직접 스킵하는 게 맞음. "호이스팅"은 PreRef를 배열의 맨
앞으로 물리적으로 옮기는 게 아니라, **PreRef 전용 선행 루프가
통째로 먼저 끝난 뒤에야 나머지 처리가 시작된다는 뜻** — 그래서
소스에서 마지막 child로 적었어도 무조건 다른 모든 처리보다 먼저
fire됨. **PreRef 슬롯을 소진시키는 게 단순 최적화가 아니라 정확성
요건인 이유**: 아래 "동적 경로 가드" Handler가 `(v=PreRef)`
매치하면 무조건 `error`를 던지므로, pre-pass가 슬롯을 안 지우면
두 번째(정상) 패스가 이미 정당하게 처리된 그 PreRef를
`Dispatch.process`로 다시 넘기게 되고, 그러면 이 가드 Handler가
엉뚱하게 매치되어 **정상적인 PreRef 사용에도 에러가 터짐** — 소진은
이 오탐을 막기 위해 반드시 필요.
- **M0 스파이크 검증 항목 갱신(2026-08-07 열 번째 세션)**: 위 "props
순회 순서" 절은 `{a=1, 2, b=3}`**구멍 없는** 테이블에서 배열
파트가 해시 파트보다 먼저 나온다는 것만 실측 확인됨(2026-08-07 세
번째 세션). 같은 세션에서 사용자가 직접 `{[1]=1,[2222]=2222,
[211]=211,...}`류 **키가 듬성듬성한(sparse)** 테이블을 REPL로
실측해, 그런 테이블은 순회 순서가 index 오름차순이 전혀 아님(해시
버킷 순서)을 확인함 — 그래서 위 pre-pass는 (nil이 아니라) `None`으로
소진해 테이블을 "구멍 없이 촘촘한" 상태로 계속 유지하는 전략으로
이 위험을 원천 회피함(검증 불필요, 애초에 구멍을 안 만드므로).
**여전히 M0에서 검증해야 하는 건 다른 케이스**: `props.Modifier`/
`props.Ref`를 caller가 안 넘겨 생기는 리터럴 `nil`-hole(`{nil, ref,
child}`, 위 "지금 할 일" 우선순위1 항목)은 caller가 직접 쓰는 raw
Lua 리터럴이라 프레임워크가 `None`으로 대신 못 채워줌 — 이번 REPL
실측으로 그 케이스의 실제 위험도가 이전 서술("뒤 항목까지 무시될 수
있음", 국소적 피해로 서술돼 있었음)보다 훨씬 큼이 드러남: 구멍이 하나만
생겨도 **테이블 전체**가 해시 파트 취급으로 넘어가 그 인스턴스의
배열 파트 전체가 순서 보장을 잃을 수 있음. M0 스파이크에서 반드시
실측하고, 심각하면 "이런 nil-hole은 raw 리터럴로 하지 말고 항상
`props.Modifier or Modifier()`처럼 non-nil을 보장하라"는 컨벤션
문서화까지 검토할 것.
- **pre-pass는 어디 사는가 — `Dispatch.drive(inst, flattened)` 자신,
새 함수 불필요(2026-08-07 아홉 번째 세션, 사용자 제안 검토 후 확정).**
`Dispatch.drive`가 이미 `(inst, flattened)`를 받아 배열→해시 두 패스를
도는 함수로 확정돼 있으므로, 그 앞에 좁은 pre-pass 한 줄을 얹는 것만으로
충분 — `Handler.process`와 이름이 겹치는 새 `Dispatch.process(inst,
flatten, prerefs)`류 함수를 따로 만들 필요가 없음(그 이름은 이미
다른 뜻으로 쓰이는 `Dispatch.process(inst,k,v)` 오케스트레이터와 겹쳐서
안 좋음). **`flatten(nonFlatten) -> flatten` 함수 자체에 얹는 방안은
검토 후 기각** — flatten은 Modifier 값을 합치는 순수 변환(현재 `inst`
안 받음, `research/existing-instance-bind-plan.md`가 다루는 "이미
마운트된 Instance 재바인드 시 Default→실값 flatten을 다시 해야 하는가"
질문이 실제로 열려있어 flatten이 한 인스턴스 생애주기 동안 **여러 번
재호출될 가능성이 있음** — 여기에 PreRef fire를 얹으면 재바인드마다
PreRef가 또 fire되어 "이 인스턴스 하나의 construction 훅"이라는 PreRef의
정의 자체가 깨짐. `Dispatch.drive`는 최초 마운트 시 정확히 한 번만
불리는 게 이미 전제라 이 위험이 없음.
- **동적 경로로 도착한 PreRef는 런타임에도 명시적으로 에러
(2026-08-07 아홉 번째 세션, 사용자 제안 채택) — 아직 문서화 안 돼
있었음, 지금 확정.** 위 "Modifier 필드로 막는 이유"/"Source/Store
값으로 막는 이유" 절은 **타입 차단**만 다뤘음 — Luau 타입은 런타임에
지워지므로(`:Peek`/`Override`/버그로 타입을 우회해 PreRef가 Modifier나
Store 값으로 실제로 흘러들어오는 경우), 런타임에도 방어가 필요함.
전용 `Handler`를 하나 등록: `{ isHandlable = function(inst,k,v) return
isPreRef(v) end, process = function(inst,k,v) error("PreRef는 children
배열 리터럴에만 놓을 수 있음") end }` — `NoneHandler`와 같은 결의
"한 값 종류만 전담하는 Handler" 패턴 재사용, 새 메커니즘 아님. 이
Handler는 **`Dispatch.process`/`getHandler`의 정상 우선순위 스캔에
등록**되는 반면(pre-pass처럼 그 밖에서 도는 게 아님), 리터럴 배열의
`PreRef`는 pre-pass가 fire와 동시에 해당 슬롯을 소진(`None` 처리,
`nil` 아님)해 정상 두 패스 스캔에 다시 노출되지 않게 하므로, 이
Handler가 실제로
매치되는 경우는 오직 "타입이 막았어야 했는데 어떻게든 동적으로
새어들어온" 버그 케이스뿐 — 그래서 no-op이 아니라 즉시 `error`.
- **일반 `Ref`는 계속 Modifier/Store 어디든 자유롭게
들어감** — Store를 통해 나중에 도착하는 Ref는 그냥 도착한 그 순간 들어감** — Store를 통해 나중에 도착하는 Ref는 그냥 도착한 그 순간
처리하면 됨, phase 개념 자체가 필요 없음("만난 순간 처리"로 충분). 처리하면 됨, phase 개념 자체가 필요 없음("만난 순간 처리"로 충분).
- **quad v1의 `OnCreated` 특수 DI 키는 이식하지 않는다.** - **quad v1의 `OnCreated` 특수 DI 키는 이식하지 않는다.**
@ -677,7 +800,7 @@ stale하다" 수준이 아니라 **영영 갱신이 안 일어날 수 있음**.
**결정(2026-08-06 후속 세션, 사용자 확정)**: 별도 `ObserverHolder` **결정(2026-08-06 후속 세션, 사용자 확정)**: 별도 `ObserverHolder`
래퍼 타입은 안 만듦 — `state:Observer(fn)`가 반환하는 값 자체가 이미 래퍼 타입은 안 만듦 — `state:Observer(fn)`가 반환하는 값 자체가 이미
"children 배열에 바로 놓을 수 있는 leaf 값"이라 감쌀 필요가 없음. "children 배열에 바로 놓을 수 있는 leaf 값"이라 감쌀 필요가 없음.
`CreatedRef`와 완전히 같은 층위. **자유 함수 `Observer(state, fn)` `Ref`와 완전히 같은 층위. **자유 함수 `Observer(state, fn)`
아니라 메소드 `state:Observer(fn)`로 확정** — `state`가 항상 필요한 아니라 메소드 `state:Observer(fn)`로 확정** — `state`가 항상 필요한
필수 인자라 `:` 리시버 자리에 자연스럽게 들어가고(다른 형태면 인자 필수 인자라 `:` 리시버 자리에 자연스럽게 들어가고(다른 형태면 인자
두 개짜리 자유 함수가 되어 읽는 순서가 어색해짐), `architecture.md` 두 개짜리 자유 함수가 되어 읽는 순서가 어색해짐), `architecture.md`
@ -728,7 +851,7 @@ retract/Destroy되면 자동으로 정리됨.
8번)과 같은 결. `base/effect-plan.md`의 Effect도 동일하게 권장. 8번)과 같은 결. `base/effect-plan.md`의 Effect도 동일하게 권장.
- **base가 제공하는 것은 `isObserver`류 타입 판별자 하나** — children - **base가 제공하는 것은 `isObserver`류 타입 판별자 하나** — children
배열 dispatch가 숫자 슬롯 값을 훑을 때 "이게 Observer인가"를 판별해 배열 dispatch가 숫자 슬롯 값을 훑을 때 "이게 Observer인가"를 판별해
`CreatedRef`와 같은 방식으로 라이프사이클에 묶어주는 것 말고는 base가 `Ref`와 같은 방식으로 라이프사이클에 묶어주는 것 말고는 base가
더 해줄 일이 없음. 새 dispatch 메커니즘이 아니라 기존 children-array 더 해줄 일이 없음. 새 dispatch 메커니즘이 아니라 기존 children-array
참가자 패턴의 반복. 참가자 패턴의 반복.
- **콜백 실행은 기존 `canExecute` predicate로 게이팅**(Slot 생존 확인과 - **콜백 실행은 기존 `canExecute` predicate로 게이팅**(Slot 생존 확인과
@ -1388,7 +1511,8 @@ function Brand.get(x) return registry[x] end -- nil이면 quad가 모르는 값
-- 각 브랜드는 고유 테이블(빈 테이블이어도 됨) — 문자열 리터럴 아님 -- 각 브랜드는 고유 테이블(빈 테이블이어도 됨) — 문자열 리터럴 아님
local ObserverTag, EffectTag, TagTag, AttributeTag, TweenTag, BlockerTag, local ObserverTag, EffectTag, TagTag, AttributeTag, TweenTag, BlockerTag,
StateTag, SourceTag, StoreTag, SlotTag = {}, {}, {}, {}, {}, {}, {}, {}, {}, {} StateTag, SourceTag, StoreTag, SlotTag, RefTag, PreRefTag, ModifierTag =
{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}
-- 각 타입의 모든 생성 지점(Observer(...), Source(...), :With(...), Tag(...) 등)에서: -- 각 타입의 모든 생성 지점(Observer(...), Source(...), :With(...), Tag(...) 등)에서:
Brand.set(newHandle, ObserverTag) Brand.set(newHandle, ObserverTag)
@ -1426,6 +1550,22 @@ end
문서가 서로 모순돼 있었음). `base/modifier-plan.md`의 "별도 `isSource` 문서가 서로 모순돼 있었음). `base/modifier-plan.md`의 "별도 `isSource`
불필요" 서술도 같이 정정 대상. 불필요" 서술도 같이 정정 대상.
**갭 보강 — `isRef`/`isPreRef`/`isModifier`가 태그 목록에서 빠져있던 것
추가(2026-08-07 열 번째 세션).** 위 코드 예시가 원래 `RefTag`/
`PreRefTag`/`ModifierTag`를 안 만들어뒀는데, 이 문서 곳곳(PreRef 절의
`isPreRef(v)`, `component-composition-plan.md``isModifier(v)` 등)이
이미 이 predicate들이 존재한다고 전제하고 써왔음 — 실제로 만들어야 하는
게 맞아서 태그 목록에 추가. **`isRef`/`isPreRef`는 `isObserver`와 같은
단순 항등**(`isRef(x) = Brand.get(x) == RefTag`, `isPreRef(x) =
Brand.get(x) == PreRefTag`) — `isState`처럼 집합 멤버십이 아님, 즉
**`isRef(preRefInstance)`는 `false`.** 이게 중요한 이유: `PreRef`
"Ref 런타임을 재사용하되 브랜드 태그만 다름"이라고 해서 `isRef`
`PreRef`도 통과시키면, 일반 `(v=Ref)` 매치 핸들러가 `PreRef` 인스턴스도
집어삼켜버려 위 "PreRef" 절이 요구하는 "일반 Ref 경로를 절대 타면 안
됨"이 깨짐 — `Ref`/`PreRef`는 State/Source 같은 상하위 관계가 아니라
서로 배타적인 형제 브랜드. `isModifier`도 같은 단순 항등
(`Brand.get(x) == ModifierTag`).
**`None`은 이 레지스트리에 안 들어감 — 싱글턴이라 항등 비교로 충분.** **`None`은 이 레지스트리에 안 들어감 — 싱글턴이라 항등 비교로 충분.**
`Observer`/`Store`처럼 인스턴스가 여러 개 생기는 타입과 달리 `None` `Observer`/`Store`처럼 인스턴스가 여러 개 생기는 타입과 달리 `None`
quad 전체에서 딱 하나만 존재하므로 weak table 조회보다 `x == None` quad 전체에서 딱 하나만 존재하므로 weak table 조회보다 `x == None`
@ -1469,10 +1609,6 @@ State<any> ... end`처럼 런타임 검증 뒤 명시적 캐스팅을 붙이는
모듈 이름** — 방향은 전부 확정, 이름만 구현 단계에서 남음(`On` 모듈은 모듈 이름** — 방향은 전부 확정, 이름만 구현 단계에서 남음(`On` 모듈은
이벤트 바인딩이 PA님 방식으로 바뀌며 아예 불필요해짐 — 위 "인스턴스 생성 / 이벤트 바인딩이 PA님 방식으로 바뀌며 아예 불필요해짐 — 위 "인스턴스 생성 /
이벤트 네이밍" 절 참고). 이벤트 네이밍" 절 참고).
- **`CreatedRef`(가칭)의 정확한 함수 이름** — children 배열에 아이템으로
넣는다는 방향, 그리고 타이밍은 옵션 값이 아니라 위치 + `PreRef` 타입으로
표현한다는 것까지 확정(위 "`phase` 옵션 폐기 → 위치로 표현, `PreRef`
신설" 절), 정확한 API 이름만 남음.
- **매 `process()` 호출마다 우선순위 스캔 비용** — 실제 구현/벤치마크 단계에서 - **매 `process()` 호출마다 우선순위 스캔 비용** — 실제 구현/벤치마크 단계에서
확인 필요(디자인 자체는 확정됐으므로 더 이상 사용자 확인 대상 아님, 구현 확인 필요(디자인 자체는 확정됐으므로 더 이상 사용자 확인 대상 아님, 구현
검증 대상). 검증 대상).

View file

@ -198,7 +198,32 @@ Modifier/Ref/자식을 구분"이 이미 v1의 유일한 해법이었던 패턴
명시적으로 원하는 내부 `Frame{...}` 호출의 배열 자리에 다시 꽂아넣음 명시적으로 원하는 내부 `Frame{...}` 호출의 배열 자리에 다시 꽂아넣음
(`return Frame { props.Modifier, props.Ref, ... }`) — **사용자 확정** (`return Frame { props.Modifier, props.Ref, ... }`) — **사용자 확정**
("결과적으로 함수 구현에선 타입을 멀쩡히 지정하는게 더 중요하니 네임드가 ("결과적으로 함수 구현에선 타입을 멀쩡히 지정하는게 더 중요하니 네임드가
맞는듯"). Compose(named `modifier` 파라미터 강제, 린트로 감시)와 맞는듯").
**⚠️ 필수 관용구 — `props.Modifier or None`/`props.Ref or None`으로
써야 함, 맨 리터럴로 꽂으면 안 됨(2026-08-07 열 번째 세션, `nil`-hole
버그 실측 후 확정).** caller가 `props.Modifier`/`props.Ref`를 안 넘기면
`nil`인데, `{nil, props.Ref, child}`처럼 Lua 배열 리터럴에 `nil`이 그대로
들어가면 그 순간 테이블의 배열 파트 전체가 순회 순서 보장을 잃을 위험이
있음(`base/bind-system-plan.md` "왜 `nil`이 아니라 `None`인가" 절 — Luau
REPL 실측으로 확인된 실제 버그, 국소적 피해가 아니라 테이블 전체에 영향).
그래서 **컴포넌트 저작자는 항상 `or None`으로 감싸서 넘겨야 함**:
```luau
return Frame { props.Modifier or None, props.Ref or None, child }
```
- **왜 `Modifier()`(빈 modifier 생성)가 아니라 `None`인가**: 별도 할당이
필요 없고, 기존 메커니즘을 그대로 재사용함 — `flatten` 단계는 애초에
`isModifier(v)`가 거짓인 값은 그냥 건드리지 않고 통과시키므로
(`None`은 Modifier가 아니라서 자동으로 이 경로), `props.Modifier or
None`이 최종적으로 배열 파트에 `None`인 채로 남으면 두 패스 루프
자신의 array-part `None`-스킵 규칙(위 "PreRef" 절)이 그대로 적용돼
아무 일도 안 일어남 — 새 특수 케이스 코드가 하나도 안 늘어남.
- 이 관용구는 컴포넌트 저작자가 **직접 챙겨야 하는 규율**(base가 강제로
검증해줄 방법은 없음, Lua는 이런 걸 린트로만 잡을 수 있음) — quad
문서화(초심자 가이드/`props.Modifier`/`props.Ref` 절)에 필수 패턴으로
명시할 것, `research/documentation-content-map.md`에 반영 필요.
Compose(named `modifier` 파라미터 강제, 린트로 감시)와
Fusion/Vide(named prop 전달, `[Children]`류 예약 키)가 서로 다른 이유로 전부 Fusion/Vide(named prop 전달, `[Children]`류 예약 키)가 서로 다른 이유로 전부
같은 결론에 도달한 유일한 실용적 패턴 — quad가 발명한 게 아니라 선례가 같은 결론에 도달한 유일한 실용적 패턴 — quad가 발명한 게 아니라 선례가
수렴하는 지점(위 "프레임워크 사례 조사" 절 참고). 수렴하는 지점(위 "프레임워크 사례 조사" 절 참고).

View file

@ -31,6 +31,14 @@ pluggable로 만들면 여러 modifier가 반응형으로 같은 키를 계속
cascade 문제가 그대로 오는데, 이건 이미 확정된 "Store 바인드 변경은 전체 cascade 문제가 그대로 오는데, 이건 이미 확정된 "Store 바인드 변경은 전체
교체, 부분 오버레이 없음"(`base/architecture.md` 3번) 원칙과 충돌함. 교체, 부분 오버레이 없음"(`base/architecture.md` 3번) 원칙과 충돌함.
**flatten이 배열 항목 중 뭐가 Modifier인지 판별하는 수단 — `isModifier`
(`Brand` 기반, 2026-08-07 열 번째 세션 명시).** 다른 모든 nominal 타입
판별과 같은 메커니즘(`bind-system-plan.md`의 `Brand` 절) 재사용 — flatten은
배열을 훑으며 `isModifier(v)`가 참인 항목만 필드를 뽑아 merge하고, 나머지는
전혀 안 건드리고 그대로 배열 파트에 남겨둠(그래서 `None`처럼 Modifier가
아닌 값은 flatten을 그냥 통과함 — `component-composition-plan.md`
"필수 관용구" 절 참고).
관련: 이미 마운트된 Instance에 재바인드할 때 Default→실값 flatten을 다시 관련: 이미 마운트된 Instance에 재바인드할 때 Default→실값 flatten을 다시
해야 하는지/clone이 필요한지는 별개 미정 문제로 해야 하는지/clone이 필요한지는 별개 미정 문제로
`research/existing-instance-bind-plan.md`의 "Default 값과 얽히는 문제" 절 `research/existing-instance-bind-plan.md`의 "Default 값과 얽히는 문제" 절
@ -112,6 +120,16 @@ mutable하게 구현하면 같은 modifier 레퍼런스를 공유하는 형제
오염 버그를 원천 차단하는 쪽이 라이브러리 복잡도/사용자 편의 양쪽에서 오염 버그를 원천 차단하는 쪽이 라이브러리 복잡도/사용자 편의 양쪽에서
낫다고 판단 — **immutable 기본으로 확정**. 낫다고 판단 — **immutable 기본으로 확정**.
**바닥 생성자 — `Modifier()`(필드 없는 빈 인스턴스, 2026-08-07 열 번째
세션 명시).** 지금까지 문서 어디에도 modifier 체이닝이 시작되는 첫
호출(`props.Modifier`처럼 이미 존재하는 modifier를 이어받지 않고 처음부터
만드는 경우)이 명시된 적이 없었던 갭 — `Source(default)`/`Ref(default)`/
`Store({defaults})`와 같은 "`Type(args)` 팩토리" 관습을 그대로 적용하면
됨, Modifier는 초기 필드가 필수가 아니므로 `args`가 비어도 되는
`Modifier()`. `mod:FontSize(20)`처럼 체이닝하는 모든 예시가 실은 이
`Modifier()`가 만든 빈 인스턴스 위에서 시작함. `base/store-semantics.md`
"독립 존재 가능한 프리미티브" 절의 예시 목록도 이걸로 갱신.
### 4. Setter는 리터럴 값과 변환 함수 둘 다 받음, 별도 Getter는 없음 ### 4. Setter는 리터럴 값과 변환 함수 둘 다 받음, 별도 Getter는 없음
`:FontSize(value)`(리터럴) / `:FontSize(function(current) return `:FontSize(value)`(리터럴) / `:FontSize(function(current) return

View file

@ -73,8 +73,9 @@ pull-recompute)·`:Compute` 인자 규칙·State 쓰기 금지·`Source` 독립
- **독립 존재 가능한 프리미티브** — Source, Ref, Store, Modifier. 다른 - **독립 존재 가능한 프리미티브** — Source, Ref, Store, Modifier. 다른
무언가 없이 그 자체로 `Type(args)` 팩토리 함수로 만들어짐(`Source(default)`/ 무언가 없이 그 자체로 `Type(args)` 팩토리 함수로 만들어짐(`Source(default)`/
`Ref(default)`/`Store({defaults})`/`mod:UICorner(8)`, 위 "생성자 `Ref(default)`/`Store({defaults})`/`Modifier()`, 위 "생성자
스타일 확정" 참고). 스타일 확정" 참고 — `Modifier()`는 빈 인스턴스, 실제 필드는
`mod:UICorner(8)`류 체이닝으로 그 위에 얹음).
- **원천에 종속된 파생 데이터** — State, Observer. 자기 혼자 존재할 수 - **원천에 종속된 파생 데이터** — State, Observer. 자기 혼자 존재할 수
없고 항상 특정 원천(Source/다른 State)에 의존 — 그래서 이 둘은 자유 없고 항상 특정 원천(Source/다른 State)에 의존 — 그래서 이 둘은 자유
함수 생성자가 없고, 항상 원천에 대한 메소드 호출로만 얻어진다 함수 생성자가 없고, 항상 원천에 대한 메소드 호출로만 얻어진다

View file

@ -64,13 +64,14 @@ context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만**
- **`Slot`(2순위)**: Vue의 "slot"(콘텐츠 주입 지점)과 이름은 같지만 의미가 - **`Slot`(2순위)**: Vue의 "slot"(콘텐츠 주입 지점)과 이름은 같지만 의미가
다름(quad의 Slot은 자식 배열 재조정 프리미티브) — Vue 배경 있는 사람이 다름(quad의 Slot은 자식 배열 재조정 프리미티브) — Vue 배경 있는 사람이
헷갈릴 수 있음. 헷갈릴 수 있음.
- **`CreatedRef`/`canExecute`(3순위, 사소함)**: `CreatedRef`는 과거분사형이라 - **`canExecute`(3순위, 사소함)**: 실제로 "이 핸들이 아직 살아있나" 확인인데
생성자처럼 안 읽힘. `canExecute`는 실제로 "이 핸들이 아직 살아있나" 이름이 범용 권한 체크처럼 들림 — `isAlive` 쪽이 더 직접적.
확인인데 이름이 범용 권한 체크처럼 들림 — `isAlive` 쪽이 더 직접적.
**(2026-08-07 추가)** `PreRef`(children 배열 전용, Modifier/Store에 **(2026-08-07 추가)** `PreRef`(children 배열 전용, Modifier/Store에
못 들어가는 Ref 특수화 — `base/bind-system-plan.md` "`phase` 옵션 폐기 → 못 들어가는 Ref 특수화 — `base/bind-system-plan.md` "`phase` 옵션 폐기 →
위치로 표현, `PreRef` 신설" 절)도 신규 이름이라 이 라운드에 같이 재검토 위치로 표현, `PreRef` 신설" 절)도 신규 이름이라 이 라운드에 같이 재검토
대상. 대상. (`CreatedRef`는 더 이상 이 목록에 없음 — 별도 이름 자체가 폐기됨,
`Ref(default)`/`PreRef(default)`를 children 배열에 직접 놓는 것으로
확정, 아래 "지금까지 확정된 것" 참고.)
- **`Ref`(3순위, 2026-08-06 추가)**: 정의가 "quad가 만든 instance를 얻는 - **`Ref`(3순위, 2026-08-06 추가)**: 정의가 "quad가 만든 instance를 얻는
통로"에서 "아무 사용자 값이나 담는 범용 값 박스"로 넓어져서(`base/ 통로"에서 "아무 사용자 값이나 담는 범용 값 박스"로 넓어져서(`base/
bind-system-plan.md` "Ref 일반화" 절), 이름이 여전히 넓어진 의미에 bind-system-plan.md` "Ref 일반화" 절), 이름이 여전히 넓어진 의미에
@ -132,10 +133,11 @@ context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만**
없이 나란히 적용됨.** 왜 이 경우만 예외로 방어하는지 명문화 필요, 또는 없이 나란히 적용됨.** 왜 이 경우만 예외로 방어하는지 명문화 필요, 또는
Luau에서 실제 타입 차단이 가능한지부터 확인(안 되면 그냥 UB로 격하) — Luau에서 실제 타입 차단이 가능한지부터 확인(안 되면 그냥 UB로 격하) —
문서모순 절 + 우선순위2-2. 문서모순 절 + 우선순위2-2.
- **`props.Modifier`/`props.Ref` forwarding 관례가 Lua 배열 리터럴 - ~~`props.Modifier`/`props.Ref` forwarding 관례가 Lua 배열 리터럴
nil-hole 함정에 그대로 노출됨** — caller가 Modifier/Ref를 안 넘기면 nil-hole 함정에 그대로 노출됨~~ — **반영 완료(2026-08-07 열 번째
`{nil, ref, child}`에서 뒤 항목까지 통째로 무시될 수 있는 버그 클래스. 세션)**. `props.Modifier or None`/`props.Ref or None` 관용구를 필수로
M0 스파이크에 이 케이스(안 넘기는 경우)를 반드시 포함시킬 것 — 우선순위1-5. 확정(`base/component-composition-plan.md` "필수 관용구" 절) — M0에선
이 관용구 자체가 타입/런타임 양쪽에서 문제없이 동작하는지만 검증.
- **`canExecute`/`Connected`의 실제 구현 방식(Parent==nil vs Connection. - **`canExecute`/`Connected`의 실제 구현 방식(Parent==nil vs Connection.
Connected vs Destroying 플래그)이 미확정인데 이미 Slot/Observer/store-bind Connected vs Destroying 플래그)이 미확정인데 이미 Slot/Observer/store-bind
retract 전역에 재사용 확정됨** — M2/M3 착수 전 실측 필요 — 우선순위1-6. retract 전역에 재사용 확정됨** — M2/M3 착수 전 실측 필요 — 우선순위1-6.

View file

@ -34,7 +34,7 @@
8. **컴포넌트 경계 넘기기**`props.Modifier`/`props.Ref` named parameter 패턴 (`component-composition-plan.md`) 8. **컴포넌트 경계 넘기기**`props.Modifier`/`props.Ref` named parameter 패턴 (`component-composition-plan.md`)
9. **이벤트** — self(Instance) 안 받음, 문자열 키(`Frame { MouseButton1Click = fn }`) (`bind-system-plan.md`) 9. **이벤트** — self(Instance) 안 받음, 문자열 키(`Frame { MouseButton1Click = fn }`) (`bind-system-plan.md`)
10. **생명주기** — GC 위임(수동 정리 불필요), Destroy 이후 대상 재사용 금지 (`lifecycle-pattern.md`) 10. **생명주기** — GC 위임(수동 정리 불필요), Destroy 이후 대상 재사용 금지 (`lifecycle-pattern.md`)
11. **Ref 기초** — 외부 관리 Instance 참조/마이그레이션용, `CreatedRef(fn)` + 배열 위치로 자식 전/후 표현, "프로퍼티보다도 먼저" 필요할 때만 `PreRef`(2026-08-07 세 번째 세션, `phase` 옵션 폐기) (`architecture.md`, `bind-system-plan.md`) 11. **Ref 기초** — 외부 관리 Instance 참조/마이그레이션용, `Ref(default):Callback(fn)`을 children 배열 숫자 슬롯에 직접 놓기 + 배열 위치로 자식 전/후 표현, "프로퍼티보다도 먼저" 필요할 때만 `PreRef`(2026-08-07 세 번째 세션, `phase` 옵션 폐기) (`architecture.md`, `bind-system-plan.md`)
12. **파생값 최소 예시**`:With(...)` + `:Compute(fn)` 기본형 (`bind-system-plan.md`, `store-semantics.md`) 12. **파생값 최소 예시**`:With(...)` + `:Compute(fn)` 기본형 (`bind-system-plan.md`, `store-semantics.md`)
13. **Tween 기초**`[Tween(key, ...)] = storeValue`, 취소 시 현재 보간값에서 자연스럽게 이어짐 (`research/tween-plan.md`) 13. **Tween 기초**`[Tween(key, ...)] = storeValue`, 취소 시 현재 보간값에서 자연스럽게 이어짐 (`research/tween-plan.md`)
14. **UI 숏핸드(quad-roblox 한정)**`UICorner`/`UIPadding`/`UIPaddingOffset`/`UIScale` 인라인 키 (`base/ui-shorthand-plan.md`) 14. **UI 숏핸드(quad-roblox 한정)**`UICorner`/`UIPadding`/`UIPaddingOffset`/`UIScale` 인라인 키 (`base/ui-shorthand-plan.md`)
@ -67,13 +67,13 @@ v1 폐기 API/버그/구조 결함 전부 v2 설계를 정당화하는 내부
그때만 재사용 가치 있음 — 지금 3축 어디에도 해당 없음. 그때만 재사용 가치 있음 — 지금 3축 어디에도 해당 없음.
### bind-system-plan.md (943줄, 최대 문서) ### bind-system-plan.md (943줄, 최대 문서)
- 초심자: Source/Store/State 기본 정의+생성자, State 읽기 전용 규칙 / dot-access가 값 읽기 1급 경로 / `:With`+`:Compute` 최소 사용법 / Ref 기본 개념+`CreatedRef` / 이벤트 self 미채택 기본 규칙+문자열 키 / 인스턴스 생성(제네릭+정적 필드) / 라이브러리 초기화 3줄(`RobloxFactory(QuadBase)`) - 초심자: Source/Store/State 기본 정의+생성자, State 읽기 전용 규칙 / dot-access가 값 읽기 1급 경로 / `:With`+`:Compute` 최소 사용법 / Ref 기본 개념(children 배열에 직접 놓기, 별도 `CreatedRef` 없음) / 이벤트 self 미채택 기본 규칙+문자열 키 / 인스턴스 생성(제네릭+정적 필드) / 라이브러리 초기화 3줄(`RobloxFactory(QuadBase)`)
- api: `state:Observer(fn)` 사용법(→심화: weak-table 내부 인덱싱) / `:Subscribe()`/`:Unsubscribe()` 시그니처(→심화: 강참조 레지스트리 구조) / Ref 일반화 표면 API(→심화: "왜 값이 아니라 콜백인가") / 이벤트 store-bind 존재+권장 안 함 가이드(→심화: 엔지니어링 비용 근거) / 핸들러 4종 계약(`isHandlable`/`priority`/`process`/`retract`) / `Attribute<T>` 특수 키 후보(미확정 명시 필요) - api: `state:Observer(fn)` 사용법(→심화: weak-table 내부 인덱싱) / `:Subscribe()`/`:Unsubscribe()` 시그니처(→심화: 강참조 레지스트리 구조) / Ref 일반화 표면 API(→심화: "왜 값이 아니라 콜백인가") / 이벤트 store-bind 존재+권장 안 함 가이드(→심화: 엔지니어링 비용 근거) / 핸들러 4종 계약(`isHandlable`/`priority`/`process`/`retract`) / `Attribute<T>` 특수 키 후보(미확정 명시 필요)
- 심화: push-invalidate/pull-recompute 전파 모델+"관측해야 실체화된다" 원칙+`previous` 캐비엇 / **왜 State를 Modifier처럼 플래튼하지 않는가**(이미 문서화 완료, 아래 3번 참고) / Store가 Store를 못 담는 이유 / 이벤트 self 미채택 4가지 근거 / store-bind 재귀 래핑 내부 메커니즘, retract가 Destroy 시 호출 안 되는 이유 / 같은 팩토리 재호출 no-op·다른 팩토리 충돌 에러 내부 안전장치 - 심화: push-invalidate/pull-recompute 전파 모델+"관측해야 실체화된다" 원칙+`previous` 캐비엇 / **왜 State를 Modifier처럼 플래튼하지 않는가**(이미 문서화 완료, 아래 3번 참고) / Store가 Store를 못 담는 이유 / 이벤트 self 미채택 4가지 근거 / store-bind 재귀 래핑 내부 메커니즘, retract가 Destroy 시 호출 안 되는 이유 / 같은 팩토리 재호출 no-op·다른 팩토리 충돌 에러 내부 안전장치
- skip: quad2-try 리서치 결과 섹션 전체(OOP 상속/커스텀 파서/Slot 스텁/`Pipe` 폐기 이력) / PA님 코드 교차검증 절(역사적 검증 기록) / "남은 열린 질문"/"확정된 것" 메타 요약 - skip: quad2-try 리서치 결과 섹션 전체(OOP 상속/커스텀 파서/Slot 스텁/`Pipe` 폐기 이력) / PA님 코드 교차검증 절(역사적 검증 기록) / "남은 열린 질문"/"확정된 것" 메타 요약
### component-composition-plan.md / module-lifecycle-plan.md ### component-composition-plan.md / module-lifecycle-plan.md
- 초심자: 컴포넌트=순수 함수 / 리프 프로퍼티엔 State만 바인딩 / `props.Modifier`/`props.Ref` named parameter 경계 전달 / `InitRoblox(Module)` 팩토리 초기화 - 초심자: 컴포넌트=순수 함수 / 리프 프로퍼티엔 State만 바인딩 / `props.Modifier`/`props.Ref` named parameter 경계 전달(**`props.Modifier or None`/`props.Ref or None` 필수 관용구 — 안 쓰면 nil-hole 버그, 2026-08-07 열 번째 세션 확정**) / `InitRoblox(Module)` 팩토리 초기화
- api: State(파생, 읽기전용) vs Source(원본, 쓰기가능) 경계 요약(→심화) / Slot 반환 컴포넌트는 Modifier/Ref 파라미터 미선언 / `Modifier.Override(mod1, mod2, ...)` 유틸(구 `Merge`, `props.Modifier` 단일 슬롯용 특수 상황으로 한정 소개 — 아래 modifier-plan.md 절 참고) / Bind는 유일 슬롯(재호출 no-op, 충돌 에러, →심화) / `:With`/`:Compute`로 파생 State 생성 시그니처 / 모듈 싱글톤 스코프 - api: State(파생, 읽기전용) vs Source(원본, 쓰기가능) 경계 요약(→심화) / Slot 반환 컴포넌트는 Modifier/Ref 파라미터 미선언 / `Modifier.Override(mod1, mod2, ...)` 유틸(구 `Merge`, `props.Modifier` 단일 슬롯용 특수 상황으로 한정 소개 — 아래 modifier-plan.md 절 참고) / Bind는 유일 슬롯(재호출 no-op, 충돌 에러, →심화) / `:With`/`:Compute`로 파생 State 생성 시그니처 / 모듈 싱글톤 스코프
- 심화: v1 `Extend` 자동 store 소유 폐지 이유(React 벤치마킹) / Source가 State를 구조적으로 만족하는 서브타입 설계(2026-08-06 후속 세션 — `StoreSource` 프록시 중간안은 폐기되고 이걸로 대체됨, `store-semantics.md` 참고) / named-parameter 경계 방식 채택 이유(Compose/Fusion/Vide/v1 선례 수렴) / 다중 루트 반환 개념 제거 근거 / 팩토리 초기화 패턴 채택 이유(RBVM `InitNamespace` 반례) / Store 책임 분리(base가 `LifetimeHandle` 소유) / v1 named 체이닝 연산 폐기 - 심화: v1 `Extend` 자동 store 소유 폐지 이유(React 벤치마킹) / Source가 State를 구조적으로 만족하는 서브타입 설계(2026-08-06 후속 세션 — `StoreSource` 프록시 중간안은 폐기되고 이걸로 대체됨, `store-semantics.md` 참고) / named-parameter 경계 방식 채택 이유(Compose/Fusion/Vide/v1 선례 수렴) / 다중 루트 반환 개념 제거 근거 / 팩토리 초기화 패턴 채택 이유(RBVM `InitNamespace` 반례) / Store 책임 분리(base가 `LifetimeHandle` 소유) / v1 named 체이닝 연산 폐기
- skip: Compose/Fusion/Vide/v1 프레임워크 비교 원자료 / provider/processor 네이밍 미정 등 열린 질문 메모 - skip: Compose/Fusion/Vide/v1 프레임워크 비교 원자료 / provider/processor 네이밍 미정 등 열린 질문 메모
@ -180,6 +180,23 @@ additional-primitives-plan.md`의 "문서화 백로그" 절이 원자료)**:
최우선 목표로 뒀는가 — 위 `심화` 3번(`왜 push-invalidate/pull-recompute 최우선 목표로 뒀는가 — 위 `심화` 3번(`왜 push-invalidate/pull-recompute
인가`)을 더 깊게 확장, `Blocker` 같은 파생 프리미티브가 이 목표 위에서 인가`)을 더 깊게 확장, `Blocker` 같은 파생 프리미티브가 이 목표 위에서
왜 자연스럽게 나왔는지까지 포함하는 설계 철학 에세이 왜 자연스럽게 나왔는지까지 포함하는 설계 철학 에세이
6. **왜 배열/해시 두 패스 순서를 안 뒤집는가, `PreRef`는 왜 그 예외로
따로 필요한가** (2026-08-07 세 번째 세션 원자료, `bind-system-plan.md`
"`phase` 옵션 폐기" 절 마지막 항목이 이 자리를 지목해뒀던 것 — 지금까지
여기 안 옮겨져 있었음) — "프로퍼티/이벤트가 항상 children/Ref보다
나중"이라는 순서를 고치는 대신 `PreRef`라는 별도 타입으로 예외를
빼낸 선택 자체가 에세이 소재. **여기 곁들일 후보 프레이밍(사용자 제시,
2026-08-07, 정확한 정의는 미확정 — 아래 5번 목록 참고)**: `Ref`
`(v=Ref)` 매치 핸들러로 처리돼 다른 핸들러들과
같은 우선순위 스캔에 참여한다는 의미에서 "hook"(순서 등록 가능, 다른
값으로 교체되면 `retract`로 취소됨)에 가깝고, `PreRef`는 그 스캔 밖의
고정 pre-pass라는 의미에서 "pre-hook"(항상 최우선 고정, 순서/취소
개념 자체가 다름)에 가깝다는 구분 — quadnomicon 에세이로 쓸 때 이
"hook"/"pre-hook" 용어 자체를 채택할지부터 먼저 확인 필요(복수 `PreRef`
간 순서는 2026-08-07 아홉 번째 세션에서 해소됨 — 배열 index 순서
그대로, 별도 규칙 없음, `bind-system-plan.md` "PreRef" 절 참고. 취소
가능 여부는 여전히 미정 — PreRef는 fire와 동시에 소진되는 1회성
pre-pass 참가자라 "취소"라는 개념 자체가 성립하는지부터 다시 볼 것).
**publish 안 하는 것과의 경계**: 세션별 정정 이력, 조사 원자료(Fusion **publish 안 하는 것과의 경계**: 세션별 정정 이력, 조사 원자료(Fusion
반응 그래프 BFS 분석, quad2-try 죽은 코드 조사 등)는 quadnomicon에도 반응 그래프 BFS 분석, quad2-try 죽은 코드 조사 등)는 quadnomicon에도
@ -204,6 +221,14 @@ additional-primitives-plan.md`의 "문서화 백로그" 절이 원자료)**:
- 키 기반 동적 컬렉션 재조정 최종 이름/시그니처(`Render`/`Draw`/`List` 등 - 키 기반 동적 컬렉션 재조정 최종 이름/시그니처(`Render`/`Draw`/`List` 등
후보만 있음, `Slot:Extract` 세부 시맨틱도 미정) — `research/ 후보만 있음, `Slot:Extract` 세부 시맨틱도 미정) — `research/
additional-primitives-plan.md`(2026-08-06 신설, 설계 진행 중) additional-primitives-plan.md`(2026-08-06 신설, 설계 진행 중)
- **"hook"/"pre-hook" 용어 채택 여부 + `PreRef`의 취소 가능성** (2026-08-07,
위 심화 후보 6번 참고) — `bind-system-plan.md``PreRef`가 위치 무관
호이스팅이라는 것과 일반 `Ref`가 우선순위 스캔에 참여한다는 것까지는
확정해뒀고(복수 `PreRef` 간 순서=배열 index 순서, 동적 경로로 도착한
PreRef는 전용 Handler가 즉시 error — 둘 다 아홉 번째 세션에서 추가
확정), "hook 대 pre-hook"이라는 용어 자체를 문서화 시 채택할지와
`PreRef`의 취소 가능성(애초에 fire와 동시에 소진되는 1회성이라
"취소"가 의미 있는 개념인지부터)만 아직 미정.
이 항목들은 `.claude/question.md`에도 이미 열린 질문으로 잡혀있음 — 여기선 이 항목들은 `.claude/question.md`에도 이미 열린 질문으로 잡혀있음 — 여기선
"확정 전엔 문서화 대상 아님"이라는 표시만 겸함. "확정 전엔 문서화 대상 아님"이라는 표시만 겸함.

View file

@ -119,7 +119,15 @@ nil-index 크래시 vs 조용한 no-op)가 안 정해져 있음.
### 1-5. `props.Modifier`/`props.Ref` forwarding 관례가 Lua 배열 리터럴의 nil-hole 함정에 그대로 노출됨 ### 1-5. `props.Modifier`/`props.Ref` forwarding 관례가 Lua 배열 리터럴의 nil-hole 함정에 그대로 노출됨
**위치**: `base/component-composition-plan.md` "최종 결론" 1번 — **[2026-08-07 열 번째 세션 갱신 — 반영 완료.]** 아래 제안 (a)/(b) 대신
더 단순한 (c)류 해법으로 확정: `props.Modifier or None`/`props.Ref or
None` 관용구를 필수로 강제 — `None`이 항상 non-nil이라 리터럴 구멍 자체가
안 생기고, 이미 있는 array-part `None`-스킵 메커니즘(PreRef 논의 중
같은 세션에서 확정)을 그대로 재사용해 새 코드가 안 늘어남.
`base/component-composition-plan.md` "필수 관용구" 절, `ROADMAP.md` M0에
반영 완료 — 더 이상 열린 항목 아님, 아래는 원래 발견 당시 기록.
**위치(당시)**: `base/component-composition-plan.md` "최종 결론" 1번 —
`return Frame { props.Modifier, props.Ref, ... }` 패턴. `return Frame { props.Modifier, props.Ref, ... }` 패턴.
**문제**: caller가 `props.Modifier``props.Ref`를 안 넘기면 그 값은 **문제**: caller가 `props.Modifier``props.Ref`를 안 넘기면 그 값은

208
CLAUDE.md
View file

@ -663,7 +663,10 @@ M7 착수 시 `modifier-plan.md` 8번 참고하면 됨.
- **콜백/대기자 실행 구현 디테일 추가**: 같은 배열 하나를 한 번의 - **콜백/대기자 실행 구현 디테일 추가**: 같은 배열 하나를 한 번의
일반화 `for`로 순회하며 `type(v)=="thread"``coroutine.resume`+ 일반화 `for`로 순회하며 `type(v)=="thread"``coroutine.resume`+
슬롯 nil 처리(1회성), 함수면 그냥 호출(유지) — 새 등록은 `table.insert` 슬롯 nil 처리(1회성), 함수면 그냥 호출(유지) — 새 등록은 `table.insert`
로 끝, 성긴 배열이어도 압축 불필요. 로 끝, 성긴 배열이어도 압축 불필요. **[정정, 2026-08-07 열 번째 세션]**
"슬롯 nil 처리"는 틀림 — 사용자가 Luau REPL로 반례 제시, 실제로는
`None`으로 소진해야 함(`#t`/`table.insert` 안전성 문제). `base/
bind-system-plan.md` "왜 `nil`이 아니라 `None`인가" 절이 최신.
- v1의 `OnCreated` 특수 DI 키는 이식 안 함 — `Ref():Callback(fn)`으로 - v1의 `OnCreated` 특수 DI 키는 이식 안 함 — `Ref():Callback(fn)`으로
완전 대체. 완전 대체.
@ -1156,3 +1159,206 @@ Tag/Attribute 전용 문서 신설.** None 논의를 파고들다 디스패치
**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — 이번 세션도 순수 **다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — 이번 세션도 순수
설계 확정이라 M0 착수 우선순위 자체는 그대로. 설계 확정이라 M0 착수 우선순위 자체는 그대로.
## 2026-08-07 아홉 번째 세션 — 코퍼스 전체 정합성 감사·수정, `agent-mistake.md`
신설, `CreatedRef` 이름 완전 폐기
여러 세션에 걸쳐 쌓인 stale 참조/자기모순을 서브에이전트 5개 병렬 감사로
찾아내 전부 수정하고(커밋 `98bd46a`), 이어서 사용자가 직접 발견한 남은
문제(`CreatedRef` 이름 자체가 더 이상 존재할 이유가 없다는 지적)까지 처리한
세션. 세 부분으로 나눠 요약:
**1. 코퍼스 정합성 감사·수정 (커밋 `98bd46a`)**: `CreatedRef``phase`
옵션 폐기가 `bind-system-plan.md` 안에서 세 곳 중 두 곳에 방치돼 있던 것,
`question.md``Ref` 재검토 대상 여부 자기모순, UICorner 숏핸드 개명이
5개 문서에 전파 안 된 것, `canExecute(handle)` 시그니처 정정이 막 확정된
직후라 두 곳에 전파 안 된 것, `architecture.md`/`ROADMAP.md`/`CLAUDE.md`의
stale 문구·누락 참조 등 12개 항목을 수정. `store-semantics.md` 제목도
"State는 **Store** 위의 캐시 레이어"에서 "State는 **Source** 위의 캐시
레이어"로 정정(사용자 확인: Source 단독 존재 가능 + Store는 Source들의
집합이라는 온톨로지가 맞음). `slot-plan.md`의 CRUD 의미론 갭 하나만
사용자가 다음 세션에서 직접 다루기로 보류.
**2. `archive/agent-mistake.md` 신설** — 설계 반전(`*-reversed.md`)/기각
후보(`*-rejected.md`)와 구분되는 세 번째 archive 카테고리: 에이전트가
문서 작성 중 스스로 낸 개념 혼동을 같은 세션 안에서 정정한 사례 전용
(`canExecute`/`isHandlable` 혼동, `isSource` 불필요 오판 2건). CLAUDE.md
세션 로그에 전체 경위가 장황하게 남아있던 것 중 최종 결론이 이미 `base/`
문서에 반영돼 중복이던 걸 옮기고 포인터만 남김 — 앞으로도 비슷한 사례가
생기면 여기로 옮길 것(사용자 확인).
**3. `CreatedRef` 이름 완전 폐기 — 사용자가 직접 발견.** "Ref가 이미 다
정해진 것 같은데 `CreatedRef`는 이제 없는 말 아니냐"는 지적: `Source(default)`/
`Ref(default)`/`Store({defaults})`가 이미 Kotlin Compose식 "타입 이름
자체가 팩토리 함수" 생성자 스타일로 확정돼 있었는데(2026-08-06 네 번째
세션), `CreatedRef(fn)`라는 별도 래퍼 이름만 그 확정 이전(2026-08-04,
Ref가 아직 "instance 얻는 통로"로 좁게 정의됐던 시절)의 잔재로 계속
남아있었던 것 — 실제로는 `Ref(default)`(또는 `PreRef(default)`)
인스턴스 자체를 children 배열 숫자 슬롯에 그대로 놓으면 `(v=Ref)` 매치
핸들러가 처리하므로, 별도 래퍼 함수가 있을 이유 자체가 없었음. `base/
bind-system-plan.md`(바인드 방법 절 재작성, "CreatedRef와의 관계" 절
삭제, "phase 옵션 폐기" 절/열린질문 절 정리) · `ROADMAP.md`(M0/M8 체크
박스) · `question.md`(용어 재검토 목록에서 제거, 해소로 표시) ·
`architecture.md`(소스트리 주석) · `research/documentation-content-map.md`
전부 동기화 완료. `archive/ref-phase-option-reversed.md`(phase 옵션
자체의 역전 이력)와 CLAUDE.md 이전 세션 로그의 `CreatedRef` 언급은
당시 기록으로서 정확하므로 그대로 둠 — 역사적 서술과 현재 유효한 설계를
헷갈리지 않도록 "phase 옵션 폐기" 절 제목에 "이 절이 당시 쓰던 이름
자체도 이후 폐기됨" 포인터만 추가.
**부수 작업 — `PreRef`/`Modifier`의 "pre-hook" 태깅 요청 처리.** 같은
세션 앞부분에서 사용자가 "PreRef와 Modifier는 문서화 시 pre-hook 태그가
필요해 보인다, hook과 pre-hook의 차이(취소 가능/순서 등록 가능)도 적어
두면 좋겠다"고 제안 — 이건 런타임 설계가 아니라 문서 사이트 콘텐츠
분류 아이디어라 base/에 "확정"으로 못박지 않고 `research/
documentation-content-map.md`(심화 콘텐츠 후보 6번 + "문서화 아직 보류"
목록)에 사용자 원문 프레이밍 그대로 미확정 표시로 남겨둠 — `PreRef`
"인스턴스에 뭐가 일어나기 전에 채워진다"는 사실 자체는 이미 확정
서술돼 있었지만(재확인 후 "메모에서 지워도 됨"으로 답변), "hook"/
"pre-hook" 용어 채택 여부·`PreRef`의 취소 가능성·복수 `PreRef` 간 순서는
다음에 사용자가 직접 정해야 base/로 승격 가능. **[정정, 2026-08-07 열
번째 세션]** 같은 대화에서 "Ref 콜백/대기자 배열은 압축 없이 `self[i]
= nil`로만 지워도 된다"는 설계가 이미 정확히 반영돼 있다고 여기 적었던
건 틀림 — 실제로는 `nil`이 아니라 `None`으로 지워야 함(아래 열 번째
세션 절 참고), 이때는 아직 발견 전이었음.
**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터). `slot-plan.md` CRUD
의미론과 "hook/pre-hook" 프레이밍 확정만 사용자가 직접 다룰 후보로 남음.
**같은 세션 후속 — `PreRef` pre-pass 구현 위치·복수 `PreRef` 순서·동적
경로 가드 확정.** 사용자가 구체적인 구현 방안 세 개를 직접 제시:
1. **복수 `PreRef` 간 순서는 배열 index 순서 그대로** — 별도 규칙 발명
불필요, 위 "props 순회 순서" 절이 이미 확정한 "배열 파트는 index
순서대로" 계약을 그냥 재사용하면 됨. 호이스팅은 "PreRef 대 나머지"
에만 적용되는 규칙이지 "PreRef끼리"엔 적용될 게 없음.
2. **pre-pass가 사는 곳 — 새 `Dispatch.*` 함수 대신 이미 확정된
`Dispatch.drive(inst, flattened)` 자신.** 사용자가 두 대안을 직접
제시(`Dispatch.process(inst, flatten, prerefs)`류 신설 함수 vs
`flatten(inst, nonFlatten)` 함수 자체에 얹기) — 검토 결과 둘 다
불필요/위험함이 드러남. 전자는 이미 `Handler.process`/`Dispatch.process`
이름이 다른 뜻으로 확정돼 있어 겹침. 후자(flatten에 얹기)는 사용자가
"가장 간단해 보인다"고 제안했지만, `research/existing-instance-bind-plan.md`
다루는 "이미 마운트된 Instance 재바인드 시 flatten을 다시 해야
하는가"라는 열린 질문이 실제로 flatten이 한 인스턴스 생애주기 동안
여러 번 재호출될 가능성을 열어두고 있어서, 거기 PreRef fire를 얹으면
재바인드마다 PreRef가 또 fire되어 "이 인스턴스 하나의 construction
훅"이라는 정의 자체가 깨짐 — 기각. `Dispatch.drive`는 최초 마운트
시 한 번만 불리는 게 이미 전제라 이 위험이 없어서 그대로 거기 좁은
pre-pass 한 줄만 얹으면 충분.
3. **동적 경로로 도착한 `PreRef`는 런타임에도 명시적으로 error —
지금까지 타입 차단만 문서화돼 있던 빈틈을 채움.** 사용자 제안
그대로 채택: `{isHandlable = v is PreRef, process = error(...)}`
전용 Handler를 정상 우선순위 레지스트리에 등록(`NoneHandler`와 같은
"한 값 종류 전담" 패턴, 새 메커니즘 아님). 리터럴 배열의 `PreRef`
pre-pass가 fire와 동시에 슬롯을 소진시켜 정상 두 패스에 다시
노출되지 않으므로, 이 Handler가 실제로 매치되는 경우는 타입 차단을
어떻게든 우회한 버그 케이스뿐 — no-op이 아니라 즉시 `error`가 맞음.
전부 `base/bind-system-plan.md` "PreRef" 절에 반영, `ROADMAP.md` M8
체크박스 갱신, `research/documentation-content-map.md`의 "복수 PreRef
순서" 미정 표시 제거(해소됨, "취소 가능성"만 계속 미정으로 남김).
**같은 세션 두 번째 후속 — "호이스팅이 물리적 재배치가 아니라 별도
선행 스캔"이라는 것과 소진 방식을 명시화(뒤이은 세 번째 후속에서
`nil`→`None`으로 다시 정정됨, 아래 참고).** 사용자가 "drive에서도
결국 PreRef를 목록에서 뽑아내야 하는데, 호이스팅 안 되면 PreRef
의미가 사라지는 거 아니냐"고 재질문 — 이전 답변이 `Dispatch.drive`
pre-pass를 갖는다고만 하고 정확한 알고리즘을 안 써서 나온 질문.
`Dispatch.drive`가 같은 `flattened` 테이블을 **두 번** 순회한다는
것으로 답변: (1) pre-pass가 배열 전체를 index 순으로 훑어 `PreRef`
fire하며 그 자리에서 슬롯을 소진, (2) 그 다음 평소 두 패스가 같은
테이블을 다시 순회하되 소진된 슬롯은 자연히 건너뜀. "호이스팅"은
PreRef를 배열 앞으로 물리적으로 옮기는 게 아니라 "PreRef 전용 선행
루프가 통째로 먼저 끝난 뒤에야 나머지가 시작된다"는 뜻이라 소스 위치와
무관하게 항상 먼저 fire됨. **소진이 최적화가 아니라 정확성 요건인 이유도
명시**: 안 지우면 두 번째 패스가 이미 처리된 PreRef를 `Dispatch.process`
다시 넘겨서, 바로 위에서 신설한 "동적 경로 가드" Handler(`(v=PreRef)`→
`error`)가 정상 사용에도 오탐 에러를 던지게 됨.
## 2026-08-07 열 번째 세션 — 소진 슬롯을 `nil`이 아니라 `None`으로,
사용자가 Luau REPL 반례로 직접 발견
같은 날 이어진 세션. 사용자가 Luau REPL에서 직접
`for i,v in {[1]=1,[2222]=2222,[211]=211,[131]=131,[3]=3,[6]=6,
[122]=122,[11]=11,[312]=312,[821]=821,[991]=991} do print(i,v) end`을
돌려 순회 순서가 `1, 6, 122, 11, 991, 2222, 131, 312, 3, 821, 211`
나온다는 걸 보여줌 — index 오름차순이 전혀 아님. 이건 위 아홉 번째
세션에서 "PreRef pre-pass가 fire된 슬롯을 `nil`로 지우면 된다"고 적은
것과 여섯 번째 세션에서 "Ref 콜백/대기자 배열도 `[i]=nil`로 소진하면
된다"고 적었던 것 둘 다를 뒤집는 반례 — 키가 촘촘한 저범위 정수에서
벗어나면(구멍이든 원래 듬성듬성이든) Luau/Lua 테이블이 해시 파트
취급으로 넘어가 순회가 해시 버킷 순서가 됨.
**해결 — 소진에 `nil` 대신 `None` 센티널 사용, 전 코퍼스에 전파.**
`None``nil`이 아닌 실재하는 값이라 그 슬롯을 "차 있다"로 유지시켜서
테이블이 "구멍 없는 시퀀스"라는 불변식이 안 깨짐 — 두 가지를 동시에
해결: (1) 순서가 실제로 중요한 배열(PreRef pre-pass)의 순서 보장 유지,
(2) `table.insert`가 내부적으로 쓰는 `#t`가 Lua 명세상 구멍 있는
테이블에서 정의되지 않은 동작이라는 문제(Ref 콜백/대기자 배열이 새
등록 때 `table.insert`를 씀 — 순서 자체는 원래도 안 중요했지만 이
`#t` 안전성 문제는 진짜 버그였음). **배열 파트의 `None`은 해시 파트의
`None`(Modifier 필드 명시적 지우기, `NoneHandler` 경유)과 의미가
다름** — 배열 파트 `None`은 처리할 핸들러가 없는 순수 빈 슬롯 표시라
`Dispatch.process`/`NoneHandler`를 안 거치고 두 패스 루프 자신이 직접
`if v == None then continue end`로 스킵.
`base/bind-system-plan.md`의 "왜 `nil`이 아니라 `None`인가"(Ref
콜백/대기자 절)와 PreRef pre-pass 절에 반영, `ROADMAP.md` M0/M8
체크박스 갱신, 위 아홉/여섯 번째 세션 문단에 정정 표시 추가(원문은
유지, 틀렸던 부분만 짧게 정정 포인터).
**부수 발견 — `props.Modifier`/`props.Ref` nil-hole 위험도가 이전
서술보다 큼.** `pre-implementation-audit.md` 1-5가 이미 이 위험을
"뒤 항목까지 무시될 수 있음"으로 국소적 피해처럼 서술해뒀는데, 이번
REPL 실측으로 실제로는 구멍이 하나만 생겨도 **그 테이블 전체**가 순서
보장을 잃을 수 있다는 게 드러남 — M0 스파이크에서 반드시 실측하고,
심각하면 "raw 리터럴 대신 `props.Modifier or Modifier()`로 non-nil
보장" 컨벤션 문서화까지 검토하기로 `ROADMAP.md` M0에 메모 추가. 이
케이스는 caller가 직접 쓰는 raw Lua 리터럴이라 `None`으로 프레임워크가
대신 채워줄 수 없어서 별도 해법이 필요함 — `None` 소진 전략과 혼동하지
말 것.
**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터). M0 착수 시 위
nil-hole 위험도 실측이 우선순위 높아짐.
**같은 세션 세 번째 후속 — `props.Modifier`/`props.Ref` nil-hole
해법을 실제로 확정, 세션 clear 전 문서 완결성 점검하며 발견한 갭
3개도 같이 보강.** 사용자가 "컴포넌트에서는 꼭 `or None`이나
`Modifier()` 같은 걸로 nil 못하게 강제하는 걸 문서화하자"고 요청, 그
자리에서 결정하고 clear 전 세션 전체를 다시 훑어 새로 알게 됐지만
아직 문서에 없던 것들을 마저 채움:
1. **`props.Modifier or None`/`props.Ref or None`을 필수 관용구로
확정** — `Modifier()`(빈 modifier 새로 생성)가 아니라 `None`을 쓰는
이유는 이미 있는 array-part `None`-스킵 메커니즘(PreRef 논의에서
확정)을 그대로 재사용해 새 코드/할당이 하나도 안 늘어나기 때문 —
`flatten``isModifier(None) == false`라 그냥 통과시키고, 이어지는
두 패스 루프가 `None`을 만나면 스킵. `base/component-composition-plan.md`
"필수 관용구" 절 신설, `ROADMAP.md` M0/`pre-implementation-audit.md`
1-5/`question.md`에 반영(1-5는 해소로 표시).
2. **`Modifier()` 바닥 생성자가 문서 어디에도 없었던 갭 발견·보강** —
`Source(default)`/`Ref(default)`/`Store({defaults})`와 나란히 있어야
할 "`Type(args)` 팩토리" 4번째 예시가 원래 없었음(이전 아홉 번째
세션에 `Modifier.Rounded(8)` stale 참조를 고치면서 실수로 체이닝
예시인 `mod:UICorner(8)`로 잘못 채워 넣었던 것도 같이 바로잡음).
`modifier-plan.md` 3번 절에 명시, `store-semantics.md` 예시 목록
정정, `ROADMAP.md` M7 체크박스 추가.
3. **`Brand` 태그 목록에 `RefTag`/`PreRefTag`/`ModifierTag`가 빠져있던
갭 발견·보강** — 이번 세션 내내 `isPreRef(v)`/`isModifier(v)`를
이미 존재하는 predicate처럼 써왔는데 정작 여덟 번째 세션의 `Brand`
태그 목록엔 없었음. 추가하면서 **`isRef`/`isPreRef`가 `isState`
달리 집합 멤버십이 아니라 단순 항등이라는 것도 명시** —
`isRef(preRefInstance)`가 참이면 일반 `(v=Ref)` 핸들러가 `PreRef`
집어삼켜 PreRef 전용 pre-pass/가드 Handler 설계 전체가 무너지므로
반드시 배타적이어야 함. `bind-system-plan.md``Brand` 절,
`ROADMAP.md` M2 체크박스에 반영.
4. **배열 파트 `None`과 해시 파트 `None`(`NoneHandler`)이 같은 센티널인데
처리 경로가 다르다는 걸 `None` 센티널 절 자체에 명시적으로
교차 참조 추가** — 이전엔 PreRef 절에만 있고 `None` 센티널 원래
정의 절엔 이 예외가 안 적혀 있어서, 그 절만 읽으면 모든 `None`
`NoneHandler`를 탄다고 오해할 수 있었음.
전부 커밋 `98bd46a` 이후 아직 커밋 안 된 이번 대화 전체 변경사항에
포함 — 다음 세션이 새로 알아야 할 건 없음, `ROADMAP.md` M0부터 그대로
시작.

View file

@ -28,15 +28,24 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
핸들러 하나 + `isHandlable` 우선순위 스캔 포함) 핸들러 하나 + `isHandlable` 우선순위 스캔 포함)
- [ ] props 순회의 "배열 파트 먼저, 해시 파트 나중" 두 패스 계약이 실제 - [ ] props 순회의 "배열 파트 먼저, 해시 파트 나중" 두 패스 계약이 실제
Luau 테이블에서 관찰한 대로 동작하는지 확인, `PreRef` pre-pass + Luau 테이블에서 관찰한 대로 동작하는지 확인, `PreRef` pre-pass +
일반 `Ref`/`CreatedRef`의 위치 기반 순서까지 최소 스파이크로 검증 일반 `Ref`의 위치 기반 순서까지 최소 스파이크로 검증
(2026-08-07 세 번째 세션, `base/bind-system-plan.md` "`phase` 옵션 (2026-08-07 세 번째 세션, `base/bind-system-plan.md` "`phase` 옵션
폐기 → 위치로 표현, `PreRef` 신설" 절) 폐기 → 위치로 표현, `PreRef` 신설" 절) — **PreRef pre-pass의 소진은
`nil`이 아니라 `None`으로(2026-08-07 열 번째 세션 정정, 사용자가
Luau REPL로 반례 제시 — 키가 듬성듬성해지면 순회가 index 순서를
전혀 안 지킴), 이 경로는 nil-hole 위험이 아예 없도록 설계됐으므로
"구멍 있는 테이블 순회" 자체를 검증할 필요는 없어짐(같은 절 "왜
`nil`이 아니라 `None`인가" 참고)**
- [ ] `props.Modifier`/`props.Ref` named-parameter로 받는 컴포넌트 하나 작성, - [ ] `props.Modifier`/`props.Ref` named-parameter로 받는 컴포넌트 하나 작성,
`export type Params = {...}`로 타입 체크되는지 확인 `export type Params = {...}`로 타입 체크되는지 확인
(`component-composition-plan.md` 최종 결론 1번) — **caller가 Modifier/Ref를 (`component-composition-plan.md` 최종 결론 1번) — **`props.Modifier or
안 넘기는 케이스(Lua 배열 리터럴의 nil-hole 함정, `{nil, ref, child}`처럼 None`/`props.Ref or None` 관용구(2026-08-07 열 번째 세션 확정,
뒤 항목이 무시될 수 있는 경우)를 반드시 케이스에 포함** `component-composition-plan.md` "필수 관용구" 절)로 nil-hole을 막는
(`research/pre-implementation-audit.md` 1-5) 케이스를 반드시 포함할 것 — caller가 Modifier/Ref를 안 넘겨도
`or None`이 항상 non-nil을 보장하므로 `{nil, ref, child}`류 리터럴
구멍 자체가 안 생김(`research/pre-implementation-audit.md` 1-5).
M0에서 검증할 것은 "어떻게 막을지"가 아니라 이 관용구가 실제로
타입 체크/런타임 양쪽에서 문제없이 동작하는지**
- [ ] 위 과정에서 소스 트리/메커니즘 문서에 고칠 부분이 생기면 그 자리에서 - [ ] 위 과정에서 소스 트리/메커니즘 문서에 고칠 부분이 생기면 그 자리에서
`.claude/base/` 갱신 `.claude/base/` 갱신
@ -71,10 +80,12 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
여덟 번째 세션 정정) 여덟 번째 세션 정정)
- [ ] `Brand.luau`(공유 weak-key 레지스트리, `Brand.set(x,tag)`/ - [ ] `Brand.luau`(공유 weak-key 레지스트리, `Brand.set(x,tag)`/
`Brand.get(x)``isState`뿐 아니라 `isObserver`/`isEffect`/`isTag`/ `Brand.get(x)``isState`뿐 아니라 `isObserver`/`isEffect`/`isTag`/
`isAttribute`/`isTween`/`isBlocker`/`isSource`/`isStore`/`isSlot` `isAttribute`/`isTween`/`isBlocker`/`isSource`/`isStore`/`isSlot`/
전부의 기반. `isNone`만 예외로 레지스트리 없이 `x == None` 항등 `isRef`/`isPreRef`/`isModifier`(2026-08-07 열 번째 세션 추가 — 원래
비교 — `bind-system-plan.md``Brand` 절, 2026-08-07 여덟 번째 태그 목록에서 빠져있었음, `isRef`/`isPreRef`는 단순 항등이지
세션 신설) `isState`처럼 집합 멤버십 아님) 전부의 기반. `isNone`만 예외로
레지스트리 없이 `x == None` 항등 비교 — `bind-system-plan.md`
`Brand` 절, 2026-08-07 여덟 번째 세션 신설)
- [ ] `LifetimeHandle.luau`/`PerInstanceState.luau` **인터페이스만**(타입 - [ ] `LifetimeHandle.luau`/`PerInstanceState.luau` **인터페이스만**(타입
계약, 실 구현 없음 — quad-roblox 실 구현은 M8) — 원래 M8에만 계약, 실 구현 없음 — quad-roblox 실 구현은 M8) — 원래 M8에만
있었으나 M4(StoreBind의 `Connected` 확인)/M6(Slot의 `canExecute`)이 있었으나 M4(StoreBind의 `Connected` 확인)/M6(Slot의 `canExecute`)이
@ -136,7 +147,12 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
## M7 — Modifier ## M7 — Modifier
- [ ] flatten-before-dispatch, immutable `table.clone` 체이닝 - [ ] `Modifier()`(빈 인스턴스 바닥 생성자, 2026-08-07 열 번째 세션
명시 — `Source(default)`/`Ref(default)`/`Store({defaults})`와 같은
`Type(args)` 팩토리 관습, `modifier-plan.md` 3번)
- [ ] flatten-before-dispatch(`isModifier(v)`로 배열 항목 중 Modifier만
판별해 필드 merge, 나머지는 안 건드리고 통과 — 2026-08-07 열 번째
세션 명시, `modifier-plan.md` 1번), immutable `table.clone` 체이닝
- [ ] `Modifier.Override(mod1, mod2, ...)`(가칭, 구 `Merge`) — 필드별 raw - [ ] `Modifier.Override(mod1, mod2, ...)`(가칭, 구 `Merge`) — 필드별 raw
덮어쓰기, 특별한 State/함수 분기 불필요(`modifier-plan.md` 9번) 덮어쓰기, 특별한 State/함수 분기 불필요(`modifier-plan.md` 9번)
- [ ] `Override`가 서브타입 관계인 서로 다른 Modifier 타입(예: `FrameModifier`/ - [ ] `Override`가 서브타입 관계인 서로 다른 Modifier 타입(예: `FrameModifier`/
@ -164,10 +180,27 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
런타임 재사용 + children 배열 전용, Modifier/Store 타입 차단, 런타임 재사용 + children 배열 전용, Modifier/Store 타입 차단,
위치 무관 호이스팅 pre-pass — `base/bind-system-plan.md` "`phase` 위치 무관 호이스팅 pre-pass — `base/bind-system-plan.md` "`phase`
옵션 폐기 → 위치로 표현, `PreRef` 신설" 절 + "API 모양" 절) 옵션 폐기 → 위치로 표현, `PreRef` 신설" 절 + "API 모양" 절)
- [ ] `CreatedRef` 메커니즘(숫자 슬롯 참가자) - [ ] `(v=Ref)` 매치 핸들러 — children 배열의 숫자 슬롯에 놓인
`Ref(default)` 인스턴스를 인식해 바인드(별도 `CreatedRef` 래퍼
없음 — 이름 자체가 폐기됨, 아래 참고)
- [ ] `PreRef` pre-pass — 새 `Dispatch.*` 함수 없이 `Dispatch.drive(inst,
flattened)` 자신이 두 패스(배열→해시) 루프 전에 배열 파트를 훑어
`PreRef` 항목만 fire(Dispatch.process/getHandler 우회하는 raw 루프,
`flatten` 함수에는 얹지 않음 — 재바인드 시 flatten 재호출 가능성과
충돌하므로 기각). 복수 `PreRef`는 배열 index 순서 그대로(별도 규칙
없음). fire된 슬롯은 그 자리에서 소진(`None` 처리, `nil` 아님 —
2026-08-07 열 번째 세션 정정)해 이어지는 정상 두 패스에 다시 노출
안 되게 함 — `base/bind-system-plan.md` "PreRef" 절
- [ ] `PreRef` 동적 경로 가드 Handler — `{isHandlable = v is PreRef,
process = error(...)}` 형태로 정상 우선순위 레지스트리에 등록,
`NoneHandler`와 같은 "한 값 종류 전담" 패턴. 리터럴 배열 경로는
pre-pass가 이미 소진시키므로 이 Handler가 매치되면 곧 타입 차단을
우회한 버그라는 뜻 — 같은 절 참고
- [ ] Ref 콜백/대기자 실행 루프(`type(v)=="thread"`면 - [ ] Ref 콜백/대기자 실행 루프(`type(v)=="thread"`면
`coroutine.resume(v, self)`+소진, 함수면 `v(value)` 호출+유지 — `coroutine.resume(v, self)`+`None`으로 소진(`nil` 아님 —
같은 배열 하나로 통합). `:Wait(thread?)``thread``nil`이면 2026-08-07 열 번째 세션 정정, `#t`/`table.insert` 안전성), 함수면
`v(value)` 호출+유지 — 같은 배열 하나로 통합). `:Wait(thread?)`
`thread``nil`이면
`coroutine.running()` 캡처+yield, 있으면 등록만 하고 즉시 `self` `coroutine.running()` 캡처+yield, 있으면 등록만 하고 즉시 `self`
반환(남의 thread를 여기서 대신 정지시킬 수 없어서) 반환(남의 thread를 여기서 대신 정지시킬 수 없어서)
- [ ] `LifetimeHandle` quad-roblox 실제 구현(Instance 생존 확인, 인터페이스 - [ ] `LifetimeHandle` quad-roblox 실제 구현(Instance 생존 확인, 인터페이스