design: 에포크 순회 처분을 sourceCountMap/sourceEmitMap 두 테이블로 확정

사용자가 열려 있던 마지막 자리를 제3안으로 닫음. 판정 기준을 둘로 나눈다 —
sourceCountMap(값 유효성)은 순회가 앞당겨 올리고, sourceEmitMap(전파 dedup)은
상류의 진짜 emit을 기다린다. emit 수신 규칙은 셋: count가 다르면 둘 다 갱신 +
rawInvalid + 전파 / count는 같은데 emit 기록이 다르면 전파만(순회가 앞질러
흡수한 경우) / 둘 다 같으면 삼킨다. 순회는 emit을 하지 않는다.

효과 — 통지가 죽는 "영구 침묵"이 사라지고, 순회가 emit을 안 하므로 게이트
누출 경로 자체가 없어져 source = nil 규약도 "게이트를 에포크 경계로" 같은
계약 반전도 불필요해진다. 직전 라운드에서 에이전트가 냈던 (c)안의 약점
(emit 도착 전까지 Get마다 재계산)도 sourceCountMap을 실제로 올리므로 없다.

같이 검토된 rawEmit+nil 안은 구조 위생(상류 emit과 내부 발생 emit의 진입점
통일)만 살리고 해법으로는 안 씀 — 막는 게이트는 보통 순회하는 노드 자신이
아니라 상류에 있어 자기 rawEmit을 태워도 누출이 남고, nil emit은 하류마다
전체 순회를 강제해 같은 문제를 연쇄시킨다.

M절에서 철회했던 seen/computedAt 분리가 다른 근거(순회가 값과 통지를
비대칭으로 앞당김)로 되살아난 것이라는 점도 명시. 이제 기제는 다 정해졌고
남은 건 채택 여부 자체 — README/question.md/ROADMAP 동기화.
doc-check.py ERROR 0.

Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01TiW21rnti9SbLgF6twtn6D
This commit is contained in:
qwreey 2026-08-21 20:49:28 +09:00
parent 9627558046
commit 76cf74e80d
Signed by: qwreey
GPG key ID: D28DB79297A214BD
6 changed files with 154 additions and 96 deletions

View file

@ -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를 실어오므로 판정 O(1)), "선언 안 된 의존성 UB 명문화"는 사용자 기각으로 빠짐. **[같은 날 3차 정정]** `rawInvalid` 기제가 뒤집혔다 — `sourceList` 순회는 `rawInvalid`**false**일 때만 돌고(목적은 "못 받은 emit 받기"), emit은 count 없이 source만 싣는다. 그래서 `seen`/`computedAt` 두 카운트 분리 요구는 **철회**되고 count 하나면 충분. 남은 세부는 게이트 해제 emit의 `source = nil` 규약 | **상 — M3(State 구현) 착수 전 필요** | | `state-epoch-validation.md` | **[2026-08-21 신설]** State 재계산 판정을 `invalid` 플래그에서 **루트 Source 에포크 비교**로 바꾸는 안(사용자 제안) — DFS 전파 도중 `Get()`이 섞인 값을 캐시하는 glitch를 없앰. 성능이 아니라 **정확성** 결정이고 State 내부 표현을 바꾸므로 M3 전에 결론 필요. 에이전트 분석: 진단·방향 모두 타당. **[같은 날 후속 확정] 중복 *통지*도 같은 장치로 접고**(emit이 발행 source를 실어오므로 판정 O(1)), "선언 안 된 의존성 UB 명문화"는 사용자 기각으로 빠짐. **[같은 날 3·4차 정정]** `rawInvalid` 기제가 뒤집혔고(순회는 `rawInvalid`**false**일 때만 돌고, emit은 count 없이 source만 싣는다), 순회가 발견한 변경의 처분이 **테이블 둘**로 확정됐다 — `sourceCountMap`(값 유효성)은 순회가 앞당겨 올리고 `sourceEmitMap`(전파 dedup)은 상류의 진짜 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 기준]** 플랜 초안 단계, 열린 질문 미해소(소스: 이 문서의 "열린 질문" 절) |

View file

@ -905,3 +905,50 @@ nativeDispose(element) -- 트리 밖
전체를 확인**하는 규약이 필요하다. 다만 사용자 판단은 *"중간에 blocker 낀건 전체를 확인**하는 규약이 필요하다. 다만 사용자 판단은 *"중간에 blocker 낀건
이전에도 있던 문제다. 이미 최종장에 쓰는게 일반적이므로 의미 없어보인다"* — 이전에도 있던 문제다. 이미 최종장에 쓰는게 일반적이므로 의미 없어보인다"* —
**채택을 막는 요소가 아니고**, `nil` 규약만 `Gate` 설계와 같이 확정하면 된다. **채택을 막는 요소가 아니고**, `nil` 규약만 `Gate` 설계와 같이 확정하면 된다.
---
# N절 — State 에포크 안: 순회 처분 확정 + `Gate``:Apply`가 아니다 (2026-08-21)
## N-1. 순회가 발견한 변경의 처분 — 테이블 둘로 확정
사용자가 M절의 열린 자리를 직접 닫았다. **문제**: 순회가 count를 최신으로
올리면 뒤늦게 온 진짜 emit이 삼켜져 **하류가 그 에포크를 영영 못 받는다**
(옛 dedup의 "영구 침묵"과 같은 계열).
**확정**: 판정 기준을 둘로 나눈다 — *"'내 값이, 상류의 상태로 하여금 사용
가능하다' 를 보는 테이블과, '내가 emit을 받을 때, 그걸 다시 던져야하는지
봐야하나' 를 보는걸 나누는거죠."*
| 테이블 | 뜻 | 순회가 건드리나 |
|---|---|---|
| `sourceCountMap = {[source]: count}` | 값 유효성 | **예** — 순회가 앞당겨 올린다 |
| `sourceEmitMap = {[source]: count}` | 전파 dedup | **아니오** — 상류의 진짜 emit을 기다린다 |
emit 수신 규칙 셋: (1) count가 다르면 둘 다 갱신 + `rawInvalid` + 전파,
(2) count는 같은데 emit 기록이 다르면 **전파만**(순회가 앞질러 흡수한 경우),
(3) 둘 다 같으면 삼킨다. 순회는 emit을 **안 한다** — *"emit 바로 안하고
상류가 emit 해줄 때 까지 기다립니다"*.
**부수 소멸**: 순회가 emit을 안 하므로 게이트 누출 경로가 아예 없어져
`source = nil` 규약도, "게이트를 에포크 경계로" 같은 계약 반전도 불필요.
같이 검토된 `rawEmit`+`nil` 안은 **구조 위생 쪽만 채택할 만하다**(상류 emit과
내부 발생 emit이 같은 진입점을 쓰는 것) — 해법으로는 부족한데, 막는 게이트는
보통 순회하는 노드 자신이 아니라 **상류**에 있어 자기 `rawEmit`을 태워도
누출이 남고, `nil` emit은 하류마다 전체 순회를 강제해 같은 문제를 연쇄시키기
때문. 상세는 `research/state-epoch-validation.md` §2·§5-3.
**메모**: 이 분리는 M절에서 **철회했던 `seen`/`computedAt` 분리가 다른 근거로
되살아난 것**이다. 옛 근거는 여전히 틀렸고, 지금 근거는 **순회가 값과 통지를
비대칭으로 앞당긴다**는 것.
## N-2. `Gate``:Apply`가 아니라 State 메소드
**사용자 확정**: *"gate 는 apply 불가하다고 판단함. 순수 슈가가 아니기 때문,
state 의 전파를 손대는 작업이라 with 처럼 다른 노드가 나는게 맞음."* 경계를
정확히 하면 `Apply`가 노드를 못 만드는 게 아니라(확정 예시 `capAt(100)`
`:With` 노드를 만든다) **프리미티브는 메소드 / 유저랜드 조합 팩토리는
`:Apply`** 라는 층위 구분이다. 부수로 `Debounce`/`Throttle`의 `:Apply` 관용구는
그대로 유효하고(팩토리가 내부에서 `:Gate`를 부름), `Blocker` 배선은 이미 확정된
`state:Block(blocker)` 메소드로 자동 해소되며, `__call`은 안 쓴다.
`research/gate-primitive.md`의 2번이 해소로 갱신됨.

View file

