From 3b46d5ef68346a259104ea59a0c092126aed0cd7 Mon Sep 17 00:00:00 2001 From: qwreey Date: Fri, 21 Aug 2026 22:14:16 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EB=91=90=20=EB=B2=88=EC=A7=B8=20/code-r?= =?UTF-8?q?eview=20high=207=EA=B1=B4=20=E2=80=94=20=EC=9C=A0=EC=8B=A4=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EB=91=98=20=ED=8F=AC=ED=95=A8=205?= =?UTF-8?q?=EA=B1=B4=20=EC=88=98=EC=A0=95,=202=EA=B1=B4=20=EC=8A=B9?= =?UTF-8?q?=EA=B2=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit High 둘은 실제 유실 경로였다. - withheld를 페이로드로 그대로 넘기면 재진입에 깨진다. 전파 중 Observer가 Set을 불러 게이트에 재진입하면 중첩 전파가 끝나며 table.clear가 돌아 바깥 전파의 남은 갈래가 빈 집합을 받는다. 전파 직전에 새 테이블로 스왑해 배치를 떼어내고 그 배치를 페이로드로 넘기는 것으로 수정. - OffWithoutEmit()이 집합을 안 비웠다. Dispatch.drive의 배치 게이팅이 매 프레임 On() -> OffWithoutEmit()을 돌므로 집합이 단조 증가하고(weak 설계와도 충돌) 나중에 아무 소스나 통과할 때 폐기분이 같이 실려 나간다. 그 경로도 비우도록 확정하고 withheld를 weak key로 명시. 나머지 수정 셋: - "무조건 withheld에 넣는다"가 수신 규칙 1~3을 건너뛴다는 뜻으로 읽히던 것을 "정책의 통과/유보와 무관하게"로 명시(그대로 두면 다이아몬드에서 Throttle 정책이 두 번 돌아 유령 trailing emit이 나간다) - luau-test/STATUS.md의 "런타임 12개"가 이미 나간 04/10/19를 포함한 옛 총계에서 이어져 온 수라 실제(9건)와 안 맞던 것 - followup D절 색인 표가 삭제된 research/ 두 문서를 현재형으로 서술하던 것 열린 항목으로 승격 둘(question.md의 "남은 것은 사용자 판단이 아니다"도 정정): - Gate 재진입 계약 — 스냅샷으로 유실은 막았으나 정책 안 재귀 호출 계약은 미정 - 소스 없는 emit(빈 배치) — 정책이 상류 신호와 무관하게 emit()을 부르면 빈 배치가 나가 하류가 조용히 삼킨다. Effect(fn, ...deps) 설치 구간 억제 용례가 정확히 이 모양이라 그대로는 성립하지 않음. 권고는 "빈 배치 = 무조건 통지" 처리 전량은 round5-followup.md의 T절. doc-check.py ERROR 0. Co-authored-by: qwreey Claude-Session: https://claude.ai/code/session_01TiW21rnti9SbLgF6twtn6D --- .claude/base/gate-plan.md | 63 +++++++++++++++---- .claude/base/state-epoch-plan.md | 18 +++--- .claude/luau-test/STATUS.md | 7 ++- .../pre-implementation-qa-round5-followup.md | 43 ++++++++++++- .claude/question.md | 6 +- ...21-02-qa-round5-and-gate-epoch-research.md | 16 +++++ 6 files changed, 128 insertions(+), 25 deletions(-) diff --git a/.claude/base/gate-plan.md b/.claude/base/gate-plan.md index 499e16d..f3a942f 100644 --- a/.claude/base/gate-plan.md +++ b/.claude/base/gate-plan.md @@ -127,14 +127,36 @@ end) 개별 emit은 기각 — 각각 O(1) 판정을 깨거나 `Blocker`의 "정확히 1회"를 깬다): - `GateNode`가 **자기를 거쳐간 소스 집합**을 들고 있는다 — - `withheld : { [source] : true }`. + `withheld : { [source] : true }`, **weak key**(소스 맵과 같은 이유, + `state-epoch-plan.md` §5의 5번). - **⭐ [2026-08-21 단순화] 통과와 유보를 구분하지 않는다.** 상류 emit이 오면 **정책을 실행하기 전에 무조건** 그 출처를 `withheld`에 넣고, 그 - 다음 정책을 실행한다. 정책이 `emit()`을 부르면 게이트가 **자기 자신을 - 출처로** 하류에 전파하고, 전파는 **동기**이므로 반환된 **뒤에** - `table.clear(withheld)` 한다. 정책이 안 부르면 집합에 그대로 쌓인다. - - **그래서 게이트는 언제나 자기를 출처로 낸다** — 그냥 통과시킬 때도 상류 - 출처를 그대로 넘기지 않는다. 사용자: *"후행 노드들은 한개가 지연된거로 + 다음 정책을 실행한다. 정책이 `emit()`을 부르면 게이트가 하류에 전파하고, + 안 부르면 집합에 그대로 쌓인다. + - **⚠️ [2026-08-21 `/code-review high`] "무조건"은 *정책의 통과/유보와 + 무관하게*라는 뜻이지 *수신 규칙을 건너뛴다*는 뜻이 아니다.** 게이트도 + 평범한 노드처럼 `state-epoch-plan.md` §2의 규칙 1~3을 **먼저** 적용하고, + 3번(둘 다 같음)으로 삼켜진 emit은 **정책도 안 돌고 집합에도 안 + 들어간다.** 안 그러면 다이아몬드(`A→B→G`, `A→C→G`)에서 `A:Set()` 한 + 번에 정책이 두 번 돌아, `Throttle`의 leading 통과 직후 두 번째 emit이 + `pending`을 세워 **이미 전달한 변경에 대한 유령 trailing emit**이 + 나간다. + - **⭐⭐ [2026-08-21 `/code-review high` — 재진입 안전] 전파 페이로드는 + `withheld` 자체가 아니라 그 자리에서 떼어낸 스냅샷이다.** "전파가 반환된 + 뒤에 `table.clear`"는 **재진입이 없을 때만** 성립한다 — `G`가 두 갈래로 + 전파하다 첫 갈래의 Observer가 `B:Set()`을 부르면(quad는 Observer 안 + `Set`을 허용) 그 파동이 `G`에 다시 도달해 중첩 전파가 끝나며 `clear`가 + 돌고, **바깥 전파의 남은 갈래는 빈 집합을 받아** 원래 변경을 영영 못 + 받는다. 그래서 전파 직전에 **스왑**한다: + ``` + local batch = self._withheld + self._withheld = {} -- 새 테이블. clear가 아니다 + emitDownstream(self, batch) -- 떼어낸 batch를 페이로드로 넘긴다 + ``` + 하류가 순회하는 것은 `gate._withheld`가 아니라 **받은 `batch`** 다. + 재진입이 나도 그 파동은 새 테이블에 쌓이므로 바깥 전파와 안 섞인다. + - **그래서 게이트는 언제나 자기(와 그 배치)를 출처로 낸다** — 그냥 + 통과시킬 때도 상류 출처를 그대로 넘기지 않는다. 사용자: *"후행 노드들은 한개가 지연된거로 생각이 될 수 있겠지만, 사실 여기서 지연과 비지연을 구분할 이유가 없습니다."* 하류가 보는 차이는 집합의 원소가 하나냐 여럿이냐뿐이고 판정 규칙은 완전히 같다. @@ -148,7 +170,7 @@ end) 유보했다가 나중에 풀면 그때는 **빈 집합**을 내보내게 되어 변경이 통째로 증발한다. 그래서 수신 시점에 **풀어서 옮겨 담아야** 한다: ``` - -- 출처가 Source면 그 하나를, GateNode면 그 집합 전부를 편다 + -- 출처가 Source면 그 하나를, 게이트 배치면 그 배치 전부를 편다 for source in unfold(origin) do self._withheld[source] = true end @@ -160,9 +182,15 @@ end) (집합 전체에 대해 한꺼번에). 그래야 "내가 하류로 던진 에포크"라는 맵의 뜻이 게이트에서도 참이 된다 — 유보 중 같은 에포크가 다른 경로로 또 오면 규칙 2로 걸려 정책을 한 번 더 태우는데, 이미 집합에 있으므로 무해하다. - - **`OffWithoutEmit()`로 emit 없이 풀어도 문제가 없다** — 하류의 - `sourceEmitMap`이 뒤에 남지만, 그 소스의 다음 진짜 emit이 규칙 1/2로 - 걸려 스스로 낫는다. + - **⭐ [2026-08-21 `/code-review high`] emit 없이 푸는 경로는 집합을 + *버려야* 한다.** `blocker:OffWithoutEmit()`은 정의상 "밀린 전파를 버리며 + 끈다"(`base/blocker-plan.md`)이므로, 그 경로도 **`withheld`를 비운다** + (전파는 안 하고 새 테이블로 스왑). 안 그러면 `Dispatch.drive`의 배치 + 게이팅이 매 프레임 `On()` → … → `OffWithoutEmit()`을 도는 동안 집합이 + **단조 증가**하고, 나중에 아무 소스나 한 번 통과하는 순간 **버리기로 했던 + 옛 소스들이 같이 실려 나가** 하류가 폐기된 통지로 무효화된다. + - 그렇게 비우고 나면 하류의 `sourceEmitMap`은 뒤에 남지만, 그 소스의 다음 + 진짜 emit이 규칙 1/2로 걸려 **스스로 낫는다** — 별도 조치 불필요. **⭐ 그래서 `setup` 시그니처는 안 바뀐다.** 집합을 채우는 건 정책이 아니라 **노드**이고, 노드는 정책이 뭘 하는지 들여다볼 필요조차 없다(위 단순화). @@ -182,7 +210,20 @@ end) 눌러뒀다가 마지막에 한 번만 실행**해야 한다(`base/effect-plan.md`의 그 절). 즉 `Gate`(또는 `Blocker`의 직접 사용)가 **"설치 구간을 감싸 최초 발화를 한 번으로 접는" 용례까지 커버해야** 한다 — 설계할 때 이 소비자를 같이 볼 것. -8. **M2 범위.** M2에 `Gate`만 넣고 `Blocker`는 M3에 그대로 둘지, 아니면 +8. **⭐ [2026-08-21 신설, `/code-review high`] 소스 없는 emit — 빈 배치를 + 어떻게 다루나.** `state:Gate`는 공개 표면이므로 유저랜드 정책이 상류 신호와 + 무관하게 `emit()`을 부를 수 있고, 위 3번 항목도 *"이 `emit`은 setup 밖으로 + 캡처해 **언제든** 부를 수 있다"*고 문서화한다. 그런데 지금 규칙대로면 그때 + 나가는 배치가 **비어 있어** 하류가 "전부 3번" 판정으로 **조용히 삼킨다.** + - 이건 아래 7번이 예고한 소비자에 직접 걸린다 — `Effect(fn, ...deps)`의 + 설치 구간 억제는 "어떤 Source 에포크와도 무관한 발화를 마지막에 한 번 + 내보내기"라, 지금 규칙으로는 성립하지 않는다. + - **에이전트 권고: 빈 배치 = 무조건 통지**(dedup 대상에서 제외). 명시적으로 + 부른 명령형 통지라 접을 근거가 없고, 규칙에 한 줄만 추가하면 된다. + 대안은 `Effect` 쪽을 게이트 없이 자체 플래그로 해결하고 게이트에는 이 + 용례를 아예 안 주는 것 — 그러면 3번 항목의 "언제든 부를 수 있다"를 + "상류 신호가 하나라도 쌓여 있을 때만 의미가 있다"로 좁혀 적어야 한다. +9. **M2 범위.** M2에 `Gate`만 넣고 `Blocker`는 M3에 그대로 둘지, 아니면 `Blocker`까지 같이 앞당길지. `Dispatch.drive`의 배치 등록이 실제로 쓰는 건 `blocker:On()`/`OffWithoutEmit()`/`IsOn()`이므로(배치 게이팅 절), **최소한 그 세 메서드가 도는 형태까지는 M2에 필요**하다. diff --git a/.claude/base/state-epoch-plan.md b/.claude/base/state-epoch-plan.md index be42c91..9679848 100644 --- a/.claude/base/state-epoch-plan.md +++ b/.claude/base/state-epoch-plan.md @@ -85,9 +85,11 @@ count가 이미 최신이면 **캐시는 유효한 채로 남는다**(아래 2 emit" 같은 건 없다. 출처로 올 수 있는 것은 **둘**이다 (**[2026-08-21 확장]** — 원래는 `Source`뿐이었다): - **`Source`** — 그 소스 하나를 확인하라. - - **`GateNode`** — 그 게이트가 **흡수해뒀던 소스 집합**을 확인하라 - (`base/gate-plan.md`의 4번). 게이트가 `blocker:Off()` 등으로 유보를 풀 때 - 쓴다. + - **게이트 배치** — 게이트가 유보를 풀며 **떼어낸 소스 집합**을 확인하라 + (`base/gate-plan.md`의 4번). 게이트가 `blocker:Off()` 등으로 풀 때 쓴다. + **게이트의 살아있는 `withheld` 테이블이 아니라 그 자리에서 스왑해 떼어낸 + 스냅샷**이다 — 재진입이 나도 바깥 전파가 빈 집합을 순회하지 않게 하기 + 위함(같은 절). **평범한 노드는 출처를 안 바꾼다** — 자기를 끼워넣지 않고 받은 출처를 그대로 아래로 넘긴다. **`GateNode`만 예외로 언제나 자기 자신을 출처로 새로 낸다** (`base/gate-plan.md`의 4번). @@ -100,14 +102,14 @@ count가 이미 최신이면 **캐시는 유효한 채로 남는다**(아래 2 갱신하고 **뒤로 emit** 한다 — `rawInvalid`는 안 건드린다. 3. 둘 다 같으면 → **삼킨다.** - **다른 소스 항목은 건드리지 않는다** — 그 소스들은 자기가 직접 emit 하므로. -- **출처가 `GateNode`면** 그 게이트의 흡수 집합을 순회하며 **각 소스에 위 1~3을 +- **출처가 게이트 배치면** 그 집합을 순회하며 **각 소스에 위 1~3을 그대로 적용**하고, 하나라도 1번이나 2번에 걸렸으면 **받은 출처(그 게이트)를 그대로** 뒤로 넘긴다. 전부 3번이면 삼킨다 — 다이아몬드에서 같은 해제 통지가 두 번 도착해도 두 번째가 접히는 건 소스 emit과 똑같다. - - **⚠️ 단, 받는 쪽이 또 다른 `GateNode`면 출처를 넘기지 않고 집합을 풀어 - 자기 `withheld`에 합친다** — 상류 게이트가 전파 직후 집합을 비우기 때문에 - 출처만 들고 있으면 나중에 빈 집합을 내보내게 된다(`base/gate-plan.md`의 - 4번). + - **⚠️ 단, 받는 쪽이 `GateNode`면 배치를 그대로 넘기지 않고 풀어서 자기 + `withheld`에 합친다** — 배치는 상류 게이트가 이번 전파에만 쓰는 일회성 + 스냅샷이라, 참조만 들고 있다가 나중에 풀면 그 배치가 이미 지나간 것이 + 된다(`base/gate-plan.md`의 4번). - **재계산 판정**: - `rawInvalid == true` → 그냥 재계산한다. 순회할 이유가 없다(이미 확정). - `rawInvalid == false` → **그때만 `sourceCountMap`을 훑는다.** 목적은 하나뿐 — diff --git a/.claude/luau-test/STATUS.md b/.claude/luau-test/STATUS.md index 20c7a5e..d94bae8 100644 --- a/.claude/luau-test/STATUS.md +++ b/.claude/luau-test/STATUS.md @@ -106,8 +106,11 @@ ## ✅ `done/` — 통과 or 판정 끝 (17건) -**런타임 12개 전원 통과**(crash 0 / FAIL 0 — **[2026-08-21]** `01`과 `05`가 -`rewrite-required/`로 나가며 14 → 12) — **[열네 번째 세션] `04`/`19`는 +**지금 `done/`에 있는 런타임 스파이크는 9건**(`02`/`03`/`06`/`07`/`11`/`17`/ +`18`/`20`/`22`), **전원 통과**(crash 0 / FAIL 0). 나머지 8건은 타입 스파이크다. +**[2026-08-21 정정]** 여기 "런타임 12개"라고 적혀 있었는데, 그 산술이 이미 +`rewrite-required/`로 나간 `04`/`10`/`19`까지 포함한 옛 총계에서 이어져 온 +것이라 실제와 안 맞았다(두 번째 `/code-review high` 발견). — **[열네 번째 세션] `04`/`19`는 검증 대상 설계가 바뀌어 `rewrite-required/`로 이동했고, [2026-08-19] `05`는 현행 모델로 재작성해 잠시 돌아왔다가 **[2026-08-21] 소스 에포크 채택으로 다시 나갔으며**, 신규 `22`(구 `13` 런타임 절반, PostRef까지 diff --git a/.claude/qa-request/pre-implementation-qa-round5-followup.md b/.claude/qa-request/pre-implementation-qa-round5-followup.md index 6b7e03b..3efc4e7 100644 --- a/.claude/qa-request/pre-implementation-qa-round5-followup.md +++ b/.claude/qa-request/pre-implementation-qa-round5-followup.md @@ -363,8 +363,13 @@ Observe/Callback하고, 최초 발화는 `Blocker`의 다른 사용법으로 억 | 문서 | 무엇 | 왜 base가 아니라 research인가 | |---|---|---| -| `research/gate-primitive.md` | `Blocker`/`Debounce`/`Throttle` 아래의 공용 게이트 노드. 사용자 스케치(2단 `setup(emit) -> onUpstreamEmit`), `DT-4`의 "Blocker+Observer로는 순서를 못 지킨다" 논거, 열린 질문 6개 | **방향은 확정, 표면·이름이 미정** — M2 착수 전에 닫아야 함 | -| `research/state-epoch-validation.md` | 소스 에포크 비교로 재계산을 판정하는 안. 문제(glitch) 재현 시나리오, 제안 정리, 에이전트 분석(고쳐지는 것/안 고쳐지는 것/선례), 비용, 열린 질문 6개, 권고 | **아무것도 확정 안 함** — `base/source-state-plan.md`가 여전히 정본, M3 전에 결론 필요 | +| `gate-primitive.md` | `Blocker`/`Debounce`/`Throttle` 아래의 공용 게이트 노드. 사용자 스케치(2단 `setup(emit) -> onUpstreamEmit`), `DT-4`의 "Blocker+Observer로는 순서를 못 지킨다" 논거, 열린 질문 6개 | (당시) **방향은 확정, 표면·이름이 미정** | +| `state-epoch-validation.md` | 소스 에포크 비교로 재계산을 판정하는 안. 문제(glitch) 재현 시나리오, 제안 정리, 에이전트 분석, 비용, 열린 질문 6개, 권고 | (당시) **아무것도 확정 안 함** | + +**⚠️ [2026-08-21 갱신] 위 표는 신설 당시 상태다.** 두 문서는 **같은 날 전부 +확정되어 `base/gate-plan.md`/`base/state-epoch-plan.md`로 승격**됐고 +`research/`에는 더 이상 없다 — 지금 상태의 소스는 그 두 파일이고, 승격 경위는 +아래 O·R·S절. 이 절 아래의 `research/...` 경로 표기도 전부 그 시점 기록이다. --- @@ -1145,3 +1150,37 @@ end 규칙 2로 걸려 정책을 한 번 더 태우지만, 이미 집합에 있으므로 무해). 반영: `base/gate-plan.md` 4번, `base/state-epoch-plan.md` §2. + +--- + +# T절 — 두 번째 `/code-review high` 7건 (2026-08-21) + +`e2b85bc`까지 반영한 뒤 사용자가 다시 돌린 리뷰. **7건 전부 유효**했고, +그중 둘은 실제 유실 경로였다. 5건 수정 + 2건 열린 항목 승격. + +## T-1. 실제 유실 경로 둘 (High) + +| # | 무엇 | 수정 | +|---|---|---| +| H-1 | **`withheld`를 페이로드로 넘기면 재진입에 깨진다.** `G`가 두 갈래로 전파하다 첫 갈래의 Observer가 `Set`을 불러 `G`에 재진입하면, 중첩 전파가 끝나며 `table.clear`가 돌아 **바깥 전파의 남은 갈래가 빈 집합**을 받는다 → 그 변경을 영영 못 받음 | 전파 직전에 **스왑**한다 — `local batch = self._withheld; self._withheld = {}` 후 `batch`를 페이로드로 넘김. `clear`가 아니라 새 테이블. 하류는 `gate._withheld`가 아니라 **받은 배치**를 순회 | +| H-2 | **`OffWithoutEmit()`이 집합을 안 비운다.** `Dispatch.drive`의 배치 게이팅은 매 프레임 `On()` → … → `OffWithoutEmit()`을 돌므로 집합이 **단조 증가**하고(weak 설계와도 충돌), 나중에 아무 소스나 통과하는 순간 **버리기로 했던 옛 소스들이 같이 실려 나간다** | 그 경로도 **비운다**(전파 없이 새 테이블로 스왑). `withheld`도 **weak key**로 명시 | + +## T-2. 수정한 나머지 3건 + +- **Medium**: "무조건 `withheld`에 넣는다"가 "수신 규칙 1~3을 건너뛴다"로 읽히던 것 → **"정책의 통과/유보와 무관하게"라는 뜻**이고 규칙 3으로 삼켜진 emit은 정책도 안 돌고 집합에도 안 들어간다고 명시. 안 그러면 다이아몬드에서 `Throttle` 정책이 두 번 돌아 **유령 trailing emit**이 나간다. +- **Medium**: `luau-test/STATUS.md`의 "런타임 12개"가 이미 나간 `04`/`10`/`19`를 포함한 옛 총계에서 이어져 온 수라 실제(9건)와 안 맞던 것. +- **Medium**: 이 문서 D절 색인 표가 삭제된 `research/` 두 문서를 **현재형**으로 서술하던 것 → 당시 상태임을 못박고 승격 사실을 배너로. + +## T-3. 열린 항목으로 승격한 2건 + +- **재진입 계약**(`gate-plan.md` 6번) — H-1의 경로는 스냅샷으로 막았지만 + "정책 안에서 같은 게이트의 `emit()`을 재귀 호출"하는 계약 자체는 여전히 + 미정. `Blocker`의 "재진입 의도적 미지원"이 `Gate` 층위로 올라가는가. +- **소스 없는 emit**(`gate-plan.md` 8번, 신설) — 정책이 상류 신호와 무관하게 + `emit()`을 부르면 **빈 배치**가 나가 하류가 조용히 삼킨다. 3번 항목이 + *"언제든 부를 수 있다"*고 문서화해둔 것과 충돌하고, `Effect(fn, ...deps)`의 + 설치 구간 억제 용례가 정확히 이 모양이라 그대로는 성립하지 않는다. + **권고: 빈 배치 = 무조건 통지**(명령형 통지라 접을 근거가 없음). + +`question.md`가 "남은 것은 사용자 판단이 아니다"라고 닫아뒀던 것도 이 둘 때문에 +정정했다(리뷰 Low-7이 지적한 `question.md`↔`gate-plan.md` 불일치). diff --git a/.claude/question.md b/.claude/question.md index 12cedce..c50e1e5 100644 --- a/.claude/question.md +++ b/.claude/question.md @@ -225,8 +225,10 @@ `state:Block(blocker)` 안에서 그 배선을 쓴다(사용자: *"Gate 는 따로 프리미티브 없이 state:Gate( (emit) -> ()->() ) 처럼 선언되고 마치 Compute 처럼 GateNode(ComputeNode 처럼) 생성된다"*). `Get()`엔 영향 없음(통지만 - 막음)까지 확정. **남은 것은 사용자 판단이 아니라 구현 시 정할 것들** - (생명주기·재진입 계약, M2에 `Blocker`까지 넣을지) — `base/gate-plan.md`가 + 막음)까지 확정. **[2026-08-21 정정]** 여기 "남은 것은 사용자 판단이 아니라 + 구현 시 정할 것들"이라 적었으나, 두 번째 `/code-review high`가 **재진입 + 계약과 빈 배치 emit을 사용자 판단 항목으로 되돌렸다**(바로 위 항목). 구현 시 + 정하면 되는 건 생명주기와 M2 범위뿐 — `base/gate-plan.md`가 소스. 아래는 열려 있던 시점의 서술: 위 항목의 결정("게이팅 먼저")에 따라 base에 만들 것이 `Blocker`가 아니라 **상류 emit을 가로채 정책이 통과 여부를 정하는 공용 게이트 노드**로 확정됐다(`Blocker`/`Debounce`/`Throttle`이 그 위의 정책). 사용자 diff --git a/.claude/session/2026-08-21-02-qa-round5-and-gate-epoch-research.md b/.claude/session/2026-08-21-02-qa-round5-and-gate-epoch-research.md index 30cacf9..6a791e7 100644 --- a/.claude/session/2026-08-21-02-qa-round5-and-gate-epoch-research.md +++ b/.claude/session/2026-08-21-02-qa-round5-and-gate-epoch-research.md @@ -332,3 +332,19 @@ O절 커밋 직후 사용자가 돌린 리뷰에서 12건이 나왔고 전부 `withheld`에 합치는** 것으로 확정. 겹겹이 쌓여도 각 층이 자기 집합을 들고 있어 어느 층이 먼저 풀리든 정보가 안 샌다. 부수로 게이트의 `sourceEmitMap`은 수신 때가 아니라 **실제 전파 때** 갱신하는 것으로 못박았다. 전량은 S절. + +## 16. 두 번째 `/code-review high` — 7건 전부 유효, 유실 경로 둘 + +`e2b85bc` 직후 돌린 리뷰. 특히 둘이 실제 유실이었다: (1) `withheld`를 +페이로드로 그대로 넘기면 **재진입**(전파 중 Observer가 `Set`) 시 중첩 전파의 +`clear`가 바깥 전파를 빈 집합으로 만들어 변경이 증발한다 → 전파 직전에 +새 테이블로 **스왑**해 배치를 떼어내는 것으로 수정. (2) `OffWithoutEmit()`이 +집합을 안 비워, 매 프레임 `On()`/`OffWithoutEmit()`을 도는 `Dispatch.drive` +배치 게이팅에서 집합이 단조 증가하고 나중에 폐기분이 같이 실려 나간다 → +그 경로도 비우도록 확정, `withheld`도 weak key로 명시. + +부수로 "무조건 `withheld`에 넣는다"가 수신 규칙을 건너뛴다는 뜻으로 읽히던 +모호함(다이아몬드에서 `Throttle` 유령 trailing emit)과 STATUS의 런타임 개수, +followup D절의 옛 `research/` 현재형 서술을 고쳤다. 재진입 계약과 "소스 없는 +emit(빈 배치)"은 사용자 판단 항목으로 승격 — 후자는 `Effect(fn, ...deps)` +설치 구간 억제 용례에 직접 걸린다. 전량은 T절.