소스 구조 확정 라운드(2026-08-04, 5차) 결과 반영

bind-system-plan/module-lifecycle-plan/slot-plan을 research/에서 base/로
승격, quad-base(인터페이스)/quad-roblox(구현) 패키지 경계와 모노레포 소스
트리를 architecture.md에 확정. Slot의 base/roblox 분리, InstanceChild
핸들러 필요성도 함께 반영.
This commit is contained in:
qwreey 2026-08-04 13:57:01 +09:00
parent 0dbbc3d0b1
commit c00e2e67d6
Signed by: qwreey
GPG key ID: D28DB79297A214BD
12 changed files with 582 additions and 194 deletions

View file

@ -14,7 +14,7 @@
| `qa-request/` | 구현 완료(코드/에이전트 검증까지 끝남) + 사용자 본인의 실기기(Roblox Studio) QA만 남음 — 지금은 구현 자체가 시작 전이라 비어있음 | | `qa-request/` | 구현 완료(코드/에이전트 검증까지 끝남) + 사용자 본인의 실기기(Roblox Studio) QA만 남음 — 지금은 구현 자체가 시작 전이라 비어있음 |
| `archive/` | 완료 + 사용자가 실사용/실기기로 직접 검증까지 마침 — 지금은 비어있음 | | `archive/` | 완료 + 사용자가 실사용/실기기로 직접 검증까지 마침 — 지금은 비어있음 |
| `feedback/` | 실사용 피드백을 정리한 긴 로그 — 지금은 비어있음(구현 시작 전) | | `feedback/` | 실사용 피드백을 정리한 긴 로그 — 지금은 비어있음(구현 시작 전) |
| `initreq/` | 프로젝트 착수 시 클론해둔 참고 레포(quad v1, fusion, vide, rbvm, tbox, code-docker) + 원본 요청(`req.md`, `raw-userinput.md`) + `quad2-try`(이전에 시도했다 폐기한 v2 재작성 시도 — 리서치 완료, 결론은 `research/bind-system-plan.md`) — 읽기 전용 리서치 소스, 여기 내용을 옮기지 말고 항상 원본 그대로 유지 | | `initreq/` | 프로젝트 착수 시 클론해둔 참고 레포(quad v1, fusion, vide, rbvm, tbox, code-docker) + 원본 요청(`req.md`, `raw-userinput.md`) + `quad2-try`(이전에 시도했다 폐기한 v2 재작성 시도 — 리서치 완료, 결론은 `base/bind-system-plan.md`) — 읽기 전용 리서치 소스, 여기 내용을 옮기지 말고 항상 원본 그대로 유지 |
`research/`의 문서가 설계 확정되면 `base/`로 승격(또는 구현 착수 시 `research/`의 문서가 설계 확정되면 `base/`로 승격(또는 구현 착수 시
`qa-request/`행). 지금은 구현 라운드 전(설계 단계)이라 전부 `base/`/`research/`에만 `qa-request/`행). 지금은 구현 라운드 전(설계 단계)이라 전부 `base/`/`research/`에만
@ -28,15 +28,15 @@
| `quad-v1-architecture.md` | v1(`initreq/quad`) 내부 동작 스냅샷 — "이 문제를 안 반복하려면"의 기준선 | | `quad-v1-architecture.md` | v1(`initreq/quad`) 내부 동작 스냅샷 — "이 문제를 안 반복하려면"의 기준선 |
| `comparison-fusion-vide.md` | Fusion/Vide 아키텍처 비교 리서치 — 설계 결정 근거 자료 | | `comparison-fusion-vide.md` | Fusion/Vide 아키텍처 비교 리서치 — 설계 결정 근거 자료 |
| `lifecycle-pattern.md` | rbvm의 `Connected`+GC 관용구를 quad-v2가 채택하는 방식 | | `lifecycle-pattern.md` | rbvm의 `Connected`+GC 관용구를 quad-v2가 채택하는 방식 |
| `store-semantics.md` | Store는 부작용 허용이 기본. State는 Store 위의 조합 가능한 캐시 레이어로 실제로 필요함(2026-08-04 정정) — Store/State/Source 온톨로지는 `research/bind-system-plan.md`에서 진행 중 | | `store-semantics.md` | Store는 부작용 허용이 기본. State는 Store 위의 조합 가능한 캐시 레이어로 실제로 필요함(2026-08-04 정정) — 온톨로지 핵심 메커니즘은 2026-08-04 2차 라운드에서 확정, 최신 상세는 `base/bind-system-plan.md` |
| `bind-system-plan.md` | pluggable key/value 핸들러 레지스트리 — `process`/`retract` 디스패치 모델, Ref, Store/State/Source 온톨로지 + 인체공학 질문 전부 확정. 디스패치 엔진은 `quad-base`가 인터페이스로 소유(2026-08-04 5차 라운드) | — |
| `module-lifecycle-plan.md` | 프로바이더 패턴, bind/store 구현 책임 분리 — 확정 | — |
| `slot-plan.md` | 뮤터블 자식 배열, 엄격한 단일 마운트 소유권, 재마운트 시 throw, base/roblox 패키지 경계까지 확정 | — |
## `research/` — 아직 착수 전, 상의 필요 ## `research/` — 아직 착수 전, 상의 필요
| 문서 | 내용 | 우선순위 | | 문서 | 내용 | 우선순위 |
|---|---|---| |---|---|---|
| `bind-system-plan.md` | pluggable key/value 핸들러 레지스트리 — `process`/`retract` 디스패치 모델(확정), Ref(확정), quad2-try 리서치 결과. **Store/State/Source 온톨로지는 2026-08-04에 새로 열린 미해결 설계 스레드**(다음 세션 최우선) | 최상 — 다른 모든 설계가 이 위에서 조립됨 |
| `module-lifecycle-plan.md` | 프로바이더 패턴, bind/store 구현 책임 분리 — 확정됨 | 최상 — 확정, 구현 착수 시 API 세부만 조정 |
| `slot-plan.md` | 뮤터블 자식 배열, 엄격한 단일 마운트 소유권, 재마운트 시 throw | 상 — bind-system 확정 후 |
| `tween-plan.md` | 트윈을 Store 밖 특수 bind key로 처리, 기본 오버라이드는 Cancel | 중 — 세부 옵션만 남음 | | `tween-plan.md` | 트윈을 Store 밖 특수 bind key로 처리, 기본 오버라이드는 Cancel | 중 — 세부 옵션만 남음 |
| `purity-and-effects-plan.md` | 컴포넌트 "순수성"이 아니라 "이식성" 문제로 재정의 — 문서 경고 수준으로 확정 | 하 — 문서화 성격, 급하지 않음 | | `purity-and-effects-plan.md` | 컴포넌트 "순수성"이 아니라 "이식성" 문제로 재정의 — 문서 경고 수준으로 확정 | 하 — 문서화 성격, 급하지 않음 |
| `existing-instance-bind-plan.md` | 이미 생성된 인스턴스 재바인드 — 착수 안 하되 "미지원" 확정도 안 함, 열린 가능성 유지 | 하 — v2 초기 스코프 제외 | | `existing-instance-bind-plan.md` | 이미 생성된 인스턴스 재바인드 — 착수 안 하되 "미지원" 확정도 안 함, 열린 가능성 유지 | 하 — v2 초기 스코프 제외 |

View file

@ -35,7 +35,7 @@ quad는 이제 "스크립트"가 아니라 **라이브러리**다. DOMless Roblo
개념을 추가하면 라이브러리 복잡도가 너무 올라간다고 판단 — 당장은 개념을 추가하면 라이브러리 복잡도가 너무 올라간다고 판단 — 당장은
TagService 그대로 사용. **대신 Ref가 도입됨** — 단 Ref의 용도는 "id로 조회"가 TagService 그대로 사용. **대신 Ref가 도입됨** — 단 Ref의 용도는 "id로 조회"가
아니라 "외부에서 이미 관리되고 있는 instance를 quad로 점진적으로 마이그레이션/ 아니라 "외부에서 이미 관리되고 있는 instance를 quad로 점진적으로 마이그레이션/
래핑하기 위해 직접 참조를 얻는 것"(`research/bind-system-plan.md`의 Ref 절 래핑하기 위해 직접 참조를 얻는 것"(`base/bind-system-plan.md`의 Ref 절
참고) — 둘을 혼동하지 말 것. 참고) — 둘을 혼동하지 말 것.
6. **함수지향 디폴트, `:` 체이닝은 예외적으로만.** 스토어 바인드처럼 체인이 정말 6. **함수지향 디폴트, `:` 체이닝은 예외적으로만.** 스토어 바인드처럼 체인이 정말
편한 경우만 `:` 사용, 나머지는 외부 함수가 인스턴스를 인자로 받는 모양. 편한 경우만 `:` 사용, 나머지는 외부 함수가 인스턴스를 인자로 받는 모양.
@ -44,7 +44,7 @@ quad는 이제 "스크립트"가 아니라 **라이브러리**다. DOMless Roblo
store 바인드를 받을 수도 있음. store 바인드를 받을 수도 있음.
8. **특수 이벤트는 특수 플러깅으로.** `PropertyChangedSignal`, `PropertyChangedEvent ""` 8. **특수 이벤트는 특수 플러깅으로.** `PropertyChangedSignal`, `PropertyChangedEvent ""`
같은 것들은 일반 이벤트 바인드가 아니라 pluggable 바인드 핸들러 중 하나로 같은 것들은 일반 이벤트 바인드가 아니라 pluggable 바인드 핸들러 중 하나로
구현(`research/bind-system-plan.md`). 구현(`base/bind-system-plan.md`).
9. **Tracker 미구현.** v1의 소스 변경 감지 자동 재렌더 기능(hot-reload watcher, 9. **Tracker 미구현.** v1의 소스 변경 감지 자동 재렌더 기능(hot-reload watcher,
실제로는 `.claude/initreq/quad/src/tracker.lua` — v1에서도 이미 `exports.lua` 실제로는 `.claude/initreq/quad/src/tracker.lua` — v1에서도 이미 `exports.lua`
연결 안 된 죽은 코드였음, `base/quad-v1-architecture.md` 참고)은 렌더 연결 안 된 죽은 코드였음, `base/quad-v1-architecture.md` 참고)은 렌더
@ -70,16 +70,88 @@ quad는 이제 "스크립트"가 아니라 **라이브러리**다. DOMless Roblo
`InitRoblox(Module)` 같은 팩토리 함수가 생성된 모듈을 뮤테이션하는 도구를 `InitRoblox(Module)` 같은 팩토리 함수가 생성된 모듈을 뮤테이션하는 도구를
주는 방식. 주는 방식.
## 구현 착수: 소스 트리 구조 확정 (2026-08-04, 5차 라운드)
**상태**: 소스 트리 레이아웃과 `quad-base`/`quad-roblox` 패키지 경계 확정 —
아래가 다음 세션에서 실제로 만들 구조. 지금은 문서 확정까지만, 실제
폴더/`wally.toml`/`project.json` 스캐폴딩은 다음 세션.
**패키징 방식(모노레포, RbxUtil 선례 채택)**: 최종적으로는 여러 개의 독립
wally 패키지로 나누고 싶지만, 지금 Luau 툴링(특히 wally로 설치된 패키지의
타입 정보 단절·`luau-lsp`의 심볼릭 링크 해석 문제 — 최근 `luau-lsp 1.63.0`
에서야 수정됨)이 아직 불안정해서 **당장은 모놀리식**으로 감. `Sleitnick/
RbxUtil`이 정확히 이 패턴(루트 하나로 통합 개발/테스트, 서브폴더마다 자체
`wally.toml`로 독립 퍼블리시)을 쓰는 선례라 그대로 채택. `.luaurc`
`aliases`**런타임 require에서 아직 엔진이 지원 안 함**(Roblox 스태프가
지원 예정이라고만 밝힌 상태, 2026-01 기준) — 그래서 alias는 편집기
자동완성/타입체크용으로만 곁들이고, 실제 크로스패키지 require는 상대경로로
쓴다. 나중에 실제로 레포를 쪼갤 때는 Rojo `project.json`의 트리 매핑 규칙만
유지하면 되고, require는 그 시점에 한 번 기계적으로 바꾸는 정도로 감수.
**패키지 경계**: `quad-base`는 다른 렌더 백엔드(GTK 등, 항목 12 참고)에서도
재사용 가능해야 한다는 전제 — Store/State/Source 온톨로지+전파뿐 아니라
**pluggable 디스패치 엔진 자체도 "인터페이스"로 base가 소유**한다(엔진마다
큰 구현을 중복하지 않기 위함 — rbvm이 relation을 하나로 통합하려 했던 것과
같은 동기). `quad-roblox`는 그 인터페이스의 **실제 구현체**만 제공.
```
quad/
├── .luaurc # @quad-base, @quad-roblox alias (편집기 경험용, 런타임 비의존)
├── default.project.json # 루트 통합 개발/테스트용 Rojo 프로젝트
├── quad-base/
│ ├── wally.toml
│ └── src/
│ ├── Source.luau # 값의 근원, 단일 지점
│ ├── State.luau # 캐시만 하는 non-owning 핸들, state(state) 분기
│ ├── Store.luau # source 집합체, dot-access, __newindex
│ ├── Dispatch/
│ │ ├── init.luau # process/retract 엔진, isHandlable 우선순위 스캔
│ │ ├── Handler.luau # 핸들러 계약 타입(isHandlable/priority/process/retract)
│ │ ├── StoreBind.luau # store 값 재귀 재실행 로직(범용, 엔진 무관)
│ │ └── Slot.luau # add/remove/clear 재조정 로직(추상 자식 참조 기준)
│ ├── LifetimeHandle.luau # Connected 계산 속성 "인터페이스"(타입/계약만)
│ ├── PerInstanceState.luau # per-instance 상태 저장 "인터페이스"
│ ├── Ref.luau # CreatedRef 메커니즘(숫자 슬롯 참가자)
│ └── init.luau
└── quad-roblox/
├── wally.toml
└── src/
├── RobloxFactory.luau # BaseModule 뮤테이션, 재호출 가드(같은 팩토리=무시/다른=에러)
├── LifetimeHandle.luau # 실제 구현(Instance 생존 확인)
├── PerInstanceState.luau # 실제 구현(weak-keyed table, Instance 키)
├── Handlers/
│ ├── Property.luau
│ ├── Event.luau # ReflectionService 기반 자동 판별
│ ├── Attribute.luau
│ ├── Tag.luau # CollectionService
│ ├── Tween.luau # 높은 우선순위 store-bind 핸들러
│ ├── Slot.luau # base Slot 재조정 로직의 실제 적용/해제(Instance Parent 조작)
│ └── InstanceChild.luau # k:number, v:Instance — 중첩 인스턴스 자식(예: Frame { Frame {} })
├── DI/
│ └── init.luau # 제네릭 생성자 + ~25개 정적 필드(UIInstances)
└── init.luau
```
**남은 것**: Slot 코어 로직의 정확한 API(`research`→`base` 승격된
`slot-plan.md` 참고)와 각 파일의 정확한 함수/타입 이름은 구현 단계에서.
Tween/purity/existing-instance-bind는 여전히 `research/`에 남아있고 이
구조 확정을 막지 않음.
## 아직 미정 (research/로 분리됨) ## 아직 미정 (research/로 분리됨)
바인드 시스템 디스패치, Slot 설계 세부, Tween 플러깅, 모듈 라이프사이클/누가 Tween 플러깅, 순수함수 범위, 이미 생성된 인스턴스에 대한 바인드 —
Store를 구현하는가, 순수함수 범위, 이미 생성된 인스턴스에 대한 바인드 — `.claude/research/` 각 문서 참고, 전체 색인은 `.claude/README.md`. 바인드
`.claude/research/` 각 문서 참고, 전체 색인은 `.claude/README.md`. 디스패치/Slot/모듈 라이프사이클은 위 "구현 착수" 섹션대로 확정되어
`.claude/base/`로 승격됨(`bind-system-plan.md`/`module-lifecycle-plan.md`/
`slot-plan.md`).
**가장 시급한 미정 사항(2026-08-04부터, 다음 세션 최우선)**: Store/State/ **Store/State/Source 온톨로지(2026-08-04 두 라운드에 걸쳐 확정)**: Store는
Source 온톨로지 — Store는 source(실제 값이 존재하는 단일 지점) 집합체이고, source(실제 값이 존재하는 단일 지점) 집합체이고, `store.key`로 접근할 때마다
`store "key"`처럼 접근할 때마다 그 source를 감싸는 새 State(자기 고유 그 source를 감싸는 새 State(자기 고유 value 없는 조합 가능한 캐시)를
value 없는 조합 가능한 캐시)를 반환한다는 모델까지는 나왔으나, `:Compute` 반환한다. 전파는 push-invalidate(신호만)/pull-recompute(`Get()` 시점) —
캐싱/무효화 전략, Luau 타입 시스템에서 커링 호출의 타입 추론 문제 등 세부는 Fusion식 eager 노드 없이도 다이아몬드 의존성 중복 재계산 문제가 풀림. State는
전부 열려있음. `research/bind-system-plan.md`의 "Store/State/Source 쓰기 대상이 아니고(값 쓰기는 항상 Store의 `__newindex`), 값 하나만 다룰 땐
온톨로지" 절, `.claude/question.md`의 "최우선 새 열린 질문" 절 참고. Store와 별개인 가벼운 `Source` 프리미티브를 씀. 남은 건 정확한 API 이름과
"`store.key` dot-access를 타입 추론 1급 경로로 삼는다"는 제안의 정식 확인
뿐 — `base/bind-system-plan.md`의 "Store/State/Source 온톨로지" 절,
`.claude/question.md` 참고.

