design: 게이트 emit 출처를 emit(self)+흡수 집합으로 확정, 재계산 시 count 전부 갱신
/code-review high가 잡은 3건에 대한 사용자 회신 반영.
1) 게이트가 유보했다 내보내는 emit — (c) 채택, 다만 에이전트 안보다 단순한
형태로. 게이트에 자체 count를 주는 대신 흡수한 소스 집합
withheld : {[source]=true} 만 들고 있다가, 풀 때 자기를 출처로 하류에
emit 하고 전파가 동기이므로 반환 뒤 table.clear 한다. 하류는 출처가
GateNode면 그 집합의 소스들에 평소 규칙(1~3)을 적용하고 받은 출처를
그대로 더 아래로 넘긴다. OffWithoutEmit도 안전(다음 진짜 emit이 스스로
낫게 함). 그래서 emit의 출처는 Source | GateNode 둘 다가 된다.
⭐ setup 시그니처는 안 바뀐다 — 흡수 집합을 채우는 건 정책이 아니라
노드이기 때문(노드가 onUpstreamEmit 전후로 정책이 그 자리에서 emit()을
불렀는지만 보면 됨). P절이 "M2 표면에 영향"이라 적은 건 기우였다.
2) 재계산 후 sourceCountMap은 자기가 읽은 상류 전부를 갱신한다(확정).
다만 이걸 제기한 에이전트 근거("A:Set(); Z:Set()이면 같은 값을 두 번
계산")는 사용자가 반증 — 전파가 동기라 A 파동이 완전히 끝난 뒤 Z:Set()이
시작되므로 통지가 두 번 나는 건 중복이 아니라 맞는 동작이다. 전부 갱신이
실제로 값을 하는 자리는 게이트 유보 중 하류가 Get()으로 앞당겨 읽는
경우뿐이고, 그때 해제 통지가 규칙 2(통지만)로 떨어져 재계산을 안 한다.
3) 같이 명문화한 대원칙: 무효화를 결정하는 건 언제나 count 비교지 emit의
도착이 아니다. emit은 "이 원천을 확인해봐"라는 요청일 뿐이라, 통과해도
count가 최신이면 캐시는 유효한 채로 남는다.
남은 열린 항목은 새 노드의 두 맵 초기값(복사 vs 첫 재계산 때 구성)과 그에
종속된 :With 병합 규칙뿐이고, 동기 전파 덕에 차이가 나는 경우가 게이트 유보
중 파생 노드가 생길 때 하나뿐이라 어느 쪽이든 무해 — M3 구현 시 결정.
처리 전량은 round5-followup.md의 Q절. 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
cb838d3172
commit
356a308ce0
6 changed files with 181 additions and 86 deletions
|
|
@ -5,9 +5,9 @@
|
|||
없이 `state:Gate( (emit) -> ()->() )` 처럼 선언되고 마치 Compute 처럼
|
||||
GateNode(ComputeNode 처럼) 생성된다 그리고 Blocker 는 해당 내부 배선을 따른다
|
||||
← 동의합니다 해당 방법대로 확정하면 됩니다."* 구현은 **M2**("게이팅 먼저"
|
||||
결정, `ROADMAP.md`). **남은 것은 아래 "아직 안 정한 것"** — 그중 **4번(유보된
|
||||
emit이 싣는 source)은 `setup` 시그니처를 바꿀 수 있어 M2 착수 전 판단이
|
||||
필요하다**(2026-08-21 `/code-review high` 발견).
|
||||
결정, `ROADMAP.md`). **남은 것은 아래 "아직 안 정한 것"의 생명주기·재진입 계약과 M2
|
||||
범위뿐** — `/code-review high`가 잡았던 4번(유보된 emit이 싣는 출처)은 같은
|
||||
날 `emit(self)` + 흡수 집합으로 닫혔고, **`setup` 시그니처는 안 바뀌었다.**
|
||||
|
||||
**⚠️ 처음 방향이 한 번 바뀌었다.** 신설 당시엔 *"공용 `Gate` 프리미티브를 꺼내고
|
||||
`Blocker`가 그걸 컴포지션한다"*였는데, 확정된 형태는 **프리미티브를 따로 안
|
||||
|
|
@ -116,33 +116,35 @@ end)
|
|||
`base/state-epoch-plan.md`가 이 계약에 **의존**한다 — 그 문서 §5의 3번이
|
||||
"게이트를 에포크 경계로 만드는" 대안을 기각한 이유가 정확히 이 계약을
|
||||
뒤집지 않기 위해서다.
|
||||
4. **⭐⭐ [2026-08-21 신설 — M2 표면에 영향] 게이트가 유보했다 내보내는 emit은
|
||||
어느 source를 싣는가.** 확정된 `setup`은 `(emit: () -> ()) -> (() -> ())`로
|
||||
**양쪽 다 source를 안 받는다.** 그런데 `base/state-epoch-plan.md` §2의 수신
|
||||
규칙 셋은 전부 `[source]` 키로 판정한다 — 게이트가 `A:Set(); Z:Set()`을
|
||||
묶어뒀다가 `blocker:Off()`로 한 번에 내보내면, 그 emit이 **어떤 source도
|
||||
지목하지 못한 채** 하류에 도착한다. 그러면 하류는 3번 규칙으로 **삼켜버리고
|
||||
배치 통지가 통째로 사라진다.** 5라운드 M절이 이미 *"`nil` 규약만 `Gate`
|
||||
설계와 같이 확정하면 된다"*고 짚어뒀는데 표면 확정 때 같이 안 닫혔다
|
||||
(2026-08-21 `/code-review high` 발견).
|
||||
후보 셋 — 어느 쪽이든 `setup`/`emit` 시그니처가 바뀐다:
|
||||
- **(a) `emit(nil)` = 전체 확인.** 받는 쪽이 `sourceCountMap` 전체를 훑는다.
|
||||
M절의 원안. 비용은 2~4칸 순회라 작지만, "판정은 O(1)"이라는 §3 서술에
|
||||
예외가 하나 생긴다.
|
||||
- **(b) 게이트가 유보한 source 집합을 기억했다가 해제 때 각각 emit.**
|
||||
판정은 O(1) 그대로. 대신 유보된 소스가 N개면 하류가 **N번** 통지받아
|
||||
`Blocker`의 "정확히 1회"가 깨진다 — `Blocker` 목적과 정면 충돌이라
|
||||
그대로는 못 쓴다.
|
||||
- **(c) `GateNode` 자신을 source처럼 취급.** 해제 emit이 `emit(self)`를
|
||||
싣고 게이트가 자기 count를 하나 올린다. 하류는 정확히 1회 통지받고
|
||||
판정도 O(1). 대신 하류의 맵에 루트 Source가 아닌 노드가 섞이므로 §2의
|
||||
"루트 Source들의 에포크"라는 서술을 넓혀야 한다.
|
||||
**에이전트 권고는 (c)** — `Blocker` 계약("정확히 1회")과 O(1) 판정을 둘 다
|
||||
지키는 유일한 안이고, "게이트가 하류에게는 새 원천처럼 보인다"는 게 게이트의
|
||||
실제 의미와도 맞는다. 단 `Get()` 계약(통지만 막음)은 그대로 유지된다 —
|
||||
§5-3이 기각한 "게이트를 **에포크 경계**로 만들기"와는 다르다. 그건 하류가
|
||||
루트 Source를 **못 보게** 만드는 안이고, (c)는 루트 Source를 그대로 보면서
|
||||
게이트를 **하나 더** 얹는 것뿐이다.
|
||||
4. **[2026-08-21 해소] 게이트가 유보했다 내보내는 emit — `emit(self)` + 흡수
|
||||
집합.** 문제는 실재했다: 확정 `setup`은 `(emit: () -> ()) -> (() -> ())`라
|
||||
양쪽 다 source를 안 받는데 `base/state-epoch-plan.md`의 수신 규칙은 전부
|
||||
`[source]` 키로 판정하므로, `blocker:Off()`가 묶어뒀던 배치 emit이 아무
|
||||
원천도 못 지목한 채 도착해 **하류에서 삼켜진다**(2026-08-21
|
||||
`/code-review high` 발견).
|
||||
|
||||
**확정 기제**(사용자 안, 에이전트가 냈던 (a) `nil` 전체 확인 / (b) 소스마다
|
||||
개별 emit은 기각 — 각각 O(1) 판정을 깨거나 `Blocker`의 "정확히 1회"를 깬다):
|
||||
|
||||
- `GateNode`가 **자기가 흡수한 소스 집합**을 들고 있는다 —
|
||||
`withheld : { [source] : true }`. 정책이 통과시키지 **않은** 상류 emit의
|
||||
출처가 여기 쌓인다.
|
||||
- 유보를 풀 때 게이트는 **자기 자신을 출처로 실어** 하류에 emit 한다.
|
||||
전파는 **동기**이므로, 그 emit이 반환된 **뒤에** `table.clear(withheld)`
|
||||
하면 하류 전원이 집합을 온전히 본다.
|
||||
- 하류는 출처가 `GateNode`면 그 집합을 순회하며 각 소스에 평소 규칙(1~3)을
|
||||
적용하고, 하나라도 걸리면 **받은 출처를 그대로** 더 아래로 넘긴다
|
||||
(`state-epoch-plan.md` §2).
|
||||
- **`OffWithoutEmit()`로 emit 없이 풀어도 문제가 없다** — 하류의
|
||||
`sourceEmitMap`이 뒤에 남지만, 그 소스의 다음 진짜 emit이 규칙 1/2로
|
||||
걸려 스스로 낫는다.
|
||||
|
||||
**⭐ 그래서 `setup` 시그니처는 안 바뀐다.** 흡수 집합을 채우는 건 정책이
|
||||
아니라 **노드**다 — 노드가 `onUpstreamEmit()`을 부르기 전후로 "정책이 그
|
||||
자리에서 `emit()`을 불렀는가"만 보면 통과/유보를 알 수 있고, 유보였으면
|
||||
그 출처를 `withheld`에 넣는다. 정책은 소스를 몰라도 되고, `Throttle`처럼
|
||||
나중에 타이머에서 `emit()`을 부르는 경우도 그대로 동작한다.
|
||||
|
||||
5. **생명주기.** 게이트 노드가 잡는 자원(타이머/플래그)이 언제 죽는가 —
|
||||
지금 설계대로면 다운스트림이 다 죽으면 GC(팩토리는 weak 추적,
|
||||
`debounce-throttle-plan.md` 5-4). `Gate` 자체에 `Flush`/`Cancel` 같은 표면을
|
||||
|
|
|
|||
|
|
@ -57,20 +57,40 @@ State
|
|||
-- "재계산이 필요하다"는 확정 플래그
|
||||
```
|
||||
|
||||
둘 다 상류에서 복사되고 `:With`에서 합쳐진다.
|
||||
노드가 이 두 맵을 **언제 어떻게 갖게 되는지**(생성 시 상류에서 복사 vs 첫
|
||||
재계산 때 구성)는 §5의 7번 — 관측 가능한 차이는 거의 없지만 아직 안 못박았다.
|
||||
|
||||
**⭐ 대원칙 — 무효화를 결정하는 건 언제나 count 비교지 emit의 도착이 아니다.**
|
||||
emit은 **"이 원천을 확인해봐"** 라는 요청일 뿐이다. 그래서 emit이 통과해도
|
||||
count가 이미 최신이면 **캐시는 유효한 채로 남는다**(아래 2번 규칙). 사용자
|
||||
정리(2026-08-21): *"emit 자체가 invalid 하게 만드는 직접 트리거는 아니라서,
|
||||
(이미 count 가 최신이면) 캐시가 유효하다."* 이 문서의 나머지 규칙은 전부 이
|
||||
원칙의 따름정리다.
|
||||
|
||||
- `Source:Set()`/`:Emit()`은 자기 count를 증가시킨다.
|
||||
- **`emit`은 발행한 source만 전달한다**(값도, count도 안 싣는다) — 받는 쪽이
|
||||
그 source의 count 필드를 **그때그때 라이브로** 읽는다. 그래서 "에포크 5의
|
||||
emit" 같은 건 없고 emit은 그냥 **"이 소스를 확인해봐"** 라는 신호다.
|
||||
- **emit을 받았을 때** — 판정은 O(1)이다, 그 소스 항목 하나만 본다:
|
||||
- **`emit`은 값을 안 싣고 count도 안 싣는다 — 싣는 건 "이 통지의 출처" 하나**다.
|
||||
받는 쪽이 거기서 count를 **그때그때 라이브로** 읽는다. 그래서 "에포크 5의
|
||||
emit" 같은 건 없다. 출처로 올 수 있는 것은 **둘**이다
|
||||
(**[2026-08-21 확장]** — 원래는 `Source`뿐이었다):
|
||||
- **`Source`** — 그 소스 하나를 확인하라.
|
||||
- **`GateNode`** — 그 게이트가 **흡수해뒀던 소스 집합**을 확인하라
|
||||
(`base/gate-plan.md`의 4번). 게이트가 `blocker:Off()` 등으로 유보를 풀 때
|
||||
쓴다.
|
||||
**출처는 전파 도중 바뀌지 않는다** — 중간 노드는 자기를 끼워넣지 않고 받은
|
||||
출처를 그대로 아래로 넘긴다(소스 emit이 원래 그러듯이).
|
||||
- **emit을 받았을 때** — 출처가 `Source`면 판정은 O(1)이다, 그 항목 하나만 본다:
|
||||
1. `sourceCountMap[source] ~= source.count` → 둘 다 `source.count`로 갱신하고
|
||||
`rawInvalid = true`를 세운 다음 **뒤로 emit** 한다.
|
||||
2. 같은데 `sourceEmitMap[source] ~= source.count` → **값은 이미 최신이지만
|
||||
통지는 아직 안 나갔다**(아래 순회가 앞질러 흡수한 경우). `sourceEmitMap`만
|
||||
통지는 아직 안 나갔다**(아래 순회가 앞질러 흡수했거나, 게이트가 붙들고
|
||||
있는 동안 하류가 `Get()`으로 앞당겨 읽은 경우). `sourceEmitMap`만
|
||||
갱신하고 **뒤로 emit** 한다 — `rawInvalid`는 안 건드린다.
|
||||
3. 둘 다 같으면 → **삼킨다.**
|
||||
- **다른 소스 항목은 건드리지 않는다** — 그 소스들은 자기가 직접 emit 하므로.
|
||||
- **출처가 `GateNode`면** 그 게이트의 흡수 집합을 순회하며 **각 소스에 위 1~3을
|
||||
그대로 적용**하고, 하나라도 1번이나 2번에 걸렸으면 **받은 출처(그 게이트)를
|
||||
그대로** 뒤로 넘긴다. 전부 3번이면 삼킨다 — 다이아몬드에서 같은 해제 통지가
|
||||
두 번 도착해도 두 번째가 접히는 건 소스 emit과 똑같다.
|
||||
- **재계산 판정**:
|
||||
- `rawInvalid == true` → 그냥 재계산한다. 순회할 이유가 없다(이미 확정).
|
||||
- `rawInvalid == false` → **그때만 `sourceCountMap`을 훑는다.** 목적은 하나뿐 —
|
||||
|
|
@ -80,7 +100,15 @@ State
|
|||
- **⭐ 순회는 `sourceEmitMap`을 건드리지 않고, 뒤로 emit 하지도 않는다.**
|
||||
그래서 나중에 진짜 emit이 도착하면 위 2번으로 걸려 **하류까지 정상적으로
|
||||
전파된다**(사용자: *"emit 바로 안하고 상류가 emit 해줄 때 까지 기다립니다"*).
|
||||
- 재계산이 끝나면 `rawInvalid = false`.
|
||||
- **재계산이 끝나면 `rawInvalid = false`, 그리고 `sourceCountMap`은 자기가 읽은
|
||||
상류 전부에 대해 갱신한다**(**[2026-08-21 확정]** — 발행 소스 항목만 갱신하는
|
||||
게 아니다). 맵의 뜻이 "내 값이 이 소스에 대해 최신인가"이므로, 방금 계산한
|
||||
값은 정의상 **모든** 상류에 대해 최신이다. `sourceEmitMap`은 **안 건드린다** —
|
||||
계산은 통지가 아니다.
|
||||
- 이게 실제로 갈리는 자리는 **게이트가 붙들고 있는 동안 하류가 `Get()`으로
|
||||
앞당겨 읽는 경우**다. 그때 `sourceCountMap`이 앞서 있으므로, 나중에 게이트가
|
||||
풀며 보내는 통지는 위 2번(통지만)으로 떨어져 **같은 값을 다시 계산하지
|
||||
않는다.**
|
||||
- count가 싫다면 `[source] -> {}` 처럼 **유니크 테이블 identity**로 같은 판정이
|
||||
가능하다(사용자 대안).
|
||||
|
||||
|
|
@ -223,28 +251,31 @@ State
|
|||
`base/source-state-plan.md`의 전파 모델 절은 **채택과 함께 그렇게 다시
|
||||
썼다.**
|
||||
|
||||
7. **⭐ [2026-08-21 신설, `/code-review high`] 두 맵의 초기값·병합·재계산 시
|
||||
갱신 범위 — M3 착수 전 필요.** §2가 "둘 다 상류에서 복사되고 `:With`에서
|
||||
합쳐진다"고만 적고 세 자리를 안 정했다:
|
||||
- **(a) 재계산이 끝났을 때 `sourceCountMap`을 어디까지 갱신하나.** §2는
|
||||
`rawInvalid = false`만 말한다. 그런데 규칙 1은 **발행 소스 항목만**
|
||||
건드리므로, `D`가 `A`,`Z`에 의존하고 `A:Set(); Z:Set()`이 연달아 오면 —
|
||||
`A`의 emit으로 재계산된 `D`의 값은 **이미 `Z`의 새 값을 포함**하는데
|
||||
`sourceCountMap[Z]`는 옛 값이라, `Z`의 emit이 규칙 1에 걸려 **같은 값을
|
||||
또 계산**한다. **에이전트 권고: 재계산은 자기가 실제로 읽은 상류 전부에
|
||||
대해 `sourceCountMap`을 갱신한다**(맵의 뜻이 "내 값이 이 소스에 대해
|
||||
최신인가"이므로 이게 직독이다). 그러면 `Z`의 emit은 규칙 2로 떨어져
|
||||
**통지는 나가되 재계산은 안 한다** — 통지가 나가는 건 옳다, 하류는 아직
|
||||
`Z`의 에포크를 못 봤으므로.
|
||||
- **(b) 새 노드가 생길 때 두 맵의 초기값.** "상류에서 복사"를 문자 그대로
|
||||
하면, 순회로 `sourceCountMap`만 앞당겨진 상류에서 파생된 새 노드가 **그
|
||||
지연분까지 상속**해야 규칙 2가 성립한다. **에이전트 권고: 복사가 아니라
|
||||
"첫 재계산 때 자기가 읽은 상류들의 맵을 합쳐 구성"** — 그러면 새 노드는
|
||||
`sourceEmitMap == sourceCountMap`인 깨끗한 상태로 시작하고 (a)와도 맞물린다.
|
||||
다만 이건 사용자 원안의 "상류에서 복사" 표현을 바꾸는 것이라 **확인 필요.**
|
||||
- **(c) `:With` 병합에서 두 상류가 같은 소스에 다른 count를 들고 있을 때.**
|
||||
(b)를 택하면 자동으로 "그때의 라이브 count"로 통일돼 문제가 사라진다.
|
||||
(b)를 안 택하면 명시 규칙이 필요하다(더 큰 쪽? 더 작은 쪽?).
|
||||
7. **[2026-08-21 — (a)는 해소, (b)/(c)만 남음] 두 맵의 초기값·병합·재계산 시
|
||||
갱신 범위.**
|
||||
- **(a) 재계산 후 `sourceCountMap` 갱신 범위 — 해소.** **자기가 읽은 상류
|
||||
전부를 갱신한다**(사용자: *"invalid 에 대한 계산을 위한 count 테이블은
|
||||
단순히 전부 업데이트 하는건 맞아보입니다"*). §2에 반영 완료.
|
||||
- ⚠️ 이 항목을 제기할 때 든 근거("`A:Set(); Z:Set()`이면 같은 값을 두 번
|
||||
계산한다")는 **틀렸었다.** 전파는 동기라 `A:Set()`의 파동이 **완전히
|
||||
끝난 뒤에** `Z:Set()`이 시작되므로, 그 사이 재계산은 `Z`의 옛 값을 읽는
|
||||
게 맞고 통지가 두 번 나는 것도 맞다(사용자 정정: *"싱크라 set 의 emit 이
|
||||
전부 전파 된 다음 z:set 이라 두번 나는게 맞긴 해요"*). 전부 갱신이
|
||||
실제로 값을 하는 자리는 **게이트가 붙들고 있는 동안 하류가 `Get()`으로
|
||||
앞당겨 읽는 경우**뿐이다 — §2의 재계산 항목이 소스.
|
||||
- **(b) 새 노드의 두 맵 초기값 — 열림(낮은 위험).** 원안은 "상류에서 복사".
|
||||
대안은 "첫 재계산 때 자기가 읽은 상류들의 맵을 합쳐 구성". **동기 전파
|
||||
덕분에 둘의 차이가 나타나는 경우는 하나뿐**이다 — 게이트가 붙들고 있는
|
||||
동안 파생 노드가 새로 생기는 경우. 그때 복사본은 상류의 "아직 안 던진"
|
||||
지연분을 물려받아 해제 시 통지를 한 번 더 받고(무해), 첫-재계산본은 이미
|
||||
최신이라 삼킨다(이것도 무해 — 새 노드의 Observer는 등록 즉시 1회 관측으로
|
||||
현재 값을 이미 봤다). **에이전트 권고: 첫 재계산 때 구성** — (c)가 자동으로
|
||||
사라지고 "값을 계산한 시점의 스냅샷"이라는 맵의 뜻과 일치한다.
|
||||
- **(c) `:With` 병합에서 두 상류가 같은 소스에 다른 count를 들 때 — (b)에
|
||||
종속.** (b)에서 "첫 재계산 때 구성"을 택하면 그 시점 라이브 count로
|
||||
통일돼 병합 규칙 자체가 필요 없다. "복사"를 택하면 명시 규칙이 필요하다
|
||||
(`sourceCountMap`은 더 큰 쪽, `sourceEmitMap`은 **더 작은 쪽** — 안 던진
|
||||
게 하나라도 있으면 안 던진 것으로 봐야 통지가 안 죽는다).
|
||||
|
||||
## 6. 곁가지 — 폴링용 sugar
|
||||
|
||||
|
|
|
|||
|
|
@ -1024,3 +1024,56 @@ Observer가 이제 변경당 **1회**만 울어야 하므로 핵심 assert가
|
|||
- `luau-test/STATUS.md`: `05` 이동이 반영 안 된 개수 3곳(`done/` 19→17·18건→17건, `rewrite-required/` 4→6, 런타임 13→12)과 "`05`가 다시 돌아왔다"는 같은 파일 안의 모순 문장.
|
||||
- `comparison-fusion-vide.md`: 배너 바로 위 본문("신호는 두 번 도착해도")이 배너와 어긋나던 것.
|
||||
- 이 파일 N절과 `README.md`가 가리키던 `research/` 옛 경로.
|
||||
|
||||
---
|
||||
|
||||
# Q절 — P절 3건에 대한 사용자 회신 (2026-08-21)
|
||||
|
||||
## Q-1. 게이트 emit의 출처 — `emit(self)` + 흡수 집합 (P절 High-1 해소)
|
||||
|
||||
사용자가 (c)를 채택하되 **에이전트 안보다 더 단순한 형태**로 확정했다.
|
||||
에이전트 안은 게이트에 자체 count를 부여하려 했는데, 그럴 필요 없이
|
||||
**흡수한 소스 집합**만 들고 있으면 된다:
|
||||
|
||||
> *"emit 으로 받은 것이 만일 gate node 라면, gate 가 흡수했었던 변경 source
|
||||
> 들이 `[source]=true` 로 담겨있는 곳이 있다면, emit 은 싱크이기 때문에 하류를
|
||||
> emit 해준 다음 해당 흡수했던 source 들의 셋을 `table.clear` 해주면 됩니다.
|
||||
> 그리고 하류에서는 emit 에 gate node 가 오면 해당 source 들의 최신 count 를
|
||||
> 자신 emit 기준 카운트로 전부 업데이트 하면 됩니다."*
|
||||
|
||||
- **동기 전파가 전제** — 하류 전파가 끝난 **뒤에** 집합을 비우므로 하류
|
||||
전원이 온전한 집합을 본다.
|
||||
- **`OffWithoutEmit()`도 안전** — 하류 `sourceEmitMap`이 뒤에 남지만 다음
|
||||
진짜 emit이 규칙 1/2로 걸려 스스로 낫는다(사용자: *"emit 없이 off 되어도,
|
||||
큰 문제는 안 보입니다"*).
|
||||
- **그래서 `Emit`의 출처는 `Source | GateNode` 둘 다**가 된다 — 소스면 그
|
||||
count를 바로 보고, 게이트면 흡수 집합을 편다.
|
||||
- **⭐ `setup` 시그니처는 안 바뀐다**(에이전트 확인) — 집합을 채우는 건
|
||||
정책이 아니라 **노드**다. 노드가 `onUpstreamEmit()` 전후로 "정책이 그
|
||||
자리에서 `emit()`을 불렀는가"만 보면 통과/유보를 알 수 있고, `Throttle`처럼
|
||||
나중에 타이머에서 부르는 경우도 그대로 동작한다. **P절이 "M2 표면에 영향"
|
||||
이라 적었던 건 결과적으로 기우였다.**
|
||||
|
||||
## Q-2. 재계산 시 count 전부 갱신 — 확정, 다만 제기 근거는 틀렸었음
|
||||
|
||||
사용자가 P절 Med-2/Med-3을 *"아닌 것 같습니다"*로 판정하면서 **에이전트의
|
||||
근거를 정정**했다:
|
||||
|
||||
> *"싱크라 set 의 emit 이 전부 전파 된 다음 z:set 이라 두번 나는게 맞긴 해요.
|
||||
> 그런데 invalid 에 대한 계산을 위한 count 테이블은 단순히 전부 업데이트
|
||||
> 하는건 맞아보입니다. emit 이 패스되더라도, emit 자체가 invalid 하게 만드는
|
||||
> 직접 트리거는 아니라서, (이미 count 가 최신이면) 캐시가 유효하다."*
|
||||
|
||||
- **틀렸던 것**: "`A:Set(); Z:Set()`이면 같은 값을 두 번 계산한다"는 P절의
|
||||
근거. 전파가 **동기**라 `A` 파동이 완전히 끝난 뒤 `Z:Set()`이 시작되므로
|
||||
그 사이 재계산이 `Z`의 옛 값을 읽는 게 맞고, 통지가 두 번 나는 것도 맞다.
|
||||
- **그래도 채택되는 것**: `sourceCountMap`은 재계산 때 **자기가 읽은 상류
|
||||
전부** 갱신. 실제로 값을 하는 자리는 **게이트가 붙들고 있는 동안 하류가
|
||||
`Get()`으로 앞당겨 읽는 경우** — 그때 뒤이은 해제 통지가 규칙 2(통지만)로
|
||||
떨어져 같은 값을 다시 계산하지 않는다.
|
||||
- **같이 명문화된 대원칙**: *"무효화를 결정하는 건 언제나 count 비교지 emit의
|
||||
도착이 아니다."* `state-epoch-plan.md` §2 머리에 넣었다.
|
||||
- **남은 것**(낮은 위험, M3 구현 시): 새 노드의 두 맵 초기값(복사 vs 첫
|
||||
재계산 때 구성)과 그에 종속된 `:With` 병합 규칙. 동기 전파 덕에 둘의 차이가
|
||||
나는 경우는 **게이트 유보 중 파생 노드가 새로 생길 때** 하나뿐이고 어느
|
||||
쪽이든 무해하다. §5 7번.
|
||||
|
|
|
|||
|
|
@ -207,25 +207,19 @@
|
|||
빈도가 다르다.** 해법은 있다 — reconcile이 `pos`처럼 **절대 offset도 러닝
|
||||
누적**으로 들고 다니면 O(n)(그게 `mountSlotTree`가 이미 하는 방식). 그렇게
|
||||
할지, 아니면 실측 전엔 그냥 둘지 판단 필요.
|
||||
- **⭐⭐ [신설, 2026-08-21 `/code-review high`] 게이트가 유보했다 내보내는
|
||||
emit은 어느 source를 싣는가 — M2 착수 전 필요.** 확정된 `setup`은
|
||||
`(emit: () -> ()) -> (() -> ())`라 **양쪽 다 source를 안 받는데**,
|
||||
`base/state-epoch-plan.md`의 수신 규칙은 전부 `[source]` 키로 판정한다.
|
||||
그래서 `blocker:Off()`가 묶어뒀던 배치 emit이 하류에서 **삼켜져 통지가
|
||||
통째로 사라진다.** 후보는 (a) `emit(nil)` = 전체 확인, (b) 유보한 소스마다
|
||||
각각 emit(→ `Blocker`의 "정확히 1회"가 깨짐), (c) `GateNode` 자신을
|
||||
source처럼 취급해 `emit(self)`. **에이전트 권고는 (c)** — 1회 통지와 O(1)
|
||||
판정을 둘 다 지킨다. 어느 쪽이든 `setup`/`emit` 시그니처가 바뀌므로 M2
|
||||
전에 정해야 한다. 상세는 `base/gate-plan.md`의 4번.
|
||||
- **⭐ [신설, 2026-08-21 `/code-review high`] State 에포크 — 두 맵의 초기값·
|
||||
병합·재계산 시 갱신 범위 — M3 착수 전 필요.** 세 자리가 안 정해져 있다:
|
||||
(a) 재계산이 끝나면 `sourceCountMap`을 **자기가 읽은 상류 전부**에 대해
|
||||
갱신할 것인가(안 그러면 다중 소스 배치에서 같은 값을 두 번 계산한다),
|
||||
(b) 새 노드의 두 맵을 "상류에서 복사"할 것인가 아니면 **첫 재계산 때
|
||||
구성**할 것인가(원안은 복사인데, 순회가 앞당겨둔 지연분까지 상속해야 규칙이
|
||||
성립한다), (c) `:With`에서 두 상류가 같은 소스에 다른 count를 들 때의 병합
|
||||
규칙. **에이전트 권고는 (a) 전부 갱신 + (b) 첫 재계산 때 구성**이고, 그러면
|
||||
(c)는 자동 해소된다. 상세는 `base/state-epoch-plan.md`의 §5 7번.
|
||||
- **[해소, 2026-08-21 같은 날] 게이트가 유보했다 내보내는 emit이 싣는 출처 —
|
||||
`emit(self)` + 흡수 집합.** `GateNode`가 흡수한 소스를 `withheld` 집합에
|
||||
들고 있다가, 풀 때 **자기를 출처로** 하류에 emit 하고 (동기 전파라) 반환 뒤
|
||||
`table.clear`한다. 하류는 출처가 게이트면 그 집합의 소스들에 평소 규칙을
|
||||
적용한다. **`setup` 시그니처는 안 바뀐다** — 집합을 채우는 건 정책이 아니라
|
||||
노드이기 때문. `base/gate-plan.md`의 4번, `base/state-epoch-plan.md` §2.
|
||||
- **[낮은 위험, 2026-08-21] State 에포크 — 새 노드의 두 맵 초기값(복사 vs 첫
|
||||
재계산 때 구성)과 그에 종속된 `:With` 병합 규칙.** 재계산 시 갱신 범위(전부
|
||||
갱신)는 같은 날 **확정**됐고, 그걸 제기할 때 든 근거는 **동기 전파를 놓친
|
||||
것이라 사용자가 정정**했다. 남은 (b)/(c)는 **동기 전파 덕에 차이가 나는
|
||||
경우가 게이트 유보 중 새 노드가 생길 때 하나뿐**이고 어느 쪽이든 무해하다 —
|
||||
권고는 "첫 재계산 때 구성". M3 구현 시 정하면 된다.
|
||||
`base/state-epoch-plan.md`의 §5 7번.
|
||||
- **[해소, 2026-08-21] 공용 게이트 노드의 이름과 표면 — `state:Gate(setup)`
|
||||
메소드 + `GateNode`로 확정.** 탑레벨 프리미티브는 안 만들고, `Blocker`는
|
||||
`state:Block(blocker)` 안에서 그 배선을 쓴다(사용자: *"Gate 는 따로
|
||||
|
|
|
|||
|
|
@ -294,3 +294,18 @@ O절 커밋 직후 사용자가 돌린 리뷰에서 12건이 나왔고 전부
|
|||
방금 옮긴 파일을 가리키는 새 텍스트). `conventions.md`의 "`/code-review`는
|
||||
감사자를 대체하지 않는다" 항목이 다시 확인된 셈. 전량은
|
||||
`qa-request/pre-implementation-qa-round5-followup.md`의 P절.
|
||||
|
||||
## 13. `/code-review` 3건에 대한 사용자 회신 — 2건 확정, 1건은 근거가 반증됨
|
||||
|
||||
게이트 emit의 출처는 **`emit(self)` + 흡수 집합**으로 닫혔다. 에이전트가
|
||||
권고한 (c)를 사용자가 더 단순화한 형태 — 게이트에 자체 count를 주는 대신
|
||||
`withheld : {[source]=true}`만 들고 있다가 풀 때 자기를 출처로 emit 하고,
|
||||
**동기 전파**라 반환 뒤 `table.clear`하면 하류 전원이 집합을 본다. 검토
|
||||
결과 **`setup` 시그니처는 안 바뀐다** — 집합을 채우는 건 정책이 아니라
|
||||
노드이기 때문(P절이 "M2 표면에 영향"이라 적은 건 기우였다).
|
||||
|
||||
에포크 쪽은 "재계산 때 count 전부 갱신"만 확정되고, 그걸 제기한 에이전트
|
||||
근거는 **사용자가 반증**했다 — 전파가 동기라 `A:Set()` 파동이 끝난 뒤에야
|
||||
`Z:Set()`이 시작되므로 통지가 두 번 나는 건 중복이 아니라 맞는 동작이다.
|
||||
그 과정에서 대원칙 하나가 명문화됐다: **무효화를 결정하는 건 언제나 count
|
||||
비교지 emit의 도착이 아니다.** 전량은 followup의 Q절.
|
||||
|
|
|
|||
|
|
@ -11,14 +11,14 @@
|
|||
`state:Gate(setup)` + `GateNode`로(`base/gate-plan.md`), State의
|
||||
재계산/전파 판정은 **소스 에포크 비교** 채택으로(`base/state-epoch-plan.md`,
|
||||
구현은 M3) 닫혔다. 두 문서 모두 `research/`가 아니라 **`base/`**에 있다.
|
||||
**⚠️ [2026-08-21 정정 — 같은 날 `/code-review high`] "막는 항목이 없다"는
|
||||
너무 이르다.** 게이트가 유보했다 내보내는 emit이 **어느 source를 싣는지**가
|
||||
안 정해져 있고(확정된 `setup`은 source를 안 받는데 에포크 수신 규칙은
|
||||
`[source]` 키로 판정한다 — 그대로면 `blocker:Off()`의 배치 통지가 하류에서
|
||||
삼켜진다), 이건 `setup` 시그니처를 바꿀 수 있어 **M2 착수 전 판단이
|
||||
필요하다**(`question.md` 3번, `base/gate-plan.md`의 4번). M3 쪽으로는 두
|
||||
맵의 초기값·병합·재계산 시 갱신 범위가 열려 있다(`state-epoch-plan.md`
|
||||
§5 7번). 그 외 남은 것은 판단이 아니라 구현 시 정할 것들 — `Gate`의
|
||||
**[2026-08-21 경위]** 같은 날 `/code-review high`가 "게이트가 유보했다
|
||||
내보내는 emit이 어느 출처를 싣는지"가 안 정해진 걸 잡아 한때 M2 항목으로
|
||||
되돌아갔으나, **사용자가 그 자리에서 `emit(self)` + 흡수 집합으로
|
||||
확정**했다(`setup` 시그니처는 안 바뀜 — `base/gate-plan.md` 4번). 같이
|
||||
제기됐던 에포크 쪽 세 자리 중 "재계산 시 count 전부 갱신"도 확정,
|
||||
나머지 둘(새 노드 맵 초기값 / `:With` 병합)은 **M3 구현 시 정하면 되는
|
||||
낮은 위험 항목**으로 남았다(`state-epoch-plan.md` §5 7번).
|
||||
그 외 남은 것은 판단이 아니라 구현 시 정할 것들 — `Gate`의
|
||||
생명주기·재진입 계약, M2에 `Blocker`까지 넣을지, 스파이크 `05` 재작성
|
||||
(`luau-test/STATUS.md`).
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue