diff --git a/.claude/base/gate-plan.md b/.claude/base/gate-plan.md index a444df0..31c2f2b 100644 --- a/.claude/base/gate-plan.md +++ b/.claude/base/gate-plan.md @@ -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` 같은 표면을 diff --git a/.claude/base/state-epoch-plan.md b/.claude/base/state-epoch-plan.md index 92c6654..e2f77ba 100644 --- a/.claude/base/state-epoch-plan.md +++ b/.claude/base/state-epoch-plan.md @@ -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 diff --git a/.claude/qa-request/pre-implementation-qa-round5-followup.md b/.claude/qa-request/pre-implementation-qa-round5-followup.md index 008aab4..73c264d 100644 --- a/.claude/qa-request/pre-implementation-qa-round5-followup.md +++ b/.claude/qa-request/pre-implementation-qa-round5-followup.md @@ -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번. diff --git a/.claude/question.md b/.claude/question.md index e683ff2..311d0fc 100644 --- a/.claude/question.md +++ b/.claude/question.md @@ -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 는 따로 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 3d0619f..0baa063 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 @@ -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절. diff --git a/.claude/todos.md b/.claude/todos.md index 99bec74..b890709 100644 --- a/.claude/todos.md +++ b/.claude/todos.md @@ -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`).