# [역전됨] "이미 `invalid`면 그 아래로 더 전파하지 않는다" — 무효화 전파 dedup **신설**: 2026-08-14 (Debounce/Throttle 설계 중 사용자 지적으로 발견) **역전 대상**: `base/source-state-plan.md`의 "전파 모델 확정" 절 (2026-08-14 세 번째 세션의 3단계 분할 전에는 `base/bind-system-plan.md`에 있었음 — 이 문서가 인용하는 옛 경로는 그 시절 것) **현재 유효한 서술**: 같은 절의 "전파 모델 확정" + "다이아몬드 의존성은 무엇이 푸는가"(2026-08-14 재작성) --- ## 뒤집힌 원문 `base/source-state-plan.md`(분할 전에는 `base/bind-system-plan.md`)에 2026-08-04부터 2026-08-14까지 이렇게 적혀 있었음: > - `Source`는 값이 바뀌면 구독 중인 State들에게 **"무효화됐다"는 신호만 > 쏜다** — 새 값 자체는 신호에 안 실림("state는 세터를 내보내기보다 > 업데이트 됐다는 신호만 쏜다" — 사용자 확정 문구). > - **신호를 받은 State는 자기 `invalid` 플래그만 세우고, 이미 `invalid`였다면 > 그 아래로 더 전파하지 않는다 — 다이아몬드 의존성에서 중복 워크를 막는 > 장치(Vide가 저자 스스로 `todo.md`에 미해결로 남긴 문제의 해결책).** > - 실제 재계산은 `:Get()`이 호출되는 시점에만 일어남 — "필요할 때 계산" > 원칙(사용자 확정). 굵게 표시한 두 번째 항목이 역전 대상. 첫/세 번째 항목은 그대로 유효함. ## 무엇이 맞는가 (사용자 확정, 2026-08-14) > **emit은 항상 전파함. `Blocker`나 emit 전파 지연요소만 이를 지연할 수 있음.** > > 재계산 막아지는 건 맞음 — 한 곳에서 `Get`이 되면, `invalid`하다면 위로 > 올라가서 받아와서 계산 처리된 게 들어오고 cache가 쓰인 다음 `invalid`가 > 꺼짐. 즉: - `invalid` 플래그의 역할은 **"내 캐시가 낡았다"는 표시 하나뿐**이고, 전파를 제어하는 장치가 아님. - 중복 **재계산**을 막는 주체는 **pull-recompute + 노드별 캐시**. - 중복 **통지**는 접지 않음 — 다이아몬드에서 아래쪽 `Observer`가 한 사이클에 두 번 우는 건 의도된 동작. 접고 싶으면 `Blocker` 같은 **명시적 게이트**를 쓸 것. ## 왜 틀렸는가 — 근거 셋 ### 1. ⭐ 확정된 `Observer` 계약과 정면 충돌 (결정적) 같은 파일의 "`state:Observer(fn)`" 절이 **`fn`이 `:Get()`을 부르지 않아도 되는 것을 명시적으로 허용**함: > 재계산이 진짜 필요한지가 다른 `:With`한 값에 따라 갈리는 경우가 있어서 > (…) `Get()` 호출 여부를 작성자가 직접 결정하게 열어둔 것. 역전 전 규칙을 액면대로 적용하면: ``` source:Set(1) → state invalid 세팅 → Observer 발화 → fn이 :Get() 안 함 → state는 invalid로 남음 source:Set(2) → state 이미 invalid → 전파 중단 → Observer 침묵 ❌ source:Set(3) → 마찬가지 ❌ ... 이후 영원히 ``` **`:Get()`을 안 하는 Observer는 딱 한 번 울고 영구히 침묵함.** 문서가 정당한 사용법으로 허용한 것이 문서의 다른 문장 때문에 조용히 깨지는 것이므로, 취향 차이가 아니라 **base 내부의 실제 모순**이었음. ### 2. `base/architecture.md`와 어긋나 있었음 `architecture.md`는 같은 다이아몬드 문제를 이렇게 서술 중이었음: > 전파는 push-invalidate(신호만)/pull-recompute(`Get()` 시점) — **Fusion식 > eager 노드 없이도 다이아몬드 의존성 중복 재계산 문제가 풀림** 즉 중복 *재계산*을 막는 주체를 **pull-recompute 자체**로 지목함(이쪽이 맞음). `source-state-plan.md`는 같은 문제의 해결 주체를 `invalid` 플래그 dedup으로 지목 — **base 안에서 두 문서가 서로 다른 것을 가리키고 있었음.** ### 3. 다이아몬드 근거가 요구하는 범위를 넘어섰음 다이아몬드는 *한 번의 변경*이 여러 경로로 같은 노드에 닿는 문제라, 막으려면 **그 전파 파동(wave) 안에서만** 접으면 됨. 그런데 역전 전 문장은 `invalid`를 **시간에 걸쳐 유지되는 상태**로 써서("이미 `invalid`였다면"), 누가 `:Get()`할 때까지 **이후의 모든 변경**까지 삼켰음. 파동 내 dedup과 시간축 dedup은 전혀 다른 범위인데 한 문장에 뭉뚱그려져 있었음. 덧붙여, "여러 emit을 하나로 접어뒀다가 나중에 1회 방출"은 사용자 지적대로 **`Blocker`의 `HasBlockedEmit`이 opt-in으로 제공하는 동작**임 (`base/blocker-plan.md`). 같은 동작을 모든 State 노드에 암묵적으로 심으면 `Blocker`의 존재 의의가 절반 사라짐. ## 어떻게 들어왔나 (추정) 사용자는 원래부터 "emit은 항상 전파"로 말해왔음(2026-08-14 확인: "내가 원래 emit은 항상 전파라 했는데 어떤 엉뚱한 에이전트가 이상한짓 하고 간듯"). Vide의 미해결 문제(`todo.md`의 "복잡한 다이아몬드 그래프에서 중복 재평가 방지")를 quad가 어떻게 푸는지 서술하는 과정에서, **이미 캐시로 풀려 있는 것**을 전파 억제 장치로 잘못 귀속시킨 것으로 보임. ## 영향 범위 — 2026-08-14에 같이 고친 것 | 파일 | 무엇이 바뀌었나 | |---|---| | `base/source-state-plan.md` | 전파 규칙 재작성 + "다이아몬드 의존성은 무엇이 푸는가" 절 신설. "왜 State 체인을 플래튼하지 않는가"와 `:With` 빌더 기각 근거 2번의 **근거를 캐시 공유로 교체**(두 결론 자체는 안 바뀜). `Observer` 절에 "이 허용은 항상-전파에 의존한다"는 상호 참조 추가 | | `base/architecture.md` | 원래도 맞는 서술이었으나, 캐시가 주체임을 명시적으로 보강 | | `base/blocker-plan.md` | "emit 전파를 지연시킬 수 있는 유일한 요소"라는 위치를 명문화 | | `reference/comparison-fusion-vide.md` | Vide 대비 서술에서 "플래그 dedup" → "캐시" | | `research/framework-comparison-findings.md` | 같은 정정 | | `research/debounce-throttle-plan.md` | 이 문서의 3절 발견이 통째로 철회됨(아래) | | `ROADMAP.md` | M0 체크리스트의 "이미 invalid면 전파 중단되는지" 항목 교체 | | `luau-test` `05-store-state-diamond-propagation.luau` | 틀린 모델을 **통과 상태로 검증 중이었음** → `rewrite-required/`로 이동 | | `audit/luau-test-first-run-2026-08-13.md` | 그 스파이크의 통과 기록에 정정 표시 | ### 부수 피해 — `research/debounce-throttle-plan.md` 3절 이 잘못된 서술을 전제로, Debounce 설계 문서가 **"파생 State 위에 얹으면 debounce가 조용히 throttle로 퇴화한다"**는 발견을 만들어냈고, 거기서 "`Debounce`는 `Source`에 가깝게 걸어라"는 규칙(“`Blocker`의 '끝에 걸어라'와 정확한 거울상”)과 열린 질문 하나가 파생됐음. **전제가 무너지면서 전부 철회됨** — 그 문서 3절에 철회 기록으로 남아 있음. ## 교훈 **확정 문서의 한 문장을 근거로 새 설계를 세울 때, 그 문장이 *같은 문서의 다른 확정 문장*과 모순되지 않는지까지 확인할 것.** 이 건은 인용 자체는 정확했는데(verbatim) 인용된 문장이 틀린 경우였고, 그 위에 두 라운드에 걸쳐 설계가 쌓였다가 통째로 무너졌음. `doc-check.py`는 참조가 *존재하는지*는 보지만 *서로 모순되는지*는 못 봄 — 이건 여전히 사람/에이전트가 손으로 대조해야 하는 영역.