quad/.claude/session/2026-08-22-01-roadmap-milestone-review.md
qwreey 9622ccd3e9
design: ROADMAP 마일스톤 경계 재편 + 전반 stale 정리
`ROADMAP.md` 전문을 `base/`의 현재 확정과 대조한 전반 점검. 사용자 판단으로
마일스톤을 각주가 아니라 체크박스째 재편했다.

## 마일스톤 이동 (M3/M7 → M2)

`EpochMap.luau` / `state:Gate`+`GateNode` / `Blocker.luau` / `None`+
`Dispatch/None.luau`. 넷 다 M2가 실제로 호출하는데 각주로만 예고돼 있었고,
`GateNode`는 체크박스 자체가 없어 M2를 훑는 구현자에게 항목으로 보이지
않았다. `LifetimeHandle` 인터페이스를 M8→M2로 옮겼던 전례와 같은 처리.
`Blocker`는 `GateNode` 위의 정책으로 얹는다(노드를 다시 만들지 말 것).

##  새 미결 — M2와 M3의 의존이 양방향

이동하다 드러났다. `Dispatch.setLength`가 `State<number>`, `setOffsetSource`가
`Source<number>`를 받고 `recompute`가 `offset:Set()`을 부르며, `Dispatch.drive`
자신도 배치 등록을 Blocker로 게이팅한다 — 즉 M2는 `Source.luau`/`State.luau`
없이 구현이 안 된다. 설계가 아니라 마일스톤 순서 문제라, 선택지 셋((a) 순서
교체 / (b) M2 분할 — 경계선은 `drive` / (c) 유지)을 `question.md` 2번으로
신설하고 유일한 소스로 지정했다. `HUMAN_TODO.md` 11번이 사용자 진입점.

## 모순 정정

- `Dispatch.drive` 순회 구현은 두 패스가 아니라 단일 일반화 `for`(`F-4-1`).
  "배열→해시 먼저"는 그 루프가 지키는 계약. 코퍼스의 "두 패스"는 본체 루프의
  옛 이름으로 정리(시점 표기는 유지, 용어 각주 신설). 따름정리로 "base는
  언어 동작에 안 기댄다"는 서술이 거짓이 됐고, 재작성될 스파이크 `01`이
  검증할 것도 언어 동작 자체로 바뀐다
- 물리 조작 주입 op 이름은 `native*` 확정(옛 가칭 `mountInst`/`unmountInst`/
  `disposeInst` 폐기). 단건 경로 순서는 `setOffsetSource` → `nativeInsert` →
  `setLength` → `recompute` — 역전 배너를 스스로 단 절 안에 옛 "부기 먼저"
  주장이 두 문단 살아 있었다
- M2 첫 체크박스가 하강 diff와 폐기된 옛 모델을 한 불릿에서 둘 다 서술

## 상태 표시

- `[x]`는 "짜야 할 코드"만. 설계 확정은 `### 확정된 것` 절 또는 전용
  불릿으로 분리(M3/M6/M11) — 이제 `[x]`는 M0/M1에만 남는다
- M0에 `### 재검증 대기` 절 신설 — 설계 변경으로 무효화된 스파이크들이
  어느 체크박스에도 없어 잊히기 쉬웠다(현황의 소스는 `luau-test/STATUS.md`)
- 주입 op 개수 하드코딩을 네 문서에서 걷고 `architecture.md`의
  `EngineOps.luau` 줄 하나로 단일화(그 줄에 빠져 있던 시간 op도 채움)

## 검증

`quad-doc-auditor` 10라운드(각도를 매번 바꿔 34건) + 사용자가 돌린
`/code-review high` 9건. `doc-check.py` ERROR 0. 경위와 실측된 실패 패턴은
`.claude/session/2026-08-22-01-roadmap-milestone-review.md`.

Claude-Session: https://claude.ai/code/session_01TiW21rnti9SbLgF6twtn6D

Co-authored-by: qwreey <me@qwreey.moe>
2026-08-22 03:19:13 +09:00

234 lines
16 KiB
Markdown

# 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) 확정 문서의 순서 계약 정정이라
정확히 그 축에 걸린다.