`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>
16 KiB
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건 만들었다
- 하드코딩 "넷" — 배너에 *"검증 스파이크 넷이 재작성 대기"*라고
쓰면서 같은 문장에서 "개수·현황의 소스는
STATUS.md, 여기서 세지 않음"이라 선언했다. 실제로는 7개. debounce-throttle-plan.md의 M3 누락 — "전부 M3/M8"에서Blocker만 M2로 옮기다 State 코어(M3)가 통째로 빠졌다.- ⭐
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.md2번이 인정한 바로 그 사실인데 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.md2번(M2/M3 마일스톤 경계) 사용자 회신 — M2 착수 전 필요.HUMAN_TODO.md11번이 사용자용 진입점.- 사용자가 직접
/code-review를 돌린 뒤 그 결과 반영 + 커밋.conventions.md가 이미 실측으로 기록해둔 대로(2026-08-18) code-review는 감사자가 못 보는 축을 본다 — 이번 변경은 (a) 마일스톤 경계 이동, (b) 여러 문서에 걸친 용어/이름 일괄 치환, (c) 확정 문서의 순서 계약 정정이라 정확히 그 축에 걸린다.