View file

@ -1,9 +1,12 @@
# Bind 시스템 — pluggable key/value 핸들러 (핵심 모델 확정, 세부 사항만 남음) # Bind 시스템 — pluggable key/value 핸들러 (base로 승격됨)
**상태**: research — 핵심 디스패치 모델(`process`/`retract`, 핸들러 4종 계약, **상태**: base — 핵심 디스패치 모델(`process`/`retract`, 핸들러 4종 계약,
Signal 미채택, Ref 역할)은 사용자 확인 완료로 사실상 확정. 남은 건 세부 Signal 미채택, Ref 역할)과 소스 트리 상 패키지 경계(디스패치 엔진은
시그니처(dependency array API, `CreatedRef` 모양) 뿐 — 이것들이 정리되면 `quad-base`가 인터페이스로 소유, `quad-roblox`는 실제 구현만)까지 전부
`base/`로 승격 예정. 원본: `.claude/initreq/raw-userinput.md` 2026-08-04 세션에서 확정되어 `research/`에서 승격됨(`base/architecture.md`의
"구현 착수: 소스 트리 구조 확정" 절 참고). 남은 건 세부 시그니처(dependency
array API, `CreatedRef` 모양) 뿐 — 구현 단계에서 자연히 정리됨. 원본:
`.claude/initreq/raw-userinput.md`
"key와 value에 대한 바인드 연산은 pluggable 하도록 구성하기" / "스토어는 스토어를 "key와 value에 대한 바인드 연산은 pluggable 하도록 구성하기" / "스토어는 스토어를
저장 가능한가" / "Ref는 고민중" 절. v1의 문제점은 `base/quad-v1-architecture.md` 저장 가능한가" / "Ref는 고민중" 절. v1의 문제점은 `base/quad-v1-architecture.md`
("ProcessQuadProperty" 하드코딩 디스패처), 참고 패턴은 `.claude/initreq/tbox` ("ProcessQuadProperty" 하드코딩 디스패처), 참고 패턴은 `.claude/initreq/tbox`
@ -111,7 +114,7 @@ src/schema/union.luau:48-68`) — 에러 메시지는 즉시 문자열로 만들
Slot이 store 바인드로 넘어오는 경우, pluggable 처리기에 `retract` 핸들러가 Slot이 store 바인드로 넘어오는 경우, pluggable 처리기에 `retract` 핸들러가
필요하다는 점(부모가 slot을 정리하고 다시 process하는 방식)도 이 래핑 방식과 필요하다는 점(부모가 slot을 정리하고 다시 process하는 방식)도 이 래핑 방식과
자연스럽게 맞음 — `research/slot-plan.md` 참고. 자연스럽게 맞음 — `base/slot-plan.md` 참고.
## Store가 Store를 저장 가능한가 ## Store가 Store를 저장 가능한가
@ -188,51 +191,138 @@ read ...`처럼, State끼리 자유롭게 합성/파이핑 가능한 것이 최
같은 축의 문제 — 옵션 2가 그 원칙과 더 잘 맞아 보이지만, 실현 가능성 자체가 같은 축의 문제 — 옵션 2가 그 원칙과 더 잘 맞아 보이지만, 실현 가능성 자체가
아직 검증 안 됨. 아직 검증 안 됨.
## Store/State/Source 온톨로지 — 진행 중인 설계 스레드 (2026-08-04 검증 라운드에서 새로 열림) ## Store/State/Source 온톨로지 — 핵심 메커니즘 확정 (2026-08-04 2차 라운드)
**이 절은 아직 결론난 설계가 아니다** — 검증 라운드 중 사용자가 실시간으로 **상태**: 전파 모델/`:Compute` 인자 규칙/State 쓰기 금지/Slot 생존 확인/타입
설계를 전개하며 나온 내용을 그대로 기록. 다음 세션에서 이어서 다룰 것. 추론(dot-access) 전부 `AskUserQuestion`으로 확인 완료. 남은 건 정확한 함수/
`base/store-semantics.md`의 "State 프리미티브는 실제로 필요하다" 정정과 생성자 이름뿐(구현 단계). `base/store-semantics.md`의 "State 프리미티브는
직결됨. 실제로 필요하다" 정정에서 이어짐.
**핵심 온톨로지**: **핵심 온톨로지** (변경 없음):
- **Source** — 실제 값이 존재하고 변경될 수 있는 단일 지점(v1의 "값의 근원"). - **Source** — 실제 값이 존재하고 변경될 수 있는 단일 지점(v1의 "값의 근원").
- **Store** — source들의 집합체. `store.a`/`store "a"`처럼 키로 접근하면 - **Store** — source들의 집합체. `store.a`처럼 키로 접근하면 그 source를
그 source를 감싼 **새 State**를 매번 만들어 반환(state가 store에 캐시되어 감싼 **새 State**를 매번 만들어 반환(state가 store에 캐시되어 재사용되는
재사용되는 게 아님 — source만 store에 귀속된 유일한 실체). 게 아님 — source만 store에 귀속된 유일한 실체).
- **State** — source(또는 다른 state)의 결과를 캐싱만 하는 존재, 자기 고유의 - **State** — source(또는 다른 state)의 결과를 캐싱만 하는 존재, 자기 고유의
독립적 value 개념이 없음. `state(state)`로 기존 state의 결과를 받아 새 독립적 value 개념이 없음. `state(state)`로 기존 state의 결과를 받아 새
state를 만들어 분기 가능 — 이게 사실상 Unix 파이프 영감의 "State끼리 state를 만들어 분기 가능 — 이게 사실상 Unix 파이프 영감의 "State끼리
합성 가능"이라는 원래 목표를 구현하는 방식. 합성 가능"이라는 원래 목표를 구현하는 방식.
- `:With(...)`/`:Compute(fn)`은 실제로는 state 위의 연산 —
`store "key1":With(store "key2"):Compute(function(key1) return key1 +
store.key2.value end)`처럼, with한 값을 fn이 클로저로 직접 읽는 모양.
**미해결 세부 사항**: **전파 모델 확정: push-invalidate(신호만) / pull-recompute(`Get()` 시점에만) —
- **`:Compute` 캐싱/무효화 전략** — 매번 새로 계산할지, 캐싱해두고 무효화 Fusion식 eager 노드·생성순 정렬은 안 만듦**
플래그로 관리할지 미정. 후보: 값이 바뀌었는데 듣는 소비자가 없으면 연산은
미루고 `invalid=true`만 세워두고, 필요해질 때(듣는 사람이 생기거나 값을 - `Source`는 값이 바뀌면 구독 중인 State들에게 **"무효화됐다"는 신호만
읽을 때) 실제로 연산하고 `true`→캐싱 후 `false`로 되돌리는 dirty-flag 방식. 쏜다** — 새 값 자체는 신호에 안 실림("state는 세터를 내보내기보다
"store가 state를 **만드느냐** 아니면 **저장하느냐**"의 문제와 직결 — 업데이트 됐다는 신호만 쏜다" — 사용자 확정 문구).
사용자 판단은 "만든다" 쪽(저장한다고 하면 한 곳에서 `:Compute`를 붙이면 - 신호를 받은 State는 자기 `invalid` 플래그만 세우고, 이미 `invalid`였다면
다른 소비자도 전부 그 compute된 값을 읽게 되어버리는 오염 문제 발생). 그 아래로 더 전파하지 않는다 — 다이아몬드 의존성에서 중복 워크를 막는
- **`emit` 필요 여부** — store가 값 변경 시 관련된 모든 state에 emit해야 장치(Vide가 저자 스스로 `todo.md`에 미해결로 남긴 문제의 해결책).
하는 구조가 맞는지 확신은 없지만, 그 외의 방법이 안 보인다는 게 사용자 - 실제 재계산은 `Get()`(또는 `.value` 인덱싱)이 호출되는 시점에만 일어남 —
현재 판단. `state(from) / state() -> (state, setState)` 같은 팩토리 "필요할 때 계산" 원칙(사용자 확정). Fusion의 `timeliness="eager"` 노드/
모양도 후보로 언급됨(React의 `useState`류 페어 반환과 유사). 생성순 정렬 장치는 만들지 않음 — quad엔 그런 다단계 즉시 재계산이 필요한
- **Luau 타입 시스템 제약**`store "key"` 같은 커링 호출로 `state<T>` 소비자가 없다는 판단. 유일하게 "즉시 반응해야 하는" 소비자는 store-bind
`T`를 정확히 추론하려면 오버로드 함수 타입(`(("a") -> number) | (("b") pluggable 핸들러(위 "확정된 디스패치 모델" 절)인데, 이건 무효화 신호를
-> boolean)`)이 필요한데, Luau는 문자열 리터럴 인자를 자동으로 `as const` 받는 즉시 자기가 알아서 `Get()`을 호출해 pull하는 방식으로 충분함 —
취급하지 않아서 타입이 좁혀지지 않는 문제가 있음. `store.states.a`처럼 State 스스로 "지금 나를 보는 eager 소비자가 있나" 같은 부기가 전혀
필드 접근으로 우회하거나, `store.a`가 바로 state를 반환하고 필요 없음.
`state.value = x`로 설정 가능하게 하는 대안도 검토됐으나, 후자는 "다른 - `emit`은 이 무효화 신호 하나로 좁혀짐 — 값을 안 실어보내므로 저렴함
source로부터 파생된 state에 value를 직접 설정하면 안 된다"는 문제와 ("emit 필요 여부" 열린 질문은 이걸로 해소).
충돌(store가 실제 값을 담는 유일한 주체여야 함). `state<Mapped>`(compute
결과)가 제대로 바인딩 안 됐을 때 생기는 타입 문제는 일단 UB로 두기로 함. **`:With`/`:Compute` — self 인자도 lazy 핸들로 통일**
- **`Pipe`(quad2-try 후보)는 사실상 폐기 쪽으로 기움** — 별도 `Pipe` 타입에
소유권/버전 가드를 넣어 재설계하는 대신, State 자체를 파이핑 결합체로 - 최초안(self 값은 포지셔널 raw 값, with한 값만 클로저로 읽음)에는 실제
보고 `state(state)`로 분기하는 쪽이 엔지니어링상 더 쉬워 보인다는 게 단점이 있었음 — self가 raw 값이면 `fn` 호출 전에 항상 self를 먼저
사용자의 최신 판단(2026-08-03 라운드의 "Pipe COW가 유력 후보"보다 우선함). `Get()`해야 하므로, `fn` 내부 로직이 with한 다른 값을 보고 "이 경우엔 self
계산 자체가 필요 없다"고 판단해도 이미 늦음(예: `:With(noprint)`이고
`noprint.value == true`면 앞단 계산을 통째로 생략하고 싶은 경우).
- **해결(사용자 확정)**: self도 raw 값이 아니라 **State 핸들 그 자체**를
`fn`의 포지셔널 인자로 넘긴다 — `fn(self: State<T>)`, 내부에서
`self.value`(또는 `self:Get()`)를 실제로 읽을 때만 계산이 트리거됨.
with한 값과 동일한 lazy 원칙을 self에도 그대로 적용 — 별도
`ComputeWithout` 변형은 불필요, `Compute` 하나로 일관.
- `.value``Get()`을 감싼 읽기 전용 계산 속성(`base/lifecycle-pattern.md`의
`Connected`와 동일한 "저장되는 필드가 아니라 계산된 속성" 패턴 재사용) —
`:Get()``.value` 둘 다 지원, `.value`가 관용적 표기.
- 예시 갱신: `store "key1":With(store "key2"):Compute(function(key1) return
key1.value + store.key2.value end)` — `key1`은 이제 raw 숫자가 아니라
State.
**State는 쓰기 대상이 아님 — 확정, Source는 독립 공개 프리미티브로 격상**
- `.value`는 항상 읽기 전용. 값을 쓰는 경로는 오직 Store의 `__newindex`
(`store.key = value`, 이미 확정된 문법)뿐 — State에는 대응하는 쓰기 API가
아예 없음. "State에 `.value = x`를 허용하면 다른 source에서 파생된
state에 직접 쓰기가 가능해져 버린다"는 이전 우려는 이걸로 근본적으로
해소(그런 API 자체가 없음).
- **`Source`는 Store의 내부 구현 디테일이 아니라 별도의 가벼운 공개
프리미티브로 노출** — Store는 다수의 source를 등록/관리하는 무거운
구조라, 값 하나만 반응형으로 다루고 싶을 때 Store를 통째로 만드는 건
비효율이라는 게 사용자 판단("store가 source 수십 개 만드는건 비효율이니
둘이 다른 구현이라 봐도 될듯"). `Source(initial)` 류의 독립 생성자
(정확한 이름은 구현 단계에서 확정)가 Store와 나란히 존재.
**Slot 생존 확인 — 별도 메커니즘 아님, `canExecute` 재사용으로 확정**
- `base/store-semantics.md`에 있던 "`isInit=false`면 허용, `isInit=true`+
생존확인 거짓이면 불허" 분기 초안은 폐기. state-invalidate 리스너
클로저도 `base/lifecycle-pattern.md`의 "생명 바인드 유틸"(canExecute
predicate)로 등록하면, 발화 시 `canExecute()` 하나만 확인하고 거짓이면
그냥 no-op — `isInit` 분기라는 별도 개념 자체가 불필요(사용자 확정:
"canExecute 하나로 통일").
**타입 추론 문제 — 확정(2026-08-04 3차 라운드)**
- `store "key"`(문자열 커링)로 `state<T>`를 오버로드 함수 타입으로 정확히
추론하려는 시도는 포기하고, **`store.key`(dot-access)를 1급 경로로 확정**
— Store 타입을 `{key: State<number>, other: State<string>}`류 평범한
레코드 타입으로 지으면 일반 구조적 필드 타이핑으로 자동 해결되고, 문자열
리터럴 narrowing 문제 자체가 안 생김. `store "key"` 문자열 커링은 동적
키가 필요할 때 쓰는 미타입(`State<any>`) 폴백으로 격하.
- 이 패턴은 Store에만 국한되지 않고 **인스턴스 생성(`DI.Frame`)/이벤트
(`On.EventName`)까지 관통하는 프로젝트 전역 관습으로 확정**됨 — 아래
"인스턴스 생성 / 이벤트 네이밍 인체공학" 절 참고.
**`Pipe`(quad2-try 후보)는 폐기 확정** — 별도 `Pipe` 타입에 소유권/버전
가드를 넣어 재설계하는 대신, State 자체가 파이핑 결합체이고
`state(state)`로 분기하는 위 모델로 완전히 대체됨.
**PA님 코드와의 교차검증(2026-08-04 4차 라운드) — 둘 다 기존 확정 유지**
`.claude/initreq/artworks/EventDrivenProgramming/`(Connection/Event/
Observable/Observer)을 조사한 결과, 두 지점에서 기존 확정과 실제로 다른
선택이 나와 재검토했으나 결론은 변경 없음:
- **전파 모델**: PA님의 pub-sub은 push-invalidate가 아니라 **push-값**
(`Event:fire(...)`가 인자를 그대로 콜백에 전달, `Observable``__newindex`
새 값을 실어 즉시 `changed:fire(key, value)`, dirty-flag/`Get()` pull 단계
자체가 없음). 한때 "leaf(source 하나→sink 하나, 파생 없음)는 PA님처럼
push-값으로 단순화하고 push-invalidate/pull-recompute는 실제 `:Compute`
파생이 있을 때만 쓰자"는 이원화를 검토했으나 **기각** — invalidate+`Get()`
방식도 leaf에서 딱히 더 복잡하지 않고(불리언 플래그 하나 + `Get()`/`emit`
둘로 나뉘는 정도), 오히려 두 메커니즘을 병행하면 "leaf State가 나중에
`:Compute`로 감싸일 때 두 메커니즘을 어떻게 연결하는가"라는 새 경계 문제가
생겨 이원화가 더 복잡함. **결정적으로, PA님 코드엔 애초에 `:Compute`/`:With`
같은 파생·합성 개념 자체가 없음** — quad-v2가 lazy pull을 도입한 이유(여러
소비자가 하나의 파생 State를 공유할 때 오염 방지, 안 쓰이는 연산 스킵)를
PA님 시스템은 처음부터 안 풀려던 문제라, 대등한 반례가 아니었음. **결론:
push-invalidate/pull-recompute로 통일 유지, 변경 없음.** 사용자 최종 확인
문구: "store 전파 처리는 우리 방식이 맞음. 이건 vide 에서 없었던것과
동일함, [PA님] 저기도 디자인 상 해결 못하는 문제가 된거거든. 비 필요
연산과 중복 연산을 지우는건 디자인 단계에서 구성할 일임. 우린 디자인
단계부터 해당 문제를 해결하고 싶었던거야."
- **라이프사이클**: PA님 코드는 GC-native가 아니라 **전부 수동 해제**
(`Connection.connected`는 계산 속성이 아니라 저장된 bool, `Observer`
8개 `subscribeXxx` 헬퍼 전부 명시적 `:unsubscribe()` 필요, weak table은
`Observable`의 subject↔observable 캐시 한 곳뿐). rbvm 기반으로 확정한
"GC 위임, 명시적 dispose 없음" 원칙과 반대 선택이라 재확인 질문했으나,
**GC-native 유지로 확정** — 지금까지 이 정도 규모(명시적 dispose가 꼭
필요할 만큼 큰 자원)를 요구하는 실제 사례가 없었다는 게 사용자 판단. 다만
**완전히 막다른 길은 아님**을 기록해둠: rbvm처럼 관계를 양쪽 다 weak-keyed로
두고 모든 걸 connection 람다에 담아 "연결이 살아있는 동안만 살아있게" 하는
방식이면, 나중에 GC만으로 정말 부족한 케이스가 생겨도 그 connection을 얻어
`disconnect()`하는 명시적 dispose 경로를 추가로 얹는 게 가능한 디자인 —
지금 마일스톤에서는 필요 없어서 안 함(사용자: "필요하다면 dispose 핸들러를
만들어주는 것도 가능한 디자인, 다만 지금까지 요구가 없었음").
## quad2-try 리서치 결과 (완료) — 이전 시도에서 뭘 가져오고 뭘 버릴지 ## quad2-try 리서치 결과 (완료) — 이전 시도에서 뭘 가져오고 뭘 버릴지
@ -256,7 +346,7 @@ State/스트림)를 다뤘던 이전 시도가 있었음. 조사 결과 요약:
- **Slot은 이 시도에서도 사실상 빈 스텁**이었음 — `Insert`의 실제 구현부가 - **Slot은 이 시도에서도 사실상 빈 스텁**이었음 — `Insert`의 실제 구현부가
전부 주석 처리되어 있고, `Notify()`도 빈 함수. 심지어 구 v1(`quad-2`)의 전부 주석 처리되어 있고, `Notify()`도 빈 함수. 심지어 구 v1(`quad-2`)의
`DEV_CHANGELOG.txt`에도 "TODO: slot 기능 구현"이 마지막까지 미완료로 남아있었음 `DEV_CHANGELOG.txt`에도 "TODO: slot 기능 구현"이 마지막까지 미완료로 남아있었음
**가져올 게 전혀 없음**, `research/slot-plan.md`의 from-scratch 설계를 **가져올 게 전혀 없음**, `base/slot-plan.md`의 from-scratch 설계를
그대로 진행하면 됨(재조사 불필요). 그대로 진행하면 됨(재조사 불필요).
- 다른 서브패키지(`quad-roblox`/`quad-gtk`/`quad-lang`/`quad-gen`/`quad-compat`/ - 다른 서브패키지(`quad-roblox`/`quad-gtk`/`quad-lang`/`quad-gen`/`quad-compat`/
`quad-debug`/`quad-docs`)는 전부 파일이 0개인 빈 디렉토리 — `quad-core` 밖엔 `quad-debug`/`quad-docs`)는 전부 파일이 0개인 빈 디렉토리 — `quad-core` 밖엔
@ -315,38 +405,97 @@ copy-on-write 절충안은 2026-08-04 검증 라운드에서 사실상 폐기
RobloxFactory(QuadBase)` 세 줄 정도로 직접 조립하면 됨(별도 번들 `quad` RobloxFactory(QuadBase)` 세 줄 정도로 직접 조립하면 됨(별도 번들 `quad`
패키지로 재수출할 필요 없음, 필요하면 만들어도 됨). 패키지로 재수출할 필요 없음, 필요하면 만들어도 됨).
**열린 질문**: `RobloxFactory`를 같은 `BaseModule`에 여러 번 호출하면 어떻게 **확정(2026-08-04 3차 라운드)**: `RobloxFactory`를 같은 `BaseModule`에 여러
되어야 하는가 — 이미 초기화됐으면 무시(rbvm의 `InitNamespace`류 가드와 번 호출했을 때 — **같은 팩토리로 재호출하면 무시(no-op)**, hot-reload처럼
유사하되, "라이브러리마다 수동 init" 패턴과는 다름)하는 쪽으로 기울어짐. 초기화 스크립트가 다시 도는 경우를 안전하게 만듦. **다른 팩토리
서로 다른 두 곳에서 같은 `base`를 require해서 `RobloxFactory` (`AnotherFactory` 등, 가상의 예)로 재호출하면 에러** — 이건 `base/module-lifecycle-plan.md`의 "bind는 유일 슬롯" 원칙(이미 구현체가 있는데 또
`AnotherFactory`(가상의 예)를 각각 실행하는 경우처럼 충돌 가능성이 있는 다른 구현체로 init하려 하면 오류)이 다루던 것과 정확히 같은 케이스, 이
시나리오가 향후 모듈 스코핑(`New()`, `base/architecture.md` 13번) 논의를 문서의 이전 "무시" 잠정안과 그 문서의 "오류" 잠정안이 서로 모순되는 게
다시 촉발할 수 있음 — 지금은 열어만 둠. 아니라 **같은 팩토리 재호출(무시) vs 다른 팩토리로 유일 슬롯 충돌(에러)이라는
서로 다른 케이스를 각각 가리키고 있었음**. 구현은 모듈 테이블에 "누가
초기화했는지" 마커(`_initializedBy = "roblox"`류, 정확한 이름은 구현 단계)만
두면 됨. 모듈 스코핑(`New()`, `base/architecture.md` 13번)과의 관계도 실은
열려있던 게 아니라 자연히 풀림 — `New()`가 생기면 각 인스턴스가 별도
테이블이 되므로 이 마커도 테이블별로 독립적으로 스코핑됨, 재설계 불필요.
## 인스턴스 생성 / 이벤트 네이밍 인체공학 (2026-08-04 검증 라운드에서 새로 나온 열린 질문) ## 인스턴스 생성 / 이벤트 네이밍 인체공학 — 확정(2026-08-04 3~4차 라운드, PA님 실 코드로 검증됨)
`Quad "Frame"`처럼 문자열로 인스턴스 종류를 지정하는 방식은 타입 추론이 `Quad "Frame"`처럼 문자열로 인스턴스 종류를 지정하는 방식은 타입 추론이
어려움(위 온톨로지 절의 Luau 오버로드 문제와 같은 원인). PA님 DI 스타일은 어려움(위 온톨로지 절의 Luau 오버로드 문제와 같은 원인). 사용자가 실제
`DI.Frame`/`DI.TextLabel`처럼 필드 접근으로 만들어서 자동완성이 자연스럽게 참고 코드를 `.claude/initreq/artworks/DeclarativeProgramming/
됨 — 목록에 없는 타입은 `DI.New<<Frame>> "Frame"`류로 폴백. 이벤트도 DeclarativeInstance.luau`(PA님 작성, UI 포함 전반적 설계 패턴을 시범 적용한
`Event ""` 대신 `On...`류 이름으로 필드 접근하면 타입/자동완성이 쉬워질 수 데모 모듈)에 공유해줘서 직접 확인 — **"DI"는 Dependency Injection이 아니라
있음. 아직 방향 결정 안 됨 — 다음 세션에서 다룰 것. "Declarative Instance"(선언형 인스턴스 생성)**.
**인스턴스 생성 — PA님 코드 그대로 채택**: 처음 제안했던 "필드=1급 타입
경로, 문자열=폴백"이라는 2트랙(`DI.Frame` vs `DI.New<<Frame>> "Frame"`) 구상
보다 실제로는 더 단순했음(`DeclarativeInstance.luau:104-160`) —
**제네릭 생성자 함수 하나(`new<ClassName>(className): from<index<UIInstances,
ClassName>>`)가 알려진 타입과 모르는 타입을 전부 커버**하고, 그중 UI에서 자주
쓰는 클래스 ~25개(`Frame`/`TextButton`/`UICorner` 등, `UIInstances` 타입
테이블에 등록된 것들)만 모듈 로드 시점에 **즉시(eager)** `constructor.Frame =
new("Frame")`처럼 필드로 미리 채워둠 — `__index` 메타메소드 지연 생성이
아니라 그냥 정적 테이블. quad-v2도 이 모양 그대로 채택: 하나의 제네릭
생성자 + 자주 쓰는 것만 정적으로 미리 바인딩.
**이벤트 바인딩 — `On.EventName` 도트액세스 안 씀, PA님 방식(평범한 문자열
키 + 런타임 리플렉션)으로 전환**: `DeclarativeInstance.luau:13-91`
`assign(instance, key, value)``ReflectionService:GetPropertiesOfClass`/
`GetEventsOfClass`로 클래스별 프로퍼티/이벤트 타입을 캐싱해두고, 키가
`RBXScriptSignal` 타입이면 자동으로 `instance[key]:Connect(value)`로 처리함
`Frame { MouseButton1Click = fn }`처럼 별도 네임스페이스 없이 그냥 문자열
키로 씀. 이건 타입 안전성을 어느 정도 포기하는 대가지만(콜백 시그니처까지
Luau가 검증 못 함 — `apply<T,U>(instance: T, properties: U): T & U`가 스키마
검증 없이 구조적으로만 merge), 이미 UB로 남긴 "테이블 리터럴 안 키별 값
타입 자동 검증 불가"와 같은 급의 한계라 손해가 크지 않고, `On.` 접두어 없이
문법이 더 간결해짐 — **사용자 확정**("PA 님 방식 괜찮은듯. 타이핑은 인라인이
되긴 하겠지 정도면 괜찮다"). quad-v2 구현에서는 이 "키가 이벤트인가"
판별을 `isHandlable`로 감싼 pluggable 핸들러(`quad-roblox`가 `Reflection
Service` 기반으로 구현)로 두면 됨 — 별도 `On` 모듈/필드 접근 구조 자체가
불필요해짐.
**Store 쪽 dot-access는 그대로 유지**: `store.key`(1급 타입 경로)/
`store "key"`(문자열 커링, 동적 키 폴백)는 이벤트와 달리 실질적으로 Luau가
타입을 좁혀주는 이득이 있어서(Store 자체가 `{key: State<number>, ...}`
평범한 레코드 타입으로 지어짐) 그대로 유지 — 이벤트만 예외였을 뿐, "정적으로
알려진 것=필드 접근" 원칙 자체가 깨진 건 아님.
**PA님 코드와 대조해서 재확인한 것(변경 없음)**:
- **OOP 회피 결정은 오히려 보강됨** — PA님의 `ObjectOrientedProgramming/
class.luau`도 `setmetatable(methods, {__index = parent})` 체이닝 상속이라
quad-v2가 피하기로 한 quad2-try `Base:Extends`와 같은 모양이고, 제네릭을
파일마다 중첩해서 재선언해야 하는 보일러플레이트까지 동일하게 나타남.
- **Instance 태그는 CollectionService 직접 사용 그대로 유지** — PA님의
`EventDrivenProgramming/Observer.luau``subscribeTaggedInstance`도 얇은
`CollectionService` 래퍼일 뿐. `DataOrientedProgramming/TagService.luau`
이것과 무관하게 plain-table 엔티티(비-Instance 데이터)용 커스텀 태그
인덱스라 지금 quad-v2 스코프 밖 — Instance가 아닌 데이터에 태깅이 필요해질
미래 시나리오를 위한 참고 자료로만 기록.
- **Store/State 전파 모델, 라이프사이클 — 둘 다 재검토 후 기존 확정 유지**
(아래 "Store/State/Source 온톨로지" 절의 "PA님 코드와의 교차검증" 참고).
## 남은 열린 질문 (`.claude/question.md`에도 취합) ## 남은 열린 질문 (`.claude/question.md`에도 취합)
- **`:Compute``with`한 값을 정확히 어떻게 읽는가** — 클로저로 직접 캡처하는 이 문서의 핵심 설계 질문은 2026-08-04 세 라운드(전파 모델/`:Compute`/State
방향은 확정(위 온톨로지 절), 캐싱/무효화 전략(dirty-flag 등)과 `emit` 필요 쓰기 금지/Slot 생존 확인 → dot-access 타입 추론/인스턴스·이벤트 네이밍/
여부는 아직 미정. `RobloxFactory` 재호출 가드)를 거치며 전부 확정됨. 남은 건 순수 API 표면
이름뿐:
- **`state()`/`Source()`/`Get()`/`DI`(또는 다른 이름) 등 정확한 함수·생성자·
모듈 이름** — 방향은 전부 확정, 이름만 구현 단계에서 남음(`On` 모듈은
이벤트 바인딩이 PA님 방식으로 바뀌며 아예 불필요해짐 — 위 "인스턴스 생성 /
이벤트 네이밍" 절 참고).
- **`CreatedRef`(가칭)의 정확한 함수/옵션 이름** — children 배열에 아이템으로 - **`CreatedRef`(가칭)의 정확한 함수/옵션 이름** — children 배열에 아이템으로
넣는다는 방향과 생성/마운트 두 시점 모두 지원한다는 것은 확정, 정확한 API 넣는다는 방향과 생성/마운트 두 시점 모두 지원한다는 것은 확정, 정확한 API
이름만 남음. 이름만 남음.
- **매 `process()` 호출마다 우선순위 스캔 비용** — 실제 구현/벤치마크 단계에서 - **매 `process()` 호출마다 우선순위 스캔 비용** — 실제 구현/벤치마크 단계에서
확인 필요(디자인 자체는 확정됐으므로 더 이상 사용자 확인 대상 아님, 구현 확인 필요(디자인 자체는 확정됐으므로 더 이상 사용자 확인 대상 아님, 구현
검증 대상). 검증 대상).
- Store가 Store를 담는 경우의 실제 소유권(누가 내부 Store를 destroy하는가) —
이 문서의 "재실행 래핑" 제안이 맞다면 자연히 바깥 Store bind가 내부 Store의 **해소된 것**: "Store가 Store를 담는 경우 이중 해제(double-dispose) 방지가
라이프타임도 감싸게 될 텐데, 이중 해제(double-dispose) 방지가 필요한지 확인. 필요한가"는 재검토 결과 질문 자체가 성립 안 함으로 결론 — State/Source
단, `base/lifecycle-pattern.md`의 "destroy 시점엔 아무것도 안 함" 원칙상 이중 그래프 구독이 전부 weak-keyed GC-native(명시적 `dispose()` 호출이 아예 없음,
해제 자체가 걱정할 필요 없는 개념일 수도 있음 — 재검토 필요. `base/lifecycle-pattern.md`의 GC 위임 원칙 재사용)라 "같은 걸 두 번 해제"할
- `RobloxFactory` 중복 호출/충돌 시나리오, 인스턴스 생성·이벤트 네이밍 행위 자체가 존재하지 않음(GC는 멱등). "`:Compute`가 with한 값을 어떻게
인체공학 — 위 두 절 참고, 둘 다 새로 열린 질문. 읽는가"/"emit 필요 여부"도 전파 모델 확정으로 해소, `RobloxFactory` 중복
호출/충돌 시나리오·인스턴스 생성/이벤트 네이밍도 위 절에서 전부 확정.

View file

@ -51,12 +51,12 @@ Store/Slot/Tween/bind-dispatch 설계 결정에 인용되는 원본 비교 자
|---|---|---|---| |---|---|---|---|
| 전파 모델 | push+pull 하이브리드, eager 집합만 즉시 재계산, 생성순 정렬로 글리치 방지 | 순수 push, 즉시 동기 재평가, 다이아몬드 중복 재평가 미해결(저자 인정) | "Store는 값 자체에 항상 eager 발화, cleanup이 key/value를 먼저 확인" 요구사항은 Vide의 push 모델 + eval-전-cleanup 패턴에 더 가까움. 단 Vide의 naive BFS 대신 Fusion의 생성순 정렬 글리치 방지 규율은 채택할 것. | | 전파 모델 | push+pull 하이브리드, eager 집합만 즉시 재계산, 생성순 정렬로 글리치 방지 | 순수 push, 즉시 동기 재평가, 다이아몬드 중복 재평가 미해결(저자 인정) | "Store는 값 자체에 항상 eager 발화, cleanup이 key/value를 먼저 확인" 요구사항은 Vide의 push 모델 + eval-전-cleanup 패턴에 더 가까움. 단 Vide의 naive BFS 대신 Fusion의 생성순 정렬 글리치 방지 규율은 채택할 것. |
| 정리/스코프 | 배열+메타테이블, dependency-agnostic, bind 시점에만 lifetime soft-check | dependency edge와 구조적 owner를 분리한 2중 관계, destroy는 owned만 cascade, 활성 스코프 destroy 하드 가드 | 둘 다 GC 비의존 eager 수동 정리 — rbvm의 Connected+GC 관용구와 정반대 축. quad의 Slot은 Vide처럼 "마운트 소유권"과 "반응 의존성"을 별개 관계로 분리하는 게 안전해 보임(`base/lifecycle-pattern.md`의 rbvm 패턴과는 다른 층위 — rbvm은 인스턴스 파괴 감지, 이건 Slot 내부 소유권 모델). | | 정리/스코프 | 배열+메타테이블, dependency-agnostic, bind 시점에만 lifetime soft-check | dependency edge와 구조적 owner를 분리한 2중 관계, destroy는 owned만 cascade, 활성 스코프 destroy 하드 가드 | 둘 다 GC 비의존 eager 수동 정리 — rbvm의 Connected+GC 관용구와 정반대 축. quad의 Slot은 Vide처럼 "마운트 소유권"과 "반응 의존성"을 별개 관계로 분리하는 게 안전해 보임(`base/lifecycle-pattern.md`의 rbvm 패턴과는 다른 층위 — rbvm은 인스턴스 파괴 감지, 이건 Slot 내부 소유권 모델). |
| 키/값 디스패치 개방성 | SpecialKey 모양은 열려있으나 우선순위 4단계 하드고정 | action()은 등록 없는 태그 인식 방식이지만 key/value 버림, 콜백+우선순위만 | quad는 Fusion의 "디스패처가 key+value+target을 다 받는" 풍부함과 Vide의 "등록 없이 태그로 인식" 인체공학을 합치되, 우선순위 축은 열린 숫자 공간으로 일반화해야 함(`research/bind-system-plan.md`). | | 키/값 디스패치 개방성 | SpecialKey 모양은 열려있으나 우선순위 4단계 하드고정 | action()은 등록 없는 태그 인식 방식이지만 key/value 버림, 콜백+우선순위만 | quad는 Fusion의 "디스패처가 key+value+target을 다 받는" 풍부함과 Vide의 "등록 없이 태그로 인식" 인체공학을 합치되, 우선순위 축은 열린 숫자 공간으로 일반화해야 함(`base/bind-system-plan.md`). |
## 추가로 기록해둘 것 ## 추가로 기록해둘 것
- Vide의 암묵적(ambient stack) 의존성 추적 vs Fusion의 명시적 `use()` 축은 - Vide의 암묵적(ambient stack) 의존성 추적 vs Fusion의 명시적 `use()` 축은
push/pull 축과 독립적인 별개 결정 — quad는 아직 미정(`research/bind-system-plan.md` push/pull 축과 독립적인 별개 결정 — quad는 아직 미정(`base/bind-system-plan.md`
열린 질문 참고). Fusion의 명시적 `use()``checkLifetime` 같은 bind-time 열린 질문 참고). Fusion의 명시적 `use()``checkLifetime` 같은 bind-time
체크를 가능하게 하는 부수 효과가 있음. 체크를 가능하게 하는 부수 효과가 있음.
- 두 라이브러리 다 mount 시 단일 소유권 가드가 없다는 것 자체가 quad Slot의 - 두 라이브러리 다 mount 시 단일 소유권 가드가 없다는 것 자체가 quad Slot의

View file

@ -71,7 +71,7 @@ rbvm 전역에 약한 테이블(weak table, `__mode = "k"/"v"/"kv"`)로 private
- `InitNamespace`/`Registered`-가드/`NewLib` 3종 세트로 "라이브러리마다 하나하나 - `InitNamespace`/`Registered`-가드/`NewLib` 3종 세트로 "라이브러리마다 하나하나
수동 init" 하는 방식은 정확히 사용자가 피하고 싶다고 한 패턴 — 순서 있는 수동 init" 하는 방식은 정확히 사용자가 피하고 싶다고 한 패턴 — 순서 있는
dispose-hook 리스트 자체는 재사용해도, 수동 init 관례는 그대로 베끼지 말 것 dispose-hook 리스트 자체는 재사용해도, 수동 init 관례는 그대로 베끼지 말 것
(팩토리 함수로 대체 — `research/module-lifecycle-plan.md` 참고). (팩토리 함수로 대체 — `base/module-lifecycle-plan.md` 참고).
## 확정: Signal 클래스는 안 만든다 ## 확정: Signal 클래스는 안 만든다
@ -97,7 +97,7 @@ Destroy되면 그 대상에 묶인 것들(Tween 등)도 자연히 죽은 상태
이 원칙 때문에 "값 교체 시 이전 처리를 무르는 것"(아래 `retract`)과 "완전 이 원칙 때문에 "값 교체 시 이전 처리를 무르는 것"(아래 `retract`)과 "완전
소멸 시 정리"는 **하나로 통일** — 후자는 애초에 안 만듦. `research/ 소멸 시 정리"는 **하나로 통일** — 후자는 애초에 안 만듦. `research/
tween-plan.md`/`research/slot-plan.md`의 "cleanup" 표기는 전부 `retract` tween-plan.md`/`base/slot-plan.md`의 "cleanup" 표기는 전부 `retract`
갱신됨(이름 변경 근거는 아래). 갱신됨(이름 변경 근거는 아래).
## 함수 안에서 만든 옵저버도 GC 대상이 되어야 함 — 범용 "생명 바인드 유틸" 필요 ## 함수 안에서 만든 옵저버도 GC 대상이 되어야 함 — 범용 "생명 바인드 유틸" 필요
@ -118,11 +118,29 @@ GC에 묶이지 않음 — v1이 여기저기서 `PropertyChangedSignal`에 연
가질 수 있어서, `Connected`가 false면 실행 자체를 건너뛸 수 있음(죽은 대상에 가질 수 있어서, `Connected`가 false면 실행 자체를 건너뛸 수 있음(죽은 대상에
대한 처리 시도 방지, 위 원칙과 직결). 대한 처리 시도 방지, 위 원칙과 직결).
이건 `research/bind-system-plan.md`의 "핸들러 내부 상태 저장" 유틸과 짝을 이건 `base/bind-system-plan.md`의 "핸들러 내부 상태 저장" 유틸과 짝을
이루는 별도 유틸 — 하나는 "상태를 어디에 저장할지"(weak-keyed per-instance 이루는 별도 유틸 — 하나는 "상태를 어디에 저장할지"(weak-keyed per-instance
저장소), 다른 하나는 "언제까지 실행되어도 되는지"(생명 바인드 + canExecute)를 저장소), 다른 하나는 "언제까지 실행되어도 되는지"(생명 바인드 + canExecute)를
다룸. 둘 다 base가 제공하는 범용 유틸로 확정. 다룸. 둘 다 base가 제공하는 범용 유틸로 확정.
**교차검증(2026-08-04 4차 라운드)**: 사용자가 공유해준 실제 참고 코드
(`.claude/initreq/artworks/EventDrivenProgramming/`, PA님 작성)는 GC-native가
아니라 전부 수동 `:unsubscribe()`/`:disconnect()`로 관리됨 — rbvm 기반
GC-native 원칙과 반대 선택이라 재확인했으나 **GC-native 유지로 확정**(지금까지
명시적 dispose가 꼭 필요할 만큼 큰 자원을 다루는 실제 사례가 없었음). **막다른
길은 아님을 기록**: rbvm처럼 관계를 양쪽 다 weak-keyed로 두고 모든 걸 connection
람다에 담아두는 방식이면, 나중에 GC만으로 부족한 케이스가 실제로 생겨도 그
connection을 얻어 `disconnect()`하는 명시적 dispose 경로를 추가로 얹는 게
가능한 디자인 — 필요성이 드러나면 그때 얹을 하이브리드 여지로만 남겨둠.
**재사용 사례(2026-08-04 2차 라운드)**: Store/State의 무효화(invalidate)
신호를 받는 리스너 클로저도 정확히 이 유틸로 등록됨 — `base/
store-semantics.md`가 예전에 "state 옵저빙 결과로 slot을 조작할 때 생존
여부를 어떻게 확인할지" 미해결로 남겨뒀던 문제가, 사실은 새 메커니즘이
필요한 게 아니라 이 canExecute 게이트를 그대로 적용하면 되는 사례였음(별도
`isInit` 분기 불필요). 상세는 `base/bind-system-plan.md`의 "Store/State/
Source 온톨로지" 절 참고.
## 2026-08-04 검증 라운드에서 보강된 내용 ## 2026-08-04 검증 라운드에서 보강된 내용
**`Connected` 체크는 rbvm 패턴을 그대로 베끼는 게 아니라 base가 인터페이스로만 **`Connected` 체크는 rbvm 패턴을 그대로 베끼는 게 아니라 base가 인터페이스로만

View file

@ -1,7 +1,8 @@
# 모듈 라이프사이클 — 프로바이더 패턴, bind/store는 누가 구현하는가 (착수 전) # 모듈 라이프사이클 — 프로바이더 패턴, bind/store는 누가 구현하는가 (base로 승격됨)
**상태**: research — 방향은 있지만 "누가 store를 구현하는가"는 사용자 스스로 **상태**: base — "누가 store를 구현하는가"까지 포함해 전부 확정되어
"진짜 애매한 지점"이라고 남긴 미해결 항목. 원본: `research/`에서 승격됨(`base/architecture.md`의 "구현 착수: 소스 트리 구조
확정" 절 참고). 원본:
`.claude/initreq/raw-userinput.md` "넘버 바인드는 누가 처리?" / "모듈은 스코핑 `.claude/initreq/raw-userinput.md` "넘버 바인드는 누가 처리?" / "모듈은 스코핑
되는가" / "pluggable 하다면 해당 플러그를 초기화하는 건 누구 몫?" / "다시 돌아와서… 되는가" / "pluggable 하다면 해당 플러그를 초기화하는 건 누구 몫?" / "다시 돌아와서…
bind는 누가 어떻게 구현" / "스토어는 누가 구현해…" 절. 확정된 상위 결정은 bind는 누가 어떻게 구현" / "스토어는 누가 구현해…" 절. 확정된 상위 결정은
@ -32,7 +33,7 @@ RBVM처럼 `init namespace` 하나하나 부르는 방식은 별로(`base/lifecy
인터페이스 상 `bind`를 두고 이것도 pluggable하게 할지 고민 — 단 **1개만 존재할 인터페이스 상 `bind`를 두고 이것도 pluggable하게 할지 고민 — 단 **1개만 존재할
수 있는 형태**로 구현하는 게 맞다고 기울어짐: 이미 bind 구현체가 있는데 또 수 있는 형태**로 구현하는 게 맞다고 기울어짐: 이미 bind 구현체가 있는데 또
init하려 하면 오류, 없는데 뭔가 생성해서 bind하려 해도 오류. 즉 "pluggable init하려 하면 오류, 없는데 뭔가 생성해서 bind하려 해도 오류. 즉 "pluggable
슬롯이지만 유일하게 채워질 수 있는 슬롯" — 위의 `research/bind-system-plan.md`가 슬롯이지만 유일하게 채워질 수 있는 슬롯" — 위의 `base/bind-system-plan.md`가
말하는 "여러 핸들러가 우선순위로 경쟁"하는 것과는 다른 층위: **핸들러 말하는 "여러 핸들러가 우선순위로 경쟁"하는 것과는 다른 층위: **핸들러
레지스트리 자체(그 배후의 실제 bind 구현/백엔드)는 유일해야 하고, 그 안에 레지스트리 자체(그 배후의 실제 bind 구현/백엔드)는 유일해야 하고, 그 안에
등록되는 개별 핸들러들은 여럿+우선순위 경쟁이 맞는 모양.** 등록되는 개별 핸들러들은 여럿+우선순위 경쟁이 맞는 모양.**
@ -46,8 +47,7 @@ init하려 하면 오류, 없는데 뭔가 생성해서 bind하려 해도 오류
계산 속성)를 소유하는 게 맞다고 확정. 추가로 명확해진 것 — **store 바인드가 계산 속성)를 소유하는 게 맞다고 확정. 추가로 명확해진 것 — **store 바인드가
수행하는 "처리된 값을 다시 `process(inst,k,realv)`로 넘기는" 재실행 로직 수행하는 "처리된 값을 다시 `process(inst,k,realv)`로 넘기는" 재실행 로직
자체도 base가 한 번만 구현**해야 함(모든 백엔드/핸들러가 각자 재구현하면 안 자체도 base가 한 번만 구현**해야 함(모든 백엔드/핸들러가 각자 재구현하면 안
됨). 근거: "모든 곳에서 다시 구현하는 건 나쁘니까." → `research/ 됨). 근거: "모든 곳에서 다시 구현하는 건 나쁘니까." → `base/bind-system-plan.md`의 "확정된 디스패치 모델" 절이 바로 이 base 제공 로직.
bind-system-plan.md`의 "확정된 디스패치 모델" 절이 바로 이 base 제공 로직.
부수적으로 확인된 것: 부수적으로 확인된 것:
- **Store 자체의 연산은 더 단순해져도 됨** — v1의 `:Add`/`:With`/`:Tween` 같은 - **Store 자체의 연산은 더 단순해져도 됨** — v1의 `:Add`/`:With`/`:Tween` 같은
@ -55,8 +55,7 @@ bind-system-plan.md`의 "확정된 디스패치 모델" 절이 바로 이 base
일반 함수를 받는 형태로 통일(`base/store-semantics.md` 참고). "너무 verbose한 일반 함수를 받는 형태로 통일(`base/store-semantics.md` 참고). "너무 verbose한
연산들은 오히려 일관성을 해친다"는 게 이유. 연산들은 오히려 일관성을 해친다"는 게 이유.
- **여러 store 값을 묶어 유연하게 처리하는 방법**(`useEffect`류 dependency - **여러 store 값을 묶어 유연하게 처리하는 방법**(`useEffect`류 dependency
array)은 있으면 좋겠다는 요청 — API 시그니처는 미정, `research/ array)은 있으면 좋겠다는 요청 — API 시그니처는 미정, `base/bind-system-plan.md`의 남은 열린 질문 참고.
bind-system-plan.md`의 남은 열린 질문 참고.
- `can execute store bind` 후킹 자체는 `Connected` 계산 속성으로 대체된다는 - `can execute store bind` 후킹 자체는 `Connected` 계산 속성으로 대체된다는
잠정 제안이 그대로 유지되고, 여기에 더해 **완전 소멸(Destroy) 시점엔 아무 잠정 제안이 그대로 유지되고, 여기에 더해 **완전 소멸(Destroy) 시점엔 아무
처리도 필요 없다**는 원칙까지 확정됨(`base/lifecycle-pattern.md`) — 즉 이 처리도 필요 없다**는 원칙까지 확정됨(`base/lifecycle-pattern.md`) — 즉 이
@ -90,7 +89,10 @@ bind-system-plan.md`의 "확정된 디스패치 모델" 절이 바로 이 base
pluggable 참가자라는 점은 확정, 이름만 미정. pluggable 참가자라는 점은 확정, 이름만 미정.
- base 유틸(per-instance 상태 저장소, 생명 바인드 유틸)이 인터페이스만 두고 - base 유틸(per-instance 상태 저장소, 생명 바인드 유틸)이 인터페이스만 두고
실제 구현은 백엔드 팩토리(`RobloxFactory(BaseModule)`류)가 뮤테이션으로 실제 구현은 백엔드 팩토리(`RobloxFactory(BaseModule)`류)가 뮤테이션으로
주입한다는 패턴이 확정됨 — 상세는 `research/bind-system-plan.md`의 "base 주입한다는 패턴이 확정됨 — 상세는 `base/bind-system-plan.md`의 "base
유틸은 인터페이스, 실제 구현은 백엔드 팩토리가 주입" 절 참고. 이 패턴을 유틸은 인터페이스, 실제 구현은 백엔드 팩토리가 주입" 절 참고. **중복 호출
중복 호출했을 때의 가드 동작(멱등 처리)과 모듈 스코핑(`New()`)의 관계는 가드/`New()`와의 관계는 2026-08-04 3차 라운드에서 확정**: 같은 팩토리로
여전히 열려있음. 재호출하면 무시(no-op), 다른 팩토리로 재호출하면 에러(유일 슬롯 충돌 —
바로 아래 "Bind는 누가, 어떻게 구현하는가" 절의 원칙과 일치) — `New()`
생기면 인스턴스별 테이블이 분리되므로 이 가드도 자연히 인스턴스별로
스코핑됨, 별도 재설계 불필요.

