design: Epoch/EpochMap/Brand 전면 승격 + 해소 기록 flatten
앞 세션이 컨텍스트 피로로 미뤄둔 승격(todos 000번)을 수행하고, 코퍼스에
쌓여 있던 [해소]/[정정] 층을 걷어냈다. 감사 3라운드 + /code-review high로
15건을 잡아 전부 반영했다.
승격 — base/ 넷 + 파급 넷
- state-epoch-plan.md 재작성: Epoch 인터페이스({Revision:number}, 그 자체로
키가 되는 unique 테이블, Source가 구조적으로 만족)와 EpochMap(Update/
Refresh/Sync/TrackFrom) 신설. State는 EpochMap을 둘 컴포지션 —
sourceCountMap/sourceEmitMap -> valueEpochMap/emitEpochMap. §1~§8로 재편.
- brand-plan.md 전면 재작성: 공유 레지스트리 + Brand.get(객체당 태그 하나)
-> 인스턴스 브랜드 Brand() + :register/:is, 다중 태깅 허용. 발단은 Source가
SourceBrand이면서 동시에 EpochBrand여야 하는데 옛 모양으로 표현 불가.
역조회는 제거(전수 조사에서 쓰는 자리 0). weak-key/테이블 아이덴티티/
duck-typing 기각 근거/predicate 합성은 전부 유지.
- source-state-plan.md: Source가 Epoch도 구조적으로 만족(Revision은 공개여야
타입 레벨에서 성립), Observer 클로저가 fn(self, from: (Epoch|EpochSet)?).
- ⭐ effect-plan.md: 다중 의존성 중복 발화 미해결 항목이 닫힘 —
EffectHandle이 자기 EpochMap을 들어 첫 번째만 통과시킨다.
- gate-plan.md/architecture.md/bind-system-plan.md/ROADMAP.md 어휘 통일,
EpochMap.luau가 M2에도 필요하다는 것 반영(GateNode가 씀).
리비전 갱신은 bit32.bnot(-rev) — 사용자 확정
- a>0이면 a-1, 0이면 4294967295인 랩어라운드 감소. 갱신과 랩이 FASTCALL
하나로 끝난다(luau 실측). 근거: 2^53 포화는 도달 불가능한데 그걸 피하려고
값을 double 영역까지 키울 이유가 없다, 매번 도는 hot path다.
- 에이전트가 이걸 band(rev+1, mask)로 잘못 옮기고 "그러니 bit32가 더 싼 건
아니다"라는 틀린 단서까지 달았다가 사용자 정정("제가 말한건 bit32.bnot(-a)
입니다"). 세 문서에 정정 경위를 남겼다.
- 따름정리: 리비전은 증가가 아니라 감소한다. ==/~= 만 쓰는 지금 규칙에서만
무해하다는 경고를 §2에 명시.
archive / flatten
- archive/brand-shared-registry-reversed.md 신설(옛 Brand 표면 원문).
- question.md 421->208줄: 해소 항목 18건을 archive/question-resolved.md로
이관. 그 문서가 스스로 정한 규칙("해소하면 여기서 지우고 archive로")을
다시 어기고 있었다. 이관분의 옛 필드명은 소급 수정하지 않고 머리에 경고만.
- todos.md 000번 삭제, "M3 착수 전 필요" 목록에서 해소 항목 일곱 제거
(실제로 열린 건 중간 State GC와 store:GetDynamic 둘뿐).
- research/ -> reference/ 이동 둘(epoch-brand-composition,
slot-attach-decomposition). 확정된 결정의 근거 기록은 research(상의 필요)도
archive(뒤집힘)도 아니므로, reference의 폴더 기준에 그 용도를 명문화했다.
감사 3라운드(각도: base 정합성 / 인덱스+luau-test / archive+qa-request)
- §8에 "기각된 대안 — 게이트를 에포크 경계로" 논거 신설(재작성 때 떨어뜨렸고,
두 문서가 서로 다른 없는 §번호를 대고 있었다).
- luau-test 스파이크 22를 done/ -> rewrite-required/(옛 Brand.set/get을 직접
구현). STATUS.md 개수와 절 제목의 하드코딩 개수 정리.
- state-epoch/source-state가 "Revision을 증가시킨다"고 적어놓고 20줄 뒤에
"감소한다"로 반박하던 자기모순 정정.
커밋 전 /code-review high — 9건, 전부 유효(감사자가 못 보는 축)
- ⭐ {Epoch}는 Luau에서 배열인데 실제 게이트 배치는 {[Epoch]:true} 집합.
그대로 ipairs로 구현하면 유보됐다 풀린 emit이 전부 삼켜진다(gate-plan 4번이
애초에 고치려던 그 버그) -> EpochSet으로 확정.
- ⭐ 새 노드 시딩이 확정된 EpochMap 표면으로 표현 불가능했다(:With의 상류는
State이지 Epoch가 아니고, 키 열거/병합 연산이 없었음) -> :TrackFrom 신설.
- GateNode 예외(emitEpochMap을 전파 시점에 갱신)가 §4에 미기록.
- 설치 발화엔 from이 없다 -> 옵셔널로, Effect의 억제 플래그가 Update보다
먼저여야 함을 명시.
- 2^32 랩을 "똑같이 도달 불가능"이라 한 근거가 틀렸다(같은 척도로 285년 vs
72분). 실제 안전 근거는 충돌 조건이 한 점이라는 것으로 정정.
- 그 외 :Sync 용도 충돌, isEpoch 누락, TweenTag 3곳, Effect(fn,state?) 4곳,
§ 참조 3곳.
에이전트가 이름 붙인 연산 둘은 사용자 검토로 확정
- :Refresh 유지 — "Update는 받은 것을 처리, Refresh는 내가 받았던 걸 처리라
표면적 의미 자체가 다르다"(오버로드로 합치지 않음).
- :Absorb -> :TrackFrom 개명 — absorb는 상위에서 제거할 것처럼 읽히고,
gate-plan이 이미 "흡수 집합"을 다른 뜻으로 쓴다. TrackFrom은 이 맵의 존재
이유("내가 뭘 추적하고 있나")를 그대로 쓰고 From이 비파괴를 못박는다.
Epoch/EpochMap/Brand에 열린 설계 항목 없음. 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:
parent
168d3d8dcc
commit
0498816c10
30 changed files with 1630 additions and 785 deletions
|
|
@ -22,7 +22,7 @@
|
|||
| 폴더 | 기준 |
|
||||
|---|---|
|
||||
| `base/` | 결정 완료 + 프로젝트 전체에 걸치는 컨텍스트 — plan/done 개념 없음, 계속 참조되는 배경지식. **항상 읽어야 하는** 배경지식만 여기 둠(다른 문서를 이해하는 데 전제되는 것) |
|
||||
| `reference/` | **[2026-08-07 신설]** 결정 자체가 아니라 다른 문서가 근거로 인용하는 온디맨드 참고 자료(v1 스냅샷, 프레임워크 비교 리서치) — "완료" 개념 없는 건 `base/`와 같지만, 항상 읽을 필요는 없고 해당 문서가 인용될 때만 열어보면 됨. `quadnomicon` 소재 후보가 많음 |
|
||||
| `reference/` | **[2026-08-07 신설]** 결정 자체가 아니라 다른 문서가 근거로 인용하는 온디맨드 참고 자료(v1 스냅샷, 프레임워크 비교 리서치) — "완료" 개념 없는 건 `base/`와 같지만, 항상 읽을 필요는 없고 해당 문서가 인용될 때만 열어보면 됨. `quadnomicon` 소재 후보가 많음. **[2026-08-21 확장] 확정된 결정의 "왜 그렇게 정했나" 근거 기록도 여기 둔다** — `research/`(아직 상의 필요)도 `archive/`(뒤집혔거나 기각됨)도 아니고, `base/`가 근거로 인용하는 온디맨드 자료라는 이 폴더의 기준에 정확히 맞기 때문(`slot-attach-decomposition.md`/`epoch-brand-composition.md`가 그렇게 들어옴) |
|
||||
| `research/` | 아직 착수 전, 사용자와 스코프/설계를 더 상의해야 함 |
|
||||
| `qa-request/` | 원래 용도는 "구현 완료(코드/에이전트 검증까지 끝남) + 사용자 본인의 실기기(Roblox Studio) QA만 남음". **[2026-08-18 확장]** 구현 전에도 **사용자 심사 라운드의 산출물**을 여기 둠 — `pre-implementation-qa-round1.md`(1라운드: `base/` 확정 문서 전체를 문항으로 재확인받아 **"아니오"가 나온 항목만** 모은 결함 목록 + 신규 요구사항(`N-n`) + 부수 오탈자. **같은 날 전부 `base/`에 반영 완료**라 지금은 "무엇이 왜 틀렸었나"의 근거 기록이고, 지금 유효한 설계는 항상 `base/`가 소스. 아직 안 닫힌 것은 `question.md` 3번과 `.claude/todos.md` 00번이 소스), `pre-implementation-qa-round2.md`(2라운드: 확정 의사코드를 실제로 손으로 실행해보는 트레이싱 — **완료**, 발견된 크래시 `RC-1`(`recompute` 트리거 모델)도 같은 날 후속 세션에서 Blocker 게이팅 설계로 해결·반영까지 끝남, `archive/question-resolved.md`에 논의 요지 정리), `pre-implementation-qa-round3.md`(3라운드: `RC-1` 해법(Blocker 게이팅)이 실제로 `attachSlot`/`recompute`에 반영된 걸 손으로 트레이싱 — **완료**, `RC-3`/`RC-4`(`activateList`가 자기 Slot의 Blocker보다 먼저 실행되는 순서 문제)와 `bk.N` 수명주기 미정을 발견했다가 같은 세션에 사용자가 최초 분석 오류를 직접 정정하며 전부 해결·`base/` 반영까지 끝남, `archive/question-resolved.md`에 논의 요지 정리. `ROADMAP.md` M2가 M3의 `Blocker.luau`에 의존하게 된 마일스톤 순서 불일치는 각주로 반영, 마일스톤 재편 여부는 열려 있음). `pre-implementation-qa-round4.md`(4라운드: 사용자 요청으로 **`base/` 확정 전체를 "예가 나와야 정상인 문항"으로 다시 뽑은 전수 문항지** — **[2026-08-21] 완료·종결**. 1~3라운드와 달리 이 파일은 **문항지 원본 그대로 남긴다** — 처리 결과 전량이 `-followup.md`에 쌓였으므로 "아니오만 남기는 재편"은 안 하기로 함(같은 정보가 두 곳에 갈라지는 걸 피함). 문항 수/문서별 분포는 그 파일 자신이 소스). `pre-implementation-qa-round4-response.md`(사용자 회신 원문 — 이 라운드는 회신을 **별도 파일**로 받았다, 1~3라운드와 다른 점), `pre-implementation-qa-round4-followup.md`(**[2026-08-20 시작, 2026-08-21 종결]** 그 회신 처리 결과 — A~H 8개 절이 4차에 걸쳐 시간순으로 쌓였고 **마지막 H절이 최신이자 소스**. B·C절의 재질문/판단 대기 항목은 F·G절을 거쳐 **H절에서 전량 닫혔다**(`Detach` 보존 주체, `KeyGone`, `Owned`, `attachSlot` 분해). **열린 질문 없음**. **[2026-08-21 정정]** 여기 적혀 있던 "5라운드 문항지는 만들지 않는다"는 뒤집혔다 — 같은 날 사용자 요청으로 5라운드를 만들었다), `pre-implementation-qa-round5.md`(**[2026-08-21 신설·처리 완료]** 5라운드: 4라운드에서 "예"로 넘어간 자리는 건너뛰고 **(1) 4라운드에 문항이 아예 없던 영역**(`project-setup-plan.md`/`quad-types-plan.md`, 그리고 **문서가 아니라 실제 커밋된 M1 코드**), **(2) 4라운드 회신 이후 새로 확정된 것**(`Detach`/`_detached`/`KeyGone`/`Owned`/`attachSlot` 분해 등), **(3) 큰 문서의 심화**(예: `debounce-throttle-plan.md`)만 묻는다. 문항 수는 그 문서 자신이 소스), `pre-implementation-qa-round5-response.md`(사용자 회신 원문 — 4라운드와 같이 별도 파일), `pre-implementation-qa-round5-followup.md`(**[2026-08-21]** 그 회신 처리 결과 — 즉시 반영분 / 재질문 / 사용자 판단 필요 / 새로 만든 문서 둘(`gate-plan.md`·`state-epoch-plan.md` — 같은 날 확정되며 `base/`로 승격)까지. **처리 결과의 소스는 이 파일**). 다음 라운드가 필요해지면 라운드마다 파일을 새로 만들고 이름에 라운드 번호를 넣을 것 |
|
||||
| `archive/` | 완료 + 사용자가 실사용/실기기로 직접 검증까지 마침 (구현 대상). **[2026-08-06 확장]** 완전히 뒤집힌 설계 결정을 원문+역전 이유+diff와 함께 보존하는 용도로도 사용(제목 `[역전됨]` — 한 번 확정했다가 뒤집힌 것) — 더 이상 능동적으로 참고 안 해도 되지만(토큰 낭비 방지 위해 `base/`/`research/`에서 뺌) `quadnomicon` 소재로는 나중에 쓸 수 있음. **[2026-08-07 확장]** 후보였다가 채택 안 된 것(확정한 적 없이 검토 후 기각)도 같은 방식으로 보존, 제목은 구분을 위해 `[기각됨]` — `[역전됨]`과 의미가 다르므로 혼동하지 말 것. **[2026-08-07 세 번째 확장]** 설계 반전/기각과 별개로, 에이전트가 문서 작성 중 스스로 낸 개념 혼동을 정정한 이력은 `[에이전트 실수]` 태그로 `agent-mistake.md` 하나에 모음(`.claude/session-summary.md`/`session/` 로그와의 중복 방지) |
|
||||
|
|
@ -59,7 +59,7 @@
|
|||
| `component-composition-plan.md` | 컴포넌트=플레인 함수, State/Source 읽기·쓰기 경계, Source가 State를 구조적으로 만족 — modifier/Ref 컴포넌트 경계 통과까지 전부 확정, 남은 건 API 이름뿐. **[2026-08-07 정리]** 폐기된 `StoreSource` 프록시 설계로의 역전 이력은 본문에서 빼고 `archive/store-source-proxy-reversed.md` 포인터로 압축 |
|
||||
| `blocker-plan.md` | **[2026-08-07 신설]** `Blocker` — 여러 Source를 한꺼번에 바꿔도 파생값 재계산이 한 번만 되게, State 마일스톤(M3)과 함께 개발. 메커니즘+이름 확정. **[2026-08-18 구현 전 QA 2라운드 후속]** `IsOn()`/`OffWithoutEmit()` 신설(`RC-1` 해결 과정에서 나옴) — `state:Block()` 없이 Blocker를 직접 쓰는 두 번째 용례(base 내부 Length/Offset 배치 게이팅)도 추가. **[2026-08-18 구현 전 QA 3라운드]** 이 용례의 존재 이유 정정 — `RC-1`의 원래 크래시는 사라졌고(`bk.N` 수명주기 재정의로), 지금 필요한 이유는 배치 등록 비용(O(N²)→O(N)) |
|
||||
| `debounce-throttle-plan.md` | **[2026-08-14 신설, 2026-08-19 전부 해소돼 `research/`에서 승격]** 시간 기반 전파 게이트 `Debounce`/`Throttle` — 사용자 요청("`Blocker`와 유사하게")으로 신설. 요지: (1) `Blocker`가 이미 쓰는 게이트 노드의 **릴리스 트리거만 타이머로 바꾼 것**이라 새 전파 메커니즘이 아님, (2) 무효화 채널만 만지므로 laziness 안 깨짐, (3) **Debounce/Throttle의 차이는 "신호가 창 타이머를 리셋하는가" 한 비트뿐** — 공개 생성자는 둘, 구현은 하나, (4) 알고리즘은 quad-base + 주입 op 2개 `setTimeout(func, delay) -> Timeout`/`clearTimeout`(Roblox `task.delay`/`task.cancel`로 배선 — **인자 순서 반대라 주의**), `Timeout`은 `{ __type_timeout: true, _native: any }`. **[2026-08-19 마지막 라운드]** 의미론은 **(A) emit-gate**(`Blocker`와 동일, `:Get()`은 항상 최신값)로 확정 — 검토했던 값-지연 안은 laziness와 상충해 철회. 제어 핸들은 개별은 `Ref` 아웃파라미터·전체는 팩토리 자체의 `:Flush()`/`:Cancel()`(weak 레지스트리)로 확정, `Time`/`MaxTime`은 `number \| State<number>`(스케줄 시점에만 폴링) 허용. 이름은 `Debounce`/`Throttle` 유지 + Roblox 관용 "debounce"와 다르다는 문서 경고. **결과적으로 quad-base에 새 코어 메커니즘을 안 더하는 순수 슈가로 귀결**(`Blocker`의 gated state + `Ref` + 주입 op 2개 위에 전부 얹힘) — 우선순위는 `Operator.*`와 같은 급으로 재평가됨. **부수 성과**: 이 설계 중 `source-state-plan.md`의 무효화 dedup 서술이 `Observer` 계약과 모순되는 게 발견돼 base 전면 정정(`archive/invalidate-dedup-propagation-reversed.md`) |
|
||||
| `effect-plan.md` | **[2026-08-07 신설, 여섯 번째 세션에 확정]** `Effect(fn, state?)` — `state` 없으면 설치 1회+leaf 사망 시 확정 정리, 있으면 내부적으로 `state:Observer(...)`를 조합해 재실행+cleanup 체이닝(React `useEffect` 동형). Observer와의 관계 해소 완료. **[2026-08-18 구현 전 QA 반영]** **`:Unsubscribe()`는 `:Subscribe()`의 짝으로 축소** — leaf 바인딩 경로에서 cleanup을 앞당기면 dedup 때문에 재바인딩이 안 일어나 Effect가 조용히 죽음(그 dedup 경로의 process/retract 대칭은 **미확인**, M3 착수 전 확인) |
|
||||
| `effect-plan.md` | **[2026-08-07 신설, 여섯 번째 세션에 확정]** `Effect(fn, ...deps)` — deps 없으면 설치 1회+leaf 사망 시 확정 정리, 있으면 내부적으로 `state:Observer(...)`를 조합해 재실행+cleanup 체이닝(React `useEffect` 동형). Observer와의 관계 해소 완료. **[2026-08-18 구현 전 QA 반영]** **`:Unsubscribe()`는 `:Subscribe()`의 짝으로 축소** — leaf 바인딩 경로에서 cleanup을 앞당기면 dedup 때문에 재바인딩이 안 일어나 Effect가 조용히 죽음. **[2026-08-20 `E-10` → 2026-08-21 `EF-3`에서 반영]** 그 dedup 경로의 process/retract 대칭은 **성립함이 확인됨**(핸들러가 `old`를 `Relate`로 직접 들고 양쪽이 같은 비교식을 씀 — 남은 건 구현 시 회귀 확인뿐, 설계상 열린 항목 아님). **[2026-08-21 5라운드 `C-6`]** 시그니처가 `Effect(fn, ...deps)`로 확장돼 의존성을 여러 개 직접 받고(`Ref`도 가능) 각각에 구독을 건다. **[2026-08-21]** 다중 의존성이 공통 상류를 공유할 때 한 파동에 `fn`이 여러 번 돌던 미해결 갭은 **`EffectHandle`이 자기 `EpochMap`을 들어** 닫힘(`base/state-epoch-plan.md`) |
|
||||
| `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` 참고. `v=nil`이면 `process` 자신이 만든 자식 제거(`retract` 아님). **[2026-08-14 세션] Tween 지원 추가** — 자식 프로퍼티를 직접 대입하지 않고 `Dispatch.process(child, prop, ..., 1)`로 위임하는 것으로 확정(프로세스 중 `inst`를 바꾸는 건 키를 바꾸는 것과 같은 층위라 UB 아님, `dispatch-core-plan.md`에 일반 규칙으로 명문화) — Tween 해석 코드가 `PropertyHandler` 하나에만 남는다는 불변식이 유지되고, 이 문서가 새로 정할 건 스칼라→프로퍼티 `wrap`을 `Tween<T>.Value`에만 적용되도록 들어올리는 헬퍼 하나뿐. 옛 "트윈까지 지원할 필요 없음" 서술은 역전됨(그때는 Tween이 독립 Dispatch 핸들러였음). ROADMAP M10에 빠져 있던 체크리스트 항목도 이 세션에 보강. **[2026-08-18 구현 전 QA 반영]** 만든 자식을 다시 찾을 때 **`FindFirstChild` 대신 `Relate` 저장**(이름은 표시·판정용, 릴레이션은 조회용), 자식 프로퍼티 세팅도 `Dispatch.process`로 위임해 Tween이 공짜로 따라오게 |
|
||||
| `tag-plan.md` | **[2026-08-08 세 번째 세션 재설계, 2026-08-12 열한 번째 세션 메커니즘 정정]** `Tag(...)` — array-part 값 객체, `Modifier`와 같은 immutable clone 체이닝(`:Added`/`:Removed`/`:Contains`/`:Apply`/`Merged`), `CollectionService` 글루만 quad-roblox. `retract`가 이전 Tag가 걸었던 이름을 이름별 참조 카운트 맵에서 빼고(다른 위치가 겹쳐 쓰면 실제 `RemoveTag`는 skip), `process`가 새 Tag의 이름을 등록 — 여러 위치가 같은 이름을 겹쳐 가져도(웹 `className`류 합집합) 안전. 구 해시 파트 boolean 모델은 `archive/tag-hash-key-model-reversed.md`, 구 `assert(v==nil)` 메커니즘은 `archive/retract-always-fires-reversed.md`. **[2026-08-12 열다섯 번째 세션]** `Added`/`Removed`가 vararg가 아니라 `string | {string}`으로 정정 — `table.unpack`이 인자 목록 tail 위치에서만 완전히 펼쳐지는 Lua 문법 제약 때문에 여러 개의 독립된 동적 이름 테이블을 한 vararg 호출로 못 합치는 경우가 생김이 발견됨, `Tag(...)` 생성자 자체는 정적 리터럴 호출이라 vararg 유지. **[2026-08-13 세션]** 참조 카운트 `holders`가 Tag 객체 identity로 키잉돼 있어서 같은 Tag 객체를 여러 위치에서 재사용하면(immutable이라 흔한 관례) 한 위치만 retract돼도 다른 위치가 쓰는 태그가 지워지는 실제 버그 발견·수정 — holders를 위치(`k`) 기준으로 재키잉, `oldv==newv`면 retract 스킵하는 최적화도 추가. **[2026-08-13 세션, 다섯 번째, 전면 반영]** `TagHandler.process`가 자기 retract 클로저를 반환하는 계약으로 전환되며 `kTagMap`(위치별 마지막 Tag)이 완전히 불필요해짐(클로저가 `v`를 직접 캡처) — `tagNameMap`(이름별 위치 집합)만 남음, `base/dispatch-core-plan.md` "Dispatch 체인" 절 참고 **[2026-08-13 열네 번째 세션]** 하강 diff 반영(`isTag(hintValue)` 방어 가드 폐지 — 클로저 인자의 타입이 계약으로 보장됨, 깜빡임 방지가 깊은 체인에서도 유지) + **패키지 재배치**(참조 카운트 Handler까지 quad-base, 백엔드는 `addTag`/`removeTag(inst, {string})`만 주입 — 웹 `className` 대응 때문에, vararg 아닌 테이블인 이유는 `Tag:Added`와 동일) |
|
||||
| `attribute-plan.md` | **[2026-08-07 여덟 번째 세션 신설]** 단일 키 `[AttributeKey<T> "Name"]`(구 `Attribute<T>`) — `SetAttribute(name, nil)`이 네이티브 지우기라 `None` 센티널과 가장 깔끔하게 맞아떨어짐. **[2026-08-11 아홉 번째 세션]** 여러 Store를 한 번에 attribute로 묶는 그룹 `Attribute(...)` 프리미티브 신설(`Tag`와 동형 array-part 값 객체, `Merged`로 헤테로지니어스 Store 합성), 이름 충돌 방지로 단일 키를 `AttributeKey`로 리네임(잠정). **[같은 세션 후속]** `AttributeKey(name)`이 이름별 weak 캐시로 동등성 보장하도록 확정되며, 그룹 Handler는 자기 완결형 재구현 대신 메모이즈된 키로 기존 단일 키 경로에 재귀 위임하는 걸로 개정(중복 구현 제거). **[2026-08-12 열 번째 세션]** 그룹/직접 쓰기가 같은 이름을 동시에 관리하는 충돌을 막기 위해 그룹은 공개 캐시 대신 `rawNew(name)` 전용 키+소유권 `Relate`로 전환. **[열한 번째 세션]** `retract`가 store 재발행마다 항상 불린다는 정정에 맞춰 `AttributeKeyHandler.retract`를 손봄(이 시점엔 `v==nil` 가드 버전 — 아래 열여섯 번째 세션에서 최종 재정정됨), 그룹의 "남아있는 이름" 위임도 매번 `retractUnder`를 먼저 부르도록 정정(체인 누수 방지). **[2026-08-12 열여섯 번째 세션, 최종 재정정]** `retract`는 완전 no-op으로 굳어짐(`SetAttribute`는 오직 `process(inst,k,nil)`에서만) — Attribute는 명시적 `None`/`nil`로만 지워지고, 그룹 diff나 컴포넌트 언마운트로 이름이 조용히 사라져도 값은 자동으로 안 지워짐(`Ref`의 "Destroy 무관, 정리는 명시적으로" 철학과 통일), 단 사라진 이름의 *구독*은 끊어 자원 누수는 막음 — 위 "v==nil 가드" 버전은 이걸로 폐기. **[2026-08-13 세션, 전면 재정정]** `rawNew`+`owners` 수동 레지스트리 방식이 "그룹이 이름을 놓았다 다시 포함하면 자기 자신과 충돌"하는 실제 버그로 확인됨 — `AttributeGroupKeyHandler`라는 `isHandlable` 없는 순수 체크포인트 핸들러를 `Dispatch.processAs`로 명시 push하고 `Dispatch.retractSelfAndUnder`로 통째 철거하는 방식으로 전면 재설계, 소유권 충돌 감지도 별도 레지스트리 없이 기존 재진입 가드가 대신 잡아줌(`bind-system-plan.md` 참고). `AttributeKeyHandler`는 다시 완전 무상태로 단순화됨. **[2026-08-13 세션, 다섯 번째, 전면 재설계 — 체크포인트조차 불필요해짐]** `Dispatch`가 인덱스 기반으로 재설계되며 `AttributeGroupKeyHandler`/`processAs`/`retractSelfAndUnder`를 전부 걷어냄 — 그룹이 그냥 공개 `AttributeKey(name)`으로 항상 인덱스 1부터 `Dispatch.process`/`retractFrom`을 직접 부르면 끝(점유 체크 자체가 소유권 충돌 감지), `groupState` Relate도 필요 없어짐(반환 클로저가 이름 집합을 직접 캡처) — 중간 버전은 `archive/checkpoint-handler-pattern-reversed.md`. **[2026-08-13 감사, 정정]** 그런데 그 의사코드가 `process` 안에서 이름마다 `retractFrom(...,1,...)`을 먼저 부르고 있어 **인덱스 1이 무조건 비워지는 바람에 점유 체크가 전혀 작동하지 않았음**(그룹↔그룹 사이에서 조용한 last-write-wins가 그대로 남아 있었음) — `process`는 `Dispatch.process`만 부르고 철거는 반환 클로저가 자기가 등록한 이름 전부에 대해 하도록 정정. 그룹 Handler 시그니처가 계약과 안 맞던 것(`process(inst,index,v)` 3-인자)도 같이 수정 **[2026-08-13 열네 번째 세션, 0-Z 확정]** 그룹이 **자기 전용 키**(비공개 `GetKey`)로 위임하고 이름 소유권은 `AttributeKeyHandler`의 **이름 claim**(`nameClaims` Relate, 충돌 시 즉시 error)이 판정 — 하강 diff에선 두 그룹이 똑같이 `StoreBind`로 보여 점유 체크가 성립하지 않기 때문. 후보 (a)(그룹 안 claimant Relate)는 **그룹↔직접 쓰기를 못 잡아** 기각. 같은 세션에 **패키지 재배치**(값·알고리즘·단일 키 전부 quad-base, 백엔드는 `setAttribute(inst,name,v)`만 주입, 엔진 고유 타입 패밀리만 백엔드). **[2026-08-18 구현 전 QA 반영]** **`Attribute.Merged`(겹치면 error) / `Attribute.Overridden`(뒤가 이김)을 둘 다 제공**으로 열린 항목 해소, 그리고 **⚠️ 같은 그룹 객체를 두 위치에 놓는 경우를 잡을 위치별 claim이 필요**하다는 미해결 항목 신설(`Ref`처럼 `bindLifetime` 재사용은 불가) |
|
||||
|
|
@ -67,12 +67,12 @@
|
|||
| `relate-plan.md` | **[2026-08-08 신설]** `Relate` — `inst`를 weak 키로 하는 범용 릴레이션 프리미티브(`SetWeak`/`GetWeak`/`SetStrong`/`GetStrong`, 비싱글톤 생성자). 구 `base.perInstanceState(inst)` placeholder를 대체·정식 승격, `lifecycle-pattern.md`의 `bindLifetime`/`canExecute`가 그 위에 얹힘. **[2026-08-12 열세/열네 번째 세션]** 서로 다른 두 `Relate`가 서로의 키를 상대방 값으로 강하게 붙잡는 상호 순환 패턴 경고 신설 — Luau에 ephemeron 테이블이 없어(공식 확인, luau.org/compatibility) 이런 순환은 실제로 GC가 안 됨, `Slot`의 `kSlotMap`/`slotOwner`가 실제 사례이자 수정 사례 |
|
||||
| `ref-plan.md` | **[2026-08-13 아홉 번째 세션, `bind-system-plan.md`에서 분리]** `Ref`/`PreRef`/`PostRef` — 지연 없는 확정 값 박스. 용도 재정의(leaf 노드를 담는 용도로도, leaf 노드에 바인딩하는 용도로도), `.Value`+`:Set`/`:Callback`/`:Wait`(전부 self 반환), `Ref`의 retract가 `TagHandler`와 같은 `Relate` diff 패턴이라는 것, 이중 바인딩 금지(`canBound`), `PreRef` 호이스팅 pre-pass와 1회용 `_fired` 가드. **분리는 순수 이동 — 결정은 하나도 안 바뀜** **[2026-08-13 열네 번째 세션]** 하강 diff 반영(0-Z 배너 해소) — "이전 클로저가 언바인딩, 다음 `process`가 바인딩" 두 단계는 그대로이고 그걸 일으키는 주체만 `Dispatch.process`의 핸들러 선비교로 바뀜 **[2026-08-14 아홉 번째 세션]** `PostRef` 확정·편입 — `PreRef`의 거울상(같은 pre-pass가 수집만 하고 두 패스가 **전부 끝난 뒤** fire, `ProcessedPostRef` 센티널+전담 Handler까지 완전 대칭). 보장 범위는 "자기 서브트리 완성"이고 **자기가 부모에 붙는 것보다는 여전히 먼저**임에 주의. 계열 안 fire 순서는 **배열 index 순서 보장 유지**(같은 세션에 미보장으로 뒤집었다 철회 — `archive/preref-order-unguaranteed-withdrawn.md`). **[2026-08-18 구현 전 QA 반영]** 내부 구조가 **별도 `.Callbacks` 테이블 + 평범한 `.Value` 필드**로 단순화(`__index` 우회 폐기), `RefLeafHandler.isHandlable`에 빠져 있던 `type(k)=="number"` 추가(leaf 바인딩은 **배열 전용**), "배열 파트의 `None`은 process를 안 탄다"는 옛 명확화 전면 정정 |
|
||||
| `event-plan.md` | **[2026-08-13 아홉 번째 세션, `bind-system-plan.md`에서 분리 — 사용자가 직접 지목]** 이벤트 바인딩 — 핸들러가 self(Instance)를 **안** 받는다는 확정(Ref가 이미 커버, 이중 쓰기 경로 방지), 이벤트도 store-bind 가능하며 **[2026-08-18 정정] `None`/`nil`을 넣으면 disconnect**(옛 `false` 센티널은 `None` 도입 전의 선택이라 폐기 — `EventHandler.isHandlable`이 `v == nil`에도 매치돼야 함). 이벤트 *네이밍* 관례는 인스턴스 생성과 한 절에 섞여 있어 `bind-system-plan.md`에 남음, `GetPropertyChangedSignal`은 `onchange-plan.md`. **분리는 순수 이동** |
|
||||
| `brand-plan.md` | **[2026-08-13 아홉 번째 세션, `bind-system-plan.md`에서 분리]** `Brand` — 런타임 nominal 타입 판별 통합 메커니즘(`Brand.set`/`Brand.get`), `isState`를 branded 타입 전부로 일반화(`isPostRef` 포함, 2026-08-14 아홉 번째 세션). 동작/구현은 확정, **이름 `Brand` 자체만 용어 정리 대기**(`question.md` 1번). **[2026-08-18 구현 전 QA 반영]** **`Brand`는 아무 의존성도 갖지 않는다** — `Brand.get`이 `x == None`을 먼저 보는 특수 분기 안은 기각(`isNone`은 그냥 `v == None`, `None`을 평범하게 태깅하는 건 무방). **분리는 순수 이동** |
|
||||
| `brand-plan.md` | **[2026-08-13 아홉 번째 세션, `bind-system-plan.md`에서 분리]** `Brand` — 런타임 nominal 타입 판별 통합 메커니즘, `isState`를 branded 타입 전부로 일반화(`isPostRef` 포함, 2026-08-14 아홉 번째 세션). **[2026-08-21 전면 재작성]** 공유 레지스트리 + `Brand.get(x) -> tag`(객체당 태그 하나)에서 **인스턴스 브랜드**(`Brand()` + `:register`/`:is`, **다중 태깅 허용**)로 바뀜 — 발단은 `Source`가 `SourceBrand`이면서 동시에 `EpochBrand`여야 하는데 옛 모양으로는 표현이 안 되던 것. 역조회는 없어졌고(멤버십 질문만 씀), weak-key·테이블 아이덴티티·duck-typing 기각 근거·포함 관계 predicate 합성은 전부 유지. 역전 원문은 `archive/brand-shared-registry-reversed.md`, 근거 기록은 `reference/epoch-brand-composition.md`. 동작/구현은 확정, **이름 `Brand` 자체만 용어 정리 대기**(`question.md` 1번). **[2026-08-18 구현 전 QA 반영]** **`Brand`는 아무 의존성도 갖지 않는다** — `None` 특수 분기 안은 기각(`isNone`은 그냥 `v == None`) |
|
||||
| `tween-plan.md` | **[2026-08-12 세션, `research/`에서 승격]** 값-레벨 `Tween<T>` 래퍼(PropertyHandler가 소비, 구 특수 bind key 모델은 `archive/tween-special-bind-key-reversed.md`). 3-상태 릴레이션 슬롯(`{Tween,Value}\|true\|nil`), `T'=T\|Tween<T>` 타입 치환. 옵션 값 모양은 `Info: TweenInfo?` 우선+편의 필드 폴백, override는 `Tween.Cancel`(기본)/`Tween.Finish` 2값. `Animate(info)`는 `Tween` opts를 `T\|State<T>`로 받아 `:Apply`로 꽂는 sugar. 자연완료 시 per-instance 북키핑은 정리 안 해도 됨으로 확정(목표값 도달 상태라 부작용 없음, Completed 이벤트 구독 장치는 오버엔지니어링으로 판단). `initValue`는 사용자가 직접 처리(에이전트 범위 제외) |
|
||||
| `fallback-plan.md` | **[2026-08-14 세션, `research/`에서 승격]** `Fallback`/`Traceback` — 컴포넌트 함수를 감싸 에러 시 플레이스홀더를 그려주는 순수 슈가(`additional-primitives-plan.md`의 "Error Boundary" 절이 내린 "빈 자리 아님" 결론 위에 얹힘). `Fallback`은 `pcall` 기반(trace 없음), `Traceback`은 `xpcall`+`debug.traceback` 기반(trace 항상 있음) — 플래그 대신 별도 함수로 분리(`Ref`/`PreRef`와 같은 패턴). `err: any`(Lua `error()`가 임의 값을 던질 수 있음, `error(msg)` 기본 호출의 위치 접두 캐비엇 포함) 확정. 패키지는 `quad-base`, 이름 확정. 메커니즘 실측은 `audit/fallback-xpcall-verification.md`. 구현 우선순위는 형제 백로그(`quad-mock`/`quad-debug`/`Operator`)와 동급, 맨 뒤 |
|
||||
| `lifecycle-hooks-plan.md` | **[2026-08-14 아홉 번째 세션, `research/`에서 승격]** 생명주기 훅 슈가 `OnCreated`/`OnRendered`/`OnDestroyed` — 각각 `PreRef():Callback(fn)`/`PostRef():Callback(fn)`/`Effect(function() return fn end)`를 반환하는 **순수 팩토리 함수**라 새 타입/Dispatch 개념이 전혀 안 생김(호출 즉시 평가돼 기존 인스턴스로 사라짐), 여러 개 나란히 등록도 자연 지원(단 **같은 계열끼리의 순서는 미보장**). 마지막 열린 항목이던 `OnRendered`는 사용자가 **채택 확정** — 메커니즘은 `PostRef`(`base/ref-plan.md`), 원래 열어뒀던 (a)/(b)/(c) 중 **(a)**. 캐비엇: `OnRendered`는 서브트리 완성은 보장하지만 **이 인스턴스가 부모에 붙기 전**에 불림(React `componentDidMount`와 다름) — 문서화 필수. 패키지 `quad-base` 확정. **[2026-08-14 열 번째 세션]** `dispose()` 범위(0-B)가 `Slot`+`Instance`로 좁혀지고 `Observer`/`Effect`는 제외되는 쪽으로 확정되며 `OnDestroyed` 이름 재검토 조건이 발동 없이 종결 — `OnDestroyed`가 최종 이름, 용어 대기열에서도 제외 |
|
||||
| `gate-plan.md` | **[2026-08-21 신설, 같은 날 표면 확정]** `state:Gate(setup)` — 상류 emit을 가로채 내려보낼지 정책이 정하는 **`GateNode`**(`ComputeNode`와 같은 층위)를 만드는 State 메소드. 탑레벨 `Gate(...)` 프리미티브는 **안 만든다**(처음 방향에서 뒤집힘) — `Blocker`가 `state:Block(blocker)` 안에서 이 배선을 쓰고, `Debounce`/`Throttle`은 `state:Apply(...)` 팩토리가 내부에서 `:Gate`를 부른다. `Get()`엔 영향 없음(통지만 막음)까지 확정. **남은 것은 생명주기·재진입 계약과 M2 범위**(`Gate`만 vs `Blocker`까지). 구현은 M2 |
|
||||
| `state-epoch-plan.md` | **[2026-08-21 신설, 같은 날 채택 확정]** State의 재계산/전파 판정을 `invalid` 플래그가 아니라 **루트 Source 에포크 비교**로 한다 — DFS 전파 도중 `Get()`이 섞인 값을 캐시하던 glitch(실재)를 없애는 **정확성** 결정. 노드가 `sourceCountMap`(값 유효성)/`sourceEmitMap`(전파 dedup) 두 테이블을 들고, emit은 발행 source만 싣고, 순회는 `rawInvalid == false`일 때만 돌며 값만 앞당기고 통지는 상류 emit을 기다린다. 중복 *통지*도 같이 접히므로 `source-state-plan.md`의 옛 "항상 전파 / 중복 통지는 안 접음" 서술이 역전됨(`archive/always-propagate-no-dedup-superseded.md`). ⚠️ 2026-08-14에 폐기된 `invalid` 기반 dedup과는 다른 장치 — 그 금지는 유효. 구현은 M3 |
|
||||
| `state-epoch-plan.md` | **[2026-08-21 신설, 같은 날 채택 확정·`Epoch` 일반화까지 반영]** State의 재계산/전파 판정을 `invalid` 플래그가 아니라 **`Epoch` 리비전 비교**로 한다 — DFS 전파 도중 `Get()`이 섞인 값을 캐시하던 glitch(실재)를 없애는 **정확성** 결정. `type Epoch = { Revision: number }`(그 자체로 키가 되는 unique 테이블, `Source`가 구조적으로 만족), 부기는 재사용 가능한 **`EpochMap`**(`:Update(Epoch|EpochSet) -> boolean`이 "뒤로 전파가 필요한가"를 답함, `:Refresh`/`:Sync`/`:TrackFrom`. `EpochSet = {[Epoch]: true}`로 **배열이 아니라 집합** — 게이트 배치가 그 모양이다)으로 떼어냈고, State는 그걸 **둘** 컴포지션한다 — `valueEpochMap`(값 유효성)/`emitEpochMap`(전파 dedup). emit은 값도 리비전도 안 싣고 **출처(`Epoch`나 그 집합)만** 싣고, 순회는 `rawInvalid == false`일 때만 돌며 값만 앞당기고 통지는 상류 emit을 기다린다. 중복 *통지*도 같이 접히므로 `source-state-plan.md`의 옛 "항상 전파 / 중복 통지는 안 접음" 서술이 역전됨(`archive/always-propagate-no-dedup-superseded.md`). ⚠️ 2026-08-14에 폐기된 `invalid` 기반 dedup과는 다른 장치 — 그 금지는 유효. 리비전 갱신은 **`bit32.bnot(-rev)`** 한 번(사용자 확정 — 랩어라운드 **감소**를 단일 FASTCALL로, hot path라 값을 uint32에 가두고 `2^53` 포화 자체를 없앰. **[2026-08-22 정정]** 한때 `band(rev + 1, mask)`로 잘못 옮겨져 있었음). **열린 설계 항목 없음.** 구현은 M3 |
|
||||
|
||||
## `reference/` — 온디맨드 참고 자료 (2026-08-07 신설)
|
||||
|
||||
|
|
@ -81,19 +81,19 @@
|
|||
| `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` 소재 후보 |
|
||||
| `comparison-charm.md` | **[2026-08-09 신설]** littensy/charm(Roblox Zustand류) 비교 — `batch()`/`atom()`/수동 dispose Effect 3가지는 quad가 이미 기각한 패턴이라 반면교사, `None` 센티널은 독립 재확인, charm-sync의 diff/patch는 quad 미착수 네트워크 복제 영역의 첫 참고자료, 이미 확정된 `:Compute`의 `previous` 인자(`base/source-state-plan.md`)엔 정황 증거(생성 시 필수 `equals`, computed의 previous-in-getter) 제공 |
|
||||
| `epoch-brand-composition.md` | **[2026-08-21 신설, 같은 날 확정·승격 완료 → `research/`에서 이동]** `Epoch` 인터페이스 + `EpochMap` 컴포지션 + `Brand` 인스턴스화의 **결정 근거 기록**. 발단은 다중 의존성 `Effect`에서 한 파동에 `fn`이 두 번 도는 갭 — `Effect`가 자기 `EpochMap`을 들면 닫힌다. 에이전트가 짚었던 대가 둘(`Brand.get` 역조회 상실 / 포함관계가 흩어짐)은 **둘 다 해소**(전자는 필요 없고, 후자는 착오 — predicate 합성으로 그대로 쓰면 됨). 에이전트가 한때 "`:Sync`가 필수"라 적은 것도 `Update`를 좁게 본 착오로 철회. 정본은 `base/state-epoch-plan.md`/`base/brand-plan.md`/`base/source-state-plan.md`/`base/effect-plan.md` |
|
||||
| `slot-attach-decomposition.md` | **[2026-08-21 신설, 같은 날 확정 → `research/`에서 이동]** `attachSlot`의 책임 분해 **결정 근거 기록** — 결론은 후보 (B) 분해 채택이고 `base/slot-plan.md`에 반영 완료(`materializeSlotTree`+`mountSlotTree`+두 줄짜리 `attachSlot` 래퍼). QA 4라운드 `F-4-3`에서 `Dispatch.setLength`를 flush 앞/뒤 어디에 둘지가 갈렸는데, 사용자가 그건 자리 선택 문제가 아니라 *"attachSlot 의 기능이 너무 다양해진게 문제"* 라고 짚어 확장 논의로 넘어간 것. `attachSlot`이 지던 책임 일곱과 순서 제약 일곱(각각 `RC-1`/`RC-3`/`RC-4` 등 실제로 밟은 버그 출처까지)을 모아두고, **C6(길이 최종값은 flush 뒤에야 정해짐)와 C7(부기가 물리보다 먼저)이 단일 함수로는 동시 만족 불가능**함을 보인 뒤 분해 후보 넷을 대조. **[2026-08-21 후속 캐비엇]** 그 `C7`은 같은 날 `native*` 계층 확정으로 **일반 계약으로서는 폐기**됐다(`archive/bookkeeping-before-physical-reversed.md`) — 결론은 그대로 유효하지만(배치 경로가 부기를 먼저 끝내는 건 `C6` 요구사항이라 별개) 그 표만 떼어 읽지 말 것, 문서 상단에 배너를 달아뒀다. 사용자 확정 논거는 *"지금 의사코드를 건들이는 비용이, 추후 실수가 누적되는 비용보다 싸다고 생각함"*. 정본은 `base/slot-plan.md` |
|
||||
|
||||
## `research/` — 아직 착수 전, 상의 필요
|
||||
|
||||
| 문서 | 내용 | 우선순위 |
|
||||
|---|---|---|
|
||||
| `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 목차 초안 포함 | 하 — 문서화 착수 시점의 목차/우선순위표로 쓸 것 |
|
||||
| `framework-comparison-findings.md` | quad vs Fusion/Vide/react-lua 정직한 비교(실 소스 근거) — quad 강점, 못 고치는 트레이드오프(의도된 설계) 정리. **[2026-08-12 열여덟 번째 세션]** "고칠 만한 것" 절에 남아있던 마지막 두 항목(use-after-destroy 검증 안전망 부재, `:With` 동적 의존성 미지원)도 사용자가 "고칠 필요 없음"으로 최종 판단해 3번 절(못 고치는 트레이드오프)로 이전 — 2번 절은 이제 해소된 항목만 남음 | 하 — 배경 자료, 더 이상 사용자 판단 대기 상태 아님 |
|
||||
| `additional-primitives-plan.md` | **[2026-08-09 세 번째 세션, 전부 해소]** 마지막으로 남아있던 키 기반 동적 컬렉션 재조정도 `Slot:List(...)`로 확정되어 `base/slot-plan.md`로 승격 — 이 문서엔 새로 열린 설계 질문 없음, "빈 자리 아닌 것"/"문서화 백로그"/조사 소스 목록만 배경 자료로 유지 | 하 — 배경 리서치 기록용, 열린 결정 없음 |
|
||||
| `pre-implementation-audit.md` | M0 착수 직전 크리티컬 감사(2026-08-06 신설) — `base/` 전체를 모호성/지연결정리스크/단순화후보 세 렌즈로 재검토, 11개 우선순위1 + 11개 우선순위2 + 2개 단순화후보. **[2026-08-12 열일곱 번째 세션]** 우선순위1 11개 전원 해소 — 남은 건 `.claude/luau-test/` 스파이크 실측 확인뿐 | 상 — 설계는 전부 해소, `.claude/luau-test/` 스파이크 실측만 남음 |
|
||||
| `slot-attach-decomposition.md` | **[2026-08-21 신설, 같은 날 확정]** `attachSlot`의 책임 분해 근거 기록 — **결론은 후보 (B) 분해 채택**이고 `base/slot-plan.md`에 반영 완료(`materializeSlotTree`+`mountSlotTree`+두 줄짜리 `attachSlot` 래퍼). 아래는 그 논의의 출발점. QA 4라운드 `F-4-3`에서 `Dispatch.setLength`를 flush 앞/뒤 어디에 둘지가 갈렸는데, 사용자가 그건 자리 선택 문제가 아니라 *"attachSlot 의 기능이 너무 다양해진게 문제"* 라고 짚어 확장 논의로 넘어간 것. 지금 `attachSlot`이 지고 있는 책임 일곱(부모 등록 offset/length, `:List` 실체화, 마운트 상태 전이, 배치 게이팅, 자식 배치, 재귀)과 순서 제약 일곱(각각 `RC-1`/`RC-3`/`RC-4` 등 실제로 밟은 버그 출처까지)을 모아두고, **C6(길이 최종값은 flush 뒤에야 정해짐)와 C7(부기가 물리보다 먼저)이 단일 함수로는 동시 만족 불가능**함을 보인 뒤 분해 후보 넷(현행 유지 / `prepare`+`mount` 2단 / 3단 / 문서만)을 대조. 사용자 확정 논거는 *"지금 의사코드를 건들이는 비용이, 추후 실수가 누적되는 비용보다 싸다고 생각함"*. 정본은 여전히 `base/slot-plan.md` | 하 — **[2026-08-21] 결론 확정·반영 완료**, 이제 근거 기록용 |
|
||||
| `operator-sugar-plan.md` | **[2026-08-12 신설]** `Sum`/`Product`/`Not`/비트연산 등 `:Compute`/`:Apply`용 연산자 콤비네이터 슈가 — 메커니즘은 이미 확정된 계약(`Animate`와 동형 패턴) 재사용이라 확정, 네임스페이스 이름만 미정. **[2026-08-12 열아홉 번째 세션]** 서브 에이전트 외부 리서치로 다른 리액티브 라이브러리 선례와 대조 — `Operator`가 가장 강한 선례(Python `operator` 모듈), `Clamp`/`Min`/`Max`가 추가 후보로 부상, 비트연산·비교연산자·`Sub`/`Div`는 선례 전무로 드랍 후보, Debounce/Throttle은 `Blocker`와 다른 시간 기반 메커니즘이라 별도 질문으로 분리(**[2026-08-19]** 그 질문은 `base/debounce-throttle-plan.md`로 전부 해소·승격 완료), `Filtered`의 Slot 안/밖 구분 판단이 ReactiveUI/SolidJS 선례로 뒷받침됨 — 최종 이름 결정은 여전히 사용자 몫. **[2026-08-13 세션, 두 번째]** Haskell 비교 리서치 중 `Alternative`(nil 대체값, coalesce류) 후보 신설 — 카탈로그 확정 규칙에 그대로 맞음, 이전엔 없던 게 확인됨 | 하 — 구현은 맨 마지막(순수 슈가, 없어도 무방, 함수 간 의존 없음), 사용자가 직접 후순위 지정 **[2026-08-13 여섯 번째 세션]** `State<State<T>|T>` → `State<T>` 평탄화 항목 신설(백로그) — `State<State<T>>`가 정상 동작하게 됐지만 `retractFrom`의 힌트가 직속 1단계에만 가서 깊은 중첩에선 깜빡임 방지가 꺼진다는 게 구체적 동기, 사용자 판단으로 "UB는 아니지만 원치 않는 방향". `Operator.*`가 아니라 `state:Flatten()` 메소드로 제공하는 게 맞아 보이며, **반환 노드가 동적 의존성을 갖는다는 난점**(quad가 의도적으로 비지원하기로 한 바로 그것)이 확정 전 최대 쟁점 |
|
||||
| `quad-recursive-acronym.md` | **[2026-08-14 신설]** GNU/WINE류로 `Quad`를 재귀 약어화하는 카피 브레인스토밍 — 설계 결정도 착수 게이팅도 아니고 나중에 README.md 헤딩 등에 쓸 캐치프레이즈 후보 모음. 자학 개그 방향(기각)과 지연평가/재귀·커링/펑터/클로저를 자랑하는 방향(채택 후보, 미확정) 정리 | 하 — 카피 소재, 설계 상의 필요 없음. 사용자가 최종 문구 고르면 반영 |
|
||||
| `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 코어 구현 시점까지 미결 |
|
||||
|
|
@ -105,7 +105,8 @@
|
|||
|
||||
| 문서 | 내용 |
|
||||
|---|---|
|
||||
| `always-propagate-no-dedup-superseded.md` | **[역전됨, 2026-08-21 신설]** `source-state-plan.md`가 확정해뒀던 "emit은 자기 `invalid`와 무관하게 **항상** 전파된다 / quad가 접지 않는 것은 중복 *통지*뿐이다" — `base/state-epoch-plan.md` 채택으로 "같은 소스의 같은 에포크가 두 번째로 도착하면 접는다"로 바뀜. ⚠️ 2026-08-14의 `invalid` 기반 dedup 역전을 되돌린 게 아님(그 금지는 유효) |
|
||||
| `always-propagate-no-dedup-superseded.md` | **[역전됨, 2026-08-21 신설]** `source-state-plan.md`가 확정해뒀던 "emit은 자기 `invalid`와 무관하게 **항상** 전파된다 / quad가 접지 않는 것은 중복 *통지*뿐이다" — `base/state-epoch-plan.md` 채택으로 "같은 `Epoch`의 같은 리비전이 두 번째로 도착하면 접는다"로 바뀜. ⚠️ 2026-08-14의 `invalid` 기반 dedup 역전을 되돌린 게 아님(그 금지는 유효) |
|
||||
| `brand-shared-registry-reversed.md` | **[역전됨, 2026-08-21 신설]** `Brand`의 옛 표면 — 공유 weak 레지스트리 하나 + `Brand.set`/`Brand.get(x) -> tag`(**객체당 태그 하나**). `Source`가 `SourceBrand`이면서 동시에 `EpochBrand`여야 하는 요구(`base/state-epoch-plan.md`의 `Epoch` 인터페이스)를 표현할 수 없어 **인스턴스 브랜드**로 재작성됨(`base/brand-plan.md`). 같이 버려진 역조회 `Brand.get`은 코퍼스 전수 조사에서 쓰는 자리가 하나도 없어 대가가 아니었고, "포함 관계가 코드에 드러난다"는 우려는 에이전트 착오로 철회됨 |
|
||||
| `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` 신설로 대체됨 |
|
||||
| `preref-order-unguaranteed-withdrawn.md` | **[철회됨, 2026-08-14 아홉 번째 세션 신설]** 복수 `PreRef`/`PostRef` 간 fire 순서를 "배열 index 순서 보장"에서 **미보장으로 바꾸려던 안** — 같은 세션에 제안·철회, 현재 계약은 **보장**(2026-08-07 결정 그대로). 반례는 `FastQuery(...) -> PreRef`류 조합(앞자리 항목이 뒤 항목의 전제를 만들어주는 정당한 합성), 보장 비용 0 + 배열 파트 index 순서 계약의 자동 귀결이라 새로 내주는 자유도 없음. 양쪽 논거 보존 |
|
||||
|
|
|
|||
|
|
@ -18,7 +18,10 @@
|
|||
|
||||
**역전 이유**: 플래그가 아니라 에포크로 판정하면 (a) DFS 전파 도중 `Get()`이
|
||||
섞인 값을 캐시하는 glitch가 없어지고, (b) 그 판정이 중복 통지까지 O(1)로 접는다.
|
||||
성능이 아니라 **정확성** 동기다 — 경위는 `base/state-epoch-plan.md` §1·§3.
|
||||
성능이 아니라 **정확성** 동기다 — 경위는 `base/state-epoch-plan.md` §1(실재하는
|
||||
glitch)과 §4(수신 규칙). **[2026-08-22 정정]** 여기 `§1·§3`으로 적혀 있었으나
|
||||
그 문서가 `Epoch`/`EpochMap` 승격 때 §1~§8로 재편되며 옛 §3("이게 실제로
|
||||
무엇을 고치는가")은 규칙 본문으로 흡수됐다.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
89
.claude/archive/brand-shared-registry-reversed.md
Normal file
89
.claude/archive/brand-shared-registry-reversed.md
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
# [역전됨] `Brand` — 공유 레지스트리 + `Brand.get(x) -> tag` (객체당 태그 하나)
|
||||
|
||||
> **역전일**: 2026-08-21. **대체된 곳**: `base/brand-plan.md`(인스턴스 브랜드로
|
||||
> 전면 재작성). **역전 근거 원문**: `reference/epoch-brand-composition.md` §1의
|
||||
> 6번 / §3.
|
||||
>
|
||||
> **뒤집힌 것은 API 표면 하나뿐이다** — weak-key 레지스트리를 쓴다는 것,
|
||||
> 태그가 문자열이 아니라 테이블 아이덴티티라는 것, duck-typing을 안 쓰는
|
||||
> 두 근거, 포함 관계를 predicate 합성으로 표현한다는 것은 **전부 그대로
|
||||
> 살아 있다**(`base/brand-plan.md`가 소스). 아래는 그 표면의 옛 모양만
|
||||
> 보존한 것.
|
||||
|
||||
## 무엇이 뒤집혔나
|
||||
|
||||
**옛 모양** — 모듈 하나가 공유 레지스트리 하나를 들고, 객체마다 태그를
|
||||
**정확히 하나** 붙인다. 판별은 그 태그를 되읽어 비교한다.
|
||||
|
||||
```lua
|
||||
local Brand = {}
|
||||
local registry = setmetatable({}, {__mode = "k"})
|
||||
|
||||
function Brand.set(x, tag) registry[x] = tag end
|
||||
function Brand.get(x) return registry[x] end -- nil이면 quad가 모르는 값
|
||||
|
||||
-- 각 브랜드는 고유 테이블(빈 테이블이어도 됨) — 문자열 리터럴 아님
|
||||
local ObserverTag, EffectTag, TagTag, AttributeTag, TweenTag, BlockerTag,
|
||||
StateTag, SourceTag, StoreTag, SlotTag, RefTag, PreRefTag, PostRefTag,
|
||||
ModifierTag =
|
||||
{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}
|
||||
|
||||
-- 각 타입의 모든 생성 지점(Observer(...), Source(...), :With(...), Tag(...) 등)에서:
|
||||
Brand.set(newHandle, ObserverTag)
|
||||
```
|
||||
|
||||
`isX`는 그 위에 얇게 얹혔다:
|
||||
|
||||
```lua
|
||||
local function isSource(x)
|
||||
return Brand.get(x) == SourceTag
|
||||
end
|
||||
local function isState(x)
|
||||
return isSource(x) or Brand.get(x) == StateTag -- Source가 State를 구조적으로 만족
|
||||
end
|
||||
|
||||
local function isPreRef(x)
|
||||
return Brand.get(x) == PreRefTag
|
||||
end
|
||||
local function isPostRef(x)
|
||||
return Brand.get(x) == PostRefTag
|
||||
end
|
||||
local function isRef(x)
|
||||
-- PreRef/PostRef가 Ref 런타임을 재사용 = 둘 다 Ref의 한 종류
|
||||
return isPreRef(x) or isPostRef(x) or Brand.get(x) == RefTag
|
||||
end
|
||||
```
|
||||
|
||||
`None` 처리도 이 모양에 묶여 있었다 — *"`None` 자체를 레지스트리에
|
||||
평범하게 태깅하는 건 무방(사용자가 허용) — 그러면 특수 분기 없이도
|
||||
`Brand.get(None)`이 답을 준다. 즉 '범용 introspection 창구'를 지키고
|
||||
싶으면 특수 분기가 아니라 평범한 등록으로 지킨다."*
|
||||
|
||||
## 왜 뒤집혔나
|
||||
|
||||
**표현할 수 없는 관계가 실재했다.** `Epoch` 인터페이스가 도입되면서
|
||||
`Source`는 `SourceBrand`이면서 **동시에** `EpochBrand`여야 하는데,
|
||||
"객체당 태그 하나" 모델로는 이걸 못 적는다. `Source`가 아닌 원천(외부
|
||||
시계 등)이 `Epoch`로 참여하려면 다중 태깅이 필수다.
|
||||
|
||||
사용자 제안(2026-08-21): *"본인이 거기 속하면, 본인이 직접 해당 브랜드를
|
||||
가져와 등록하면 … `isXXXX`에서 각각의 구현을 넣을 필요가 없어짐. 따라서
|
||||
외부 확장도 쉬워진다."*
|
||||
|
||||
## 같이 버려진 것 — `Brand.get`의 역조회
|
||||
|
||||
옛 모양은 "이 값이 대체 무엇인가"를 되묻는 **범용 introspection 창구**를
|
||||
겸했다. 새 모양엔 그게 없다(브랜드마다 자기 집합만 안다).
|
||||
|
||||
**대가가 아니었다** — 사용자 확인: *"확실히 의미가 없어진것 같습니다
|
||||
필요하진 않아요."* 코퍼스가 실제로 쓰는 건 전부 `isX` 형태의 **멤버십
|
||||
질문**이고, 역조회를 하는 자리는 전수 조사에서 하나도 없었다.
|
||||
|
||||
## 같이 검토됐다 철회된 우려 — "포함 관계가 코드에 드러난다"는 성질
|
||||
|
||||
에이전트가 "자기 등록 방식이면 `isRef`/`isState` 같은 포함 관계가 흩어진다"고
|
||||
대가로 짚었으나 **착오였고 철회됐다.** 사용자 지적: *"여전합니다.
|
||||
`PreRefBrand` 가 존재할테니. 거기에 `is()` 를 해서, 코드에 전부 드러나는거
|
||||
똑같습니다."* — 자기 등록이 **여러 브랜드에 등록하기를 강제하는 게 아니므로**,
|
||||
각 타입은 자기 브랜드에만 등록하고 포함 관계는 지금처럼 predicate 합성으로
|
||||
한 곳에 쓰면 된다. 2026-08-09에 세운 성질이 그대로 유지된다.
|
||||
|
|
@ -921,3 +921,299 @@ context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만**
|
|||
툴링 체인(pesde types emit 등)의 지원 시점은 외부 사정이고 관측 수단이
|
||||
에이전트에 없다. 그래서 질문을 닫고 `HUMAN_TODO.md` 8번(사용자가 시점을
|
||||
파악하거나, 가능해질 때 에이전트에 알림)으로 옮겼다.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## [해소 배치 이관, 2026-08-21] `question.md`가 들고 있던 해소 항목 16건
|
||||
|
||||
**왜 옮겼나**: `question.md`는 스스로 *"항목을 해소하면 여기서 지우고
|
||||
`archive/question-resolved.md`에 근거와 함께 옮길 것 — 다시 쌓이면 같은
|
||||
문제가 반복됨"*이라고 규정해뒀는데, 2026-08-21 기준 파일의 절반이 다시
|
||||
`[해소]` 마커로 차 있었다(2026-08-21 하루에만 여러 건이 닫힌 결과). 예고된
|
||||
그대로 "필터해서 읽기 힘듦"이 재발한 것이라 이 규칙대로 일괄 이관한다.
|
||||
**결정 내용은 하나도 안 바뀜 — 읽는 자리만 옮긴 것**이고, 지금 유효한
|
||||
설계는 언제나 `base/`가 소스다.
|
||||
|
||||
**⚠️ 아래 항목 중 `Epoch`/`EpochMap` 관련 서술은 이관 시점 표현 그대로다** —
|
||||
같은 날 `Epoch`/`EpochMap`/`Brand` 승격으로 **필드 이름이 바뀌었다**
|
||||
(`sourceCountMap` → `valueEpochMap`, `sourceEmitMap` → `emitEpochMap`,
|
||||
"소스"/"count" → "`Epoch`"/"리비전"). 결정 자체는 그대로이고 표현만
|
||||
바뀌었으니, 현재형 규칙은 `base/state-epoch-plan.md`를 볼 것.
|
||||
|
||||
- **[해소됨, 2026-08-18] `DI` → `D`(Declarative) 확정** — 원문과 근거는
|
||||
`archive/question-resolved.md`. 요지: `DI`가 "Dependency Injection"과
|
||||
완전히 겹쳐 실제 오해 전례가 있었고, `D`는 Instance 전용이 아닌 declare
|
||||
요소 전반으로 확장 가능하며 `D.FrameModifier`류 타입 프리픽스도 짧게
|
||||
유지된다. 미뤄뒀던 유일한 사유(한 글자 식별자의 검색성/자기설명력)는
|
||||
"문서에서 처음 나올 때 항상 `D`(Declarative)로 풀어쓴다"는 표기 규약으로
|
||||
보완하기로 같이 확정. 코퍼스 반영 완료 —
|
||||
`base/bind-system-plan.md`의 "인스턴스 생성 / 이벤트 네이밍 인체공학" 절.
|
||||
|
||||
- **[해소됨, 2026-08-19] `PopOnly` → `Detach` 확정** — 원문과 근거는
|
||||
`archive/question-resolved.md`. 요지: 이미 있는
|
||||
`Extract`(호출자가 직접 부르는 명령형 추출)와 동사가 겹치면 헷갈리는데,
|
||||
`Detach`는 "화면(부모 계층)에서만 떼어낼 뿐 관리 주체는 여전히
|
||||
reconcile"이라는 뜻이라 `Extract`의 "소유권을 통째로 넘긴다"와 자연스럽게
|
||||
구분되고, `nil`(파괴)과의 대비도 더 직접적으로 드러남. 공개 표면 위치도
|
||||
같이 확정 — `Slot`이 함수(팩토리)라 `Slot.Detach`처럼 붙이려면
|
||||
callable-table+메타테이블이 새로 필요해서 과함, `None`과 같은 선례를 따라
|
||||
**패키지 최상위 export**로. 코퍼스 반영 완료 — `base/slot-plan.md`의
|
||||
"`nil` 리턴은 파괴가 기본" 절.
|
||||
|
||||
- **[해소, 2026-08-21 구현 전 QA 5라운드 `CR-3`] M2가 M3의 `Blocker.luau`에
|
||||
구조적으로 의존하던 순서 문제 — "게이팅 먼저"로 결정.** 사용자 판정:
|
||||
*"게이팅 먼저. 게이팅을 base 에 만들 준비를 해야한다. 실질적 모양 정의가
|
||||
필요함."* 즉 로드맵 순서를 유지하지 않고 게이팅을 M2로 앞당긴다. **다만
|
||||
앞당기는 대상이 `Blocker` 그 자체가 아니라 그 아래의 공용 `Gate` 노드로
|
||||
바뀌었고**(같은 라운드 `DT-4`), 그 표면/이름이 미정이라 **새 열린 항목으로
|
||||
이어진다 — 바로 아래 `Gate` 항목.** 아래는 해소 전 서술: 이대로 각주만 두고
|
||||
로드맵 순서를 유지할지,
|
||||
`Blocker.luau`(또는 최소 표면 `On`/`Off`/`IsOn`/`OffWithoutEmit`)를 M2로
|
||||
앞당길지, M2/M3 경계 자체를 재검토할지.** `RC-1`의 Blocker 게이팅 해법
|
||||
때문에 `ROADMAP.md` M2의 `Dispatch.setLength`/`setOffsetSource` 체크박스가
|
||||
`getBlocker`/`:On()`/`:IsOn()`/`:OffWithoutEmit()`을 호출하는데, 정작
|
||||
`Blocker.luau` 자체는 M3 체크박스에 있다 — 로드맵 순서대로면 M2가 아직
|
||||
없는 걸 참조하게 된다. 지금은 M2 체크박스에 이 사실만 각주로 남겨둔
|
||||
임시 조치(가장 보수적인 선택, 마일스톤 재편은 안 함) — **M2 착수 전
|
||||
필요**. 상세는 `qa-request/pre-implementation-qa-round3.md`의
|
||||
"ROADMAP.md 마일스톤 정합성" 절.
|
||||
|
||||
- **[해소, 2026-08-21 구현 전 QA 5라운드 H절] `mountInst`의 삽입 위치 + 중첩
|
||||
offset 결함 — `Dispatch.getOffsetAt(ownerKey, i)` 신설로 확정.**
|
||||
`setOffsetSource(None)`은 얼리 리턴하고, 숫자가 필요한 쪽이 `getOffsetAt`을
|
||||
직접 부른다(사용자안 — 병렬 배열을 안 늘리는 pull 방식). 베이스는 `isSlot`
|
||||
분기의 정체가 "베이스가 있나"임을 명확히 했고(베이스는 따로 저장하지 않고
|
||||
Slot의 `.Offset`을 그대로 읽는다 — 최상위 물리 inst는 항상 0), 중첩 Slot은
|
||||
자기 `Offset`을 관측해 깊은 전파를 한다. 반영하다 **재마운트가 `Offset` Source를 새로 만들던 결함**도
|
||||
같이 잡았다(identity 재사용). 상세는
|
||||
`qa-request/pre-implementation-qa-round5-followup.md`의 H절.
|
||||
아래는 열려 있던 시점의 서술: 둘이 한 덩어리다:
|
||||
(a) `setOffsetSource`의 `None`이 "아무것도 안 차지함"과 "발행 채널 없음"
|
||||
두 뜻을 겸하고 있어 **plain 요소의 offset 숫자가 계산조차 안 된다**(그래서
|
||||
DOM류 백엔드가 삽입 위치를 알 방법이 없다 — 사용자 지적), (b) `recompute`가
|
||||
`sum = 0`에서 시작해 `ownerKey.Offset`을 안 읽으므로 **depth ≥ 2에서 중첩
|
||||
Slot의 자식 offset이 부모 베이스만큼 어긋난다**(이번에 발견, 지금까지 depth 1만
|
||||
써서 안 드러났음). 제안은 `bk.offsetList` 신설(항상 숫자 계산) +
|
||||
`mountInst(target, element, index)` + `recompute`의 `base` 시드 + 중첩 Slot이
|
||||
자기 `Offset`을 관측해 재계산하는 구독 하나 —
|
||||
`qa-request/pre-implementation-qa-round5-followup.md`의 G절이 소스.
|
||||
|
||||
- **[해소, 2026-08-21] offset이 바뀌면 이미 배치된 물리 노드를 옮겨야 하는가 —
|
||||
아니오.** **사용자 확정**: *"애초에 offset 바뀌여도 상관 없는게 위에서 넣고
|
||||
빼면 insert 같은거로 일어나서 뒤로 밀린다는거였긴함"* — DOM류는 삽입/삭제
|
||||
자체가 뒤 형제를 물리적으로 밀고 당기므로, **이미 놓인 노드를 다시 옮길 일이
|
||||
없다.** offset 숫자는 그 자리가 **다음에** insert/remove할 때 쓰는 것뿐이라는
|
||||
기존 서술이 그대로 맞다. 이게 성립하려면 백엔드 op이 **아토믹한 최소 단위**
|
||||
(`mountInst`/`unmountInst`/reposition)여야 한다는 게 같이 확인된 요구사항.
|
||||
아래는 열려 있던 시점의 서술: `mountInst(target, element,
|
||||
index)`가 **삽입 시점의 위치만** 받는 일회성 호출이라, 이미 마운트된 요소의
|
||||
물리 위치는 offset이 바뀌어도 갱신되지 않는다. Roblox는 `LayoutOrder`가
|
||||
프로퍼티라 `updateFn`이 반응형으로 처리하면 되지만 **DOM은 물리 순서 자체가
|
||||
배치**다. `dispatch-core-plan.md`는 "quad-web의 offset 핸들러는 no-op이고 숫자는
|
||||
*다음에* 스스로 insert/remove할 때만 쓴다"고 적어뒀는데, 앞 형제의 길이가 변해
|
||||
뒤 형제들의 offset이 밀리는 흔한 경우에 **이미 놓인 노드를 옮길지**가 그 서술만
|
||||
으론 안 갈린다. 선택지는 (a) 옮긴다(백엔드가 offset 변경을 관측해 재배치),
|
||||
(b) 안 옮긴다(그러면 DOM에선 순서가 실제로 어긋남), (c) `:List`가 재조정 때
|
||||
필요한 것만 명시적으로 다시 `mountInst`. quad-web이 실제로 생길 때까지 미룰 수
|
||||
있으나, **계약을 지금 정해두지 않으면 M6 구현이 (b)를 전제로 굳는다.**
|
||||
|
||||
- **[해소, 2026-08-21] `:List` 재조정의 `getOffsetAt` 비용 — 접두합 캐시로.**
|
||||
**사용자 제안 채택**: *"getOffsetAt 은 compute 된걸 캐시해도 될듯. length
|
||||
변경되는 뒤는 캐시가 무효화되도록 shouldRecomputeAfter 등을 둬서 특정 인덱스
|
||||
초과부는 offset 다시 계산하고, 해당 값 위치 자체는 유효하므로 그것을 통해 더
|
||||
length 를 이어붙이면 될듯."* → `bk.offsetCache` + **`bk.invalidAfter`**("여기까지는 유효")로 반영
|
||||
(`base/dispatch-core-plan.md`). **무효화 규칙은 하나** —
|
||||
`invalidAfter = min(invalidAfter, i)`(`setLength`도 splice도 자기 인덱스까지,
|
||||
베이스 변경만 `0`). `recompute`도 이 캐시 위에 얹혀 O(N)이라
|
||||
**"캐시를 누가 채우나"라는 갈래가 없어졌다**(사용자 정정: 함수를 나눌 이유가
|
||||
없다). 아래는 열려 있던 시점의 서술:
|
||||
`settle`이 키마다 `rawAdd`/`rawReplace`를 부르고 그 각각이 `getOffsetAt`(O(i))을
|
||||
부르므로, 이미 마운트된 리스트의 데이터가 통째로 바뀌면 **O(N²)**다(최초 마운트는
|
||||
`_mounted == false`라 얼리 리턴이 막아준다). `DC-9`에서 `setOffsetSource`의 즉시
|
||||
계산이 O(N²)인 걸 "배치 등록 1회니 감수"로 판단했지만 **이건 매 reconcile이라
|
||||
빈도가 다르다.** 해법은 있다 — reconcile이 `pos`처럼 **절대 offset도 러닝
|
||||
누적**으로 들고 다니면 O(n)(그게 `mountSlotTree`가 이미 하는 방식). 그렇게
|
||||
할지, 아니면 실측 전엔 그냥 둘지 판단 필요.
|
||||
|
||||
- **[해소, 2026-08-21 같은 날] 게이트가 유보했다 내보내는 emit이 싣는 출처 —
|
||||
`emit(self)` + 흡수 집합.** `GateNode`가 흡수한 소스를 `withheld` 집합에
|
||||
들고 있다가, 풀 때(=flush) **집합을 그 자리에서 떼어내 새 테이블로 스왑하고**
|
||||
자기를 출처로 그 배치를 하류에 넘긴다. 하류는 출처가 게이트면 그 집합의 소스들에 평소 규칙을
|
||||
적용한다. **`setup` 시그니처는 안 바뀐다** — 집합을 채우는 건 정책이 아니라
|
||||
노드이기 때문. `base/gate-plan.md`의 4번, `base/state-epoch-plan.md` §4
|
||||
(이관 당시엔 §2였으나 그 문서가 §1~§8로 재편됨).
|
||||
|
||||
- **[해소, 2026-08-21 같은 날] State 에포크 — 새 노드의 두 맵 초기값과
|
||||
`:With` 병합.** `sourceEmitMap`은 **비우고**(어떤 emit이 와도 "처음 보는
|
||||
것"으로 걸리는 게 맞다 — 새 노드는 개념적으로 emit을 받아본 적이 없다),
|
||||
`sourceCountMap`은 **전부 끌어와 실제 count로 채운 뒤 `rawInvalid = true`**
|
||||
(비워두면 순회가 훑을 목록 자체가 없어 "유효하다"로 오판한다 — *"'내가 뭘
|
||||
추적하고 있나' 가 필요하죠"*). 그래서 `:With` 병합 규칙은 **필요 없어졌다.**
|
||||
재계산 시 갱신 범위(전부 갱신)도 확정. `base/state-epoch-plan.md`의 §2·§5 7번.
|
||||
|
||||
- **[해소, 2026-08-21 같은 날] `Gate` — 소스 없는 emit(빈 배치)은 **아무것도
|
||||
안 한다**.** 에이전트 권고("빈 배치 = 무조건 통지")는 **사용자 기각** —
|
||||
*"쌓아둔것 자체가 없는데 뒤로 넘긴다는건 이상합니다 … 표면적으로 보면 State
|
||||
중간에 Emit 을 추가하는 격"*. 새 규칙도 아니다: `blocker-plan.md`가 이미
|
||||
"`HasBlockedEmit`이 false면 아무 것도 안 함(idempotent)"으로 확정해뒀고,
|
||||
`withheld`는 그 플래그의 일반화다. **따름정리로 `Effect(fn, ...deps)`의 설치
|
||||
구간 억제는 `Gate` 소비자가 아니게 됐다** — `Effect` 내부 플래그로 처리하고,
|
||||
`effect-plan.md`에 있던 "`Gate`보다 뒤" 순서 제약도 사라졌다.
|
||||
`base/gate-plan.md`의 7·8번.
|
||||
- **같이 제기됐던 "재진입 계약"은 열린 항목이 아니었다**(사용자 지적으로
|
||||
2026-08-21 정리) — `blocker-plan.md`의 재진입은 **같은 인스턴스 중첩**을
|
||||
말하는 것이지 정책의 `emit()` 호출과 무관하고, 끝나지 않는 되먹임은
|
||||
2026-08-04 확정 원칙대로 **UB**이며, 유한한 재진입은 flush 진입 시 스왑으로
|
||||
이미 안전하다. `gate-plan.md`의 6번.
|
||||
|
||||
- **[해소, 2026-08-21] 공용 게이트 노드의 이름과 표면 — `state:Gate(setup)`
|
||||
메소드 + `GateNode`로 확정.** 탑레벨 프리미티브는 안 만들고, `Blocker`는
|
||||
`state:Block(blocker)` 안에서 그 배선을 쓴다(사용자: *"Gate 는 따로
|
||||
프리미티브 없이 state:Gate( (emit) -> ()->() ) 처럼 선언되고 마치 Compute
|
||||
처럼 GateNode(ComputeNode 처럼) 생성된다"*). `Get()`엔 영향 없음(통지만
|
||||
막음)까지 확정. **[2026-08-21 정정]** 여기 "남은 것은 사용자 판단이 아니라
|
||||
구현 시 정할 것들"이라 적었으나, 두 번째 `/code-review high`가 빈 배치
|
||||
emit을 잠시 사용자 판단 항목으로 되돌렸다 — **같은 날 해소돼(바로 위 항목)
|
||||
원래 서술로 돌아왔다.** 구현 시
|
||||
정하면 되는 건 생명주기와 M2 범위뿐 — `base/gate-plan.md`가
|
||||
소스. 아래는 열려 있던 시점의 서술: 위 항목의 결정("게이팅 먼저")에 따라 base에 만들 것이
|
||||
`Blocker`가 아니라 **상류 emit을 가로채 정책이 통과 여부를 정하는 공용 게이트
|
||||
노드**로 확정됐다(`Blocker`/`Debounce`/`Throttle`이 그 위의 정책). 사용자
|
||||
스케치는 `Gate(function(emit) return function() ... end end)` 2단 구조이고,
|
||||
**공개 API로 낸다**(사용자: *"이 API가 비공개일 이유는 없어보인다"*). 남은 것 —
|
||||
(a) **이름**(사용자: *"프리미티브 명을 Gater? 뭔가 이상하게 들어간다는게 약간의
|
||||
문제"*, 에이전트 권고는 `Gate` 그대로 — `gate`는 이미 장치를 가리키는 명사),
|
||||
(b) M2에 `Gate`만 넣을지 `Blocker`까지 넣을지, 그리고 생명주기 계약.
|
||||
**[2026-08-21 해소]** 여기 있던 "`:Apply` 팩토리인가"와 "`Blocker`가 그 위에
|
||||
어떻게 얹히는가"는 닫혔다 — **사용자 확정으로 `Gate`는 `:Apply`가 아니라
|
||||
`:With`류 State 메소드**(*"state 의 전파를 손대는 작업이라 with 처럼 다른
|
||||
노드가 나는게 맞음"*)이고, 그러면 `Blocker`는 이미 확정된
|
||||
`state:Block(blocker)` 메소드가 내부에서 그걸 부르면 되므로 배선 문제 자체가
|
||||
없어진다. 상세는 `base/gate-plan.md`.
|
||||
|
||||
- **[해소, 2026-08-21] State 재계산/전파 판정을 "소스 에포크 비교"로 바꿀지 —
|
||||
채택 확정**(사용자: *"gate 와 epoch 가 제가 만족할만한 정도로 올라왔습니다.
|
||||
채택하면 될것 같아요."*). 규칙 전량은 `base/state-epoch-plan.md`, 구현은 M3.
|
||||
아래는 열려 있던 시점의 서술: 사용자 제안: 각 State가 자기 상류 루트
|
||||
`Source`들의 카운트를 들고 있다가 `Get()` 때 비교해 재계산 여부를 정한다.
|
||||
동기는 성능이 아니라 **정확성** — DFS 전파 도중 Observer가 `Get()`을 부르면
|
||||
아직 신호를 못 받은 다른 가지의 옛 캐시가 섞여 들어가는 glitch가 지금 모델에
|
||||
실재한다. 에이전트 분석 결과 진단·방향 모두 타당하고 선례도 있다
|
||||
(MobX/Adapton류 버전 검증). **[2026-08-21 갱신 — 여기 있던 "중복 통지는 안
|
||||
고쳐지고 선언 안 된 의존성을 UB로 명문화해야 한다"는 서술은 같은 날 둘 다
|
||||
뒤집혔다]**: 중복 통지도 **같은 장치로 접고**, UB 조항은 사용자 기각으로
|
||||
빠졌다. 이어진 3·4차 정정으로 **기제는 사실상 다 정해졌다** — 순회는
|
||||
`rawInvalid`가 **false**일 때만 돌고(목적은 "못 받은 emit 받기"), emit은
|
||||
count 없이 **발행 source만** 싣고, 순회가 발견한 변경은 **테이블 둘**로
|
||||
처분한다(`sourceCountMap`은 순회가 앞당겨 올리고, `sourceEmitMap`은 상류의
|
||||
진짜 emit을 기다림 — *"emit 바로 안하고 상류가 emit 해줄 때 까지
|
||||
기다립니다"*). 그래서 통지가 죽지도 게이트를 새지도 않아 `source = nil`
|
||||
규약도 필요 없어졌다. **남은 판단은 채택 여부 자체 하나**이고, State 내부 표현을
|
||||
바꾸는 결정이라 M3 뒤로 미루면 되돌리는 비용이 크다. 상세는
|
||||
`base/state-epoch-plan.md`.
|
||||
|
||||
- **[해소, 2026-08-21 구현 전 QA 5라운드 `C-4`] `Dispatch.setLength`의 Observer
|
||||
앵커 — 물리 target으로 확정(4라운드 `D-56` 역전).** `setLength`가
|
||||
`(ownerKey, i, len, anchor)`로 4번째 인자를 받아 **부기 키와 생명주기 앵커를
|
||||
분리**한다. 그래서 `bindLifetime`은 항상 물리 Instance만 상대하고,
|
||||
`isBoundAlive`의 세 번째 분기(형태 미정으로 열려 있던 것)도 **필요 자체가
|
||||
없어졌다.** 역전 원문은 `archive/bindlifetime-slot-owner-reversed.md`, 지금
|
||||
결론은 `base/dispatch-core-plan.md`의 "`setLength` 구현" 절 뒤 문단. 아래는
|
||||
열려 있던 시점의 서술: 그때 확정(4라운드 `D-56`)은
|
||||
"`bindLifetime`의 첫 인자가 Slot일 수 있으니 백엔드가 그 경우를 핸들링하라"인데,
|
||||
사용자가 그 전제 자체에 의문을 제기했다: *"애초에 Slot 이 effect 나 다른
|
||||
요소들을 소유할 수가 없다 … 실제 observer/effect 는 실제 inst 에 불림 …
|
||||
우리가 왜 slot 을 소유 대상으로 둘 수 있게 한거였는지 다시 생각해봐야할
|
||||
부분."* 대안은 **부기 키(`ownerKey`)와 생명주기 앵커(물리 `physicalTarget`)를
|
||||
분리**하는 것 — 그러면 `bindLifetime`은 항상 Instance만 받고,
|
||||
`isBoundAlive`의 세 번째 분기(지금 ⚠️ 미정)도 통째로 불필요해진다. 상세와
|
||||
트레이싱은 `qa-request/pre-implementation-qa-round5-followup.md`.
|
||||
|
||||
- **[해소, 2026-08-21] `Detach` 홀드 중 키가 사라졌을 때의 처분** —
|
||||
선택지 (c)로 확정: `updateFn`을 **`KeyGone`으로 한 번 더 불러 처분을
|
||||
묻는다**. 같이 확정된 것 — detach된 요소는 `userdata`가 아니라
|
||||
**`slot._detached` 필드**가 보유하고(그래야 `destroySlotTree` walk가
|
||||
닿고 소유권도 유지됨), owner가 죽으면 `activateList`가 설치한 `Effect`가
|
||||
정리한다. 원래 갭이
|
||||
치명적이었던 이유는 gcconn 트릭 때문에 detach된 quad-제작 Instance가
|
||||
**GC 폴백조차 없이 영구히 남기** 때문. 상세는 `base/slot-plan.md`의
|
||||
"Detach된 요소는 `slot._detached`가 보유한다"/"`KeyGone`" 절.
|
||||
|
||||
- **[해소, 2026-08-21] `attachSlot` 책임 분해** — **(B) 분해 채택으로 확정**,
|
||||
`base/slot-plan.md`에 반영 완료(`materializeSlotTree`/`mountSlotTree`/얇은
|
||||
`attachSlot`). 근거 기록은 `reference/slot-attach-decomposition.md`.
|
||||
아래는 그 열려 있던 시점의 서술: 한
|
||||
함수가 부모 등록(offset/length) / `:List` 실체화 / 마운트 상태 전이 / 배치
|
||||
게이팅 / 자식 배치 / 재귀를 다 지고 있어서, **"부모에게 알리는 길이의
|
||||
최종값은 flush가 끝나야 정해진다"와 "부기가 물리 조작보다 먼저"가 동시에
|
||||
만족되지 않는다**(지금은 후자를 지키고 전자를 포기 — 부모 `recompute`가
|
||||
1회 헛돎). 사용자 판단으로 확장 논의 대기 — 책임 목록/순서 제약 출처/분해
|
||||
후보 넷은 `reference/slot-attach-decomposition.md`. **M6(`:List`) 착수 전
|
||||
필요**, 선행으로 아래 `Detach` 보관 위치가 먼저 닫히는 게 나음.
|
||||
|
||||
- **[해소됨, 2026-08-18] `Attribute.Merged`의 이름 중복** — `Merged`(겹치면
|
||||
error)와 `Overridden`(겹치면 뒤가 이김)을 **둘 다 제공**하는 것으로 확정
|
||||
(제3안). 근거·파급은 `base/attribute-plan.md`의 "채택안 — `Tag`와 동형인
|
||||
array-part 값 객체" 절.
|
||||
|
||||
- **[신설, 2026-08-18 구현 전 QA] 그룹 `Attribute`의 위치별 claim 설계** —
|
||||
같은 그룹 객체를 두 위치에 놓는 경우(`Frame { a, a }`)를 잡으려면 위치별
|
||||
claim 레지스트리가 하나 필요하다는 **방향은 확정**됐고(`Ref`처럼
|
||||
`bindLifetime`을 재사용할 수는 없음 — 그룹 값은 여러 곳에서 쓸 수 있어야
|
||||
하므로), **[2026-08-20 QA 4라운드] 이름도 `groupClaimKeys`로 확정**.
|
||||
**[해소, 2026-08-21 QA 5라운드 `AT-1`] 키도 `(inst, groupValue) → k`로 확정**
|
||||
(사용자: *"group 에 따라 key 가 따로 생성되므로 다른 그룹에 대해서는 잡을
|
||||
필요가 없고, 그건 key->name 이 유일성을 검증해준다"*), `nameClaims`보다
|
||||
**위치 claim을 먼저** 본다 — `base/attribute-plan.md`의 "이름 소유권" 절.
|
||||
**이 항목은 닫혔다.**
|
||||
|
||||
- **`Gate`(2순위, 2026-08-21 신설)**: `Blocker`/`Debounce`/`Throttle` 아래의
|
||||
공용 게이트 노드 이름. 사용자 지적 — *"프리미티브 명을 Gater? 뭔가 이상하게
|
||||
들어간다는게 약간의 문제"* — 코퍼스가 `Blocker`/`Modifier`/`Observer`처럼
|
||||
`-er`를 많이 쓰는데 `Gater`는 영어로 어색하다. 에이전트 권고는 **`Gate`
|
||||
그대로**(`gate`는 이미 행위자가 아니라 **장치**를 가리키는 명사라 `-er`가
|
||||
불필요 — `Source`/`Ref`/`Slot`/`Tween`도 같은 계열), 대안 후보는
|
||||
`Valve`/`Relay`. **[2026-08-21 해소] `Gate`로 확정**(탑레벨 생성자를 안
|
||||
만들고 `state:Gate(setup)` 메소드로 가면서 `Gater` 문제 자체가 사라짐 —
|
||||
메소드 자리에서는 `:With`/`:Compute`와 나란히 자연스럽다). 노드 타입 이름은
|
||||
`GateNode`. `base/gate-plan.md`의 1번이 소스.
|
||||
|
||||
## [해소됨, 2026-08-21] `Epoch`의 리비전 증가 방식 — `bit32` 랩으로 확정
|
||||
|
||||
**결론**: `self.Revision = bit32.bnot(-self.Revision)`.
|
||||
리비전은 uint32 안에 머문다. 정본은 `base/state-epoch-plan.md` §2.
|
||||
**[2026-08-22 정정]** 이 항목을 처음 적을 때 에이전트가 형태를
|
||||
`bit32.band(rev + 1, 0xFFFFFFFF)`로 잘못 옮겼다 — 사용자가 말한 건 처음부터
|
||||
`bit32.bnot(-a)`였고(*"제가 말한건, bit32.bnot(-a) 입니다"*), 그건 랩어라운드
|
||||
**감소**를 **연산 하나로** 한다(`a > 0`이면 `a - 1`, `0`이면 `4294967295`).
|
||||
|
||||
**사용자 논거**: *"그건 luau 에서 native call 이라 아주 빨라요. 반면 double 의
|
||||
연산이 느린편인데, 희소 수준이 아니라, 사실상 만나는걸 수년간 보기 어려운
|
||||
라운드되어 동일해 무시되는 경우를 막기 위해 double 까지 올려야할 이유를
|
||||
모르겠어요. 매번 도는 코드인지라, 값 싸게 native call + num 연산으로 가볍게
|
||||
가고 싶어요."* — `2^53` 포화는 어차피 도달 불가능한 시나리오인데 그걸
|
||||
피하겠다고 값을 double 영역까지 키울 이유가 없다는 것. 매 `Set`마다 도는
|
||||
hot path다.
|
||||
|
||||
**에이전트가 달았다가 철회한 단서**: "`bit32`라서 더 싸다는 건 아니다 —
|
||||
`n + 1`은 어느 쪽이든 double 덧셈이고 `bit32`는 fastcall을 하나 더 얹는다"고
|
||||
적었으나, **이건 `band(rev + 1, mask)`라는 다른 형태를 놓고 한 비교라
|
||||
틀렸다**(2026-08-22 정정). `bit32.bnot(-a)`는 덧셈 위에 얹히는 게 아니라
|
||||
**갱신 자체를 대체**하는 단일 FASTCALL이므로, "native call이라 아주 빠르다"는
|
||||
사용자 서술이 맞다.
|
||||
|
||||
**따름정리**: 이 방식은 **단조 증가가 아니라 랩어라운드 감소**다. 지금 규칙이
|
||||
`==`/`~=`만 쓰기 때문에 무해하지만, 순서 비교를 넣고 싶어지면 이 결정부터
|
||||
되짚어야 한다.
|
||||
|
||||
아래는 열려 있던 시점의 서술:
|
||||
|
||||
- **`Epoch`의 리비전 필드 증가 방식(3순위, 2026-08-21 신설)**: `bit32` 랩
|
||||
(uint32 랩어라운드라 double 포화가 없음, 사용자가 염두에 둔 쪽) vs 평이한
|
||||
`+1`(할당·연산 더 쌈, 포화가 `2^53`이라 `bit32`의 `2^32` 랩보다 충돌 거리가
|
||||
오히려 넓음). **둘 다 도달 불가능이라 실질 위험은 없고 취향 문제다.**
|
||||
**이게 `Epoch`/`EpochMap` 승격 후 남은 유일한 미정 항목이다** —
|
||||
`base/state-epoch-plan.md` §2, 근거 기록은
|
||||
`reference/epoch-brand-composition.md` §4의 4번.
|
||||
(필드 이름 `Revision`과 타입 `Epoch`는 확정, 승격 시 `base/`에 반영됨.)
|
||||
|
|
|
|||
|
|
@ -268,8 +268,8 @@ quad/
|
|||
│ ├── Tag.luau # 값 타입+immutable clone 체이닝(`Tag(...)`/`:Added`/`:Removed`/`:Contains`/`:Apply`/`Merged`/`:Names`) — 참조 카운트 Handler는 Dispatch/Tag.luau(아래), 엔진 호출은 주입된 addTag/removeTag(`base/tag-plan.md`)
|
||||
│ ├── Attribute.luau # 그룹 값 타입+API(`Attribute(store1, store2, ...)`/`Merged`/`:NameMap`, `Tag`와 동형) — Handler는 Dispatch/Attribute.luau(아래) (`base/attribute-plan.md`)
|
||||
│ ├── AttributeKey.luau # 단일 키 `AttributeKey<<T>>(name)` + 이름별 weak 캐시(동등성 보장) + 스칼라 편의 패밀리(String/Number/BooleanAttribute) — 엔진 고유 타입 패밀리(Color3Attribute류)만 백엔드 소속(`base/attribute-plan.md` "패키지 배치" 절, 2026-08-13 열네 번째 세션 재배치)
|
||||
│ ├── Tween.luau # 값 타입만(`Tween(opts)` 팩토리, `isTween`/`TweenTag`) — 엔진 무관, 독립 Dispatch 핸들러 아님. 실제 애니메이션 처리는 quad-roblox Handlers/Property.luau 내부 분기(`base/tween-plan.md`, 2026-08-10 세션 재설계)
|
||||
│ ├── Effect.luau # `Effect(fn, state?)` — state 없으면 설치1회+leaf사망시 정리, 있으면 State.Observer를 조합해 재실행(`base/effect-plan.md`)
|
||||
│ ├── Tween.luau # 값 타입만(`Tween(opts)` 팩토리, `isTween`/`TweenBrand`) — 엔진 무관, 독립 Dispatch 핸들러 아님. 실제 애니메이션 처리는 quad-roblox Handlers/Property.luau 내부 분기(`base/tween-plan.md`, 2026-08-10 세션 재설계)
|
||||
│ ├── Effect.luau # `Effect(fn, ...deps)` — state 없으면 설치1회+leaf사망시 정리, 있으면 State.Observer를 조합해 재실행(`base/effect-plan.md`)
|
||||
│ ├── Dispatch/
|
||||
│ │ ├── init.luau # process 엔진 — `chains`(inst,k별 인덱스 배열, 슬롯마다 {handler, retractor}) + 하강 diff(핸들러가 같으면 그 자리 클로저에 새 값을 넘기고 재process, 다르면 그 자리부터 retractFrom) + 3-인자 `retractFrom(inst,k,index)` (`dispatch-core-plan.md` "Dispatch 체인" 절, 2026-08-08 신설 → 2026-08-13 다섯 번째 세션 인덱스화 → 같은 날 열네 번째 세션 하강 diff)
|
||||
│ │ ├── Handler.luau # 핸들러 계약 타입(isHandlable/priority/process — process가 자기 retract 클로저를 반환)
|
||||
|
|
@ -322,7 +322,7 @@ existing-instance-bind는 **[2026-08-14 세션] 기각되어 `archive/`로
|
|||
프리미티브 타입 자신의 공개 어휘**라는 것:
|
||||
1. 프리미티브 타입 생성자, `Type(args)` 스타일: `Source(default)`/
|
||||
`Ref(default)`/`Store({defaults})`/`Modifier()`/`Relate()`/
|
||||
`Effect(fn, state?)`/`PreRef(default)`/`PostRef(default)`.
|
||||
`Effect(fn, ...deps)`/`PreRef(default)`/`PostRef(default)`.
|
||||
2. 그 인스턴스의 콜론 메서드: `state:Get()`/`:With(...)`/`:Compute(fn)`/
|
||||
`:Observer(fn)`/`:Apply(factory)`/`:Peek(key)`, `source:Set(v)`/`:Emit()`,
|
||||
`ref:Set(v)`/`:Callback(fn)`/`:Wait(thread?)`, `observer:Subscribe()`/
|
||||
|
|
@ -354,9 +354,11 @@ existing-instance-bind는 **[2026-08-14 세션] 기각되어 `archive/`로
|
|||
`isPostRef`/`isModifier`/`isObserver`/... `Brand` 절), 생명주기 게이트(`canExecute`/
|
||||
`bindLifetime`, `base/lifecycle-pattern.md`), 그리고 **프리미티브가
|
||||
아닌** 내부 엔진/레지스트리의 네임스페이스 멤버(`Dispatch.process`/
|
||||
`getHandler`/`addHandler`/`drive`, `Brand.set`/`get`) — 이 셋은 "타입
|
||||
`getHandler`/`addHandler`/`drive`, `Brand()`의 `:register`/`:is`) — 이 셋은 "타입
|
||||
고유의 어휘"가 아니라 여러 타입에 걸쳐 쓰이거나(`isX`류) 프리미티브
|
||||
자체가 아닌 것(Dispatch/Brand는 `Type(args)` 생성자가 없는 내부 엔진)의
|
||||
자체가 아닌 것(Dispatch는 `Type(args)` 생성자가 없는 내부 엔진이고,
|
||||
`Brand`는 생성자가 있지만 사용자 표면이 아닌 base 내부 유틸 — 사용자에게
|
||||
노출되는 건 `isX` wrapper들이다)의
|
||||
구성원이라 PascalCase 대상이 아님. Handler 계약 필드(`isHandlable`/
|
||||
`priority`/`process` — 2026-08-13 다섯 번째 세션에 `retract`가 `process`의
|
||||
반환값으로 합쳐지기 전엔 4종이었음)도 여기 속함 — 이건 애초에 "함수"라기보다
|
||||
|
|
@ -452,11 +454,12 @@ pull-recompute(`Get()` 시점) — Fusion식 eager 노드 없이도 다이아몬
|
|||
**emit 전파는 자기 `invalid` 상태와 무관함**. 한때 `source-state-plan.md`가
|
||||
"이미 `invalid`면 전파 중단"으로 서술했으나 `Observer` 계약과 모순돼 역전됨 —
|
||||
`archive/invalidate-dedup-propagation-reversed.md`. **[2026-08-21 갱신]**
|
||||
전파를 접는 판정은 이제 `invalid`가 아니라 **소스 에포크 비교**가 한다 —
|
||||
같은 소스의 같은 에포크가 두 경로로 도착하면 두 번째는 접히고(다이아몬드에서
|
||||
전파를 접는 판정은 이제 `invalid`가 아니라 **`Epoch` 리비전 비교**가 한다 —
|
||||
같은 `Epoch`의 같은 리비전이 두 경로로 도착하면 두 번째는 접히고(다이아몬드에서
|
||||
값도 통지도 한 번), DFS 도중 `Get()`이 섞인 값을 캐시하던 glitch도 같이
|
||||
사라진다. 규칙 전량은 `base/state-epoch-plan.md`, 명시적 게이트는
|
||||
`base/gate-plan.md`(`state:Gate`)와 그 위의 `Blocker`). State는 쓰기 대상이 아니고, 값을 쓰는
|
||||
사라진다. 그 부기는 재사용 가능한 **`EpochMap`**으로 떼어져 있어 State가 아닌
|
||||
소비자(`Effect`)도 같은 판정을 쓴다. 규칙 전량은 `base/state-epoch-plan.md`,
|
||||
명시적 게이트는 `base/gate-plan.md`(`state:Gate`)와 그 위의 `Blocker`). State는 쓰기 대상이 아니고, 값을 쓰는
|
||||
경로는 `source:Set(value)`(Source가 State보다 넓은 인터페이스를 가짐 —
|
||||
`:Get()`/`:With`/`:Compute` 위에 `:Set`/`:Emit` 추가; [정정, 2026-08-07]
|
||||
읽기는 `:Get()` 하나로 통일 — 프로퍼티 읽기 표기는 Ref의 `.Value` 전용으로 좁혀짐. **[표기 정정, 2026-08-18]** 여기 소문자 `.value`로 적혀 있었음). 값 하나만
|
||||
|
|
|
|||
|
|
@ -56,7 +56,8 @@ Signal 미채택, Ref 역할)과 소스 트리 상 패키지 경계(디스패치
|
|||
store-bind 가능(`None`/`nil`로 disconnect) → **`base/event-plan.md`**. 단 이벤트
|
||||
*네이밍* 관례는 인스턴스 생성과 한 절에 섞여 있어 아래 "인스턴스 생성 /
|
||||
이벤트 네이밍 인체공학" 절에 그대로 있음.
|
||||
- **`Brand`** — 런타임 nominal 타입 판별 통합 메커니즘(`Brand.set`/`Brand.get`,
|
||||
- **`Brand`** — 런타임 nominal 타입 판별 통합 메커니즘(**[2026-08-21 재작성]**
|
||||
인스턴스 브랜드 `Brand()` + `:register`/`:is`, 다중 태깅 허용,
|
||||
`isState`를 branded 타입 전부로 일반화) → **`base/brand-plan.md`**.
|
||||
- **`Tag` / `Attribute` 특수 키** → **`base/tag-plan.md`** /
|
||||
**`base/attribute-plan.md`**. 이 문서가 예전에 다루던 타입 파라미터화 문제
|
||||
|
|
|
|||
|
|
@ -2,20 +2,17 @@
|
|||
|
||||
> **[2026-08-13 아홉 번째 세션] `bind-system-plan.md`에서 분리됨.**
|
||||
> 자기 완결적인 유틸이라 디스패치 코어와 같은 파일에 있을 이유가 없었음.
|
||||
> **내용은 옮기기만 했고 결정은 하나도 안 바뀜.**
|
||||
>
|
||||
> **[2026-08-21 전면 재작성] 공유 레지스트리 + `Brand.get(x) -> tag`(객체당
|
||||
> 태그 하나)에서 **인스턴스 브랜드**(`Brand()` + `:register`/`:is`, 다중 태깅
|
||||
> 허용)로 바뀜.** 역전 원문은 `archive/brand-shared-registry-reversed.md`,
|
||||
> 근거 기록은 `reference/epoch-brand-composition.md`. **뒤집힌 건 API 표면
|
||||
> 하나뿐**이고 weak-key 레지스트리·테이블 아이덴티티·duck-typing 기각 근거·
|
||||
> predicate 합성은 전부 그대로다.
|
||||
|
||||
**상태**: base — 동작/구현 방식은 확정, **이름 `Brand` 자체만 용어 정리
|
||||
대기**(`question.md` 1번).
|
||||
|
||||
**⚠️ [2026-08-21] 이 문서를 전면 재작성하는 제안이 `research/`에 대기 중이다** —
|
||||
단일 공유 레지스트리 + `Brand.get(x) -> tag`(객체당 태그 하나)를 **인스턴스
|
||||
브랜드**(`Brand()` + `:register`/`:is`, **다중 태깅 허용**)로 바꾸는 안
|
||||
(`research/epoch-brand-composition.md`). 발단은 `Source`가 `SourceBrand`이면서
|
||||
동시에 `EpochBrand`여야 하는데 지금 구조로는 표현이 안 되는 것. **역조회
|
||||
`Brand.get`은 불필요로 확정**됐고, 아래 "포함 관계가 코드 모양에 드러난다"는
|
||||
성질은 **그대로 유지된다**(predicate 합성을 계속 쓰면 됨). **커밋된 M1 코드는
|
||||
아직 `Brand`를 안 쓰므로 전환 비용은 문서뿐.** 승격 전엔 이 문서가 정본.
|
||||
|
||||
## `Brand` — 런타임 nominal 타입 판별 통합 메커니즘, `isState`를 일반화 (2026-08-07 여덟 번째 세션)
|
||||
|
||||
**배경**: `isState`(2026-08-07 다섯 번째 세션 확정, `:Peek<<T>>(key):
|
||||
|
|
@ -37,62 +34,111 @@ Store인가/Tag인가" 판별, 또는 PropertyHandler의 `process` 내부에서
|
|||
읽기 부작용도 안 만든다 — 아래 duck-typing 기각 근거 두 개가 정확히 이
|
||||
한 줄에서 나온다.
|
||||
|
||||
**구현: 공유 weak-key 레지스트리 하나 + 테이블 아이덴티티를 태그로
|
||||
사용(문자열 아님).**
|
||||
## ⭐ 구현 — 인스턴스 브랜드, 브랜드마다 자기 weak 집합 하나 (2026-08-21 확정)
|
||||
|
||||
```
|
||||
local Brand = {}
|
||||
local registry = setmetatable({}, {__mode = "k"})
|
||||
**`Brand()`가 브랜드 객체 하나를 만든다.** 그 객체가 weak-key 집합 하나를
|
||||
들고, 값은 **자기가 속한 브랜드에 스스로 등록**한다.
|
||||
|
||||
function Brand.set(x, tag) registry[x] = tag end
|
||||
function Brand.get(x) return registry[x] end -- nil이면 quad가 모르는 값
|
||||
```lua
|
||||
local function Brand()
|
||||
local members = setmetatable({}, {__mode = "k"})
|
||||
return {
|
||||
register = function(self, x) members[x] = true end,
|
||||
is = function(self, x) return members[x] == true end,
|
||||
}
|
||||
end
|
||||
|
||||
-- 각 브랜드는 고유 테이블(빈 테이블이어도 됨) — 문자열 리터럴 아님
|
||||
local ObserverTag, EffectTag, TagTag, AttributeTag, TweenTag, BlockerTag,
|
||||
StateTag, SourceTag, StoreTag, SlotTag, RefTag, PreRefTag, PostRefTag,
|
||||
ModifierTag =
|
||||
{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}
|
||||
-- 각 타입이 자기 브랜드를 하나씩 소유
|
||||
local ObserverBrand, EffectBrand, TagBrand, AttributeBrand, TweenBrand,
|
||||
BlockerBrand, StateBrand, SourceBrand, StoreBrand, SlotBrand,
|
||||
RefBrand, PreRefBrand, PostRefBrand, ModifierBrand, EpochBrand =
|
||||
Brand(), Brand(), Brand(), Brand(), Brand(), Brand(), Brand(), Brand(),
|
||||
Brand(), Brand(), Brand(), Brand(), Brand(), Brand(), Brand()
|
||||
|
||||
-- 각 타입의 모든 생성 지점(Observer(...), Source(...), :With(...), Tag(...) 등)에서:
|
||||
Brand.set(newHandle, ObserverTag)
|
||||
ObserverBrand:register(newHandle)
|
||||
```
|
||||
|
||||
**문자열 대신 테이블 아이덴티티를 태그로 쓰는 이유(사용자 제안)** —
|
||||
Luau의 인터닝된 문자열 비교도 이미 사실상 O(1) 포인터 비교라 성능 차는
|
||||
무시할 만하지만, **오타 안전성**이 실질적 이득: 태그가 오타난 문자열
|
||||
리터럴("Oberver")이면 등록/조회 양쪽이 조용히 어긋나는데, 테이블
|
||||
레퍼런스는 잘못된 변수를 참조하면 즉시 드러나거나 최소한 진짜 다른 값이
|
||||
되어 헷갈릴 여지가 없음.
|
||||
|
||||
**`isX`는 `Brand`를 직접 노출 안 하고 각자 얇은 wrapper로 감쌈** —
|
||||
단순 항등인 경우(`isObserver(x) = Brand.get(x) == ObserverTag`)와, 상위
|
||||
관계(subtype)가 있어 **더 구체적인 브랜드 체크 위에 OR로 얹는** 경우
|
||||
(`isState`/`isRef`)로 갈림. **[정정, 2026-08-09 열한 번째 세션]** 후자를
|
||||
"집합 멤버십"(`t == A or t == B`, 플랫한 셋 체크)으로 구현하던 방식을
|
||||
"더 구체적인 predicate를 먼저 정의하고 그 위에 얹는" 합성 방식으로
|
||||
재정리 — 동작은 동일하지만, 어느 predicate가 다른 predicate를 내포하는지
|
||||
(포함 관계의 방향)가 코드 모양 자체에 드러나게 함:
|
||||
**⭐ 다중 태깅이 이 설계의 존재 이유다.** 한 값이 **여러 브랜드에 동시에**
|
||||
속할 수 있다 — 실제로 그게 필요한 자리가 있다:
|
||||
|
||||
```lua
|
||||
-- Source는 Source이면서 동시에 Epoch다 (base/state-epoch-plan.md)
|
||||
SourceBrand:register(source)
|
||||
EpochBrand:register(source)
|
||||
```
|
||||
|
||||
옛 모양(`Brand.get(x) -> tag`, 객체당 태그 하나)으로는 이걸 표현할 수 없었고,
|
||||
그게 재작성의 직접 발단이다 — `archive/brand-shared-registry-reversed.md`.
|
||||
|
||||
**부수 이득 — 외부 확장이 열린다.** `Source`가 아닌 원천(외부 시계 등)이
|
||||
`Epoch`로 참여하고 싶으면 `EpochBrand:register(self)` 한 줄이면 되고,
|
||||
`isEpoch` 구현을 고칠 필요가 없다. 사용자 논거: *"본인이 거기 속하면, 본인이
|
||||
직접 해당 브랜드를 가져와 등록하면 … `isXXXX`에서 각각의 구현을 넣을 필요가
|
||||
없어짐. 따라서 외부 확장도 쉬워진다."*
|
||||
|
||||
**역조회는 없다** — "이 값이 대체 무엇인가"를 되묻는 창구는 제공하지 않는다.
|
||||
코퍼스가 실제로 쓰는 건 전부 `isX` 형태의 **멤버십 질문**뿐이고, 역조회를 하는
|
||||
자리는 전수 조사에서 하나도 없었다(사용자 확인: *"확실히 의미가 없어진것 같습니다
|
||||
필요하진 않아요."*).
|
||||
|
||||
**브랜드 아이덴티티가 곧 테이블 레퍼런스다 — 문자열 태그를 안 쓰는 이유는
|
||||
그대로 유효(사용자 제안)**: Luau의 인터닝된 문자열 비교도 이미 사실상 O(1)
|
||||
포인터 비교라 성능 차는 무시할 만하지만, **오타 안전성**이 실질적 이득 —
|
||||
태그가 오타난 문자열 리터럴("Oberver")이면 등록/조회 양쪽이 조용히 어긋나는데,
|
||||
잘못된 브랜드 **변수**를 참조하면 즉시 드러나거나 최소한 진짜 다른 집합이
|
||||
되어 헷갈릴 여지가 없음. 새 모양에선 이게 더 강해진다 — 브랜드가 값이라
|
||||
`nil`을 인덱싱하면 그 자리에서 에러가 난다.
|
||||
|
||||
**메소드 이름이 소문자인 건 이 유틸의 기존 관례를 잇는 것**(`Brand.set`/
|
||||
`Brand.get`이 그랬다). quad 공개 표면의 PascalCase 메소드 관례(`:Get`/`:Set`/
|
||||
`:With`)와 다른데, `Brand`는 사용자가 직접 부르는 프리미티브가 아니라 base
|
||||
내부 유틸이고 사용자에게 노출되는 건 `isX` wrapper들이다 — 이름 자체가
|
||||
용어 정리 대기 항목이므로 케이싱도 그때 같이 본다(`question.md` 1번).
|
||||
|
||||
## `isX` wrapper — 포함 관계는 predicate 합성으로 (2026-08-09 열한 번째 세션)
|
||||
|
||||
**`isX`는 브랜드를 직접 노출 안 하고 각자 얇은 wrapper로 감쌈** — 단순
|
||||
항등인 경우(`isObserver(x) = ObserverBrand:is(x)`)와, 상위 관계(subtype)가
|
||||
있어 **더 구체적인 브랜드 체크 위에 OR로 얹는** 경우(`isState`/`isRef`)로
|
||||
갈림. **[정정, 2026-08-09 열한 번째 세션]** 후자를 "집합 멤버십"(플랫한 셋
|
||||
체크)으로 구현하던 방식을 "더 구체적인 predicate를 먼저 정의하고 그 위에
|
||||
얹는" 합성 방식으로 재정리 — 동작은 동일하지만, 어느 predicate가 다른
|
||||
predicate를 내포하는지(포함 관계의 방향)가 코드 모양 자체에 드러나게 함:
|
||||
|
||||
```lua
|
||||
local function isSource(x)
|
||||
return Brand.get(x) == SourceTag
|
||||
return SourceBrand:is(x)
|
||||
end
|
||||
local function isState(x)
|
||||
return isSource(x) or Brand.get(x) == StateTag -- Source가 State를 구조적으로 만족
|
||||
return isSource(x) or StateBrand:is(x) -- Source가 State를 구조적으로 만족
|
||||
end
|
||||
|
||||
local function isPreRef(x)
|
||||
return Brand.get(x) == PreRefTag
|
||||
return PreRefBrand:is(x)
|
||||
end
|
||||
local function isPostRef(x) -- [2026-08-14 아홉 번째 세션] PostRef 확정
|
||||
return Brand.get(x) == PostRefTag
|
||||
return PostRefBrand:is(x)
|
||||
end
|
||||
local function isRef(x)
|
||||
-- PreRef/PostRef가 Ref 런타임을 재사용 = 둘 다 Ref의 한 종류
|
||||
return isPreRef(x) or isPostRef(x) or Brand.get(x) == RefTag
|
||||
return isPreRef(x) or isPostRef(x) or RefBrand:is(x)
|
||||
end
|
||||
```
|
||||
|
||||
**⭐ 다중 태깅이 가능해져도 포함 관계는 계속 이 합성으로 쓴다.** *"`Source`를
|
||||
`StateBrand`에도 같이 등록하면 `isState`가 한 줄이 되지 않나"*는 하지 않는다 —
|
||||
등록 지점이 여러 곳에 흩어지면 "어느 브랜드에 등록하는 걸 빠뜨렸나"가 조용한
|
||||
버그가 되고, 포함 관계가 코드 모양에서 사라진다. **각 타입은 자기 브랜드에만
|
||||
등록하고, 포함 관계는 predicate 한 곳에 쓴다.** 사용자 확인: *"여전합니다.
|
||||
`PreRefBrand` 가 존재할테니. 거기에 `is()` 를 해서, 코드에 전부 드러나는거
|
||||
똑같습니다."*
|
||||
|
||||
- **예외는 "구조적 인터페이스"뿐** — `Source`를 `EpochBrand`에 같이 등록하는
|
||||
건 포함 관계가 아니라 **서로 다른 축의 계약을 동시에 만족**하는 것이라
|
||||
합성으로 표현할 수가 없다(`isEpoch`가 `isSource`를 알아야 할 이유가 없고,
|
||||
`Source`가 아닌 원천도 참여해야 한다). 이게 다중 등록의 정당한 용례다.
|
||||
|
||||
**정정 — `isSource`는 별도로 필요함, 다섯 번째 세션의 "불필요" 서술을
|
||||
뒤집음(2026-08-07 여덟 번째 세션).** 그때는 "State면 충분한 용도"만
|
||||
염두에 뒀지만, `Source`는 State보다 진짜로 더 많은 능력(`:Set`/`:Emit`)을
|
||||
|
|
@ -104,7 +150,7 @@ end
|
|||
문서가 서로 모순돼 있었음). `base/modifier-plan.md`의 "`isState(x): boolean` 필요" 절에 있던 "별도 `isSource` 불필요" 서술은
|
||||
`session/2026-08-07-08-none-sentinel-dispatch-brand.md`에서 이미 정정됨.
|
||||
|
||||
**갭 보강 — `isRef`/`isPreRef`/`isModifier`가 태그 목록에서 빠져있던 것
|
||||
**갭 보강 — `isRef`/`isPreRef`/`isModifier`가 목록에서 빠져있던 것
|
||||
추가(2026-08-07 열 번째 세션), 이후 `isRef`/`isPreRef` 관계 자체가
|
||||
재정정됨(2026-08-09 열한 번째 세션).** 처음엔 `isRef`/`isPreRef`를
|
||||
`isObserver`와 같은 단순 항등으로 두고 서로 배타적인 형제 브랜드로
|
||||
|
|
@ -113,9 +159,9 @@ end
|
|||
**`PreRef`도 "Ref 런타임을 그대로 재사용하는" 관계라 같은 포함
|
||||
방향(상위=Ref, 하위=PreRef)으로 다뤄야 일관적**이라는 지적으로 뒤집힘.
|
||||
|
||||
- **`isPreRef(x)`가 가장 구체적인 항등 체크**(`Brand.get(x) ==
|
||||
PreRefTag`), **`isRef(x)`는 그 위에 `Brand.get(x)==RefTag`를 OR로
|
||||
얹은 상위 개념** — 즉 이제 **`isRef(preRefInstance)`는 `true`.**
|
||||
- **`isPreRef(x)`가 가장 구체적인 항등 체크**(`PreRefBrand:is(x)`),
|
||||
**`isRef(x)`는 그 위에 `RefBrand:is(x)`를 OR로 얹은 상위 개념** — 즉
|
||||
**`isRef(preRefInstance)`는 `true`.**
|
||||
- **`(v=Ref)` children 배열 leaf 매치 핸들러(`Dispatch/Leaf.luau`)는
|
||||
이제 `isHandlable`을 `isRef(v) and not isPreRef(v) and not isPostRef(v)`로
|
||||
명시적으로 좁혀야 함**(**[2026-08-14 아홉 번째 세션]** `PostRef` 확정으로
|
||||
|
|
@ -124,41 +170,39 @@ end
|
|||
명시적으로 말해야 하는 모양으로 바뀜(두 pre-pass 소진이 이미 걸러줘
|
||||
정상 경로에선 거의 안 걸리지만, `base/ref-plan.md`의 두 동적 경로 가드
|
||||
Handler와 이 조합이 같이 "일반 Ref 경로를 절대 타면 안 됨"을 보장).
|
||||
`isModifier`도 같은 단순 항등(`Brand.get(x) == ModifierTag`, 상위 개념
|
||||
없음).
|
||||
`isModifier`도 같은 단순 항등(`ModifierBrand:is(x)`, 상위 개념 없음).
|
||||
- **`PostRef`도 `PreRef`와 완전히 같은 포함 방향** — `Ref` 런타임을 그대로
|
||||
재사용하고 브랜드 태그만 다르므로 `isRef(postRefInstance)`도 `true`.
|
||||
재사용하고 브랜드만 다르므로 `isRef(postRefInstance)`도 `true`.
|
||||
즉 `isRef`는 이제 `{Ref, PreRef, PostRef}` 셋을 통과시키는 상위 개념이고,
|
||||
`isPreRef`/`isPostRef`가 각각 가장 구체적인 항등 — `PreRef`/`PostRef`
|
||||
사이엔 포함 관계가 없음(서로 배타적인 형제).
|
||||
|
||||
**같은 이유로 `isSlot`/`isEffect`도 명시(2026-08-09 세션)** —
|
||||
`Brand.get(x) == SlotTag`/`Brand.get(x) == EffectTag`인 단순 항등
|
||||
predicate, 태그 자체는 원래부터 목록에 있었지만(`SlotTag`) `isX`
|
||||
wrapper로 명시적으로 안 적혀 있던 것을 `base/modifier-plan.md`의
|
||||
`SlotBrand:is(x)`/`EffectBrand:is(x)`인 단순 항등 predicate, 브랜드
|
||||
자체는 원래부터 목록에 있었지만 `isX` wrapper로 명시적으로 안 적혀 있던
|
||||
것을 `base/modifier-plan.md`의
|
||||
"Modifier 필드에 핸들러 계층 값(Ref/PreRef/PostRef/Observer/Effect/Slot/Modifier)이
|
||||
들어오면 즉시 error" 절이 필요로 해서 이번에
|
||||
같이 적음.
|
||||
|
||||
**[정정, 2026-08-18 구현 전 QA] `Brand`는 아무 의존성도 갖지 않는다 —
|
||||
`None`을 위한 특수 분기를 두지 않는다.** 옛 서술은 `Brand.get(x)`가 범용
|
||||
introspection 창구 역할까지 겸하려면 `None`도 빠지면 안 되므로 *"`Brand.get`이
|
||||
내부적으로 `x == None`을 먼저 확인하는 특수 분기를 하나 두고"* 그 뒤에
|
||||
레지스트리 조회로 폴백하며, `isNone`이 그 특수 분기의 구현체가 된다고 했다.
|
||||
사용자 판정: *"Brand 는 None 을 참조할 필요는 없음. Brand 자체는 아에
|
||||
의존성 없고, None 도 테깅되는건 맞으나, isNone 대신 필요한 곳에서 v ==
|
||||
None 하면 되는 일, 혹은 isNone 구현 자체를 그렇게 해주면 되는 일."*
|
||||
`None`을 위한 특수 분기를 두지 않는다.** 옛 서술은 판별 창구가 범용
|
||||
introspection을 겸하려면 `None`도 빠지면 안 되므로 *"내부적으로 `x == None`을
|
||||
먼저 확인하는 특수 분기를 하나 두고"* 그 뒤에 레지스트리 조회로 폴백하며,
|
||||
`isNone`이 그 특수 분기의 구현체가 된다고 했다. 사용자 판정: *"Brand 는 None 을
|
||||
참조할 필요는 없음. Brand 자체는 아에 의존성 없고, None 도 테깅되는건 맞으나,
|
||||
isNone 대신 필요한 곳에서 v == None 하면 되는 일, 혹은 isNone 구현 자체를
|
||||
그렇게 해주면 되는 일."*
|
||||
|
||||
- **`Brand → None` 의존을 만들지 않는다** — 특수 분기를 넣는 순간 가장
|
||||
밑바닥 유틸이어야 할 `Brand`가 다른 프리미티브를 참조하게 된다.
|
||||
- **`isNone`은 그냥 `v == None`** — 그런 이름의 함수를 두더라도 구현이
|
||||
레퍼런스 비교 한 줄이면 된다. 싱글턴이라 그게 제일 싸고 정확하다는 판단
|
||||
자체는 그대로 유효.
|
||||
- **`None` 자체를 레지스트리에 평범하게 태깅하는 건 무방**(사용자가
|
||||
허용) — 그러면 특수 분기 없이도 `Brand.get(None)`이 답을 준다. 즉
|
||||
"범용 introspection 창구"를 지키고 싶으면 **특수 분기가 아니라 평범한
|
||||
등록**으로 지킨다. 등록을 안 하기로 하면 `None`은 그 창구에서 빠지는
|
||||
것을 받아들인다 — 어느 쪽이든 `Brand` 쪽 코드는 그대로다.
|
||||
- **`None`을 `NoneBrand`에 평범하게 등록하는 것 자체는 무방**(사용자가
|
||||
허용) — 다만 **[2026-08-21]** 역조회 창구가 없어졌으므로 등록의 유일한
|
||||
효용은 `NoneBrand:is(x)`뿐이고, 그건 `v == None`이 더 싸다. 어느 쪽이든
|
||||
`Brand` 쪽 코드는 그대로다.
|
||||
|
||||
**duck-typing(예: `type(x) == "table" and x.Compute ~= nil`)을 쓰지 않는
|
||||
이유 — 서로 독립된 두 가지(2026-08-20 `B-4`에서 분리 명시)**:
|
||||
|
|
@ -173,11 +217,13 @@ None 하면 되는 일, 혹은 isNone 구현 자체를 그렇게 해주면 되
|
|||
`isHandlable` 계약(`base/dispatch-core-plan.md`의 "핸들러 계약" 절)과
|
||||
정면으로 부딪힌다. 최악의 경우 엔진이 죽는 상황까지 있다.
|
||||
|
||||
weak-key 레지스트리 조회는 포인터 해싱 한 번이라 `pcall`도, 오인도 없다.
|
||||
weak-key 조회는 포인터 해싱 한 번이라 `pcall`도, 오인도 없다. 브랜드마다
|
||||
집합이 갈려도 **조회 비용은 그대로 한 번**이고, `isState`처럼 OR로 합성된
|
||||
predicate만 최대 브랜드 수만큼 조회한다(전부 2~3개).
|
||||
weak-key 레지스트리는 rbvm 네임스페이스 추적(`base/lifecycle-pattern.md`)과
|
||||
같은 이미 확정된 패턴 재사용이라 새 아이디어 아님 — weak 키라 등록된 값이
|
||||
GC되면 레지스트리 엔트리도 자동으로 사라짐(살려두는 목적의 강참조
|
||||
레지스트리인 Observer의 `:Subscribe` 레지스트리와는 반대 성격).
|
||||
GC되면 엔트리도 자동으로 사라짐(살려두는 목적의 강참조 레지스트리인
|
||||
Observer의 `:Subscribe` 레지스트리와는 반대 성격).
|
||||
|
||||
**Luau 타입 narrowing은 자동으로 안 됨 — 명시적 `::` 캐스팅 필요(사용자
|
||||
확인, Luau가 원래 그렇게 동작함).** `isX(v)`가 참이어도 Luau 컴파일러가
|
||||
|
|
@ -187,6 +233,10 @@ State<any> ... end`처럼 런타임 검증 뒤 명시적 캐스팅을 붙이는
|
|||
패턴. 여전히 duck-typing/`pcall`보다 훨씬 안전하니 가치는 있음, 다만
|
||||
"자동 narrowing"을 기대하면 안 됨.
|
||||
|
||||
**이름은 전부 가칭 — `Brand`/`ObserverTag`류 포함 용어 정리 대상,
|
||||
`.claude/question.md`에 반영.**
|
||||
**마일스톤**: **커밋된 M1 코드는 아직 `Brand`를 안 쓴다**(`quad-base/src`는
|
||||
`init.luau`/`Relate.luau`/`Debug`뿐, 2026-08-21 확인) — 이 재작성의 전환
|
||||
비용은 문서뿐이었다. 실제 구현은 각 프리미티브가 만들어지는 마일스톤에서
|
||||
같이 간다.
|
||||
|
||||
**이름은 전부 가칭 — `Brand`/`ObserverBrand`류 포함 용어 정리 대상,
|
||||
`.claude/question.md`에 반영.**
|
||||
|
|
|
|||
|
|
@ -294,8 +294,8 @@ quad의 전파 모델은 `base/source-state-plan.md`의 "전파 모델 확정"
|
|||
### 그래서 정정 후 그림 (권고)
|
||||
|
||||
- **emit(무효화 신호)은 구독자에게 전파된다.** State가 이미 `invalid`인지는
|
||||
전파 여부와 무관. **[2026-08-21 갱신]** "항상"은 빠졌다 — 소스 에포크
|
||||
비교를 채택하면서 **같은 에포크가 두 번째로 도착하면 접힌다**
|
||||
전파 여부와 무관. **[2026-08-21 갱신]** "항상"은 빠졌다 — `Epoch` 리비전
|
||||
비교를 채택하면서 **같은 `Epoch`의 같은 리비전이 두 번째로 도착하면 접힌다**
|
||||
(`base/state-epoch-plan.md`). `invalid`로 접는 것이 금지인 건 그대로.
|
||||
- 다이아몬드 중복 *재계산*은 **pull-recompute가 이미 구조적으로**
|
||||
막음(`architecture.md`의 서술이 맞음) — 별도 장치 불필요.
|
||||
|
|
@ -330,8 +330,8 @@ quad의 전파 모델은 `base/source-state-plan.md`의 "전파 모델 확정"
|
|||
> 위로 올라가서 받아와서 계산 처리된 게 들어오고 cache가 쓰인 다음
|
||||
> `invalid`가 꺼짐.
|
||||
|
||||
**[2026-08-21 후속]** 위는 2026-08-14 시점 확정 원문이다. 그 뒤 소스 에포크
|
||||
비교를 채택하면서 "항상"에 예외가 하나 생겼다 — **같은 소스의 같은 에포크가
|
||||
**[2026-08-21 후속]** 위는 2026-08-14 시점 확정 원문이다. 그 뒤 `Epoch` 리비전
|
||||
비교를 채택하면서 "항상"에 예외가 하나 생겼다 — **같은 `Epoch`의 같은 리비전이
|
||||
두 번째로 도착하면 접힌다**(`base/state-epoch-plan.md`). `invalid`로 접는 것이
|
||||
금지라는 이 문단의 요지는 그대로다.
|
||||
|
||||
|
|
|
|||
|
|
@ -266,21 +266,41 @@ Effect의 의존성이 될 방법이 아예 없다.** 사용자 제기: *"Effect
|
|||
성립하지 않는 게 확인됐다(`base/gate-plan.md`의 8번) — 설치 구간엔 어떤
|
||||
`Set`도 안 일어나 게이트에 쌓이는 소스가 없으므로 게이트가 내보낼 것 자체가
|
||||
없다. 설치 중 발화를 누르는 플래그 하나면 되고 새 메커니즘이 필요 없다.
|
||||
- **⚠️ [2026-08-21 신설 — 미해결] 의존성들이 공통 상류를 공유하면 한 파동에
|
||||
`fn`이 여러 번 돈다.** `A → b`, `A → c`, `Effect(fn, b, c)`에서 `A:Set()`
|
||||
한 번에 `b`가 자기 observer를, `c`가 자기 observer를 **각각 정당하게** 깨워
|
||||
`fn`이 두 번 돈다. **소스 에포크 dedup으로는 안 접힌다** — `b`와 `c`는 서로
|
||||
다른 노드라 접어줄 **공통 하류가 없고**, 둘 다 규칙 1에 정당하게 걸린다
|
||||
(`base/state-epoch-plan.md`). 위의 "설치 구간 억제"도 이건 안 덮는다(그건
|
||||
등록 시점만).
|
||||
- **해법 후보**: `Effect`가 **자기 `EpochMap`을 하나 들고** 각 내부 observer가
|
||||
그걸 `Update`해서 첫 번째만 통과시키는 것 —
|
||||
`research/epoch-brand-composition.md`가 정확히 이걸 노린 제안이고
|
||||
승격 대기 중이다. (검토했다 접은 대안: deps를 하나의 파생 노드로 수렴시켜
|
||||
다이아몬드 dedup에 태우기 — 노드가 늘고 "N deps → N observers" 구조를
|
||||
바꿔야 해서 위 안보다 못하다.)
|
||||
- **그 제안을 안 쓴다면** `useEffect`처럼 "N번 돌아도 무방"으로 계약을
|
||||
명시하는 선택지도 있다. **어느 쪽이든 M3 구현 전에 정할 것.**
|
||||
- **⭐ [2026-08-21 해소] 의존성들이 공통 상류를 공유해도 한 파동에 `fn`은 한 번만
|
||||
돈다 — `Effect`가 자기 `EpochMap`을 하나 든다.** 갭은 실재했다: `A → b`,
|
||||
`A → c`, `Effect(fn, b, c)`에서 `A:Set()` 한 번에 `b`가 자기 observer를,
|
||||
`c`가 자기 observer를 **각각 정당하게** 깨워 `fn`이 두 번 돌았다. State 층
|
||||
dedup으론 안 접힌다 — `b`와 `c`는 서로 다른 노드라 접어줄 **공통 하류가
|
||||
없고**, 둘 다 §4의 1번 규칙에 정당하게 걸린다(`base/state-epoch-plan.md`).
|
||||
위의 "설치 구간 억제"도 이건 안 덮는다(그건 등록 시점만).
|
||||
- **확정된 해법**: `EffectHandle`이 `EpochMap`을 **하나** 들고, 각 내부
|
||||
Observer의 클로저가 받은 `from`으로 그걸 `Update`한다. **`true`일 때만
|
||||
`fn`을 부른다.** `Effect`가 곧 그 dep들의 **공통 하류**가 되므로, 한
|
||||
파동에 몇 개가 깨우든 첫 번째만 통과한다.
|
||||
```lua
|
||||
-- 각 dep의 내부 Observer가 공통으로 거는 클로저
|
||||
function(self, from)
|
||||
if handle._installing then return end -- 설치 구간 억제 (아래)
|
||||
if handle._epochs:Update(from) then
|
||||
handle:Rerun() -- 직전 cleanup 호출 후 fn 재실행
|
||||
end
|
||||
end
|
||||
```
|
||||
**⚠️ 억제 플래그가 `Update`보다 먼저여야 한다** — 등록 시점의 즉시 1회
|
||||
실행에는 `from`이 없어서(`nil`, `base/source-state-plan.md`의
|
||||
"`state:Observer(fn)`" 절) `Update(nil)`이 들어가게 된다. 순서를 뒤집으면
|
||||
설치 발화가 맵을 건드려 **그 파동의 첫 진짜 emit이 접힐** 수 있다
|
||||
(2026-08-21 커밋 전 `/code-review high` 발견).
|
||||
- **`Ref` 의존성은 이 맵에 안 낀다** — `Ref`는 `Epoch`가 아니고
|
||||
`:Callback`으로 발화하므로 `from`이 없다. `Ref` 쪽 발화는 그대로 매번
|
||||
`fn`을 돌린다(`Ref`는 반복 재설정마다 도는 게 계약이고, 공통 상류 문제
|
||||
자체가 없다).
|
||||
- **검토했다 접은 대안**: deps를 하나의 파생 노드로 수렴시켜 다이아몬드
|
||||
dedup에 태우기 — 노드가 늘고 "N deps → N observers" 구조를 바꿔야 해서
|
||||
위 안보다 못하다. `useEffect`처럼 "N번 돌아도 무방"으로 계약을 느슨하게
|
||||
두는 선택지도 있었으나, 접는 비용이 맵 하나뿐이라 채택 안 함.
|
||||
- 근거 기록은 `reference/epoch-brand-composition.md`(이 갭이 `EpochMap`
|
||||
분리의 직접 발단이었다).
|
||||
- **leaf dedup/cascade가 전부를 덮어야 한다** — 의존성이 N개면 내부 Observer도
|
||||
N개라, `EffectHandle`의 bind/unbind cascade와 dedup 분기가 **그 전부**를
|
||||
같이 처리해야 한다(위 `E-10`/`EF-5`와 같은 함정). 사용자 확인: *"어차피
|
||||
|
|
|
|||
|
|
@ -16,11 +16,13 @@ GateNode(ComputeNode 처럼) 생성된다 그리고 Blocker 는 해당 내부
|
|||
표면을 주기도 하구요."* 아래 본문 중 "공개 프리미티브로 꺼낸다"류 서술은 그
|
||||
이전 시점 표현이니 이 배너 기준으로 읽을 것.
|
||||
|
||||
**⚠️ [2026-08-21] emit 페이로드 표현을 바꾸는 제안이 `research/`에 대기 중**
|
||||
(`research/epoch-brand-composition.md`) — 하류가 게이트 identity를 한 번도 안
|
||||
쓰므로 페이로드를 `Epoch|{Epoch}`로 일반화하는 안. 아래 4번의 **기제(흡수 집합,
|
||||
flush 시 스왑, 게이트-게이트 unfold, 빈 배치 무통지)는 그대로 유효**하고 넘기는
|
||||
값의 타입만 바뀐다. 승격 전엔 이 문서가 정본.
|
||||
**⚠️ [2026-08-21 반영 완료] emit 페이로드는 `Epoch | EpochSet`이다**
|
||||
(`EpochSet = { [Epoch]: true }` — **배열이 아니라 집합**,
|
||||
`base/state-epoch-plan.md` §3) — 하류가
|
||||
게이트 identity를 한 번도 안 쓰므로 게이트 노드 자체는 안 싣고 **떼어낸
|
||||
`Epoch` 집합 스냅샷**만 넘긴다(근거 기록은
|
||||
`reference/epoch-brand-composition.md`). 아래 4번의 기제(흡수 집합, flush 시
|
||||
스왑, 게이트-게이트 unfold, 빈 배치 무통지)는 그대로다.
|
||||
|
||||
**한 줄**: `Blocker`가 쓰던 "게이티드 State 노드"를 한 겹 일반화해서, **상류
|
||||
emit을 가로채 내려보낼지 말지를 정책이 정하는** 노드를 **`state:Gate(setup)`
|
||||
|
|
@ -119,29 +121,29 @@ end)
|
|||
`Debounce`/`Throttle`도 emit-gate로 확정돼 있어(`base/debounce-throttle-plan.md`
|
||||
§4) `Gate`도 같은 계약으로 통일한다. 공개 계약 문구: **"게이트를 통과하지
|
||||
않은 값도 `:Get()`으로는 보인다."**
|
||||
`base/state-epoch-plan.md`가 이 계약에 **의존**한다 — 그 문서 §5의 3번이
|
||||
"게이트를 에포크 경계로 만드는" 대안을 기각한 이유가 정확히 이 계약을
|
||||
뒤집지 않기 위해서다.
|
||||
`base/state-epoch-plan.md`가 이 계약에 **의존**한다 — 그 문서가 "게이트를
|
||||
에포크 경계로 만드는" 대안을 기각한 이유가 정확히 이 계약을 뒤집지 않기
|
||||
위해서다 — 그 문서 §8의 "기각된 대안 — 게이트를 에포크 경계로" 항목.
|
||||
4. **[2026-08-21 해소] 게이트가 유보했다 내보내는 emit — `emit(self)` + 흡수
|
||||
집합.** 문제는 실재했다: 확정 `setup`은 `(emit: () -> ()) -> (() -> ())`라
|
||||
양쪽 다 source를 안 받는데 `base/state-epoch-plan.md`의 수신 규칙은 전부
|
||||
`[source]` 키로 판정하므로, `blocker:Off()`가 묶어뒀던 배치 emit이 아무
|
||||
양쪽 다 출처를 안 받는데 `base/state-epoch-plan.md`의 수신 규칙은 전부
|
||||
`[epoch]` 키로 판정하므로, `blocker:Off()`가 묶어뒀던 배치 emit이 아무
|
||||
원천도 못 지목한 채 도착해 **하류에서 삼켜진다**(2026-08-21
|
||||
`/code-review high` 발견).
|
||||
|
||||
**확정 기제**(사용자 안, 에이전트가 냈던 (a) `nil` 전체 확인 / (b) 소스마다
|
||||
개별 emit은 기각 — 각각 O(1) 판정을 깨거나 `Blocker`의 "정확히 1회"를 깬다):
|
||||
|
||||
- `GateNode`가 **자기를 거쳐간 소스 집합**을 들고 있는다 —
|
||||
`withheld : { [source] : true }`, **weak key**(소스 맵과 같은 이유,
|
||||
`state-epoch-plan.md` §5의 5번).
|
||||
- `GateNode`가 **자기를 거쳐간 `Epoch` 집합**을 들고 있는다 —
|
||||
`withheld : { [epoch] : true }`, **weak key**(`EpochMap`과 같은 이유,
|
||||
`state-epoch-plan.md` §3).
|
||||
- **⭐ [2026-08-21 단순화] 통과와 유보를 구분하지 않는다.** 상류 emit이
|
||||
오면 **정책을 실행하기 전에 무조건** 그 출처를 `withheld`에 넣고, 그
|
||||
다음 정책을 실행한다. 정책이 `emit()`을 부르면 게이트가 하류에 전파하고,
|
||||
안 부르면 집합에 그대로 쌓인다.
|
||||
- **⚠️ [2026-08-21 `/code-review high`] "무조건"은 *정책의 통과/유보와
|
||||
무관하게*라는 뜻이지 *수신 규칙을 건너뛴다*는 뜻이 아니다.** 게이트도
|
||||
평범한 노드처럼 `state-epoch-plan.md` §2의 규칙 1~3을 **먼저** 적용하고,
|
||||
평범한 노드처럼 `state-epoch-plan.md` §4의 규칙 1~3을 **먼저** 적용하고,
|
||||
3번(둘 다 같음)으로 삼켜진 emit은 **정책도 안 돌고 집합에도 안
|
||||
들어간다.** 안 그러면 다이아몬드(`A→B→G`, `A→C→G`)에서 `A:Set()` 한
|
||||
번에 정책이 두 번 돌아, `Throttle`의 leading 통과 직후 두 번째 emit이
|
||||
|
|
@ -166,17 +168,17 @@ end)
|
|||
emitDownstream(self, batch) -- 떼어낸 batch를 페이로드로 넘긴다
|
||||
```
|
||||
하류가 순회하는 것은 `gate._withheld`가 아니라 **받은 `batch`** 다.
|
||||
중첩 파동은 새 테이블에 쌓이므로 바깥 전파와 안 섞인다. 같은 에포크가
|
||||
중첩으로 두 번 도달하는 경우는 애초에 문제가 아니다 — 하류 count가 이미
|
||||
중첩 파동은 새 테이블에 쌓이므로 바깥 전파와 안 섞인다. 같은 리비전이
|
||||
중첩으로 두 번 도달하는 경우는 애초에 문제가 아니다 — 하류 맵이 이미
|
||||
최신이라 규칙 3으로 삼켜진다.
|
||||
- **그래서 게이트는 언제나 자기(와 그 배치)를 출처로 낸다** — 그냥
|
||||
통과시킬 때도 상류 출처를 그대로 넘기지 않는다. 사용자: *"후행 노드들은 한개가 지연된거로
|
||||
생각이 될 수 있겠지만, 사실 여기서 지연과 비지연을 구분할 이유가
|
||||
없습니다."* 하류가 보는 차이는 집합의 원소가 하나냐 여럿이냐뿐이고 판정
|
||||
규칙은 완전히 같다.
|
||||
- 하류가 **평범한 노드**면, 출처가 `GateNode`일 때 그 집합을 순회하며 각
|
||||
소스에 평소 규칙(1~3)을 적용하고, 하나라도 걸리면 **받은 출처를 그대로**
|
||||
더 아래로 넘긴다(`state-epoch-plan.md` §2).
|
||||
- 하류가 **평범한 노드**면 그냥 `EpochMap:Update(batch)`가 집합을 순회하고,
|
||||
하나라도 걸리면 **받은 배치를 그대로** 더 아래로 넘긴다
|
||||
(`state-epoch-plan.md` §4 — 단일이든 집합이든 같은 규칙이다).
|
||||
- **⭐ [2026-08-21 신설] 하류가 또 다른 게이트면 — 받은 집합을 풀어
|
||||
자기 `withheld`에 합친다.** 게이트가 게이트 emit을 받는 경우가 정의돼
|
||||
있지 않던 구멍이었다(사용자 발견). **출처를 그대로 넘기면 안 된다** —
|
||||
|
|
@ -184,27 +186,29 @@ end)
|
|||
하류 게이트가 그 참조만 들고 유보했다가 나중에 풀면 이미 지나간 배치를
|
||||
내보내게 된다. 그래서 수신 시점에 **풀어서 옮겨 담아야** 한다:
|
||||
```
|
||||
-- 출처가 Source면 그 하나를, 게이트 배치면 그 배치 전부를 편다
|
||||
for source in unfold(origin) do
|
||||
self._withheld[source] = true
|
||||
-- 출처가 Epoch 하나면 그 하나를, 배치면 그 배치 전부를 편다
|
||||
for epoch in unfold(from) do
|
||||
self._withheld[epoch] = true
|
||||
end
|
||||
-- 그 다음 평소대로 정책 실행
|
||||
```
|
||||
게이트가 몇 겹으로 겹쳐도 각 층이 자기 집합을 들고 있으므로 어느 층이
|
||||
먼저 풀리든 정보가 안 샌다.
|
||||
- **게이트의 `sourceEmitMap`은 수신 때가 아니라 실제로 전파할 때** 갱신한다
|
||||
(집합 전체에 대해 한꺼번에). 그래야 "내가 하류로 던진 에포크"라는 맵의
|
||||
뜻이 게이트에서도 참이 된다 — 유보 중 같은 에포크가 다른 경로로 또 오면
|
||||
규칙 2로 걸려 정책을 한 번 더 태우는데, 이미 집합에 있으므로 무해하다.
|
||||
- **게이트의 `emitEpochMap`은 수신 때가 아니라 실제로 전파할 때** 갱신한다
|
||||
(집합 전체에 대해 한꺼번에, `:Sync(batch)`). **이건 `state-epoch-plan.md`
|
||||
§4 의사코드의 유일한 예외이고, 그 문서에 예외로 기록돼 있다.** 그래야 "내가 하류로 던진
|
||||
리비전"이라는 맵의 뜻이 게이트에서도 참이 된다 — 유보 중 같은 리비전이
|
||||
다른 경로로 또 오면 규칙 2로 걸려 정책을 한 번 더 태우는데, 이미 집합에
|
||||
있으므로 무해하다.
|
||||
- **⭐ [2026-08-21 `/code-review high`] emit 없이 푸는 경로는 집합을
|
||||
*버려야* 한다.** `blocker:OffWithoutEmit()`은 정의상 "밀린 전파를 버리며
|
||||
끈다"(`base/blocker-plan.md`)이므로, 그 경로도 **`withheld`를 비운다**
|
||||
(전파는 안 하고 새 테이블로 스왑). 안 그러면 `Dispatch.drive`의 배치
|
||||
게이팅이 매 프레임 `On()` → … → `OffWithoutEmit()`을 도는 동안 집합이
|
||||
**단조 증가**하고, 나중에 아무 소스나 한 번 통과하는 순간 **버리기로 했던
|
||||
옛 소스들이 같이 실려 나가** 하류가 폐기된 통지로 무효화된다.
|
||||
- 그렇게 비우고 나면 하류의 `sourceEmitMap`은 뒤에 남지만, 그 소스의 다음
|
||||
진짜 emit이 규칙 1/2로 걸려 **스스로 낫는다** — 별도 조치 불필요.
|
||||
**단조 증가**하고, 나중에 아무 `Epoch`나 한 번 통과하는 순간 **버리기로
|
||||
했던 옛 원천들이 같이 실려 나가** 하류가 폐기된 통지로 무효화된다.
|
||||
- 그렇게 비우고 나면 하류의 `emitEpochMap`은 뒤에 남지만, 그 `Epoch`의
|
||||
다음 진짜 emit이 규칙 1/2로 걸려 **스스로 낫는다** — 별도 조치 불필요.
|
||||
|
||||
**⭐ 그래서 `setup` 시그니처는 안 바뀐다.** 집합을 채우는 건 정책이 아니라
|
||||
**노드**이고, 노드는 정책이 뭘 하는지 들여다볼 필요조차 없다(위 단순화).
|
||||
|
|
|
|||
|
|
@ -134,8 +134,8 @@ value)`류 base 유틸을 문서가 `inst`라고만 부르는 것과 같은 관
|
|||
|
||||
### `OnDestroyed(fn)`
|
||||
|
||||
`Effect(function() return fn end)`를 반환하는 팩토리. `Effect(fn, state?)`가
|
||||
`state` 생략 시 "설치 시 즉시 1회 실행 + 반환값이 leaf 사망 시 정확히
|
||||
`Effect(function() return fn end)`를 반환하는 팩토리. `Effect(fn, ...deps)`가
|
||||
**deps 생략 시** "설치 시 즉시 1회 실행 + 반환값이 leaf 사망 시 정확히
|
||||
1회 호출되는 cleanup"이라는 기존 계약(`base/effect-plan.md` 28행)을
|
||||
그대로 재사용 — 다만 여기서는 **설치 단계에서 실행되는 함수가 `fn`
|
||||
자신이 아니라 `function() return fn end`라는 래퍼**라는 점에 주의.
|
||||
|
|
|
|||
|
|
@ -1955,7 +1955,7 @@ nil/None 금지)는 그대로.
|
|||
> 않는다**는 것이었다(`setLength` 슬롯이 하나뿐이라 원리적으로 불가능).
|
||||
> **결론: `materializeSlotTree`(부기) + `mountSlotTree`(물리)로 분해하고
|
||||
> `attachSlot`은 그 둘을 부르는 두 줄짜리 래퍼로 남긴다** — 이름/시그니처/
|
||||
> 호출부 전부 불변. 근거 기록은 `research/slot-attach-decomposition.md`.
|
||||
> 호출부 전부 불변. 근거 기록은 `reference/slot-attach-decomposition.md`.
|
||||
|
||||
**✅ [해결, 2026-08-18 구현 전 QA 2라운드 후속]** 아래가 재사용하는
|
||||
`Dispatch.setLength`/`setOffsetSource`/`recompute`가 배치 등록 중 크래시할
|
||||
|
|
@ -2006,7 +2006,7 @@ weak 키로 받음) — **Slot 자신을 owner 키로 재사용하면 최상위
|
|||
-- [전면 재작성, 2026-08-21 구현 전 QA 4라운드 확정] 옛 단일 `attachSlot`을
|
||||
-- **비공개 재귀 둘 + 얇은 공개 진입점**으로 분해. 공개 표면(이름/시그니처/
|
||||
-- 호출부 셋)은 하나도 안 바뀐다 — 쪼갠 건 함수가 아니라 **재귀**다.
|
||||
-- 근거와 대안 비교는 `research/slot-attach-decomposition.md`.
|
||||
-- 근거와 대안 비교는 `reference/slot-attach-decomposition.md`.
|
||||
|
||||
-- (1) 부기만 만든다. 물리 마운트(`Parent` 대입)를 단 한 줄도 안 한다.
|
||||
local function materializeSlotTree(slot, physicalTarget, ownerKey, position)
|
||||
|
|
@ -2086,7 +2086,7 @@ end
|
|||
-- 아래 `acc`가 `slot.Offset:Get()`에서 시작하는데, 그 값이 최종값이 되는 건
|
||||
-- materialize의 마지막 `recompute`가 끝난 뒤다. 순서를 뒤집거나 materialize를
|
||||
-- 건너뛰고 부르면 물리 삽입 위치가 조용히 어긋난다(공개 `attachSlot`이 둘을
|
||||
-- 붙여 부르는 것이 이 계약의 전부 — `research/slot-attach-decomposition.md`의
|
||||
-- 붙여 부르는 것이 이 계약의 전부 — `reference/slot-attach-decomposition.md`의
|
||||
-- "prepare만 하고 mount 안 한 중간 상태" 항목이 아직 열려 있는 이유이기도 하다).
|
||||
local function mountSlotTree(slot, physicalTarget)
|
||||
slot._mounted = true
|
||||
|
|
@ -2117,7 +2117,7 @@ end
|
|||
```
|
||||
|
||||
**왜 쪼갰나 — 이득 넷**(상세와 기각된 대안은
|
||||
`research/slot-attach-decomposition.md`):
|
||||
`reference/slot-attach-decomposition.md`):
|
||||
|
||||
1. **C6와 C7이 처음으로 동시에 만족된다.** 옛 코드는 `setLength`의 자리가
|
||||
하나뿐이라 "최종값으로 등록"(C6)과 "부기가 물리보다 먼저"(C7) 중 하나를
|
||||
|
|
|
|||
|
|
@ -94,6 +94,18 @@ RefSource라는 별도 타입은 폐기**하는 쪽으로 수렴.
|
|||
불필요 — Source 객체 자체가 저장소 역할을 함. 이 모델은 이전에
|
||||
검토했던 "State를 weak table로 캐싱" 절충안보다 더 싸다(래퍼 생성/
|
||||
캐싱 단계 자체가 사라짐).
|
||||
- **⭐ [2026-08-21 확정] `Source`는 `Epoch` 인터페이스도 같은 방식으로
|
||||
구조적으로 만족한다** — `type Epoch = { Revision: number }`이고, `:Set()`/
|
||||
`:Emit()`이 그 `Revision`을 **갱신한다**(직전과 다른 값으로 — `Epoch`
|
||||
계약이 요구하는 건 "다르다"뿐이라 방향은 계약이 아니고, 확정된 연산
|
||||
`bit32.bnot(-rev)`는 실제로 **감소**한다). **`Revision`은 공개 필드여야
|
||||
한다** — 비공개면 구조적 만족이 타입 레벨에서 성립하지 않는다(사용자:
|
||||
*"그래야 타입 상 `Source` 가 `Epoch`를 만족해요."*). `Store`와 달리
|
||||
`Source`는 키가 사용자 것이 아니라 예약 이름이 늘어도 충돌하지 않는다.
|
||||
런타임 판별은 `SourceBrand`와 **동시에** `EpochBrand`에 등록하는 것으로
|
||||
하고(다중 태깅, `base/brand-plan.md`), `State`를 만족하는 관계와 달리 이건
|
||||
포함 관계가 아니라 **다른 축의 계약**이라 predicate 합성으로는 표현되지
|
||||
않는다. 계약 전량은 `base/state-epoch-plan.md` §2가 소스.
|
||||
- **이 서브타입 관계는 `quad2-try`에서 기각한 컴포넌트/클래스 OOP 상속과는
|
||||
다른 층위.** 그때 금지한 건 사용자가 짜는 컴포넌트 계층 구조(`Class:Extend()`류
|
||||
매직)였고, 지금은 두 프리미티브 타입 사이의 구조적 서브타이핑(런타임
|
||||
|
|
@ -180,14 +192,6 @@ Handler가 애초에 다른 층위. 관련해서 Handler를 담는 엔진(`Dispa
|
|||
왜 프리미티브가 아니라 탑레벨 싱글톤인지는 `base/dispatch-core-plan.md`의
|
||||
"Dispatch는 프리미티브가 아니다" 절 참고.
|
||||
|
||||
> **⚠️ [2026-08-21] 이 문서의 두 자리를 바꾸는 제안이 `research/`에 대기 중**
|
||||
> (`research/epoch-brand-composition.md`, 승격만 남음) — (1) `Source`가
|
||||
> **`Epoch` 인터페이스**(`{Revision: number}`)를 구조적으로 만족한다는 서술
|
||||
> 추가(`State`를 만족하는 기존 패턴과 같은 모양, 리비전 필드는 **공개**여야
|
||||
> 타입 레벨에서 성립), (2) 아래 "`state:Observer(fn)`" 절의 클로저가
|
||||
> `:Compute`와 같은 모양인 **`fn(self, from: Epoch|{Epoch})`**로 인자를 받음.
|
||||
> 값은 여전히 안 실어주므로 그 계약은 안 깨진다.
|
||||
|
||||
## 전파 모델 확정: push-invalidate(신호만) / pull-recompute(`Get()` 시점에만)
|
||||
|
||||
**Fusion식 eager 노드·생성순 정렬은 안 만듦.**
|
||||
|
|
@ -195,11 +199,12 @@ Handler가 애초에 다른 층위. 관련해서 Handler를 담는 엔진(`Dispa
|
|||
- `Source`는 값이 바뀌면 구독 중인 State들에게 **"무효화됐다"는 신호만
|
||||
쏜다** — 새 값 자체는 신호에 안 실림("state는 세터를 내보내기보다
|
||||
업데이트 됐다는 신호만 쏜다" — 사용자 확정 문구).
|
||||
- **⭐ [2026-08-21 재확정 — 판정 주체가 `invalid`에서 소스 에포크로 바뀜]
|
||||
- **⭐ [2026-08-21 재확정 — 판정 주체가 `invalid`에서 `Epoch` 리비전으로 바뀜]
|
||||
emit은 자기 `invalid` 상태와 **무관하게** 전파되고, 접히는 것은 오직
|
||||
**같은 소스의 같은 에포크가 두 번째로 도착했을 때**뿐이다.** 판정 규칙
|
||||
**같은 `Epoch`의 같은 리비전이 두 번째로 도착했을 때**뿐이다.** 판정 규칙
|
||||
전량은 **`base/state-epoch-plan.md`가 소스** — 여기선 이 절의 다른 서술과
|
||||
어긋나지 않게 요지만 적는다.
|
||||
어긋나지 않게 요지만 적는다. 여기 있던 "emit은 *항상* 전파된다"는 무조건
|
||||
서술의 역전 원문은 `archive/always-propagate-no-dedup-superseded.md`.
|
||||
- **`invalid`(구현 이름 `rawInvalid`) 플래그의 역할은 "내 캐시가 낡았다"는
|
||||
표시 하나뿐** — 전파를 제어하는 장치가 **아님**. `:Get()`이 호출되면
|
||||
상류로 올라가 재계산하고, 그 결과를 캐시에 넣고, 플래그를 끈다.
|
||||
|
|
@ -207,15 +212,13 @@ Handler가 애초에 다른 층위. 관련해서 Handler를 담는 엔진(`Dispa
|
|||
방식이 폐기된 이유는 `:Get()`을 호출하지 않는 `Observer`(아래
|
||||
"`state:Observer(fn)`" 절이 명시적으로 허용하는 사용법)가 **한 번 울고
|
||||
영구히 침묵**하기 때문이고, 그 실패 모드는 지금도 유효하다(원문은
|
||||
`archive/invalidate-dedup-propagation-reversed.md`). 에포크 비교엔 그 모드가
|
||||
없다 — 매 `Set`이 **새 에포크**라 항상 통과하고, 접히는 건 **다이아몬드에서
|
||||
같은 에포크가 두 경로로 도착한 두 번째**뿐이다.
|
||||
`archive/invalidate-dedup-propagation-reversed.md`). 리비전 비교엔 그
|
||||
모드가 없다 — 매 `Set`이 **새 리비전**이라 항상 통과하고, 접히는 건
|
||||
**다이아몬드에서 같은 리비전이 두 경로로 도착한 두 번째**뿐이다.
|
||||
- **emit 전파를 늦추거나 흡수할 수 있는 건 위 dedup 외엔 명시적인 게이트
|
||||
요소뿐** — `state:Gate(setup)`(`base/gate-plan.md`)와 그 위에 얹히는
|
||||
`Blocker`(`base/blocker-plan.md`), `base/debounce-throttle-plan.md`의 시간
|
||||
기반 정책. **평범한 State는 그 외의 이유로 신호를 삼키지 않는다.**
|
||||
- **[2026-08-21] 여기 있던 "emit은 *항상* 전파된다"는 무조건 서술은
|
||||
역전됐다** — 원문은 `archive/always-propagate-no-dedup-superseded.md`.
|
||||
- 실제 재계산은 `:Get()`이 호출되는 시점에만 일어남 —
|
||||
"필요할 때 계산" 원칙(사용자 확정). Fusion의 `timeliness="eager"` 노드/
|
||||
생성순 정렬 장치는 만들지 않음 — quad엔 그런 다단계 즉시 재계산이 필요한
|
||||
|
|
@ -250,7 +253,7 @@ Handler가 애초에 다른 층위. 관련해서 Handler를 담는 엔진(`Dispa
|
|||
**⭐ [2026-08-21 역전] 중복 *통지*도 이제 접힌다.** 여기엔 원래 "quad가 추가로
|
||||
접지 않는 것은 중복 통지뿐이고, `d` 아래 `Observer`가 한 사이클에 두 번 우는
|
||||
것은 의도된 동작"이라고 적혀 있었으나, `base/state-epoch-plan.md` 채택으로
|
||||
**두 번째 신호는 삼켜진다** — 그 신호가 나르는 에포크를 `d`가 이미 봤기
|
||||
**두 번째 신호는 삼켜진다** — 그 신호가 나르는 리비전을 `d`가 이미 봤기
|
||||
때문이다. 그래서 위 1단계는 "`d`가 두 번 받는다"가 아니라 **"두 번째는
|
||||
`d`에서 멈춘다"**가 된다. 역전 원문과 이게 2026-08-14 역전을 되돌린 게 아닌
|
||||
이유는 `archive/always-propagate-no-dedup-superseded.md`.
|
||||
|
|
@ -258,7 +261,7 @@ Handler가 애초에 다른 층위. 관련해서 Handler를 담는 엔진(`Dispa
|
|||
**부수로 같이 고쳐진 것 — 섞인 값(glitch).** 옛 모델에선 전파가 DFS라
|
||||
`b` 가지가 먼저 끝까지 내려가고, 그 아래 Observer가 `d:Get()`을 부르면 `c`는
|
||||
아직 신호를 못 받아 **옛 캐시를 반환**해 `d`가 `(b_new, c_old)`를 캐시했다.
|
||||
에포크 비교는 `c`가 신호 없이도 스스로 낡음을 알아채므로 이 창이 없다 —
|
||||
리비전 비교는 `c`가 신호 없이도 스스로 낡음을 알아채므로 이 창이 없다 —
|
||||
상세와 재현 시나리오는 `base/state-epoch-plan.md` §1.
|
||||
|
||||
**전역 원칙으로 명문화: "관측해야 실체화된다" (2026-08-04 세션)**
|
||||
|
|
@ -929,6 +932,26 @@ retract/Destroy되면 자동으로 정리됨.
|
|||
두는 것만으로 최초 적용까지 공짜로 됨(별도의 "설치 시 1회 적용" 코드를
|
||||
따로 안 짜도 됨). `state:Observer()`(인자 없는 "항상 관측" 유틸)도
|
||||
이 규칙을 그대로 따름 — 호출 즉시 한 번 관측이 트리거됨.
|
||||
- **⭐ [2026-08-21 확정] `fn`의 시그니처는
|
||||
`fn(self, from: (Epoch | EpochSet)?)` — `:Compute`와 같은 모양이다.** 사용자: *"Compute 와 유사하게 나올 수
|
||||
있다 봐요. self 를 넘겨주고, 그 뒤에 epoch|{epoch} 를 주는게 맞아보입니다."*
|
||||
위 "`:With`/`:Compute` — self 인자도 lazy 핸들로 통일" 절과 같은 결이다.
|
||||
- `self`는 이 Observer가 붙은 State의 **lazy 핸들**(값이 아님).
|
||||
- `from`은 **이 통지의 출처**다 — `Epoch` 하나이거나 `Epoch`들의 **집합**
|
||||
(`{[Epoch]: true}`, 게이트가 유보를 풀며 떼어낸 스냅샷). 값도 리비전도
|
||||
안 실린다. 분기는 `isEpoch`로. 계약은 `base/state-epoch-plan.md` §5가 소스.
|
||||
- **⚠️ [2026-08-22 신설] 등록 시점의 즉시 1회 실행에는 `from`이 없다** —
|
||||
그건 통지가 아니라 설치라 출처가 존재하지 않는다. 그래서 `from`은
|
||||
**옵셔널**이고, 이때만 `nil`이다(2026-08-21 커밋 전 `/code-review high`
|
||||
발견 — 한때 non-optional로 적혀 있었다). `fn`이 `from`을 실제로 쓰는
|
||||
소비자라면 `nil`을 "설치 발화"로 분기해야 한다.
|
||||
- **이건 "값을 안 실어주는 구독" 계약을 안 깬다** — 넘기는 건 값이 아니라
|
||||
**핸들과 메타데이터**뿐이다.
|
||||
- 인자 없는 `state:Observer()`(항상 관측 유틸)도 그대로 성립한다 — 넘겨줄
|
||||
`fn`이 없으니 인자 얘기가 아예 안 나온다.
|
||||
- **`from`을 실제로 쓰는 첫 소비자는 `Effect`다** — 자기 `EpochMap`을
|
||||
들고 각 내부 Observer가 그걸 `Update`해서 다중 의존성 중복 발화를
|
||||
접는다(`base/effect-plan.md`의 "`Effect(fn, ...deps)`" 절).
|
||||
- **값을 안 실어줌 — 반드시 `Get()`을 다시 해야 함.** 기존 "emit은
|
||||
무효화 신호 하나로 좁혀짐 — 값을 안 실어보내므로 저렴함" 원칙(위
|
||||
"전파 모델 확정" 절)이 그대로 적용됨: `fn`은 "뭔가
|
||||
|
|
@ -938,18 +961,18 @@ retract/Destroy되면 자동으로 정리됨.
|
|||
`:With`한 값에 따라 갈리는 경우가 있어서(위 "포지셔널 인자 지양" 절의
|
||||
`noprint` 예시처럼 계산 자체를 통째로 생략하고 싶을 수 있음) — `Get()`
|
||||
호출 여부를 작성자가 직접 결정하게 열어둔 것.
|
||||
- **⚠️ 이 허용이 전파 규칙에 의존한다(2026-08-14 명시,
|
||||
[2026-08-21 갱신]).** 지금 형태로 말하면 **"`Set` 한 번은 새 에포크라
|
||||
항상 통과한다"**에 의존한다 — 에포크 dedup이 접는 건 *같은* 에포크의
|
||||
두 번째 도착뿐이라 이 Observer는 매 변경마다 정확히 한 번 운다
|
||||
(`base/state-epoch-plan.md`). 아래 서술의 "항상 전파"는 그 뜻으로 읽을 것. `fn`이 `:Get()`을 안 하면 상류 State는 계속
|
||||
`invalid`로 남는데, 만약 "이미 `invalid`면 전파를 멈춘다"는 규칙이
|
||||
있으면 **이 Observer는 두 번째 변경부터 영원히 안 울림**. 실제로
|
||||
2026-08-14 이전까지 위 "전파 모델 확정" 절에 그런 문장이 있었고,
|
||||
이 계약과 정면 충돌하는 상태로 방치돼 있었음
|
||||
(`archive/invalidate-dedup-propagation-reversed.md`). **두 서술은
|
||||
같이 움직여야 함** — 전파를 접는 최적화를 다시 넣고 싶어지면
|
||||
반드시 이 항목부터 확인할 것.
|
||||
- **⚠️ 이 허용이 전파 규칙에 의존한다(2026-08-14 명시, 2026-08-21 갱신).**
|
||||
의존하는 명제는 **"`Set` 한 번은 새 리비전이라 항상 통과한다"** 하나다 —
|
||||
dedup이 접는 건 *같은* `Epoch`의 *같은* 리비전이 두 번째로 도착한
|
||||
것뿐이라, 이 Observer는 매 변경마다 정확히 한 번 운다
|
||||
(`base/state-epoch-plan.md` §4). **`invalid`로 전파를 접는 최적화는
|
||||
지금도 금지**다 — `fn`이 `:Get()`을 안 하면 상류 State는 계속
|
||||
`invalid`로 남으므로, 그런 규칙이 있으면 **이 Observer는 두 번째
|
||||
변경부터 영원히 안 울린다**(2026-08-14 이전에 실제로 그 문장이 위
|
||||
"전파 모델 확정" 절에 있었고 이 계약과 충돌한 채 방치됐었다 —
|
||||
`archive/invalidate-dedup-propagation-reversed.md`). **두 서술은 같이
|
||||
움직여야 함** — 전파를 접는 최적화를 다시 넣고 싶어지면 반드시 이
|
||||
항목부터 확인할 것.
|
||||
- **`fn`을 커링 스타일로 짜는 것도 모듈화 관용구로 권장(2026-08-07 여섯
|
||||
번째 세션)** — `state:Observer(makeLogger("x"))`처럼 팩토리가 실제
|
||||
`fn`을 만들어 반환하는 패턴, `Modifier`의 `Boldify(10)` 커링(`modifier-plan.md`
|
||||
|
|
|
|||
|
|
@ -1,26 +1,23 @@
|
|||
# State 재계산/전파 판정 — 소스 에포크 비교 (2026-08-21 확정)
|
||||
# State 재계산/전파 판정 — `Epoch` 비교와 `EpochMap` 컴포지션 (2026-08-21 확정)
|
||||
|
||||
**상태**: **확정.** 사용자 제안으로 시작해 같은 날 네 라운드에 걸쳐 다듬은 뒤
|
||||
**상태**: **확정.** 사용자 제안으로 시작해 같은 날 여러 라운드에 걸쳐 다듬은 뒤
|
||||
**채택 확정**됨 — *"gate 와 epoch 가 제가 만족할만한 정도로 올라왔습니다.
|
||||
채택하면 될것 같아요."* 구현은 **M3(Source/State)**.
|
||||
|
||||
**⚠️ 이 문서는 `base/source-state-plan.md`의 "전파 모델 확정" 절을 대체하는
|
||||
게 아니라 그 절이 정한 push-invalidate/pull-recompute 위에 **판정 규칙**을
|
||||
얹는다.** 그 절이 원래 갖고 있던 두 서술은 이 채택으로 뒤집혔고(아래 §3),
|
||||
역전 원문은 `archive/always-propagate-no-dedup-superseded.md`에 있다.
|
||||
얹는다.** 그 절이 원래 갖고 있던 두 서술은 이 채택으로 뒤집혔고, 역전 원문은
|
||||
`archive/always-propagate-no-dedup-superseded.md`에 있다.
|
||||
|
||||
**⚠️ [2026-08-21] 이 문서의 구조를 바꾸는 제안이 `research/`에 대기 중이다** —
|
||||
두 맵을 `EpochMap`으로 컴포지션하고 `Source` 대신 **`Epoch` 인터페이스**로
|
||||
일반화하는 안(`research/epoch-brand-composition.md`). **사실상 전량 확정됐고
|
||||
승격만 남았다**(같은 날 세션이 길어져 다음 세션으로 미룸). 여기 적힌 규칙 자체는
|
||||
그대로 성립하고 표현만 바뀐다 — 승격 전엔 이 문서가 정본.
|
||||
|
||||
**읽는 순서**: 규칙만 필요하면 **§2**만 보면 된다. §1은 왜 이걸 하는가(실재하는
|
||||
glitch), §3은 무엇이 고쳐지는가, §5는 세부 계약, §7은 구현 시 확인할 것.
|
||||
**읽는 순서**: 규칙만 필요하면 **§2~§4**만 보면 된다. §1은 왜 이걸 하는가
|
||||
(실재하는 glitch), §5는 emit 페이로드, §6은 State 밖의 소비자, §7은 비용,
|
||||
§8은 구현 시 확인할 것.
|
||||
|
||||
**히스토리**: 구현 전 QA 5라운드(`SS-2`/`SS-3`)에서 나왔고, 회신 원문은
|
||||
`qa-request/pre-implementation-qa-round5-response.md`, 네 라운드의 정정 경위는
|
||||
`qa-request/pre-implementation-qa-round5-followup.md`의 M·N절이 소스.
|
||||
`qa-request/pre-implementation-qa-round5-response.md`, 여러 라운드의 정정
|
||||
경위는 `qa-request/pre-implementation-qa-round5-followup.md`의 M·N절이 소스.
|
||||
`Epoch`/`EpochMap`으로 일반화한 마지막 라운드의 근거 기록은
|
||||
`reference/epoch-brand-composition.md`.
|
||||
|
||||
## 1. 사용자가 지목한 문제 (실재함)
|
||||
|
||||
|
|
@ -29,7 +26,8 @@ A ──> B ──┐
|
|||
└──> C ──┴──> D
|
||||
```
|
||||
|
||||
`A:Set()` 한 번에 대해 지금 모델(push-invalidate + pull-recompute)에서:
|
||||
`A:Set()` 한 번에 대해 옛 모델(push-invalidate + pull-recompute, 판정 주체가
|
||||
`invalid` 플래그)에서:
|
||||
|
||||
1. `A`가 구독자에게 무효화 신호를 전파한다. 순회가 DFS라 **`B` 쪽 가지가 먼저
|
||||
끝까지 내려간다** — `D`가 `B`를 통해 신호를 받고, 그 아래 Observer가 발화한다.
|
||||
|
|
@ -41,280 +39,400 @@ A ──> B ──┐
|
|||
|
||||
즉 **(a) 한 사이클 안에서 잘못된 값이 한 번 관측되고**(그 값으로 이미 프로퍼티가
|
||||
써지는 등 부작용이 나간다), **(b) 같은 계산이 두 번 돈다.** 리액티브 문헌에서
|
||||
말하는 전형적인 **glitch**이고, 지금 quad 문서 어디에도 이 현상이 서술돼 있지
|
||||
않다. `base/source-state-plan.md`의 "다이아몬드 의존성은 무엇이 푸는가" 절은
|
||||
말하는 전형적인 **glitch**다.
|
||||
|
||||
`base/source-state-plan.md`의 "다이아몬드 의존성은 무엇이 푸는가" 절은
|
||||
**중복 재계산이 없다**고만 말하는데, 그 논증은 *"누군가 `d:Get()`을 부르는
|
||||
시점"이 전파 파동이 끝난 뒤*라고 암묵적으로 가정한다 — Observer가 전파 도중에
|
||||
발화한다는 사실과 겹쳐 읽으면 그 가정이 깨진다.
|
||||
발화한다는 사실과 겹쳐 읽으면 그 가정이 깨진다. 아래 규칙이 이걸 고친다.
|
||||
|
||||
## 2. 사용자 제안 (최종 정리형) — **[2026-08-21 3차 정정본]**
|
||||
## 2. `Epoch` — 판정의 최소 인터페이스
|
||||
|
||||
각 State가 **자기에게 영향을 주는 루트 `Source`들의 에포크(count)** 를 들고
|
||||
있다가, 그것과 실제 Source의 현재 count를 비교해 재계산/전파 여부를 정한다.
|
||||
**[2026-08-21] 마지막 정정으로 테이블이 둘로 갈렸다** — 아래 "왜 둘인가" 참고.
|
||||
**판정에 필요한 건 둘뿐이다: identity와 "직전과 달라지는 리비전".** 그걸
|
||||
이름 붙인 것이 `Epoch`다.
|
||||
|
||||
```lua
|
||||
type Epoch = { Revision: number }
|
||||
```
|
||||
|
||||
- **그 자체로 키가 되는 unique 테이블**이다 — `EpochMap`이 `[epoch] = revision`
|
||||
으로 들고 있는다.
|
||||
- **`Source`가 이 인터페이스를 구조적으로 만족한다** — `Source`가 `State`를
|
||||
구조적으로 만족하는 기존 패턴(`base/source-state-plan.md`의 "Source가 State를
|
||||
만족함" 절)과 정확히 같은 모양. `Source:Set()`/`:Emit()`이 자기 `Revision`을
|
||||
**갱신한다**(직전과 다른 값으로 — 방향은 계약이 아니고, 아래 확정된 연산은
|
||||
실제로 증가가 아니라 **감소**한다).
|
||||
- **`Revision`은 공개 필드다.** 비공개면 구조적 만족이 **타입 레벨에서 성립하지
|
||||
않는다**(사용자: *"그래야 타입 상 `Source` 가 `Epoch`를 만족해요."*).
|
||||
`Store`와 달리 `Source`는 키가 사용자 것이 아니므로 예약 이름이 늘어도
|
||||
충돌하지 않는다 — *"`Source` 는 예약 이름이 늘어나도 됩니다(store 아님)."*
|
||||
- **런타임 판별은 `EpochBrand:is(x)`**(`base/brand-plan.md`). `Source`는
|
||||
`SourceBrand`이면서 동시에 `EpochBrand`에 등록된다 — 이 다중 태깅 요구가
|
||||
`Brand`를 인스턴스 브랜드로 재작성한 직접 발단이다.
|
||||
- **왜 `Source`가 아니라 `Epoch`인가**: 맵이 요구하는 건 identity + 리비전
|
||||
둘뿐인데 그걸 `Source`로 못 박으면 계약이 실제보다 좁아진다. 사용자:
|
||||
*"'소스를 전해주는것' 이라고 보기엔 너무 협소하고, 일반화된 형태가 아님."*
|
||||
일반화의 실익은 **`Source`가 아닌 원천(외부 시계 등)이 특수 분기 없이
|
||||
낀다**는 것이고, 그건 `EpochBrand:register(self)` 한 줄로 끝난다.
|
||||
|
||||
**계약은 "직전 값과 다르다"만 요구한다.** 순서 비교(`<`)는 아래 규칙 어디에도
|
||||
안 쓰이고 전부 `==`/`~=`뿐이라, 단조 증가조차 계약으로는 과하다 — 실제로
|
||||
**아래 확정된 방식은 증가가 아니라 감소한다**(그리고 0에서 한 바퀴 돈다).
|
||||
규칙이 `==`/`~=`만 쓰기 때문에 그게 문제가 안 되는 것이므로, **나중에 순서
|
||||
비교를 넣고 싶어지면 이 결정부터 되짚을 것.** 이름이 `Revision`인 것도
|
||||
순서를 뜻하지 않는다 — "직전과 구별되는 표식"이라는 뜻이다.
|
||||
|
||||
- **배경 — 평이한 `+1`이었다면 `2^53`에서 계약이 깨졌다.** Luau 숫자는
|
||||
double이라 랩어라운드가 아니라 **포화**한다 — `2^53`을 넘으면 `n + 1 == n`이
|
||||
되어 "다르다"는 보장이 정확히 그 지점에서 깨진다(**초당 100만 `Set`으로
|
||||
285년**이라 도달 불가능하긴 하다). 아래 확정된 `bit32` 랩은 **이 지점 자체를
|
||||
없앤다.** 이걸 근거로 적을 땐 "오버플로해도 다르다"가 아니라 **"도달
|
||||
불가능하다"**로 적을 것 — 전자는 틀린 서술이다.
|
||||
- **⭐ [2026-08-21 확정] 리비전 갱신은 `bit32.bnot(-rev)` 한 번이다 — 리비전은
|
||||
uint32 안에 머문다.**
|
||||
|
||||
```lua
|
||||
self.Revision = bit32.bnot(-self.Revision)
|
||||
```
|
||||
|
||||
**[2026-08-22 실측] 이건 랩어라운드 감소다** — `a > 0`이면 `a - 1`,
|
||||
`a == 0`이면 `4294967295`로 한 바퀴 돈다. `luau`로 확인한 값:
|
||||
|
||||
| `rev` | `bit32.bnot(-rev)` |
|
||||
|---|---|
|
||||
| 0 | 4294967295 |
|
||||
| 1 | 0 |
|
||||
| 2 | 1 |
|
||||
| 4294967295 | 4294967294 |
|
||||
|
||||
(원리: `bit32.bnot(x) == 4294967295 - (x mod 2^32)`이고 `a > 0`에서
|
||||
`(-a) mod 2^32 == 2^32 - a`라 `a - 1`이 된다. 시작값이 무엇이든
|
||||
상관없다 — 매 호출이 직전과 다른 값을 준다는 것만 계약이다.)
|
||||
|
||||
**사용자 논거**(2026-08-21): *"그건 luau 에서 native call 이라 아주
|
||||
빨라요. 반면 double 의 연산이 느린편인데, 희소 수준이 아니라, 사실상
|
||||
만나는걸 수년간 보기 어려운 라운드되어 동일해 무시되는 경우를 막기 위해
|
||||
double 까지 올려야할 이유를 모르겠어요. 매번 도는 코드인지라, 값 싸게
|
||||
native call + num 연산으로 가볍게 가고 싶어요."* — 즉 **`2^53` 포화는
|
||||
어차피 도달 불가능한 시나리오인데, 그걸 피하겠다고 값을 double 영역까지
|
||||
키울 이유가 없다**는 것. 이건 매 `Set`마다 도는 hot path다.
|
||||
- **갱신과 랩이 같은 연산 하나다.** `bit32.bnot`은 Luau가 FASTCALL로
|
||||
거는 빌트인이라, 별도의 덧셈도 마스킹도 없다 — 단항 부호 반전 하나가
|
||||
붙을 뿐이다.
|
||||
- 랩이라 **`2^53` 포화(`n + 1 == n`)가 아예 안 생긴다** — 위 배경 항목이
|
||||
말하는 계약 파손 지점 자체가 사라진다.
|
||||
- **⚠️ [2026-08-22 정정] 대신 생기는 `2^32` 랩은 "똑같이 도달 불가능"이
|
||||
아니다.** 여기 그렇게 적어뒀는데 **수치가 틀렸다** — 위 배경 항목과 같은
|
||||
척도(초당 100만 `Set`)로 `2^53`은 285년이지만 **`2^32`는 약 72분**이다
|
||||
(20만 배 차이). 현실적인 부하(초당 1만 `Set`)로도 5일 남짓이다.
|
||||
**그래도 위험하지 않은 이유는 도달 시간이 아니라 충돌 조건이 한 점이기
|
||||
때문이다**: 오판정이 나려면 **어떤 맵 항목이 그 `Epoch`에 대해 정확히
|
||||
`2^32`만큼 뒤처져** 있어야 한다. 한 바퀴에서 하나라도 어긋나면 값이 달라
|
||||
정상 판정된다. 그 항목은 emit을 받거나 `:Refresh`를 도는 순간 갱신되므로,
|
||||
"정확히 한 바퀴 동안 한 번도 안 건드려진 항목"이라야 한다. 확률적으로
|
||||
무시 가능하다는 뜻이지 **산술적으로 불가능하다는 뜻이 아니다** — 이 문서가
|
||||
바로 위에서 "근거를 정확히 적을 것"이라 규정했으므로 같은 기준을 적용한다.
|
||||
(2026-08-21 커밋 전 `/code-review high` 발견.)
|
||||
- **⚠️ [2026-08-22 정정] 여기 한때 `bit32.band(rev + 1, 0xFFFFFFFF)`라
|
||||
적고 "`bit32`가 double 덧셈 위에 fastcall을 하나 더 얹는다"는 단서를
|
||||
달아뒀는데, 둘 다 틀렸다.** 사용자가 말한 형태는 처음부터
|
||||
`bit32.bnot(-a)`였고(*"제가 말한건, bit32.bnot(-a) 입니다"*), 그건
|
||||
**덧셈을 얹는 게 아니라 갱신 자체를 대체한다.** 그래서 "`bit32`라서
|
||||
더 싸다"는 사용자 서술이 맞고, 그걸 반박한 에이전트 단서가 틀렸었다 —
|
||||
`band(n + 1, mask)`라는 **다른 형태**를 놓고 한 비교였기 때문.
|
||||
- **테이블 identity를 리비전으로 쓰는 대안은 채택 안 함**(사용자 선택:
|
||||
*"Revision 숫자로 가는걸 저는 선택하고 싶어요"*). 기능적으로는 둘 다
|
||||
성립하지만 테이블안은 **`Set` 한 번마다 테이블 하나를 할당**해서, 트윈처럼
|
||||
매 프레임 `Set`하는 소스가 여럿이면 GC 압력을 만든다(quad는 GC-native
|
||||
아키텍처라 이 축을 신경 써왔다).
|
||||
|
||||
## 3. `EpochMap` — 컴포지션 가능한 부기 객체
|
||||
|
||||
**에포크 부기를 State에서 떼어내 재사용 가능한 객체로 만든다**(사용자 제안).
|
||||
그래야 **노드가 아닌 소비자(leaf)도 같은 판정을 쓸 수 있다** — `Effect`가
|
||||
그 첫 수요자다(§6).
|
||||
|
||||
```lua
|
||||
type EpochSet = { [Epoch]: true } -- 배열이 아니라 집합이다 (아래 ⚠️ 참고)
|
||||
|
||||
EpochMap() -> EpochMap
|
||||
|
||||
EpochMap:Update(Epoch | EpochSet) -> boolean -- "뒤로 전파가 필요한가"
|
||||
EpochMap:Refresh() -> boolean -- 자기 키 전부를 라이브로 다시 읽음
|
||||
EpochMap:Sync(Epoch | EpochSet) -- 읽지 않고 쓰기만 (반환값 없음)
|
||||
EpochMap:TrackFrom(other: EpochMap) -- other가 추적 중인 키를 넘겨받아 라이브 리비전으로 채움
|
||||
```
|
||||
|
||||
**⚠️ [2026-08-22 정정] 여러 개를 넘길 때는 `{Epoch}`(배열)가 아니라
|
||||
`{[Epoch]: true}`(집합)다.** 여기 한때 `{Epoch}`로 적혀 있었는데, Luau에서
|
||||
그건 `{[number]: Epoch}` 배열이라 **실제로 넘어오는 게이트 배치와 타입이
|
||||
다르다** — 배치는 `base/gate-plan.md` 4번이 확정한 `withheld : { [epoch] : true }`를
|
||||
그대로 스왑해 넘긴 것이다. 이 표기를 믿고 `ipairs`로 구현하면 배치를 순회할 때
|
||||
**원소가 0개**가 되어, 유보됐다 풀린 emit이 하류에서 전부 조용히 삼켜진다 —
|
||||
`gate-plan.md` 4번이 애초에 고치려던 바로 그 버그다. **집합이어야 하는 이유는
|
||||
게이트 쪽 요구**다: 유보 중 같은 `Epoch`가 여러 경로로 도착해도
|
||||
`withheld[epoch] = true`가 저절로 접어주고, 게이트-게이트 unfold(같은 절)도
|
||||
집합이라야 중복 없이 합쳐진다. (2026-08-21 커밋 전 `/code-review high` 발견.)
|
||||
|
||||
- **`:Update`가 이 객체의 전부다.** 넘어온 각 `Epoch`에 대해 저장된 리비전과
|
||||
`epoch.Revision`을 비교하고, 다르면 새 값으로 덮는다. **하나라도 달랐으면
|
||||
`true`.** 사용자: *"애초에 Update 자체가 전부 최신 상태로 만들고, 업데이트
|
||||
된게 있으면 true 를 던지는거라."*
|
||||
- **`EpochSet`을 받으므로 sync 연산이 따로 필요 없다** — 전체를 넘기면
|
||||
그게 곧 sync다. (한때 에이전트가 "`:Sync`가 필수"라고 적었으나 `Update`를
|
||||
"하나만 받는 것"으로 좁게 본 착오였고 철회됐다.)
|
||||
- **⭐ 이건 `invalid`와 다른 물건이다**(사용자: *"이건 invalid 랑은 다른
|
||||
구현이야."*). 반환값의 뜻은 "내 캐시가 낡았다"가 아니라 **"뒤로 전파가
|
||||
필요한가"** 하나다.
|
||||
- **내부 최적화**: 목록을 돌 때 diff 때문에 읽기가 들어가는데, **한 번
|
||||
다름을 찾으면 반환값이 이미 `true`로 확정**되므로 나머지는 읽지 않고
|
||||
쓰기만 하면 된다(사용자 제안).
|
||||
- **`:Refresh`는 인자 없는 `:Update`다** — 자기가 이미 들고 있는 키 전부를
|
||||
라이브로 다시 읽어 갱신하고, 하나라도 달랐으면 `true`. 아래 §4의 **순회**가
|
||||
이걸 쓴다(순회가 훑을 대상 목록이 곧 이 맵 자신이라 인자로 받을 게 없다).
|
||||
- **`:Sync`는 읽기를 건너뛰고 쓰기만 하는 변형**이다(반환값 없음). *"없다고
|
||||
안되는건 아닌데, 그냥 다 안 읽고 set 만 해버리는것은 처음 셋팅에 도움은
|
||||
됩니다."* **[2026-08-22 정정]** 여기 "초기화에만 쓴다"고 적혀 있었으나
|
||||
`base/gate-plan.md` 4번이 게이트의 flush 경로에서도 `:Sync(batch)`를 쓰는
|
||||
것으로 확정돼 있다 — 쓰는 자리는 **"반환값이 필요 없다고 이미 아는 곳"**
|
||||
둘이다: 노드 생성 시딩, 그리고 게이트가 실제로 전파할 때.
|
||||
- **⭐ [2026-08-22 신설] `:TrackFrom(other)` — 새 노드 시딩이 이걸 쓴다.**
|
||||
`other`가 추적 중인 키를 전부 넘겨받아 **라이브 리비전으로** 채운다
|
||||
(`other`의 저장값을 복사하는 게 아니다).
|
||||
- **왜 필요한가**: §4의 시딩 규칙은 "상류의 `Epoch`를 전부 끌어와 채운다"인데,
|
||||
`:With(a, b)`의 상류 `a`/`b`는 **State이지 `Epoch`가 아니다.** 그 State가
|
||||
추적 중인 루트 `Epoch` 집합은 그 State의 `valueEpochMap` 안에만 있으므로,
|
||||
키를 넘겨받는 연산이 없으면 시딩을 **표면으로 표현할 수가 없다**
|
||||
(2026-08-21 커밋 전 `/code-review high` 발견).
|
||||
- **새 설계가 아니라 이미 확정된 동작에 이름을 붙인 것**이다 — 사용자
|
||||
확정 문구가 이미 *"전부 가져와서, 실제 count 로 둡니다"*였다. `:Refresh`도
|
||||
같은 성격의 명명이다(§4의 "순회").
|
||||
- `Source`처럼 **자기가 곧 `Epoch`인 상류**는 `:TrackFrom`이 아니라
|
||||
`:Sync(dep)`로 직접 넣는다 — 아래 시딩 규칙 참고.
|
||||
- **이름 근거(사용자 확정, 2026-08-22)**: 가칭은 `Absorb`였는데 *"조금 상위
|
||||
요소꺼를 흡수해서 상위 요소에서 제거할것만 같은 이름"*이라 바꿨다 — 이
|
||||
연산은 `other`를 **전혀 안 건드린다**. 게다가 `base/gate-plan.md`가 이미
|
||||
"흡수 집합"을 **다른 뜻**(emit을 붙들고 있음)으로 쓰고 있어 한 코퍼스 안에
|
||||
같은 단어가 두 의미로 놓이는 문제도 있었다. `TrackFrom`은 이 맵의 존재
|
||||
이유를 사용자가 표현한 말(*"'내가 뭘 추적하고 있나' 가 필요하죠"*)을 그대로
|
||||
쓰고, `From`이 방향을 못박아 비파괴가 드러나며, 나중에 동적 의존성으로
|
||||
생성 이후에 키를 더하는 자리가 생겨도 이름이 그대로 맞는다(그래서
|
||||
`SeedFrom`보다 낫다). 후보 비교는 `question.md`가 아니라 여기서 끝났다 —
|
||||
같은 자리에서 확정됐으므로 열린 항목이 아니다.
|
||||
- **키는 weak다.** `epoch`가 죽으면 항목이 사라진다. `base/relate-plan.md`가
|
||||
경고하는 "값이 키를 되참조하면 안 된다"는 제약은 값이 숫자라 문제없다.
|
||||
|
||||
## 4. State는 `EpochMap`을 둘 컴포지션한다
|
||||
|
||||
```
|
||||
State
|
||||
sourceCountMap : { [source (weak key)] : count }
|
||||
-- "내 값이 이 소스에 대해 최신인가" (값 유효성)
|
||||
sourceEmitMap : { [source (weak key)] : count }
|
||||
-- "이 소스의 이 에포크를 내가 하류로 이미 던졌는가" (전파 dedup)
|
||||
rawInvalid : boolean
|
||||
-- "재계산이 필요하다"는 확정 플래그
|
||||
valueEpochMap : EpochMap -- "내 값이 이 Epoch에 대해 최신인가" (값 유효성)
|
||||
emitEpochMap : EpochMap -- "이 Epoch의 이 리비전을 하류로 이미 던졌는가" (전파 dedup)
|
||||
rawInvalid : boolean -- "재계산이 필요하다"는 확정 플래그
|
||||
```
|
||||
|
||||
**⭐ [2026-08-21 확정] 노드가 생길 때의 초기값은 두 맵이 서로 다르다.**
|
||||
**⭐ 왜 둘인가 — 순회 때문이다.** 사용자 정리: *"'내 값이, 상류의 상태로
|
||||
하여금 사용 가능하다' 를 보는 테이블과, '내가 emit을 받을 때, 그걸 다시
|
||||
던져야하는지 봐야하나' 를 보는걸 나누는거죠."* 순회가 없다면 **"값을
|
||||
최신화했다"와 "통지를 내려보냈다"가 항상 같은 스텝에서 같이 일어나서** 맵
|
||||
하나로 충분하다. 순회를 넣는 순간 그 둘이 갈라진다 — **순회는 값만 앞당기고
|
||||
통지는 안 한다.**
|
||||
|
||||
- **`sourceEmitMap`은 비운 채로 시작한다.** `nil ~= source.count`라 어떤 emit이
|
||||
와도 "처음 보는 것"으로 걸린다 — 그리고 그게 맞다. 사용자: *"새로 생성된
|
||||
노드에서 들어온 emit 은, 개념적으로 해당 노드가 한번도 받아본적 없는
|
||||
emit 입니다."*
|
||||
- **`sourceCountMap`은 반대로 상류에서 전부 끌어와 실제 count로 채우고,
|
||||
`rawInvalid = true`로 시작한다.** 이쪽은 비워두면 안 된다 — 순회가 훑을
|
||||
**⭐ 대원칙 — 무효화를 결정하는 건 언제나 리비전 비교지 emit의 도착이 아니다.**
|
||||
emit은 **"이 원천을 확인해봐"** 라는 요청일 뿐이다. 그래서 emit이 통과해도
|
||||
리비전이 이미 최신이면 **캐시는 유효한 채로 남는다**(아래 2번 규칙). 사용자
|
||||
정리: *"emit 자체가 invalid 하게 만드는 직접 트리거는 아니라서, (이미 count 가
|
||||
최신이면) 캐시가 유효하다."* 아래 나머지 규칙은 전부 이 원칙의 따름정리다.
|
||||
|
||||
### 노드가 생길 때의 초기값 — 두 맵이 서로 다르다
|
||||
|
||||
- **`emitEpochMap`은 비운 채로 시작한다.** 어떤 emit이 와도 "처음 보는 것"으로
|
||||
걸린다 — 그리고 그게 맞다. 사용자: *"새로 생성된 노드에서 들어온 emit 은,
|
||||
개념적으로 해당 노드가 한번도 받아본적 없는 emit 입니다."*
|
||||
- **`valueEpochMap`은 반대로 상류가 추적 중인 `Epoch`를 전부 끌어와 채우고,
|
||||
`rawInvalid = true`로 시작한다.** dep마다 갈린다 — **dep이 `Epoch`면**
|
||||
(`Source`) `:Sync(dep)`, **dep이 State면** `:TrackFrom(dep.valueEpochMap)`.
|
||||
분기는 `isEpoch`로 한다.
|
||||
```lua
|
||||
for _, dep in deps do
|
||||
if isEpoch(dep) then self.valueEpochMap:Sync(dep)
|
||||
else self.valueEpochMap:TrackFrom(dep.valueEpochMap) end
|
||||
end
|
||||
self.rawInvalid = true
|
||||
``` 이쪽은 비워두면 안 된다 — 순회가 훑을
|
||||
대상 목록이 곧 이 맵이라, 비어 있으면 **"훑을 게 없으니 유효하다"**로
|
||||
오판한다. 사용자: *"이건 상류가 주는대로 lazy 할 순 없는게, '내가 뭘
|
||||
추적하고 있나' 가 필요하죠. 따라서 다음을 제안합니다: 전부 가져와서, 실제
|
||||
count 로 둡니다. 그러고 `rawInvalid` 를 true 로 두세요."*
|
||||
- 그래서 **`:With`에서 두 상류가 같은 소스에 다른 count를 들고 있을 때의 병합
|
||||
규칙도 필요 없다** — 어차피 생성 시점의 라이브 count로 통일된다.
|
||||
- 그래서 **`:With`에서 두 상류가 같은 `Epoch`에 다른 리비전을 들고 있을 때의
|
||||
병합 규칙도 필요 없다** — 어차피 생성 시점의 라이브 리비전으로 통일된다.
|
||||
|
||||
**⭐ 대원칙 — 무효화를 결정하는 건 언제나 count 비교지 emit의 도착이 아니다.**
|
||||
emit은 **"이 원천을 확인해봐"** 라는 요청일 뿐이다. 그래서 emit이 통과해도
|
||||
count가 이미 최신이면 **캐시는 유효한 채로 남는다**(아래 2번 규칙). 사용자
|
||||
정리(2026-08-21): *"emit 자체가 invalid 하게 만드는 직접 트리거는 아니라서,
|
||||
(이미 count 가 최신이면) 캐시가 유효하다."* 이 문서의 나머지 규칙은 전부 이
|
||||
원칙의 따름정리다.
|
||||
### emit을 받았을 때 — 두 맵을 각각 `Update` 하고 그 boolean으로 결정한다
|
||||
|
||||
- `Source:Set()`/`:Emit()`은 자기 count를 증가시킨다.
|
||||
- **`emit`은 값을 안 싣고 count도 안 싣는다 — 싣는 건 "이 통지의 출처" 하나**다.
|
||||
받는 쪽이 거기서 count를 **그때그때 라이브로** 읽는다. 그래서 "에포크 5의
|
||||
emit" 같은 건 없다. 출처로 올 수 있는 것은 **둘**이다
|
||||
(**[2026-08-21 확장]** — 원래는 `Source`뿐이었다):
|
||||
- **`Source`** — 그 소스 하나를 확인하라.
|
||||
- **게이트 배치** — 게이트가 유보를 풀며 **떼어낸 소스 집합**을 확인하라
|
||||
(`base/gate-plan.md`의 4번). 게이트가 `blocker:Off()` 등으로 풀 때 쓴다.
|
||||
**게이트의 살아있는 `withheld` 테이블이 아니라 그 자리에서 스왑해 떼어낸
|
||||
스냅샷**이다 — 재진입이 나도 바깥 전파가 빈 집합을 순회하지 않게 하기
|
||||
위함(같은 절).
|
||||
**평범한 노드는 출처를 안 바꾼다** — 자기를 끼워넣지 않고 받은 출처를 그대로
|
||||
아래로 넘긴다. **`GateNode`만 예외로 언제나 자기 자신을 출처로 새로 낸다**
|
||||
(`base/gate-plan.md`의 4번).
|
||||
- **emit을 받았을 때** — 출처가 `Source`면 판정은 O(1)이다, 그 항목 하나만 본다:
|
||||
1. `sourceCountMap[source] ~= source.count` → 둘 다 `source.count`로 갱신하고
|
||||
`rawInvalid = true`를 세운 다음 **뒤로 emit** 한다.
|
||||
2. 같은데 `sourceEmitMap[source] ~= source.count` → **값은 이미 최신이지만
|
||||
통지는 아직 안 나갔다**(아래 순회가 앞질러 흡수했거나, 게이트가 붙들고
|
||||
있는 동안 하류가 `Get()`으로 앞당겨 읽은 경우). `sourceEmitMap`만
|
||||
갱신하고 **뒤로 emit** 한다 — `rawInvalid`는 안 건드린다.
|
||||
3. 둘 다 같으면 → **삼킨다.**
|
||||
- **다른 소스 항목은 건드리지 않는다** — 그 소스들은 자기가 직접 emit 하므로.
|
||||
- **게이트는 배치가 비어 있으면 애초에 통지하지 않는다**(`base/gate-plan.md`의
|
||||
8번) — 빈 배치를 흘리는 건 "쌓인 게 없는데 뒤로 넘기는" 꼴이라 State 층에
|
||||
`Source:Emit`을 추가하는 것과 같아진다. 그래서 아래 규칙이 빈 집합을 받는
|
||||
경우는 없다.
|
||||
- **출처가 게이트 배치면** 그 집합을 순회하며 **각 소스에 위 1~3을
|
||||
그대로 적용**하고, 하나라도 1번이나 2번에 걸렸으면 **받은 출처(그 게이트)를
|
||||
그대로** 뒤로 넘긴다. 전부 3번이면 삼킨다 — 다이아몬드에서 같은 해제 통지가
|
||||
두 번 도착해도 두 번째가 접히는 건 소스 emit과 똑같다.
|
||||
- **⚠️ 단, 받는 쪽이 `GateNode`면 배치를 그대로 넘기지 않고 풀어서 자기
|
||||
`withheld`에 합친다** — 배치는 상류 게이트가 이번 전파에만 쓰는 일회성
|
||||
스냅샷이라, 참조만 들고 있다가 나중에 풀면 그 배치가 이미 지나간 것이
|
||||
된다(`base/gate-plan.md`의 4번).
|
||||
- **재계산 판정**:
|
||||
- `rawInvalid == true` → 그냥 재계산한다. 순회할 이유가 없다(이미 확정).
|
||||
- `rawInvalid == false` → **그때만 `sourceCountMap`을 훑는다.** 목적은 하나뿐 —
|
||||
**못 받은 emit을 여기서 먼저 받아주는 것**(중간 게이트에 막혀 있었다거나
|
||||
전파 파동이 아직 이 가지에 안 닿았다거나). 다른 항목이 발견되면
|
||||
**`sourceCountMap`만** 갱신하고 `rawInvalid = true`로 만든다.
|
||||
- **⭐ 순회는 `sourceEmitMap`을 건드리지 않고, 뒤로 emit 하지도 않는다.**
|
||||
```lua
|
||||
local valueChanged = self.valueEpochMap:Update(from)
|
||||
local emitChanged = self.emitEpochMap:Update(from)
|
||||
if valueChanged then self.rawInvalid = true end
|
||||
if valueChanged or emitChanged then
|
||||
-- 뒤로 emit (받은 from을 그대로 넘긴다)
|
||||
end
|
||||
```
|
||||
|
||||
세 갈래로 읽으면 이렇다:
|
||||
|
||||
1. **`valueEpochMap`이 달랐다** → 값이 낡았다. `rawInvalid = true`를 세우고
|
||||
**뒤로 emit** 한다(두 맵 모두 갱신됨).
|
||||
2. **`valueEpochMap`은 같은데 `emitEpochMap`이 달랐다** → **값은 이미 최신이지만
|
||||
통지는 아직 안 나갔다**(순회가 앞질러 흡수했거나, 게이트가 붙들고 있는 동안
|
||||
하류가 `Get()`으로 앞당겨 읽은 경우). `emitEpochMap`만 갱신되고 **뒤로
|
||||
emit** 한다 — `rawInvalid`는 안 건드린다.
|
||||
3. **둘 다 같다** → **삼킨다.** 다이아몬드에서 같은 리비전이 두 경로로 도착한
|
||||
두 번째가 여기서 접힌다.
|
||||
|
||||
- **⚠️ [2026-08-22 신설] `GateNode`는 이 의사코드를 그대로 쓰지 않는다.**
|
||||
위 코드는 `emitEpochMap`을 **수신 시점에** 갱신하는데, 게이트는
|
||||
`base/gate-plan.md` 4번이 **전파할 때 `:Sync(batch)`로** 갱신하는 것으로
|
||||
확정돼 있다(그래야 "내가 하류로 던진 리비전"이라는 맵의 뜻이 게이트에서도
|
||||
참이 된다 — 유보 중엔 아직 안 던졌으니까). 그래서 게이트에서는:
|
||||
- 판정(규칙 1~3)은 **똑같이** 먼저 돈다. 규칙 3으로 삼켜지면 정책도 안 돌고
|
||||
흡수 집합에도 안 들어간다(`gate-plan.md` 4번).
|
||||
- 다만 `emitEpochMap:Update`를 수신 시점에 부르지 않으므로, **유보 중에
|
||||
같은 리비전이 다른 경로로 또 오면 규칙 2로 걸려 정책이 한 번 더 돈다.**
|
||||
이미 흡수 집합에 있어 무해하다(같은 절). 정책이 실제로 emit한 뒤에는
|
||||
`:Sync(batch)`가 돌아 있으므로 그 다음 도착은 정상적으로 규칙 3에 걸린다.
|
||||
- **이 예외가 여기 기록돼 있지 않았다**(2026-08-21 커밋 전 `/code-review high`
|
||||
발견) — §4대로 구현하면 `gate-plan.md` 4번의 계약이 조용히 깨진다.
|
||||
- **`from`이 하나면 판정은 O(1)이다** — 그 항목 하나만 본다. **다른 항목은
|
||||
건드리지 않는다**(그 `Epoch`들은 자기가 직접 emit 하므로).
|
||||
- **`from`이 집합이면**(게이트 배치, §5) `Update`가 알아서 순회하고, 하나라도
|
||||
달랐으면 `true`를 준다 — 규칙이 그대로 성립한다.
|
||||
|
||||
### 재계산 판정
|
||||
|
||||
- **`rawInvalid == true`** → 그냥 재계산한다. 순회할 이유가 없다(이미 확정).
|
||||
- **`rawInvalid == false`** → **그때만 `valueEpochMap:Refresh()`를 부른다.**
|
||||
목적은 하나뿐 — **못 받은 emit을 여기서 먼저 받아주는 것**(중간 게이트에
|
||||
막혀 있었다거나 전파 파동이 아직 이 가지에 안 닿았다거나). `true`가 나오면
|
||||
`rawInvalid = true`로 만들고 재계산한다.
|
||||
- **⭐ 순회는 `emitEpochMap`을 건드리지 않고, 뒤로 emit 하지도 않는다.**
|
||||
그래서 나중에 진짜 emit이 도착하면 위 2번으로 걸려 **하류까지 정상적으로
|
||||
전파된다**(사용자: *"emit 바로 안하고 상류가 emit 해줄 때 까지 기다립니다"*).
|
||||
- **재계산이 끝나면 `rawInvalid = false`, 그리고 `sourceCountMap`은 자기가 읽은
|
||||
상류 전부에 대해 갱신한다**(**[2026-08-21 확정]** — 발행 소스 항목만 갱신하는
|
||||
게 아니다). 맵의 뜻이 "내 값이 이 소스에 대해 최신인가"이므로, 방금 계산한
|
||||
값은 정의상 **모든** 상류에 대해 최신이다. `sourceEmitMap`은 **안 건드린다** —
|
||||
계산은 통지가 아니다.
|
||||
- 이게 실제로 갈리는 자리는 **게이트가 붙들고 있는 동안 하류가 `Get()`으로
|
||||
앞당겨 읽는 경우**다. 그때 `sourceCountMap`이 앞서 있으므로, 나중에 게이트가
|
||||
풀며 보내는 통지는 위 2번(통지만)으로 떨어져 **같은 값을 다시 계산하지
|
||||
않는다.**
|
||||
- count가 싫다면 `[source] -> {}` 처럼 **유니크 테이블 identity**로 같은 판정이
|
||||
가능하다(사용자 대안).
|
||||
이게 없으면 **값은 맞는데 통지가 죽는** 실패 모드가 생긴다 — 2026-08-14에
|
||||
폐기된 옛 dedup의 "영구 침묵"과 같은 계열이다
|
||||
(`archive/invalidate-dedup-propagation-reversed.md`).
|
||||
|
||||
**⭐ 왜 테이블이 둘인가 — 순회 때문이다.** 사용자 정리: *"'내 값이, 상류의
|
||||
상태로 하여금 사용 가능하다' 를 보는 테이블과, '내가 emit을 받을 때, 그걸 다시
|
||||
던져야하는지 봐야하나' 를 보는걸 나누는거죠."* 순회가 없다면 **"값을
|
||||
최신화했다"와 "통지를 내려보냈다"가 항상 같은 스텝에서 같이 일어나서** 카운트
|
||||
하나로 충분하다 — 실제로 2026-08-21 2차 정정에서 그렇게 정리했었다. 순회를
|
||||
넣는 순간 그 둘이 갈라진다(순회는 값만 앞당기고 통지는 안 한다). 그래서 이
|
||||
분리는 **한 번 철회됐던 `seen`/`computedAt` 분리가, 다른 이유로 되살아난
|
||||
것**이다 — 옛 근거("전파 시점에 갱신된 count가 캐시를 신선한 것으로 오인시킨다")는
|
||||
여전히 틀렸고, 지금 근거는 **순회가 값과 통지를 비대칭으로 앞당긴다**는 것이다.
|
||||
### 재계산이 끝나면
|
||||
|
||||
**⚠️ [2026-08-21 `/code-review high`] 아래 희소 구현 메모는 "둘 다 상류에서
|
||||
복사"와 그대로는 안 맞는다** — 아래 §5의 7번이 소스. 두 맵을 명시적으로 다
|
||||
들고 시작하는 게 안전하고, 희소화는 그 규칙이 정해진 뒤 얹을 것.
|
||||
- **`rawInvalid = false`**, 그리고 **`valueEpochMap`은 자기가 읽은 상류
|
||||
전부에 대해 갱신한다**(발행 `Epoch` 항목만이 아니다). 맵의 뜻이 "내 값이 이
|
||||
`Epoch`에 대해 최신인가"이므로, 방금 계산한 값은 정의상 **모든** 상류에 대해
|
||||
최신이다. 사용자: *"invalid 에 대한 계산을 위한 count 테이블은 단순히 전부
|
||||
업데이트 하는건 맞아보입니다."*
|
||||
- **`emitEpochMap`은 안 건드린다 — 계산은 통지가 아니다.**
|
||||
- 이 "전부 갱신"이 실제로 값을 하는 자리는 **게이트가 붙들고 있는 동안 하류가
|
||||
`Get()`으로 앞당겨 읽는 경우**다. 그때 `valueEpochMap`이 앞서 있으므로,
|
||||
나중에 게이트가 풀며 보내는 통지는 위 2번(통지만)으로 떨어져 **같은 값을
|
||||
다시 계산하지 않는다.**
|
||||
|
||||
**구현 메모 — `sourceEmitMap`은 희소 테이블로 두면 된다.** emit이 count를 안
|
||||
싣고 받는 쪽이 라이브로 읽으므로, 이 테이블에 실제로 필요한 정보는 **"순회가
|
||||
앞질러 흡수해서 아직 안 던진 소스가 무엇인가"** 뿐이다. 평상시엔 비어 있고
|
||||
순회가 발견했을 때만 채워지는 `pending` 집합으로 구현해도 위 세 규칙이 그대로
|
||||
성립한다(1번에서 지우고, 2번에서 있으면 던지고 지운다).
|
||||
## 5. emit 페이로드 — `Epoch | EpochSet`, 게이트는 안 싣는다
|
||||
|
||||
## 3. 에이전트 분석 — 이게 실제로 무엇을 고치는가
|
||||
- **emit은 값도 리비전도 안 싣는다 — 싣는 건 "이 통지의 출처"뿐**이고, 그
|
||||
출처는 `Epoch` 하나이거나 `EpochSet`(`{[Epoch]: true}`)이다. 받는 쪽이 거기서 리비전을
|
||||
**그때그때 라이브로** 읽는다. 그래서 "리비전 5의 emit" 같은 건 없다.
|
||||
- **게이트 노드 자체는 페이로드에 안 싣는다.** 게이트가 유보를 풀 때
|
||||
(`blocker:Off()` 등) 넘기는 건 **그 자리에서 스왑해 떼어낸 `Epoch` 집합
|
||||
스냅샷**이다(`base/gate-plan.md`의 4번) — 게이트의 살아있는 `withheld`
|
||||
테이블이 아니다(재진입이 나도 바깥 전파가 빈 집합을 순회하지 않게 하기
|
||||
위함). 하류는 게이트 identity를 **한 번도 안 쓴다** — 게이트를 에포크
|
||||
경계로 만드는 안은 §8의 "기각된 대안 — 게이트를 에포크 경계로" 항목에서
|
||||
기각됐다. 사용자: *"하류가 Gate 노드를 받을 이유도 없거든."*
|
||||
- **런타임 분기는 `isEpoch`로 한다** — 단일이냐 집합이냐.
|
||||
- **평범한 노드는 출처를 안 바꾼다** — 자기를 끼워넣지 않고 받은 출처를 그대로
|
||||
아래로 넘긴다. **`GateNode`만 예외로 언제나 자기 배치를 새로 낸다**
|
||||
(`base/gate-plan.md`의 4번).
|
||||
- **⚠️ 받는 쪽이 `GateNode`면 배치를 그대로 넘기지 않고 풀어서 자기
|
||||
`withheld`에 합친다** — 배치는 상류 게이트가 이번 전파에만 쓰는 일회성
|
||||
스냅샷이라, 참조만 들고 있다가 나중에 풀면 그 배치가 이미 지나간 것이
|
||||
된다(같은 절).
|
||||
- **게이트는 배치가 비어 있으면 애초에 통지하지 않는다**(`base/gate-plan.md`의
|
||||
8번) — 그래서 위 규칙이 빈 집합을 받는 경우는 없다.
|
||||
|
||||
**결론부터: 문제 진단도 해법 방향도 맞다. 그리고 이건 성능 최적화가 아니라
|
||||
`Get()`의 의미론을 바꾸는 결정이다.**
|
||||
## 6. State 밖의 소비자도 같은 판정을 쓴다 — `Effect`
|
||||
|
||||
- **고쳐진다 — 섞인 값.** 위 3단계에서 `D`가 `C:Get()`을 부르면, `C`도 자기
|
||||
`sourceCountMap`에서 `A`의 count가 자기가 기록한 것보다 앞선 걸 보고 **신호가
|
||||
아직 안 왔어도 스스로 재계산**한다. 그래서 `D`는 항상 `(B_new, C_new)`를
|
||||
얻는다. **`Get()`이 "지금 이 순간의 일관된 값"을 반환한다는 보장이 처음으로
|
||||
성립**한다.
|
||||
- **고쳐진다 — 중복 재계산.** 뒤늦게 `C` 쪽 신호가 도착해도 `sourceCountMap`이
|
||||
"이미 최신"이라 `rawInvalid`가 켜지지 않고, 따라서 재계산도 안 일어난다
|
||||
(§2의 2번/3번 규칙 — **[2026-08-21 정정]** 여기 "`rawInvalid`가 켜져도"라고
|
||||
적혀 있었으나 §2 규칙상 켜지지 않는다).
|
||||
- **⭐ [2026-08-21 확정] 중복 *통지*도 같이 접는다 — `source-state-plan.md`가
|
||||
"접지 않는다"고 확정해뒀던 것의 역전이다**(역전 원문은
|
||||
`archive/always-propagate-no-dedup-superseded.md`). 처음엔 "값만
|
||||
고쳐지고 통지는 두 번 그대로"로 정리했는데, **사용자 판정으로 통지도
|
||||
같은 장치로 접기로 했다**: *"중복 통지는 단순히, count 목록을 순회해보고
|
||||
이미 모든 소스가 최신이면 무시하는게 맞아보인다. 특히 emit 은 자신 소스를
|
||||
주게 되므로, 자신 소스 카운트만 빠르게 비교가 쉽다. 우리에게 중요한 점은,
|
||||
앞단의 변경이 뒤로 흘렀냐일 뿐이라서 해당 방식을 택하는데 있어 문제가 없다."*
|
||||
- **판정은 O(1)이다** — emit이 **어느 source가 발행했는지**를 실어 오므로
|
||||
**그 소스 하나만** 비교하면 된다(전체 목록 순회가 아님).
|
||||
- **⚠️ 이건 2026-08-14에 뒤집힌 옛 dedup과 다른 장치다.** 그때 폐기된 건
|
||||
*"이미 `invalid`면 더 안 내려보낸다"*로, `:Get()`을 안 부르는 Observer가
|
||||
**한 번 울고 영구히 침묵**하는 실패 모드가 있었다
|
||||
(`archive/invalidate-dedup-propagation-reversed.md`). 에포크 비교는 그
|
||||
모드가 없다 — 매 `Set`마다 카운트가 **새 값**이라 항상 통과하고, 접히는 건
|
||||
**같은 에포크가 두 경로로 도착한 두 번째**뿐이다.
|
||||
- **⚠️ [2026-08-21 철회 후 재도입] 여기 있던 "카운트를 `seen`/`computedAt`
|
||||
둘로 분리하라"는 요구는 한 번 철회됐다가 **다른 근거로 되살아났다** —
|
||||
지금 형태는 `sourceCountMap`/`sourceEmitMap`이고, 근거는 원래 적었던
|
||||
"전파 시점 갱신이 캐시를 오인시킨다"(틀림)가 아니라 **순회가 값만 앞당기고
|
||||
통지는 안 한다**는 비대칭이다. §2의 "왜 테이블이 둘인가" 문단이 소스.
|
||||
- 비교는 두 맵 모두 `source.count`와 같으면 삼킨다(단조 증가라 안전).
|
||||
- **선례가 있다** — 값 자체를 비교하는 게 아니라 **버전/에포크를 비교해
|
||||
lazy하게 검증**하는 건 MobX(global state version + observing 검사),
|
||||
Adapton류 incremental computing, "reactively" 계열 라이브러리가 쓰는 표준
|
||||
기법이다. Fusion처럼 **그래프를 위상정렬해 eager로 미는 방식**(quad가 이미
|
||||
기각한 것)의 대안으로 자주 쓰인다 — 즉 이 제안은 quad가 이미 택한
|
||||
pull 모델과 **결이 같다**.
|
||||
`EpochMap`을 떼어낸 실익이 여기서 나온다. **`Effect`가 자기 `EpochMap`을 하나
|
||||
들고** 각 의존성의 내부 Observer가 그걸 `Update`하면, 한 파동에 여러 dep가
|
||||
깨워도 **첫 번째만 `true`** 라 `fn`이 한 번만 돈다.
|
||||
|
||||
## 4. 비용
|
||||
이건 `A → b`, `A → c`, `Effect(fn, b, c)`에서 **접어줄 공통 하류가 없어**
|
||||
State 층 dedup이 못 닫던 갭이다 — `Effect`가 자기 맵을 들면 그 지점이 곧
|
||||
공통 하류가 된다. 계약 전량은 `base/effect-plan.md`의 "`Effect(fn, ...deps)`"
|
||||
절이 소스.
|
||||
|
||||
**그래서 `Observer` 클로저는 출처를 인자로 받는다** —
|
||||
`fn(self, from: (Epoch | EpochSet)?)`. `:Compute`의 `fn(self, ...)`와 같은
|
||||
모양이고(설치 발화에는 출처가 없어 `nil`이다),
|
||||
값이 아니라 **핸들과 메타데이터**만 넘기므로 "값을 안 실어주는 구독" 계약은
|
||||
안 깨진다. `base/source-state-plan.md`의 "`state:Observer(fn)`" 절이 소스.
|
||||
|
||||
## 7. 비용
|
||||
|
||||
사용자 추산(*"해시 for은 이미 빠르고, Source가 … 수 자체가 적다. 2~4개에 대해
|
||||
인덱싱 하는 정도"*)에 동의한다. 덧붙일 것 둘:
|
||||
|
||||
- 두 맵의 크기는 **그 노드 상류에 있는 서로 다른 루트 Source의 수**다.
|
||||
- 두 맵의 크기는 **그 노드 상류에 있는 서로 다른 루트 `Epoch`의 수**다.
|
||||
체인이 길어져도 안 늘고, `:With`로 합류할 때만 는다. UI 파생값에서 이 수가
|
||||
큰 경우는 드물다.
|
||||
- **[2026-08-21 정정]** 순회 조건이 뒤집혔으므로 비용 구도도 뒤집힌다 —
|
||||
**훑는 쪽이 흔한 경로**(`rawInvalid == false`, 즉 "안 바뀐 것 같다")다.
|
||||
그래도 훑는 대상이 위 크기(2~4)라 상수 시간에 가깝고, 정작 비싼
|
||||
**재계산은 안 도는** 경로다. 반대로 `rawInvalid == true`면 순회를 아예
|
||||
건너뛰고 바로 재계산한다.
|
||||
- **훑는 쪽(`rawInvalid == false`, 즉 "안 바뀐 것 같다")이 흔한 경로**다.
|
||||
그래도 훑는 대상이 위 크기(2~4)라 상수 시간에 가깝고, 정작 비싼 **재계산은
|
||||
안 도는** 경로다. 반대로 `rawInvalid == true`면 순회를 아예 건너뛰고 바로
|
||||
재계산한다.
|
||||
- 맵 하나당 객체 하나가 늘지만(State당 둘), 옛 모양도 테이블 둘이었으므로
|
||||
컴포지션으로 바뀌며 늘어난 비용은 메소드 디스패치뿐이다.
|
||||
|
||||
## 5. 열린 질문 (채택 전에 답이 필요)
|
||||
|
||||
1. **[해소, 2026-08-21] 선언 안 된 의존성 — 별도 UB 조항을 만들지 않는다.**
|
||||
에이전트가 "새 모델은 더 강한 약속을 하니 예외를 UB로 못 박아야 한다"고
|
||||
제안했으나 **사용자가 기각**: *"그건 아니다. 이 동작으로 인해 이제 정말로
|
||||
항상 state 는 get 이 최신을 던지는게 맞다. 상류의 상태를 물어보므로 그러함.
|
||||
이전과 다른게 없다고 생각한다."* — 선언 안 한 Source를 클로저로 읽는 건
|
||||
**지금 모델에서도 똑같이 stale**이고 이 변경이 악화시키는 게 없으므로,
|
||||
새 조항 없이 기존 "의존성은 선언한다"는 관례 그대로 둔다.
|
||||
2. **동적 의존성.** 조건에 따라 다른 상류를 읽는 계산이면 `sourceCountMap`이
|
||||
보수적 상위집합이 된다 — 틀리진 않고 재계산이 조금 더 잦아질 뿐이다.
|
||||
그대로 감수할지 확인.
|
||||
3. **[2026-08-21 해소] 순회가 발견한 변경을 어떻게 처분하는가 — 두 테이블로
|
||||
나눠 "값은 앞당기고 통지는 기다린다"로 확정.**
|
||||
|
||||
**문제**(사용자 지적): 순회가 count를 최신으로 올려두면 뒤늦게 도착한 진짜
|
||||
emit이 삼켜져 **하류가 그 에포크를 영영 못 받는다.** 값은 맞는데 통지가
|
||||
죽는, 2026-08-14에 폐기된 옛 dedup의 "영구 침묵"과 같은 계열의 실패 모드다
|
||||
(`archive/invalidate-dedup-propagation-reversed.md`).
|
||||
|
||||
**확정 해법**(사용자 제안, §2가 소스): 판정 기준을 **둘로 나눈다** —
|
||||
*"'내 값이, 상류의 상태로 하여금 사용 가능하다' 를 보는 테이블과, '내가
|
||||
emit을 받을 때, 그걸 다시 던져야하는지 봐야하나' 를 보는걸 나누는거죠."*
|
||||
순회는 `sourceCountMap`만 올리고 **emit은 안 한다**. 나중에 진짜 emit이
|
||||
오면 count는 같지만 `sourceEmitMap`이 달라 **그때 정상적으로 전파**된다
|
||||
(*"emit 바로 안하고 상류가 emit 해줄 때 까지 기다립니다"*). 결과:
|
||||
- 통지가 죽지 않는다.
|
||||
- 순회가 emit을 안 하므로 **게이트를 새게 하는 경로가 아예 없다** —
|
||||
`source = nil` 규약도, "게이트를 에포크 경계로" 같은 계약 반전도 필요 없다.
|
||||
- `Get()`은 순회 덕에 항상 신선하고, 순회가 `sourceCountMap`을 실제로
|
||||
올리므로 **다음 `Get()`이 같은 차이를 재발견해 또 재계산하는 낭비도 없다**
|
||||
(에이전트가 냈던 대안 (c)의 유일한 약점이 여기서 사라진다).
|
||||
|
||||
**같이 검토된 대안 — `rawEmit`을 태우고 `nil`을 던지는 안**(사용자 제안 1안):
|
||||
순회가 만든 emit을 **상류가 부르는 것과 같은 내부 진입점(`rawEmit`)** 으로
|
||||
흘려서, 그 노드에 `Gate`가 껴 있으면 자연히 게이트를 타게 하고, 순회가 찾은
|
||||
변경은 여러 소스일 수 있으니 하류엔 `source = nil`을 던진다는 안.
|
||||
**구조 위생 쪽은 채택할 만하다** — 상류 emit과 내부 발생 emit이 같은 진입점을
|
||||
쓰면 게이트가 붙은 노드에서 두 경로가 갈리지 않는다. 다만 **이 문제의 해법으로는
|
||||
위 2안이 더 낫다**:
|
||||
- 막고 있는 게이트는 보통 **순회하는 노드 자신이 아니라 상류**에 있다. 자기
|
||||
`rawEmit`을 태워봐야 상류 게이트는 안 물어보므로 **누출이 그대로 남는다.**
|
||||
- `nil` emit은 받는 쪽마다 **전체 순회를 강제**하고, 그 순회가 다시 count를
|
||||
올리면 같은 "영구 침묵" 문제가 하류에서 재발한다(연쇄).
|
||||
- 2안은 순회가 애초에 emit을 안 하므로 두 문제가 **생기지 않는다.**
|
||||
|
||||
**부수 확인**: `blocker:OffWithoutEmit()`처럼 emit 없이 푸는 경로에서
|
||||
`sourceEmitMap`이 뒤에 남아도, 그 소스의 **다음 진짜 emit**이 위 1번(둘 다
|
||||
다름)으로 걸려 정상화된다 — 별도 조치가 필요 없다.
|
||||
|
||||
4. **`:Emit()`(값을 제자리에서 mutate하고 알리는 경로)** — count만 올리면 되므로
|
||||
그대로 동작한다. 확인만.
|
||||
5. **weak 키.** `[source] -> count`를 weak-key로 두면 source가 죽을 때 항목이
|
||||
사라진다. 다만 `base/relate-plan.md`가 경고하듯 **값이 키를 되참조하면 안
|
||||
된다** — count는 숫자라 문제없다(테이블 identity 방식을 택하면 그 테이블이
|
||||
source를 참조하지 않게 할 것).
|
||||
6. **[2026-08-21 반영 완료] `Get`의 계약 문구.** 사용자 지적(*"Get 이 항상 최신
|
||||
상태를 가져온다라는 말이 여기서 무력화되는 부분"*)의 방향은 사실 **반대**였다
|
||||
— 옛 모델이 "최신"을 못 지키고 있었고 이 안이 그걸 지키게 만든다.
|
||||
`base/source-state-plan.md`의 전파 모델 절은 **채택과 함께 그렇게 다시
|
||||
썼다.**
|
||||
|
||||
7. **[2026-08-21 전량 해소] 두 맵의 초기값·병합·재계산 시 갱신 범위.**
|
||||
- **(a) 재계산 후 `sourceCountMap` 갱신 범위** — **자기가 읽은 상류 전부를
|
||||
갱신한다**(사용자: *"invalid 에 대한 계산을 위한 count 테이블은 단순히
|
||||
전부 업데이트 하는건 맞아보입니다"*). §2에 반영.
|
||||
- ⚠️ 이 항목을 제기할 때 든 근거("`A:Set(); Z:Set()`이면 같은 값을 두 번
|
||||
계산한다")는 **틀렸었다.** 전파는 동기라 `A:Set()`의 파동이 **완전히
|
||||
끝난 뒤에** `Z:Set()`이 시작되므로, 그 사이 재계산은 `Z`의 옛 값을 읽는
|
||||
게 맞고 통지가 두 번 나는 것도 맞다(사용자 정정: *"싱크라 set 의 emit 이
|
||||
전부 전파 된 다음 z:set 이라 두번 나는게 맞긴 해요"*). 전부 갱신이
|
||||
실제로 값을 하는 자리는 **게이트가 붙들고 있는 동안 하류가 `Get()`으로
|
||||
앞당겨 읽는 경우**뿐이다.
|
||||
- **(b) 새 노드의 두 맵 초기값** — `sourceEmitMap`은 **비우고**,
|
||||
`sourceCountMap`은 **전부 끌어와 실제 count로 채운 뒤 `rawInvalid = true`**.
|
||||
§2의 "노드가 생길 때의 초기값" 문단이 소스.
|
||||
- **(c) `:With` 병합 규칙** — (b)로 인해 **필요 없어졌다.**
|
||||
|
||||
## 6. 곁가지 — 폴링용 sugar
|
||||
|
||||
사용자 제안: *"폴링을 위해서는 Apply(Realtime()) 같은 슈거를 주면 된다.
|
||||
옵져버로 항상 get 하고 value 를 실시간으로 읽을 수 있게 해주는것. 단순하게
|
||||
Ref<T> 로 변환해주는 등"*. 즉 "항상 최신 값을 필드로 들고 있는 박스"를 만드는
|
||||
순수 슈가. **이건 이 문서의 결정과 독립**이고(에포크를 채택하든 안 하든 쓸 수
|
||||
있다), `research/operator-sugar-plan.md`의 콤비네이터 계열에 더 가깝다 —
|
||||
채택되면 그쪽으로 옮길 것.
|
||||
|
||||
## 7. 구현 시 확인할 것 (채택 확정 후 남은 실무 항목)
|
||||
## 8. 구현 시 확인할 것
|
||||
|
||||
- **역전된 두 서술은 `archive/always-propagate-no-dedup-superseded.md`에 있다** —
|
||||
"emit은 자기 `invalid`와 무관하게 **항상** 전파된다"와 "quad가 접지 않는 것은
|
||||
중복 *통지*뿐이다". 지금 계약은 **"`invalid`로는 절대 안 접고, 같은 소스의 같은
|
||||
에포크가 두 번째로 도착했을 때만 접는다"**이다. 이 구분을 흐리면 2026-08-14에
|
||||
폐기된 "영구 침묵" 버그로 되돌아간다(§3).
|
||||
- **선언 안 된 의존성에 대한 UB 조항은 안 만든다** — §5의 1번(사용자 기각).
|
||||
- **`luau-test` 스파이크 하나**: §1의 다이아몬드를 그대로 짜서 (a) 에포크 없이는
|
||||
섞인 값이 실제로 관측되는지, (b) 에포크 비교를 넣으면 사라지는지 대조.
|
||||
- **`sourceEmitMap`은 희소 `pending` 구현으로 시작해도 된다** — §2의 구현 메모.
|
||||
- 곁가지였던 폴링 슈가는 §6 그대로 — 이 결정과 독립이고 `operator-sugar-plan.md`
|
||||
계열로 남는다.
|
||||
중복 *통지*뿐이다". 지금 계약은 **"`invalid`로는 절대 안 접고, 같은 `Epoch`의
|
||||
같은 리비전이 두 번째로 도착했을 때만 접는다"**이다. 이 구분을 흐리면
|
||||
2026-08-14에 폐기된 "영구 침묵" 버그로 되돌아간다.
|
||||
- **선언 안 된 의존성에 대한 UB 조항은 안 만든다.** 에이전트가 "새 모델은 더
|
||||
강한 약속을 하니 예외를 UB로 못 박아야 한다"고 제안했으나 **사용자가 기각**:
|
||||
*"그건 아니다. 이 동작으로 인해 이제 정말로 항상 state 는 get 이 최신을
|
||||
던지는게 맞다. 상류의 상태를 물어보므로 그러함. 이전과 다른게 없다고
|
||||
생각한다."* — 선언 안 한 Source를 클로저로 읽는 건 **옛 모델에서도 똑같이
|
||||
stale**이었고 이 변경이 악화시키는 게 없다.
|
||||
- **동적 의존성**은 `valueEpochMap`이 보수적 상위집합이 된다 — 틀리진 않고
|
||||
재계산이 조금 더 잦아질 뿐이다.
|
||||
- **`Source:Emit()`**(값을 제자리에서 mutate하고 알리는 경로)은 `Revision`만
|
||||
갱신하면 그대로 동작한다(`:Set()`과 같은 연산 — §2).
|
||||
- **`blocker:OffWithoutEmit()`처럼 emit 없이 푸는 경로**에서 `emitEpochMap`이
|
||||
뒤에 남아도, 그 `Epoch`의 **다음 진짜 emit**이 §4의 1번(둘 다 다름)으로
|
||||
걸려 정상화된다 — 별도 조치가 필요 없다.
|
||||
- **`luau-test` 스파이크 하나**: §1의 다이아몬드를 그대로 짜서 (a) 리비전 비교
|
||||
없이는 섞인 값이 실제로 관측되는지, (b) 비교를 넣으면 사라지는지 대조.
|
||||
- **기각된 대안 — 게이트를 에포크 경계로.** 게이트가 **자기 자신을 하나의
|
||||
`Epoch`로** 내세우고(자기 `Revision`은 유보를 풀 때만 갱신) 하류가 상류
|
||||
원천 대신 **그 게이트만** 추적하게 하는 안. 배치를 나를 필요가 없어져
|
||||
페이로드가 단순해지지만, **`base/gate-plan.md` 3번의 공개 계약을
|
||||
정면으로 깬다** — *"게이트를 통과하지 않은 값도 `:Get()`으로는 보인다."*
|
||||
- **깨지는 경로**: 게이트가 붙들고 있는 동안 하류가 `:Get()`을 부르면
|
||||
§4의 재계산 판정이 `valueEpochMap:Refresh()`를 돈다. 그 맵이 추적하는
|
||||
게 게이트 하나뿐이면 게이트의 `Revision`은 아직 안 올랐으므로 **"나는
|
||||
최신"으로 오판하고 옛 캐시를 반환**한다. 지금처럼 원천 `Epoch`를 직접
|
||||
추적해야 순회가 상류의 실제 리비전을 보고 스스로 낡음을 알아챈다.
|
||||
- 즉 게이트는 **emit만 가로채지 값을 가리지 않는다**는 성질이 이 문서의
|
||||
핵심 보장(`:Get()`은 항상 최신)과 한 몸이다. `Blocker`
|
||||
(`base/blocker-plan.md`의 "`:Get()`엔 영향 없음")와
|
||||
`base/debounce-throttle-plan.md` §4도 같은 계약 위에 서 있으므로,
|
||||
이걸 뒤집으면 셋이 같이 무너진다.
|
||||
- **기각된 대안 — 순회가 만든 emit을 `rawEmit`으로 흘리고 `nil`을 던지는 안.**
|
||||
구조 위생은 좋았으나(상류 emit과 내부 발생 emit이 같은 진입점) 이 문제의
|
||||
해법으로는 못 쓴다 — (a) 막고 있는 게이트는 보통 **순회하는 노드 자신이
|
||||
아니라 상류**에 있어 누출이 그대로 남고, (b) `nil` emit은 받는 쪽마다 전체
|
||||
순회를 강제해 같은 "영구 침묵"이 하류에서 연쇄로 재발한다. 지금 안은 순회가
|
||||
애초에 emit을 안 하므로 두 문제가 **생기지 않는다.**
|
||||
- **곁가지 — 폴링용 슈가.** 사용자 제안: *"폴링을 위해서는 Apply(Realtime())
|
||||
같은 슈거를 주면 된다. 옵져버로 항상 get 하고 value 를 실시간으로 읽을 수
|
||||
있게 해주는것. 단순하게 `Ref<T>` 로 변환해주는 등"*. **이 문서의 결정과
|
||||
독립**이고 `research/operator-sugar-plan.md`의 콤비네이터 계열에 속한다.
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ State<T | Tween<T>>`가 나옴 — Modifier/State/Source/StoreBind 코드엔
|
|||
|
||||
**핸들러 계층 UB 체크와도 안 부딪힘** — `Tween<T>`는 `Ref`/`Observer`/
|
||||
`Slot`류처럼 dispatch 참가자(`process`를 가진 Handler에 매칭되는 값)가 아니라 `None`/
|
||||
`Tag`처럼 순수 raw 데이터 값(별도 `TweenTag` Brand)이라, Modifier 필드/
|
||||
`Tag`처럼 순수 raw 데이터 값(별도 `TweenBrand`)이라, Modifier 필드/
|
||||
`State<Modifier>`가 막는 "핸들러 계층 값" 규칙(`base/modifier-plan.md`)에
|
||||
안 걸림 — 그 문서가 원래 Tween을 Slot/Tag/Attribute와 같은 "dispatch
|
||||
참가자" 그룹으로 분류해뒀던 건 부정확했던 것으로 이번에 정정(아래
|
||||
|
|
@ -373,7 +373,7 @@ quad-roblox 레벨 편의 함수라 base 계약에 영향 없음.
|
|||
## 패키지 경계 — `Tag`가 이미 밟은 것과 같은 분리 (2026-08-10 세션 확정)
|
||||
|
||||
- **quad-base**: `Tween.luau` — 값 타입(`Tween(opts)` 팩토리, `isTween`
|
||||
predicate/`TweenTag` Brand)만. 엔진 무관.
|
||||
predicate/`TweenBrand` Brand)만. 엔진 무관.
|
||||
- **quad-roblox**: `Handlers/Property.luau`(기존 프로퍼티 세팅 로직에
|
||||
`isTween` 분기 + 3-상태 릴레이션 저장 + override 정책 추가) +
|
||||
`Animate.luau`(편의 콤비네이터, 신규).
|
||||
|
|
|
|||
|
|
@ -44,10 +44,16 @@ ROADMAP 항목 근거인지, 어떻게 실행하는지, 실행 후 뭘 확인해
|
|||
|
||||
| 환경 | 필요한 것 | 해당 파일 |
|
||||
|---|---|---|
|
||||
| **순수 Luau CLI** (`luau`) | [luau-lang/luau 릴리즈](https://github.com/luau-lang/luau/releases)의 `luau` 인터프리터, 또는 `lune` | 01, 02, 03, 04, 05, 06(런타임 부분), 07, 11, 13(런타임 부분), 17, 18, 19, 20 |
|
||||
| **Luau 타입체커** (`luau-analyze` 또는 `luau-lsp`) | 같은 릴리즈에 포함된 `luau-analyze`, 또는 `luau-lsp analyze`/에디터 인라인 진단 | 06(타입 부분), 08, 09, 12, 13(타입 부분), 14, 15, 16 |
|
||||
| **순수 Luau CLI** (`luau`) | [luau-lang/luau 릴리즈](https://github.com/luau-lang/luau/releases)의 `luau` 인터프리터, 또는 `lune` | 01, 02, 03, 04, 05, 06(런타임 부분), 07, 11, 17, 18, 19, 20, 22 |
|
||||
| **Luau 타입체커** (`luau-analyze` 또는 `luau-lsp`) | 같은 릴리즈에 포함된 `luau-analyze`, 또는 `luau-lsp analyze`/에디터 인라인 진단 | 06(타입 부분), 08, 09, 12, 13, 14, 15, 16, 21, 23 |
|
||||
| **Roblox Studio** | 별도 계정으로 로그인(`HUMAN_TODO.md` 1번, `SAFETY.md` 준수) | 10 |
|
||||
|
||||
**[2026-08-21 정정]** 이 표가 `21`/`22`/`23`을 빠뜨린 채 `13`을 "런타임 부분/
|
||||
타입 부분"으로 쪼개 적고 있었다 — `13`의 런타임 절반은 2026-08-19에 `22`로
|
||||
분리돼 나갔으므로 지금 `13`은 **순수 타입 스파이크**다. **각 파일이 어느
|
||||
환경에서 도는지는 파일 맨 위 주석이 소스**이고, 이 표는 그걸 환경별로 묶어
|
||||
보여주는 편의 색인일 뿐이다 — 파일이 늘면 여기도 같이 고칠 것.
|
||||
|
||||
**16은 특히 `type function`이라는 비교적 최근/계속 진화 중인 Luau 기능을
|
||||
쓰므로, luau-analyze 버전이 오래되면 아예 문법 자체를 못 알아볼 수 있음**
|
||||
— 그 경우는 실패가 아니라 "이 Luau 버전에서 type function 자체가 아직
|
||||
|
|
@ -88,7 +94,7 @@ ROADMAP 항목 근거인지, 어떻게 실행하는지, 실행 후 뭘 확인해
|
|||
| `19-ownership-refcount-relate-patterns.luau` | **[2026-08-13 신규, 같은 날 B/C 전면 재작성 — 지금은 현행 설계 기준]** 세 소유권/참조카운트 알고리즘 검증. **A**: Tag `tagNameMap` 참조 카운트(여러 위치가 같은 이름을 겹쳐 가져도 마지막 홀더가 빠질 때만 실제 `RemoveTag`. 옛 `kTagMap`은 클로저 캡처로 대체돼 삭제됨). **B**: Attribute 이름 소유권 — 공개 `AttributeKey(name)` 캐시 + `Dispatch.process`의 인덱스 1 **점유 체크**가 충돌을 잡는지(옛 `rawNew`+`owners` 수동 레지스트리는 폐기). **C**: Slot 소유권 — nested 엄격 `claimOwner`(같은 owner 재클레임도 error) vs top-level `claimOwnerAt(inst,k)`(정확히 같은 자리 재발행만 no-op). **셋 다 음성 대조군 포함** — 옛 로직이 `Slot{a,a}`/`Frame{slot,slot}`을 조용히 통과시키는 걸 재현. **[2026-08-13 열네 번째 세션] 0-Z가 확정되며 B 섹션이 낡음 → `rewrite-required/`** — 이제 "그룹 전용 키 + `AttributeKeyHandler`의 이름 claim"을 검증해야 함(A/C는 그대로 유효) | `tag-plan.md` "메커니즘", `attribute-plan.md` "이름 소유권", `slot-plan.md` "요소 소유권" |
|
||||
| `20-slot-splice-index-arithmetic.luau` | **[2026-08-13 신규]** `Slot:Splice(index, removeCount, ...newElements)`의 shift+recompute 1회 계산이, `Extract`/`Add` 반복으로 재현한 참조 구현과 항상 같은 결과를 내는지 — 제거/삽입 길이가 다를 때(delta 양수/음수) 뒤 요소가 밀리는 방향과 양을 헷갈리는 off-by-one 위험(이 프로젝트가 `Dispatch.recompute`에서 실제로 냈던 것과 같은 클래스의 버그)을 경계값 케이스로 검증 | `slot-plan.md` "확정" CRUD 표 + "`Splice` 신설" 절(2026-08-12 열다섯 번째 세션), `dispatch-core-plan.md`의 `recompute` off-by-one 수정 사례(2026-08-11 여섯 번째 세션) |
|
||||
| `21-type-store-undeclared-key-rejected.luau` (타입체크 전용) | **[2026-08-19 신규]** `Store<{field: T}>`로 선언 안 된 이름에 dot-access하면 `type function`이 합성한 결과 타입(`ProcessStoreType`, `16`과 동일)에 그 프로퍼티가 없어 타입 시간에 거부되는지 — `store-plan.md`가 "아마 그럴 것"으로만 적어뒀던 걸 M0에서 실측. 통과: 미선언 키 접근 2건이 정확히 `TypeError`로 걸림 | `store-plan.md` "Store = Source들의 이름 붙은 모음" 절의 "[확인 요구, 2026-08-18 구현 전 QA]" 항목, `todos.md` 00번 |
|
||||
| `22-runtime-ref-preref-postref-brand.luau` | **[2026-08-19 신규]** 구 `13`의 런타임(B) 절반을 분리한 것 — `isPreRef`/`isPostRef`가 같은 층위의 배타적 형제(둘 다 `isRef`엔 `true`, 서로에겐 `false`)인지, Leaf 핸들러 흉내(`isRef(v) and not isPreRef(v) and not isPostRef(v)`)가 Ref/PreRef/PostRef 셋을 정확히 갈라내는지 | `ref-plan.md`의 "`PostRef`" 절, `brand-plan.md`의 `Brand` 절 |
|
||||
| `22-runtime-ref-preref-postref-brand.luau` | **[2026-08-19 신규]** 구 `13`의 런타임(B) 절반을 분리한 것 — `isPreRef`/`isPostRef`가 같은 층위의 배타적 형제(둘 다 `isRef`엔 `true`, 서로에겐 `false`)인지, Leaf 핸들러 흉내(`isRef(v) and not isPreRef(v) and not isPostRef(v)`)가 Ref/PreRef/PostRef 셋을 정확히 갈라내는지. **[2026-08-21] `rewrite-required/`로 이동** — 파일이 직접 구현해 쓰는 `Brand.set`/`Brand.get`이 인스턴스 브랜드 재작성으로 역전된 옛 API가 됐다(검증 대상 자체는 그대로 유효, 상태와 재작성 지침은 `STATUS.md`가 소스) | `ref-plan.md`의 "`PostRef`" 절, `brand-plan.md`의 "⭐ 구현 — 인스턴스 브랜드" 절 |
|
||||
| `23-type-quadtypes-checkversion-addplugin.luau` (타입체크 전용) | **[2026-08-19 신규, 같은 날 후속으로 재작성]** 실제 `quad-types`/`quad-base`/`type-version-check`를 `require`해서 `CheckedQuad<T, Pattern>`(글롭/캐럿 버전 패턴 체크, `type-version-check` 위에 얹힘)이 `AddPlugin<Self,P>` 체이닝과 맞물려 동작하는지 — 양성(버전 일치 + 2단 체이닝 + 이전 확장 필드 보존), 음성(버전 불일치 → 강제 참조 시점에 정확히 `TypeError`). `type function`을 거친 값은 패스스루라도 이후 제네릭 self 체이닝이 깨진다는 걸 이 스파이크가 재작성 과정에서 직접 발견. 재작성 과정에서 `export type function`(cross-package 필수)과 2개 이상 명시 제네릭 인스턴스화의 이중 꺾쇠(`Foo<<A,B>>`) 요구도 추가로 실측 확인 | `quad-types-plan.md`, `typing-limits.md` §6 |
|
||||
|
||||
## 공통 유틸리티
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
# 스파이크 상태판 — **폴더가 곧 상태**
|
||||
|
||||
> 마지막 갱신: **2026-08-21** — QA 4라운드 `F-4-1`로 `Dispatch.drive`의
|
||||
> 마지막 갱신: **2026-08-21** — `Brand`가 **인스턴스 브랜드**로 전면
|
||||
> 재작성되면서(`base/brand-plan.md`) 옛 `Brand.set`/`Brand.get`을 직접 구현해
|
||||
> 쓰던 `22`가 `done/` → `rewrite-required/` 이동(검증 대상인 `isRef`/`isPreRef`
|
||||
> 포함 관계 자체는 그대로). 같은 날 `Epoch`/`EpochMap` 승격도 있었으나 그건
|
||||
> 이미 `rewrite-required/`에 있는 `05`의 지침에 반영돼 있다. 직전 갱신도
|
||||
> 같은 날 — QA 4라운드 `F-4-1`로 `Dispatch.drive`의
|
||||
> props 순회가 **단일 일반화 `for`**로 정정되면서, 두 루프 버전을 검증하던
|
||||
> `01`이 낡아 `done/` → `rewrite-required/` 이동(검증 대상인 순서 계약
|
||||
> 자체는 그대로). 같이 **만들어야 할 스파이크** 절 신설 — 아직 파일이
|
||||
|
|
@ -40,9 +45,9 @@
|
|||
| 폴더 | 뜻 | 개수 | 누가 처리 |
|
||||
|---|---|---|---|
|
||||
| `review-required/` | **설계가 걸림 — 사람 결정 필요** | **0** | ⭐ 사용자 |
|
||||
| `rewrite-required/` | 스파이크가 낡음(코드가 깨졌거나, 설계가 바뀌어 옛 모델을 검증 중) | 6 | 에이전트 |
|
||||
| `rewrite-required/` | 스파이크가 낡음(코드가 깨졌거나, 설계가 바뀌어 옛 모델을 검증 중) | 7 | 에이전트 |
|
||||
| `not-run/` | 이 환경에서 못 돌림(Studio 전용) | 0(+헬퍼 1) | 사용자 or MCP 연결 후 에이전트 |
|
||||
| `done/` | 통과 or 판정 끝, 더 할 일 없음 | 17 | — |
|
||||
| `done/` | 통과 or 판정 끝, 더 할 일 없음 | 16 | — |
|
||||
|
||||
**폴더를 옮기는 게 곧 상태 갱신** — 스파이크를 고치거나 돌렸으면 파일을
|
||||
해당 폴더로 `git mv`하고 아래 표의 줄도 같이 옮길 것. 파일별 "무엇을 왜
|
||||
|
|
@ -67,7 +72,10 @@
|
|||
`rewrite-required/`에 그대로 둠 — 재작성 대상이지 사람 결정 대상이
|
||||
아님(계약 자체는 위에서 이미 확정됨).
|
||||
|
||||
## 🟠 `rewrite-required/` — 스파이크가 낡음 (5건)
|
||||
## 🟠 `rewrite-required/` — 스파이크가 낡음
|
||||
|
||||
(개수는 위 표와 폴더가 소스 — 여기서 다시 세지 않는다. 예전엔 이 제목이
|
||||
개수를 들고 있다가 실제와 어긋난 적이 있다.)
|
||||
|
||||
**[2026-08-13 열네 번째 세션] 앞의 두 건은 "코드가 깨진" 게 아니라 "설계가
|
||||
바뀐" 경우** — `question.md` 0-A/0-Z 확정으로 재디스패치가 **하강 diff**가
|
||||
|
|
@ -86,6 +94,15 @@
|
|||
통과 상태로 `done/`에 두면 `01`은 구현이 안 하는 두 루프 순회를, `05`는
|
||||
**이제 접히는 중복 통지가 안 접힌다는 것**을 "검증됨"으로 오독하게 된다.
|
||||
|
||||
**[2026-08-21 후속] `22`도 같은 이유로 합류** — `Brand`가 **인스턴스
|
||||
브랜드**로 전면 재작성되면서(`base/brand-plan.md`) 이 스파이크가 직접 구현해
|
||||
쓰는 `Brand.set`/`Brand.get`/`XxxTag`가 **역전된 옛 API**가 됐다
|
||||
(`archive/brand-shared-registry-reversed.md`). **검증 대상 자체는 그대로
|
||||
유효하다** — `isPreRef`/`isPostRef`가 서로 배타적 형제이고 둘 다 `isRef`엔
|
||||
`true`라는 포함 관계는 재작성 후에도 안 바뀌었다. 옮기는 이유는 결론이
|
||||
틀려서가 아니라, 통과 상태로 `done/`에 두면 **구현자가 그 파일의 `Brand`
|
||||
구현을 참고 모델로 오독**하기 때문이다.
|
||||
|
||||
| 파일 | 상태 | 무엇을 고쳐야 하나 |
|
||||
|---|---|---|
|
||||
| `01-two-pass-array-hash-order.luau` | 옛 형태 기준으로는 ✅ 통과였음 | 숫자 `for` + 일반화 `for` **두 루프**로 짜여 있는데, 구현은 **단일 일반화 `for`**로 정정됨(`base/dispatch-core-plan.md`의 "props 순회 순서" 절, QA 4라운드 `F-4-1`) — Luau의 일반화 `for`가 배열 파트를 먼저 다 돌고 해시 파트로 넘어간다는 것 자체를 **한 루프로** 검증하도록 다시 쓸 것. **검증 대상(순서 계약)은 그대로**라 결론이 바뀌는 건 아님 |
|
||||
|
|
@ -93,6 +110,7 @@
|
|||
| `04-dispatch-chain-retractFrom.luau` | 옛 모델 기준으로는 ✅ 통과였음 | (1) `chains` 슬롯이 `{handler, retractor}`가 되고 `Dispatch.process`가 핸들러를 먼저 비교하는 **하강 diff**로 재작성, (2) `retractFrom`은 **3-인자**(힌트 인자 없음), (3) "힌트가 target 인덱스에만 간다"를 검증하던 부분은 **정반대**로 뒤집힘 — 이제 각 레벨이 자기 값을 받는지를 검증해야 함. **살릴 것**: `chains:SetStrong` 순서 음성 대조군(그 버그는 새 모델에서도 그대로 유효) |
|
||||
| `19-ownership-refcount-relate-patterns.luau` | A/C ✅ 유효, **B 섹션이 낡음** | B가 검증하던 "공개 `AttributeKey(name)` + 인덱스 1 점유 체크"가 폐기됨 — **그룹 전용 키 + `AttributeKeyHandler`의 이름 claim**으로 재작성하고, 음성 대조군도 "두 그룹이 같은 이름 → 즉시 error", "그룹↔직접 쓰기 → 즉시 error"로 바꿀 것(0-Z 확정 내용). A/C는 손댈 것 없음 |
|
||||
| `15-type-compute-trailing-deps-typepack.luau` | **파싱 실패**(SyntaxError) | 음성 대조군의 타입 표기가 `TypeError`가 아니라 `SyntaxError`로 걸려 **파일 전체가 아무것도 검증 못 함** — 대조군을 별도 파일/블록으로 격리 |
|
||||
| `22-runtime-ref-preref-postref-brand.luau` | 옛 `Brand` API 기준으로는 ✅ 통과였음 | **[2026-08-21] `Brand`가 인스턴스 브랜드로 재작성됨** — 파일 안의 `Brand.set(x, tag)`/`Brand.get(x)`/`XxxTag` 변수를 `Brand()` + `SomeBrand:register(x)`/`SomeBrand:is(x)`로 바꿔 쓸 것(`base/brand-plan.md`). **검증 대상(`isPreRef`/`isPostRef` 배타 + 둘 다 `isRef`엔 `true`, Leaf 핸들러 흉내)은 그대로**라 assert는 손댈 게 없다. **새로 넣을 것**: 다중 태깅이 실제로 되는지 — 한 값을 두 브랜드에 등록하고 양쪽 `:is`가 다 `true`인지(`Source`가 `SourceBrand`+`EpochBrand`인 자리, `base/state-epoch-plan.md` §2) |
|
||||
| `10-roblox-studio-checks.server.luau` (Studio 전용) | 미실행 + **A 섹션이 옛 모델** | A가 옛 2-인자 `canExecute(inst,value)`와 `bindLifetime`의 `.Subscribed` 세팅을 검증 중 — **`bindLifetime`이 gcconn을 `value` 쪽 릴레이션에 복사하는 모델**로 재작성할 것(`base/lifecycle-pattern.md`). **[2026-08-14 열한 번째 세션 재정정, 2026-08-18 방향 정정]** 이중 바인딩 게이트는 `canBound(value)`(`if not canBound(v) then error(...) end` — `canBound` 참 = "지금 묶어도 됨") — `canExecute`는 State emit 전파 게이팅 전용으로 분리됨, 둘 다 비공개 헬퍼 `isBoundAlive`를 공유하는 1-인자 진입점이지만 **서로의 부정**(`base/lifecycle-pattern.md`의 "`canBound` vs `canExecute`" 절). **살릴 것**: "ClassName 신호 미발화 / Destroy 시 `Connected` 즉시 전환" 검증(새 모델에서 더 중요해짐), gcconn/gchold를 **Instance 생성 시점**에 만드는 것으로 바꿀 것(옛 lazy 생성 폐기). B/C 섹션은 손댈 것 없음 |
|
||||
|
||||
## ⚪ `not-run/` — 이 환경에서 못 돌림
|
||||
|
|
@ -104,10 +122,15 @@
|
|||
|---|---|
|
||||
| `gc-trigger-helper.server.luau` | 스파이크가 아니라 **헬퍼** — Studio에 `collectgarbage()`가 없어서 GC를 강제 트리거하는 기법. `10`을 돌릴 때 같이 씀 |
|
||||
|
||||
## ✅ `done/` — 통과 or 판정 끝 (17건)
|
||||
## ✅ `done/` — 통과 or 판정 끝
|
||||
|
||||
**지금 `done/`에 있는 런타임 스파이크는 9건**(`02`/`03`/`06`/`07`/`11`/`17`/
|
||||
`18`/`20`/`22`), **전원 통과**(crash 0 / FAIL 0). 나머지 8건은 타입 스파이크다.
|
||||
(개수는 위 표와 폴더가 소스 — 여기서 다시 세지 않는다.)
|
||||
|
||||
**지금 `done/`에 있는 런타임 스파이크는 `02`/`03`/`06`/`07`/`11`/`17`/`18`/`20`,
|
||||
전원 통과**(crash 0 / FAIL 0). 나머지는 타입 스파이크다.
|
||||
**[2026-08-21] `22`는 여기서 빠졌다** — `Brand` 인스턴스 브랜드 재작성으로
|
||||
파일이 쓰는 `Brand.set`/`Brand.get`이 옛 API가 되어 `rewrite-required/`로
|
||||
이동(검증 대상 자체는 유효, 위 표의 재작성 지침 참고).
|
||||
**[2026-08-21 정정]** 여기 "런타임 12개"라고 적혀 있었는데, 그 산술이 이미
|
||||
`rewrite-required/`로 나간 `04`/`10`/`19`까지 포함한 옛 총계에서 이어져 온
|
||||
것이라 실제와 안 맞았다(두 번째 `/code-review high` 발견). — **[열네 번째 세션] `04`/`19`는
|
||||
|
|
@ -126,7 +149,6 @@
|
|||
| `17-modifier-index-tableclone-chaining` | 제네릭 `__index` + `table.clone` 체이닝, 메타테이블 참조 공유, 형제 분기 무오염 |
|
||||
| `18-relate-mutual-cycle-gc` | **두 `Relate` 상호 순환은 실제로 GC 안 됨**(아래 별도 절) |
|
||||
| `20-slot-splice-index-arithmetic` | `Splice` 산술 11개 경계 케이스 전부 참조 구현과 일치 |
|
||||
| `22-runtime-ref-preref-postref-brand` | **[2026-08-19 신규]** `isPreRef`/`isPostRef`가 서로 배타적 형제(둘 다 `isRef`엔 `true`, 서로에겐 `false`)임을 확인, Leaf 핸들러 흉내(`isRef(v) and not isPreRef(v) and not isPostRef(v)`)가 Ref/PreRef/PostRef 셋을 정확히 갈라냄 |
|
||||
|
||||
**타입 스파이크 중 판정이 끝나 더 할 일 없는 것**:
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,9 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션
|
|||
- `.claude/reference/` — **[2026-08-07 신설]** base처럼 확정된 건 아니지만
|
||||
base 문서가 근거로 인용하는 온디맨드 참고 자료(v1 내부 동작 스냅샷,
|
||||
Fusion/Vide 비교 리서치) — 항상 읽을 필요는 없고 인용될 때만 열어볼 것.
|
||||
**[2026-08-21 확장]** 확정된 결정의 **근거 기록**(그 결정이 왜 그렇게
|
||||
났는지)도 여기 둠 — `research/`를 떠났지만 `archive/` 대상은 아닌 것들.
|
||||
어떤 문서가 있는지는 `.claude/README.md`가 소스.
|
||||
- `.claude/research/` — 아직 착수 전, 사용자와 상의 필요한 설계 논의. 전부
|
||||
후순위. **어떤 문서가 있는지·우선순위가 뭔지는 여기서 세지도 나열하지도
|
||||
않고 `.claude/README.md`의 `research/` 표로 미룸**(개수뿐 아니라 파일명
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
않는다(사용자 지시). 아래 A~G절은 거기까지 온 처리 과정의 기록.
|
||||
|
||||
**[2026-08-21] 3차 처리 — 아래 G절.**
|
||||
`F-4`는 전부 닫혔고(`F-4-3`은 `research/slot-attach-decomposition.md`로 넘어감),
|
||||
`F-4`는 전부 닫혔고(`F-4-3`은 `reference/slot-attach-decomposition.md`로 넘어감),
|
||||
그 시점에 남았던 건 `F-3`의 (1)~(3)과 "+"였다(요약은 `G-3`, H절에서 닫힘).
|
||||
|
||||
**[2026-08-21] 2차 처리 — 아래 F절.**
|
||||
|
|
@ -1030,7 +1030,7 @@ if input[key] ~= nil then continue end -- "이미 있으면 건너뛴다" =
|
|||
맞을지도. attachSlot 의 기능이 너무 다양해진게 문제같음. 이 부분에 있어서는
|
||||
확장 논의를 하게 준비해두자."*
|
||||
|
||||
→ **`research/slot-attach-decomposition.md` 신설.** `setLength`를 어느 줄에
|
||||
→ **`reference/slot-attach-decomposition.md` 신설.** `setLength`를 어느 줄에
|
||||
둘지 고르는 문제가 아니라 분해 문제라는 진단에 동의하고, 논의가 바로 시작될 수
|
||||
있게 재료만 모아뒀다(**아무것도 확정 안 함**, `base/slot-plan.md`가 여전히 정본):
|
||||
|
||||
|
|
@ -1141,7 +1141,7 @@ if input[key] ~= nil then continue end -- "이미 있으면 건너뛴다" =
|
|||
|
||||
## H-4. `attachSlot` 분해 (확정·반영)
|
||||
|
||||
`research/slot-attach-decomposition.md`를 **확정**으로 승격하고 의사코드를
|
||||
`reference/slot-attach-decomposition.md`를 **확정**으로 승격하고 의사코드를
|
||||
`base/slot-plan.md`에 반영했다. 결론은 후보 **(B)**:
|
||||
|
||||
- **`materializeSlotTree(slot, physicalTarget, ownerKey, position)`** — 부기만.
|
||||
|
|
|
|||
|
|
@ -724,7 +724,7 @@ G절에서 신설한 `_baseObserver`(자기 `Offset`을 관측해 자식 offset
|
|||
| 2 | **`effect-plan.md`가 자기 자신과 모순** | 문서 최상단 시그니처가 `Effect(fn, state?)`이고 "**`Effect(fn, a, b, c)`처럼 trailing args로 받는 sugar는 의도적으로 안 만듦**"이라는 확정 문단이 그대로 남은 채, 같은 파일 아래에 `C-6`의 `Effect(fn, ...deps)` 절이 추가돼 있었다 — **역전 배너 없이 정반대 두 서술이 공존** | 시그니처를 `Effect(fn, ...deps)`로 갱신, 옛 문단에 🔄 역전 배너 + **옛 근거가 무너진 이유 둘**(`Ref`는 `:With`로 합칠 수 없어 애초에 의존성이 될 방법이 없었다 / 구독을 따로 걸면 합치는 노드 자체가 안 생겨 "감출 비용"이 없다) |
|
||||
| 3 | **`indexOfRaw`가 어디에도 정의돼 있지 않음** | 신설 `rawReplace`가 쓰는데 문서에 없었다. 구현자가 공개 `IndexOf`를 그대로 쓰면 **래핑된 자리에서 어긋난다**(그건 언래핑 기준 비교) | `indexOfRaw` 한 줄 정의 + `IndexOf`와의 차이 명시 |
|
||||
| 4 | **index/element 혼용 캐비엇 목록이 안 늘어남** | 기존 캐비엇이 `rawRemove`/`rawUnmount`만 나열하는데, 이번에 같은 불일치를 물려받은 `rawDetach`/`releaseElement`/`rawReplace`가 빠져 있었다 | 캐비엇에 셋 추가 |
|
||||
| 5 | **`mountSlotTree`의 전제가 코드에 없었다** | `acc = slot.Offset:Get()`이 정확하려면 **`materializeSlotTree`가 먼저 돌아야** 하는데, 분해된 함수의 계약이 주석에 없었다(`research/slot-attach-decomposition.md`에 "중간 상태 처리"가 열린 항목이라 더 위험) | ⚠️ 전제 주석 추가 |
|
||||
| 5 | **`mountSlotTree`의 전제가 코드에 없었다** | `acc = slot.Offset:Get()`이 정확하려면 **`materializeSlotTree`가 먼저 돌아야** 하는데, 분해된 함수의 계약이 주석에 없었다(`reference/slot-attach-decomposition.md`에 "중간 상태 처리"가 열린 항목이라 더 위험) | ⚠️ 전제 주석 추가 |
|
||||
| 6 | **`Replace`의 `destroyOld`가 base 본문에 없었다** | `rawReplace`에 인자가 있는데 CRUD 표/산문이 그 존재를 설명 안 함. followup은 처리 기록이지 소스가 아니다(이번 라운드 `CR-1`의 교훈 그대로) | 공개 `Replace`는 항상 파괴 / `:List`만 `_owned`를 넘긴다를 산문에 명시 |
|
||||
|
||||
부수로 인덱스도 같이 정리했다 — **`Gate` 이름이 `question.md` 1번(용어
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@
|
|||
| `QT` | quad-types / 버전 체크 (4라운드 문항 없음) | `base/quad-types-plan.md` |
|
||||
| `IM` | **실제 커밋된 M1 코드** (문서 아님) | `quad-base/src`, `quad-types/src`, `type-version-check/src` |
|
||||
| `DE` | `Detach`/`_detached`/`KeyGone`/`Owned` (신규 확정) | `base/slot-plan.md` |
|
||||
| `AS` | `attachSlot` 분해 (신규 확정) | `base/slot-plan.md`, `research/slot-attach-decomposition.md` |
|
||||
| `AS` | `attachSlot` 분해 (신규 확정) | `base/slot-plan.md`, `reference/slot-attach-decomposition.md` |
|
||||
| `DC` | 디스패치 코어 심화 | `base/dispatch-core-plan.md` |
|
||||
| `SS` | Source/State 심화 | `base/source-state-plan.md` |
|
||||
| `LC` | 생명주기 배관 심화 | `base/lifecycle-pattern.md`, `base/relate-plan.md` |
|
||||
|
|
|
|||
|
|
@ -40,40 +40,6 @@
|
|||
`base/lifecycle-pattern.md`의 "`canBound` vs `canExecute`" 절,
|
||||
`archive/canexecute-inst-arg-reversed.md` 하단 addendum 참고.)
|
||||
|
||||
- **[해소됨, 2026-08-18] `DI` → `D`(Declarative) 확정** — 원문과 근거는
|
||||
`archive/question-resolved.md`. 요지: `DI`가 "Dependency Injection"과
|
||||
완전히 겹쳐 실제 오해 전례가 있었고, `D`는 Instance 전용이 아닌 declare
|
||||
요소 전반으로 확장 가능하며 `D.FrameModifier`류 타입 프리픽스도 짧게
|
||||
유지된다. 미뤄뒀던 유일한 사유(한 글자 식별자의 검색성/자기설명력)는
|
||||
"문서에서 처음 나올 때 항상 `D`(Declarative)로 풀어쓴다"는 표기 규약으로
|
||||
보완하기로 같이 확정. 코퍼스 반영 완료 —
|
||||
`base/bind-system-plan.md`의 "인스턴스 생성 / 이벤트 네이밍 인체공학" 절.
|
||||
- **[해소됨, 2026-08-19] `PopOnly` → `Detach` 확정** — 원문과 근거는
|
||||
`archive/question-resolved.md`. 요지: 이미 있는
|
||||
`Extract`(호출자가 직접 부르는 명령형 추출)와 동사가 겹치면 헷갈리는데,
|
||||
`Detach`는 "화면(부모 계층)에서만 떼어낼 뿐 관리 주체는 여전히
|
||||
reconcile"이라는 뜻이라 `Extract`의 "소유권을 통째로 넘긴다"와 자연스럽게
|
||||
구분되고, `nil`(파괴)과의 대비도 더 직접적으로 드러남. 공개 표면 위치도
|
||||
같이 확정 — `Slot`이 함수(팩토리)라 `Slot.Detach`처럼 붙이려면
|
||||
callable-table+메타테이블이 새로 필요해서 과함, `None`과 같은 선례를 따라
|
||||
**패키지 최상위 export**로. 코퍼스 반영 완료 — `base/slot-plan.md`의
|
||||
"`nil` 리턴은 파괴가 기본" 절.
|
||||
- **`Gate`(2순위, 2026-08-21 신설)**: `Blocker`/`Debounce`/`Throttle` 아래의
|
||||
공용 게이트 노드 이름. 사용자 지적 — *"프리미티브 명을 Gater? 뭔가 이상하게
|
||||
들어간다는게 약간의 문제"* — 코퍼스가 `Blocker`/`Modifier`/`Observer`처럼
|
||||
`-er`를 많이 쓰는데 `Gater`는 영어로 어색하다. 에이전트 권고는 **`Gate`
|
||||
그대로**(`gate`는 이미 행위자가 아니라 **장치**를 가리키는 명사라 `-er`가
|
||||
불필요 — `Source`/`Ref`/`Slot`/`Tween`도 같은 계열), 대안 후보는
|
||||
`Valve`/`Relay`. **[2026-08-21 해소] `Gate`로 확정**(탑레벨 생성자를 안
|
||||
만들고 `state:Gate(setup)` 메소드로 가면서 `Gater` 문제 자체가 사라짐 —
|
||||
메소드 자리에서는 `:With`/`:Compute`와 나란히 자연스럽다). 노드 타입 이름은
|
||||
`GateNode`. `base/gate-plan.md`의 1번이 소스.
|
||||
- **`Epoch`의 리비전 필드 증가 방식(3순위, 2026-08-21 신설)**: `bit32` 랩
|
||||
(uint32 랩어라운드라 double 포화가 없음, 사용자가 염두에 둔 쪽) vs 평이한
|
||||
`+1`(할당·연산 더 쌈, 포화가 `2^53`이라 `bit32`의 `2^32` 랩보다 충돌 거리가
|
||||
오히려 넓음). **둘 다 도달 불가능이라 실질 위험은 없고 취향 문제다.**
|
||||
필드 이름은 `Revision` 권고(코퍼스 공개 필드 PascalCase 관례).
|
||||
`research/epoch-brand-composition.md` §4의 4번.
|
||||
- **`Slot`(2순위)**: Vue의 "slot"(콘텐츠 주입 지점)과 이름은 같지만 의미가
|
||||
다름(quad의 Slot은 자식 배열 재조정 프리미티브) — Vue 배경 있는 사람이
|
||||
헷갈릴 수 있음.
|
||||
|
|
@ -109,13 +75,18 @@
|
|||
안 바꾸고 대기열에만 올림(의사코드는 새로 쓰는 자리부터 `nextValue`를
|
||||
쓰기 시작했음).
|
||||
- **`Brand`(3순위, 사소함, 2026-08-07 여덟 번째 세션 추가)**: 런타임
|
||||
nominal 타입 판별 통합 메커니즘(`Brand.set`/`Brand.get`, `isState`를
|
||||
branded 타입 전부로 일반화) — `brand-plan.md`의 `Brand`
|
||||
절에서 동작/구현 방식은 확정, "OOP 인스턴스의 클래스명을 얻는 느낌"을
|
||||
전달할 더 나은 이름이 있는지가 열린 질문(사용자가 직접 제기) — `Tag`는
|
||||
이미 quad-roblox의 `CollectionService` 래퍼로 쓰여서 이름 충돌, 후보로
|
||||
nominal 타입 판별 통합 메커니즘 — `base/brand-plan.md`에서 동작/구현
|
||||
방식은 확정, 이름만 열린 질문(사용자가 직접 제기). `Tag`는 이미
|
||||
quad-roblox의 `CollectionService` 래퍼로 쓰여서 이름 충돌, 후보로
|
||||
"type namespace"류를 사용자가 검토했으나 미확정. **(2026-08-08 재확인)**
|
||||
사용자가 다시 짚었지만 여전히 미정.
|
||||
- **[2026-08-21 갱신] 표면이 바뀌어서 "OOP 인스턴스의 클래스명을 얻는
|
||||
느낌"이라는 원래 요구는 이제 안 맞는다** — 인스턴스 브랜드로 재작성되며
|
||||
역조회(`Brand.get`)가 없어졌고, 지금 하는 일은 **집합 멤버십**
|
||||
(`SomeBrand:is(x)`)이다. 이름 후보도 그 방향으로 다시 볼 것.
|
||||
- **메소드 케이싱도 같이 볼 것** — `:register`/`:is`가 소문자인데 quad
|
||||
공개 표면 관례는 PascalCase다(`:Get`/`:Set`). base 내부 유틸이라 지금은
|
||||
기존 `Brand.set`/`Brand.get` 관례를 이었지만, 이름을 정할 때 같이 정리.
|
||||
- **`Tag`/`Added`/`Removed`/`Merged`(3순위, 사소함, 2026-08-08 세 번째
|
||||
세션 array-part 값 객체 재설계 때 확정된 API 표면)**: `base/tag-plan.md`가
|
||||
"열린 질문 없음, 값 모양/메커니즘/retract/패키지 배치 전부 확정, 이름
|
||||
|
|
@ -142,164 +113,6 @@
|
|||
|
||||
## 3. 낮은 우선순위 — 열려 있지만 급하지 않음
|
||||
|
||||
- **[해소, 2026-08-21 구현 전 QA 5라운드 `CR-3`] M2가 M3의 `Blocker.luau`에
|
||||
구조적으로 의존하던 순서 문제 — "게이팅 먼저"로 결정.** 사용자 판정:
|
||||
*"게이팅 먼저. 게이팅을 base 에 만들 준비를 해야한다. 실질적 모양 정의가
|
||||
필요함."* 즉 로드맵 순서를 유지하지 않고 게이팅을 M2로 앞당긴다. **다만
|
||||
앞당기는 대상이 `Blocker` 그 자체가 아니라 그 아래의 공용 `Gate` 노드로
|
||||
바뀌었고**(같은 라운드 `DT-4`), 그 표면/이름이 미정이라 **새 열린 항목으로
|
||||
이어진다 — 바로 아래 `Gate` 항목.** 아래는 해소 전 서술: 이대로 각주만 두고
|
||||
로드맵 순서를 유지할지,
|
||||
`Blocker.luau`(또는 최소 표면 `On`/`Off`/`IsOn`/`OffWithoutEmit`)를 M2로
|
||||
앞당길지, M2/M3 경계 자체를 재검토할지.** `RC-1`의 Blocker 게이팅 해법
|
||||
때문에 `ROADMAP.md` M2의 `Dispatch.setLength`/`setOffsetSource` 체크박스가
|
||||
`getBlocker`/`:On()`/`:IsOn()`/`:OffWithoutEmit()`을 호출하는데, 정작
|
||||
`Blocker.luau` 자체는 M3 체크박스에 있다 — 로드맵 순서대로면 M2가 아직
|
||||
없는 걸 참조하게 된다. 지금은 M2 체크박스에 이 사실만 각주로 남겨둔
|
||||
임시 조치(가장 보수적인 선택, 마일스톤 재편은 안 함) — **M2 착수 전
|
||||
필요**. 상세는 `qa-request/pre-implementation-qa-round3.md`의
|
||||
"ROADMAP.md 마일스톤 정합성" 절.
|
||||
- **[해소, 2026-08-21 구현 전 QA 5라운드 H절] `mountInst`의 삽입 위치 + 중첩
|
||||
offset 결함 — `Dispatch.getOffsetAt(ownerKey, i)` 신설로 확정.**
|
||||
`setOffsetSource(None)`은 얼리 리턴하고, 숫자가 필요한 쪽이 `getOffsetAt`을
|
||||
직접 부른다(사용자안 — 병렬 배열을 안 늘리는 pull 방식). 베이스는 `isSlot`
|
||||
분기의 정체가 "베이스가 있나"임을 명확히 했고(베이스는 따로 저장하지 않고
|
||||
Slot의 `.Offset`을 그대로 읽는다 — 최상위 물리 inst는 항상 0), 중첩 Slot은
|
||||
자기 `Offset`을 관측해 깊은 전파를 한다. 반영하다 **재마운트가 `Offset` Source를 새로 만들던 결함**도
|
||||
같이 잡았다(identity 재사용). 상세는
|
||||
`qa-request/pre-implementation-qa-round5-followup.md`의 H절.
|
||||
아래는 열려 있던 시점의 서술: 둘이 한 덩어리다:
|
||||
(a) `setOffsetSource`의 `None`이 "아무것도 안 차지함"과 "발행 채널 없음"
|
||||
두 뜻을 겸하고 있어 **plain 요소의 offset 숫자가 계산조차 안 된다**(그래서
|
||||
DOM류 백엔드가 삽입 위치를 알 방법이 없다 — 사용자 지적), (b) `recompute`가
|
||||
`sum = 0`에서 시작해 `ownerKey.Offset`을 안 읽으므로 **depth ≥ 2에서 중첩
|
||||
Slot의 자식 offset이 부모 베이스만큼 어긋난다**(이번에 발견, 지금까지 depth 1만
|
||||
써서 안 드러났음). 제안은 `bk.offsetList` 신설(항상 숫자 계산) +
|
||||
`mountInst(target, element, index)` + `recompute`의 `base` 시드 + 중첩 Slot이
|
||||
자기 `Offset`을 관측해 재계산하는 구독 하나 —
|
||||
`qa-request/pre-implementation-qa-round5-followup.md`의 G절이 소스.
|
||||
- **[해소, 2026-08-21] offset이 바뀌면 이미 배치된 물리 노드를 옮겨야 하는가 —
|
||||
아니오.** **사용자 확정**: *"애초에 offset 바뀌여도 상관 없는게 위에서 넣고
|
||||
빼면 insert 같은거로 일어나서 뒤로 밀린다는거였긴함"* — DOM류는 삽입/삭제
|
||||
자체가 뒤 형제를 물리적으로 밀고 당기므로, **이미 놓인 노드를 다시 옮길 일이
|
||||
없다.** offset 숫자는 그 자리가 **다음에** insert/remove할 때 쓰는 것뿐이라는
|
||||
기존 서술이 그대로 맞다. 이게 성립하려면 백엔드 op이 **아토믹한 최소 단위**
|
||||
(`mountInst`/`unmountInst`/reposition)여야 한다는 게 같이 확인된 요구사항.
|
||||
아래는 열려 있던 시점의 서술: `mountInst(target, element,
|
||||
index)`가 **삽입 시점의 위치만** 받는 일회성 호출이라, 이미 마운트된 요소의
|
||||
물리 위치는 offset이 바뀌어도 갱신되지 않는다. Roblox는 `LayoutOrder`가
|
||||
프로퍼티라 `updateFn`이 반응형으로 처리하면 되지만 **DOM은 물리 순서 자체가
|
||||
배치**다. `dispatch-core-plan.md`는 "quad-web의 offset 핸들러는 no-op이고 숫자는
|
||||
*다음에* 스스로 insert/remove할 때만 쓴다"고 적어뒀는데, 앞 형제의 길이가 변해
|
||||
뒤 형제들의 offset이 밀리는 흔한 경우에 **이미 놓인 노드를 옮길지**가 그 서술만
|
||||
으론 안 갈린다. 선택지는 (a) 옮긴다(백엔드가 offset 변경을 관측해 재배치),
|
||||
(b) 안 옮긴다(그러면 DOM에선 순서가 실제로 어긋남), (c) `:List`가 재조정 때
|
||||
필요한 것만 명시적으로 다시 `mountInst`. quad-web이 실제로 생길 때까지 미룰 수
|
||||
있으나, **계약을 지금 정해두지 않으면 M6 구현이 (b)를 전제로 굳는다.**
|
||||
- **[해소, 2026-08-21] `:List` 재조정의 `getOffsetAt` 비용 — 접두합 캐시로.**
|
||||
**사용자 제안 채택**: *"getOffsetAt 은 compute 된걸 캐시해도 될듯. length
|
||||
변경되는 뒤는 캐시가 무효화되도록 shouldRecomputeAfter 등을 둬서 특정 인덱스
|
||||
초과부는 offset 다시 계산하고, 해당 값 위치 자체는 유효하므로 그것을 통해 더
|
||||
length 를 이어붙이면 될듯."* → `bk.offsetCache` + **`bk.invalidAfter`**("여기까지는 유효")로 반영
|
||||
(`base/dispatch-core-plan.md`). **무효화 규칙은 하나** —
|
||||
`invalidAfter = min(invalidAfter, i)`(`setLength`도 splice도 자기 인덱스까지,
|
||||
베이스 변경만 `0`). `recompute`도 이 캐시 위에 얹혀 O(N)이라
|
||||
**"캐시를 누가 채우나"라는 갈래가 없어졌다**(사용자 정정: 함수를 나눌 이유가
|
||||
없다). 아래는 열려 있던 시점의 서술:
|
||||
`settle`이 키마다 `rawAdd`/`rawReplace`를 부르고 그 각각이 `getOffsetAt`(O(i))을
|
||||
부르므로, 이미 마운트된 리스트의 데이터가 통째로 바뀌면 **O(N²)**다(최초 마운트는
|
||||
`_mounted == false`라 얼리 리턴이 막아준다). `DC-9`에서 `setOffsetSource`의 즉시
|
||||
계산이 O(N²)인 걸 "배치 등록 1회니 감수"로 판단했지만 **이건 매 reconcile이라
|
||||
빈도가 다르다.** 해법은 있다 — reconcile이 `pos`처럼 **절대 offset도 러닝
|
||||
누적**으로 들고 다니면 O(n)(그게 `mountSlotTree`가 이미 하는 방식). 그렇게
|
||||
할지, 아니면 실측 전엔 그냥 둘지 판단 필요.
|
||||
- **[해소, 2026-08-21 같은 날] 게이트가 유보했다 내보내는 emit이 싣는 출처 —
|
||||
`emit(self)` + 흡수 집합.** `GateNode`가 흡수한 소스를 `withheld` 집합에
|
||||
들고 있다가, 풀 때(=flush) **집합을 그 자리에서 떼어내 새 테이블로 스왑하고**
|
||||
자기를 출처로 그 배치를 하류에 넘긴다. 하류는 출처가 게이트면 그 집합의 소스들에 평소 규칙을
|
||||
적용한다. **`setup` 시그니처는 안 바뀐다** — 집합을 채우는 건 정책이 아니라
|
||||
노드이기 때문. `base/gate-plan.md`의 4번, `base/state-epoch-plan.md` §2.
|
||||
- **[해소, 2026-08-21 같은 날] State 에포크 — 새 노드의 두 맵 초기값과
|
||||
`:With` 병합.** `sourceEmitMap`은 **비우고**(어떤 emit이 와도 "처음 보는
|
||||
것"으로 걸리는 게 맞다 — 새 노드는 개념적으로 emit을 받아본 적이 없다),
|
||||
`sourceCountMap`은 **전부 끌어와 실제 count로 채운 뒤 `rawInvalid = true`**
|
||||
(비워두면 순회가 훑을 목록 자체가 없어 "유효하다"로 오판한다 — *"'내가 뭘
|
||||
추적하고 있나' 가 필요하죠"*). 그래서 `:With` 병합 규칙은 **필요 없어졌다.**
|
||||
재계산 시 갱신 범위(전부 갱신)도 확정. `base/state-epoch-plan.md`의 §2·§5 7번.
|
||||
- **[해소, 2026-08-21 같은 날] `Gate` — 소스 없는 emit(빈 배치)은 **아무것도
|
||||
안 한다**.** 에이전트 권고("빈 배치 = 무조건 통지")는 **사용자 기각** —
|
||||
*"쌓아둔것 자체가 없는데 뒤로 넘긴다는건 이상합니다 … 표면적으로 보면 State
|
||||
중간에 Emit 을 추가하는 격"*. 새 규칙도 아니다: `blocker-plan.md`가 이미
|
||||
"`HasBlockedEmit`이 false면 아무 것도 안 함(idempotent)"으로 확정해뒀고,
|
||||
`withheld`는 그 플래그의 일반화다. **따름정리로 `Effect(fn, ...deps)`의 설치
|
||||
구간 억제는 `Gate` 소비자가 아니게 됐다** — `Effect` 내부 플래그로 처리하고,
|
||||
`effect-plan.md`에 있던 "`Gate`보다 뒤" 순서 제약도 사라졌다.
|
||||
`base/gate-plan.md`의 7·8번.
|
||||
- **같이 제기됐던 "재진입 계약"은 열린 항목이 아니었다**(사용자 지적으로
|
||||
2026-08-21 정리) — `blocker-plan.md`의 재진입은 **같은 인스턴스 중첩**을
|
||||
말하는 것이지 정책의 `emit()` 호출과 무관하고, 끝나지 않는 되먹임은
|
||||
2026-08-04 확정 원칙대로 **UB**이며, 유한한 재진입은 flush 진입 시 스왑으로
|
||||
이미 안전하다. `gate-plan.md`의 6번.
|
||||
- **[해소, 2026-08-21] 공용 게이트 노드의 이름과 표면 — `state:Gate(setup)`
|
||||
메소드 + `GateNode`로 확정.** 탑레벨 프리미티브는 안 만들고, `Blocker`는
|
||||
`state:Block(blocker)` 안에서 그 배선을 쓴다(사용자: *"Gate 는 따로
|
||||
프리미티브 없이 state:Gate( (emit) -> ()->() ) 처럼 선언되고 마치 Compute
|
||||
처럼 GateNode(ComputeNode 처럼) 생성된다"*). `Get()`엔 영향 없음(통지만
|
||||
막음)까지 확정. **[2026-08-21 정정]** 여기 "남은 것은 사용자 판단이 아니라
|
||||
구현 시 정할 것들"이라 적었으나, 두 번째 `/code-review high`가 빈 배치
|
||||
emit을 잠시 사용자 판단 항목으로 되돌렸다 — **같은 날 해소돼(바로 위 항목)
|
||||
원래 서술로 돌아왔다.** 구현 시
|
||||
정하면 되는 건 생명주기와 M2 범위뿐 — `base/gate-plan.md`가
|
||||
소스. 아래는 열려 있던 시점의 서술: 위 항목의 결정("게이팅 먼저")에 따라 base에 만들 것이
|
||||
`Blocker`가 아니라 **상류 emit을 가로채 정책이 통과 여부를 정하는 공용 게이트
|
||||
노드**로 확정됐다(`Blocker`/`Debounce`/`Throttle`이 그 위의 정책). 사용자
|
||||
스케치는 `Gate(function(emit) return function() ... end end)` 2단 구조이고,
|
||||
**공개 API로 낸다**(사용자: *"이 API가 비공개일 이유는 없어보인다"*). 남은 것 —
|
||||
(a) **이름**(사용자: *"프리미티브 명을 Gater? 뭔가 이상하게 들어간다는게 약간의
|
||||
문제"*, 에이전트 권고는 `Gate` 그대로 — `gate`는 이미 장치를 가리키는 명사),
|
||||
(b) M2에 `Gate`만 넣을지 `Blocker`까지 넣을지, 그리고 생명주기 계약.
|
||||
**[2026-08-21 해소]** 여기 있던 "`:Apply` 팩토리인가"와 "`Blocker`가 그 위에
|
||||
어떻게 얹히는가"는 닫혔다 — **사용자 확정으로 `Gate`는 `:Apply`가 아니라
|
||||
`:With`류 State 메소드**(*"state 의 전파를 손대는 작업이라 with 처럼 다른
|
||||
노드가 나는게 맞음"*)이고, 그러면 `Blocker`는 이미 확정된
|
||||
`state:Block(blocker)` 메소드가 내부에서 그걸 부르면 되므로 배선 문제 자체가
|
||||
없어진다. 상세는 `base/gate-plan.md`.
|
||||
- **[해소, 2026-08-21] State 재계산/전파 판정을 "소스 에포크 비교"로 바꿀지 —
|
||||
채택 확정**(사용자: *"gate 와 epoch 가 제가 만족할만한 정도로 올라왔습니다.
|
||||
채택하면 될것 같아요."*). 규칙 전량은 `base/state-epoch-plan.md`, 구현은 M3.
|
||||
아래는 열려 있던 시점의 서술: 사용자 제안: 각 State가 자기 상류 루트
|
||||
`Source`들의 카운트를 들고 있다가 `Get()` 때 비교해 재계산 여부를 정한다.
|
||||
동기는 성능이 아니라 **정확성** — DFS 전파 도중 Observer가 `Get()`을 부르면
|
||||
아직 신호를 못 받은 다른 가지의 옛 캐시가 섞여 들어가는 glitch가 지금 모델에
|
||||
실재한다. 에이전트 분석 결과 진단·방향 모두 타당하고 선례도 있다
|
||||
(MobX/Adapton류 버전 검증). **[2026-08-21 갱신 — 여기 있던 "중복 통지는 안
|
||||
고쳐지고 선언 안 된 의존성을 UB로 명문화해야 한다"는 서술은 같은 날 둘 다
|
||||
뒤집혔다]**: 중복 통지도 **같은 장치로 접고**, UB 조항은 사용자 기각으로
|
||||
빠졌다. 이어진 3·4차 정정으로 **기제는 사실상 다 정해졌다** — 순회는
|
||||
`rawInvalid`가 **false**일 때만 돌고(목적은 "못 받은 emit 받기"), emit은
|
||||
count 없이 **발행 source만** 싣고, 순회가 발견한 변경은 **테이블 둘**로
|
||||
처분한다(`sourceCountMap`은 순회가 앞당겨 올리고, `sourceEmitMap`은 상류의
|
||||
진짜 emit을 기다림 — *"emit 바로 안하고 상류가 emit 해줄 때 까지
|
||||
기다립니다"*). 그래서 통지가 죽지도 게이트를 새지도 않아 `source = nil`
|
||||
규약도 필요 없어졌다. **남은 판단은 채택 여부 자체 하나**이고, State 내부 표현을
|
||||
바꾸는 결정이라 M3 뒤로 미루면 되돌리는 비용이 크다. 상세는
|
||||
`base/state-epoch-plan.md`.
|
||||
- **[해소, 2026-08-21 구현 전 QA 5라운드 `C-4`] `Dispatch.setLength`의 Observer
|
||||
앵커 — 물리 target으로 확정(4라운드 `D-56` 역전).** `setLength`가
|
||||
`(ownerKey, i, len, anchor)`로 4번째 인자를 받아 **부기 키와 생명주기 앵커를
|
||||
분리**한다. 그래서 `bindLifetime`은 항상 물리 Instance만 상대하고,
|
||||
`isBoundAlive`의 세 번째 분기(형태 미정으로 열려 있던 것)도 **필요 자체가
|
||||
없어졌다.** 역전 원문은 `archive/bindlifetime-slot-owner-reversed.md`, 지금
|
||||
결론은 `base/dispatch-core-plan.md`의 "`setLength` 구현" 절 뒤 문단. 아래는
|
||||
열려 있던 시점의 서술: 그때 확정(4라운드 `D-56`)은
|
||||
"`bindLifetime`의 첫 인자가 Slot일 수 있으니 백엔드가 그 경우를 핸들링하라"인데,
|
||||
사용자가 그 전제 자체에 의문을 제기했다: *"애초에 Slot 이 effect 나 다른
|
||||
요소들을 소유할 수가 없다 … 실제 observer/effect 는 실제 inst 에 불림 …
|
||||
우리가 왜 slot 을 소유 대상으로 둘 수 있게 한거였는지 다시 생각해봐야할
|
||||
부분."* 대안은 **부기 키(`ownerKey`)와 생명주기 앵커(물리 `physicalTarget`)를
|
||||
분리**하는 것 — 그러면 `bindLifetime`은 항상 Instance만 받고,
|
||||
`isBoundAlive`의 세 번째 분기(지금 ⚠️ 미정)도 통째로 불필요해진다. 상세와
|
||||
트레이싱은 `qa-request/pre-implementation-qa-round5-followup.md`.
|
||||
- **`Operator` 콤비네이터 슈가 네임스페이스 이름+포함 범위(2026-08-12 신설,
|
||||
같은 날 후속으로 외부 리서치 완료)** — `Sum`/`Product`/`Not`/비트연산 등
|
||||
`:Compute`/`:Apply`용 슈가 함수 모음의 이름. 흔한 단어라 top-level
|
||||
|
|
@ -334,36 +147,6 @@
|
|||
`getDynamic(store, name)` — "특정 프리미티브에 안 묶인 범용 유틸은 소문자
|
||||
탑레벨"이라는 기존 네이밍 규칙에는 오히려 더 맞는다. **M3/M4 착수 전
|
||||
필요**, `base/store-plan.md`의 "타입 추론 문제" 절.
|
||||
- **[해소, 2026-08-21] `Detach` 홀드 중 키가 사라졌을 때의 처분** —
|
||||
선택지 (c)로 확정: `updateFn`을 **`KeyGone`으로 한 번 더 불러 처분을
|
||||
묻는다**. 같이 확정된 것 — detach된 요소는 `userdata`가 아니라
|
||||
**`slot._detached` 필드**가 보유하고(그래야 `destroySlotTree` walk가
|
||||
닿고 소유권도 유지됨), owner가 죽으면 `activateList`가 설치한 `Effect`가
|
||||
정리한다. 원래 갭이
|
||||
치명적이었던 이유는 gcconn 트릭 때문에 detach된 quad-제작 Instance가
|
||||
**GC 폴백조차 없이 영구히 남기** 때문. 상세는 `base/slot-plan.md`의
|
||||
"Detach된 요소는 `slot._detached`가 보유한다"/"`KeyGone`" 절.
|
||||
- **[해소, 2026-08-21] `attachSlot` 책임 분해** — **(B) 분해 채택으로 확정**,
|
||||
`base/slot-plan.md`에 반영 완료(`materializeSlotTree`/`mountSlotTree`/얇은
|
||||
`attachSlot`). 근거 기록은 `research/slot-attach-decomposition.md`.
|
||||
아래는 그 열려 있던 시점의 서술: 한
|
||||
함수가 부모 등록(offset/length) / `:List` 실체화 / 마운트 상태 전이 / 배치
|
||||
게이팅 / 자식 배치 / 재귀를 다 지고 있어서, **"부모에게 알리는 길이의
|
||||
최종값은 flush가 끝나야 정해진다"와 "부기가 물리 조작보다 먼저"가 동시에
|
||||
만족되지 않는다**(지금은 후자를 지키고 전자를 포기 — 부모 `recompute`가
|
||||
1회 헛돎). 사용자 판단으로 확장 논의 대기 — 책임 목록/순서 제약 출처/분해
|
||||
후보 넷은 `research/slot-attach-decomposition.md`. **M6(`:List`) 착수 전
|
||||
필요**, 선행으로 아래 `Detach` 보관 위치가 먼저 닫히는 게 나음.
|
||||
- **[신설, 2026-08-18 구현 전 QA] 그룹 `Attribute`의 위치별 claim 설계** —
|
||||
같은 그룹 객체를 두 위치에 놓는 경우(`Frame { a, a }`)를 잡으려면 위치별
|
||||
claim 레지스트리가 하나 필요하다는 **방향은 확정**됐고(`Ref`처럼
|
||||
`bindLifetime`을 재사용할 수는 없음 — 그룹 값은 여러 곳에서 쓸 수 있어야
|
||||
하므로), **[2026-08-20 QA 4라운드] 이름도 `groupClaimKeys`로 확정**.
|
||||
**[해소, 2026-08-21 QA 5라운드 `AT-1`] 키도 `(inst, groupValue) → k`로 확정**
|
||||
(사용자: *"group 에 따라 key 가 따로 생성되므로 다른 그룹에 대해서는 잡을
|
||||
필요가 없고, 그건 key->name 이 유일성을 검증해준다"*), `nameClaims`보다
|
||||
**위치 claim을 먼저** 본다 — `base/attribute-plan.md`의 "이름 소유권" 절.
|
||||
**이 항목은 닫혔다.**
|
||||
- **[신설, 2026-08-18 구현 전 QA] 중간 State GC 미검증** — `State → State →
|
||||
State → Observer` 체인에서 중간 노드를 강하게 붙잡는 주체가 문서 어디에도
|
||||
없어 전파가 조용히 끊길 수 있음. 방향(상류 strong / 하류 weak)은 사용자가
|
||||
|
|
@ -376,10 +159,6 @@
|
|||
문서화만** 했는데(`base/attribute-plan.md` "메커니즘" 절), 원자적
|
||||
롤백(그룹 `process`에만 국소적인 unwind)을 넣을지는 열어둠. 지금 결정
|
||||
불필요 — M10 구현 시점에 판단.
|
||||
- **[해소됨, 2026-08-18] `Attribute.Merged`의 이름 중복** — `Merged`(겹치면
|
||||
error)와 `Overridden`(겹치면 뒤가 이김)을 **둘 다 제공**하는 것으로 확정
|
||||
(제3안). 근거·파급은 `base/attribute-plan.md`의 "채택안 — `Tag`와 동형인
|
||||
array-part 값 객체" 절.
|
||||
- **`quad-debug` 세부 API 이름** — `research/debug-tooling-plan.md` 참고.
|
||||
채널 실현 가능성(BindableEvent/Function이 플러그인↔Play 중 게임 경계를
|
||||
넘는지)까지 사용자가 Studio에서 직접 실측 검증 완료 — 기술적 불확실성은
|
||||
|
|
|
|||
|
|
@ -1,14 +1,30 @@
|
|||
# `Epoch` 인터페이스 + `EpochMap` 컴포지션, 그리고 `Brand` 인스턴스화 (2026-08-21 신설)
|
||||
# `Epoch` 인터페이스 + `EpochMap` 컴포지션, 그리고 `Brand` 인스턴스화 — 결정 근거 기록 (2026-08-21)
|
||||
|
||||
**상태**: 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`이 두 번 도는" 갭
|
||||
**상태**: **[2026-08-21] 전량 확정·승격 완료.** 이 문서는 이제 "왜 그렇게
|
||||
정했나"의 근거 기록이고, **지금 유효한 설계는 `base/`가 소스**다 —
|
||||
`base/state-epoch-plan.md`(`Epoch`/`EpochMap`/State의 두 맵),
|
||||
`base/brand-plan.md`(인스턴스 브랜드), `base/source-state-plan.md`(`Source`가
|
||||
`Epoch`를 구조적으로 만족 + `Observer` 클로저 시그니처),
|
||||
`base/effect-plan.md`(`Effect`가 자기 `EpochMap`을 듦).
|
||||
**[2026-08-21 `research/` → `reference/` 이동]** 확정된 뒤엔 "상의가 더 필요한
|
||||
설계"가 아니라 "다른 문서가 근거로 인용하는 온디맨드 자료"이므로
|
||||
(`slot-attach-decomposition.md`와 같은 처리).
|
||||
|
||||
**[2026-08-21] 열린 항목 없음** — 마지막까지 남았던 리비전 증가 방식도
|
||||
같은 날 `bit32` 랩으로 확정됐다(§4의 4번).
|
||||
|
||||
**⚠️ [2026-08-22] 아래 본문의 `{Epoch}` 표기는 제안 당시 표기 그대로다** —
|
||||
승격 후 실측에서 그게 Luau의 **배열**(`{[number]: Epoch}`)이라 실제 게이트
|
||||
배치(`{[Epoch]: true}` 집합)와 안 맞는 게 드러나 **`EpochSet`으로
|
||||
확정**됐다(`base/state-epoch-plan.md` §3). 마찬가지로 `Observer` 클로저의
|
||||
`from`은 **옵셔널**이다(설치 발화엔 출처가 없다). 정본은 언제나 `base/`.
|
||||
|
||||
발단은 "다중 의존성 `Effect`에서 한 파동에 `fn`이 두 번 도는" 갭
|
||||
(`base/effect-plan.md`의 다중 deps 절, 2026-08-21 대화에서 에이전트가 제기).
|
||||
|
||||
아래는 그 결론에 이른 제안·평가·회신을 그대로 보존한 것 — 원래 서술은
|
||||
"승격 대기 중인 제안"이었다.
|
||||
|
||||
## 1. 제안 (사용자 원문 요지)
|
||||
|
||||
1. **에포크 부기를 State에서 떼어내 컴포지션 가능한 객체로.** 가칭
|
||||
|
|
@ -36,7 +52,8 @@
|
|||
|
||||
- **`EpochMap` 분리는 이미 코퍼스가 발견한 구분을 형식화한다.** 같은 날
|
||||
`state-epoch-plan.md`가 맵을 둘로 가른 이유가 정확히 "값 유효성"과 "전파
|
||||
dedup"이 **비대칭으로 움직인다**는 것이었다(§2의 "왜 테이블이 둘인가").
|
||||
dedup"이 **비대칭으로 움직인다**는 것이었다(승격 후 정본은
|
||||
`base/state-epoch-plan.md` §4의 "왜 둘인가 — 순회 때문이다" 문단).
|
||||
후자만 떼어내 재사용 가능한 객체로 만들면, **노드가 아닌 소비자(leaf)도
|
||||
같은 판정을 쓸 수 있다** — 지금 State에만 있어서 못 쓰던 것.
|
||||
- **다중 dep `Effect` 갭이 이걸로 정확히 닫힌다.** `A → b`, `A → c`,
|
||||
|
|
@ -46,8 +63,8 @@
|
|||
노드로 수렴시키기"보다 낫다 — 노드를 더 안 만들고, `effect-plan.md`가
|
||||
확정한 "의존성 N개면 내부 Observer도 N개" 구조를 안 건드린다.
|
||||
- **게이트를 페이로드에서 빼는 것도 맞다.** 하류는 게이트 identity를 **한
|
||||
번도 안 쓴다**(에포크 경계로 만드는 안은 `state-epoch-plan.md` §5-3에서
|
||||
이미 기각됨). 배치가 "그 전파에만 쓰는 일회성 스냅샷"이라는 성질도
|
||||
번도 안 쓴다**(에포크 경계로 만드는 안은 `base/state-epoch-plan.md` §8의
|
||||
"기각된 대안 — 게이트를 에포크 경계로" 항목에서 기각됨). 배치가 "그 전파에만 쓰는 일회성 스냅샷"이라는 성질도
|
||||
그대로 유지된다.
|
||||
- **`Epoch`로의 일반화가 실제로 계약을 정확하게 만든다.** 맵이 `Source`에서
|
||||
요구하는 건 **identity + 단조 증가 카운터** 둘뿐이다. 그걸 이름 붙이면
|
||||
|
|
@ -112,14 +129,19 @@
|
|||
- **숫자**: 할당 0, 비교도 더 쌈. 위험은 도달 불가능한 `2^53`뿐.
|
||||
- **에이전트 권고도 숫자(`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`이 넓다.
|
||||
- **[해소, 2026-08-21] `bit32.bnot(-rev)`로 확정.** 사용자가 인용한
|
||||
`bit32.bnot(-1) == 0`, `bit32.bnot(-0) == 4294967295`,
|
||||
`bit32.bnot(-4294967295) == 4294967294`가 **예시가 아니라 연산 자체**
|
||||
였다 — `bit32.bnot(-a)`는 `a > 0`이면 `a - 1`, `0`이면 `4294967295`인
|
||||
**랩어라운드 감소**이고, 갱신과 랩이 **FASTCALL 하나**로 끝난다.
|
||||
랩이라 double 포화(`n + 1 == n`)가 안 생긴다. 충돌 거리 자체는
|
||||
오히려 짧아지지만(`2^53` vs `2^32`) **둘 다 도달 불가능이라 정확성은
|
||||
동률**이고, 사용자가 hot path 비용을 근거로 골랐다: *"매번 도는
|
||||
코드인지라, 값 싸게 native call + num 연산으로 가볍게 가고 싶어요."*
|
||||
- **⚠️ 에이전트가 한 번 잘못 옮겼다** — 형태를 `band(rev + 1, mask)`로
|
||||
적고 "덧셈 위에 fastcall이 하나 더 얹힌다"고 단서까지 달았는데,
|
||||
사용자 정정(*"제가 말한건, bit32.bnot(-a) 입니다"*)으로 둘 다 틀린
|
||||
게 확인됐다. 실측·정본은 `base/state-epoch-plan.md` §2.
|
||||
5. **[해소] State는 `EpochMap`을 둘 컴포지션하고, `:Sync`는 필수 연산이
|
||||
아니다 — 에이전트 착오 정정.** 에이전트가 "재계산 후 전부 최신으로
|
||||
맞추려면 `Update` 외의 연산이 필요하다"고 적었으나, **`Update`가
|
||||
|
|
@ -129,7 +151,8 @@
|
|||
본 탓이다.
|
||||
- **초기화도 같은 연산 하나로 끝난다** — `:With`/`:Compute`의 deps를
|
||||
전부 `Update`하고 **반환값은 안 보고** `rawInvalid = true`로 둔다.
|
||||
이미 확정된 노드 생성 규칙(`base/state-epoch-plan.md` §2)과 정확히
|
||||
이미 확정된 노드 생성 규칙(`base/state-epoch-plan.md` §4의 "노드가
|
||||
생길 때의 초기값" 절)과 정확히
|
||||
같은 동작이다.
|
||||
- **내부 최적화(사용자 제안)**: `Update`가 목록을 돌 때 diff 때문에
|
||||
읽기가 들어가는데, **한 번 다름을 찾으면 반환값이 이미 `true`로
|
||||
|
|
@ -152,10 +175,13 @@
|
|||
`Debug`뿐, 2026-08-21 확인) — 전환 비용은 **문서뿐**이다. `Brand`라는 이름
|
||||
자체는 여전히 용어 정리 대기(`question.md` 1번).
|
||||
|
||||
## 관련 문서
|
||||
## 관련 문서 (전부 승격 반영 완료)
|
||||
|
||||
- `base/state-epoch-plan.md` — 지금 확정된 두 맵과 수신 규칙(§2).
|
||||
- `base/state-epoch-plan.md` — `Epoch`/`EpochMap`과 State의 두 맵, 수신 규칙.
|
||||
- `base/brand-plan.md` — 인스턴스 브랜드(옛 단일 레지스트리는
|
||||
`archive/brand-shared-registry-reversed.md`).
|
||||
- `base/source-state-plan.md` — `Source`가 `Epoch`를 구조적으로 만족,
|
||||
`state:Observer(fn)`의 `fn(self, from)` 계약.
|
||||
- `base/effect-plan.md` — `Effect`가 자기 `EpochMap`을 들어 다중 deps 중복
|
||||
발화를 접는다(이 제안이 닫은 갭).
|
||||
- `base/gate-plan.md` — 배치 페이로드(4번), 빈 배치 무통지(8번).
|
||||
- `base/brand-plan.md` — 현행 단일 레지스트리 설계와 서브타입 OR 합성.
|
||||
- `base/effect-plan.md` — 다중 deps 절(이 제안이 닫으려는 갭).
|
||||
- `base/source-state-plan.md` — `state:Observer(fn)` 계약.
|
||||
|
|
@ -4,6 +4,8 @@
|
|||
반영 완료.** 이 문서는 이제 "왜 그렇게 정했나"의 근거 기록이고, **지금 유효한
|
||||
설계는 `base/slot-plan.md`의 "재귀 메커니즘" 절**(`materializeSlotTree` /
|
||||
`mountSlotTree` / 얇은 `attachSlot`)이 소스다.
|
||||
**[2026-08-21 `research/` → `reference/` 이동]** 확정된 뒤엔 "상의가 더 필요한
|
||||
설계"가 아니라 "다른 문서가 근거로 인용하는 온디맨드 자료"이므로.
|
||||
|
||||
**사용자 확정 근거**(2026-08-21): *"함수 분해는 확정해도 좋을것 같음. 이게
|
||||
하나의 큰 복잡한 복합 함수라 여러 session 간의 실수가 발생하던 부분이고, 지금
|
||||
|
|
@ -14,6 +16,16 @@
|
|||
아래는 그 결론에 이른 조사/논거를 그대로 보존한 것 — 원래 서술은 "논의 전
|
||||
준비 자료"였다.
|
||||
|
||||
**⚠️ [2026-08-21 후속] 아래 2절의 제약 `C7`("부기 갱신이 물리 트리 조작보다
|
||||
먼저")은 그 뒤 *일반 계약으로서는* 폐기됐다** — 같은 날 `native*` 주입 op 계층이
|
||||
확정되면서(`archive/bookkeeping-before-physical-reversed.md`), "자기 자리를
|
||||
정하는 것 먼저 / 뒤를 미는 것 나중" 하나로 줄었다. **이 문서의 결론은 그대로
|
||||
유효하다** — 배치 경로(`materializeSlotTree` → `mountSlotTree`)가 부기를 먼저
|
||||
끝내는 것은 `C7`이 아니라 **`C6`가 요구하는 별개 사안**이고, 그 역전 문서도
|
||||
그렇게 명시한다. 다만 아래 표만 단독으로 읽고 `C7`을 지금도 유효한 일반
|
||||
규칙으로 옮겨 쓰지 말 것 — 지금 유효한 서술은
|
||||
`base/dispatch-core-plan.md`의 "일반 계약 — 물리와" 절이 소스다.
|
||||
|
||||
**왜 생겼나**: 2026-08-21 구현 전 QA 4라운드 `F-4-3`에서 `Dispatch.setLength`를
|
||||
flush 루프 앞에 둘지 뒤에 둘지가 갈렸는데, 사용자가 그 자리를 고르는 문제가
|
||||
아니라고 짚었다 — *"리스트 액티베이션을 먼저 하고 length 를 얻어 밀고
|
||||
|
|
@ -1725,4 +1725,67 @@ State의 재계산/전파 판정은 **소스 에포크 비교 채택**으로 닫
|
|||
한 파동에 `fn`이 두 번 도는 갭**(공통 하류가 없어 에포크 dedup이 못 접는다)이고,
|
||||
`Effect`가 자기 `EpochMap`을 들면 그게 공통 하류가 되어 닫힌다. **사실상 전량
|
||||
확정됐으나 세션 길이 때문에 `base/` 승격은 다음 세션으로 미뤄졌다** —
|
||||
`research/epoch-brand-composition.md`가 소스, `todos.md` 000번이 진입점.
|
||||
`reference/epoch-brand-composition.md`가 소스, `todos.md` 000번이 진입점.
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-21 (세 번째) — `Epoch`/`EpochMap`/`Brand` 전면 승격, 그리고 해소 기록 flatten
|
||||
|
||||
원문: `session/2026-08-21-03-epoch-brand-promotion-and-flatten.md`
|
||||
|
||||
앞 세션이 미뤄둔 승격을 실제로 수행했다. **`Epoch`**(`{Revision: number}`,
|
||||
그 자체로 키가 되는 unique 테이블 — `Source`가 구조적으로 만족하고
|
||||
`EpochBrand`에도 등록됨)와 **`EpochMap`**(`:Update(Epoch|{Epoch}) -> boolean`이
|
||||
"뒤로 전파가 필요한가"를 답함, `:Refresh`/`:Sync`)이 `base/`에 들어갔고,
|
||||
State는 그걸 **둘** 컴포지션한다(`sourceCountMap`/`sourceEmitMap` →
|
||||
`valueEpochMap`/`emitEpochMap`). **`Brand`는 인스턴스 브랜드로 전면
|
||||
재작성**됐다 — `Brand()` + `:register`/`:is`, **다중 태깅 허용**, 역조회
|
||||
`Brand.get`은 제거(옛 표면은 `archive/brand-shared-registry-reversed.md`).
|
||||
그 부수로 **`effect-plan.md`의 다중 의존성 중복 발화 미해결 항목이
|
||||
닫혔다**(`EffectHandle`이 자기 `EpochMap`을 들어 첫 번째만 통과시킴).
|
||||
**마지막 미정이던 리비전 갱신 방식도 같은 세션에 `bit32.bnot(-rev)`로
|
||||
확정**됐다 — 사용자 논거는 *"2^53 포화는 어차피 도달 불가능한데 그걸
|
||||
피하겠다고 값을 double 영역까지 키울 이유가 없다, 매번 도는 코드라 값싸게
|
||||
가고 싶다"*. **에이전트가 이 형태를 `band(rev + 1, mask)`로 잘못 옮기고
|
||||
"그러니 `bit32`가 더 싼 건 아니다"라는 단서까지 붙였다가 사용자에게
|
||||
정정당했다**(*"제가 말한건, bit32.bnot(-a) 입니다"*) — 사용자가 근거로 든
|
||||
REPL 출력 셋이 예시가 아니라 **연산 자체**였다. `luau` 실측 결과
|
||||
`bit32.bnot(-a)`는 `a > 0`이면 `a - 1`, `0`이면 `4294967295`인 **랩어라운드
|
||||
감소**이고 갱신과 랩이 **FASTCALL 하나**로 끝난다. 즉 사용자 서술이 맞았고
|
||||
에이전트 단서가 틀렸다. 따름정리로 리비전은 **증가가 아니라 감소**하며,
|
||||
`==`/`~=`만 쓰는 지금 규칙에서만 무해하다는 경고를 `base/`에 남겼다.
|
||||
**`Epoch`/`EpochMap`/`Brand`에 열린 설계 항목은 없다.**
|
||||
|
||||
커밋 전 `/code-review high`가 **9건을 더 냈고 전부 유효**했다(감사자 3라운드가
|
||||
수렴한 뒤였다 — 둘이 보는 축이 다르다는 `conventions.md` 서술의 재확인).
|
||||
치명적인 둘은 **표기가 실제 메커니즘과 안 맞던 것**이다: (1) `{Epoch}`가
|
||||
Luau에선 **배열**인데 실제 게이트 배치는 `{[Epoch]: true}` **집합**이라, 그대로
|
||||
`ipairs`로 구현하면 유보됐다 풀린 emit이 전부 삼켜진다 → `EpochSet`으로 확정,
|
||||
(2) 새 노드 시딩("상류의 `Epoch`를 전부 끌어와")이 **확정된 `EpochMap` 표면으로
|
||||
표현 불가능**했다(`:With`의 상류는 State이지 `Epoch`가 아니고, 키 열거/병합
|
||||
연산이 없었음) → `:TrackFrom` 신설. 그 외 `GateNode` 예외 미기록, 설치 발화의
|
||||
`from`이 non-optional, `2^32` 랩을 "똑같이 도달 불가능"이라 한 **틀린 수치
|
||||
근거**(같은 척도로 285년 vs 72분 — 실제 안전 근거는 도달 시간이 아니라 충돌
|
||||
조건이 한 점이라는 것), 옛 이름 잔재(`TweenTag` 3곳/`Effect(fn, state?)` 4곳),
|
||||
`§` 참조 3곳.
|
||||
|
||||
**에이전트가 이름 붙인 연산 둘은 사용자 검토로 확정**됐다 — `:Refresh`는 그대로
|
||||
(*"Update 에 인자 없는건 좀 아니야 … 리프레시는 내가 받았던걸 처리하겠다는거라
|
||||
표면적 의미 자체가 다르지"*), `:Absorb`는 **`:TrackFrom`으로 개명**
|
||||
(*"absorb 는 … 상위 요소에서 제거할것만 같은 이름"* + `gate-plan.md`가 이미
|
||||
"흡수 집합"을 다른 뜻으로 씀). 이 세션의 반복 교훈은 하나 — **승격은 문장을
|
||||
옮기는 작업이 아니라 표기가 가리키는 것이 실제로 성립하는지 확인하는
|
||||
작업**이다(`bit32` 형태, `{Epoch}` 타입, 시딩 표현 셋 다 같은 유형이었다).
|
||||
|
||||
**확정된 결정의 근거 기록이 갈 자리를 정했다 — `reference/`.**
|
||||
`slot-attach-decomposition.md`와 `epoch-brand-composition.md` 둘 다
|
||||
`research/`(아직 상의 필요)도 `archive/`(뒤집혔거나 기각됨)도 아니라
|
||||
"`base/`가 근거로 인용하는 온디맨드 자료"이므로. 폴더 기준 자체에 이 용도를
|
||||
명문화했다.
|
||||
|
||||
**flatten** — 사용자 지적("재정정 기록이 쌓인 부분")대로 세 군데를 걷어냈다.
|
||||
`question.md`가 스스로 정한 규칙(해소되면 archive로 옮김)을 어기고 다시
|
||||
절반이 `[해소]`로 차 있어 **16건을 일괄 이관**(421→208줄), `todos.md`의
|
||||
"M3 착수 전 필요" 목록도 절반 넘게 해소 항목이라 실제로 열린 둘만 남겼다.
|
||||
이관한 히스토리의 원문은 소급해 고치지 않고 **머리에 "그 뒤 이름이 바뀌었다"
|
||||
경고만** 달았다.
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
**소스 관계**: 지금 유효한 설계는 항상 `base/slot-plan.md`가 소스이고,
|
||||
처리 경과 요약은 `qa-request/pre-implementation-qa-round4-followup.md`의
|
||||
H절, 분해 근거는 `research/slot-attach-decomposition.md`. 이 파일은 그
|
||||
H절, 분해 근거는 `reference/slot-attach-decomposition.md`. 이 파일은 그
|
||||
결정들이 **어떤 논의를 거쳐 나왔는지**의 원문 기록이다.
|
||||
|
||||
---
|
||||
|
|
@ -127,7 +127,7 @@ detach된 요소를 `userdata` 안에 넣어두고 `:List`가 그 키를 잊어
|
|||
`ChildAdded` 핸들러가 볼 때 서브트리의 `Length`/`Offset`이 이미 최종값이라는
|
||||
점뿐이고, 옛 코드는 거기서 **미완성 스냅샷**을 보여줬다. 즉 이 변화는
|
||||
동치성 손실이 아니라 **엄밀히 더 정확해지는 방향**이다. 분석 원문은
|
||||
`research/slot-attach-decomposition.md` 7절.
|
||||
`reference/slot-attach-decomposition.md` 7절.
|
||||
|
||||
## 6. 이 세션이 안 한 것
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,336 @@
|
|||
# 2026-08-21-03 — `Epoch`/`EpochMap`/`Brand` 전면 승격 + 해소 기록 flatten
|
||||
|
||||
**요지**: 앞 세션(`2026-08-21-02-qa-round5-and-gate-epoch-research.md`)이
|
||||
컨텍스트 피로로 미뤄둔 승격을 실제로 수행하고, 그 과정에서 코퍼스 곳곳에 쌓여
|
||||
있던 `[해소]`/`[정정]` 층을 걷어냈다. 지금 유효한 설계는 전부 `base/`가 소스 —
|
||||
이 파일은 "무엇을 어디로 옮겼나"의 기록이다.
|
||||
|
||||
## 1. 사용자 지시
|
||||
|
||||
두 갈래였다.
|
||||
|
||||
1. **질문**: *"slot-attach-decomposition.md 은 이제 확정 이야기이기에, 리서치
|
||||
대상이 아니지 않나요? 또, 이것이 base에 적용되어 있습니까?"*
|
||||
2. **작업 지시**: `state-epoch-plan.md`/`source-state-plan.md`/`effect-plan.md`/
|
||||
`brand-plan.md` 넷을 전면 승격하고, *"이 승격을 하며 더이상 필요 없어진
|
||||
요소들은 archive 하세요. 특히 재정정 기록이 쌓인 부분이 있는데, 풀어가며
|
||||
archive 해서 flatten 해야할 필요가 보입니다."*
|
||||
|
||||
## 2. 1번 질문에 대한 답 — 둘 다 예
|
||||
|
||||
- **`base/slot-plan.md`에 이미 반영돼 있다** — `materializeSlotTree` /
|
||||
`mountSlotTree` / 두 줄짜리 `attachSlot` 래퍼가 그 문서의 "재귀 메커니즘"
|
||||
절에 의사코드로 들어가 있고, 그 문서가 근거로 이 파일을 네 곳에서 인용한다.
|
||||
- **`research/`에 있을 이유는 없다** — `.claude/README.md`의 폴더 기준상
|
||||
`research/`는 "아직 착수 전, 상의 필요"다.
|
||||
|
||||
**어디로 옮길지**가 실제 판단이었다. `archive/`는 기준이 "뒤집혔거나 기각됨"
|
||||
이라 채택된 결정의 근거 기록엔 안 맞고, `reference/`의 기준("결정 자체가
|
||||
아니라 다른 문서가 근거로 인용하는 온디맨드 자료")엔 정확히 맞는다. 그래서
|
||||
**`reference/`로 옮기고 폴더 기준 자체에 그 용도를 명문화**했다
|
||||
(`README.md` 폴더 기준표 + `project-context.md`). 선례도 있다 —
|
||||
`quad-v1-architecture.md`/`comparison-fusion-vide.md`가 `base/`에서 같은
|
||||
이유로 이동해온 것.
|
||||
|
||||
같은 처리를 `epoch-brand-composition.md`에도 적용했다(승격이 끝나면 성격이
|
||||
똑같아지므로).
|
||||
|
||||
## 3. 승격 — 무엇이 어떻게 바뀌었나
|
||||
|
||||
### `base/state-epoch-plan.md` (재작성)
|
||||
|
||||
- **`Epoch` 인터페이스 신설** — `type Epoch = { Revision: number }`, 그 자체로
|
||||
키가 되는 unique 테이블. `Source`가 구조적으로 만족하고 `EpochBrand`에
|
||||
등록된다. "루트 `Source`의 에포크"라는 좁은 서술이 전부 여기로 일반화됨.
|
||||
- **`EpochMap` 신설** — `:Update(Epoch|{Epoch}) -> boolean`(반환값의 뜻은
|
||||
"뒤로 전파가 필요한가"), `:Refresh()`, `:Sync()`. 부기가 State에서 떨어져
|
||||
나와 재사용 가능한 객체가 됐다.
|
||||
- **`:Refresh`는 이 세션이 이름만 붙인 것**이다 — "순회"라는 연산 자체는
|
||||
이미 확정돼 있었고(`rawInvalid == false`일 때 자기 맵을 훑는다), 맵이
|
||||
키를 소유하므로 인자 없는 형태가 자연스러워서 그렇게 적었다. 새 설계가
|
||||
아니다.
|
||||
- **State의 두 맵이 `EpochMap` 둘이 됐다** — `sourceCountMap` →
|
||||
`valueEpochMap`, `sourceEmitMap` → `emitEpochMap`. 이름을 바꾼 이유는
|
||||
"source"가 더 이상 정확하지 않아서다.
|
||||
- **문서 구조를 §1~§8로 다시 짰다.** 옛 §3(에이전트 분석)과 §5(열린 질문)에
|
||||
결론과 정정 경위가 섞여 있었는데, 결론은 규칙 본문(§2~§5)으로 올리고
|
||||
경위는 걷어냈다(원문은 `qa-request/pre-implementation-qa-round5-followup.md`
|
||||
M·N절과 `reference/epoch-brand-composition.md`에 있다).
|
||||
|
||||
### `base/brand-plan.md` (전면 재작성)
|
||||
|
||||
- **인스턴스 브랜드** — `Brand()`가 브랜드마다 weak 집합 하나를 들고,
|
||||
`SomeBrand:register(x)` / `SomeBrand:is(x)`. **다중 태깅**이 이 재작성의
|
||||
존재 이유다(`Source`가 `SourceBrand`이면서 동시에 `EpochBrand`).
|
||||
- **역조회 `Brand.get`은 없어졌다.**
|
||||
- **유지된 것**: weak-key 레지스트리, 테이블 아이덴티티, duck-typing 기각
|
||||
근거 둘, 포함 관계 predicate 합성, `Brand`가 `None`에 의존하지 않는다는
|
||||
결정, Luau narrowing 주의.
|
||||
- **새로 명시한 것 하나** — 다중 태깅이 가능해져도 **포함 관계는 계속
|
||||
predicate 합성으로 쓴다**(`Source`를 `StateBrand`에도 등록하지 않는다).
|
||||
등록 지점이 흩어지면 조용한 버그가 되고 포함 관계가 코드에서 사라지기
|
||||
때문. 정당한 다중 등록은 `Epoch`처럼 **다른 축의 계약**뿐이다.
|
||||
- 옛 표면은 `archive/brand-shared-registry-reversed.md`.
|
||||
|
||||
### `base/source-state-plan.md`
|
||||
|
||||
- "Source가 State를 만족함" 절에 **`Epoch`도 같은 방식으로 만족**한다는 항목
|
||||
추가(`Revision`이 공개여야 하는 이유 포함).
|
||||
- `state:Observer(fn)` 절에 **`fn(self, from: Epoch | {Epoch})`** 시그니처
|
||||
확정 추가. "값을 안 실어주는 구독" 계약이 안 깨지는 이유도 같이.
|
||||
- 승격 대기 ⚠️ 배너 제거, 그 절들의 "에포크" 표현을 "리비전"으로 통일.
|
||||
|
||||
### `base/effect-plan.md`
|
||||
|
||||
- **⚠️ 미해결이던 다중 의존성 중복 발화가 닫혔다** — `EffectHandle`이
|
||||
`EpochMap`을 하나 들고 각 내부 Observer가 받은 `from`으로 `Update`,
|
||||
`true`일 때만 `fn`을 돌린다. `Effect`가 곧 deps의 공통 하류가 된다.
|
||||
- **`Ref` 의존성은 이 맵에 안 낀다**(이 세션이 명시) — `Ref`는 `Epoch`가
|
||||
아니고 `:Callback`으로 발화해 `from`이 없다. 공통 상류 문제 자체가 없다.
|
||||
|
||||
### `base/gate-plan.md`
|
||||
|
||||
승격 대기 배너를 반영 완료로 바꾸고, 페이로드/`withheld`/`emitEpochMap`
|
||||
표현을 `Epoch` 어휘로 통일. **기제는 하나도 안 바뀌었다.**
|
||||
|
||||
## 4. flatten — 걷어낸 것들
|
||||
|
||||
사용자가 지목한 "재정정 기록이 쌓인 부분"은 실제로 세 군데였다.
|
||||
|
||||
1. **`question.md`가 다시 해소 항목으로 절반이 찼다.** 그 문서 스스로
|
||||
*"항목을 해소하면 여기서 지우고 `archive/question-resolved.md`에 근거와
|
||||
함께 옮길 것 — 다시 쌓이면 같은 문제가 반복됨"*이라 규정해뒀는데,
|
||||
2026-08-21 하루에 여러 건이 닫히면서 예고대로 재발했다. **16건을 일괄
|
||||
이관**하고(`Gate` 이름 항목 포함), 파일이 421줄 → 208줄이 됐다.
|
||||
- 이관분 중 `Epoch` 관련 서술은 **이관 시점 표현 그대로 뒀고**, 대신
|
||||
이관 섹션 머리에 "필드 이름이 그 뒤 바뀌었다"는 경고를 붙였다. 히스토리
|
||||
문서의 원문을 소급해 고치지 않는다는 관례를 지키면서 오독만 막는 처리.
|
||||
2. **`todos.md`의 "M3 착수 전에 결론이 필요한 항목 목록"이 절반 넘게 `[해소]`
|
||||
였다.** 실제로 열려 있는 건 둘뿐(중간 State GC 미검증, `store:GetDynamic`)
|
||||
인데 해소 항목 일곱이 섞여 있어 "지금 할 일"로 안 읽혔다. 걷어냄.
|
||||
3. **`todos.md` 000번**(다음 세션 첫 작업 = 이 승격)은 완료됐으므로 삭제하고,
|
||||
00번 안에 승격 완료 사실만 남겼다.
|
||||
|
||||
## 5. 마지막 미정도 같은 세션에 닫힘 — 리비전 증가는 `bit32` 랩
|
||||
|
||||
승격 보고 직후 사용자가 그 자리에서 정했다: *"리비전 증가는 bit32 로
|
||||
두고싶어요. 그건 luau 에서 native call 이라 아주 빨라요. 반면 double 의
|
||||
연산이 느린편인데, 희소 수준이 아니라, 사실상 만나는걸 수년간 보기 어려운
|
||||
라운드되어 동일해 무시되는 경우를 막기 위해 double 까지 올려야할 이유를
|
||||
모르겠어요. 매번 도는 코드인지라, 값 싸게 native call + num 연산으로 가볍게
|
||||
가고 싶어요."*
|
||||
|
||||
논거의 핵심은 **"`2^53` 포화는 어차피 도달 불가능한 시나리오인데, 그걸
|
||||
피하겠다고 값을 double 영역까지 키울 이유가 없다"**이고, 매 `Set`마다 도는
|
||||
hot path라는 것.
|
||||
|
||||
**⚠️ 에이전트가 형태를 잘못 옮겼고, 사용자가 그 자리에서 정정했다.**
|
||||
에이전트는 이걸 `bit32.band(rev + 1, 0xFFFFFFFF)`로 적고, 거기에 "그러니
|
||||
`bit32`라서 증가가 더 싼 건 아니다 — `n + 1`은 어느 쪽이든 double 덧셈이고
|
||||
`bit32`는 fastcall을 하나 더 얹는다"는 단서까지 달았다. 사용자 정정:
|
||||
|
||||
> *"잠시만요. 제가 말한건, bit32.bnot(-a) 입니다"* — 그리고 REPL 출력 셋을
|
||||
> 그대로 제시했다(`bit32.bnot(-1)` → `0`, `bit32.bnot(-0)` → `4294967295`,
|
||||
> `bit32.bnot(-4294967295)` → `4294967294`).
|
||||
|
||||
**즉 그 세 줄은 예시가 아니라 연산 자체였다.** `luau`로 재확인한 결과:
|
||||
|
||||
```
|
||||
bump(a) = bit32.bnot(-a)
|
||||
0 → 4294967295 | 1 → 0 | 2 → 1 | 4294967295 → 4294967294
|
||||
```
|
||||
|
||||
`a > 0`이면 `a - 1`, `0`이면 `4294967295`인 **랩어라운드 감소**이고, **갱신과
|
||||
랩이 FASTCALL 하나로** 끝난다. 그래서:
|
||||
|
||||
- **에이전트가 붙였던 단서는 틀렸다** — `band(rev + 1, mask)`라는 **다른
|
||||
형태**를 놓고 한 비교였다. `bnot(-a)`는 덧셈 위에 얹히는 게 아니라 갱신
|
||||
자체를 대체하므로, *"native call 이라 아주 빨라요"*라는 사용자 서술이 맞다.
|
||||
- **따름정리는 방향만 바뀌어 유효하다** — 단조 증가가 아닌 게 아니라 아예
|
||||
**감소**한다. 지금 규칙이 `==`/`~=`만 쓰기 때문에 무해하지만, 순서 비교를
|
||||
넣고 싶어지면 이 결정부터 되짚어야 한다. `Revision`이라는 이름이 순서를
|
||||
뜻하지 않는다는 것도 같이 적어뒀다.
|
||||
|
||||
**교훈**: 사용자가 근거로 든 REPL 출력을 "그 함수가 랩한다는 예시"로만 읽고
|
||||
연산 형태를 에이전트가 임의로 재구성했다. `conventions.md`의 "사용자 발언을
|
||||
근거로 인용할 때는 결론만 적지 말고 논거까지 남길 것" 항목이 겨냥하는 실패에
|
||||
가깝다 — 이 경우엔 논거를 남기긴 했는데 **형태를 바꿔 남겼다.**
|
||||
|
||||
**그래서 `Epoch`/`EpochMap`/`Brand`에 열린 설계 항목은 하나도 남지 않았다.**
|
||||
|
||||
`doc-check.py` ERROR 0 유지(WARN 30건은 전부 이 세션 이전부터 있던 것 —
|
||||
축약 파일명 인용과 날짜 없는 시한부 주장).
|
||||
|
||||
|
||||
## 6. 감사 루프 기록
|
||||
|
||||
`conventions.md`의 "핸드오버 준비하고 커밋해" 절차대로 **한 턴에 하나씩**,
|
||||
라운드마다 각도를 바꿔 돌렸다.
|
||||
|
||||
### 1라운드 — `base/` 정합성 + diff 범위
|
||||
|
||||
발견 넷 중 **하나는 stale**(감사자가 `bit32` 편집 이전 스냅샷을 읽어
|
||||
"리비전 증가 방식이 아직 미정"이라 보고했으나 다섯 곳 전부 이미 갱신돼
|
||||
있었음 — 감사자가 도는 도중 메인 세션이 같은 파일을 고치면 생기는 일이라,
|
||||
**감사 리포트를 받으면 항상 현재 파일로 재확인할 것**). 나머지 셋은 유효:
|
||||
|
||||
1. **`state-epoch-plan.md`가 "§8에서 기각됨"으로 실재하지 않는 내용을
|
||||
가리켰다** — 재작성하면서 "게이트를 에포크 경계로" 기각 논거를 떨어뜨렸고,
|
||||
`gate-plan.md`와 `reference/`는 또 **다른** 번호(§5-3)를 대고 있었다.
|
||||
→ §8에 실제 논거를 쓰고 세 포인터를 절 제목으로 통일. **`doc-check.py`는
|
||||
산문 속 `§번호`를 검증하지 못한다** — 문서를 재작성할 때 §번호 포인터는
|
||||
손으로 훑어야 한다.
|
||||
2. `ROADMAP.md` M7이 `isState`/`isSource`를 아직 "공유 레지스트리 기반"으로
|
||||
서술 → 멤버십 기반으로.
|
||||
3. **스파이크 `22`가 역전된 `Brand.set`/`Brand.get`을 직접 구현한 채
|
||||
`done/`에 있었다** → `rewrite-required/`로 이동. 검증 대상(포함 관계)은
|
||||
유효하므로 결론이 틀려서가 아니라 **구현자가 그 파일의 `Brand` 구현을
|
||||
참고 모델로 오독**하는 걸 막기 위함. `05`와 같은 처리.
|
||||
|
||||
### 2라운드 — 인덱스 레이어 + `luau-test` + 1라운드 이후 diff
|
||||
|
||||
여섯 건, 전부 유효:
|
||||
|
||||
1. **`state-epoch-plan.md`가 자기 문서 안에서 모순**했다 — §2가 "`Revision`을
|
||||
**증가**시킨다"라고 적어놓고 20줄 뒤에 "확정된 방식은 증가가 아니라
|
||||
**감소**한다"라고 반박. `bit32.bnot(-a)` 정정을 반영하면서 앞쪽 문장을
|
||||
안 고친 것. → 방향을 안 담는 "갱신한다"로.
|
||||
2. `source-state-plan.md`에 **같은 문장이 복붙돼 있었다** — 같이 정정.
|
||||
3. `README.md`의 `effect-plan.md` 행이 dedup 대칭을 아직 "미확인"으로 서술
|
||||
(본문은 `EF-3`에서 이미 "성립함"으로 확정). 세션 전부터 있던 stale.
|
||||
4. `STATUS.md`의 `✅ done/` 절이 **같은 파일 위쪽과 어긋났다** — 배너와 표는
|
||||
`22` 이동을 반영했는데 절 제목("17건")과 본문 나열("런타임 9건 …/`22`")은
|
||||
옛 상태. → 하드코딩 개수를 빼고 폴더/표를 소스로.
|
||||
5. `luau-test/README.md`의 "실행 환경 세 갈래" 표가 `21`/`22`/`23`을
|
||||
빠뜨리고 `13`을 아직 "런타임/타입 부분"으로 쪼개 적고 있었다(런타임
|
||||
절반은 2026-08-19에 `22`로 분리돼 나갔다). → 실제와 맞추고, 파일 헤더가
|
||||
소스임을 명시.
|
||||
6. `reference/slot-attach-decomposition.md`의 제약 `C7`이 그 뒤 `native*`
|
||||
계층 확정으로 **일반 계약으로서는 폐기**됐는데 문서에 아무 표시가
|
||||
없었다. 결론 자체는 유효하지만(배치 경로가 부기를 먼저 끝내는 건 `C6`
|
||||
요구사항이라 별개) 표만 떼어 읽으면 오독하므로 상단에 캐비엇 배너.
|
||||
`reference/comparison-fusion-vide.md`가 이미 같은 형태의 캐비엇을 달고
|
||||
있어 선례를 따랐다.
|
||||
|
||||
**교훈 하나** — 4·5번은 둘 다 **"같은 문서 안에서 위쪽만 갱신되고 아래쪽이
|
||||
남은"** 형태다. 개수를 제목에 박아두면 정확히 이렇게 갈라지므로, 이번에
|
||||
`STATUS.md`의 절 제목 두 곳에서 하드코딩 개수를 뺐다.
|
||||
|
||||
### 3라운드 — `archive/` 배너 정합성 + `qa-request/`의 "반영 완료" 주장
|
||||
|
||||
**확실한 모순 발견 0건 — 수렴.** 감사자가 대조한 것: `archive/`의 배너
|
||||
포인터 넷(`brand-shared-registry-reversed` / `question-resolved`의 이관 섹션 /
|
||||
`always-propagate-no-dedup-superseded` / `invalidate-dedup-propagation-reversed` /
|
||||
`bookkeeping-before-physical-reversed`), 2라운드가 고친 여섯 자리, 그리고
|
||||
`round4/5-followup.md`가 소스라고 선언한 항목들.
|
||||
|
||||
특히 **2라운드에서 내가 새로 쓴 `C7` 캐비엇**(*"배치 경로가 부기를 먼저
|
||||
끝내는 건 `C7`이 아니라 `C6`가 요구하는 별개 사안"*)이
|
||||
`base/dispatch-core-plan.md`의 "일반 계약 — 물리와" 절 4번과 정확히 대응함이
|
||||
확인됐다 — 새로 쓴 서술이 기존 확정과 어긋나지 않는지가 이 라운드의 핵심
|
||||
질문이었다.
|
||||
|
||||
유일한 지적은 문체였다 — §8에 "`Revision`만 **올리면**"이라는 방향 어감이
|
||||
남아 있던 것(§2가 이미 "방향은 계약이 아니다"로 커버하고 있어 모순은 아님).
|
||||
같이 정리했다.
|
||||
|
||||
**수렴 판정**: 3라운드에서 새 발견 0건이므로 `conventions.md`의 루프 종료
|
||||
조건을 만족한다. 총 소비는 서브에이전트 3패스(각도: `base/` 정합성 →
|
||||
인덱스 레이어+`luau-test` → `archive/`+`qa-request/`).
|
||||
|
||||
## 7. 커밋 전 `/code-review high` — 감사 3라운드가 못 본 축에서 9건
|
||||
|
||||
`conventions.md`가 명문화한 그대로였다 — **감사자와 code-review는 보는 축이
|
||||
다르다.** 감사자 3라운드가 수렴(0건)한 뒤 사용자가 `/code-review high`를
|
||||
돌리자 **9건이 더 나왔고 전부 유효**했다. 감사자는 코퍼스 전체의 **의미론적
|
||||
정합성**(A 문서 결정 ↔ B 문서 서술)을 보고, code-review는 **diff 자체의
|
||||
결함**(이번에 새로 쓴 서술 안의 모순, 새 표면이 기존 계약과 충돌하는가)을
|
||||
본다.
|
||||
|
||||
### 구현을 실제로 막았을 것들
|
||||
|
||||
1. **⭐ `{Epoch}` 표기가 실제 배치 모양과 달랐다.** Luau에서 `{Epoch}`는
|
||||
**배열**(`{[number]: Epoch}`)인데, 실제로 넘어오는 게이트 배치는
|
||||
`gate-plan.md` 4번이 확정한 `withheld : { [epoch] : true }` — **집합**이다.
|
||||
이 표기를 믿고 `ipairs`로 구현하면 배치 순회에서 **원소가 0개**가 되어
|
||||
유보됐다 풀린 emit이 전부 조용히 삼켜진다. `gate-plan.md` 4번이 애초에
|
||||
고치려던 바로 그 버그였다. → `type EpochSet = { [Epoch]: true }`로 확정.
|
||||
**집합이어야 하는 이유는 게이트 쪽 요구**다(흡수·unfold가 저절로 접혀야
|
||||
함). 이건 사용자 원 제안의 `Epoch|{Epoch}` 표기를 에이전트가 그대로
|
||||
옮기면서 **Luau 타입으로서 뭘 뜻하는지 확인 안 한** 결과다.
|
||||
2. **⭐ 새 노드 시딩이 확정된 `EpochMap` 표면으로 표현 불가능했다.**
|
||||
"상류의 `Epoch`를 전부 끌어와 채운다"인데 `:With(a, b)`의 상류는
|
||||
**State이지 `Epoch`가 아니다.** 상류가 추적 중인 루트 집합은 그 State의
|
||||
`valueEpochMap` 안에만 있는데 §3 표면엔 **키 열거도 병합도 없었다.**
|
||||
→ `:TrackFrom(other)` 신설(가칭 `Absorb`, 같은 날 개명 — §8 참고).
|
||||
`:Refresh`와 같은 성격으로 **이미 확정된 동작에 이름을 붙인 것**이지
|
||||
새 설계가 아니다(사용자 확정 문구가 이미 *"전부 가져와서, 실제 count 로
|
||||
둡니다"*였다). dep이 `Epoch`면 `:Sync`, State면 `:TrackFrom`으로 갈리는
|
||||
것도 같이 적었다.
|
||||
3. **`GateNode` 예외가 §4에 기록돼 있지 않았다.** §4 의사코드는
|
||||
`emitEpochMap:Update(from)`을 **수신 시점에 무조건** 부르는데,
|
||||
`gate-plan.md` 4번은 게이트가 **전파할 때** `:Sync(batch)`로 갱신하는
|
||||
것으로 확정돼 있다. §4대로 구현하면 그 계약이 조용히 깨진다.
|
||||
4. **설치 시 즉시 1회 발화에 `from`이 없다.** `fn(self, from)`을
|
||||
non-optional로 선언해뒀는데 등록 시점 발화엔 출처가 없다 —
|
||||
`Effect`의 확정 클로저가 `Update(nil)`을 부르게 된다. → 옵셔널로 바꾸고,
|
||||
`Effect`의 억제 플래그가 `Update`보다 **먼저** 와야 함을 명시(순서를
|
||||
뒤집으면 설치 발화가 맵을 건드려 그 파동의 첫 진짜 emit이 접힌다).
|
||||
|
||||
### 근거·표기 정확성
|
||||
|
||||
5. **`2^32` 랩이 "똑같이 도달 불가능"이라는 근거가 틀렸다.** 같은 척도(초당
|
||||
100만 `Set`)로 `2^53`은 285년인데 **`2^32`는 약 72분**이다(20만 배 차이).
|
||||
실제로 안전한 이유는 **도달 시간이 아니라 충돌 조건이 한 점**이라는 것
|
||||
("정확히 `2^32`만큼 뒤처진 항목"이라야 하고, 한 바퀴 중 한 번이라도
|
||||
건드려지면 갱신됨). 이 문서가 바로 위 항목에서 *"근거를 정확히 적을 것"*
|
||||
이라 스스로 규정해놓고 20줄 뒤에 어긴 셈이라 그대로 정정했다.
|
||||
6. `:Sync`를 "초기화에만 쓴다"고 적었으나 `gate-plan.md`가 flush 경로에서도
|
||||
쓴다 → "반환값이 필요 없다고 이미 아는 곳 둘"로 정정.
|
||||
7. `ROADMAP.md`의 `Brand.luau` predicate 목록에 `isEpoch` 누락.
|
||||
8. **역전된 옛 이름 잔재** — `TweenTag`(3곳)와 `Effect(fn, state?)`(4곳).
|
||||
후자는 code-review가 짚은 것보다 실제로 더 많았다
|
||||
(`architecture.md`/`lifecycle-hooks-plan.md`/`ROADMAP.md`/`README.md`).
|
||||
9. **절 재편에 안 따라온 `§` 참조 3곳** — 1라운드가 같은 유형을 잡았는데도
|
||||
남아 있었다. `doc-check.py`가 `§번호`를 검증 못 하는 사각지대다.
|
||||
|
||||
**교훈**: 1·2번은 둘 다 **"사용자 제안의 표기를 그대로 옮겼는데 그게 실제
|
||||
메커니즘과 안 맞는" 유형**이다. 승격은 문장을 옮기는 작업이 아니라 **표기가
|
||||
가리키는 것이 실제로 성립하는지 확인하는 작업**이라는 게 이번의 교훈.
|
||||
|
||||
## 8. `:Refresh`/`:TrackFrom` — 에이전트가 이름 붙인 둘, 사용자 검토로 확정
|
||||
|
||||
`/code-review`까지 처리한 뒤, **에이전트가 임의로 이름 붙인 연산 둘을
|
||||
사용자에게 명시적으로 올렸다.** 동작 자체는 회신에 이미 확정돼 있었지만 표면
|
||||
이름은 에이전트 판단이었기 때문 — `conventions.md`의 "애매하면 임의로 정하지
|
||||
말고 그 자리에서 사용자에게 보고할 것"에 해당한다.
|
||||
|
||||
**`:Refresh` — 그대로 확정.** 사용자가 `:Update`와 합치지 않는 이유를 직접
|
||||
정리했다: *"Update 에 인자 없는건 좀 아니야. 뭔가 받아서 받은것들에 대해서
|
||||
처리하겠다는건데, 리프레시는 아무래도 내가 받았던걸 처리하겠다는거라
|
||||
표면적 의미 자체가 다르지."* — **인자를 받아 그것을 처리하는 연산**과
|
||||
**자기가 이미 들고 있는 것을 처리하는 연산**은 표면적 의미가 달라서 오버로드로
|
||||
합치면 안 된다는 것. 표면이 하나 줄어드는 것보다 이 구분이 값이 크다.
|
||||
|
||||
**`:Absorb` → `:TrackFrom`으로 개명.** 사용자 지적: *"absorb 는 조금 상위
|
||||
요소꺼를 흡수해서 상위 요소에서 제거할것만 같은 이름이긴 하네."* 맞다 — 이
|
||||
연산은 `other`를 **전혀 안 건드린다.** 덧붙여 `base/gate-plan.md`가 이미
|
||||
"흡수 집합"을 **다른 뜻**(emit을 붙들고 있음)으로 쓰고 있어, 한 코퍼스 안에
|
||||
같은 단어가 두 의미로 놓이는 문제도 있었다.
|
||||
|
||||
후보를 넷 올렸고(`TrackFrom`/`SeedFrom`/`Include`/`Adopt`) 권장안이 채택됐다.
|
||||
`TrackFrom`을 고른 근거 셋:
|
||||
|
||||
1. **이 맵의 존재 이유를 사용자가 표현한 말이 "추적"이었다** —
|
||||
*"'내가 뭘 추적하고 있나' 가 필요하죠"*(§4의 시딩 규칙 근거). 코퍼스가 이
|
||||
맵을 설명하는 말과 메소드 이름이 일치한다.
|
||||
2. **`From`이 방향을 못박아 비파괴가 드러난다** — `Absorb`가 실패한 지점.
|
||||
3. **`SeedFrom`보다 오래 간다** — 동적 의존성으로 "생성 이후에 키를 더하는"
|
||||
자리가 생겨도 이름이 그대로 맞다. `Seed`는 그때 거짓말이 된다.
|
||||
|
||||
배제한 것도 근거가 있다 — `Extend`/`Inherit`은 `quad2-try`의 `Base:Extends`
|
||||
OOP 상속이 **확인된 죽은 접근**이라 그 어휘를 되살리면 오독을 부르고,
|
||||
`Extract`는 코퍼스에서 이미 "소유권을 통째로 넘긴다"는 뜻이다.
|
||||
|
||||
**두 이름 다 `question.md`에 안 올린다** — 같은 자리에서 확정됐으므로 열린
|
||||
항목이 아니다.
|
||||
|
|
@ -5,38 +5,28 @@
|
|||
(`.claude/question.md`, `luau-test/STATUS.md` 등).
|
||||
|
||||
|
||||
000. **⭐⭐⭐ [2026-08-21 신설] 다음 세션의 첫 작업 — `Epoch`/`EpochMap`/`Brand`
|
||||
제안을 `base/`로 승격할 것.** 설계는 **사실상 전량 확정**됐고
|
||||
(`research/epoch-brand-composition.md`가 소스, §4에 결정 상태 전부),
|
||||
같은 날 세션이 너무 길어져 **사용자 지시로 승격만 다음 세션에 미뤘다**
|
||||
(*"전면 승격을 하고 싶지만, 이 세션은 너무 길어요. 컨텍스트 피로 상
|
||||
핸드오버 이후 승격조치를 해야할것 같습니다"*).
|
||||
- **고쳐야 할 문서 넷**: `base/state-epoch-plan.md`(두 맵 → `EpochMap` 둘,
|
||||
"Source의 에포크" → `Epoch`), `base/source-state-plan.md`(`Source`가
|
||||
`Epoch`를 구조적으로 만족 + `Observer` 클로저가 `fn(self, from)`),
|
||||
`base/effect-plan.md`(`Effect`가 자기 `EpochMap`을 들어 다중 dep 중복
|
||||
발화를 접음 — 그 문서의 ⚠️ 미해결 항목이 이걸로 닫힌다),
|
||||
`base/brand-plan.md`(인스턴스 브랜드로 전면 재작성).
|
||||
넷 다 상단에 이 제안을 가리키는 ⚠️ 배너가 이미 달려 있다.
|
||||
- **남은 미정은 하나** — 리비전 증가를 `bit32` 랩으로 할지 평이한 `+1`로
|
||||
할지(둘 다 실질 위험 없음, `question.md` 1번).
|
||||
- **감사는 승격 이후에 돌린다**(사용자 지시) — 지금 돌리면 곧 바뀔 서술을
|
||||
감사하게 된다.
|
||||
|
||||
00. **⭐⭐ [2026-08-18 신설] 구현 전 QA — [2026-08-21] 1~5라운드 전부 `base/`
|
||||
반영 완료. ⭐ 같은 날 마지막에 `Gate`와 State 에포크까지 확정되면서
|
||||
**M2 착수를 막는 설계 항목은 더 이상 없다.**** `Gate`는
|
||||
`state:Gate(setup)` + `GateNode`로(`base/gate-plan.md`), State의
|
||||
재계산/전파 판정은 **소스 에포크 비교** 채택으로(`base/state-epoch-plan.md`,
|
||||
재계산/전파 판정은 **`Epoch` 리비전 비교** 채택으로(`base/state-epoch-plan.md`,
|
||||
구현은 M3) 닫혔다. 두 문서 모두 `research/`가 아니라 **`base/`**에 있다.
|
||||
**[2026-08-21 후속] `Epoch`/`EpochMap`/`Brand` 승격도 같은 날 완료** —
|
||||
부기가 재사용 가능한 `EpochMap`으로 떨어져 나오고, 판정 인터페이스가
|
||||
`Source`에서 `Epoch`로 일반화되고, `Brand`가 인스턴스 브랜드로 재작성됐다
|
||||
(근거 기록은 `reference/epoch-brand-composition.md`, 옛 `Brand` 표면은
|
||||
`archive/brand-shared-registry-reversed.md`). 그 부수로 `effect-plan.md`의
|
||||
다중 의존성 중복 발화 미해결 항목도 닫혔다. **[같은 날] 마지막 미정이던
|
||||
리비전 증가 방식도 `bit32` 랩으로 확정** — `Epoch`/`EpochMap`/`Brand`에
|
||||
열린 설계 항목은 **하나도 없다**(`base/state-epoch-plan.md` §2).
|
||||
**[2026-08-21 경위]** 같은 날 `/code-review high`가 "게이트가 유보했다
|
||||
내보내는 emit이 어느 출처를 싣는지"가 안 정해진 걸 잡아 한때 M2 항목으로
|
||||
되돌아갔으나, **사용자가 그 자리에서 `emit(self)` + 흡수 집합으로
|
||||
확정**했다(`setup` 시그니처는 안 바뀜 — `base/gate-plan.md` 4번). 같이
|
||||
제기됐던 에포크 쪽 세 자리도 **전량 확정**됐다(재계산 시 count 전부 갱신 /
|
||||
새 노드는 `sourceEmitMap`은 비우고 `sourceCountMap`은 실제 count로 채운 뒤
|
||||
제기됐던 에포크 쪽 세 자리도 **전량 확정**됐다(재계산 시 리비전 전부 갱신 /
|
||||
새 노드는 `emitEpochMap`은 비우고 `valueEpochMap`은 실제 리비전으로 채운 뒤
|
||||
`rawInvalid = true` / 그래서 `:With` 병합 규칙은 불필요) —
|
||||
`state-epoch-plan.md` §5 7번. **[같은 날 두 번째 `/code-review high`]**
|
||||
`base/state-epoch-plan.md` §4. **[같은 날 두 번째 `/code-review high`]**
|
||||
7건이 더 나왔고 전부 유효했는데(재진입 시 빈 배치가 새어 변경이 증발하던
|
||||
것, `OffWithoutEmit`이 흡수 집합을 안 비우던 것 등), 그중 사용자 판단으로
|
||||
올라갔던 둘도 같은 날 닫혔다 — "재진입 계약"은 애초에 **잘못 옮긴
|
||||
|
|
@ -54,7 +44,7 @@
|
|||
**`Detach` 보존 주체(`userdata` → `slot._detached`)**, **`KeyGone` 센티널**,
|
||||
**`Owned` 설치 플래그**, 그리고 **`attachSlot` 분해**
|
||||
(`materializeSlotTree` + `mountSlotTree`, 근거는
|
||||
`research/slot-attach-decomposition.md`)가 전부 확정·반영됐다.
|
||||
`reference/slot-attach-decomposition.md`)가 전부 확정·반영됐다.
|
||||
**[2026-08-21 정정] 5라운드 문항지를 만들었다** — 4라운드 처리 때는 사용자
|
||||
지시("이후 stale 만 잡는것으로 끝낼 수 있어보임")로 안 만들기로 했으나, 같은
|
||||
날 사용자가 5라운드를 요청("4차에서 예로 넘어갔던건 스킵하고, 새로운
|
||||
|
|
@ -121,45 +111,19 @@
|
|||
재편 여부는 열림 — `pre-implementation-qa-round3.md`의 "ROADMAP.md
|
||||
마일스톤 정합성" 절 참고).
|
||||
|
||||
**아래는 M3 착수 전에 결론이 필요한 항목 목록**(M0/M2는 여전히 막혀
|
||||
있지 않음, 0번 항목 참고 — **단, M2가 M3의 `Blocker.luau`를 선당겨야
|
||||
하는지는 별개로 열려 있음, 바로 아래 첫 항목**) — 대부분 `question.md`
|
||||
3번에도 올라가
|
||||
있고(**[정정, 2026-08-18 `/code-review high`] 사용자 판단이 필요한
|
||||
항목만 그렇다 — 아래 "dedup 경로" 대칭 확인은 판단이 아니라 구현 시
|
||||
검증 작업이라 `question.md`엔 없음, 여기 목록이 소스**), 각 `base/`
|
||||
문서에도 ⚠️로 표시돼 있다:
|
||||
- **M2가 M3의 `Blocker.luau`에 의존하게 된 순서 문제**(`ROADMAP.md`
|
||||
M2 체크박스 각주) — 지금은 각주만 달아둔 임시 조치, `Blocker.luau`
|
||||
(또는 최소 표면)를 M2로 앞당길지 로드맵 순서를 유지할지 **M2 착수
|
||||
전 필요**. `qa-request/pre-implementation-qa-round3.md`의
|
||||
"ROADMAP.md 마일스톤 정합성" 절.
|
||||
**아래는 M3 착수 전에 결론이 필요한 항목 목록** — M0/M2는 막혀 있지
|
||||
않다(0번 항목 참고). 둘 다 `question.md` 3번에도 있고 각 `base/` 문서에도
|
||||
⚠️로 표시돼 있다. **[2026-08-21 정리]** 여기 쌓여 있던 `[해소]` 항목들
|
||||
(`Blocker.luau` 마일스톤 순서, 그룹 `Attribute` 위치 claim 키,
|
||||
`SetAndDispose`, `PopOnly`→`Detach`, `KeyGone` 처분, `Store` 미선언 키
|
||||
타입 에러, dedup 경로 대칭)은 **전부 `archive/question-resolved.md`와
|
||||
각 `base/` 문서로 옮겼다** — 목록이 절반 넘게 해소 항목으로 차 있어
|
||||
"지금 할 일"로 읽히지 않던 것을 걷어낸 것.
|
||||
- **중간 State GC 미검증**(`base/source-state-plan.md`) — 상류 strong /
|
||||
하류 weak 불변식을 명문화할지 + `luau-test` 실측. **M3 착수 전 필요.**
|
||||
- **그룹 `Attribute`의 위치별 claim 설계**(`base/attribute-plan.md`) —
|
||||
방향은 확정, 키 설계가 미정. M10 착수 전 필요.
|
||||
- **[2026-08-20 해소]** `SetAndDispose` 방향 — **`source:SetAndDispose(value)`
|
||||
콜론 메서드로 확정**(`:Set`과 한 세트). `state:Apply` 시그니처엔 영향
|
||||
없음(`Apply` 오버라이딩은 `Source`→`State` 단방향 때문에 타입이 안
|
||||
성립해서 애초에 불가). `base/slot-plan.md`의 `dispose` 절.
|
||||
- **dedup 경로의 process/retract 대칭 확인**(`base/effect-plan.md`
|
||||
`:Unsubscribe()` 절) — M3 착수 전 확인.
|
||||
- **[2026-08-19 해소]** `PopOnly` 이름 — **`Detach`로 확정**(공개 표면
|
||||
위치도 `None`과 같은 최상위 export로 같이 확정). 원문은
|
||||
`archive/question-resolved.md`.
|
||||
- **[2026-08-21 해소]** `Detach` 홀드 중 키가 사라졌을 때의 처분 —
|
||||
**`KeyGone` 센티널로 `updateFn`에게 묻는 것**으로 확정. detach 요소는
|
||||
`slot._detached` 필드가 보유하고 owner 사망 시 `activateList`가 건
|
||||
`Effect`가 정리.
|
||||
`base/slot-plan.md`의 "`KeyGone`" 절.
|
||||
- **`store:GetDynamic`을 콜론 메소드로 둘지 탑레벨 함수로 둘지**
|
||||
(`base/store-plan.md`) — 콜론이면 `GetDynamic`이 모든 Store의 예약 키가
|
||||
됨(lazy `__index`와 충돌). M3/M4 착수 전 필요.
|
||||
- **[2026-08-19 해소]** `Store` 미선언 키가 실제로 타입 에러가
|
||||
나는지 — **예, 확인됨**(`luau-test/done/21-type-store-undeclared-key-rejected.luau`,
|
||||
`ProcessStoreType`이 합성한 레코드 타입은 인덱서가 없어 미선언 키
|
||||
접근이 정확히 `TypeError`로 거부됨). `base/store-plan.md`의 "Store =
|
||||
Source들의 이름 붙은 모음" 절의 "확인 요구" 표시도 해소로 갱신 필요.
|
||||
|
||||
0. **⭐ M0 착수를 막는 결정은 이제 없음 (2026-08-14 열한 번째 세션 기준).**
|
||||
`question.md`의 최우선 항목이 **전부 비었음** — `0-Y`(`:Compute` lazy
|
||||
|
|
|
|||
66
ROADMAP.md
66
ROADMAP.md
|
|
@ -42,13 +42,13 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
것은 "이미 invalid면 전파 중단되는지"가 **아니라** 그 반대:
|
||||
**emit은 자기 invalid 상태와 무관하게 전파되고**, 중복 재계산은
|
||||
`:Get()` 시점 캐시로만 막히는지(**[2026-08-21]** 그 뒤 "항상"에서
|
||||
"같은 에포크의 두 번째만 접힘"으로 좁혀졌다 — 아래 참고). 특히 `:Get()`을 안 부르는
|
||||
"같은 `Epoch`의 같은 리비전이 두 번째로 도착했을 때만 접힘"으로 좁혀졌다 — 아래 참고). 특히 `:Get()`을 안 부르는
|
||||
`Observer`가 매 변경마다 계속 울리는지 — 옛 모델에선 두 번째부터
|
||||
침묵했음(`archive/invalidate-dedup-propagation-reversed.md`).
|
||||
스파이크 `05-store-state-diamond-propagation.luau`는 **[2026-08-19
|
||||
재작성 완료]** 그 모델("emit은 항상 전파 + `:Get()` 시점 캐시로만
|
||||
dedup")로 재검증 통과 — **[2026-08-21] 그 모델이 다시 바뀌어
|
||||
`rewrite-required/`로 되돌아갔다**(소스 에포크 채택으로 다이아몬드
|
||||
`rewrite-required/`로 되돌아갔다**(`Epoch` 리비전 비교 채택으로 다이아몬드
|
||||
Observer가 이제 변경당 **1회**만 울어야 함, `base/state-epoch-plan.md`))
|
||||
- [x] Source가 State를 구조적으로 만족하는 제네릭 타입(`:Compute<U>(self:
|
||||
Source<T>, ...) -> State<U>`류, self 타이핑 + State 참조 혼합)이
|
||||
|
|
@ -152,8 +152,14 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
`process(inst,k,v,index) -> (hintValue)->()` **3종** — `isHandlable`도
|
||||
`inst`를 받도록 확정(2026-08-07 여덟 번째 세션), 별도 `retract` 필드는
|
||||
`process` 반환값으로 합쳐짐(2026-08-13 다섯 번째 세션))
|
||||
- [ ] `Brand.luau`(공유 weak-key 레지스트리, `Brand.set(x,tag)`/
|
||||
`Brand.get(x)` — `isState`뿐 아니라 `isObserver`/`isEffect`/`isTag`/
|
||||
- [ ] `Brand.luau`(**[2026-08-21 재작성]** 인스턴스 브랜드 — `Brand()`가
|
||||
브랜드마다 weak-key 집합 하나를 들고 `:register(x)`/`:is(x)`,
|
||||
**다중 태깅 허용**(`Source`가 `SourceBrand`이면서 동시에 `EpochBrand`).
|
||||
옛 공유 레지스트리 + `Brand.get(x) -> tag`는
|
||||
`archive/brand-shared-registry-reversed.md`. **[2026-08-22] `isEpoch`도
|
||||
여기 포함** — `Epoch` 인터페이스 확정으로 `EpochBrand`가 생겼고
|
||||
`base/state-epoch-plan.md`/`base/source-state-plan.md`가 "런타임 분기는
|
||||
`isEpoch`로"라고 확정했다 — `isState`뿐 아니라 `isObserver`/`isEffect`/`isTag`/
|
||||
`isAttributeKey`/`isAttribute`/`isTween`/`isBlocker`/`isSource`/
|
||||
`isStore`/`isSlot`/`isRef`/`isPreRef`/`isModifier`(2026-08-07 열 번째
|
||||
세션 추가 — 원래 태그 목록에서 빠져있었음. **[정정, 2026-08-09
|
||||
|
|
@ -256,6 +262,9 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
형태까지는 M2에 필요하다. 경위는
|
||||
`qa-request/pre-implementation-qa-round3.md`의 "ROADMAP.md 마일스톤
|
||||
정합성" 절과 `pre-implementation-qa-round5-followup.md`.
|
||||
**[2026-08-21 추가] `GateNode`가 `emitEpochMap`을 쓰므로 `EpochMap.luau`
|
||||
(M3 목록에 있음)도 M2에 같이 들어와야 한다** — `base/gate-plan.md` 4번,
|
||||
`base/state-epoch-plan.md` §3.
|
||||
- [ ] 핸들러 계약 검증: `process`가 retractor 클로저를 **반환하지 않는**
|
||||
핸들러를 등록하면 리뷰/린트에서 걸러내기(정리할 게 없어도 항상
|
||||
`function() end`를 반환 — `Dispatch.retractFrom`이 nil 체크 없이
|
||||
|
|
@ -349,14 +358,23 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
**공용 게이트 노드는 M2에서 이미 만들어져 있다**("게이팅 먼저" 결정,
|
||||
위 M2 각주 + `base/gate-plan.md`). M3에서 `Blocker`를 짤 때는
|
||||
그 노드를 **다시 만들지 말고 그 위의 정책으로** 얹을 것.
|
||||
- [ ] **[2026-08-21 5라운드 — 채택 확정]** State의 재계산/전파 판정은
|
||||
**소스 에포크 비교**다(`base/state-epoch-plan.md`) — `invalid` 플래그가
|
||||
아니다. 아래 `Source.luau`/`State.luau`가 이걸 전제로 짜여야 한다:
|
||||
노드가 `sourceCountMap`(값 유효성)/`sourceEmitMap`(전파 dedup) 두
|
||||
테이블을 들고, emit은 발행 source만 싣고, 순회는 `rawInvalid == false`
|
||||
일 때만 돌며 **값만 앞당기고 통지는 상류 emit을 기다린다**. 다이아몬드
|
||||
중복 통지가 접히므로 스파이크 `05`도 그에 맞춰 재작성해야 한다
|
||||
- [ ] **[2026-08-21 5라운드 — 채택 확정, 같은 날 `Epoch`로 일반화]** State의
|
||||
재계산/전파 판정은 **`Epoch` 리비전 비교**다(`base/state-epoch-plan.md`)
|
||||
— `invalid` 플래그가 아니다. 아래 `Source.luau`/`State.luau`가 이걸
|
||||
전제로 짜여야 한다: `Source`가 `type Epoch = { Revision: number }`를
|
||||
구조적으로 만족하고(`EpochBrand`에도 등록), 부기는 재사용 가능한
|
||||
**`EpochMap`**(`:Update(Epoch|EpochSet) -> boolean`, `:Refresh`, `:Sync`,
|
||||
`:TrackFrom` — `EpochSet = {[Epoch]: true}`, **배열 아님**)
|
||||
으로 떼어내며, State가 그걸 **둘** 컴포지션한다 — `valueEpochMap`(값
|
||||
유효성)/`emitEpochMap`(전파 dedup). emit은 값도 리비전도 안 싣고
|
||||
**출처(`Epoch`나 그 집합)만** 싣고, 순회는 `rawInvalid == false`일 때만
|
||||
돌며 **값만 앞당기고 통지는 상류 emit을 기다린다**. 다이아몬드 중복
|
||||
통지가 접히므로 스파이크 `05`도 그에 맞춰 재작성해야 한다
|
||||
(`luau-test/STATUS.md`).
|
||||
- [ ] `EpochMap.luau` — 위 부기 객체. `Effect`(M3)와 `GateNode`(M2)도 같은
|
||||
것을 쓰므로 `State.luau`에 묻지 말고 별도 모듈로 낼 것.
|
||||
**`GateNode`가 M2로 앞당겨졌으므로 이 모듈도 실제로는 M2에 필요하다**
|
||||
(위 M2 게이팅 각주).
|
||||
- [ ] `Blocker.luau`(`base/blocker-plan.md` 참고 — 여러 Source를
|
||||
한꺼번에 바꿔도 파생값 재계산/재대입이 한 번만 되게 하는 primitive,
|
||||
State와 밀접히 연관돼 있어 같은 마일스톤에서 개발)
|
||||
|
|
@ -370,10 +388,14 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
경로 가드**(`{priority = HANDLER_PRIORITY_FALLBACK, isHandlable = v
|
||||
is Observer, process = error(...)}`, `k` 타입 안 가림, 2026-08-14
|
||||
열한 번째 세션 — `PreRef`와 같은 패턴)도 같이 등록
|
||||
- [ ] `Effect(fn, state?)`(`base/effect-plan.md`) — `state` 생략 시 설치
|
||||
1회+leaf 사망 시 확정 정리, `state` 지정 시 내부적으로
|
||||
`state:Observer(...)`를 조합해 재실행+cleanup 체이닝(React
|
||||
`useEffect` 동형). Observer 구현 이후에 착수(의존 관계).
|
||||
- [ ] `Effect(fn, ...deps)`(`base/effect-plan.md`, **[2026-08-21 5라운드
|
||||
`C-6`]** 옛 시그니처는 `Effect(fn, state?)`) — deps 생략 시 설치
|
||||
1회+leaf 사망 시 확정 정리, deps 지정 시 **각각에 맞는 구독**
|
||||
(State/Source는 `Observer`, `Ref`는 `:Callback`)을 걸어
|
||||
재실행+cleanup 체이닝(React `useEffect` 동형). **`EffectHandle`이
|
||||
`EpochMap`을 하나 들어** 공통 상류로 인한 중복 발화를 접고, 설치 구간
|
||||
억제 플래그가 그 `Update`보다 먼저 와야 함
|
||||
(`base/state-epoch-plan.md`). Observer 구현 이후에 착수(의존 관계).
|
||||
`EffectHandle:Subscribe()`/`:Unsubscribe()`도 추가(leaf 없이 쓰는
|
||||
모듈/스크립트 레벨 Effect) — `:Unsubscribe()`는 Observer와 달리
|
||||
마지막 cleanup을 1회 트리거해야 함(2026-08-07 일곱 번째 세션).
|
||||
|
|
@ -663,7 +685,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
`recompute`가 2회→1회로 준다. 순서 제약이 줄 순서가 아니라 **함수
|
||||
경계로 강제**되므로 `RC-1`/`RC-3`/`RC-4` 같은 "줄 순서를 잘못 잡아서"
|
||||
나던 버그 클래스가 구조적으로 사라짐. 근거 기록은
|
||||
`research/slot-attach-decomposition.md`.
|
||||
`reference/slot-attach-decomposition.md`.
|
||||
**관측 가능한 변화 하나**: `Parent` 대입 순서는 그대로지만 물리 마운트가
|
||||
"부기 완료 후 일괄"이 되어, `ChildAdded` 핸들러가 볼 때 서브트리 전체의
|
||||
`Length`/`Offset`이 이미 최종값이다(옛 코드는 미완성 스냅샷을 보여줬음).
|
||||
|
|
@ -736,9 +758,13 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
- [ ] `:Apply(factory)` 팩토리 함수 체이닝(`modifier-plan.md` 8번, 예약 키
|
||||
`Apply`가 제네릭 `__index` 필드 setter와 안 겹치는지 확인)
|
||||
- [ ] `:Peek<<T>>(key): T|State<T>|nil` 필드 읽기 접근자 +
|
||||
`isState(x)`/`isSource(x): boolean`(`Brand` 공유 레지스트리 기반 —
|
||||
`modifier-plan.md` 9번, `brand-plan.md`의 `Brand` 절, M2의
|
||||
`Brand.luau`에 이미 구현돼 있어야 함)
|
||||
`isState(x)`/`isSource(x): boolean`(**[2026-08-21 갱신]** 인스턴스
|
||||
브랜드 멤버십 기반 — `isSource(x)`는 `SourceBrand:is(x)`, `isState`는
|
||||
그 위에 `StateBrand:is(x)`를 OR로 얹은 상위 개념. 옛 "공유 레지스트리 +
|
||||
`Brand.get(x) == SourceTag`" 서술은 역전됨,
|
||||
`archive/brand-shared-registry-reversed.md`. `modifier-plan.md` 9번,
|
||||
`brand-plan.md`의 "`isX` wrapper" 절, M2의 `Brand.luau`에 이미
|
||||
구현돼 있어야 함)
|
||||
- [ ] 인라인 키/setter로 modifier 필드를 명시적으로 지우는 `None` 센티널
|
||||
(이름 확정, `modifier-plan.md` 2-1번, `Peek` 반환 타입에 `None` 추가) +
|
||||
이를 `nil`로 재디스패치하는 base 내장 `NoneHandler`
|
||||
|
|
@ -970,7 +996,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
재작성), 구 모델은 `archive/tween-special-bind-key-reversed.md`.
|
||||
|
||||
- [ ] `quad-base/Tween.luau`(값 타입만 — `Tween(opts)` 팩토리, `isTween`/
|
||||
`TweenTag` Brand, `Value: T` plain만 받고 State 재귀 없음)
|
||||
`TweenBrand`, `Value: T` plain만 받고 State 재귀 없음)
|
||||
- [ ] `Handlers/Property.luau`에 `isTween(realv)` 분기 추가(기존
|
||||
`Handlers/Tween.luau` 독립 핸들러는 폐기) + 3-상태 릴레이션 슬롯
|
||||
(`RobloxTween | true | nil` — `nil`=첫 세팅, `true`=세팅됨/트윈
|
||||
|
|
|
|||
Loading…
Reference in a new issue