design: State 에포크 안 3차 정정 — rawInvalid 기제 반전, seen/computedAt 분리 철회
사용자가 research/state-epoch-validation.md를 직접 읽고 기제 서술 세 건을
정정. 채택 여부 자체는 여전히 미정(M3 전 결론 필요).
- sourceList 순회 조건이 반대였다: rawInvalid == false일 때만 돈다.
true면 재계산이 이미 확정이라 훑을 이유가 없고, 순회의 목적은 오직
"못 받은 emit(게이트에 막혔던 것)을 여기서 먼저 받는 것".
- emit은 (source, count)가 아니라 발행 source만 싣는다 — 받는 쪽이
source의 count 필드를 그냥 읽으면 된다.
- 에이전트가 요구했던 seen/computedAt 두 카운트 분리는 철회. count 갱신과
rawInvalid = true가 같은 스텝이라 캐시 오인 경로가 없다.
- emit 수신 규약 확정형: 같으면 삼킴 / 다르면 count 먼저 갱신 →
rawInvalid = true → 그 다음 뒤로 emit. 다른 소스 항목은 안 건드림.
- 부수로 열린 것: 순회가 발견한 변경을 뒤로 emit 할 것인가. 다이아몬드
쪽은 사용자가 스스로 안전으로 정정(D도 count를 갱신해 중복을 삼킴),
게이트 쪽만 "해제 emit이 source = nil을 싣고 받는 쪽이 전체 확인"
규약으로 남음 — 게이트는 보통 최종단이라 채택을 막지 않는다는 판단.
- 비용 서술도 뒤집었고(훑는 쪽이 흔한 경로), question.md에 남아 있던
이미 뒤집힌 옛 결론("중복 통지는 안 고쳐짐 / UB 명문화 필요")도 정정.
처리 기록은 round5-followup.md M절. 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
a4b517aee2
commit
4c395cd383
6 changed files with 135 additions and 32 deletions
|
|
@ -93,7 +93,7 @@
|
||||||
| `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] 결론 확정·반영 완료**, 이제 근거 기록용 |
|
| `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가 의도적으로 비지원하기로 한 바로 그것)이 확정 전 최대 쟁점 |
|
| `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가 의도적으로 비지원하기로 한 바로 그것)이 확정 전 최대 쟁점 |
|
||||||
| `gate-primitive.md` | **[2026-08-21 신설]** `Blocker`가 쓰는 게이티드 State 노드를 공용 `Gate`로 일반화 — 상류 emit을 가로채 내려보낼지 정책이 정하는 노드. `Blocker`/`Debounce`/`Throttle`이 그 위의 정책이 됨. **방향은 사용자 확정(게이팅을 M2로 앞당김), 이름(`Gate` vs `Gater`)·표면·`Blocker`와의 결합 방식이 미정** | **상 — M2 착수 전 필요**(`Dispatch.drive`의 배치 등록이 이 게이팅을 전제) |
|
| `gate-primitive.md` | **[2026-08-21 신설]** `Blocker`가 쓰는 게이티드 State 노드를 공용 `Gate`로 일반화 — 상류 emit을 가로채 내려보낼지 정책이 정하는 노드. `Blocker`/`Debounce`/`Throttle`이 그 위의 정책이 됨. **방향은 사용자 확정(게이팅을 M2로 앞당김), 이름(`Gate` vs `Gater`)·표면·`Blocker`와의 결합 방식이 미정** | **상 — M2 착수 전 필요**(`Dispatch.drive`의 배치 등록이 이 게이팅을 전제) |
|
||||||
| `state-epoch-validation.md` | **[2026-08-21 신설]** State 재계산 판정을 `invalid` 플래그에서 **루트 Source 에포크 비교**로 바꾸는 안(사용자 제안) — DFS 전파 도중 `Get()`이 섞인 값을 캐시하는 glitch를 없앰. 성능이 아니라 **정확성** 결정이고 State 내부 표현을 바꾸므로 M3 전에 결론 필요. 에이전트 분석: 진단·방향 모두 타당. **[같은 날 후속 확정] 중복 *통지*도 같은 장치로 접고**(emit이 `(source, count)`를 실어오므로 판정 O(1)), "선언 안 된 의존성 UB 명문화"는 사용자 기각으로 빠짐 — 대신 노드가 `seen`(전파 dedup)/`computedAt`(캐시 검증) 두 카운트를 따로 들어야 한다는 구현 요구가 생김 | **상 — M3(State 구현) 착수 전 필요** |
|
| `state-epoch-validation.md` | **[2026-08-21 신설]** State 재계산 판정을 `invalid` 플래그에서 **루트 Source 에포크 비교**로 바꾸는 안(사용자 제안) — DFS 전파 도중 `Get()`이 섞인 값을 캐시하는 glitch를 없앰. 성능이 아니라 **정확성** 결정이고 State 내부 표현을 바꾸므로 M3 전에 결론 필요. 에이전트 분석: 진단·방향 모두 타당. **[같은 날 후속 확정] 중복 *통지*도 같은 장치로 접고**(emit이 발행 source를 실어오므로 판정 O(1)), "선언 안 된 의존성 UB 명문화"는 사용자 기각으로 빠짐. **[같은 날 3차 정정]** `rawInvalid` 기제가 뒤집혔다 — `sourceList` 순회는 `rawInvalid`가 **false**일 때만 돌고(목적은 "못 받은 emit 받기"), emit은 count 없이 source만 싣는다. 그래서 `seen`/`computedAt` 두 카운트 분리 요구는 **철회**되고 count 하나면 충분. 남은 세부는 게이트 해제 emit의 `source = nil` 규약 | **상 — M3(State 구현) 착수 전 필요** |
|
||||||
| `quad-recursive-acronym.md` | **[2026-08-14 신설]** GNU/WINE류로 `Quad`를 재귀 약어화하는 카피 브레인스토밍 — 설계 결정도 착수 게이팅도 아니고 나중에 README.md 헤딩 등에 쓸 캐치프레이즈 후보 모음. 자학 개그 방향(기각)과 지연평가/재귀·커링/펑터/클로저를 자랑하는 방향(채택 후보, 미확정) 정리 | 하 — 카피 소재, 설계 상의 필요 없음. 사용자가 최종 문구 고르면 반영 |
|
| `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 코어 구현 시점까지 미결 |
|
| `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 코어 구현 시점까지 미결 |
|
||||||
| `doc-include-plan.md` | **[2026-08-14 신설]** 문서 stale 감소용 include 도구 `doc-include.py`(가칭) — 원본 파일에 `<!--#summary-->` 류 마커로 요약 구간을 표시해두면 인용하는 문서가 그 구간을 기계적으로 추출해 붙여넣게 하는 도구. `doc-check.py`(사후 탐지)와 짝을 이루는 사전 차단 장치. AsciiDoc tagged include/markdown-magic이 선례, build vs buy 검토 후 Python 표준 라이브러리로 직접 제작(~100줄) 채택. 파일럿은 `.claude/session-summary.md` ← `.claude/session/*.md` 요약 마커부터(CLAUDE.md 분할로 목적지가 "통째로 생성되는 파일"이 돼 단방향 생성으로 단순화됨) | 하 — M0/설계 게이트와 무관한 메타 도구. **[2026-08-16 기준]** 플랜 초안 단계, 열린 질문 미해소(소스: 이 문서의 "열린 질문" 절) |
|
| `doc-include-plan.md` | **[2026-08-14 신설]** 문서 stale 감소용 include 도구 `doc-include.py`(가칭) — 원본 파일에 `<!--#summary-->` 류 마커로 요약 구간을 표시해두면 인용하는 문서가 그 구간을 기계적으로 추출해 붙여넣게 하는 도구. `doc-check.py`(사후 탐지)와 짝을 이루는 사전 차단 장치. AsciiDoc tagged include/markdown-magic이 선례, build vs buy 검토 후 Python 표준 라이브러리로 직접 제작(~100줄) 채택. 파일럿은 `.claude/session-summary.md` ← `.claude/session/*.md` 요약 마커부터(CLAUDE.md 분할로 목적지가 "통째로 생성되는 파일"이 돼 단방향 생성으로 단순화됨) | 하 — M0/설계 게이트와 무관한 메타 도구. **[2026-08-16 기준]** 플랜 초안 단계, 열린 질문 미해소(소스: 이 문서의 "열린 질문" 절) |
|
||||||
|
|
|
||||||
|
|
@ -876,3 +876,32 @@ nativeDispose(element) -- 트리 밖
|
||||||
`rawDetach`/`mountSlotTree`/`unmountSlotTree` 의사코드), `dispatch-core-plan.md`
|
`rawDetach`/`mountSlotTree`/`unmountSlotTree` 의사코드), `dispatch-core-plan.md`
|
||||||
(C-7 재정의 + 주입 op 목록), `architecture.md`(`EngineOps.luau`), `ROADMAP.md`(M5
|
(C-7 재정의 + 주입 op 목록), `architecture.md`(`EngineOps.luau`), `ROADMAP.md`(M5
|
||||||
주입 표면), `archive/`(역전 원문), `README.md` 색인.
|
주입 표면), `archive/`(역전 원문), `README.md` 색인.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# M절 — State 에포크 안: 사용자 3차 정정 (2026-08-21)
|
||||||
|
|
||||||
|
`research/state-epoch-validation.md`를 사용자가 직접 읽고 **기제 서술의
|
||||||
|
오류 세 건**을 정정했다. **채택 여부 자체는 여전히 미정**이지만, 채택하면
|
||||||
|
어떤 모양이 되는지는 이제 거의 확정형이다. 반영은 그 문서(§2가 소스) +
|
||||||
|
`README.md`/`question.md`/`ROADMAP.md` 인덱스.
|
||||||
|
|
||||||
|
| # | 문서가 적고 있던 것 | 사용자 정정 |
|
||||||
|
|---|---|---|
|
||||||
|
| M-1 | `sourceList` 순회는 **`rawInvalid == true`일 때만** 돈다 | **반대.** `rawInvalid == false`일 때만 돈다. `true`면 이미 재계산이 확정이라 훑을 이유가 없고, 순회의 목적은 오직 **"못 받은 emit을 여기서 먼저 받는 것"**이다 |
|
||||||
|
| M-2 | `emit`은 `(source, count)`를 실어 보낸다 | **source만** 보내면 된다 — 받는 쪽이 `source`의 count 필드를 그냥 읽는다 |
|
||||||
|
| M-3 | 노드가 `seen`(전파 dedup)/`computedAt`(캐시 검증) **두 카운트**를 따로 들어야 한다 | **철회.** count 갱신과 `rawInvalid = true`가 **같은 스텝**에서 일어나므로 "전파 때 갱신된 count가 캐시를 신선한 것으로 오인시키는" 실패 모드가 없다. 캐시 유효성은 `rawInvalid`가 들고 count는 "이 에포크를 봤는가"만 답한다 — *"그냥 지금 순수 count 와 source 를 ref해두는 구현은 문제가 없어보인다"* |
|
||||||
|
|
||||||
|
**emit 수신 규약(확정형)**: `sourceList[source] == source.count`면 삼키고,
|
||||||
|
다르면 **count를 먼저 갱신** → `rawInvalid = true` → **그 다음** 뒤로 emit.
|
||||||
|
다른 소스 항목은 건드리지 않는다(그 소스는 자기가 직접 emit 하므로).
|
||||||
|
|
||||||
|
**부수로 열린 채 남은 것 — 순회로 발견한 변경을 뒤로 emit 할 것인가.**
|
||||||
|
사용자가 `A → {B, C} → D` 다이아몬드로 (b) 즉시 emit의 위험을 의심했다가
|
||||||
|
**스스로 안전하다고 정정**했다(*"D조차도 count를 업데이트 하기 때문에 중복을
|
||||||
|
무시한다"*). **[2026-08-21 기준]** 그래서 걸리는 곳은 게이트 하나 — 앞에서 막아둔 emit이 뒤의 `Get()`이 촉발한
|
||||||
|
순회로 새어나갈 수 있으므로 **block이 풀린 뒤의 emit까지 기다려야** 하고,
|
||||||
|
게이트 해제 emit은 여러 소스가 섞여 있어 **`source = nil`을 싣고 받는 쪽이
|
||||||
|
전체를 확인**하는 규약이 필요하다. 다만 사용자 판단은 *"중간에 blocker 낀건
|
||||||
|
이전에도 있던 문제다. 이미 최종장에 쓰는게 일반적이므로 의미 없어보인다"* —
|
||||||
|
**채택을 막는 요소가 아니고**, `nil` 규약만 `Gate` 설계와 같이 확정하면 된다.
|
||||||
|
|
|
||||||
|
|
@ -221,10 +221,18 @@
|
||||||
`Source`들의 카운트를 들고 있다가 `Get()` 때 비교해 재계산 여부를 정한다.
|
`Source`들의 카운트를 들고 있다가 `Get()` 때 비교해 재계산 여부를 정한다.
|
||||||
동기는 성능이 아니라 **정확성** — DFS 전파 도중 Observer가 `Get()`을 부르면
|
동기는 성능이 아니라 **정확성** — DFS 전파 도중 Observer가 `Get()`을 부르면
|
||||||
아직 신호를 못 받은 다른 가지의 옛 캐시가 섞여 들어가는 glitch가 지금 모델에
|
아직 신호를 못 받은 다른 가지의 옛 캐시가 섞여 들어가는 glitch가 지금 모델에
|
||||||
실재한다. 에이전트 분석 결과 진단·방향 모두 타당하고 선례도 있으나
|
실재한다. 에이전트 분석 결과 진단·방향 모두 타당하고 선례도 있다
|
||||||
(MobX/Adapton류 버전 검증), **중복 *통지*는 안 고쳐지고 "선언 안 된 의존성"을
|
(MobX/Adapton류 버전 검증). **[2026-08-21 갱신 — 여기 있던 "중복 통지는 안
|
||||||
UB로 명문화해야 한다.** State 내부 표현을 바꾸는 결정이라 M3 뒤로 미루면
|
고쳐지고 선언 안 된 의존성을 UB로 명문화해야 한다"는 서술은 같은 날 둘 다
|
||||||
되돌리는 비용이 크다. 상세는 `research/state-epoch-validation.md`.
|
뒤집혔다]**: 중복 통지도 **같은 장치로 접고**, UB 조항은 사용자 기각으로
|
||||||
|
빠졌다. 이어진 3차 정정으로 기제도 확정형에 가까워졌다 — `sourceList` 순회는
|
||||||
|
`rawInvalid`가 **false**일 때만 돌고(목적은 "못 받은 emit 받기"), emit은
|
||||||
|
count 없이 **발행 source만** 싣고, 카운트는 **하나면 충분**하다. 사용자
|
||||||
|
정리: *"그냥 지금 순수 count 와 source 를 ref해두는 구현은 문제가
|
||||||
|
없어보인다."* **남은 판단은 (a) 채택 여부 자체와 (b) 게이트 해제 emit이
|
||||||
|
`source = nil`을 실어 "전체 확인"을 시키는 규약**이고, State 내부 표현을
|
||||||
|
바꾸는 결정이라 M3 뒤로 미루면 되돌리는 비용이 크다. 상세는
|
||||||
|
`research/state-epoch-validation.md`.
|
||||||
- **[해소, 2026-08-21 구현 전 QA 5라운드 `C-4`] `Dispatch.setLength`의 Observer
|
- **[해소, 2026-08-21 구현 전 QA 5라운드 `C-4`] `Dispatch.setLength`의 Observer
|
||||||
앵커 — 물리 target으로 확정(4라운드 `D-56` 역전).** `setLength`가
|
앵커 — 물리 target으로 확정(4라운드 `D-56` 역전).** `setLength`가
|
||||||
`(ownerKey, i, len, anchor)`로 4번째 인자를 받아 **부기 키와 생명주기 앵커를
|
`(ownerKey, i, len, anchor)`로 4번째 인자를 받아 **부기 키와 생명주기 앵커를
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# State 재계산 판정을 "소스 에포크" 비교로 바꾸는 안 (2026-08-21 신설)
|
# State 재계산 판정을 "소스 에포크" 비교로 바꾸는 안 (2026-08-21 신설)
|
||||||
|
|
||||||
**상태**: research — **사용자 제안, 에이전트 분석 완료, 세부 두 건은 2026-08-21에 추가 확정(중복 통지도 접음 / 선언 안 된 의존성 조항 기각). 채택 자체는 여전히 미정.**
|
**상태**: research — **사용자 제안, 에이전트 분석 완료. 2026-08-21에 세부가 세 차례 갱신됐다(중복 통지도 접음 / 선언 안 된 의존성 조항 기각 / `rawInvalid` 기제와 `emit` 인자 정정 + `seen`·`computedAt` 분리 철회). 채택 자체는 여전히 미정.**
|
||||||
구현 전 QA 5라운드(`SS-2`/`SS-3`)에서 나왔고 사용자 스스로 *"더 생각해볼
|
구현 전 QA 5라운드(`SS-2`/`SS-3`)에서 나왔고 사용자 스스로 *"더 생각해볼
|
||||||
이야기라 백로깅이나 리서치에 들어가야할듯"*이라 함. 회신 원문은
|
이야기라 백로깅이나 리서치에 들어가야할듯"*이라 함. 회신 원문은
|
||||||
`qa-request/pre-implementation-qa-round5-response.md`.
|
`qa-request/pre-implementation-qa-round5-response.md`.
|
||||||
|
|
@ -34,25 +34,52 @@ A ──> B ──┐
|
||||||
시점"이 전파 파동이 끝난 뒤*라고 암묵적으로 가정한다 — Observer가 전파 도중에
|
시점"이 전파 파동이 끝난 뒤*라고 암묵적으로 가정한다 — Observer가 전파 도중에
|
||||||
발화한다는 사실과 겹쳐 읽으면 그 가정이 깨진다.
|
발화한다는 사실과 겹쳐 읽으면 그 가정이 깨진다.
|
||||||
|
|
||||||
## 2. 사용자 제안 (최종 정리형)
|
## 2. 사용자 제안 (최종 정리형) — **[2026-08-21 정정본]**
|
||||||
|
|
||||||
각 State가 **자기에게 영향을 주는 루트 `Source`들의 에포크**를 들고 있다가,
|
각 State가 **자기에게 영향을 주는 루트 `Source`들의 에포크(count)** 를 들고
|
||||||
`Get()` 때 그것과 실제 Source의 현재 에포크를 비교해 재계산 여부를 정한다.
|
있다가, 그것과 실제 Source의 현재 count를 비교해 재계산/전파 여부를 정한다.
|
||||||
|
|
||||||
```
|
```
|
||||||
State
|
State
|
||||||
sourceList : { [source (weak key)] : count } -- 상류에서 복사, :With에서 합침
|
sourceList : { [source (weak key)] : count }
|
||||||
rawInvalid : boolean -- emit이 오면 그냥 true(값싼 캐시)
|
-- "이 소스의 이 에포크까지는 이미 봤다". 상류에서 복사, :With에서 합침
|
||||||
invalid : rawInvalid가 true일 때만 sourceList를 훑어 "정말 계산이 필요한가" 판정
|
rawInvalid : boolean
|
||||||
|
-- "재계산이 필요하다"는 확정 플래그. emit이 실제로 통과하면 켜진다
|
||||||
```
|
```
|
||||||
|
|
||||||
- `emit`은 **발행한 source와 그 count를 같이 전달**한다(값은 여전히 안 싣는다).
|
|
||||||
- `Source:Set()`/`:Emit()`은 자기 count를 증가시킨다.
|
- `Source:Set()`/`:Emit()`은 자기 count를 증가시킨다.
|
||||||
- 캐시를 채울 때 그 시점의 각 source count를 같이 기록해두고, `Get()` 때
|
- **`emit`은 발행한 source만 전달한다**(값도, count도 안 싣는다) — 받는 쪽이
|
||||||
하나라도 다르면 재계산한다.
|
그 source의 count 필드를 그냥 읽으면 된다. **[2026-08-21 사용자 정정]**
|
||||||
|
앞선 정리는 `(source, count)` 쌍을 실어 보낸다고 적었는데 불필요하다.
|
||||||
|
- **emit을 받았을 때** — 판정은 O(1)이다, 그 소스 항목 하나만 본다:
|
||||||
|
1. `sourceList[source] == source.count` → **삼킨다.** 이 에포크는 이미 다른
|
||||||
|
경로로 봤다는 뜻이므로 뒤로 안 내려보낸다.
|
||||||
|
2. 다르면 → **`sourceList[source]`를 먼저 갱신**하고 `rawInvalid = true`를
|
||||||
|
둔 다음, **그러고 나서** 뒤로 emit 한다.
|
||||||
|
- **다른 소스 항목은 건드리지 않는다** — 그 소스들은 자기가 직접 emit 하므로.
|
||||||
|
- **재계산 판정**:
|
||||||
|
- `rawInvalid == true` → 그냥 재계산한다. `sourceList`를 훑을 이유가 없다
|
||||||
|
(이미 확정이라 더 알아낼 게 없다).
|
||||||
|
- `rawInvalid == false` → **그때만 `sourceList`를 훑는다.** 목적은 하나뿐 —
|
||||||
|
**못 받은 emit을 여기서 먼저 받아주는 것**(중간 게이트에 막혀 있었다거나).
|
||||||
|
다른 항목이 발견되면 count를 갱신하고 `rawInvalid = true`로 만든다.
|
||||||
|
- **[2026-08-21 사용자 정정]** 앞선 정리는 이 순회 조건을 `rawInvalid == true`
|
||||||
|
일 때로 적었는데 **반대**다.
|
||||||
|
- 재계산이 끝나면 `rawInvalid = false`. count는 emit을 통과시킬 때 이미
|
||||||
|
갱신돼 있으므로 따로 손댈 게 없다.
|
||||||
- count가 싫다면 `[source] -> {}` 처럼 **유니크 테이블 identity**로 같은 판정이
|
- count가 싫다면 `[source] -> {}` 처럼 **유니크 테이블 identity**로 같은 판정이
|
||||||
가능하다(사용자 대안).
|
가능하다(사용자 대안).
|
||||||
|
|
||||||
|
**⭐ [2026-08-21] 그래서 카운트는 하나면 된다 — `seen`/`computedAt` 분리는
|
||||||
|
철회.** 앞선 정리는 노드가 전파 dedup용 `seen`과 캐시 검증용 `computedAt`을
|
||||||
|
따로 들어야 한다고 적었으나, 사용자 정정으로 **불필요**하다: count 갱신과
|
||||||
|
`rawInvalid = true`가 **같은 스텝에서 같이 일어나기** 때문에 "전파 시점에
|
||||||
|
갱신된 count가 캐시를 신선한 것으로 오인하게 만드는" 실패 모드 자체가
|
||||||
|
생기지 않는다. 캐시 유효성은 count가 아니라 `rawInvalid`가 들고, count는
|
||||||
|
순수하게 **"이 에포크를 봤는가"** 만 답한다. 사용자 정리: *"그냥 지금 순수
|
||||||
|
count 와 source 를 ref해두는 구현은 문제가 없어보인다. 단순 rawInvalid 가
|
||||||
|
약간 이상하게 사용/기제된 부분만 보일 뿐임."*
|
||||||
|
|
||||||
## 3. 에이전트 분석 — 이게 실제로 무엇을 고치는가
|
## 3. 에이전트 분석 — 이게 실제로 무엇을 고치는가
|
||||||
|
|
||||||
**결론부터: 문제 진단도 해법 방향도 맞다. 그리고 이건 성능 최적화가 아니라
|
**결론부터: 문제 진단도 해법 방향도 맞다. 그리고 이건 성능 최적화가 아니라
|
||||||
|
|
@ -71,19 +98,19 @@ State
|
||||||
이미 모든 소스가 최신이면 무시하는게 맞아보인다. 특히 emit 은 자신 소스를
|
이미 모든 소스가 최신이면 무시하는게 맞아보인다. 특히 emit 은 자신 소스를
|
||||||
주게 되므로, 자신 소스 카운트만 빠르게 비교가 쉽다. 우리에게 중요한 점은,
|
주게 되므로, 자신 소스 카운트만 빠르게 비교가 쉽다. 우리에게 중요한 점은,
|
||||||
앞단의 변경이 뒤로 흘렀냐일 뿐이라서 해당 방식을 택하는데 있어 문제가 없다."*
|
앞단의 변경이 뒤로 흘렀냐일 뿐이라서 해당 방식을 택하는데 있어 문제가 없다."*
|
||||||
- **판정은 O(1)이다** — emit이 `(source, count)`를 실어 오므로 **그 소스
|
- **판정은 O(1)이다** — emit이 **어느 source가 발행했는지**를 실어 오므로
|
||||||
하나만** 비교하면 된다(전체 목록 순회가 아님).
|
**그 소스 하나만** 비교하면 된다(전체 목록 순회가 아님).
|
||||||
- **⚠️ 이건 2026-08-14에 뒤집힌 옛 dedup과 다른 장치다.** 그때 폐기된 건
|
- **⚠️ 이건 2026-08-14에 뒤집힌 옛 dedup과 다른 장치다.** 그때 폐기된 건
|
||||||
*"이미 `invalid`면 더 안 내려보낸다"*로, `:Get()`을 안 부르는 Observer가
|
*"이미 `invalid`면 더 안 내려보낸다"*로, `:Get()`을 안 부르는 Observer가
|
||||||
**한 번 울고 영구히 침묵**하는 실패 모드가 있었다
|
**한 번 울고 영구히 침묵**하는 실패 모드가 있었다
|
||||||
(`archive/invalidate-dedup-propagation-reversed.md`). 에포크 비교는 그
|
(`archive/invalidate-dedup-propagation-reversed.md`). 에포크 비교는 그
|
||||||
모드가 없다 — 매 `Set`마다 카운트가 **새 값**이라 항상 통과하고, 접히는 건
|
모드가 없다 — 매 `Set`마다 카운트가 **새 값**이라 항상 통과하고, 접히는 건
|
||||||
**같은 에포크가 두 경로로 도착한 두 번째**뿐이다.
|
**같은 에포크가 두 경로로 도착한 두 번째**뿐이다.
|
||||||
- **그래서 노드가 기록해야 하는 카운트가 둘이다**: 전파 dedup용
|
- **⚠️ [2026-08-21 철회] 여기 있던 "카운트를 `seen`/`computedAt` 둘로
|
||||||
"이 소스의 이 에포크를 이미 내려보냈다"(`seen`)와, 캐시 검증용 "이 값을
|
분리하라"는 요구는 사용자 정정으로 없어졌다** — §2 마지막 문단이 소스.
|
||||||
계산할 때의 에포크"(`computedAt`). 하나로 합치면 전파 시점에 갱신되는
|
캐시 유효성을 `rawInvalid`가 들고 있고 count 갱신이 `rawInvalid = true`와
|
||||||
쪽이 캐시를 신선한 것으로 오인하게 만든다 — **구현 시 반드시 분리할 것.**
|
같은 스텝이라, 하나로 합쳐도 캐시를 신선한 것으로 오인하는 경로가 없다.
|
||||||
- 비교는 `incoming <= seen[source]`면 삼킨다(단조 증가라 안전).
|
- 비교는 `sourceList[source] == source.count`면 삼킨다(단조 증가라 안전).
|
||||||
- **선례가 있다** — 값 자체를 비교하는 게 아니라 **버전/에포크를 비교해
|
- **선례가 있다** — 값 자체를 비교하는 게 아니라 **버전/에포크를 비교해
|
||||||
lazy하게 검증**하는 건 MobX(global state version + observing 검사),
|
lazy하게 검증**하는 건 MobX(global state version + observing 검사),
|
||||||
Adapton류 incremental computing, "reactively" 계열 라이브러리가 쓰는 표준
|
Adapton류 incremental computing, "reactively" 계열 라이브러리가 쓰는 표준
|
||||||
|
|
@ -99,8 +126,11 @@ State
|
||||||
- `sourceList`의 크기는 **그 노드 상류에 있는 서로 다른 루트 Source의 수**다.
|
- `sourceList`의 크기는 **그 노드 상류에 있는 서로 다른 루트 Source의 수**다.
|
||||||
체인이 길어져도 안 늘고, `:With`로 합류할 때만 는다. UI 파생값에서 이 수가
|
체인이 길어져도 안 늘고, `:With`로 합류할 때만 는다. UI 파생값에서 이 수가
|
||||||
큰 경우는 드물다.
|
큰 경우는 드물다.
|
||||||
- `rawInvalid`가 false면 훑지도 않으므로, **정말 안 바뀐 흔한 경로는 지금과
|
- **[2026-08-21 정정]** 순회 조건이 뒤집혔으므로 비용 구도도 뒤집힌다 —
|
||||||
같은 비용**(플래그 하나)이다.
|
**훑는 쪽이 흔한 경로**(`rawInvalid == false`, 즉 "안 바뀐 것 같다")다.
|
||||||
|
그래도 훑는 대상이 위 크기(2~4)라 상수 시간에 가깝고, 정작 비싼
|
||||||
|
**재계산은 안 도는** 경로다. 반대로 `rawInvalid == true`면 순회를 아예
|
||||||
|
건너뛰고 바로 재계산한다.
|
||||||
|
|
||||||
## 5. 열린 질문 (채택 전에 답이 필요)
|
## 5. 열린 질문 (채택 전에 답이 필요)
|
||||||
|
|
||||||
|
|
@ -114,10 +144,32 @@ State
|
||||||
2. **동적 의존성.** 조건에 따라 다른 상류를 읽는 계산이면 `sourceList`가
|
2. **동적 의존성.** 조건에 따라 다른 상류를 읽는 계산이면 `sourceList`가
|
||||||
보수적 상위집합이 된다 — 틀리진 않고 재계산이 조금 더 잦아질 뿐이다.
|
보수적 상위집합이 된다 — 틀리진 않고 재계산이 조금 더 잦아질 뿐이다.
|
||||||
그대로 감수할지 확인.
|
그대로 감수할지 확인.
|
||||||
3. **`Blocker`/`Gate`와의 상호작용.** 게이트는 **통지만** 막고 `Get()`엔 영향이
|
3. **⭐ [2026-08-21 확장] 순회로 발견한 변경을 뒤로 emit 할 것인가 —
|
||||||
없다는 게 확정 계약이므로(`base/blocker-plan.md`), 게이트 아래에서 `Get()`하면
|
그리고 `Blocker`/`Gate`와의 상호작용.**
|
||||||
에포크 비교가 "갱신 필요"로 판정해 **막아둔 값이 그대로 보인다**. 지금도
|
`rawInvalid == false`에서 순회를 돌다 "못 받은 emit"을 발견해
|
||||||
같은 동작이지만, 새 모델에선 그게 더 또렷해지므로 문서에 명시할 것.
|
`rawInvalid = true`가 됐을 때, **그 자리에서 뒤로 emit 하는가**가 열려 있다.
|
||||||
|
두 안:
|
||||||
|
- **(a) 지연** — 아무것도 안 내려보내고, 나중에 그 소스의 진짜 emit이
|
||||||
|
도착할 때 정상 경로로 처리한다.
|
||||||
|
- **(b) 즉시** — 재계산하라는 지시를 받았고 invalid로 판정됐으니 그때 emit.
|
||||||
|
|
||||||
|
사용자가 (b)의 위험을 먼저 의심했다가 **스스로 안전하다고 정정**했다:
|
||||||
|
`A → {B, C} → D`에서 `B`가 `C`를 요구하다 `C`의 변경분이 흘러 `D`가 다시
|
||||||
|
깨어나는 것처럼 보이지만, **`D`도 count를 갱신하므로 중복을 삼킨다** —
|
||||||
|
*"안전히 C가 emit 되어도 되는 상황"*.
|
||||||
|
|
||||||
|
남는 건 게이트 쪽이다. 게이트가 **앞에서 막아둔 emit**을, 뒤에서 `Get()`이
|
||||||
|
촉발한 순회가 그대로 흘려보낼 수 있다(*"앞에서 막아뒀는데, 뒤에서 필요하다고
|
||||||
|
해서 그대로 흘러버릴 가능성"*). 그래서 (b)를 택하더라도 **실제 block이 풀린
|
||||||
|
뒤의 emit까지는 기다려야 한다**. 그리고 **게이트가 풀 때 내보내는 emit은
|
||||||
|
여러 소스가 섞인 것**이라 어느 소스가 바뀌었는지 지목할 수 없다 — 사용자
|
||||||
|
제안은 그 emit이 **`source = nil`을 싣고, `nil`이면 받는 쪽이 전체 목록을
|
||||||
|
확인**하는 것. 게이트는 `Get()`은 막지 않고 emit만 유보하므로 이게 성립한다.
|
||||||
|
|
||||||
|
**다만 사용자 판단은 "중간에 게이트가 끼는 건 원래 있던 문제이고, 게이트는
|
||||||
|
보통 최종단에 쓰므로 실질적으로 의미 없어 보인다"**로 정리됐다 — 즉 이
|
||||||
|
항목은 **채택을 막는 요소가 아니고**, `nil` emit 규약만 `Gate` 설계
|
||||||
|
(`research/gate-primitive.md`)와 같이 확정하면 된다.
|
||||||
4. **`:Emit()`(값을 제자리에서 mutate하고 알리는 경로)** — count만 올리면 되므로
|
4. **`:Emit()`(값을 제자리에서 mutate하고 알리는 경로)** — count만 올리면 되므로
|
||||||
그대로 동작한다. 확인만.
|
그대로 동작한다. 확인만.
|
||||||
5. **weak 키.** `[source] -> count`를 weak-key로 두면 source가 죽을 때 항목이
|
5. **weak 키.** `[source] -> count`를 weak-key로 두면 source가 죽을 때 항목이
|
||||||
|
|
@ -146,8 +198,9 @@ Ref<T> 로 변환해주는 등"*. 즉 "항상 최신 값을 필드로 들고 있
|
||||||
- **[2026-08-21] 에이전트가 붙였던 조건(선언 안 된 의존성을 UB로 명문화)은
|
- **[2026-08-21] 에이전트가 붙였던 조건(선언 안 된 의존성을 UB로 명문화)은
|
||||||
사용자 기각으로 빠졌다** — §5의 1번 참고.
|
사용자 기각으로 빠졌다** — §5의 1번 참고.
|
||||||
- **채택 시 같이 확정되는 것: 중복 통지도 접는다**(§3) — 그러면 다이아몬드에서
|
- **채택 시 같이 확정되는 것: 중복 통지도 접는다**(§3) — 그러면 다이아몬드에서
|
||||||
값도 통지도 한 번씩만 간다. 대신 노드가 `seen`/`computedAt` 두 카운트를
|
값도 통지도 한 번씩만 간다. **[2026-08-21 정정]** 여기 있던 "노드가
|
||||||
들어야 한다는 게 구현 요구사항으로 추가된다.
|
`seen`/`computedAt` 두 카운트를 들어야 한다"는 추가 요구는 **철회**됐고,
|
||||||
|
대신 확정할 것은 **게이트 해제 emit의 `source = nil` 규약**(§5의 3번)이다.
|
||||||
- 실측은 `luau-test`에 스파이크 하나면 충분하다 — 위 다이아몬드를 그대로 짜서
|
- 실측은 `luau-test`에 스파이크 하나면 충분하다 — 위 다이아몬드를 그대로 짜서
|
||||||
(a) 지금 모델에서 섞인 값이 실제로 관측되는지, (b) 에포크 비교를 넣으면
|
(a) 지금 모델에서 섞인 값이 실제로 관측되는지, (b) 에포크 비교를 넣으면
|
||||||
사라지는지 대조.
|
사라지는지 대조.
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,19 @@
|
||||||
- `qa-request/pre-implementation-qa-round5.md` — 문항지(205문항)
|
- `qa-request/pre-implementation-qa-round5.md` — 문항지(205문항)
|
||||||
- `qa-request/pre-implementation-qa-round5-response.md` — 회신 원문
|
- `qa-request/pre-implementation-qa-round5-response.md` — 회신 원문
|
||||||
- `qa-request/pre-implementation-qa-round5-followup.md` — **처리 결과의 소스**
|
- `qa-request/pre-implementation-qa-round5-followup.md` — **처리 결과의 소스**
|
||||||
(A~E절 1차, **F절이 최신**)
|
(절이 라운드마다 쌓임 — **마지막 절이 최신**)
|
||||||
- `archive/bindlifetime-slot-owner-reversed.md` — 역전된 `D-56` 원문
|
- `archive/bindlifetime-slot-owner-reversed.md` — 역전된 `D-56` 원문
|
||||||
- `research/gate-primitive.md`, `research/state-epoch-validation.md`
|
- `research/gate-primitive.md`, `research/state-epoch-validation.md`
|
||||||
|
|
||||||
|
## 8. 후속 — State 에포크 안 3차 정정 (같은 날)
|
||||||
|
|
||||||
|
사용자가 `research/state-epoch-validation.md`를 직접 읽고 기제 서술 세 건을
|
||||||
|
정정했다: (1) `sourceList` 순회는 `rawInvalid`가 **false**일 때만 돈다(문서는
|
||||||
|
반대로 적고 있었다, 목적은 "못 받은 emit 받기"), (2) `emit`은 count 없이
|
||||||
|
**발행 source만** 싣는다, (3) 에이전트가 요구했던 **`seen`/`computedAt` 두
|
||||||
|
카운트 분리는 철회** — count 갱신과 `rawInvalid = true`가 같은 스텝이라
|
||||||
|
캐시 오인 경로가 없다. 부수로 "순회가 발견한 변경을 뒤로 emit 할 것인가"가
|
||||||
|
열렸는데, 다이아몬드 쪽은 사용자가 스스로 안전으로 정정했고 게이트 쪽만
|
||||||
|
**해제 emit이 `source = nil`을 싣는 규약**으로 남았다(게이트는 보통 최종단에
|
||||||
|
쓰므로 채택을 막지 않는다는 판단). 상세는 그 문서의 §2·§5와
|
||||||
|
`qa-request/pre-implementation-qa-round5-followup.md`의 M절.
|
||||||
|
|
|
||||||
|
|
@ -347,7 +347,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
||||||
- [ ] **[2026-08-21 5라운드 — 착수 전 확인]** State 재계산 판정을 "소스
|
- [ ] **[2026-08-21 5라운드 — 착수 전 확인]** State 재계산 판정을 "소스
|
||||||
에포크 비교"로 바꿀지가 **미정인 채 열려 있다**(`research/state-epoch-validation.md`,
|
에포크 비교"로 바꿀지가 **미정인 채 열려 있다**(`research/state-epoch-validation.md`,
|
||||||
`question.md` 3번). 채택하면 **State 내부 표현이 바뀌므로**(노드가
|
`question.md` 3번). 채택하면 **State 내부 표현이 바뀌므로**(노드가
|
||||||
`seen`/`computedAt` 두 카운트를 들고, `Get`이 상류 에포크를 검증)
|
루트 Source별 count를 들고, `Get`이 상류 에포크를 검증)
|
||||||
아래 `Source.luau`/`State.luau`를 짜기 **전에** 결론이 나야 한다 —
|
아래 `Source.luau`/`State.luau`를 짜기 **전에** 결론이 나야 한다 —
|
||||||
그 문서 자신이 "M3 뒤로 미루면 되돌리는 비용이 크다"고 경고한다.
|
그 문서 자신이 "M3 뒤로 미루면 되돌리는 비용이 크다"고 경고한다.
|
||||||
- [ ] `Blocker.luau`(`base/blocker-plan.md` 참고 — 여러 Source를
|
- [ ] `Blocker.luau`(`base/blocker-plan.md` 참고 — 여러 Source를
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue