# 2026-08-14 여덟 번째 세션 — Debounce/Throttle 백로그 + "emit은 항상 전파" 정정

> **[읽기 전 주의 — 경로 표기]** 이 세션은 **워크트리에서** 진행됐고, 그
> 워크트리는 `10cd31b` 기준이라 **`bind/store/state` 3단계 분할(같은 날
> 일곱 번째 세션) 이전**이었음. 그래서 아래 서술은 전파 모델이
> `bind-system-plan.md`에 있다고 말하지만, **메인에 옮겨진 지금은
> `base/source-state-plan.md`의 "전파 모델 확정" 절**임. 원문 보존
> 원칙에 따라 본문은 당시 표기 그대로 두고 여기 한 줄로만 짚어둠.

**요청**: "`Blocker`와 유사하게 Debounce/Throttle를 만들어야 하는데, 이에
대한 백로그를 짜줘. 너가 일단 다 정의해보고 내가 그 정의를 보고 판단해볼게.
워크트리 하나 파서 작업해."

즉 **에이전트가 먼저 전부 정의하고 사용자가 판정하는** 모드 — 확정 문서를
만드는 게 아니라 판단 재료를 만드는 세션. 산출물은
`research/debounce-throttle-plan.md` 하나 + 인덱스 레이어 반영.

## 워크트리 관련 시행착오 (기록)

`EnterWorktree`가 기본 설정(`worktree.baseRef = fresh`)대로 `origin/master`
에서 브랜치를 땄는데, 이 레포의 `origin/master`는 **quad v1 시절 원격의 옛
히스토리**라 `.claude/`가 통째로 없는 상태로 시작됐음(`4824bab Merge pull
request #7 ...`). 로컬 `main`(`10cd31b`)으로 `git reset --hard`해서
바로잡음. 다음에 워크트리를 팔 때도 같은 일이 생길 것 — **이 레포에서
워크트리를 만들면 항상 로컬 `main` 기준인지 먼저 확인할 것.**

## 읽은 것

`base/blocker-plan.md`(게이티드 State의 정확한 계약), `base/effect-plan.md`,
`base/lifecycle-pattern.md`(GC 위임 원칙/`bindLifetime`), `base/
module-lifecycle-plan.md`(백엔드 주입 경로), `base/purity-and-effects-plan.md`,
`base/bind-system-plan.md`의 온톨로지/전파 모델 절, `base/tween-plan.md`
헤딩(엔진 종속 프리미티브의 선례), `research/operator-sugar-plan.md`(이
항목이 원래 매달려 있던 자리), `research/additional-primitives-plan.md`.

## 실제로 새로 알아낸 것 세 가지

### 1. `Blocker`와 같은 자리, 다른 트리거

`blocker-plan.md`의 게이티드 노드 계약(블록 중이면 전파 안 하고
`HasBlockedEmit`만 세팅, 열릴 때 정확히 1회 전파)이 debounce/throttle이
필요로 하는 것과 **글자 그대로 같음**. 차이는 여는 주체뿐(사용자 `:Off()`
vs 타이머). 그래서 새 전파 메커니즘이 아니라 **릴리스 트리거 교체**로
정리했고, 구현 권고도 "`Blocker` 구현 시점에 게이트 노드를 공용으로
빼두라"가 됨.

부수적으로 확인한 것: **공개 `Blocker` API 위에 얹어서는 못 만듦** —
"상류 신호가 지금 도착했다"는 통지가 공개 API에 없어서 타이머를
(재)시작할 시점을 알 방법이 없음. 그래서 내부 훅이 필요하고, 그 훅은
`Blocker`가 이미 갖고 있는 것과 같은 것.

### 2. ⭐ 무효화 dedup 때문에 파생 State 위에선 debounce가 throttle로 퇴화

이게 이번 세션의 진짜 발견. `bind-system-plan.md`의 확정 규칙 —

> 신호를 받은 State는 자기 `invalid` 플래그만 세우고, 이미 `invalid`였다면
> 그 아래로 더 전파하지 않는다

— 때문에, 게이트가 **파생 State 위**에 얹히면 버스트 중 두 번째 이후
신호를 아예 못 받음(중간 노드가 이미 invalid라 삼킴). 결과적으로 "마지막
변경 후 T초"가 아니라 "첫 변경 후 T초"가 되어 **주기 T의 throttle처럼**
동작함. 값 자체는 커밋 시점에 pull하므로 항상 최신 — 정확성 버그가
아니라 **타이밍 의미론이 조용히 바뀌는** 종류라 오히려 더 안 보임.

`Source` 바로 위에 얹으면 문제없음(Source는 `invalid` 개념이 없어 매
`:Set()`마다 신호). 그래서 규칙이 나옴 — **Debounce/Throttle은 Source에
가깝게, `Blocker`는 파이프라인 끝에.** 정확한 거울상이라 문서화하기 좋음.

"게이트가 신호 받을 때마다 상류를 `:Get()`해서 체인을 되살린다"는 우회는
laziness를 정면으로 깨고 debounce의 존재 이유와 반대라 기각 권고로 적음.

### 3. 배치 근거가 `operator-sugar-plan.md`의 추측과 다름

그 문서는 "타이머가 필요하니 `Tween`처럼 quad-roblox"로 추정해뒀는데,
`Tween`이 quad-roblox인 진짜 이유는 TweenService라는 **엔진 기계**(보간
엔진/easing/per-instance 객체)에 의존해서고, debounce가 엔진에서 필요로
하는 건 **시계 하나**뿐임. 14차 세션이 `Tag`/`Attribute`에서 내린 판단
(부기 알고리즘은 quad-base, 엔진에 손대는 한 줄만 주입)과 정확히 같은
상황이라 **quad-base + 주입 op 2개**를 권고로 뒤집음.

주입 op는 (당시 가칭) `scheduleAfter(seconds, fn) -> handle` /
`cancelScheduled(handle)` 둘 — **2차 리뷰에서 사용자가
`setTimeout(func, delay)` / `clearTimeout(handle)`로 지정, 인자 순서도
함수 먼저로 확정**. **`now()`는 일부러 뺐음**(이 판단도 2차 리뷰에서
정정됨, 아래 참고) — Debounce/Throttle/
MaxTime 전부 "창이 끝날 때 콜백"으로 표현돼서 시계 없이 닫힘. 부수 효과로
`quad-mock`에서 가상 시계 결정론적 테스트가 공짜(이것 자체가 base 배치를
미는 또 하나의 근거).

## 그 밖에 정리한 것

- **의미론 두 갈래**: (A) emit-gate(`Blocker`와 동일, `:Get()`은 최신값)
  vs (B) value-hold(`:Get()`도 지연된 값). (B) 권장 — VueUse/RxJS 의미론과
  일치하고 `Blocker`와 역할이 깔끔히 갈림. 확인 과정에서 **`blocker-plan.md`가
  인용하는 "`Get()`은 라이브 레퍼런스를 준다"가 실제로는 `store-semantics.md`의
  *레퍼런스 의미론*(테이블을 복사본 아닌 라이브 참조로 준다) 원칙이지
  "항상 상류 최신값"이 아니라는 걸 확인** — 그래서 (B)가 확정 원칙과
  충돌하지 않음. 다만 그 한 줄이 오해될 여지가 있어 열린 질문 Q7로 남김
  (확정 문서라 임의 수정 안 함).
- **`Throttle`은 `Debounce` 프리셋으로** 권고 —
  `Throttle{Time=t} == Debounce{Time=t, Leading=true, Trailing=true, MaxTime=t}`
  가 lodash의 실제 구현 관계 그대로. quad가 반복해온 "같은 일 하는 두 번째
  경로 안 만들기"와 맞음.
- **이름 위험**: Roblox 커뮤니티의 `debounce`는 재진입 방지 불리언이라
  정면 충돌. 그래도 업계 표준 이름 유지 + 문서 경고를 권고(검색/이주
  비용). `-ed`를 안 붙이는 건 코퍼스 규칙으로 이미 결정됨(lazy한 건 원형,
  `Compute`가 `Computed`가 아닌 것과 같은 이유) — 열린 질문 아님.
