# 2026-08-24-02 — M2/M3 마일스톤 순서 교체 (반응형 먼저) **요청**: *"질문절에 마일스톤 순서 이슈는 (a) M2와 M3의 순서를 바꾼다 를 택하는게 맞겠던데 어떻게 봐?"* → 검토 후 동의, *"진행해줘"*로 전량 반영. ## 1. 발단 2026-08-22 `ROADMAP.md` 전반 점검이 남긴 마지막 열린 항목. 옛 M2(디스패치 엔진)와 옛 M3(Store/State/Source)의 의존이 양방향이라 그 순서로는 M2를 끝까지 짤 수 없었다. 설계 결정이 아니라 **순서** 문제라 `question.md`의 최우선 절이 아니라 2번 항목으로 따로 뒀었다. ## 2. 검토 — (a)에 동의한 근거 사용자가 이미 (a)로 기울어 있었고, 확인 결과 방향은 맞았다. 근거 셋: 1. **의존의 비대칭이 명확하다.** 디스패치 → 반응형은 *본체* 의존이라 우회 불가(`setLength`가 `State`를, `setOffsetSource`가 `Source`를 받고 `recompute`가 `offset:Set()`을 부름). 반대는 *등록 표면* 의존(핸들러 3개 등록)이라 뒤로 미루면 그만이다. 2. **옛 순서면 디스패치 마일스톤의 `mock 대상 테스트` 체크박스가 원리적으로 불가능했다** — `setLength`/`recompute`를 State 없이 테스트할 수 없다. 반대로 반응형 코어는 순수 Lua라 `luau`만으로 단독 테스트가 된다. 3. **2026-08-22의 `EpochMap`/`GateNode`/`Blocker` 앞당김이 불필요해진다.** 그 이동은 "디스패치가 먼저인데 디스패치가 쟤들을 호출한다"는 이유였다. 순서를 바꾸면 마일스톤 내용이 의존 계층과 그대로 일치하고 끌어온 항목이 0개가 된다. "게이팅 먼저"도 그대로 지켜진다(게이팅이 디스패치보다 먼저). **(b) 분할을 안 고른 이유**: 참조 비용이 오히려 크다 — 기존 `M2` 참조 전부가 "M2a인가 M2b인가"로 애매해지는데 순서 교체처럼 기계적으로 못 푼다. `Brand`를 앞으로 빼는 일은 (b)에서도 똑같이 해야 하고, 얻는 건 "디스패치 코어를 조금 일찍 짠다"뿐인데 M4 전까지 그걸 소비하는 게 없다. **(c) 유지를 안 고른 이유**: `project-context.md`가 못박은 "순서의 소스는 `ROADMAP.md`"를 스스로 무효화한다. ## 3. 그냥 맞바꾸기로는 안 끝났다 — 공통 기반 셋 검토 중 드러난 것. 옛 M2 안에 **반응형보다 먼저 와야 하는** 항목이 셋 섞여 있었고, State-free이자 dispatch-free라 어느 쪽에도 안 걸린다: - `Brand.luau` — `Source`가 `SourceBrand`+`EpochBrand`에 등록돼야 함. - `Relate.luau` — `bindLifetime`/`unbindLifetime`이 그 위에 구현됨. - `LifetimeHandle.luau` 인터페이스 — State 전파 루프가 매 발화마다 `canExecute`를, 이중 바인딩 게이트가 `canBound`를 부름. 새 M2 앞머리에 `### 공통 기반` 절을 만들어 셋을 옮겼다. 반대로 새 M3로 넘어간 것은 둘 — Observer/Effect **동적 경로 가드 등록**과 **`ObserverEffectLeafHandler`**(`H-39`). 결과적으로 역방향 간선이 없다. ## 4. 실행 중 낸 실수 하나 — 한글 인접 `M2`가 안 바뀌었다 첫 일괄 치환에 `re.sub(r'\bM([23])\b', ...)`를 썼는데, **Python `re`의 `\w`는 유니코드라 한글도 단어 문자**다. 그래서 `M2는`/`M2로`/`M2가`처럼 한글이 바로 뒤에 오는 경우 `\b`가 성립하지 않아 **155건만 바뀌고 93건이 그대로 남았다** — 코퍼스가 옛 번호와 새 번호가 섞인 상태가 됐다. `git checkout -- .`은 하네스가 막아서(destructive) `git show HEAD:<경로>`로 26개 파일을 되돌린 뒤, ASCII 경계만 보는 `(?