# 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로 뜨지만 워크트리 아티팩트, 본 레포에선 정상).