## 2026-08-09 세 번째 세션 — Slot CRUD 완전 확정, 키 기반 동적 컬렉션 재조정이 `Slot:List(...)` 메소드로 통합·승격 위에서 예고된 "다음 세션 주제"(Slot과 키 기반 동적 컬렉션 재조정)를 실제로 다룬 세션. `pre-implementation-audit.md` 1-7/1-8과 `research/additional-primitives-plan.md`의 마지막 열린 항목이 전부 `base/slot-plan.md`에 흡수·확정됐음 — 상세는 그 문서 본문이 소스, 여기는 요지만: - **Slot CRUD 최종 확정**: `Add(element, index?)`/`Remove(element)`(제거+파괴)/ `Extract(element)`(제거, 파괴 안 함)/`Clear()`(전체 `Remove`) — `get`/`set`은 드롭(YAGNI). 식별은 항상 element 레퍼런스 기준(인덱스 아님). 에러 조건 전부 즉시 `error()`(이미 다른 곳에 마운트된 element를 `Add`, 멤버 아닌 element를 `Remove`/`Extract`) — fail-fast 톤 유지. 재진입성은 별도 가드 불필요(기존 "무한루프 방어 안 함" 원칙 재사용). `Slot()`은 인자 없는 빈 생성자로 확정. - **`isMounted` 이중 추적 분리(1-8 해소)**: Slot 컨테이너 자신은 `self._mounted`(트리거는 `Dispatch.process`가 이 Slot 객체에 실제로 호출된 시점 — 다른 모든 "마운트됨" 판정과 동일하게 dispatch-process 기준), 개별 element는 전역 weak-set(라이브러리 전역 다중 마운트 금지 불변식이라 특정 Slot에 안 묶임). - **`Extract` 후 portal 범위 — 임의의 다른 Slot으로 자유 이동 확정.** 기존 "retract되는 slot은 폐기되지 옮겨지지 않는다"는 확정은 **프레임워크가 store-bind 재실행으로 값을 통째로 갈아치우는 시나리오**에만 해당하고, 사용자가 명시적으로 `Extract`→`Add` 두 번 호출해서 옮기는 것과는 다른 얘기라는 걸 명확히 구분(사용자 확인). - **키 기반 동적 컬렉션 재조정 — `Slot:List(data, keyFn, renderFn) -> Slot`로 확정, 자유 함수/새 타입 둘 다 기각.** 처음엔 `List(...) -> Slot` 자유 함수를 검토했으나, "타입 이름=반환 타입"이라는 `Source(default)`류 팩토리 컨벤션이 깨진다는 문제를 사용자가 직접 지적 — Source⊇State식 구조적 서브타입도 검토했으나 List가 Slot 위에 새 공개 메소드를 안 얹으므로(그냥 "자동으로 채워지는 Slot") 별도 타입일 근거가 약해 기각. 최종적으로 "원천에 종속된 파생 데이터는 메소드로만 얻어진다"(State/ Observer와 같은 원칙, 여기 원천은 Slot 자신)로 수렴 — `Ref():Callback(fn)` 체이닝과 같은 패턴. Fusion `ForPairs`/`ForKeys`/`ForValues` 3분할도 단일 `:List`로 통합 확정. - **구현 메커니즘은 전부 기존 프리미티브 재사용, 새 개념 없음** — 사용자가 "너무 마법같다"고 지적해 의사코드까지 구체화해서 검증: `data:Observer(fn)` (2026-08-07 확정된 "등록 즉시 1회 실행"), `Source(item)`, 방금 확정한 Slot CRUD의 비공개(가드 안 거치는) 버전 세 개의 조합일 뿐. `itemSources`/ `elements`/`order`는 Slot 인스턴스의 평범한 클로저 업밸류(별도 전역 저장소 불필요). 리오더는 `Extract`+`Add(index)` 조합, 최소-이동 알고리즘 자체는 구현 시점 최적화로 미룸. - **`renderFn(key, itemState)`의 `itemState`는 내부 `Source`를 그냥 `State`로 다운캐스트해서 넘김 — 별도 `ReadOnlySource` 타입 안 만듦** (사용자 확인: "그걸 위해 ReadOnlySource 같은 걸 만들 이유가 있냐 하면 아니다, 이미 그게 State다"). 타입 레벨 힌트만, 런타임 강제 없음(`Peek`/ Modifier UB와 같은 "규율 위반은 방어 안 함" 기조) — 나중에 진짜 런타임 강제가 필요해지면 `src:Compute(function(v) return v end)`(항등 함수 Compute)로 `:Set` 없는 State를 만드는 가벼운 대안이 있다는 것만 메모. - **백로그, 착수 안 함(연구만) — reconcile의 무조건 `:Set()` 재전파.** `data`가 테이블 뮤테이션+`:Emit()`으로 오는 경로도 지원해야 해서 이전 값과 동등성 비교를 할 방법이 없고, 그래서 값이 실제로 안 바뀐 item도 매 재계산마다 재전파됨 — 사용자 판단: "이 재계산 비용은 우리가 핸들해야 할 부분은 아닌 것 같다", `Blocker`류 값-동등성 기반 전파 억제도 검토했으나 "확정 안 하면 이전 값 자체가 없어서 비교가 안 된다"는 근본적 어려움이 있어 기술적으로 더 논의해볼 만한 주제로만 `research/ additional-primitives-plan.md`에 백로깅. - **`research/additional-primitives-plan.md` 사실상 전부 해소** — 마지막 열린 항목(키 기반 컬렉션)까지 없어져서, 이 문서엔 이제 새로 열린 설계 질문이 없음(배경 자료로만 유지). `question.md`/`ROADMAP.md`(M6 체크박스)/ `README.md` 전부 동기화 완료. **다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — Slot/키 기반 컬렉션 재조정이 이번 세션에서 완결됐으므로 더 이상 "다음 세션 예고" 대상 아님. 남은 열린 것은 여전히 `question.md`의 `DI`→`D`/`canExecute`→`isAlive`/ `Brand` 이름, `pre-implementation-audit.md` 1-3(우선순위 스캔 동률 처리), "여러 Slot이 형제로 섞일 때 순서 보장"(Roblox 단일 백엔드론 급하지 않음) 정도. **같은 세션 후속 — quad-roblox 구현 관점에서 재검토, `Move`/`Swap` 공개 CRUD로 추가(원시 최소화 원칙 뒤집음), `renderFn`에 `indexState` 추가.** 사용자가 "Slot 값 변경을 quad-roblox가 실제로 어떻게 따라가나"를 구체적으로 캐물으며 세 가지가 드러남: - **`renderFn(key, itemState) -> element`에 위치 정보가 빠져있었음** — Roblox는 순서를 `LayoutOrder`로 표현하므로 `renderFn`이 그걸 반응형으로 바인딩하려면 위치도 State로 받아야 함. `itemState`와 독립된 `indexState: State`를 추가(`renderFn(key, itemState, indexState)`) — 값 변경/위치 변경은 서로 독립 신호라는 게 근거, Slot이 `LayoutOrder`를 대신 관리해주는 마법은 안 둠. - **`Extract`+`Add(index)`로 리오더를 구현하면 백엔드에서 진짜 Parent 조작이 두 번(detach+reattach) 일어난다는 게 드러남** — Roblox `AncestryChanged` 발화, 잠재적 깜빡임, 불필요한 재바인딩 비용까지 딸려올 수 있어 매 `:List` 재계산마다 흔한 케이스치고 과함. **`Move`(O(n), 배열 splice 의미)/`Swap`(O(1), 순수 페어 교환)을 공개 CRUD로 추가** — 둘 다 Parent를 안 건드림. `:List` 없이 수동으로 Slot을 구성하는 사용자에게 애초에 리오더 수단이 아예 없었다는 것도 같이 드러난 공백 — "원시 연산 최소화" 원칙보다 이 두 실사용 공백이 우선한다고 판단해 뒤집음(같은 세션 내 정정이라 별도 archive 없이 `slot-plan.md` 본문에 "원시 최소화 원칙 정정" 절로 직접 반영). - **base/roblox 패키지 경계에 mount/unmount 둘로는 부족, reposition 훅이 세 번째로 필요함** — `Dispatch/Slot.luau`/`Handlers/Slot.luau`가 이제 "Parent 조작(mount/unmount)"뿐 아니라 "Parent 안 건드리는 재배치 (reposition, `Move`/`Swap`)"까지 계약해야 함. quad-roblox가 이걸 `SetSiblingIndex`로 구현할지, `LayoutOrder` 기반 정렬이라 사실상 no-op 으로 둘지는 구현 선택으로 열어둠. - **item 값 전파(무조건, 백로그)와 index 전파(실제 변경시만)가 비대칭인 이유도 명확해짐** — item 값은 외부 뮤테이션+`Emit()` 경로 때문에 "이전 값"을 비교할 방법이 없지만, `:List`가 전적으로 소유하는 `keyIndex`는 "실제로 위치가 바뀌었는지"를 정확히 알 수 있어 index 쪽엔 같은 문제가 없음 — 그래서 index 전파는 처음부터 조건부로 구현. 전부 `base/slot-plan.md`(CRUD 표, "원시 최소화 원칙 정정" 신규 절, `:List` 구현 스케치·설명 갱신)/`ROADMAP.md`(M6)/`README.md` 반영 완료. `question.md`엔 새로 열린 항목 없음 — 이번 후속도 순수 확정/구현 세부 명확화. **같은 세션 두 번째 후속 — `Swap`을 element 레퍼런스가 아니라 인덱스 기준으로 정정, "공개 CRUD는 가드+`raw*` 위임" 구조 명문화.** 사용자가 `Swap(elementA, elementB)`를 바로 잡음 — element 레퍼런스로 받으면 Slot이 element→index 역방향 맵을 안 갖고 있는 이상 두 element의 현재 위치를 각각 찾는 데 O(n)씩(총 2n) 들어서, `Swap`이 약속한 O(1)이 그 자리에서 깨짐. `Move`는 시프트 자체가 O(n)이라 조회 비용이 묻히지만 `Swap`은 조회 비용이 곧 전체 비용이라 이 차이가 그대로 드러남 — `Slot:Swap(indexA, indexB)`로 정정(호출부가 이미 "몇 번째와 몇 번째를 바꿀지"를 아는 상황, 예: 드래그 리오더 UI, 이라는 것도 자연스러움의 근거). 이어서 사용자가 "`Slot:Move` 구현은 결국 락(`_listed`) 확인만 하고 실제 로직은 `rawMove`류에 다 있는 구조 아니냐"고 확인 요청 — 맞다고 답하며 이걸 여섯 CRUD 전체에 적용되는 일반 구조로 명문화: `Add`/`Remove`/`Extract`/`Clear`/`Move`/`Swap` 전부 "`self._listed` 확인 + `raw*` 위임"뿐인 얇은 wrapper, 실제 로직은 전부 `raw*` 함수 세트 하나에 있고 `:List`의 reconcile도 그 세트를 가드 없이 직접 호출. 전부 `base/slot-plan.md`/`ROADMAP.md` 반영 완료. **다음 세션이 할 일**: 여전히 안 바뀜(`ROADMAP.md` M0부터). **같은 세션 세 번째 후속 — Slot 요소 타입 제약 신설: `nil` 금지/`None` 허용/핸들러 계층 값(Ref/PreRef/Observer/Effect/Modifier) 금지, `Slot()` 제네릭화.** 사용자가 "Slot 안에 뭐가 들어갈 수 있는지 정해진 바 없다"고 지적하며 시작 — 처음엔 제가 "Ref/Observer/PreRef도 Slot 요소로 허용, `D.InstSlot = Slot<>`류 백엔드 별칭으로 좁히자"고 제안했으나, 사용자가 바로 반박: Slot이 동적으로 다뤄지는데 그 안에 Ref/Observer가 들어가면 quad-roblox가 그걸 처리할 방법이 없고(특수 대응을 새로 만들어야 해서 오버엔지니어링),애초에 왜 필요한지도 불명확하다는 지적 — 검증해보니 정확히 맞았음: - `Dispatch/Leaf.luau`가 처리하는 "children 배열에 Ref/Observer/PreRef가 직접 놓이는" 케이스는 **그 컴포넌트가 지금 만들고 있는 Instance 자기 자신을 가리키는 self-ref 캡처**라(`Frame { PreRef():Callback(fn) }`가 그 Frame 자신을 잡음), `inst`가 "지금 생성 중인 바로 그 하나의 Instance"로 고정돼 있어야 의미가 성립함. Slot은 특정 컴포넌트 호출 하나에 안 묶이고 이미 존재하는 부모에 나중에 독립적으로 붙는 동적 리스트라 이 전제 자체가 없음 — Slot 안의 Ref가 "무엇"을 가리켜야 하는지 정의가 안 됨. - 대체 경로가 이미 있어 능력 손실도 없음 — 특정 child에 ref가 필요하면 그 child를 만드는 컴포넌트 호출 자체에 Ref를 넘기면 됨 (`slot:Add(Frame { Ref = myRef })`). - 사용자가 직접 대비시킨 반례도 정확함: `State`(Slot 자체가 State의 값)은 retract 시 통째로 버려지고 다시 채워지는 굵은 단위 교체라 이미 확정된 모델(폐기, 재구성)과 맞지만, Slot **요소 하나하나**로 Ref/Observer가 들어가는 건 그런 굵은 단위 교체가 아니라 세밀한 CRUD 대상이라 성격이 다름. - **결론**: `Modifier` 필드가 핸들러 계층 값을 담으면 즉시 `error`로 확정했던 것과 같은 판별 메커니즘(`isRef`/`isPreRef`/`isObserver`/ `isEffect`/`isModifier` Brand predicate)을 Slot에도 재사용 — 새 메커니즘 없이 그대로 막음. 덕분에 `Slot`의 `T`도 "실제로 마운트 가능한 최종 값의 타입"으로 단순해짐 — quad-roblox엔 사실상 `T = Instance` 하나뿐이라 `D.InstSlot = Slot<>`가 사실상 "그" Slot 타입. `nil`은 기존 배열 파트 `None` 원칙을 그대로 적용해 금지, `None`은 `:List`의 `renderFn`이 "이 item은 이번엔 스킵"을 표현하는 용도로 허용 — `renderFn`의 반환 타입도 `T | None`으로 갱신. - `Slot()`가 무인자 생성자라 `T` 추론이 안 되므로 tbox 명시적 제네릭 적용(`Slot<>()`)이 필요하다는 것도 같이 반영 — 정확한 문법은 "자식으로 넘기는 클래스 스토어" 절의 기존 tbox 참고 미결과 같은 갈래로 묶어 열어둠. 전부 `base/slot-plan.md`(신규 "요소 타입 제약" 절, CRUD 에러 조건, `renderFn` 반환 타입) 반영 완료. **다음 세션이 할 일**: 여전히 안 바뀜(`ROADMAP.md` M0부터). **같은 세션 네 번째 후속 — `Slot:List`의 `renderFn`을 "1회 호출"에서 "매 사이클 호출 + `before` 재사용"으로 재설계, filter/toggle 문제 해결.** 사용자가 두 가지를 연달아 제기: (1) `renderFn`이 `None`을 반환해 "지연 렌더"를 표현하는 아이디어는 좋지만, State 변경으로 이미 렌더된 필드를 나중에 다시 지워야 하는 경우(filter)는 기존 "1회만 호출" 모델로 안 풀림. (2) filter/sort를 Slot에서 어떻게 구현할지가 문제 — 흔한 회피책인 "`Visible`만 토글"은 필터링된 item도 여전히 완전히 살아있는 Instance로 남겨서(애니메이션/이벤트 연결 계속 돎) 200개+ 리스트에서 lazy하지 않다는 실질적 비용이 됨. **해법 — 사용자가 직접 제시**: `renderFn(itemState, before: inst?): inst?` 모양으로 바꿔 **매 reconcile 사이클마다 호출**하되, 이전에 마운트된 element(`before`, 없으면 `nil`)를 받아서 `if before then return before end`(바꿀 거 없으면 그대로 반환, 값 갱신은 이미 물려있는 반응형 바인딩이 자동으로 함)로 저비용 재사용 경로를 만듦 — filter 탈락 시엔 `nil` 반환으로 **진짜 파괴**(Visible 토글 아님). 편의상 `renderFn`이 raw `nil`을 던지는 게(Lua에서 자연스러운 관용구) `None`보다 편하다는 것도 사용자가 지적 — 검토 결과 `renderFn`의 반환값은 raw Slot 요소로 직접 들어가는 게 아니라 `:List`의 reconcile이 해석만 하는 것이라, `nil`을 받아도 위 "요소 타입 제약"(raw Slot 요소는 `nil` 금지)과 전혀 안 부딪힘 — `nil`/ `None` 둘 다 "스킵" 신호로 동일하게 받아들이기로 정리. **부수적으로 드러난 것 — "이전 상태를 다음 렌더에 어떻게 넘기냐" 문제는 이미 해소돼 있었음.** 사용자가 "item이 보통 plain table이라 매 렌더마다 Source/Store를 새로 안 만들려면 이전 상태를 어딘가 저장해야 하는데 그게 어렵다"고 우려했으나, 확인해보니 `itemSources[key]`/`indexSources[key]`가 `renderFn` 호출 여부와 무관하게 **처음부터 `:List` 자신이 계속 소유**하고 있어서(원래 설계 그대로) — `renderFn`이 매 사이클 불려도 이 부분은 전혀 안 바뀜, item이 filter 탈락 후 재등장해 Instance가 파괴됐다 새로 만들어져도 반응형 Source는 안 끊기고 그대로 이어짐. 이 부분은 재설계가 아니라 기존 설계가 이미 답이었다는 걸 확인한 것. **sort는 이번 재설계와 무관** — 호출부가 `data` 순서를 바꾸면 기존 `keyIndex`/`Move` 메커니즘이 이미 처리, 새로 손댈 것 없음(사용자가 filter와 같이 물었던 것 중 이건 원래도 문제 없었음). 전부 `base/slot-plan.md`(요소 타입 제약 절 "None 허용" → "nil/None 둘 다 금지"로 정정, `:List`의 `renderFn` 시그니처·구현 스케치·"왜 매 사이클 호출로 바뀌었는가" 신규 절)/`ROADMAP.md`(M6)/`README.md` 반영 완료. **다음 세션이 할 일**: 여전히 안 바뀜(`ROADMAP.md` M0부터). **같은 세션 다섯 번째 후속 — `renderFn` → `updateFn` 개명, `:List`가 `Source` 생성을 그만두고 `userdata`로 그 권한을 통째로 넘김.** 사용자가 "`renderFn`이 아니라 `updateFn`이 맞고, `itemState`도 `:List`가 강제로 만들지 말고 원문 item + `userdata: UD?` + `prev: T?`를 주는 게 낫다"고 제안 — 검토 후 채택, 근거: - **`itemState`/`indexState`를 `:List`가 강제로 만드는 건 불필요한 강요였음** — 반응형이 필요 없는 단순한 행까지 전부 `Source` 생성 비용을 지게 했음. `userdata`로 권한을 넘기면 필요한 item만 자기 `Source`를 만들어 `userdata`에 담고, 나머지는 매번 raw `item`에서 다시 계산해도 됨 — `:List`가 미리 정할 이유가 없는 선택. - **"이전 상태를 다음 호출에 넘기는" 문제, 원래 걱정했던 것과 달리 `userdata`라는 명시적 채널로 완전히 해소됨** — item이 plain table이라 매번 `Source`를 새로 안 만들려면 어딘가 저장해야 한다는 우려가 있었는데, `userdata`가 정확히 그 저장소. - **`prev`(구 `before`)와 `userdata`가 원래 비일관적이었음** — 사용자가 직접 지적: 하나(`prev`)는 `:List`가 자동 관리하는데 다른 하나(`userdata`)만 수동 반환을 요구했음. 해법은 **둘 사이 커플링을 완전히 제거** — `result`가 `nil`이라고 `:List`가 `userdata`를 자동으로 안 지움, 그대로 기록만 함. 흔한 경우(둘 다 리셋)는 `return nil` 하나로 Lua가 나머지 반환 슬롯을 알아서 `nil`로 채워주고, "파괴하되 캐시는 남기고 싶다"는 정당한 패턴은 `return nil, ud`로 명시적으로 표현 가능해짐 — 이전 설계(result nil이면 userdata 자동 삭제)로는 이 패턴이 원천 봉쇄돼 있었음. - **제가 놓칠 뻔한 버그를 사용자와의 논의 과정에서 직접 잡음**: `userdata`가 이제 `mounted`(실제 element)보다 오래 살 수 있게 되므로, 정리 루프가 `pairs(mounted)`만 순회하면 "필터 탈락 상태(mounted=nil)로 `userdata`만 살아있던 key가 `data`에서 완전히 사라지는" 케이스를 못 잡고 새서 — 직전 사이클의 전체 key 집합(`keyIndex`, 매 사이클 모든 key에 대해 채워짐)을 순회하도록 정정. - **부수 효과 — "item 값 무조건 재전파" 백로그가 사라짐**: `:List`가 더 이상 `Source`를 안 만드므로 그 문제 자체가 `:List` 소관이 아니게 됨, `updateFn` 작성자의 선택으로 넘어감. - `userdata = userdata or {}`류 lazy-init 관용구가 `UD`가 자유 제네릭인 채로 Luau 타입 시스템에서 잘 좁혀지는지는 실측 필요 항목으로 명시적으로 남김(사용자가 직접 이 불확실성을 짚음) — M0/M6 착수 시 확인. 전부 `base/slot-plan.md`(`:List` 절 전면 재작성 — `updateFn` 시그니처/구현/ "왜 `Source`를 `:List`가 안 만드는가" 신규 절)/`ROADMAP.md`(M6)/ `README.md` 반영 완료. **다음 세션이 할 일**: 여전히 안 바뀜(`ROADMAP.md` M0부터). **같은 세션 여섯 번째 후속 — `keyFn` 선택 인자화(파라미터 순서 정정), `userdata` cleanup 훅 검토 후 기각·GC-native 제약 명문화, 문서화 순서 질문은 백로그로 이관.** 사용자가 세 가지를 짧게 제기: 1. **`Slot:List(data, keyFn, updateFn)` → `Slot:List(data, updateFn, keyFn?)`로 파라미터 순서 정정, `keyFn` 선택 인자화.** 실사용 대부분 (사용자 추정 80%)이 item identity 추적 없이 순번을 key로 써도 충분한 단순 목록이라 매번 `keyFn`을 명시하게 하는 게 불필요한 보일러플레이트 — 생략 시 `function(item, index) return index end` 기본값. tradeoff(중간 삽입/삭제 시 그 뒤 항목들이 "다른 item인데 같은 key"로 오인돼 캐스케이드 갱신 — identity 보존 없음, 파괴/재생성 자체는 없음)는 React `key` 생략 시 index 기본값 등 업계 흔한 관행과 같은 결이라 새로 설명할 개념 아님. 2. **`updateFn(item?, ...)`로 바꿔 최종 제거 시 "정리용 1회 추가 호출"을 주는 안 — 검토 후 기각, 사용자가 직접 반례를 찾음.** 이 훅은 `data`에서 key가 빠져 `reconcile`이 다시 도는 정상 경로에서만 발화하는데, **Slot을 담은 부모 Instance 자체가 `Destroy`되는(가장 흔한) 경로는 `reconcile`이 다시 안 돌아서 이 훅이 전혀 안 불림** — 절반만 동작하는 정리 메커니즘은 없는 것보다 위험(사용자가 "정리가 보장된다"고 오해하고 `Subscribe`류를 `userdata`에 넣었다가 Destroy 경로에서 조용히 샘). `retract`가 Destroy 시 절대 안 불린다는 기존 원칙(`lifecycle-pattern.md` "quad는 라이프사이클 중간에 있지 않다")과 정확히 같은 이유로 기각. **대신 `userdata`엔 GC-native 값만 담고, `:Subscribe()`한 Observer류처럼 명시적 cleanup이 필요한 값을 담는 건 UB로 명문화** — quad 전역 GC-native 원칙을 `:List`라는 구체 지점에 그대로 적용한 것뿐, 새 원칙 아님. 3. **문서화 순서(getting-started에서 단순 버전만 가르치고 나중에 `prev`/`userdata` 최적화를 알려줄지, 아니면 Slot이 학습 순서상 후반부라 처음부터 완전한 형태로 가르칠지)는 결정 안 함** — `research/ documentation-content-map.md`의 modifier/slot 절에 백로그로 추가, 제 의견(후자 쪽으로 기욺)만 메모, 실제 콘텐츠 작성 시점 결정 사항이라 지금 확정 안 함. 전부 `base/slot-plan.md`(`:List` 시그니처/코드 재정렬, `keyFn` 기본값 설명, "`userdata`의 생명주기 제약" 신규 절)/`ROADMAP.md`(M6)/`README.md`/ `research/documentation-content-map.md` 반영 완료. **다음 세션이 할 일**: 여전히 안 바뀜(`ROADMAP.md` M0부터).