docs: .claude/ 코퍼스 정리 — reference/ 신설, 승격/기각 분리, 역전 이력 트리밍

base 밖으로 늘 읽을 필요 없는 참고자료(quad-v1-architecture, comparison-fusion-vide)를
새 reference/ 폴더로 분리하고, ui-shorthand-plan을 base로 승격(RoundSize 드롭+
UICorner/UIPadding/UIScale 리네임), additional-primitives-plan을 Blocker/Effect(base
승격)·Batch/Context(archive 기각)·키 기반 컬렉션 재조정(research 잔류)으로 4분할했다.
component-composition-plan의 중복 역전 서사는 기존 archive 포인터로 압축하고, archive
제목 컨벤션을 [역전됨]/[기각됨]로 분화했다. tween-plan에는 retract/canExecute 구분
메모와 트윈 옵션 값 모양 논의를 추가했다. Effect가 Observer 변형인지는 임의로
결론내지 않고 question.md에 열린 질문으로 남겼다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
qwreey 2026-08-07 14:39:20 +09:00
parent e4d6181fcf
commit 5c9d10df66
Signed by: qwreey
GPG key ID: D28DB79297A214BD
26 changed files with 731 additions and 476 deletions

View file

@ -9,10 +9,11 @@
| 폴더 | 기준 |
|---|---|
| `base/` | 결정 완료 + 프로젝트 전체에 걸치는 컨텍스트 — plan/done 개념 없음, 계속 참조되는 배경지식 |
| `base/` | 결정 완료 + 프로젝트 전체에 걸치는 컨텍스트 — plan/done 개념 없음, 계속 참조되는 배경지식. **항상 읽어야 하는** 배경지식만 여기 둠(다른 문서를 이해하는 데 전제되는 것) |
| `reference/` | **[2026-08-07 신설]** 결정 자체가 아니라 다른 문서가 근거로 인용하는 온디맨드 참고 자료(v1 스냅샷, 프레임워크 비교 리서치) — "완료" 개념 없는 건 `base/`와 같지만, 항상 읽을 필요는 없고 해당 문서가 인용될 때만 열어보면 됨. `quadnomicon` 소재 후보가 많음 |
| `research/` | 아직 착수 전, 사용자와 스코프/설계를 더 상의해야 함 |
| `qa-request/` | 구현 완료(코드/에이전트 검증까지 끝남) + 사용자 본인의 실기기(Roblox Studio) QA만 남음 — 지금은 구현 자체가 시작 전이라 비어있음 |
| `archive/` | 완료 + 사용자가 실사용/실기기로 직접 검증까지 마침 (구현 대상). **[2026-08-06 확장]** 완전히 뒤집힌 설계 결정을 원문+역전 이유+diff와 함께 보존하는 용도로도 사용 — 더 이상 능동적으로 참고 안 해도 되지만(토큰 낭비 방지 위해 `base/`/`research/`에서 뺌) `quadnomicon` 소재로는 나중에 쓸 수 있음 |
| `archive/` | 완료 + 사용자가 실사용/실기기로 직접 검증까지 마침 (구현 대상). **[2026-08-06 확장]** 완전히 뒤집힌 설계 결정을 원문+역전 이유+diff와 함께 보존하는 용도로도 사용(제목 `[역전됨]` — 한 번 확정했다가 뒤집힌 것) — 더 이상 능동적으로 참고 안 해도 되지만(토큰 낭비 방지 위해 `base/`/`research/`에서 뺌) `quadnomicon` 소재로는 나중에 쓸 수 있음. **[2026-08-07 확장]** 후보였다가 채택 안 된 것(확정한 적 없이 검토 후 기각)도 같은 방식으로 보존, 제목은 구분을 위해 `[기각됨]``[역전됨]`과 의미가 다르므로 혼동하지 말 것 |
| `feedback/` | 실사용 피드백을 정리한 긴 로그 — 지금은 비어있음(구현 시작 전) |
| `initreq/` | 프로젝트 착수 시 클론해둔 참고 레포(quad v1, fusion, vide, rbvm, tbox, code-docker) + PA님 실 코드(`artworks/`, 4차 라운드 교차검증 근거) + 원본 요청(`req.md`, `raw-userinput.md`) + `quad2-try`(이전에 시도했다 폐기한 v2 재작성 시도 — 리서치 완료, 결론은 `base/bind-system-plan.md`) — 읽기 전용 리서치 소스, 여기 내용을 옮기지 말고 항상 원본 그대로 유지 |
@ -25,8 +26,6 @@
| 문서 | 내용 |
|---|---|
| `architecture.md` | quad-v2 전체 아키텍처 확정 사항 요약(제일 먼저 볼 문서) |
| `quad-v1-architecture.md` | v1(`initreq/quad`) 내부 동작 스냅샷 — "이 문제를 안 반복하려면"의 기준선 |
| `comparison-fusion-vide.md` | Fusion/Vide 아키텍처 비교 리서치 — 설계 결정 근거 자료(전파 모델 등 일부 서술은 이후 라운드에서 뒤집혔으니 `bind-system-plan.md` 쪽을 최신으로 볼 것) |
| `lifecycle-pattern.md` | rbvm의 `Connected`+GC 관용구를 quad-v2가 채택하는 방식 |
| `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차 라운드) |
@ -34,20 +33,28 @@
| `slot-plan.md` | 뮤터블 자식 배열, 엄격한 단일 마운트 소유권, 재마운트 시 throw, base/roblox 패키지 경계까지 확정 |
| `modifier-plan.md` | Modifier는 런타임 plug 아닌 정적 merge, immutable+clone 기반 체이닝 — 메커니즘 확정, getter 이름만 남음 |
| `purity-and-effects-plan.md` | 컴포넌트 "순수성"이 아니라 "이식성" 문제로 재정의 — 문서 경고 수준으로 확정 |
| `component-composition-plan.md` | 컴포넌트=플레인 함수, State/Source 읽기·쓰기 경계, Source가 State를 구조적으로 만족(`StoreSource`/`RefSource` 중간안은 전부 폐기됨) — modifier/Ref 컴포넌트 경계 통과까지 전부 확정, 남은 건 API 이름뿐 [정정: 2026-08-04 승격됐으나 이 표에 반영이 안 돼있던 걸 2026-08-06 뒤늦게 수정] |
| `component-composition-plan.md` | 컴포넌트=플레인 함수, State/Source 읽기·쓰기 경계, Source가 State를 구조적으로 만족 — modifier/Ref 컴포넌트 경계 통과까지 전부 확정, 남은 건 API 이름뿐. **[2026-08-07 정리]** 폐기된 `StoreSource` 프록시 설계로의 역전 이력은 본문에서 빼고 `archive/store-source-proxy-reversed.md` 포인터로 압축 |
| `additional-primitives.md` | **[2026-08-07 신설]** `Blocker`(여러 Source를 한꺼번에 바꿔도 파생값 재계산이 한 번만 되게, State 마일스톤과 함께 개발)와 `Effect`(leaf 죽음에 확정 정리) — Blocker는 메커니즘+이름 확정, Effect는 Observer와의 관계가 아직 미해결(문서 내 "미해결" 절, `question.md` 0번) |
| `ui-shorthand-plan.md` | **[2026-08-07 `research/`에서 승격]** `UICorner`/`UIPadding`/`UIScale` 인라인 편의 키 — 이름(v1 `Corner`/`PaddingAll`/`Scale`에서 Modifier 필드명과 안 겹치게 `UI` 프리픽스로 확정)·메커니즘(Handler)·패키지 배치(quad-roblox 코어)·store-bind 가능성까지 전부 확정. 이미지 라운드 트릭(`RoundSize`)은 드롭 — `archive/ui-shorthand-roundsize-dropped.md` 참고 |
## `reference/` — 온디맨드 참고 자료 (2026-08-07 신설)
| 문서 | 내용 |
|---|---|
| `quad-v1-architecture.md` | v1(`initreq/quad`) 내부 동작 스냅샷 — "이 문제를 안 반복하려면"의 기준선. **[2026-08-07 `base/`→`reference/` 이동]** v2의 결정 자체가 아니라 다른 문서가 인용하는 온디맨드 자료라 항상 읽을 필요는 없음 |
| `comparison-fusion-vide.md` | Fusion/Vide 아키텍처 비교 리서치 — 설계 결정 근거 자료(전파 모델 등 일부 서술은 이후 라운드에서 뒤집혔으니 `bind-system-plan.md` 쪽을 최신으로 볼 것). **[2026-08-07 `base/`→`reference/` 이동]**, `quadnomicon` 소재 후보 |
## `research/` — 아직 착수 전, 상의 필요
| 문서 | 내용 | 우선순위 |
|---|---|---|
| `tween-plan.md` | 트윈을 Store 밖 특수 bind key로 처리, 기본 오버라이드는 Cancel | 중 — 세부 옵션만 남음 |
| `tween-plan.md` | 트윈을 Store 밖 특수 bind key로 처리, 기본 오버라이드는 Cancel, 트윈 옵션 값 모양(TweenInfo vs 편의 필드)은 신규 열린 논의 | 중 — 세부 옵션만 남음 |
| `existing-instance-bind-plan.md` | 이미 생성된 인스턴스 재바인드 — 착수 안 하되 "미지원" 확정도 안 함, 열린 가능성 유지 | 하 — v2 초기 스코프 제외 |
| `debug-tooling-plan.md` | 실물 Instance→코드 위치 역추적 Studio 플러그인(`quad-debug`) — 채널 실현 가능성(BindableEvent/Function 크로스 컨텍스트)까지 실측 검증 완료, 세부 API 이름·구현만 남음 | 하 — 사용자가 "quad 개발 완료 전엔 착수 못 함"으로 직접 후순위 지정, base 설계 시 훅 확장 지점만 고려 |
| `documentation-plan.md` | 문서 사이트 구조(초심자/api/심화/`quadnomicon` 4축, 백엔드별 트랙 분리) + UI 네이밍 컨벤션·Store 부작용 패턴·권장 이벤트 핸들링 3개 세부 문서 뼈대 | 하 — 착수 시점 미정, 구조/스코프만 합의된 상태 |
| `documentation-content-map.md` | 위 4축에 실제로 뭘 채울지 `base/` 전체를 초심자/api/심화/skip으로 서베이한 콘텐츠 맵 — 초심자 core loop 목차 초안 포함 | 하 — 문서화 착수 시점의 목차/우선순위표로 쓸 것 |
| `framework-comparison-findings.md` | quad vs Fusion/Vide/react-lua 정직한 비교(실 소스 근거) — quad 강점, 진짜 불리한 점 중 고칠 만한 것 3개, 못 고치는 트레이드오프 정리 | 하 — 사용자 검토 후 반영 여부 결정 대기 |
| `ui-shorthand-plan.md` | UICorner/UIPadding/UIScale 인라인 편의 키(v1 `Corner`/`PaddingAll`/`Scale` 선례) — 여전히 필요한 기능으로 재확정(RoundSize만 네이티브 UICorner로 대체돼 불필요), 메커니즘(Handler)·패키지 배치(quad-roblox 코어) 확정 | 하 — 결론 남, M10 전후 구현하면 됨 |
| `additional-primitives-plan.md` | 확정 프리미티브(Source/State/Store/Ref/Observer/Modifier/Slot/DI)만으로 충분한지 웹 프레임워크·Fusion/Vide/v1 소스 근거로 조사 — 키 기반 동적 컬렉션 재조정(Fusion `ForPairs`/Vide `indexes()`류)이 가장 명확한 빈 자리로 확인, Effect/Batch/Context는 부차적 후보 | 상 — 사용자 판단 대기, 착수 전 M0/M1 스코프에 영향 줄 수 있음 |
| `additional-primitives-plan.md` | **[2026-08-07 범위 축소]** 확정/기각된 Effect·Blocker·Batch·Context는 `base/additional-primitives.md`·`archive/`로 분리됨 — 이제 **키 기반 동적 컬렉션 재조정**(Fusion `ForPairs`/Vide `indexes()`류에 대응하는 프리미티브가 quad엔 없음) 하나만 다룸 | 상 — 사용자 판단 대기, 착수 전 M0/M1 스코프에 영향 줄 수 있음 |
| `pre-implementation-audit.md` | M0 착수 직전 크리티컬 감사(2026-08-06 신설) — `base/` 전체를 모호성/지연결정리스크/단순화후보 세 렌즈로 재검토, 11개 우선순위1(M0~M4 착수 전 확인 권장) + 11개 우선순위2 + 2개 단순화후보 | 상 — M0 착수 전 최소 우선순위1 항목 확인 권장 |
| `v1-compat-plan.md` | v1 하위호환(compat) 레이어 — `quad-roblox-v1-compat` 패키지, v2→v1 단방향 브리지(`state:Observer()`+v1 프로퍼티 재대입), v2-in-v1/v1-in-v2 두 임베딩 방향의 기술 규칙까지 확정. quad2-try의 `quad-compat`은 빈 폴더로 실제 시도된 적 없었음을 확인 | 하 — Slot이 foreign Instance를 어떻게 다루는지만 Slot 코어 구현 시점까지 미결 |
@ -55,7 +62,11 @@
| 문서 | 내용 |
|---|---|
| `store-source-proxy-reversed.md` | 2026-08-04에 확정했던 `StoreSource` 프록시 설계(Store가 Source를 감춘 별도 프록시로 감쌈) — 2026-08-06 세 번째 세션에서 "Source가 State를 구조적으로 만족" 재구성으로 완전히 대체됨. 원문·역전 이유·신구 비교표 보존, `quadnomicon` 소재 후보 |
| `store-source-proxy-reversed.md` | [역전됨] 2026-08-04에 확정했던 `StoreSource` 프록시 설계(Store가 Source를 감춘 별도 프록시로 감쌈) — 2026-08-06 세 번째 세션에서 "Source가 State를 구조적으로 만족" 재구성으로 완전히 대체됨. 원문·역전 이유·신구 비교표 보존, `quadnomicon` 소재 후보 |
| `ref-phase-option-reversed.md` | [역전됨] `CreatedRef``phase` 옵션 — 위치 기반 순서 + `PreRef` 신설로 대체됨 |
| `ui-shorthand-roundsize-dropped.md` | **[기각됨, 2026-08-07 신설]** v1 `RoundSize`(이미지 9-slice 라운드 트릭) — 네이티브 `UICorner`로 대체되어 포팅 불필요. 이 판단이 한 차례 "Corner/PaddingAll/Scale 숏핸드 전체가 불필요하다"로 과잉일반화됐다가 정정된 이력 포함 |
| `batch-rejected.md` | **[기각됨, 2026-08-07 신설]** lexical `Batch(fn)` — 코루틴 yield 위에서 구조적으로 위험해 기각, 값 기반 `Blocker`(`base/additional-primitives.md`)로 대체 |
| `context-rejected.md` | **[기각됨, 2026-08-07 신설]** `Context`(트리 하위 암묵 전파) + 대안이던 레이어드 Store 둘 다 기각 — 명시적 타입 강제 Store 전달로 충분하다는 판단 |
## 참고

View file

@ -0,0 +1,63 @@
# [기각됨] `Batch(fn)` — lexical block 기반 지연/합치기, `Blocker`로 대체됨
**기각 일시**: 2026-08-06~07. **현재 유효한 설계**: `base/
additional-primitives.md`의 "Blocker" 절 — 이 문서가 다루는 것과 같은
문제("여러 Source를 한꺼번에 바꿔도 파생값 재계산이 한 번만 되게")를
값 기반으로 풀어 대체함. 이 파일은 더 이상 능동적으로 참고할 필요 없음
(구현에 안 씀) — "왜 lexical Batch를 기각하고 값 기반 Blocker를 택했는가"가
`quadnomicon`(프레임워크 설계자용 심화 콘텐츠) 소재로 가치 있어서 사유를
통째로 보존해둔 것.
**중요**: 이건 "값 기반 지연/합치기"라는 문제 자체를 기각한 게 아니라,
**그 문제를 lexical block(Solid `batch()`/MobX `runInAction()`류)으로
풀려는 접근만** 기각한 것 — 실제 해법은 완전히 다른 별개 primitive인
`Blocker`로 채택됨.
## 무엇을 검토했었나
`Batch(fn)`을 "플래그 세우고 `fn` 실행, 끝나면 flush"로 구현하는 안 —
`fn` 안에서 여러 `Set()`을 몰아서 호출해도 소비자에게 전파는 `fn`이 끝난
뒤 딱 한 번만 되게 하는, 함수/코루틴 스코프 lexical transaction 블록.
### "즉시 pull"이 뭔지 (참고용 예시)
store-bind 핸들러(예: `Frame { BackgroundColor3 = total }`)는 lazy가
아니다 — 화면에 실제로 반영하는 "누군가"가 바로 이 핸들러 자신이라,
무효화 신호를 받자마자 스스로 `Get()`하고 바로 대입한다:
```lua
local total = a:With(b):Compute(function(av, bv) return av + bv end)
Frame { BackgroundColor3 = total }
a:Set(1) -- total 무효화 → 핸들러가 즉시 (av=1, bv=이전b)로 재계산+대입
b:Set(2) -- total 무효화 → 핸들러가 즉시 (av=1, bv=2)로 다시 재계산+대입
-- BackgroundColor3가 중간값을 한 번 거쳐가고, 두 번 대입됨
```
## 기각 이유 — 코루틴 yield 위에서 구조적으로 위험
`Batch(fn)`을 "플래그 세우고 `fn` 실행, 끝나면 flush"로 구현하면 **`fn`
yield하는 순간 위험해진다**(사용자 지적, 정확함):
1. 플래그가 전역이면, yield 중 스케줄러가 돌리는 무관한 코루틴의 `Set()`
이 Batch에 잘못 휘말릴 수 있음.
2. 플래그를 코루틴 스코프로 만들어도(Fusion `Contextual`류 코루틴 키 weak
table), `fn` 안에서 새 코루틴을 스폰하는 API(Promise, `task.spawn`)를
부르면 그 새 코루틴은 배치 스코프를 상속 못 받아 일부 Set이 새어나감.
3. `fn`이 영원히 재개 안 되면(장시간 대기, 리크된 코루틴) flush가 영영 안
일어나 store-bind 핸들러들이 화면을 무기한 stale 상태로 방치 — 즉시
pull보다 더 나쁜 실패 모드.
이건 구현을 잘 짜서 피할 수 있는 버그가 아니라 **lexical block 모델
자체가 협조적 스케줄링 환경과 구조적으로 안 맞는 것**으로 판단, 기각.
## 왜 완전히 헛수고는 아니었나
"지연 구간을 표현하고 싶다"는 문제의식 자체는 정확했고, `Blocker`
정확히 그 문제를 콜스택/코루틴이 아니라 **값**(`Blocker` 객체의
`On()`/`Off()`)으로 표현해 풀었다 — Batch가 무너뜨렸던 세 가지 실패
모드(전역 플래그 오염, 새 코루틴 미상속, 영구 yield로 인한 무기한 방치)가
Blocker에선 구조적으로 전부 해당 안 됨(`On()`/`Off()` 사이에 얼마나 많은
yield/코루틴 전환이 끼어도, 심지어 완전히 다른 코루틴에서 `Off()`
불러도 문제없음). `quadnomicon`에서 "콜스택/코루틴 스코프로 상태를
표현하려던 시도가 왜 항상 위험한가"의 구체 사례로 쓰기 좋음.

View file

@ -0,0 +1,61 @@
# [기각됨] `Context`(트리 하위 암묵 전파) + 대안이었던 "레이어드 Store"
**기각 일시**: 2026-08-06~07. **현재 유효한 설계**: 명시적 타입 강제
Store 전달(`props.Theme: Store<Theme>`처럼 컴포넌트가 필요한 걸 named
parameter로 명시적으로 요구) + 오버라이드가 필요한 지점에서
`Store({...부모값, 변경필드=새값})`을 한 번 명시적으로 만들어 그 지점부터
평소처럼 prop으로 넘기는 것 — 새 primitive 없이 이미 있는 Modifier의
"merge, 나중 게 이김" 패턴 재사용. 이 파일은 더 이상 능동적으로 참고할
필요 없음(구현에 안 씀) — "왜 Context가 없는가"가 `quadnomicon`(프레임워크
설계자용 심화 콘텐츠) 소재로 가치 있어서 사유를 통째로 보존해둔 것.
## 무엇을 검토했었나
React `Context`/Vue `provide`-`inject`류, 트리 상위에서 값을 하나 심어두면
중간 컴포넌트가 명시적으로 전달하지 않아도 하위 어디서든 그 값을 읽을 수
있는 암묵적 전파 메커니즘.
### 난이도 판정 요약
서브에이전트 조사 결과: 동기적 저작 트리에 한정한 얕은 버전(Fusion
`Contextual`류 코루틴 키 weak table push-pop)은 구현 난이도 **낮음**이지만,
quad가 정상 패턴으로 확정한 "Slot에 이벤트 핸들러/코루틴에서 비동기로
자식이 추가되는 경우"엔 **에러 없이 조용히 기본값으로 폴백**하는 함정이
있음. 완전 자동(비동기 추가까지 자동 전파)은 Roblox Luau에
thread-local/async-context-propagation 훅이 없어 **사실상 불가**(플랫폼
한계, Node `AsyncLocalStorage`/Python `contextvars`도 "자동"이 아니라
"async 경계마다 명시적 캡처+재진입"으로 같은 문제를 품).
## 기각 이유
얕은 버전조차: (1) `base/slot-plan.md`가 정상 패턴으로 명시한 Slot 비동기
추가에서 가장 먼저, 가장 조용히 깨짐. (2) `research/debug-tooling-plan.md`
"모든 연결은 선언된 그래프여야 한다"는 quad-debug 철학과 충돌하는 안 보이는
채널을 만듦.
## 대안이었던 "레이어드 Store"도 철회 (사용자 반박 수용)
Context 대신 권고했던 대안 — "레이어드 Store"(자식 Source 모음이 없는
키는 부모로 `__index` 폴백)도 사용자 반박으로 철회됨:
- quad는 이미 **타입으로 강제되는 명시적 Store 전달**을 갖고 있고, 이건
Context의 "Provider 안 넣으면 조용히 기본값/에러"보다 **더 안전**하다
(컴파일타임에 걸림 vs 런타임에 조용히 새는 값) — Context보다 나은 지점.
- "필드 일부만 오버라이드, 나머지는 부모 값 그대로"가 필요하면, 오버라이드
지점에서 `Store({...부모값, 변경필드=새값})`을 **한 번 명시적으로**
만들어서 그 지점부터 평소처럼 prop으로 넘기면 끝 — Modifier가 이미 쓰는
"merge, 나중 게 이김" 패턴 재사용 가능, 새 primitive 불필요. 레이어드
Store(읽는 시점에 몇 단계를 거슬러 올라가는지 안 보이는 자동 폴백)는
정확히 Context와 같은 이유(디버깅 어려움 — "이 값이 왜 이거지?"를
추적하려면 부모 체인을 다 훑어야 함)로 얻는 것보다 잃는 게 크다.
- 서드파티 라이브러리가 뭔가 필요하면, 타입이 강제하는 명시적 요구
(`props.Theme: Store<Theme>`)가 "몰래 안 줘서 죽는다"보다 나은 실패
모드 — Slot 기반 저작 모델(부모가 자손을 직접 구성)에서도 이런 요청은
자연스럽게 props로 흐른다.
## 결론
Context, 레이어드 Store 둘 다 프리미티브로 만들지 않음. "왜 Context가
없는가"(명시적 Store 전달이 이미 그 역할을 하고, 타입 강제가 Context의
실패 모드보다 안전하다는 논증)는 `quadnomicon` 에세이 후보로 등록
(`research/documentation-content-map.md` 참고).

View file

@ -0,0 +1,44 @@
# [기각됨] `RoundSize`(이미지 9-slice 라운드 트릭) 포팅 — 네이티브 `UICorner`로 대체되어 불필요
**기각 일시**: 2026-08-06. **현재 유효한 설계**: `base/ui-shorthand-plan.md`
이 문서는 v1의 `RoundSize`가 왜 포팅 대상에서 빠졌는지, 그리고 그 판단이
한 차례 잘못 일반화됐다가 정정된 이력을 보존해둔 것. 능동적으로 참고할
필요 없음(구현에 안 씀) — `RoundSize`류 "네이티브 Instance가 나중에 생겨
워크어라운드가 필요 없어진 사례"는 `quadnomicon` 소재로 가치 있음.
## 무엇이었나
v1 `class.lua`가 지원하던 특수 키 `RoundSize = 16`(`ImageLabel`/
`ImageButton` 전용) — `UICorner`가 아니라 이미지 자체를 9-slice로 잘라
둥글게 보이게 만드는 트릭(`round.SetRound()`). `UICorner`/`UIPadding`/
`UIScale` 자동 생성 숏핸드(`Corner`/`PaddingAll`/`Scale`, 현재
`base/ui-shorthand-plan.md`가 이어받은 기능)와 겉보기엔 "인라인 리터럴 값
하나로 GUI를 꾸민다"는 카테고리가 비슷해 보이지만, **메커니즘 자체가
완전히 다름**(하나는 별도 Instance 생성, 하나는 이미지 처리) — 이 문서가
쓰인 이유가 바로 이 둘을 혼동하지 않기 위함.
## 기각 이유
`RoundSize`는 **당시 Roblox에 `UICorner` 같은 네이티브 구현체가 없었기
때문에** 존재하던 워크어라운드였음. 지금은 `UICorner`가 안정적인 네이티브
Instance라 이미지 대상에도 그냥 실제 `UICorner`를 붙이면 되므로, 이미지를
9-slice로 잘라 둥글게 "보이게" 만드는 트릭 자체를 그대로 포팅할 이유가
없음 — **포팅 안 함으로 확정**.
## 왜 archive에 남기나 — 한 차례 과잉일반화됐다가 정정된 이력
`RoundSize` 하나를 드롭하기로 한 판단이, 초안 작성 과정에서 실수로
**"UICorner가 네이티브가 됐으니 Corner/PaddingAll/Scale 숏핸드 자체가
불필요하다"는 훨씬 넓은 결론으로 잘못 일반화된 적이 있었음**("이전 정리
('포팅 불필요')는 오해였고 정정함"). 사용자가 직접 반박해 정정됨:
`UICorner`가 네이티브 Instance가 됐다는 사실은 "이미지를 트릭으로 둥글게
보이게 할 필요가 없어졌다"는 것만 의미할 뿐 — `UIScale`/`UIPadding`류가
**여전히 부모에 Parent해야 하는 별도 Instance**라는 구조적 사실 자체는
전혀 안 바뀌었으므로, `Corner`/`PaddingAll`/`Scale` 숏핸드(현재
`UICorner`/`UIPadding`/`UIScale`)의 존재 이유는 그대로 유효.
**교훈(재사용 가능)**: "네이티브 Instance가 생겼다"는 사실 하나로부터
"관련 숏핸드 전체가 불필요해졌다"를 성급히 일반화하지 말 것 — 워크어라운드가
드롭되는 이유(네이티브 대체재 등장)와 편의 숏핸드가 필요한 이유(별도
Instance를 만들어 Parent해야 하는 구조적 번거로움)는 서로 다른 축이라,
하나가 해소됐다고 다른 하나도 자동으로 해소되는 게 아님.

View file

@ -0,0 +1,157 @@
# 추가 확정 프리미티브 — Blocker / Effect
**상태**: base — `research/additional-primitives-plan.md`(다른 프레임워크 대비
갭 분석)에서 갈라져 나온 두 확정 프리미티브. Batch(lexical block)/Context는
기각되어 각각 `archive/batch-rejected.md`/`archive/context-rejected.md`로,
아직 미확정인 키 기반 동적 컬렉션 재조정은 `research/additional-primitives-plan.md`
그대로 남아있음 — 이 문서는 **확정된 것만** 다룬다.
## Blocker — 여러 Source를 한꺼번에 바꿔도 파생값 재계산이 한 번만 되게
**왜 필요한가**: `state1, state2 -> state3`처럼 여러 소스가 한 파생값에
합류할 때, 둘을 한 번에 바꾸면 소비자에게 두 번 전파(재계산+재대입)되는
문제. lexical `Batch(fn)`(Solid `batch()`/MobX `runInAction()`류)으로
풀려던 접근은 코루틴 yield 위에서 구조적으로 위험해 기각됨 — 상세 근거는
`archive/batch-rejected.md` 참고, 여기서 반복하지 않음. **Blocker는 그
문제를 콜스택/코루틴이 아니라 사용자가 들고 있는 "값"으로 표현**해서 이
위험을 구조적으로 우회한다.
**store 개발(M3)과 밀접하게 연관됨** — `state:Block(blocker)`가 State
위에 얹히는 메소드이므로 `base/store-semantics.md`의 Store/State/Source
온톨로지, 특히 push-invalidate/pull-recompute 전파 모델(`base/
bind-system-plan.md` "전파 모델 확정" 절)을 전제로 함. 별도 파일로 두되
State와 같은 마일스톤(`ROADMAP.md` M3)에서 함께 구현할 것.
### 메커니즘 (확정)
```
Blocker() -> blocker -- 생성자
blocker:On() -> self -- IsBlocked = true로만 설정, 그 외 아무것도 안 함
blocker:Off() -> self -- IsBlocked = false로 먼저 설정, 그 다음 등록된
-- onunblock 핸들 전부 실행(순서 무관, idempotent)
state:Block(blocker) -> state -- 새 gated state 반환. **호출되는 즉시**(나중에
-- 처음 블록될 때가 아니라) onunblock 핸들을
-- blocker의 weak 배열에 등록.
```
gated state의 동작:
- 원본 state가 emit(무효화)될 때, 이 gated state로 전파를 시도.
- `blocker.IsBlocked`이면: 전파 안 하고 `HasBlockedEmit = true`만 세팅.
- `blocker.IsBlocked`가 아니면: 평소처럼 그냥 전파(투명하게 통과).
- `blocker:Off()`가 실행하는 onunblock 핸들은: `HasBlockedEmit`을 확인해
true면 그제서야 정확히 1회 전파(emit)하고 플래그를 리셋. 이미 false면
아무 것도 안 함(idempotent).
**`:Get()`엔 영향 없음** — 블록은 emit **전파**만 지연시킨다. 블록 중이라도
누군가 명시적으로 `:Get()`하면 그 순간의 실제 값을 정상적으로 계산해서
준다 — `store-semantics.md`의 "`Get()`은 라이브 레퍼런스를 준다" 원칙과 일치.
### 사용 예시
`state1`/`state2` 각각이 아니라 **결합된 결과(`state3`) 하나에만** `:Block`
건다:
```lua
local blocker = Blocker()
local gated3 = state3:Block(blocker) -- 소비자는 gated3를 구독
blocker:On()
state1:Set(1) -- state3 무효화 → gated3로 전파 시도 → 블록됨 → HasBlockedEmit=true
state2:Set(2) -- state3 무효화 → gated3로 전파 시도 → 이미 true, 그대로
blocker:Off() -- onunblock 핸들 실행 → HasBlockedEmit 확인 → 딱 한 번 emit
```
**일반 사용 가이드(확정, 문서화 필수)**: Block은 **파이프라인의 최종 연산
지점**(실제로 무거운 계산이 일어나는 derived state, eager 소비자에 가장
가까운 지점)에 거는 게 원칙 — 소스가 여러 개든, 하나가 한 주기에 여러 번
바뀌든 상관없이 이 지점 하나만 지키면 됨. 소스 쪽에 각각 거는 게 아니다.
### 이름 확정
- 클래스: `Blocker``Observer`/`Modifier`/`Ref`와 같은 명사-행위자
네이밍 관례와 일치.
- Blocker 자신의 토글: **`On()`/`Off()` -> self** (`Block()`/`Unblock()`
아님) — `state:Block(blocker)`가 이미 "배선(wiring)" 동작의 동사로
"Block"을 쓰고 있어서, Blocker 자신의 토글까지 같은 단어를 쓰면
`blocker:Block()`(블로커를 켠다)과 `state:Block(blocker)`(state를 이
블로커에 배선한다)가 같은 단어로 다른 두 동작을 가리키게 됨.
- 필드: **`IsBlocked`**(Blocker 자신의 On/Off 상태), **`HasBlockedEmit`**
(gated state의 대기 플래그, `Is`/`Has` 접두어로 불리언임을 바로 알려줌).
- 메소드: `state:Block(blocker) -> state`.
### 재진입(네스팅) — 의도적으로 미지원, 강한 문서화 필수
`IsBlocked`는 카운터가 아니라 단순 불리언이고, **의도적으로 그렇게 둔다.**
레퍼런스 카운팅으로 네스팅을 지원할 수도 있었지만, 그러면 "`On()` 여러
번, `Off()` 실수로 적게" 같은 버그가 **영구 블록으로 조용히 새는**
위험한 실패 모드를 만든다("poisoned mutex" 트래킹류 해키함도 만들지
않기로 함).
**대신 확정된 규칙**: 겹치는 배치가 필요하면 **각자 새 `Blocker` 인스턴스를
만들 것** — 하나의 Blocker를 여러 컨텍스트에서 재사용/중첩하지 않는다.
`Off()`는 스태킹 없이 즉시 그 자리에서 꺼진다. **이 제약은 반드시 사용자
문서(API 레퍼런스 수준)에 명시적으로 강조할 것** — 네스팅을 시도하면
조용히 잘못된 시점에 조기 해제되는, 원인 추적이 어려운 버그로 이어짐.
### 상태: 핵심 메커니즘+이름 확정. 남은 건 문서화뿐
`quadnomicon`에서 "Batch를 기각하고 왜 Blocker로 갔는가"를 비교 설명하는
게 좋은 소재(`archive/batch-rejected.md`와 나란히 인용).
---
## Effect — leaf 죽음에 확정 정리, 재실행 개념 없음
**Observer와는 별개의, 완전히 새로운 요소로 확정** — Ref/PreRef 같은
서로 파생된 관계가 아니라 독립적으로 존재하는 primitive. Roblox엔
`task.spawn`으로 코루틴에 반복문/타이머를 돌리는 패턴이 흔하고, Luau
테이블엔 `__gc` 같은 GC 시점 훅이 없어서 "이게 진짜 사라지는 순간"을 아는
유일한 방법은 `Instance.Destroying`류 명시적 신호뿐 — 이런 케이스(타이머
시작 → leaf가 죽을 때 반드시 정지)를 위한 별도 primitive로 합의됨.
```
Effect(fn) -> EffectHandle -- fn을 즉시 1회 실행, 리턴값(nil | () -> ())은
-- 이 Effect가 바인드된 leaf가 죽을 때 정확히 1회 호출
```
**재실행 개념이 없다** — 값 변화에 반응해 다시 도는 건 Observer(+클로저로
직접 짠 cleanup)의 영역이고, Effect는 순수하게 "설치 + 확정 정리" 페어
하나만 담당한다. children 배열에 leaf로 놓는 기존 Observer 바인딩 패턴을
그대로 재사용(그 leaf가 살아있는 동안만 유효, leaf가 죽으면 정리 콜백
호출). 비용은 leaf당 실제 Destroying 바인딩 하나(공유 weak table로 되는
Observer보다 비쌈) — 필요할 때만 쓰는 걸로 충분.
**Observer에 cleanup 반환 계약을 추가하는 안은 기각됨** — React `useEffect`류로
`fn``nil | () -> ()`를 반환하면 다음 재실행 직전에 그걸 불러주는 안을
검토했으나, 클로저 업밸류로 이미 쉽게 되고 잘 작동해서(`local lastConn;
state:Observer(function() if lastConn then lastConn:Disconnect() end;
lastConn = ... end)`) 프레임워크가 이걸 대신해줄 이유가 약하다는 판단 —
`state:Observer(fn)` 자체는 여전히 재실행 계약만 갖고, Effect가 별도로
"1회 설치 + 확정 정리"를 담당하는 이 분리 구조가 유지됨.
### ⚠️ 미해결 — Effect와 Observer의 관계, 사용자 확인 필요
**임의로 결론내지 않고 열어둠(2026-08-07 문서 정리 세션)**: 위 스펙은
`Effect(fn)`를 State에 종속되지 않는 완전한 자유 함수로 서술하지만, 아래
두 가지가 문서상 명확히 확인되지 않음:
1. **`Effect`가 실제로는 `state:Effect(fn)`처럼 State의 메소드(=Observer의
변형, "재실행 없음 + 확정 정리 추가"만 다른 버전)로 구현/노출되어야
하는 것 아닌가?** — 그렇다면 "독립 존재 가능한 프리미티브 vs 원천에
종속된 파생 데이터"(`base/store-semantics.md`) 분류상 Effect도
Observer처럼 후자(자유 함수 생성자 없음, 항상 `:` 메소드)로 재분류해야
함. 지금 이 문서는 이전 조사(`research/additional-primitives-plan.md`)를
따라 자유 함수로 서술했지만, 이게 최종 확정인지는 불명확.
2. **`state:Observer(fn)`가 생성 시점에 `fn`을 즉시 1회 실행하는지가 문서
어디에도 명시돼 있지 않음** — `base/bind-system-plan.md`의 Observer
절은 "값을 안 실어줌, `fn` 본문에서 `Get()`을 다시 읽어야 함"만
명시할 뿐 "생성 즉시 1회 호출되는지"는 다루지 않는다. Effect는
"즉시 1회 실행"이 스펙에 명시돼 있어 이 부분만 보면 둘이 겹쳐
보인다.
이 두 질문이 풀리면 Effect가 (a) 완전히 별개인 자유 함수 primitive로
남는지, (b) `state:Effect(fn)`로 Observer 계열에 합류하는지가 갈린다.
**확인 전까지는 위에 적은 자유 함수 스펙을 잠정 스펙으로 두되, 구현
착수(M3~M4 전후) 전에 반드시 사용자와 다시 확인할 것** — `.claude/
question.md`에 같은 항목 등재됨.

View file

@ -3,8 +3,8 @@
**상태**: base — 횡단 결정의 최종 상태 요약. 특정 기능 plan이 아니라 프로젝트
전체에 걸친 결정이라 완료 개념 없음. 근거가 된 원본 브레인스토밍은
`.claude/initreq/raw-userinput.md`(안 옮기고 그대로 둠 — 이 문서들로 나누기 전의
raw chain-of-thought 백업 역할). 현재 v1 구조는 `base/quad-v1-architecture.md`,
비교 리서치는 `base/comparison-fusion-vide.md`, `base/lifecycle-pattern.md` 참고.
raw chain-of-thought 백업 역할). 현재 v1 구조는 `reference/quad-v1-architecture.md`,
비교 리서치는 `reference/comparison-fusion-vide.md`, `base/lifecycle-pattern.md` 참고.
## 한 줄 요약
@ -23,7 +23,7 @@ quad는 이제 "스크립트"가 아니라 **라이브러리**다. DOMless Roblo
바인드처럼 정말 체인이 자연스러운 곳에만(`:` 문법) 남김. 타입 작성 난이도가
OOP 스타일에서 너무 커진다는 게 이유.
3. **복사(clone) 구현 지양, 팩토리 함수로 대체.** v1의 metatable 체이닝(1-필드
테이블을 계속 쌓는 방식, `base/quad-v1-architecture.md` 참고)은 폐기.
테이블을 계속 쌓는 방식, `reference/quad-v1-architecture.md` 참고)은 폐기.
store 바인드에 대한 변경은 "전체 변경"으로 간주(UB 아님, 문서화된 의미론) —
부분 복사/오버레이가 필요하면 팩토리 함수로 필요한 곳만 명시적으로 복사.
4. **PA님 스타일 DI 키 계속 지원**: `[Attribute "Name"]`, `[Tag ""] = true` 같은
@ -67,12 +67,12 @@ quad는 이제 "스크립트"가 아니라 **라이브러리**다. DOMless Roblo
구현(`base/bind-system-plan.md`).
9. **Tracker 미구현.** v1의 소스 변경 감지 자동 재렌더 기능(hot-reload watcher,
실제로는 `.claude/initreq/quad/src/tracker.lua` — v1에서도 이미 `exports.lua`
연결 안 된 죽은 코드였음, `base/quad-v1-architecture.md` 참고)은 렌더
연결 안 된 죽은 코드였음, `reference/quad-v1-architecture.md` 참고)은 렌더
라이브러리 범위 밖으로 판단. 스토리북 구현체(https://ui-labs.luau.page/docs/getstarted)가
이미 존재하므로 대체.
10. **lang 모듈 미구현, 분리.** 로케일/문자열 처리는 리액터블 라이브러리와
별개 라이브러리로 존재해야 함(v1 `lang.lua`의 전역 스코프 버그 등은
`base/quad-v1-architecture.md` 참고 — 애초에 반면교사).
`reference/quad-v1-architecture.md` 참고 — 애초에 반면교사).
11. **커스텀 Signal 클래스 미구현.** 콜백 정도로 이벤트 바인드 뒤에 함수를
넣는 것만으로 충분하다고 판단. (이전 초안엔 "rbvm의 Signal이 재사용
가능해 보여 상충한다"는 메모가 있었으나 2026-08-04 검증 라운드에서 최종

View file

@ -8,9 +8,9 @@ Signal 미채택, Ref 역할)과 소스 트리 상 패키지 경계(디스패치
array API, `CreatedRef` 모양) 뿐 — 구현 단계에서 자연히 정리됨. 원본:
`.claude/initreq/raw-userinput.md`
"key와 value에 대한 바인드 연산은 pluggable 하도록 구성하기" / "스토어는 스토어를
저장 가능한가" / "Ref는 고민중" 절. v1의 문제점은 `base/quad-v1-architecture.md`
저장 가능한가" / "Ref는 고민중" 절. v1의 문제점은 `reference/quad-v1-architecture.md`
("ProcessQuadProperty" 하드코딩 디스패처), 참고 패턴은 `.claude/initreq/tbox`
(레지스트리)와 Fusion/Vide 비교는 `base/comparison-fusion-vide.md` 참고.
(레지스트리)와 Fusion/Vide 비교는 `reference/comparison-fusion-vide.md` 참고.
## 문제
@ -352,7 +352,7 @@ flatten된 값은 해시 파트(프로퍼티 키)로 존재하게 되고, Store
## 이벤트 핸들러는 self(Instance)를 받지 않는다 — 확정 (2026-08-06)
**결정**: v1의 `function(self, ...)` 관습(`self`/`this`로 이벤트 대상
Instance를 넘겨주는 것, `.claude/base/quad-v1-architecture.md` 참고 —
Instance를 넘겨주는 것, `.claude/reference/quad-v1-architecture.md` 참고 —
실제로 `event.lua``Bind``func(self or this, ...)`로 넘겨줌)은
**채택하지 않는다.** quad-roblox의 이벤트 핸들러는 엔진이 네이티브로
주는 이벤트 인자만 받는다(React의 `onXxx`가 DOM 노드가 아니라

View file

@ -10,7 +10,7 @@ Store/State/Source 온톨로지가 먼저 확정된 뒤에야 이 논의가 열
## 문제
v1의 `Class.Extend()`(Init/Render/AfterRender/Getter/Setter/UpdateTriggers,
`base/quad-v1-architecture.md` 참고)는 이미 OOP 상속 스타일이라 폐기
`reference/quad-v1-architecture.md` 참고)는 이미 OOP 상속 스타일이라 폐기
방향이지만, 그게 제공하던 실제 편의 기능(컴포넌트가 자기 store를 자동으로
가짐, props로 넘어온 State를 자동 흡수, `self:Default`/`self "key"`로
기본값·바인딩)까지 같이 버려도 되는지가 미결이었음. `MyComp {...}` 형태로
@ -56,42 +56,30 @@ State는 `:With`/`:Compute`로 만들어진 파생값일 수 있어 쓰기가
의미 있음 — **사용자 확정**("맞음. 확실해"). 이 원칙 자체는 그대로 유지되고,
아래 3번의 구체적 메커니즘만 2026-08-06 후속 세션에서 더 단순하게 갱신됨.
### 3. [정정, 2026-08-06 후속 세션] `StoreSource` 프록시 개념 폐기 — Source가 State를 구조적으로 만족하므로 Store가 내부 Source를 그대로 반환
### 3. Store는 내부 Source를 그대로 반환 — Source가 State를 구조적으로 만족
**원래 이 절은 "Source를 인터페이스+구현체로 두고 Store 키에서 얇은 프록시
(`StoreSource`)를 받는다"는 방향이었음 — 지금은 폐기됨.** 이후 세션에서
Store/Source dot-access 타입 문제(레코드 타입의 읽기/쓰기 비대칭)를
다루다가 더 근본적인 재구성으로 수렴: **`Source<T>`가 구조적으로
`State<T>`를 만족**(단방향 호환, Svelte `Writable<T> extends Readable<T>`
같은 모양)하도록 만들면, Store가 "내부 Source를 감추고 별도 프록시를
새로 만들어 노출"할 이유 자체가 없어짐 — `store.key`가 Store 생성 시
이미 만들어둔 진짜 Source 객체를 그대로 돌려줘도 안전함(Source 자체가
이미 State의 읽기 계약을 전부 만족하고, 거기에 `:Set(value)`/`:Emit()`이
추가로 있을 뿐이라 "원본이라 쓰기 가능"이라는 위 2번 규칙과도 자연히
맞아떨어짐). 상세 근거·타입 설계·Luau 솔버 검증 필요 항목은
`base/store-semantics.md`의 "Source가 State를 만족함" 절이 최종 소스 —
이 문서는 배경만 유지.
**확정**: `Source<T>`가 구조적으로 `State<T>`를 만족하므로(단방향 호환,
Svelte `Writable<T> extends Readable<T>`와 같은 모양), `store.key`는 Store
생성 시 이미 만들어둔 진짜 Source 객체를 그대로 반환한다 — 별도 프록시
타입도, 별도 캐싱 계층도 없음(Source 자체가 이미 State의 읽기 계약을
전부 만족하고 거기에 `:Set(value)`/`:Emit()`이 추가로 있을 뿐이라 "원본이라
쓰기 가능"이라는 위 2번 규칙과도 자연히 맞아떨어짐). 쓰기 문법도 같이
바뀜: `store.key = v`가 아니라 `store.key:Set(v)`(레코드 타입 읽기/쓰기
대칭 + lazy 동작에 `=`가 암시하는 "즉시 커밋"이 안 맞는다는 논거). 상세
근거·타입 설계·Luau 솔버 검증 필요 항목은 `base/store-semantics.md`
"Source가 State를 만족함" 절이 최종 소스.
- **쓰기 문법도 같이 바뀜**: `store.key = v`가 아니라 `store.key:Set(v)`
(레코드 타입 읽기/쓰기 대칭 + lazy 동작에 `=`가 암시하는 "즉시 커밋"이
안 맞는다는 논거, 같은 절 참고).
- **캐시 문제도 이걸로 자연히 해소**: State를 "매번 새로 만듦"이던 이전
모델과 달리, 이제 Store는 생성 시 만들어둔 Source를 그대로 갖고 있다가
돌려주기만 하므로 별도 캐싱 메커니즘 자체가 불필요(래퍼 생성 단계가
아예 없어짐 — 이전보다 더 쌈).
**[이전에 확정했다가 폐기된 `StoreSource` 프록시 설계는 이 결론으로
완전히 대체됨 — 원문·역전 이유·신구 비교표는
`archive/store-source-proxy-reversed.md` 참고, 여기서는 반복하지 않음.]**
### 4. [정정, 2026-08-06 후속 세션] Source 직접 전달 — 타입 유니온도 불필요해짐
### 4. Source 직접 전달 — 타입 유니온 불필요, 서브타입 호환으로 자동 통과
원래 "핸들러가 `Source<T> | State<T>` 유니온으로 받는다"는 방향이었으나,
Source가 State를 구조적으로 만족하는 지금은 **유니온 자체가 필요 없음**
핸들러는 그냥 `State<T>` 하나만 받아도 Source 인스턴스가 자동으로 그
자리에 들어감(서브타입 호환). `isHandlable`/`priority`/`process`/`retract`
4종 계약에 5번째 항목을 추가할 필요 없다는 결론은 그대로 유지, 다만 근거가
"타입 유니온으로 처리"에서 "서브타입이라 유니온 자체가 불필요"로 더
단순해짐. 단, 핸들러가 "이거 Source면 역방향 쓰기까지 걸고 싶다"처럼
**런타임에** Source인지 구분하고 싶은 경우는 여전히 있을 수 있음 —
그건 타입 유니온이 아니라 런타임 판별자(`isSource`류, `isObserver`
패턴과 동일한 결)로 처리하면 됨.
핸들러는 `State<T>` 하나만 받아도 Source 인스턴스가 서브타입 호환으로
자동 통과된다(`Source<T> | State<T>` 유니온 불필요, `isHandlable`/
`priority`/`process`/`retract` 4종 계약에 5번째 항목 추가 불필요). 런타임에
"이게 Source면 역방향 쓰기까지 걸고 싶다"처럼 구분하고 싶은 경우는
`isSource`류 판별자로(`isObserver`와 동일한 패턴).
- **실사용 범위가 좁다는 판단은 그대로 유지**: `isEnabled`처럼 여러 조건에
영향받는(=파생된) 값은 애초에 State지 Source가 아니므로 이 경로로 못

View file

@ -123,7 +123,7 @@ canExecute 같은 람다 함수 하나 달게 해서 Connected 상태 보게 하
즉, 핸들러가 처리 도중 만든 구독/옵저버 클로저는 그 자체로는 아무것도 자동으로
GC에 묶이지 않음 — v1이 여기저기서 `PropertyChangedSignal`에 연결해 참조를
붙잡아두던 "GC 방지 핫팩"(`base/quad-v1-architecture.md` 참고)과 같은 문제.
붙잡아두던 "GC 방지 핫팩"(`reference/quad-v1-architecture.md` 참고)과 같은 문제.
**base가 범용 유틸로 제공할 것: 임의의 클로저/구독을 실제 Roblox 객체의
생명주기에 바인드하는 도구** — 내부적으로 v1/rbvm이 쓰던 "connect 트릭"(어떤
신호에든 연결해서 참조를 죽을 때까지만 붙잡아두는 것)을 그대로 재사용. 이

View file

@ -190,7 +190,7 @@ PA님 방식인 문자열 키 + 런타임 리플렉션으로 감, `base/bind-sys
`Modifier.Rounded(8)`가 실제로 어떻게 UICorner 자식을 만들어 붙이는지(v1의
`Corner` 특수 프로퍼티 선례, 핸들러 배치 소견)는
`research/ui-shorthand-plan.md` 참고 — 이 문서는 Modifier 값 자체의
`base/ui-shorthand-plan.md` 참고 — 이 문서는 Modifier 값 자체의
동작만 다루므로 분리.
### 6. State/Pipe 쪽엔 영향 없음 — 이미 있던 결정의 재확인일 뿐