View file

@ -68,12 +68,12 @@ v2에서 태그 시스템으로 대체 예정(`base/store-and-tags.md` 참고).
1. Metatable 체이닝으로 "불변 빌더" 흉내내기 → 대신 팩토리 함수로 필요한 곳만 복사 1. Metatable 체이닝으로 "불변 빌더" 흉내내기 → 대신 팩토리 함수로 필요한 곳만 복사
(`raw-userinput.md` "복사 구현은 지양" 항목, `.claude/initreq/raw-userinput.md:83-86`). (`raw-userinput.md` "복사 구현은 지양" 항목, `.claude/initreq/raw-userinput.md:83-86`).
2. 하드코딩된 중앙 디스패처 → pluggable `isHandlable(key,value)` + 우선순위 핸들러 2. 하드코딩된 중앙 디스패처 → pluggable `isHandlable(key,value)` + 우선순위 핸들러
레지스트리 (`research/bind-system-plan.md`). 레지스트리 (`base/bind-system-plan.md`).
3. 흩어진 "GC 안 되게 참조 붙잡기" 핫팩 → rbvm 스타일 `Connected` 계산 속성 + 3. 흩어진 "GC 안 되게 참조 붙잡기" 핫팩 → rbvm 스타일 `Connected` 계산 속성 +
명시적 라이프타임 홀더 (`base/lifecycle-pattern.md`). 명시적 라이프타임 홀더 (`base/lifecycle-pattern.md`).
4. mount가 여러 책임(부모 부기+파괴+child 레지스트리)을 한 모듈에 다 지는 구조 → 4. mount가 여러 책임(부모 부기+파괴+child 레지스트리)을 한 모듈에 다 지는 구조 →
Slot이 child CRUD를 전담, mount는 단일-마운트 강제만 전담 Slot이 child CRUD를 전담, mount는 단일-마운트 강제만 전담
(`research/slot-plan.md`). (`base/slot-plan.md`).
5. tracker.lua, lang.lua 내장 → 둘 다 라이브러리 범위 밖으로 분리(스토리북/ 5. tracker.lua, lang.lua 내장 → 둘 다 라이브러리 범위 밖으로 분리(스토리북/
외부 로케일 라이브러리에 위임). 외부 로케일 라이브러리에 위임).

