research: Epoch/EpochMap 2차 회신 반영 — :Sync 필수 주장 철회, 남은 미정 하나

- 리비전은 숫자(Revision)로 확정. 증가 방식만 미정 — 사용자는 bit32 랩을
  염두에 뒀고(uint32 랩어라운드라 double 포화가 안 생김) 그건 맞지만, 충돌
  거리는 오히려 짧아진다는 점을 같이 기록했다(평이한 +1은 2^53 포화,
  bit32 랩은 2^32마다 한 바퀴). 둘 다 도달 불가능이라 실질 위험은 없다.
- "재계산 후 전부 최신으로 맞추려면 Update 외의 연산(:Sync)이 필요하다"는
  에이전트 주장 철회 — Update가 Epoch|{Epoch}를 받으므로 전체 deps를 넘기면
  그게 곧 sync다. Update를 "하나만 받는 것"으로 좁게 본 착오. 노드 초기화도
  같은 연산 하나로 끝나고(반환값 무시 + rawInvalid = true), 이미 확정된
  생성 규칙과 정확히 같은 동작이다. :Sync는 읽기를 건너뛰는 순수 최적화
  변형으로만 남는다. 목록 순회 중 한 번 다름을 찾으면 반환값이 확정되므로
  나머지는 읽지 않고 쓰기만 하면 된다는 내부 최적화도 기록.
- Observer 클로저는 :Compute와 같은 모양 fn(self, from: Epoch|{Epoch})로
  방향 확정. 값이 아니라 핸들과 메타데이터만 넘기므로 "값을 안 실어주는
  구독" 계약이 안 깨진다.

base/ 승격은 사용자 확인 대기 — 승격 시 state-epoch-plan/source-state-plan/
effect-plan/brand-plan 넷을 같이 고쳐야 한다. doc-check.py ERROR 0.

Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01TiW21rnti9SbLgF6twtn6D
This commit is contained in:
qwreey 2026-08-21 23:27:15 +09:00
parent 0ebf14405f
commit 782e576218
Signed by: qwreey
GPG key ID: D28DB79297A214BD
2 changed files with 44 additions and 16 deletions

View file

@ -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 설계 시 훅 확장 지점만 고려 | | `debug-tooling-plan.md` | 실물 Instance→코드 위치 역추적 Studio 플러그인(`quad-debug`) — 채널 실현 가능성(BindableEvent/Function 크로스 컨텍스트)까지 실측 검증 완료, 세부 API 이름·구현만 남음 | 하 — 사용자가 "quad 개발 완료 전엔 착수 못 함"으로 직접 후순위 지정, base 설계 시 훅 확장 지점만 고려 |
| `documentation-plan.md` | 문서 사이트 구조(초심자/api/심화/`quadnomicon` 4축, 백엔드별 트랙 분리) + UI 네이밍 컨벤션·Store 부작용 패턴·권장 이벤트 핸들링 3개 세부 문서 뼈대 | 하 — 착수 시점 미정, 구조/스코프만 합의된 상태 | | `documentation-plan.md` | 문서 사이트 구조(초심자/api/심화/`quadnomicon` 4축, 백엔드별 트랙 분리) + UI 네이밍 컨벤션·Store 부작용 패턴·권장 이벤트 핸들링 3개 세부 문서 뼈대 | 하 — 착수 시점 미정, 구조/스코프만 합의된 상태 |
| `documentation-content-map.md` | 위 4축에 실제로 뭘 채울지 `base/` 전체를 초심자/api/심화/skip으로 서베이한 콘텐츠 맵 — 초심자 core loop 목차 초안 포함 | 하 — 문서화 착수 시점의 목차/우선순위표로 쓸 것 | | `documentation-content-map.md` | 위 4축에 실제로 뭘 채울지 `base/` 전체를 초심자/api/심화/skip으로 서베이한 콘텐츠 맵 — 초심자 core loop 목차 초안 포함 | 하 — 문서화 착수 시점의 목차/우선순위표로 쓸 것 |

View file