View file

@ -4,7 +4,7 @@
소스 트리 상 패키지 경계까지 확정되어 `research/`에서 승격됨(`base/
architecture.md`의 "구현 착수: 소스 트리 구조 확정" 절 참고). 원본:
`.claude/initreq/raw-userinput.md` "slot을 구현하도록 하기로 했음" 절. Fusion의
`Children` SpecialKey와 Vide의 mount 무가드 비교는 `base/comparison-fusion-vide.md`
`Children` SpecialKey와 Vide의 mount 무가드 비교는 `reference/comparison-fusion-vide.md`
참고 — 결론: **두 라이브러리 어디에도 이런 엄격한 단일 마운트 가드가 없음,
quad의 진짜 개선점.**
@ -34,7 +34,7 @@ InstanceChild.luau`. Slot은 "뮤터블 배열"을 다루고 이 핸들러는 "
Slot에 들어간 요소는 **ownership이 귀속**되며 다른 곳에 마운트할 수 없게 된다.
`isMounted`를 관리해서, **한 인스턴스에 대한 다중 마운팅이 라이브러리 차원에서
절대 일어나지 않도록 강제**하는 게 v1 대비 핵심 디자인 변화. v1의 `mount()`
별다른 강제를 안 했지만(`base/quad-v1-architecture.md`의 mount.lua 분석 참고 —
별다른 강제를 안 했지만(`reference/quad-v1-architecture.md`의 mount.lua 분석 참고 —
실제로는 부모/자식 부기까지 했지만 다중 마운트 방지는 없었음), v2는 mount
함수 자체가 이 강제를 담당.

View file

@ -282,7 +282,7 @@ Source가 State 계약을 만족하는 이상 같은 이유(Modifier용 processo
`useEffect`처럼 여러 store 값을 디펜던시로 묶어 파생값을 계산하고 싶다는
요구가 있었음(v1의 `myStore "a,b"` 콤마-조인 문자열 방식은 폐기 대상 —
`base/quad-v1-architecture.md`의 "문자열 DSL" 문제점 참고). **v1의
`reference/quad-v1-architecture.md`의 "문자열 DSL" 문제점 참고). **v1의
`:Add`/`:With`/`:Tween`처럼 값을 직접 가공하는 이름 붙은(named) 체이닝 연산은
만들지 않음** — 대신 일반 함수를 받아 처리. (주의: 아래의 v2 `:With(...)`
이름만 같을 뿐 v1의 `:With`와는 다른 연산임 — v1은 "함수/테이블에서 값을
@ -291,3 +291,12 @@ Source가 State 계약을 만족하는 이상 같은 이유(Modifier용 processo
`:Compute(fn)`으로 파생 State를 만드는 것으로 확정 — `Store.Combine({a,b},
fn)`류 포지셔널 인자 방식은 기각됨. 정확한 lazy 인자 규칙(self/with 값 둘 다
State 핸들로 넘기고 `:Get()`을 실제로 읽을 때만 계산)은 `base/bind-system-plan.md`의 "Store/State/Source 온톨로지" 절 참고.
**여러 소스를 한 번에 바꿔도 파생값 재계산/재대입이 한 번만 되게 하려면
`Blocker` 참고.** 위 `:With`+`:Compute`만으로는 "state1, state2를 연달아
Set하면 결합된 파생값이 두 번 재계산/재대입된다"는 문제(즉시 pull하는
store-bind 소비자 기준)는 안 풀림 — 이건 별도 확정 프리미티브
`base/additional-primitives.md`의 "Blocker" 절이 다룸(State 개발과 같은
마일스톤, `ROADMAP.md` M3에서 함께 구현). lexical `Batch(fn)`으로 풀려던
초기 시도는 코루틴 yield 위에서 구조적으로 위험해 기각됨 —
`archive/batch-rejected.md` 참고.

View file

@ -0,0 +1,108 @@
# UI 편의 숏핸드 (UICorner/UIPadding/UIScale) — 인라인 적용
**상태**: base — 기능 필요 여부·이름·메커니즘·패키지 배치·store-bind 가능성까지
전부 확정(2026-08-07 문서 정리에서 `research/`→`base/` 승격). 남은 건 구현
단계의 세부 시그니처뿐.
## 배경
사용자 기억: v1을 쓸 때 "UICorner/UIPadding/UIScale 같은 걸 직접
`Instance.new`로 만들어 Parent하는 귀찮은 작업 없이, Frame 안에 인라인으로
넣기만 해도 CSS 스타일처럼 적용됐다 — 코드가 줄고 읽기도 편해서 꽤
괜찮았다"는 것. v1 소스(`.claude/initreq/quad`)와 PA님 코드
(`.claude/initreq/artworks`)를 서브에이전트로 조사해 확인.
## v1 실제 메커니즘 (조사 완료)
`class.lua``SetProperty`/`GetProperty`(38~109행)와 `ProcessQuadProperty`
(134~213행)에 하드코딩된 if/elseif 분기로 특수 문자열 키 5종을 지원했음 —
`Corner = 8` → 숫자 하나, 기존 `UICorner` 자식이 있으면 재사용, 없으면
`Instance.new("UICorner", item)`으로 생성(`Name = "_quad_round"`),
`CornerRadius = UDim.new(0, value)` 설정. `PaddingAll`/`PaddingAllOffset`,
`Scale`도 동일 패턴(`UIPadding`/`UIScale`, `_quad_padding`/`_quad_scale`).
값 모양은 항상 **리터럴 하나**(숫자/UDim) — 테이블도 `__type` 태그도 아님.
v1엔 이 5종과 별개로 `RoundSize`(이미지 9-slice 라운드 트릭, UICorner와는
전혀 다른 메커니즘)도 있었으나 **이건 드롭 확정** — 자세한 사유는
`archive/ui-shorthand-roundsize-dropped.md` 참고, 이 문서에서는 반복하지
않음.
**`UIListLayout`/`UIGridLayout`/flex 전용 숏핸드는 v1에 없었음** —
`ProcessQuadProperty`의 범용 자식 나열 분기(배열 인덱스로 놓인 Instance/
Class 결과를 자동 mount)로 `UIListLayout{...}`을 그냥 직접 나열했을 뿐,
`List = true` 같은 전용 축약 문법은 레포 전체(PA님 코드 포함)에서 찾지
못했음. quad-v2도 이 부분은 이미 있는 children-array + 인스턴스 생성
문법으로 그대로 커버됨 — 새로 설계할 것 없음.
## 결론 — 이름은 UICorner/UIPadding/UIScale로 확정 (프리픽스 필요)
**기능은 여전히 필요**: `UICorner`가 Roblox 네이티브 Instance가 됐어도
"별도 Instance를 만들어 부모에 Parent해야 한다"는 구조적 번거로움 자체는
없어지지 않으므로, 이 숏핸드의 존재 이유는 그대로 유효 — **사용자
재확정**("UIScale 같은 건 여전히 별도의 Instance고 부모 Frame에 영향을
주는 구조, 숏핸드는 여전히 필요하다").
**이름은 v1의 `Corner`/`PaddingAll`/`Scale`을 그대로 안 가져오고 실제
Roblox Instance 이름과 맞춘 `UICorner`/`UIPadding`(+`UIPaddingOffset`)/
`UIScale`로 확정** — v1식 짧은 이름을 그대로 쓰면 Modifier 체이닝
메소드(`mod:Corner(8)`)가 "진짜 UICorner를 만드는 숏핸드"인지 그냥 우연히
비슷한 이름의 부가 Modifier 필드인지 구분이 안 됨(사용자 지적). 접두어
`UI`를 붙이면 실제 대응하는 Roblox Instance 클래스 이름과 1:1로 읽혀서
이 모호함 자체가 사라짐 — `Frame { UICorner = 8 }`, `mod:UICorner(8)`.
## 메커니즘 — 새 아키텍처 개념 불필요
이미 있는 pluggable Handler로 그대로 커버됨. `UICorner`/`UIPadding`/
`UIScale` 같은 특수 키를 인식하는 Handler(`isHandlable`이 그 키를 매칭)가
"이름 붙은 자식을 찾거나 만들고 프로퍼티 세팅"을 `process(inst, k, v)`
구현 — v1의 하드코딩 if/elseif 대신 정식 핸들러 계약(`isHandlable`/
`priority`/`process`/`retract`)을 따르는 것만 다름. `modifier-plan.md`
이미 예시로 든 `Modifier.Rounded(8)`은 이 특수 키를 flatten해서 props에
꽂아넣는 사탕 문법일 뿐, 실제 처리는 이 Handler가 함 — Modifier를 안 거치고
`Frame { UICorner = 8 }`처럼 순수 인라인 키로 직접 써도(v1처럼) 동일하게
작동함, `architecture.md``[Attribute "Name"]`류 특수 키와 같은 층위.
자동 생성된 자식은 기존 관례대로 `_`/`QUAD_` 접두어 네이밍
(`research/debug-tooling-plan.md` 9번, v1의 `_quad_round`류 그대로 재사용).
**기존 자식과의 매칭 기준(2026-08-06 감사에서 지적된 항목, 여기서 같이
확정)**: 재사용 대상은 quad가 이전에 만든 고정 이름(`_quad_corner`류)
자식으로 한정 — 타입만 보고(`UICorner`이기만 하면 아무거나) 재사용하지
않음. 사용자가 직접 만든 `UICorner`를 quad가 멋대로 건드리는 부작용을
피하기 위함.
## store-bind — 이 숏핸드도 지원, Tween만큼 무겁게 안 가도 됨
v1에서도 `Corner`/`PaddingAll`/`Scale`은 store 값으로 바인드 가능했음
(`myStore "key"` 체이닝으로 다른 프로퍼티와 동일하게 취급됨) — quad-v2도
이 능력을 유지한다. 트윈처럼 애니메이션까지 지원할 필요는 없음(API 표면만
복잡해짐) — 그냥 값이 바뀌면 `CornerRadius`/`Padding`/`Scale` 프로퍼티를
다시 세팅하는 정도로 충분. 구현 비용도 낮음: 각 Handler가 `process`에서
"이전에 자기가 찾거나 만든 자식 Instance"를 얻어야 하는데, 이건 이미
base가 범용 유틸로 제공하기로 확정한 per-instance weak-keyed 저장소
(`base.perInstanceState(inst)`, `base/bind-system-plan.md` "핸들러 내부
상태 저장" 절)를 그대로 재사용하면 됨 — Tween 핸들러가 실행 중인 Tween
객체를 기억해두는 것과 정확히 같은 패턴. 새 메커니즘 발명 불필요, 이미
있는 "store 바인드는 pluggable 바인드를 재실행하는 래핑" 원칙
(`base/bind-system-plan.md` "확정된 디스패치 모델" 절)이 그대로 적용됨.
## 패키지 배치 — `quad-roblox` 코어에 직접 포함, 확정
"트윈도 인스턴스 생성/제어를 직접 구현 가능한 걸 하나로 묶어 쉽게 쓰게
합친 것 — 너무 잘게 쪼개 오버엔지니어링하기보다 확실히 하나로 코어에
넣어도 충분하다, opt-out할 이유가 별로 없다"는 게 사용자 판단 — **작고
항상 켜져 있어도 비용이 무시할 만한 편의 기능은 별도 opt-out 패키지로
쪼개지 말고 `quad-roblox` 코어에 직접 포함한다**는 원칙으로 확정(이미
계획된 Tween 핸들러가 같은 모양이라는 게 근거). 이 원칙은 일반화해서
재사용 가능 — 앞으로 비슷한 "작은 인스턴스 편의 기능"이 제안되면
`quad-roblox-util` 같은 걸 새로 만들지 않고 이 선례를 따르면 됨.
**중요도**: 낮음("이건 나중에도 쉽게 구현됨" — 사용자) — 지금 M0 우선순위를
바꿀 이유는 없음, M10(Handlers/Attribute 등) 전후로 다른 세부 Handler와
함께 구현하면 충분.
## 남은 열린 질문 (단순화 후보, 사소함)
- Corner/PaddingAll/Scale 3개 거의 동일한 형태의 Handler를 각각 만들지,
`{key -> {ChildClassName, ChildDefaultName, Property, wrap=fn}}` 룩업
테이블로 구동되는 단일 `Handlers/InstanceShorthand.luau`로 통합할지 —
`research/pre-implementation-audit.md` 3-2번 참고, 강제 사항 아님,
구현 시점에 결정할 정도의 사소한 개선 후보.

View file

@ -14,8 +14,11 @@
사용자 질문: "다른 독립 프리미티브나 종속 파생 데이터는 뭐가 더 필요할 것
같나요. 이것만으로 이 프로젝트는 충분하다 생각해요?" — 여러 서브에이전트
조사 + 사용자와 라이브 논의로 계속 수렴 중, 상세는 `research/
additional-primitives-plan.md`. 요지:
조사 + 사용자와 라이브 논의로 계속 수렴 중. **2026-08-07 문서 정리에서
확정/기각된 항목은 `research/additional-primitives-plan.md`에서
분리됨**: Effect/Blocker → `base/additional-primitives.md`, Batch →
`archive/batch-rejected.md`, Context(+레이어드 Store) → `archive/
context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만** 남김.
- **키 기반 동적 컬렉션 재조정(유일하게 아직 완전히 열려있음, 최우선)**:
Fusion `ForPairs`/`ForKeys`/`ForValues`, Vide `indexes()`/`values()`,
@ -26,33 +29,20 @@ additional-primitives-plan.md`. 요지:
못 쓴다는 반례로 철회됨), Slot에 파괴 없이 빼내는 `Extract` 연산 추가
필요 — 최종 이름만 미정(아래 "용어 정리" 절에 후보 추가). **사용자가
"작업 전에 모든 정의를 마치고 싶다"고 명시** — M0 이전 완전 확정 목표.
- **Effect — 거의 수렴**: leaf가 죽을 때 확정적으로 정리 콜백을 부르는
단순 primitive(재실행 개념 없음, Observer와 별개)로 합의, 시그니처만
남음. Observer에 cleanup 반환 계약을 얹는 안은 기각(클로저 업밸류로
이미 충분 — `pre-implementation-audit.md` 3-1과 같은 논리).
- **Blocker — 채택, 핵심 메커니즘+이름 확정(2026-08-07 신설)**: 여러
Source를 한꺼번에 바꿔도 파생값 재계산/재대입이 한 번만 되게 하는
primitive — lexical Batch(아래 항목, 기각)와 달리 콜스택/코루틴이 아니라
**값**(`Blocker` 객체의 `On()`/`Off()`)으로 지연 구간을 표현해 코루틴
yield 위험을 구조적으로 우회함. `state:Block(blocker) -> state`
gated state를 반환(호출 즉시 onunblock 핸들 등록), `IsBlocked`/
`HasBlockedEmit` 필드, 재진입(네스팅)은 의도적으로 미지원(Rust
poisoned-mutex류 위험 회피 — 겹치는 배치는 각자 새 `Blocker`를 쓸 것,
**문서화에서 강하게 명시 필요**). `base/`로 승격 가능한 수준, 남은 건
문서화뿐.
- **Batch(함수/코루틴 스코프 lexical block) — 기각 확정**: lexical
transaction 블록이 코루틴 yield 위에서 구조적으로 위험(전역/코루틴
스코프 플래그 둘 다 새 코루틴 스폰이나 영구 yield에 깨짐) — Blocker로
대체됐으므로 더 이상 미해결 문제 아님, quadnomicon "왜 Batch 대신
Blocker인가" 에세이 대상.
- **Context — 기각 확정, 대안이던 레이어드 Store도 철회**: 완전 자동
버전은 Roblox Luau 플랫폼 한계로 사실상 불가, 얕은 버전도 Slot 비동기
추가에서 조용히 깨짐 + quad-debug 철학과 충돌. 레이어드 Store 대안은
사용자 반박으로 철회 — 이미 있는 타입 강제 명시적 Store 전달 +
오버라이드 지점에서 새 Store를 만들어 넘기는 것으로 충분하다는 판단.
둘 다 "왜 없는가" quadnomicon 에세이 대상.
상세는 `research/additional-primitives-plan.md`(이제 이 주제 전용).
- **Effect가 Observer의 변형(`state:Effect()`)인지, 완전히 독립된 free
function인지 — 신규, 2026-08-07 문서 정리 세션에서 발견.** `base/
additional-primitives.md`가 지금까지의 조사대로 Effect를 "재실행 없는
독립 free function"으로 서술해뒀지만, 사용자가 직접 `state:Effect()`
형태(=Observer에 "확정 정리" 계약만 추가된 변형)로 기억하고 있어서
확인이 필요함. 관련 하위 질문: `state:Observer(fn)`가 생성 시점에
`fn`을 즉시 1회 실행하는지도 현재 문서 어디에도 명시돼 있지 않음(Effect는
"즉시 1회 실행"이 스펙에 있음 — 이 부분만 보면 둘이 겹쳐 보이는 이유).
**임의로 결론내지 않고 열어둠** — 구현 착수(M3~M4 전후) 전에 확인 필요,
상세는 `base/additional-primitives.md`의 "미해결" 절.
- Untrack/Suspense/Error Boundary/Readonly는 조사 결과 새 프리미티브 없이
기존 설계·Lua 자체 기능으로 이미 충분한 것으로 판단.
기존 설계·Lua 자체 기능으로 이미 충분한 것으로 판단(`research/
additional-primitives-plan.md` "빈 자리 아닌 것" 절).
### 1. 용어 정리 (사용자 요청, 진행 중)
@ -95,7 +85,8 @@ additional-primitives-plan.md`. 요지:
사용자 피드백으로 탈락. 후보: `Render`(가장 직접적이지만 "quad엔 렌더
주기가 없다"는 기존 원칙과 이름이 충돌해 보일 수 있음), `Draw`(짧지만
즉시모드 GUI 뉘앙스), `List`(중립적이나 메커니즘을 안 알려줌) — 아직
미정, `research/additional-primitives-plan.md` 1번 절 참고.
미정, `research/additional-primitives-plan.md`의 "키 기반 동적 컬렉션
재조정" 절 참고.
- **"프로바이더"(3순위, 사소함)**: `base/module-lifecycle-plan.md`
"provider"라고 불러온, `isHandlable`로 참여 여부를 결정하고 우선순위대로
스캔되는 pluggable 참가자 개념 — 정확한 이름을 "provider"/"processor"/
@ -131,9 +122,10 @@ additional-primitives-plan.md`. 요지:
- **`canExecute`/`Connected`의 실제 구현 방식(Parent==nil vs Connection.
Connected vs Destroying 플래그)이 미확정인데 이미 Slot/Observer/store-bind
retract 전역에 재사용 확정됨** — M2/M3 착수 전 실측 필요 — 우선순위1-6.
- **`LifetimeHandle` 인터페이스가 M8에 배치돼 있지만 M4/M6이 이미 그걸
필요로 함(로드맵 순서 역전)** — quad-base 인터페이스 정의를 M2/M3로
옮기는 게 자연스러워 보임, `ROADMAP.md` 수정 필요 — 우선순위1-9.
- **~~`LifetimeHandle` 인터페이스가 M8에 배치돼 있지만 M4/M6이 이미 그걸
필요로 함(로드맵 순서 역전)~~ — 반영 완료(2026-08-07 세 번째 세션)**:
`LifetimeHandle`/`PerInstanceState` 인터페이스(타입만)를 `ROADMAP.md`
M2로 옮기고, quad-roblox 실 구현만 M8에 남김 — 우선순위1-9 해소.
- 그 외(Slot CRUD 의미론 미정의, retract 시 "이전 핸들러" 추적 책임 소재,
우선순위 스캔 동률/매치실패 처리, `:Compute``previous` 인자가
오버엔지니어링일 수 있음, UI shorthand의 기존 UICorner 매칭 기준 등)는
@ -141,10 +133,19 @@ additional-primitives-plan.md`. 요지:
### 3. 낮은 우선순위
- **`None`(가칭) 센티널 프리미티브 — 미확정, 2026-08-07 세 번째 세션
신설.** `{ Override = nil, mod }`처럼 인라인 키로 modifier가 주는 값을
명시적으로 지우고 싶어도 Lua 테이블 리터럴의 `키 = nil`은 키가 아예
없는 것과 구별이 안 돼서 "인라인이 modifier보다 무조건 우선"이라는
기존 merge 규칙(`modifier-plan.md` 2번)이 이 케이스에선 실제로 작동을
안 함. `false`를 이벤트 disconnect 센티널로 쓴 선례처럼 실재하는 값
`None`을 도입하는 방향만 나왔고 상세(타입, flatten 내부 표현, State
필드에도 같은 문제가 적용되는지)는 미정 — `modifier-plan.md` "2-1"절
참고, M7(Modifier) 착수 전 확인.
- `research/existing-instance-bind-plan.md` — 스코프 논의만 필요, 구현
착수를 막지 않음.
- **v1 `objectListClass.__newIndex` 오타 기능의 재현 테스트 필요**
`base/quad-v1-architecture.md`에 남겨진 v1 내부 동작 확인 사항, 마이그레이션
`reference/quad-v1-architecture.md`에 남겨진 v1 내부 동작 확인 사항, 마이그레이션
가이드 작성 시점에 필요. 지금은 그냥 백로그로만 기록.
- **여러 Slot이 형제로 섞일 때 순서 보장**`base/slot-plan.md`의 "여러
Slot이 섞일 때 순서 보장" 절 참고. Roblox 단일 백엔드로는 급하지 않음
@ -172,10 +173,6 @@ additional-primitives-plan.md`. 요지:
중 뭘로 갈지 — 소견은 DI 인스턴스 생성 패턴처럼 "제네릭 하나 + 자주
쓰는 타입만 정적 지름길" 절충이지만 확정 아님, M10(Handlers/Attribute)
착수 전 아무 때나 확인해도 됨.
- **UICorner/UIPadding/UIScale 인라인 편의 키 세부** — `research/
ui-shorthand-plan.md`. 기능 필요 여부(여전히 필요로 재확정)·메커니즘
(Handler)·패키지 배치(quad-roblox 코어)는 확정, 남은 건 이름 재검토
(용어 정리 합류)와 `RoundSize`(이미지 라운드) 대체 방식뿐.
- **v1 하위호환(compat) 레이어 — `quad-roblox-v1-compat`**
`research/v1-compat-plan.md`(신규, 2026-08-06, 두 차례 후속 논의로 수렴).
방향 확정: v1을 그대로 병행 실행 + 경계에서만 `state:Observer()`(lazy
@ -203,8 +200,11 @@ additional-primitives-plan.md`. 요지:
| Modifier(정적 merge, immutable 체이닝, State 필드 지원) | `base/modifier-plan.md` |
| 컴포넌트화(플레인 함수, State/Source 경계, 컴포넌트 경계 modifier/Ref는 named parameter로 전달, multi-root 개념 폐기, `Modifier.Merge`) | `base/component-composition-plan.md` |
| 컴포넌트 이식성(전역 store 참조 시 재사용성 문제) | `base/purity-and-effects-plan.md` |
| Fusion/Vide 비교 리서치(주의: 일부 서술은 이후 라운드에서 뒤집힘, 문서 내 정정 표시 참고) | `base/comparison-fusion-vide.md` |
| v1 내부 동작 스냅샷 | `base/quad-v1-architecture.md` |
| Blocker(값 기반 emit 지연/합치기), Effect(leaf 죽음에 확정 정리 — 단 Observer와의 관계는 위 0번 열린 질문 참고) | `base/additional-primitives.md` |
| UICorner/UIPadding/UIScale 인라인 편의 키 — 이름·메커니즘·store-bind 가능성까지 확정 | `base/ui-shorthand-plan.md` |
| Batch(lexical) 기각, Context(+레이어드 Store) 기각 | `archive/batch-rejected.md`, `archive/context-rejected.md` |
| Fusion/Vide 비교 리서치(주의: 일부 서술은 이후 라운드에서 뒤집힘, 문서 내 정정 표시 참고) | `reference/comparison-fusion-vide.md` |
| v1 내부 동작 스냅샷 | `reference/quad-v1-architecture.md` |
| 트윈 오버라이드(기본값 Cancel), 세부 옵션만 남음 | `research/tween-plan.md` |
| quad2-try(폐기된 이전 시도) 리서치 — OOP 상속/커스텀 파서/Slot 스텁/`Pipe` COW 전부 죽은 접근으로 확인, 반복 조사 금지 | `base/bind-system-plan.md` |