- **GC**: 대기 중 타이머가 게이트를 강참조하므로 다운스트림이 다 죽어도
  최대 `Time`초 생존 — 유계·자가치유라 누수 아님. 게이트는 `inst`에 안
  묶인 순수 값 계층이라 `bindLifetime` 배선 불필요, `Relate`도 안 써서
  두-`Relate` 상호 순환 위험과 무관. **Dispatch 변경 없음.**
- 열린 질문 8개를 문서 12번 절에 번호로 모으고, 그중 사용자 취향이 실제로
  갈리는 넷(의미론/제어핸들/이름/파생 State 적용)만 `question.md` 3번에
  요약.

## 1차 리뷰 라운드 (같은 세션, 사용자가 초안 읽고 지적)

사용자 지적: "스로틀은 '나중에 처리해준다'가 필요한 부분이다. 1초 스로틀에
0.0과 0.1에 누르면 1.0에 0.1 때의 값이 다시 적용돼야 한다. 즉 setTimeout이든
뭐든 태스크가 필요하다. 로블록스는 `task.wait`/`delay`가 있지만 다른 엔진은
다를 수 있고, **`task` 자체가 그냥 Luau에는 없다.** 기본 구현은 quad-base에
있고 엔진 따라 해당 태스크 부분만 배선하면 되도록 만들어져야 한다 생각함."
+ "디바운스가 정확히 보통 어떤 동작인지 알려달라."

세 가지가 바뀜:

1. **Q5(패키지 경계) 해소** — 사용자가 quad-base + 엔진별 배선으로 확정.
   추가로 얻은 근거: 순수 Luau엔 `task`가 없을 뿐 아니라 이벤트 루프
   자체가 없어서 **base가 "동작하는 기본 스케줄러"를 제공할 수가 없음** —
   엔진 op 3개와 같은 관례대로 미배선 시 명확한 에러를 내는 스텁이어야
   함. 그리고 사용자가 짚은 대로 `Throttle`도 trailing 때문에 이 배선이
   똑같이 필요함(스로틀만 타이머 없이 되는 게 아님).

2. ⭐ **초안 의사코드의 실제 버그 발견·수정.** 사용자의 0.0/0.1 시나리오를
   트레이싱하다가, 초안이 lodash식 `MaxTime`(maxWait) 공식을 그대로 옮긴
   탓에 **trailing 통과 직후 타이머를 전부 회수해버려 바로 뒤 신호가
   "창 밖"으로 판정돼 또 즉시 발화**하는 걸 발견(1초 안에 두 번). 고치면서
   훨씬 나은 정식화가 나옴 — **디바운스와 스로틀의 차이는 "신호가 창
   타이머를 리셋하는가" 한 비트뿐**이고, leading/trailing/통과 후 창
   재개방은 완전히 동일함. `maxWait` 트릭 없이 스로틀이 정확히 나오고
   이중 발화 구멍도 구조적으로 사라짐. 그래서 구 Q3("Throttle은 Debounce의
   프리셋")을 폐기하고 **공개 생성자 2개 + 내부 구현 1개(`Reset` 파라미터,
   비공개)**로 개정. `MaxTime`은 디바운스 전용 안전장치로 역할 축소
   ("신호가 안 끊기면 영원히 발화 안 함"이 디바운스의 정의라서 필요한
   것이고, 스로틀은 원래 주기 발화라 무의미).

3. **3절 발견의 범위 축소.** 무효화 dedup 퇴화는 **`Debounce`만** 겪음 —
   스로틀은 창 안에서 "뭔가 바뀌었나" 불리언 하나만 알면 되고, leading
   통과가 소비자의 `:Get()`을 유발해 체인을 되살리므로 사실상 면역.
   초안이 둘 다 영향받는 것처럼 써놨던 걸 정정. Q6의 적용 범위도 같이 축소.

디바운스 동작 설명 요청에 대한 답은 문서 1-1절로 들어감(정의 + 타임라인
+ "버스트 중간/끝/무한 연속" 3열 비교표). 핵심은 **신호가 끊이지 않으면
디바운스는 영원히 발화하지 않는다**는 것이 버그가 아니라 정의 그 자체라는
점 — lodash `maxWait`의 존재 이유이기도 함.