@ -1,8 +1,10 @@
# `Epoch` 인터페이스 + `EpochMap` 컴포지션, 그리고 `Brand` 인스턴스화 (2026-08-21 신설) # `Epoch` 인터페이스 + `EpochMap` 컴포지션, 그리고 `Brand` 인스턴스화 (2026-08-21 신설)
**상태**: research — **사용자 제안, 에이전트 평가 완료, [2026-08-21] 사용자 **상태**: research — **사용자 제안, 두 차례 회신으로 §4가 사실상 전부 닫혔다**
회신으로 §4의 1·2·3·6·7이 닫힘. 남은 열린 항목은 4·5와 "리비전을 숫자로 둘지 (남은 미정은 리비전 증가를 `bit32` 랩으로 할지 평이한 `+1`로 할지 하나뿐이고,
테이블로 둘지"뿐.** 그래도 `base/`엔 아직 아무것도 안 옮겼다. 그건 어느 쪽이든 실질 위험이 없다). **다만 아직 `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`가 여전히 `base/state-epoch-plan.md`/`base/gate-plan.md`/`base/brand-plan.md`가 여전히
정본이다. 발단은 "다중 의존성 `Effect`에서 한 파동에 `fn`이 두 번 도는" 갭 정본이다. 발단은 "다중 의존성 `Effect`에서 한 파동에 `fn`이 두 번 도는" 갭
(`base/effect-plan.md`의 다중 deps 절, 2026-08-21 대화에서 에이전트가 제기). (`base/effect-plan.md`의 다중 deps 절, 2026-08-21 대화에서 에이전트가 제기).
@ -100,24 +102,50 @@
**다르다는 보장이 정확히 그 지점에서 깨진다.** 실제로는 도달 불가능하고 **다르다는 보장이 정확히 그 지점에서 깨진다.** 실제로는 도달 불가능하고
(초당 100만 `Set`으로 285년), 그래서 **문제로 보지 않는다** — 근거만 (초당 100만 `Set`으로 285년), 그래서 **문제로 보지 않는다** — 근거만
"오버플로해도 다르다"가 아니라 **"도달 불가능하다"**로 적어둘 것. "오버플로해도 다르다"가 아니라 **"도달 불가능하다"**로 적어둘 것.
4. **[열림] 숫자 리비전 vs 테이블 토큰.** 사용자 대안: *"아니면 안전하게 그냥 4. **[해소] 숫자 리비전으로 간다** — 사용자 선택(*"Revision 숫자로 가는걸
저는 선택하고 싶어요"*). 아래는 판단 재료였던 대조: 사용자 대안: *"아니면 안전하게 그냥
테이블을 비교자로 씁시다. 별로 무겁지 않다고 보여요."* 비교가 identity 테이블을 비교자로 씁시다. 별로 무겁지 않다고 보여요."* 비교가 identity
동등성이라 **기능적으로는 둘 다 성립한다.** 동등성이라 **기능적으로는 둘 다 성립한다.**
- **테이블**: 포화 문제 자체가 없음. 대신 **`Set` 한 번마다 테이블 하나를 - **테이블**: 포화 문제 자체가 없음. 대신 **`Set` 한 번마다 테이블 하나를
할당**한다 — 트윈처럼 매 프레임 `Set`하는 소스가 여럿이면 GC 압력이 할당**한다 — 트윈처럼 매 프레임 `Set`하는 소스가 여럿이면 GC 압력이
생긴다(quad는 GC-native 아키텍처라 이 축을 신경 써왔다). 생긴다(quad는 GC-native 아키텍처라 이 축을 신경 써왔다).
- **숫자**: 할당 0, 비교도 더 쌈. 위험은 도달 불가능한 `2^53`뿐. - **숫자**: 할당 0, 비교도 더 쌈. 위험은 도달 불가능한 `2^53`뿐.
- **에이전트 권고: 숫자(`Revision`)**. 다만 이건 실측 없이도 판단 가능한 - **에이전트 권고도 숫자(`Revision`)** — 트윈처럼 매 프레임 `Set`하는
자리라 사용자 취향으로 정해도 무방. 소스가 여럿이면 테이블안은 GC 압력을 만든다.
5. **[열림] State는 `EpochMap`을 둘 컴포지션하는가.** 지금 맵이 둘이므로 - **증가 방식 — `bit32` 랩 vs 평이한 `+1`, 아직 안 정함.** 사용자는
자연스럽게는 `_valueEpochs`/`_emitEpochs` 두 인스턴스다. 그러면 `bit32`가 uint32 안에서 **랩어라운드**한다는 걸 근거로 그 구조를
`Update`만으론 부족하고 **재계산 후 "읽은 것 전부 최신으로" 동기화하는 염두에 뒀다(`bit32.bnot(-1) == 0`, `bit32.bnot(0) == 4294967295`).
연산**(가칭 `:Sync`)이 하나 더 필요하다(`base/state-epoch-plan.md` §2의 맞다 — 랩이면 double 포화(`n + 1 == n`)가 아예 안 생긴다. **다만 충돌
재계산 규칙). 거리는 오히려 짧아진다**: 평이한 `+1``2^53`에서 포화하고, `bit32`
6. **[열림] `Observer` 클로저 인자 추가.** 지금 계약은 *"값을 안 실어주는 랩은 `2^32`마다 한 바퀴라 "정확히 `2^32`만큼 벌어진 두 리비전"이 같아
구독"*이라 인자가 없다. 출처를 넘기는 건 값이 아니라 메타데이터라 취지에는 보인다. 둘 다 도달 불가능이라 **어느 쪽이든 실질 위험은 없고**, 안전
안 어긋나지만, `base/source-state-plan.md`의 그 절과 인자 없는 마진만 보면 평이한 `+1`이 넓다.
`state:Observer()` 유틸까지 같이 손봐야 한다. 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` 전환 범위와 마일스톤** — 역조회가 불필요하고 포함 관계 7. **[해소] `Brand` 전환 범위와 마일스톤** — 역조회가 불필요하고 포함 관계
성질도 유지되므로 **인스턴스 브랜드로 전면 전환**이면 된다. **커밋된 M1 성질도 유지되므로 **인스턴스 브랜드로 전면 전환**이면 된다. **커밋된 M1
코드는 `Brand`를 아직 안 쓴다**(`quad-base/src`는 `init.luau`/`Relate.luau`/ 코드는 `Brand`를 아직 안 쓴다**(`quad-base/src`는 `init.luau`/`Relate.luau`/