View file

@ -1,7 +1,11 @@
# Fusion / Vide 아키텍처 비교 — quad-v2 설계 근거
**상태**: base — 리서치 스냅샷(참고용 근거 자료), "완료" 개념 없음. quad-v2의
Store/Slot/Tween/bind-dispatch 설계 결정에 인용되는 원본 비교 자료.
**상태**: reference — 온디맨드 참고 자료, "완료" 개념 없음. **[2026-08-07
문서 정리에서 `base/`→`reference/`로 이동]** quad에 관한 결정 자체가 아니라
Fusion/Vide 리서치 스냅샷이라 항상 읽어야 하는 base 컨텍스트는 아님 —
`quadnomicon`(프레임워크 설계자용 심화 콘텐츠) 소재 후보이기도 함. quad-v2의
Store/Slot/Tween/bind-dispatch 설계 결정에 근거로 인용될 때만 열어볼 것,
실제 확정 사항은 인용하는 쪽 `base/` 문서가 소스.
## Fusion (`.claude/initreq/fusion/`)

View file

@ -1,9 +1,13 @@
# quad v1 내부 구조 (재작성 이전 기준선)
**상태**: base — 참고용 스냅샷, "완료" 개념 없음. v1(`.claude/initreq/quad/`)이
실제로 어떻게 동작하는지 정리한 문서로, v2 설계 시 "이 문제를 안 반복하려면"의
기준선으로 계속 참조됨. 아래는 리서치 에이전트가 file:line까지 확인한 내용의 요약 —
정확한 인용이 필요하면 `.claude/initreq/quad/src/*.lua` 원본을 볼 것.
**상태**: reference — 온디맨드 참고 자료, "완료" 개념 없음. **[2026-08-07
문서 정리에서 `base/`→`reference/`로 이동]** v1 자체에 대한 스냅샷일 뿐 v2의
결정 사항이 아니라서 항상 읽어야 하는 base 컨텍스트는 아님 — 다른 문서가
"v1은 이랬는데"를 인용할 때만 열어볼 것. v2 설계 시 "이 문제를 안 반복하려면"의
기준선으로 근거 인용되는 용도는 그대로 유지(각 인용 지점은 여전히
`base/`에 있음, 이 문서는 그 인용의 원본 소스). 아래는 리서치 에이전트가
file:line까지 확인한 내용의 요약 — 정확한 인용이 필요하면
`.claude/initreq/quad/src/*.lua` 원본을 볼 것.
## 공개 API 개요

