# 2026-08-22-01 — `ROADMAP.md` 전반 점검: 마일스톤 경계 재편과 stale 정리

사용자 요청: *"마일스톤 전체가 이제 손봐야할 부분이 많다고 생각해. 전반을
확인해볼래?"* — 직전 세션(`2026-08-21-03`)이 `Epoch`/`EpochMap`/`Brand`를
승격하며 커밋 메시지에 마일스톤 관련 항목 셋을 남긴 게 발단.

**이 세션은 커밋하지 않았다** — 사용자 지시(*"이거 다 하고 내가 직접
코드리뷰 트리거해줄게 커밋 하지 말아줘"*). 작업 트리에 17개 파일이 그대로
남아 있고, 사용자가 `/code-review`를 돌린 뒤 그 결과까지 반영해서 커밋하는
게 다음 단계다.

## 1. 무엇을 했나

`ROADMAP.md` 전문(1074줄)을 읽고 `base/`의 현재 확정과 대조했다. 발견을
셋으로 갈라 보고했다 — A(내용 모순), B(순서/소속), C(상태 표시 신뢰성).
사용자가 B와 C-3에 대해 각각 "실제로 옮긴다", "설계 확정은 별도 표기로
분리"를 선택했고 그대로 실행했다.

### A. 모순 — 그대로 읽으면 반대로 구현되는 것

- **M2 첫 체크박스가 자기 자신을 부정**했다. 불릿 머리는 하강 diff인데
  꼬리가 *"`Dispatch.process`는 diff를 하지 않음"*(2026-08-13 다섯 번째
  세션 모델, **같은 날 열네 번째 세션에 폐기**)으로 끝났다.
- **`Dispatch.drive`의 순회 계약이 옛 버전**이었다. QA 4라운드 `F-4-1`로
  "두 패스 구현"이 폐기되고 단일 일반화 `for`로 정정됐는데 ROADMAP 두 곳이
  안 따라왔다. 더 나쁜 건 **`dispatch-core-plan.md` 자신이 모순**이었다는
  것 — `F-4-1` 정정(§390)이 "이식성 논거는 과했다"고 명시적으로 기각한
  근거를 34줄 뒤 `D-10` 문단(§440)이 여전히 현재형으로 주장했다.
- **물리 조작 주입 op 이름이 두 벌** 돌았다. `slot-plan.md`/`architecture.md`는
  `native*`로 넘어갔는데 ROADMAP M6와 `dispatch-core-plan.md`는 가칭
  `mountInst`/`unmountInst`/`disposeInst`를 썼다. 뿌리는
  `dispatch-core-plan.md`가 *"이름은 아직 가칭이라 정식 등재하는 건 그
  확정 시점에 한다"*로 멈춰 있던 것 — 확정이 났는데 그 문단이 안 열렸다.
- **"주입되는 엔진 op 3개"** 하드코딩. `architecture.md`의 `EngineOps.luau`
  줄을 단일 소스로 지정하고 개수를 지웠다.

### B. 순서/소속 — 각주로 덮어온 자리 (사용자 선택: "실제로 옮긴다")

M2가 실제로 필요로 하는 모듈 넷이 전부 뒤 마일스톤에 체크박스로 있었고,
**옮기는 대신 각주가 붙어 있었다**:

| 모듈 | 있던 곳 | 옮긴 곳 |
|---|---|---|
| `GateNode` / `state:Gate` | **체크박스 자체가 없음**(각주 안에만) | M2 |
| `EpochMap.luau` | M3 | M2 |
| `Blocker.luau` | M3 | M2 |
| `None` + `Dispatch/None.luau` | M7 | M2 |

`GateNode`가 특히 나빴다 — M2 체크박스를 훑는 구현자에게 **항목으로 보이지
않았다**. 전례가 있다: `LifetimeHandle` 인터페이스도 같은 이유로 M8→M2
실제 이동을 했었다(각주가 아니라).

### C. 상태 표시

- **M0이 전부 `[x]`인데 검증 스파이크 둘이 무효화**돼 있었고(`01`: 두 패스→
  단일 for, `05`: Epoch 리비전 채택), 재작성 작업이 어느 체크박스에도
  없었다. `### 재검증 대기` 절을 신설해 7건 + 파일 없는 실측 2건을 모았다.
- **머리 blockquote가 `[2026-08-19 기준]`**에서 멈춰 있었다.
- **`[x]`의 의미가 마일스톤마다 달랐다** — M0/M1은 "실제로 짜서 통과",
  M6/M11은 "설계 확정"(코드 0줄). `initValue` 항목은 아예 스코프 배제가
  `[x]`였다. 사용자 선택대로 M6/M11에 `### 확정된 것 — 코드 아님, 구현 전
  필독` 절을 만들어 10개 항목을 옮기고, 체크박스는 "짜야 할 코드"만 남겼다.

## 2. ⭐ 이동하다 드러난 것 — M2와 M3의 의존이 양방향이다

**이게 이 세션의 실질적 산출물이다.** `EpochMap`/`Gate`/`Blocker`를 M2로
옮기려다 확인한 것:

- **M2 → M3 (본체 의존)**: `Dispatch.setLength`가 `len: number | State<number>`를,
  `setOffsetSource`가 `Source<number>`를 받고 `recompute`가 `offset:Set(abs)`/
  `ownerKey.Length:Set(sum)`을 부른다(`dispatch-core-plan.md:1279-1280`,
  `:1621`). 즉 **M2는 `Source.luau`/`State.luau` 없이는 구현이 안 된다.**
  M2로 옮긴 `GateNode`/`Blocker`도 State 위에 얹히므로 마찬가지.
- **M3 → M2 (얕은 의존)**: `state:Observer`/`Effect`의 동적 경로 가드가
  `Dispatch.addHandler` + `Handler.luau` 계약을 쓴다 — M2 앞머리 두 항목이면
  충족.

선택지 셋((a) M2/M3 순서 교체 / (b) M2를 M2a·M2b로 분할 / (c) 유지)을
`question.md` 2번으로 신설하고 **유일한 소스**로 지정했다. `ROADMAP.md` M2
배너 / `todos.md` 00번 / `CLAUDE.md` / `project-context.md` /
`HUMAN_TODO.md` 11번은 전부 가리키기만 한다.

**중요한 구분**: `todos.md`가 오래 유지해온 "M2 착수를 막는 **설계** 항목은
없다"는 여전히 참이다 — 이건 설계가 아니라 **순서** 문제다. 그 구분을
안 하면 두 서술이 모순으로 읽히므로 다섯 곳 전부에 명시했다.

## 3. 감사 루프 — 8라운드, 30건

`conventions.md`의 절차대로 `quad-doc-auditor`를 **한 턴에 하나씩**, 라운드마다
각도를 바꿔가며 돌렸다. 발견 추이: **9 → 5 → 7 → 3 → 3 → 1 → 1 → 0**.

라운드별 각도: (1) diff 범위, (2) 인덱스 레이어 + `base/` 프리미티브,
(3) `archive/`·`reference/`·`qa-request/`, (4) **내 수정 자체를 의심**,
(5) 통독 시점 + `architecture.md`/`source-state-plan.md`/`HUMAN_TODO.md`,
(6) 아직 안 본 `base/` 17개 + `session-summary.md`, (7) "이름만 고치고 의미는
안 고친" 각도, (8) 마일스톤 소속 주장 전수.

### ⭐ 이 루프에서 배운 것 — 내가 고치면서 새 결함을 3건 만들었다

1. **하드코딩 "넷"** — 배너에 *"검증 스파이크 **넷**이 재작성 대기"*라고
   쓰면서 **같은 문장에서** "개수·현황의 소스는 `STATUS.md`, 여기서 세지
   않음"이라 선언했다. 실제로는 7개.
2. **`debounce-throttle-plan.md`의 M3 누락** — "전부 M3/M8"에서 `Blocker`만
   M2로 옮기다 **State 코어(M3)가 통째로 빠졌다.**
3. **⭐ `nativeInsert` 치환 시 순서 주장 미검토** — 3라운드에서
   `dispatch-core-plan.md`의 `mountInst`를 `nativeInsert`로 바꾸면서
   **이름만 갈고 그 이름이 든 문장이 무슨 주장을 하는지 안 읽었다.** 그
   문단은 역전된 옛 C-7(*"부기를 먼저 완결하고 그 다음 물리 마운트"*)을
   현재형으로 주장하고 있었고, 지금 단건 경로는 정반대다
   (`setOffsetSource` → `nativeInsert` → `setLength` → `recompute`,
   `slot-plan.md`의 `rawAdd` 의사코드가 소스). **역전 배너를 스스로 달아둔
   바로 그 절 안에** 옛 주장이 살아 있었다.

셋 다 `conventions.md` 핸드오버 체크리스트 2번(*"배너를 달았으면 그 배너가
부정하는 문장을 같은 커밋에서 고쳤는지 확인"*)이 경고하는 패턴이다.
**일괄 치환은 그 자체가 이 실패 모드를 만든다** — 치환한 줄이 속한 문단을
안 읽으면, 옛 주장이 새 어휘를 입고 살아남는다. 앞으로 용어/이름 치환을
할 땐 치환 지점마다 **문단 단위로** 읽을 것.

### 감사자가 잡은 것 중 성격이 두드러진 것

- **`ROADMAP.md` 맨 위 헤드라인 배너에만 M2 블로커 경고가 빠져 있었다** —
  `CLAUDE.md`/`project-context.md`/`todos.md`/`question.md` 넷은 다 넣어놓고,
  정작 그 넷이 "진행 상황의 소스"로 지목하는 문서의 첫 배너를 빠뜨렸다.
  ROADMAP만 읽는 사람이 M2를 바로 시작할 수 있는 상태였다.
- **`architecture.md` 소스 트리에 `EpochMap.luau`가 아예 없었다** — ROADMAP
  M2가 "별도 모듈로 낼 것"이라 명시하는데, "다음 세션이 실제로 만들 구조"라
  자임하는 정본에서 빠져 있었다.
- **`source-state-plan.md`가 `Blocker`를 "M3에서 함께 구현"이라 서술** —
  `blocker-plan.md`엔 정정 배너를 달았는데 정작 **그 사실을 처음 서술한
  본문**은 안 고쳤던, 체크리스트 2번 그대로의 사례.
- **`HUMAN_TODO.md`만 인덱스 레이어 동기화에서 빠졌다** — *"`question.md`엔
  이제 '결정 대기' 절 자체가 없음"*이라 단정 중이었다.
- **`두 패스`가 코퍼스 전반에 남아 있었다** — `ref-plan.md`가 "본체 루프"로
  고친 **같은 문장 안에서** 다시 "두 패스로 처리"라고 썼다. 구현 단정
  용법은 전부 "본체 루프"로 바꾸고, `ref-plan.md`/`lifecycle-hooks-plan.md`에
  용어 각주를 신설했다. **"두 패스보다 먼저"/"두 패스가 전부 끝난 뒤" 같은
  *시점* 표기는 의도적으로 남겼다** — `PostRef` 절 제목이기도 하고 인용
  대상이라, 바꾸면 절 참조가 깨진다.

## 4. `/code-review high` — 감사 0건 뒤에 9건 더

감사 루프가 0건으로 수렴한 **뒤에** 사용자가 `/code-review high`를 돌렸고
**9건이 더 나왔다. 전부 유효했다.** `conventions.md`가 2026-08-18에 실측으로
기록해둔 것("code-review는 감사자를 대체하지 않는다 — 둘이 보는 축이 다르다")이
이번에도 그대로 재현됐다.

**⭐ 가장 나빴던 것 — 내가 새로 쓴 체크박스가 역전 전 표기를 구현 지시로
복제했다.** M2의 `GateNode` 항목에 *"내보내는 emit은 `emit(self)` + 흡수
집합"*이라 적었는데, 확정 계약은 정반대다 — `state-epoch-plan.md` §5는
*"게이트 노드 자체는 페이로드에 안 싣는다 … 하류는 게이트 identity를 한 번도
안 쓴다"*이고, 넘기는 건 스왑해 떼어낸 `EpochSet` 스냅샷뿐이다. `emit(self)`는
`Epoch` 일반화 **이전** 표기인데 `gate-plan.md`의 배너와 4번 제목에 잔존해
있었고, 내가 그걸 그대로 베껴 **M2를 짜는 사람이 게이트 노드를 출처로 싣게
만드는 지시**로 승격시켰다. 뿌리인 `gate-plan.md` 두 곳까지 같이 고쳤다.

나머지 여덟 건의 성격:

- **`F-4-1` 정정이 자기 절 안의 다음 문장을 거짓으로 만들었다.** 구현을 단일
  일반화 `for`로 확정해놓고, 아래 문단은 여전히 *"base는 이 우연한 동작에
  기대지 않고 명시적으로 강제한다"*고 주장했다. **단일 `for`는 정확히 그
  언어 동작에 기대는 구현이다.** 따라서 재작성될 `01`이 검증할 것도 "우리
  드라이버가 계약대로 도는가"가 아니라 **언어 동작 그 자체**로 바뀐다. 그리고
  `nil`-hole 방어를 "계약이 보장하니 불필요"로 생략하면 안 된다는 따름정리도
  같이 적었다.
- **`gate-plan.md`가 마일스톤 순환을 "해소"로 서술**했다. `Blocker`를 M2로
  옮겼다고 순환이 닫히지 않는다 — `state:Gate`는 State 메소드라 M2→M3 참조가
  그대로 남는다. `question.md` 2번이 인정한 바로 그 사실인데 gate-plan만
  읽으면 닫힌 것으로 읽혔다.
- **"M2 앞머리 두 항목이면 충족"이 부정확**했다. `Dispatch/init.luau`에
  `Dispatch.drive`가 들어 있고 그 자신이 배치 등록을 Blocker로 게이팅한다
  (`dispatch-core-plan.md`의 "적용 지점" 확정). 선택지 (b)로 쪼갠다면
  `drive`를 어느 쪽에 두느냐가 경계선이 된다 — 사용자가 (a)/(c)를 고를 때
  결합도를 실제보다 얕게 볼 뻔했다.
- **"M0 체크박스는 전부 닫혔다"가 같은 편집이 M0에 넣은 미체크 8개와 충돌.**
  새로 선언한 `[x]` 규약상 그것들은 실제 열린 작업이다.
- **단일 소스로 지정한 자리가 정작 불완전**했다 — `architecture.md`의
  `EngineOps.luau` 줄에 `setTimeout`/`clearTimeout`이 없었고, 인접
  `RobloxFactory.luau` 줄의 주입 대상 목록도 `native*` 없이 멈춰 있었다.
  "개수 하드코딩을 지우고 소스를 하나로"라는 의도가 그 소스가 불완전해서
  성립하지 않았다. 소스 쪽을 실제로 채웠다.
- **근거로 제시한 문서를 열면 확인이 안 되는 주장** — `None.luau`가
  소스 트리에 "같은 위치로 있다"고 적었으나 트리엔 `Dispatch/None.luau`만
  있고 탑레벨 센티널도 `Brand.luau`도 줄이 없었다.
- **새로 세운 `[x]` 규약이 같은 문서에서 이미 깨져 있었다** — M3의
  `store.key` 타입 추론 확인이 코드가 아닌데 `[x]`였다. 배너가 위반 사례를
  "M6/M11"로 한정 열거해서 감사에서 빠졌다. 규약 문구를 일반화하고 그
  항목도 뺐다.
- **재작성 지시가 없는 경로를 가리켰다** — `luau-test/done/01-...`인데 실제
  파일은 `rewrite-required/`에 있다. 하필 *"⚠️ 스파이크 `01` 재작성 필요"*
  지시문 자체가 그랬다.

**이 세션의 교훈을 한 줄로**: 감사 0건은 "코퍼스가 스스로와 정합한다"는
뜻이지 "이번에 새로 쓴 문장이 옳다"는 뜻이 아니다. 특히 **다른 문서에서
문장을 베껴 새 체크박스를 만들 때, 그 문장이 이미 역전된 표기인지를
확인해야 한다** — 감사자는 "베껴온 곳과 원본이 같다"를 보고 정합으로
판정한다(실제로 `emit(self)`가 gate-plan에도 남아 있었으므로 둘은 "일치"했다).

## 5. code-review 뒤 감사 2라운드 — 그리고 실측된 패턴 하나

사용자 요청으로 code-review 수정분에 감사를 두 라운드 더 돌렸다.

- **1라운드 3건, 전부 "직전 수정이 만든 것"** — `todos.md`가 `emit(self)`를
  "사용자 확정"으로 인용하면서 **내가 방금 그 표기를 부정하는 배너를 단
  문서**를 근거로 가리키던 것, "주입 op 전체 목록의 소스는 `architecture.md`"라고
  선언한 **6줄 아래에 그 목록이 그대로 있던** 것(게다가 `native*`도 시간
  op도 없는 불완전한 셋이라, 단일 소스를 세우려던 편집이 오히려 "목록이 두
  곳"을 고착시킬 뻔했다), `nil`-hole 문단 중복.
- **2라운드 1건 — 이번엔 내 수정이 만든 게 아니라 앞 라운드들이 놓친 기존
  자리였다.** `module-lifecycle-plan.md`가 *"주입 대상 목록에 엔진 op 3개가
  추가됨"* + *"`addTag`/`removeTag`/`setAttribute`**만** 채우는 타입 계약"*으로
  같은 하드코딩을 들고 있었다. 스윕해보니 `debounce-throttle-plan.md`도 두
  곳에서 "엔진 op 3개"를 관례 비유로 쓰고 있었는데, **그 문서 자신이
  `setTimeout`/`clearTimeout`을 추가해 그 개수를 늘리는 주체**라 자기모순이었다.

### ⭐ 실측된 패턴 — 개수 하드코딩은 "한 곳을 고치면 끝"이 아니다

"주입 op 3개"라는 같은 사실이 **네 문서**(`ROADMAP.md`, `dispatch-core-plan.md`,
`module-lifecycle-plan.md`, `debounce-throttle-plan.md`)에 흩어져 있었고,
`conventions.md` 체크리스트 4번이 예고한 그대로 전부 갈라져 있었다. 처음
둘만 고치고 "단일 소스로 정리했다"고 선언한 게 이 세션인데, **선언 자체가
나머지 둘을 못 찾았다.** 개수를 지울 땐 그 개수의 **모든 사본**을 grep으로
전수 찾아야 하고, "소스는 X다"라고 선언한 뒤엔 **X가 실제로 완전한지**와
**다른 사본이 남았는지**를 같은 자리에서 확인해야 한다.

## 6. 남은 것

- **`question.md` 2번(M2/M3 마일스톤 경계) 사용자 회신** — M2 착수 전 필요.
  `HUMAN_TODO.md` 11번이 사용자용 진입점.
- **사용자가 직접 `/code-review`를 돌린 뒤 그 결과 반영 + 커밋.**
  `conventions.md`가 이미 실측으로 기록해둔 대로(2026-08-18) code-review는
  감사자가 못 보는 축을 본다 — 이번 변경은 (a) 마일스톤 경계 이동,
  (b) 여러 문서에 걸친 용어/이름 일괄 치환, (c) 확정 문서의 순서 계약 정정이라
  정확히 그 축에 걸린다.