View file

@ -1,10 +1,27 @@
# Slot — 뮤터블 자식 배열, 엄격한 단일 마운트 소유권 (착수 전) # Slot — 뮤터블 자식 배열, 엄격한 단일 마운트 소유권 (base로 승격됨)
**상태**: research — 설계 방향은 상당히 잡혀 있으나 세부(특히 소유권 이전/해제 **상태**: base — 설계 방향(소유권 귀속, 재마운트 시 throw, retract=폐기)과
시맨틱)는 사용자와 확인 필요. 원본: `.claude/initreq/raw-userinput.md` "slot을 소스 트리 상 패키지 경계까지 확정되어 `research/`에서 승격됨(`base/
구현하도록 하기로 했음" 절. Fusion의 `Children` SpecialKey와 Vide의 mount 무가드 architecture.md`의 "구현 착수: 소스 트리 구조 확정" 절 참고). 원본:
비교는 `base/comparison-fusion-vide.md` 참고 — 결론: **두 라이브러리 어디에도 `.claude/initreq/raw-userinput.md` "slot을 구현하도록 하기로 했음" 절. Fusion의
이런 엄격한 단일 마운트 가드가 없음, quad의 진짜 개선점.** `Children` SpecialKey와 Vide의 mount 무가드 비교는 `base/comparison-fusion-vide.md`
참고 — 결론: **두 라이브러리 어디에도 이런 엄격한 단일 마운트 가드가 없음,
quad의 진짜 개선점.**
## base/roblox 패키지 경계 (2026-08-04, 5차 라운드 확정)
Slot의 add/remove/clear 재조정 로직(추상 자식 참조 기준 — "이 자리에 뭐가
있어야 하는가"를 결정하는 순수 로직)은 `quad-base/src/Dispatch/Slot.luau`
소유. 실제 트리 조작(Instance `Parent` 설정/`Destroy`)은 `quad-roblox/src/
Handlers/Slot.luau`가 그 위에서 적용/해제만 담당 — 다른 모든 인터페이스/구현
분리와 동일한 패턴(`base/architecture.md`의 소스 트리 참고). Slot 자체는
당연히 Instance들을 담게 될 것으로 취급.
**추가로 필요해진 핸들러**: Slot과는 별개로, `k`가 number이고 `v`가 이미
만들어진 Instance인 경우(중첩 인스턴스를 자식으로 직접 넣는 경우, 예:
`Frame { Frame {} }`)를 위한 핸들러도 필요 — `quad-roblox/src/Handlers/
InstanceChild.luau`. Slot은 "뮤터블 배열"을 다루고 이 핸들러는 "정적으로
하나 박아넣는" 더 단순한 경우라 별개로 둠.
## 개념 ## 개념
@ -57,11 +74,11 @@ Slot이 store 바인드로 들어오는 경우, pluggable 처리기에 `retract`
`process`하면 되므로, 부모에게 위임(자식 slot 요소 자체가 스스로 정리를 `process`하면 되므로, 부모에게 위임(자식 slot 요소 자체가 스스로 정리를
실행하는 게 아니라). 실행하는 게 아니라).
이건 `research/bind-system-plan.md`의 "Store 바인드는 재실행 래핑" 확정 이건 `base/bind-system-plan.md`의 "Store 바인드는 재실행 래핑" 확정
모델과 맞물림 — slot이 store 값으로 오면, store 바인드 핸들러가 이전 slot 모델과 맞물림 — slot이 store 값으로 오면, store 바인드 핸들러가 이전 slot
상태를 `retract`하고 새 slot 상태로 다시 `process`하는 사이클을 돈다는 뜻. 상태를 `retract`하고 새 slot 상태로 다시 `process`하는 사이클을 돈다는 뜻.
Slot 핸들러 자신이 감시 중인 값(배열/스토어)이 바뀔 때 child를 갱신하는 Slot 핸들러 자신이 감시 중인 값(배열/스토어)이 바뀔 때 child를 갱신하는
추적(구독)도 `research/bind-system-plan.md`가 말하는 "process 함수가 다른 값 추적(구독)도 `base/bind-system-plan.md`가 말하는 "process 함수가 다른 값
변경을 추적해도 됨" 범위에 속하고, `retract` 시점엔 그 추적만 풀면 됨 — 변경을 추적해도 됨" 범위에 속하고, `retract` 시점엔 그 추적만 풀면 됨 —
Destroy 시점엔 `retract`가 호출되지 않는다는 원칙(`base/lifecycle-pattern.md`)도 Destroy 시점엔 `retract`가 호출되지 않는다는 원칙(`base/lifecycle-pattern.md`)도
동일하게 적용. 동일하게 적용.

View file

@ -1,8 +1,7 @@
# Store 의미론 — 부작용 허용, State는 Store 위의 조합 가능한 캐시 레이어 # Store 의미론 — 부작용 허용, State는 Store 위의 조합 가능한 캐시 레이어
**상태**: base — 부작용 허용/Store 문법 부분은 확정. State/Source 온톨로지는 **상태**: base — 부작용 허용/Store 문법 부분은 확정. State/Source 온톨로지는
2026-08-04 검증 라운드에서 새로 열린 진행 중인 설계 스레드(`research/ 2026-08-04 검증 라운드에서 새로 열린 진행 중인 설계 스레드(`base/bind-system-plan.md` 참고). 원본: `.claude/initreq/raw-userinput.md`
bind-system-plan.md` 참고). 원본: `.claude/initreq/raw-userinput.md`
"store는 부작용을 허용함" / "state는 어떻게 구현하는가" 절. "store는 부작용을 허용함" / "state는 어떻게 구현하는가" 절.
## Store는 부작용을 허용하는 게 기본 디자인 ## Store는 부작용을 허용하는 게 기본 디자인
@ -26,17 +25,23 @@ purity-and-effects-plan.md`와 연결됨).
막을 수는 없지만, 라이브러리로 재사용하려는 컴포넌트가 이런 부작용을 막을 수는 없지만, 라이브러리로 재사용하려는 컴포넌트가 이런 부작용을
가지면 이식성이 떨어짐(`research/purity-and-effects-plan.md`와 연결). 가지면 이식성이 떨어짐(`research/purity-and-effects-plan.md`와 연결).
**미해결 열린 질문**: state를 옵저빙해서 나온 결과로 slot에 `clear`/`add` 같은 **해소됨(2026-08-04 2차 라운드)**: state를 옵저빙해서 나온 결과로 slot에
연산을 할 때, 그 시점에 대상 slot이 이미 죽어있으면 어떻게 되는가 — state가 `clear`/`add` 같은 연산을 할 때, 그 시점에 대상 slot이 이미 죽어있으면
생성되는 지점과 slot이 적용되는 지점이 서로 다른 스코프라, "state 변경이 어떻게 되는가 — 별도 메커니즘을 새로 만들 필요 없이, `base/
발생했을 때 그 slot이 아직 살아있는지"를 어떻게 연관지어 확인할지 아직 명확한 lifecycle-pattern.md`의 "생명 바인드 유틸"(canExecute predicate)을 state-
설계가 없음(`isInit=false`일 때는 허용, `isInit=true`이고 생존 확인 함수가 invalidate 리스너 클로저 등록에도 그대로 재사용하면 됨: 발화 시
거짓이면 불허 정도의 방향은 있으나 미완성). 사용자 본인도 "더 리서치가 필요" `canExecute()` 하나만 확인, 거짓이면 no-op. 한때 검토했던 `isInit=false`
하다고 명시적으로 표시 — `research/bind-system-plan.md`의 "Store/State/Source 허용/`isInit=true`+생존확인 거짓이면 불허 분기 초안은 폐기 — `canExecute`
온톨로지" 스레드와 함께 다룰 것. 하나로 통일(사용자 확정). 상세는 `base/bind-system-plan.md`
"Store/State/Source 온톨로지" 절 참고.
## 정정(2026-08-04 검증 라운드): `State` 프리미티브는 실제로 필요하다 ## 정정(2026-08-04 검증 라운드): `State` 프리미티브는 실제로 필요하다
**후속(2026-08-04 2차 라운드)**: 아래 온톨로지의 전파 모델(push-invalidate/
pull-recompute)·`:Compute` 인자 규칙·State 쓰기 금지·`Source` 독립
프리미티브화·Slot 생존 확인까지 전부 확정됨 — 최신 상세는 `base/bind-system-plan.md`의 "Store/State/Source 온톨로지 — 핵심 메커니즘 확정"
절이 최종 소스, 이 절은 배경/온톨로지 명칭 정의로만 유지.
**이전 버전의 이 절("State 프리미티브는 만들지 않는다")은 틀렸음 — 사용자가 **이전 버전의 이 절("State 프리미티브는 만들지 않는다")은 틀렸음 — 사용자가
검증 라운드에서 직접 정정.** 정확한 모델: 검증 라운드에서 직접 정정.** 정확한 모델:
@ -54,7 +59,7 @@ purity-and-effects-plan.md`와 연결됨).
- 이건 quad2-try(폐기된 이전 시도)의 `Pipe` copy-on-write 절충안을 대체하는 - 이건 quad2-try(폐기된 이전 시도)의 `Pipe` copy-on-write 절충안을 대체하는
방향으로 좁혀짐 — 별도 `Pipe` 타입을 만들어 소유권/버전 가드를 넣는 대신 방향으로 좁혀짐 — 별도 `Pipe` 타입을 만들어 소유권/버전 가드를 넣는 대신
State 자체가 "파이핑 결합체"이고 `state(state)`로 분기하면 될 걸로 보임 State 자체가 "파이핑 결합체"이고 `state(state)`로 분기하면 될 걸로 보임
(`Pipe` 후보는 사실상 폐기 쪽으로 기움). 상세는 `research/bind-system-plan.md`의 (`Pipe` 후보는 사실상 폐기 쪽으로 기움). 상세는 `base/bind-system-plan.md`의
"Store/State/Source 온톨로지" 절 참고 — **아직 완전히 결론난 설계는 아니고, "Store/State/Source 온톨로지" 절 참고 — **아직 완전히 결론난 설계는 아니고,
구현 단계에서 더 다뤄야 할 진행 중인 스레드.** 구현 단계에서 더 다뤄야 할 진행 중인 스레드.**
@ -62,7 +67,7 @@ purity-and-effects-plan.md`와 연결됨).
없으면 연산을 미루는 dirty-flag 방식 등), Luau 타입 시스템에서 `store "key"` 없으면 연산을 미루는 dirty-flag 방식 등), Luau 타입 시스템에서 `store "key"`
같은 커링 호출이 오버로드 함수 타입으로 `state<T>`를 정확히 추론하기 어려운 같은 커링 호출이 오버로드 함수 타입으로 `state<T>`를 정확히 추론하기 어려운
문제(문자열 리터럴이 as-const로 좁혀지지 않는 문제) — 둘 다 열린 채로 문제(문자열 리터럴이 as-const로 좁혀지지 않는 문제) — 둘 다 열린 채로
`research/bind-system-plan.md`에서 계속 다룰 것. `base/bind-system-plan.md`에서 계속 다룰 것.
## Store 값 설정 문법 — v1 인체공학 유지 (확정) ## Store 값 설정 문법 — v1 인체공학 유지 (확정)
@ -82,12 +87,13 @@ purity-and-effects-plan.md`와 연결됨).
인체공학(사용자가 좋아하는 부분)은 유지하되 내부 구현(체이닝이 아니라 팩토리 인체공학(사용자가 좋아하는 부분)은 유지하되 내부 구현(체이닝이 아니라 팩토리
함수)만 바꾼다. 함수)만 바꾼다.
## 여러 스토어 값을 묶어 처리하는 것 (dependency array) — 연구 필요 ## 여러 스토어 값을 묶어 처리하는 것 (dependency array) — 확정
`useEffect`처럼 여러 store 값을 디펜던시로 묶어 파생값을 계산하고 싶다는 `useEffect`처럼 여러 store 값을 디펜던시로 묶어 파생값을 계산하고 싶다는
요구가 있음(v1의 `myStore "a,b"` 콤마-조인 문자열 방식은 폐기 대상 — 요구가 있었음(v1의 `myStore "a,b"` 콤마-조인 문자열 방식은 폐기 대상 —
`base/quad-v1-architecture.md`의 "문자열 DSL" 문제점 참고). 단, **v1의 `base/quad-v1-architecture.md`의 "문자열 DSL" 문제점 참고). **v1의
`:Add`/`:With`/`:Tween` 같은 이름 붙은(named) 체이닝 연산은 만들지 않기로 `:Add`/`:With`/`:Tween` 같은 이름 붙은(named) 체이닝 연산은 만들지 않음** —
확정** — 대신 일반 함수를 받아 처리하는 쪽이 일관적이라는 판단. 구체적인 API 대신 일반 함수를 받아 처리. 최종 형태는 `:With(...)`로 의존성을 모으고
모양(`Store.Combine({a,b}, function(a,b) ... end)`류)은 아직 미정 — `:Compute(fn)`으로 파생 State를 만드는 것으로 확정 — `Store.Combine({a,b},
`research/bind-system-plan.md` 참고. fn)`류 포지셔널 인자 방식은 기각됨. 정확한 lazy 인자 규칙(self/with 값 둘 다
State 핸들로 넘기고 `.value`를 실제로 읽을 때만 계산)은 `base/bind-system-plan.md`의 "Store/State/Source 온톨로지" 절 참고.

View file

@ -5,6 +5,31 @@
있음. 사용자가 Lua/Roblox 엔진에 대해 깊이 아는 사람이라는 전제로, 우선순위 있음. 사용자가 Lua/Roblox 엔진에 대해 깊이 아는 사람이라는 전제로, 우선순위
높은 것부터 정렬. 높은 것부터 정렬.
## 2026-08-04 5차 라운드 완료 — 소스 트리 구조 확정
`.claude/base/architecture.md`의 "구현 착수: 소스 트리 구조 확정" 절 참고.
`base/bind-system-plan.md`/`base/module-lifecycle-plan.md`/`base/slot-plan.md`가
이 라운드에서 `research/`에서 승격됨.
- **패키징**: 최종 목표는 다중 wally 패키지지만, 지금 Luau 툴링(wally 타입
단절, `luau-lsp` 심볼릭 링크 해석 문제)이 불안정해서 당장은 모놀리식 —
`Sleitnick/RbxUtil` 패턴(루트 통합 개발/테스트, 서브폴더마다 자체
`wally.toml`) 채택. `.luaurc` alias는 런타임 require에서 아직 엔진 미지원 —
편집기 경험용으로만 사용, 런타임 require는 상대경로.
- **패키지 경계**: `quad-base` = Store/State/Source 온톨로지+전파 **+**
pluggable 디스패치 엔진(`process`/`retract`, 핸들러 계약, `LifetimeHandle`/
`PerInstanceState` 인터페이스, Ref, Slot 코어 재조정 로직) — 전부
"인터페이스"로, 다른 엔진(GTK 등)에서도 재사용 가능해야 한다는 전제.
`quad-roblox` = 위 인터페이스의 실제 구현체(`RobloxFactory`, Property/Event/
Attribute/Tag/Tween/Slot 적용 핸들러, `DI` 인스턴스 생성자) — 이유: 엔진마다
큰 구현을 중복하지 않기 위함(rbvm의 relation 통합 시도와 같은 동기).
- **Slot 패키지 경계**: 재조정 로직(add/remove/clear)은 base, 실제 Instance
`Parent`/`Destroy` 조작은 roblox의 핸들러가 담당 — `base/slot-plan.md`
"base/roblox 패키지 경계" 절.
- **새 핸들러 필요성 확인**: `k:number, v:Instance`(중첩 인스턴스를 직접
자식으로 넣는 경우, `Frame { Frame {} }`)를 위한 `InstanceChild` 핸들러가
Slot과 별개로 필요 — `quad-roblox/src/Handlers/InstanceChild.luau`.
## 2026-08-04 검증 라운드 완료 ## 2026-08-04 검증 라운드 완료
아래 "확정됨" 절 전체(architecture.md 14개 항목, lifecycle-pattern.md, 아래 "확정됨" 절 전체(architecture.md 14개 항목, lifecycle-pattern.md,
@ -18,51 +43,100 @@ slot-plan.md, tween-plan.md)를 `AskUserQuestion`으로 하나씩 예/아니오
전체가 새로운 열린 설계 스레드로 떠올랐음** — 아래 "최우선 새 열린 질문" 전체가 새로운 열린 설계 스레드로 떠올랐음** — 아래 "최우선 새 열린 질문"
참고. 참고.
- Slot의 `retract` 동작이 "부모 위임" 잠정안에서 "폐기(옮기지 않음)"로 확정 - Slot의 `retract` 동작이 "부모 위임" 잠정안에서 "폐기(옮기지 않음)"로 확정
`research/slot-plan.md`. `base/slot-plan.md`.
- quad2-try의 `Pipe` copy-on-write 후보는 사실상 폐기, `state(state)` 조합 - quad2-try의 `Pipe` copy-on-write 후보는 사실상 폐기, `state(state)` 조합
모델로 대체 — `research/bind-system-plan.md`. 모델로 대체 — `base/bind-system-plan.md`.
- `Connected` 체크/GC 위임/`Destroying` 훅 관련 뉘앙스 보강(엔진별 인터페이스 - `Connected` 체크/GC 위임/`Destroying` 훅 관련 뉘앙스 보강(엔진별 인터페이스
주입, quad는 rbvm보다 즉시정리 필요성이 낮음) — `base/lifecycle-pattern.md`. 주입, quad는 rbvm보다 즉시정리 필요성이 낮음) — `base/lifecycle-pattern.md`.
- base 유틸(per-instance 저장소, 생명 바인드)은 인터페이스만, 실제 구현은 - base 유틸(per-instance 저장소, 생명 바인드)은 인터페이스만, 실제 구현은
`RobloxFactory(BaseModule)`류 백엔드 팩토리가 주입 — `research/ `RobloxFactory(BaseModule)`류 백엔드 팩토리가 주입 — `base/bind-system-plan.md`.
bind-system-plan.md`.
## 최우선 새 열린 질문 (검증 라운드에서 새로 터져나옴) ## 최우선 새 열린 질문 (검증 라운드에서 새로 터져나옴)
- **Store/State/Source 온톨로지 전체** — store는 source 집합체, state는 **전부 확정됨** — 아래 "2026-08-04 3차 라운드" 절 참고. 이 섹션에 새 항목이
source를 감싸는 조합 가능한 캐시(자기 고유 value 없음), `state(state)` 생기면 여기 추가.
분기. `:Compute`의 캐싱/무효화 전략(dirty-flag 등), `emit` 필요 여부, Luau
타입 시스템에서 `store "key"` 커링 호출의 `state<T>` 추론 문제까지 전부 ## 2026-08-04 4차 라운드 완료 — PA님 실 코드(`initreq/artworks`) 교차검증
미정 — 다음 세션 최우선 논의 대상. → `research/bind-system-plan.md`
"Store/State/Source 온톨로지" 절. 사용자가 실제 참고 코드를 공유(`.claude/initreq/artworks/`, PA님 작성) —
- **부작용이 slot 생존 여부와 어떻게 연관되는가** — state 옵저빙 결과로 아래 두 항목이 3차 라운드 잠정안에서 정정됨, 나머지는 재검토 후 기존 확정
slot을 조작할 때, 그 시점에 대상 slot이 죽어있으면 어떻게 처리할지 사용자도 유지:
아직 명확한 답이 없다고 명시. → `base/store-semantics.md`.
- **인스턴스 생성/이벤트 네이밍 인체공학**`Quad "Frame"` 문자열 방식 vs - **"DI" = Declarative Instance**(Dependency Injection 아님) — 3차 라운드의
`DI.Frame` 필드 접근 방식(자동완성/타입추론 트레이드오프). → `research/ 오해 정정.
bind-system-plan.md`. - **이벤트 바인딩 정정**: `On.EventName` 도트액세스 안 씀 — PA님 방식(평범한
- `RobloxFactory` 같은 백엔드 팩토리를 같은 base에 중복 호출했을 때의 가드 문자열 키 + `ReflectionService` 기반 자동 판별, `Frame { MouseButton1Click
동작, 모듈 스코핑(`New()`)과의 관계. → `research/bind-system-plan.md`. = fn }`)으로 전환. Store의 `store.key`는 실질적 타입 이득이 있어 dot-access
유지, 이벤트만 예외.
- **인스턴스 생성**: 2트랙(`DI.Frame`/`DI.New<<Frame>>`) 대신 PA님 코드처럼
제네릭 생성자 함수 하나 + 자주 쓰는 클래스만 정적 필드로 미리 바인딩하는
더 단순한 모양으로 정정.
- **전파 모델(push-invalidate/pull-recompute)·라이프사이클(GC-native)은
재검토 후 기존 확정 유지** — PA님 코드가 반례처럼 보였으나(전자는 push-값
단순 pub-sub, 후자는 전부 수동 해제) 대등한 비교가 아니었거나(파생/합성
개념 자체가 없음) 지금 필요성이 없다는 게 사용자 판단. 라이프사이클은
나중에 하이브리드로 확장 가능한 여지만 기록.
- OOP 회피 결정은 PA님의 `class.luau`도 같은 체이닝 상속 보일러플레이트를
보여 오히려 보강됨. Instance 태그는 CollectionService 직접 사용 유지.
→ 상세: `base/bind-system-plan.md`의 "인스턴스 생성 / 이벤트 네이밍
인체공학" 절, "Store/State/Source 온톨로지"의 "PA님 코드와의 교차검증" 절,
`base/lifecycle-pattern.md`의 "교차검증" 절.
## 2026-08-04 3차 라운드 완료 — dot-access 관습 확정, RobloxFactory 가드 확정 (일부 4차 라운드에서 정정됨)
- **dot-access를 프로젝트 전역 관습으로 확정**: "정적으로 알려진 것=필드
접근, 동적인 것=문자열 호출 폴백"이 Store(`store.key`/`store "key"`)와
인스턴스 생성에 적용됨 — **이벤트는 4차 라운드에서 예외로 정정**(위 참고).
- **`RobloxFactory` 재호출 가드 확정**: 같은 팩토리로 재호출 시 무시
(no-op, hot-reload 안전), 다른 팩토리로 재호출 시 에러(유일 슬롯 충돌).
`New()`와는 인스턴스별 테이블 분리로 자연히 공존 — 재설계 불필요. (4차
라운드에서 변경 없음)
→ 상세: `base/bind-system-plan.md`의 "인스턴스 생성 / 이벤트 네이밍
인체공학" 절, "base 유틸은 인터페이스..." 절의 재호출 가드 부분.
## 2026-08-04 2차 라운드 완료 — Store/State/Source 온톨로지 핵심 메커니즘
위 최우선 질문 중 "Store/State/Source 온톨로지 전체"와 "부작용이 slot 생존
여부와 어떻게 연관되는가"는 `AskUserQuestion`으로 확인 완료, 더 이상 열려있지
않음:
- **전파 모델**: push-invalidate(신호만, 값 안 실음) / pull-recompute(`Get()`
시점에만 재계산) — Fusion식 eager 노드·생성순 정렬은 안 만듦.
- **`:Compute`의 self 인자**: raw 값이 아니라 State 핸들 자체를 넘겨서 self도
with한 값과 동일하게 lazy하게(`.value`를 실제로 읽을 때만 계산) 처리 —
별도 `ComputeWithout` 불필요.
- **State는 쓰기 대상이 아님**: `.value`는 읽기 전용, 값 쓰기는 항상 Store의
`__newindex`로만. `Source`는 Store 내부 디테일이 아니라 값 하나만 다룰 때
쓰는 별도의 가벼운 공개 프리미티브로 격상.
- **Slot 생존 확인**: 별도 메커니즘 없이 기존 "생명 바인드 유틸"의
`canExecute`로 통일 게이트.
- **`store.key` dot-access 타입 추론 제안**: 3차 라운드에서 정식 확정됨(위
"2026-08-04 3차 라운드 완료" 절 참고).
→ 상세: `base/bind-system-plan.md`의 "Store/State/Source 온톨로지 —
핵심 메커니즘 확정" 절, `base/store-semantics.md`, `base/lifecycle-pattern.md`.
## 확정됨 (2026-08-03 질의응답 라운드, 더 이상 열려있지 않음) ## 확정됨 (2026-08-03 질의응답 라운드, 더 이상 열려있지 않음)
- **Store 책임 분리**: base가 `LifetimeHandle` 추상화 + store-bind의 재실행 - **Store 책임 분리**: base가 `LifetimeHandle` 추상화 + store-bind의 재실행
로직(`process(inst,k,realv)` 재귀)을 소유, provider는 "언제 죽었다고 로직(`process(inst,k,realv)` 재귀)을 소유, provider는 "언제 죽었다고
판단할지"(Roblox `Destroying` 등)만 결정. → `research/module-lifecycle-plan.md`, 판단할지"(Roblox `Destroying` 등)만 결정. → `base/module-lifecycle-plan.md`,
`research/bind-system-plan.md` `base/bind-system-plan.md`
- **Signal 클래스**: 안 만듦 — 콜백 + `Connected` 계산 속성만. → `base/ - **Signal 클래스**: 안 만듦 — 콜백 + `Connected` 계산 속성만. → `base/
lifecycle-pattern.md` lifecycle-pattern.md`
- **핸들러 계약**: `isHandlable`+`priority`+`process`+`retract` 4종 유지, - **핸들러 계약**: `isHandlable`+`priority`+`process`+`retract` 4종 유지,
tbox식 세분화는 지금 안 함. → `research/bind-system-plan.md` tbox식 세분화는 지금 안 함. → `base/bind-system-plan.md`
- **Ref**: 도입하되 용도는 "id 조회 대체"가 아니라 "외부 관리 instance를 - **Ref**: 도입하되 용도는 "id 조회 대체"가 아니라 "외부 관리 instance를
점진적으로 마이그레이션/래핑하기 위한 직접 참조 획득". Tween 등 어떤 점진적으로 마이그레이션/래핑하기 위한 직접 참조 획득". Tween 등 어떤
핸들러도 대상 획득에 Ref가 필요하지 않음(항상 `inst`를 직접 받음). 핸들러도 대상 획득에 Ref가 필요하지 않음(항상 `inst`를 직접 받음).
`research/bind-system-plan.md` `base/bind-system-plan.md`
- **`retract`(구 cleanup) 호출 시점**: 값 교체 시에만 호출, Destroy 시엔 - **`retract`(구 cleanup) 호출 시점**: 값 교체 시에만 호출, Destroy 시엔
호출 안 함(quad는 자신이 만든 instance의 생명주기 중간에 있지 않으므로 호출 안 함(quad는 자신이 만든 instance의 생명주기 중간에 있지 않으므로
destroy-time 정리 자체가 불필요/불가능). → `base/lifecycle-pattern.md` destroy-time 정리 자체가 불필요/불가능). → `base/lifecycle-pattern.md`
- **핸들러 내부 상태 저장**: base가 범용 weak-keyed per-instance 저장 유틸 - **핸들러 내부 상태 저장**: base가 범용 weak-keyed per-instance 저장 유틸
제공(모든 핸들러 재사용). → `research/bind-system-plan.md`, 제공(모든 핸들러 재사용). → `base/bind-system-plan.md`,
`base/lifecycle-pattern.md` `base/lifecycle-pattern.md`
- **Store 값 설정 문법**: `__newindex`(`myStore.key = v`) 유지, 괄호 생략 - **Store 값 설정 문법**: `__newindex`(`myStore.key = v`) 유지, 괄호 생략
커링/`:` 체이닝 인체공학도 유지 — 바뀌는 건 내부 구현(팩토리 함수)뿐. 커링/`:` 체이닝 인체공학도 유지 — 바뀌는 건 내부 구현(팩토리 함수)뿐.
@ -75,14 +149,14 @@ slot-plan.md, tween-plan.md)를 `AskUserQuestion`으로 하나씩 예/아니오
- **트윈 오버라이드 기본값**: 멈춤(Cancel), 새 트윈은 현재 보간된 값에서 시작. - **트윈 오버라이드 기본값**: 멈춤(Cancel), 새 트윈은 현재 보간된 값에서 시작.
나머지 세 동작(오버라이드/삭제후재시작/끝점이동후재시작)은 옵션으로 선택 나머지 세 동작(오버라이드/삭제후재시작/끝점이동후재시작)은 옵션으로 선택
가능. → `research/tween-plan.md` 가능. → `research/tween-plan.md`
- **Slot 재마운트 에러**: 즉시 throw. → `research/slot-plan.md` - **Slot 재마운트 에러**: 즉시 throw. → `base/slot-plan.md`
- **`CreatedRef` 콜백 타이밍**: 생성 시점/마운트 시점 둘 다 옵션으로 지원. - **`CreatedRef` 콜백 타이밍**: 생성 시점/마운트 시점 둘 다 옵션으로 지원.
`research/bind-system-plan.md` `base/bind-system-plan.md`
- **여러 store 값 묶기**: `Store.Combine`류 포지셔널 인자 방식과 Vide식 암묵적 - **여러 store 값 묶기**: `Store.Combine`류 포지셔널 인자 방식과 Vide식 암묵적
추적 둘 다 기각 — `:With(...)` + `:Compute(fn)`(fn은 with한 값을 포지셔널 추적 둘 다 기각 — `:With(...)` + `:Compute(fn)`(fn은 with한 값을 포지셔널
인자가 아니라 클로저로 읽음) 방식으로 확정. Unix 파이프에서 영감받은 완전 인자가 아니라 클로저로 읽음) 방식으로 확정. Unix 파이프에서 영감받은 완전
합성 가능한 State 스트림이 이상향이나 기술적 난이도 미확정 — 과거 시도 합성 가능한 State 스트림이 이상향이나 기술적 난이도 미확정 — 과거 시도
(`quad2-try/quad-core`) 리서치 진행 중. → `research/bind-system-plan.md` (`quad2-try/quad-core`) 리서치 진행 중. → `base/bind-system-plan.md`
## quad2-try(이전 폐기된 시도) 리서치 완료 — 추가 확정 ## quad2-try(이전 폐기된 시도) 리서치 완료 — 추가 확정
@ -95,7 +169,7 @@ slot-plan.md, tween-plan.md)를 `AskUserQuestion`으로 하나씩 예/아니오
간단하다는 판단(위 "최우선 새 열린 질문"의 Store/State/Source 온톨로지 간단하다는 판단(위 "최우선 새 열린 질문"의 Store/State/Source 온톨로지
절로 흡수됨). 절로 흡수됨).
- **`Depend(...)` 액션, `:With` 네이밍**은 이전 시도에서도 지향했던 것과 일치 - **`Depend(...)` 액션, `:With` 네이밍**은 이전 시도에서도 지향했던 것과 일치
— 그대로 채택. → `research/bind-system-plan.md` — 그대로 채택. → `base/bind-system-plan.md`
## 순수성/이식성, 기존 인스턴스 바인드 — 확인 완료, 낮은 우선순위로 유지 ## 순수성/이식성, 기존 인스턴스 바인드 — 확인 완료, 낮은 우선순위로 유지
@ -114,7 +188,7 @@ slot-plan.md, tween-plan.md)를 `AskUserQuestion`으로 하나씩 예/아니오
심각하게 볼지 — 지금은 "별도 네임스페이스 개념은 복잡도 대비 이득이 적다"는 심각하게 볼지 — 지금은 "별도 네임스페이스 개념은 복잡도 대비 이득이 적다"는
판단으로 보류 중. → `base/architecture.md` 5번 항목. 판단으로 보류 중. → `base/architecture.md` 5번 항목.
- Store가 Store를 담는 경우 이중 해제(double-dispose) 방지가 실제로 필요한 - Store가 Store를 담는 경우 이중 해제(double-dispose) 방지가 실제로 필요한
상황이 있는지 — 구현 단계에서 실사례로 재검증. → `research/bind-system-plan.md` 상황이 있는지 — 구현 단계에서 실사례로 재검증. → `base/bind-system-plan.md`
--- ---
전체 순서/우선순위는 루트 `CLAUDE.md`가 최종 소스 — 위 표는 힌트일 뿐 그쪽이 전체 순서/우선순위는 루트 `CLAUDE.md`가 최종 소스 — 위 표는 힌트일 뿐 그쪽이

View file

@ -32,13 +32,13 @@
## 정정: Ref 불필요 — 핸들러는 항상 대상 Instance를 직접 받는다 ## 정정: Ref 불필요 — 핸들러는 항상 대상 Instance를 직접 받는다
**이전 초안의 전제가 틀렸음.** Tween 핸들러도 `research/bind-system-plan.md`의 **이전 초안의 전제가 틀렸음.** Tween 핸들러도 `base/bind-system-plan.md`의
"확정된 디스패치 모델"을 그대로 따르는 store-bind 핸들러 중 하나 — `process(inst, "확정된 디스패치 모델"을 그대로 따르는 store-bind 핸들러 중 하나 — `process(inst,
k, v)`가 항상 대상 Instance(`inst`)를 직접 받으므로, 트윈 대상을 얻기 위해 k, v)`가 항상 대상 Instance(`inst`)를 직접 받으므로, 트윈 대상을 얻기 위해
Ref나 네임스페이스드 조회가 필요하지 않음. Tween의 store-bind 핸들러는 "`k`는 Ref나 네임스페이스드 조회가 필요하지 않음. Tween의 store-bind 핸들러는 "`k`는
무엇이든, `v`가 Store인 것"을 잡아내는 우선순위 매우 높은 핸들러로 등록되고, 무엇이든, `v`가 Store인 것"을 잡아내는 우선순위 매우 높은 핸들러로 등록되고,
`inst`는 이미 파라미터로 주어짐. (Ref 자체는 도입되지만 전혀 다른 `inst`는 이미 파라미터로 주어짐. (Ref 자체는 도입되지만 전혀 다른
용도 — `research/bind-system-plan.md`의 Ref 절 참고.) 용도 — `base/bind-system-plan.md`의 Ref 절 참고.)
## `retract`(구 cleanup)로 확정된 오버라이드 시맨틱 ## `retract`(구 cleanup)로 확정된 오버라이드 시맨틱
@ -62,7 +62,7 @@ Ref나 네임스페이스드 조회가 필요하지 않음. Tween의 store-bind
라이브러리가 강제하지 않고, `[Tween(key, tweenData, {onOverride=...})]`처럼 라이브러리가 강제하지 않고, `[Tween(key, tweenData, {onOverride=...})]`처럼
키 설정으로 사용자가 고를 수 있게 열어둠 — `retract(inst, k, v)`가 이전 키 설정으로 사용자가 고를 수 있게 열어둠 — `retract(inst, k, v)`가 이전
값(v)을 받으므로 여기서 선택된 동작을 구현. `retract`가 접근해야 할 "이전에 값(v)을 받으므로 여기서 선택된 동작을 구현. `retract`가 접근해야 할 "이전에
생성한 실제 Tween 객체"는 `research/bind-system-plan.md`가 말하는 base 제공 생성한 실제 Tween 객체"는 `base/bind-system-plan.md`가 말하는 base 제공
범용 유틸(`inst`를 키로 하는 weak-keyed per-instance 상태 저장소)에 담아두면 범용 유틸(`inst`를 키로 하는 weak-keyed per-instance 상태 저장소)에 담아두면
됨. 됨.

116
CLAUDE.md
View file

@ -30,11 +30,25 @@ v2 재작성 시도(`.claude/initreq/quad2-try`)도 리서치 완료 — OOP 상
절충안은 한때 살려볼 후보였으나 **2026-08-04에 사실상 폐기로 재평가**됨(State 절충안은 한때 살려볼 후보였으나 **2026-08-04에 사실상 폐기로 재평가**됨(State
자체가 `state(state)`로 분기하는 쪽으로 대체). 자체가 `state(state)`로 분기하는 쪽으로 대체).
**지금 유일하게 결론 안 난 핵심 설계 이슈는 Store/State/Source 온톨로지** **Store/State/Source 온톨로지 및 관련 인체공학 질문은 2026-08-04 네 라운드에
— 검증 라운드 중 "State 프리미티브는 안 만든다"던 기존 결정이 틀렸다는 게 걸쳐 전부 확정됨**(사용자가 공유해준 실제 참고 코드 `.claude/initreq/
드러나며 새로 열림. `.claude/research/bind-system-plan.md`의 "Store/State/ artworks/`, PA님 작성, 로 4차 교차검증까지 마침) — push-invalidate/
Source 온톨로지" 절, `.claude/question.md`의 "최우선 새 열린 질문" 절 참고 — pull-recompute 전파 모델, `:Compute` self/with 인자를 둘 다 lazy State
아래 "지금 할 일" 1번이 다음 세션이 여기서부터 시작해야 함을 명시. 핸들로 통일, State는 쓰기 불가(값 쓰기는 항상 Store의 `__newindex`),
`Source`는 Store와 별개인 독립 프리미티브로 격상, Slot 생존 확인은 기존
canExecute 유틸 재사용으로 해소, `store.key` dot-access를 타입 추론 1급
경로로(인스턴스 생성도 같은 관습, 단 이벤트는 PA님 방식인 평범한 문자열
키+런타임 리플렉션으로 예외), `RobloxFactory` 재호출 가드(같은 팩토리=무시,
다른 팩토리=에러)까지 확정. 남은 건 정확한 API 표면 이름뿐 —
`.claude/base/bind-system-plan.md` 전체, `.claude/question.md`
"2026-08-04" 절들 참고.
**소스 트리 구조도 확정됨(2026-08-04 5차 라운드)**: `bind-system-plan.md`/
`module-lifecycle-plan.md`/`slot-plan.md` 모두 `research/`에서 `base/`
승격 완료. 모노레포(`quad-base`/`quad-roblox` 서브폴더, RbxUtil 패턴)로
당장은 모놀리식 진행, 패키지 경계(디스패치 엔진까지 base가 인터페이스로
소유)까지 확정 — `base/architecture.md`의 "구현 착수: 소스 트리 구조 확정"
절 참고. 아래 "지금 할 일" 1번이 다음 단계(실제 스캐폴딩)를 명시.
## 계획 문서 구조 ## 계획 문서 구조
@ -80,40 +94,76 @@ Source 온톨로지" 절, `.claude/question.md`의 "최우선 새 열린 질문"
## 지금 할 일 (우선순위순) ## 지금 할 일 (우선순위순)
1. **[다음 세션 최우선] Store/State/Source 온톨로지 설계.** 2026-08-04 검증 1. **[다음 세션 최우선] 실제 스캐폴딩.** 소스 트리 구조는 문서로 이미 확정됨
라운드 중 "State 프리미티브는 안 만든다"는 이전 결정이 틀렸다는 게 (`base/architecture.md`의 "구현 착수: 소스 트리 구조 확정" 절) — 다음
밝혀지면서 새로 터져나온 핵심 설계 이슈 — Store=source 집합체, State= 세션에서 실제로 `quad-base/`, `quad-roblox/` 폴더, 각각의 `wally.toml`,
source를 감싸는 조합 가능한 캐시(`state(state)`로 분기), `:Compute` 루트 `default.project.json`, `.luaurc`를 만들 것. 이 시점부터 `qa-request/`/
캐싱/무효화 전략, Luau 타입 시스템에서 커링 호출의 `state<T>` 추론 문제 `archive/` 폴더가 실제로 쓰이기 시작함.
등이 전부 미정. `.claude/research/bind-system-plan.md`의 "Store/State/ 2. 남은 세부 시그니처(`CreatedRef`/`state()`/`Source()`/`DI`류 정확한
Source 온톨로지" 절에 지금까지 나온 내용이 정리되어 있음 — 이어서 설계를 이름)는 위 항목과 자연스럽게 같이 확정 가능 — PA님 실 코드(`.claude/
구체화할 것. `.claude/question.md`의 "최우선 새 열린 질문" 절도 함께 참고. initreq/artworks/`)를 이미 받아서 교차검증 완료(아래 인수인계 메모
2. 위 온톨로지가 어느 정도 정리되면 `research/bind-system-plan.md`/ 참고), `On` 모듈은 이벤트 바인딩 방식이 바뀌며 아예 불필요해짐.
`research/module-lifecycle-plan.md``base/`로 승격하고, 3. `research/existing-instance-bind-plan.md`는 급하지 않음 — 스코프 논의만
`base/architecture.md`에 "구현 착수" 섹션을 추가해 실제 소스 트리 구조 필요, 구현 착수를 막지 않음.
(어느 서브패키지가 뭘 갖는지)를 확정 — 이 시점부터 `qa-request/`/`archive/` 4. 자율 작업 루프/스케줄 설정 여부는 사용자 결정 대기 중
폴더가 실제로 쓰이기 시작함.
3. 남은 세부 시그니처(`CreatedRef` 정확한 이름, 인스턴스 생성/이벤트 네이밍
인체공학, `RobloxFactory`류 팩토리 중복 호출 가드)는 온톨로지 설계와
자연스럽게 같이 확정 가능.
4. `research/purity-and-effects-plan.md`(특히 "state 옵저빙 결과로 slot을
조작할 때 생존 여부 확인" 열린 질문), `research/existing-instance-bind-plan.md`
급하지 않음 — 스코프 논의만 필요, 구현 착수를 막지 않음.
5. 자율 작업 루프/스케줄 설정 여부는 사용자 결정 대기 중
(`HUMAN_TODO.md` 2번 항목). (`HUMAN_TODO.md` 2번 항목).
## 인수인계 메모 (2026-08-04 세션 종료 시점) ## 인수인계 메모 (2026-08-04 세션 종료 시점, 5차 라운드까지 반영)
**5차 라운드(소스 구조 확정)**: 4차 라운드 종료 시점에 서브에이전트로 먼저
계획 문서 전체의 정합성을 점검(차질 없음 확인) 후 진행. 패키징 방식은
서브에이전트 웹 리서치로 확인(`.luaurc` alias 런타임 미지원, wally 심볼릭
링크/타입 문제, `Sleitnick/RbxUtil`의 모노레포+개별 wally.toml 선례,
`pesde`는 아직 이름) — 모노레포로 당장 진행, 나중에 실제 분리 결정.
패키지 경계는 사용자가 "base=인터페이스, roblox=구현"이라는 원칙을 명확히
해서 확정 — Store/State/Source 온톨로지뿐 아니라 `process`/`retract`
디스패치 엔진, `LifetimeHandle`/`PerInstanceState` 인터페이스, Ref, Slot
코어 재조정 로직까지 전부 `quad-base`가 소유(다른 엔진에서도 재사용
가능해야 한다는 전제, 엔진마다 큰 구현 중복 방지가 목적). Slot도 같은
원칙 적용 확정, 그 과정에서 `k:number,v:Instance` 중첩 인스턴스 자식용
`InstanceChild` 핸들러가 추가로 필요하다는 게 밝혀짐. `bind-system-plan.md`/
`module-lifecycle-plan.md`/`slot-plan.md` 세 문서 모두 `research/`에서
`base/`로 승격 완료, `base/architecture.md`에 전체 소스 트리가 문서화됨 —
실제 폴더/파일 스캐폴딩은 다음 세션(위 "지금 할 일" 1번).
## 인수인계 메모 (2026-08-04 세션 종료 시점, 4차 라운드까지 반영)
2026-08-03에 확정됐다고 표시된 결정 전체(architecture.md 14개 + lifecycle- 2026-08-03에 확정됐다고 표시된 결정 전체(architecture.md 14개 + lifecycle-
pattern/store-semantics/bind-system-plan/module-lifecycle-plan/slot-plan/ pattern/store-semantics/bind-system-plan/module-lifecycle-plan/slot-plan/
tween-plan)를 `AskUserQuestion`으로 하나씩 예/아니오 검증 완료 — 상세는 tween-plan)를 `AskUserQuestion`으로 하나씩 예/아니오 검증 완료 — 상세는
`.claude/question.md`의 "2026-08-04 검증 라운드 완료" 절. 대부분 그대로 `.claude/question.md`의 "2026-08-04 검증 라운드 완료" 절. 검증 과정에서
확인됐지만, 검증 과정에서 사용자가 실시간으로 설계를 더 전개하면서 **"State 사용자가 실시간으로 설계를 더 전개하면서 **"State 프리미티브는 안 만든다"는
프리미티브는 안 만든다"는 기존 결정이 틀렸다는 게 밝혀짐** — Store/State/ 기존 결정이 틀렸다는 게 밝혀짐** — Store/State/Source 온톨로지 전체가 이
Source 온톨로지 전체가 이번 세션에서 새로 열린 가장 중요한 설계 스레드로 세션에서 새로 열린 가장 중요한 설계 스레드로 떠올랐음.
떠올랐고, 아직 결론이 안 났음(위 "지금 할 일" 1번). 그 외 자잘한 정정들(Slot
retract=폐기 확정, Pipe COW 후보 폐기 등)은 각 문서에 바로 반영해둠 — 재조사 **같은 날 이어진 2차/3차 라운드에서 그 온톨로지와 인체공학 질문 전부를
불필요. 확정함**: push-invalidate/pull-recompute 전파 모델(Fusion식 eager 노드/생성순
정렬 불필요), `:Compute`의 self/with 인자를 둘 다 lazy State 핸들로 통일
(별도 `ComputeWithout` 불필요), State는 쓰기 불가(값 쓰기는 Store의
`__newindex`로만) 확정, `Source`는 Store 내부 디테일이 아니라 값 하나만
다룰 때 쓰는 독립 공개 프리미티브로 격상, Slot 생존 확인 문제는 새 메커니즘
없이 기존 canExecute 유틸 재사용으로 해소(부수 효과로 "Store가 Store를 담을
때 이중 해제 방지 필요한가" 백로그 항목도 "명시적 dispose가 없어 질문 자체가
성립 안 함"으로 닫힘), `store.key` dot-access를 타입 추론 1급 경로로 삼는
관습을 인스턴스 생성까지 프로젝트 전역으로 확정, `RobloxFactory` 재호출
가드(같은 팩토리=무시, 다른 팩토리=에러, `New()`와는 인스턴스별 테이블
분리로 자연히 공존)까지 확정.
**4차 라운드에서 사용자가 실제 참고 코드(`.claude/initreq/artworks/`, PA님
작성 — UI 포함 전반적 설계 패턴을 시범 적용한 데모 모듈)를 공유해줘서
교차검증**: "DI"는 Dependency Injection이 아니라 Declarative Instance였음
(정정). 인스턴스 생성은 2트랙 구상보다 단순한 "제네릭 생성자 함수 하나 +
자주 쓰는 클래스만 정적 필드로 미리 바인딩" 모양으로 정정. **이벤트
바인딩은 `On.EventName` 도트액세스를 접고 PA님 방식(평범한 문자열 키 +
`ReflectionService` 기반 자동 판별)으로 전환** — Store의 dot-access는 실질적
타입 이득이 있어 그대로 유지, 이벤트만 예외. 전파 모델(push-invalidate/
pull-recompute)과 라이프사이클(GC-native)은 PA님 코드가 반례처럼 보였으나
(각각 파생 개념이 없는 단순 pub-sub, 전부 수동 해제) 재검토 후 **기존
확정 유지** — 라이프사이클은 나중에 하이브리드로 확장 가능한 여지만 기록.
OOP 회피 결정은 PA님의 `class.luau`도 같은 체이닝 상속 문제를 보여 오히려
보강됨. **더 이상 열려있는 핵심 설계 질문은 없음** — 남은 건 API 표면 이름뿐
(위 "지금 할 일" 참고). 그 외 자잘한 정정들(Slot retract=폐기 확정, Pipe COW
후보 폐기 등)은 각 문서에 바로 반영해둠 — 재조사 불필요.
이전 세션(2026-08-03) 종료 시점 메모: `.claude/` 전체 스캐폴드 + 대부분의 이전 세션(2026-08-03) 종료 시점 메모: `.claude/` 전체 스캐폴드 + 대부분의
핵심 아키텍처 결정을 완료, 로컬 git 저장소 초기화+첫 커밋(원격 없음, 핵심 아키텍처 결정을 완료, 로컬 git 저장소 초기화+첫 커밋(원격 없음,