`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>
234 lines
16 KiB
Markdown
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) 확정 문서의 순서 계약 정정이라
|
|
정확히 그 축에 걸린다.
|