@ -229,12 +229,13 @@
(MobX/Adapton류 버전 검증). **[2026-08-21 갱신 — 여기 있던 "중복 통지는 안 (MobX/Adapton류 버전 검증). **[2026-08-21 갱신 — 여기 있던 "중복 통지는 안
고쳐지고 선언 안 된 의존성을 UB로 명문화해야 한다"는 서술은 같은 날 둘 다 고쳐지고 선언 안 된 의존성을 UB로 명문화해야 한다"는 서술은 같은 날 둘 다
뒤집혔다]**: 중복 통지도 **같은 장치로 접고**, UB 조항은 사용자 기각으로 뒤집혔다]**: 중복 통지도 **같은 장치로 접고**, UB 조항은 사용자 기각으로
빠졌다. 이어진 3차 정정으로 기제도 확정형에 가까워졌다 — `sourceList` 순회는 빠졌다. 이어진 3·4차 정정으로 **기제는 사실상 다 정해졌다** 순회는
`rawInvalid`가 **false**일 때만 돌고(목적은 "못 받은 emit 받기"), emit은 `rawInvalid`가 **false**일 때만 돌고(목적은 "못 받은 emit 받기"), emit은
count 없이 **발행 source만** 싣고, 카운트는 **하나면 충분**하다. 사용자 count 없이 **발행 source만** 싣고, 순회가 발견한 변경은 **테이블 둘**로
정리: *"그냥 지금 순수 count 와 source 를 ref해두는 구현은 문제가 처분한다(`sourceCountMap`은 순회가 앞당겨 올리고, `sourceEmitMap`은 상류의
없어보인다."* **남은 판단은 (a) 채택 여부 자체와 (b) 게이트 해제 emit이 진짜 emit을 기다림 — *"emit 바로 안하고 상류가 emit 해줄 때 까지
`source = nil`을 실어 "전체 확인"을 시키는 규약**이고, State 내부 표현을 기다립니다"*). 그래서 통지가 죽지도 게이트를 새지도 않아 `source = nil`
규약도 필요 없어졌다. **남은 판단은 채택 여부 자체 하나**이고, State 내부 표현을
바꾸는 결정이라 M3 뒤로 미루면 되돌리는 비용이 크다. 상세는 바꾸는 결정이라 M3 뒤로 미루면 되돌리는 비용이 크다. 상세는
`research/state-epoch-validation.md`. `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

View file

@ -1,6 +1,6 @@
# State 재계산 판정을 "소스 에포크" 비교로 바꾸는 안 (2026-08-21 신설) # State 재계산 판정을 "소스 에포크" 비교로 바꾸는 안 (2026-08-21 신설)
**상태**: research — **사용자 제안, 에이전트 분석 완료. 2026-08-21에 세부가 차례 갱신됐다(중복 통지도 접음 / 선언 안 된 의존성 조항 기각 / `rawInvalid` 기제와 `emit` 인자 정정 + `seen`·`computedAt` 분리 철회). 채택 자체는 여전히 미정.** **상태**: research — **사용자 제안, 에이전트 분석 완료. 2026-08-21에 세부가 차례 갱신됐다(중복 통지도 접음 / 선언 안 된 의존성 조항 기각 / `rawInvalid` 기제와 `emit` 인자 정정 / 순회 처분을 `sourceCountMap`·`sourceEmitMap` 두 테이블로 확정). 이제 기제는 사실상 다 정해졌고 남은 건 **채택 여부 자체**다 — 여전히 미정.**
구현 전 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,53 +34,64 @@ A ──> B ──┐
시점"이 전파 파동이 끝난 뒤*라고 암묵적으로 가정한다 — Observer가 전파 도중에 시점"이 전파 파동이 끝난 뒤*라고 암묵적으로 가정한다 — Observer가 전파 도중에
발화한다는 사실과 겹쳐 읽으면 그 가정이 깨진다. 발화한다는 사실과 겹쳐 읽으면 그 가정이 깨진다.
## 2. 사용자 제안 (최종 정리형) — **[2026-08-21 정정본]** ## 2. 사용자 제안 (최종 정리형) — **[2026-08-21 3차 정정본]**
각 State가 **자기에게 영향을 주는 루트 `Source`들의 에포크(count)** 를 들고 각 State가 **자기에게 영향을 주는 루트 `Source`들의 에포크(count)** 를 들고
있다가, 그것과 실제 Source의 현재 count를 비교해 재계산/전파 여부를 정한다. 있다가, 그것과 실제 Source의 현재 count를 비교해 재계산/전파 여부를 정한다.
**[2026-08-21] 마지막 정정으로 테이블이 둘로 갈렸다** — 아래 "왜 둘인가" 참고.
``` ```
State State
sourceList : { [source (weak key)] : count } sourceCountMap : { [source (weak key)] : count }
-- "이 소스의 이 에포크까지는 이미 봤다". 상류에서 복사, :With에서 합침 -- "내 값이 이 소스에 대해 최신인가" (값 유효성)
sourceEmitMap : { [source (weak key)] : count }
-- "이 소스의 이 에포크를 내가 하류로 이미 던졌는가" (전파 dedup)
rawInvalid : boolean rawInvalid : boolean
-- "재계산이 필요하다"는 확정 플래그. emit이 실제로 통과하면 켜진다 -- "재계산이 필요하다"는 확정 플래그
``` ```
둘 다 상류에서 복사되고 `:With`에서 합쳐진다.
- `Source:Set()`/`:Emit()`은 자기 count를 증가시킨다. - `Source:Set()`/`:Emit()`은 자기 count를 증가시킨다.
- **`emit`은 발행한 source만 전달한다**(값도, count도 안 싣는다) — 받는 쪽이 - **`emit`은 발행한 source만 전달한다**(값도, count도 안 싣는다) — 받는 쪽이
그 source의 count 필드를 그냥 읽으면 된다. **[2026-08-21 사용자 정정]** 그 source의 count 필드를 **그때그때 라이브로** 읽는다. 그래서 "에포크 5의
앞선 정리는 `(source, count)` 쌍을 실어 보낸다고 적었는데 불필요하다. emit" 같은 건 없고 emit은 그냥 **"이 소스를 확인해봐"** 라는 신호다.
- **emit을 받았을 때** — 판정은 O(1)이다, 그 소스 항목 하나만 본다: - **emit을 받았을 때** — 판정은 O(1)이다, 그 소스 항목 하나만 본다:
1. `sourceList[source] == source.count`**삼킨다.** 이 에포크는 이미 다른 1. `sourceCountMap[source] ~= source.count` → 둘 다 `source.count`로 갱신하고
경로로 봤다는 뜻이므로 뒤로 안 내려보낸다. `rawInvalid = true`를 세운 다음 **뒤로 emit** 한다.
2. 다르면 → **`sourceList[source]`를 먼저 갱신**하고 `rawInvalid = true` 2. 같은데 `sourceEmitMap[source] ~= source.count` → **값은 이미 최신이지만
둔 다음, **그러고 나서** 뒤로 emit 한다. 통지는 아직 안 나갔다**(아래 순회가 앞질러 흡수한 경우). `sourceEmitMap`
갱신하고 **뒤로 emit** 한다 — `rawInvalid`는 안 건드린다.
3. 둘 다 같으면 → **삼킨다.**
- **다른 소스 항목은 건드리지 않는다** — 그 소스들은 자기가 직접 emit 하므로. - **다른 소스 항목은 건드리지 않는다** — 그 소스들은 자기가 직접 emit 하므로.
- **재계산 판정**: - **재계산 판정**:
- `rawInvalid == true` → 그냥 재계산한다. `sourceList`를 훑을 이유가 없다 - `rawInvalid == true` → 그냥 재계산한다. 순회할 이유가 없다(이미 확정).
(이미 확정이라 더 알아낼 게 없다). - `rawInvalid == false`**그때만 `sourceCountMap`을 훑는다.** 목적은 하나뿐 —
- `rawInvalid == false`**그때만 `sourceList`를 훑는다.** 목적은 하나뿐 — **못 받은 emit을 여기서 먼저 받아주는 것**(중간 게이트에 막혀 있었다거나
**못 받은 emit을 여기서 먼저 받아주는 것**(중간 게이트에 막혀 있었다거나). 전파 파동이 아직 이 가지에 안 닿았다거나). 다른 항목이 발견되면
다른 항목이 발견되면 `rawInvalid = true`로 만든다. **`sourceCountMap`만** 갱신하고 `rawInvalid = true`로 만든다.
**⚠️ 이때 count를 갱신할지는 열려 있다 — §5의 3번이 소스**(갱신하면 나중에 - **⭐ 순회는 `sourceEmitMap`을 건드리지 않고, 뒤로 emit 하지도 않는다.**
도착하는 진짜 emit이 삼켜져 하류가 영영 통지를 못 받는다). 그래서 나중에 진짜 emit이 도착하면 위 2번으로 걸려 **하류까지 정상적으로
- **[2026-08-21 사용자 정정]** 앞선 정리는 이 순회 조건을 `rawInvalid == true` 전파된다**(사용자: *"emit 바로 안하고 상류가 emit 해줄 때 까지 기다립니다"*).
일 때로 적었는데 **반대**다. - 재계산이 끝나면 `rawInvalid = false`.
- 재계산이 끝나면 `rawInvalid = false`. count는 emit을 통과시킬 때 이미
갱신돼 있으므로 따로 손댈 게 없다.
- count가 싫다면 `[source] -> {}` 처럼 **유니크 테이블 identity**로 같은 판정이 - count가 싫다면 `[source] -> {}` 처럼 **유니크 테이블 identity**로 같은 판정이
가능하다(사용자 대안). 가능하다(사용자 대안).
**⭐ [2026-08-21] 그래서 카운트는 하나면 된다 — `seen`/`computedAt` 분리는 **⭐ 왜 테이블이 둘인가 — 순회 때문이다.** 사용자 정리: *"'내 값이, 상류의
철회.** 앞선 정리는 노드가 전파 dedup용 `seen`과 캐시 검증용 `computedAt` 상태로 하여금 사용 가능하다' 를 보는 테이블과, '내가 emit을 받을 때, 그걸 다시
따로 들어야 한다고 적었으나, 사용자 정정으로 **불필요**하다: count 갱신과 던져야하는지 봐야하나' 를 보는걸 나누는거죠."* 순회가 없다면 **"값을
`rawInvalid = true`**같은 스텝에서 같이 일어나기** 때문에 "전파 시점에 최신화했다"와 "통지를 내려보냈다"가 항상 같은 스텝에서 같이 일어나서** 카운트
갱신된 count가 캐시를 신선한 것으로 오인하게 만드는" 실패 모드 자체가 하나로 충분하다 — 실제로 2026-08-21 2차 정정에서 그렇게 정리했었다. 순회를
생기지 않는다. 캐시 유효성은 count가 아니라 `rawInvalid`가 들고, count는 넣는 순간 그 둘이 갈라진다(순회는 값만 앞당기고 통지는 안 한다). 그래서 이
순수하게 **"이 에포크를 봤는가"** 만 답한다. 사용자 정리: *"그냥 지금 순수 분리는 **한 번 철회됐던 `seen`/`computedAt` 분리가, 다른 이유로 되살아난
count 와 source 를 ref해두는 구현은 문제가 없어보인다. 단순 rawInvalid 가 것**이다 — 옛 근거("전파 시점에 갱신된 count가 캐시를 신선한 것으로 오인시킨다")는
약간 이상하게 사용/기제된 부분만 보일 뿐임."* 여전히 틀렸고, 지금 근거는 **순회가 값과 통지를 비대칭으로 앞당긴다**는 것이다.
**구현 메모 — `sourceEmitMap`은 희소 테이블로 두면 된다.** emit이 count를 안
싣고 받는 쪽이 라이브로 읽으므로, 이 테이블에 실제로 필요한 정보는 **"순회가
앞질러 흡수해서 아직 안 던진 소스가 무엇인가"** 뿐이다. 평상시엔 비어 있고
순회가 발견했을 때만 채워지는 `pending` 집합으로 구현해도 위 세 규칙이 그대로
성립한다(1번에서 지우고, 2번에서 있으면 던지고 지운다).
## 3. 에이전트 분석 — 이게 실제로 무엇을 고치는가 ## 3. 에이전트 분석 — 이게 실제로 무엇을 고치는가
@ -108,11 +119,12 @@ count 와 source 를 ref해두는 구현은 문제가 없어보인다. 단순 ra
(`archive/invalidate-dedup-propagation-reversed.md`). 에포크 비교는 그 (`archive/invalidate-dedup-propagation-reversed.md`). 에포크 비교는 그
모드가 없다 — 매 `Set`마다 카운트가 **새 값**이라 항상 통과하고, 접히는 건 모드가 없다 — 매 `Set`마다 카운트가 **새 값**이라 항상 통과하고, 접히는 건
**같은 에포크가 두 경로로 도착한 두 번째**뿐이다. **같은 에포크가 두 경로로 도착한 두 번째**뿐이다.
- **⚠️ [2026-08-21 철회] 여기 있던 "카운트를 `seen`/`computedAt` 둘로 - **⚠️ [2026-08-21 철회 후 재도입] 여기 있던 "카운트를 `seen`/`computedAt`
분리하라"는 요구는 사용자 정정으로 없어졌다** — §2 마지막 문단이 소스. 둘로 분리하라"는 요구는 한 번 철회됐다가 **다른 근거로 되살아났다**
캐시 유효성을 `rawInvalid`가 들고 있고 count 갱신이 `rawInvalid = true` 지금 형태는 `sourceCountMap`/`sourceEmitMap`이고, 근거는 원래 적었던
같은 스텝이라, 하나로 합쳐도 캐시를 신선한 것으로 오인하는 경로가 없다. "전파 시점 갱신이 캐시를 오인시킨다"(틀림)가 아니라 **순회가 값만 앞당기고
- 비교는 `sourceList[source] == source.count`면 삼킨다(단조 증가라 안전). 통지는 안 한다**는 비대칭이다. §2의 "왜 테이블이 둘인가" 문단이 소스.
- 비교는 두 맵 모두 `source.count`와 같으면 삼킨다(단조 증가라 안전).
- **선례가 있다** — 값 자체를 비교하는 게 아니라 **버전/에포크를 비교해 - **선례가 있다** — 값 자체를 비교하는 게 아니라 **버전/에포크를 비교해
lazy하게 검증**하는 건 MobX(global state version + observing 검사), lazy하게 검증**하는 건 MobX(global state version + observing 검사),
Adapton류 incremental computing, "reactively" 계열 라이브러리가 쓰는 표준 Adapton류 incremental computing, "reactively" 계열 라이브러리가 쓰는 표준
@ -146,60 +158,43 @@ count 와 source 를 ref해두는 구현은 문제가 없어보인다. 단순 ra
2. **동적 의존성.** 조건에 따라 다른 상류를 읽는 계산이면 `sourceList` 2. **동적 의존성.** 조건에 따라 다른 상류를 읽는 계산이면 `sourceList`
보수적 상위집합이 된다 — 틀리진 않고 재계산이 조금 더 잦아질 뿐이다. 보수적 상위집합이 된다 — 틀리진 않고 재계산이 조금 더 잦아질 뿐이다.
그대로 감수할지 확인. 그대로 감수할지 확인.
3. **⭐⭐ [2026-08-21 재확장] 순회가 발견한 변경을 어떻게 처분하는가 — 이 안에서 3. **[2026-08-21 해소] 순회가 발견한 변경을 어떻게 처분하는가 — 두 테이블로
유일하게 안 닫힌 자리.** 나눠 "값은 앞당기고 통지는 기다린다"로 확정.**
**먼저 확정된 것: 순회가 count를 갱신하면 안 된다(또는 갱신하면 반드시 **문제**(사용자 지적): 순회가 count를 최신으로 올려두면 뒤늦게 도착한 진짜
emit해야 한다).** 사용자 지적 — *"순회가 발견한거면, count 를 최신으로 emit이 삼켜져 **하류가 그 에포크를 영영 못 받는다.** 값은 맞는데 통지가
올리기에 나중에 emit 와도 전파가 안 되는 이슈가 생기긴 함."* 실제로 그렇다: 죽는, 2026-08-14에 폐기된 옛 dedup의 "영구 침묵"과 같은 계열의 실패 모드다
`C`가 순회로 `A`의 변경을 발견해 count를 최신으로 올려두면, 뒤늦게 도착한
`A`의 진짜 emit이 `C`에서 **삼켜지고**(count가 같으므로) `C`의 하류는 그
에포크를 **영영** 못 받는다. 값은 맞지만 통지가 죽는, **2026-08-14에 폐기된
옛 dedup의 "영구 침묵"과 같은 계열의 실패 모드**다
(`archive/invalidate-dedup-propagation-reversed.md`). (`archive/invalidate-dedup-propagation-reversed.md`).
두 해법이 있다: **확정 해법**(사용자 제안, §2가 소스): 판정 기준을 **둘로 나눈다**
*"'내 값이, 상류의 상태로 하여금 사용 가능하다' 를 보는 테이블과, '내가
emit을 받을 때, 그걸 다시 던져야하는지 봐야하나' 를 보는걸 나누는거죠."*
순회는 `sourceCountMap`만 올리고 **emit은 안 한다**. 나중에 진짜 emit이
오면 count는 같지만 `sourceEmitMap`이 달라 **그때 정상적으로 전파**된다
(*"emit 바로 안하고 상류가 emit 해줄 때 까지 기다립니다"*). 결과:
- 통지가 죽지 않는다.
- 순회가 emit을 안 하므로 **게이트를 새게 하는 경로가 아예 없다**
`source = nil` 규약도, "게이트를 에포크 경계로" 같은 계약 반전도 필요 없다.
- `Get()`은 순회 덕에 항상 신선하고, 순회가 `sourceCountMap`을 실제로
올리므로 **다음 `Get()`이 같은 차이를 재발견해 또 재계산하는 낭비도 없다**
(에이전트가 냈던 대안 (c)의 유일한 약점이 여기서 사라진다).
- **(b) 순회가 count를 올리고, 그 자리에서 뒤로 emit 한다** — 사용자 제안. **같이 검토된 대안 — `rawEmit`을 태우고 `nil`을 던지는 안**(사용자 제안 1안):
통지가 죽지 않는다. 다이아몬드에서 새는 것도 없다(사용자가 스스로 정정한 순회가 만든 emit을 **상류가 부르는 것과 같은 내부 진입점(`rawEmit`)** 으로
대로 `D`도 count를 갱신해 중복을 삼킨다). **남는 문제는 게이트뿐** — 아래. 흘려서, 그 노드에 `Gate`가 껴 있으면 자연히 게이트를 타게 하고, 순회가 찾은
- **(c) 순회는 `rawInvalid = true`만 세우고 count는 그대로 둔다** — 에이전트 변경은 여러 소스일 수 있으니 하류엔 `source = nil`을 던진다는 안.
제안. count의 역할을 **"이 에포크를 내가 실제로 전파했는가"** 하나로 **구조 위생 쪽은 채택할 만하다** — 상류 emit과 내부 발생 emit이 같은 진입점을
유지한다. `Get()``rawInvalid` 덕에 신선하고, 나중에 진짜 emit이 오면 쓰면 게이트가 붙은 노드에서 두 경로가 갈리지 않는다. 다만 **이 문제의 해법으로는
count가 다르므로 **정상 경로로** 하류까지 전파된다. **게이트를 새게 하는 위 2안이 더 낫다**:
경로가 아예 생기지 않는다**(순회는 절대 emit하지 않으므로). - 막고 있는 게이트는 보통 **순회하는 노드 자신이 아니라 상류**에 있다. 자기
- 대가: emit이 도착하기 전까지 **`Get()`마다 다시 재계산**한다(매번 순회가 `rawEmit`을 태워봐야 상류 게이트는 안 물어보므로 **누출이 그대로 남는다.**
같은 차이를 재발견하므로). 일반 경로에선 파동이 같은 틱에 도착하니 창이 - `nil` emit은 받는 쪽마다 **전체 순회를 강제**하고, 그 순회가 다시 count를
아주 좁고, 게이트 경로에선 "묶어놨다가 한 번에 푼다"가 목적이라 그 구간에 올리면 같은 "영구 침묵" 문제가 하류에서 재발한다(연쇄).
`Get()`이 잦을 이유가 없다. - 2안은 순회가 애초에 emit을 안 하므로 두 문제가 **생기지 않는다.**
- ⚠️ 단 `blocker:OffWithoutEmit()`처럼 **emit 없이 푸는 경로**에선 count가
영원히 안 따라잡아 그 상태가 무기한 남는다 — 그 경로가 count를 밀어주게
할지 같이 정해야 한다.
**게이트 문제(=(b)를 택할 때만 남는다).** 게이트가 앞에서 막아둔 emit을, **부수 확인**: `blocker:OffWithoutEmit()`처럼 emit 없이 푸는 경로에서
뒤의 `Get()`이 촉발한 순회가 그대로 흘려보낸다. 사용자 판단은 *"gate 의 경우 `sourceEmitMap`이 뒤에 남아도, 그 소스의 **다음 진짜 emit**이 위 1번(둘 다
이것 또한 gate 가 emit 하는, 즉 blocker off 되는 시점까지 밀리면 된다"*인데, 다름)으로 걸려 정상화된다 — 별도 조치가 필요 없다.
**그걸 실행할 기제가 간단하지 않다** — 순회하는 노드 `C`는 자기 상류 어딘가에
게이트가 걸려 있다는 걸 알 방법이 없다(`sourceList`는 루트 Source로 평탄화돼
있어 중간 노드가 안 보인다). 기제 후보는 둘이고 **둘 다 대가가 있다**:
- **게이트를 에포크 경계로 만든다** — 게이트 하류의 `sourceList`가 루트
Source 대신 **게이트 자신**을 (자기 count와 함께) 들게 한다. 그러면 막혀
있는 동안 하류 순회가 변경을 **발견 자체를 못 하므로** 새지 않고, 해제 emit도
`source = nil`이 필요 없어진다(게이트가 곧 그 소스다). **대가가 크다**
하류 `Get()`이 막힌 값을 못 보게 되어 `base/blocker-plan.md`의 확정 계약
(`:Get()`엔 영향 없음)이 **뒤집힌다.**
- **해제 emit이 `source = nil`을 싣는다** — 게이트가 풀 때 내보내는 emit은
여러 소스가 섞여 있어 지목이 안 되므로, `nil`을 싣고 받는 쪽이 전체 목록을
확인한다. 새는 것 자체는 못 막고, 새고 난 뒤 정합성만 맞춘다.
**에이전트 권고: (c).** 사용자가 발견한 문제의 원인이 정확히 "순회가 count를
올리는 것"이므로 **그것만 안 하면** 통지 죽음도, 게이트 누출도, `nil` emit
규약도, 계약 반전도 전부 안 생긴다. 가장 적게 건드리는 해법이다.
`OffWithoutEmit` 캐비엇 하나만 같이 정하면 된다.
**다만 사용자 판단은 "중간에 게이트가 끼는 건 원래 있던 문제이고, 게이트는
보통 최종단에 쓰므로 실질적으로 의미 없어 보인다"**로 이미 나와 있다 — 즉
(b)를 택해도 채택을 막는 요소는 아니다.
4. **`:Emit()`(값을 제자리에서 mutate하고 알리는 경로)** — count만 올리면 되므로 4. **`:Emit()`(값을 제자리에서 mutate하고 알리는 경로)** — count만 올리면 되므로
그대로 동작한다. 확인만. 그대로 동작한다. 확인만.
@ -231,9 +226,10 @@ Ref<T> 로 변환해주는 등"*. 즉 "항상 최신 값을 필드로 들고 있
- **채택 시 같이 확정되는 것: 중복 통지도 접는다**(§3) — 그러면 다이아몬드에서 - **채택 시 같이 확정되는 것: 중복 통지도 접는다**(§3) — 그러면 다이아몬드에서
값도 통지도 한 번씩만 간다. **[2026-08-21 정정]** 여기 있던 "노드가 값도 통지도 한 번씩만 간다. **[2026-08-21 정정]** 여기 있던 "노드가
`seen`/`computedAt` 두 카운트를 들어야 한다"는 추가 요구는 **철회**됐고, `seen`/`computedAt` 두 카운트를 들어야 한다"는 추가 요구는 **철회**됐고,
대신 확정할 것은 **순회가 발견한 변경의 처분**(§5의 3번) — 에이전트 권고는 **[2026-08-21 갱신]** 대신 확정된 것은 **테이블 둘**(`sourceCountMap` /
"순회는 count를 안 올리고 `rawInvalid`만 세운다"이고, 그러면 `nil` emit `sourceEmitMap`)이다 — 순회가 값만 앞당기고 통지는 상류 emit을 기다린다.
규약도 계약 반전도 필요 없어진다. 그래서 `nil` emit 규약도, 게이트를 에포크 경계로 만드는 계약 반전도 필요
없다(§5의 3번).
- 실측은 `luau-test`에 스파이크 하나면 충분하다 — 위 다이아몬드를 그대로 짜서 - 실측은 `luau-test`에 스파이크 하나면 충분하다 — 위 다이아몬드를 그대로 짜서
(a) 지금 모델에서 섞인 값이 실제로 관측되는지, (b) 에포크 비교를 넣으면 (a) 지금 모델에서 섞인 값이 실제로 관측되는지, (b) 에포크 비교를 넣으면
사라지는지 대조. 사라지는지 대조.

View file

@ -246,3 +246,17 @@ count가 안 따라잡는 것. 또 (b)에서 "게이트까지 민다"를 실제
됨), `Blocker` 배선 문제는 이미 확정된 `state:Block(blocker)` 메소드로 됨), `Blocker` 배선 문제는 이미 확정된 `state:Block(blocker)` 메소드로
자동 해소, `__call`은 안 씀(사용자 선호 + Luau 함수 타입 자리 통과 여부 자동 해소, `__call`은 안 씀(사용자 선호 + Luau 함수 타입 자리 통과 여부
불확실). `research/gate-primitive.md`의 2번이 해소로 갱신됨. 불확실). `research/gate-primitive.md`의 2번이 해소로 갱신됨.
## 10. 후속 — 순회 처분을 테이블 둘로 확정 (같은 날)
9절에서 열어둔 (b)/(c)를 사용자가 **제3안으로 닫았다**: 판정 기준을 둘로
나눠 `sourceCountMap`(값 유효성, 순회가 앞당겨 올림)과 `sourceEmitMap`(전파
dedup, 상류의 진짜 emit을 기다림)을 따로 둔다. 순회는 emit을 안 하므로
게이트 누출이 아예 안 생기고, 뒤늦은 emit은 "count는 같은데 emit 기록이
다름"으로 걸려 정상 전파된다. (c)의 유일한 약점(`Get()`마다 재계산)도
사라진다 — 순회가 `sourceCountMap`을 실제로 올리기 때문. 같이 나온
`rawEmit`+`nil` 안은 구조 위생(상류 emit과 내부 emit의 진입점 통일)만
살리고 해법으로는 안 쓴다(막는 게이트는 보통 상류에 있어 자기 `rawEmit`
태워도 누출이 남고, `nil` emit은 하류마다 전체 순회를 강제해 연쇄한다).
M절에서 철회했던 두-카운트 분리가 **다른 근거로 되살아난** 셈이다.
이제 이 안은 기제가 다 정해졌고 **남은 건 채택 여부 자체**다.

View file

@ -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 내부 표현이 바뀌므로**(노드가
루트 Source별 count 들고, `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를