## 2차 리뷰 라운드 (같은 세션) — 주입 op 시그니처 확정

사용자가 구현 방식까지 구체적으로 지정: `os.clock()`으로 버스트 시작
시각을 잡아 maxWait을 처리하거나, 아니면 처음에 maxWait 타이머와
리셋 가능한 타이머 둘을 걸고 변경마다 후자를 재시작하는 식. 그리고
**"set/clear timeout 둘 다 quad-base에서 프로바이더가 구현해야 할
사항으로 넣자"** — `setTimeout(func, delay) -> Timeout` /
`clearTimeout(Timeout)`. Roblox는 `task.delay(duration, fn, ...)`/
`task.cancel(thread)`로 배선하고, quad-roblox가 간단한 릴레이션으로
`Timeout -> coroutine`를 얻어내면 됨.

반영하면서 정리된 것 넷:

1. **⭐ `os.clock()`은 주입 대상이 아님 — 초안 판단 정정.** 초안은
   "시계가 필요해지면 `now()`를 세 번째 주입 op로 추가"라고 적어뒀는데,
   사용자가 알려준 사실로 무효가 됨 — `os.clock()`은 **Luau 표준
   라이브러리**이지 `task`처럼 Roblox가 얹은 전역이 아니고, Lua 5.x가
   리눅스에서 "프로세스가 소비한 CPU 시간"을 주는 것과 달리 **Luau는
   일관되게 고정밀 값**을 줌. 그래서 결론이 깔끔해짐: **주입이 필요한
   건 "미래에 실행시키는 능력"뿐, "얼마나 지났나"는 언어가 이미 준다.**
   덕분에 6-1절의 최적화들(lazy timer, `MaxTime` 단일 타이머화)이 주입
   표면을 안 늘리고 가능해짐.
   **[같은 라운드 재지적]** 단 `os.clock()`은 **"현재 시각"이 아니라
   기준점 없는 카운터라 diff 전용** — 절대 시각으로 해석하거나 다른 시간
   개념과 비교하면 안 됨. 이 설계는 원래 `maxDeadline - os.clock()` 같은
   **남은 시간 계산**만 하므로 제약을 안 건드리고, 부수적으로 벽시계
   보정(NTP 등)에 영향받지 않는다는 장점까지 있음. 문서 6절에 경고
   박스로 명시.

2. **`MaxTime`을 타이머 1개로.** 사용자가 제시한 두 갈래 중 의사코드는
   (b)(타이머 둘)를 쓰지만, `os.clock()`을 그냥 쓸 수 있으니 마감을
   `min(Time, maxDeadline - os.clock())`로 잡으면 **한 타이머가 "조용해짐"과
   "더 못 기다림" 둘 다를 표현**함. `MaxTime`이 없으면 `maxDeadline`이
   무한대라 `min`이 항상 `Time`이 되어 분기 없이 흡수됨. 의사코드는
   읽기 쉬운 (b)로 남기고 이 최적화는 구현 권고로 적어둠.

