워크트리(worktree-debounce-throttle-plan)에서 네 라운드로 다듬은 결과를
메인의 3단계 분할 구조에 맞춰 필요한 변경만 이식.
## 신설: research/debounce-throttle-plan.md
- Blocker가 이미 쓰는 게이티드 노드의 릴리스 트리거만 타이머로 바꾼 것.
공개 Blocker API엔 "상류 신호 도착" 통지가 없어 그 위엔 못 얹음 →
M3에서 게이트를 공용 Gate로 뺄 것.
- 두 도구의 차이는 "신호가 창 타이머를 리셋하는가" 한 비트뿐.
공개 생성자 2개 + 내부 구현 1개(초안이 옮겨온 lodash식 maxWait 공식엔
trailing 통과 직후 이중 발화 버그가 있었음).
- quad-base + 주입 op 2개: setTimeout(func, delay) -> Timeout /
clearTimeout. Roblox는 task.delay/task.cancel로 배선(인자 순서 반대).
os.clock()은 Luau 표준 라이브러리라 주입 대상 아님(diff 전용).
Timeout = { __type_timeout: true, _native: any }.
## 역전: emit은 자기 invalid 상태와 무관하게 항상 전파된다
source-state-plan.md의 "이미 invalid였다면 그 아래로 더 전파하지 않는다"가
확정된 Observer 계약(fn이 :Get()을 안 불러도 됨)과 정면 충돌 — 액면대로면
:Get() 안 하는 Observer는 한 번 울고 영구 침묵. architecture.md가 같은
다이아몬드 문제를 pull-recompute로 설명하는 것과도 어긋나 있었음.
정정 모델: invalid는 캐시 낡음 표시일 뿐, 중복 재계산은 pull-recompute+
캐시가 막고 중복 통지는 안 접음(접으려면 Blocker 같은 명시적 게이트).
- source-state-plan.md: 전파 규칙 재작성, "다이아몬드 의존성은 무엇이
푸는가" 절 신설, Observer 절 상호 참조. 플래튼 기각/:With 빌더 기각
근거를 캐시 공유로 재작성(두 결론 유지, 근거 강도는 상승)
- architecture.md, blocker-plan.md(전파를 지연시키는 유일한 요소로 위치
명문화), comparison-fusion-vide.md, framework-comparison-findings.md
- ROADMAP M0 체크리스트: 확인할 것이 정반대가 됨
- luau-test 05 → rewrite-required/(옛 모델을 통과 상태로 검증 중이었음),
STATUS.md 개수 동기화(rewrite 6→7, done 14→13)
- audit: 05 행 정정 + "12개 전원 통과"를 액면대로 읽지 말라는 경고
- archive/invalidate-dedup-propagation-reversed.md 신설
doc-check: ERROR 0 / WARN 84(작업 전 85).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7.5 KiB
[역전됨] "이미 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는 참조가 존재하는지는
보지만 서로 모순되는지는 못 봄 — 이건 여전히 사람/에이전트가 손으로
대조해야 하는 영역.