diff --git a/.claude/README.md b/.claude/README.md index b794219..2a94bbd 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` 최우선 절과 `.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`). `pre-implementation-handtrace-round7.md`(**[2026-08-25 신설, 회신 대기]** 7라운드: 6라운드와 같은 손 트레이싱이되 범위가 **M2(반응형 코어)와 M2→M3 경계**다. 패스 6개가 각기 다른 각도를 쓴다 — 1차는 프리미티브 사이의 *호출 순서*를 시간축으로 겹쳐 보기, 2차는 문서가 "확인했다"고 적은 런타임/타입 주장을 실제로 `luau`/`luau-analyze`에 걸어보기, 3차는 **커밋된 M1 코드·툴체인을 이 저장소에서 그대로 돌려보기** + 1·2차가 뺐던 "M2를 *소비하는* 문서", 4차는 **M2 코어를 문서 그대로 옮긴 참조 구현을 돌려보기** + 아무 문서도 안 정한 **예외 경로**, 5차는 **확정된 M2 표면을 실제 Luau 타입으로 선언해 `luau-analyze`에 걸어보기**(확정 시그니처가 확정 관용구를 통과시키는가), 6차는 **그 참조 구현을 M2→M3 경계(Length/Offset 부기·Dispatch 체인)까지 이어 붙여 돌려보기** + **quad 자신이 던지기로 확정한 error 42곳의 계약 감사**. 발견 번호는 6라운드에서 이어서 `H-55`부터, 패스를 가로질러 연속으로 매긴다. 발견 수/심각도 분포는 그 문서 자신이 소스. 각 패스의 "문제가 없던 것" 부록은 다시 파지 않아도 되는 자리를 적어둔 것. **아직 아무것도 `base/`에 반영하지 않았다** — 판정은 사용자가 하고, 결정이 나면 6라운드처럼 `-followup.md`를 새로 만든다), `pre-implementation-handtrace-round7-verification.md`(**[2026-08-25 신설]** 그 52건이 **정말 유효한지만** 판정한 검증 패스 — 새 발견은 없다. ⚠️ **재실행이 아니라 대조로 판정했다**: 4·5·6차 패스가 쓴 전사물(`audit/handtrace-round7-reference-impl/spikes/`), `pre-implementation-handtrace-round7-followup.md`(**[2026-08-25 신설] 그 52건에 대한 사용자 결정과 `base/` 반영 결과 — 결정 단위 12묶음(🅐~🅜) 순서로 대화형 처리. **이 파일이 결정의 소스**이고 발견 원문은 앞의 두 파일. 무효/기각 7건(`H-73`~`H-77` 계열: `<>`가 값 호출부에서 동작함이 실측으로 드러나 Store 재설계로 이어짐, `RunInit` 누수는 성립 안 하는 사용법), 나머지는 전량 반영. **부수로 `question.md` 최우선 두 항목이 같이 닫혀 M2 착수 게이트가 0이 됐다**)과 `base/` 확정 의사코드를 줄 단위로 맞춰봤고(전사 오류로 생긴 발견은 **없었다**, 미신고 차이 3개는 전부 발견을 만들지 않는 방향), Luau 언어 동작·저장소 상태를 주장하는 것만 직접 재실행했다. 판정 분포와 항목별 근거는 그 문서 자신이 소스 — 여기서 세지 않는다. **결정 전에 반드시 읽을 것**: 부정 주장·개수 주장 몇 건이 정정됐고, 특히 **이미 사용자가 판단한 항목을 다시 묻게 되는 자리**가 하나 있다. 마지막 절의 **batch용 요약**이 유효 항목을 "한 결정으로 닫히는 묶음"으로 재편성해뒀으니 회신은 번호순이 아니라 그 묶음 단위로 하는 게 싸다), `pre-implementation-handtrace-round8.md`(**[2026-08-26 신설, 회신 대기]** 8라운드: 7라운드 반영으로 새로 쓰인 `base/` 서술을 **서로 겹쳐서** 재트레이싱한 것 — 개별 함수가 아니라 "반영된 결정들이 조합될 때 성립하는가"가 주제. 3패스에 걸쳐 `base/` 전 문서를 완독했고, 발견 번호는 `H-107`부터 이어 매김(발견 수·심각도 분포는 그 문서 자신이 소스), 실측 스파이크는 발견 항목에 인라인 전사. 사용자 결정 문항 Q1~Q10이 §4에 배치 회신용으로 정리돼 있다. 커밋 전 `/code-review`가 이 문서 자체의 결함 7건을 잡아 반영됐다(각 항목의 `[code-review 정정/추가]` 표시). **⭐ [2026-08-26] 전량 처리·반영 완료** — 그 문서는 이제 **발견 당시의 기록**이라 각 항목의 "갈래"는 선택 전 목록이니 그대로 믿지 말 것), `pre-implementation-handtrace-round8-followup.md`(**[2026-08-26 신설] 8라운드의 결정과 근거가 여기 소스다** — Q1~Q10을 사용자와 대화형으로 처리한 기록. **역전은 없고** 전부 "7라운드 확정이 `base/`에 내려앉을 때 생긴 누락·충돌"을 닫은 것. ⭐ 사용자가 **문항의 전제 자체를 정정한 것이 둘** 있다(Ref 콜백 ↔ Observer 콜백은 애초에 통합 대상이 아니다 / `H-118`은 소유권 문제가 아니라 문장이 틀린 것) — 결정만 읽지 말고 그 두 절을 볼 것), `pre-implementation-handtrace-round9-brief.md`(**[2026-08-26 신설]** 9라운드를 돌릴 감사자에게 주는 **지시서** — 발견 보고가 아니라 그 앞단이다. 이런 지시서를 저장하는 건 이번이 처음으로, 7·8라운드 것은 대화에만 있었고 저장되지 않았다(8라운드 본문이 인용하는 "감사 지시서 §2"가 코퍼스 어디에도 없는 이유). 스코프를 **커밋 `9dd8213` 하나의 델타**로 정의한 게 핵심 — 8라운드 결정 반영과 그 뒤 `/code-review high` 7패스 수정이 전부 그 커밋에 들어 있고 아무도 트레이싱한 적이 없다. 레인 셋(A: 그 델타의 상호 간섭 / C: `audit/handtrace-round7-reference-impl/`을 지금 계약으로 갱신해 실행 / B: 8라운드 §6이 남긴 M5+ 값 단위 트레이싱)과 우선순위 근거, `-round8-followup.md`가 적어둔 **반복 실패 모드 7개**를 사냥 목록으로 옮겨 실었다. 각도 문자(A~H)는 8라운드와 같은 걸 유지해 상호참조가 되게 했다), `pre-implementation-handtrace-round9.md`(**[2026-08-26]** 9라운드 발견 보고 — 레인 A(`9dd8213` 델타 상호 간섭)·C(참조 구현을 현재 계약으로 재전사해 `luau` 실행)·G(Luau 사실 재확인)·D(ROADMAP M2 시뮬레이션)·B(M5+ 값 단위, 첫 시도) — 발견 `H-124`~`H-141`, 🔴 둘(`recompute`의 `lengthList[i]` nil 읽기 / 재마운트 시 `_baseObserver` 미바인드 캐시 stale) 다 실측 재현. §4 배치 문항 Q1~Q10 + §5 이상 없음(⭐ `keyof<{}>` 빈 Store 실측으로 클린) + §6), `pre-implementation-handtrace-round9-followup.md`(**[2026-08-27] 9라운드 결정의 소스 — 전량 처리 완료** — Q1(`recompute` 되감기 판정을 읽기 앞으로) / Q2(`Offset`·`_baseObserver`를 Slot 생성자로, `materializeSlotTree` 순서, `_destroyed` 플래그) / Q3(`element → index`는 `bk.indexOfElement` 하나 — 사용자가 정한 적 없는 `token` 폐기, `H-137` 소멸, `H-141` 신설) / Q4(`EffectHandle` 네 진입점 의사코드) / Q5(M2에 `Ref` 최소형) / Q6(`WeakUnsubscribe` 관대) / Q7(폐기 블록 `archive/`) / Q8(`InstanceChildHandler` 부기) / **Q9(문항 전제가 틀림 — Tween 절 스케치 한 줄 복사 오류)** / Q10(`reconcile` 배치 Blocker) + `H-138`(숏핸드 우선순위) / `H-139`(`New`/`drive` 파이프라인 의사코드) / **`H-142`(props에 `Parent` 금지 — 순서 문제 소멸)** / **`H-143`~`H-146`**(`/code-review`가 낸 새 메커니즘 넷 — 전부 권고 (a), `H-144`는 사용자 요청으로 재구독 뒤 epoch·Blocker 상호작용을 재트레이싱한 기록 포함). 진행 표가 상태의 소스, 사용자 회신 원문 셋 인용; **[2026-08-28]** `H-143`~`H-146` 반영분의 감사 8라운드 표와 `/code-review high` 기록도 여기), `pre-implementation-handtrace-round10-brief.md`(**[2026-08-28 신설]** 10라운드 지시서 — 델타가 아니라 **광범위** 스코프(M2~M8, 레인 A 반응형 코어 전체 / C 참조 구현 갱신·실행 / B 디스패치·Slot / D 타입·M1 코드), 신선한 탐사자가 한 번에 문항지를 만들어 사용자가 배치로 결정하게 하는 것이 목적), `pre-implementation-handtrace-round10.md`(**[2026-08-28 신설, 회신 대기]** 10라운드 발견 보고 + §4 배치 문항지 — 씨앗은 `/code-review`가 낸 `H-147`~`H-149`(죽은 핸들에서 `Rerun` / `Parent` 거부 전용 문구는 새 메커니즘 / Observer `Subscribe` 위임과 `level 2`), 탐사자 발견은 `H-150`부터. 레인 C 실행 기록은 `audit/handtrace-round10-reference-impl/`. **[2026-08-28 탐사 완료]** 🔴 0 / 🟡 5 / 🟢 3, §4 문항 7건(기존 3 + `H-150` `Effect._blocker` 죽은 부품 / `H-151` 게이트 우회 계약 / `H-153` Store 예약 이름 런타임 가드 / `H-154` `InstanceChildHandler` dedup) — 전부 권고 (a). 레인 A·C 완료, B 부분, D `./scripts/test.sh` ALL PASS), `pre-implementation-handtrace-round10-followup.md`(**[2026-08-28] 10라운드 결정의 소스 — 전량 처리 완료.** 사용자가 *"하나하나 같이 보자"*라 대화형으로 처리. **문항의 전제를 뒤집은 것 둘**: `H-147`은 "죽은 핸들에서 `Rerun`"이 아니라 **`fn`/cleanup이 자기 구독을 바꿀 수 있다는 허용 자체가 모순**(→ (A) 금지, `H-143` 소멸, `rawRerun(force)`/`Rerun` 분리), `H-148`은 "문구"가 아니라 **루트 마운트 표면의 부재**(→ `Claim` + `D.Mapper`, `research/existing-mount-plan.md`). 나머지: `H-149` Observer 진입점 인라인 / `H-150` `_blocker` 제거 / `H-151` `Refresh` 캐치업 폐기 — 게이트는 emit 경로만 미룬다 / `H-153` Store 예약 이름 가드 / `H-154` dedup. 새로 생긴 `H-158`(`:Block` 슈가)은 미결). 다음 라운드가 필요해지면 라운드마다 파일을 새로 만들고 이름에 라운드 번호를 넣을 것(지시서도 같이 남길 것 — `-roundN-brief.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`). `pre-implementation-handtrace-round7.md`(**[2026-08-25 신설, 회신 대기]** 7라운드: 6라운드와 같은 손 트레이싱이되 범위가 **M2(반응형 코어)와 M2→M3 경계**다. 패스 6개가 각기 다른 각도를 쓴다 — 1차는 프리미티브 사이의 *호출 순서*를 시간축으로 겹쳐 보기, 2차는 문서가 "확인했다"고 적은 런타임/타입 주장을 실제로 `luau`/`luau-analyze`에 걸어보기, 3차는 **커밋된 M1 코드·툴체인을 이 저장소에서 그대로 돌려보기** + 1·2차가 뺐던 "M2를 *소비하는* 문서", 4차는 **M2 코어를 문서 그대로 옮긴 참조 구현을 돌려보기** + 아무 문서도 안 정한 **예외 경로**, 5차는 **확정된 M2 표면을 실제 Luau 타입으로 선언해 `luau-analyze`에 걸어보기**(확정 시그니처가 확정 관용구를 통과시키는가), 6차는 **그 참조 구현을 M2→M3 경계(Length/Offset 부기·Dispatch 체인)까지 이어 붙여 돌려보기** + **quad 자신이 던지기로 확정한 error 42곳의 계약 감사**. 발견 번호는 6라운드에서 이어서 `H-55`부터, 패스를 가로질러 연속으로 매긴다. 발견 수/심각도 분포는 그 문서 자신이 소스. 각 패스의 "문제가 없던 것" 부록은 다시 파지 않아도 되는 자리를 적어둔 것. **아직 아무것도 `base/`에 반영하지 않았다** — 판정은 사용자가 하고, 결정이 나면 6라운드처럼 `-followup.md`를 새로 만든다), `pre-implementation-handtrace-round7-verification.md`(**[2026-08-25 신설]** 그 52건이 **정말 유효한지만** 판정한 검증 패스 — 새 발견은 없다. ⚠️ **재실행이 아니라 대조로 판정했다**: 4·5·6차 패스가 쓴 전사물(`audit/handtrace-round7-reference-impl/spikes/`), `pre-implementation-handtrace-round7-followup.md`(**[2026-08-25 신설] 그 52건에 대한 사용자 결정과 `base/` 반영 결과 — 결정 단위 12묶음(🅐~🅜) 순서로 대화형 처리. **이 파일이 결정의 소스**이고 발견 원문은 앞의 두 파일. 무효/기각 7건(`H-73`~`H-77` 계열: `<>`가 값 호출부에서 동작함이 실측으로 드러나 Store 재설계로 이어짐, `RunInit` 누수는 성립 안 하는 사용법), 나머지는 전량 반영. **부수로 `question.md` 최우선 두 항목이 같이 닫혀 M2 착수 게이트가 0이 됐다**)과 `base/` 확정 의사코드를 줄 단위로 맞춰봤고(전사 오류로 생긴 발견은 **없었다**, 미신고 차이 3개는 전부 발견을 만들지 않는 방향), Luau 언어 동작·저장소 상태를 주장하는 것만 직접 재실행했다. 판정 분포와 항목별 근거는 그 문서 자신이 소스 — 여기서 세지 않는다. **결정 전에 반드시 읽을 것**: 부정 주장·개수 주장 몇 건이 정정됐고, 특히 **이미 사용자가 판단한 항목을 다시 묻게 되는 자리**가 하나 있다. 마지막 절의 **batch용 요약**이 유효 항목을 "한 결정으로 닫히는 묶음"으로 재편성해뒀으니 회신은 번호순이 아니라 그 묶음 단위로 하는 게 싸다), `pre-implementation-handtrace-round8.md`(**[2026-08-26 신설, 회신 대기]** 8라운드: 7라운드 반영으로 새로 쓰인 `base/` 서술을 **서로 겹쳐서** 재트레이싱한 것 — 개별 함수가 아니라 "반영된 결정들이 조합될 때 성립하는가"가 주제. 3패스에 걸쳐 `base/` 전 문서를 완독했고, 발견 번호는 `H-107`부터 이어 매김(발견 수·심각도 분포는 그 문서 자신이 소스), 실측 스파이크는 발견 항목에 인라인 전사. 사용자 결정 문항 Q1~Q10이 §4에 배치 회신용으로 정리돼 있다. 커밋 전 `/code-review`가 이 문서 자체의 결함 7건을 잡아 반영됐다(각 항목의 `[code-review 정정/추가]` 표시). **⭐ [2026-08-26] 전량 처리·반영 완료** — 그 문서는 이제 **발견 당시의 기록**이라 각 항목의 "갈래"는 선택 전 목록이니 그대로 믿지 말 것), `pre-implementation-handtrace-round8-followup.md`(**[2026-08-26 신설] 8라운드의 결정과 근거가 여기 소스다** — Q1~Q10을 사용자와 대화형으로 처리한 기록. **역전은 없고** 전부 "7라운드 확정이 `base/`에 내려앉을 때 생긴 누락·충돌"을 닫은 것. ⭐ 사용자가 **문항의 전제 자체를 정정한 것이 둘** 있다(Ref 콜백 ↔ Observer 콜백은 애초에 통합 대상이 아니다 / `H-118`은 소유권 문제가 아니라 문장이 틀린 것) — 결정만 읽지 말고 그 두 절을 볼 것), `pre-implementation-handtrace-round9-brief.md`(**[2026-08-26 신설]** 9라운드를 돌릴 감사자에게 주는 **지시서** — 발견 보고가 아니라 그 앞단이다. 이런 지시서를 저장하는 건 이번이 처음으로, 7·8라운드 것은 대화에만 있었고 저장되지 않았다(8라운드 본문이 인용하는 "감사 지시서 §2"가 코퍼스 어디에도 없는 이유). 스코프를 **커밋 `9dd8213` 하나의 델타**로 정의한 게 핵심 — 8라운드 결정 반영과 그 뒤 `/code-review high` 7패스 수정이 전부 그 커밋에 들어 있고 아무도 트레이싱한 적이 없다. 레인 셋(A: 그 델타의 상호 간섭 / C: `audit/handtrace-round7-reference-impl/`을 지금 계약으로 갱신해 실행 / B: 8라운드 §6이 남긴 M5+ 값 단위 트레이싱)과 우선순위 근거, `-round8-followup.md`가 적어둔 **반복 실패 모드 7개**를 사냥 목록으로 옮겨 실었다. 각도 문자(A~H)는 8라운드와 같은 걸 유지해 상호참조가 되게 했다), `pre-implementation-handtrace-round9.md`(**[2026-08-26]** 9라운드 발견 보고 — 레인 A(`9dd8213` 델타 상호 간섭)·C(참조 구현을 현재 계약으로 재전사해 `luau` 실행)·G(Luau 사실 재확인)·D(ROADMAP M2 시뮬레이션)·B(M5+ 값 단위, 첫 시도) — 발견 `H-124`~`H-141`, 🔴 둘(`recompute`의 `lengthList[i]` nil 읽기 / 재마운트 시 `_baseObserver` 미바인드 캐시 stale) 다 실측 재현. §4 배치 문항 Q1~Q10 + §5 이상 없음(⭐ `keyof<{}>` 빈 Store 실측으로 클린) + §6), `pre-implementation-handtrace-round9-followup.md`(**[2026-08-27] 9라운드 결정의 소스 — 전량 처리 완료** — Q1(`recompute` 되감기 판정을 읽기 앞으로) / Q2(`Offset`·`_baseObserver`를 Slot 생성자로, `materializeSlotTree` 순서, `_destroyed` 플래그) / Q3(`element → index`는 `bk.indexOfElement` 하나 — 사용자가 정한 적 없는 `token` 폐기, `H-137` 소멸, `H-141` 신설) / Q4(`EffectHandle` 네 진입점 의사코드) / Q5(M2에 `Ref` 최소형) / Q6(`WeakUnsubscribe` 관대) / Q7(폐기 블록 `archive/`) / Q8(`InstanceChildHandler` 부기) / **Q9(문항 전제가 틀림 — Tween 절 스케치 한 줄 복사 오류)** / Q10(`reconcile` 배치 Blocker) + `H-138`(숏핸드 우선순위) / `H-139`(`New`/`drive` 파이프라인 의사코드) / **`H-142`(props에 `Parent` 금지 — 순서 문제 소멸)** / **`H-143`~`H-146`**(`/code-review`가 낸 새 메커니즘 넷 — 전부 권고 (a), `H-144`는 사용자 요청으로 재구독 뒤 epoch·Blocker 상호작용을 재트레이싱한 기록 포함). 진행 표가 상태의 소스, 사용자 회신 원문 셋 인용; **[2026-08-28]** `H-143`~`H-146` 반영분의 감사 8라운드 표와 `/code-review high` 기록도 여기), `pre-implementation-handtrace-round10-brief.md`(**[2026-08-28 신설]** 10라운드 지시서 — 델타가 아니라 **광범위** 스코프(M2~M8, 레인 A 반응형 코어 전체 / C 참조 구현 갱신·실행 / B 디스패치·Slot / D 타입·M1 코드), 신선한 탐사자가 한 번에 문항지를 만들어 사용자가 배치로 결정하게 하는 것이 목적), `pre-implementation-handtrace-round10.md`(**[2026-08-28 신설, 회신 대기]** 10라운드 발견 보고 + §4 배치 문항지 — 씨앗은 `/code-review`가 낸 `H-147`~`H-149`(죽은 핸들에서 `Rerun` / `Parent` 거부 전용 문구는 새 메커니즘 / Observer `Subscribe` 위임과 `level 2`), 탐사자 발견은 `H-150`부터. 레인 C 실행 기록은 `audit/handtrace-round10-reference-impl/`. **[2026-08-28 탐사 완료]** 🔴 0 / 🟡 5 / 🟢 3, §4 문항 7건(기존 3 + `H-150` `Effect._blocker` 죽은 부품 / `H-151` 게이트 우회 계약 / `H-153` Store 예약 이름 런타임 가드 / `H-154` `InstanceChildHandler` dedup) — 전부 권고 (a). 레인 A·C 완료, B 부분, D `./scripts/test.sh` ALL PASS), `pre-implementation-handtrace-round10-followup.md`(**[2026-08-28] 10라운드 결정의 소스 — 전량 처리 완료.** 사용자가 *"하나하나 같이 보자"*라 대화형으로 처리. **문항의 전제를 뒤집은 것 둘**: `H-147`은 "죽은 핸들에서 `Rerun`"이 아니라 **`fn`/cleanup이 자기 구독을 바꿀 수 있다는 허용 자체가 모순**(→ (A) 금지, `H-143` 소멸, `rawRerun(force)`/`Rerun` 분리), `H-148`은 "문구"가 아니라 **루트 마운트 표면의 부재**(→ `Claim` + `D.Mapper`, `base/claim-plan.md`). 나머지: `H-149` Observer 진입점 인라인 / `H-150` `_blocker` 제거 / `H-151` `Refresh` 캐치업 폐기 — 게이트는 emit 경로만 미룬다 / `H-153` Store 예약 이름 가드 / `H-154` dedup. 새로 생긴 `H-158`(`:Block` 슈가)도 같은 날 확정 — 폐기, `state:Apply(blocker)`. **[2026-08-28 후속 3]** `Claim` 갈래 여덟 전량 확정 → `base/claim-plan.md`로 승격, 그 경위도 이 파일 끝 절). 다음 라운드가 필요해지면 라운드마다 파일을 새로 만들고 이름에 라운드 번호를 넣을 것(지시서도 같이 남길 것 — `-roundN-brief.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` | @@ -72,6 +72,7 @@ | `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`가 최종 이름, 용어 대기열에서도 제외 **⚠️ [2026-08-26, 8라운드 `H-120`] 실제로는 `Callback(guard(fn))`이다** — `Ref` 콜백의 *"등록 즉시 1회, 값이 nil이어도"* 계약 때문에 맨 `fn`을 걸면 **생성 시점에 `fn(nil)`이 먼저 불려** `inst`를 바로 쓰는 콜백이 pre-pass에 닿기도 전에 죽는다. `guard(fn) = function(v) if v ~= nil then fn(v) end end`. `Ref` 계약은 안 건드리고 슈가 쪽에서 막는다. | | `gate-plan.md` | **[2026-08-21 신설, 같은 날 표면 확정]** `state:Gate(setup)` — 상류 emit을 가로채 내려보낼지 정책이 정하는 **`GateNode`**(`ComputeNode`와 같은 층위)를 만드는 State 메소드. 탑레벨 `Gate(...)` 프리미티브는 **안 만든다**(처음 방향에서 뒤집힘) — `Blocker`가 `state:Apply(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 | +| `claim-plan.md` | **[2026-08-28 신설·같은 날 확정, `research/`에서 승격]** 이미 있는 트리(PlayerGui, `Clone()` 사본, Studio에서 만든 GUI)를 quad가 **소유**하는 `Claim(inst, D.Mapper.(key) {…}) -> inst` — 루트가 Slot일 수 없던 공백(`H-146`/`H-148`)을 `drive` 위의 한 겹(DFS 이름 해석 → bottom-up `drive`)으로 닫는다. 계약은 claim-once·own-all(부기 대상 자식 전부 매핑, 디스크립터 순서가 정본, 이름 중복·부재 UB + debug 검사, 같은 `inst` 이중 claim error, 다중 quad UB). 루트 키는 센티널 `D.Mapper.Root`, props 타입은 `D.`와 `type Param` 공유, `Claim`은 타입 인자 없음(추론). **루트의 `.Parent`는 부기 밖이라 밖에서 대입 허용**(`H-146` 예외를 좁혀 복원 — 여러 스크립트가 한 `PlayerGui`를 쓰는 흔한 경우의 답, PlayerGui 직하 Slot 공유는 중간 모듈). `archive/existing-instance-bind-rejected.md`와 다름(재바인드 아님). 프로바이더 op `nativeFindChild`. **M5 스코프**(`H-161`) | | `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`나 그 집합)만** 싣고, 순회는 **캐시 카운터가 같을 때만** 돌며 값만 앞당기고(**[2026-08-25 `H-85`]** 옛 `rawInvalid` 불린은 `cacheTargetCount`/`cacheCurrCount` 쌍으로 교체 — 재계산 *도중* 도착한 무효화를 꼬리가 지우던 것과, `fn`이 던졌을 때 계산된 적 없는 캐시를 유효하다고 확신하던 것 둘을 같이 닫음) 통지는 상류 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 신설) @@ -99,7 +100,6 @@ | `v1-compat-plan.md` | v1 하위호환(compat) 레이어 — `quad-roblox-v1-compat` 패키지, v2→v1 단방향 브리지(`state:Observer()`+v1 프로퍼티 재대입), v2-in-v1/v1-in-v2 두 임베딩 방향의 기술 규칙까지 확정. quad2-try의 `quad-compat`은 빈 폴더로 실제 시도된 적 없었음을 확인 | 하 — Slot이 foreign Instance를 어떻게 다루는지만 Slot 코어 구현 시점까지 미결 | | `doc-include-plan.md` | **[2026-08-14 신설]** 문서 stale 감소용 include 도구 `doc-include.py`(가칭) — 원본 파일에 `` 류 마커로 요약 구간을 표시해두면 인용하는 문서가 그 구간을 기계적으로 추출해 붙여넣게 하는 도구. `doc-check.py`(사후 탐지)와 짝을 이루는 사전 차단 장치. AsciiDoc tagged include/markdown-magic이 선례, build vs buy 검토 후 Python 표준 라이브러리로 직접 제작(~100줄) 채택. 파일럿은 `.claude/session-summary.md` ← `.claude/session/*.md` 요약 마커부터(CLAUDE.md 분할로 목적지가 "통째로 생성되는 파일"이 돼 단방향 생성으로 단순화됨) | 하 — M0/설계 게이트와 무관한 메타 도구. **[2026-08-16 기준]** 플랜 초안 단계, 열린 질문 미해소(소스: 이 문서의 "열린 질문" 절) | | `fastscroll-plan.md` | **[2026-08-18 신설]** 사용자 아이디어 메모 — 완전 외부 패키지 `quad-roblox-fastscroll`(리스트/그리드 내 상대 위치 계산으로 움직일 요소만 갱신, 배경의 빈 공간만 스크롤). 가상 레이아웃 유틸이 선행 요구사항으로 보임. 설계 논의 전, 아직 아이디어 단계 | 최하 — 사용자가 "quad가 잘 작동하게 될 때" 직접 검토하겠다고 후순위 지정. 선행 확인 필요 사항(`Visible=false`일 때 `AbsoluteSize`/`AbsolutePosition` 갱신 여부)은 Roblox Studio 실측 필요 | -| `existing-mount-plan.md` | **[2026-08-28 신설, 사용자 발의]** 이미 있는 트리(PlayerGui, `Clone()` 사본, Studio에서 만든 GUI)를 quad가 **소유**하는 `Claim(inst, D.Mapper. "Name" {…})` — 루트가 Slot일 수 없던 공백(`H-146`/`H-148`)을 위에서 닫는다. `archive/existing-instance-bind-rejected.md`와 다름(재바인드 아님, claim-once·own-all). 방향 확정, 갈래 미결(개수는 §5가 소스 — 루트 이름·물리 순서·debug 검사 범위 등). **M5 스코프**(`H-161`), M2 게이트 아님 | 중 — 다음 배치 문항 | | `spring-plan.md` | **[2026-08-18 신설]** 사용자 아이디어 메모 — 스프링 물리 기반 지속 업데이트 프리미티브(`quad-spring`), 이전 상태와 비교해 스프링 연산을 수행하는 중간 핸들러. 참고 구현 [qwreey/spring.lua](https://github.com/qwreey/spring.lua) 사용 가능 여부 확인 필요. 확정 `Tween` 모델과는 별개 트랙 — `quad-base`의 `onStep`류 후킹 인터페이스로 얹을지, 엔진별 `quad-roblox-spring`으로 각자 구현할지, `Source` 확장 primitive로 둘지 미정 | 최하 — "모든게 완성된 후, 별도 모듈로 분화"라고 사용자가 직접 명시 | ## `archive/` — 완료됐거나 완전히 뒤집힌 것, 능동 참고 불필요 @@ -128,7 +128,7 @@ | `invalidate-dedup-propagation-reversed.md` | **[역전됨, 2026-08-14 신설]** "신호를 받은 State는 이미 `invalid`였다면 그 아래로 더 전파하지 않는다"(다이아몬드 중복 워크 방지) — 실제로는 **emit이 자기 `invalid` 상태와 무관하게 항상 전파**되고, 중복 재계산은 pull-recompute+캐시가 막음. 옛 서술은 확정된 `Observer` 계약(`fn`이 `:Get()`을 안 불러도 됨)과 정면 충돌해 **`:Get()` 안 하는 Observer가 한 번 울고 영구 침묵**하게 만들었고, `architecture.md`와도 어긋나 있었음. Debounce 설계 중 사용자 지적으로 발견 — 그 위에 쌓였던 `debounce-throttle-plan.md` 3절 발견도 같이 철회됨 | | `retract-always-fires-reversed.md` | **[역전됨, 2026-08-12 열한 번째 세션 신설]** "핸들러 타입이 안 바뀌면 retract 없이 process가 diff" — 실제로는 `retract`가 store 재발행마다 항상 불림(핸들러 타입 무관). `Tag`/`Ref`/`Slot`/`Attribute` 전부 이 오류 위에서 설계돼 있었음이 드러나 한 세션에 전부 정정 | | `slot-discard-no-portal-reversed.md` | **[역전됨, 2026-08-13 일곱 번째 세션 신설]** Slot의 **"retract = 폐기, 옮기지 않음"(2026-08-04 확정) + "portal은 오버엔지니어링이라 안 함"** — 여섯 번째 세션에 `State` 교체가 파괴에서 **언마운트**로 뒤집히며 portal이 별도 기능이 아니라 그 귀결이 됨(`state`와 동일한 시맨틱). `base/slot-plan.md`에 히스토리로 남아 있던 세 덩어리(확정 문단 + `State` 왕복 분석 + 포탈 검토와 숙제 셋)를 원문 그대로 이전, 숙제 셋이 각각 어떻게 결말났는지도 정리 | -| `existing-instance-bind-rejected.md` | **[기각됨, 2026-08-14 세션 — `research/`에서 이전]** 이미 생성된 Instance에 나중에 `{k=v}` 프롭 테이블을 바인드하는 기능 — 오래 "열린 가능성"으로 남겨뒀으나 사용자 확정으로 기각. 사유: 허용하면 `Dispatch.setOffsetSource`/`setLength` 같은 "quad가 만든 트리" 전제의 부기를 바깥에서 밀고 당기는 부가 작용이 전부 가능해져 **버그 표면이 치명적으로 넓어짐**. `pre-implementation-audit.md` 2-4(Slot 단일 마운트 소유권과의 충돌)도 이걸로 해소 **[2026-08-28] 좁은 형태로 부활** — 재바인드가 아니라 claim-once·own-all(`research/existing-mount-plan.md`), 기각 사유 자체는 유효 | +| `existing-instance-bind-rejected.md` | **[기각됨, 2026-08-14 세션 — `research/`에서 이전]** 이미 생성된 Instance에 나중에 `{k=v}` 프롭 테이블을 바인드하는 기능 — 오래 "열린 가능성"으로 남겨뒀으나 사용자 확정으로 기각. 사유: 허용하면 `Dispatch.setOffsetSource`/`setLength` 같은 "quad가 만든 트리" 전제의 부기를 바깥에서 밀고 당기는 부가 작용이 전부 가능해져 **버그 표면이 치명적으로 넓어짐**. `pre-implementation-audit.md` 2-4(Slot 단일 마운트 소유권과의 충돌)도 이걸로 해소 **[2026-08-28] 좁은 형태로 부활** — 재바인드가 아니라 claim-once·own-all(`base/claim-plan.md`), 기각 사유 자체는 유효 | | `question-resolved.md` | **[해소 아카이브, 2026-08-13 아홉 번째 세션 신설]** `question.md`에서 걷어낸 **결정 완료** 항목 전부(당시 32개 `[해소됨]` 마커) — 추가 프리미티브 필요성 라운드, 구현 착수 직전 감사 요약, 확정된 용어들(`State`/`Relate`/`List`/`canBound`(2026-08-14 다섯 번째 세션에 폐기됐다가 열한 번째 세션에 별도 진입점으로 재도입 — `canExecute`와 판정 로직만 공유)/`Ref`/`PreRef`/`Peek`/`isState`/`None`/`Handler`), 포탈·`State` 왕복 해소 등. 분리 직전 전문을 그대로 보존. **`question.md`는 이제 사용자가 답해야 할 것만 담음** — 항목이 해소되면 여기로 옮길 것 | | `dispatch-hintvalue-model-reversed.md` | **[2026-08-13 열네 번째 세션 신설 — 옛 이름은 research/ 아래의 dispatch-redispatch-diff-plan]** 뒤집힌 **"철거 후 재구축 + `hintValue` 힌트"** 재디스패치 모델 원문 + 역전을 이끈 분석 전문(`None`/`State` 래퍼가 힌트로 새는 재현 사례, 깊은 인덱스 힌트 유실, 옛 점유 체크가 Attribute 소유권을 대신하던 구조). 지금 유효한 모델은 `base/dispatch-core-plan.md` | | `checkpoint-handler-pattern-reversed.md` | **[역전됨, 2026-08-13 다섯 번째 세션 신설]** `AttributeGroupHandler`의 이름 소유권 충돌을 고치려고 만든 `Dispatch.processAs`/`Dispatch.retractSelfAndUnder` 체크포인트 핸들러 패턴(같은 날 네 번째 세션 신설) — `chains`를 핸들러 identity가 아니라 재귀 깊이 인덱스로 추적하는 더 근본적인 재설계로 대체되며 같은 날 바로 불필요해짐. `State>`도 이 재설계로 UB에서 정상 지원 대상으로 바뀜 | diff --git a/.claude/archive/existing-instance-bind-rejected.md b/.claude/archive/existing-instance-bind-rejected.md index 5385490..7e067fb 100644 --- a/.claude/archive/existing-instance-bind-rejected.md +++ b/.claude/archive/existing-instance-bind-rejected.md @@ -3,7 +3,7 @@ > **⛔ [2026-08-14 세션, 사용자 확정 — 기각]** `research/`에서 > `archive/`로 이전. **더 이상 "열린 가능성"이 아니라 미지원으로 확정.** > -> **⭐ [2026-08-28] 좁은 형태로 부활 — `research/existing-mount-plan.md`.** 여기서 +> **⭐ [2026-08-28] 좁은 형태로 부활 — `base/claim-plan.md`.** 여기서 > 기각된 것은 *"이미 있는 Instance에 나중에 새 props를 다시 바인드"*이고 그 > 사유(바깥이 자식 구성을 밀고 당기면 부기가 깨진다)는 그대로 유효하다. 부활한 > 것은 그 반대 방향 — **한 번 `Claim`하면 quad가 소유하고 직계 자식은 사용자가 diff --git a/.claude/archive/question-resolved.md b/.claude/archive/question-resolved.md index 9bffd51..4bee4aa 100644 --- a/.claude/archive/question-resolved.md +++ b/.claude/archive/question-resolved.md @@ -1292,3 +1292,16 @@ L2 디스패치 Handler · Dispatch 코어 · chains · None · ┐ 히스토리 문서라 소급 수정하지 않았다.** 2026-08-24 이전에 쓰인 그 문서들의 `M2`/`M3`는 **옛 의미**(M2=디스패치, M3=반응형)로 읽을 것. 이 경고는 `ROADMAP.md`의 M2 배너에도 있다. + +## [해소됨, 2026-08-28] `Claim` 갈래 — `research/existing-mount-plan` §5 여덟 항목 + +**2026-08-28 10라운드 `H-148`에서 신설된 research 문서의 §5 갈래 여덟 개가 같은 날 +사용자와 대화형으로 전량 확정돼 `base/claim-plan.md`로 승격됐다** — 결정과 사용자 +원문은 그 문서 §7, 대화 원문은 `session/2026-08-28-02-claim-promotion.md`. 요지: +루트 키는 센티널(맨 테이블 + `Claim<>`는 타입 자동완성 손실로 기각) / 디스크립터 +순서 정본 / claim된 부모 안 `New` 자식 허용(위치는 프로바이더 몫) / debug 검사 범위는 +`research/debug-tooling-plan.md`로 이동 / 이름 `Claim` + `D.Mapper` / M5 / +**§5-7(여러 스크립트가 한 `PlayerGui`)은 `Claim` 1회·전체 소유 유지 + 루트의 +`.Parent =`는 밖에서 허용으로 복원**(사용자: *"정확히는 두번 Claim 불가하다는 의미. +필요하다면 Slot 을 안에 만들고 리턴하는 중간 모듈을 만들어야함. 밖에서 .Parent +설정하는건 괜찮아"*) / 매핑된 정적 자식은 같은 `InstanceChildHandler`. diff --git a/.claude/base/architecture.md b/.claude/base/architecture.md index ad701ea..9f87e9c 100644 --- a/.claude/base/architecture.md +++ b/.claude/base/architecture.md @@ -297,7 +297,7 @@ quad/ ├── pesde.toml # quad-base가 아니라 quad-types에만 workspace 의존 └── src/ ├── RobloxFactory.luau # BaseModule 뮤테이션, 재호출 가드(같은 팩토리=무시/다른=에러) — 주입 대상 목록은 아래 EngineOps.luau 줄이 소스 — 여기서 다시 나열하지 않는다(**[2026-08-22]** 예전엔 addTag/removeTag/setAttribute까지만 적혀 있어 native*/setTimeout이 빠져 있었음). bindLifetime/canBound/canExecute도 같은 경로로 주입됨 - ├── EngineOps.luau # 주입되는 엔진 op 구현: addTag(inst,{string})/removeTag(inst,{string})=CollectionService, setAttribute(inst,name,v)=inst:SetAttribute(v==nil이면 삭제), nativeDispose(inst)=inst:Destroy()(`dispose(value)`가 `isSlot`이 아닐 때 위임, `base/slot-plan.md`), **[2026-08-21 5라운드 신설, 이름 확정] `native*` 물리 트리 조작 계층** — nativeInsert/nativeExtract/nativeRemove/nativeMove/nativeSwap(0-based 절대 offset + 대상 요소 배열을 받음; Roblox는 offset을 무시하고 배열을 쓰고 DOM은 둘 다 씀). 미주입이면 에러가 아니라 **조합 폴백**. **[2026-08-22 추가] 시간 op 둘** — setTimeout(func, delay) -> Timeout / clearTimeout(t), Roblox는 task.delay/task.cancel로 배선(**인자 순서가 반대라 주의**); `Debounce`/`Throttle`이 얹힐 때 필요하고 그 전엔 미주입이어도 무방(`base/debounce-throttle-plan.md`). **이 줄이 주입 op 전체 목록의 단일 소스다** — 다른 문서는 개수를 세지 말고 여기를 가리킬 것 (`base/dispatch-core-plan.md` "base가 소유하는 핸들러와 주입되는 엔진 op" 절). **[2026-08-24 6라운드 신설] `isInst(value): boolean`**(`H-40` — 요소 타입 검증을 화이트리스트로 뒤집으면서 생긴 판정 술어, quad-roblox는 `typeof(value) == "Instance"`)**와 `onDestroying(inst, fn): Connection`**(`H-11` — `Effect`의 leaf 사망 cleanup을 발화시키는 훅, `bindLifetime`이 `isEffect`일 때 부른다, quad-roblox는 `inst.Destroying:Connect(fn)`). **⚠️ 이 둘은 `native*`의 "미주입이면 조합 폴백" 규칙의 예외다 — 조작이 아니라 판정/훅이라 조합으로 만들 수 없어 미주입이면 명확한 에러**(`addTag`/`setAttribute`와 같은 취급) + ├── EngineOps.luau # 주입되는 엔진 op 구현: addTag(inst,{string})/removeTag(inst,{string})=CollectionService, setAttribute(inst,name,v)=inst:SetAttribute(v==nil이면 삭제), nativeDispose(inst)=inst:Destroy()(`dispose(value)`가 `isSlot`이 아닐 때 위임, `base/slot-plan.md`), **[2026-08-21 5라운드 신설, 이름 확정] `native*` 물리 트리 조작 계층** — nativeInsert/nativeExtract/nativeRemove/nativeMove/nativeSwap(0-based 절대 offset + 대상 요소 배열을 받음; Roblox는 offset을 무시하고 배열을 쓰고 DOM은 둘 다 씀). 미주입이면 에러가 아니라 **조합 폴백**. **[2026-08-22 추가] 시간 op 둘** — setTimeout(func, delay) -> Timeout / clearTimeout(t), Roblox는 task.delay/task.cancel로 배선(**인자 순서가 반대라 주의**); `Debounce`/`Throttle`이 얹힐 때 필요하고 그 전엔 미주입이어도 무방(`base/debounce-throttle-plan.md`). **이 줄이 주입 op 전체 목록의 단일 소스다** — 다른 문서는 개수를 세지 말고 여기를 가리킬 것 (`base/dispatch-core-plan.md` "base가 소유하는 핸들러와 주입되는 엔진 op" 절). **[2026-08-24 6라운드 신설] `isInst(value): boolean`**(`H-40` — 요소 타입 검증을 화이트리스트로 뒤집으면서 생긴 판정 술어, quad-roblox는 `typeof(value) == "Instance"`)**와 `onDestroying(inst, fn): Connection`**(`H-11` — `Effect`의 leaf 사망 cleanup을 발화시키는 훅, `bindLifetime`이 `isEffect`일 때 부른다, quad-roblox는 `inst.Destroying:Connect(fn)`). **⚠️ 이 둘은 `native*`의 "미주입이면 조합 폴백" 규칙의 예외다 — 조작이 아니라 판정/훅이라 조합으로 만들 수 없어 미주입이면 명확한 에러**(`addTag`/`setAttribute`와 같은 취급) **[2026-08-28 `Claim`, M5 — `base/claim-plan.md`] `nativeFindChild(inst, key): inst?`** — 매퍼 디스크립터의 키(Roblox는 `Name`, web은 id/selector)로 직계 자식을 찾는 조회 op, quad-roblox는 `inst:FindFirstChild(key)`. 조회라 조합으로 만들 수 없어 `isInst`/`onDestroying`처럼 **조합 폴백의 예외**(미주입이면 명확한 에러 — 이 분류는 에이전트 판단, 사용자 확정은 "필요 핸들을 프로바이더에 남긴다"까지) ├── LifetimeHandle.luau # bindLifetime/canBound/canExecute 실제 구현 — GetPropertyChangedSignal("ClassName") 연결 트릭으로 gcconn 확보, Relate:SetWeak으로 gcconn/gchold 저장(**[정정, 2026-08-18] `SetStrong`이 아님 — 생존은 클로저 upvalue와 `gchold[1]`이 이미 보장, strong으로 잡으면 상호 강참조 누수**, `base/lifecycle-pattern.md`). `canBound`/`canExecute`는 비공개 헬퍼 하나를 공유하는 얇은 진입점(2026-08-14 열한 번째 세션). Relate 자체는 순수 Lua라 quad-roblox 쪽 재구현 없음(quad-base 그대로 재사용) ├── Handlers/ │ ├── Property.luau # 일반 프로퍼티 세팅 + `isTween(realv)` 분기(3-상태 릴레이션 슬롯 `RobloxTween|true|nil`, hasBeenSet 억제, override 정책) — 구 `Handlers/Tween.luau`(높은 우선순위 store-bind 핸들러)는 폐기(`archive/tween-special-bind-key-reversed.md`) diff --git a/.claude/base/bind-system-plan.md b/.claude/base/bind-system-plan.md index 5a57ee2..d61ce3b 100644 --- a/.claude/base/bind-system-plan.md +++ b/.claude/base/bind-system-plan.md @@ -177,6 +177,10 @@ New<> "Frame" { ... } -- 직접 사용도 같은 모양 D.Frame = New<> "Frame" :: (({ ...타입명시 }) -> Frame) ``` +**[2026-08-28 `Claim`]** 그 `{ ...타입명시 }`의 **필드 파트**는 생성기가 `type +Param`으로 이름 붙여 찍고 `D.Mapper.`와 공유한다(사용자 확정, +`base/claim-plan.md` §2). children 배열 파트를 어떻게 가르는지는 그 문서 §10-D. + - **이름은 대문자 `New`로 통일**(사용자 확정: *"2. New입니다."*) — PA님 코드 인용의 소문자 `new`와 섞여 있던 것을 정리. - **뒤집는 게 아니라 명시화**다 — PA님 패턴의 `constructor.Frame = @@ -336,14 +340,20 @@ end 그건 새 메커니즘이었다(`isHandlable` 거부는 `Dispatch.process`의 일반 매치 실패 문구로 떨어지고 그 자리에 특수 분기는 두지 않기로 확정돼 있다) — **철회**, 일반 문구 그대로. 오해는 사용자 문서가 맡는다. - - **⛔ [2026-08-28 폐기, 10라운드 `H-148` → `research/existing-mount-plan.md`] - 아래 "루트는 사용자가 밖에서 `.Parent =`" 예외는 하루 만에 뒤집혔다** — 사용자: - *"slot 은 물리 장치에 mount 할 방법이 거의 존재하지 않음 … PlayerGui 가 - 상위에 있고 거기에 GUI 를 여럿 바운딩 해야해서 `Slot { Shop{} … }` 하는게 안 - 될것 같은 느낌이 듦. 이건 Parent 이상의 문제인것 같아."* 루트는 밖에서 - `Parent`를 만지는 게 아니라 **quad가 `Claim`으로 소유**한다(PlayerGui·`Clone()` - 사본·Studio GUI — 그 research 문서가 소스, **M5 스코프**(`H-161`)). 그러면 `.Parent =`를 - 사용자가 쓸 자리 자체가 없어진다. 아래는 폐기 전 서술: + - **⭐ [2026-08-28 좁혀서 복원 — `base/claim-plan.md` §5] 아래 "루트는 + 사용자가 밖에서 `.Parent =`" 예외는 10라운드 `H-148`에서 한때 폐기됐다가 같은 날 + `Claim` 갈래 확정에서 복원됐다** — 루트의 `Parent`는 만든 방법(`New`/`Claim`)과 + 무관하게 어느 부기에도 속하지 않으므로 밖에서 대입해도 된다(사용자: *"밖에서 + .Parent 설정하는건 괜찮아. 루트도 quad 소유이긴 한데 … 정확히는 ScreenGUI 가 + 이미 존재해도 똑같음"*). 여러 스크립트가 한 `PlayerGui`를 쓰는 흔한 경우가 이 + 경로다. 이미 있는 트리를 quad 소유로 만드는 것은 별개 표면 `Claim`(`base/claim-plan.md`, + **M5 스코프** — `H-161`). 아래 원 서술과 그 안의 "만들지 않는다"는 그대로 유효하다. + **[당시 폐기 논거 — 지금은 유효하지 않음]** `H-148`은 사용자의 *"slot 은 물리 + 장치에 mount 할 방법이 거의 존재하지 않음 … PlayerGui 가 상위에 있고 거기에 GUI 를 + 여럿 바운딩 해야해서 `Slot { Shop{} … }` 하는게 안 될것 같은 느낌이 듦. 이건 + Parent 이상의 문제인것 같아."*에서 출발해 "루트도 `Claim`으로만 소유하고 `.Parent =`를 + 사용자가 쓸 자리 자체가 없어진다"로 갔었다 — 그 뒤 `Claim`이 1회·전체 소유라 + 여러 스크립트의 PlayerGui를 못 담는다는 게 드러나 위처럼 복원됐다. 아래 원 서술: **[2026-08-27 확정, 9라운드 `H-146`] 루트는 이 금지의 범위 밖이다 — quad 트리의 최상위를 quad 밖 부모에 붙이는 건 사용자가 밖에서 `.Parent =`로 한다.** 위 인용문의 *"외부에서 직접 Parent 설정해주지 말것"*이 막는 것은 diff --git a/.claude/base/claim-plan.md b/.claude/base/claim-plan.md new file mode 100644 index 0000000..2834459 --- /dev/null +++ b/.claude/base/claim-plan.md @@ -0,0 +1,321 @@ +# 이미 있는 트리를 quad가 소유하기 — `Claim` + `D.Mapper` + +> **[2026-08-28 신설·같은 날 확정 — `research/`에서 승격(옛 파일명 `existing-mount-plan`)]** +> 10라운드 `H-148`(`Parent` 거부 문구)을 논의하다 **루트 마운트 표면의 부재**가 +> 드러나 사용자 발의로 만든 문서. 방향(§1~§4)은 `session/2026-08-28-01-handtrace-round10-resolution.md`에서, +> 갈래(§7)는 `session/2026-08-28-02-claim-promotion.md`에서 사용자가 확정했다. +> **M5 스코프**(`H-161` — 프로바이더 op `nativeFindChild`가 필요하니 프로바이더 +> 마일스톤이 자연스러운 자리). M2 착수 게이트 아님. 구현 체크리스트는 §9. +> +> **`archive/existing-instance-bind-rejected.md`(2026-08-14 기각)와의 관계**: +> 그 기각은 *"이미 있는 Instance에 나중에 새 props를 다시 바인드"*였고 사유는 +> "quad가 만들지 않은 트리의 자식 구성을 바깥이 밀고 당기면 `setLength`/ +> `setOffsetSource` 부기가 깨진다"였다. 이 문서는 그 반대 방향 — **한 번 +> claim하면 quad가 소유하고 직계 자식은 사용자가 전부 매핑한다**는 계약이라 +> claim 뒤엔 quad가 만든 트리와 같은 불변식이 성립한다. **재바인드는 여전히 +> 미지원**(claim은 1회, 디스크립터는 `Processed`로 소진). + +## 1. 왜 필요한가 (사용자 원문) + +`H-146`이 "루트는 사용자가 밖에서 `.Parent =`"로 닫혔는데, 사용자가 이어서 +지적했다: *"slot 은 물리 장치에 mount 할 방법이 거의 존재하지 않음. Parent = +처럼 마운트 할 방법이 없는데? 그럼 PlayerGui 가 상위에 있고 거기에 GUI 를 +여럿 바운딩 해야해서 `Slot { Shop{} … }` 하는게 안 될것 같은 느낌이 듦. 이건 +Parent 이상의 문제인것 같아."* — 즉 **루트가 Slot일 수 없다**(Slot은 quad가 +부기를 가진 부모 `inst` 아래에만 산다). 그리고: *"생성할 요소들 자체가 너무 +많은 경우 Clone 이 엇청 더 싸서, 그 Clone 된 것 아래 quad 를 바인딩 할 방법이 +있으면 좋은것도 사실인듯. … web 에서도 템플릿에 의해 유효한 요소일꺼고, +roblox 에서도 보면서 만들어낸 GUI를 바인딩하는건 흔한 요구라서 이 역시 흔한 +필요일꺼야."* + +결론(사용자): *"이런 방식으로, 이미 있는 PlayerGui 아래 마운트를 거는거지. +… 이것도 똑같이 Quad 가 소유하게 될 요소가 되는거지."* — quad 밖에서 온 +트리(PlayerGui, `Clone()` 사본, Studio에서 만든 GUI)를 **quad 소유**로 만드는 +표면이다. 루트의 `.Parent`를 밖에서 만지는 일은 이것과 별개로 **계속 허용**된다 +(§5 — 처음엔 "parent 를 설정할 문제 자체가 사라져"로 봤으나 §7-7에서 좁혀 복원). + +## 2. 모양 + +```lua +local M = D.Mapper -- 정의는 D 안에 산다. 유저가 필요하면 꺼낸다 +local cloned = Claim(template:Clone(), M.Frame(M.Root) { -- 루트는 이름 대신 센티널 + M.TextLabel "Title" { Text = title }, + M.Frame "List" { + Slot { … }, -- 기존 부모 아래 Slot — 이제 가능 + }, + BackgroundColor3 = color, -- props는 New와 같은 derive 테이블 +}) -- -> cloned (claim한 루트 Instance — 타입은 넣은 inst의 타입 그대로) +``` + +- **`D.Mapper.(key) { … }`는 Instance를 만들지 않고 디스크립터만 + 만든다**(브랜드 `Mapper`류) — derive 테이블 + 매칭 키. `D.Frame`에 직접 얹지 + 않는다(사용자: *"D.Frame 에 바로 바인딩은 위험한듯. 의미가 겹쳐버려"*). + `key`는 자식이면 이름(`string`), 루트면 센티널 `D.Mapper.Root`(§7-1). +- **props 타입은 `D.`와 공유한다.** 생성기가 클래스마다 `type FrameParam + = { … }`를 찍고 `D.Frame`과 `D.Mapper.Frame`이 그 하나를 쓴다 — 리턴만 다르다 + (`Frame` vs `MapperDescriptor`). 사용자: *"D.Frame 의 함수의 부분들을 + type FrameParam = {} 형태로 빼서 공유되는 타입 부분으로 D.Mapper.Frame 도 + 구성되고, 리턴부분만 다르게"*. `base/bind-system-plan.md`의 `D.Frame = + New<> "Frame" :: ((…) -> Frame)` 캐스트가 인라인 타입 대신 이 이름을 + 쓰게 되는 것뿐이고, `D`가 전량 생성기 산출물이라 손으로 쓸 곳은 없다. + + ```luau + type FrameParam = { … } -- 생성기 산출물 (필드 파트) + D.Frame :: (FrameParam) -> Frame + D.Mapper.Frame :: (key: string | MapperRoot) -> (FrameParam) -> MapperDescriptor + Claim :: (inst: T, desc: MapperDescriptor) -> T -- T는 inst에서 그대로 + ``` + + **⚠️ [2026-08-28 `/code-review`] 배열 파트는 그대로 공유할 수 없다** — `New`의 + children 배열엔 `MapperDescriptor`가 올 수 없고(오면 런타임 "매치 핸들러 없음"), + 매퍼의 배열엔 와야 한다(§2 예시의 `M.TextLabel "Title" {…}`). 사용자 인용은 + **필드 파트의 타입 공유**를 승인한 것이고 배열 파트를 어떻게 가를지는 정하지 + 않았다 → §10-D. `base/bind-system-plan.md`의 `D` 생성기 절엔 아직 `Param`이 + 없다 — 생성기 구현(`ROADMAP.md` M5 `D/init.luau`)이 이 문서를 같이 본다. + +- **`Claim(inst, descriptor) -> inst`가 최상위이고 타입 인자를 받지 않는다.** + 사용자: *"Claim 자체는 타입을 받는건 말이 안되어보임. New 와는 완전 다른 + 계열이라서: New 는 후행 입력에 대한 타입을 선언시키는 D 계열이지만, Claim 은 + 공유 부분이고 … 엔진 요소를 알 수 없어"*. 반환 타입은 **넣은 `inst`의 타입 + 그대로**(`(inst: T, …) -> T`) — 처음엔 "디스크립터가 클래스 타입을 실어 `inst`와 + 대조·반환을 좁힌다"(에이전트 제안)로 적었으나 **[2026-08-28 `/code-review`]** + 실사용 `inst`는 `template:Clone()`·`PlayerGui` 둘 다 `Instance` 타입이라 그 추론은 + 성립하지 않는다(좁히려면 사용자가 `::` 캐스트 — `New<>`의 "범위 밖은 `any`"와 + 같은 취급). 디스크립터 클래스 ↔ `inst` 클래스 대조는 **debug 검사**(§3)의 몫. + `inst`는 quad 밖에서 온 것. 이 호출로 `inst`와 + 매핑된 하위 전부가 **quad 소유**가 된다 — `New`가 만든 것과 같은 gcconn/gchold·부기 + (그 셋업을 누가 하는지는 §10-A). +- **디스크립터는 1회용** — `PreRef`의 `_fired`처럼 **디스크립터 객체에 소진 + 플래그**를 세우고 재사용이면 `error(…, 2)`. **[2026-08-28 `/code-review` 정정]** + `PreRef` 관용구의 다른 절반(배열 슬롯을 `ProcessedPreRef` 센티널로 교체)은 + **가져오지 않는다** — 매핑 자식의 배열 슬롯은 §4대로 **해석된 Instance로 교체**돼야 + `InstanceChildHandler`에 닿고, 루트 디스크립터는 배열에 있지도 않다. 사용자 + 테이블을 in-place로 바꿀지 새 테이블을 만들지(그러면 `ProcessedModifier` 자리와 + 인덱스가 `New`와 달라진다)와 Modifier 필드 안에 숨은 디스크립터를 DFS가 보는지는 + **구현 시 정할 것**(§9). **같은 `inst`를 두 번 `Claim`하는 것도 error**(§7-7) — + 어느 레지스트리로 판정하는지는 §10-B. +- **매칭은 프로바이더 주입 op** `nativeFindChild(inst, key)`(가칭) — Roblox는 + `Name`, web은 id/selector. **quad-base가 순회·부기 전반을 구현하고 + 프로바이더는 이 핸들만 낸다**(사용자: *"quad-base 에서 전반을 구현해주고 + 필요 핸들을 구현하라고 남기는건 괜찮은 생각"*). 주입 op 전체 목록의 단일 + 소스는 `base/architecture.md`의 소스 트리(`EngineOps.luau` 줄) — 거기 추가한다. +- **여러 quad 인스턴스가 한 트리를 claim — UB**(사용자 확정). 같은 quad의 이중 + claim은 위처럼 error. +- **`H-142`(props에 `Parent` 금지)는 그대로.** 매퍼 디스크립터의 props도 `New`와 + 같은 derive 테이블이라 같은 금지를 받는다. + +## 3. 계약 — 자식은 전부 매핑한다 + +사용자: *"모든 개체를 유저가 직접 네임을 매핑해서 derive 테이블 안에서 내부 +요소를 전부 매핑해준다를 계약으로 잡으면 문제가 없다고 생각해."* + +- **부기 대상(그려지는 자식)은 전부 매핑해야 한다.** 안 된 자식이 남으면 + `nativeInsert`의 삽입 위치(web은 곧 DOM 순서)와 Length/Offset이 어긋난다. +- **디스크립터 배열 순서가 정본이다**(§7-2 (a)). 기존 트리의 실제 순서가 + 다르면 일치는 사용자 책임 — quad는 `nativeMove`로 맞추지 않는다. Roblox는 + 물리 순서가 의미 없어 비용 0, web에선 어긋나면 UB(debug 검사 대상). +- **숏핸드(`UICorner` 등 `UI*`)는 부기 대상이 아니다** — 그려지지 않고 Roblox에만 + 있으며 단순 `Parent` 대입 요소. 사용자 확정: *"숏핸드를 quad 에서만 직접 + 쓰거나 … 아니면 실제 UI 객체를 바인딩해서 숏핸드를 안 쓰거나"* — 둘 중 하나: + (i) 템플릿엔 `UI*`가 없고 quad가 숏핸드 키로 만든다, (ii) 템플릿의 `UI*`를 + `M.UICorner "UICorner" {…}`처럼 **실제 객체로 매핑**하고 그 부모에 숏핸드 + 키는 안 쓴다. 섞으면(템플릿에 `UICorner`가 있는데 숏핸드 키도 씀) 둘이 + 생기는 것은 UB. +- **이름 중복·부재는 UB**(사용자 확정). **debug 모드**에선 `seen` 맵으로 중복을 + 잡아 error(사용자 제안). 검사 범위를 어디까지 넓힐지(부재·클래스 불일치· + 미매핑 부기 대상·물리 순서 불일치·이중 claim)는 **디버깅 도구 설계의 몫**으로 + 옮겼다(§7-4 → `research/debug-tooling-plan.md`의 "열린 질문" 절) — 여기선 + "debug 검사가 있다"까지만 확정. + +## 4. 처리 순서 — `drive` 위의 한 겹 + +**`New`와 반대 방향에서 시작한다.** `New`는 안쪽 생성자가 먼저 평가돼 자연히 +bottom-up이지만, 매퍼의 안쪽은 평가 시점에 자기 Instance를 모른다(부모가 +아직 없다). 그래서 `Claim`이 **DFS로 내려가며 이름으로 해석 → 자식부터 +`drive` → 올라오며 부모 `drive`**. 사용자: *"핸들러가 된다면 위험해. 일반 +생성과 다르게, 상위 부터 처리하거든 … DFS 로써, 내려가는게 먼저고 그 뒤에서 +derive 를 걸어야해. 이건 derive 에선 구현하지 않고, 그 위의 무언가로써 +구현되어야할듯."* — `drive`/핸들러 층은 안 바뀌고 그 **위의 한 겹**이다. + +- 매핑된 정적 자식은 부모의 derive 테이블에서 **평범한 정적 자식**(배열 자리의 + Instance)으로 보인다 — 해석이 끝난 뒤엔 `InstanceChildHandler`가 그대로 받아 + `setOffsetSource(inst, k, None)` → `v.Parent = inst` → `setLength(inst, k, 1, inst)` + (`base/dispatch-core-plan.md`의 `H-134` 문단). **별도 핸들러는 없다**(§7-8) — + 이미 거기 있는 자식에 같은 `Parent`를 재대입하는 것은 엔진 no-op이고, `H-154` + 문단이 이미 *"`Parent = inst`(같은 값, 엔진 no-op)"*으로 전제한 사실. +- Slot은 평소처럼 `native*`로 기존 부모 아래 끼운다. +- **⚠️ [2026-08-28 `/code-review`] `PreRef`/`OnCreated`의 불변식이 약해진다.** + `drive`를 그대로 쓰므로 claim된 inst에서도 `PreRef`가 먼저 발화하지만, `New`가 + 보장하던 *"아직 자식도 프로퍼티도 없다"*(`base/bind-system-plan.md`)·*"이 인스턴스에 + 뭐가 됐든 일어나기 전"*(`base/lifecycle-hooks-plan.md`)은 **거짓**이다 — inst는 이미 + 템플릿의 자식·프로퍼티를 갖고 있고, 매핑 자식의 `ChildAdded`는 아예 안 뜬다. + `Claim`에서 `PreRef`가 뜻하는 것은 "quad가 이 inst에 무언가 하기 전"뿐. §4가 + 특수 분기를 금지하므로 지키려 하지 않고 **문서화 대상**(§9)으로 둔다. + +## 5. 루트의 `Parent`는 부기 밖이다 — 밖에서 `.Parent =` 허용 + +**`H-146`의 루트 예외는 좁혀서 복원된다**(§7-7). 사용자: *"밖에서 .Parent +설정하는건 괜찮아. 루트도 quad 소유이긴 한데, .Parent 를 밖에서 설정하는건 +괜찮음. 정확히는 ScreenGUI 가 이미 존재해도 똑같음."* + +- **quad 트리의 루트**(`New`로 만든 것이든 `Claim`한 것이든)의 `.Parent`는 + 어느 Length/형제 순서 부기에도 속하지 않는다 — 그래서 사용자가 밖에서 + `root.Parent = PlayerGui`로 붙이고 떼는 것은 **허용**이고, `H-146`의 사용자 + 논거(*"부기가 없는 객체에 Quad 의 객체를 주입하는 성격의 API 는 아니거든 … + 해당 부분은 각 엔진을 사용하는 최종 사용자의 몫"*)가 그대로 성립한다. + `Mount(root, parent)`류 표면은 여전히 만들지 않는다. +- **금지는 그대로 "quad가 소유한 부모 *아래*"다** — Slot 요소·정적 자식 자리에 + 밖에서 끼우거나 빼는 것(`base/slot-plan.md`의 "동적 자식은 반드시" 절). 루트의 + 부모는 quad가 소유하지 않은 것이라 이 금지 밖이다. +- **props의 `Parent`는 여전히 금지**(`H-142`) — 붙이는 건 props가 아니라 밖의 한 줄. + 거부 배선의 에러 문구는 일반 매치 실패 그대로(`H-148`), 오해는 사용자 문서가 맡는다. +- **여러 스크립트가 한 `PlayerGui`를 쓰는 흔한 경우는 이걸로 닫힌다** — 각 + 스크립트가 자기 `ScreenGui`를 `New`(또는 `Claim`)하고 `.Parent = PlayerGui` + (이 경로는 PlayerGui를 claim하지 않으므로 §6/§10-C의 한계와 무관). + `Claim(PlayerGui, …)`은 **PlayerGui 전체를 그 스크립트가 소유하겠다**는 뜻이라 + 한 번만 가능하고, 여러 스크립트가 PlayerGui 직하 `Slot`을 공유해야 하면 + **`Claim`을 한 번 하고 그 안에 Slot을 만들어 반환하는 중간 모듈**을 둔다 + (사용자: *"정확히는 두번 Claim 불가하다는 의미. 필요하다면 Slot 을 안에 만들고 + 리턴하는 중간 모듈을 만들어야함"*) — 단 PlayerGui 자체를 claim하는 그 경우는 + §6/§10-C(엔진이 자식을 넣는 컨테이너)의 답에 걸린다. + +## 6. 이 문서가 여는 것 + +- **루트 컨테이너**: `Claim(PlayerGui, M.PlayerGui(M.Root) { Slot {…} })` — PlayerGui가 + quad 소유 부모가 되어 Slot이 그 아래 산다(한 스크립트가 PlayerGui 전체를 + 소유하는 경우). **⚠️ [2026-08-28 `/code-review`] 이건 own-all 계약(§3)의 한계 + 사례다** — Roblox `PlayerGui`는 엔진이 `StarterGui`를 스폰·리스폰마다 새로 복제해 + 넣는 컨테이너라(`ResetOnSpawn`), 디스크립터가 매핑할 수 없는 자식이 **quad 소유 + 부모 아래 밖에서** 들어온다(`base/slot-plan.md`의 "동적 자식은 반드시" 절이 UB로 + 못 박은 그것). 계약 그대로면 *"엔진이 자식을 넣는 컨테이너를 claim하는 것은 UB"* + 이고 흔한 경로는 §5의 `.Parent =`다 — 이 한계를 계약으로 명문화할지, `StarterGui`를 + 안 쓰는 전제를 문서화로 둘지는 **§10-C**. 같은 자리: 매퍼 생성기 범위(GUI 클래스)에 + `PlayerGui`류 컨테이너가 들어가는지도 거기서. +- **템플릿 대량 생성**: `template:Clone()` → `Claim` — 각 사본이 독립 소유. + Claim이 Instance를 돌려주므로 **Slot 요소로도 그대로 쓸 수 있다**(요소는 + `inst`) — "요소가 너무 많은 경우"의 답. +- **비루트 사용**: `New "Frame" { Claim(clone, …) }` — 반환된 `inst`가 정적 + 자식으로 들어가면 `InstanceChildHandler`가 `Parent =`와 부기를 한다. 평가 + 순서상 `Claim`이 먼저 끝나므로 bottom-up이 유지된다. +- **claim된 부모 안의 `New` 자식**: `M.Frame "List" { New "Frame" {…} }` — 매핑 + (이미 있음)과 생성(새로 붙임)이 한 배열에 섞여도 된다(§7-3). `New` 자식은 + 디스크립터 테이블이 평가될 때 이미 다 구워져(자기 서브트리 `drive` 완료) 있고, + `Claim`이 올라오며 부모를 `drive`할 때 정적 자식으로 부기된다 — + `New "Frame" { New "Frame" {} }`과 같은 모양. **위치는 프로바이더의 몫**: + Roblox는 `.Parent =`라 순서가 무의미하고, **web은 정적 자식 핸들러가 + `nativeInsert(offset)`을 써야 디스크립터 순서 자리에 놓인다** — 안 그러면 맨 뒤 + (그리고 **이미 붙어 있는 매핑 자식**엔 그 핸들러가 `nativeInsert`를 다시 부르면 + 안 된다 — 같은 부모 안 재삽입은 이동이라 §3 "quad는 `nativeMove`로 맞추지 않는다"와 + 어긋난다; web 정적 자식 핸들러는 "이미 그 부모의 자식이면 건너뜀"을 가져야 한다) + (사용자: *"Add 가 위치를 진짜 실어서 보내지 않으면 맨 뒤에 놓인다는게 문제일 + 뿐"*). §3 "디스크립터 순서가 정본"의 따름정리이고 `Claim`의 결정이 아니다. + +## 7. 결정 기록 — `research/` 시절 §5 갈래의 답 (2026-08-28) + +당시 갈래 목록(a/b/c 선택지 원문)은 `session/2026-08-28-02-claim-promotion.md` +끝의 "옛 §5 원문" 절에 전문 보존. 번호는 그 research 문서의 §5 번호 그대로. + +1. **루트 디스크립터의 키 — (a) 센티널.** 갈래 (b)("클래스 없는 맨 테이블 + + `Claim<<"Frame">>`", 에이전트 권고)는 **기각** — 사용자: *"권고 b는 문제가 + 생겨. 루트에 대해서 {} 안의 타입체크와 타입 자동완성이 전혀 안 먹음."* 그리고 + `Claim`이 타입 인자를 받는 것 자체가 `New` 계열과 어울리지 않는다(§2 인용). + (c)("이름을 받되 무시")도 안 씀. 센티널은 사용자 스케치 `MapperRoot = {} + Mapper.Frame (MapperRoot) {}` 그대로이고, **놓는 자리 `D.Mapper.Root`는 + 에이전트 제안**(매퍼 옆에 두면 `M.Frame(M.Root)`로 읽힌다) — 이름만 바뀔 수 + 있는 항목. 자식 디스크립터에 센티널을 주는 것(`M.Frame(M.Root)`가 루트가 + 아닌 자리에)은 debug 검사 후보. +2. **물리 순서 — (a) 디스크립터 순서가 정본, 일치는 사용자 책임.** 사용자: + *"나는 처음에 A 를 생각했어. 권고 그대로 가줘."* (b)(`nativeMove`로 quad가 + 맞춤)는 기각. +3. **claim된 부모 안의 `New` 자식 — 허용.** 사용자: *"새로 붙임 자체는 한 배열에 + 섞이는게 문제는 없어보여. 그 경우에서도 순차 마운트 Add 는 작동할것이거든."* + 지적한 순서·위치 문제는 §6 마지막 항목(프로바이더 요구사항)으로. +4. **debug 검사의 범위 — 여기서 정하지 않고 `research/debug-tooling-plan.md`로 + 이동.** 사용자: *"디버깅 도구 만들 때 고려해야할 점으로 옮겨져야해. 부분 부분 + 디버깅 가능성을 아직 다 논한게 없어서 지금 그림으로 보면 작은 그림을 먼저 + 그리는거라서, 미결상황으로, 위치 이동이 필요함"*. 이 문서엔 "debug 검사가 + 있다"(§3)만 남는다. +5. **표면 이름 — `Claim` + `D.Mapper`.** 사용자: *"표면 이름은 Claim 이 가장 + 마음에 들어. D.Mapper 가 이미 있는걸 매핑해서 내가 가진다는 의미적으로 가장 + 맞고."* `Mount`/`Adopt`/`D.Existing` 기각. +6. **마일스톤 — M5**(`H-161`, 헤더). +7. **여러 스크립트가 한 `PlayerGui` — (α) `Claim`은 1회·전체 소유, 루트의 + `.Parent =`는 밖에서 허용.** 문항 원문은 "여러 스크립트/여러 quad"를 나란히 + 놓았지만 실제 흔한 경우는 **한 quad·여러 스크립트**(같은 `quad` 모듈을 + require — 다중 quad UB가 아니라 이중 claim error에 걸린다)라, 그 사례를 막는 게 + 진짜 문제였다. 갈래 (a)(루트 컨테이너용 + 별도 표면 — `H-146` 인용문이 정확히 반대한 것) / (b)(부분 매핑 모드 — web에서 + offset이 남의 자식을 못 봐 `Claim`의 의미가 엔진 의존이 됨) / (c)(다중 claim, + 각자 자기 자식만 소유 — (b)와 같은 약화) 전부 기각. 확정은 §5 — 사용자 원문 + 그대로 *"정확히는 두번 Claim 불가하다는 의미. 필요하다면 Slot 을 안에 만들고 + 리턴하는 중간 모듈을 만들어야함. 밖에서 .Parent 설정하는건 괜찮아. 루트도 + quad 소유이긴 한데, .Parent 를 밖에서 설정하는건 괜찮음. 정확히는 ScreenGUI 가 + 이미 존재해도 똑같음."* 마지막 문장이 `Claim`한 루트에도 같은 허용을 준다 — + 루트의 `Parent`는 만든 방법과 무관하게 부기 밖. +8. **매핑된 정적 자식의 `Parent` 대입 — 같은 핸들러, 재대입 감수.** 사용자: + *"5-8 확인완료."* 근거는 §4. + +## 8. 검토 후 안 만들기로 한 것 + +- `Claim<<"Frame">>` 타입 인자 / 루트를 맨 테이블로(§7-1). +- 루트 컨테이너용 별도 얇은 표면, `Claim`의 부분 매핑 모드, 다중 claim(§7-7). +- claim 시 `nativeMove`로 물리 순서를 디스크립터에 맞추기(§7-2). +- 매핑된 자식 전용 핸들러(§7-8). +- `Mount(root, parent)`류 표면(`H-146`, §5). +- 이미 있는 Instance에 나중에 props를 재바인드(`archive/existing-instance-bind-rejected.md`, + 헤더). + +## 9. 구현 체크리스트 (M5) · 문서화 대상 + +- `D.Mapper.` 생성기 산출 + `type Param` 필드 파트 공유(사용자 확정; + 배열 파트는 §10-D) + 루트 센티널(사용자 확정 — 놓는 자리 `D.Mapper.Root`는 + 에이전트 제안) + 디스크립터 브랜드(`MapperDescriptor`는 가칭 — `Brand` 인스턴스 + 브랜드로 만든다는 것은 `base/brand-plan.md`의 일반 규칙이지 새 결정 아님). +- `Claim(inst, desc) -> inst` — DFS 해석 → bottom-up `drive`, 소진 플래그(§2), 같은 + `inst` 이중 claim error(레지스트리는 §10-B), 이미 quad 소유인 inst의 gcconn/gchold + 셋업(§10-A). **구현 시 정할 것**: 사용자 테이블 in-place 교체 vs 새 테이블, + Modifier 안의 디스크립터 처리, 패키지 안 정의 파일 위치(`quad-base/src/Claim.luau` + 가칭 — `base/architecture.md` 소스 트리에 반영은 M5 착수 때). +- 프로바이더 op `nativeFindChild(inst, key)`(이름 가칭) — `base/architecture.md` 주입 op + 목록에 추가됨(quad-roblox는 `inst:FindFirstChild(key)`). "`native*` 조합 폴백의 + 예외 — 조회라 조합으로 만들 수 없어 `isInst`처럼 미주입이면 명확한 error"는 + **에이전트 분류**(사용자 발언은 "필요 핸들을 구현하라고 남기는 건 괜찮다"까지). +- debug 모드 `seen` 맵(범위는 `research/debug-tooling-plan.md`가 소스). +- `ROADMAP.md` M5 체크박스가 진행의 소스. +- **문서화 대상**(`research/documentation-content-map.md` §4): "전부 매핑" 계약과 + 숏핸드 (i)/(ii) 규칙, 루트 `.Parent =`는 밖에서 / 그 아래는 절대 직접 하지 말 것, + 여러 스크립트의 PlayerGui는 각자 `ScreenGui` + 중간 모듈 패턴, claim된 inst에선 + `PreRef`/`OnCreated`가 "이미 있는 것 위에서" 뜬다는 것(§4). + +## 10. 사용자 판단 필요 — M5 착수 전 (2026-08-28 `/code-review high`가 낸 공백) + +전부 **새 메커니즘이 필요한 공백**이라 `conventions.md` 규칙대로 결정 없이 본문에 +넣지 않았다. M2 게이트 아님. `question.md`에도 같은 넷이 올라가 있다. + +- **A. claim한 inst의 gcconn/gchold 셋업 자리.** §2는 *"`New`가 만든 것과 같은 + gcconn/gchold·부기"*를 약속하는데, 그 (0) 셋업은 quad-roblox `New` ②단계의 + **인라인 코드**(`base/bind-system-plan.md`의 `New` 의사코드 ② 단계, `lifecycle-pattern.md` + (0))이고 `Claim`은 quad-base다. §4대로 `drive`만 부르면 claim된 루트 아래 첫 + `Slot`이 `bindLifetime`의 `gchold[value] = true`에서 nil 인덱스로 죽는다. 갈래: + (a) 프로바이더가 (0) 셋업을 op로 노출(`nativeAdopt(inst)` 가칭)하고 `Claim`이 + 해석한 inst마다 부른다 / (b) `Claim` 본체를 프로바이더에 둔다(quad-base는 순회 + 알고리즘만) / (c) (0) 셋업 자체를 quad-base 함수로 옮기고 `New`도 그걸 쓴다. + **권고 (a)** — `nativeFindChild`와 같은 모양이고 `New`의 경로가 안 바뀐다. +- **B. 같은 `inst` 이중 claim의 판정 레지스트리.** 기존 소유권 레지스트리 + `elementOwner`(`base/slot-plan.md`)에 기록하면 `claimOwner`가 §6의 *"claim한 + inst를 Slot 요소/정적 자식으로 그대로 쓴다"*를 error로 죽인다. 갈래: (a) (0) + 셋업이 만드는 per-inst `InstData`에 `claimed` 플래그 — 셋업과 같은 자리라 새 + Relate가 없다 / (b) 별도 weak-key 레지스트리. **권고 (a)** — A의 답에 따라 + 자동으로 정해지는 모양. +- **C. 엔진이 자식을 넣는 컨테이너(`PlayerGui`)와 own-all 계약.** §6 첫 항목. 갈래: + (a) 계약에 *"엔진이 자식을 넣는 컨테이너를 claim하는 것은 UB"*를 명문화하고 + 흔한 경로는 `.Parent =`(§5)로 — `StarterGui`를 안 쓰는 프로젝트만 PlayerGui를 + claim / (b) 컨테이너용 부분 매핑 — §7-7에서 이미 기각. **권고 (a)**. 부수: 매퍼 + 생성기 범위에 `PlayerGui`류 컨테이너를 넣을지(`base/bind-system-plan.md`의 생성 범위 + *"GUI에 쓰이는 모든 인스턴스"*엔 없다). +- **D. `type Param`의 배열 파트.** 필드 파트 공유는 확정(§2). children + 배열의 원소 유니언은 `New`(Instance·Slot·State…)와 매퍼(+ `MapperDescriptor`)가 + 달라야 한다. 갈래: (a) `type FrameParam = { [number]: C, …필드 }`처럼 원소 + 타입을 파라미터로 — 생성기 한 줄 / (b) 필드 파트만 `FrameFields`로 빼고 배열은 + 각자 — 타입 둘. **권고 (a)**. 어느 쪽이든 `luau-analyze` 스파이크로 확인 + (`base/typing-limits.md` 설계 체크리스트 6번 — *"추론만으로 … 확정하지 말 것"*). diff --git a/.claude/base/lifecycle-hooks-plan.md b/.claude/base/lifecycle-hooks-plan.md index e51f84f..3fa554f 100644 --- a/.claude/base/lifecycle-hooks-plan.md +++ b/.claude/base/lifecycle-hooks-plan.md @@ -131,6 +131,8 @@ value)`류 base 유틸을 문서가 `inst`라고만 부르는 것과 같은 관 호이스팅되어 fire, 즉 "이 인스턴스에 뭐가 됐든 일어나기 전"에 콜백이 불림. 새 Dispatch 메커니즘 불필요, `PreRef` 그대로 재사용. +**[2026-08-28 `Claim` 캐비엇]** `Claim`(`base/claim-plan.md` §4)한 inst에선 이 보장이 약해진다 — inst는 이미 템플릿의 자식·프로퍼티를 갖고 있고 `OnCreated`가 뜻하는 건 "quad가 손대기 전"뿐이다. + **v1과의 관계 — 이름이 같아 보여도 메커니즘은 다름.** `base/ref-plan.md` "`phase` 옵션 폐기 → 위치로 표현, `PreRef` 신설" 절에 이미 이렇게 확정돼 있음: diff --git a/.claude/base/slot-plan.md b/.claude/base/slot-plan.md index 75bf1a7..489a93b 100644 --- a/.claude/base/slot-plan.md +++ b/.claude/base/slot-plan.md @@ -115,7 +115,10 @@ base는 여전히 `T`가 뭔지 모른다 — **아는 건 백엔드고 base는 quad 자신의 배관은 `Destroying` 하나만 보므로 안 깨진다 — 영향은 사용자 코드/렌더 쪽이다. - `isInst`는 이 조합 폴백의 예외다(위 문단) — 판정이라 조합으로 만들 수 - 없고, 미주입이면 명확한 에러여야 한다. + 없고, 미주입이면 명확한 에러여야 한다. 같은 예외가 **`onDestroying`**(`H-11`, + 훅)과 **[2026-08-28] `nativeFindChild`**(`Claim`의 조회 op, `base/claim-plan.md` — + 예외 분류는 에이전트 판단)에도 적용된다. 전체 목록의 소스는 + `base/architecture.md`의 소스 트리(`EngineOps.luau` 줄). - **⚠️ 전제 — 한 Slot의 물리 자식은 부모 안에서 연속 구간을 차지한다.** 범위 op이 성립하는 근거가 전부 이것이다(offset이 누적합이고 중첩 Slot도 같은 `physicalTarget`을 공유하므로 구조적으로 참). quad 밖에서 그 부모에 자식을 끼워 @@ -234,8 +237,8 @@ Slot에 들어간 요소는 **ownership이 귀속**되며 다른 곳에 마운 별다른 강제를 안 했지만(`reference/quad-v1-architecture.md`의 mount.lua 분석 참고 — 실제로는 부모/자식 부기까지 했지만 다중 마운트 방지는 없었음), v2는 **Slot의 마운트 경로 자체**(`attachSlot` 분해분 — 별도 `Mount` 함수가 아니다; **[2026-08-28]** -이미 있는 트리를 quad가 소유하는 `Claim`은 `research/existing-mount-plan.md`에서 -논의 중이고 그것도 이 단일 마운트 불변식을 그대로 진다)가 이 강제를 담당. +이미 있는 트리를 quad가 소유하는 `Claim`(`base/claim-plan.md`, 같은 `inst` 이중 +claim은 error)도 이 단일 마운트 불변식을 그대로 진다)가 이 강제를 담당. Fusion의 `Children` SpecialKey는 이걸 "특정 SpecialKey 하나의 내부 부기"로만 구현했고(재사용 가능한 1급 프리미티브가 아님), Vide는 아예 이 개념이 없어서 @@ -2152,11 +2155,14 @@ UI에 직접 관측, (2) `Dispatch.setLength(inst, i, slot.Length)`가 형제 정확히 호출하는 유일한 정당 경로라, 이걸 우회해서(예: 외부 코드가 Slot이 마운트해둔 부모 Instance에 직접 `.Parent = parentInst`로 자식을 끼워 넣는 것) 자식을 추가/제거하면 `Length`/형제 순서 계산이 그 변화를 몰라 -조용히 어긋남 — 별도 방어 로직 없음, 문서 경고로만 남김. **[2026-08-28 10라운드 -`H-148`]** 루트(`PlayerGui` 등 quad 밖 부모)는 사용자가 `.Parent =`로 붙이는 게 -아니라 **quad가 `Claim`으로 소유**하는 쪽으로 방향이 확정됐다 -(`research/existing-mount-plan.md`, M5 스코프 — `H-161`) — 그래서 이 금지에 예외가 없어진다. -(2026-08-27에 하루 있었던 "루트는 밖에서" 예외는 폐기.) +조용히 어긋남 — 별도 방어 로직 없음, 문서 경고로만 남김. **[2026-08-28 확정]** 이 금지는 +**quad가 소유한 부모 *아래***에만 걸린다 — **루트**(quad 트리의 최상위, `New`로 만든 +것이든 `Claim`한 것이든)의 `.Parent`는 어느 부기에도 속하지 않으므로 사용자가 밖에서 +`root.Parent = PlayerGui`로 붙이고 떼는 것은 허용이다(`base/claim-plan.md` §5 — +10라운드 `H-148`에서 한때 "루트도 `Claim`으로만"으로 폐기됐다가 같은 날 좁혀 +복원, 사용자: *"밖에서 .Parent 설정하는건 괜찮아"*). 이미 있는 트리(PlayerGui +전체·`Clone()` 사본)를 quad 소유 부모로 만들어 그 아래 Slot을 두는 것은 별개 표면 +`Claim`(M5 스코프 — `H-161`). ## `Slot:Single(state, updateFn?, opts?)` — 확정 (2026-08-11 세션, `:List` 위의 순수 sugar) diff --git a/.claude/project-context.md b/.claude/project-context.md index 5f59b8d..33e0e36 100644 --- a/.claude/project-context.md +++ b/.claude/project-context.md @@ -27,7 +27,7 @@ M3=반응형)다. 그 교체의 부작용으로 한때 `question.md` 최우선 (소스는 `qa-request/pre-implementation-handtrace-round8-followup.md`), `question.md` 최우선 절은 다시 비어 있다. **[2026-08-27] 9라운드**(그 커밋 `9dd8213`의 델타 재트레이싱, 발견 `H-124`~`H-141`)는 **Q1~Q3가 `base/`에 -반영됐고**, 같은 날 Q4~Q10·`H-138`·`H-139`·`H-142`까지 **전량 반영** — 소스는 `-round9-followup.md`. 그 반영분에 `/code-review high`가 낸 새 메커니즘 넷(`H-143`~`H-146`)도 **같은 날 `session/2026-08-27-03-handtrace-round9-h143-h146.md`에서 전부 권고 (a)로 확정·반영**돼 **[2026-08-28] 10라운드**(`-round10.md`, 광범위 탐사 `H-150`~`H-157` 포함)도 같은 날 전량 결정·반영(소스 `-round10-followup.md`) — 둘이 뒤집혔다(`fn`은 자기 구독을 못 바꿈 / 루트는 `Claim`으로 quad 소유, `research/existing-mount-plan.md`). 같은 날 후속 `H-158`~`H-164`(`EmitReceive`·`_catchUp` 포함)까지 반영. 남은 미결은 `question.md` 최우선 절의 `Claim` 갈래 하나(게이트 아님). 저장소 루트에 +반영됐고**, 같은 날 Q4~Q10·`H-138`·`H-139`·`H-142`까지 **전량 반영** — 소스는 `-round9-followup.md`. 그 반영분에 `/code-review high`가 낸 새 메커니즘 넷(`H-143`~`H-146`)도 **같은 날 `session/2026-08-27-03-handtrace-round9-h143-h146.md`에서 전부 권고 (a)로 확정·반영**돼 **[2026-08-28] 10라운드**(`-round10.md`, 광범위 탐사 `H-150`~`H-157` 포함)도 같은 날 전량 결정·반영(소스 `-round10-followup.md`) — 둘이 뒤집혔다(`fn`은 자기 구독을 못 바꿈 / 루트는 `Claim`으로 quad 소유, `base/claim-plan.md` — 다만 루트의 `.Parent =`는 같은 날 밖에서 허용으로 복원). 같은 날 후속 `H-158`~`H-164`(`EmitReceive`·`_catchUp` 포함)까지 반영. 같은 날 `Claim` 갈래까지 전량 확정돼 `base/claim-plan.md`로 승격 — `question.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/qa-request/pre-implementation-handtrace-round10-followup.md b/.claude/qa-request/pre-implementation-handtrace-round10-followup.md index b07f2d8..9d8f22d 100644 --- a/.claude/qa-request/pre-implementation-handtrace-round10-followup.md +++ b/.claude/qa-request/pre-implementation-handtrace-round10-followup.md @@ -10,7 +10,7 @@ | 문항 | 무엇 | 상태 | |---|---|---| | `H-147` | 죽은 핸들에서 `Rerun()` | ✅ **확정 — 문항의 전제를 뒤집음**: `fn`/cleanup은 자기 생명주기를 못 바꾼다(A). `H-143`도 함께 소멸 | -| `H-148` | `Parent` 거부 문구 | ✅ **전제 정정** — 문구가 아니라 루트 마운트 표면의 부재. `research/existing-mount-plan.md` 신설(`Claim` + `D.Mapper`), `H-146` 루트 예외 폐기, 전용 문구 철회 | +| `H-148` | `Parent` 거부 문구 | ✅ **전제 정정** — 문구가 아니라 루트 마운트 표면의 부재. `base/claim-plan.md` 신설(`Claim` + `D.Mapper`), `H-146` 루트 예외 폐기(**→ 같은 날 후속 3에서 좁혀 복원** — 루트의 `.Parent =`는 밖에서 허용, 아래 마지막 절), 전용 문구 철회 | | `H-149` | Observer `Subscribe` 위임과 `level 2` | ✅ **확정 (a)** — `Subscribe`/`Unsubscribe`도 게이트·등록을 인라인, 위임 없음 | | `H-150` | `Effect._blocker` 죽은 부품 | ✅ **확정 (a)** — 제거, 억제는 Effect 핸들의 `canExecute` | | `H-151` | 게이트 우회 계약 | ✅ **확정 — (a) 문서화 + `Refresh` 캐치업 폐기**: Effect의 `_epochs`는 emit 수신 때만 갱신, 재바인드/재구독은 초기 설치와 같다 | @@ -20,7 +20,7 @@ | `H-163` | Slot 내부 Observer × 홀드 발화 | ✅ **확정 (a′)** `_listObserver`는 트리 확정 뒤(`materializeSlotTree` 꼬리)에 끄고 묶고, 홀드가 있었으면 reconcile 1회; `_baseObserver`는 끄고 묶음 + **`EmitReceive` 인터페이스**(전파 루프 계층 분리) | | `H-164` | 홀드 발화의 `emitFrom == nil` | ✅ **전제 정정 (c)** — `nil` = 출처 없음(설치 또는 캐치업), `from` 보관 기각 | | `H-160` | `Destroying` 경로 cleanup `Rerun` | ✅ **확정 (a) → `H-159`로 정정**: `rawRerun`이 `_cleanupRunning`이면 **버리지 않고 `_rerunRequired`로 홀드** + "error 나면 그 Effect는 죽는다" 계약 | -| `H-161` | M5 루트 부착·다중 스크립트 `Claim` | ✅ **확정 (a)** `Claim`을 M5 스코프로; §5-7 다중 스크립트는 미결 | +| `H-161` | M5 루트 부착·다중 스크립트 `Claim` | ✅ **확정 (a)** `Claim`을 M5 스코프로; §5-7 다중 스크립트는 미결 → **같은 날 후속 3에서 확정**(`Claim` 1회·전체 소유 + 루트의 `.Parent =`는 밖에서 허용, `base/claim-plan.md` §7-7) | | `H-153` | Store 예약 이름 런타임 가드 | ✅ **확정 (a)** — 생성자·`Of(name)`에 예약 이름 검사(level 2), 그림자 = store 자신 (I) | | `H-154` | `InstanceChildHandler` dedup | ✅ **확정 (a)** — retractor 첫 줄 `if nextValue == v then return end` | | `H-152`/`H-155`~`H-157` | 갈래 없음 | ✅ 반영(`gate-plan.md` 조립 첫 줄 `StateBrand:register` / `ROADMAP.md` M6×3·M11 / `debounce-throttle-plan.md` 7절 `H-32` 문단 / `store-plan.md` 빈 Store 실측 완료) | @@ -92,7 +92,7 @@ 거기에 GUI 를 여럿 바운딩 해야해서 `Slot { Shop{} … }` 하는게 안 될것 같은 느낌이 듦. 이건 Parent 이상의 문제인것 같아."* → 이미 있는 트리를 quad가 소유하는 `Claim(inst, D.Mapper. "Name" {…})` 제안(원문·확정·갈래 전량은 -`research/existing-mount-plan.md`). +`base/claim-plan.md`). **확정된 것**: 방향 자체 / 디스크립터는 `D.Mapper`에(`D.Frame`에 직접 얹지 않음) / `Claim` DFS(내려가며 해석 → 자식부터 `drive`)는 derive 위의 한 겹 / 부기 대상 @@ -100,8 +100,11 @@ 매핑하거나 / 이름 중복·부재는 UB + debug 모드 `seen` 검사 / `nativeFindChild` 프로바이더 op, 순회는 quad-base / 다중 quad 한 트리 UB / M5 스코프(`H-161`로 당김). **따름**: 전용 문구 **철회**(일반 매치 실패 그대로), `H-146` (a)의 "루트는 밖에서 -`.Parent =`" **폐기**(루트가 quad 소유). `H-142` 키 금지는 그대로. -**미결**(개수는 그 research 문서 §5가 소스)은 다음 배치 문항. +`.Parent =`" **폐기**(루트가 quad 소유) — **[당시 결정, 같은 날 후속 3에서 좁혀 +복원됨]** 루트의 `Parent`는 부기 밖이라 밖에서 대입 허용(`base/claim-plan.md` §5). +`H-142` 키 금지는 그대로. +**미결**이던 갈래 여덟은 같은 날 전량 확정(이 파일 마지막 절, 결정 기록은 +`base/claim-plan.md` §7). ## `H-149` — Observer `Subscribe`/`Unsubscribe`도 인라인 (a) @@ -221,7 +224,7 @@ then return end`(`SlotHandler` 동형). 같은 값 재발행에 `Parent = nil `-round9-followup.md`/`-round9.md`의 `H-143`/`H-144`/`H-146` 소멸·정정 배너. **남은 미결**: `H-158`(`state:Block` 슈가 폐기 → `state:Apply(blocker)`, 권고만) — -`question.md`에; `research/existing-mount-plan.md` §5 갈래 — 다음 배치. (이후 `H-158`은 확정, 아래 절.) +`question.md`에; `base/claim-plan.md` §5 갈래 — 다음 배치. (이후 `H-158`은 확정, 아래 절.) ## 감사 루프 (2026-08-28, 10라운드 반영분) @@ -281,11 +284,11 @@ cleanupRunning 플래그가 풀리지 않는 문제가 날듯. 모든 재 진입 계약으로 상향되어도 문제는 없는듯. 이미 _running 도 그러한 제약을 받으니까."* — `effect-plan.md` "error 시 UB" bullet을 **"그 Effect는 죽는다"** 계약으로. -## `H-161` — `Claim`을 M5 스코프로 (a); §5-7은 미결 +## `H-161` — `Claim`을 M5 스코프로 (a); §5-7은 미결 → 같은 날 후속 3에서 확정 **사용자 확정**: *"H-161 는 확인. M5 스코프로 올라가도 될것으로 보임."* — `ROADMAP.md` -M5 체크박스·`research/existing-mount-plan.md` 헤더·§5-6. 다중 스크립트/루트 컨테이너 -(§5-7)는 답이 없어 미결 유지. +M5 체크박스·`base/claim-plan.md` 헤더·§5-6. 다중 스크립트/루트 컨테이너 +(§5-7)는 답이 없어 미결 유지 — **[같은 날 후속 3에서 확정]** `Claim` 1회·전체 소유 + 루트의 `.Parent =`는 밖에서 허용(`base/claim-plan.md` §7-7, 아래 마지막 절). ## `H-159` — `_rerunRequired` 홀드 플래그 (사용자 제안, 확정) — `_installed` 흡수, `H-160` 정정 @@ -408,3 +411,14 @@ observer 측에서 해당 emit 을 처리하는 함수를 만들어주는게 맞 짝. Slot 꼬리는 bind 자체가 확정 뒤라 `bindLifetime` 안의 `_catchUp`이 reconcile 1회를 맡는다 — 별도 끄기/호출 제거. 반영: `source-state-plan.md`(정의), `lifecycle-pattern.md` 셋 + 필드 목록, `slot-plan.md` 꼬리. + +## [2026-08-28 후속 3] `Claim` 갈래 전량 확정 — `research/`에서 `base/claim-plan.md`로 승격 + +위 `H-148`/`H-161`이 남겨둔 research 문서 §5의 갈래 여덟(루트 키 / 물리 순서 / +claim된 부모 안 `New` / debug 검사 범위 / 표면 이름 / 마일스톤 / 여러 스크립트 / +`Parent` 재대입)이 같은 날 사용자와 대화형으로 전부 닫혔다. **결정과 사용자 원문은 +`base/claim-plan.md` §7이 소스**, 대화 원문은 `session/2026-08-28-02-claim-promotion.md`. +이 파일 위쪽의 *"`base/claim-plan.md` §5 갈래 — 다음 배치"*류 서술은 당시 기록이다 +(경로만 승격 뒤 이름으로 치환됨). 눈에 띄는 것 하나 — **`H-146`의 "루트는 밖에서 +`.Parent =`"는 `H-148`에서 폐기됐다가 여기서 좁혀 복원됐다**(루트의 `Parent`는 만든 +방법과 무관하게 부기 밖). `Claim`은 여전히 1회·전체 소유. diff --git a/.claude/qa-request/pre-implementation-handtrace-round10.md b/.claude/qa-request/pre-implementation-handtrace-round10.md index 6923244..b9da40a 100644 --- a/.claude/qa-request/pre-implementation-handtrace-round10.md +++ b/.claude/qa-request/pre-implementation-handtrace-round10.md @@ -8,7 +8,7 @@ > 새로 만들고 `base/`에 반영한다 — **이 파일은 발견 당시의 기록**이라 각 항목의 > "갈래"는 선택 전 목록이니 반영 뒤엔 그대로 믿지 말 것. > -> 상태: **[2026-08-28] 탐사 완료(발견 `H-150`~`H-157`, 🔴 0 · 🟡 5 · 🟢 3, §4 문항 7) → 같은 날 사용자와 대화형으로 전량 결정·반영 — 결정의 소스는 `-round10-followup.md`.** 후속 `H-158`~`H-162`도 같은 날 확정(`H-159`는 `/code-review` 권고 (a) `Refresh` 복원이 아니라 사용자 제안 **`_rerunRequired` 홀드**로). 미결은 `research/existing-mount-plan.md` §5의 갈래들뿐(개수는 거기가 소스). `H-147`~`H-149`는 +> 상태: **[2026-08-28] 탐사 완료(발견 `H-150`~`H-157`, 🔴 0 · 🟡 5 · 🟢 3, §4 문항 7) → 같은 날 사용자와 대화형으로 전량 결정·반영 — 결정의 소스는 `-round10-followup.md`.** 후속 `H-158`~`H-162`도 같은 날 확정(`H-159`는 `/code-review` 권고 (a) `Refresh` 복원이 아니라 사용자 제안 **`_rerunRequired` 홀드**로). 남았던 `Claim` 갈래 여덟도 같은 날 전량 확정돼 `research/`에서 `base/claim-plan.md`로 승격(결정 기록은 그 §7). `H-147`~`H-149`는 > `H-143`~`H-146` 반영분에 `/code-review high`가 낸 10건 중 새 메커니즘·기존 > 결정 변경이라 문항으로 올린 셋(나머지 일곱은 반영 — `-round9-followup.md`의 > 마지막 code-review 절). @@ -349,7 +349,7 @@ spurious 재발행은 `Source:Set`이 같은 값도 emit한다는 확정(`t18` | **`H-154`** | `InstanceChildHandler` spurious dedup | (a) retractor `if nextValue == v then return end`(`SlotHandler` 동형) / (b) 그대로(정책 문서화) | **(a)** — `Slot`/`Ref`/Leaf가 이미 채택한 정책 | | **`H-159`** | **[2026-08-28 `/code-review`, 반영 뒤]** `H-151`이 잃은 캐치업 — 바인드 **전**에 온 emit(특히 `Ref`)은 다시 안 온다 | (a) `_bindDestroying`/`resubscribeTail`에 **"묶이는 시점 1회 `Refresh`"**만 되살림(emit 경로의 `_epochs` 갱신은 `H-151`대로 `Update`만) / (b) `Ref` dep만 바인드 시 `.Revision` 대조 / (c) 계약으로 두고 사용자에게 "`Ref`를 dep으로 쓰는 Effect는 그 leaf 뒤에 두라" 문서화 | **(a)** — `H-151`의 근거("다음 emit이 잡는다")가 `Ref`엔 성립하지 않는다; (a)는 `H-151`을 되돌리는 게 아니라 "emit 경로만 미룬다"는 계약과 양립(바인드는 emit 경로가 아님) | | **`H-160`** | leaf `Destroying` 콜백이 도는 cleanup 안의 `self:Rerun()`/`dep:Set()` — `canExecute`가 아직 참이라 죽는 inst에서 `fn`이 돌고 새 cleanup이 영구 고아 | (a) `rawRerun` 진입에서 `_cleanupRunning`이면 **버린다**(no-op) — "cleanup은 자기 생명주기를 못 바꾼다"의 `Rerun`판 / (b) `Destroying` 콜백이 `_consumeCleanup` **전에** `.Subscribed`류 표식으로 죽음을 먼저 세움(새 상태) / (c) UB 문서화 | **(a)** — 새 상태 없이 기존 플래그 하나로, `Unsubscribe` 경로와 같은 결과 | -| **`H-161`** | `H-148` 이후 **M5에 승인된 루트 부착 경로가 없다** + 여러 스크립트가 같은 `PlayerGui`를 `Claim`하면 이중 claim error / 다중 quad UB라 `Claim`이 자기 동기 사례를 막는다 | (a) `Claim`을 **M5 스코프**로 당기고(프로바이더 마일스톤이라 자연스러움) `research/existing-mount-plan.md` §5-7·8 갈래를 같이 정한다 / (b) `Claim` 전까지 임시로 `H-146` 루트 예외(밖에서 `.Parent =`)를 M5 한정으로 되살림 / (c) 루트 컨테이너(부기 대상 아님)는 claim 없이 자식만 붙이는 얇은 표면 신설 | **(a)** — 임시 예외는 하루 만에 뒤집힌 것을 되살리는 것이고, (c)는 `Mount` 기각의 재개방. §5-7(다중 스크립트)은 `Claim`의 "전부 매핑" 계약이 **루트 컨테이너에는 안 맞는다**는 신호라 갈래를 그 문서에 적었다 | +| **`H-161`** | `H-148` 이후 **M5에 승인된 루트 부착 경로가 없다** + 여러 스크립트가 같은 `PlayerGui`를 `Claim`하면 이중 claim error / 다중 quad UB라 `Claim`이 자기 동기 사례를 막는다 | (a) `Claim`을 **M5 스코프**로 당기고(프로바이더 마일스톤이라 자연스러움) `base/claim-plan.md` §5-7·8 갈래(승격 뒤 번호는 §7-7·§7-8)를 같이 정한다 / (b) `Claim` 전까지 임시로 `H-146` 루트 예외(밖에서 `.Parent =`)를 M5 한정으로 되살림 / (c) 루트 컨테이너(부기 대상 아님)는 claim 없이 자식만 붙이는 얇은 표면 신설 | **(a)** — 임시 예외는 하루 만에 뒤집힌 것을 되살리는 것이고, (c)는 `Mount` 기각의 재개방. §5-7(다중 스크립트)은 `Claim`의 "전부 매핑" 계약이 **루트 컨테이너에는 안 맞는다**는 신호라 갈래를 그 문서에 적었다 | | **`H-163`** ✅ (a) → **(a′)** | **[2026-08-28 `/code-review`, `H-159` 반영 뒤]** Slot 내부 Observer(`_listObserver`·`_baseObserver`)에도 홀드 발화가 걸려 재마운트의 `bindLifetime`이 `materializeSlotTree` **도중** `reconcile`을 동기 실행 → 자리 이중 등록, 중첩 Slot이면 `canBound` error | (a) Slot이 자기 내부 Observer를 다시 묶기 전에 `_rerunRequired`를 **지운다**(재마운트 캐치업은 `activateList`가 이미 명시적으로 한다 — 이중) / (b) 홀드 발화를 사용자 Observer에만(내부 Observer는 브랜드로 구분 — 새 구분) / (c) 홀드 발화를 `bindLifetime` 안이 아니라 `materializeSlotTree` 끝(`blocker:OffWithoutEmit()` 뒤)으로 미룸 | **(a) → (a′)** — (a)의 전제("재마운트 캐치업은 `activateList`가 이미 한다")는 감사 2라운드가 반증(그 분기는 앵커만 옮긴다) → 트리 확정 뒤 끄고 묶고 홀드가 있었으면 reconcile 1회. 소스는 `-round10-followup.md` | | **`H-164`** ✅ (c) — 문항 전제 정정 | Observer 홀드 발화가 `emitFrom = nil`로 오면 계약("`nil` = 설치 발화")과 구분 불가 — `if emitFrom == nil then initOnly()`로 짠 소비자가 변경을 놓침 | (a) 홀드 시 **마지막 `from`을 보관**(`_rerunRequired = from`, 진리값으로 플래그 겸용)해 그것을 넘김 / (b) 전용 센티널(`HeldEmit`) / (c) 계약 문구만 "`nil` = 설치 **또는** 묶일 때 캐치업" | **(c) — 문항 전제 정정**: 홀드 발화는 출처 있는 통지가 아니라 "묶였으니 값을 읽어라"라 설치 발화와 같은 종류 — `nil` = 출처 없음(설치 또는 캐치업). (a)의 `from` 보관은 사용자 기각(여러 홀드가 오면 앞 것이 날아감, 보관할 이유 없음). 소스는 `-round10-followup.md` | @@ -390,10 +390,10 @@ emit이 오는 dep**에만 성립한다. 갈래·권고는 §4 표. `H-148`이 `H-146`의 루트 예외를 폐기하고 `Claim`은 "M5 이후" 백로그라, M5(프로바이더· `D`·`InstanceChildHandler`)가 끝나도 quad가 만든 트리를 `PlayerGui`에 붙이는 승인된 -경로가 코퍼스에 없다. 그리고 `research/existing-mount-plan.md`의 *이중 claim은 error / +경로가 코퍼스에 없다. 그리고 `base/claim-plan.md`의 *이중 claim은 error / 여러 quad가 한 트리를 claim은 UB / 부기 대상 자식은 전부 매핑* 계약을 그대로 두면 `Shop.client.luau`와 `Inventory.client.luau`가 각각 `Claim(PlayerGui, …)`하는 **가장 -흔한 사례가 error**다 — 그 문서 §5엔 이 문항이 없었다(추가: §5-7·§5-8). 갈래·권고는 +흔한 사례가 error**다 — 그 문서 §5엔 이 문항이 없었다(추가: §5-7·§5-8 — 승격 뒤 `base/claim-plan.md` §7-7·§7-8). 갈래·권고는 §4 표. ## §5 이상 없다고 확인한 것 diff --git a/.claude/qa-request/pre-implementation-handtrace-round9-followup.md b/.claude/qa-request/pre-implementation-handtrace-round9-followup.md index 4c40d5d..5f76e0e 100644 --- a/.claude/qa-request/pre-implementation-handtrace-round9-followup.md +++ b/.claude/qa-request/pre-implementation-handtrace-round9-followup.md @@ -31,7 +31,7 @@ | — | `H-139` `New`/`D` 파이프라인 | ✅ **의사코드 신설** — 쓰면서 `H-142` 발견 | | — | `H-132`/`H-137`/`H-140` | ✅ Q1~Q3 처리 때 닫힘 | | — | `H-142` 해시 파트 `Parent` 순서 | ✅ **확정·반영** — props에 `Parent` 금지(순서 문제 소멸) | -| — | `H-143`~`H-146` (`/code-review high` 발견 중 새 메커니즘 넷) | ✅ 2026-08-27 확정·반영 → **⚠️ [2026-08-28] 10라운드가 셋을 뒤집음**(`-round10-followup.md`가 소스): `H-143` 소멸(`fn` 안 자기 해제 지원 폐기, `H-147`) / `H-144` 꼬리는 유지하되 `Refresh` 먼저는 폐기(`H-151`), 진입점은 `EffectHandle` 자기 것 (b) / `H-145` weak-key **유지** / `H-146` 루트 예외·전용 문구 폐기(`H-148` → `Claim`) | +| — | `H-143`~`H-146` (`/code-review high` 발견 중 새 메커니즘 넷) | ✅ 2026-08-27 확정·반영 → **⚠️ [2026-08-28] 10라운드가 셋을 뒤집음**(`-round10-followup.md`가 소스): `H-143` 소멸(`fn` 안 자기 해제 지원 폐기, `H-147`) / `H-144` 꼬리는 유지하되 `Refresh` 먼저는 폐기(`H-151`), 진입점은 `EffectHandle` 자기 것 (b) / `H-145` weak-key **유지** / `H-146` 루트 예외·전용 문구 폐기(`H-148` → `Claim`; **루트 예외는 같은 날 후속 3에서 좁혀 복원** — `base/claim-plan.md` §5) | **[2026-08-27] Q1~Q3는 `base/`·`ROADMAP.md`에 반영했다** — 반영 중 드러난 것과 열어둔 확인은 아래 "반영 기록" 절. **같은 날 이어서 Q4~Q8·Q10·`H-138`·`H-139`를 @@ -742,11 +742,11 @@ epoch 를 전부 잘 설정해주는게 나을수도 있어. 왜냐하면 처음 5번째 인자 근거 정리(weak가 되며 "옛 키 잔존" 근거는 사라지고 "지속 클로저 없음"만 남음), `ROADMAP.md` M3 `getBookkeeping` 체크박스. -### `H-146` — ⛔ [2026-08-28 폐기, 10라운드 `H-148`] 루트는 밖에서 `.Parent =`가 아니라 quad가 `Claim`으로 소유 — 아래는 하루 살았던 결정 +### `H-146` — ⚠️ [2026-08-28 10라운드 `H-148`에서 폐기됐다가 같은 날 후속 3에서 좁혀 복원] 루트의 `.Parent =`는 밖에서 허용, 이미 있는 트리 소유는 별개 표면 `Claim`(`base/claim-plan.md` §5) — 아래 원 서술은 다시 유효(전용 문구만 철회) -**`research/existing-mount-plan.md`와 `-round10-followup.md` `H-148`이 소스.** 전용 문구도 철회. +**`base/claim-plan.md`와 `-round10-followup.md` `H-148`이 소스.** 전용 문구도 철회. -#### (폐기) 루트는 금지 범위 밖, `Mount` 표면 없음, 거부는 전용 문구 (a) +#### (루트 예외·`Mount` 없음은 복원돼 유효, 전용 문구만 폐기) 루트는 금지 범위 밖, `Mount` 표면 없음, 거부는 전용 문구 (a) - 인용문 *"외부에서 직접 Parent 설정해주지 말것"*의 범위를 **quad가 관리하는 자식 자리**로 명문화. 루트 부착은 엔진마다 다른 최종 사용자 코드. (b) diff --git a/.claude/qa-request/pre-implementation-handtrace-round9.md b/.claude/qa-request/pre-implementation-handtrace-round9.md index 9799144..2e8e278 100644 --- a/.claude/qa-request/pre-implementation-handtrace-round9.md +++ b/.claude/qa-request/pre-implementation-handtrace-round9.md @@ -60,7 +60,7 @@ high` 7패스의 수정)를 처음부터 다시 트레이싱한 결과. 발견 | `H-143` | 🟡 | **[2026-08-27 `/code-review high`]** `fn` 안에서 `self:Unsubscribe()`를 부르면(문서가 허용하는 자리) `Rerun`이 `fn`의 반환 cleanup을 그대로 `_cleanup`에 저장하고 `_installed = true`로 되돌려 **아무도 소진 못 하는 cleanup**이 남는다 — "마지막 cleanup 정확히 1회" 계약 위반 | `effect-plan.md` `Rerun` | 사냥 밖 — 리뷰 발견, 처방은 새 메커니즘. **[2026-08-27] 확정 (a) → [2026-08-28] 소멸**(10라운드 `H-147`: `fn` 안 자기 해제 자체가 폐기) | — | | `H-144` | 🟡 | **[2026-08-27 `/code-review high`]** `EffectHandle.Subscribe = Observer.Subscribe` 배정이라 `Unsubscribe`로 cleanup을 소진한 핸들을 다시 `Subscribe`하면 `.Subscribed = true`만 서고 **재설치(`Rerun`)가 없다** — leaf 재바인드 경로(`_bindDestroying`의 `not _installed → Rerun`, `H-65`)와 비대칭 | `effect-plan.md` | 리뷰 발견, 처방은 새 메커니즘. **[2026-08-27] 확정 (a) 두 구독 진입점 모두 꼬리; 진입점은 (b) `EffectHandle` 자기 것 — [2026-08-28] `Refresh` 먼저는 폐기(10라운드 `H-151`)** | — | | `H-145` | 🟡 | **[2026-08-27 `/code-review high`]** 최상위 `SlotHandler` retractor가 `setLength(inst, k, 0)`으로 해제할 때 `bk(inst).indexOfElement[slot]`이 **안 지워진다**(해제 호출엔 요소가 없고 `setLength`는 `element ~= nil`일 때만 쓴다) — 교체마다 옛 Slot이 부모 `bk`에 강참조로 쌓여 부모가 죽을 때까지 산다(`InstanceChildHandler` 쪽은 5번째 인자를 안 넘기는 것으로 닫았지만 Slot은 `Length`가 State라 요소가 필요하다) | `dispatch-core-plan.md` `setLength` × `slot-plan.md` 494 | 리뷰 발견, 처방은 새 메커니즘. **[2026-08-27] 확정 (a) weak-key** | — | -| `H-146` | 🟡 | **[2026-08-27 `/code-review high`]** `H-142`(props에 `Parent` 금지, 대입은 자식을 받는 쪽만)를 닫고 나니 **루트**(`ScreenGui` → `PlayerGui`)를 붙이는 승인된 경로가 코퍼스 어디에도 없다 — 유일한 선례는 v1 `Mount(ScreenGui, …)`, `slot-plan.md` 233행의 *"v2는 mount 함수 자체가"*의 그 함수는 어느 표면에도 없다. 부수로 거부 배선의 에러가 일반 매치 실패 메시지(*"provider가 초기화됐는지 확인"*)라 오해를 부른다 | `bind-system-plan.md` `H-142` | 리뷰 발견, 처방은 새 표면. **[2026-08-27] 확정 (a) 루트 예외 + 전용 문구 → [2026-08-28] 둘 다 폐기**(10라운드 `H-148`: 루트는 `Claim`으로 quad 소유, `research/existing-mount-plan.md`) | — | +| `H-146` | 🟡 | **[2026-08-27 `/code-review high`]** `H-142`(props에 `Parent` 금지, 대입은 자식을 받는 쪽만)를 닫고 나니 **루트**(`ScreenGui` → `PlayerGui`)를 붙이는 승인된 경로가 코퍼스 어디에도 없다 — 유일한 선례는 v1 `Mount(ScreenGui, …)`, `slot-plan.md` 233행의 *"v2는 mount 함수 자체가"*의 그 함수는 어느 표면에도 없다. 부수로 거부 배선의 에러가 일반 매치 실패 메시지(*"provider가 초기화됐는지 확인"*)라 오해를 부른다 | `bind-system-plan.md` `H-142` | 리뷰 발견, 처방은 새 표면. **[2026-08-27] 확정 (a) 루트 예외 + 전용 문구 → [2026-08-28] 둘 다 폐기** → **[2026-08-28 후속 3] 루트 예외는 같은 날 좁혀 복원**(루트의 `.Parent =`는 밖에서 허용, `base/claim-plan.md` §5; 전용 문구 철회만 유지; 이미 있는 트리 소유는 별개 표면 `Claim`, `base/claim-plan.md`) | — | --- diff --git a/.claude/question.md b/.claude/question.md index 16a63c3..f718f9b 100644 --- a/.claude/question.md +++ b/.claude/question.md @@ -12,18 +12,15 @@ --- -## ⭐ 최우선 — `Claim` 갈래 (2026-08-28 갱신) +## ⭐ 최우선 — 비어 있음 (2026-08-28 갱신) **[2026-08-28] 10라운드 §4 문항 7건과 그 반영분의 후속(`H-158`~`H-164` — `EmitReceive`· `Observer:_catchUp` 포함)까지 사용자와 대화형으로 전량 결정·반영됐습니다**(소스는 -`qa-request/pre-implementation-handtrace-round10-followup.md`). 남은 건 하나 — -**M2 게이트 아님**: -- **`research/existing-mount-plan.md` §5** — 루트/템플릿을 quad가 소유하는 - `Claim` + `D.Mapper`의 갈래들(루트 디스크립터 이름·물리 순서 계약·비루트 - 사용·debug 검사 범위·표면 이름 … — 개수는 그 문서 §5가 소스). 방향은 확정, M5 - 스코프(`H-161`). **특히 §5-7**(여러 스크립트/여러 quad가 같은 `PlayerGui`를 - 쓰는 경우 — "전부 매핑" 계약이 루트 컨테이너엔 안 맞는다는 신호, 권고 없음)이 - `Claim` 설계의 핵심 미결입니다. +`qa-request/pre-implementation-handtrace-round10-followup.md`). 같은 날 마지막으로 +남아 있던 **`Claim` 갈래 여덟도 전량 확정**돼 `research/`에서 `base/claim-plan.md`로 +승격됐습니다(결정 기록은 그 §7, 원문은 `session/2026-08-28-02-claim-promotion.md`, +해소 요지는 `archive/question-resolved.md`의 "`Claim` 갈래" 절). **M2 착수를 막는 +항목은 없습니다.** **[2026-08-27] 9라운드**(Q1~Q10·`H-138`·`H-139`·`H-142`·`H-143`~`H-146`)는 전량 처리·반영됐습니다 — 소스는 `-round9-followup.md`. @@ -77,6 +74,21 @@ > `base/dispatch-core-plan.md`(0-A/0-Z가 반영된 디스패치 코어 — 열네 번째 > 세션에 `bind-system-plan.md`에서 분리 신설). +## ⭐ M5 착수 전 — `Claim` 문항 넷 (2026-08-28 신설, M2 게이트 아님) + +**[2026-08-28 저녁, `/code-review high`] `Claim` 승격 뒤 M5 착수 전 문항 넷** — M2 +게이트 아님, 결정은 M5 착수 때까지면 됨. 소스는 `base/claim-plan.md` §10(권고 포함): +- **A.** claim한 inst의 gcconn/gchold (0) 셋업 자리 — 프로바이더 op(`nativeAdopt` 가칭) / + `Claim` 본체를 프로바이더로 / (0)을 quad-base로. 권고 op. +- **B.** 같은 `inst` 이중 claim 판정 레지스트리 — `elementOwner`는 못 씀(claim한 inst를 + Slot 요소로 쓰는 §6과 충돌). 권고 per-inst `InstData` 플래그. +- **C.** 엔진이 자식을 넣는 컨테이너(`PlayerGui`, `ResetOnSpawn`)와 own-all 계약 — + 권고 "UB로 명문화, 흔한 경로는 `.Parent =`". 매퍼 생성기 범위에 컨테이너 포함 여부도. +- **D.** `type Param`의 children 배열 파트 — 원소 타입 파라미터 vs 필드만 공유. + 권고 파라미터 + 스파이크. +- 부수(우선순위 낮음): `Claim` debug 검사 범위는 `research/debug-tooling-plan.md` + "열린 질문" 절로 이관돼 있음 — 디버깅 도구 설계 때. + ## 1. 용어 정리 — 아직 안 정해진 것만 (사용자 요청, 진행 중) 사용자 원 메모: "quad는 register라던가 좀 부정확하거나 느낌이 바로 와닿지 diff --git a/.claude/research/debug-tooling-plan.md b/.claude/research/debug-tooling-plan.md index ae98e95..226eea3 100644 --- a/.claude/research/debug-tooling-plan.md +++ b/.claude/research/debug-tooling-plan.md @@ -511,6 +511,17 @@ Tween mock 등 동적 동작 포함")와 목적이 다름: - Attribute 이름 네임스페이싱(`__quadSource`류)과 노출 정보 범위(스크립트 전체 경로를 노출해도 되는지, 파일명만 남길지 등 보안/정보노출 고려). +**`Claim` debug 검사의 범위 — [2026-08-28 `base/claim-plan.md` §7-4에서 이관]** + +- `Claim(inst, D.Mapper…)`은 이름 중복·부재를 UB로 두고 debug 모드에서만 `seen` + 맵으로 잡는다(사용자 제안). **어디까지 잡을지는 여기서 정한다** — 후보: 이름 + 중복 / 부재 / 클래스 불일치 / 미매핑 부기 대상 자식 / 디스크립터 순서와 물리 + 순서 불일치(web) / 같은 `inst` 이중 claim / 루트 센티널 `D.Mapper.Root`가 자식 + 자리에 온 것. 사용자 판단(2026-08-28): *"디버깅 도구 만들 때 고려해야할 점으로 + 옮겨져야해. 부분 부분 디버깅 가능성을 아직 다 논한게 없어서 지금 그림으로 보면 + 작은 그림을 먼저 그리는거라서, 미결상황으로, 위치 이동이 필요함"* — 즉 "debug + 모드가 무엇을 언제 검사하는가"의 큰 그림(이 문서) 안에서 같이 정할 것. + **백로그(채택 여부만 남음, 핵심 설계와 무관)** - 외부 변경 감지(핵심 설계 방향 4번)를 보조 신호로라도 실제로 켤지 — 타이밍 diff --git a/.claude/research/documentation-content-map.md b/.claude/research/documentation-content-map.md index d59ae3c..1c92cc2 100644 --- a/.claude/research/documentation-content-map.md +++ b/.claude/research/documentation-content-map.md @@ -176,6 +176,16 @@ v1 폐기 API/버그/구조 결함 전부 v2 설계를 정당화하는 내부 무시될 수도 있다. 지금 설계로 막을 일이 아니라 **문서화할 사실**이다 (버전 정책의 타입 레벨 축은 `type-version-check`가 이미 다룬다 — 이건 그 런타임 판별 판). 코퍼스 어디에도 언급이 없어서 여기 등록한다 +21. **[2026-08-28 신설] 이미 있는 트리 — `Claim`과 루트 `.Parent`** — + `base/claim-plan.md` §9의 문서화 대상: (1) claim-once·own-all 계약 — 부기 대상 + 자식은 전부 매핑, 디스크립터 순서가 정본, 숏핸드 `UI*`는 (i) 템플릿에 없고 + quad가 만들거나 (ii) 실제 객체로 매핑하거나 둘 중 하나(섞으면 UB), 이름 + 중복·부재는 UB(debug에서 error); (2) **루트의 `.Parent =`는 밖에서, 그 아래는 + 절대 직접 하지 말 것** — props에 `Parent`를 넣으면 일반 매치 실패 문구가 나오는 + 이유도 여기서 설명; (3) 여러 스크립트가 한 `PlayerGui`를 쓸 땐 각자 `ScreenGui`를 + 만들어 `.Parent = PlayerGui`, PlayerGui 직하 Slot을 공유해야 하면 `Claim`을 한 + 번 하고 Slot을 반환하는 중간 모듈 패턴(**단 PlayerGui 자체를 claim하는 것은 + `base/claim-plan.md` §10-C 미결** — 확정 전엔 쓰지 말 것) (13번이었던 "Fusion/Vide 경험자용 비교 섹션"은 2026-08-06 재분류로 아래 6번 `quadnomicon`으로 이동) diff --git a/.claude/research/existing-mount-plan.md b/.claude/research/existing-mount-plan.md deleted file mode 100644 index 044c0be..0000000 --- a/.claude/research/existing-mount-plan.md +++ /dev/null @@ -1,162 +0,0 @@ -# 이미 있는 트리를 quad가 소유하기 — `Claim` + `D.Mapper` (가칭) - -> **[2026-08-28 신설, 사용자 발의]** 10라운드 `H-148`(`Parent` 거부 문구)을 -> 논의하다 **더 큰 표면의 공백**이 드러나 만든 문서. 상태: **설계 논의 중 — -> 방향은 사용자 확정, 갈래 몇 개 미결(§5)**. M2 착수 게이트 아님, **M5 스코프** -> (**[2026-08-28 `H-161`]** "M5 이후"에서 당김 — 루트 예외 폐기 뒤 M5의 유일한 루트 -> 부착 경로; 프로바이더 op가 필요하니 M5가 자연스러운 자리). 결정이 나면 `base/`로 -> 승격한다. -> -> **`archive/existing-instance-bind-rejected.md`(2026-08-14 기각)와의 관계**: -> 그 기각은 *"이미 있는 Instance에 나중에 새 props를 다시 바인드"*였고 사유는 -> "quad가 만들지 않은 트리의 자식 구성을 바깥이 밀고 당기면 `setLength`/ -> `setOffsetSource` 부기가 깨진다"였다. 이 문서는 그 반대 방향 — **한 번 -> claim하면 quad가 소유하고 직계 자식은 사용자가 전부 매핑한다**는 계약이라 -> claim 뒤엔 quad가 만든 트리와 같은 불변식이 성립한다. **재바인드는 여전히 -> 미지원**(claim은 1회, 디스크립터는 `Processed`로 소진). 그 archive엔 "좁은 -> 형태로 부활"이라는 배너를 달 것(반영 시). - -## 1. 왜 필요한가 (사용자 원문) - -`H-146`이 "루트는 사용자가 밖에서 `.Parent =`"로 닫혔는데, 사용자가 이어서 -지적했다: *"slot 은 물리 장치에 mount 할 방법이 거의 존재하지 않음. Parent = -처럼 마운트 할 방법이 없는데? 그럼 PlayerGui 가 상위에 있고 거기에 GUI 를 -여럿 바운딩 해야해서 `Slot { Shop{} … }` 하는게 안 될것 같은 느낌이 듦. 이건 -Parent 이상의 문제인것 같아."* — 즉 **루트가 Slot일 수 없다**(Slot은 quad가 -부기를 가진 부모 `inst` 아래에만 산다). 그리고: *"생성할 요소들 자체가 너무 -많은 경우 Clone 이 엇청 더 싸서, 그 Clone 된 것 아래 quad 를 바인딩 할 방법이 -있으면 좋은것도 사실인듯. … web 에서도 템플릿에 의해 유효한 요소일꺼고, -roblox 에서도 보면서 만들어낸 GUI를 바인딩하는건 흔한 요구라서 이 역시 흔한 -필요일꺼야."* - -결론(사용자): *"이런 방식으로, 이미 있는 PlayerGui 아래 마운트를 거는거지. -… 이것도 똑같이 Quad 가 소유하게 될 요소가 되는거지. … 이렇게 끝내면 parent -를 설정할 문제 자체가 사라져"* — **`H-146`의 루트 예외와 `H-148`의 문구 문제가 -같이 소멸**한다. - -## 2. 모양 (사용자 스케치 + 확정된 것) - -```lua -local M = D.Mapper -- 정의는 D 안에 산다. 유저가 필요하면 꺼낸다 -local cloned = Claim(template:Clone(), M.Frame "root" { -- ← 루트 이름은 미결(§5-1) - M.TextLabel "Title" { Text = title }, - M.Frame "List" { - Slot { … }, -- 기존 부모 아래 Slot — 이제 가능 - }, - BackgroundColor3 = color, -- props는 New와 같은 derive 테이블 -}) -- -> cloned (claim한 루트 Instance) -``` - -- **`D.Mapper. "Name" { … }`는 Instance를 만들지 않고 디스크립터만 - 만든다**(브랜드 `Mapper`류) — derive 테이블 + 매칭 키. `D.Frame`에 직접 얹지 - 않는다(사용자: *"D.Frame 에 바로 바인딩은 위험한듯. 의미가 겹쳐버려"*). -- **`Claim(inst, descriptor) -> inst`가 최상위**. `inst`는 quad 밖에서 온 것 - (PlayerGui, `Clone()` 결과, Studio에서 만든 GUI). 이 호출로 `inst`와 매핑된 - 하위 전부가 **quad 소유**가 된다 — `New`가 만든 것과 같은 gcconn/gchold·부기. -- **처리 순서는 `New`와 반대 방향에서 시작한다.** `New`는 안쪽 생성자가 먼저 - 평가돼 자연히 bottom-up이지만, 매퍼의 안쪽은 평가 시점에 자기 Instance를 - 모른다(부모가 아직 없다). 그래서 `Claim`이 **DFS로 내려가며 이름으로 해석 → - 자식부터 `drive` → 올라오며 부모 `drive`**. 사용자: *"핸들러가 된다면 위험해. - 일반 생성과 다르게, 상위 부터 처리하거든 … DFS 로써, 내려가는게 먼저고 그 - 뒤에서 derive 를 걸어야해. 이건 derive 에선 구현하지 않고, 그 위의 무언가로써 - 구현되어야할듯."* — `drive`/핸들러 층은 안 바뀌고 그 **위의 한 겹**이다. -- **매핑된 정적 자식은 `InstanceChildHandler`와 같은 부기(`setLength(inst,k,1)`)만 - 하고 `.Parent =`는 안 한다**(이미 거기 있다). Slot은 평소처럼 `native*`로 - 기존 부모 아래 끼운다. -- **디스크립터는 1회용** — claim 뒤 `Processed`로 소진(`PreRef`와 같은 관용구). - 재사용·이중 claim은 error. -- **매칭은 프로바이더 주입 op** `nativeFindChild(inst, key)`(가칭) — Roblox는 - `Name`, web은 id/selector. **quad-base가 순회·부기 전반을 구현하고 - 프로바이더는 이 핸들만 낸다**(사용자: *"quad-base 에서 전반을 구현해주고 - 필요 핸들을 구현하라고 남기는건 괜찮은 생각"*). -- **여러 quad 인스턴스가 한 트리를 claim — UB**(사용자 확정). -- **`H-142`(props에 `Parent` 금지)는 그대로.** 루트가 quad 소유가 되므로 - `H-146`의 "루트는 밖에서 `.Parent =`" 예외는 **폐기**. - -## 3. 계약 — 자식은 전부 매핑한다 - -사용자: *"모든 개체를 유저가 직접 네임을 매핑해서 derive 테이블 안에서 내부 -요소를 전부 매핑해준다를 계약으로 잡으면 문제가 없다고 생각해."* - -- **부기 대상(그려지는 자식)은 전부 매핑해야 한다.** 안 된 자식이 남으면 - `nativeInsert`의 삽입 위치(web은 곧 DOM 순서)와 Length/Offset이 어긋난다. -- **숏핸드(`UICorner` 등 `UI*`)는 부기 대상이 아니다** — 그려지지 않고 Roblox에만 - 있으며 단순 `Parent` 대입 요소. 사용자 확정: *"숏핸드를 quad 에서만 직접 - 쓰거나 … 아니면 실제 UI 객체를 바인딩해서 숏핸드를 안 쓰거나"* — 둘 중 하나: - (i) 템플릿엔 `UI*`가 없고 quad가 숏핸드 키로 만든다, (ii) 템플릿의 `UI*`를 - `M.UICorner "UICorner" {…}`처럼 **실제 객체로 매핑**하고 그 부모에 숏핸드 - 키는 안 쓴다. 섞으면(템플릿에 `UICorner`가 있는데 숏핸드 키도 씀) 둘이 - 생기는 것은 UB. -- **이름 중복·부재는 UB**(사용자 확정). **debug 모드**에선 `seen` 맵으로 중복을 - 잡아 error(사용자 제안) — 부재·클래스 불일치도 같은 자리에서 검사. - -## 4. 이 문서가 여는 것 - -- **루트**: `Claim(PlayerGui, M.ScreenGui "…" { Slot {…} })` — PlayerGui가 - quad 소유 부모가 되어 Slot이 그 아래 산다. `ScreenGui`를 `New`로 만들어 - 붙이는 경우도 `Claim(PlayerGui, M.PlayerGui(...) { New "ScreenGui" {…} })`처럼 - 정적 자식으로 들어간다(→ §5-3 루트 이름 문제). -- **템플릿 대량 생성**: `template:Clone()` → `Claim` — 각 사본이 독립 소유. - Claim이 Instance를 돌려주므로 **Slot 요소로도 그대로 쓸 수 있다**(요소는 - `inst`) — "요소가 너무 많은 경우"의 답. -- **비루트 사용**: `New "Frame" { Claim(clone, …) }` — 반환된 `inst`가 정적 - 자식으로 들어가면 `InstanceChildHandler`가 `Parent =`와 부기를 한다. 평가 - 순서상 `Claim`이 먼저 끝나므로 bottom-up이 유지된다. - -## 5. 미결 — 다음 배치 문항 - -1. **루트 디스크립터의 이름** — 루트는 `Claim`이 `inst`를 직접 받으니 매칭 키가 - 필요 없다. 사용자: *"최상위는 이름을 뭐로 둬야할지 아직 모르겠음. 비워두는걸 - D.Mapper.Frame{} 으로 제공하는건 더 나빠보이는데. 아니면 테이블로써 - MapperRoot = {} Mapper.Frame (MapperRoot) {} 모양이 되어도 될것같음."* - 갈래: (a) 센티널 `MapperRoot`(`M.Frame(MapperRoot) {…}`) / (b) 루트는 - `Claim(inst, { … })`처럼 클래스 없는 맨 테이블(클래스는 `inst`가 이미 안다) - / (c) 이름을 받되 무시. **권고 (b)** — 루트 클래스를 두 번 말하지 않고, - `M.`는 "찾아야 하는 자식"에만 쓰여 뜻이 하나가 된다. 단 타입(`D` - 생성기가 만든 props 타입)을 잃으므로 `Claim<<"Frame">>(inst, {…})`처럼 - 타입 인자로 보완 — `New<>`와 같은 관용구. -2. **물리 순서 계약** — 디스크립터 배열 순서와 기존 트리의 실제 순서가 다를 때. - Roblox는 물리 순서가 의미 없어 무관, web은 DOM 순서라 `nativeInsert` 위치가 - 어긋난다. 갈래: (a) 디스크립터 순서가 정본이고 일치는 사용자 책임(UB, - debug 검사) / (b) claim 시 quad가 `nativeMove`로 실제 순서를 디스크립터에 - 맞춘다. **권고 (a)** — "이미 있는 걸 그대로"의 취지, Roblox에선 비용 0. -3. **`New`로 만든 자식을 claim된 부모에 넣는 것** — 위 §4 첫 항목. 정적 자식이니 - `InstanceChildHandler`가 `Parent =`를 하면 되고 새 결정은 없어 보이나, - "매핑(이미 있음)"과 "생성(새로 붙임)"이 한 배열에 섞이는 것이 계약상 - 괜찮은지 확인 문항. -4. **debug 검사의 범위** — 이름 중복 / 부재 / 클래스 불일치 / 미매핑 부기 - 대상 자식 / 같은 quad의 이중 claim 중 어디까지. 권고: 전부(debug에선 싸다). -5. **표면 이름** — `Claim`/`Mount`/`Adopt`, `D.Mapper`/`D.Existing`. - `Mount(root, parent)`를 `H-146`에서 기각한 사유("부기 없는 대상에 quad 객체 - 주입")는 여기 반대로 적용된다 — claim은 부기를 *세우는* 행위. 권고 `Claim`. -6. **마일스톤** — M5 이후(`nativeFindChild`가 프로바이더 표면). `ROADMAP.md` - 백로그에 포인터만. **[2026-08-28 `/code-review`, `H-161`]** 단 `H-146` 루트 - 예외를 폐기한 지금 **M5에 승인된 루트 부착 경로가 없다** — (a) `Claim`을 M5 - 스코프로 당김 / (b) `Claim` 전까지 M5 한정 임시 예외 / (c) 루트 컨테이너용 - 얇은 표면. **사용자 확정 (a)**(*"M5 스코프로 올라가도 될것으로 보임"*) — 헤더와 - `ROADMAP.md` M5 체크박스에 반영. §5-7(다중 스크립트)은 여전히 미결. -7. **[2026-08-28 `/code-review`, `H-161`] 여러 스크립트/여러 quad가 같은 루트 - 컨테이너를 쓰는 경우** — 위 "이중 claim error / 다중 quad UB / 부기 대상 자식 - 전부 매핑"을 그대로 두면 `Shop.client.luau`와 `Inventory.client.luau`가 각각 - `Claim(PlayerGui, …)`하는 **가장 흔한 사례가 막힌다**. 이건 "전부 매핑" 계약이 - **루트 컨테이너**(부기 대상이 아닌 `PlayerGui`·`CoreGui`류 — 자식 순서가 - 의미 없고 quad가 그 형제들을 관리하지 않는다)에는 안 맞는다는 신호다. 갈래: - (a) `Claim`은 **부기를 갖는 노드**에만, 루트 컨테이너엔 "quad가 만든 자식 - 하나를 붙이는" 별개 표면(부기 없음, 여러 스크립트 공존, 이름은 §5-5와 같이) / - (b) `Claim`에 "이 노드의 다른 자식은 관리하지 않는다"(부분 매핑) 모드 — 단 - web처럼 물리 순서가 의미 있는 엔진에선 위험 / (c) 다중 claim을 허용하되 각 - claim이 자기가 매핑한 자식만 소유(UB 대신 정의) — 부기 충돌 없음이 조건. - **권고 없음** — (a)는 `H-146`에서 기각한 `Mount(root, parent)`의 재개방과 - 경계가 얇고, (b)(c)는 계약을 약화시킨다. 사용자 판단. -8. **매핑된 정적 자식의 `Parent` 대입** — §2는 "부기만, `.Parent =`는 안 한다"인데 - `InstanceChildHandler`는 `v.Parent = inst`가 계약(`dispatch-core-plan.md` - `H-134`). 같은 핸들러를 쓰면 이미 거기 있는 자식에 같은 값을 재대입(엔진 - no-op)하는 것뿐이라 별도 핸들러가 필요 없어 보인다 — 확인 문항(권고: 같은 - 핸들러, 재대입 감수). - -## 6. 반영 시 고칠 자리 (승격 때 체크리스트) - -`archive/existing-instance-bind-rejected.md` 배너 / `base/bind-system-plan.md` -`H-142` 항목의 `H-146` 루트 bullet(폐기 → 이 문서 포인터) / `base/slot-plan.md` -"동적 자식은 반드시" 절 각주 / `ROADMAP.md` M5 `Property.luau` 전용 문구 삭제 + -백로그 항목 / `research/documentation-content-map.md` / `question.md`. diff --git a/.claude/session-summary.md b/.claude/session-summary.md index 38b6bdd..a3a1a5c 100644 --- a/.claude/session-summary.md +++ b/.claude/session-summary.md @@ -1983,7 +1983,7 @@ Q4(`EffectHandle` 네 진입점 의사코드 — Observer 것 재사용, `Unsubs 결정·반영. **뒤집힌 것 둘**: `fn`/cleanup은 자기 구독을 못 바꾼다(`H-147` (A) — 어제 `H-143`의 원샷 지원 소멸, `rawRerun(force)`/`Rerun` 분리, 네 진입점 `_running` 가드) / 루트는 밖에서 `.Parent =`가 아니라 **quad가 `Claim`으로 소유**(`H-148` → - `research/existing-mount-plan.md`, 2026-08-14 기각과 다른 claim-once·own-all). + `base/claim-plan.md`, 2026-08-14 기각과 다른 claim-once·own-all). 나머지: Observer 진입점 인라인(`H-149`) / `Effect._blocker` 제거(`H-150`) / `_epochs`는 emit 때만 갱신, `Refresh` 캐치업 폐기 + "게이트는 emit 경로만 미룬다" 계약(`H-151`) / `GateNode` 브랜드 등록(`H-152`) / Store 예약 이름 런타임 @@ -1995,3 +1995,19 @@ Q4(`EffectHandle` 네 진입점 의사코드 — Observer 것 재사용, `Unsubs (전파 루프는 `sub:_receive(from)`만, 사용자 지시) · Slot 재마운트 캐치업 (a′) · `emitFrom == nil` = 출처 없음 · `Observer:_catchUp()`. 소스는 `qa-request/pre-implementation-handtrace-round10-followup.md`. +- **`session/2026-08-28-02-claim-promotion.md`** — 10라운드가 남긴 `Claim` 갈래 여덟을 + 사용자가 한 메시지로 답하고(1~5) 에이전트가 §5-7을 "한 quad·여러 스크립트" 문제로 + 다시 세워 되물은 뒤 전량 확정 → `research/existing-mount-plan`을 **`base/claim-plan.md`로 + 승격**. 루트 키 센티널(맨 테이블 + `Claim<>`는 타입 자동완성 손실로 기각, `type + Param` 공유) / 디스크립터 순서 정본 / `New` 자식 허용(위치는 프로바이더 몫) / + debug 검사 범위는 `debug-tooling-plan.md`로 / 이름 `Claim`·`D.Mapper` / **루트의 + `.Parent =`는 밖에서 허용으로 복원**(`H-146` 예외가 `Claim`한 루트까지 넓혀 복원, + `Claim`은 1회·전체 소유, PlayerGui 직하 Slot 공유는 중간 모듈) / 매핑된 자식은 + `InstanceChildHandler` 그대로. `nativeFindChild` 주입 op 등록. `question.md` 최우선 절 비움. + 감사 6라운드(발견 2→3→1→3→2→0, 3~5라운드는 전부 낮의 "폐기" 배너가 저녁의 복원을 + 모르던 것) → `/code-review high` 10건: 서술·라벨·stale 여섯 반영, **새 메커니즘 넷**은 + `base/claim-plan.md` §10 + `question.md` 문항으로(gcconn/gchold 셋업 자리 / 이중 claim + 레지스트리 / `PlayerGui` own-all vs `ResetOnSpawn` / `Param` 배열 파트). 리뷰가 + 잡은 실질 정정: `Claim` 추론은 `Clone()`이 `Instance`를 돌려줘 성립 안 함 → + 반환은 inst 타입 그대로 / `Processed` 관용구는 플래그 절반만 / claim된 inst에선 + `PreRef`/`OnCreated` 불변식이 약해짐. diff --git a/.claude/session/2026-08-28-01-handtrace-round10-resolution.md b/.claude/session/2026-08-28-01-handtrace-round10-resolution.md index fc8340a..1022f8a 100644 --- a/.claude/session/2026-08-28-01-handtrace-round10-resolution.md +++ b/.claude/session/2026-08-28-01-handtrace-round10-resolution.md @@ -16,7 +16,7 @@ 없다"*고 한 것을 스스로 *"엄청난 모순이네"*로 뒤집은 자리. 2. `H-148`에서 사용자가 더 큰 공백을 짚음 — 루트가 Slot일 수 없다(`PlayerGui` 아래 `Slot { Shop{} }` 불가). 2026-08-14 기각(재바인드)과 다른 방향(claim-once· - own-all)임을 archive와 대조해 확인하고 `research/existing-mount-plan.md` 신설. + own-all)임을 archive와 대조해 확인하고 research 문서 신설(당시 `research/existing-mount-plan`, 같은 날 세션 02에서 `base/claim-plan.md`로 승격). `H-146`의 "루트는 밖에서 `.Parent =`" 예외는 하루 만에 폐기. 3. `H-149`~`H-154`는 권고대로. `H-151`에서 사용자가 *"우린 애초에 Refresh 를 할 필요가 없는거야"* — 어제 `H-144`에서 세운 "`Refresh` 먼저" 하위 결정이 소멸. diff --git a/.claude/session/2026-08-28-02-claim-promotion.md b/.claude/session/2026-08-28-02-claim-promotion.md new file mode 100644 index 0000000..1141043 --- /dev/null +++ b/.claude/session/2026-08-28-02-claim-promotion.md @@ -0,0 +1,135 @@ +# 2026-08-28 (02) — `Claim` 갈래 확정, `research/`에서 `base/claim-plan.md`로 승격 + +> 이 파일은 **원문 기록**이다(시행착오 포함). 지금 유효한 설계는 `base/claim-plan.md`. +> 앞 세션 `session/2026-08-28-01-handtrace-round10-resolution.md`가 만든 research +> 문서(옛 `existing-mount-plan`) §5 갈래 여덟을 사용자와 대화형으로 닫은 기록. + +## 경위 + +10라운드 후속 2까지 커밋(`f020f3f`)한 뒤 사용자가 *"`research/existing-mount-plan.md` +관해서 다뤄보자. 우선 5. 미결을 보자"*로 이어갔다. 사용자가 항목 1~5를 먼저 적어 +보냈고("내가 적어둔걸 볼래?"), 에이전트가 사실 셋을 확인한 뒤 의견과 함께 답이 +없던 §5-7·§5-8을 되물었다. 사용자가 둘을 답해 전량 확정. + +## 사용자 원문 (1차 메시지) + +> 1. 의 권고 b는 문제가 생겨. 루트에 대해서 {} 안의 타입체크와 타입 자동완성이 +> 전혀 안 먹음. 내 생각에는 D.Frame 의 함수의 부분들을 type FrameParam = {} +> 형태로 빼서 공유되는 타입 부분으로 D.Mapper.Frame 도 구성되고, 리턴부분만 +> 다르게 될것 같아. Claim 자체는 타입을 받는건 말이 안되어보임. New 와는 완전 +> 다른 계열이라서: New 는 후행 입력에 대한 타입을 선언시키는 D 계열이지만, +> Claim 은 공유 부분이고 말한것 처럼 엔진 요소를 알 수 없어. a 가 가장 정확한 +> 구현으로 보임. +> 2. 나는 처음에 A 를 생각했어. 권고 그대로 가줘. +> 3. 그런데 호출 순서가 달라진다는 문제가 있긴함. D.Mapper 의 프레임이 전부 +> 구워지는 도중에 생기고 각각 claim 매핑 되기 전에 New 가 호출됨. 새로 붙임 +> 자체는 한 배열에 섞이는게 문제는 없어보여. 그 경우에서도 순차 마운트 Add 는 +> 작동할것이거든. 대신 Add 가 위치를 진짜 실어서 보내지 않으면 맨 뒤에 +> 놓인다는게 문제일 뿐. +> 4. 는 디버깅 도구 만들 때 고려해야할 점으로 옮겨져야해. 부분 부분 디버깅 +> 가능성을 아직 다 논한게 없어서 지금 그림으로 보면 작은 그림을 먼저 +> 그리는거라서, 미결상황으로, 위치 이동이 필요함 +> 5. 표면 이름은 Claim 이 가장 마음에 들어. D.Mapper 가 이미 있는걸 매핑해서 +> 내가 가진다는 의미적으로 가장 맞고. +> 승격은 지금도 가능해보임. 간단한 레이어라서. + +## 에이전트가 확인한 사실과 되물은 것 + +- `D.Frame = New<> "Frame" :: (({ ...타입명시 }) -> Frame)` — props 타입이 + 캐스트 안 인라인이고 `D` 전체가 생성기 산출물(`base/bind-system-plan.md`) → + `type FrameParam` 공유는 생성기 한 줄. `InstanceChildHandler`는 `v.Parent = inst`이고 + `H-154` 문단이 이미 "같은 값, 엔진 no-op"을 전제 → §5-8 근거. `nativeInsert`는 + Roblox가 offset을 무시(`architecture.md`) → §5-3의 위치 문제는 web 프로바이더 요구사항. +- 제안 둘(사용자 거부 없음, 결정은 아님으로 표시): 센티널을 `D.Mapper.Root`에 + 두기 / `Claim(inst: T, desc: MapperDescriptor): T` **추론**(타입 인자 없음은 + 사용자 결정). +- **§5-7 문항을 다시 세움** — "여러 quad 인스턴스"가 아니라 **한 quad·여러 + 스크립트**(같은 `quad` 모듈 require)가 흔한 경우라 이중 claim error가 그 사례를 + 막는 게 진짜 문제. 갈래 (α) `Claim`은 전체 소유만, 루트 `.Parent =`는 밖에서 + (`H-146` 복원, 새 메커니즘 0) [권고] / (β) 다중 claim·부분 소유(web에서 offset이 + 남의 자식을 못 봄) / (γ) 별도 표면(`H-146` 인용문이 반대한 것). + +## 사용자 원문 (2차 메시지) — §5-7·§5-8 + +> 5-8 확인완료. 5-7 에서는 정확히는 두번 Claim 불가하다는 의미. 필요하다면 +> Slot 을 안에 만들고 리턴하는 중간 모듈을 만들어야함. 밖에서 .Parent 설정하는건 +> 괜찮아. 루트도 quad 소유이긴 한데, .Parent 를 밖에서 설정하는건 괜찮음. +> 정확히는 ScreenGUI 가 이미 존재해도 똑같음. + +읽기: (α) 채택. `Claim`은 같은 `inst`에 1회, 소유는 전체. 루트의 `Parent`는 +**만든 방법과 무관하게**(`New`든 `Claim`한 기존 `ScreenGui`든) 부기 밖이라 밖에서 +대입 허용 — `H-146`의 예외가 좁혀서(그리고 `Claim`한 루트까지 넓혀서) 복원됐다. +PlayerGui 직하 Slot을 여러 스크립트가 공유하려면 `Claim` 한 번 + Slot 반환 중간 모듈. + +## 반영 + +`git mv research/existing-mount-plan.md base/claim-plan.md` 후 전면 재작성(§7 결정 +기록, §8 안 만들기로 한 것, §9 구현 체크리스트·문서화 대상). 포인터 갱신: +`bind-system-plan.md` `H-146` 배너("폐기" → "좁혀서 복원") / `slot-plan.md` 두 곳 / +`architecture.md` 주입 op 목록에 `nativeFindChild`(조합 폴백 예외) / `ROADMAP.md` +M2 배너·M5 두 체크박스 / `question.md` 최우선 절 비움 + `archive/question-resolved.md` +절 / `debug-tooling-plan.md` "열린 질문"에 검사 범위 이관 / `documentation-content-map.md` +§4 21번 / `README.md` 행 이동 / `CLAUDE.md`·`todos.md`·`project-context.md` / +`-round10-followup.md` 후속 3 절. 코퍼스의 옛 경로는 새 경로로 일괄 치환(히스토리 +문서 포함 — 파일 참조가 깨지지 않게). + +## 옛 §5 원문 (승격 직전 커밋 `f020f3f`의 `research/existing-mount-plan.md` §5 — 갈래 a/b/c 선택지 전문) + + +1. **루트 디스크립터의 이름** — 루트는 `Claim`이 `inst`를 직접 받으니 매칭 키가 + 필요 없다. 사용자: *"최상위는 이름을 뭐로 둬야할지 아직 모르겠음. 비워두는걸 + D.Mapper.Frame{} 으로 제공하는건 더 나빠보이는데. 아니면 테이블로써 + MapperRoot = {} Mapper.Frame (MapperRoot) {} 모양이 되어도 될것같음."* + 갈래: (a) 센티널 `MapperRoot`(`M.Frame(MapperRoot) {…}`) / (b) 루트는 + `Claim(inst, { … })`처럼 클래스 없는 맨 테이블(클래스는 `inst`가 이미 안다) + / (c) 이름을 받되 무시. **권고 (b)** — 루트 클래스를 두 번 말하지 않고, + `M.`는 "찾아야 하는 자식"에만 쓰여 뜻이 하나가 된다. 단 타입(`D` + 생성기가 만든 props 타입)을 잃으므로 `Claim<<"Frame">>(inst, {…})`처럼 + 타입 인자로 보완 — `New<>`와 같은 관용구. +2. **물리 순서 계약** — 디스크립터 배열 순서와 기존 트리의 실제 순서가 다를 때. + Roblox는 물리 순서가 의미 없어 무관, web은 DOM 순서라 `nativeInsert` 위치가 + 어긋난다. 갈래: (a) 디스크립터 순서가 정본이고 일치는 사용자 책임(UB, + debug 검사) / (b) claim 시 quad가 `nativeMove`로 실제 순서를 디스크립터에 + 맞춘다. **권고 (a)** — "이미 있는 걸 그대로"의 취지, Roblox에선 비용 0. +3. **`New`로 만든 자식을 claim된 부모에 넣는 것** — 위 §4 첫 항목. 정적 자식이니 + `InstanceChildHandler`가 `Parent =`를 하면 되고 새 결정은 없어 보이나, + "매핑(이미 있음)"과 "생성(새로 붙임)"이 한 배열에 섞이는 것이 계약상 + 괜찮은지 확인 문항. +4. **debug 검사의 범위** — 이름 중복 / 부재 / 클래스 불일치 / 미매핑 부기 + 대상 자식 / 같은 quad의 이중 claim 중 어디까지. 권고: 전부(debug에선 싸다). +5. **표면 이름** — `Claim`/`Mount`/`Adopt`, `D.Mapper`/`D.Existing`. + `Mount(root, parent)`를 `H-146`에서 기각한 사유("부기 없는 대상에 quad 객체 + 주입")는 여기 반대로 적용된다 — claim은 부기를 *세우는* 행위. 권고 `Claim`. +6. **마일스톤** — M5 이후(`nativeFindChild`가 프로바이더 표면). `ROADMAP.md` + 백로그에 포인터만. **[2026-08-28 `/code-review`, `H-161`]** 단 `H-146` 루트 + 예외를 폐기한 지금 **M5에 승인된 루트 부착 경로가 없다** — (a) `Claim`을 M5 + 스코프로 당김 / (b) `Claim` 전까지 M5 한정 임시 예외 / (c) 루트 컨테이너용 + 얇은 표면. **사용자 확정 (a)**(*"M5 스코프로 올라가도 될것으로 보임"*) — 헤더와 + `ROADMAP.md` M5 체크박스에 반영. §5-7(다중 스크립트)은 여전히 미결. +7. **[2026-08-28 `/code-review`, `H-161`] 여러 스크립트/여러 quad가 같은 루트 + 컨테이너를 쓰는 경우** — 위 "이중 claim error / 다중 quad UB / 부기 대상 자식 + 전부 매핑"을 그대로 두면 `Shop.client.luau`와 `Inventory.client.luau`가 각각 + `Claim(PlayerGui, …)`하는 **가장 흔한 사례가 막힌다**. 이건 "전부 매핑" 계약이 + **루트 컨테이너**(부기 대상이 아닌 `PlayerGui`·`CoreGui`류 — 자식 순서가 + 의미 없고 quad가 그 형제들을 관리하지 않는다)에는 안 맞는다는 신호다. 갈래: + (a) `Claim`은 **부기를 갖는 노드**에만, 루트 컨테이너엔 "quad가 만든 자식 + 하나를 붙이는" 별개 표면(부기 없음, 여러 스크립트 공존, 이름은 §5-5와 같이) / + (b) `Claim`에 "이 노드의 다른 자식은 관리하지 않는다"(부분 매핑) 모드 — 단 + web처럼 물리 순서가 의미 있는 엔진에선 위험 / (c) 다중 claim을 허용하되 각 + claim이 자기가 매핑한 자식만 소유(UB 대신 정의) — 부기 충돌 없음이 조건. + **권고 없음** — (a)는 `H-146`에서 기각한 `Mount(root, parent)`의 재개방과 + 경계가 얇고, (b)(c)는 계약을 약화시킨다. 사용자 판단. +8. **매핑된 정적 자식의 `Parent` 대입** — §2는 "부기만, `.Parent =`는 안 한다"인데 + `InstanceChildHandler`는 `v.Parent = inst`가 계약(`dispatch-core-plan.md` + `H-134`). 같은 핸들러를 쓰면 이미 거기 있는 자식에 같은 값을 재대입(엔진 + no-op)하는 것뿐이라 별도 핸들러가 필요 없어 보인다 — 확인 문항(권고: 같은 + 핸들러, 재대입 감수). + + +## 승격 뒤 `/code-review high` (2026-08-28) + +감사 6라운드 수렴 뒤 돌린 리뷰가 10건을 냈다. 서술·라벨·stale 여섯은 반영, **새 +메커니즘이 필요한 넷**(gcconn/gchold 셋업 자리 / 이중 claim 레지스트리 / `PlayerGui` +own-all vs `ResetOnSpawn` / 공유 `Param` 배열 파트)은 `base/claim-plan.md` §10과 +`question.md`에 문항으로 — 사용자 결정 대기. 리뷰가 제안한 이름(`nativeAdopt`, +`FrameParam`)은 전부 가칭. diff --git a/.claude/todos.md b/.claude/todos.md index 0d6a48c..816e9a7 100644 --- a/.claude/todos.md +++ b/.claude/todos.md @@ -56,11 +56,17 @@ 와 함께 **같은 날 대화형으로 전량 결정·반영**(소스 `-round10-followup.md`). **어제 결정 중 뒤집힌 것 셋**: `fn`/cleanup은 자기 구독을 못 바꾼다(`H-147`, `H-143` 소멸) / 재구독·재바인드의 `Refresh` 캐치업 폐기(`H-151`) / 루트는 밖에서 `.Parent =`가 - 아니라 quad가 `Claim`으로 소유(`H-148`, `research/existing-mount-plan.md`). **같은 날 후속** `H-158`~`H-162`(`:Block` 폐기 → `state:Apply(blocker)` / + 아니라 quad가 `Claim`으로 소유(`H-148`, `base/claim-plan.md`). **같은 날 후속** `H-158`~`H-162`(`:Block` 폐기 → `state:Apply(blocker)` / `_rerunRequired` 홀드 플래그가 `_installed`를 흡수 / `Claim` M5 스코프 / `Void` export)도 확정·반영, **후속 2** `H-163`/`H-164`(`EmitReceive` 인터페이스 — 전파 루프는 - `sub:_receive(from)`만 / Slot 재마운트 캐치업 / `emitFrom == nil` 계약 / `Observer:_catchUp`)까지. 남은 미결은 `question.md` 최우선 절의 `Claim` 갈래(특히 §5-7 다중 - 스크립트) 하나 — 게이트 아님. 남은 액션: M2 착수.** + `sub:_receive(from)`만 / Slot 재마운트 캐치업 / `emitFrom == nil` 계약 / `Observer:_catchUp`)까지. **[2026-08-28 후속 3]** `Claim` 갈래 여덟도 같은 날 전량 확정돼 + `research/`에서 **`base/claim-plan.md`로 승격**(결정 기록은 그 §7 — 루트 키 센티널 / + 디스크립터 순서 정본 / debug 검사 범위는 `research/debug-tooling-plan.md`로 / + **루트의 `.Parent =`는 밖에서 허용으로 복원**, `Claim`은 1회·전체 소유). + `question.md` 최우선 절은 비어 있다. 승격 뒤 `/code-review high`가 **M5 착수 전 + 문항 넷**(gcconn/gchold 셋업 자리 / 이중 claim 레지스트리 / `PlayerGui` own-all / + `Param` 배열 파트)을 냈다 — `base/claim-plan.md` §10·`question.md`, M2 게이트 + 아님. 남은 액션: M2 착수.** 아래는 돌리기 전(2026-08-26) 서술: 지시서는 `qa-request/pre-implementation-handtrace-round9-brief.md`. 스코프는 **커밋 `9dd8213` 하나의 델타**다 — 8라운드 결정 반영과 그 뒤 diff --git a/CLAUDE.md b/CLAUDE.md index 552c5d5..a0a81e7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,9 +16,8 @@ M2(반응형 코어 — Source/State/Store)**. **⚠️ [2026-08-24] M2와 M3의 `-round9-followup.md` — Q1~Q10·`H-138`·`H-139`·`H-142`, 그리고 `/code-review`가 낸 `H-143`~`H-146`까지 전량 반영 완료**; **[2026-08-28] 10라운드 몫은 `-round10-followup.md`** — 광범위 탐사 `H-150`~`H-157`까지 전량 결정·반영, 그중 -`H-143`과 `H-146` 루트 예외는 하루 만에 뒤집힘)이고, `.claude/question.md` 최우선 -절엔 `Claim` 갈래(`research/existing-mount-plan.md` §5, 특히 다중 스크립트)만 -남아 있다(M2 착수 게이트는 아님). 같은 상태를 `.claude/project-context.md`도 +`H-143`과 `H-146` 루트 예외는 하루 만에 뒤집힘 — 후자는 같은 날 다시 좁혀 복원)이고, **같은 날 `Claim` 갈래까지 전량 확정돼 `research/`에서 +`base/claim-plan.md`로 승격** — `.claude/question.md` 최우선 절은 **비어 있다**. 같은 상태를 `.claude/project-context.md`도 서술하니 마일스톤이 넘어갈 때 두 곳을 같이 고칠 것. 진행 상황의 소스는 항상 루트 `ROADMAP.md`. diff --git a/ROADMAP.md b/ROADMAP.md index dccd630..94b59e2 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -31,8 +31,10 @@ quad-v2 구현 단계 실행 계획. 설계 근거/아키텍처 자체는 여기 > weak-key / 루트 부착은 사용자 몫)도 같은 날 확정·반영. **[2026-08-28] 10라운드** > (`-round10-followup.md`가 소스) — 그중 둘은 하루 만에 다시 뒤집혔다: `fn`/cleanup은 > 자기 구독을 못 바꾼다(`H-147`, `H-143` 소멸 · `rawRerun(force)`/`Rerun` 분리) / -> 루트는 밖에서 `.Parent =`가 아니라 **quad가 `Claim`으로 소유**(`H-148`, -> `research/existing-mount-plan.md`, **M5 스코프** — `H-161`). 그 밖에 `_epochs`는 emit 때만 갱신 +> 이미 있는 트리는 **quad가 `Claim`으로 소유**(`H-148`, `base/claim-plan.md`, +> **M5 스코프** — `H-161`; 같은 날 갈래까지 전량 확정 — 그 과정에서 **루트의 +> `.Parent =`는 밖에서 허용으로 복원**, 요지는 M5 `Claim` 체크박스; `/code-review`가 +> 낸 M5 착수 전 문항 넷은 그 문서 §10). 그 밖에 `_epochs`는 emit 때만 갱신 > (`Refresh` 캐치업 폐기, `H-151`) / `Effect._blocker` 제거(`H-150`) / Observer > 진입점 인라인(`H-149`) / `GateNode` 브랜드 등록(`H-152`) / Store 예약 이름 > 런타임 가드(`H-153`) / `InstanceChildHandler` dedup(`H-154`). @@ -949,6 +951,9 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 > **`onDestroying(inst, fn)`**(`H-11`): `Effect`의 leaf 사망 cleanup을 발화시키는 > 훅으로, `bindLifetime`이 `isEffect`일 때 부른다(quad-roblox 구현은 > `inst.Destroying:Connect(fn)`). 이것도 조합으로 못 만들므로 미주입이면 에러다. +> **[2026-08-28]** 셋째로 **`nativeFindChild(inst, key)`**(`Claim`의 조회 op, 아래 +> `Claim` 체크박스·`base/claim-plan.md`) — 같은 예외 취급(에이전트 분류). 전체 +> 목록의 소스는 `base/architecture.md`. > **⚠️ 구현 관례**: `quad-roblox`의 공개 타입은 지금부터 단일 파일 > (`src/init.luau` 또는 `types.luau`)에 몰아둘 것 — 나중에 필요해지면 @@ -960,7 +965,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 `QuadRoblox(Quad): QuadRoblox`가 `QuadTypes.CheckedQuad`으로 주입받은 quad-base 버전을 확인(`base/quad-types-plan.md` 참고) - [ ] `D/init.luau`(제네릭 생성자 `New` + 생성기가 찍는 정적 별칭 필드 — **[2026-08-18]** 범위는 "GUI에 쓰이는 모든 인스턴스", 이벤트 필드의 콜백 타입까지 생성, `base/bind-system-plan.md`의 "인스턴스 생성 / 이벤트 네이밍 인체공학" 절). **[2026-08-27 9라운드 `H-142`] 생성되는 props 타입에서 `Parent`를 제외할 것** — props에 `Parent`는 올 수 없다(부모가 하는 일, 같은 문서의 파이프라인 절). `New` ①~④ 순서는 그 절의 의사코드가 소스 -- [ ] `Handlers/Property.luau`(**[2026-08-27 9라운드 `H-142`]** `isHandlable`이 `"Parent"` 키를 **거부**한다 — 매치 핸들러가 없어지면 `Dispatch.process`의 "매치 핸들러 없음 → 즉시 error"에 걸리는 것으로 런타임 가드가 공짜로 생긴다, 새 메커니즘 없음. **사용자 확정은 "props에 `Parent` 금지"라는 규칙이고, 이 거부 배선은 에이전트 선택** — `base/bind-system-plan.md`의 `H-142` 항목이 그렇게 갈라 적음. **[2026-08-28 10라운드 `H-148`]** 전용 문구는 **철회**(일반 매치 실패 그대로) — 루트는 밖에서 `.Parent =`가 아니라 quad가 `Claim`으로 소유하는 쪽으로(`research/existing-mount-plan.md`, 아래 백로그)), `Handlers/InstanceChild.luau`(**[2026-08-28 `H-154`]** retractor 첫 줄 `if nextValue == v then return end` — 같은 값 재발행 dedup, `SlotHandler` 동형) — +- [ ] `Handlers/Property.luau`(**[2026-08-27 9라운드 `H-142`]** `isHandlable`이 `"Parent"` 키를 **거부**한다 — 매치 핸들러가 없어지면 `Dispatch.process`의 "매치 핸들러 없음 → 즉시 error"에 걸리는 것으로 런타임 가드가 공짜로 생긴다, 새 메커니즘 없음. **사용자 확정은 "props에 `Parent` 금지"라는 규칙이고, 이 거부 배선은 에이전트 선택** — `base/bind-system-plan.md`의 `H-142` 항목이 그렇게 갈라 적음. **[2026-08-28 10라운드 `H-148`]** 전용 문구는 **철회**(일반 매치 실패 그대로). 루트 부착 경로가 무엇인지는 아래 `Claim` 체크박스가 소스), `Handlers/InstanceChild.luau`(**[2026-08-28 `H-154`]** retractor 첫 줄 `if nextValue == v then return end` — 같은 값 재발행 dedup, `SlotHandler` 동형) — **⭐ [2026-08-27 9라운드 `H-134`] `InstanceChildHandler`도 말단이라 부기를 등록한다**: `process`에서 `setOffsetSource(inst, k, None)` → `v.Parent = inst` → `setLength(inst, k, 1, inst)`(정적 단일 자식은 상수 @@ -971,7 +976,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 문단. 빠뜨리면 `Frame { Frame{}, Slot() }`이 첫 마운트에서 죽는다 — `base/dispatch-core-plan.md`의 `H-39` 블록(그 다섯째 항목)이 소스. -- [ ] **[2026-08-28 M5 스코프, `H-161`]** `Claim(inst, D.Mapper. "Name" {…})` — 이미 있는 트리(PlayerGui·`Clone()` 사본)를 quad가 소유. `H-146` 루트 예외를 폐기한 뒤 M5에 승인된 루트 부착 경로가 이것뿐이라 **백로그가 아니라 M5 안**(사용자 확정). 프로바이더 op `nativeFindChild` 필요. 갈래 미결(특히 §5-7 다중 스크립트/루트 컨테이너) — `research/existing-mount-plan.md` §5가 소스(개수도), 다음 배치 문항. +- [ ] **[2026-08-28 M5 스코프, `H-161`; 같은 날 갈래 전량 확정]** `Claim(inst, D.Mapper.(key) {…}) -> inst` — 이미 있는 트리(PlayerGui·`Clone()` 사본·Studio GUI)를 quad가 소유(`base/claim-plan.md`가 소스, 구현 체크리스트는 그 §9). 요지: `drive` 위의 한 겹(DFS 이름 해석 → bottom-up `drive`), 매핑된 자식은 `InstanceChildHandler` 그대로(별도 핸들러 없음), 루트 키 센티널 `D.Mapper.Root`, `type Param`을 `D.`와 공유, `Claim`은 타입 인자 없음, 같은 `inst` 이중 claim error, `Processed` 소진. 프로바이더 op **`nativeFindChild(inst, key)`**(조회라 조합 폴백 예외 — 미주입이면 error). **루트 부착의 흔한 경로는 이게 아니라 밖에서 `.Parent =`** — 루트의 `Parent`는 부기 밖이라 허용(`base/claim-plan.md` §5가 소스; 10라운드 `H-148`에서 한때 "루트도 `Claim`으로만"으로 폐기됐다가 같은 날 복원). **⚠️ 착수 전 사용자 문항 넷**(gcconn/gchold 셋업 자리 / 이중 claim 레지스트리 / `PlayerGui` own-all / `Param` 배열 파트) — `base/claim-plan.md` §10. `D/init.luau` 생성기는 `type Param`(필드 파트)을 `D.`·`D.Mapper.`가 공유하도록 찍는다. - [ ] **Instance 생성 시점의 gcconn/gchold 셋업**(2026-08-14 다섯 번째 세션 확정, 옛 "`bindLifetime` 첫 호출에서 lazy 생성"에서 전환 — `base/ lifecycle-pattern.md`의 "(0) gcconn/gchold는 Instance 생성 시점에