3. **`Timeout` 타입 — 에이전트가 `any`를 권했다가 사용자 반론으로 뒤집힘.**
   에이전트 근거 셋은 (a) `bindLifetime(inst: any, value: any)` 선례,
   (b) base 내부 배관이라 타입 안전이 사줄 게 없음, (c) 전용 타입이면
   Roblox가 타이머마다 테이블 + `Relate` 엔트리를 강제당함(디바운스는
   신호마다 거는 핫패스)였는데, 사용자가 셋 다 반박:
   - **(a)가 핵심 오류** — `bindLifetime`이 `any`인 건 **거기 진짜로
     아무거나 오기 때문**이고, `setTimeout`/`clearTimeout`은 **자기가
     만들어낸 것만 주고받는 닫힌 루프**라 성격이 정반대. 같은 선례로
     묶을 수 없음.
   - **(b)도 뒤집힘** — 닫힌 루프이기 때문에 오히려 `clearTimeout(1)`을
     **타입 에러로 잡아줄 수 있음**. `any`는 그 공짜 검사를 스스로 끄는 것.
   - **(c)는 더 나은 구현으로 무력화** — `Relate`를 걸 게 아니라
     **네이티브 핸들을 `Timeout` 테이블 필드에 직접 넣으면 됨**(타입은
     캐스트로 맞춤). 릴레이션 층이 사라지고 남는 건 테이블 1개 할당인데,
     `task.delay`가 **코루틴을 하나 만드는** 호출이라 그 옆에서 노이즈
     수준 — 핫패스 논거가 성립 안 함.
   - 런타임에 마커 필드를 넣는 것도 무방하다고 확인해줌.

   **확정**(후속 한 왕복으로 페이로드 자리까지 합의):

   ```lua
   export type Timeout = {
       __type_timeout: true,  -- 판별 마커. 런타임에도 실제로 넣음
       _native: any,          -- 백엔드 전용 페이로드. base는 절대 안 읽음
   }
   ```

   `_native`를 타입에 미리 선언해두는 쪽을 에이전트가 제안하고 사용자가
   동의 — 백엔드가 `:: any` 캐스트 없이 그냥 대입할 수 있고 `any` 탈출이
   필드 하나에 갇혀 경계가 문서화됨(캐스트 방식은 탈출구가 호출 지점마다
   흩어져서, 나중에 누가 다른 필드를 더 끼워넣어도 아무도 모르는 게
   문제였음 — `typing-limits.md`가 싫어하는 "조용히 타입 검사 끄기"). `_`
   접두사는 `handle._observer`/`slot._mountedInst`/`_fired` 관례와 일치.
   부수 효과로 "취소 없는 엔진" 스케치도 깔끔해짐 — 그 백엔드는 `_native`에
   coroutine 대신 취소 클로저를 담으면 되고, base는 어느 쪽이든 모름.
   `Brand` 편입은 불필요 — `Brand`는 사용자가 값 종류를 판별하는 용도인데
   `Timeout`은 사용자 표면에 안 나옴.

4. **취소 없는 엔진 대응이 확인됨.** 사용자가 제시한 래핑+유효 플래그
   트릭으로 어떤 엔진에서도 `clearTimeout`을 구현할 수 있으므로,
   **`clearTimeout`은 base가 백엔드에 요구해도 되는 계약**. 단 이 방식은
   타이머가 예정대로 깨어나 아무 일도 안 하는 형태라 8절의 "대기 타이머가
   게이트를 붙잡는다"는 성질은 그대로 남음(여전히 유계).

**`setTimeout`/`clearTimeout`이라는 이름을 고른 근거(사용자 명시)**:
`task`는 표준도 아니고 Luau의 것도 아닌 **한 엔진의 것**이라, base처럼
"누가 실제로 그려주는지 모르는" 층에 그 어휘를 새기면 특정 백엔드만
특별대우하는 셈이 됨 — 그래서 가장 대중적이고 엔진 중립적인 JS 어휘를
가져옴. (14차 세션이 엔진 op를 `addTag`/`setAttribute`로 정할 때 Roblox
`CollectionService`와 웹 `className`/`data-*` 양쪽에 걸치는 이름을 고른
것과 같은 결.) 에이전트가 처음엔 이걸 단순 "함정"으로만 적었다가,
사용자가 "내부 배선에서 틀리면 안 되는 건 맞지만 이유는 있었다"고
짚어줘 근거를 같이 기록.

부수적으로 기록한 구현 함정 둘: **`task.delay`는 시간이 먼저**라
`setTimeout(func, delay)`로 배선할 때 인자가 뒤집힘(중립 이름을 택한
대가이고, 배선 지점이 백엔드당 한 곳뿐이라 감당 가능), 그리고
`task.delay`의 `...`(추가 인자 전달)은 **일부러 안 받음** — 게이트
콜백은 게이트당 하나씩 만들어져 재사용되는 안정된 클로저라 varargs로
아낄 할당이 애초에 없음.

## 3차 리뷰 라운드 (같은 세션) — 3절 전제 붕괴, base 모순 발견

