From e2b85bca557e5562a50101c39ddbbfa854fcefdc Mon Sep 17 00:00:00 2001 From: qwreey Date: Fri, 21 Aug 2026 22:02:01 +0900 Subject: [PATCH] =?UTF-8?q?design:=20=EA=B2=8C=EC=9D=B4=ED=8A=B8=EA=B0=80?= =?UTF-8?q?=20=EA=B2=8C=EC=9D=B4=ED=8A=B8=20emit=EC=9D=84=20=EB=B0=9B?= =?UTF-8?q?=EB=8A=94=20=EA=B2=BD=EC=9A=B0=20=ED=99=95=EC=A0=95=20=E2=80=94?= =?UTF-8?q?=20=EC=B6=9C=EC=B2=98=EB=A5=BC=20=EB=84=98=EA=B8=B0=EC=A7=80=20?= =?UTF-8?q?=EB=A7=90=EA=B3=A0=20=ED=92=80=EC=96=B4=EC=84=9C=20=ED=95=A9?= =?UTF-8?q?=EC=B9=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자 발견. R절까지의 규칙("출처가 GateNode면 받은 출처를 그대로 아래로 넘긴다")은 받는 쪽이 또 게이트인 경우가 정의돼 있지 않았고, 그대로 넘기면 깨진다 — 상류 게이트는 자기 전파가 끝나자마자 table.clear 하므로 하류 게이트가 출처만 들고 유보했다가 나중에 풀면 빈 집합을 내보내 변경이 통째로 증발한다. 확정: 수신 시점에 unfold 해서 자기 _withheld에 합친다(Source면 하나, GateNode면 그 집합 전부). 게이트가 몇 겹으로 겹쳐도 각 층이 자기 집합을 들고 있으므로 어느 층이 먼저 풀리든 정보가 안 샌다. 같이 못박은 것: 게이트의 sourceEmitMap은 수신 때가 아니라 실제로 전파할 때 집합 전체에 대해 갱신한다 — 그래야 "내가 하류로 던진 에포크"라는 맵의 뜻이 게이트에서도 참이 된다. 반영은 base/gate-plan.md 4번, base/state-epoch-plan.md §2, followup S절. doc-check.py ERROR 0. Co-authored-by: qwreey Claude-Session: https://claude.ai/code/session_01TiW21rnti9SbLgF6twtn6D --- .claude/base/gate-plan.md | 25 +++++++++++++-- .claude/base/state-epoch-plan.md | 4 +++ .../pre-implementation-qa-round5-followup.md | 31 +++++++++++++++++++ ...21-02-qa-round5-and-gate-epoch-research.md | 9 ++++++ 4 files changed, 66 insertions(+), 3 deletions(-) diff --git a/.claude/base/gate-plan.md b/.claude/base/gate-plan.md index 3b0a3ae..499e16d 100644 --- a/.claude/base/gate-plan.md +++ b/.claude/base/gate-plan.md @@ -138,9 +138,28 @@ end) 생각이 될 수 있겠지만, 사실 여기서 지연과 비지연을 구분할 이유가 없습니다."* 하류가 보는 차이는 집합의 원소가 하나냐 여럿이냐뿐이고 판정 규칙은 완전히 같다. - - 하류는 출처가 `GateNode`면 그 집합을 순회하며 각 소스에 평소 규칙(1~3)을 - 적용하고, 하나라도 걸리면 **받은 출처를 그대로** 더 아래로 넘긴다 - (`state-epoch-plan.md` §2). + - 하류가 **평범한 노드**면, 출처가 `GateNode`일 때 그 집합을 순회하며 각 + 소스에 평소 규칙(1~3)을 적용하고, 하나라도 걸리면 **받은 출처를 그대로** + 더 아래로 넘긴다(`state-epoch-plan.md` §2). + - **⭐ [2026-08-21 신설] 하류가 또 다른 게이트면 — 받은 집합을 풀어 + 자기 `withheld`에 합친다.** 게이트가 게이트 emit을 받는 경우가 정의돼 + 있지 않던 구멍이었다(사용자 발견). **출처를 그대로 넘기면 안 된다** — + 상류 게이트는 자기 전파가 끝나자마자 `table.clear` 하므로, 하류 게이트가 + 유보했다가 나중에 풀면 그때는 **빈 집합**을 내보내게 되어 변경이 통째로 + 증발한다. 그래서 수신 시점에 **풀어서 옮겨 담아야** 한다: + ``` + -- 출처가 Source면 그 하나를, GateNode면 그 집합 전부를 편다 + for source in unfold(origin) do + self._withheld[source] = true + end + -- 그 다음 평소대로 정책 실행 + ``` + 게이트가 몇 겹으로 겹쳐도 각 층이 자기 집합을 들고 있으므로 어느 층이 + 먼저 풀리든 정보가 안 샌다. + - **게이트의 `sourceEmitMap`은 수신 때가 아니라 실제로 전파할 때** 갱신한다 + (집합 전체에 대해 한꺼번에). 그래야 "내가 하류로 던진 에포크"라는 맵의 + 뜻이 게이트에서도 참이 된다 — 유보 중 같은 에포크가 다른 경로로 또 오면 + 규칙 2로 걸려 정책을 한 번 더 태우는데, 이미 집합에 있으므로 무해하다. - **`OffWithoutEmit()`로 emit 없이 풀어도 문제가 없다** — 하류의 `sourceEmitMap`이 뒤에 남지만, 그 소스의 다음 진짜 emit이 규칙 1/2로 걸려 스스로 낫는다. diff --git a/.claude/base/state-epoch-plan.md b/.claude/base/state-epoch-plan.md index 8d809ba..be42c91 100644 --- a/.claude/base/state-epoch-plan.md +++ b/.claude/base/state-epoch-plan.md @@ -104,6 +104,10 @@ count가 이미 최신이면 **캐시는 유효한 채로 남는다**(아래 2 그대로 적용**하고, 하나라도 1번이나 2번에 걸렸으면 **받은 출처(그 게이트)를 그대로** 뒤로 넘긴다. 전부 3번이면 삼킨다 — 다이아몬드에서 같은 해제 통지가 두 번 도착해도 두 번째가 접히는 건 소스 emit과 똑같다. + - **⚠️ 단, 받는 쪽이 또 다른 `GateNode`면 출처를 넘기지 않고 집합을 풀어 + 자기 `withheld`에 합친다** — 상류 게이트가 전파 직후 집합을 비우기 때문에 + 출처만 들고 있으면 나중에 빈 집합을 내보내게 된다(`base/gate-plan.md`의 + 4번). - **재계산 판정**: - `rawInvalid == true` → 그냥 재계산한다. 순회할 이유가 없다(이미 확정). - `rawInvalid == false` → **그때만 `sourceCountMap`을 훑는다.** 목적은 하나뿐 — diff --git a/.claude/qa-request/pre-implementation-qa-round5-followup.md b/.claude/qa-request/pre-implementation-qa-round5-followup.md index 18adbca..6b7e03b 100644 --- a/.claude/qa-request/pre-implementation-qa-round5-followup.md +++ b/.claude/qa-request/pre-implementation-qa-round5-followup.md @@ -1114,3 +1114,34 @@ Q절은 "노드가 정책이 그 자리에서 `emit()`을 불렀는지 되짚어 그래서 **`:With` 병합 규칙은 필요 없어졌다** — 생성 시점 라이브 count로 통일되므로 두 상류가 다른 count를 들 일이 없다. P절 Med-3이 열어둔 (b)/(c)가 이걸로 전부 닫혔다. + +--- + +# S절 — 미정의 행동 하나: 게이트가 게이트 emit을 받으면 (2026-08-21) + +사용자 발견. R절까지의 규칙은 "출처가 `GateNode`면 집합을 순회하고 **받은 +출처를 그대로** 아래로 넘긴다"였는데, **받는 쪽이 또 게이트인 경우**가 정의돼 +있지 않았다. + +**그대로 넘기면 깨진다**: 상류 게이트는 자기 전파가 끝나자마자 +`table.clear(withheld)` 한다. 하류 게이트가 그 출처만 들고 유보했다가 나중에 +풀면, 그때 참조하는 집합은 **이미 비어 있다** → 변경이 통째로 증발. + +**확정**: 수신 시점에 **풀어서 자기 `withheld`에 합친다.** + +``` +for source in unfold(origin) do -- Source면 하나, GateNode면 그 집합 전부 + self._withheld[source] = true +end +-- 그 다음 평소대로 정책 실행 +``` + +게이트가 몇 겹으로 겹쳐도 각 층이 자기 집합을 들고 있으므로 어느 층이 먼저 +풀리든 정보가 안 샌다. + +**같이 못박은 것**: 게이트의 `sourceEmitMap`은 **수신 때가 아니라 실제로 +전파할 때** 집합 전체에 대해 갱신한다. 그래야 "내가 하류로 던진 에포크"라는 +맵의 뜻이 게이트에서도 참이 된다(유보 중 같은 에포크가 다른 경로로 또 오면 +규칙 2로 걸려 정책을 한 번 더 태우지만, 이미 집합에 있으므로 무해). + +반영: `base/gate-plan.md` 4번, `base/state-epoch-plan.md` §2. 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 e20dba7..30cacf9 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 @@ -323,3 +323,12 @@ O절 커밋 직후 사용자가 돌린 리뷰에서 12건이 나왔고 전부 `rawInvalid = true`(순회가 훑을 목록이 곧 이 맵이라 비워두면 "유효"로 오판). 이걸로 `:With` 병합 규칙이 필요 없어지며 `/code-review` Med-3이 닫혔다. 전량은 followup의 R절. + +## 15. 게이트가 게이트 emit을 받는 경우 — 미정의였고, 풀어 담는 것으로 확정 + +사용자가 마지막으로 잡은 구멍. "받은 출처를 그대로 넘긴다"는 규칙이 하류가 +또 게이트일 때 깨진다 — 상류 게이트가 전파 직후 집합을 비우므로 하류 게이트가 +나중에 풀면 빈 집합을 내보내 변경이 증발한다. 수신 시점에 **풀어서 자기 +`withheld`에 합치는** 것으로 확정. 겹겹이 쌓여도 각 층이 자기 집합을 들고 +있어 어느 층이 먼저 풀리든 정보가 안 샌다. 부수로 게이트의 `sourceEmitMap`은 +수신 때가 아니라 **실제 전파 때** 갱신하는 것으로 못박았다. 전량은 S절.