From a55e726808eddef00735a6c87556f051b4bef463 Mon Sep 17 00:00:00 2001 From: qwreey Date: Fri, 21 Aug 2026 22:35:58 +0900 Subject: [PATCH] =?UTF-8?q?design:=20=EB=B9=88=20=EB=B0=B0=EC=B9=98=20emit?= =?UTF-8?q?=EC=9D=80=20=ED=86=B5=EC=A7=80=ED=95=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EC=9D=8C=20=E2=80=94=20=EC=97=90=EC=9D=B4=EC=A0=84=ED=8A=B8=20?= =?UTF-8?q?=EA=B6=8C=EA=B3=A0=20=EA=B8=B0=EA=B0=81,=20Blocker=20=EA=B3=84?= =?UTF-8?q?=EC=95=BD=EC=9D=98=20=EC=9D=BC=EB=B0=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 에이전트 권고("빈 배치 = 무조건 통지")를 사용자가 기각. 빈 배치는 "이미 다 던져서 더 던질 게 없다"는 뜻이고, 같은 카운트가 두 번 흘러든 emit을 전파하지 않는 것과 같은 자리다. 그걸 흘리는 건 표면적으로 State 중간에 Source:Emit을 추가하는 격이라 Gate의 성격과 안 맞는다. 확정: next(withheld) == nil이면 통지 자체를 안 한다. 새 규칙이 아니라 기존 계약의 일반화임을 확인 — blocker-plan.md가 이미 "HasBlockedEmit이 false면 emit 값과 무관하게 아무 것도 안 함(idempotent)"으로 확정해뒀고 HasBlockedEmit은 next(withheld) ~= nil의 특수형이다. Debounce/Throttle도 if pending일 때만 passThrough()를 부른다. 따름정리 — Effect(fn, ...deps)의 설치 구간 억제가 Gate 소비자에서 빠졌다. 설치 구간엔 어떤 Set도 안 일어나 게이트에 쌓이는 소스가 없으므로 게이트가 내보낼 것 자체가 없고, Effect 내부 플래그면 충분하다. effect-plan.md에서 "⚠️ 억제 장치의 모양은 Gate 설계에 딸려 있다"와 우선순위 문단의 "Gate보다 뒤다"라는 순서 제약이 같이 빠졌다. 이로써 Gate에 사용자 판단이 필요한 항목은 없다 — 남은 건 생명주기 계약과 M2 범위뿐이고 둘 다 구현 시 결정. 처리 전량은 V절. doc-check.py ERROR 0. Co-authored-by: qwreey Claude-Session: https://claude.ai/code/session_01TiW21rnti9SbLgF6twtn6D --- .claude/base/effect-plan.md | 15 ++++-- .claude/base/gate-plan.md | 51 +++++++++++++------ .claude/base/state-epoch-plan.md | 4 ++ .../pre-implementation-qa-round5-followup.md | 40 +++++++++++++++ .claude/question.md | 21 ++++---- ...21-02-qa-round5-and-gate-epoch-research.md | 14 +++++ .claude/todos.md | 8 ++- 7 files changed, 122 insertions(+), 31 deletions(-) diff --git a/.claude/base/effect-plan.md b/.claude/base/effect-plan.md index 1defe8c..7f11d4f 100644 --- a/.claude/base/effect-plan.md +++ b/.claude/base/effect-plan.md @@ -259,17 +259,22 @@ Effect의 의존성이 될 방법이 아예 없다.** 사용자 제기: *"Effect 가능하므로 그때마다). 채워지지 않은 상태는 발화가 아니다. - **최초 1회를 한 번만 돌리는 장치**: 의존성마다 구독을 걸면 각 구독의 "등록 즉시 1회 실행"이 N번 발화하므로, 설치 구간 동안 발화를 눌러뒀다가 마지막에 - 한 번만 실행한다 — **`Blocker`의 "`state:Block()` 없이 직접 쓰는" 용례**를 - 그대로 재사용(`base/blocker-plan.md`). **⚠️ 이 억제 장치의 정확한 모양은 - `Gate`(공용 게이트 노드) 설계에 딸려 있다** — `base/gate-plan.md`가 - 닫힌 뒤에 확정할 것. + 한 번만 실행한다. **[2026-08-21 확정] 이건 `Effect` 내부 플래그로 한다 — + 게이트도 `Blocker`도 안 쓴다.** 한때 *"`Blocker`의 "`state:Block()` 없이 + 직접 쓰는" 용례를 그대로 재사용"*이라 적고 정확한 모양을 `Gate` 설계에 + 걸어뒀는데, `Gate`가 **빈 배치일 땐 통지를 안 하는 것**으로 확정되면서 + 성립하지 않는 게 확인됐다(`base/gate-plan.md`의 8번) — 설치 구간엔 어떤 + `Set`도 안 일어나 게이트에 쌓이는 소스가 없으므로 게이트가 내보낼 것 자체가 + 없다. 설치 중 발화를 누르는 플래그 하나면 되고 새 메커니즘이 필요 없다. - **leaf dedup/cascade가 전부를 덮어야 한다** — 의존성이 N개면 내부 Observer도 N개라, `EffectHandle`의 bind/unbind cascade와 dedup 분기가 **그 전부**를 같이 처리해야 한다(위 `E-10`/`EF-5`와 같은 함정). 사용자 확인: *"어차피 모든 옵져버들이 내부에 들어가 있을것이므로 가능하다."* **우선순위**: 새 코어 메커니즘이 아니라 `Effect` 표면 확장이므로 M3의 -`Effect` 구현과 같이 간다 — 다만 위 ⚠️(억제 장치) 때문에 `Gate`보다 뒤다. +`Effect` 구현과 같이 간다. **[2026-08-21]** 여기 있던 "억제 장치 때문에 +`Gate`보다 뒤"라는 순서 제약은 **없어졌다** — 억제가 `Effect` 내부 플래그로 +확정돼 `Gate`에 안 걸린다. ## 해결됨 — Effect/Observer 관계 (2026-08-07 여섯 번째 세션, 이전 미해결 절 대체) diff --git a/.claude/base/gate-plan.md b/.claude/base/gate-plan.md index 52386ed..64adf6a 100644 --- a/.claude/base/gate-plan.md +++ b/.claude/base/gate-plan.md @@ -5,8 +5,8 @@ 없이 `state:Gate( (emit) -> ()->() )` 처럼 선언되고 마치 Compute 처럼 GateNode(ComputeNode 처럼) 생성된다 그리고 Blocker 는 해당 내부 배선을 따른다 ← 동의합니다 해당 방법대로 확정하면 됩니다."* 구현은 **M2**("게이팅 먼저" -결정, `ROADMAP.md`). **남은 것은 아래 "아직 안 정한 것"의 생명주기·빈 배치 emit·M2 -범위뿐** — `/code-review high`가 잡았던 4번(유보된 emit이 싣는 출처)은 같은 +결정, `ROADMAP.md`). **남은 것은 아래 "아직 안 정한 것"의 생명주기와 M2 범위뿐이고, 둘 다 +구현 시 정하면 되는 것들이다 — 사용자 판단이 필요한 항목은 없다** — `/code-review high`가 잡았던 4번(유보된 emit이 싣는 출처)은 같은 날 `emit(self)` + 흡수 집합으로 닫혔고, **`setup` 시그니처는 안 바뀌었다.** **⚠️ 처음 방향이 한 번 바뀌었다.** 신설 당시엔 *"공용 `Gate` 프리미티브를 꺼내고 @@ -224,25 +224,44 @@ end) **flush 진입 시 스왑**이다. - **같은 게이트/`Blocker` 인스턴스를 중첩해 쓰지 않는다** — `Blocker`의 기존 규칙이 그대로 적용된다(겹치는 배치는 새 인스턴스). -7. **⭐ 소비자가 하나 더 있다 — `Effect(fn, ...deps)`의 최초 1회 억제.** +7. **[2026-08-21 해소] `Effect(fn, ...deps)`의 최초 1회 억제는 `Gate` + 소비자가 **아니다**.** 한때 이 용례까지 게이트가 커버해야 한다고 적어뒀으나, + 위 8번(빈 배치는 통지 안 함)으로 **성립하지 않는 게 확인됐다** — 설치 구간엔 + 어떤 `Set`도 안 일어나 쌓이는 소스가 없으므로 게이트가 내보낼 것 자체가 없다. + `Effect`가 **자기 내부 플래그로** 설치 중 발화를 누르고 마지막에 한 번 + 직접 실행하면 되고, 새 메커니즘이 필요 없다. `base/effect-plan.md`의 그 + 항목에 달려 있던 "⚠️ `Gate` 설계에 딸려 있다"도 같이 해소됐다. 아래는 + 원 서술: 2026-08-21 5라운드 `C-6`에서 확정된 다중 의존성 `Effect`는, 의존성마다 구독을 걸면 각 구독의 "등록 즉시 1회 실행"이 N번 발화하므로 **설치 구간 동안 발화를 눌러뒀다가 마지막에 한 번만 실행**해야 한다(`base/effect-plan.md`의 그 절). 즉 `Gate`(또는 `Blocker`의 직접 사용)가 **"설치 구간을 감싸 최초 발화를 한 번으로 접는" 용례까지 커버해야** 한다 — 설계할 때 이 소비자를 같이 볼 것. -8. **⭐ [2026-08-21 신설, `/code-review high`] 소스 없는 emit — 빈 배치를 - 어떻게 다루나.** `state:Gate`는 공개 표면이므로 유저랜드 정책이 상류 신호와 - 무관하게 `emit()`을 부를 수 있고, 위 3번 항목도 *"이 `emit`은 setup 밖으로 - 캡처해 **언제든** 부를 수 있다"*고 문서화한다. 그런데 지금 규칙대로면 그때 - 나가는 배치가 **비어 있어** 하류가 "전부 3번" 판정으로 **조용히 삼킨다.** - - 이건 아래 7번이 예고한 소비자에 직접 걸린다 — `Effect(fn, ...deps)`의 - 설치 구간 억제는 "어떤 Source 에포크와도 무관한 발화를 마지막에 한 번 - 내보내기"라, 지금 규칙으로는 성립하지 않는다. - - **에이전트 권고: 빈 배치 = 무조건 통지**(dedup 대상에서 제외). 명시적으로 - 부른 명령형 통지라 접을 근거가 없고, 규칙에 한 줄만 추가하면 된다. - 대안은 `Effect` 쪽을 게이트 없이 자체 플래그로 해결하고 게이트에는 이 - 용례를 아예 안 주는 것 — 그러면 3번 항목의 "언제든 부를 수 있다"를 - "상류 신호가 하나라도 쌓여 있을 때만 의미가 있다"로 좁혀 적어야 한다. +8. **[2026-08-21 해소] 소스 없는 emit(빈 배치) — 아무것도 안 한다.** + `/code-review high`가 "정책이 상류 신호와 무관하게 flush를 부르면 빈 배치가 + 나가 하류가 조용히 삼킨다"를 문제로 제기했고, 에이전트는 "빈 배치 = 무조건 + 통지"를 권고했다. **사용자 기각**: + + > *"빈 배치면 이미 하류로 한번 다 던져서 더 던질게 없다는 의미입니다. 마치 + > 두번 흘러들어온 같은 카운트의 emit 과 유사한데요. 그건 전파 안 합니다. … + > 애초에 Gate 는 중간에 emit 을 할 수 있는 핸들을 노출하는, `Source:Emit` + > 같은걸 주는 요소도 아니고, 쌓아두다 뒤로 넘기는건데, 쌓아둔것 자체가 + > 없는데 뒤로 넘긴다는건 이상합니다."* + + **확정: `next(withheld) == nil`이면 통지 자체를 안 한다.** 그래야 다른 + State와 동작이 같아진다 — 빈 배치를 흘리는 건 표면적으로 **State 중간에 + `Source:Emit`을 추가하는 격**이고, `Gate`는 그런 요소가 아니다. + - **새 규칙이 아니라 기존 계약의 일반화다.** `base/blocker-plan.md`는 이미 + *"이미 `HasBlockedEmit`이 false면 `emit` 값과 무관하게 아무 것도 안 함 + (idempotent)"*이라고 확정해뒀다 — 즉 `HasBlockedEmit`은 + `next(withheld) ~= nil`의 특수형이다. `Debounce`/`Throttle`도 + `if pending`일 때만 `passThrough()`를 부른다 + (`base/debounce-throttle-plan.md`의 `onWindowEnd`). + - **3번 항목의 "`emit`은 언제든 부를 수 있다"는 그대로 유효하다** — 언제 + 불러도 되지만, 쌓인 게 없으면 그 호출은 no-op이라는 뜻으로 읽는다. + - **따름정리: `Effect(fn, ...deps)`의 설치 구간 억제는 `Gate` 소비자가 + 아니다** — 아래 7번 참고. + 9. **M2 범위.** M2에 `Gate`만 넣고 `Blocker`는 M3에 그대로 둘지, 아니면 `Blocker`까지 같이 앞당길지. `Dispatch.drive`의 배치 등록이 실제로 쓰는 건 `blocker:On()`/`OffWithoutEmit()`/`IsOn()`이므로(배치 게이팅 절), **최소한 diff --git a/.claude/base/state-epoch-plan.md b/.claude/base/state-epoch-plan.md index 9679848..b119ad2 100644 --- a/.claude/base/state-epoch-plan.md +++ b/.claude/base/state-epoch-plan.md @@ -102,6 +102,10 @@ count가 이미 최신이면 **캐시는 유효한 채로 남는다**(아래 2 갱신하고 **뒤로 emit** 한다 — `rawInvalid`는 안 건드린다. 3. 둘 다 같으면 → **삼킨다.** - **다른 소스 항목은 건드리지 않는다** — 그 소스들은 자기가 직접 emit 하므로. +- **게이트는 배치가 비어 있으면 애초에 통지하지 않는다**(`base/gate-plan.md`의 + 8번) — 빈 배치를 흘리는 건 "쌓인 게 없는데 뒤로 넘기는" 꼴이라 State 층에 + `Source:Emit`을 추가하는 것과 같아진다. 그래서 아래 규칙이 빈 집합을 받는 + 경우는 없다. - **출처가 게이트 배치면** 그 집합을 순회하며 **각 소스에 위 1~3을 그대로 적용**하고, 하나라도 1번이나 2번에 걸렸으면 **받은 출처(그 게이트)를 그대로** 뒤로 넘긴다. 전부 3번이면 삼킨다 — 다이아몬드에서 같은 해제 통지가 diff --git a/.claude/qa-request/pre-implementation-qa-round5-followup.md b/.claude/qa-request/pre-implementation-qa-round5-followup.md index 8a33052..ffec3d2 100644 --- a/.claude/qa-request/pre-implementation-qa-round5-followup.md +++ b/.claude/qa-request/pre-implementation-qa-round5-followup.md @@ -1231,3 +1231,43 @@ T절이 "재진입 위험"이라 부른 것은 **에이전트가 적은 "전파 **빈 배치 emit**(소스 없는 flush)을 무조건 통지로 볼 것인가 — `gate-plan.md` 8번, `question.md` 3번. 나머지(생명주기, M2 범위)는 구현 시 정하면 된다. + +--- + +# V절 — 빈 배치 emit: 에이전트 권고 기각, "아무것도 안 함"으로 확정 (2026-08-21) + +U절이 남긴 마지막 사용자 판단 항목이 같은 날 닫혔다. 에이전트 권고는 +"빈 배치 = 무조건 통지"였고 **기각**됐다: + +> *"빈 배치면 이미 하류로 한번 다 던져서 더 던질게 없다는 의미입니다. 마치 +> 두번 흘러들어온 같은 카운트의 emit 과 유사한데요. 그건 전파 안 합니다. … +> 애초에 Gate 는 중간에 emit 을 할 수 있는 핸들을 노출하는, `Source:Emit` +> 같은걸 주는 요소도 아니고, 쌓아두다 뒤로 넘기는건데, 쌓아둔것 자체가 없는데 +> 뒤로 넘긴다는건 이상합니다. … 비어있는걸 뒤로 넘겨 Effect 가 받게 만드는건 +> … 표면적으로 보면 State 중간에 Emit 을 추가하는 격이라서요."* + +**확정**: `next(withheld) == nil`이면 **통지 자체를 안 한다.** + +- **새 규칙이 아니라 기존 계약의 일반화다** — `base/blocker-plan.md`가 이미 + *"이미 `HasBlockedEmit`이 false면 `emit` 값과 무관하게 아무 것도 안 함 + (idempotent)"*으로 확정해뒀고, `HasBlockedEmit`은 `next(withheld) ~= nil`의 + 특수형이다. `Debounce`/`Throttle`도 `if pending`일 때만 `passThrough()`를 + 부른다. +- **3번 항목의 "`emit`은 언제든 부를 수 있다"는 유지** — 언제 불러도 되지만 + 쌓인 게 없으면 no-op이라는 뜻. + +## V-1. 따름정리 — `Effect`의 설치 구간 억제가 `Gate` 소비자에서 빠졌다 + +`gate-plan.md` 7번이 예고하던 소비자가 **성립하지 않는 게 확인됐다**: 설치 +구간엔 어떤 `Set`도 안 일어나 게이트에 쌓이는 소스가 없으므로 게이트가 +내보낼 것 자체가 없다. `Effect`가 **자기 내부 플래그로** 설치 중 발화를 누르고 +마지막에 한 번 직접 실행하면 되고 새 메커니즘이 필요 없다. + +그래서 `base/effect-plan.md`에서 둘이 같이 빠졌다 — 그 항목의 "⚠️ 이 억제 +장치의 정확한 모양은 `Gate` 설계에 딸려 있다"와, 우선순위 문단의 "`Gate`보다 +뒤다"라는 **순서 제약**. + +## V-2. 현재 상태 + +`Gate`에 **사용자 판단이 필요한 항목은 없다.** 남은 건 생명주기 계약과 +M2 범위(`Gate`만 vs `Blocker`까지)이고 둘 다 구현 시 정하면 된다. diff --git a/.claude/question.md b/.claude/question.md index 49bb856..43898fa 100644 --- a/.claude/question.md +++ b/.claude/question.md @@ -220,13 +220,15 @@ (비워두면 순회가 훑을 목록 자체가 없어 "유효하다"로 오판한다 — *"'내가 뭘 추적하고 있나' 가 필요하죠"*). 그래서 `:With` 병합 규칙은 **필요 없어졌다.** 재계산 시 갱신 범위(전부 갱신)도 확정. `base/state-epoch-plan.md`의 §2·§5 7번. -- **⭐ [신설, 2026-08-21 두 번째 `/code-review high`] `Gate` — 소스 없는 - emit(빈 배치)을 어떻게 다루나. M2 착수 전.** 정책이 상류 신호와 무관하게 - `emit()`(=flush)을 부르면 **빈 배치**가 나가 하류가 조용히 삼킨다. - `Effect(fn, ...deps)`의 설치 구간 억제가 정확히 이 모양(어떤 소스 에포크와도 - 무관한 발화를 마지막에 한 번)이라 그 용례가 직접 걸린다. 권고는 **"빈 배치 = - 무조건 통지"**(명령형 통지라 접을 근거가 없음). 상세는 `base/gate-plan.md`의 - 8번. +- **[해소, 2026-08-21 같은 날] `Gate` — 소스 없는 emit(빈 배치)은 **아무것도 + 안 한다**.** 에이전트 권고("빈 배치 = 무조건 통지")는 **사용자 기각** — + *"쌓아둔것 자체가 없는데 뒤로 넘긴다는건 이상합니다 … 표면적으로 보면 State + 중간에 Emit 을 추가하는 격"*. 새 규칙도 아니다: `blocker-plan.md`가 이미 + "`HasBlockedEmit`이 false면 아무 것도 안 함(idempotent)"으로 확정해뒀고, + `withheld`는 그 플래그의 일반화다. **따름정리로 `Effect(fn, ...deps)`의 설치 + 구간 억제는 `Gate` 소비자가 아니게 됐다** — `Effect` 내부 플래그로 처리하고, + `effect-plan.md`에 있던 "`Gate`보다 뒤" 순서 제약도 사라졌다. + `base/gate-plan.md`의 7·8번. - **같이 제기됐던 "재진입 계약"은 열린 항목이 아니었다**(사용자 지적으로 2026-08-21 정리) — `blocker-plan.md`의 재진입은 **같은 인스턴스 중첩**을 말하는 것이지 정책의 `emit()` 호출과 무관하고, 끝나지 않는 되먹임은 @@ -238,8 +240,9 @@ 프리미티브 없이 state:Gate( (emit) -> ()->() ) 처럼 선언되고 마치 Compute 처럼 GateNode(ComputeNode 처럼) 생성된다"*). `Get()`엔 영향 없음(통지만 막음)까지 확정. **[2026-08-21 정정]** 여기 "남은 것은 사용자 판단이 아니라 - 구현 시 정할 것들"이라 적었으나, 두 번째 `/code-review high`가 **빈 배치 - emit을 사용자 판단 항목으로 되돌렸다**(바로 위 항목). 구현 시 + 구현 시 정할 것들"이라 적었으나, 두 번째 `/code-review high`가 빈 배치 + emit을 잠시 사용자 판단 항목으로 되돌렸다 — **같은 날 해소돼(바로 위 항목) + 원래 서술로 돌아왔다.** 구현 시 정하면 되는 건 생명주기와 M2 범위뿐 — `base/gate-plan.md`가 소스. 아래는 열려 있던 시점의 서술: 위 항목의 결정("게이팅 먼저")에 따라 base에 만들 것이 `Blocker`가 아니라 **상류 emit을 가로채 정책이 통과 여부를 정하는 공용 게이트 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 bcf7b2e..a2061c3 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 @@ -363,3 +363,17 @@ emit(빈 배치)"은 사용자 판단 항목으로 승격 — 후자는 `Effect( 그대로 유효하고 서술만 고쳤다. 그래서 `Gate`에 남은 **사용자 판단은 빈 배치 emit 하나뿐**이다. 전량은 U절. + +## 18. 빈 배치 emit — 에이전트 권고 기각, 기존 Blocker 계약의 일반화로 확정 + +마지막 열린 항목. 에이전트는 "빈 배치 = 무조건 통지"를 권고했으나 사용자가 +기각했다 — 빈 배치는 "더 던질 게 없다"는 뜻이고 그걸 흘리는 건 State 층에 +`Source:Emit`을 추가하는 격이다. 확정은 **통지 자체를 안 함**이고, 확인해보니 +새 규칙도 아니었다: `blocker-plan.md`의 "`HasBlockedEmit`이 false면 아무 것도 +안 함(idempotent)"이 이미 같은 계약이고 `withheld`가 그 플래그의 일반화다. + +따름정리로 `Effect(fn, ...deps)`의 설치 구간 억제가 **`Gate` 소비자에서 +빠졌다** — 설치 구간엔 `Set`이 없어 쌓일 게 없으므로 게이트가 내보낼 것이 +없고, `Effect` 내부 플래그면 충분하다. `effect-plan.md`의 "`Gate`보다 뒤" +순서 제약도 같이 사라졌다. 이로써 `Gate`에 사용자 판단 항목은 남지 않았다. +전량은 V절. diff --git a/.claude/todos.md b/.claude/todos.md index 76ce353..9bc8ffe 100644 --- a/.claude/todos.md +++ b/.claude/todos.md @@ -18,7 +18,13 @@ 제기됐던 에포크 쪽 세 자리도 **전량 확정**됐다(재계산 시 count 전부 갱신 / 새 노드는 `sourceEmitMap`은 비우고 `sourceCountMap`은 실제 count로 채운 뒤 `rawInvalid = true` / 그래서 `:With` 병합 규칙은 불필요) — - `state-epoch-plan.md` §5 7번. + `state-epoch-plan.md` §5 7번. **[같은 날 두 번째 `/code-review high`]** + 7건이 더 나왔고 전부 유효했는데(재진입 시 빈 배치가 새어 변경이 증발하던 + 것, `OffWithoutEmit`이 흡수 집합을 안 비우던 것 등), 그중 사용자 판단으로 + 올라갔던 둘도 같은 날 닫혔다 — "재진입 계약"은 애초에 **잘못 옮긴 + 서술**이었고, "빈 배치 emit"은 **아무것도 안 하는 것**으로 확정(그 따름정리로 + `Effect`의 설치 구간 억제가 `Gate` 소비자에서 빠지며 `effect-plan.md`의 + 순서 제약도 사라짐). **다시, M2 착수를 막는 설계 항목은 없다.** 그 외 남은 것은 판단이 아니라 구현 시 정할 것들 — `Gate`의 생명주기·재진입 계약, M2에 `Blocker`까지 넣을지, 스파이크 `05` 재작성 (`luau-test/STATUS.md`).