사용자 지적: **"emit은 항상 재전파되고. 정확히 저 동작은 Blocker가
하는거야. 아니면 너가 잘못 읽었을지도."** — 이 문서 3절("가장 중요한
발견")이 인용하던 `bind-system-plan.md`의 무효화 dedup 문장에 대해.

원문 확인 결과 **인용 자체는 정확**했음(분할 전 `bind-system-plan.md` 708-710행(현 `source-state-plan.md`),
verbatim). 즉 잘못 읽은 게 아니라 **그 base 문장 자체가 문제**. 확인한
근거 셋:

1. **다이아몬드 근거가 요구하는 범위를 넘어섬** — 다이아몬드는 *한 번의
   변경*이 여러 경로로 같은 노드에 닿는 것이라 **전파 파동 안에서만**
   접으면 되는데, 문장은 `invalid`를 시간에 걸쳐 유지되는 상태로 써서
   ("이미 `invalid`였다면") 이후의 모든 변경까지 삼킴.
2. **`architecture.md`와 어긋남** — 그쪽은 같은 문제를 "pull-recompute
   (`Get()` 시점) — Fusion식 eager 노드 없이도 다이아몬드 의존성 중복
   재계산 문제가 풀림"이라고 서술. 즉 중복 *평가*를 막는 주체는
   pull-recompute 자체고, `invalid` dedup이 아낄 수 있는 건 순회 비용뿐.
   base 안에서 같은 문제의 해결 주체를 두 문서가 다르게 지목 중이었음.