View file

@ -1,10 +1,12 @@
# 추가 프리미티브 필요성 조사 — 다른 프레임워크 대비 갭 분석
**상태**: research — 사용자와 라이브 논의로 수렴(2026-08-06~07). Context/
Batch(lexical)는 **기각 확정**. **Blocker**(새 primitive, Batch의 대안으로
채택)는 핵심 메커니즘+이름 확정, 문서화만 남음. 키 기반 동적 컬렉션
재조정은 설계 진행 중(사용자가 "작업 전에 모든 정의를 마치고 싶다"고
명시 — M0 전 완전 확정이 목표). Effect는 거의 수렴, 시그니처만 남음.
**상태**: research — 사용자와 라이브 논의로 대부분 수렴(2026-08-06~07),
**2026-08-07 문서 정리에서 확정/기각된 항목을 분리**: Effect/Blocker →
`base/additional-primitives.md`, Batch(lexical) → `archive/
batch-rejected.md`, Context(+레이어드 Store 대안) → `archive/
context-rejected.md`. 이 문서에는 **아직 완전히 열려있는 것 하나만** 남음
— 키 기반 동적 컬렉션 재조정. 사용자가 "작업 전에 모든 정의를 마치고
싶다"고 명시 — M0 전 완전 확정이 목표.
## 배경
@ -25,20 +27,20 @@ Vide/v1/artworks 소스 근거 조사, Context 구현 난이도 판정) + 그
## 결론 요약
| 후보 | 판정 | 상태 |
| 후보 | 판정 | 현재 위치 |
|---|---|---|
| 키 기반 동적 컬렉션 재조정 | **진짜 빈 자리, 최우선** | 설계 진행 중 |
| Effect(leaf 죽음에 확정 정리) | 진짜 빈 자리 | 거의 수렴, 시그니처만 남음 |
| **Blocker(값 기반 emit 지연/합치기)** | **채택** — Batch의 대안 | 핵심 메커니즘+이름 확정, 문서화만 남음 |
| Batch(함수/코루틴 스코프 lexical block) | **기각** | 결정 완료 |
| Context(트리 하위 암묵 전파) | **기각** — 대안(레이어드 Store)도 철회 | 결정 완료 |
| Observer에 cleanup 반환 계약 추가 | **기각** — 클로저 업밸류로 이미 충분 | 결정 완료 |
| Untrack/Peek | 빈 자리 아님 | - |
| Suspense/비동기 경계 | 빈 자리 아님(문서화 문제로 재분류) | - |
| Error Boundary | 빈 자리 아님 | - |
| Readonly wrapper | 빈 자리 아님 | - |
| 키 기반 동적 컬렉션 재조정 | **진짜 빈 자리, 최우선** — 아직 열려있음 | 이 문서(아래) |
| Effect(leaf 죽음에 확정 정리) | 채택, 단 Observer와의 관계는 미해결 | `base/additional-primitives.md` |
| Blocker(값 기반 emit 지연/합치기) | **채택** — Batch의 대안 | `base/additional-primitives.md` |
| Batch(함수/코루틴 스코프 lexical block) | **기각** | `archive/batch-rejected.md` |
| Context(트리 하위 암묵 전파) + 레이어드 Store | **기각** | `archive/context-rejected.md` |
| Observer에 cleanup 반환 계약 추가 | **기각** — 클로저 업밸류로 이미 충분 | `base/additional-primitives.md`(Effect 절에 근거만 인용) |
| Untrack/Peek | 빈 자리 아님 | 아래 "빈 자리 아닌 것" 절 |
| Suspense/비동기 경계 | 빈 자리 아님(문서화 문제로 재분류) | 아래 "빈 자리 아닌 것" 절 |
| Error Boundary | 빈 자리 아님 | 아래 "빈 자리 아닌 것" 절 |
| Readonly wrapper | 빈 자리 아님 | 아래 "빈 자리 아닌 것" 절 |
## 1. 키 기반 동적 컬렉션 재조정 — 최우선, 설계 진행 중
## 키 기반 동적 컬렉션 재조정 — 최우선, 설계 진행 중
**무엇인가**: 데이터 배열(인벤토리, 리더보드, 채팅로그처럼 삽입/삭제/
재정렬되는 목록)을 UI로 렌더링할 때, 이전 렌더 결과와 새 데이터를
@ -157,231 +159,6 @@ Slot:Add(element, index?) -- 기존 그대로, Extract로 뺀 것도 다시
먼저 정하고 나중에 여기를 끼워맞추면 재작업이 날 가능성이 높음. Slot
CRUD 시맨틱을 정의할 때 이 프리미티브의 요구사항을 같이 고려할 것.
## 2. Effect — 거의 수렴, 시그니처만 남음
### Observer에 cleanup 반환 계약을 추가하는 안 — 기각
React `useEffect`류처럼 "`fn`이 `nil | () -> ()`를 반환하면 다음 재실행
직전에 그걸 불러준다"를 `state:Observer(fn)`에 얹는 안을 검토했으나
**사용자가 기각** — 클로저 업밸류로 이미 쉽게 되고 잘 작동하는데
(`local lastConn; state:Observer(function() if lastConn then
lastConn:Disconnect() end; lastConn = ... end)`), 프레임워크가 이걸
대신해줄 이유가 약하다는 판단. 이건 `pre-implementation-audit.md`
3-1번("`:Compute(fn)`의 `previous` 인자 — 클로저 업밸류로 이미 되는 걸
별도 API로 만든 것일 수 있음")과 **정확히 같은 논리** — 일관성 있는
판단으로 보임(3-1 자체도 같은 이유로 재검토 대상일 수 있음, 별도 항목).
### Effect — 별도의 단순한 primitive로, "leaf 죽음에 확정 정리"만 담당
Roblox엔 `task.spawn`으로 코루틴에 반복문/타이머를 돌리는 패턴이 흔하고,
Luau 테이블엔 `__gc` 같은 GC 시점 훅이 없어서 "이게 진짜 사라지는 순간"을
아는 유일한 방법은 `Instance.Destroying`류 명시적 신호뿐이다. 이런
케이스(타이머 시작 → leaf가 죽을 때 반드시 정지)를 위한 별도 primitive는
필요하다는 데 합의:
```
Effect(fn) -> EffectHandle -- fn을 즉시 1회 실행, 리턴값(nil | () -> ())은
-- 이 Effect가 바인드된 leaf가 죽을 때 정확히 1회 호출
```
Observer와 달리 **재실행 개념이 없다** — 값 변화에 반응해 다시 도는 건
Observer(+클로저로 직접 짠 cleanup)의 영역이고, Effect는 순수하게 "설치 +
확정 정리" 페어 하나만 담당한다. children 배열에 leaf로 놓는 기존 Observer
바인딩 패턴을 그대로 재사용(그 leaf가 살아있는 동안만 유효, leaf가 죽으면
정리 콜백 호출). 비용은 leaf당 실제 Destroying 바인딩 하나(공유 weak
table로 되는 Observer보다 비쌈) — 사용자가 필요할 때만 쓰는 걸로 충분.
**상태**: 사실상 수렴 — 시그니처(`fn`의 인자 유무, `EffectHandle`의 모양)만
다듬으면 `base/`로 승격 가능해 보임. 계속 논의 원하면 이어감.
## 3. Batch(함수/코루틴 스코프 lexical block) — 기각 확정
**중요**: 아래는 "값 기반 지연/합치기"라는 문제 자체를 기각한 게 아니라,
**그 문제를 lexical block(Solid `batch()`/MobX `runInAction()`류)으로
풀려는 접근만** 기각한 것이다. 실제 해법은 완전히 다른 별개 primitive인
**Blocker**(아래 3-1번)로 채택됨 — 이 절은 "왜 lexical 접근은 안 되는가"만
다루는 순수 반면교사 기록으로 남긴다.
### "즉시 pull"이 뭔지 (참고용 예시)
store-bind 핸들러(예: `Frame { BackgroundColor3 = total }`)는 lazy가
아니다 — 화면에 실제로 반영하는 "누군가"가 바로 이 핸들러 자신이라,
무효화 신호를 받자마자 스스로 `Get()`하고 바로 대입한다:
```lua
local total = a:With(b):Compute(function(av, bv) return av + bv end)
Frame { BackgroundColor3 = total }
a:Set(1) -- total 무효화 → 핸들러가 즉시 (av=1, bv=이전b)로 재계산+대입
b:Set(2) -- total 무효화 → 핸들러가 즉시 (av=1, bv=2)로 다시 재계산+대입
-- BackgroundColor3가 중간값을 한 번 거쳐가고, 두 번 대입됨
```
### 왜 lexical block 방식을 안 쓰는가
`Batch(fn)`을 "플래그 세우고 fn 실행, 끝나면 flush"로 구현하면 **fn이
yield하는 순간 위험해진다**(사용자 지적, 정확함):
1. 플래그가 전역이면, yield 중 스케줄러가 돌리는 무관한 코루틴의 `Set()`
이 Batch에 잘못 휘말릴 수 있음.
2. 플래그를 코루틴 스코프로 만들어도(Fusion `Contextual`류 코루틴 키 weak
table), `fn` 안에서 새 코루틴을 스폰하는 API(Promise, `task.spawn`)를
부르면 그 새 코루틴은 배치 스코프를 상속 못 받아 일부 Set이 새어나감.
3. `fn`이 영원히 재개 안 되면(장시간 대기, 리크된 코루틴) flush가 영영 안
일어나 store-bind 핸들러들이 화면을 무기한 stale 상태로 방치 — 즉시
pull보다 더 나쁜 실패 모드.
이건 구현을 잘 짜서 피할 수 있는 버그가 아니라 **lexical block 모델
자체가 협조적 스케줄링 환경과 구조적으로 안 맞는 것**으로 판단, 기각.
## 3-1. Blocker — 채택된 새 primitive (Batch의 대안, 2026-08-06~07 세션)
### 핵심 아이디어 — 콜스택/코루틴이 아니라 값으로 지연 구간을 표현
Batch가 실패하는 근본 이유는 "지연 구간"을 콜스택/코루틴 스코프로
표현하려 했기 때문이다. `Blocker`는 그 구간을 **그냥 사용자가 들고 있는
값**으로 표현한다 — `On()`/`Off()`를 부르는 두 시점 사이에 얼마나 많은
yield/코루틴 전환이 끼어도, 심지어 완전히 다른 코루틴에서 `Off()`
불러도 아무 문제가 없다. Batch를 무너뜨렸던 세 가지 실패 모드(전역 플래그
오염, 새 코루틴 미상속, 영구 yield로 인한 무기한 방치)가 구조적으로 전부
해당 안 됨.
### 메커니즘 (확정)
```
Blocker() -> blocker -- 생성자
blocker:On() -> self -- IsBlocked = true로만 설정, 그 외 아무것도 안 함
blocker:Off() -> self -- IsBlocked = false로 먼저 설정, 그 다음 등록된
-- onunblock 핸들 전부 실행(순서 무관, idempotent)
state:Block(blocker) -> state -- 새 gated state 반환. **호출되는 즉시**(나중에
-- 처음 블록될 때가 아니라) onunblock 핸들을
-- blocker의 weak 배열에 등록.
```
gated state의 동작:
- 원본 state가 emit(무효화)될 때, 이 gated state로 전파를 시도.
- `blocker.IsBlocked`이면: 전파 안 하고 `HasBlockedEmit = true`만 세팅.
- `blocker.IsBlocked`가 아니면: 평소처럼 그냥 전파(투명하게 통과).
- `blocker:Off()`가 실행하는 onunblock 핸들은: `HasBlockedEmit`을 확인해
true면 그제서야 정확히 1회 전파(emit)하고 플래그를 리셋. 이미 false면
아무 것도 안 함(이미 언블록 상태에서 다시 Off를 불러도 안전 —
idempotent).
**`:Get()`엔 영향 없음** — 블록은 emit **전파**(eager 소비자에게 "바뀌었다"
알리는 신호)만 지연시킨다. 블록 중이라도 누군가 명시적으로 `:Get()`하면
그 순간의 실제 값을 정상적으로 계산해서 준다 — `store-semantics.md`
"Get()은 라이브 레퍼런스를 준다" 원칙과 일치.
### 사용 예시 — `state1, state2 -> state3` 케이스의 정답
처음 문제 제기("state1, state2 -> state3로 갈 때 둘 다 업데이트하면 state3가
두 번 계산됨, 한번에 할 방법이 없다")에 대한 답: **`state3`(결합된 결과)
하나에만 `:Block`을 걸면 된다** — `state1`/`state2` 각각에 걸 필요 없음:
```lua
local blocker = Blocker()
local gated3 = state3:Block(blocker) -- 소비자는 gated3를 구독
blocker:On()
state1:Set(1) -- state3 무효화 → gated3로 전파 시도 → 블록됨 → HasBlockedEmit=true
state2:Set(2) -- state3 무효화 → gated3로 전파 시도 → 이미 true, 그대로
blocker:Off() -- onunblock 핸들 실행 → HasBlockedEmit 확인 → 딱 한 번 emit
```
**일반 사용 가이드(확정, 문서화 필수)**: Block은 **파이프라인의 최종
연산 지점**(실제로 무거운 계산이 일어나는 derived state, eager 소비자에
가장 가까운 지점)에 거는 게 원칙 — 소스가 여러 개든, 하나가 한 주기에
여러 번 바뀌든 상관없이 이 지점 하나만 지키면 됨. 소스 쪽에 각각 거는 게
아니다.
### 이름 확정
- 클래스: `Blocker``Observer`/`Modifier`/`Ref`와 같은 명사-행위자
네이밍 관례와 일치.
- Blocker 자신의 토글: **`On()`/`Off()` -> self** (`Block()`/`Unblock()`
아님) — `state:Block(blocker)`가 이미 "배선(wiring)" 동작의 동사로
"Block"을 쓰고 있어서, Blocker 자신의 토글까지 같은 단어를 쓰면
`blocker:Block()`(블로커를 켠다)과 `state:Block(blocker)`(state를 이
블로커에 배선한다)가 같은 단어로 다른 두 동작을 가리키게 됨 — 자체
API 안에서 `register`→`State` 리네임 때 겪었던 "모호함은 풀었는데
충돌이 새로 생긴" 패턴이 반복될 뻔한 걸 미리 피함.
- 필드: **`IsBlocked`**(Blocker 자신의 On/Off 상태) — `Enabled`보다
명확(enabled는 "정상 작동 중"으로도 읽혀 헷갈릴 수 있음).
- 필드: **`HasBlockedEmit`**(gated state의 대기 플래그) — `Is`/`Has`
접두어로 불리언임을 바로 알려줌.
- 메소드: `state:Block(blocker) -> state`.
### 재진입(네스팅) — 의도적으로 미지원, 강한 문서화 필수
`IsBlocked`는 카운터가 아니라 단순 불리언이고, **의도적으로 그렇게
둔다.** 레퍼런스 카운팅으로 네스팅을 지원할 수도 있었지만, 그러면
"`On()` 여러 번, `Off()` 실수로 적게" 같은 버그가 **영구 블록으로 조용히
새는** 더 위험한 실패 모드를 만든다. 언어 차원에서 "On~Off 사이 코드가
죽었는지, 스레드가 죽었는지"를 추적하는 것도 해키해서 하지 않기로 함(Rust의
"poisoned mutex" — 락 구간 안에서 패닉이 나면 락이 오염 상태가 되는 것과
유사한 문제의식, 그 트래킹 자체를 만들지 않기로 함).
**대신 확정된 규칙**: 겹치는 배치가 필요하면 **각자 새 `Blocker` 인스턴스를
만들 것** — 하나의 Blocker를 여러 컨텍스트에서 재사용/중첩하지 않는다.
`Off()`는 스태킹 없이 즉시 그 자리에서 꺼진다. **이 제약은 반드시 사용자
문서(API 레퍼런스 수준)에 명시적으로 강조할 것** — 네스팅을 시도하면
조용히 잘못된 시점에 조기 해제되는, 원인 추적이 어려운 버그로 이어짐.
### 상태: 핵심 메커니즘+이름 확정. 남은 건 문서화뿐
`Extract`/`Add`처럼 세부 시그니처가 더 필요한 다른 항목들과 달리, Blocker는
설계 질문이 남아있지 않음 — `base/`로 승격 가능한 수준. quadnomicon에서
"Batch를 기각하고 왜 Blocker로 갔는가"를 비교 설명하는 게 좋은 소재(아래
"문서화 백로그" 참고).
## 4. Context — 기각 확정, 레이어드 Store 대안도 철회 (결정 완료)
### 난이도 판정 요약
서브에이전트 조사 결과: 동기적 저작 트리에 한정한 얕은 버전(Fusion
`Contextual`류 코루틴 키 weak table push-pop)은 구현 난이도 **낮음**이지만,
quad가 정상 패턴으로 확정한 "Slot에 이벤트 핸들러/코루틴에서 비동기로
자식이 추가되는 경우"엔 **에러 없이 조용히 기본값으로 폴백**하는 함정이
있음. 완전 자동(비동기 추가까지 자동 전파)은 Roblox Luau에
thread-local/async-context-propagation 훅이 없어 **사실상 불가**(플랫폼
한계, Node `AsyncLocalStorage`/Python `contextvars`도 "자동"이 아니라
"async 경계마다 명시적 캡처+재진입"으로 같은 문제를 품).
### 기각 이유
얕은 버전조차: (1) `base/slot-plan.md`가 정상 패턴으로 명시한 Slot 비동기
추가에서 가장 먼저, 가장 조용히 깨짐. (2) `research/debug-tooling-plan.md`
"모든 연결은 선언된 그래프여야 한다"는 quad-debug 철학과 충돌하는 안 보이는
채널을 만듦.
### 레이어드 Store 대안도 철회 (사용자 반박 수용)
이전 라운드에서 대안으로 "레이어드 Store"(자식 Source 모음이 없는 키는
부모로 `__index` 폴백)를 권고했는데, 사용자 반박으로 철회함:
- quad는 이미 **타입으로 강제되는 명시적 Store 전달**을 갖고 있고, 이건
Context의 "Provider 안 넣으면 조용히 기본값/에러"보다 **더 안전**하다
(컴파일타임에 걸림 vs 런타임에 조용히 새는 값) — Context보다 나은 지점.
- "필드 일부만 오버라이드, 나머지는 부모 값 그대로"가 필요하면, 오버라이드
지점에서 `Store({...부모값, 변경필드=새값})`을 **한 번 명시적으로**
만들어서 그 지점부터 평소처럼 prop으로 넘기면 끝 — Modifier가 이미 쓰는
"merge, 나중 게 이김" 패턴 재사용 가능, 새 primitive 불필요. 레이어드
Store(읽는 시점에 몇 단계를 거슬러 올라가는지 안 보이는 자동 폴백)는
정확히 Context와 같은 이유(디버깅 어려움 — "이 값이 왜 이거지?"를
추적하려면 부모 체인을 다 훑어야 함)로 얻는 것보다 잃는 게 크다.
- 서드파티 라이브러리가 뭔가 필요하면, 타입이 강제하는 명시적 요구
(`props.Theme: Store<Theme>`)가 "몰래 안 줘서 죽는다"보다 나은 실패
모드 — Slot 기반 저작 모델(부모가 자손을 직접 구성)에서도 이런 요청은
자연스럽게 props로 흐름.
### 결론
Context, 레이어드 Store 둘 다 프리미티브로 만들지 않음. "왜 Context가
없는가"(명시적 Store 전달이 이미 그 역할을 하고, 타입 강제가 Context의
실패 모드보다 안전하다는 논증)도 quadnomicon 에세이 후보로 등록(아래
"문서화 백로그" 참고).
## 빈 자리 아닌 것으로 확인된 것들
- **Untrack/Peek**(Solid `untrack()`, Vue `toRaw`): quad는 Vide식 암묵
@ -411,9 +188,10 @@ Context, 레이어드 Store 둘 다 프리미티브로 만들지 않음. "왜 Co
## 문서화 백로그 (2026-08-06~07, `documentation-content-map.md`에도 반영)
- **quadnomicon 에세이**:
- "왜 lexical Batch를 기각하고 대신 값 기반 Blocker를 택했는가" — 코루틴
yield 위험 분석(Batch 절)과 Blocker의 설계(3-1절)를 나란히 비교.
- "왜 Context가 없는가"(명시적 타입 강제 Store 전달이 이미 그 역할을 함).
- "왜 lexical Batch를 기각하고 대신 값 기반 Blocker를 택했는가" —
`archive/batch-rejected.md``base/additional-primitives.md`
Blocker 절을 나란히 비교.
- "왜 Context가 없는가" — `archive/context-rejected.md` 참고.
- "왜 push-invalidate/pull-recompute 설계가 laziness와 재계산 방지를
최우선 목표로 뒀는가" — Blocker 같은 파생 프리미티브가 이 목표 위에서
자연스럽게 나온 이유까지 포함해 기존 심화 콘텐츠 후보 3번(`왜
@ -426,22 +204,12 @@ Context, 레이어드 Store 둘 다 프리미티브로 만들지 않음. "왜 Co
유연한 구조" — 명시적 의존성 선언(`:With`) 위에서도 실제 계산은
조건부로 일부만 쓸 수 있다는 팁.
- "Blocker 사용 가이드" — 파이프라인 최종 연산 지점에 배치, **네스팅
금지를 강하게 명시**(위 3-1절 "재진입" 참고, 문서화 시 최우선 강조
항목).
금지를 강하게 명시**(`base/additional-primitives.md`의 "재진입" 절
참고, 문서화 시 최우선 강조 항목).
- "여러 Source를 한꺼번에 바꿀 때 Blocker 없이도 중복 재계산을 피하는
파이프라인/업데이트 순서 팁"(Blocker를 안 쓰는 단순 케이스용 보조 팁,
기존 "심화 최적화 팁" 항목을 Blocker 존재를 전제로 재조정).
## 제안 우선순위
1. **키 기반 컬렉션 재조정** — 여전히 최우선, 설계 진행 중. M0 이전 완전
확정이 목표.
2. **Effect** — 거의 수렴, 시그니처만 다듬으면 됨.
3. **Blocker** — 핵심 메커니즘+이름 확정, `base/`로 승격 가능한 수준.
문서화(특히 네스팅 금지 강조)만 남음.
4. **Batch(lexical)/Context** — 둘 다 결정 완료(기각), 더 이상 검토 대상
아님.
## 참고: 조사에 사용한 소스 근거
- Fusion: `State/ForPairs.luau`, `State/ForKeys.luau`,

View file

@ -328,7 +328,7 @@ UI를 클릭한 순간)에 강제로 그 계산을 트리거하는 건 이 전
**네이밍 컨벤션(사용자 제안)**: 내부 자동 생성 helper Instance는 `_`
`QUAD_` 같은 접두어를 붙여서 Explorer에서 직접 봤을 때 헷갈리지 않게 함 —
이름 바꾸는 건 비용이 크지 않음. v1이 이미 `_quad_round`/`_quad_padding`/
`_quad_scale` 네이밍(`research/ui-shorthand-plan.md` 참고)으로 정확히
`_quad_scale` 네이밍(`base/ui-shorthand-plan.md` 참고)으로 정확히
이 관습을 썼던 전례 — quad-v2에서 내부 자동 생성물이 생기면 그대로
재사용. `research/documentation-plan.md`의 "UI 네이밍 컨벤션 문서"
백로그에도 이 구체적 규칙을 추가해둠.

View file

@ -37,7 +37,7 @@
11. **Ref 기초** — 외부 관리 Instance 참조/마이그레이션용, `CreatedRef(fn)` + 배열 위치로 자식 전/후 표현, "프로퍼티보다도 먼저" 필요할 때만 `PreRef`(2026-08-07 세 번째 세션, `phase` 옵션 폐기) (`architecture.md`, `bind-system-plan.md`)
12. **파생값 최소 예시**`:With(...)` + `:Compute(fn)` 기본형 (`bind-system-plan.md`, `store-semantics.md`)
13. **Tween 기초**`[Tween(key, ...)] = storeValue`, 취소 시 현재 보간값에서 자연스럽게 이어짐 (`research/tween-plan.md`)
14. **UI 숏핸드(quad-roblox 한정)**`Corner`/`PaddingAllOffset`/`Scale` 인라인 키 (`research/ui-shorthand-plan.md`)
14. **UI 숏핸드(quad-roblox 한정)**`UICorner`/`UIPadding`/`UIPaddingOffset`/`UIScale` 인라인 키 (`base/ui-shorthand-plan.md`)
---
@ -135,7 +135,7 @@ v1 폐기 API/버그/구조 결함 전부 v2 설계를 정당화하는 내부
실제 일어나는 derived state)에 배치하는 게 원칙이라는 것, **네스팅
금지를 최우선으로 강조**(겹치는 배치는 각자 새 `Blocker`를 만들 것 —
안 지키면 조용히 잘못된 시점에 조기 해제되는 원인 추적 어려운 버그로
이어짐) — `research/additional-primitives-plan.md` 3-1
이어짐) — `base/additional-primitives.md`의 "Blocker"
19. 여러 Source를 한꺼번에 바꿀 때 Blocker 없이도 중복 재계산/재대입을
피하는 파이프라인/업데이트 순서 팁(Blocker를 안 쓰는 단순 케이스용
보조 팁) — `research/additional-primitives-plan.md` "문서화 백로그" 절
@ -197,13 +197,15 @@ additional-primitives-plan.md`의 "문서화 백로그" 절이 원자료)**:
## 5. 문서화 아직 보류(미확정 설계라 쓰면 안 됨)
- Slot 형제 순서 보장 (`slot-plan.md`)
- Tween 오버라이드/삭제후재시작/끝점이동 세부 옵션 키 이름 (`research/tween-plan.md`)
- UI 숏핸드 `RoundSize` 드롭 여부 (`research/ui-shorthand-plan.md`)
- Tween 오버라이드/삭제후재시작/끝점이동 세부 옵션 키 이름, 트윈 옵션 값
모양(TweenInfo vs 편의 필드) (`research/tween-plan.md`)
- `Attribute<T>` 제네릭 vs 타입별 정적 생성자 (`bind-system-plan.md`)
- provider/processor 네이밍 (`module-lifecycle-plan.md`)
- 키 기반 동적 컬렉션 재조정 최종 이름/시그니처(`Render`/`Draw`/`List` 등
후보만 있음, `Slot:Extract` 세부 시맨틱도 미정) — `research/
additional-primitives-plan.md`(2026-08-06 신설, 설계 진행 중)
- Effect가 `state:Effect()`로 Observer를 확장하는 형태인지, 완전히 독립된
free function인지 (`base/additional-primitives.md`의 "미해결" 절)
이 항목들은 `.claude/question.md`에도 이미 열린 질문으로 잡혀있음 — 여기선
"확정 전엔 문서화 대상 아님"이라는 표시만 겸함.

