diff --git a/.claude/README.md b/.claude/README.md index 69e744a..aa9cf90 100644 --- a/.claude/README.md +++ b/.claude/README.md @@ -86,7 +86,7 @@ | 문서 | 내용 | 우선순위 | |---|---|---| -| `epoch-brand-composition.md` | **[2026-08-21 신설]** 에포크 부기를 State에서 떼어내 `EpochMap`으로 컴포지션하고, emit 페이로드를 `Source` 대신 **`Epoch` 인터페이스**(`{Count: number}`, 그 자체로 키)로 일반화하며, 그걸 위해 `Brand`를 **인스턴스화 가능**하게(다중 태깅) 바꾸자는 사용자 제안. 발단은 다중 의존성 `Effect`에서 한 파동에 `fn`이 두 번 도는 갭 — `Effect`가 자기 `EpochMap`을 들면 닫힌다. 에이전트가 짚었던 대가 둘(`Brand.get` 역조회 상실 / 포함관계가 흩어짐)은 **같은 날 둘 다 해소**(전자는 필요 없고, 후자는 착오 — predicate 합성으로 그대로 쓰면 됨). `Epoch`는 `Source`가 구조적으로 만족하고 리비전 필드는 공개까지 확정. **남은 결정은 숫자 리비전 vs 테이블 토큰, State가 `EpochMap`을 둘 드는지, `Observer` 클로저 인자 셋** | 중 — M3(State) 전. `Brand` 전환은 M1 코드가 아직 `Brand`를 안 써서 문서 비용뿐 | +| `epoch-brand-composition.md` | **[2026-08-21 신설]** 에포크 부기를 State에서 떼어내 `EpochMap`으로 컴포지션하고, emit 페이로드를 `Source` 대신 **`Epoch` 인터페이스**(`{Count: number}`, 그 자체로 키)로 일반화하며, 그걸 위해 `Brand`를 **인스턴스화 가능**하게(다중 태깅) 바꾸자는 사용자 제안. 발단은 다중 의존성 `Effect`에서 한 파동에 `fn`이 두 번 도는 갭 — `Effect`가 자기 `EpochMap`을 들면 닫힌다. 에이전트가 짚었던 대가 둘(`Brand.get` 역조회 상실 / 포함관계가 흩어짐)은 **같은 날 둘 다 해소**(전자는 필요 없고, 후자는 착오 — predicate 합성으로 그대로 쓰면 됨). `Epoch`는 `Source`가 구조적으로 만족하고 리비전 필드는 공개까지 확정. **[같은 날 2차 회신으로 사실상 전량 확정]** 리비전은 숫자, `Update(전체 deps)`가 곧 sync라 `:Sync`는 순수 최적화, `Observer` 클로저는 `:Compute`와 같은 `fn(self, from)`. 남은 미정은 리비전 증가를 `bit32` 랩으로 할지 평이한 `+1`로 할지 하나뿐(둘 다 실질 위험 없음). **`base/` 승격은 사용자 확인 대기** — 승격하면 `state-epoch-plan`/`source-state-plan`/`effect-plan`/`brand-plan` 넷을 같이 고쳐야 함 | 중 — M3(State) 전. `Brand` 전환은 M1 코드가 아직 `Brand`를 안 써서 문서 비용뿐 | | `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 목차 초안 포함 | 하 — 문서화 착수 시점의 목차/우선순위표로 쓸 것 | diff --git a/.claude/research/epoch-brand-composition.md b/.claude/research/epoch-brand-composition.md index 51037e9..b78a602 100644 --- a/.claude/research/epoch-brand-composition.md +++ b/.claude/research/epoch-brand-composition.md @@ -1,8 +1,10 @@ # `Epoch` 인터페이스 + `EpochMap` 컴포지션, 그리고 `Brand` 인스턴스화 (2026-08-21 신설) -**상태**: research — **사용자 제안, 에이전트 평가 완료, [2026-08-21] 사용자 -회신으로 §4의 1·2·3·6·7이 닫힘. 남은 열린 항목은 4·5와 "리비전을 숫자로 둘지 -테이블로 둘지"뿐.** 그래도 `base/`엔 아직 아무것도 안 옮겼다. +**상태**: research — **사용자 제안, 두 차례 회신으로 §4가 사실상 전부 닫혔다** +(남은 미정은 리비전 증가를 `bit32` 랩으로 할지 평이한 `+1`로 할지 하나뿐이고, +그건 어느 쪽이든 실질 위험이 없다). **다만 아직 `base/`로 승격하지 않았다** — +승격하면 `state-epoch-plan.md`/`source-state-plan.md`/`effect-plan.md`/ +`brand-plan.md` 넷을 같이 고쳐야 하므로 사용자 확인 후에 한다. `base/state-epoch-plan.md`/`base/gate-plan.md`/`base/brand-plan.md`가 여전히 정본이다. 발단은 "다중 의존성 `Effect`에서 한 파동에 `fn`이 두 번 도는" 갭 (`base/effect-plan.md`의 다중 deps 절, 2026-08-21 대화에서 에이전트가 제기). @@ -100,24 +102,50 @@ **다르다는 보장이 정확히 그 지점에서 깨진다.** 실제로는 도달 불가능하고 (초당 100만 `Set`으로 285년), 그래서 **문제로 보지 않는다** — 근거만 "오버플로해도 다르다"가 아니라 **"도달 불가능하다"**로 적어둘 것. -4. **[열림] 숫자 리비전 vs 테이블 토큰.** 사용자 대안: *"아니면 안전하게 그냥 +4. **[해소] 숫자 리비전으로 간다** — 사용자 선택(*"Revision 숫자로 가는걸 + 저는 선택하고 싶어요"*). 아래는 판단 재료였던 대조: 사용자 대안: *"아니면 안전하게 그냥 테이블을 비교자로 씁시다. 별로 무겁지 않다고 보여요."* 비교가 identity 동등성이라 **기능적으로는 둘 다 성립한다.** - **테이블**: 포화 문제 자체가 없음. 대신 **`Set` 한 번마다 테이블 하나를 할당**한다 — 트윈처럼 매 프레임 `Set`하는 소스가 여럿이면 GC 압력이 생긴다(quad는 GC-native 아키텍처라 이 축을 신경 써왔다). - **숫자**: 할당 0, 비교도 더 쌈. 위험은 도달 불가능한 `2^53`뿐. - - **에이전트 권고: 숫자(`Revision`)**. 다만 이건 실측 없이도 판단 가능한 - 자리라 사용자 취향으로 정해도 무방. -5. **[열림] State는 `EpochMap`을 둘 컴포지션하는가.** 지금 맵이 둘이므로 - 자연스럽게는 `_valueEpochs`/`_emitEpochs` 두 인스턴스다. 그러면 - `Update`만으론 부족하고 **재계산 후 "읽은 것 전부 최신으로" 동기화하는 - 연산**(가칭 `:Sync`)이 하나 더 필요하다(`base/state-epoch-plan.md` §2의 - 재계산 규칙). -6. **[열림] `Observer` 클로저 인자 추가.** 지금 계약은 *"값을 안 실어주는 - 구독"*이라 인자가 없다. 출처를 넘기는 건 값이 아니라 메타데이터라 취지에는 - 안 어긋나지만, `base/source-state-plan.md`의 그 절과 인자 없는 - `state:Observer()` 유틸까지 같이 손봐야 한다. + - **에이전트 권고도 숫자(`Revision`)** — 트윈처럼 매 프레임 `Set`하는 + 소스가 여럿이면 테이블안은 GC 압력을 만든다. + - **증가 방식 — `bit32` 랩 vs 평이한 `+1`, 아직 안 정함.** 사용자는 + `bit32`가 uint32 안에서 **랩어라운드**한다는 걸 근거로 그 구조를 + 염두에 뒀다(`bit32.bnot(-1) == 0`, `bit32.bnot(0) == 4294967295`). + 맞다 — 랩이면 double 포화(`n + 1 == n`)가 아예 안 생긴다. **다만 충돌 + 거리는 오히려 짧아진다**: 평이한 `+1`은 `2^53`에서 포화하고, `bit32` + 랩은 `2^32`마다 한 바퀴라 "정확히 `2^32`만큼 벌어진 두 리비전"이 같아 + 보인다. 둘 다 도달 불가능이라 **어느 쪽이든 실질 위험은 없고**, 안전 + 마진만 보면 평이한 `+1`이 넓다. +5. **[해소] State는 `EpochMap`을 둘 컴포지션하고, `:Sync`는 필수 연산이 + 아니다 — 에이전트 착오 정정.** 에이전트가 "재계산 후 전부 최신으로 + 맞추려면 `Update` 외의 연산이 필요하다"고 적었으나, **`Update`가 + `Epoch|{Epoch}`를 받으므로 전체 deps를 넘기면 그게 곧 sync다**(사용자: + *"애초에 Update 자체가 전부 최신 상태로 만들고, 업데이트 된게 있으면 + true 를 던지는거라"*). 에이전트가 `Update`를 "하나만 받는 것"으로 좁게 + 본 탓이다. + - **초기화도 같은 연산 하나로 끝난다** — `:With`/`:Compute`의 deps를 + 전부 `Update`하고 **반환값은 안 보고** `rawInvalid = true`로 둔다. + 이미 확정된 노드 생성 규칙(`base/state-epoch-plan.md` §2)과 정확히 + 같은 동작이다. + - **내부 최적화(사용자 제안)**: `Update`가 목록을 돌 때 diff 때문에 + 읽기가 들어가는데, **한 번 다름을 찾으면 반환값이 이미 `true`로 + 확정**되므로 나머지는 읽지 않고 쓰기만 하면 된다. + - **`:Sync`는 순수 최적화로만 둘 수 있다** — 읽기를 아예 건너뛰고 쓰기만 + 하는 변형(반환값 없음). *"없다고 안되는건 아닌데, 그냥 다 안 읽고 set + 만 해버리는것은 처음 셋팅에 도움은 됩니다."* +6. **[방향 확정] `Observer` 클로저 인자는 `:Compute`와 같은 모양** — + `fn(self, from: Epoch|{Epoch})`. 사용자: *"Compute 와 유사하게 나올 수 + 있다 봐요. self 를 넘겨주고, 그 뒤에 epoch|{epoch} 를 주는게 맞아보입니다."* + `base/source-state-plan.md`의 "`:With`/`:Compute` — self 인자도 lazy + 핸들로 통일" 절과 같은 결이고, 값이 아니라 **핸들과 메타데이터**만 + 넘기므로 *"값을 안 실어주는 구독"* 계약도 안 깨진다. + - 반영 시 같이 손볼 것: 그 계약 문단과 인자 없는 `state:Observer()` 유틸, + 그리고 `base/effect-plan.md`의 내부 Observer 등록부(여기서 `Effect`가 + 자기 `EpochMap`을 `Update`한다). 7. **[해소] `Brand` 전환 범위와 마일스톤** — 역조회가 불필요하고 포함 관계 성질도 유지되므로 **인스턴스 브랜드로 전면 전환**이면 된다. **커밋된 M1 코드는 `Brand`를 아직 안 쓴다**(`quad-base/src`는 `init.luau`/`Relate.luau`/