3. **⭐ 확정된 `Observer` 계약과 정면 충돌** — 같은 파일이 `fn`에서
   `:Get()`을 안 부르는 걸 명시적으로 허용해뒀는데("`Get()` 호출 여부를
   작성자가 직접 결정하게 열어둔 것"), dedup 문장을 액면대로 적용하면
   그런 Observer는 **한 번 울고 영구 침묵**함. 취향이 아니라 실제 모순.

사용자의 "저건 Blocker가 하는 것"도 정확 — `HasBlockedEmit`이 바로 그
동작이고 `Blocker`는 그걸 **opt-in 게이트**로 제공함. 모든 State에
암묵적으로 심으면 `Blocker`의 존재 의의가 절반 사라짐.

**이 문서에 미친 영향(큼)**: 3절 전체 철회. 파생 State 위 퇴화 없음,
"`Source`에 가깝게 걸어라" 규칙 폐기(그 "Blocker의 정확한 거울상"이라는
예쁜 대칭도 틀린 전제 위였음), Q6 소멸, 2차 리뷰의 "Throttle은 면역"
범위 축소도 무의미. **두 라운드를 버틴 "가장 중요한 발견"이 세 번째
라운드에 통째로 무너진 사례**로 기록해둠 — 확정 문서를 인용할 때
"그 문장이 다른 확정 문장과 모순되지 않는가"까지 확인하지 않으면
발견이 통째로 헛돈다는 교훈.

**대신 생긴 것**: base 정정 항목 `question.md` **0-E** 신설(당시엔 확정
문서라 안 고치고 승인 대기) — 아래 4차 라운드에서 사용자 확정으로
**같은 세션에 해소·전면 반영됨**.

## 4차 리뷰 라운드 (같은 세션) — 사용자 확정 후 코퍼스 전면 정정

사용자가 세 항목에 각각 답하며 모델을 확정:

> 2(재계산 방지)는 맞음 — 한 곳에서 `Get`이 되면, `invalid`하다면 위로
> 올라가서 받아와서 계산 처리된 게 들어오고 cache가 쓰인 다음 `invalid`가
> 꺼짐. 1: **emit은 항상 전파함. `Blocker`나 emit 전파 지연요소만 이를
> 지연할 수 있음.** 3: 맞음, 내가 원래 emit은 항상 전파라 했는데 어떤
> 엉뚱한 에이전트가 이상한짓 하고 간듯. **다 다시 써야 해.**

즉 에이전트가 3차 라운드에서 제시한 두 갈래 중 (a)에 가깝되, "파동 단위
dedup"은 **지금 넣지 않고** 순수 구현 최적화로만 남기는 쪽. 확정 모델:

- `invalid` = **"내 캐시가 낡았다"는 표시 하나뿐**, 전파 제어 장치 아님.
- **emit은 자기 `invalid` 상태와 무관하게 항상 전파.**
- 중복 **재계산**은 pull-recompute + 노드별 캐시가 막음.
- 중복 **통지**는 안 접음 — 다이아몬드에서 아래쪽 Observer가 한 사이클에
  두 번 우는 건 의도된 동작. 접으려면 `Blocker` 같은 **명시적 게이트**.

### 고친 곳 (코퍼스 전체)

| 파일 | 무엇을 |
|---|---|
| `base/source-state-plan.md` | 전파 규칙 재작성, **"다이아몬드 의존성은 무엇이 푸는가" 절 신설**(캐시가 주체임을 3단계 트레이싱으로), `Observer` 절에 "이 허용은 항상-전파에 의존한다"는 상호 참조 추가, 플래튼 기각 근거와 `:With` 빌더 기각 근거 2번을 **캐시 공유**로 재작성(두 결론 모두 유지, 근거 강도는 오히려 상승 — 예전엔 순회 비용이었지만 지금은 실제 중복 계산) |
| `base/architecture.md` | 원래도 맞는 서술("pull-recompute가 다이아몬드를 푼다")이었으나 주체가 캐시임을 명시 보강 + 역전 포인터 |
| `base/blocker-plan.md` | **"emit 전파를 지연시킬 수 있는 유일한 요소"**로 위치 명문화 — 옛 서술이 사실상 Blocker의 일을 모든 State에 암묵적으로 심어 존재 의의를 반쯤 지웠다는 점까지 |
| `reference/comparison-fusion-vide.md` | Vide 대비 "전파를 끊어서"가 아니라 "애초에 push 시점에 계산을 안 해서"로 |
| `research/framework-comparison-findings.md` | 같은 정정. quad가 더 낫다는 결론은 유지, 다만 중복 *통지*는 안 접음을 명시 |
| `ROADMAP.md` | M0 체크리스트의 "이미 invalid면 전파 중단되는지"를 **정반대**로 교체 + `:Get()` 안 하는 Observer 확인 항목 추가 |
| `luau-test` `05-store-state-diamond-propagation.luau` | **옛 모델을 통과 상태로 검증 중이었음** → `rewrite-required/`. `STATUS.md` 개수(done 14→13, rewrite 5→6)/`README.md`도 동기화 |
| `audit/luau-test-first-run-2026-08-13.md` | `05` 행 정정 + **"런타임 12개 전원 통과"를 액면대로 읽지 말라**는 경고 추가(검증 대상이 바뀐 게 이제 셋이라 현행 설계 기준으론 9개) |
| `archive/invalidate-dedup-propagation-reversed.md` | **신설** — 원문·역전 근거 셋·영향 범위 표·교훈 |
| `question.md` → `archive/question-resolved.md` | 0-E 해소 이전(신설된 날 해소) |

**교훈으로 남긴 것**: `doc-check.py`는 참조가 *존재하는지*는 보지만
*서로 모순되는지*는 못 봄. 이 건은 인용이 verbatim으로 정확했는데
**인용된 문장 자체가 틀린** 경우였고, 그 위에 두 라운드에 걸쳐 설계가
쌓였다가 통째로 무너졌음.

## 반영한 인덱스 레이어

`README.md` research 표 새 행, `question.md` 3번, `ROADMAP.md` 백로그
(+"M3에서 `Blocker` 구현할 때 게이트를 공용으로 빼둘 것" 지시),
`operator-sugar-plan.md`(이관 완료 표시 + 배치 추측 정정),
`additional-primitives-plan.md`(2026-08-06의 "빈 자리 아님" 판정이 뒤집힘
표시). `doc-check.py` — 이번 변경으로 새로 생긴 ERROR/WARN 0건(워크트리엔
`initreq/`가 gitignore로 없어서 그 참조 1건이 ERROR로 뜨지만 워크트리
아티팩트, 본 레포에선 정상).