View file

@ -102,7 +102,7 @@ Roblox가 Play 중 라이브 UI 편집 도구를 꺼버려서 실제 화면의 U
강제할까(과한 선택지, 참고만)?
**구체적으로 결정된 하위 규칙 하나(2026-08-06)**: quad가 내부적으로
자동 생성하는 helper Instance(예: `research/ui-shorthand-plan.md`의
자동 생성하는 helper Instance(예: `base/ui-shorthand-plan.md`의
UICorner/UIPadding/UIScale 숏핸드가 만드는 자식)는 `_``QUAD_` 같은
접두어를 붙여 Explorer에서 직접 볼 때 사용자가 만든 것과 헷갈리지 않게
함 — v1의 `_quad_round`류 네이밍 재사용(`research/debug-tooling-plan.md`

View file

@ -442,7 +442,7 @@ M11 착수 시.
### 2-11. UI shorthand — 기존 UICorner 자식과의 매칭 기준(이름 vs 타입)이 불명, 사용자 실수 유발 위험 높음
**위치**: `research/ui-shorthand-plan.md` "v1 실제 메커니즘" 절.
**위치**: `base/ui-shorthand-plan.md` "v1 실제 메커니즘" 절.
**문제**: v1 메커니즘은 "기존 `UICorner` 자식이 있으면 재사용, 없으면
`Instance.new("UICorner", item)`(`Name = "_quad_round"`)"라고 서술되는데,
@ -485,7 +485,7 @@ M11 착수 시.
### 3-2. Corner/PaddingAll/Scale 개별 Handler 대신 데이터 테이블 구동 단일 제네릭 Handler
**위치**: `research/ui-shorthand-plan.md` "메커니즘 — 새 아키텍처 개념
**위치**: `base/ui-shorthand-plan.md` "메커니즘 — 새 아키텍처 개념
불필요" 절.
**문제**: 문서는 "Corner/PaddingAll/Scale 같은 특수 키를 인식하는

View file

@ -2,12 +2,16 @@
**상태**: research — 방향은 뚜렷하게 잡혀 있고(라이브러리가 트윈을 직접
구현하지 않는다), `retract` 순서/오버라이드 기본값(Cancel)도 확정됨. 남은 건
기본값 외 나머지 오버라이드 동작을 고르는 옵션 키의 정확한 이름/시그니처
정도. 원본:
기본값 외 나머지 오버라이드 동작을 고르는 옵션 키의 정확한 이름/시그니처와,
트윈 옵션을 어떤 값 모양으로 받을지(아래 "트윈 옵션 값 모양" 절, 신규)
정도. **중요 — 놓치기 쉬운 포인트**: `retract`는 Destroy(완전 소멸) 시엔
호출되지 않는다(아래 "`retract`는 완전 소멸 시엔 호출되지 않는다" 절) —
Tween 오버라이드 로직을 짤 때 "인스턴스가 파괴될 때도 이 코드가 실행될
것"이라고 가정하면 틀림. 원본:
`.claude/initreq/raw-userinput.md` "트윈은 어떻게 할 것이냐" / "스토어 값은
항상 먼저 캐치한다" / "네임스페이스드 객체" 절. Fusion의 Tween/Spring이
반응 그래프 안에 있는 설계는 명시적 반면교사 — [정정: 절 제목이 실제와
달랐음] `base/comparison-fusion-vide.md`의 "Fusion" 절 마지막 불릿
달랐음] `reference/comparison-fusion-vide.md`의 "Fusion" 절 마지막 불릿
("Tween/Spring이 State그래프 안의 1급 노드") 참고.
## 확정된 방향: 트윈을 Store/반응 그래프 밖에 둔다
@ -79,6 +83,44 @@ Ref나 네임스페이스드 조회가 필요하지 않음. Tween의 store-bind
Destroy 시점엔 아무 것도 안 함(라이프타임 `Connected` 체크로 처리 자체를
멈추는 것만으로 충분).**
**메모 — `retract``canExecute`는 서로 다른 문제를 다룬다, 나중에 quadnomicon
급에서 제대로 설명 필요.** "그럼 값 교체가 아니라 값을 계속 관측하는 쪽
(예: `state:Observer(fn)`으로 Tween을 건 경우)은 Destroy 시 어떻게
정리되는가?"라는 질문이 자연스럽게 따라오는데, 이건 `retract`의 영역이
아니라 `canExecute`(라이프타임 predicate, `base/lifecycle-pattern.md`
"생명 바인드 유틸" 절)의 영역이다 — Destroy되면 `retract` 호출 없이 그냥
`canExecute`가 false가 되어 이후 처리 시도 자체가 조용히 no-op된다.
store-bind 일반(Tween 포함)도 같은 결이라 실제로는 이미 일관되게 명시돼
있지만(`base/bind-system-plan.md` "확정된 디스패치 모델" 절), "왜 이
경로엔 retract를 쓰고 저 경로엔 canExecute를 쓰는가"라는 내부 구조상의
이유는 quadnomicon 콘텐츠로 풀어서 설명할 필요가 있음(`research/
documentation-content-map.md` 심화 콘텐츠 후보에 메모) — 지금은 이 메모만
남겨두고 상세 설명은 나중 문서화 단계로 미룸.
## 트윈 옵션 값 모양 — TweenInfo 그대로 vs 편의 필드 (신규, 열린 논의)
**아직 논의 시작 단계 — 나중에 더 다룰 주제로만 남겨둠.** Roblox의
`TweenInfo.new(time, easingStyle, easingDirection, repeatCount, reverses,
delayTime)`는 순수 포지셔널 생성자인데, Luau엔 named call 문법이 없어서
직접 쓰면 `TweenInfo.new(0.3, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)`
처럼 각 인자가 뭘 뜻하는지 호출부만 보고 알기 어렵다. 후보:
1. **`TweenInfo`를 그대로 받는다** — 사용자가 이미 만들어둔 `TweenInfo`
재사용하고 싶은 경우엔 상관없지만, 대부분의 흔한 케이스(길이/이징만
바꾸고 싶음)에서 매번 포지셔널 생성자를 마주해야 함.
2. **편의 필드로 개별 인자를 받고 기본값을 제공** — 예:
`{Time=0.3, Style=Enum.EasingStyle.Quad, Reverses=false, ...}`처럼
이름 붙은 키로 받고 흔한 기본값(예: `Time=0.2`, `Style=Quad`,
`Direction=Out`)을 채워줌. 명시적으로 `TweenInfo`가 이미 있어서
재사용하고 싶다는 케이스에도 열어두면(예: `Info = someTweenInfo`
필드로), 둘 다 지원 가능.
**현재 소견(확정 아님)**: 2번(편의 필드 + 기본값)이 흔한 사용 경험상 더
낫다는 쪽으로 기움 — 대부분의 호출에서 named call이 없는 `TweenInfo.new`
가독성 문제를 피할 수 있고, 기본값 덕에 짧은 호출도 가능해짐. 다만
구체적인 필드 이름/기본값/`TweenInfo` 재사용 경로의 정확한 문법은 아직
확정 아님 — 나중 논의 대상으로 남김.
## 네임스페이스드 객체 (성능상 이유로 보류)
트윈 대상을 이름으로 찾는 별도 네임스페이스는 성능상 별로라고 판단 —
@ -92,3 +134,7 @@ CollectionService를 쓰는 게 나아 보이지만, 트윈 전용 네임스페
- 기본값(Cancel)은 확정됨. 남은 건 나머지 세 동작(오버라이드/삭제 후 재시작/
끝점 이동 후 재시작)을 선택하는 옵션 키의 정확한 이름/시그니처 — 구현
단계에서 확정.
- 트윈 옵션 값 모양(위 "트윈 옵션 값 모양" 절, 신규) — `TweenInfo` 그대로
받을지 편의 필드+기본값으로 받을지, 소견은 후자 쪽이지만 확정 아님.
- 자연 완료(Completed) 시 per-instance 북키핑 정리 여부 —
`research/pre-implementation-audit.md` 2-10번 참고, M11 착수 시 확정.

View file

@ -1,102 +0,0 @@
# UI 편의 숏핸드 (Corner/Padding/Scale 등) — 인라인 적용 계획
**상태**: research — 2026-08-06 세션에서 결론까지 남. `Corner`/
`PaddingAll`/`Scale` 숏핸드 자체는 **여전히 필요**(사용자 재확정, 아래
"결론" 절 — 이전에 이 문서가 한 차례 "포팅 불필요"로 잘못 정리했던 걸
정정함). 패키지 배치는 `quad-roblox` 코어 직접 포함으로 확정.
## 배경
사용자 기억: v1을 쓸 때 "UICorner/UIPadding/UIScale 같은 걸 직접
`Instance.new`로 만들어 Parent하는 귀찮은 작업 없이, Frame 안에 인라인으로
넣기만 해도 CSS 스타일처럼 적용됐다 — 코드가 줄고 읽기도 편해서 꽤
괜찮았다"는 것. 문서 어디에도 기록된 적 없어 v1 소스(`.claude/initreq/quad`)와
PA님 코드(`.claude/initreq/artworks`)를 서브에이전트로 조사.
## v1 실제 메커니즘 (조사 완료)
`class.lua``SetProperty`/`GetProperty`(38~109행)와 `ProcessQuadProperty`
(134~213행)에 하드코딩된 if/elseif 분기로 특수 문자열 키 5종을 지원:
- `RoundSize = 16``ImageLabel`/`ImageButton` 전용, UICorner가 아니라
이미지 자체의 9-slice 라운드 처리(`round.SetRound()`) — **UICorner 계열과
메커니즘이 다름**.
- `Corner = 8` → 숫자 하나. 기존 `UICorner` 자식이 있으면 재사용, 없으면
`Instance.new("UICorner", item)`으로 생성(`Name = "_quad_round"`),
`CornerRadius = UDim.new(0, value)` 설정.
- `PaddingAll = UDim.new(...)` / `PaddingAllOffset = 50` → 동일 패턴,
`UIPadding`(`_quad_padding`).
- `Scale = 1.2` → 동일 패턴, `UIScale`(`_quad_scale`).
값 모양은 항상 **리터럴 하나**(숫자/UDim) — 테이블도 `__type` 태그도 아님.
실사용 예시(`md/kr/tutorial/7_quadProperty.md`):
```lua
Frame "mainFrame" {
PaddingAllOffset = 50;
ImageFrame { RoundSize = 16; ... };
}
```
**`UIListLayout`/`UIGridLayout`/flex는 이런 전용 숏핸드가 v1에 없었음** —
`ProcessQuadProperty`의 범용 자식 나열 분기(배열 인덱스로 놓인 Instance/
Class 결과를 자동 mount, 207~213행)로 `UIListLayout{...}`을 그냥 직접
나열했을 뿐, `List = true` 같은 전용 축약 문법은 레포 전체(PA님 코드
포함)에서 찾지 못함. **quad-v2도 이 부분은 이미 있는 children-array +
인스턴스 생성 문법으로 그대로 커버됨 — 새로 설계할 것 없음.** 사용자
기억 중 이 부분은 "전용 숏핸드"가 아니라 "선언형 문법 자체가 원래
간결하다"는 것과 섞였을 가능성이 큼.
## 결론 (2026-08-06, 한 차례 오해 후 재정정)
**RoundSize와 Corner는 서로 다른 이유로 존재했던 별개 기능 — 혼동하지
말 것**:
- **`RoundSize`(이미지 9-slice 라운드)**: `ImageLabel`/`Button`을
이미지 트릭으로 둥글게 보이게 하던 것 — **당시 Roblox에 `UICorner` 같은
네이티브 구현체가 없었기 때문에** 존재하던 워크어라운드. 지금은
`UICorner`가 안정적인 네이티브 Instance라 이 이미지 트릭 자체를 그대로
포팅할 이유는 없음(이미지에도 그냥 실제 `UICorner`를 쓰면 됨) —
**RoundSize는 포팅 안 함**.
- **`Corner`/`PaddingAll`/`Scale`(UICorner/UIPadding/UIScale 자동
생성)**: 이건 워크어라운드가 아니라 **지금도 유효한 편의 기능**
**사용자 재확정**: "UIScale 같은 건 여전히 별도의 Instance고 부모
Frame에 영향을 주는 구조, 숏핸드는 여전히 필요하다". `UICorner`
네이티브가 됐다고 해서 "별도 Instance를 만들어 부모에 Parent해야
한다"는 구조적 번거로움 자체가 없어지는 게 아니므로, 이 숏핸드의
존재 이유는 여전히 유효함 — **이전 정리("포팅 불필요")는 오해였고
정정함, `Corner`/`PaddingAll`/`Scale`은 그대로 포팅 대상.**
**메커니즘 — 새 아키텍처 개념 불필요**: 이미 있는 pluggable Handler로
그대로 커버됨. `Corner`/`PaddingAll`/`Scale` 같은 특수 키를 인식하는
Handler(`isHandlable`이 그 키를 매칭)가 "이름 붙은 자식을 찾거나 만들고
프로퍼티 세팅"을 `process(inst, k, v)`에 구현 — v1의 하드코딩 if/elseif
대신 정식 핸들러 계약(`isHandlable`/`priority`/`process`/`retract`)을
따르는 것만 다름. `modifier-plan.md`가 이미 예시로 든
`Modifier.Rounded(8)`은 이 특수 키를 flatten해서 props에 꽂아넣는 사탕
문법일 뿐, 실제 처리는 이 Handler가 함 — Modifier를 안 거치고
`Frame { Corner = 8 }`처럼 순수 인라인 키로 직접 써도(v1처럼) 동일하게
작동함, `architecture.md``[Attribute "Name"]`류 특수 키와 같은 층위.
자동 생성된 자식은 위 "핵심 설계 방향" 관례대로 `_`/`QUAD_` 접두어
네이밍(`research/debug-tooling-plan.md` 9번, v1의 `_quad_round`
그대로 재사용).
**패키지 배치 — `quad-roblox` 코어에 직접 포함, 확정**: "트윈도 인스턴스
생성/제어를 직접 구현 가능한 걸 하나로 묶어 쉽게 쓰게 합친 것 — 너무
잘게 쪼개 오버엔지니어링하기보다 확실히 하나로 코어에 넣어도 충분하다,
opt-out할 이유가 별로 없다"는 게 사용자 판단 — **작고 항상 켜져 있어도
비용이 무시할 만한 편의 기능은 별도 opt-out 패키지로 쪼개지 말고
`quad-roblox` 코어에 직접 포함한다**는 원칙으로 확정(이미 계획된 Tween
핸들러가 같은 모양이라는 게 근거). 이 원칙은 일반화해서 재사용 가능 —
앞으로 비슷한 "작은 인스턴스 편의 기능"이 제안되면 `quad-roblox-util`
같은 걸 새로 만들지 않고 이 선례를 따르면 됨.
**중요도**: 낮음("이건 나중에도 쉽게 구현됨" — 사용자) — 지금 M0 우선순위를
바꿀 이유는 없음, M10(Handlers/Attribute 등) 전후로 다른 세부 Handler와
함께 구현하면 충분.
## 열린 질문 (`.claude/question.md`에도 취합)
- 이름 그대로 가져올지(`Corner`/`PaddingAll`/`PaddingAllOffset`/`Scale`)
재검토할지 — 진행 중인 용어 정리(`CLAUDE.md` "지금 할 일" 2번)에 합류
대상.
- `RoundSize`(이미지 라운드)를 완전히 드롭할지, 아니면 이미지 대상에도
그냥 실제 `UICorner`를 자동 적용하는 것으로 대체할지 — 후순위.

View file

@ -61,7 +61,7 @@ v1(`.claude/initreq/quad/src`) 조사 결과, API는 성격이 다른 두 계층
핸들러 분기도 안 생김. 단, "quad-debug 추적 밖 mutate 경로가 생긴다"는
근거(4번)는 격리해도 남는 문제라 별도 검토 필요.
- **RoundSize 등 특수 키**: `Corner`/`PaddingAll`/`Scale`은 이미
`research/ui-shorthand-plan.md`에서 네이티브 포팅 확정됨 — 별도 compat
`base/ui-shorthand-plan.md`에서 네이티브 포팅 확정됨 — 별도 compat
작업 불필요. `RoundSize`(이미지 9-slice 라운드 트릭)만 UICorner 없던
시절 워크어라운드라 재현 자체가 불필요하다고 이미 결론남.
- **`target()`/Linker**: 위 정정대로 "이름 있는 자식 등록 + 시그널 중계"일

View file

@ -38,10 +38,14 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션
`.claude/README.md`가 색인. 요약:
- `.claude/base/` — 확정된 아키텍처/컨텍스트, plan/done 개념 없음. 먼저
`.claude/base/architecture.md`를 읽을 것.
- `.claude/reference/`**[2026-08-07 신설]** base처럼 확정된 건 아니지만
base 문서가 근거로 인용하는 온디맨드 참고 자료(v1 내부 동작 스냅샷,
Fusion/Vide 비교 리서치) — 항상 읽을 필요는 없고 인용될 때만 열어볼 것.
- `.claude/research/` — 아직 착수 전, 사용자와 상의 필요한 설계 논의.
`tween-plan.md`/`existing-instance-bind-plan.md`/`debug-tooling-plan.md`/
`ui-shorthand-plan.md`/`documentation-plan.md`/`documentation-content-map.md`/
`framework-comparison-findings.md` — 전부 후순위(급한 건 `tween-plan.md`
`documentation-plan.md`/`documentation-content-map.md`/
`framework-comparison-findings.md`/`additional-primitives-plan.md`(키 기반
동적 컬렉션 재조정만 남음) — 전부 후순위(급한 건 `tween-plan.md`
세부 옵션 정도). 최신 목록·우선순위는 `.claude/README.md`가 소스, 여기서
개수 반복 안 함(과거에 "두 개뿐"이라 적어놨다가 새 문서 추가될 때마다
안 갱신되는 패턴이 반복돼서 아예 안 세기로 함).
@ -676,3 +680,71 @@ M7 착수 시 `modifier-plan.md` 8번 참고하면 됨.
풀리는 문제 — `false`를 이벤트 disconnect 센티널로 쓴 선례처럼 `None`
(가칭) 프리미티브를 도입하는 방향만 `base/modifier-plan.md` "2-1"절에
짧게 메모해두고 상세 설계는 다음 세션으로 미룸.
## 2026-08-07 네 번째 세션 — `.claude/` 코퍼스 전반 정리(폴더 재편, 승격, 기각 분리)
사용자가 코퍼스 전체를 훑고 "실제 코딩에 필요한가"를 기준으로 남길 것과
분리할 것을 판단해 달라고 요청 — 여러 문서에 쌓인 역전 이력/quad
자체와 무관한 배경자료/이미 기각된 후보가 뒤섞여 있어 컨텍스트 크기와
가독성 둘 다 해치고 있다는 문제의식. 아래 6가지를 처리, 전부 반영 완료:
1. **`reference/` 폴더 신설** — `quad-v1-architecture.md`,
`comparison-fusion-vide.md``base/`에서 이동. 항상 읽어야 하는
결정사항(`base/`)과, 다른 문서가 근거로 인용할 때만 열어보면 되는
온디맨드 스냅샷/비교자료(`reference/`)를 분리 — 전자는 "결정 완료",
후자는 "결정이 아니라 결정의 근거"라는 차이. 전체 문서의 상호참조
경로도 전부 갱신함.
2. **`component-composition-plan.md`의 누적 역전 이력 트리밍** —
`StoreSource` 프록시 폐기 이력이 "원래 이랬다 → 이렇게 뒤집혔다"를
본문에서 장황하게 반복 서술하고 있었는데, 이미 `archive/
store-source-proxy-reversed.md`에 원문·이유·비교표가 전부 보존돼
있으므로 본문은 최종 확정만 남기고 포인터로 압축.
3. **`ui-shorthand-plan.md``research/`→`base/`로 승격, 재작성** —
(a) 이미지 라운드 트릭 `RoundSize`는 완전히 드롭, 근거는
`archive/ui-shorthand-roundsize-dropped.md`로 분리(이 판단이 한 차례
"Corner/PaddingAll/Scale 전체가 불필요하다"로 잘못 일반화됐다가
정정된 이력도 같이 보존). (b) 이름을 v1 그대로(`Corner`/`PaddingAll`/
`Scale`)가 아니라 실제 Roblox Instance 이름과 맞춘 `UICorner`/
`UIPadding`/`UIScale`로 확정 — v1식 짧은 이름은 Modifier 체이닝
메소드와 겹쳐 "진짜 UICorner 숏핸드인지 그냥 비슷한 이름의 부가
Modifier인지" 구분이 안 된다는 사용자 지적 반영. (c) store-bind
가능성 명시 — v1에서도 가능했던 기능이고, Tween처럼 무거운 API
표면 없이 기존 per-instance weak-table 유틸(`base.perInstanceState`)
재사용만으로 충분하다는 점을 추가.
4. **`additional-primitives-plan.md`를 4갈래로 분리**: 확정된 `Blocker`/
`Effect`는 새 `base/additional-primitives.md`로 승격(Blocker는
State와 같은 마일스톤에서 개발하기로 해서 `store-semantics.md`
교차 참조 추가, `ROADMAP.md` M3에도 체크박스 반영). 기각된 `Batch`
(lexical block)와 `Context`(+대안이던 레이어드 Store)는 각각
`archive/batch-rejected.md`/`archive/context-rejected.md`로 분리.
`research/additional-primitives-plan.md`엔 아직 실제로 열려있는
것(키 기반 동적 컬렉션 재조정) 하나만 남김.
5. **archive 제목 컨벤션을 둘로 분화** — 기존 `[역전됨]`(한 번 확정했다가
뒤집힌 것, `store-source-proxy-reversed.md`/`ref-phase-option-reversed.md`)과
새로 생긴 `[기각됨]`(확정한 적 없이 후보였다가 채택 안 된 것,
`batch-rejected.md`/`context-rejected.md`/`ui-shorthand-roundsize-dropped.md`)을
구분 — `README.md``archive/` 폴더 기준 설명에 두 컨벤션 차이를
명시.
6. **`tween-plan.md` 보강** — `retract`가 Destroy 시엔 호출 안 된다는
사실을 상단 상태 요약에서도 짚도록 가시성 강화, `canExecute`(Destroy
시 처리)와 `retract`(값 교체 시 처리)가 서로 다른 문제를 다룬다는
점을 quadnomicon급 문서화 숙제로 메모(지금은 상세 설명 안 하고
메모만). 트윈 옵션 값 모양(raw `TweenInfo` vs 이름 붙은 편의
필드+기본값) 논의를 새로 열어둠 — Luau가 named call을 지원 안 해서
`TweenInfo.new(...)` 포지셔널 생성자가 읽기 어렵다는 문제의식,
소견은 편의 필드 쪽이지만 확정 아님, 나중 논의 대상으로만 남김.
**미해결로 남긴 것 — 임의로 결론내지 않음**: Effect가 `state:Effect()`
형태로 Observer를 확장하는 변형인지, 완전히 독립된 free function인지가
불명확함(사용자가 "확인 필요, 아니라면 논의해야 할 상태로 남겨두라"고
명시). 관련 하위 질문으로 `state:Observer(fn)`가 생성 시 `fn`을 즉시
1회 실행하는지도 문서 어디에도 명시돼 있지 않음이 이번에 드러남(Effect는
"즉시 1회 실행"이 스펙에 명시돼 있어 이 부분만 보면 둘이 겹쳐 보임).
`base/additional-primitives.md`의 "미해결" 절과 `.claude/question.md`
0번에 반영 — 구현 착수(M3~M4 전후) 전에 반드시 재확인할 것.
**다음 세션이 할 일**: 안 바뀜(위 2026-08-06 네 번째 세션 절 "다음 세션이
할 일" 참고, `ROADMAP.md` M0부터). 이번 세션은 순수 문서 정리라 설계
결정 자체는 늘지 않았음 — 단, M3 체크리스트에 `Blocker.luau` 항목이
하나 추가된 것과, 위 Effect/Observer 미해결 항목은 M3~M4 착수 전에
확인해야 함.

View file

@ -52,12 +52,21 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
- [ ] `Dispatch/init.luau`(`process`/`retract` 엔진, `isHandlable` 우선순위 스캔)
- [ ] `Handler.luau`(핸들러 계약 타입)
- [ ] `LifetimeHandle.luau`/`PerInstanceState.luau` **인터페이스만**(타입
계약, 실 구현 없음 — quad-roblox 실 구현은 M8) — 원래 M8에만
있었으나 M4(StoreBind의 `Connected` 확인)/M6(Slot의 `canExecute`)이
이미 이 인터페이스를 전제로 서술돼 있어 로드맵 순서가 역전돼
있었음(`pre-implementation-audit.md` 우선순위1-9, `question.md` 2번
— 2026-08-07 세 번째 세션에 반영)
- [ ] mock 대상 테스트
## M3 — Store/State/Source
- [ ] `Source.luau`/`State.luau`/`Store.luau`
- [ ] `store.key` dot-access 타입 추론 확인
- [ ] `Blocker.luau`(`base/additional-primitives.md` 참고 — 여러 Source를
한꺼번에 바꿔도 파생값 재계산/재대입이 한 번만 되게 하는 primitive,
State와 밀접히 연관돼 있어 같은 마일스톤에서 개발)
- [ ] mock 대상 테스트
## M4 — 첫 end-to-end 반응형 업데이트
@ -87,12 +96,23 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
- [ ] `State<Modifier>` 조합 타입 차단 확인(`modifier-plan.md` 7번, UB 확정)
- [ ] `:Apply(factory)` 팩토리 함수 체이닝(`modifier-plan.md` 8번, 예약 키
`Apply`가 제네릭 `__index` 필드 setter와 안 겹치는지 확인)
- [ ] 인라인 키로 modifier 필드를 명시적으로 지우는 문제 확인 — `None`
(가칭) 센티널 프리미티브 도입 여부(`modifier-plan.md` 2-1번, 아직
미정 — 착수 전 사용자 확인 필요, 확정 안 되면 이번 마일스톤은
스킵하고 다음으로 미뤄도 됨)
## M8 — Ref
- [ ] `CreatedRef` 메커니즘(숫자 슬롯 참가자)
- [ ] `LifetimeHandle` 인터페이스 + quad-roblox 실제 구현(Instance 생존 확인)
- [ ] `PerInstanceState` 인터페이스 + quad-roblox 실제 구현(weak-keyed table)
- [ ] `CreatedRef` 메커니즘(숫자 슬롯 참가자) + `PreRef`(children 배열
전용, Modifier/Store 타입 차단, 위치 무관 호이스팅 pre-pass —
`base/bind-system-plan.md` "`phase` 옵션 폐기 → 위치로 표현,
`PreRef` 신설" 절)
- [ ] Ref 콜백/대기자 실행 루프(`type(v)=="thread"`면 resume+소진,
함수면 호출+유지 — 같은 배열 하나로 통합)
- [ ] `LifetimeHandle` quad-roblox 실제 구현(Instance 생존 확인, 인터페이스
자체는 M2로 이동됨)
- [ ] `PerInstanceState` quad-roblox 실제 구현(weak-keyed table, 인터페이스
자체는 M2로 이동됨)
## M9 — 컴포넌트 합성 레이어