# [역전됨] "이미 `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`는 참조가 *존재하는지*는
보지만 *서로 모순되는지*는 못 봄 — 이건 여전히 사람/에이전트가 손으로
대조해야 하는 영역.
