diff --git a/.claude/README.md b/.claude/README.md index e46c475..0b608a5 100644 --- a/.claude/README.md +++ b/.claude/README.md @@ -24,7 +24,7 @@ | `base/` | 결정 완료 + 프로젝트 전체에 걸치는 컨텍스트 — plan/done 개념 없음, 계속 참조되는 배경지식. **항상 읽어야 하는** 배경지식만 여기 둠(다른 문서를 이해하는 데 전제되는 것) | | `reference/` | **[2026-08-07 신설]** 결정 자체가 아니라 다른 문서가 근거로 인용하는 온디맨드 참고 자료(v1 스냅샷, 프레임워크 비교 리서치) — "완료" 개념 없는 건 `base/`와 같지만, 항상 읽을 필요는 없고 해당 문서가 인용될 때만 열어보면 됨. `quadnomicon` 소재 후보가 많음. **[2026-08-21 확장] 확정된 결정의 "왜 그렇게 정했나" 근거 기록도 여기 둔다** — `research/`(아직 상의 필요)도 `archive/`(뒤집혔거나 기각됨)도 아니고, `base/`가 근거로 인용하는 온디맨드 자료라는 이 폴더의 기준에 정확히 맞기 때문(`slot-attach-decomposition.md`/`epoch-brand-composition.md`가 그렇게 들어옴) | | `research/` | 아직 착수 전, 사용자와 스코프/설계를 더 상의해야 함 | -| `qa-request/` | 원래 용도는 "구현 완료(코드/에이전트 검증까지 끝남) + 사용자 본인의 실기기(Roblox Studio) QA만 남음". **[2026-08-18 확장]** 구현 전에도 **사용자 심사 라운드의 산출물**을 여기 둠 — `pre-implementation-qa-round1.md`(1라운드: `base/` 확정 문서 전체를 문항으로 재확인받아 **"아니오"가 나온 항목만** 모은 결함 목록 + 신규 요구사항(`N-n`) + 부수 오탈자. **같은 날 전부 `base/`에 반영 완료**라 지금은 "무엇이 왜 틀렸었나"의 근거 기록이고, 지금 유효한 설계는 항상 `base/`가 소스. 아직 안 닫힌 것은 `question.md` 3번과 `.claude/todos.md` 00번이 소스), `pre-implementation-qa-round2.md`(2라운드: 확정 의사코드를 실제로 손으로 실행해보는 트레이싱 — **완료**, 발견된 크래시 `RC-1`(`recompute` 트리거 모델)도 같은 날 후속 세션에서 Blocker 게이팅 설계로 해결·반영까지 끝남, `archive/question-resolved.md`에 논의 요지 정리), `pre-implementation-qa-round3.md`(3라운드: `RC-1` 해법(Blocker 게이팅)이 실제로 `attachSlot`/`recompute`에 반영된 걸 손으로 트레이싱 — **완료**, `RC-3`/`RC-4`(`activateList`가 자기 Slot의 Blocker보다 먼저 실행되는 순서 문제)와 `bk.N` 수명주기 미정을 발견했다가 같은 세션에 사용자가 최초 분석 오류를 직접 정정하며 전부 해결·`base/` 반영까지 끝남, `archive/question-resolved.md`에 논의 요지 정리. `ROADMAP.md` M2가 M3의 `Blocker.luau`에 의존하게 된 마일스톤 순서 불일치는 각주로 반영, 마일스톤 재편 여부는 열려 있음). `pre-implementation-qa-round4.md`(4라운드: 사용자 요청으로 **`base/` 확정 전체를 "예가 나와야 정상인 문항"으로 다시 뽑은 전수 문항지** — **[2026-08-21] 완료·종결**. 1~3라운드와 달리 이 파일은 **문항지 원본 그대로 남긴다** — 처리 결과 전량이 `-followup.md`에 쌓였으므로 "아니오만 남기는 재편"은 안 하기로 함(같은 정보가 두 곳에 갈라지는 걸 피함). 문항 수/문서별 분포는 그 파일 자신이 소스). `pre-implementation-qa-round4-response.md`(사용자 회신 원문 — 이 라운드는 회신을 **별도 파일**로 받았다, 1~3라운드와 다른 점), `pre-implementation-qa-round4-followup.md`(**[2026-08-20 시작, 2026-08-21 종결]** 그 회신 처리 결과 — A~H 8개 절이 4차에 걸쳐 시간순으로 쌓였고 **마지막 H절이 최신이자 소스**. B·C절의 재질문/판단 대기 항목은 F·G절을 거쳐 **H절에서 전량 닫혔다**(`Detach` 보존 주체, `KeyGone`, `Owned`, `attachSlot` 분해). **열린 질문 없음**. **[2026-08-21 정정]** 여기 적혀 있던 "5라운드 문항지는 만들지 않는다"는 뒤집혔다 — 같은 날 사용자 요청으로 5라운드를 만들었다), `pre-implementation-qa-round5.md`(**[2026-08-21 신설·처리 완료]** 5라운드: 4라운드에서 "예"로 넘어간 자리는 건너뛰고 **(1) 4라운드에 문항이 아예 없던 영역**(`project-setup-plan.md`/`quad-types-plan.md`, 그리고 **문서가 아니라 실제 커밋된 M1 코드**), **(2) 4라운드 회신 이후 새로 확정된 것**(`Detach`/`_detached`/`KeyGone`/`Owned`/`attachSlot` 분해 등), **(3) 큰 문서의 심화**(예: `debounce-throttle-plan.md`)만 묻는다. 문항 수는 그 문서 자신이 소스), `pre-implementation-qa-round5-response.md`(사용자 회신 원문 — 4라운드와 같이 별도 파일), `pre-implementation-qa-round5-followup.md`(**[2026-08-21]** 그 회신 처리 결과 — 즉시 반영분 / 재질문 / 사용자 판단 필요 / 새로 만든 문서 둘(`gate-plan.md`·`state-epoch-plan.md` — 같은 날 확정되며 `base/`로 승격)까지. **처리 결과의 소스는 이 파일**). `pre-implementation-handtrace-round6.md`(**[2026-08-22 신설]** 6라운드: 문항지가 아니라 **손 트레이싱**이다(2·3라운드와 같은 성격) — 사용자가 지목한 최근 확정 5개 영역(`Effect(fn, ...deps)`/`Gate`·`Blocker`/State 전파(`rawInvalid`·emit 지연)/Slot의 `native*`·offset·length·mount/`Brand`·`Epoch`·`EpochMap`)을 실제 값으로 돌려본 결과. 발견 번호는 `H-n`. **[2026-08-23] 2차 패스** — 1차가 안 본 영역(디스패치 코어 전체/라이프타임 유틸/Ref·Tag·Attribute·UI 숏핸드 핸들러/Slot의 `raw*` 계층). **[2026-08-23] 3차 패스** — 1·2차가 한 번도 안 연 문서 전체(Store/State/Source 코어, Modifier·컴포넌트 합성, 이벤트·라이프사이클·에러 격리, Tween·시간 게이트, 타입 계약과 실제 커밋된 M1 코드) + 통합 시나리오. **[2026-08-24] 4차 패스** — 문서 단위가 아니라 **축을 바꿔서**(핸들러 레지스트리 전수/두 대형 핸들러 문서 심층/`luau-test` 스파이크 실제 재실행/프리미티브 조합 매트릭스/`reference`·`archive`·로드맵 M3~M9/엔진·언어 사실 주장 전수 검증). 3·4차는 추론으로 끝내지 않고 로컬 `luau`/`luau-analyze`와 공식 문서로 **직접 재현·교차검증**했고, 그 부수로 기존 `H-2`의 크래시 주장이 틀렸음도 드러났다(3차 패스 머리의 정정 절). 발견 번호는 패스를 가로질러 이어서 매긴다. 발견 수/심각도 분포는 그 문서 자신이 소스. 각 패스의 "확인만 하고 문제 없었던 것" 절은 다시 트레이싱할 필요 없는 자리를 적어둔 것. **⭐ [2026-08-24] 전량 처리·반영 완료** — 그 문서는 이제 **발견 당시의 기록**이라 각 항목의 "갈래"는 선택 전 목록이니 그대로 믿지 말 것), `pre-implementation-handtrace-round6-followup.md`(**[2026-08-24 신설] 6라운드의 결정과 근거가 여기 소스다** — `H-1`~`H-54`를 사용자와 대화형으로 하나씩 결정한 기록이고, 반영 후 `/code-review high`가 잡은 7건(그중 셋이 이번 반영이 만든 회귀)도 D절에 있다. 진행 경위와 사용자 발언 원문은 `session/2026-08-24-01-handtrace-round6-resolution.md`). 다음 라운드가 필요해지면 라운드마다 파일을 새로 만들고 이름에 라운드 번호를 넣을 것 | +| `qa-request/` | 원래 용도는 "구현 완료(코드/에이전트 검증까지 끝남) + 사용자 본인의 실기기(Roblox Studio) QA만 남음". **[2026-08-18 확장]** 구현 전에도 **사용자 심사 라운드의 산출물**을 여기 둠 — `pre-implementation-qa-round1.md`(1라운드: `base/` 확정 문서 전체를 문항으로 재확인받아 **"아니오"가 나온 항목만** 모은 결함 목록 + 신규 요구사항(`N-n`) + 부수 오탈자. **같은 날 전부 `base/`에 반영 완료**라 지금은 "무엇이 왜 틀렸었나"의 근거 기록이고, 지금 유효한 설계는 항상 `base/`가 소스. 아직 안 닫힌 것은 `question.md` 최우선 절과 `.claude/todos.md` 00번이 소스), `pre-implementation-qa-round2.md`(2라운드: 확정 의사코드를 실제로 손으로 실행해보는 트레이싱 — **완료**, 발견된 크래시 `RC-1`(`recompute` 트리거 모델)도 같은 날 후속 세션에서 Blocker 게이팅 설계로 해결·반영까지 끝남, `archive/question-resolved.md`에 논의 요지 정리), `pre-implementation-qa-round3.md`(3라운드: `RC-1` 해법(Blocker 게이팅)이 실제로 `attachSlot`/`recompute`에 반영된 걸 손으로 트레이싱 — **완료**, `RC-3`/`RC-4`(`activateList`가 자기 Slot의 Blocker보다 먼저 실행되는 순서 문제)와 `bk.N` 수명주기 미정을 발견했다가 같은 세션에 사용자가 최초 분석 오류를 직접 정정하며 전부 해결·`base/` 반영까지 끝남, `archive/question-resolved.md`에 논의 요지 정리. `ROADMAP.md` M3가 M2의 `Blocker.luau`에 의존하게 된 마일스톤 순서 불일치는 각주로 반영 — **[2026-08-24]** 그때 열어뒀던 마일스톤 재편은 M2/M3 순서 교체로 닫혔다). `pre-implementation-qa-round4.md`(4라운드: 사용자 요청으로 **`base/` 확정 전체를 "예가 나와야 정상인 문항"으로 다시 뽑은 전수 문항지** — **[2026-08-21] 완료·종결**. 1~3라운드와 달리 이 파일은 **문항지 원본 그대로 남긴다** — 처리 결과 전량이 `-followup.md`에 쌓였으므로 "아니오만 남기는 재편"은 안 하기로 함(같은 정보가 두 곳에 갈라지는 걸 피함). 문항 수/문서별 분포는 그 파일 자신이 소스). `pre-implementation-qa-round4-response.md`(사용자 회신 원문 — 이 라운드는 회신을 **별도 파일**로 받았다, 1~3라운드와 다른 점), `pre-implementation-qa-round4-followup.md`(**[2026-08-20 시작, 2026-08-21 종결]** 그 회신 처리 결과 — A~H 8개 절이 4차에 걸쳐 시간순으로 쌓였고 **마지막 H절이 최신이자 소스**. B·C절의 재질문/판단 대기 항목은 F·G절을 거쳐 **H절에서 전량 닫혔다**(`Detach` 보존 주체, `KeyGone`, `Owned`, `attachSlot` 분해). **열린 질문 없음**. **[2026-08-21 정정]** 여기 적혀 있던 "5라운드 문항지는 만들지 않는다"는 뒤집혔다 — 같은 날 사용자 요청으로 5라운드를 만들었다), `pre-implementation-qa-round5.md`(**[2026-08-21 신설·처리 완료]** 5라운드: 4라운드에서 "예"로 넘어간 자리는 건너뛰고 **(1) 4라운드에 문항이 아예 없던 영역**(`project-setup-plan.md`/`quad-types-plan.md`, 그리고 **문서가 아니라 실제 커밋된 M1 코드**), **(2) 4라운드 회신 이후 새로 확정된 것**(`Detach`/`_detached`/`KeyGone`/`Owned`/`attachSlot` 분해 등), **(3) 큰 문서의 심화**(예: `debounce-throttle-plan.md`)만 묻는다. 문항 수는 그 문서 자신이 소스), `pre-implementation-qa-round5-response.md`(사용자 회신 원문 — 4라운드와 같이 별도 파일), `pre-implementation-qa-round5-followup.md`(**[2026-08-21]** 그 회신 처리 결과 — 즉시 반영분 / 재질문 / 사용자 판단 필요 / 새로 만든 문서 둘(`gate-plan.md`·`state-epoch-plan.md` — 같은 날 확정되며 `base/`로 승격)까지. **처리 결과의 소스는 이 파일**). `pre-implementation-handtrace-round6.md`(**[2026-08-22 신설]** 6라운드: 문항지가 아니라 **손 트레이싱**이다(2·3라운드와 같은 성격) — 사용자가 지목한 최근 확정 5개 영역(`Effect(fn, ...deps)`/`Gate`·`Blocker`/State 전파(`rawInvalid`·emit 지연)/Slot의 `native*`·offset·length·mount/`Brand`·`Epoch`·`EpochMap`)을 실제 값으로 돌려본 결과. 발견 번호는 `H-n`. **[2026-08-23] 2차 패스** — 1차가 안 본 영역(디스패치 코어 전체/라이프타임 유틸/Ref·Tag·Attribute·UI 숏핸드 핸들러/Slot의 `raw*` 계층). **[2026-08-23] 3차 패스** — 1·2차가 한 번도 안 연 문서 전체(Store/State/Source 코어, Modifier·컴포넌트 합성, 이벤트·라이프사이클·에러 격리, Tween·시간 게이트, 타입 계약과 실제 커밋된 M1 코드) + 통합 시나리오. **[2026-08-24] 4차 패스** — 문서 단위가 아니라 **축을 바꿔서**(핸들러 레지스트리 전수/두 대형 핸들러 문서 심층/`luau-test` 스파이크 실제 재실행/프리미티브 조합 매트릭스/`reference`·`archive`·로드맵 M2~M9/엔진·언어 사실 주장 전수 검증). 3·4차는 추론으로 끝내지 않고 로컬 `luau`/`luau-analyze`와 공식 문서로 **직접 재현·교차검증**했고, 그 부수로 기존 `H-2`의 크래시 주장이 틀렸음도 드러났다(3차 패스 머리의 정정 절). 발견 번호는 패스를 가로질러 이어서 매긴다. 발견 수/심각도 분포는 그 문서 자신이 소스. 각 패스의 "확인만 하고 문제 없었던 것" 절은 다시 트레이싱할 필요 없는 자리를 적어둔 것. **⭐ [2026-08-24] 전량 처리·반영 완료** — 그 문서는 이제 **발견 당시의 기록**이라 각 항목의 "갈래"는 선택 전 목록이니 그대로 믿지 말 것), `pre-implementation-handtrace-round6-followup.md`(**[2026-08-24 신설] 6라운드의 결정과 근거가 여기 소스다** — `H-1`~`H-54`를 사용자와 대화형으로 하나씩 결정한 기록이고, 반영 후 `/code-review high`가 잡은 7건(그중 셋이 이번 반영이 만든 회귀)도 D절에 있다. 진행 경위와 사용자 발언 원문은 `session/2026-08-24-01-handtrace-round6-resolution.md`). 다음 라운드가 필요해지면 라운드마다 파일을 새로 만들고 이름에 라운드 번호를 넣을 것 | | `archive/` | 완료 + 사용자가 실사용/실기기로 직접 검증까지 마침 (구현 대상). **[2026-08-06 확장]** 완전히 뒤집힌 설계 결정을 원문+역전 이유+diff와 함께 보존하는 용도로도 사용(제목 `[역전됨]` — 한 번 확정했다가 뒤집힌 것) — 더 이상 능동적으로 참고 안 해도 되지만(토큰 낭비 방지 위해 `base/`/`research/`에서 뺌) `quadnomicon` 소재로는 나중에 쓸 수 있음. **[2026-08-07 확장]** 후보였다가 채택 안 된 것(확정한 적 없이 검토 후 기각)도 같은 방식으로 보존, 제목은 구분을 위해 `[기각됨]` — `[역전됨]`과 의미가 다르므로 혼동하지 말 것. **[2026-08-07 세 번째 확장]** 설계 반전/기각과 별개로, 에이전트가 문서 작성 중 스스로 낸 개념 혼동을 정정한 이력은 `[에이전트 실수]` 태그로 `agent-mistake.md` 하나에 모음(`.claude/session-summary.md`/`session/` 로그와의 중복 방지) | | `feedback/` | 실사용 피드백을 정리한 긴 로그 — **[2026-08-19 기준] 폴더 자체가 아직 없음**(M0/M1 스캐폴딩만으론 안 생기고 실제로 렌더링해보고 쓰는 단계부터, 첫 피드백이 생길 때 만들면 됨). `qa-request/`는 **[2026-08-18] 더 이상 비어 있지 않음**(구현 전 QA 1라운드 산출물이 들어감) — 여긴 아직 폴더도 없음 | | `luau-test/` | **[2026-08-09 신설]** `base/` 확정 사항 중 "추론만으로 확정하고 실제 Luau로 부딪혀본 적 없는 것"(M0 스파이크 대상)을 `luau`/`luau-analyze`/`luau-lsp`/Roblox Studio로 사용자가 직접 돌려볼 독립 실행 스크립트 모음. **[2026-08-13 여섯 번째 세션, 첫 실측]** `luau`/`luau-analyze` 바이너리가 생겨 처음으로 실제 실행 — **런타임 12개 전원 통과**, 타입 쪽에서 `:Compute(fn)` lazy 핸들 계약이 Luau 추론과 충돌하는 게 드러남(당시 `question.md` 0-Y). **[2026-08-13 열세 번째 세션]** 그 0-Y가 해소되며 `review-required/`가 **비었음** — 계약은 유지 확정, 남은 건 Luau 자체 한계라 `base/typing-limits.md`가 담당. **`STATUS.md`가 상태의 소스**(pass / 사람 결정 필요 / 스파이크 깨짐 / 미실행 분류 — 사람이 먼저 볼 것만 위에), `luau-test/README.md`는 각 파일의 검증 의도·배경, 실행 결과 상세는 `audit/luau-test-first-run-2026-08-13.md` | @@ -48,16 +48,16 @@ | `typing-limits.md` | **[2026-08-13 열세 번째 세션 신설]** Luau 타입 시스템이 quad 설계에 대해 **못 해주는 것**을 한 군데 모은 확정 문서 — 여러 `base/` 문서에 캐비엇으로 흩어져 있던 걸 통합. 대전제는 "**Luau의 한계를 우회하려고 타입/API를 비틀지 않는다**"(비틀면 나중에 Luau가 고쳐줘도 자동 수혜를 못 받고 되돌리는 마이그레이션이 생김). 1번 항목이 가장 큼 — **재귀 제네릭이 다른 타입 인자로 자기를 반환하면(`Compute(self: State,...) -> State`) 타입 안전성이 에러 없이 조용히 사라짐**(구 `question.md` 0-Y, 스파이크 다수로 확정 — 근거·개수는 `audit/type-recursion-issue/`). 대응은 두 개: (a) 타입 선언을 "데이터부/메소드부"로 쪼개 콜백 파라미터 추론을 살리고, (b) **파생 State를 만드는 자리마다 결과 타입을 명시 주석으로 바인딩**(그 한 줄만 검증 안 되고 다운스트림 전체는 정상 체크됨). Luau RFC `relax-recursive-type-restriction`이 `Promise.andThen`으로 예시 든 바로 그 패턴이라 **지금 선언 그대로 두면 Luau 쪽 수정만으로 코드 변경 없이 풀림**(추적: `luau-lang/luau#2380`). **[2026-08-15 추가]** ③ 인라인 대신 이름 붙은 함수 + `typeof`로 선언하면 콜백 파라미터 주석은 여전히 필요하지만 LHS 명시 없이도 다운스트림이 안전해짐(①을 대체하지 않음, 보강). 그 외 Modifier `Overridden` 서브타입/Attribute 제네릭 키 narrowing/nilable default 오버로드도 여기 통합, `store.key` type function 한계는 **검증 완료로 승격**(§5), §8에 **새 타입·API 설계 시 체크리스트**. **[2026-08-19 신설]** §6 — `type function`을 거친 값(패스스루라도)은 이후 제네릭 self 메소드 체이닝(`AddPlugin`류)이 조용히 깨짐, `quad-types-plan.md`의 `CheckedQuad` 배선 중 실측 발견·회피(원본은 type function을 절대 안 거치게 하고 검사 결과는 별도 필드로 격리). 실측 근거는 `audit/type-recursion-issue/` + `audit/type-recursive-issue-with-typeof/` + `luau-test/23` **[2026-08-24 6라운드 `H-24` — 실측]** 영향 범위 표에 **`tween:Mapped`** 추가 — `tween:Mapped(fn: (T) -> U): Tween`가 1번이 지목한 모양(`Foo` 안에서 `-> Foo`)과 **글자 그대로 같은데** 이 문서도 `tween-plan.md`도 그걸 모르고 있었다. 인라인 제네릭 메소드로 선언하면 `luau-analyze`가 **진단 없이 조용히 통과**시키고, ③(`typeof(named function)`)로 바꾸면 정상적으로 잡힌다 — **기존 완화책이 그대로 통하는데 아무도 적용을 지시하지 않고 있었다** | | `lifecycle-pattern.md` | rbvm의 `Connected`+GC 관용구를 quad-v2가 채택하는 방식. **[2026-08-14 다섯 번째 세션, 시그니처 정정]** `bindLifetime(inst,value)`/`unbindLifetime(value)`/`canExecute(value)` — 뒤의 둘은 `inst`를 안 받음(`bindLifetime`이 바인딩 시점에 gcconn 참조를 `value` 쪽 `Relate`로 복사해두므로 `value` 하나로 생존을 물을 수 있고, 실제 호출부인 State 전파 루프엔 애초에 `inst`가 없음). `.Subscribed`는 전역 `:Subscribe()` 전용 필드로 분리(`bindLifetime`은 읽지도 쓰지도 않음), gcconn/gchold는 lazy가 아니라 **Instance 생성 시점**에 만들고 클로저가 `gchold`와 `inst`를 둘 다 캡처(userdata 포인터 동일성 = `inst`-키 `Relate` 전체의 전제). 옛 2-인자 모델은 `archive/canexecute-inst-arg-reversed.md`. **[2026-08-14 열한 번째 세션]** 별도 `canBound`가 다시 도입됨 — `bindLifetime`/`Observer:Subscribe()`의 이중 바인딩 가드는 `canBound`, State emit 전파 게이팅만 `canExecute`(판정 로직은 비공개 헬퍼 `isBoundAlive` 하나를 공유). **[2026-08-18 구현 전 QA 반영]** **두 predicate는 값이 같은 게 아니라 서로의 부정**(`canBound` 참 = "지금 묶어도 됨")이라 게이트가 전부 `if not canBound(v) then error(...)`로 정정됨 — 옛 서술대로 짰으면 정상 첫 바인드가 전부 에러났음. gcconn/gchold 저장도 `SetStrong`→**`SetWeak`** 정정 **[2026-08-24 6라운드 `H-11`]** `bindLifetime`/`unbindLifetime`이 **`isEffect`를 보고 `Destroying`을 걸고/끊는다** — `LP-2`가 *"`Effect`가 그 훅을 쓰는 유일한 소비자"*라 확정해뒀는데 **실제로 거는 코드가 어디에도 없었다.** `unbind`는 cleanup을 부르지 않는다(대칭이라 포탈이 자연히 성립) | | `store-plan.md` | **[2026-08-14 신설 — `bind-system-plan.md` 3단계 분할 + 구 store-semantics.md 흡수]** Store = **이름 붙은 Source 모음, 그 이상 아님** — Store 부작용 허용이 기본 디자인(국소적 vs 경계를 넘는 부작용), `defaults`는 선택적 초기값 템플릿(원본을 나중에 mutate해도 UB 아님)이고 **eager 생성과 lazy 생성이 둘 다 필요**(Luau 타입은 런타임에 강제 안 되므로), `table.clone` 기반 eager 생성 스케치, `store.key`(dot-access)가 1급 경로(**[2026-08-18] `store "key"` 문자열 커링은 기각** — 동적 키는 `store:GetDynamic<>(name)`), 레코드 필드 타이핑은 Luau `type function`으로 해결 확인, `store.key = value` 폐기 → `store.key:Set(value)`(타입 대칭성+lazy 정직성), "Store가 Store를 저장 가능한가"는 **그런 경우를 안 만듦**으로 확정(`State>`와는 다른 축) | -| `source-state-plan.md` | **[2026-08-14 신설 — `bind-system-plan.md` 3단계 분할 + 구 store-semantics.md 흡수]** 반응형 코어: `Source`⊇`State` 구조적 서브타입(`RefSource` 폐기, 단방향 의존으로 Luau 솔버 회피 — 스파이크 `08` 통과), **push-invalidate/pull-recompute** 전파 모델과 "관측해야 실체화된다" 전역 원칙, State 체인 플래튼 기각(캐싱이 State의 존재 이유), `:With`도 매번 새 노드(clone 계열인 `Tag`/`Modifier`와 혼동 주의), `:Compute`의 lazy 핸들 계약(`:Get()` 누락이 반복되는 실수)·trailing args sugar·`fn(self, previous?, ...deps)` 순서·`previous`, `:Apply`, `:Emit()`(Source 원천 전용 하드 경계)과 `Store`/`Source`의 `T`가 Modifier일 수 없는 따름정리, `state:Observer(fn)`, `:Subscribe()`/`:Unsubscribe()`, **이중 바인딩 금지 게이트**(`canBound`, State emit 전파 게이팅은 `canExecute` — `base/lifecycle-pattern.md`의 "`canBound` vs `canExecute`" 절이 소스), PA님 코드 교차검증. **[2026-08-14 열두 번째 세션]** 새 절 "Observer/Effect Leaf dedup" — `RefLeafHandler`와 같은 `old ~= v` dedup(성능 최적화, correctness엔 불필요). **[2026-08-18 구현 전 QA 반영]** `canBound` 방향 정정, `:Compute` 콜백 표기 정정(`fn(self, previous?, ...deps)`), FALLBACK 가드 에러에 `k` 타입 싣기, 그리고 **⚠️ 미해결로 신설된 "중간 State가 살아남는가"**(상류 strong/하류 weak 불변식 — M3 착수 전 결론 필요) **[2026-08-24 6라운드]** 전파 루프가 구독자 집합을 **스냅샷으로 복사한 뒤** 돈다 — 순회 중 새 구독자 추가가 정상 경로인데 Lua에서 미정의라, 실측에서 **실행마다 결과가 달라지고 한 Observer가 통째로 누락**됐다(`H-23`, `Epoch` dedup으로는 이중 발화만 접히고 누락은 안 접힌다). `Effect(fn, ...deps)` 역전이 이 문서에 미반영이던 것도 정정 — **`Observer`만 기각 유지**이고 그 근거를 새로 씀("Observer는 리시버 State 하나에 붙는 구독, 여럿을 엮는 건 Effect가 대신한다", `H-13`) 그리고 **`ObserverEffectLeafHandler`도 자기 배열 자리의 `setOffsetSource`/`setLength`를 안 등록하던 것**을 정정(`H-39` — 말단 핸들러 넷이 같은 결함이었고 이게 그중 하나다, `Frame { someObserver, Frame{} }`가 첫 `recompute`에서 죽었다) | +| `source-state-plan.md` | **[2026-08-14 신설 — `bind-system-plan.md` 3단계 분할 + 구 store-semantics.md 흡수]** 반응형 코어: `Source`⊇`State` 구조적 서브타입(`RefSource` 폐기, 단방향 의존으로 Luau 솔버 회피 — 스파이크 `08` 통과), **push-invalidate/pull-recompute** 전파 모델과 "관측해야 실체화된다" 전역 원칙, State 체인 플래튼 기각(캐싱이 State의 존재 이유), `:With`도 매번 새 노드(clone 계열인 `Tag`/`Modifier`와 혼동 주의), `:Compute`의 lazy 핸들 계약(`:Get()` 누락이 반복되는 실수)·trailing args sugar·`fn(self, previous?, ...deps)` 순서·`previous`, `:Apply`, `:Emit()`(Source 원천 전용 하드 경계)과 `Store`/`Source`의 `T`가 Modifier일 수 없는 따름정리, `state:Observer(fn)`, `:Subscribe()`/`:Unsubscribe()`, **이중 바인딩 금지 게이트**(`canBound`, State emit 전파 게이팅은 `canExecute` — `base/lifecycle-pattern.md`의 "`canBound` vs `canExecute`" 절이 소스), PA님 코드 교차검증. **[2026-08-14 열두 번째 세션]** 새 절 "Observer/Effect Leaf dedup" — `RefLeafHandler`와 같은 `old ~= v` dedup(성능 최적화, correctness엔 불필요). **[2026-08-18 구현 전 QA 반영]** `canBound` 방향 정정, `:Compute` 콜백 표기 정정(`fn(self, previous?, ...deps)`), FALLBACK 가드 에러에 `k` 타입 싣기, 그리고 **⚠️ 미해결로 신설된 "중간 State가 살아남는가"**(상류 strong/하류 weak 불변식 — M2 착수 전 결론 필요) **[2026-08-24 6라운드]** 전파 루프가 구독자 집합을 **스냅샷으로 복사한 뒤** 돈다 — 순회 중 새 구독자 추가가 정상 경로인데 Lua에서 미정의라, 실측에서 **실행마다 결과가 달라지고 한 Observer가 통째로 누락**됐다(`H-23`, `Epoch` dedup으로는 이중 발화만 접히고 누락은 안 접힌다). `Effect(fn, ...deps)` 역전이 이 문서에 미반영이던 것도 정정 — **`Observer`만 기각 유지**이고 그 근거를 새로 씀("Observer는 리시버 State 하나에 붙는 구독, 여럿을 엮는 건 Effect가 대신한다", `H-13`) 그리고 **`ObserverEffectLeafHandler`도 자기 배열 자리의 `setOffsetSource`/`setLength`를 안 등록하던 것**을 정정(`H-39` — 말단 핸들러 넷이 같은 결함이었고 이게 그중 하나다, `Frame { someObserver, Frame{} }`가 첫 `recompute`에서 죽었다) | | `dispatch-core-plan.md` | **[2026-08-13 열네 번째 세션 신설 — `bind-system-plan.md` 2단계 분할 + 0-A/0-Z 반영]** 디스패치 코어: 핸들러 계약(`isHandlable`/`priority`/`process`가 retract 클로저를 반환) / **하강 diff 재디스패치**(래핑 핸들러의 `retractFrom` 선행 호출 폐기, `Dispatch.process`가 슬롯의 `handler`를 먼저 비교해 — 같으면 그 자리 클로저에 새 값을 넘기고 재`process`, 다르면 그 자리부터 전량 철거) / `chains` 인덱스 체인과 **3-인자** `Dispatch.retractFrom(inst,k,index)`(힌트 인자 소멸 — 값 전달 경로가 (A) 분기 하나로 통일) / `None` 센티널 / Handler 작성 체크리스트 8개 / Length·Offset 형제 순서 보장 / "store 바인드는 래핑" 결론. **새 결정 둘**: `HANDLER_PRIORITY_FALLBACK`(base 제공 핸들러의 기본 밴드 — 백엔드가 평범한 우선순위로 덮어쓰면 언제나 이김), **"base가 소유하는 핸들러와 주입되는 엔진 op"**(부기가 엔진 지식을 요구하지 않으면 알고리즘은 base, 마지막 한 줄만 주입 — `addTag`/`removeTag`/`setAttribute`, **[2026-08-14 열 번째 세션]** 같은 패턴을 Dispatch 밖의 `dispose(value)`/`nativeDispose`에도 재사용 — **[2026-08-22]** 그 op 이름은 `native*` 계층으로 확정, 옛 가칭 `disposeInst`). 옛 힌트 모델은 `archive/dispatch-hintvalue-model-reversed.md`. **[2026-08-14 열두 번째 세션]** Observer/Effect Leaf도 `Ref`와 같은 identical-value dedup 채택(성능 최적화). **[2026-08-18 구현 전 QA 반영]** **`Dispatch.drive`의 `None` 스킵 분기 폐기**(반응형 값이 내놓는 `None`은 어차피 `process`에 도착) → `NoneHandler`는 재귀 전담, **`NilHandler` 신설**(`k=number and v==nil` 말단, `setLength(0)`/`setOffsetSource(None)` 등록 담당). Length/Offset 등록 책임도 "처음 매치한 Handler"→**말단 Handler**로 정정. base 소유 Fallback Handler **등록 주체는 백엔드 팩토리→quad-base 자신으로 재역전**. "방어 가드는 죽은 코드" 서술에 한정 추가(한 핸들러가 여러 값 모양을 받으면 판별은 그 핸들러 몫), `PreRef`가 "배열 먼저" 보장 위에 성립한다는 근거 정정(별도 pre-pass라 독립), `Quad.debug` 게이팅. **[2026-08-18 구현 전 QA 2라운드 후속]** "Length/Offset" 절에 크래시하던 `recompute` 트리거 모델(`RC-1`)을 owner별 `Blocker` 게이팅으로 고친 "배치 등록을 안전하게 만드는 Blocker 게이팅" 절 신설 — `setLength`/`setOffsetSource` 재작성, `Dispatch.drive`도 자기 Blocker로 배열 파트 순회를 감쌈. **[2026-08-18 구현 전 QA 3라운드]** "저장 위치" 절에 `bk.N`(recompute 순회 상한) 수명주기 신설(그때그때 실제 개수, `inst`/Slot 두 owner 타입 동일 규칙 — `setLength`가 갱신, `setOffsetSource`는 안 건드림) — 부수로 `RC-1`의 원래 크래시 서술도 정정("N이 배치 전에 고정"이라는 옛 전제의 부산물이었을 뿐, 지금 Blocker 게이팅이 필요한 이유는 크래시 방지가 아니라 비용) **[2026-08-24 6라운드]** (1) **말단 핸들러 4종**(Tag/AttributeGroup/RefLeaf/ObserverEffectLeaf)이 `setOffsetSource`/`setLength`를 **아예 등록 안 하던 것**이 발견돼 계약대로 등록하게 됨 — 안 그러면 `Frame { Tag("x"), Child{} }` 같은 흔한 배치가 첫 `recompute`에서 명시적 error로 죽는다(`H-39`). (2) 접두합 캐시 무효화 3규칙이 **산문으로만 있고 코드 경로가 없던 것**을 `setLength`/`spliceArrays*`/`_baseObserver`에 실제 배치(`H-3`), `bk` 스펙에 `offsetCache`/`invalidAfter` 추가(`H-4`). (3) **`recompute` 호출은 `setLength`의 단독 책임**(`H-19`). (4) Blocker 범위를 **`drive` 전체**로 정정하고 `PostRef` 콜백이 게이트 안에서 돈다는 걸 계약화(`H-17`). (5) *"잔여 부기는 인스턴스 GC로 정리된다"*는 **틀린 안전망 주장 삭제**(gcconn 불멸성과 양립 불가, `H-26`) | | `bind-system-plan.md` | **[2026-08-14, 3단계 분할로 203줄까지 축소 — 지금은 "인스턴스 생성/이벤트 네이밍 인체공학 + 분할 색인" 문서]** 반응형 코어는 `source-state-plan.md`, Store는 `store-plan.md`, 디스패치 코어는 `dispatch-core-plan.md`로 나갔음. 아래 이력은 분할 전 이 파일이 담고 있던 결정들의 기록(현행 소스는 각 분할 문서). pluggable key/value 핸들러 레지스트리 — `process`/`retract` 디스패치 모델, Ref, Store/State/Source 온톨로지 + 인체공학 질문 전부 확정. 디스패치 엔진은 `quad-base`가 인터페이스로 소유(2026-08-04 5차 라운드). **[2026-08-11 세션, 여섯 번째]** `Dispatch.setLength`/`setOffsetSource`의 owner 키가 물리 Instance로 한정될 필요 없음을 명시(Slot-in-Slot 재귀의 근거) — 같은 절 `recompute`의 off-by-one 버그 발견·수정(`offset`이 자기 자신을 포함해 누적되던 것), 재진입 방지 가드는 검토 후 기각(`Source⊇State` 단방향 원칙과 같은 카테고리의 UB로 명명, 각 Slot이 독립 `bk`를 가져 nesting만으로는 재진입 경로 자체가 없음을 확인). **[2026-08-12 열한 번째 세션, 전면 정정]** "핸들러 타입이 안 바뀌면 retract 없이 process가 diff"는 틀렸음 — `retract`는 store 재발행마다(핸들러 타입 무관) 항상 불림, `v`는 대체 값 자체일 수 있어 `nil`로 가정 금지. `Tag`/`Ref`/`Slot`/`Attribute` 전부 이 오류로 설계돼 있었음이 드러나 한 세션에 전부 정정(`archive/retract-always-fires-reversed.md`). **[2026-08-12 세션 후속]** `retractUnder`의 `A and B or C` 삼항 관용구 버그(`v`가 `false`일 때 `nil`로 새던 것)를 `if-then-else`로 수정한 게 계기가 되어 `and`/`or` 삼항 전면 금지 규칙으로 발전(`architecture.md` "코드 스타일" 절). **[2026-08-12 열일곱 번째 세션]** 우선순위 동률/매치 실패 처리(`HANDLER_PRIORITY_*` 상수+디버그 동률 감지, 매치실패는 즉시 error) 확정, `store.key` 레코드 필드 타이핑이 Luau `type function`으로 가능함을 스케치로 확인(`pre-implementation-audit.md` 1-3/1-4/1-10 해소). **[2026-08-12 스무 번째 세션]** Ref 사용 관례 명문화 — React `useRef`급 스코프 감각(만든 컴포넌트 자신이 쓰거나 자식에게 넘기는 용도, 경계 밖 반출·전역 장기 보관은 비권장). **[2026-08-12 스물한 번째 세션]** `:With`가 `Tag`/`Modifier`의 `:` clone 체이닝과 겉보기엔 같은 문법이지만 실제로는 정반대(clone 아니라 매번 새 State 노드)라는 혼동 경고 추가, `Compute`가 `-ed`(`Computed`)가 아닌 이유 절 신설(quad 자기 관례상 `Tag.Added`/`Modifier.Overridden`이 이미 "-ed = clone 후 즉시 확정된 값"을 선점해 lazy한 State에 재사용하면 충돌). **[2026-08-13 세션, 두 번째]** `State>`(store가 emit하는 값 자체가 또 State/Source)가 같은 `(inst,k)`에 같은 핸들러를 중복 push시켜 `retractUnder`의 첫-매치 cutoff가 안쪽 자신을 잘못 retract하는 실제 체인 파손 버그로 확인됨(손 트레이싱, `luau-test/04`가 no-op `retract` 스텁 때문에 이 증상을 못 잡던 사각지대였음도 같이 발견) — `Dispatch.process`에 중복 핸들러 즉시 error 가드 추가, "동일한 재귀적 디스패치로 처리 가능"이라던 낙관적 서술과 "Store가 Store를 저장 가능한가" 절도 정정. **[2026-08-13 세션, 네 번째]** 사각지대 손 트레이싱 라운드에서 `isHandlable` 필드를 선택적으로 허용(생략하면 스캔에 안 걸림)하고, 그런 "체크포인트" 핸들러를 명시적으로 체인에 꽂는 `Dispatch.processAs`/`Dispatch.retractSelfAndUnder`(target 자신 포함 철거) 신설 — `attribute-plan.md`의 그룹/직접쓰기 이름 소유권 충돌을 별도 레지스트리 없이 기존 재진입 가드로 흡수하는 데 씀. **[2026-08-13 세션, 다섯 번째, 전면 재설계 — 위 processAs/retractSelfAndUnder 대체]** `chains`를 핸들러 객체 identity가 아니라 **재귀 깊이 인덱스**로 추적하도록 재설계 — `Dispatch.process(inst,k,v,index)`가 핸들러 호출 *전에* 그 인덱스 점유 여부를 체크(핸들러 부작용 낭비 없음), `process`는 이제 `retract` 필드 대신 자기 retract 클로저(`(hintValue)->()`)를 반환. 같은 키 재귀는 `index+1`, 다른 키 위임은 항상 `1`부터 — 이걸로 `State>`가 UB에서 정상 지원 대상으로 재정정됨(각 재귀 단계가 다른 슬롯을 쓰니 identity 충돌 자체가 없어짐), `retractUnder`/`retractSelfAndUnder`도 `Dispatch.retractFrom(inst,k,index,v)` 하나로 통합(자기 포함/미만은 호출자가 넘기는 인덱스로 표현)되며 체크포인트 패턴 자체가 불필요해짐(`archive/checkpoint-handler-pattern-reversed.md`). 계기: `AttributeGroupHandler` 소유권 버그를 체크포인트로 고치다, 그 근본 원인(identity 기반 추적)을 되짚은 사용자 지적. **[2026-08-13 감사]** 위 재설계 의사코드에서 실제 버그 셋 발견·수정 — (1) `chains:SetStrong`이 `handler.process` *뒤*에 있어 최초 마운트에서 하위 위임 retractor가 통째로 유실되던 것(재귀가 자기 테이블을 만들었다 바깥이 덮어씀), (2) `Ref` retractor가 spurious 재발행에서도 `relate`를 지워 dedup이 무력화되던 것, (3) `Dispatch.drive`의 진입 인덱스(`1`) 미명시. 덧붙여 retractor 안에서는 *같은* 키에 대한 `retractFrom`도 `process`와 똑같이 금지(진행 중인 루프가 `#list`를 이미 캡처)임을 명문화 **[2026-08-13 열네 번째 세션] 2단계 분할 + 모델 교체 — 디스패치 코어 전체가 `dispatch-core-plan.md`로 나갔고(이 문서엔 반응형 코어와 인체공학만 남음), 나가면서 **하강 diff**로 재작성됨. 따라서 위 5차 세션 서술 중 "`Dispatch.process`가 인덱스 **점유 여부**를 먼저 체크"와 "`retractFrom(inst,k,index,v)` **4-인자**"는 **더 이상 현행이 아님**(점유 체크 폐지 → 핸들러 비교, 힌트 인자 소멸 → 3-인자) — 현행은 `dispatch-core-plan.md`. **[2026-08-18 구현 전 QA 반영]** 남아 있던 인체공학 절이 크게 갱신됨 — 네임스페이스 **`DI`→`D`(Declarative) 확정**(코퍼스 전수 반영, "특수 DI 키"라는 설명 표현은 "특수 키"로 단순화), **`New`는 커링**(`New "Frame" {...}`)이고 **`D`는 전량 코드 생성된 순수 별칭 테이블**(생성 범위는 "GUI에 쓰이는 모든 인스턴스", 밖은 `any`), 그리고 **"이벤트 콜백 시그니처는 Luau가 검증 못 한다"는 옛 전제가 거짓**임이 사용자 반례로 확인돼 "생성기가 이벤트 필드의 콜백 타입까지 만든다"로 바뀜 | | `module-lifecycle-plan.md` | 프로바이더 패턴, bind/store 구현 책임 분리 — 확정. **[2026-08-18 구현 전 QA 반영]** 모듈 표면에 **`Quad.debug`(기본 `false`)** 신설(지금은 핸들러 우선순위 동률 경고를 게이팅), base 소유 Fallback Handler 등록 주체가 quad-base 자신이라는 **명시적 예외** 반영. **[2026-08-19 신설, 같은 날 후속 정정]** "New()의 내부 구성" 절 — `InitXxx(module)` 팩토리 체이닝 + `module:RunInit(initFn)`(함수 자체를 릴레이션 키로 쓰는 공유 멱등 가드, 파일마다 따로 두던 센티널 폐기). 실제로 `quad-base/src/init.luau`에 구현·검증됨. `RunInit`은 backend 설치엔 재사용 안 함 — `_initializedBy` 문자열 마커(같은 팩토리=no-op, 다른 팩토리=에러)를 별도로 유지하는 걸로 확정(2026-08-19 해소) | -| `quad-types-plan.md` | **[2026-08-19 신설, 같은 날 후속으로 확장]** 워크스페이스 세 번째 멤버 `quad-types` — 구현 없는 `Quad` 타입 계약 + `AddPlugin`(실측 검증된 제네릭 self 플러그인 체이닝) + `CheckedQuad`(런타임 주입 때문에 pesde semver 보호가 안 걸리는 자리를 메꾸는 컴파일 타임 버전 체크, 글롭/캐럿 패턴 지원). 버전 패턴 매칭 자체는 quad에 종속되지 않은 범용 패키지 `type-version-check`(워크스페이스 네 번째 멤버, 사용자가 나중에 독립 저장소로 분리 예정 — `HUMAN_TODO.md` 9번)로 분리됐고 `CheckedQuad`가 그 위에 얹힘. `type function`이 `T`를 패스스루만 해도 이후 제네릭 self 메소드 체이닝이 조용히 깨진다는 새 Luau 함정을 발견·회피(별도 가상 필드로 격리), `export type function`/이중 꺾쇠 제네릭 인스턴스화 등 cross-package 사용 함정도 정리. quad-roblox가 quad-base 대신 이 가벼운 패키지만 의존 — dev-dependency로 두면 게시 후 소비자 환경에서 타입-전용 require가 런타임 크래시하는 문제를 원천 회피 **[2026-08-24 6라운드 `H-25` — 실측]** `Quad`가 **5필드 닫힌 레코드**라 `RunInit`으로 붙인 서브시스템이 타입에 안 보인다(`quad.Dispatch`가 `luau-analyze`에서 타입에러 — `architecture.md` 결정 13번이 그 접근을 표준 사용법으로 확정해둔 것과 정면 충돌). **확정: `quad-types`의 `Quad`를 마일스톤마다 갱신한다** — M2(`Dispatch`)를 시작으로 M3/M6/M7/M8/M10에 체크박스가 뿌려졌다. 타입만 재수출하므로 "가벼운 타입 계약"이라는 존재 이유와 안 부딪힌다 | +| `quad-types-plan.md` | **[2026-08-19 신설, 같은 날 후속으로 확장]** 워크스페이스 세 번째 멤버 `quad-types` — 구현 없는 `Quad` 타입 계약 + `AddPlugin`(실측 검증된 제네릭 self 플러그인 체이닝) + `CheckedQuad`(런타임 주입 때문에 pesde semver 보호가 안 걸리는 자리를 메꾸는 컴파일 타임 버전 체크, 글롭/캐럿 패턴 지원). 버전 패턴 매칭 자체는 quad에 종속되지 않은 범용 패키지 `type-version-check`(워크스페이스 네 번째 멤버, 사용자가 나중에 독립 저장소로 분리 예정 — `HUMAN_TODO.md` 9번)로 분리됐고 `CheckedQuad`가 그 위에 얹힘. `type function`이 `T`를 패스스루만 해도 이후 제네릭 self 메소드 체이닝이 조용히 깨진다는 새 Luau 함정을 발견·회피(별도 가상 필드로 격리), `export type function`/이중 꺾쇠 제네릭 인스턴스화 등 cross-package 사용 함정도 정리. quad-roblox가 quad-base 대신 이 가벼운 패키지만 의존 — dev-dependency로 두면 게시 후 소비자 환경에서 타입-전용 require가 런타임 크래시하는 문제를 원천 회피 **[2026-08-24 6라운드 `H-25` — 실측]** `Quad`가 **5필드 닫힌 레코드**라 `RunInit`으로 붙인 서브시스템이 타입에 안 보인다(`quad.Dispatch`가 `luau-analyze`에서 타입에러 — `architecture.md` 결정 13번이 그 접근을 표준 사용법으로 확정해둔 것과 정면 충돌). **확정: `quad-types`의 `Quad`를 마일스톤마다 갱신한다** — 서브시스템을 붙이는 모든 마일스톤(M2/M3/M6/M7/M8/M10)에 체크박스가 뿌려졌다(규칙이 쓰인 계기는 `Dispatch`(M3)이지만 순서상 첫 적용은 M2다). 타입만 재수출하므로 "가벼운 타입 계약"이라는 존재 이유와 안 부딪힌다 | | `slot-plan.md` | 뮤터블 자식 배열, 엄격한 단일 마운트 소유권, 재마운트 시 throw, base/roblox 패키지 경계까지 확정. **[2026-08-09 세 번째 세션]** `Add`/`Remove`/`Extract`/`Clear`/`Move`/`Swap` CRUD(복잡도 표기 포함), `isMounted` 이중 추적 분리, 요소 타입 제약(`nil`/`None`/핸들러 계층 값 금지, `Slot()` 제네릭), 키 기반 동적 컬렉션 재조정(`Slot:List(data, updateFn, keyFn?)`)까지 전부 확정 통합, base/roblox 경계에 reposition 훅 추가. **[2026-08-09 열한 번째 세션, 중간검토]** CRUD 식별 기준을 element 레퍼런스에서 인덱스 기준으로 재정정(`Remove(index)`/`Extract(index, newElement?)`/`Move(oldIndex, newIndex)`), `ExtractAll`/`Get`/`IndexOf` 신설. **[2026-08-11 세션]** `updateFn(item, index: number, offset: Source, prev: T?, userdata: UD?): (T|nil, UD?)`로 시그니처 확정(`Slot.Offset`도 `Length`처럼 공개 필드로 신설) — `LayoutOrder` 등은 Slot이 자동으로 안 세팅, `index`/`offset` raw 값만 전달하고 실제 반영은 `updateFn`이 "버림/다시 그림/source만 갱신" 세 갈래로 직접 처리(재사용 Source에 미리 `Set` 후 결국 다시 그리면 무의미한 연산이 되므로). **[2026-08-11 세션, 여섯 번째]** `Slot:Single(state, updateFn)` 확정(`:List` 위의 순수 sugar) — Slot-in-Slot 중첩도 확정, 요소 타입 제약에서 `Slot` 배제 해제(`T = Instance | Slot`), `Dispatch.setLength`/`setOffsetSource`를 Slot 자신을 owner 키로 재사용하는 재귀 `attachSlot`(새 프리미티브 없음), 파괴는 재귀 `Clear()` 대신 flat `destroySlotTree`+명시적 `unbindLifetime`. `Slot(initial?: {T})` 생성자 부활(순수 `:Add` sugar) + `_crudUsed`↔`_listed` 상호 배타 가드 신설. `base/dispatch-core-plan.md`의 `recompute` off-by-one 버그도 이 세션에 같이 수정됨. **[2026-08-11 세션, 일곱 번째]** 반응형 raw 요소(`Slot:Add`가 `State`/`Source`도 받음) 확정 — 새 메커니즘 아니라 `isState(element)`면 내부적으로 `Slot():Single(element)`(nested Slot)를 대신 삽입하는 순수 sugar(최초 검토했던 별도 position-keyed StoreBind 구독 안은 `None`/Length/Move-Swap 문제로 기각). `Slot:Single(state, updateFn?)`도 `updateFn` 선택 인자화(기본값 identity)로 이 sugar를 지지. `:List`의 `reconcile`도 nested-Slot을 반환하는 아이템의 `.Length`만큼 다음 형제 `index`가 건너뛰도록 `pos` 커밋 공식 수정. **[2026-08-12 열두 번째 세션]** 소유권 판정을 위치별 relate 비교에서, Slot 자신이 지금 어느 `inst`에 바인딩됐는지 직접 추적하는 `slotOwner`(slot→inst)로 전환(같은 Slot이 동시에 다른 위치에 마운트되는 경우까지 잡기 위함) — `owner==inst`면 emit 전파로 무시, 다른 inst면 즉시 error. **[2026-08-12 열세 번째 세션]** `slotOwner`/`kSlotMap`이 서로를 강하게 참조하는 두-`Relate` 상호 GC 순환 발견·수정 — 둘 다 `SetWeak`로 낮추고 실제 GC 앵커는 `bindLifetime`/`unbindLifetime` 하나로 통일(`attachSlot`에 `bindLifetime(physicalTarget, slot)` 추가, `destroySlotTree`에 짝인 `unbindLifetime` 추가). **[2026-08-12 열네 번째 세션]** 위 순환이 Luau에 ephemeron이 없어 실제로 GC 안 되는 게 공식 문서(luau.org/compatibility)로 확인됨 — "혹시 몰라서"가 아니라 확정된 필수 조치로 격상(`relate-plan.md`에 일반 규칙으로도 승격). **[2026-08-12 열다섯 번째 세션]** `Slot:Splice(index, removeCount, ...newElements)` CRUD 신설(구간 제거+삽입을 shift/recompute 1회로 묶는 순수 최적화, `newElements`는 의도적으로 vararg 유지 — `Tag:Added`의 `string|{string}` 전환과는 다른 이유). **[2026-08-12 열여섯 번째 세션]** `slotOwner`를 top-level/nested 이중 마운트 gap까지 잡는 `elementOwner`로 일반화, `bindLifetime`을 top-level 전용으로 축소(nested는 `_elements` 강참조로 transitively 생존). **[2026-08-13 세션]** `releaseOwner`가 소유권 불일치를 조용히 무시하던 걸 즉시 error로 강화, `bindLifetime`을 `attachSlot`의 조건 분기에서 `SlotHandler.process`(Handler 층위)로 이동해 `unbindLifetime`과 대칭을 맞춤. **[2026-08-13 세션, 다섯 번째, 전면 반영]** `Dispatch`가 핸들러 identity 대신 인덱스로 재추적되며 `SlotHandler.process`가 `retract` 별도 필드 없이 자기 retract 클로저를 반환하는 계약으로 전환 — `kSlotMap`이 완전히 불필요해짐(어느 `process` 호출이 반환한 클로저든 `slotValue`/`inst`를 동일하게 캡처해 대칭적으로 동작하므로), `base/dispatch-core-plan.md` "Dispatch 체인" 절 참고. **[2026-08-13 감사]** 그 "대칭적으로 동작"이 `claimOwner`의 false가 *같은 (inst,k) 재발행*일 때만 참이었음이 드러나 소유권 판정을 둘로 분리 — nested(`rawAdd`)는 엄격 `claimOwner`(같은 owner 재클레임도 error, `Slot{a,a}`가 조용히 통과하던 것 차단), top-level은 `claimOwnerAt(element,inst,k)`으로 위치까지 봐서 `Frame{slot,slot}`을 error로 잡음. 추가로 `rawRemove`의 `releaseOwner` 누락(산문엔 있고 의사코드엔 없었음)과 `destroySlotTree`가 자식 소유권/`_mounted`를 안 되돌려 GC 타이밍 의존 오류를 내던 것도 수정. `State` 재설정 경로가 안전함(reconcile이 제거→`rawAdd` 순서라 release→claim)은 별도 절로 확인 기록. **[2026-08-13 세션, 여섯 번째 — 전면 역전]** `State` 교체가 **파괴에서 언마운트로 뒤집힘**(`state`와 동일 — "이전 값을 지울지는 그 값을 만든 쪽이 정한다"는 `Ref`/`Attribute`와 같은 철학) — 이에 따라 (a) 비파괴 짝 `rawUnmount`/`unmountSlotTree` 신설(`rawRemove`/`destroySlotTree`와 딱 하나만 다름: 안 죽임)되고 `reconcile`이 직접 부르는 게 `rawAdd`/`rawUnmount`/`rawMove`로 바뀜, (b) **오래 "오버엔지니어링"으로 기각돼 있던 포탈이 별도 기능이 아니라 이 결정의 자연스러운 귀결이 됨**(옛 "폐기, 옮기지 않음" 결정은 역전, `archive/slot-discard-no-portal-reversed.md`), (c) 명시적 파괴 수단으로 base 탑레벨 `dispose(value)` 신설 — 아직 트리가 살아있길 요구하는 값이면 파괴를 **거부하고 error** **[2026-08-13 열네 번째 세션]** 하강 diff 반영 — `SlotHandler`의 클로저가 받는 값이 항상 `Slot`이거나 `nil`임이 계약으로 보장되고, 언마운트 경로의 `setOffsetSource(None)`/`setLength(0)` 순서는 그대로. **[2026-08-14 열 번째 세션]** `dispose`의 시그니처/범위(`question.md` 0-B) 확정 — `dispose(value: Slot | Instance)`, `isSlot`이 아니면 백엔드 주입 op `nativeDispose(element)`로 위임(**[2026-08-22]** 옛 가칭 `disposeInst`)(`addTag`/`removeTag`/`setAttribute`와 같은 패턴), `Observer`/`Effect`는 GC-native lifecycle만으로 충분해 범위에서 명시적으로 제외. **[2026-08-18 구현 전 QA 반영]** **`:List` reconcile의 `nil` 리턴은 다시 파괴가 기본**(값 교체와 새 `PopOnly`(가칭)만 비파괴 — 2026-08-13의 "전부 비파괴" 일반화가 `:List`엔 안 맞았음), `dispose` 절에 `SetAndDispose` 백로그 후보 추가. **[2026-08-18 구현 전 QA 2라운드 후속]** "재귀 메커니즘" 절의 `attachSlot`이 자기 flush 루프를 자기 자신의 `Blocker`로 감싸도록 재작성돼 `RC-1` 해결(부모와 별도 Blocker, 런타임 단건 `Add`는 게이팅 불필요). **[2026-08-18 구현 전 QA 3라운드]** `attachSlot`이 `slot._mounted = true`를 `activateList` 호출 뒤로 미루도록 재정렬 — `:List` 최초 population이 무게이팅 recompute를 태우던 것(`RC-3`)과 nested Slot이 이중 `attachSlot`되던 것(`RC-4`) 둘 다 해결. `spliceArraysDown`이 밀어야 할 배열에 `bk.observers`/`bk.N` 갱신도 명문화. **[2026-08-19]** 가칭 `PopOnly`를 `Detach`로 리네임 확정(`Extract`의 명령형 추출과 동사가 겹치지 않으면서 "관리 주체는 reconcile"이라는 뜻을 살림) — 공개 표면 위치도 `None` sentinel 선례를 따라 패키지 최상위 export로 같이 확정(`Slot`이 함수라 `Slot.Detach` 형태로 못 붙임), 정의 파일 배치는 M6 구현 시점 확정. **[2026-08-20 구현 전 QA 4라운드 회신 1차]** `SetAndDispose`를 `source:SetAndDispose(value)` 콜론 메서드로 확정, `unmountSlotTree`가 `slot.Offset`은 안 건드림(`SL-75`) 등 회신 반영 다수. **[2026-08-21 구현 전 QA 4라운드 회신 4차 — 이 문서 최대 규모 변경]** (a) **`Detach`의 보존 주체가 `userdata`에서 새 `slot._detached` 필드로 전면 뒤집힘** — gcconn 트릭 때문에 detach된 quad-제작 Instance는 GC 폴백이 아예 없는데 `userdata`는 `:List`에게 opaque라 최종 처분이 불가능했음. 재-`Detach`는 nop, `prev`를 그대로 반환하면 재마운트. (b) 이에 맞춰 **raw 3형제로 분화** — `rawRemove`(소유권 해제+파괴)/`rawUnmount`(해제+파괴 안 함)/**`rawDetach`(소유권 유지+파괴 안 함)**, 그리고 정상 사이클과 소멸 루프가 공유하는 처분 함수 `settle()` 신설. (c) **`KeyGone` 센티널 신설** — 키가 데이터에서 사라진 자리를 조용히 처분하지 않고 `updateFn(KeyGone, 0, offset, prev, ud)`로 한 번 더 물음(오래 ⚠️ 미결이던 항목 해소), owner 사망 시 최종 정리는 `activateList`가 거는 `Effect`(**[같은 날 이관]** 원래 `mountSlotTree`였으나 `_detached`를 채우는 건 `:List`뿐이라 옮김). (d) **`Owned` 설치 플래그 신설** — `Detach`(사이클 단위)와 직교하는 축으로, `false`면 어떤 경로로도 파괴 안 함(`state` 의미론 충돌 해소). 이로써 값 교체는 `Owned = true`면 **파괴가 맞다**로 재정정(2026-08-18의 "값 교체는 비파괴"를 뒤집음). (e) **`attachSlot`이 `materializeSlotTree`(부기) + `mountSlotTree`(물리)로 분해** — "부모에게 미는 길이는 최종값"(C6)과 "부기가 물리보다 먼저"(C7)가 단일 함수로는 동시 만족 불가라는 진단이 근거, 공개 `attachSlot`은 두 줄짜리 래퍼로 남아 호출부 무변경. (f) `SL-38`의 `userdata` 생명주기 제약에 "quad-제작 Instance는 GC로 안 죽는다"를 예시로 추가. **같은 날 반영 후 감사 6라운드로 실제 크래시 3건이 더 나와 닫혔다**(detach 재마운트가 `claimOwner`에서 죽던 것 등) — **개별 항목을 여기 쌓지 않는다, 처리 전량의 소스는 `qa-request/pre-implementation-qa-round4-followup.md`의 H절·I절** **[2026-08-24 6라운드 손 트레이싱 — 이 문서가 가장 크게 바뀌었다]** (1) **상태가 셋이 됐다**(미실체화/실체화/마운트) — `_mounted`는 이제 **물리 인스턴스 유무**만 뜻하고 `slot._physicalTarget`이 신설됐다. `raw*`는 **부기를 실체화 시점부터 항상** 하고 `native*`만 `_mounted`로 가른다(`H-2`/`H-12`). (2) **`slot._elemIndex`**(물리 요소→인덱스 역방향 맵) 신설 — `indexOfRaw`가 O(1) 기본 경로가 되고 `:List`의 `keyIndex`는 **단순 키 집합(`prevKeys`)**으로 강등(`H-1`). (3) `updateFn`의 `index`를 **`Dispatch.getOffsetAt`에서** 구한다 — 옛 `result.Length:Get()` 가산은 마운트 전 Slot의 Length가 항상 0이라 아무것도 반영 못 했다(`H-2`). (4) 요소 타입 검증이 **블랙리스트→화이트리스트**(`isSlot`→`isState`→**주입 술어 `isInst`**), 관문은 `wrapElement` 하나(`H-40`). (5) `unwrapElement`에 `isSlot` 가드(Instance에서 항상 죽던 것, `H-21`), `identityUpdateFn`이 `KeyGone` 흡수(`H-22`), `dispose` 가드를 분기 밖으로(`H-28`/`H-43`), `collectLeaves` 신설 + 미작성 `raw*` 규약(`H-29`), 선행 검증 패스(`H-30`/`H-31`), `unmountSlotTree` 역순 순회(`H-6`), top-level `Slot.luau` 신설(`H-46`) | | `modifier-plan.md` | Modifier는 런타임 plug 아닌 정적 merge, immutable+clone 기반 체이닝 — 메커니즘 확정. **[2026-08-07 다섯 번째 세션 추가]** `:Apply(factory)` 팩토리 체이닝, `Overridden`(구 `Merge`→`Override`, 2026-08-08 세션에서 이름까지 확정) 값 결합+성능 기준, `:Peek`/`isState` 필드 읽기까지 전부 확정(`Peek`/`isState`는 이름만 용어 정리 라운드까지 잠정). **[2026-08-12 열일곱 번째 세션]** `table.clone`이 메타테이블을 참조로 공유한다는 핵심 전제(M7 "클래스별 코드 없이 제네릭 `__index` 하나로 충분" 설계의 근거)가 실제 Luau 동작으로 확인됨(`pre-implementation-audit.md` 1-11 해소). Property에 Attribute식 이름 소유권 레지스트리를 적용하는 안은 검토 후 기각(엔진이 정한 유한 프로퍼티 이름 집합은 전용 키를 못 만들어 소유권 판정 자체가 성립 안 함 — Property가 override 우선순위를 쓰는 이유). **[2026-08-18 구현 전 QA 반영]** 고정 메소드(=Modifier 필드 이름 예약)는 `Apply` 하나가 아니라 **`Apply`/`Peek`/`Overridden` 셋**(M7 타입 생성 스크립트 제외 목록에 반영 필요), `Overridden`은 닷/콜론 둘 다 가능 **[2026-08-24 6라운드 `H-35`]** `flatten`이 만드는 `ProcessedModifier` 센티널을 받는 **`ProcessedModifierHandler`의 의사코드가 없고 색인 두 곳에서도 빠져 있던 것**을 보강 — `Modifier`가 하나라도 든 리터럴은 **전부** 이 핸들러를 거치므로, 이 문서를 안 읽고 색인만 보고 구현하면 존재 자체를 놓친다. 소속 파일은 `quad-base/Dispatch/Modifier.luau`(`architecture.md` 소스 트리와 `ROADMAP.md` M7에도 등재) | | `purity-and-effects-plan.md` | 컴포넌트 "순수성"이 아니라 "이식성" 문제로 재정의 — 문서 경고 수준으로 확정 | | `component-composition-plan.md` | 컴포넌트=플레인 함수, State/Source 읽기·쓰기 경계, Source가 State를 구조적으로 만족 — modifier/Ref 컴포넌트 경계 통과까지 전부 확정, 남은 건 API 이름뿐. **[2026-08-07 정리]** 폐기된 `StoreSource` 프록시 설계로의 역전 이력은 본문에서 빼고 `archive/store-source-proxy-reversed.md` 포인터로 압축 | -| `blocker-plan.md` | **[2026-08-07 신설]** `Blocker` — 여러 Source를 한꺼번에 바꿔도 파생값 재계산이 한 번만 되게. **[2026-08-22 정정]** 구현은 M3가 아니라 **M2**이고, 바닥부터 짜는 게 아니라 공용 `GateNode`(`gate-plan.md`) 위의 **정책**이다. 메커니즘+이름 확정. **[2026-08-18 구현 전 QA 2라운드 후속]** `IsOn()`/`OffWithoutEmit()` 신설(`RC-1` 해결 과정에서 나옴) — `state:Block()` 없이 Blocker를 직접 쓰는 두 번째 용례(base 내부 Length/Offset 배치 게이팅)도 추가. **[2026-08-18 구현 전 QA 3라운드]** 이 용례의 존재 이유 정정 — `RC-1`의 원래 크래시는 사라졌고(`bk.N` 수명주기 재정의로), 지금 필요한 이유는 배치 등록 비용(O(N²)→O(N)) **[2026-08-24 6라운드 `H-33`/`H-49`]** **`blocker:Policy(emit) -> onUpstreamEmit`** 신설 — 자기 게이트 정책을 값으로 내주고 `state:Block(b)`가 그 위의 얇은 래퍼가 된다. `Debounce`/`Throttle`이 이걸로 자기 Blocker를 조종한다 | +| `blocker-plan.md` | **[2026-08-07 신설]** `Blocker` — 여러 Source를 한꺼번에 바꿔도 파생값 재계산이 한 번만 되게. **[2026-08-24 재확정]** 구현은 State 코어와 같은 **M2**이고(2026-08-22에 디스패치 쪽으로 앞당겼다가 마일스톤 순서 교체로 되돌아옴), 바닥부터 짜는 게 아니라 공용 `GateNode`(`gate-plan.md`) 위의 **정책**이다. 메커니즘+이름 확정. **[2026-08-18 구현 전 QA 2라운드 후속]** `IsOn()`/`OffWithoutEmit()` 신설(`RC-1` 해결 과정에서 나옴) — `state:Block()` 없이 Blocker를 직접 쓰는 두 번째 용례(base 내부 Length/Offset 배치 게이팅)도 추가. **[2026-08-18 구현 전 QA 3라운드]** 이 용례의 존재 이유 정정 — `RC-1`의 원래 크래시는 사라졌고(`bk.N` 수명주기 재정의로), 지금 필요한 이유는 배치 등록 비용(O(N²)→O(N)) **[2026-08-24 6라운드 `H-33`/`H-49`]** **`blocker:Policy(emit) -> onUpstreamEmit`** 신설 — 자기 게이트 정책을 값으로 내주고 `state:Block(b)`가 그 위의 얇은 래퍼가 된다. `Debounce`/`Throttle`이 이걸로 자기 Blocker를 조종한다 | | `debounce-throttle-plan.md` | **[2026-08-14 신설, 2026-08-19 전부 해소돼 `research/`에서 승격]** 시간 기반 전파 게이트 `Debounce`/`Throttle` — 사용자 요청("`Blocker`와 유사하게")으로 신설. 요지: (1) `Blocker`가 이미 쓰는 게이트 노드의 **릴리스 트리거만 타이머로 바꾼 것**이라 새 전파 메커니즘이 아님, (2) 무효화 채널만 만지므로 laziness 안 깨짐, (3) **Debounce/Throttle의 차이는 "신호가 창 타이머를 리셋하는가" 한 비트뿐** — 공개 생성자는 둘, 구현은 하나, (4) 알고리즘은 quad-base + 주입 op 2개 `setTimeout(func, delay) -> Timeout`/`clearTimeout`(Roblox `task.delay`/`task.cancel`로 배선 — **인자 순서 반대라 주의**), `Timeout`은 `{ __type_timeout: true, _native: any }`. **[2026-08-19 마지막 라운드]** 의미론은 **(A) emit-gate**(`Blocker`와 동일, `:Get()`은 항상 최신값)로 확정 — 검토했던 값-지연 안은 laziness와 상충해 철회. 제어 핸들은 개별은 `Ref` 아웃파라미터·전체는 팩토리 자체의 `:Flush()`/`:Cancel()`(weak 레지스트리)로 확정, `Time`/`MaxTime`은 `number \| State`(스케줄 시점에만 폴링) 허용. **⚠️ [2026-08-24 6라운드 `H-32`/`H-33`] 7절 의사코드에 무효화 배너가 붙었다 — 그 골격은 확정된 `state:Gate(setup)` API로는 성립하지 않아 재작성 대상이다.** 새 모델은 `Debounce`/`Throttle`이 **emit을 아예 안 쥐고** 자기 `Blocker`를 사적으로 하나 갖고(적용 핸들당 하나) `On()`/`Off()` 시점만 정하는 것 — `pending`도 Blocker의 `HasBlockedEmit`으로 흡수되어 `Trailing=false`+`MaxTime`에서 `Flush`가 영구 no-op이던 결함(`H-32`)이 구조적으로 사라진다. 창/타이머 정책 자체(`openWindow`/`onWindowEnd`/`MaxTime` 분기)는 그대로 유효. 이름은 `Debounce`/`Throttle` 유지 + Roblox 관용 "debounce"와 다르다는 문서 경고. **결과적으로 quad-base에 새 코어 메커니즘을 안 더하는 순수 슈가로 귀결**(`Blocker`의 gated state + `Ref` + 주입 op 2개 위에 전부 얹힘) — 우선순위는 `Operator.*`와 같은 급으로 재평가됨. **부수 성과**: 이 설계 중 `source-state-plan.md`의 무효화 dedup 서술이 `Observer` 계약과 모순되는 게 발견돼 base 전면 정정(`archive/invalidate-dedup-propagation-reversed.md`) | | `effect-plan.md` | **[2026-08-07 신설, 여섯 번째 세션에 확정]** `Effect(fn, ...deps)` — deps 없으면 설치 1회+leaf 사망 시 확정 정리, 있으면 **각 dep에 구독을 따로 걸어**(State/Source면 `Observer`, `Ref`면 `:Callback`) 재실행+cleanup 체이닝(React `useEffect` 동형). **[2026-08-24 표기 정정]** 여기 원래 *"내부적으로 `state:Observer(...)`를 조합"*이라 적혀 있었는데 그건 단수 시절 모델이고, 같은 행 뒤쪽의 `C-6` 서술과 어긋났다. Observer와의 관계 해소 완료. **[2026-08-18 구현 전 QA 반영]** **`:Unsubscribe()`는 `:Subscribe()`의 짝으로 축소** — leaf 바인딩 경로에서 cleanup을 앞당기면 dedup 때문에 재바인딩이 안 일어나 Effect가 조용히 죽음. **[2026-08-20 `E-10` → 2026-08-21 `EF-3`에서 반영]** 그 dedup 경로의 process/retract 대칭은 **성립함이 확인됨**(핸들러가 `old`를 `Relate`로 직접 들고 양쪽이 같은 비교식을 씀 — 남은 건 구현 시 회귀 확인뿐, 설계상 열린 항목 아님). **[2026-08-21 5라운드 `C-6`]** 시그니처가 `Effect(fn, ...deps)`로 확장돼 의존성을 여러 개 직접 받고(`Ref`도 가능) 각각에 구독을 건다. **[2026-08-21]** 다중 의존성이 공통 상류를 공유할 때 한 파동에 `fn`이 여러 번 돌던 미해결 갭은 **`EffectHandle`이 자기 `EpochMap`을 들어** 닫힘(`base/state-epoch-plan.md`). **[2026-08-24 6라운드]** `fn` 시그니처가 **`fn(self: EffectHandle) -> (() -> ())?`**로 확정(**`...deps`는 의존성 선언일 뿐 `fn`에 안 넘어간다**, `H-14`), `_observer`(단수)→`_observers`(배열)(`H-8`), 그리고 **leaf 사망 cleanup을 실제로 발화시키는 배선이 없던 것**이 `bindLifetime`/`unbindLifetime`이 `isEffect`를 보고 `Destroying`을 걸고/끊는 것으로 닫힘(`H-11` — 이게 없어서 `slot._detachCleanup`과 `OnDestroyed`가 통째로 무동작이었다). `Ref` dep 콜백은 해제 경로(`:Uncallback`)와 **발화 시 `canExecute` 확인**을 둘 다 갖는다(`H-7`) | | `ui-shorthand-plan.md` | **[2026-08-07 `research/`에서 승격]** `UICorner`/`UIPadding`/`UIScale` 인라인 편의 키 — 이름(v1 `Corner`/`PaddingAll`/`Scale`에서 Modifier 필드명과 안 겹치게 `UI` 프리픽스로 확정)·메커니즘(Handler)·패키지 배치(quad-roblox 코어)·store-bind 가능성까지 전부 확정. 이미지 라운드 트릭(`RoundSize`)은 드롭 — `archive/ui-shorthand-roundsize-dropped.md` 참고. `v=nil`이면 `process` 자신이 만든 자식 제거(`retract` 아님). **[2026-08-14 세션] Tween 지원 추가** — 자식 프로퍼티를 직접 대입하지 않고 `Dispatch.process(child, prop, ..., 1)`로 위임하는 것으로 확정(프로세스 중 `inst`를 바꾸는 건 키를 바꾸는 것과 같은 층위라 UB 아님, `dispatch-core-plan.md`에 일반 규칙으로 명문화) — Tween 해석 코드가 `PropertyHandler` 하나에만 남는다는 불변식이 유지되고, 이 문서가 새로 정할 건 스칼라→프로퍼티 `wrap`을 `Tween.Value`에만 적용되도록 들어올리는 헬퍼 하나뿐. 옛 "트윈까지 지원할 필요 없음" 서술은 역전됨(그때는 Tween이 독립 Dispatch 핸들러였음). ROADMAP M10에 빠져 있던 체크리스트 항목도 이 세션에 보강. **[2026-08-18 구현 전 QA 반영]** 만든 자식을 다시 찾을 때 **`FindFirstChild` 대신 `Relate` 저장**(이름은 표시·판정용, 릴레이션은 조회용), 자식 프로퍼티 세팅도 `Dispatch.process`로 위임해 Tween이 공짜로 따라오게 | @@ -71,8 +71,8 @@ | `tween-plan.md` | **[2026-08-12 세션, `research/`에서 승격]** 값-레벨 `Tween` 래퍼(PropertyHandler가 소비, 구 특수 bind key 모델은 `archive/tween-special-bind-key-reversed.md`). 3-상태 릴레이션 슬롯(`{Tween,Value}\|true\|nil`), `T'=T\|Tween` 타입 치환. 옵션 값 모양은 `Info: TweenInfo?` 우선+편의 필드 폴백, override는 `Tween.Cancel`(기본)/`Tween.Finish` 2값. `Animate(info)`는 `Tween` opts를 `T\|State`로 받아 `:Apply`로 꽂는 sugar. 자연완료 시 per-instance 북키핑은 정리 안 해도 됨으로 확정(목표값 도달 상태라 부작용 없음, Completed 이벤트 구독 장치는 오버엔지니어링으로 판단). `initValue`는 사용자가 직접 처리(에이전트 범위 제외) **[2026-08-24 6라운드 `H-24`]** `:Mapped` 절에 타입 단서 추가 — 그 시그니처는 재귀 제네릭 누수 패턴이라 **인라인이 아니라 `typeof(named function)`으로 선언해야** 체커가 지켜준다(`base/typing-limits.md`). "타입이 안전하게 성립한다"는 *의미론상* 맞지만 *체커가 지켜준다*는 뜻이 아니었다 | | `fallback-plan.md` | **[2026-08-14 세션, `research/`에서 승격]** `Fallback`/`Traceback` — 컴포넌트 함수를 감싸 에러 시 플레이스홀더를 그려주는 순수 슈가(`additional-primitives-plan.md`의 "Error Boundary" 절이 내린 "빈 자리 아님" 결론 위에 얹힘). `Fallback`은 `pcall` 기반(trace 없음), `Traceback`은 `xpcall`+`debug.traceback` 기반(trace 항상 있음) — 플래그 대신 별도 함수로 분리(`Ref`/`PreRef`와 같은 패턴). `err: any`(Lua `error()`가 임의 값을 던질 수 있음, `error(msg)` 기본 호출의 위치 접두 캐비엇 포함) 확정. 패키지는 `quad-base`, 이름 확정. 메커니즘 실측은 `audit/fallback-xpcall-verification.md`. 구현 우선순위는 형제 백로그(`quad-mock`/`quad-debug`/`Operator`)와 동급, 맨 뒤 **⚠️ [2026-08-24 6라운드 `H-26`] 미해결 항목이 하나 신설됐다** — **실패 이전에 생성된 부분 트리는 회수되지 않는다.** quad Instance는 gcconn 때문에 `Destroy`로만 회수되는데, 컴포넌트가 리터럴을 만들다 던지면 그때까지 완성된 형제/자손이 트리에 붙지도 파괴되지도 않은 채 사라지고 `Fallback`은 그 존재를 알 방법이 없다. **`Fallback`/`Traceback`이 그 경로를 계속 살려두는 걸 존재 이유로 삼는 대표 사용처**라 층위가 다르다 — **백로그**(그 둘이 슈가라 구현 시점에 같이 다룬다, 그 문서의 ⚠️ 절이 소스) | | `lifecycle-hooks-plan.md` | **[2026-08-14 아홉 번째 세션, `research/`에서 승격]** 생명주기 훅 슈가 `OnCreated`/`OnRendered`/`OnDestroyed` — 각각 `PreRef():Callback(fn)`/`PostRef():Callback(fn)`/`Effect(function() return fn end)`를 반환하는 **순수 팩토리 함수**라 새 타입/Dispatch 개념이 전혀 안 생김(호출 즉시 평가돼 기존 인스턴스로 사라짐), 여러 개 나란히 등록도 자연 지원(단 **같은 계열끼리의 순서는 미보장**). 마지막 열린 항목이던 `OnRendered`는 사용자가 **채택 확정** — 메커니즘은 `PostRef`(`base/ref-plan.md`), 원래 열어뒀던 (a)/(b)/(c) 중 **(a)**. 캐비엇: `OnRendered`는 서브트리 완성은 보장하지만 **이 인스턴스가 부모에 붙기 전**에 불림(React `componentDidMount`와 다름) — 문서화 필수. 패키지 `quad-base` 확정. **[2026-08-14 열 번째 세션]** `dispose()` 범위(0-B)가 `Slot`+`Instance`로 좁혀지고 `Observer`/`Effect`는 제외되는 쪽으로 확정되며 `OnDestroyed` 이름 재검토 조건이 발동 없이 종결 — `OnDestroyed`가 최종 이름, 용어 대기열에서도 제외 | -| `gate-plan.md` | **[2026-08-21 신설, 같은 날 표면 확정]** `state:Gate(setup)` — 상류 emit을 가로채 내려보낼지 정책이 정하는 **`GateNode`**(`ComputeNode`와 같은 층위)를 만드는 State 메소드. 탑레벨 `Gate(...)` 프리미티브는 **안 만든다**(처음 방향에서 뒤집힘) — `Blocker`가 `state:Block(blocker)` 안에서 이 배선을 쓰고, `Debounce`/`Throttle`은 `state:Apply(...)` 팩토리가 내부에서 `:Gate`를 부른다. `Get()`엔 영향 없음(통지만 막음)까지 확정. **[2026-08-24 6라운드 `H-33`/`H-49`] 열린 항목이 전부 닫혔다** — 재진입은 2026-08-21에 이미 닫혀 있었고, 마지막 남은 생명주기(=`Gate`에 `Flush`/`Cancel` 표면을 둘지)는 **안 두는 것**으로 확정: `blocker:Policy(emit)`을 노출하고 `Debounce`/`Throttle`이 자기 `Blocker`를 조종하는 정책이 된다(정책 합성은 손으로 중첩). (**[2026-08-22]** 미결이던 "M2 범위 — `Gate`만 vs `Blocker`까지"는 **둘 다 M2**로 해소.) 구현은 M2 | -| `state-epoch-plan.md` | **[2026-08-21 신설, 같은 날 채택 확정·`Epoch` 일반화까지 반영]** State의 재계산/전파 판정을 `invalid` 플래그가 아니라 **`Epoch` 리비전 비교**로 한다 — DFS 전파 도중 `Get()`이 섞인 값을 캐시하던 glitch(실재)를 없애는 **정확성** 결정. `type Epoch = { Revision: number }`(그 자체로 키가 되는 unique 테이블, `Source`가 구조적으로 만족), 부기는 재사용 가능한 **`EpochMap`**(`:Update(Epoch|EpochSet) -> boolean`이 "뒤로 전파가 필요한가"를 답함, `:Refresh`/`:Sync`/`:TrackFrom`. `EpochSet = {[Epoch]: true}`로 **배열이 아니라 집합** — 게이트 배치가 그 모양이다)으로 떼어냈고, State는 그걸 **둘** 컴포지션한다 — `valueEpochMap`(값 유효성)/`emitEpochMap`(전파 dedup). emit은 값도 리비전도 안 싣고 **출처(`Epoch`나 그 집합)만** 싣고, 순회는 `rawInvalid == false`일 때만 돌며 값만 앞당기고 통지는 상류 emit을 기다린다. 중복 *통지*도 같이 접히므로 `source-state-plan.md`의 옛 "항상 전파 / 중복 통지는 안 접음" 서술이 역전됨(`archive/always-propagate-no-dedup-superseded.md`). ⚠️ 2026-08-14에 폐기된 `invalid` 기반 dedup과는 다른 장치 — 그 금지는 유효. 리비전 갱신은 **`bit32.bnot(-rev)`** 한 번(사용자 확정 — 랩어라운드 **감소**를 단일 FASTCALL로, hot path라 값을 uint32에 가두고 `2^53` 포화 자체를 없앰. **[2026-08-22 정정]** 한때 `band(rev + 1, mask)`로 잘못 옮겨져 있었음). **열린 설계 항목 없음.** **[2026-08-22 정정]** 구현 마일스톤은 둘로 갈린다 — 부기 객체 `EpochMap.luau`와 `Epoch` 인터페이스·리비전 갱신은 **M2**(`GateNode`가 씀), State 본체 통합은 **M3** | +| `gate-plan.md` | **[2026-08-21 신설, 같은 날 표면 확정]** `state:Gate(setup)` — 상류 emit을 가로채 내려보낼지 정책이 정하는 **`GateNode`**(`ComputeNode`와 같은 층위)를 만드는 State 메소드. 탑레벨 `Gate(...)` 프리미티브는 **안 만든다**(처음 방향에서 뒤집힘) — `Blocker`가 `state:Block(blocker)` 안에서 이 배선을 쓰고, `Debounce`/`Throttle`은 `state:Apply(...)` 팩토리가 내부에서 `:Gate`를 부른다. `Get()`엔 영향 없음(통지만 막음)까지 확정. **[2026-08-24 6라운드 `H-33`/`H-49`] 열린 항목이 전부 닫혔다** — 재진입은 2026-08-21에 이미 닫혀 있었고, 마지막 남은 생명주기(=`Gate`에 `Flush`/`Cancel` 표면을 둘지)는 **안 두는 것**으로 확정: `blocker:Policy(emit)`을 노출하고 `Debounce`/`Throttle`이 자기 `Blocker`를 조종하는 정책이 된다(정책 합성은 손으로 중첩). (**[2026-08-22]** 미결이던 "마일스톤 범위 — `Gate`만 vs `Blocker`까지"는 **둘 다 같은 마일스톤**으로 해소.) 구현은 M2 | +| `state-epoch-plan.md` | **[2026-08-21 신설, 같은 날 채택 확정·`Epoch` 일반화까지 반영]** State의 재계산/전파 판정을 `invalid` 플래그가 아니라 **`Epoch` 리비전 비교**로 한다 — DFS 전파 도중 `Get()`이 섞인 값을 캐시하던 glitch(실재)를 없애는 **정확성** 결정. `type Epoch = { Revision: number }`(그 자체로 키가 되는 unique 테이블, `Source`가 구조적으로 만족), 부기는 재사용 가능한 **`EpochMap`**(`:Update(Epoch|EpochSet) -> boolean`이 "뒤로 전파가 필요한가"를 답함, `:Refresh`/`:Sync`/`:TrackFrom`. `EpochSet = {[Epoch]: true}`로 **배열이 아니라 집합** — 게이트 배치가 그 모양이다)으로 떼어냈고, State는 그걸 **둘** 컴포지션한다 — `valueEpochMap`(값 유효성)/`emitEpochMap`(전파 dedup). emit은 값도 리비전도 안 싣고 **출처(`Epoch`나 그 집합)만** 싣고, 순회는 `rawInvalid == false`일 때만 돌며 값만 앞당기고 통지는 상류 emit을 기다린다. 중복 *통지*도 같이 접히므로 `source-state-plan.md`의 옛 "항상 전파 / 중복 통지는 안 접음" 서술이 역전됨(`archive/always-propagate-no-dedup-superseded.md`). ⚠️ 2026-08-14에 폐기된 `invalid` 기반 dedup과는 다른 장치 — 그 금지는 유효. 리비전 갱신은 **`bit32.bnot(-rev)`** 한 번(사용자 확정 — 랩어라운드 **감소**를 단일 FASTCALL로, hot path라 값을 uint32에 가두고 `2^53` 포화 자체를 없앰. **[2026-08-22 정정]** 한때 `band(rev + 1, mask)`로 잘못 옮겨져 있었음). **열린 설계 항목 없음.** **[2026-08-24 재확정]** 구현 마일스톤은 전부 **M2**다 — 2026-08-22엔 `GateNode`가 디스패치 쪽에 있어 `EpochMap.luau`/`Epoch` 인터페이스만 갈려 있었으나, 마일스톤 순서 교체로 그 분리가 없어졌다 | ## `reference/` — 온디맨드 참고 자료 (2026-08-07 신설) diff --git a/.claude/archive/question-resolved.md b/.claude/archive/question-resolved.md index e24146f..9bffd51 100644 --- a/.claude/archive/question-resolved.md +++ b/.claude/archive/question-resolved.md @@ -1217,3 +1217,78 @@ hot path다. `base/state-epoch-plan.md` §2, 근거 기록은 `reference/epoch-brand-composition.md` §4의 4번. (필드 이름 `Revision`과 타입 `Epoch`는 확정, 승격 시 `base/`에 반영됨.) + +## [해소됨, 2026-08-24] M2/M3 마일스톤 경계 — 순서 교체로 확정 + +2026-08-22 `ROADMAP.md` 전반 점검에서 드러난 항목. **설계 결정이 아니라 +마일스톤 *순서* 문제**라 당시 `question.md`의 최우선 절이 아니라 2번 +항목으로 따로 뒀었다. 2026-08-24에 사용자가 **(a) 순서 교체**를 선택해 +같은 날 전량 반영됐다. + +**발단** — 옛 M2(디스패치 엔진)와 옛 M3(Store/State/Source)의 의존이 +양방향이라 그 순서로는 M2를 끝까지 짤 수 없었다. + +- **디스패치 → 반응형 (본체 의존)**: `Dispatch.setLength`가 + `len: number | State`를, `Dispatch.setOffsetSource`가 + `Source`를 받고, `recompute`가 `offset:Set()`을 부른다 + (`base/dispatch-core-plan.md`의 "Length/Offset" 절). 2026-08-22에 + 디스패치 쪽으로 앞당겼던 `GateNode`/`Blocker`도 State 위에 얹힌다. +- **반응형 → 디스패치 (얕은 의존)**: `state:Observer`/`Effect`의 동적 경로 + 가드가 `Dispatch.addHandler` + `Handler.luau` 계약을 쓴다 — 레지스트리 + 등록 표면만 있으면 되므로 디스패치 **전체**를 요구하지 않는다. + +**선택지와 판단** — (a) 순서 교체 / (b) 디스패치를 둘로 분할 / (c) 지금 +구조를 두고 구현 시 알아서 오간다. **사용자 선택은 (a)**("(a) M2와 M3의 +순서를 바꾸는게 맞겠던데"). 그 자리에서 같이 확인된 근거: + +1. **의존의 비대칭**이 명확하다 — 무거운 쪽(본체 의존)을 아래에 깔고 + 가벼운 쪽(핸들러 등록)을 뒤로 미루는 게 맞다. +2. 옛 순서로는 **디스패치 마일스톤의 `mock 대상 테스트` 체크박스가 + 원리적으로 불가능**했다(`setLength`/`recompute`를 State 없이 테스트할 + 수 없음). 반대로 반응형 코어는 순수 Lua라 `luau`만으로 단독 테스트가 + 된다. +3. 2026-08-22의 `EpochMap`/`GateNode`/`Blocker` 앞당김이 **불필요해진다** + — 그 이동은 "디스패치가 먼저인데 디스패치가 쟤들을 호출한다"는 이유로 + 한 것이었다. 순서를 바꾸면 마일스톤 내용이 의존 계층과 그대로 일치하고 + 끌어온 항목이 0개가 된다. "게이팅 먼저"는 그대로 지켜진다. + +**(b)를 안 고른 이유**: 참조 비용이 오히려 더 크다 — 기존 `M2` 참조 전부가 +"M2a인가 M2b인가"로 애매해지는데 이건 순서 교체처럼 기계적으로 못 푼다. +게다가 `Brand`를 앞으로 빼는 일은 (b)에서도 똑같이 해야 하고, 얻는 건 +"디스패치 코어를 조금 일찍 짠다"뿐인데 M4 전까지 그걸 소비하는 게 없다. +**(c)를 안 고른 이유**: `project-context.md`가 못박은 *"어떤 순서로 +만드는가"*의 소스가 `ROADMAP.md`라는 원칙을 스스로 무효화한다. + +**같이 확정된 것 — 순서만 바꿔서는 안 끝났다.** 옛 M2 안에 반응형보다 +먼저 와야 하는 항목이 셋 섞여 있었고, 이들은 새 M2 앞머리의 +`### 공통 기반` 절로 옮겨졌다(State-free이자 dispatch-free): + +- **`Brand.luau`** — `Source`가 `SourceBrand`+`EpochBrand`에 등록돼야 함 + (`base/source-state-plan.md`), `isState`/`isObserver`/`isEffect`가 전부 + 여기 얹힘. +- **`Relate.luau`** — `bindLifetime`/`unbindLifetime`이 그 위에 구현됨 + (`base/lifecycle-pattern.md`의 `InstData`/`BindData`). +- **`LifetimeHandle.luau` 인터페이스** — State 전파 루프가 발화마다 + `canExecute`를, 이중 바인딩 게이트가 `canBound`를 부름. 이미 M8→디스패치로 + 한 번 앞당긴 전례가 있던 항목이라 한 칸 더 앞당긴 것뿐. + +반대로 새 M3로 넘어간 것은 둘 — Observer/Effect **동적 경로 가드 등록**과 +**`ObserverEffectLeafHandler`**(`H-39`). 둘 다 "핸들러를 등록한다"뿐이라 +본체와 잘라내기 쉽고, **이것이 M2가 M3에 개념상 지던 유일한 의존**이었다 — +그 둘을 M3로 미뤘으므로 **빌드 순서상 역방향 간선은 남지 않는다**: + +``` +L0 공통 기반 Brand · Relate · LifetimeHandle(인터페이스) ┐ +L1 반응형 Source/State/Store · EpochMap · Gate/GateNode · ├ M2 + Blocker · :Compute/:Apply · Observer · Effect ┘ +L2 디스패치 Handler · Dispatch 코어 · chains · None · ┐ + setLength/setOffsetSource/getOffsetAt · ├ M3 + Leaf · 가드 등록 · ObserverEffectLeafHandler ┘ +``` + +**⚠️ 번호 재부여의 부작용** — 라이브 문서의 `M2`/`M3` 참조는 전부 새 번호로 +맞췄지만(코퍼스의 참조가 거의 전부 "그 내용이 사는 마일스톤"을 가리켜서 +기계적 맞교환으로 의미가 보존됨), **`session/`·`archive/`·`qa-request/`는 +히스토리 문서라 소급 수정하지 않았다.** 2026-08-24 이전에 쓰인 그 문서들의 +`M2`/`M3`는 **옛 의미**(M2=디스패치, M3=반응형)로 읽을 것. 이 경고는 +`ROADMAP.md`의 M2 배너에도 있다. diff --git a/.claude/base/attribute-plan.md b/.claude/base/attribute-plan.md index 8ec0a4a..c95eabf 100644 --- a/.claude/base/attribute-plan.md +++ b/.claude/base/attribute-plan.md @@ -483,7 +483,7 @@ end 멈춤 — 조용한 오작동이 아니라 **반복 재현되는 시끄러운 실패**. 그래서 별도 롤백 장치를 넣지 않음(코퍼스의 "에러=패닉 상태, 그 이후 정합성은 관리 대상 아님" 원칙과 같은 결). 원자적 롤백이 필요하다고 판단되면 - 그때 그룹 `process`에만 국소적으로 넣을 수 있음 — `question.md` 3번에 + 그때 그룹 `process`에만 국소적으로 넣을 수 있음 — `question.md` 2번에 열어둠. - **`groupKey(v, name)`는 그룹 값 객체별·이름별 메모이즈** — 같은 그룹 값이 재프로세스될 때 같은 키가 나와야 claim이 자기 자신과 안 부딪힘. diff --git a/.claude/base/blocker-plan.md b/.claude/base/blocker-plan.md index 9635bf2..ba6f33e 100644 --- a/.claude/base/blocker-plan.md +++ b/.claude/base/blocker-plan.md @@ -16,13 +16,16 @@ 문제를 콜스택/코루틴이 아니라 사용자가 들고 있는 "값"으로 표현**해서 이 위험을 구조적으로 우회한다. -**store 개발(M3)과 밀접하게 연관됨** — `state:Block(blocker)`가 State +**store 개발(M2)과 밀접하게 연관됨** — `state:Block(blocker)`가 State 위에 얹히는 메소드이므로 `base/source-state-plan.md`의 Source/State 온톨로지, 특히 push-invalidate/pull-recompute 전파 모델(`base/source-state-plan.md` "전파 모델 확정" 절)을 전제로 함. -**⚠️ [2026-08-22 정정] 구현 마일스톤은 M3가 아니라 M2다** — 여기 "State와 -같은 마일스톤(`ROADMAP.md` M3)에서 함께 구현할 것"이라고 적혀 있었으나, -`Dispatch.drive`의 배치 등록이 `Blocker`를 호출하므로 "게이팅 먼저" 결정에 -따라 `Blocker.luau` 체크박스가 M2로 이동했다. 별도 파일로 두는 것은 그대로. +**[2026-08-24 재확정] 구현 마일스톤은 다시 M2다 — "State와 같은 +마일스톤에서 함께 구현"이라는 원래 서술이 맞다.** 2026-08-22엔 "게이팅 +먼저"(`Dispatch.drive`의 배치 등록이 `Blocker`를 호출한다) 결정에 따라 +`Blocker.luau` 체크박스를 디스패치 쪽으로 앞당겼었는데, 2026-08-24에 +마일스톤 순서 자체가 교체되어(반응형이 M2, 디스패치가 M3) 앞당길 이유가 +사라졌다 — 게이팅은 여전히 디스패치보다 먼저 지어진다. 별도 파일로 두는 +것은 그대로. **그리고 이제 `Blocker`는 바닥부터 짜는 게 아니라 공용 `GateNode` (`base/gate-plan.md`) 위에 얹는 정책이다** — 노드를 다시 만들지 말 것. diff --git a/.claude/base/debounce-throttle-plan.md b/.claude/base/debounce-throttle-plan.md index ad41231..e3f4331 100644 --- a/.claude/base/debounce-throttle-plan.md +++ b/.claude/base/debounce-throttle-plan.md @@ -201,7 +201,9 @@ leading/trailing/통과 후 창 재개방은 **완전히 동일**. 그래서 공 생겼으니 이름 붙여 꺼내는 것뿐. **[2026-08-21 실현 — 이 권고가 확정됐다]** 그 노드는 `state:Gate(setup)`가 -만드는 **`GateNode`**이고 **M2**에서 구현된다(`base/gate-plan.md`). 다만 +만드는 **`GateNode`**이고 **M2**에서 구현된다(`base/gate-plan.md` — +**[2026-08-24]** 2026-08-22엔 디스패치 쪽이었다가 마일스톤 순서 교체로 +반응형 코어로 돌아왔다). 다만 "내부 공용"은 아니게 됐다 — **공개 표면**이다. `Debounce`/`Throttle` 쪽 관용구는 안 바뀐다: `Debounce{...}`가 돌려주는 팩토리가 내부에서 `s:Gate(policy)`를 부르므로 `state:Apply(Debounce{...})`가 그대로 성립한다. @@ -1186,15 +1188,15 @@ additional-primitives-plan.md`가 원래 "안 만들어도 된다"고 판단했 맨 뒤로 미뤄도 됨(다른 기능이 이걸 의존하지 않고, 없어도 다른 기능이 안 막힘). -**의존성**: State 코어(`ROADMAP.md` M3) + 백엔드 주입 표면(`setTimeout`/ +**의존성**: State 코어(`ROADMAP.md` M2) + 백엔드 주입 표면(`setTimeout`/ `clearTimeout`) + `Blocker`(gated state) + `Ref`. **[2026-08-22 정정]** -각각 **M2**(게이트/`Blocker`) / **M3**(State 코어) / **M8**(`Ref`)에서 -확정되는 것들이라 그 이후 언제든 얹을 수 있다(옛 표기는 "전부 M3/M8" — -`Blocker.luau`가 M2로 옮겨지기 전 기준). **[정정, 2026-08-21 구현 전 QA 5라운드] 그 "게이트 노드를 -공용으로 빼는" 작업은 M3가 아니라 M2로 앞당겨졌다** — 사용자 결정 -"게이팅 먼저"(`Dispatch.drive`의 배치 등록이 이미 그 게이팅에 의존하므로). -1절에서 봤듯 같은 노드를 공유하므로 따로 하면 같은 걸 두 번 설계하게 되는 -것은 그대로이고, 바뀐 건 **언제**뿐이다. 표면/이름은 아직 미정 — -`base/gate-plan.md`가 소스(이 문서의 1절이 그 일반화를 처음 +**[2026-08-24 재정리]** 각각 **M2**(State 코어 · 게이트 · `Blocker`) / +**M8**(`Ref`)에서 확정되는 것들이라 그 이후 언제든 얹을 수 있다. 2026-08-22엔 +`Blocker.luau`가 디스패치 쪽으로 앞당겨져 있어서 "게이트/`Blocker`는 다른 +마일스톤"이라고 적었으나, 2026-08-24 마일스톤 순서 교체로 되돌아왔다. 그 +"게이트 노드를 공용으로 빼는" 작업도 같은 M2에서 State 코어와 함께 한다 — +사용자 결정 "게이팅 먼저"(`Dispatch.drive`의 배치 등록이 이미 그 게이팅에 +의존하므로)는 그대로 지켜진다. 1절에서 봤듯 같은 노드를 공유하므로 따로 +하면 같은 걸 두 번 설계하게 되는 것도 그대로다. 표면/이름은 `base/gate-plan.md`가 소스(이 문서의 1절이 그 일반화를 처음 권고한 자리로 거기 인용돼 있다). 프리미티브 자체(`Debounce`/`Throttle` 함수)는 그 위에 아무 때나 나중에 얹으면 된다. diff --git a/.claude/base/dispatch-core-plan.md b/.claude/base/dispatch-core-plan.md index e860562..2ec07ee 100644 --- a/.claude/base/dispatch-core-plan.md +++ b/.claude/base/dispatch-core-plan.md @@ -205,7 +205,7 @@ src/schema/union.luau:48-68`) — 에러 메시지는 즉시 문자열로 만들 그래서 `Dispatch.listHandlers()`는 현재 등록된 전체 핸들러(이름/priority)를 **반환**하는 함수로 둔다. 구현 비용이 거의 없고 실제 개발 중 디버깅에 바로 도움되는 항목이라 - M2(Dispatch 엔진) 착수 시 기본 기능으로 같이 넣음 — 런타임 플러그인인 + M3(Dispatch 엔진) 착수 시 기본 기능으로 같이 넣음 — 런타임 플러그인인 `quad-debug`(후순위, `research/debug-tooling-plan.md`)와는 다른 층위의, 라이브러리 자체에 내장된 개발자 편의 기능. diff --git a/.claude/base/effect-plan.md b/.claude/base/effect-plan.md index 4da607d..92b4582 100644 --- a/.claude/base/effect-plan.md +++ b/.claude/base/effect-plan.md @@ -215,7 +215,14 @@ end ### 동적 경로 가드 — `k` 무관 매치, `HANDLER_PRIORITY_FALLBACK` (2026-08-14 열한 번째 세션, `PreRef`/`Observer`와 같은 패턴, `base/ -source-state-plan.md`의 "동적 경로 가드" 절 참고.) `EffectHandle`도 +source-state-plan.md`의 "동적 경로 가드" 절 참고.) +**⚠️ [2026-08-24] 이 가드를 실제로 `Dispatch.addHandler`로 등록하는 것은 +M3(디스패치)다.** `HANDLER_PRIORITY_FALLBACK` 상수도 `Dispatch.addHandler`도 +M3에서 처음 생기므로, M2(반응형 코어)에서 본체를 짤 때는 **핸들러 정의만 +준비해두고 등록 호출은 미룬다** — `ROADMAP.md` M3의 "Observer/Effect 동적 +경로 가드 등록" 체크박스가 그 자리다(2026-08-24 마일스톤 순서 교체의 산물, +M2가 M3에 개념상 지던 유일한 의존이라 이쪽으로 미뤄졌다). + `EffectHandle`도 children 배열 리터럴 전용이라, 해시 파트 named 자리 등으로 동적으로 흘러들어오면 명확히 에러내야 함 — `{ priority = HANDLER_PRIORITY_FALLBACK, isHandlable = function(inst,k,v) return isEffect(v) end, process = @@ -472,7 +479,7 @@ Effect의 의존성이 될 방법이 아예 없다.** 사용자 제기: *"Effect 같이 처리해야 한다(위 `E-10`/`EF-5`와 같은 함정). 사용자 확인: *"어차피 모든 옵져버들이 내부에 들어가 있을것이므로 가능하다."* -**우선순위**: 새 코어 메커니즘이 아니라 `Effect` 표면 확장이므로 M3의 +**우선순위**: 새 코어 메커니즘이 아니라 `Effect` 표면 확장이므로 M2의 `Effect` 구현과 같이 간다. **[2026-08-21]** 여기 있던 "억제 장치 때문에 `Gate`보다 뒤"라는 순서 제약은 **없어졌다** — 억제가 `Effect` 내부 플래그로 확정돼 `Gate`에 안 걸린다. diff --git a/.claude/base/gate-plan.md b/.claude/base/gate-plan.md index 832c4b3..b6d3561 100644 --- a/.claude/base/gate-plan.md +++ b/.claude/base/gate-plan.md @@ -4,8 +4,9 @@ 메커니즘을 `state:Gate(setup)` **메소드**로 확정했다 — *"Gate 는 따로 프리미티브 없이 `state:Gate( (emit) -> ()->() )` 처럼 선언되고 마치 Compute 처럼 GateNode(ComputeNode 처럼) 생성된다 그리고 Blocker 는 해당 내부 배선을 따른다 -← 동의합니다 해당 방법대로 확정하면 됩니다."* 구현은 **M2**("게이팅 먼저" -결정, `ROADMAP.md`). **남은 것은 아래 "아직 안 정한 것"의 생명주기와 M2 범위뿐이고, 둘 다 +← 동의합니다 해당 방법대로 확정하면 됩니다."* 구현은 **M2**(반응형 코어 — +**[2026-08-24 정정]** 2026-08-22에 디스패치 쪽으로 앞당겼다가 마일스톤 순서 +교체로 되돌아옴, `ROADMAP.md`). **남은 것은 아래 "아직 안 정한 것"의 생명주기와 마일스톤 범위뿐이고, 둘 다 구현 시 정하면 되는 것들이다 — 사용자 판단이 필요한 항목은 없다** — `/code-review high`가 잡았던 4번(유보된 emit이 싣는 출처)은 같은 날 흡수 집합으로 닫혔고, **`setup` 시그니처는 안 바뀌었다.** (**[2026-08-22 표기 정정]** 여기와 4번 제목에 `emit(self)`라 적혀 있었으나 @@ -35,18 +36,19 @@ emit을 가로채 내려보낼지 말지를 정책이 정하는** 노드를 **`s ## 왜 지금인가 — 두 갈래가 같은 자리를 가리켰다 1. **`CR-3`(마일스톤 순서)** — `Dispatch.drive`의 배치 등록이 Blocker 게이팅을 - 전제하므로 M2가 M3의 `Blocker.luau`에 구조적으로 의존한다. 사용자 결정: + 전제하므로 M3가 M2의 `Blocker.luau`에 구조적으로 의존한다. 사용자 결정: **게이팅을 먼저 만든다.** (이건 그 결정에 이르게 된 *당시* 상태 서술이다 — - **[2026-08-22] 지금은 `Blocker.luau` 자체가 M2에 있다**, 아래 9번. - **⚠️ 다만 그걸로 순환이 닫힌 건 아니다** — `state:Gate`는 State 메소드이고 - `GateNode`/`Blocker`는 State 위에 얹히므로, M2로 옮겨도 M2→M3 참조는 - 그대로 남는다. 그 사실은 `.claude/question.md` 2번이 별도 미결로 다룬다.) + **[2026-08-24] 지금은 `Blocker.luau`가 M2(반응형 코어)에 있고 그 M2가 + 먼저 지어진다**, 아래 9번. 2026-08-22에 잠깐 디스패치 쪽으로 옮겼던 것은 + 마일스톤 순서 교체로 되돌려졌다 — `state:Gate`가 State 메소드이고 + `GateNode`/`Blocker`가 State 위에 얹히는 이상 게이팅을 State보다 먼저 둘 + 수는 없었고, 그래서 **반응형 전체를 앞으로 옮기는** 쪽으로 풀렸다.) 2. **`DT-4`(Debounce/Throttle)** — 공개 `Blocker` API 위에는 시간 기반 게이트를 못 얹는다. `base/debounce-throttle-plan.md`가 이미 "게이티드 노드를 내부 공용 `Gate`로 일반화하고 그 위에 정책을 얹으라"고 권고해뒀고, Blocker를 만들 때 같이 해두지 않으면 같은 설계를 두 번 하게 된다(항목 1과 마찬가지로 - **당시** 서술은 "M3에서 Blocker를 만들 때"였다 — **[2026-08-22]** 지금은 - `Blocker.luau`도 M2다, 아래 9번). + **당시** 서술은 "반응형 마일스톤에서 Blocker를 만들 때"였다 — + **[2026-08-24]** 지금은 `Blocker.luau`도 M2다, 아래 9번). **사용자 논거(`DT-4`)** — Blocker + Observer 조합으로는 왜 안 되는가: *"스로틀/디바운싱에 Observer 를 걸어야하는데, 이 옵저버의 emit 이 먼저이냐 @@ -330,17 +332,17 @@ end) - **따름정리: `Effect(fn, ...deps)`의 설치 구간 억제는 `Gate` 소비자가 아니다** — 아래 7번 참고. -9. **M2 범위 — [2026-08-22 해소] `Gate`와 `Blocker` 둘 다 M2다.** - `Dispatch.drive`의 배치 등록이 실제로 쓰는 건 +9. **마일스톤 범위 — [2026-08-24 재확정] `Gate`와 `Blocker` 둘 다 M2(반응형 + 코어)다.** `Dispatch.drive`의 배치 등록이 실제로 쓰는 건 `blocker:On()`/`OffWithoutEmit()`/`IsOn()`이므로(배치 게이팅 절) 최소한 - 그 세 메서드가 도는 형태까지는 M2에 필요한데, 그렇다고 `Blocker`만 - M3에 남겨두면 M2가 다시 뒤 마일스톤을 참조하게 된다. 같은 날 `ROADMAP.md` - 전반 점검에서 `EpochMap.luau`/`GateNode`/`Blocker.luau` **체크박스가 전부 - M2로 이동**했다(사용자 판단). `Blocker`는 `GateNode`를 다시 만들지 말고 - 그 위의 정책으로 얹을 것. - **⚠️ 이 이동이 M2↔M3 의존을 없애지는 않는다** — 셋 다 State 위에 - 얹히므로 M2는 여전히 `Source.luau`/`State.luau`를 필요로 한다. 마일스톤 - 경계를 어떻게 그을지는 `.claude/question.md` 2번(사용자 회신 대기). + 그 세 메서드가 도는 형태까지는 M3(디스패치)가 요구한다. 2026-08-22엔 + 그래서 `EpochMap.luau`/`GateNode`/`Blocker.luau` 체크박스를 디스패치 + 쪽으로 **앞당겼는데**, 셋 다 State 위에 얹히는 이상 그걸로는 순환이 안 + 닫혔다 — 디스패치가 여전히 `Source.luau`/`State.luau`를 필요로 했다. + **2026-08-24에 마일스톤 순서 자체를 교체**(반응형이 M2, 디스패치가 M3)해 + 그 앞당김은 되돌려졌고, 셋은 반응형 쪽으로 복귀했다. 결과적으로 + "게이팅 먼저"는 그대로 지켜진다 — 게이팅이 디스패치보다 먼저 지어진다. + `Blocker`는 `GateNode`를 다시 만들지 말고 그 위의 정책으로 얹을 것. ## 관련 문서 @@ -348,5 +350,5 @@ end) - `base/debounce-throttle-plan.md` — "공개 `Blocker` API 위엔 못 얹음" 절이 `Gate` 일반화를 처음 권고한 자리, 그리고 정책 쪽 설계 전량. - `base/dispatch-core-plan.md` — "배치 등록을 안전하게 만드는 Blocker 게이팅" - 절이 M2가 실제로 요구하는 표면. + 절이 M3가 실제로 요구하는 표면. - `ROADMAP.md` M2/M3. diff --git a/.claude/base/project-setup-plan.md b/.claude/base/project-setup-plan.md index 5c733ec..880f877 100644 --- a/.claude/base/project-setup-plan.md +++ b/.claude/base/project-setup-plan.md @@ -8,7 +8,7 @@ 설치·`pesde install` 실행으로 검증]** **전제**: 이 문서가 서술하는 건 **M0/M1 스캐폴딩 단계에서 확인된 사실**이지 -M3 이후 실제 구현이 아님 — `quad-base/src`는 아직 `Relate.luau`/골격 +M2 이후 실제 구현이 아님 — `quad-base/src`는 아직 `Relate.luau`/골격 `New()`/`Debug` 서브시스템뿐이고 `quad-roblox/src`는 비어 있음 (`.claude/todos.md`가 여전히 진행 상황의 소스). 여기 적힌 require/pesde 규칙은 실제 소스가 늘어나도 안 바뀔 구조적 사실이라 base로 승격했지만, diff --git a/.claude/base/quad-types-plan.md b/.claude/base/quad-types-plan.md index 99814b9..0b8065f 100644 --- a/.claude/base/quad-types-plan.md +++ b/.claude/base/quad-types-plan.md @@ -96,16 +96,20 @@ local quad = New() quad.Dispatch.addHandler(function() end) ``` `luau-analyze` → `TypeError: Key 'Dispatch' not found in table 'Quad'`. -즉 M2가 `Dispatch.luau`를 만들고 `module:RunInit(InitDispatch)` 패턴(이 문서가 +즉 M3가 `Dispatch.luau`를 만들고 `module:RunInit(InitDispatch)` 패턴(이 문서가 이미 예시로 보여준 그 패턴)으로 붙이면 **런타임엔 붙지만 타입엔 영원히 안 보인다.** `ROADMAP.md` M5의 quad-roblox 주입 경로도 같은 벽에 부딪힌다 — quad-roblox는 `quad-types`의 좁은 `Quad`만 본다. **확정(사용자, 2026-08-24): `quad-types`의 `Quad`를 마일스톤마다 갱신한다.** -- **M2**가 `Dispatch: Dispatch` 필드와 그 타입 재수출을 여기 추가한다 — - `ROADMAP.md` M2 체크리스트에 **항목으로 명시**한다(지금까지 아무도 이 - 필요성을 항목화해두지 않았다). -- 이후 서브시스템도 같은 규칙을 따른다. +- 규칙이 쓰인 계기는 `Dispatch`이고, **M3**가 `Dispatch: Dispatch` 필드와 + 그 타입 재수출을 여기 추가한다 — `ROADMAP.md` M3 체크리스트에 **항목으로 + 명시**한다(지금까지 아무도 이 필요성을 항목화해두지 않았다). +- **[2026-08-24 정정] 다만 규칙이 *처음 적용되는* 마일스톤은 M2다** — + 마일스톤 순서 교체로 반응형 코어가 앞에 오면서, `Source`/`State`/`Store` + 필드 추가가 `Dispatch`보다 먼저 온다(`ROADMAP.md` M2의 `H-25` 파생 항목). +- 이후 서브시스템도 같은 규칙을 따른다 — 서브시스템을 붙이는 **모든** + 마일스톤(M2 · M3 · M6 · M7 · M8 · M10)이 같은 항목을 진다. - **"가벼운 타입 계약"이라는 이 패키지의 존재 이유와 상충하지 않는다** — 타입만 재수출하므로 런타임 무게는 안 는다. - 검토했다 기각된 둘: **quad-base 내부만 넓은 로컬 교차 타입** diff --git a/.claude/base/slot-plan.md b/.claude/base/slot-plan.md index 7dcb0c4..9cb6e19 100644 --- a/.claude/base/slot-plan.md +++ b/.claude/base/slot-plan.md @@ -3383,7 +3383,7 @@ state 가 안전히 성립 못해서, Apply 라는 이름을 그대로 쓰지는 - **확정 형태**: `source:SetAndDispose(value)` — `:Set(value)`와 **한 세트로 묶인 `Source` 전용 콜론 메서드**. `Set`(언마운트) → 옛 값 `dispose` 순서를 안에서 수행하므로 호출부가 `Get()`으로 옛 값을 미리 잡아둘 필요가 없다. -- **`state:Apply`는 손대지 않는다** — 시그니처 영향이 없으므로 M3 착수 전 +- **`state:Apply`는 손대지 않는다** — 시그니처 영향이 없으므로 M2 착수 전 결론이 필요하던 항목에서 빠진다(`question.md`/`.claude/todos.md`에서 제거). #### 구현상 바뀌어야 하는 것 diff --git a/.claude/base/source-state-plan.md b/.claude/base/source-state-plan.md index 806b6ae..cb9b74b 100644 --- a/.claude/base/source-state-plan.md +++ b/.claude/base/source-state-plan.md @@ -314,7 +314,7 @@ Observer와 동일한 패턴(외부 weak table, `{[child] = true}` 류)으로 유일한 중복 방지 수단이 되면서 "State는 캐싱하는 존재"라는 근거가 더 강해짐. -### ⚠️ 미해결 — 중간 State가 살아남는가(구독 엣지의 방향성) (2026-08-18 구현 전 QA에서 제기, **M3 착수 전 결론 필요**) +### ⚠️ 미해결 — 중간 State가 살아남는가(구독 엣지의 방향성) (2026-08-18 구현 전 QA에서 제기, **M2 착수 전 결론 필요**) **사용자가 지목한 미검증 항목**: *"확인해봐야 하는게 State -> State -> State -> Observer Leaf Bind 에서 중간 State 는 참조되지 않아도 사라지지 @@ -343,7 +343,7 @@ With 등이 있는 경우 parent 와 연결된 상대를 자기 자신에 가지 **해야 할 일**: (a) 이 방향성(상류 strong / 하류 weak)을 이 문서의 불변식으로 명문화할지 결정, (b) `luau-test`에 실측 스파이크 추가 (`07-relate-weak-table-gc.luau`가 연쇄 GC를 이미 다루므로 그 옆에). -**미검증 상태로 M3에 착수하면 안 되는 항목** — 아래 "결론"의 "관리 부담은 +**미검증 상태로 M2에 착수하면 안 되는 항목** — 아래 "결론"의 "관리 부담은 작음"은 이 항목이 닫히기 전까지는 잠정이다. **결론**: 노드별 캐시 유지(현재 모델) 유지, 플래튼 기각. Modifier가 @@ -389,11 +389,12 @@ lazy State 핸들로 통일, 아래 "`:With`/`:Compute` — self 인자도 lazy `Blocker` 참고.** 위 `:With`+`:Compute`만으로는 "state1, state2를 연달아 Set하면 결합된 파생값이 두 번 재계산/재대입된다"는 문제(즉시 pull하는 store-bind 소비자 기준)는 안 풀림 — 이건 별도 확정 프리미티브 -`base/blocker-plan.md`가 다룸(**[2026-08-22 정정]** 여기 "State 개발과 같은 -마일스톤, `ROADMAP.md` M3에서 함께 구현"이라 적혀 있었으나 `Blocker.luau`는 -**M2**로 이동했고, 바닥부터 짜는 게 아니라 공용 `GateNode` -(`base/gate-plan.md`) 위의 정책이다 — 마일스톤 소속의 소스는 -`blocker-plan.md`의 정정 배너와 `ROADMAP.md` M2). lexical `Batch(fn)`으로 풀려던 +`base/blocker-plan.md`가 다룸(**[2026-08-24 재확정]** "State 개발과 같은 +마일스톤, `ROADMAP.md` M2에서 함께 구현"이 맞다 — 2026-08-22엔 `Blocker.luau`가 +디스패치 쪽으로 앞당겨져 갈라져 있었으나 마일스톤 순서 교체로 되돌아왔다. +다만 바닥부터 짜는 게 아니라 공용 `GateNode`(`base/gate-plan.md`) 위의 +정책이라는 점은 그대로 — 마일스톤 소속의 소스는 `blocker-plan.md`의 정정 +배너와 `ROADMAP.md` M2). lexical `Batch(fn)`으로 풀려던 초기 시도는 코루틴 yield 위에서 구조적으로 위험해 기각됨 — `archive/batch-rejected.md` 참고. @@ -665,7 +666,7 @@ deps만 받고 싶어도 `previous`가 2번째 자리를 차지하므로, 그 제약상 다른 선택지가 없음(대안은 애초에 이 확장 자체를 안 하는 것뿐). **실측 필요 — `luau-test`의 `15-type-compute-trailing-deps-typepack.luau` -신규(ROADMAP.md M3 반영).** 순서 문제 자체는 위 정정으로 구조적으로 +신규(ROADMAP.md M2 반영).** 순서 문제 자체는 위 정정으로 구조적으로 풀렸으므로, 스파이크가 실제로 확인할 진짜 불확실성은 (B) 하나로 좁혀짐 — 나머지는 그 결론을 뒷받침하는 대조군: (A) 균일 타입 dep 1개를 고정 인자로 좁히는 대조군(실패하면 B/C/D를 볼 것도 없이 기반 자체가 문제), @@ -1014,6 +1015,13 @@ retract/Destroy되면 자동으로 정리됨. ### 동적 경로 가드 — `k` 무관 매치, `HANDLER_PRIORITY_FALLBACK` (2026-08-14 열한 번째 세션, `PreRef`의 동적 경로 가드와 같은 패턴.) +**⚠️ [2026-08-24] 이 가드를 실제로 `Dispatch.addHandler`로 등록하는 것은 +M3(디스패치)다.** `HANDLER_PRIORITY_FALLBACK` 상수도 `Dispatch.addHandler`도 +M3에서 처음 생기므로, M2(반응형 코어)에서 본체를 짤 때는 **핸들러 정의만 +준비해두고 등록 호출은 미룬다** — `ROADMAP.md` M3의 "Observer/Effect 동적 +경로 가드 등록" 체크박스가 그 자리다(2026-08-24 마일스톤 순서 교체의 산물, +M2가 M3에 개념상 지던 유일한 의존이라 이쪽으로 미뤄졌다). + `Observer`도 children 배열 리터럴 전용이라, 해시 파트 named 자리 등으로 동적으로 흘러들어오면(타입 우회 버그) 명확히 에러내야 함 — 전용 `Handler` 등록: `{ priority = HANDLER_PRIORITY_FALLBACK, diff --git a/.claude/base/state-epoch-plan.md b/.claude/base/state-epoch-plan.md index ec2e405..49bb2cc 100644 --- a/.claude/base/state-epoch-plan.md +++ b/.claude/base/state-epoch-plan.md @@ -3,11 +3,13 @@ **상태**: **확정.** 사용자 제안으로 시작해 같은 날 여러 라운드에 걸쳐 다듬은 뒤 **채택 확정**됨 — *"gate 와 epoch 가 제가 만족할만한 정도로 올라왔습니다. 채택하면 될것 같아요."* -**구현 마일스톤은 둘로 갈린다** — **[2026-08-22 정정]** 여기 "구현은 M3"라고만 -적혀 있었다. 부기 객체 `EpochMap.luau`와 `Epoch` 인터페이스·리비전 갱신 -규칙은 **M2**(`GateNode`가 `emitEpochMap`을 쓰므로 — `ROADMAP.md` M2), -그걸 `valueEpochMap`/`emitEpochMap` 둘로 컴포지션하는 **State 본체 통합은 -M3**다. +**구현 마일스톤은 전부 M2**(반응형 코어)다 — 부기 객체 `EpochMap.luau`, +`Epoch` 인터페이스·리비전 갱신 규칙, 그걸 `valueEpochMap`/`emitEpochMap` +둘로 컴포지션하는 State 본체 통합이 전부 한 마일스톤 안이다. +**[2026-08-24 재확정]** 2026-08-22엔 `GateNode`가 디스패치 쪽에 있어서 +`EpochMap`/`Epoch`만 그리 앞당겨져 **둘로 갈려 있었는데**, 마일스톤 순서 +교체(`ROADMAP.md`의 M2 배너)로 `GateNode`가 반응형으로 돌아오면서 그 분리 +자체가 없어졌다. **⚠️ 이 문서는 `base/source-state-plan.md`의 "전파 모델 확정" 절을 대체하는 게 아니라 그 절이 정한 push-invalidate/pull-recompute 위에 **판정 규칙**을 diff --git a/.claude/base/store-plan.md b/.claude/base/store-plan.md index 3bba40b..9d3062c 100644 --- a/.claude/base/store-plan.md +++ b/.claude/base/store-plan.md @@ -175,7 +175,7 @@ Store는 "이름 붙은 Source 모음, 그 이상 아님"으로 더 단순해짐 탑레벨"이라는 기존 네이밍 규칙(`base/architecture.md`의 "코드 스타일 — 네이밍 케이싱")에도 오히려 더 맞는다. 사용자가 지정한 표기는 `GetDynamic(name)`이므로 **일단 콜론 메소드 + 예약 키로 적어두되, - M3/M4 구현 전에 어느 쪽인지 확인할 것**(`question.md` 3번). + M2/M4 구현 전에 어느 쪽인지 확인할 것**(`question.md` 최우선 절). - 이 패턴은 Store에만 국한되지 않고 **인스턴스 생성까지 관통하는 프로젝트 전역 관습으로 확정**됨 — 단 이벤트는 이후 4차 라운드에서 이 관습의 **유일한 예외**로 빠졌음(PA님 방식인 문자열 키+런타임 리플렉션으로 전환). @@ -216,8 +216,8 @@ end (flatten) 익명 타입이지만, **Luau는 이름이 아니라 "만족하는가"로 구조적 일치를 검사**하므로 문제없이 `Source` 자리에 대입 가능 — 오히려 이 방식과 정확히 맞는 조합. 이걸로 `store.key`가 실제로 타입 명시 가능함이 -확인돼 M0/M3 어느 시점에 검증해도 기술적으로 막힐 위험은 없음 — -`ROADMAP.md`의 M0/M3 배치를 강제로 바꿀 필요는 없어짐, 설계 레벨의 검증 +확인돼 M0/M2 어느 시점에 검증해도 기술적으로 막힐 위험은 없음 — +`ROADMAP.md`의 M0/M2 배치를 강제로 바꿀 필요는 없어짐, 설계 레벨의 검증 난이도 문제였던 것만 해소. **[2026-08-15] 이 `type function` 접근 자체의 실측도 완료** — 스파이크(`luau-test/done/16-type-store-key- typefunction.luau`)는 원래 `types.newfunction` 시그니처 불일치로 깨져 diff --git a/.claude/luau-test/STATUS.md b/.claude/luau-test/STATUS.md index aeec802..8b5c180 100644 --- a/.claude/luau-test/STATUS.md +++ b/.claude/luau-test/STATUS.md @@ -207,5 +207,5 @@ inst 5개만 살린 상태 → 살아남은 payload 5 / 엔트리 5 (기대치 | 검증할 것 | 왜 | 출처 | |---|---|---| | ~~`table.insert`가 배열 중간의 구멍을 재사용하는가~~ **[2026-08-24 폐기]** | **전제 자체가 없어졌다** — 6라운드 `H-7`로 `Ref.Callbacks`가 배열에서 `{[callback\|thread] = true}` **해시맵 셋**으로 바뀌었고, 해시맵엔 border 개념도 구멍도 없다(`base/ref-plan.md`). 이 스파이크는 만들지 말 것 | QA 4라운드 `R-11`(폐기), 6라운드 `H-7` | -| 중간 State가 상류 strong / 하류 weak 불변식으로 실제로 살아남는가 | `State → State → State → Observer` 체인에서 중간 노드를 강하게 붙잡는 주체가 문서 어디에도 없어 전파가 조용히 끊길 수 있음. **M3 착수 전 필요** | `base/source-state-plan.md`의 "미해결 — 중간 State가 살아남는가" 절, `question.md` 3번 | +| 중간 State가 상류 strong / 하류 weak 불변식으로 실제로 살아남는가 | `State → State → State → Observer` 체인에서 중간 노드를 강하게 붙잡는 주체가 문서 어디에도 없어 전파가 조용히 끊길 수 있음. **M2 착수 전 필요** | `base/source-state-plan.md`의 "미해결 — 중간 State가 살아남는가" 절, `question.md` 최우선 절 | | `Visible = false`인 GuiObject의 `AbsoluteSize`/`AbsolutePosition`이 갱신되는가 | `quad-roblox-fastscroll` 설계의 선행 실측. **Studio 필요** — 만들면 `not-run/`행 | `research/fastscroll-plan.md` | diff --git a/.claude/project-context.md b/.claude/project-context.md index 0e5183e..a1dee89 100644 --- a/.claude/project-context.md +++ b/.claude/project-context.md @@ -10,12 +10,17 @@ Roblox 엔진에서 동작하는 DOMless UI 렌더러 **quad**를 처음부터 지속 가능성 — 빠른 이터레이션보다 정확성/설계 정합성이 우선. 작업 기간은 길게 잡음. -**[2026-08-22 기준] M0(스파이크 검증)/M1(스캐폴딩) 완료, 다음은 M2(디스패치 -엔진)**(마일스톤이 넘어갈 때 루트 `CLAUDE.md` 머리말도 -같이 고칠 것 — 같은 상태를 두 곳이 서술하고 있음). **⚠️ 단 M2 착수를 막는 -순서 문제가 하나 열려 있음** — M2↔M3 양방향 의존, 소스는 -`.claude/question.md` 2번(설계 결정이 아니라 마일스톤 경계 문제라 -"설계 게이트는 없다"는 아래·`todos.md` 서술과 모순되지 않음). 저장소 루트에 +**[2026-08-24 기준] M0(스파이크 검증)/M1(스캐폴딩) 완료, 다음은 M2(반응형 +코어 — Source/State/Store)**(마일스톤이 넘어갈 때 루트 `CLAUDE.md` 머리말도 +같이 고칠 것 — 같은 상태를 두 곳이 서술하고 있음). **⚠️ [2026-08-24] M2와 +M3의 번호·순서가 맞바뀌었다** — 열려 있던 마일스톤 순서 문제가 (a) 순서 +교체로 닫힌 결과다(경위는 `archive/question-resolved.md`의 "마일스톤 경계" +절, 새 구성은 `ROADMAP.md`의 M2 배너). **2026-08-24 이전에 쓰인 +`session/`·`archive/`·`qa-request/`의 `M2`/`M3`는 옛 의미**(M2=디스패치, +M3=반응형)다. 그 교체의 부작용으로 **M2 착수 전에 답이 필요한 항목이 +`question.md` 최우선 절로 올라왔다**(무엇이 몇 개인지는 그 절이 소스 — +여기서 세지 않는다. 설계 게이트가 아니라 실측·표면 선택이라, 설계 게이트가 +없다는 아래와 `todos.md`의 서술과 모순되지 않음). 저장소 루트에 `quad-base/src/`(`New()`/`RunInit`/`AddPlugin`/`Relate`/`Debug`)/ `quad-types/src/`/`type-version-check/src/`가 실제로 존재(`quad-roblox/src`는 아직 빈 폴더 — M5에서 채워짐), 자세한 진행 상황은 루트 `ROADMAP.md`가 diff --git a/.claude/question.md b/.claude/question.md index f15eb19..b300a88 100644 --- a/.claude/question.md +++ b/.claude/question.md @@ -12,18 +12,42 @@ --- -## ⭐ 최우선 — 설계 결정은 **없음**, 다만 순서 문제 하나 (2026-08-22 갱신) +## ⭐ 최우선 — **M2(반응형 코어) 착수 전에 답이 필요한 것 둘** (2026-08-24 갱신) -> **[2026-08-22] 아래 2번(M2/M3 마일스톤 경계)이 M2 착수를 막습니다.** -> 그건 "무엇을 확정할까"가 아니라 "어떤 순서로 짤까"라 성격이 달라서 -> 이 절이 아니라 2번에 뒀습니다 — **설계 결정 대기는 여전히 0건**입니다. +> **[2026-08-24] M2/M3 마일스톤 경계 문제는 닫혔습니다.** 그건 설계 결정이 +> 아니라 순서 문제라 이 절이 아니라 별도 항목으로 뒀었는데, 사용자가 +> **(a) 순서 교체**(반응형이 M2, 디스패치가 M3)를 선택해 같은 날 전량 +> 반영됐습니다 — 결정과 근거는 `archive/question-resolved.md`의 +> "마일스톤 경계" 절, 새 마일스톤 구성은 `ROADMAP.md`의 M2 배너. +> +> **⚠️ 다만 그 교체의 부작용으로 아래 둘이 "한 마일스톤 뒤"에서 +> "바로 다음"으로 올라왔습니다.** 둘 다 반응형(옛 M3, 지금 M2)의 게이트라 +> 예전엔 급하지 않았는데, 반응형이 먼저 지어지게 되면서 **지금 답이 +> 필요합니다.** 순수한 *설계* 결정 대기는 여전히 0건입니다 — 하나는 실측 +> 미완이고 하나는 표면 위치 선택입니다. + +- **[신설, 2026-08-18 구현 전 QA] 중간 State GC 미검증** — `State → State → + State → Observer` 체인에서 중간 노드를 강하게 붙잡는 주체가 문서 어디에도 + 없어 전파가 조용히 끊길 수 있음. 방향(상류 strong / 하류 weak)은 사용자가 + 지목했고, **명문화 여부 결정 + `luau-test` 실측이 M2 착수 전에 필요** — + `base/source-state-plan.md`의 "미해결 — 중간 State가 살아남는가" 절. +- **[신설, 2026-08-18 커밋 전 `/code-review high`] `store:GetDynamic`을 + 콜론 메소드로 둘지, 탑레벨 함수로 둘지** — 콜론 메소드로 두면 Store의 + lazy `__index`(없는 키를 인덱싱하면 그 자리에서 `Source`를 만들어 저장)와 + 부딪혀서, `__index`가 고정 메소드 테이블을 먼저 확인해야 하고 그 결과 + **`GetDynamic`이 모든 Store의 예약 키 이름**이 된다(그 이름의 Source는 + dot-access로 못 만듦). Store 키는 사용자 도메인 데이터 이름이라 충돌 + 확률이 `Modifier`의 예약 이름들보다 높다. 대안은 탑레벨 + `getDynamic(store, name)` — "특정 프리미티브에 안 묶인 범용 유틸은 소문자 + 탑레벨"이라는 기존 네이밍 규칙에는 오히려 더 맞는다. **M2/M4 착수 전 + 필요**, `base/store-plan.md`의 "타입 추론 문제" 절. > **M0 착수를 막던 항목이 전부 해소됐습니다.** `0-Y`(`:Compute(fn)`의 > lazy 핸들 계약)는 열세 번째 세션에, `0-Z`(Attribute 이름 소유권)와 > `0-A`(재디스패치 하강 diff)는 열네 번째 세션에, **`0-W`(`Ref` 이중 > 배치 방지)는 2026-08-14 열한 번째 세션에 확정·`base/` 반영 완료** — -> 그래서 이 문서엔 이제 "결정 대기" 절 자체가 없음(비어서 헤딩째로 -> 삭제). 해소 전 원문과 결론은 +> 그래서 이 문서의 "결정 대기" 절은 한동안 비어 있었음(위 두 항목이 +> 2026-08-24 순서 교체로 올라오기 전까지). 해소 전 원문과 결론은 > `archive/question-resolved.md`, 뒤집힌 옛 재디스패치 모델은 > `archive/dispatch-hintvalue-model-reversed.md`. > @@ -115,42 +139,7 @@ - `Store`/`Source`/`Modifier`/`process`/`retract`/`isHandlable`은 업계 선례와 잘 맞거나 이미 신중하게 결정된 이름들이라 특별한 문제 없음. -## 2. M2/M3 마일스톤 경계 — **M2 착수 전에 답이 필요** (2026-08-22 신설) - -**M2(디스패치 엔진)와 M3(Store/State/Source)의 의존이 양방향이라, -`ROADMAP.md` 순서대로면 M2를 끝까지 짤 수 없습니다.** - -- **M2 → M3 (본체 의존)**: `Dispatch.setLength`가 - `len: number | State`를, `Dispatch.setOffsetSource`가 - `Source`를 받고, `recompute`가 `offset:Set()`을 부릅니다 - (`base/dispatch-core-plan.md`의 "Length/Offset" 절). 2026-08-22에 M2로 - 옮긴 `GateNode`/`Blocker`도 State 위에 얹힙니다. 즉 **M2는 - `Source.luau`/`State.luau` 없이는 구현이 안 됩니다.** -- **M3 → M2 (얕은 의존)**: `state:Observer`/`Effect`의 동적 경로 가드가 - `Dispatch.addHandler` + `Handler.luau` 계약을 씁니다 — 이건 레지스트리 - 등록 표면만 있으면 되므로 M2 **전체**를 요구하지 않습니다. - **⚠️ 다만 "M2 앞머리 두 항목(`Dispatch/init.luau` + `Handler.luau`)이면 - 된다"고는 말할 수 없습니다** — `Dispatch/init.luau`에는 `Dispatch.drive`가 - 들어 있고, `dispatch-core-plan.md`가 *"적용 지점 — `Dispatch.drive`와 - `attachSlot`, 각각 자기 owner 키로 별도 Blocker"*라고 확정해 `drive`도 - 게이팅을 씁니다. 즉 그 첫 항목 자체가 State-free가 아닙니다. 선택지 (b)로 - 쪼갠다면 `drive`를 어느 쪽에 두느냐가 경계선이 됩니다. - -**선택지**: -- **(a) M2와 M3의 순서를 바꾼다** — 반응형(Source/State/EpochMap/Gate/ - Blocker)을 먼저 짜고 그 위에 디스패치를 올림. 얕은 쪽(가드 Handler)만 - 뒤로 미루면 됨. 지금까지의 결정 흐름("게이팅 먼저")과 방향이 같음. -- **(b) M2를 둘로 쪼갠다** — `Dispatch.getHandler`/`process`/`retractFrom`/ - `Handler`/`Brand`/`Relate`/`chains`까지가 M2a, State가 필요한 - Length/Offset·게이팅은 M3 뒤의 M2b로. -- **(c) 지금 구조를 두고 구현 시 알아서 오간다** — 로드맵은 "순서"가 - 아니라 "묶음"으로만 읽음. - -**[2026-08-22 기준] 이 항목이 M2 착수를 막습니다** — `.claude/todos.md` -0번이 "M2 착수를 막는 설계 항목은 없다"고 하는 것은 **설계** 얘기이고, -이건 설계가 아니라 **순서** 문제라 별개입니다. - -## 3. 낮은 우선순위 — 열려 있지만 급하지 않음 +## 2. 낮은 우선순위 — 열려 있지만 급하지 않음 - **`Operator` 콤비네이터 슈가 네임스페이스 이름+포함 범위(2026-08-12 신설, 같은 날 후속으로 외부 리서치 완료)** — `Sum`/`Product`/`Not`/비트연산 등 @@ -176,21 +165,6 @@ 남은 근거는 편의성과 Slot offset이 밀리고 당겨지는 케이스뿐이라 우선순위가 더 내려감 — `state:Flatten()`류 콤비네이터 아이디어는 그대로 백로그. 상세는 `research/operator-sugar-plan.md` 마지막 절. -- **[신설, 2026-08-18 커밋 전 `/code-review high`] `store:GetDynamic`을 - 콜론 메소드로 둘지, 탑레벨 함수로 둘지** — 콜론 메소드로 두면 Store의 - lazy `__index`(없는 키를 인덱싱하면 그 자리에서 `Source`를 만들어 저장)와 - 부딪혀서, `__index`가 고정 메소드 테이블을 먼저 확인해야 하고 그 결과 - **`GetDynamic`이 모든 Store의 예약 키 이름**이 된다(그 이름의 Source는 - dot-access로 못 만듦). Store 키는 사용자 도메인 데이터 이름이라 충돌 - 확률이 `Modifier`의 예약 이름들보다 높다. 대안은 탑레벨 - `getDynamic(store, name)` — "특정 프리미티브에 안 묶인 범용 유틸은 소문자 - 탑레벨"이라는 기존 네이밍 규칙에는 오히려 더 맞는다. **M3/M4 착수 전 - 필요**, `base/store-plan.md`의 "타입 추론 문제" 절. -- **[신설, 2026-08-18 구현 전 QA] 중간 State GC 미검증** — `State → State → - State → Observer` 체인에서 중간 노드를 강하게 붙잡는 주체가 문서 어디에도 - 없어 전파가 조용히 끊길 수 있음. 방향(상류 strong / 하류 weak)은 사용자가 - 지목했고, **명문화 여부 결정 + `luau-test` 실측이 M3 착수 전에 필요** — - `base/source-state-plan.md`의 "미해결 — 중간 State가 살아남는가" 절. - **[신설, 2026-08-14 리뷰] `AttributeGroupHandler.process`의 부분 실패 롤백** — 이름 순회 도중 소유권 충돌 error가 나면 그 전에 등록된 이름들이 이 사이클엔 회수되지 않음(클로저가 안 만들어짐). 피해는 그 인스턴스 @@ -205,8 +179,8 @@ 읽는 게 quad 관습"이라는 언급은 2026-08-06 후속 세션에서 해소 — 채택 안 함으로 확정, `base/event-plan.md` "이벤트 핸들러는 self(Instance)를 받지 않는다" 절 참고). 사용자가 "quad 개발 완료 전엔 - 착수 못 함"으로 직접 후순위 지정한 건 여전함 — base 설계(M2 Dispatch/ - M3 Source/M5 `D` 생성자) 시점에 훅 확장 지점만 고려해두면 됨. + 착수 못 함"으로 직접 후순위 지정한 건 여전함 — base 설계(M3 Dispatch/ + M2 Source/M5 `D` 생성자) 시점에 훅 확장 지점만 고려해두면 됨. - **문서화 전략(UI 네이밍 컨벤션, Store 부작용을 게임 시스템에서 쓰는 패턴)** — `research/documentation-plan.md`(뼈대만). 정식 백로그 항목으로 올릴지, 착수 시점을 언제로 볼지 사용자 판단 필요. @@ -228,9 +202,13 @@ Instance를 동적 배열 원소로 받을 수 있는지, retract 시 어떻게 다루는지. **Slot 코어 구현(M6) 시점에 확인** — `research/v1-compat-plan.md` 7-3. -> **없어진 번호에 대해**: 예전 "0번(추가 프리미티브)"과 "2번(구현 착수 -> 직전 감사 결과)"은 전원 해소되어 통째로 `archive/question-resolved.md`로 -> 갔음. 우선순위1 11개의 개별 상태가 궁금하면 +> **⚠️ 번호는 재사용된다 — 옛 문서가 가리키는 번호를 그대로 믿지 말 것.** +> 예전 "0번(추가 프리미티브)"과 "2번(구현 착수 직전 감사 결과)"은 전원 +> 해소되어 통째로 `archive/question-resolved.md`로 갔고, 그 뒤 **"2번"은 두 +> 번 더 다른 내용으로 재사용됐다** — 2026-08-22엔 "M2/M3 마일스톤 경계" +> (2026-08-24 해소·아카이브 이관), 지금은 바로 위 "낮은 우선순위" 절이다. +> 옛 세션/아카이브 문서가 `question.md` N번을 가리키면 **그 문서가 쓰인 +> 시점의 번호**로 읽을 것. 우선순위1 11개의 개별 상태가 궁금하면 > `research/pre-implementation-audit.md`가 원본이자 최신. --- diff --git a/.claude/research/debug-tooling-plan.md b/.claude/research/debug-tooling-plan.md index 24f07da..ae98e95 100644 --- a/.claude/research/debug-tooling-plan.md +++ b/.claude/research/debug-tooling-plan.md @@ -471,10 +471,10 @@ Tween mock 등 동적 동작 포함")와 목적이 다름: 설계 시 "고려는 해두되 지금 확정/구현하지는 않는" 참고용 메모로 남김 (`ROADMAP.md`의 M2/M3/M5 근처에 훅 확장 지점 존재 가능성만 인지해두는 정도): -- M2(디스패치 엔진) 구현 시 `process`/`retract` 스캔 루프에 나중에 훅 +- M3(디스패치 엔진) 구현 시 `process`/`retract` 스캔 루프에 나중에 훅 하나를 끼워 넣기 쉬운 모양으로 짜여 있는지 정도만 유의(지금 훅 자체를 만들 필요는 없음). -- M3(Source) 구현 시 마찬가지로 나중에 weak-registry 등록 훅을 끼우기 +- M2(Source) 구현 시 마찬가지로 나중에 weak-registry 등록 훅을 끼우기 쉬운 생성자 모양인지만 유의. - M5(quad-roblox `D` 제네릭 생성자) 구현 시 caller 정보를 나중에 끼워넣기 쉬운 단일 진입점(생성자 함수 하나)인지만 유의 — 이건 이미 diff --git a/.claude/research/operator-sugar-plan.md b/.claude/research/operator-sugar-plan.md index 3b94433..9781dd1 100644 --- a/.claude/research/operator-sugar-plan.md +++ b/.claude/research/operator-sugar-plan.md @@ -179,7 +179,7 @@ introspection 로직을 추가하는 거라 라이브러리 복잡도가 늘어 `modifier-plan.md` 8번 절), 네임스페이스 이름으로 쓰면 "이 특정 모듈"과 "패턴을 가리키는 일반 용어"가 헷갈릴 수 있어 후보에서 제외. -`.claude/question.md` 3번(낮은 우선순위)에도 반영. 용어 정리 라운드 +`.claude/question.md` 2번(낮은 우선순위)에도 반영. 용어 정리 라운드 (`question.md` 1번, `Brand`/`Tag`류)와 같은 카테고리로 나중에 같이 검토해도 됨 — 급하지 않음. diff --git a/.claude/research/pre-implementation-audit.md b/.claude/research/pre-implementation-audit.md index 04fa967..6b168ea 100644 --- a/.claude/research/pre-implementation-audit.md +++ b/.claude/research/pre-implementation-audit.md @@ -89,7 +89,7 @@ store.color }`처럼 애니메이션 없이 그냥 반응형으로 값만 바뀌 여러 단계(A→B→C)로 겹칠 때 자기 자신의 상태와 위임한 핸들러의 상태가 슬롯 하나를 두고 충돌하는 문제가 있어 기각되고, 대신 Dispatch 자신이 전체 체인을 배열로 들고 있는 쪽으로 정리됨. 상세는 `base/ -dispatch-core-plan.md` "Dispatch 체인" 절, `ROADMAP.md` M2/M4. 아래는 +dispatch-core-plan.md` "Dispatch 체인" 절, `ROADMAP.md` M3/M4. 아래는 원래 발견 당시 기록. **위치**: `base/dispatch-core-plan.md` "확정된 디스패치 모델" 절 90-91행 — @@ -107,7 +107,7 @@ dispatch-core-plan.md` "Dispatch 체인" 절, `ROADMAP.md` M2/M4. 아래는 **제안**: `Dispatch/StoreBind.luau`가 "마지막으로 선택된 핸들러" 자체를 `(inst, k)`별 상태로 들고 있다가, 새 `realv` 처리 전에 그 핸들러의 -`retract`를 호출하는 식으로 지금 결정해두는 게 좋아 보임 — M2/M4에서 바로 +`retract`를 호출하는 식으로 지금 결정해두는 게 좋아 보임 — M3/M4에서 바로 부딪힐 지점. ### 1-3. 우선순위 스캔의 동률 처리, 매치 실패 시 동작이 정의 안 됨 — [해소됨, 2026-08-12 열일곱 번째 세션] @@ -116,7 +116,7 @@ dispatch-core-plan.md` "Dispatch 체인" 절, `ROADMAP.md` M2/M4. 아래는 (`HANDLER_PRIORITY_HIGH` 등, "밴드+오프셋" 패턴)로 애초에 안 나게 유도, 매치 실패는 조용한 무시 없이 즉시 `error`(브랜드+`typeof` 출력, provider 초기화 확인 안내)로 확정. 핸들러 등록 시점 동률 감지 print 경고와 -`Dispatch.listHandlers()`류 전체 목록 조회 함수도 M2 기본 기능으로 +`Dispatch.listHandlers()`류 전체 목록 조회 함수도 M3 기본 기능으로 확정 — 상세는 `base/dispatch-core-plan.md` "우선순위 동률/매치 실패 처리" 절. 아래는 원래 발견 당시 기록. @@ -131,7 +131,7 @@ dispatch-core-plan.md` "Dispatch 체인" 절, `ROADMAP.md` M2/M4. 아래는 **제안**: 최소한 "매치 실패는 에러(silent 무시 금지)"만이라도 지금 결정해두면 구현 중 인터럽트를 막을 수 있음. 동률은 "등록 순서가 tiebreak" -정도로 명시만 해둬도 충분. M2(Dispatch 엔진) 착수 직전 확인. +정도로 명시만 해둬도 충분. M3(Dispatch 엔진) 착수 직전 확인. ### 1-4. provider(팩토리) 미주입 상태에서 dispatch가 호출되면 어떻게 되는지 세 번째 케이스가 빠짐 — [해소됨, 2026-08-12 열일곱 번째 세션] @@ -153,7 +153,7 @@ nil-index 크래시 vs 조용한 no-op)가 안 정해져 있음. **제안**: base dispatch 엔진이 "아직 provider 미주입" 상태를 감지해 명확한 에러를 던지도록 지금 결정해두면, 구현 중 흔한 초기화 순서 실수를 훨씬 덜 -헷갈리게 만들 수 있음. 1-2번과 같은 타이밍(M2)에 같이 확정. +헷갈리게 만들 수 있음. 1-2번과 같은 타이밍(M3)에 같이 확정. ### 1-5. `props.Modifier`/`props.Ref` forwarding 관례가 Lua 배열 리터럴의 nil-hole 함정에 그대로 노출됨 @@ -278,7 +278,8 @@ element별 weak-set) — throw 조건을 "Slot 핸들러의 `process`가 같은 **[2026-08-07 세 번째 세션 갱신 — 반영 완료.]** 아래 제안대로 `LifetimeHandle.luau`/`PerInstanceState.luau` 인터페이스가 `ROADMAP.md` -M2로 이동됐고, M8은 quad-roblox 실제 구현만 담당하도록 분리됨 — 더 이상 +M2로 이동됐고(**[2026-08-24]** 마일스톤 순서 교체로 반응형 코어 앞머리의 +"공통 기반" 절), M8은 quad-roblox 실제 구현만 담당하도록 분리됨 — 더 이상 열린 항목 아님, 아래는 원래 발견 당시 기록. **위치(당시)**: `ROADMAP.md` M8 "Ref" — `"LifetimeHandle 인터페이스 + quad-roblox @@ -287,40 +288,44 @@ M2로 이동됐고, M8은 quad-roblox 실제 구현만 담당하도록 분리됨 **문제**: `base/lifecycle-pattern.md`("생명 바인드 유틸"을 State-invalidate 리스너 클로저 등록에도 재사용)와 `base/slot-plan.md`(Slot의 `retract`가 같은 canExecute 패턴을 그대로 씀)는 둘 다 이 유틸을 State/Store 구독 -(M3/M4 영역)과 Slot(M6)에서 이미 쓴다고 명시하는데, `LifetimeHandle` +(M2/M4 영역)과 Slot(M6)에서 이미 쓴다고 명시하는데, `LifetimeHandle` 인터페이스 자체는 M8에서야 정의된다. 즉 M4/M6이 개념적으로 필요로 하는 base 인터페이스가 그보다 늦은 M8에서 만들어지는 순서 역전. -**제안**: `LifetimeHandle.luau`(quad-base, 인터페이스만)를 M2(Dispatch -엔진) 또는 M3(Store/State)로 옮기고, M8은 "quad-roblox 실제 구현(Instance +**제안**: `LifetimeHandle.luau`(quad-base, 인터페이스만)를 M3(Dispatch +엔진) 또는 M2(Store/State)로 옮기고, M8은 "quad-roblox 실제 구현(Instance `Connected` 기반)"만 담당하도록 분리. M1 mock에도 이 인터페이스의 트리비얼 스텁(항상 true)을 붙여두면 M4/M6 테스트가 자연스러워짐. -### 1-10. `store.key`의 레코드 필드 타이핑 검증이 M0가 아니라 M3로 밀려 있음 — [해소됨, 2026-08-12 열일곱 번째 세션] +### 1-10. `store.key`의 레코드 필드 타이핑 검증이 M0가 아니라 M2로 밀려 있음 — [해소됨, 2026-08-12 열일곱 번째 세션] **해소**: 걱정했던 "검증 난이도" 자체가 사라짐 — Luau `type function` (`https://luau.org/types/type-functions/`)으로 `Store`가 `T`의 각 필드를 `Source`로 감싼 레코드 타입을 실제로 합성 가능함을 확인(tbox에서도 이미 쓰이는 패턴). 결과가 구조적으로 `Source`를 만족하기만 하면 Luau가 이름이 아니라 구조로 일치를 검사하므로 문제없음. 기술적으로 막힐 위험이 -없어졌으니 M0/M3 어느 시점에 검증해도 무방 — `ROADMAP.md` 배치를 억지로 +없어졌으니 M0/M2 어느 시점에 검증해도 무방 — `ROADMAP.md` 배치를 억지로 안 옮겨도 됨. 상세는 `base/typing-limits.md` "`store.key` 레코드 필드 타이핑" 절, 실제 문법 실측은 `luau-test/done/16-type-store-key- typefunction.luau`(**[2026-08-15] 통과** — 원래 스파이크가 API 버전 드리프트로 깨져있던 걸 고침, `audit/type-recursive-issue-with-typeof/ REPORT.md` 6-1절). 아래는 원래 발견 당시 기록. -**위치**: `ROADMAP.md` M0 vs M3 `"store.key dot-access 타입 추론 확인"`. +**위치**: `ROADMAP.md` M0 vs M2 `"store.key dot-access 타입 추론 확인"`. **문제**: M0의 정의 자체가 "추론만으로 확정하고 실제 Luau로 부딪혀본 적 없는 것"을 검증하는 단계다. `base/source-state-plan.md`가 요청한 M0 항목( "Source가 State를 만족하는 제네릭 메소드 체이닝"의 솔버 안정성)은 이미 반영됐지만, 이건 `:Compute` 같은 제네릭 메소드 체이닝만 다루고 `{key: Source}` 같은 **레코드 필드로서의 dot-access 타이핑**(읽기/쓰기 -대칭성 논거의 핵심 전제)은 별개로 M3에 남아있다. 같은 리스크 카테고리인데 -M1(스캐폴딩)·M2(디스패치 엔진) 투자가 먼저 이뤄진 뒤에야 검증되는 셈이라, -여기서 걸리면 이미 만든 스캐폴딩/디스패치 타입 시그니처를 다시 손봐야 할 -수 있음. +대칭성 논거의 핵심 전제)은 별개로 Store/State 마일스톤에 남아있다. 같은 +리스크 카테고리인데 M1(스캐폴딩)·디스패치 엔진 투자가 먼저 이뤄진 뒤에야 +검증되는 셈이라, 여기서 걸리면 이미 만든 스캐폴딩/디스패치 타입 시그니처를 +다시 손봐야 할 수 있음. (**[2026-08-24]** 이 문단은 *발견 당시*의 서술이고, +그 우려는 **[2026-08-15 실측 완료]**로 이미 해소됐다. 덧붙여 2026-08-24 +마일스톤 순서 교체로 Store/State가 디스패치보다 **먼저** 오게 되어 "디스패치 +투자를 다 한 뒤에야 검증된다"는 전제 자체도 더는 성립하지 않는다 — 히스토리 +블록이라 원문 논거는 그대로 두고 마일스톤 번호만 뺐다.) **제안**: M0 항목에 "`store.key`가 실제로 `Source` 레코드 필드로 안전하게 추론되는지"도 같이 넣을 것 — 어차피 같은 스파이크 파일에서 몇 줄 @@ -714,7 +719,7 @@ Handler"라고만 서술해, 사실상 3개의 거의 동일한 형태(리터럴 개념을 참조하는 채로 남아있진 않은지 확인 — 이미 "Source가 State를 만족함" 최신 모델로 정정돼 있어 문제없음. - `ROADMAP.md`에 `store.key = value`(구 `__newindex`) 모델을 암시하는 - 잔여 표현은 없음 — M3/M4 서술 모두 문법을 명시하지 않아 최신 `:Set()` + 잔여 표현은 없음 — M2/M4 서술 모두 문법을 명시하지 않아 최신 `:Set()` 모델과 직접 충돌하는 곳은 없음. - M9(컴포넌트 합성)이 M7(Modifier)·M8(Ref) 뒤에 오는 순서 — M9는 "M0 스파이크(named-parameter 전달)를 정식 Modifier/Ref로 검증"하는 단계라고 @@ -732,7 +737,7 @@ Handler"라고만 서술해, 사실상 3개의 거의 동일한 형태(리터럴 착수 전에 열려있는 우선순위1 항목이 없음. 아래는 남은 실측/검증 항목만 정리(전부 "설계는 확정, 실제 Luau/구현으로 부딪혀볼 것만 남음" 상태): -- **M2(Dispatch) 착수 시**: 1-2/1-3/1-4는 설계 확정 완료 — M2 스파이크에서 +- **M3(Dispatch) 착수 시**: 1-2/1-3/1-4는 설계 확정 완료 — M3 스파이크에서 다단 체인 케이스, 동률 감지 디버그 print, 매치 실패 에러 경로가 실제로 맞게 동작하는지 실측. - **M2/M3 착수 전**: 1-6(canExecute 실제 구현) 실측(1-9는 반영 완료, 위 diff --git a/.claude/session-summary.md b/.claude/session-summary.md index 59b081f..943e19d 100644 --- a/.claude/session-summary.md +++ b/.claude/session-summary.md @@ -1839,3 +1839,38 @@ CRUD는 상호배타"라며 승인받은 분기가 **재마운트 경로를 안 자리 넷을 열거해 세기")으로 잡은 게 그래서고 거기서 4건이 더 나왔다. 감사에서 파생된 새 결정 셋(주입 op `onDestroying` 신설, `EffectHandle`의 필드 다섯, `quad-types`의 `Quad` 갱신을 M3/M6/M7/M8/M10에도 항목화)도 같이 반영했다. + +## 2026-08-24-02 — M2/M3 마일스톤 순서 교체 (반응형 먼저) + +2026-08-22가 남긴 마지막 열린 항목(마일스톤 순서)을 사용자가 **(a) 순서 교체**로 +닫았다 — 이제 **M2=반응형 코어, M3=디스패치 엔진**이다. 의존이 양방향처럼 +보였지만 디스패치→반응형만 본체 의존이었고, 옛 순서로는 디스패치 마일스톤의 +`mock 대상 테스트`조차 State 없이 불가능했다. 부수로 **`Brand`/`Relate`/ +`LifetimeHandle` 인터페이스가 M2 앞머리 "공통 기반" 절로** 왔고(반응형이 이 셋을 +먼저 요구), 2026-08-22에 디스패치로 앞당겼던 `EpochMap`/`GateNode`/`Blocker`는 +**되돌아왔다**(앞당길 이유가 사라짐, "게이팅 먼저"는 그대로). 역방향으로 남은 +건 "핸들러를 등록한다"뿐인 둘(동적 경로 가드, `ObserverEffectLeafHandler`)이라 +M3로 넘겼다 — 그래서 빌드 순서상 역방향 간선이 없다. +**대가 하나** — 반응형이 앞으로 오면서 그 게이트 둘(중간 State GC 실측, +`store:GetDynamic` 위치)이 낮은 우선순위에서 **`question.md` 최우선 절로 +승격**됐다. **실행 중 실수 하나** — 일괄 치환에 `\bM([23])\b`를 써서 한글이 +바로 뒤에 오는 `M2는`/`M2로` 93건이 안 바뀌었다(Python `\w`는 유니코드라 한글도 +단어 문자). `git show HEAD:<경로>`로 되돌린 뒤 ASCII 경계 lookaround로 재실행해 +248건 전량 교체. **라이브 문서만 새 번호로 맞췄고 `session/`·`archive/`· +`qa-request/`는 히스토리라 소급 수정하지 않았다** — 그 경고를 인덱스 레이어 +다섯 곳에 박아뒀다. 원문은 `session/2026-08-24-02-milestone-order-swap.md`. + +**검증**: `quad-doc-auditor` 루프가 **7라운드에서 새 발견 0건으로 수렴** +(10→8→1→1→2→2→0, 총 24건 수정, 라운드마다 각도 교체). 가장 값이 큰 건 +5라운드(구현 순서 시뮬레이션) — 새 M2에서 `Source`/`State`/`Store`가 +`EpochMap.luau`보다 앞에 놓여 **그 순서로는 `State.luau`를 못 짜는** 걸 +잡았고(State가 `valueEpochMap`/`emitEpochMap`을 컴포지션), 같은 라운드가 +순서 교체의 전제("M2가 디스패치 없이 완주 가능한가")도 검증했다. +**수렴 뒤 `/code-review high`가 5건을 더 잡았고 전부 유효** — 다섯 중 넷이 +*"라벨은 치환됐는데 그 라벨을 설명하던 산문이 안 고쳐진"* 종류(그중 둘은 +`quad-types`의 "마일스톤마다 갱신" 규칙이 M3에 앵커된 채 첫 적용만 M2로 +옮겨간 같은 뿌리)였고, 그중 +하나는 `research/` 안의 **히스토리 블록**이 치환을 맞아 원래 논거가 문장 +그대로 거짓이 된 것이었다(소급 수정 제외 대상을 `session/`·`archive/`· +`qa-request/`로만 잡은 게 샜다). `conventions.md`의 *"`/code-review`는 +감사자를 대체하지 않는다"*가 또 재확인됐다. diff --git a/.claude/session/2026-08-24-02-milestone-order-swap.md b/.claude/session/2026-08-24-02-milestone-order-swap.md new file mode 100644 index 0000000..92f0b0f --- /dev/null +++ b/.claude/session/2026-08-24-02-milestone-order-swap.md @@ -0,0 +1,170 @@ +# 2026-08-24-02 — M2/M3 마일스톤 순서 교체 (반응형 먼저) + +**요청**: *"질문절에 마일스톤 순서 이슈는 (a) M2와 M3의 순서를 바꾼다 를 +택하는게 맞겠던데 어떻게 봐?"* → 검토 후 동의, *"진행해줘"*로 전량 반영. + +## 1. 발단 + +2026-08-22 `ROADMAP.md` 전반 점검이 남긴 마지막 열린 항목. 옛 M2(디스패치 +엔진)와 옛 M3(Store/State/Source)의 의존이 양방향이라 그 순서로는 M2를 +끝까지 짤 수 없었다. 설계 결정이 아니라 **순서** 문제라 `question.md`의 +최우선 절이 아니라 2번 항목으로 따로 뒀었다. + +## 2. 검토 — (a)에 동의한 근거 + +사용자가 이미 (a)로 기울어 있었고, 확인 결과 방향은 맞았다. 근거 셋: + +1. **의존의 비대칭이 명확하다.** 디스패치 → 반응형은 *본체* 의존이라 + 우회 불가(`setLength`가 `State`를, `setOffsetSource`가 + `Source`를 받고 `recompute`가 `offset:Set()`을 부름). 반대는 + *등록 표면* 의존(핸들러 3개 등록)이라 뒤로 미루면 그만이다. +2. **옛 순서면 디스패치 마일스톤의 `mock 대상 테스트` 체크박스가 원리적으로 + 불가능했다** — `setLength`/`recompute`를 State 없이 테스트할 수 없다. + 반대로 반응형 코어는 순수 Lua라 `luau`만으로 단독 테스트가 된다. +3. **2026-08-22의 `EpochMap`/`GateNode`/`Blocker` 앞당김이 불필요해진다.** + 그 이동은 "디스패치가 먼저인데 디스패치가 쟤들을 호출한다"는 이유였다. + 순서를 바꾸면 마일스톤 내용이 의존 계층과 그대로 일치하고 끌어온 항목이 + 0개가 된다. "게이팅 먼저"도 그대로 지켜진다(게이팅이 디스패치보다 먼저). + +**(b) 분할을 안 고른 이유**: 참조 비용이 오히려 크다 — 기존 `M2` 참조 +전부가 "M2a인가 M2b인가"로 애매해지는데 순서 교체처럼 기계적으로 못 푼다. +`Brand`를 앞으로 빼는 일은 (b)에서도 똑같이 해야 하고, 얻는 건 "디스패치 +코어를 조금 일찍 짠다"뿐인데 M4 전까지 그걸 소비하는 게 없다. +**(c) 유지를 안 고른 이유**: `project-context.md`가 못박은 "순서의 소스는 +`ROADMAP.md`"를 스스로 무효화한다. + +## 3. 그냥 맞바꾸기로는 안 끝났다 — 공통 기반 셋 + +검토 중 드러난 것. 옛 M2 안에 **반응형보다 먼저 와야 하는** 항목이 셋 +섞여 있었고, State-free이자 dispatch-free라 어느 쪽에도 안 걸린다: + +- `Brand.luau` — `Source`가 `SourceBrand`+`EpochBrand`에 등록돼야 함. +- `Relate.luau` — `bindLifetime`/`unbindLifetime`이 그 위에 구현됨. +- `LifetimeHandle.luau` 인터페이스 — State 전파 루프가 매 발화마다 + `canExecute`를, 이중 바인딩 게이트가 `canBound`를 부름. + +새 M2 앞머리에 `### 공통 기반` 절을 만들어 셋을 옮겼다. 반대로 새 M3로 +넘어간 것은 둘 — Observer/Effect **동적 경로 가드 등록**과 +**`ObserverEffectLeafHandler`**(`H-39`). 결과적으로 역방향 간선이 없다. + +## 4. 실행 중 낸 실수 하나 — 한글 인접 `M2`가 안 바뀌었다 + +첫 일괄 치환에 `re.sub(r'\bM([23])\b', ...)`를 썼는데, **Python `re`의 +`\w`는 유니코드라 한글도 단어 문자**다. 그래서 `M2는`/`M2로`/`M2가`처럼 +한글이 바로 뒤에 오는 경우 `\b`가 성립하지 않아 **155건만 바뀌고 93건이 +그대로 남았다** — 코퍼스가 옛 번호와 새 번호가 섞인 상태가 됐다. + +`git checkout -- .`은 하네스가 막아서(destructive) `git show HEAD:<경로>`로 +26개 파일을 되돌린 뒤, ASCII 경계만 보는 +`(?`/ - `Source`를 쓰므로 M2는 `Source.luau`/`State.luau` 없이 구현이 - 안 되고, 반대로 M3의 `Observer`/`Effect` 동적 경로 가드는 M2의 - `Dispatch.addHandler`를 쓴다. 선택지 셋(M2/M3 순서 교체 / M2를 둘로 - 분할 / 지금 구조 유지)은 **`question.md` 2번이 소스** — 여기서 반복하지 - 않는다. 같은 점검에서 `EpochMap`/`GateNode`/`Blocker`/`None`이 M3·M7에서 - **M2로 실제 이동**했고(각주로만 예고돼 있던 것), `Dispatch.drive`가 - 두 패스가 아니라 단일 일반화 `for`라는 `F-4-1` 정정과 물리 조작 주입 op - 이름 `native*` 확정이 `ROADMAP.md`/`dispatch-core-plan.md`에 반영됐다. - M0 체크박스가 검증했던 스파이크 중 재작성 대기인 것들도 - `ROADMAP.md`의 "재검증 대기" 절로 모았다(현황의 소스는 여전히 - `luau-test/STATUS.md`). + **✅ [2026-08-24 해소] 설계가 아니라 *순서*가 막던 것도 닫혔다.** + 2026-08-22 `ROADMAP.md` 전반 점검에서 옛 M2(디스패치)와 옛 M3(반응형)의 + 의존이 양방향인 게 드러났었는데, **사용자가 (a) 순서 교체를 선택**해 + 같은 날 전량 반영됐다 — 이제 **M2가 반응형 코어, M3가 디스패치 엔진** + 이고 역방향 의존이 없다. 결정·근거·기각된 선택지는 + `archive/question-resolved.md`의 "마일스톤 경계" 절이 소스, 새 마일스톤 + 구성은 `ROADMAP.md`의 M2 배너 — 여기서 반복하지 않는다. 부수로 + `Brand`/`Relate`/`LifetimeHandle` 인터페이스가 M2 앞머리 "공통 기반" + 절로 왔고, 2026-08-22에 디스패치로 앞당겼던 + `EpochMap`/`GateNode`/`Blocker`는 M2로 되돌아왔으며, Observer/Effect + 동적 경로 가드 등록과 `ObserverEffectLeafHandler`는 M3로 갔다. + **⚠️ 2026-08-24 이전에 쓰인 `session/`·`archive/`·`qa-request/`의 + `M2`/`M3`는 옛 의미**(M2=디스패치, M3=반응형)로 읽을 것. + 같은 2026-08-22 점검에서 `Dispatch.drive`가 두 패스가 아니라 단일 일반화 + `for`라는 `F-4-1` 정정과 물리 조작 주입 op 이름 `native*` 확정도 + `ROADMAP.md`/`dispatch-core-plan.md`에 반영됐고, M0 체크박스가 검증했던 + 스파이크 중 재작성 대기인 것들도 `ROADMAP.md`의 "재검증 대기" 절로 + 모았다(현황의 소스는 여전히 `luau-test/STATUS.md`). 그 외 남은 것은 판단이 아니라 구현 시 정할 것 하나 — 스파이크 `05` 재작성(`luau-test/STATUS.md`). - **[2026-08-22 해소]** 여기 있던 "M2에 `Blocker`까지 넣을지"는 같은 날 - `ROADMAP.md` 전반 점검에서 **둘 다 M2**로 확정되며 닫혔다. **[2026-08-24 해소, 6라운드 `H-49`]** 여기 `Gate`의 "생명주기·재진입 계약"을 같이 나열했었는데 **두 항목 다 부정확했다** — 재진입은 이미 `[2026-08-21 정리 — 열린 항목 아님]`으로 닫혀 있었고, 생명주기는 "판단이 @@ -73,7 +76,7 @@ **M2/M3에 직접 걸리던 것들이 전부 닫혔다** — 말단 핸들러 4종의 `setLength`/`setOffsetSource` 미등록(`H-39`), `New(): Quad`가 닫힌 타입이라 - `quad.Dispatch`가 타입에러인 것(`H-25`, M2 체크리스트에 항목 신설), + `quad.Dispatch`가 타입에러인 것(`H-25`, M3 체크리스트에 항목 신설), `Effect`의 leaf 사망 cleanup 배선 부재(`H-11`), `:List`의 인덱스/좌표계 결함 둘(`H-1`/`H-2`). @@ -112,7 +115,7 @@ **`Owned = false`에서 `Detach`는 `_detached`에 안 들어감**, 조상 파괴 시 unowned 요소도 같이 죽는다는 계약 신설, `groupClaimKeys` 키 확정 (`(inst, groupValue) → k`), `Tween:Mapped` 이름 확정, 4라운드가 반영을 - 빠뜨렸던 `E-10` dedup 대칭 결론 실반영, 그리고 **"게이팅 먼저"(M2로 앞당김)**. + 빠뜨렸던 `E-10` dedup 대칭 결론 실반영, 그리고 **"게이팅 먼저"**(당시엔 디스패치 마일스톤으로 앞당기는 형태였으나 **[2026-08-24]** 순서 교체로 반응형이 먼저가 되며 그 앞당김은 불필요해짐). **아직 회신 대기인 것은 그 followup의 C절**(`rawAdd` 의사코드 승인, `rawAdd`의 `Length:Set` 제거, `updateFn`이 State를 반환할 때의 래핑/`prev`, `setLength` 앵커를 물리 target으로 되돌리기, `Gate` 이름·표면, `Effect` @@ -158,26 +161,29 @@ 단순한 해법을 직접 제시 — flush 루프를 분기하는 대신 `attachSlot`의 `slot._mounted = true`를 `activateList` 호출 뒤로 옮기는 것 하나로 둘 다 닫힘. 부수로 `spliceArraysDown`이 밀어야 할 배열에 - `bk.observers`가 빠져 있던 것도 발견·반영, `ROADMAP.md` M2가 M3의 - `Blocker.luau`에 구조적으로 의존하게 된 것도 각주로 반영(마일스톤 - 재편 여부는 열림 — `pre-implementation-qa-round3.md`의 "ROADMAP.md - 마일스톤 정합성" 절 참고). + `bk.observers`가 빠져 있던 것도 발견·반영, `ROADMAP.md` M3(디스패치)가 + M2의 `Blocker.luau`에 구조적으로 의존하게 된 것도 각주로 반영 + (**[2026-08-24]** 그때 열어뒀던 마일스톤 재편 여부가 순서 교체로 + 닫혔다 — 위 00번 머리말). - **아래는 M3 착수 전에 결론이 필요한 항목 목록** — **설계** 게이트 - 얘기다. M0은 아예 막혀 있지 않고, M2도 설계로는 안 막혀 있으나 - **[2026-08-22] 마일스톤 순서 문제 하나가 M2를 막는다**(위 00번의 - ⚠️ 문단 + `question.md` 2번). 둘 다 `question.md` 3번에도 있고 각 `base/` 문서에도 - ⚠️로 표시돼 있다. **[2026-08-21 정리]** 여기 쌓여 있던 `[해소]` 항목들 + **⚠️⚠️ [2026-08-24 승격] 아래 둘은 이제 *바로 다음* 마일스톤의 + 게이트다.** 순서 교체 전엔 반응형이 M3라 "한 마일스톤 뒤"의 일이었는데, + 반응형이 M2가 되면서 **지금 착수 직전에 결론이 필요한 항목**이 됐다 + (위 00번이 "M2 착수를 막는 **설계** 항목은 없다"고 하는 것과 모순되지 + 않는다 — 하나는 실측 미완, 하나는 표면 위치 선택이라 성격이 다르지만, + **어느 쪽이든 M2를 짜기 전에 답이 있어야 한다**). 둘 다 + `question.md`의 **최우선 절**로 올라가 있고(2026-08-24에 낮은 우선순위 + 절에서 승격), 각 `base/` 문서에도 ⚠️로 표시돼 있다. **[2026-08-21 정리]** 여기 쌓여 있던 `[해소]` 항목들 (`Blocker.luau` 마일스톤 순서, 그룹 `Attribute` 위치 claim 키, `SetAndDispose`, `PopOnly`→`Detach`, `KeyGone` 처분, `Store` 미선언 키 타입 에러, dedup 경로 대칭)은 **전부 `archive/question-resolved.md`와 각 `base/` 문서로 옮겼다** — 목록이 절반 넘게 해소 항목으로 차 있어 "지금 할 일"로 읽히지 않던 것을 걷어낸 것. - **중간 State GC 미검증**(`base/source-state-plan.md`) — 상류 strong / - 하류 weak 불변식을 명문화할지 + `luau-test` 실측. **M3 착수 전 필요.** + 하류 weak 불변식을 명문화할지 + `luau-test` 실측. **M2 착수 전 필요.** - **`store:GetDynamic`을 콜론 메소드로 둘지 탑레벨 함수로 둘지** (`base/store-plan.md`) — 콜론이면 `GetDynamic`이 모든 Store의 예약 키가 - 됨(lazy `__index`와 충돌). M3/M4 착수 전 필요. + 됨(lazy `__index`와 충돌). M2/M4 착수 전 필요. 0. **⭐ M0 착수를 막는 결정은 이제 없음 (2026-08-14 열한 번째 세션 기준).** `question.md`의 최우선 항목이 **전부 비었음** — `0-Y`(`:Compute` lazy @@ -287,13 +293,13 @@ 백로그이지만 위 항목들과는 발단이 다름 — **사용자가 직접 요청한 실제 기능 갭**에서 시작됨(그 문서 13절). 다만 제어 핸들 설계까지 닫히고 나니 실제로 quad-base에 새 코어 메커니즘을 추가하지 않는 **순수 슈가**로 - 확인돼(같은 절), 위 항목들과 우선순위는 다시 같아짐 — M0/M3를 막지 + 확인돼(같은 절), 위 항목들과 우선순위는 다시 같아짐 — M0/M2를 막지 않고, **그 게이티드 노드는 [2026-08-21] `state:Gate`로 확정돼 M2에서 만들어진다**(`base/gate-plan.md`) — `Debounce`/`Throttle`은 그 위의 정책으로 얹으면 되고, 같은 설계를 두 번 할 일은 없어졌다. 주입 op 2개(`setTimeout`/`clearTimeout`)가 백엔드 팩토리 표면에 추가될 예정이라는 것도 M1 설계 시 인지. 남은 열린 질문 없음(구 - `question.md` 3번, 전량 해소로 항목 자체가 빠짐). + `question.md` 낮은 우선순위 절, 전량 해소로 항목 자체가 빠짐). **[2026-08-18 추가]** 사용자 아이디어 메모 두 건도 같은 성격의 백로그로 신설 — 스크롤 최적화 외부 유틸 `quad-roblox-fastscroll` (`research/fastscroll-plan.md`, 선행으로 `Visible=false`일 때 diff --git a/CLAUDE.md b/CLAUDE.md index e476229..df62012 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,12 +1,16 @@ # CLAUDE.md Roblox 엔진용 DOMless UI 렌더러 **quad**를 처음부터 다시 짜는 프로젝트. -**[2026-08-22 기준] M0(스파이크 검증)/M1(스캐폴딩)까지 완료, 다음은 -M2(디스패치 엔진)** — 다만 **⚠️ M2 착수를 막는 항목이 하나 있음**: -M2와 M3의 의존이 양방향이라(M2의 Length/Offset 배관이 `State`/`Source`를 -씀) 지금 순서로는 M2를 끝까지 짤 수 없다. 설계 결정이 아니라 **마일스톤 -순서** 문제이고, 선택지와 근거의 소스는 `.claude/question.md` 2번(사용자 -회신 대기). 같은 상태를 `.claude/project-context.md`도 +**[2026-08-24 기준] M0(스파이크 검증)/M1(스캐폴딩)까지 완료, 다음은 +M2(반응형 코어 — Source/State/Store)**. **⚠️ [2026-08-24] M2와 M3의 +번호·순서가 맞바뀌었다** — 예전엔 M2=디스패치, M3=반응형이었는데 의존이 +한 방향(디스패치 → 반응형)이라 반응형을 먼저 짓기로 확정했다. 그래서 +**2026-08-24 이전에 쓰인 `session/`·`archive/`·`qa-request/` 문서의 +`M2`/`M3`는 옛 의미로 읽을 것**(라이브 문서는 전부 새 번호로 맞춰뒀음). +경위는 `.claude/archive/question-resolved.md`의 "마일스톤 경계" 절, +새 구성은 `ROADMAP.md`의 M2 배너. **⚠️ M2 착수 전에 답이 필요한 항목이 +남아 있다** — 무엇이 몇 개인지는 여기서 세지 말고 `.claude/question.md`의 +최우선 절을 볼 것. 같은 상태를 `.claude/project-context.md`도 서술하니 마일스톤이 넘어갈 때 두 곳을 같이 고칠 것. 진행 상황의 소스는 항상 루트 `ROADMAP.md`. diff --git a/HUMAN_TODO.md b/HUMAN_TODO.md index 0b6a0c4..dc6349e 100644 --- a/HUMAN_TODO.md +++ b/HUMAN_TODO.md @@ -6,17 +6,17 @@ 올렸었음**(0-Z) — **[2026-08-13 열네 번째 세션] 그 0-Z도 해소되어 지금은 사람이 결정해야 M0가 열리는 항목이 없음**(0-Y는 열세 번째 세션에 해소). -## ⭐ 11. **[2026-08-22 신설] M2 착수를 막는 마일스톤 순서 결정** — 사용자 답변 필요 +## ✅ 11. **[2026-08-22 신설 → 2026-08-24 해소] 마일스톤 순서 결정** -**설계 결정이 아니라 순서 문제**라서 에이전트가 임의로 정하면 안 되는 자리다. -`ROADMAP.md` 전반 점검에서 **M2(디스패치 엔진)와 M3(Store/State/Source)의 -의존이 양방향**인 게 드러났다 — M2의 Length/Offset 배관이 `State`/ -`Source`를 쓰므로 **M2는 State/Source 없이 구현이 안 되고**, 반대로 -M3의 동적 경로 가드는 M2의 `Dispatch.addHandler`를 쓴다(그쪽은 얕음). +사용자가 **(a) 순서 교체**를 선택해 닫혔다 — 반응형이 M2, 디스패치가 M3다. +결정과 근거는 `.claude/archive/question-resolved.md`의 "마일스톤 경계" 절, +새 마일스톤 구성은 `ROADMAP.md`의 M2 배너가 소스. -**선택지 (a) M2/M3 순서 교체 / (b) M2를 둘로 분할 / (c) 지금 구조 유지**의 -근거와 상세는 **`.claude/question.md` 2번이 소스** — 여기서 반복하지 않는다. -답이 나오면 `ROADMAP.md`의 마일스톤 경계와 `question.md` 2번을 같이 닫으면 된다. +**⚠️ 다만 그 교체로 `question.md` 최우선 절에 항목 둘이 올라왔다** — 중간 +State GC 실측과 `store:GetDynamic` 위치. 둘 다 원래 반응형(옛 M3)의 +게이트라 급하지 않았는데 반응형이 먼저 지어지게 되면서 **지금 답이 +필요**해졌다. 둘 다 설계 질문이라 이 문서가 아니라 `question.md`가 +소스다(아래 3번 항목이 가리키는 그 절) — 여기서 따로 번호를 만들지 않는다. --- @@ -205,7 +205,7 @@ Debounce/Throttle 작업에 쓴 워크트리는 **사용자 확인 후 정리 Tween 자체가 다른 base 요소와 깊게 안 얽혀 있어(거의 전부 `Handlers/Property.luau` 한 파일 + 릴레이션 슬롯) 사용자가 직접 처리하는 데 범위상 문제가 없다. -- **지금 상태**: 미확정("필요성 낮은 쪽으로 기움", 완전 폐기는 아님). **M0/M2를 +- **지금 상태**: 미확정("필요성 낮은 쪽으로 기움", 완전 폐기는 아님). **M0/M2(지금 착수 대상)를 막지 않으므로 급하지 않고**, 실제로 진입 애니메이션이 필요해지는 시점에 사용자가 착수하면 된다. 설계 맥락은 `base/tween-plan.md`의 "초기 진입 애니메이션(`initValue`)" 절. @@ -214,9 +214,11 @@ Debounce/Throttle 작업에 쓴 워크트리는 **사용자 확인 후 정리 디자인 결정 중 Lua/Roblox 엔진에 대한 깊은 경험이 필요한 것들은 합리적 기본값으로 진행하면서 `.claude/question.md`에 모아두는 중. 깨어있을 때 훑어보고 기본값이 -마음에 안 드는 것만 답해주면 됨 — **[2026-08-22 갱신] 설계 결정 대기는 -여전히 0건**이지만, **마일스톤 순서 결정 하나가 새로 열렸다**(`question.md` -2번, 위 11번 항목이 소스). 아래는 그 전 서술: **[2026-08-14 열한 번째 세션 기준] +마음에 안 드는 것만 답해주면 됨 — **[2026-08-24 갱신] 순수 설계 결정 +대기는 여전히 0건**이고 2026-08-22에 열렸던 마일스톤 순서 결정도 +**해소됐다**(위 11번 항목). 다만 그 해소의 부작용으로 **`question.md` +최우선 절에 M2 착수 전 답이 필요한 항목 둘**이 올라와 있다(중간 State GC +실측, `store:GetDynamic` 위치). 아래는 그 전 서술: **[2026-08-14 열한 번째 세션 기준] `question.md`엔 이제 "결정 대기" 절 자체가 없음**(비어서 헤딩째로 삭제 — 마지막 남았던 0-W `Ref` 이중 배치도 이 세션에 해소 — `base/ref-plan.md` "이중 배치 방지" diff --git a/ROADMAP.md b/ROADMAP.md index 2eb4e65..96849c5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -7,23 +7,31 @@ quad-v2 구현 단계 실행 계획. 설계 근거/아키텍처 자체는 여기 **2026-08-04 세션에 준비만 해둔 상태로 신설, 이후 여러 세션에 걸쳐 설계가 확정될 때마다 각 마일스톤 체크박스가 계속 갱신돼왔음.** -> **✅ [2026-08-22 기준] M0/M1의 *원래* 체크박스는 전부 닫혔고 다음은 -> M2(디스패치 엔진)** — 단 M0에는 **[2026-08-22] "재검증 대기" 미체크 +> **✅ [2026-08-24 기준] M0/M1의 *원래* 체크박스는 전부 닫혔고 다음은 +> M2(반응형 코어)** — 단 M0에는 **[2026-08-22] "재검증 대기" 미체크 > 항목들이 새로 붙었습니다**(설계가 바뀌어 무효화된 스파이크들, 아래 그 -> 절이 소스). 그건 아직 열린 작업이므로 "M0 완료"로 읽고 넘어가면 안 됩니다. — **⚠️ 다만 M2를 바로 착수하면 안 됩니다: M2와 M3의 의존이 -> 양방향이라 이 순서로는 M2를 끝까지 짤 수 없습니다**(설계가 아니라 -> 마일스톤 **순서** 문제 — 아래 M2 배너와 `.claude/question.md` 2번, -> 사용자 회신 대기). M1까지의 산출물은 +> 절이 소스). 그건 아직 열린 작업이므로 "M0 완료"로 읽고 넘어가면 안 됩니다. +> **[2026-08-24] 착수를 막던 마일스톤 순서 문제는 해소됐습니다** — M2와 +> M3의 번호를 맞바꿔 반응형을 먼저 짓기로 확정했습니다(아래 M2 배너가 +> 소스). **⚠️ 다만 그 교체로 M2 착수 전에 답이 필요한 항목 둘이 +> `.claude/question.md` 최우선 절로 올라왔습니다** — 중간 State GC 실측과 +> `store:GetDynamic` 위치(원래 반응형의 게이트였는데 반응형이 앞으로 +> 오면서 같이 당겨짐). M1까지의 산출물은 > quad-base/quad-roblox 폴더+pesde.toml, 루트 > default.project.json/.luaurc, mock 테스트 하네스, `New()`/`RunInit`/ > `AddPlugin` 골격. **다만 M0의 검증 스파이크 여러 개가 설계 변경으로 > 재작성 대기 상태입니다** — 아래 "재검증 대기" 절 참고(개수·현황의 > 소스는 `.claude/luau-test/STATUS.md`, 여기서도 그 절에서도 세지 않음). > -> **[2026-08-22] 이 문서에 이번에 반영된 것**: `Dispatch.drive`가 두 -> 패스가 아니라 단일 일반화 `for`(`F-4-1`) / 물리 조작 주입 op 이름이 -> `native*`로 확정 / `EpochMap`·`GateNode`·`Blocker`·`None`이 M2로 이동 / -> M2↔M3 의존이 양방향이라는 것(M2 헤더 배너) / `[x]` 표기 의미 분리 +> **[2026-08-24] 이 문서에 이번에 반영된 것**: M2(반응형)와 M3(디스패치)의 +> **번호·순서 교체**, 그에 따라 `Brand`/`Relate`/`LifetimeHandle` +> 인터페이스가 M2 앞머리로 이동하고 `EpochMap`/`GateNode`/`Blocker`가 M2로 +> 복귀, Observer/Effect 동적 경로 가드 등록과 `ObserverEffectLeafHandler`가 +> M3로 이동. +> +> **[2026-08-22] 그 전 회차에 반영된 것**: `Dispatch.drive`가 두 패스가 +> 아니라 단일 일반화 `for`(`F-4-1`) / 물리 조작 주입 op 이름이 `native*`로 +> 확정 / `None`이 M7에서 디스패치로 이동 / `[x]` 표기 의미 분리 > (바로 아래). > > **⚠️ `[x]`의 의미** — 체크박스는 **"짜야 할 코드"만** 담습니다. @@ -31,13 +39,13 @@ quad-v2 구현 단계 실행 계획. 설계 근거/아키텍처 자체는 여기 > 마일스톤의 **`### 확정된 것`** 절(항목이 여럿일 때) 또는 > **`- **[확정된 것 — 코드 아님]**`** 불릿(하나뿐일 때)에 둡니다. 예전엔 > 둘이 같은 목록에 섞여 있어 `[x]`만 보고는 코드가 있는지 알 수 -> 없었습니다 — M0/M1의 `[x]`는 실제 구현이고, M3/M6/M11에 섞여 있던 +> 없었습니다 — M0/M1의 `[x]`는 실제 구현이고, M2/M6/M11에 섞여 있던 > `[x]`는 설계 확정·실측이었습니다. **새 항목을 추가할 때도 이 구분을 > 지킬 것.** > M1 착수 도중 > wally→pesde 전환이 확정돼(`base/project-setup-plan.md`) M1 체크박스의 -> `wally.toml` 표기도 `pesde.toml`로 정정. 부수로 M3가 의존하는 +> `wally.toml` 표기도 `pesde.toml`로 정정. 부수로 M2가 의존하는 > `quad-types`/`type-version-check` 두 워크스페이스 멤버도 이 과정에서 > 먼저 신설됨(`base/quad-types-plan.md`). @@ -141,7 +149,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 Observer가 이제 변경당 **1회**만 울어야 함(옛 "emit은 항상 전파" 모델을 검증 중). `base/state-epoch-plan.md` 기준으로 재작성 - [ ] **`04`(Dispatch 체인 retractFrom)** — 하강 diff 확정으로 무효화. - **M2 착수 시 같이 처리**하는 게 자연스러움 + **M3 착수 시 같이 처리**하는 게 자연스러움 - [ ] **`19`(소유권/참조카운트 Relate 패턴)** — **B 섹션만** 낡음 (공개 `AttributeKey(name)` + 인덱스 1 점유 체크가 폐기되고 그룹 전용 키 + `AttributeKeyHandler`의 이름 claim으로 바뀜, `0-Z` 확정). @@ -154,13 +162,18 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 **M8 착수 시 같이 처리** - [ ] **`15`(`:Compute` trailing deps 타입팩)** — 이형 다중 deps를 제네릭 타입 팩으로 표현 가능한지 미실측(안 되면 동종 dep 1개로 한정). - **M3 착수 시 같이 처리** + **M2 착수 시 같이 처리** - [ ] **`10`(Roblox Studio 확인)** — `bindLifetime`/`canExecute`/ `unbindLifetime` 재정정으로 무효화. **Studio 작업이라 `HUMAN_TODO.md` 1번(계정 분리)이 선행** -- [ ] **아직 파일이 없는 실측 항목** — `R-11`의 `table.insert` 구멍 재사용, - **중간 State GC**(`base/source-state-plan.md`, 상류 strong / 하류 weak - 불변식 — `.claude/todos.md`가 "M3 착수 전 필요"로 지정) +- [ ] **아직 파일이 없는 실측 항목** — **중간 State GC** + (`base/source-state-plan.md`, 상류 strong / 하류 weak 불변식 — + `.claude/todos.md`가 "M2 착수 전 필요"로 지정. **[2026-08-24 승격]** + 순서 교체로 반응형이 바로 다음 마일스톤이 되면서 `question.md` + 최우선 절로 올라갔다). **[2026-08-24 정리]** 여기 같이 적혀 있던 + `R-11`의 `table.insert` 구멍 재사용은 6라운드 `H-7`로 + **전제 자체가 없어져 폐기**됐다(`Ref.Callbacks`가 해시맵 셋이 되어 + 구멍 개념이 없음) — `luau-test/STATUS.md`가 소스 ## M1 — 실제 스캐폴딩 @@ -194,82 +207,34 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 계속 같은 방식으로 쓰이는 중이라 "실사용 시작"이라는 조건은 사실상 항상 충족돼 있었음) -## M2 — 디스패치 엔진 +## M2 — 반응형 코어 (Source/State/Store) -> **✅ [2026-08-13 열네 번째 세션] 재디스패치 모델 교체 완료 — 아래 -> 체크리스트는 새 모델("하강 diff") 기준으로 갱신됐습니다.** 래핑 핸들러의 -> 선행 `retractFrom`은 폐기됐고, `Dispatch.process`가 슬롯의 `handler`를 -> 먼저 비교해 (같으면 그 자리 클로저에 새 값을 넘기고 자기 `process` -> 재호출, 다르면 그 자리부터 전량 철거) 처리합니다. 정본은 -> `.claude/base/dispatch-core-plan.md`(같은 세션에 `bind-system-plan.md`에서 -> 분리 신설), 뒤집힌 옛 모델은 -> `.claude/archive/dispatch-hintvalue-model-reversed.md`. - -> **⚠️ [2026-08-22] M2는 이름과 달리 "디스패치만"이 아닙니다 — 반응형 -> 하부구조 일부가 여기 있습니다.** `EpochMap`/`GateNode`/`Blocker`/`None` -> 체크박스가 M3·M7에서 옮겨왔습니다(각 항목의 이동 표시 참고). 그동안 -> 각주로만 예고돼 있어 M2 체크리스트를 훑는 구현자에게 **항목으로 보이지 -> 않던** 것을 고친 것이고, `LifetimeHandle` 인터페이스를 M8→M2로 옮겼던 -> 것과 같은 처리입니다. +> **⭐ [2026-08-24 순서 교체] 옛 M3(반응형)와 옛 M2(디스패치)의 번호를 +> 맞바꿨습니다.** 의존이 양방향처럼 보였지만 실제로는 한 방향이었고 +> (디스패치 → 반응형이 본체 의존, 반대는 핸들러 등록 표면뿐), 옛 순서로는 +> 디스패치의 Length/Offset 배관도 그 마일스톤의 `mock 대상 테스트`도 State +> 없이 짤 수 없었습니다. 근거와 기각된 선택지는 +> `.claude/archive/question-resolved.md`의 "마일스톤 경계" 절. > -> **⚠️ 다만 이동으로 드러난 더 큰 문제가 하나 남아 있습니다 — M2와 M3의 -> 의존이 양방향이라, 이 순서대로면 M2를 끝까지 짤 수 없습니다.** -> 요지만: **M2는 `Source.luau`/`State.luau` 없이는 구현할 수 없고** -> (Length/Offset 배관이 `State`/`Source`를 쓰고, -> `Dispatch.drive` 자신도 배치 등록을 Blocker로 게이팅합니다), -> 반대 방향은 핸들러 등록 표면만 요구하는 얕은 의존입니다. -> **어느 쪽으로 가를지(순서 교체 / M2 분할 / 유지)는 사용자 결정 -> 대기이고, 근거와 선택지의 소스는 `.claude/question.md` 2번입니다** — -> 여기서 반복하지 않습니다. +> **⚠️ 이 날짜 이전에 쓰인 `session/`·`archive/`·`qa-request/` 문서의 +> `M2`/`M3`는 옛 의미**(M2=디스패치, M3=반응형)입니다 — 히스토리 문서라 +> 소급 수정하지 않았습니다. 라이브 문서(`base/`/`research/`/`reference/`/ +> `luau-test/`/인덱스 레이어)는 전부 새 번호로 맞췄습니다. +> +> 부수로 **`Brand`/`Relate`/`LifetimeHandle` 인터페이스가 디스패치에서 이 +> 마일스톤 앞머리로 왔습니다** — 반응형이 이 셋을 먼저 요구하기 때문: +> `Source`가 `SourceBrand`+`EpochBrand`에 등록되고(`Brand`), State 전파 +> 루프가 매 발화마다 `canExecute`를 부르며(`LifetimeHandle`), 그 판정이 +> `Relate`에 복사해둔 gcconn 위에 얹힙니다. 반대로 2026-08-22에 여기서 +> 디스패치로 옮겼던 **`EpochMap`/`GateNode`/`Blocker`는 되돌아왔습니다** — +> 앞당길 이유 자체가 순서 교체로 사라졌고, "게이팅 먼저"는 그대로 +> 지켜집니다. +### 공통 기반 — 반응형보다 먼저 (구 M2, 지금의 M3에서 이동) + +> 셋 다 State-free이자 dispatch-free라 어느 쪽에도 안 걸립니다. 이 절이 +> 끝나야 아래 반응형 본체를 짤 수 있습니다. -- [ ] `Dispatch/init.luau` — `Dispatch.getHandler(inst,k,v): Handler?`(순수 - 스캔, `isHandlable`+`priority`) / `Dispatch.process(inst,k,v,index)` - (오케스트레이터: getHandler → **그 인덱스의 기존 핸들러와 비교** → - 같으면 그 자리 클로저에 새 값을 넘기고 같은 핸들러의 `.process`로 - 자리 교체, 다르면 `retractFrom` 후 새로 설치. 반환값이 `nil`이면 - 즉시 error) / **3-인자** `Dispatch.retractFrom(inst,k,index)` - (아래 항목) / `Dispatch.addHandler(handler)`(레지스트리 등록, - quad-roblox가 팩토리 뮤테이션 시점에 호출) / `Dispatch.drive(inst, - flattened)`(**일반화 `for` 한 번**으로 순회하며 각 `(k,v)`에 - `Dispatch.process(inst,k,v,1)` 호출 — `dispatch-core-plan.md`의 `None` - 센티널 절, 2026-08-07 여덟 번째 세션에 네이밍 확정). - **[2026-08-21 구현 전 QA 4라운드 `F-4-1`] 순회는 두 패스가 아니라 - 단일 일반화 `for`다** — "배열 파트 전체가 해시 파트보다 먼저"는 여전히 - base가 보장하는 **계약**이지만, `flattened`가 항상 평범한 Luau - 테이블이라 일반화 `for` 한 번이 그 순서를 그대로 주고 두 층위는 - `type(k) == "number"` 분기로 가른다(`base/dispatch-core-plan.md`의 - `F-4-1` 정정 문단). 옛 "명시적 두 패스" 서술은 구현까지 2회 순회로 - 못박은 것처럼 읽혀 정정됨 — 그 때문에 스파이크 `01`도 재작성 대기 - (`luau-test/STATUS.md`) -- [ ] **⭐ [2026-08-24 신설, 6라운드 손 트레이싱 `H-39`] 말단 핸들러는 예외 - 없이 자기 배열 자리의 `setOffsetSource(inst,k,None)` → `setLength(inst,k,0)`을 - 등록한다** — 이 계약을 핸들러 작성 체크리스트에 넣고, 실제로 넷이 - 빠져 있었으니 각 마일스톤에서 확인할 것: `TagHandler`(M10) / - `AttributeGroupHandler`(M10) / `RefLeafHandler`(M8) / - `ObserverEffectLeafHandler`(M3). 안 하면 `bk.N`이 다른 자리 등록으로 - 커질 때 그 구멍이 범위 안에 끼어 `recompute`가 **명시적 error로 죽는다** - (`Frame { Tag("card"), TextLabel{} }`처럼 말단이 앞에 오는 흔한 배치). - **배열 맨 끝이면 안 터지므로 "가끔 되고 가끔 터지는" 형태로 드러난다.** - 소스는 `base/dispatch-core-plan.md`의 등록 책임 절 -- [ ] **⭐ [2026-08-24 신설, 6라운드 손 트레이싱 `H-25`] `quad-types`의 `Quad` - 타입에 `Dispatch` 필드와 그 타입 재수출을 추가** — `Quad`가 5필드 닫힌 - 레코드이고 `RunInit`은 반환값이 없어 타입을 못 넓히므로, 이걸 안 하면 - `module:RunInit(InitDispatch)` 뒤의 `quad.Dispatch` 접근이 **런타임엔 - 되는데 `luau-analyze`에선 타입에러**다(실측 재현). `base/architecture.md`의 - 확정된 결정 13번이 그 접근을 표준 사용법으로 못박아뒀다. - 상세는 `base/quad-types-plan.md`의 "`Quad` 타입 — 확정된 표면" 절. - **⚠️ 이건 M2 한 번으로 끝나는 일이 아니다** — 그 문서가 *"마일스톤마다 - 갱신한다 … 이후 서브시스템도 같은 규칙을 따른다"*로 확정했으므로, - **서브시스템을 붙이는 모든 마일스톤이 같은 항목을 진다**: - M3(`Source`/`State`/`Store`) · M6(`Slot`) · M7(`Modifier`) · - M8(`Ref`) · M10(`Tag`/`Attribute`). 빠뜨리면 그 마일스톤 완료 후 - `quad.Store`/`quad.Slot` 접근이 런타임엔 되는데 `luau-analyze`에선 - 타입에러인, `H-25`가 실측한 그 문제가 **마일스톤마다 반복된다.** -- [ ] `Handler.luau`(핸들러 계약 타입: `isHandlable(inst,k,v)`/`priority`/ - `process(inst,k,v,index) -> (hintValue)->()` **3종** — `isHandlable`도 - `inst`를 받도록 확정(2026-08-07 여덟 번째 세션), 별도 `retract` 필드는 - `process` 반환값으로 합쳐짐(2026-08-13 다섯 번째 세션)) - [ ] `Brand.luau`(**[2026-08-21 재작성]** 인스턴스 브랜드 — `Brand()`가 브랜드마다 weak-key 집합 하나를 들고 `:register(x)`/`:is(x)`, **다중 태깅 허용**(`Source`가 `SourceBrand`이면서 동시에 `EpochBrand`). @@ -294,49 +259,12 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 array-part `v`를 판별, `isTag`와 같은 결)로 분리됨 — 그룹 `Attribute(...)` 프리미티브 신설로 같은 이름이 서로 다른 두 대상(키 vs 값)을 가리키게 돼서 갈라짐, `base/attribute-plan.md` - 참고) 전부의 기반. `isNone`만 예외로 레지스트리 없이 `x == None` - 항등 비교 — `brand-plan.md`의 `Brand` 절, 2026-08-07 여덟 - 번째 세션 신설) -- [ ] **[2026-08-22 M3에서 이동] `EpochMap.luau`** — 재사용 가능한 에포크 - 부기 객체(`:Update(Epoch|EpochSet) -> boolean`이 "뒤로 전파가 - 필요한가"를 답함, `:Refresh`/`:Sync`/`:TrackFrom`. `EpochSet = - {[Epoch]: true}`로 **배열이 아니라 집합**). `State.luau`에 묻지 말고 - 별도 모듈로 낼 것 — `GateNode`(아래)와 `State`(M3)/`Effect`(M3)가 - 전부 같은 것을 쓴다. `Epoch` 인터페이스 자체(`{ Revision: number }`)와 - 리비전 갱신(`bit32.bnot(-rev)`)도 여기서 확정 — `base/state-epoch-plan.md` -- [ ] **[2026-08-22 M3에서 이동] `state:Gate(setup)` + `GateNode`** — - emit을 가로채 유보했다가 한 번에 내보내는 공용 게이트 노드 - (`ComputeNode`와 같은 층위, 탑레벨 `Gate(...)` 프리미티브는 안 만듦). - 유보 배치는 `withheld : { [epoch] : true }`(집합), flush 때 테이블을 - 통째로 갈고, **내보내는 emit이 싣는 건 그렇게 떼어낸 `EpochSet` - 스냅샷뿐이다 — 게이트 노드 자신은 안 싣는다**(하류가 게이트 identity를 - 한 번도 안 쓴다, `base/state-epoch-plan.md` §5). **빈 배치는 - 아무것도 안 함**. `emitEpochMap`을 쓰므로 위 `EpochMap.luau`가 선행 — - `base/gate-plan.md`. **"게이팅 먼저" 결정으로 M3에서 앞당겨진 항목** - (사용자: *"게이팅 먼저. 게이팅을 base 에 만들 준비를 해야한다"*) -- [ ] **[2026-08-22 M3에서 이동] `Blocker.luau`** — 위 `GateNode` 위에 - 얹히는 **정책**(다시 노드를 만들지 말 것). 여러 Source를 한꺼번에 - 바꿔도 파생값 재계산/재대입이 한 번만 되게 하는 primitive - (`base/blocker-plan.md`). 아래 `Dispatch.setLength`/`setOffsetSource`의 - 배치 등록이 `:On()`/`:IsOn()`/`:OffWithoutEmit()` **세 메서드**를 - 호출하므로(`base/gate-plan.md` 9번이 소스 — Blocker 인스턴스를 lazy - 조회하는 `getBlocker(ownerKey)`는 Blocker 메서드가 아니라 Dispatch - 쪽 헬퍼다) **최소한 그 셋이 도는 형태까지는 M2에 필요** -- [ ] **[2026-08-22 M7에서 이동] `None.luau`(센티널) + `Dispatch/None.luau` - (`NoneHandler` + `NilHandler`)** — `None`은 modifier 전용 값이 아니라 - 디스패치 배관이라 여기 소속(`architecture.md`의 소스 트리에 있는 건 - 핸들러 쪽 `Dispatch/None.luau`뿐 — **[2026-08-22] 탑레벨 센티널 - `None.luau`와 `Brand.luau`는 그 트리에 아직 줄이 없다**, M2 구현 시 - 트리도 같이 채울 것). - `NoneHandler`는 배열/해시 구분 없이 `Dispatch.process(inst,k,nil,index+1)` - **재귀만** 하고(선행 `retractFrom` 없음 — 하강 diff), 실제 정리는 - `NilHandler`(`isHandlable`이 `type(k) == "number" and v == nil`일 때만 - 매치하는 말단)가 `Dispatch.setLength(inst,k,0)` + - `Dispatch.setOffsetSource(inst,k,None)` 등록으로 맡는다. - **`Dispatch.drive`에 `None` 스킵 분기는 없다**(반응형 값이 내놓는 - `None`은 어차피 `process`에 도착하므로 — 2026-08-18 재설계) — - `base/dispatch-core-plan.md`의 "`None` 센티널"/"`NilHandler`" 절. - Modifier 쪽 표면(인라인 키로 필드 지우기, `Peek` 반환 타입)은 M7 + 참고) 전부의 기반. `isNone`은 레지스트리를 안 쓰고 `x == None` 항등 + 비교이지만 **`Brand.luau`가 `None`을 참조하지는 않는다** + (**[2026-08-18 정정]** `brand-plan.md`가 *"`Brand → None` 의존을 만들지 + 않는다"*로 확정 — `isNone`은 `None.luau`(M3) 쪽에 산다. 이 마일스톤 + 분리로 처음 눈에 띈 잔재를 **[2026-08-24]** 정정한 것) — + `brand-plan.md`의 `Brand` 절, 2026-08-07 여덟 번째 세션 신설) - [ ] `Relate.luau`(전체가 quad-base, 순수 Lua — `base/relate-plan.md`) — `Relate()` 비싱글톤 생성자, `:SetWeak`/`:GetWeak`/`:SetStrong`/`:GetStrong`. `inst`(첫 인자)는 항상 weak, `StrongMap`/`WeakMap` 서브테이블은 lazy @@ -373,9 +301,259 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 `canExecute`/`unbindLifetime` — 확정" 절 참고. **이중 바인딩 금지 게이트는 `canBound`**(`canExecute`는 emit 전파 게이팅 전용 — **[2026-08-14 열한 번째 세션] `canBound`가 별도 진입점으로 재도입되어 - 다시 갈라짐, 판정 로직은 공유하는 비공개 헬퍼 하나 — M3 체크박스 + 다시 갈라짐, 판정 로직은 공유하는 비공개 헬퍼 하나 — M2 체크박스 참고**), children 배열 leaf 부착이 실제로는 `bindLifetime` 호출이라 이 게이트를 그대로 탐 + +### 반응형 본체 + +- [ ] **`EpochMap.luau`** (**[2026-08-24]** 2026-08-22에 디스패치로 옮겼다가 순서 교체로 되돌아옴) — 재사용 가능한 에포크 + 부기 객체(`:Update(Epoch|EpochSet) -> boolean`이 "뒤로 전파가 + 필요한가"를 답함, `:Refresh`/`:Sync`/`:TrackFrom`. `EpochSet = + {[Epoch]: true}`로 **배열이 아니라 집합**). `State.luau`에 묻지 말고 + 별도 모듈로 낼 것 — `GateNode`(아래)와 `State`/`Effect`가 + 전부 같은 것을 쓴다. `Epoch` 인터페이스 자체(`{ Revision: number }`)와 + 리비전 갱신(`bit32.bnot(-rev)`)도 여기서 확정 — `base/state-epoch-plan.md` +- [ ] **[2026-08-21 5라운드 — 채택 확정, 같은 날 `Epoch`로 일반화]** State의 + 재계산/전파 판정은 **`Epoch` 리비전 비교**다(`base/state-epoch-plan.md`) + — `invalid` 플래그가 아니다. **아래 `Source.luau`/`State.luau`가 이걸 + 전제로 짜여야 하므로 `EpochMap.luau`(위 항목)가 State 본체보다 먼저 + 온다**(`State`가 `valueEpochMap`/`emitEpochMap` 둘을 컴포지션한다 — + **[2026-08-24] 감사에서 순서가 반대로 놓여 있던 걸 잡아 앞으로 옮김**): `Source`가 `type Epoch = { Revision: number }`를 + 구조적으로 만족하고(`EpochBrand`에도 등록), 부기는 재사용 가능한 + **`EpochMap`**(`:Update(Epoch|EpochSet) -> boolean`, `:Refresh`, `:Sync`, + `:TrackFrom` — `EpochSet = {[Epoch]: true}`, **배열 아님**) + 으로 떼어내며, State가 그걸 **둘** 컴포지션한다 — `valueEpochMap`(값 + 유효성)/`emitEpochMap`(전파 dedup). emit은 값도 리비전도 안 싣고 + **출처(`Epoch`나 그 집합)만** 싣고, 순회는 `rawInvalid == false`일 때만 + 돌며 **값만 앞당기고 통지는 상류 emit을 기다린다**. 다이아몬드 중복 + 통지가 접히므로 스파이크 `05`도 그에 맞춰 재작성해야 한다 + (`luau-test/STATUS.md`). +- [ ] `Source.luau`/`State.luau`/`Store.luau` +- [ ] **[2026-08-18 신설]** `store:GetDynamic<>(name): Source` — 런타임에 + 이름이 정해지는 동적 키의 정식 창구(옛 `store "key"` 문자열 커링은 + 기각). **⚠️ 콜론 메소드로 두면 `__index`가 고정 메소드 테이블을 먼저 + 확인해야 하고 `GetDynamic`이 예약 키가 됨** — 탑레벨 함수로 둘지 + 아직 미결(`base/store-plan.md`의 "타입 추론 문제" 절, `question.md` 최우선 절) +- [ ] **State 전파 루프 — 구독자는 weak, 발화마다 `canExecute` 게이팅** + (2026-08-14 다섯 번째 세션 확정, `base/lifecycle-pattern.md`의 "실제 + 호출부 — State 전파(`emit`)가 `canExecute`로 게이팅한다" 절) — + State는 구독자(Observer의 emit 클로저)를 **weak로만** 담고, 살려두는 + 책임은 `gchold`(leaf) 또는 전역 `Subscribed` 테이블(전역)에 있음 + (어디에도 안 묶인 Observer는 GC되어 목록에서 자연히 빠짐). 발화 시 + 각 구독자에 대해 `canExecute(observer)`가 거짓이면 **그 구독자만 + 조용히 건너뜀**(no-op) — 이게 `canExecute`의 유일한 실제 호출부이고, + `inst`를 인자로 받을 수 없는 이유(State는 자기가 어느 Instance에 + 걸렸는지 모름). `state:Observer(fn)`의 "등록 즉시 1회 실행"은 + `bindLifetime` 이전에 동기적으로 일어나므로 이 게이팅과 무관 +- **[확정된 것 — 코드 아님]** `store.key` dot-access 타입 추론 확인 — Luau `type function` + (`WrapStore`/`ProcessStoreType`)으로 `Store`가 `T`의 각 필드를 + `Source`로 감싼 레코드 타입을 합성 가능함을 확인(2026-08-12 열일곱 + 번째 세션, `base/typing-limits.md` §5) — **[2026-08-15 실측 완료]** + `luau-test/done/16-type-store-key-typefunction.luau` 통과(원인은 + 설계 문제가 아니라 `types.newfunction` API 버전 드리프트였음) +- [ ] `:Compute(fn, ...)` — trailing args로 추가 의존성 직접 받는 sugar + (2026-08-11 세션, `base/source-state-plan.md` "`:Compute(fn, ...)`" + 절) — `:With(...):Compute(fn)` 체인과 달리 노드 1개(Compute 노드 + 자신에 구독만 추가)로 끝나야 함, 새 노드 생성 없이 구현되는지 M0/M2 + 스파이크에서 확인. **[2026-08-24 정정, 6라운드 `H-13`]** 여기 원래 + *"`Effect`/`Observer`는 대칭 sugar 없이 `:With` 명시 유지"*라고 적혀 + 있었는데 **`Effect`는 `C-6`에서 이미 역전됐다** — 각 dep에 구독을 따로 + 걸면 합치는 노드 자체가 안 생겨 감출 비용이 없다. **기각으로 남은 건 + `Observer` 하나**이고 근거도 새로 쓰였다("Observer는 리시버 State + 하나에 붙는 구독, 여럿을 엮는 건 Effect가 대신한다") +- [ ] `state:Apply(factory)`(`base/source-state-plan.md` "`state:Apply(factory)`" + 절, 2026-08-07 일곱 번째 세션) — `factory(self)`를 체이닝 문법으로 + 부르는 순수 설탕, `factory: (State) -> U): U`로 열린 타입. Source도 + 기존 `:With`/`:Compute` 델리게이션에 얹혀 자동 포함 +- [ ] `state:Observer(fn)` — children 배열 leaf 참가자, **등록 즉시 1회 + 실행 확정**(`base/source-state-plan.md`의 Observer 절), `isObserver` + 판별자, canExecute 게이팅, `:Subscribe()`/`:Unsubscribe()`. **동적 + 경로 가드**(`{priority = HANDLER_PRIORITY_FALLBACK, isHandlable = v + is Observer, process = error(...)}`, `k` 타입 안 가림, 2026-08-14 + 열한 번째 세션 — `PreRef`와 같은 패턴)도 같이 등록 + **⚠️ [2026-08-24] 단 그 가드를 `Dispatch.addHandler`로 등록하는 것 + 자체는 M3다** — 레지스트리가 거기서 생긴다(M3의 그 항목). +- [ ] `Effect(fn, ...deps)`(`base/effect-plan.md`, **[2026-08-21 5라운드 + `C-6`]** 옛 시그니처는 `Effect(fn, state?)`) — deps 생략 시 설치 + 1회+leaf 사망 시 확정 정리, deps 지정 시 **각각에 맞는 구독** + (State/Source는 `Observer`, `Ref`는 `:Callback`)을 걸어 + 재실행+cleanup 체이닝(React `useEffect` 동형). **`EffectHandle`이 + `EpochMap`을 하나 들어** 공통 상류로 인한 중복 발화를 접고, 설치 구간 + 억제 플래그가 그 `Update`보다 먼저 와야 함 + (`base/state-epoch-plan.md`). Observer 구현 이후에 착수(의존 관계). + `EffectHandle:Subscribe()`/`:Unsubscribe()`도 추가(leaf 없이 쓰는 + 모듈/스크립트 레벨 Effect) — `:Unsubscribe()`는 Observer와 달리 + 마지막 cleanup을 1회 트리거해야 함(2026-08-07 일곱 번째 세션). + **동적 경로 가드**도 Observer와 같은 패턴으로 등록(`base/effect-plan.md` + "동적 경로 가드" 절, 2026-08-14 열한 번째 세션) + **⚠️ [2026-08-24] 단 그 가드를 `Dispatch.addHandler`로 등록하는 것 + 자체는 M3다** — 레지스트리가 거기서 생긴다(M3의 그 항목). +- [ ] **⭐ [2026-08-24 신설, 6라운드] `Effect` 구현 시 같이 만들 것** — + **`handle._observers`**(배열, 단수 `_observer`에서 바뀜 `H-8`) · + **`handle._cleanup`**(직전 cleanup 보관, `Rerun`과 `Destroying` 클로저가 + 같은 자리를 읽는다) · **`handle._refDeps`/`_refCallbacks`**(`Ref` dep과 + 거기 건 클로저 — 해제 시 값으로 떼야 해서 보관 필요) · + **`handle._destroyConn`** · **`:_bindDestroying(inst)`/`:_unbindDestroying()`** + (`bindLifetime`/`unbindLifetime`이 `isEffect`일 때 부르는 훅, `H-11`). + `fn` 시그니처는 **`fn(self: EffectHandle) -> (() -> ())?`**이고 + **`...deps`는 `fn`에 안 넘어간다**(`H-14`). `Ref` 콜백은 본문 맨 앞에서 + **`canExecute(handle)`를 확인**한다(`H-7`). 의사코드는 + `base/effect-plan.md`가 소스 +- [ ] **[2026-08-24 `H-23`]** State 전파 루프는 구독자 집합을 **배열로 + 스냅샷한 뒤** 돈다 — 순회 중 새 구독자 추가가 정상 경로인데 Lua에서 + 미정의라, 실측에서 실행마다 결과가 달라지고 한 Observer가 통째로 + 누락됐다. "이번 파동 중에 붙은 구독자는 다음 파동부터"가 계약 +- [ ] Observer/Effect 이중 바인딩 금지 — `canBound(value)` 게이트로 + `:Subscribe()`(전역)와 `bindLifetime`(inst-scoped, leaf 부착도 + 내부적으로 이걸 호출)이 동시에 걸리면 즉시 `error`(`base/source-state-plan.md` "이중 바인딩 금지" 절, 2026-08-07 일곱 번째 + 세션 신설, 2026-08-09 여섯 번째 세션에서 "leaf 부착=bindLifetime + 호출"로 정정 — 진짜 독립 경로는 둘뿐). + **[2026-08-14 다섯 번째 세션에 별도 predicate `canBound(handle)`을 + 폐기하고 `canExecute` 하나로 합쳤다가, 같은 날 열한 번째 세션에 + 다시 갈라짐]** — "지금 묶어도 되는가"(bound 문맥)와 "지금 + 발화해도 되는가"(execute 문맥)는 호출부의 질문이 다르고 + **[2026-08-18 구현 전 QA 정정] 판정값도 같은 게 아니라 서로의 + 부정**이라(`canBound(v) == not canExecute(v)`, 게이트는 항상 + `if not canBound(v) then error(...)`), `Ref` 이중 배치 + 방지(`question.md` 0-W)를 계기로 `canBound`가 + 별도 진입점으로 재도입됨 — 판정 로직(비공개 `isBoundAlive` 헬퍼)은 + 공유해 코드 중복은 없음. **이 절이 쓰는 게이트는 이제 `canBound`** + (emit 전파 게이팅 전용 `canExecute`가 아님). `.Subscribed` 필드가 + leaf 경로와 무관하다는 것, leaf 생존 판정을 `bindLifetime`이 `value` + 쪽 `Relate`에 복사해둔 gcconn으로 하는 것은 안 바뀜 — `base/ + lifecycle-pattern.md`의 "`canBound` vs `canExecute`" 절, 역전 경위는 + `archive/canexecute-inst-arg-reversed.md`. 부수 효과로 **바인딩이 + 죽은 뒤(`Destroy`/`unbindLifetime`)의 재사용은 게이트를 통과** + (살아있는 바인딩만 막는 게 의도, 안 바뀜) +- [ ] **`state:Gate(setup)` + `GateNode`** (**[2026-08-24]** 위 `EpochMap`과 같이 되돌아옴) — + emit을 가로채 유보했다가 한 번에 내보내는 공용 게이트 노드 + (`ComputeNode`와 같은 층위, 탑레벨 `Gate(...)` 프리미티브는 안 만듦). + 유보 배치는 `withheld : { [epoch] : true }`(집합), flush 때 테이블을 + 통째로 갈고, **내보내는 emit이 싣는 건 그렇게 떼어낸 `EpochSet` + 스냅샷뿐이다 — 게이트 노드 자신은 안 싣는다**(하류가 게이트 identity를 + 한 번도 안 쓴다, `base/state-epoch-plan.md` §5). **빈 배치는 + 아무것도 안 함**. `emitEpochMap`을 쓰므로 위 `EpochMap.luau`가 선행 — + `base/gate-plan.md`. **"게이팅 먼저" 결정의 산물** + (사용자: *"게이팅 먼저. 게이팅을 base 에 만들 준비를 해야한다"*) +- [ ] **`Blocker.luau`** (**[2026-08-24]** 위 둘과 같이 되돌아옴) — 위 `GateNode` 위에 + 얹히는 **정책**(다시 노드를 만들지 말 것). 여러 Source를 한꺼번에 + 바꿔도 파생값 재계산/재대입이 한 번만 되게 하는 primitive + (`base/blocker-plan.md`). **M3의** `Dispatch.setLength`/`setOffsetSource`의 + 배치 등록이 `:On()`/`:IsOn()`/`:OffWithoutEmit()` **세 메서드**를 + 호출하므로(`base/gate-plan.md` 9번이 소스 — Blocker 인스턴스를 lazy + 조회하는 `getBlocker(ownerKey)`는 Blocker 메서드가 아니라 Dispatch + 쪽 헬퍼다) **최소한 그 셋이 도는 형태까지는 M3(디스패치)가 요구** +- [ ] **[2026-08-24 `H-25` 파생]** `quad-types`의 `Quad`에 `Source`/`State`/ + `Store` 필드 추가 — **규칙 요지**: `New(): Quad`가 닫힌 레코드이고 + `RunInit`은 반환값이 없어 타입을 못 넓히므로, 서브시스템을 붙이는 + 마일스톤마다 `quad-types`의 `Quad`에 그 필드와 타입 재수출을 같이 + 추가한다. 안 하면 그 마일스톤 완료 후 `quad.Store` 접근이 런타임엔 + 되는데 `luau-analyze`에선 타입에러다(`H-25` 실측). **순서 교체로 + 이 마일스톤이 그 규칙의 첫 적용 지점**이고, 규칙의 정본은 + `base/quad-types-plan.md`의 "`Quad` 타입 — 확정된 표면" 절 + (M3의 `H-25` 항목이 같은 규칙을 `Dispatch` 기준으로 서술한다) +- [ ] trailing deps를 `fn`에 lazy positional 인자로도 노출(**⚠️ [2026-08-24 + `H-14`] 이 항목은 `:Compute` 한정이다** — `Effect`의 `fn`엔 deps가 안 + 넘어간다) — (`fn(self, + previous?, dep1, ..., depN)` — 순서는 Luau 값 레벨 `...`가 파라미터 + 리스트 맨 끝이어야 하는 것과 같은 이유로 `previous?`가 deps 팩 + **앞**에 와야 함, 2026-08-11 후속 세션 제안 → 같은 날 세 번째 + 세션에 순서 정정, `base/source-state-plan.md` "trailing deps를 fn에 + lazy positional 인자로도 노출" 절) — 방향/순서는 확정, + `luau-test`의 `15-type-compute-trailing-deps-typepack.luau`로 + 이형 다중 deps를 제네릭 타입 팩으로 표현 가능한지만 실측 필요(안 + 되면 동종 타입 dep 1개로 한정) +- [ ] mock 대상 테스트 + +## M3 — 디스패치 엔진 + +> **✅ [2026-08-13 열네 번째 세션] 재디스패치 모델 교체 완료 — 아래 +> 체크리스트는 새 모델("하강 diff") 기준으로 갱신됐습니다.** 래핑 핸들러의 +> 선행 `retractFrom`은 폐기됐고, `Dispatch.process`가 슬롯의 `handler`를 +> 먼저 비교해 (같으면 그 자리 클로저에 새 값을 넘기고 자기 `process` +> 재호출, 다르면 그 자리부터 전량 철거) 처리합니다. 정본은 +> `.claude/base/dispatch-core-plan.md`(같은 세션에 `bind-system-plan.md`에서 +> 분리 신설), 뒤집힌 옛 모델은 +> `.claude/archive/dispatch-hintvalue-model-reversed.md`. + +> **⭐ [2026-08-24 순서 교체] 이 마일스톤은 옛 M2입니다** — 번호를 반응형 +> 코어와 맞바꿔 그 뒤로 옮겼습니다(경위는 M2 배너가 소스, 여기서 반복하지 +> 않음). 2026-08-22에 여기로 옮겼던 `EpochMap`/`GateNode`/`Blocker`와, +> 반응형이 먼저 요구하는 `Brand`/`Relate`/`LifetimeHandle` 인터페이스는 +> M2로 갔습니다. 여기 남은 것은 전부 디스패치 배관이고, **이제 이 +> 마일스톤은 M2 위에 단방향으로 얹힙니다.** 개념상 역방향이던 둘 +> (Observer/Effect 가드 등록, `ObserverEffectLeafHandler` — "핸들러를 +> 등록한다"뿐인 것)은 **맨 아래 두 항목으로 여기 옮겨져** 있으므로, +> **빌드 순서상 역방향 간선은 없습니다.** + + +- [ ] `Dispatch/init.luau` — `Dispatch.getHandler(inst,k,v): Handler?`(순수 + 스캔, `isHandlable`+`priority`) / `Dispatch.process(inst,k,v,index)` + (오케스트레이터: getHandler → **그 인덱스의 기존 핸들러와 비교** → + 같으면 그 자리 클로저에 새 값을 넘기고 같은 핸들러의 `.process`로 + 자리 교체, 다르면 `retractFrom` 후 새로 설치. 반환값이 `nil`이면 + 즉시 error) / **3-인자** `Dispatch.retractFrom(inst,k,index)` + (아래 항목) / `Dispatch.addHandler(handler)`(레지스트리 등록, + quad-roblox가 팩토리 뮤테이션 시점에 호출) / `Dispatch.drive(inst, + flattened)`(**일반화 `for` 한 번**으로 순회하며 각 `(k,v)`에 + `Dispatch.process(inst,k,v,1)` 호출 — `dispatch-core-plan.md`의 `None` + 센티널 절, 2026-08-07 여덟 번째 세션에 네이밍 확정). + **[2026-08-21 구현 전 QA 4라운드 `F-4-1`] 순회는 두 패스가 아니라 + 단일 일반화 `for`다** — "배열 파트 전체가 해시 파트보다 먼저"는 여전히 + base가 보장하는 **계약**이지만, `flattened`가 항상 평범한 Luau + 테이블이라 일반화 `for` 한 번이 그 순서를 그대로 주고 두 층위는 + `type(k) == "number"` 분기로 가른다(`base/dispatch-core-plan.md`의 + `F-4-1` 정정 문단). 옛 "명시적 두 패스" 서술은 구현까지 2회 순회로 + 못박은 것처럼 읽혀 정정됨 — 그 때문에 스파이크 `01`도 재작성 대기 + (`luau-test/STATUS.md`) +- [ ] **⭐ [2026-08-24 신설, 6라운드 손 트레이싱 `H-39`] 말단 핸들러는 예외 + 없이 자기 배열 자리의 `setOffsetSource(inst,k,None)` → `setLength(inst,k,0)`을 + 등록한다** — 이 계약을 핸들러 작성 체크리스트에 넣고, 실제로 넷이 + 빠져 있었으니 각 마일스톤에서 확인할 것: `TagHandler`(M10) / + `AttributeGroupHandler`(M10) / `RefLeafHandler`(M8) / + `ObserverEffectLeafHandler`(이 마일스톤, 맨 아래). 안 하면 `bk.N`이 다른 자리 등록으로 + 커질 때 그 구멍이 범위 안에 끼어 `recompute`가 **명시적 error로 죽는다** + (`Frame { Tag("card"), TextLabel{} }`처럼 말단이 앞에 오는 흔한 배치). + **배열 맨 끝이면 안 터지므로 "가끔 되고 가끔 터지는" 형태로 드러난다.** + 소스는 `base/dispatch-core-plan.md`의 등록 책임 절 +- [ ] **⭐ [2026-08-24 신설, 6라운드 손 트레이싱 `H-25`] `quad-types`의 `Quad` + 타입에 `Dispatch` 필드와 그 타입 재수출을 추가** — `Quad`가 5필드 닫힌 + 레코드이고 `RunInit`은 반환값이 없어 타입을 못 넓히므로, 이걸 안 하면 + `module:RunInit(InitDispatch)` 뒤의 `quad.Dispatch` 접근이 **런타임엔 + 되는데 `luau-analyze`에선 타입에러**다(실측 재현). `base/architecture.md`의 + 확정된 결정 13번이 그 접근을 표준 사용법으로 못박아뒀다. + 상세는 `base/quad-types-plan.md`의 "`Quad` 타입 — 확정된 표면" 절. + **⚠️ 이건 M3 한 번으로 끝나는 일이 아니고, 첫 적용은 오히려 M2다** — + 그 문서가 *"마일스톤마다 갱신한다 … 이후 서브시스템도 같은 규칙을 + 따른다"*로 확정했으므로, **서브시스템을 붙이는 모든 마일스톤이 같은 + 항목을 진다**: **M2(`Source`/`State`/`Store`, 규칙이 처음 적용되는 + 자리)** · M6(`Slot`) · M7(`Modifier`) · M8(`Ref`) · + M10(`Tag`/`Attribute`). (**[2026-08-24]** 규칙 자체는 `Dispatch`를 + 계기로 쓰였지만 마일스톤 순서 교체로 M2가 앞에 오게 됐다 — M2를 짜는 + 사람이 이 항목을 앞으로 참조하지 않아도 되게, 규칙 요지는 M2의 + `H-25` 파생 항목에도 적어뒀다.) 빠뜨리면 그 마일스톤 완료 후 + `quad.Store`/`quad.Slot` 접근이 런타임엔 되는데 `luau-analyze`에선 + 타입에러인, `H-25`가 실측한 그 문제가 **마일스톤마다 반복된다.** +- [ ] `Handler.luau`(핸들러 계약 타입: `isHandlable(inst,k,v)`/`priority`/ + `process(inst,k,v,index) -> (hintValue)->()` **3종** — `isHandlable`도 + `inst`를 받도록 확정(2026-08-07 여덟 번째 세션), 별도 `retract` 필드는 + `process` 반환값으로 합쳐짐(2026-08-13 다섯 번째 세션)) +- [ ] **[2026-08-22 M7에서 이동] `None.luau`(센티널) + `Dispatch/None.luau` + (`NoneHandler` + `NilHandler`)** — `None`은 modifier 전용 값이 아니라 + 디스패치 배관이라 여기 소속(`architecture.md`의 소스 트리에 있는 건 + 핸들러 쪽 `Dispatch/None.luau`뿐 — **[2026-08-22] 탑레벨 센티널 + `None.luau`와 `Brand.luau`는 그 트리에 아직 줄이 없다**, M3 구현 시 + 트리도 같이 채울 것). + `NoneHandler`는 배열/해시 구분 없이 `Dispatch.process(inst,k,nil,index+1)` + **재귀만** 하고(선행 `retractFrom` 없음 — 하강 diff), 실제 정리는 + `NilHandler`(`isHandlable`이 `type(k) == "number" and v == nil`일 때만 + 매치하는 말단)가 `Dispatch.setLength(inst,k,0)` + + `Dispatch.setOffsetSource(inst,k,None)` 등록으로 맡는다. + **`Dispatch.drive`에 `None` 스킵 분기는 없다**(반응형 값이 내놓는 + `None`은 어차피 `process`에 도착하므로 — 2026-08-18 재설계) — + `base/dispatch-core-plan.md`의 "`None` 센티널"/"`NilHandler`" 절. + Modifier 쪽 표면(인라인 키로 필드 지우기, `Peek` 반환 타입)은 M7 - [ ] `Dispatch.setLength(ownerKey,i,len:number|State,anchor?)`/ `Dispatch.setOffsetSource(ownerKey,i,offset:Source|None)`/ **`Dispatch.getOffsetAt(ownerKey,i)`** — @@ -418,9 +596,12 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 (`:On()`/`:IsOn()`/`:OffWithoutEmit()` 세 메서드 + 그 인스턴스를 lazy 조회하는 Dispatch 쪽 헬퍼 `getBlocker(ownerKey)`)를 호출하는 이유는 크래시 방지가 아니라 배치 등록 비용(O(N²)→O(N)) 절감. - **[2026-08-22] 그래서 필요한 `GateNode`/`Blocker`/`EpochMap`은 이제 - 이 마일스톤에 체크박스로 있다**(위 세 항목) — 예전엔 M3에 있는 걸 - 각주로 가리키기만 했다. 결정 경위("게이팅 먼저", 그리고 앞당기는 + **[2026-08-24 정정] 그래서 필요한 `GateNode`/`Blocker`/`EpochMap`은 + M2(반응형 코어)에 체크박스로 있다** — 2026-08-22엔 각주로만 예고돼 + 있던 걸 이 마일스톤으로 앞당겼었는데, 같은 달 24일 마일스톤 순서 + 교체로 M2가 먼저 지어지게 되면서 셋 다 그리로 되돌아갔다(앞당길 이유가 + 사라짐). 여기서는 **M2가 이미 만들어둔 것을 호출하기만 한다.** + 결정 경위("게이팅 먼저", 그리고 앞당기는 대상이 `Blocker` 자체가 아니라 그 아래 공용 `Gate` 노드로 바뀐 것)는 `qa-request/pre-implementation-qa-round5-followup.md`와 `base/gate-plan.md`가 소스 — 여기서 반복하지 않는다. @@ -471,137 +652,20 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 `Dispatch.drive`의 진입도 항상 `1` - **소유권 충돌 감지는 Dispatch의 일이 아님**(옛 점유 error 폐지) — 필요한 도메인이 직접(Attribute 이름 claim, M10) -- [ ] mock 대상 테스트 - -## M3 — Store/State/Source - -- [ ] `Source.luau`/`State.luau`/`Store.luau` -- [ ] **[2026-08-18 신설]** `store:GetDynamic<>(name): Source` — 런타임에 - 이름이 정해지는 동적 키의 정식 창구(옛 `store "key"` 문자열 커링은 - 기각). **⚠️ 콜론 메소드로 두면 `__index`가 고정 메소드 테이블을 먼저 - 확인해야 하고 `GetDynamic`이 예약 키가 됨** — 탑레벨 함수로 둘지 - 아직 미결(`base/store-plan.md`의 "타입 추론 문제" 절, `question.md` 3번) -- [ ] **State 전파 루프 — 구독자는 weak, 발화마다 `canExecute` 게이팅** - (2026-08-14 다섯 번째 세션 확정, `base/lifecycle-pattern.md`의 "실제 - 호출부 — State 전파(`emit`)가 `canExecute`로 게이팅한다" 절) — - State는 구독자(Observer의 emit 클로저)를 **weak로만** 담고, 살려두는 - 책임은 `gchold`(leaf) 또는 전역 `Subscribed` 테이블(전역)에 있음 - (어디에도 안 묶인 Observer는 GC되어 목록에서 자연히 빠짐). 발화 시 - 각 구독자에 대해 `canExecute(observer)`가 거짓이면 **그 구독자만 - 조용히 건너뜀**(no-op) — 이게 `canExecute`의 유일한 실제 호출부이고, - `inst`를 인자로 받을 수 없는 이유(State는 자기가 어느 Instance에 - 걸렸는지 모름). `state:Observer(fn)`의 "등록 즉시 1회 실행"은 - `bindLifetime` 이전에 동기적으로 일어나므로 이 게이팅과 무관 -- **[확정된 것 — 코드 아님]** `store.key` dot-access 타입 추론 확인 — Luau `type function` - (`WrapStore`/`ProcessStoreType`)으로 `Store`가 `T`의 각 필드를 - `Source`로 감싼 레코드 타입을 합성 가능함을 확인(2026-08-12 열일곱 - 번째 세션, `base/typing-limits.md` §5) — **[2026-08-15 실측 완료]** - `luau-test/done/16-type-store-key-typefunction.luau` 통과(원인은 - 설계 문제가 아니라 `types.newfunction` API 버전 드리프트였음) -- [ ] `:Compute(fn, ...)` — trailing args로 추가 의존성 직접 받는 sugar - (2026-08-11 세션, `base/source-state-plan.md` "`:Compute(fn, ...)`" - 절) — `:With(...):Compute(fn)` 체인과 달리 노드 1개(Compute 노드 - 자신에 구독만 추가)로 끝나야 함, 새 노드 생성 없이 구현되는지 M0/M3 - 스파이크에서 확인. **[2026-08-24 정정, 6라운드 `H-13`]** 여기 원래 - *"`Effect`/`Observer`는 대칭 sugar 없이 `:With` 명시 유지"*라고 적혀 - 있었는데 **`Effect`는 `C-6`에서 이미 역전됐다** — 각 dep에 구독을 따로 - 걸면 합치는 노드 자체가 안 생겨 감출 비용이 없다. **기각으로 남은 건 - `Observer` 하나**이고 근거도 새로 쓰였다("Observer는 리시버 State - 하나에 붙는 구독, 여럿을 엮는 건 Effect가 대신한다") -- [ ] **⭐ [2026-08-24 신설, 6라운드] `Effect` 구현 시 같이 만들 것** — - **`handle._observers`**(배열, 단수 `_observer`에서 바뀜 `H-8`) · - **`handle._cleanup`**(직전 cleanup 보관, `Rerun`과 `Destroying` 클로저가 - 같은 자리를 읽는다) · **`handle._refDeps`/`_refCallbacks`**(`Ref` dep과 - 거기 건 클로저 — 해제 시 값으로 떼야 해서 보관 필요) · - **`handle._destroyConn`** · **`:_bindDestroying(inst)`/`:_unbindDestroying()`** - (`bindLifetime`/`unbindLifetime`이 `isEffect`일 때 부르는 훅, `H-11`). - `fn` 시그니처는 **`fn(self: EffectHandle) -> (() -> ())?`**이고 - **`...deps`는 `fn`에 안 넘어간다**(`H-14`). `Ref` 콜백은 본문 맨 앞에서 - **`canExecute(handle)`를 확인**한다(`H-7`). 의사코드는 - `base/effect-plan.md`가 소스 -- [ ] **[2026-08-24 `H-39`]** `ObserverEffectLeafHandler.process`가 자기 배열 - 자리의 `setOffsetSource(inst,k,None)`/`setLength(inst,k,0)`을 등록 — - 빠져 있었다(위 M2의 그 항목) -- [ ] **[2026-08-24 `H-23`]** State 전파 루프는 구독자 집합을 **배열로 - 스냅샷한 뒤** 돈다 — 순회 중 새 구독자 추가가 정상 경로인데 Lua에서 - 미정의라, 실측에서 실행마다 결과가 달라지고 한 Observer가 통째로 - 누락됐다. "이번 파동 중에 붙은 구독자는 다음 파동부터"가 계약 -- [ ] **[2026-08-24 `H-25` 파생]** `quad-types`의 `Quad`에 `Source`/`State`/ - `Store` 필드 추가(위 M2 항목의 "마일스톤마다" 규칙) -- [ ] trailing deps를 `fn`에 lazy positional 인자로도 노출(**⚠️ [2026-08-24 - `H-14`] 이 항목은 `:Compute` 한정이다** — `Effect`의 `fn`엔 deps가 안 - 넘어간다) — (`fn(self, - previous?, dep1, ..., depN)` — 순서는 Luau 값 레벨 `...`가 파라미터 - 리스트 맨 끝이어야 하는 것과 같은 이유로 `previous?`가 deps 팩 - **앞**에 와야 함, 2026-08-11 후속 세션 제안 → 같은 날 세 번째 - 세션에 순서 정정, `base/source-state-plan.md` "trailing deps를 fn에 - lazy positional 인자로도 노출" 절) — 방향/순서는 확정, - `luau-test`의 `15-type-compute-trailing-deps-typepack.luau`로 - 이형 다중 deps를 제네릭 타입 팩으로 표현 가능한지만 실측 필요(안 - 되면 동종 타입 dep 1개로 한정) -> **[2026-08-22 이동] `EpochMap.luau` / `state:Gate` + `GateNode` / -> `Blocker.luau`는 M2로 옮겼습니다** — 셋 다 M2가 실제로 호출하는데 -> 각주로만 예고돼 있어서, `LifetimeHandle` 인터페이스를 M8→M2로 옮겼던 -> 전례대로 체크박스 자체를 옮겼습니다. M3는 그 위에 State/Source 본체를 -> 얹기만 하면 됩니다. -- [ ] **[2026-08-21 5라운드 — 채택 확정, 같은 날 `Epoch`로 일반화]** State의 - 재계산/전파 판정은 **`Epoch` 리비전 비교**다(`base/state-epoch-plan.md`) - — `invalid` 플래그가 아니다. 아래 `Source.luau`/`State.luau`가 이걸 - 전제로 짜여야 한다: `Source`가 `type Epoch = { Revision: number }`를 - 구조적으로 만족하고(`EpochBrand`에도 등록), 부기는 재사용 가능한 - **`EpochMap`**(`:Update(Epoch|EpochSet) -> boolean`, `:Refresh`, `:Sync`, - `:TrackFrom` — `EpochSet = {[Epoch]: true}`, **배열 아님**) - 으로 떼어내며, State가 그걸 **둘** 컴포지션한다 — `valueEpochMap`(값 - 유효성)/`emitEpochMap`(전파 dedup). emit은 값도 리비전도 안 싣고 - **출처(`Epoch`나 그 집합)만** 싣고, 순회는 `rawInvalid == false`일 때만 - 돌며 **값만 앞당기고 통지는 상류 emit을 기다린다**. 다이아몬드 중복 - 통지가 접히므로 스파이크 `05`도 그에 맞춰 재작성해야 한다 - (`luau-test/STATUS.md`). -- [ ] `state:Apply(factory)`(`base/source-state-plan.md` "`state:Apply(factory)`" - 절, 2026-08-07 일곱 번째 세션) — `factory(self)`를 체이닝 문법으로 - 부르는 순수 설탕, `factory: (State) -> U): U`로 열린 타입. Source도 - 기존 `:With`/`:Compute` 델리게이션에 얹혀 자동 포함 -- [ ] `state:Observer(fn)` — children 배열 leaf 참가자, **등록 즉시 1회 - 실행 확정**(`base/source-state-plan.md`의 Observer 절), `isObserver` - 판별자, canExecute 게이팅, `:Subscribe()`/`:Unsubscribe()`. **동적 - 경로 가드**(`{priority = HANDLER_PRIORITY_FALLBACK, isHandlable = v - is Observer, process = error(...)}`, `k` 타입 안 가림, 2026-08-14 - 열한 번째 세션 — `PreRef`와 같은 패턴)도 같이 등록 -- [ ] `Effect(fn, ...deps)`(`base/effect-plan.md`, **[2026-08-21 5라운드 - `C-6`]** 옛 시그니처는 `Effect(fn, state?)`) — deps 생략 시 설치 - 1회+leaf 사망 시 확정 정리, deps 지정 시 **각각에 맞는 구독** - (State/Source는 `Observer`, `Ref`는 `:Callback`)을 걸어 - 재실행+cleanup 체이닝(React `useEffect` 동형). **`EffectHandle`이 - `EpochMap`을 하나 들어** 공통 상류로 인한 중복 발화를 접고, 설치 구간 - 억제 플래그가 그 `Update`보다 먼저 와야 함 - (`base/state-epoch-plan.md`). Observer 구현 이후에 착수(의존 관계). - `EffectHandle:Subscribe()`/`:Unsubscribe()`도 추가(leaf 없이 쓰는 - 모듈/스크립트 레벨 Effect) — `:Unsubscribe()`는 Observer와 달리 - 마지막 cleanup을 1회 트리거해야 함(2026-08-07 일곱 번째 세션). - **동적 경로 가드**도 Observer와 같은 패턴으로 등록(`base/effect-plan.md` - "동적 경로 가드" 절, 2026-08-14 열한 번째 세션) -- [ ] Observer/Effect 이중 바인딩 금지 — `canBound(value)` 게이트로 - `:Subscribe()`(전역)와 `bindLifetime`(inst-scoped, leaf 부착도 - 내부적으로 이걸 호출)이 동시에 걸리면 즉시 `error`(`base/source-state-plan.md` "이중 바인딩 금지" 절, 2026-08-07 일곱 번째 - 세션 신설, 2026-08-09 여섯 번째 세션에서 "leaf 부착=bindLifetime - 호출"로 정정 — 진짜 독립 경로는 둘뿐). - **[2026-08-14 다섯 번째 세션에 별도 predicate `canBound(handle)`을 - 폐기하고 `canExecute` 하나로 합쳤다가, 같은 날 열한 번째 세션에 - 다시 갈라짐]** — "지금 묶어도 되는가"(bound 문맥)와 "지금 - 발화해도 되는가"(execute 문맥)는 호출부의 질문이 다르고 - **[2026-08-18 구현 전 QA 정정] 판정값도 같은 게 아니라 서로의 - 부정**이라(`canBound(v) == not canExecute(v)`, 게이트는 항상 - `if not canBound(v) then error(...)`), `Ref` 이중 배치 - 방지(`question.md` 0-W)를 계기로 `canBound`가 - 별도 진입점으로 재도입됨 — 판정 로직(비공개 `isBoundAlive` 헬퍼)은 - 공유해 코드 중복은 없음. **이 절이 쓰는 게이트는 이제 `canBound`** - (emit 전파 게이팅 전용 `canExecute`가 아님). `.Subscribed` 필드가 - leaf 경로와 무관하다는 것, leaf 생존 판정을 `bindLifetime`이 `value` - 쪽 `Relate`에 복사해둔 gcconn으로 하는 것은 안 바뀜 — `base/ - lifecycle-pattern.md`의 "`canBound` vs `canExecute`" 절, 역전 경위는 - `archive/canexecute-inst-arg-reversed.md`. 부수 효과로 **바인딩이 - 죽은 뒤(`Destroy`/`unbindLifetime`)의 재사용은 게이트를 통과** - (살아있는 바인딩만 막는 게 의도, 안 바뀜) +- [ ] **[2026-08-24 M2에서 이동] Observer/Effect 동적 경로 가드 등록** — + `{priority = HANDLER_PRIORITY_FALLBACK, isHandlable = v가 + Observer/Effect, process = error(...)}` 둘을 `Dispatch.addHandler`로 + 등록(`k` 타입 안 가림, `PreRef`와 같은 패턴 — + `base/source-state-plan.md`의 Observer 절, `base/effect-plan.md`의 + "동적 경로 가드" 절). **M2에서 Observer/Effect 본체를 짤 때는 이 등록만 + 빼고 간다** — 레지스트리(`Dispatch.addHandler` + `Handler.luau` 계약)가 + 이 마일스톤에서 생기기 때문. **M2가 M3에 개념상 지던 의존은 이 항목과 + 바로 아래 항목뿐이었고, 둘 다 "핸들러를 등록한다"뿐이라 이쪽으로 미룰 + 수 있었다** — 그래서 빌드 순서엔 역방향 간선이 남지 않는다. + 순서 교체(2026-08-24)의 근거 +- [ ] **[2026-08-24 M2에서 이동, `H-39`]** `ObserverEffectLeafHandler.process`가 + 자기 배열 자리의 `setOffsetSource(inst,k,None)`/`setLength(inst,k,0)`을 + 등록 — 빠져 있었다(위 말단 핸들러 항목) - [ ] mock 대상 테스트 ## M4 — 첫 end-to-end 반응형 업데이트 @@ -619,7 +683,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 정확히 불린다" 확인 + **`State>`(값이 또 State/Source)가 인덱스 N/N+1로 안 겹치고 정상 동작하는지**(2026-08-13 다섯 번째 세션에 UB→정상 지원으로 재정정) + **최초 마운트 직후 첫 재발행에서 - 인덱스 2의 retractor가 실제로 불리는지**(위 M2의 `SetStrong` 순서 + 인덱스 2의 retractor가 실제로 불리는지**(위 M3의 `SetStrong` 순서 버그가 정확히 여기서 증상으로 나타남) ## M5 — quad-roblox 최소 프로바이더 @@ -696,7 +760,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 이때 확정(CRUD/`:List` 여부 무관 항상 노출, 순서 계산과 "n개 검색됨" UI 둘 다 겸함) — 구현 시 이 두 API를 `:List`/CRUD의 `raw*`가 호출. **`recompute` 트리거 모델의 크래시(`RC-1`)는 Blocker 게이팅으로 - 해결됨**, 위 M2 항목 참고. + 해결됨**, 위 M3의 `Dispatch.setLength`/`setOffsetSource` 항목 참고. - **Slot의 `Add`/`Remove`/`Extract`/`ExtractAll`/`Clear`/`Move`/`Swap`/ `Get`/`IndexOf`/`Splice` CRUD 의미론 확정** (2026-08-09 세 번째 세션, 2026-08-09 열한 번째 세션에 식별 기준 재정정, `Splice`는 2026-08-12 @@ -766,7 +830,8 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 `disposeInst`가 남아 있었음), 중첩 offset이 부모 베이스를 못 받던 결함과 재마운트가 `Offset` Source를 새로 만들던 결함도 같이 수정됐다. **`recompute` 트리거 모델의 크래시(`RC-1`)는 Blocker 게이팅으로 - 해결됨 — 위 M2 항목 참고(`base/slot-plan.md` "재귀 메커니즘" 절).** + 해결됨 — 위 M3의 `Dispatch.setLength`/`setOffsetSource` 항목 + 참고(`base/slot-plan.md` "재귀 메커니즘" 절).** **[재설계, 2026-08-21] `attachSlot`은 비공개 재귀 둘로 분해됨** — `materializeSlotTree`(부기만, Blocker가 감싸는 건 이제 여기뿐) + `mountSlotTree`(물리 `Parent` 대입만, Blocker 불필요), 그리고 그 둘을 @@ -855,7 +920,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 `:List`의 **`prevKeys`**(옛 `keyIndex`의 강등판, 단순 키 집합). 전부 `base/slot-plan.md`가 소스 - [ ] **[2026-08-24 `H-25` 파생]** `quad-types`의 `Quad`에 `Slot` 필드 추가 - (위 M2 항목의 "마일스톤마다" 규칙) + (위 M3 항목의 "마일스톤마다" 규칙) - [ ] **[2026-08-13 여섯 번째 세션 — 이 세션의 Slot 결정 전부, 구현 전 필독]** - **`State` 교체 = 파괴가 아니라 언마운트**(`state`와 동일). 비파괴 경로 `unmountSlotTree`를 `destroySlotTree`와 별도로 구현 — @@ -934,7 +999,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 **`bindLifetime`/`unbindLifetime`이 `isEffect(value)`를 보고 `Destroying`을 걸고/끊는 것**으로 닫혔고(`base/effect-plan.md` + `base/lifecycle-pattern.md`의 그 의사코드), 이 문장은 그 배선이 구현된 - 뒤에야 참이 된다 — M3 `Effect` 구현이 M6의 이 항목을 **선행**한다는 뜻이다. + 뒤에야 참이 된다 — M2 `Effect` 구현이 M6의 이 항목을 **선행**한다는 뜻이다. (**[같은 날 재결정]** 처음엔 *"`EffectHandle`이 자기 `bindLifetime` 직후에 건다"*였으나 `/code-review high`가 **그 호출부가 실재하지 않는다**는 걸 지적했다 — 핸들은 남이 자기를 bind하는 걸 관측할 수 없고, `Effect`가 @@ -1022,9 +1087,9 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 `brand-plan.md`의 "`isX` wrapper" 절, M2의 `Brand.luau`에 이미 구현돼 있어야 함) > **[2026-08-22 이동] `None` 센티널 + `Dispatch/None.luau`(`NoneHandler`/ -> `NilHandler`)는 M2로 옮겼습니다** — `None`은 modifier 전용 값이 아니라 +> `NilHandler`)는 M3로 옮겼습니다** — `None`은 modifier 전용 값이 아니라 > quad-base 디스패치 배관이고(`architecture.md`의 소스 트리에서도 -> `Dispatch/None.luau`), M0의 `props.Modifier or None` 관용구 · M2의 +> `Dispatch/None.luau`), M0의 `props.Modifier or None` 관용구 · M3의 > `Dispatch.drive` · M6의 `setOffsetSource(..., None)`이 전부 이미 전제합니다. > M7에서는 **Modifier 쪽 표면만** 다룹니다 — 인라인 키/setter로 필드를 > 지우는 용법과 `Peek` 반환 타입에 `None`을 추가하는 것(`modifier-plan.md` @@ -1043,12 +1108,12 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 빠져 있어 구현자가 존재 자체를 놓칠 수 있던 자리다. 의사코드는 `base/modifier-plan.md`의 flatten 절이 소스 - [ ] **[2026-08-24 `H-25` 파생]** `quad-types`의 `Quad`에 `Modifier` 관련 - 표면이 노출돼야 하면 같이 갱신(위 M2 항목의 "마일스톤마다" 규칙) + 표면이 노출돼야 하면 같이 갱신(위 M3 항목의 "마일스톤마다" 규칙) ## M8 — Ref - [ ] **[2026-08-24 `H-25` 파생]** `quad-types`의 `Quad`에 `Ref` 필드 추가 - (위 M2 항목의 "마일스톤마다" 규칙) + (위 M3 항목의 "마일스톤마다" 규칙) - [ ] `Ref.luau`(`.Value` 읽기 전용 필드 + `:Set(value)`/`:Callback(fn)`/ `:Wait(thread?)`, 전부 self 반환) + `PreRef.luau`/`PostRef.luau`(별도 파일, Ref 런타임 재사용 + children 배열 전용, Modifier/Store 타입 차단, @@ -1186,7 +1251,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 - [ ] **[2026-08-24 `H-39`]** `TagHandler`/`AttributeGroupHandler`가 자기 배열 자리의 `setOffsetSource(inst,k,None)`/`setLength(inst,k,0)`을 등록 — - **둘 다 0건이었다**(위 M2의 그 항목). 같이 **`type(k) == "number"` 가드**도 + **둘 다 0건이었다**(위 M3의 그 항목). 같이 **`type(k) == "number"` 가드**도 추가(`H-52` — `RefLeafHandler`가 2026-08-18에 받은 수정을 이 둘은 못 받았다) - [ ] **[2026-08-24 `H-41`]** `AttributeGroupHandler.process`에 `groupClaimKeys` 위치 claim 배선 — 5라운드 `AT-1`에서 `(inst, groupValue) → k`로 확정해놓고 @@ -1196,7 +1261,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 없으면 `None`으로 콜백을 끄는 게 실제로는 **나중에 터질 Connection을 새로 심는** 동작이 된다 - [ ] **[2026-08-24 `H-25` 파생]** `quad-types`의 `Quad`에 `Tag`/`Attribute` - 필드 추가(위 M2 항목의 "마일스톤마다" 규칙) + 필드 추가(위 M3 항목의 "마일스톤마다" 규칙) - [ ] `Handlers/Event.luau`(`ReflectionService` 기반 자동 판별) - [ ] `Handlers/OnChange.luau`(`OnChange(name)` 특수 키 팩토리+Handler, `GetPropertyChangedSignal` 바인딩 — 제네릭 없이 콜백 타입은 인라인 @@ -1352,13 +1417,14 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 시간 기반 전파 게이트 `Debounce`/`Throttle`(`base/debounce-throttle-plan.md`) — 제어 핸들 설계까지 닫히면서 quad-base에 새 코어 메커니즘을 추가하지 않는 **순수 슈가**로 확인됨(`Blocker`의 gated state + `Ref` + - 아래 주입 op 2개 위에 전부 얹힘, 그 문서 13절). **[정정, 2026-08-21 5라운드] - 그 공용 `Gate` 추출은 M3가 아니라 M2로 앞당겨졌다** — "게이팅 먼저" - 결정(위 M2 각주)으로 `Dispatch.drive`의 배치 등록이 쓰는 게이팅부터 - 만들기로 했고, 그때 `Blocker`/`Debounce`/`Throttle`이 공유할 노드를 - 같이 빼둔다(따로 하면 같은 설계를 두 번 함). **[2026-08-21] 표면 확정 — + 아래 주입 op 2개 위에 전부 얹힘, 그 문서 13절). **[정정, 2026-08-24] + 그 공용 `Gate` 추출은 M2(반응형 코어)에서 이뤄진다** — 2026-08-21에 + "게이팅 먼저" 결정으로 디스패치 쪽에 앞당겨뒀다가, 2026-08-24 마일스톤 + 순서 교체(위 M2 배너)로 반응형이 먼저가 되면서 앞당길 필요 자체가 + 사라졌다. `Blocker`/`Debounce`/`Throttle`이 공유할 노드를 거기서 같이 + 빼둔다(따로 하면 같은 설계를 두 번 함). **[2026-08-21] 표면 확정 — `state:Gate(setup)` 메소드**, `base/gate-plan.md`. - 프리미티브 자체는 그 위에 나중에 얹으면 되고 M0/M3를 막지 않음. + 프리미티브 자체는 그 위에 나중에 얹으면 되고 M0/M2를 막지 않음. 주입 op 2개(`setTimeout(func, delay) -> Timeout` / `clearTimeout`, Roblox는 `task.delay`/`task.cancel`로 배선 — **인자 순서가 반대라 주의**)가 `bindLifetime`/`canExecute`와 같은 base 범용 유틸 그룹에