design: 에포크 순회의 count 갱신 문제 정리 + Gate는 :Apply가 아니라 State 메소드로 확정
(1) research/state-epoch-validation.md §5-3 재작성. 사용자 지적대로 순회가 count를 올리면 뒤늦게 온 진짜 emit이 삼켜져 하류 통지가 영구히 죽는다 (2026-08-14에 폐기된 옛 dedup의 "영구 침묵"과 같은 계열). 두 해법을 대조로 남김 — (b) 순회도 emit(사용자 제안, 게이트 누출이 남고 그 누출을 막을 기제가 둘 다 대가가 큼: 에포크 경계는 blocker의 ":Get()엔 영향 없음" 확정 계약을 뒤집고, source = nil 규약은 사후 정합성만 맞춤) vs (c) 순회는 rawInvalid만 세우고 count는 안 올림(에이전트 권고 — 원인만 제거하므로 누출도 nil 규약도 계약 반전도 안 생김, 대가는 emit 도착 전까지 Get마다 재계산과 OffWithoutEmit 캐비엇). 미결로 남김. (2) research/gate-primitive.md 2번 해소. 사용자 확정으로 Gate는 state:Gate(setup) 메소드다. 경계는 "Apply가 노드를 못 만든다"가 아니라 "프리미티브는 메소드 / 유저랜드 조합 팩토리는 :Apply". 그래서 Debounce/Throttle의 Apply 관용구는 유지되고, Blocker 배선은 이미 확정된 state:Block(blocker)가 내부에서 :Gate를 부르는 것으로 자동 해소되며, __call은 쓰지 않는다. 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:
parent
4c395cd383
commit
9627558046
4 changed files with 118 additions and 33 deletions
|
|
@ -213,9 +213,13 @@
|
||||||
**공개 API로 낸다**(사용자: *"이 API가 비공개일 이유는 없어보인다"*). 남은 것 —
|
**공개 API로 낸다**(사용자: *"이 API가 비공개일 이유는 없어보인다"*). 남은 것 —
|
||||||
(a) **이름**(사용자: *"프리미티브 명을 Gater? 뭔가 이상하게 들어간다는게 약간의
|
(a) **이름**(사용자: *"프리미티브 명을 Gater? 뭔가 이상하게 들어간다는게 약간의
|
||||||
문제"*, 에이전트 권고는 `Gate` 그대로 — `gate`는 이미 장치를 가리키는 명사),
|
문제"*, 에이전트 권고는 `Gate` 그대로 — `gate`는 이미 장치를 가리키는 명사),
|
||||||
(b) `:Apply` 팩토리인지 독립 생성자인지, (c) `Blocker`가 그 위에 어떻게
|
(b) M2에 `Gate`만 넣을지 `Blocker`까지 넣을지, 그리고 생명주기/재진입 계약.
|
||||||
얹히는지, (d) M2에 `Gate`만 넣을지 `Blocker`까지 넣을지. 상세는
|
**[2026-08-21 해소]** 여기 있던 "`:Apply` 팩토리인가"와 "`Blocker`가 그 위에
|
||||||
`research/gate-primitive.md`.
|
어떻게 얹히는가"는 닫혔다 — **사용자 확정으로 `Gate`는 `:Apply`가 아니라
|
||||||
|
`:With`류 State 메소드**(*"state 의 전파를 손대는 작업이라 with 처럼 다른
|
||||||
|
노드가 나는게 맞음"*)이고, 그러면 `Blocker`는 이미 확정된
|
||||||
|
`state:Block(blocker)` 메소드가 내부에서 그걸 부르면 되므로 배선 문제 자체가
|
||||||
|
없어진다. 상세는 `research/gate-primitive.md`.
|
||||||
- **⭐ [신설, 2026-08-21 구현 전 QA 5라운드] State 재계산 판정을 "소스 에포크
|
- **⭐ [신설, 2026-08-21 구현 전 QA 5라운드] State 재계산 판정을 "소스 에포크
|
||||||
비교"로 바꿀지 — M3 착수 전 필요.** 사용자 제안: 각 State가 자기 상류 루트
|
비교"로 바꿀지 — M3 착수 전 필요.** 사용자 제안: 각 State가 자기 상류 루트
|
||||||
`Source`들의 카운트를 들고 있다가 `Get()` 때 비교해 재계산 여부를 정한다.
|
`Source`들의 카운트를 들고 있다가 `Get()` 때 비교해 재계산 여부를 정한다.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
# `Gate` — emit을 가로채는 공용 게이트 노드 (2026-08-21 신설)
|
# `Gate` — emit을 가로채는 공용 게이트 노드 (2026-08-21 신설)
|
||||||
|
|
||||||
**상태**: research — **방향은 사용자 확정, 정확한 표면이 미정.
|
**상태**: research — **방향은 사용자 확정, 정확한 표면이 미정(단 [2026-08-21]
|
||||||
|
"`:Apply`가 아니라 State 메소드"까지는 확정됨 — 아래 2번).
|
||||||
[2026-08-21] 사용자 지시로 설계 자체는 다음 세션으로 미룸** — *"고칠것이 많으므로
|
[2026-08-21] 사용자 지시로 설계 자체는 다음 세션으로 미룸** — *"고칠것이 많으므로
|
||||||
Gate 는 다음 세션에 다루겠음. 해당 부분은 정정이 아니고 추가이고, 새 인터페이스를
|
Gate 는 다음 세션에 다루겠음. 해당 부분은 정정이 아니고 추가이고, 새 인터페이스를
|
||||||
고민해야하므로 해결해야할 일로 남겨두길 바람. 단지 지금 세션 상 지식만 이전될 수
|
고민해야하므로 해결해야할 일로 남겨두길 바람. 단지 지금 세션 상 지식만 이전될 수
|
||||||
|
|
@ -62,11 +63,32 @@ end)
|
||||||
(`Source`/`Ref`/`Slot`/`Tween`도 전부 `-er` 없는 명사). 대안 후보:
|
(`Source`/`Ref`/`Slot`/`Tween`도 전부 `-er` 없는 명사). 대안 후보:
|
||||||
`Valve`(밸브 — 흐름 제어라는 뜻은 더 정확하지만 코퍼스 어휘와 멀다),
|
`Valve`(밸브 — 흐름 제어라는 뜻은 더 정확하지만 코퍼스 어휘와 멀다),
|
||||||
`Relay`(전기 릴레이 — "받아서 다시 보낸다"는 뜻은 맞으나 "중계"로 오독 여지).
|
`Relay`(전기 릴레이 — "받아서 다시 보낸다"는 뜻은 맞으나 "중계"로 오독 여지).
|
||||||
2. **`:Apply` 팩토리인가, 독립 생성자인가.** `Debounce`/`Throttle`이
|
2. **[2026-08-21 해소] `:Apply`가 아니라 State의 메소드다 — `state:Gate(setup)`.**
|
||||||
`state:Apply(Debounce{...})` 관용구로 확정돼 있으므로 `state:Apply(Gate(setup))`가
|
**사용자 확정**: *"gate 는 apply 불가하다고 판단함. 순수 슈가가 아니기 때문,
|
||||||
자연스럽다. 그런데 `Blocker()`는 **여러 state에 공유되는 외부 객체**라 모양이
|
state 의 전파를 손대는 작업이라 with 처럼 다른 노드가 나는게 맞음."*
|
||||||
다르다 — `Blocker`가 `Gate` 위에 어떻게 얹히는지(`blocker`가 각 gated state마다
|
- **정확한 경계**: `Apply`는 `factory(self)`일 뿐이라(`base/source-state-plan.md`의
|
||||||
`Gate`를 하나씩 만들어 자기 정책을 심는 형태?)를 같이 정해야 한다.
|
"`state:Apply(factory)`" 절) 팩토리가 노드를 만드는 것 **자체는** 금지가
|
||||||
|
아니다 — 확정 예시의 `capAt(100)`도 `:With` 노드를 만든다. 갈리는 지점은
|
||||||
|
**누가 프리미티브인가**다: `:With`/`:Compute`처럼 **전파 경로에 새 종류의
|
||||||
|
노드를 끼우는 것은 State의 메소드**, 그 프리미티브들을 조합한 **유저랜드
|
||||||
|
팩토리는 `:Apply`**. `Gate`는 전자다.
|
||||||
|
- **그래서 `Debounce`/`Throttle`은 `:Apply` 그대로 둔다** —
|
||||||
|
`Debounce{...}`가 돌려주는 팩토리가 내부에서 `s:Gate(policy)`를 부르면
|
||||||
|
되므로 `base/debounce-throttle-plan.md`의 확정 관용구는 안 건드려도 된다.
|
||||||
|
- **`Blocker` 배선 문제도 같이 사라진다.** `base/blocker-plan.md`가 이미
|
||||||
|
**`state:Block(blocker) -> state`(새 gated state 반환)** 라는 **메소드**로
|
||||||
|
확정해뒀으므로, `Block`이 내부에서 `self:Gate(blocker의 정책)`을 부르는
|
||||||
|
것으로 끝난다 — `blocker` 객체를 `Apply`에 넘길 일이 없다.
|
||||||
|
- **`__call`은 안 쓴다.** 사용자도 *"이상적이여 보이지는 않음"*이라 했고,
|
||||||
|
타입 쪽 근거가 하나 더 있다 — `__call` 테이블이 Luau에서 `(State<T>) -> U`
|
||||||
|
함수 타입 자리에 그대로 들어가는지가 불확실하다(들어가지 않는 쪽이 유력).
|
||||||
|
`Apply`를 쓸 이유 자체가 없어졌으므로 확인할 필요도 없어졌지만, 혹시
|
||||||
|
되살아나면 `luau-test` 스파이크 한 개로 판정할 것.
|
||||||
|
- **2단 구조는 그대로 유효하다** — 사용자 관찰(*"Gate 의 callback 으로 얻어진
|
||||||
|
emit과, 리턴해낸 클로저가 호출되는걸로 배선은 가능"*) 대로, 바깥 함수가
|
||||||
|
**그 노드의 `emit`을 캡처**하고 반환 클로저가 상류 emit마다 정책을 태운다.
|
||||||
|
`Blocker`처럼 **여러 노드가 공유하는 정책**은 공유 상태를 바깥 객체가 들고,
|
||||||
|
노드별 `emit`만 2단 구조로 받아 등록하면 된다.
|
||||||
3. **`Get()`과의 관계.** `Blocker`는 `:Get()`에 영향이 없고(`base/blocker-plan.md`),
|
3. **`Get()`과의 관계.** `Blocker`는 `:Get()`에 영향이 없고(`base/blocker-plan.md`),
|
||||||
`Debounce`/`Throttle`도 emit-gate로 확정됐다(`base/debounce-throttle-plan.md` §4).
|
`Debounce`/`Throttle`도 emit-gate로 확정됐다(`base/debounce-throttle-plan.md` §4).
|
||||||
`Gate`도 **값이 아니라 통지만 막는다**로 통일하는 게 맞는지 확인 필요 —
|
`Gate`도 **값이 아니라 통지만 막는다**로 통일하는 게 맞는지 확인 필요 —
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,9 @@ State
|
||||||
(이미 확정이라 더 알아낼 게 없다).
|
(이미 확정이라 더 알아낼 게 없다).
|
||||||
- `rawInvalid == false` → **그때만 `sourceList`를 훑는다.** 목적은 하나뿐 —
|
- `rawInvalid == false` → **그때만 `sourceList`를 훑는다.** 목적은 하나뿐 —
|
||||||
**못 받은 emit을 여기서 먼저 받아주는 것**(중간 게이트에 막혀 있었다거나).
|
**못 받은 emit을 여기서 먼저 받아주는 것**(중간 게이트에 막혀 있었다거나).
|
||||||
다른 항목이 발견되면 count를 갱신하고 `rawInvalid = true`로 만든다.
|
다른 항목이 발견되면 `rawInvalid = true`로 만든다.
|
||||||
|
**⚠️ 이때 count를 갱신할지는 열려 있다 — §5의 3번이 소스**(갱신하면 나중에
|
||||||
|
도착하는 진짜 emit이 삼켜져 하류가 영영 통지를 못 받는다).
|
||||||
- **[2026-08-21 사용자 정정]** 앞선 정리는 이 순회 조건을 `rawInvalid == true`
|
- **[2026-08-21 사용자 정정]** 앞선 정리는 이 순회 조건을 `rawInvalid == true`
|
||||||
일 때로 적었는데 **반대**다.
|
일 때로 적었는데 **반대**다.
|
||||||
- 재계산이 끝나면 `rawInvalid = false`. count는 emit을 통과시킬 때 이미
|
- 재계산이 끝나면 `rawInvalid = false`. count는 emit을 통과시킬 때 이미
|
||||||
|
|
@ -144,32 +146,61 @@ count 와 source 를 ref해두는 구현은 문제가 없어보인다. 단순 ra
|
||||||
2. **동적 의존성.** 조건에 따라 다른 상류를 읽는 계산이면 `sourceList`가
|
2. **동적 의존성.** 조건에 따라 다른 상류를 읽는 계산이면 `sourceList`가
|
||||||
보수적 상위집합이 된다 — 틀리진 않고 재계산이 조금 더 잦아질 뿐이다.
|
보수적 상위집합이 된다 — 틀리진 않고 재계산이 조금 더 잦아질 뿐이다.
|
||||||
그대로 감수할지 확인.
|
그대로 감수할지 확인.
|
||||||
3. **⭐ [2026-08-21 확장] 순회로 발견한 변경을 뒤로 emit 할 것인가 —
|
3. **⭐⭐ [2026-08-21 재확장] 순회가 발견한 변경을 어떻게 처분하는가 — 이 안에서
|
||||||
그리고 `Blocker`/`Gate`와의 상호작용.**
|
유일하게 안 닫힌 자리.**
|
||||||
`rawInvalid == false`에서 순회를 돌다 "못 받은 emit"을 발견해
|
|
||||||
`rawInvalid = true`가 됐을 때, **그 자리에서 뒤로 emit 하는가**가 열려 있다.
|
|
||||||
두 안:
|
|
||||||
- **(a) 지연** — 아무것도 안 내려보내고, 나중에 그 소스의 진짜 emit이
|
|
||||||
도착할 때 정상 경로로 처리한다.
|
|
||||||
- **(b) 즉시** — 재계산하라는 지시를 받았고 invalid로 판정됐으니 그때 emit.
|
|
||||||
|
|
||||||
사용자가 (b)의 위험을 먼저 의심했다가 **스스로 안전하다고 정정**했다:
|
**먼저 확정된 것: 순회가 count를 갱신하면 안 된다(또는 갱신하면 반드시
|
||||||
`A → {B, C} → D`에서 `B`가 `C`를 요구하다 `C`의 변경분이 흘러 `D`가 다시
|
emit해야 한다).** 사용자 지적 — *"순회가 발견한거면, count 를 최신으로
|
||||||
깨어나는 것처럼 보이지만, **`D`도 count를 갱신하므로 중복을 삼킨다** —
|
올리기에 나중에 emit 와도 전파가 안 되는 이슈가 생기긴 함."* 실제로 그렇다:
|
||||||
*"안전히 C가 emit 되어도 되는 상황"*.
|
`C`가 순회로 `A`의 변경을 발견해 count를 최신으로 올려두면, 뒤늦게 도착한
|
||||||
|
`A`의 진짜 emit이 `C`에서 **삼켜지고**(count가 같으므로) `C`의 하류는 그
|
||||||
|
에포크를 **영영** 못 받는다. 값은 맞지만 통지가 죽는, **2026-08-14에 폐기된
|
||||||
|
옛 dedup의 "영구 침묵"과 같은 계열의 실패 모드**다
|
||||||
|
(`archive/invalidate-dedup-propagation-reversed.md`).
|
||||||
|
|
||||||
남는 건 게이트 쪽이다. 게이트가 **앞에서 막아둔 emit**을, 뒤에서 `Get()`이
|
두 해법이 있다:
|
||||||
촉발한 순회가 그대로 흘려보낼 수 있다(*"앞에서 막아뒀는데, 뒤에서 필요하다고
|
|
||||||
해서 그대로 흘러버릴 가능성"*). 그래서 (b)를 택하더라도 **실제 block이 풀린
|
- **(b) 순회가 count를 올리고, 그 자리에서 뒤로 emit 한다** — 사용자 제안.
|
||||||
뒤의 emit까지는 기다려야 한다**. 그리고 **게이트가 풀 때 내보내는 emit은
|
통지가 죽지 않는다. 다이아몬드에서 새는 것도 없다(사용자가 스스로 정정한
|
||||||
여러 소스가 섞인 것**이라 어느 소스가 바뀌었는지 지목할 수 없다 — 사용자
|
대로 `D`도 count를 갱신해 중복을 삼킨다). **남는 문제는 게이트뿐** — 아래.
|
||||||
제안은 그 emit이 **`source = nil`을 싣고, `nil`이면 받는 쪽이 전체 목록을
|
- **(c) 순회는 `rawInvalid = true`만 세우고 count는 그대로 둔다** — 에이전트
|
||||||
확인**하는 것. 게이트는 `Get()`은 막지 않고 emit만 유보하므로 이게 성립한다.
|
제안. count의 역할을 **"이 에포크를 내가 실제로 전파했는가"** 하나로
|
||||||
|
유지한다. `Get()`은 `rawInvalid` 덕에 신선하고, 나중에 진짜 emit이 오면
|
||||||
|
count가 다르므로 **정상 경로로** 하류까지 전파된다. **게이트를 새게 하는
|
||||||
|
경로가 아예 생기지 않는다**(순회는 절대 emit하지 않으므로).
|
||||||
|
- 대가: emit이 도착하기 전까지 **`Get()`마다 다시 재계산**한다(매번 순회가
|
||||||
|
같은 차이를 재발견하므로). 일반 경로에선 파동이 같은 틱에 도착하니 창이
|
||||||
|
아주 좁고, 게이트 경로에선 "묶어놨다가 한 번에 푼다"가 목적이라 그 구간에
|
||||||
|
`Get()`이 잦을 이유가 없다.
|
||||||
|
- ⚠️ 단 `blocker:OffWithoutEmit()`처럼 **emit 없이 푸는 경로**에선 count가
|
||||||
|
영원히 안 따라잡아 그 상태가 무기한 남는다 — 그 경로가 count를 밀어주게
|
||||||
|
할지 같이 정해야 한다.
|
||||||
|
|
||||||
|
**게이트 문제(=(b)를 택할 때만 남는다).** 게이트가 앞에서 막아둔 emit을,
|
||||||
|
뒤의 `Get()`이 촉발한 순회가 그대로 흘려보낸다. 사용자 판단은 *"gate 의 경우
|
||||||
|
이것 또한 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` 캐비엇 하나만 같이 정하면 된다.
|
||||||
|
|
||||||
**다만 사용자 판단은 "중간에 게이트가 끼는 건 원래 있던 문제이고, 게이트는
|
**다만 사용자 판단은 "중간에 게이트가 끼는 건 원래 있던 문제이고, 게이트는
|
||||||
보통 최종단에 쓰므로 실질적으로 의미 없어 보인다"**로 정리됐다 — 즉 이
|
보통 최종단에 쓰므로 실질적으로 의미 없어 보인다"**로 이미 나와 있다 — 즉
|
||||||
항목은 **채택을 막는 요소가 아니고**, `nil` emit 규약만 `Gate` 설계
|
(b)를 택해도 채택을 막는 요소는 아니다.
|
||||||
(`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가 죽을 때 항목이
|
||||||
|
|
@ -200,7 +231,9 @@ Ref<T> 로 변환해주는 등"*. 즉 "항상 최신 값을 필드로 들고 있
|
||||||
- **채택 시 같이 확정되는 것: 중복 통지도 접는다**(§3) — 그러면 다이아몬드에서
|
- **채택 시 같이 확정되는 것: 중복 통지도 접는다**(§3) — 그러면 다이아몬드에서
|
||||||
값도 통지도 한 번씩만 간다. **[2026-08-21 정정]** 여기 있던 "노드가
|
값도 통지도 한 번씩만 간다. **[2026-08-21 정정]** 여기 있던 "노드가
|
||||||
`seen`/`computedAt` 두 카운트를 들어야 한다"는 추가 요구는 **철회**됐고,
|
`seen`/`computedAt` 두 카운트를 들어야 한다"는 추가 요구는 **철회**됐고,
|
||||||
대신 확정할 것은 **게이트 해제 emit의 `source = nil` 규약**(§5의 3번)이다.
|
대신 확정할 것은 **순회가 발견한 변경의 처분**(§5의 3번) — 에이전트 권고는
|
||||||
|
"순회는 count를 안 올리고 `rawInvalid`만 세운다"이고, 그러면 `nil` emit
|
||||||
|
규약도 계약 반전도 필요 없어진다.
|
||||||
- 실측은 `luau-test`에 스파이크 하나면 충분하다 — 위 다이아몬드를 그대로 짜서
|
- 실측은 `luau-test`에 스파이크 하나면 충분하다 — 위 다이아몬드를 그대로 짜서
|
||||||
(a) 지금 모델에서 섞인 값이 실제로 관측되는지, (b) 에포크 비교를 넣으면
|
(a) 지금 모델에서 섞인 값이 실제로 관측되는지, (b) 에포크 비교를 넣으면
|
||||||
사라지는지 대조.
|
사라지는지 대조.
|
||||||
|
|
|
||||||
|
|
@ -220,3 +220,29 @@
|
||||||
**해제 emit이 `source = nil`을 싣는 규약**으로 남았다(게이트는 보통 최종단에
|
**해제 emit이 `source = nil`을 싣는 규약**으로 남았다(게이트는 보통 최종단에
|
||||||
쓰므로 채택을 막지 않는다는 판단). 상세는 그 문서의 §2·§5와
|
쓰므로 채택을 막지 않는다는 판단). 상세는 그 문서의 §2·§5와
|
||||||
`qa-request/pre-implementation-qa-round5-followup.md`의 M절.
|
`qa-request/pre-implementation-qa-round5-followup.md`의 M절.
|
||||||
|
|
||||||
|
## 9. 후속 — 순회의 count 갱신 문제 + `Gate`는 `:Apply`가 아니다 (같은 날)
|
||||||
|
|
||||||
|
**(1) 순회가 count를 올리면 통지가 죽는다.** 사용자 지적 — 순회로 발견해
|
||||||
|
count를 최신으로 올려두면 뒤늦게 온 진짜 emit이 삼켜져 하류가 그 에포크를
|
||||||
|
영영 못 받는다(2026-08-14에 폐기된 옛 dedup의 "영구 침묵"과 같은 계열).
|
||||||
|
사용자 해법은 **(b) 순회도 emit 한다**이고, 게이트 경우엔 그 emit을 해제
|
||||||
|
시점까지 민다는 것. 에이전트는 **(c) 순회는 `rawInvalid`만 세우고 count는
|
||||||
|
안 올린다**를 대안으로 냈다 — 원인이 정확히 "순회가 count를 올리는 것"이라
|
||||||
|
그것만 안 하면 통지 죽음도 게이트 누출도 `nil` emit 규약도 안 생긴다.
|
||||||
|
대가는 emit이 올 때까지 `Get()`마다 재계산, 그리고 `OffWithoutEmit` 경로에서
|
||||||
|
count가 안 따라잡는 것. 또 (b)에서 "게이트까지 민다"를 실제로 구현하려면
|
||||||
|
순회하는 노드가 상류 게이트를 알아야 하는데 `sourceList`가 루트로 평탄화돼
|
||||||
|
있어 불가능하고, 유일하게 깔끔한 기제(게이트를 에포크 경계로)는
|
||||||
|
`blocker-plan.md`의 "`:Get()`엔 영향 없음" 확정 계약을 뒤집는다는 걸 짚었다.
|
||||||
|
**미결** — `research/state-epoch-validation.md` §5의 3번이 소스.
|
||||||
|
|
||||||
|
**(2) `Gate`는 `:Apply`가 아니라 State 메소드.** 사용자 확정 — *"state 의
|
||||||
|
전파를 손대는 작업이라 with 처럼 다른 노드가 나는게 맞음."* 경계를 정확히
|
||||||
|
적어두면, `Apply`가 노드를 못 만드는 게 아니라(확정 예시 `capAt(100)`도
|
||||||
|
`:With` 노드를 만든다) **프리미티브는 메소드 / 유저랜드 조합 팩토리는
|
||||||
|
`:Apply`**라는 층위 구분이다. 부수 결론 셋 — `Debounce`/`Throttle`의
|
||||||
|
`state:Apply(...)` 관용구는 그대로 유효(팩토리가 내부에서 `:Gate`를 부르면
|
||||||
|
됨), `Blocker` 배선 문제는 이미 확정된 `state:Block(blocker)` 메소드로
|
||||||
|
자동 해소, `__call`은 안 씀(사용자 선호 + Luau 함수 타입 자리 통과 여부
|
||||||
|
불확실). `research/gate-primitive.md`의 2번이 해소로 갱신됨.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue