From 0ec22fbe739b8c291dca6ca94d9e73bf2244d69c Mon Sep 17 00:00:00 2001 From: qwreey Date: Fri, 28 Aug 2026 00:28:52 +0900 Subject: [PATCH] =?UTF-8?q?qa:=209=EB=9D=BC=EC=9A=B4=EB=93=9C=20=ED=9B=84?= =?UTF-8?q?=EC=86=8D=20H-143~H-146=20=ED=99=95=EC=A0=95=C2=B7=EB=B0=98?= =?UTF-8?q?=EC=98=81=20+=20=EA=B0=90=EC=82=AC=208=EB=9D=BC=EC=9A=B4?= =?UTF-8?q?=EB=93=9C=C2=B7code-review=20=E2=80=94=20=EC=9E=94=EC=97=AC=20?= =?UTF-8?q?=EC=85=8B=EC=9D=80=2010=EB=9D=BC=EC=9A=B4=EB=93=9C=20=EB=AC=B8?= =?UTF-8?q?=ED=95=AD=EC=A7=80=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - H-143 Rerun 꼬리: 실행 중 사망(wasAlive and not canExecute)이면 cleanup 즉시 소진 (처음 쓴 not canExecute 판정은 생성자 최초 설치를 죽여 감사 2라운드가 정정) - H-144 재구독 꼬리(Refresh 먼저) + 진입점은 EffectHandle 자기 것 (b) (Observer 함수 배정은 콜론 위임으로 꼬리 2회 — 감사 4라운드, luau 재현) → conventions.md 설계 원칙 신설: 하나의 무언가가 두 일을 하지 않는가 - H-145 bk.indexOfElement weak-key / H-146 루트 .Parent는 사용자 몫, Mount 없음 - 감사 1→1→1→1→1→1→1→0, /code-review high 10건 중 7 반영 - H-147~H-149는 qa-request/pre-implementation-handtrace-round10.md §4로 (배치 회신) - round10 지시서(-brief.md) 신설, 광범위 탐사 예정 Co-authored-by: qwreey Claude-Session: https://claude.ai/code/session_01546hjsYNLSMZdHdPyTZaGb --- .claude/README.md | 4 +- .claude/base/architecture.md | 2 +- .claude/base/bind-system-plan.md | 23 ++- .claude/base/dispatch-core-plan.md | 30 ++- .claude/base/effect-plan.md | 184 +++++++++++++++--- .claude/base/lifecycle-pattern.md | 22 ++- .claude/base/ref-plan.md | 5 +- .claude/base/slot-plan.md | 10 +- .claude/base/source-state-plan.md | 9 +- .claude/conventions.md | 20 ++ .claude/project-context.md | 2 +- ...-implementation-handtrace-round10-brief.md | 169 ++++++++++++++++ .../pre-implementation-handtrace-round10.md | 99 ++++++++++ ...mplementation-handtrace-round9-followup.md | 173 +++++++++++++++- .../pre-implementation-handtrace-round9.md | 10 +- .claude/question.md | 29 ++- .claude/session-summary.md | 18 ++ ...026-08-27-03-handtrace-round9-h143-h146.md | 75 +++++++ .claude/todos.md | 9 +- CLAUDE.md | 7 +- ROADMAP.md | 30 ++- 21 files changed, 838 insertions(+), 92 deletions(-) create mode 100644 .claude/qa-request/pre-implementation-handtrace-round10-brief.md create mode 100644 .claude/qa-request/pre-implementation-handtrace-round10.md create mode 100644 .claude/session/2026-08-27-03-handtrace-round9-h143-h146.md diff --git a/.claude/README.md b/.claude/README.md index c3e10c0..6ce83a1 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` 금지 — 순서 문제 소멸)**. 진행 표가 상태의 소스, 사용자 회신 원문 둘 인용). 다음 라운드가 필요해지면 라운드마다 파일을 새로 만들고 이름에 라운드 번호를 넣을 것(지시서도 같이 남길 것 — `-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/`). 다음 라운드가 필요해지면 라운드마다 파일을 새로 만들고 이름에 라운드 번호를 넣을 것(지시서도 같이 남길 것 — `-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` | @@ -59,7 +59,7 @@ | `component-composition-plan.md` | 컴포넌트=플레인 함수, State/Source 읽기·쓰기 경계, Source가 State를 구조적으로 만족 — modifier/Ref 컴포넌트 경계 통과까지 전부 확정, 남은 건 API 이름뿐. **[2026-08-07 정리]** 폐기된 `StoreSource` 프록시 설계로의 역전 이력은 본문에서 빼고 `archive/store-source-proxy-reversed.md` 포인터로 압축 | | `blocker-plan.md` | **[2026-08-07 신설]** `Blocker` — 여러 Source를 한꺼번에 바꿔도 파생값 재계산이 한 번만 되게. **[2026-08-24 재확정]** 구현은 State 코어와 같은 **M2**이고(2026-08-22에 디스패치 쪽으로 앞당겼다가 마일스톤 순서 교체로 되돌아옴), 바닥부터 짜는 게 아니라 공용 `GateNode`(`gate-plan.md`) 위의 **정책**이다. 메커니즘+이름 확정. **[2026-08-18 구현 전 QA 2라운드 후속]** `IsOn()`/`OffWithoutEmit()` 신설(`RC-1` 해결 과정에서 나옴) — `state:Block()` 없이 Blocker를 직접 쓰는 두 번째 용례(base 내부 Length/Offset 배치 게이팅)도 추가. **[2026-08-18 구현 전 QA 3라운드]** 이 용례의 존재 이유 정정 — `RC-1`의 원래 크래시는 사라졌고(`bk.N` 수명주기 재정의로), 지금 필요한 이유는 배치 등록 비용(O(N²)→O(N)) **[2026-08-24 6라운드 `H-33`/`H-49`]** **`blocker:Policy(emit) -> onUpstreamEmit`** 신설 — 자기 게이트 정책을 값으로 내주고 `state:Block(b)`가 그 위의 얇은 래퍼가 된다. `Debounce`/`Throttle`이 이걸로 자기 Blocker를 조종한다 | | `debounce-throttle-plan.md` | **[2026-08-14 신설, 2026-08-19 전부 해소돼 `research/`에서 승격]** 시간 기반 전파 게이트 `Debounce`/`Throttle` — 사용자 요청("`Blocker`와 유사하게")으로 신설. 요지: (1) `Blocker`가 이미 쓰는 게이트 노드의 **릴리스 트리거만 타이머로 바꾼 것**이라 새 전파 메커니즘이 아님, (2) 무효화 채널만 만지므로 laziness 안 깨짐, (3) **Debounce/Throttle의 차이는 "신호가 창 타이머를 리셋하는가" 한 비트뿐** — 공개 생성자는 둘, 구현은 하나, (4) 알고리즘은 quad-base + 주입 op 2개 `setTimeout(func, delay) -> Timeout`/`clearTimeout`(Roblox `task.delay`/`task.cancel`로 배선 — **인자 순서 반대라 주의**), `Timeout`은 `{ __type_timeout: true, _native: any }`. **[2026-08-19 마지막 라운드]** 의미론은 **(A) emit-gate**(`Blocker`와 동일, `:Get()`은 항상 최신값)로 확정 — 검토했던 값-지연 안은 laziness와 상충해 철회. 제어 핸들은 개별은 `Ref` 아웃파라미터·전체는 팩토리 자체의 `:Flush()`/`:Cancel()`(weak 레지스트리)로 확정, `Time`/`MaxTime`은 `number \| State`(스케줄 시점에만 폴링) 허용. **⚠️ [2026-08-24 6라운드 `H-32`/`H-33`] 7절 의사코드에 무효화 배너가 붙었다 — 그 골격은 확정된 `state:Gate(setup)` API로는 성립하지 않아 재작성 대상이다.** 새 모델은 `Debounce`/`Throttle`이 **emit을 아예 안 쥐고** 자기 `Blocker`를 사적으로 하나 갖고(적용 핸들당 하나) `On()`/`Off()` 시점만 정하는 것 — `pending`도 Blocker의 `HasBlockedEmit`으로 흡수되어 `Trailing=false`+`MaxTime`에서 `Flush`가 영구 no-op이던 결함(`H-32`)이 구조적으로 사라진다. 창/타이머 정책 자체(`openWindow`/`onWindowEnd`/`MaxTime` 분기)는 그대로 유효. 이름은 `Debounce`/`Throttle` 유지 + Roblox 관용 "debounce"와 다르다는 문서 경고. **결과적으로 quad-base에 새 코어 메커니즘을 안 더하는 순수 슈가로 귀결**(`Blocker`의 gated state + `Ref` + 주입 op 2개 위에 전부 얹힘) — 우선순위는 `Operator.*`와 같은 급으로 재평가됨. **부수 성과**: 이 설계 중 `source-state-plan.md`의 무효화 dedup 서술이 `Observer` 계약과 모순되는 게 발견돼 base 전면 정정(`archive/invalidate-dedup-propagation-reversed.md`) **⚠️⚠️ [2026-08-26 정정, 8라운드 `H-118`]** 이 셀이 "새 모델"이라 부르는 문장은 **두 겹으로 낡았다** — (1) *"`pending`도 `HasBlockedEmit`으로 흡수"*는 7라운드 `H-86`이 이미 뒤집었다(실제 통로는 `emit()`의 **반환값**), (2) *"`emit`을 아예 안 쥐고"*라는 머리 문장 자체가 8라운드에 폐기됐다 — `setup(emit)`이 계약이라 **`emit`은 정의상 정책 손에 있고**, Blocker에 위임되는 건 *"emit된 적 있던가"의 부기*뿐이다. 경로가 둘이다: 상류 emit 도착은 `pass()`, 타이머/제어 핸들의 flush·버리기·조회는 `emit()`/`emit(false)` 직접 호출. 최신 서술은 `base/debounce-throttle-plan.md`와 `base/gate-plan.md` 5번이 소스. | -| `effect-plan.md` | **[2026-08-07 신설, 여섯 번째 세션에 확정]** `Effect(fn, ...deps)` — deps 없으면 설치 1회+leaf 사망 시 확정 정리, 있으면 **각 dep에 구독을 따로 걸어**(State/Source면 `Observer`, `Ref`면 `:WeakCallback` — **[2026-08-27 `H-129`]** 옛 `:Callback` 표기는 `H-58` 정정의 잔재) 재실행+cleanup 체이닝(React `useEffect` 동형). **[2026-08-24 표기 정정]** 여기 원래 *"내부적으로 `state:Observer(...)`를 조합"*이라 적혀 있었는데 그건 단수 시절 모델이고, 같은 행 뒤쪽의 `C-6` 서술과 어긋났다. Observer와의 관계 해소 완료. **[2026-08-18 구현 전 QA 반영]** **`:Unsubscribe()`는 `:Subscribe()`의 짝으로 축소** — leaf 바인딩 경로에서 cleanup을 앞당기면 dedup 때문에 재바인딩이 안 일어나 Effect가 조용히 죽음. **[2026-08-20 `E-10` → 2026-08-21 `EF-3`에서 반영]** 그 dedup 경로의 process/retract 대칭은 **성립함이 확인됨**(핸들러가 `old`를 `Relate`로 직접 들고 양쪽이 같은 비교식을 씀 — 남은 건 구현 시 회귀 확인뿐, 설계상 열린 항목 아님). **[2026-08-21 5라운드 `C-6`]** 시그니처가 `Effect(fn, ...deps)`로 확장돼 의존성을 여러 개 직접 받고(`Ref`도 가능) 각각에 구독을 건다. **[2026-08-21]** 다중 의존성이 공통 상류를 공유할 때 한 파동에 `fn`이 여러 번 돌던 미해결 갭은 **`EffectHandle`이 자기 `EpochMap`을 들어** 닫힘(`base/state-epoch-plan.md`). **[2026-08-24 6라운드]** `fn` 시그니처가 **`fn(self: EffectHandle) -> (() -> ())?`**로 확정(**`...deps`는 의존성 선언일 뿐 `fn`에 안 넘어간다**, `H-14`), `_observer`(단수)→`_observers`(배열)(`H-8`), 그리고 **leaf 사망 cleanup을 실제로 발화시키는 배선이 없던 것**이 `bindLifetime`/`unbindLifetime`이 `isEffect`를 보고 `Destroying`을 걸고/끊는 것으로 닫힘(`H-11` — 이게 없어서 `slot._detachCleanup`과 `OnDestroyed`가 통째로 무동작이었다). `Ref` dep 콜백은 해제 경로(`:Uncallback`)와 **발화 시 `canExecute` 확인**을 둘 다 갖는다(`H-7`) **⭐⭐ [2026-08-25 7라운드 재설계]** dep 등록이 **생성자 한 곳**으로 모이고(`:WeakCallback`/`:WeakSubscribe` — `Weak` 쪽이 프리미티브), 강한 주인이 **`_deps` 하나**로 통합됐다(옛 `_observers`/`_refDeps`/`_refCallbacks`/`_installing` 전부 폐기, 억제는 사적 `Blocker`). `bindLifetime`/`unbindLifetime`은 **핸들 하나에만** 적용되고 내부 Observer로 cascade하지 않으며, `Ref` 콜백을 떼지도 않는다 — 발화 게이팅은 `canExecute(handle)` 하나(`H-58`/`H-59`). `Ref`가 `Epoch`로 승격돼 `_epochs`가 dep 종류를 균일하게 담고, 포탈 캐치업이 `if not self._installed or self._epochs:Refresh() then self:Rerun() end` 한 줄이 됐다(`H-64`/`H-65` — cleanup 반환이 **선택**이라 `_cleanup` 유무로는 설치 여부를 못 판정한다). **`:Rerun()` 정의 신설**(재진입은 지연 재실행, error는 UB) 및 `:_consumeCleanup()`(읽고→지우고→실행), 값 교체 retract가 cleanup을 소진 호출(`H-57`), deps 검증(`nil`/이물 error, 중복 무시). 재사용은 `Clone`/`Userdata`가 아니라 **`({...}) -> Effect` 팩토리 패턴** **⭐⭐ [2026-08-26, 8라운드 `H-107`/Q2-후속]** dep 종류별로 **클로저를 따로** 단다(`onRefFire(_, ref)` / `onStateFire(_, _, from)`) — 여기 있던 *"클로저는 **하나**로 통일한다"*는 근거 없는 서술이라 삭제됐다. **사용자 확정**: *"observer 에는 epoch 란게 존재하지 않음. emit 으로 온 epoch 를 넘겨줄 뿐, 그러나 ref 는 그 자체로 epoch임"* — 두 콜백은 이질적이라 애초에 통합 대상이 아니었고, dedup은 클로저 identity가 아니라 `_deps`/`_epochs` 맵이 한다. **[2026-08-27 9라운드 `H-127`/`H-130`]** `EffectHandle` 네 진입점 의사코드 신설 — Observer 것을 그대로 재사용, `Unsubscribe`만 **게이트 통과 뒤** cleanup 소진(산문 순서대로 짜면 leaf 바인딩된 핸들의 cleanup이 error보다 먼저 소진됐다). 옛 `_observers` cascade 블록은 `archive/effect-internal-observer-cascade-reversed.md`로 이전. | +| `effect-plan.md` | **[2026-08-07 신설, 여섯 번째 세션에 확정]** `Effect(fn, ...deps)` — deps 없으면 설치 1회+leaf 사망 시 확정 정리, 있으면 **각 dep에 구독을 따로 걸어**(State/Source면 `Observer`, `Ref`면 `:WeakCallback` — **[2026-08-27 `H-129`]** 옛 `:Callback` 표기는 `H-58` 정정의 잔재) 재실행+cleanup 체이닝(React `useEffect` 동형). **[2026-08-24 표기 정정]** 여기 원래 *"내부적으로 `state:Observer(...)`를 조합"*이라 적혀 있었는데 그건 단수 시절 모델이고, 같은 행 뒤쪽의 `C-6` 서술과 어긋났다. Observer와의 관계 해소 완료. **[2026-08-18 구현 전 QA 반영]** **`:Unsubscribe()`는 `:Subscribe()`의 짝으로 축소** — leaf 바인딩 경로에서 cleanup을 앞당기면 dedup 때문에 재바인딩이 안 일어나 Effect가 조용히 죽음. **[2026-08-20 `E-10` → 2026-08-21 `EF-3`에서 반영]** 그 dedup 경로의 process/retract 대칭은 **성립함이 확인됨**(핸들러가 `old`를 `Relate`로 직접 들고 양쪽이 같은 비교식을 씀 — 남은 건 구현 시 회귀 확인뿐, 설계상 열린 항목 아님). **[2026-08-21 5라운드 `C-6`]** 시그니처가 `Effect(fn, ...deps)`로 확장돼 의존성을 여러 개 직접 받고(`Ref`도 가능) 각각에 구독을 건다. **[2026-08-21]** 다중 의존성이 공통 상류를 공유할 때 한 파동에 `fn`이 여러 번 돌던 미해결 갭은 **`EffectHandle`이 자기 `EpochMap`을 들어** 닫힘(`base/state-epoch-plan.md`). **[2026-08-24 6라운드]** `fn` 시그니처가 **`fn(self: EffectHandle) -> (() -> ())?`**로 확정(**`...deps`는 의존성 선언일 뿐 `fn`에 안 넘어간다**, `H-14`), `_observer`(단수)→`_observers`(배열)(`H-8`), 그리고 **leaf 사망 cleanup을 실제로 발화시키는 배선이 없던 것**이 `bindLifetime`/`unbindLifetime`이 `isEffect`를 보고 `Destroying`을 걸고/끊는 것으로 닫힘(`H-11` — 이게 없어서 `slot._detachCleanup`과 `OnDestroyed`가 통째로 무동작이었다). `Ref` dep 콜백은 해제 경로(`:Uncallback`)와 **발화 시 `canExecute` 확인**을 둘 다 갖는다(`H-7`) **⭐⭐ [2026-08-25 7라운드 재설계]** dep 등록이 **생성자 한 곳**으로 모이고(`:WeakCallback`/`:WeakSubscribe` — `Weak` 쪽이 프리미티브), 강한 주인이 **`_deps` 하나**로 통합됐다(옛 `_observers`/`_refDeps`/`_refCallbacks`/`_installing` 전부 폐기, 억제는 사적 `Blocker`). `bindLifetime`/`unbindLifetime`은 **핸들 하나에만** 적용되고 내부 Observer로 cascade하지 않으며, `Ref` 콜백을 떼지도 않는다 — 발화 게이팅은 `canExecute(handle)` 하나(`H-58`/`H-59`). `Ref`가 `Epoch`로 승격돼 `_epochs`가 dep 종류를 균일하게 담고, 포탈 캐치업이 `if not self._installed or self._epochs:Refresh() then self:Rerun() end` 한 줄이 됐다(`H-64`/`H-65` — cleanup 반환이 **선택**이라 `_cleanup` 유무로는 설치 여부를 못 판정한다). **`:Rerun()` 정의 신설**(재진입은 지연 재실행, error는 UB) 및 `:_consumeCleanup()`(읽고→지우고→실행), 값 교체 retract가 cleanup을 소진 호출(`H-57`), deps 검증(`nil`/이물 error, 중복 무시). 재사용은 `Clone`/`Userdata`가 아니라 **`({...}) -> Effect` 팩토리 패턴** **⭐⭐ [2026-08-26, 8라운드 `H-107`/Q2-후속]** dep 종류별로 **클로저를 따로** 단다(`onRefFire(_, ref)` / `onStateFire(_, _, from)`) — 여기 있던 *"클로저는 **하나**로 통일한다"*는 근거 없는 서술이라 삭제됐다. **사용자 확정**: *"observer 에는 epoch 란게 존재하지 않음. emit 으로 온 epoch 를 넘겨줄 뿐, 그러나 ref 는 그 자체로 epoch임"* — 두 콜백은 이질적이라 애초에 통합 대상이 아니었고, dedup은 클로저 identity가 아니라 `_deps`/`_epochs` 맵이 한다. **[2026-08-27 9라운드 `H-127`/`H-130`]** `EffectHandle` 네 진입점 의사코드 신설 — **[같은 날 (b)로 정정, `H-144` 후속]** 네 진입점은 `EffectHandle` 자기 것(공유는 `Observer.luau`의 레지스트리 둘과 `canBound`뿐 — Observer 함수를 배정하면 콜론 위임이 오버라이드를 타 재구독 꼬리가 두 번 돈다), `Unsubscribe`는 **게이트 통과 뒤** cleanup 소진(산문 순서대로 짜면 leaf 바인딩된 핸들의 cleanup이 error보다 먼저 소진됐다), `Subscribe`/`WeakSubscribe`는 등록 끝에 `_epochs:Refresh()` + 조건부 `Rerun`(재구독 재설치), `Rerun` 꼬리는 실행 중 죽었으면 cleanup 즉시 소진(`H-143`). 옛 `_observers` cascade 블록은 `archive/effect-internal-observer-cascade-reversed.md`로 이전. | | `ui-shorthand-plan.md` | **[2026-08-07 `research/`에서 승격]** `UICorner`/`UIPadding`/`UIScale` 인라인 편의 키 — 이름(v1 `Corner`/`PaddingAll`/`Scale`에서 Modifier 필드명과 안 겹치게 `UI` 프리픽스로 확정)·메커니즘(Handler)·패키지 배치(quad-roblox 코어)·store-bind 가능성까지 전부 확정. 이미지 라운드 트릭(`RoundSize`)은 드롭 — `archive/ui-shorthand-roundsize-dropped.md` 참고. `v=nil`이면 `process` 자신이 만든 자식 제거(`retract` 아님). **[2026-08-14 세션] Tween 지원 추가** — 자식 프로퍼티를 직접 대입하지 않고 `Dispatch.process(child, prop, ..., 1)`로 위임하는 것으로 확정(프로세스 중 `inst`를 바꾸는 건 키를 바꾸는 것과 같은 층위라 UB 아님, `dispatch-core-plan.md`에 일반 규칙으로 명문화) — Tween 해석 코드가 `PropertyHandler` 하나에만 남는다는 불변식이 유지되고, 이 문서가 새로 정할 건 스칼라→프로퍼티 `wrap`을 `Tween.Value`에만 적용되도록 들어올리는 헬퍼 하나뿐. 옛 "트윈까지 지원할 필요 없음" 서술은 역전됨(그때는 Tween이 독립 Dispatch 핸들러였음). ROADMAP M10에 빠져 있던 체크리스트 항목도 이 세션에 보강. **[2026-08-18 구현 전 QA 반영]** 만든 자식을 다시 찾을 때 **`FindFirstChild` 대신 `Relate` 저장**(이름은 표시·판정용, 릴레이션은 조회용), 자식 프로퍼티 세팅도 `Dispatch.process`로 위임해 Tween이 공짜로 따라오게 **[2026-08-27 9라운드 `H-138`]** 숏핸드 핸들러가 `PropertyHandler`보다 우선순위가 높다(리플렉션 거부에 기대지 않는다), 충돌 방지는 `UI` 접두어의 몫. | | `tag-plan.md` | **[2026-08-08 세 번째 세션 재설계, 2026-08-12 열한 번째 세션 메커니즘 정정]** `Tag(...)` — array-part 값 객체, `Modifier`와 같은 immutable clone 체이닝(`:Added`/`:Removed`/`:Contains`/`:Apply`/`Merged`), `CollectionService` 글루만 quad-roblox. `retract`가 이전 Tag가 걸었던 이름을 이름별 참조 카운트 맵에서 빼고(다른 위치가 겹쳐 쓰면 실제 `RemoveTag`는 skip), `process`가 새 Tag의 이름을 등록 — 여러 위치가 같은 이름을 겹쳐 가져도(웹 `className`류 합집합) 안전. 구 해시 파트 boolean 모델은 `archive/tag-hash-key-model-reversed.md`, 구 `assert(v==nil)` 메커니즘은 `archive/retract-always-fires-reversed.md`. **[2026-08-12 열다섯 번째 세션]** `Added`/`Removed`가 vararg가 아니라 `string | {string}`으로 정정 — `table.unpack`이 인자 목록 tail 위치에서만 완전히 펼쳐지는 Lua 문법 제약 때문에 여러 개의 독립된 동적 이름 테이블을 한 vararg 호출로 못 합치는 경우가 생김이 발견됨, `Tag(...)` 생성자 자체는 정적 리터럴 호출이라 vararg 유지. **[2026-08-13 세션]** 참조 카운트 `holders`가 Tag 객체 identity로 키잉돼 있어서 같은 Tag 객체를 여러 위치에서 재사용하면(immutable이라 흔한 관례) 한 위치만 retract돼도 다른 위치가 쓰는 태그가 지워지는 실제 버그 발견·수정 — holders를 위치(`k`) 기준으로 재키잉, `oldv==newv`면 retract 스킵하는 최적화도 추가. **[2026-08-13 세션, 다섯 번째, 전면 반영]** `TagHandler.process`가 자기 retract 클로저를 반환하는 계약으로 전환되며 `kTagMap`(위치별 마지막 Tag)이 완전히 불필요해짐(클로저가 `v`를 직접 캡처) — `tagNameMap`(이름별 위치 집합)만 남음, `base/dispatch-core-plan.md` "Dispatch 체인" 절 참고 **[2026-08-13 열네 번째 세션]** 하강 diff 반영(`isTag(hintValue)` 방어 가드 폐지 — 클로저 인자의 타입이 계약으로 보장됨, 깜빡임 방지가 깊은 체인에서도 유지) + **패키지 재배치**(참조 카운트 Handler까지 quad-base, 백엔드는 `addTag`/`removeTag(inst, {string})`만 주입 — 웹 `className` 대응 때문에, vararg 아닌 테이블인 이유는 `Tag:Added`와 동일) **[2026-08-24 6라운드]** `TagHandler`가 **자기 배열 자리의 `setOffsetSource`/`setLength`를 아예 등록하지 않던 것**을 정정(`H-39` — `Frame { Tag("card"), TextLabel{} }`처럼 Tag를 자식보다 앞에 두는 흔한 배치가 첫 `recompute`에서 error로 죽었다). `isHandlable`에 **`type(k) == "number"` 가드**도 추가(`H-52` — `RefLeafHandler`가 2026-08-18에 받은 수정을 이쪽은 못 받고 있었다) | | `attribute-plan.md` | **[2026-08-07 여덟 번째 세션 신설]** 단일 키 `[AttributeKey "Name"]`(구 `Attribute`) — `SetAttribute(name, nil)`이 네이티브 지우기라 `None` 센티널과 가장 깔끔하게 맞아떨어짐. **[2026-08-11 아홉 번째 세션]** 여러 Store를 한 번에 attribute로 묶는 그룹 `Attribute(...)` 프리미티브 신설(`Tag`와 동형 array-part 값 객체, `Merged`로 헤테로지니어스 Store 합성), 이름 충돌 방지로 단일 키를 `AttributeKey`로 리네임(잠정). **[같은 세션 후속]** `AttributeKey(name)`이 이름별 weak 캐시로 동등성 보장하도록 확정되며, 그룹 Handler는 자기 완결형 재구현 대신 메모이즈된 키로 기존 단일 키 경로에 재귀 위임하는 걸로 개정(중복 구현 제거). **[2026-08-12 열 번째 세션]** 그룹/직접 쓰기가 같은 이름을 동시에 관리하는 충돌을 막기 위해 그룹은 공개 캐시 대신 `rawNew(name)` 전용 키+소유권 `Relate`로 전환. **[열한 번째 세션]** `retract`가 store 재발행마다 항상 불린다는 정정에 맞춰 `AttributeKeyHandler.retract`를 손봄(이 시점엔 `v==nil` 가드 버전 — 아래 열여섯 번째 세션에서 최종 재정정됨), 그룹의 "남아있는 이름" 위임도 매번 `retractUnder`를 먼저 부르도록 정정(체인 누수 방지). **[2026-08-12 열여섯 번째 세션, 최종 재정정]** `retract`는 완전 no-op으로 굳어짐(`SetAttribute`는 오직 `process(inst,k,nil)`에서만) — Attribute는 명시적 `None`/`nil`로만 지워지고, 그룹 diff나 컴포넌트 언마운트로 이름이 조용히 사라져도 값은 자동으로 안 지워짐(`Ref`의 "Destroy 무관, 정리는 명시적으로" 철학과 통일), 단 사라진 이름의 *구독*은 끊어 자원 누수는 막음 — 위 "v==nil 가드" 버전은 이걸로 폐기. **[2026-08-13 세션, 전면 재정정]** `rawNew`+`owners` 수동 레지스트리 방식이 "그룹이 이름을 놓았다 다시 포함하면 자기 자신과 충돌"하는 실제 버그로 확인됨 — `AttributeGroupKeyHandler`라는 `isHandlable` 없는 순수 체크포인트 핸들러를 `Dispatch.processAs`로 명시 push하고 `Dispatch.retractSelfAndUnder`로 통째 철거하는 방식으로 전면 재설계, 소유권 충돌 감지도 별도 레지스트리 없이 기존 재진입 가드가 대신 잡아줌(`bind-system-plan.md` 참고). `AttributeKeyHandler`는 다시 완전 무상태로 단순화됨. **[2026-08-13 세션, 다섯 번째, 전면 재설계 — 체크포인트조차 불필요해짐]** `Dispatch`가 인덱스 기반으로 재설계되며 `AttributeGroupKeyHandler`/`processAs`/`retractSelfAndUnder`를 전부 걷어냄 — 그룹이 그냥 공개 `AttributeKey(name)`으로 항상 인덱스 1부터 `Dispatch.process`/`retractFrom`을 직접 부르면 끝(점유 체크 자체가 소유권 충돌 감지), `groupState` Relate도 필요 없어짐(반환 클로저가 이름 집합을 직접 캡처) — 중간 버전은 `archive/checkpoint-handler-pattern-reversed.md`. **[2026-08-13 감사, 정정]** 그런데 그 의사코드가 `process` 안에서 이름마다 `retractFrom(...,1,...)`을 먼저 부르고 있어 **인덱스 1이 무조건 비워지는 바람에 점유 체크가 전혀 작동하지 않았음**(그룹↔그룹 사이에서 조용한 last-write-wins가 그대로 남아 있었음) — `process`는 `Dispatch.process`만 부르고 철거는 반환 클로저가 자기가 등록한 이름 전부에 대해 하도록 정정. 그룹 Handler 시그니처가 계약과 안 맞던 것(`process(inst,index,v)` 3-인자)도 같이 수정 **[2026-08-13 열네 번째 세션, 0-Z 확정]** 그룹이 **자기 전용 키**(비공개 `GetKey`)로 위임하고 이름 소유권은 `AttributeKeyHandler`의 **이름 claim**(`nameClaims` Relate, 충돌 시 즉시 error)이 판정 — 하강 diff에선 두 그룹이 똑같이 `StoreBind`로 보여 점유 체크가 성립하지 않기 때문. 후보 (a)(그룹 안 claimant Relate)는 **그룹↔직접 쓰기를 못 잡아** 기각. 같은 세션에 **패키지 재배치**(값·알고리즘·단일 키 전부 quad-base, 백엔드는 `setAttribute(inst,name,v)`만 주입, 엔진 고유 타입 패밀리만 백엔드). **[2026-08-18 구현 전 QA 반영]** **`Attribute.Merged`(겹치면 error) / `Attribute.Overridden`(뒤가 이김)을 둘 다 제공**으로 열린 항목 해소, 그리고 **⚠️ 같은 그룹 객체를 두 위치에 놓는 경우를 잡을 위치별 claim이 필요**하다는 미해결 항목 신설(`Ref`처럼 `bindLifetime` 재사용은 불가) **[2026-08-24 6라운드]** `AttributeGroupHandler`에 (1) 배열 자리 부기 등록(`H-39`, Tag와 같은 결함 — 층위 예외를 두지 않고 다른 말단 핸들러와 똑같이 등록한다), (2) `type(k) == "number"` 가드(`H-52`), (3) **`groupClaimKeys` 위치 claim 배선**(`H-41` — 5라운드 `AT-1`에서 키를 확정해놓고 의사코드에 안 들어가 있었다, `nameClaims`보다 **먼저** 해야 절반만 기록되는 중간 상태가 안 생긴다). 그리고 **attribute 이름을 서로 다른 두 자리 사이에서 옮기는 것은 UB로 확정**(`H-18`/`H-45` — 두 체인이 별개라 emit 순서에 따라 성공하거나 크래시하는데, 사용자 판단: 막으려면 process/retract 계약 전체에 예외가 생겨 오버엔지니어링) | diff --git a/.claude/base/architecture.md b/.claude/base/architecture.md index 530ff5e..b0fe8d4 100644 --- a/.claude/base/architecture.md +++ b/.claude/base/architecture.md @@ -262,7 +262,7 @@ quad/ │ └── src/ │ ├── Source.luau # 값의 근원, 단일 지점. Source가 State를 구조적으로 만족(`__index` 델리게이션) │ ├── State.luau # 캐시만 하는 non-owning 핸들, state(state) 분기, `:With`/`:Compute`/`:Observer`(등록 즉시 1회 실행)/`:Gate`(`GateNode`, `ComputeNode`와 같은 층위 — `base/gate-plan.md`) 전부 여기 소속 -│ ├── Observer.luau # ⭐ [2026-08-25 신설, 7라운드 `H-99`] `Observer` 객체와 **`:Subscribe()`/`:WeakSubscribe()` 전역 레지스트리의 소유 모듈** — `EpochMap.luau`와 같은 이유로 `State.luau`에 묻지 않는다(`Effect`/`Gate`/leaf 핸들러가 전부 이 레지스트리를 본다) +│ ├── Observer.luau # ⭐ [2026-08-25 신설, 7라운드 `H-99`] `Observer` 객체와 **`:Subscribe()`/`:WeakSubscribe()` 전역 레지스트리의 소유 모듈** — `EpochMap.luau`와 같은 이유로 `State.luau`에 묻지 않는다(`Effect`/`Gate`/leaf 핸들러가 전부 이 레지스트리를 본다 — **[2026-08-27 `H-144` (b)]** `Effect.luau`는 이 두 테이블과 `canBound`만 공유하고 네 진입점 본문은 자기 것) │ ├── EpochMap.luau # 재사용 가능한 Epoch 부기 객체(`:Update`/`:Refresh`/`:Sync`/`:TrackFrom`) — `State.luau`에 묻지 않고 별도 모듈, `GateNode`/`State`/`Effect`가 전부 씀(`base/state-epoch-plan.md`) │ ├── Store.luau # source 집합체, dot-access로 Source 그대로 반환(**평범한 레코드 필드** — 타입 함수 안 씀). **[2026-08-25]** 생성은 **명시적 초기화**(타입 인자에 `Source` 직접, `defaults`에도 `Source(v)` 직접 — 옛 lazy `__index` 폐기), 동적 키는 `:Of<>(name)` 하나(옛 `GetDynamic` 흡수), `:Names()`, 예약 키 진단용 `CheckReservedKeys>`(**[2026-08-26 `H-112`]** 옛 이름 `CheckReserved`는 `T`를 통째로 받아 실사용 `T`에서 안 돌았다 — `base/store-plan.md`) │ ├── Blocker.luau # 값 기반 emit 지연/합치기(`base/blocker-plan.md`) — 위 `state:Gate`의 `GateNode` 위에 얹히는 **정책**, 바닥부터 짜지 않음 diff --git a/.claude/base/bind-system-plan.md b/.claude/base/bind-system-plan.md index 057bc7e..46ddc8b 100644 --- a/.claude/base/bind-system-plan.md +++ b/.claude/base/bind-system-plan.md @@ -331,7 +331,28 @@ end 배선은 사용자 확정이 아니라 **규칙을 기존 계약에 얹은 제 선택**이다 — `H-142` 처방 후보 (a)/(b)/(c)가 전부 새 메커니즘이라 정하지 않았던 것을 "키 금지"로 바꾸니 필요한 코드가 이 거부 한 줄뿐이다. 다른 모양이 낫다면 - 갈아끼울 것.) 순서 문제는 키가 없어지면서 소멸한다. + 갈아끼울 것.) 순서 문제는 키가 없어지면서 소멸한다. **[2026-08-27 `H-146`]** + 그 거부는 일반 매치 실패 문구(*"no handler … check quad-roblox provider"*)가 + 아니라 **전용 문구**를 낸다 — provider 설정을 의심하게 만들지 않도록 + ("`Parent` is not a prop: the parent attaches its children; a quad root is + parented by the caller" 취지, 정확한 문장은 구현 시). + - **⭐ [2026-08-27 확정, 9라운드 `H-146`] 루트는 이 금지의 범위 밖이다 — + quad 트리의 최상위를 quad 밖 부모에 붙이는 건 사용자가 밖에서 `.Parent =`로 + 한다.** 위 인용문의 *"외부에서 직접 Parent 설정해주지 말것"*이 막는 것은 + **quad가 관리하는 자식 자리**(Slot 요소·정적 자식)에 밖에서 끼우는 것이고 + (`slot-plan.md`의 "동적 자식은 반드시" 절), 루트는 어느 `Length`/형제 순서 + 부기에도 속하지 않아 어긋날 부기가 없다(`gcconn`/`gchold`는 `Destroying` + 기반이라 `Parent`와 무관). **`Mount(root, parent)`류 표면은 만들지 않는다** + — 사용자 확정: *"React 에서도 최상위 경로는 … root 를 돔 api 로 가져오고 + 거기에 바운딩 하는 처리를 해야하거든. … Quad 가 제공하는 마운트는 완전히 + 다른 성격이라서(부기에 대한 처리가 들어가는데 PlayerGUI 등 Quad 가 제공하지 + 않은 부기가 없는 객체에 대해서 Quad 의 객체를 주입하는 성격의 API 는 + 아니거든) 해당 부분을 해결하기 위해서 다른 API 를 제공 할 이유가 없기도 해. + 해당 부분은 각 엔진을 사용하는 최종 사용자의 몫."* 즉 루트 부착은 엔진마다 + 다를 수 있는 최종 사용자 코드고, quad의 마운트(Slot)는 *부기가 있는* 자리에 + 넣는 별개 개념이다. 루트 전용 props 키(`H-142` 취지와 충돌)도 기각. 사용자 + 문서에 "루트는 직접 `.Parent =`, 그 아래는 절대 직접 하지 말 것"을 같이 + 적을 것(`research/documentation-content-map.md` 대상). **이벤트 바인딩 — `On.EventName` 도트액세스 안 씀, PA님 방식(평범한 문자열 키 + 런타임 리플렉션)으로 전환**: `DeclarativeInstance.luau:13-91`의 diff --git a/.claude/base/dispatch-core-plan.md b/.claude/base/dispatch-core-plan.md index b5fb5eb..91a6a55 100644 --- a/.claude/base/dispatch-core-plan.md +++ b/.claude/base/dispatch-core-plan.md @@ -1502,8 +1502,9 @@ Dispatch.getOffsetAt(ownerKey, i): number -- [2026-08-21 5라운드] 그 (`nativeInsert` → `setLength` → `recompute`)과 같게. 옛 서술(`setLength` → `Parent`)은 단건 경로에서 `recompute`가 부착 전에 돌았다. (3) **5번째 인자 `element`는 안 넘긴다** — 상수 길이라 지속 클로저가 없어 Q3 계약상 생략 - 대상이고, 넘기면 `setLength(…, 0)` 해제가 `bk.indexOfElement`의 옛 키를 - 안 지워(해제 호출엔 요소가 없다) 교체마다 옛 자식이 강참조로 쌓인다. 대안 — `Dispatch.drive`가 `type(k) == "number"` 분기에서 일괄 등록 — + 대상이다(**[2026-08-27 `H-145`]** 처음엔 "넘기면 해제가 옛 키를 안 지워 + 강참조로 쌓인다"도 근거였으나 그 맵이 weak-key가 되며 그 근거는 사라졌다 — + 남는 근거는 "지속 클로저가 없어 조회할 일이 없다" 하나). 대안 — `Dispatch.drive`가 `type(k) == "number"` 분기에서 일괄 등록 — 은 아래 *"모든 핸들러가 `k=number`일 때 처리하도록 두는"*에서 **이미 기각된 안**이라 다시 열지 않는다. `ROADMAP.md` M5 체크박스에 같은 두 줄을 적었다. @@ -1548,8 +1549,29 @@ Dispatch.getOffsetAt(ownerKey, i): number -- [2026-08-21 5라운드] 그 같은 파일 안에서 어떤 자리는 가드하고 어떤 자리는 안 하는 불일치를 만든다. **가드를 두지 말 것.** - **`getBookkeeping`이 `bk`를 만들 때 `offsetCache = {}`, `offsetCacheValidUpTo = 0`, - `offsetSetUpTo = 0`, `recomputeBlocker = Blocker()`, **`indexOfElement = {}`** - (**[2026-08-27 Q3]**)으로 초기화한다.** + `offsetSetUpTo = 0`, `recomputeBlocker = Blocker()`, + **`indexOfElement = setmetatable({}, { __mode = "k" })`**(**[2026-08-27 Q3]**, + **[2026-08-27 weak-key 확정, 9라운드 `H-145`]**)으로 초기화한다.** + - **왜 weak-key인가**: 최상위 `SlotHandler` retractor의 해제 `setLength(inst, + k, 0)`엔 요소 인자가 없어 옛 Slot 키가 안 지워지고, `bk`는 `Relate(inst)`라 + 강한 키 맵이면 **`State` 교체마다 옛 Slot(과 그 트리)이 부모가 죽을 + 때까지 산다.** 마운트 중엔 `_elements`/`bindLifetime`이 강하게 잡으므로 + 키가 빠질 일이 없고, 해제 뒤엔 저절로 빠진다 — "다른 곳에서 안전하게 + 유지되는 것은 항상 weak로 잡는다"(`base/lifecycle-pattern.md`) 그대로. 값이 + 정수라 Luau의 ephemeron 미지원(값이 키를 참조하면 안 빠지는 것)도 안 + 걸린다. 사용자 확정(*"컴퓨팅 비용이 더 안들고, 복잡하지 않고 깔끔한 + 경로"*) — 단점으로 짚은 "한 Slot이 포탈로 여러 `bk`에 들락거리면 여러 + 맵에 남을 수 있다"는 Slot 수·포탈 경로 수가 적어 문제 아님, 단 **요소가 + 마운트 중 GC되지 않아야 한다**는 조건 부착. 사용자는 그 보장자로 *"자신을 + gchold 로 잡으니"*를 들었는데 **[2026-08-28 `/code-review` 정정]** gchold는 + `inst → 값` 방향 홀더라 `inst` 자신을 잡지 않는다 — 실제 보장자는 + **`slot._elements`(강한 배열)와 `gatedRecompute`의 요소 캡처**다. 이 둘을 + 약하게 바꾸면 이 맵의 키가 마운트 중 빠져 `bk.indexOfElement[element]`가 + `nil`이 된다 — 그 둘은 이 맵의 불변식을 지탱하는 자리로 표시할 것. + Instance를 `__mode = "k"` 키로 쓰는 경로 자체는 + `audit/gcconn-trick-verification.md`가 **미확인**으로 남겨둔 항목이라 M3 + 구현 시 실측 대상. 해제에 요소를 넘겨 `setLength`가 지우게 하는 안(시그니처 의미 확장)과 + inst 층 명시 삭제 API는 기각. (**[2026-08-26]** `offsetCacheValidUpTo`은 같은 날 `offsetSetUpTo`에서 갈라져 나온 필드다 — 아래 "두 필드" 절.) (`bk.N`만 `nil` 시작을 유지한다 — 그쪽은 `or 0` 방어가 이미 자리를 잡았고 "아직 아무 자리도 등록 diff --git a/.claude/base/effect-plan.md b/.claude/base/effect-plan.md index f2b11bf..6fe467e 100644 --- a/.claude/base/effect-plan.md +++ b/.claude/base/effect-plan.md @@ -332,7 +332,10 @@ end 게 흔한 정상 용례) `_cleanup`이 **항상 `nil`**인 Effect가 존재한다. 그러면 바인드/포탈 재마운트마다 조건이 참이 되어 `fn`이 다시 돌고 — 이 재설계가 없애려던 `H-58`(바인드마다 `Rerun`)이 **그대로 되살아난다.** -`_installed`는 `Rerun`이 끝날 때 참, `_consumeCleanup`에서 거짓이 된다. +`_installed`는 `Rerun`이 끝날 때 참(**[2026-08-27 `H-143`]** 단 그 실행 중에 +핸들이 죽었으면 — `wasAlive and not canExecute` — 세우지 않는다, +`_consumeCleanup`이 걸어둔 거짓이 그대로 남는다), `_consumeCleanup`에서 거짓이 +된다. **⭐ [2026-08-25 신설, 7라운드 `H-60`] `EffectHandle:Rerun()` 정의.** 지금까지 호출부만 다섯 곳이고 정의가 없었다. @@ -347,8 +350,34 @@ function EffectHandle:Rerun() -- 공개 메소드, 무인자 repeat self._pending = false self:_consumeCleanup() - self._cleanup = self.fn(self) - self._installed = true -- cleanup 반환 여부와 무관하게 "설치됨" + local wasAlive = canExecute(self) -- `fn` 진입 전 상태 + local c = self.fn(self) + -- ⭐ [2026-08-27 확정, 9라운드 `H-143`] **이 실행 중에 죽었으면 cleanup만 + -- 한다.** `fn` 안에서 `self:Unsubscribe()`(또는 `WeakUnsubscribe`)가 + -- 통과했으면(강/약 구독 핸들 — leaf 바인딩 핸들은 자기 해제로 죽지 + -- 않으므로 이 분기에 오지 않는다) 이 핸들은 더 이상 어느 레지스트리에도 + -- 없고 leaf도 아니라 **아무도 이 cleanup을 소진할 수 없다** — 저장하지 + -- 말고 즉시 소진한다. + -- 판정은 "살아 있다가 → 죽었다"(`wasAlive and not canExecute`)다. + -- `not canExecute` 하나로 하면 안 된다 — **생성자의 최초 `Rerun()`은 + -- 어떤 바인드·구독보다 먼저** 돌아 `canExecute`가 항상 거짓이라, 첫 + -- 실행의 cleanup을 그 자리에서 소진하고 `_installed`를 거짓으로 남겨 + -- 첫 바인드에서 `fn`이 또 돈다(감사 2라운드가 잡은 회귀 — `H-58`의 + -- 재현). 약한 해제도 같은 경로로 소진된다(죽은 핸들에 매달린 cleanup은 + -- "영원히 안 불림"보다 "한 번 불림"이 계약에 가깝다). 사용자 확정: + -- *"특정 state 에 변경을 딱 한번만 처리하고 cleanup 되는걸 만드는 요구가 + -- 존재하지 않을 이유가 딱히 없다"*, *"실행중 죽으면 클린업만 하기"* — + -- `fn` 안 `Unsubscribe`는 지원 대상이다. + if wasAlive and not canExecute(self) then + if c then c() end -- `_installed`는 이미 `_consumeCleanup`이 거짓으로 + self._pending = false -- 죽은 핸들의 재요청(`fn` 안 `self:Rerun()`)은 버린다 + -- — `fire`가 죽은 핸들의 emit을 버리는 것과 같은 규칙. + -- 안 버리면 아래 `until`이 한 바퀴 더 돌아 죽은 + -- 핸들에서 `fn`이 또 돌고 그 cleanup이 다시 고아가 된다. + else + self._cleanup = c + self._installed = true -- cleanup 반환 여부와 무관하게 "설치됨" + end until not self._pending -- 재요청이 또 오면 또 돈다 self._running = false end @@ -404,7 +433,8 @@ end `fn|Observer`, **강참조**), `_epochs`(`EpochMap` — `Ref`도 `Epoch`라 균일), `_blocker`(등록 구간 억제), `_cleanup`, **`_installed`**(설치 여부 — cleanup 반환이 선택이라 `_cleanup`으로는 판정 못 한다), - `_running`/`_pending`(재진입). + `_running`/`_pending`(재진입), **`.Subscribed`**(공개 플래그 — `canExecute`가 + 읽는 그것, 네 진입점이 세우고 내린다, 아래 "`EffectHandle:Subscribe()`" 절). **옛 `_refDeps`/`_refCallbacks`/`_observers`/`_installing`은 `_deps` 하나와 `_blocker`로 대체됐다.** @@ -497,37 +527,122 @@ quad의 반응형 그래프/cleanup 인체공학만 재사용하는 경우)로 **확정**: `EffectHandle`에도 `:Subscribe()`/`:Unsubscribe()` 추가, 둘 다 `self` 반환(Observer와 동일한 fluent 대칭). -**⭐ [2026-08-27 확정, 9라운드 `H-127`] 의사코드 — 네 진입점은 Observer의 -것을 재사용하고, `Unsubscribe`만 *통과한 뒤* cleanup을 덧붙인다.** 아래 -산문(번호 목록)은 이 블록을 풀어 쓴 것이고, **순서는 이 블록이 정본**이다. +**⭐ [2026-08-27 확정, 9라운드 `H-127` → 같은 날 (b)로 정정] 의사코드 — 네 +진입점은 `EffectHandle` 자기 것**(Observer와 같은 레지스트리·같은 `canBound` +게이트를 쓰되 함수 본문은 공유하지 않는다 — 아래 블록 머리 주석), **`Unsubscribe`는 +게이트를 *통과한 뒤* cleanup을 덧붙인다.** 아래 산문(번호 목록)은 이 블록을 풀어 +쓴 것이고, **순서는 이 블록이 정본**이다. 산문의 번호 순서(플래그 → cleanup → fail-fast)대로 짜면 leaf 바인딩된 핸들에 `:Unsubscribe()`를 불렀을 때 **cleanup을 소진한 뒤에야 error**가 나서 `E-11`이 막으려던 피해(cleanup 앞당김, `_installed = false`)가 이미 일어난 뒤다 — Observer 쪽 의사코드는 가드가 첫 줄이라 이 문제가 없었다. ```lua --- 레지스트리(`Subscribed`/`WeakSubscribed`)·`canBound` 게이트·`.Subscribed` --- 플래그 전부 `base/lifecycle-pattern.md` (2)의 Observer 네 진입점과 **같은 --- 구현**이다 — 메소드 테이블에 그대로 배정한다. 등록되는 건 **핸들 자신뿐** --- (내부 Observer·`Ref` 콜백은 생성자에서 이미 `Weak*`로 걸려 있다, `H-59`). -EffectHandle.Subscribe = Observer.Subscribe -- canBound 게이트 + 강한 킵 -EffectHandle.WeakSubscribe = Observer.WeakSubscribe -- 게이트 + 약한 등록 + 플래그 -EffectHandle.WeakUnsubscribe = Observer.WeakUnsubscribe -- 관대(`H-133`) — cleanup 안 건드림 +-- ⭐⭐ [2026-08-27 확정 (b), 9라운드 `H-144` 후속 — 감사 4라운드] **`EffectHandle`은 +-- 네 진입점을 자기 것으로 가진다 — Observer의 함수 본문을 배정하지 않는다.** +-- 공유하는 건 **레지스트리 두 개**(`Subscribed`/`WeakSubscribed`, 소유 모듈은 +-- `Observer.luau` — `H-99`)와 **`canBound` 게이트**(`LifetimeHandle.luau`의 +-- 탑레벨 함수)뿐이고, `.Subscribed` 플래그의 뜻도 같다. 여기 한때(Q4/`H-127`) +-- `EffectHandle.Subscribe = Observer.Subscribe`처럼 **함수 객체를 그대로 배정**해 +-- 뒀는데, `Observer:Subscribe`의 본문이 `self:WeakSubscribe()`로 **콜론 위임**하는 +-- 탓에 `self`가 `EffectHandle`이면 그 조회가 `EffectHandle`의 오버라이드로 가서 +-- 재구독 꼬리가 두 번 돌고, 첫 번째는 강한 킵이 서기 **전에** `Rerun`해 `fn` 안 +-- `self:Unsubscribe()`(`H-143`이 지원하는 패턴)가 *"not subscribed strongly"*로 +-- error했다(로컬 `luau`로 재현). **사용자 확정**: *"b가 맞아. 내 머리에서 나왔던 +-- 처음 구조는 그것이였어. … '하나의 무언가가 두 일을 동작하지 않는가에 +-- 유의하자' — 이것도 마찬가지야. 버그를 유발하기 좋은 포인트였고"* — +-- Observer와 Effect는 이질적 타입이라(생성 방법부터 다르다) 본문을 섞지 않는다 +-- (`conventions.md`의 "설계 원칙" 절에 원칙으로 승격). 등록되는 건 **핸들 +-- 자신뿐**(내부 Observer·`Ref` 콜백은 생성자에서 이미 `Weak*`로 걸려 있다, +-- `H-59`). 레지스트리 두 테이블을 `Effect.luau`가 어떻게 받는지(모듈 내부 +-- export, 공개 API 아님)는 구현 세부 — 이름은 구현 시. + +-- ⭐ [2026-08-27 확정, 9라운드 `H-144`] 구독 둘은 등록 뒤 leaf 재바인드 +-- (`_bindDestroying`, `H-65`)와 **정확히 같은 꼬리**를 붙인다. `Unsubscribe`로 +-- cleanup을 소진한 핸들을 다시 `Subscribe`하면 `.Subscribed = true`만 서고 `fn`이 +-- 안 돌아, deps 없는 Effect는 레지스트리가 살려두는 죽은 핸들(누수)이 되고 +-- deps 있는 것은 다음 emit까지 죽어 있었다. `Refresh()`를 **먼저** 부르는 이유는 +-- 287행 캐비엇과 같다 — 해제 중 도착한 emit은 `fire`의 `canExecute` 가드가 +-- `_epochs:Update` 앞에서 버리므로 `_epochs`가 해제 전 리비전에 멈춰 있고, 안 +-- 맞추면 재구독 뒤 첫 emit(예: 게이트 유보가 풀리며 오는 배치)에 같은 값으로 +-- `fn`이 한 번 더 돈다. 사용자 확정: *"초기에 epoch 를 전부 잘 설정해주는게 +-- 나을수도 … 처음 연산 기준에 있어서도 전부 값은 최신 값이거든. 따라서 다음 +-- emit 을 받아야할 이유가 없을수도 있어."* Blocker는 안 쓴다 — dep을 다시 +-- 등록하지 않으므로(생성자에서 한 번, `_deps` 강참조 유지) 억제할 발화가 없다. +local function resubscribeTail(self) + local depsChanged = self._epochs:Refresh() + if not self._installed or depsChanged then + self:Rerun() + end +end + +-- 꼬리는 항상 **등록이 전부 끝난 뒤** 한 번 — `Subscribe`는 `WeakSubscribe`를 +-- 부르지 않고 등록 세 줄을 자기 안에 펼쳐 쓴다. 위임하면(콜론이든 dot이든) +-- 꼬리가 강한 킵 **앞**에서 돌거나 두 번 돈다 — 감사 4라운드가 잡은 바로 그 +-- 모양이다. 게이트·메시지 분기는 Observer의 것과 같다(`lifecycle-pattern.md` (2)). +function EffectHandle:WeakSubscribe() + if not canBound(self) then + error(if self.Subscribed then "already subscribed" else "already bound to an Instance", 2) + end + self.Subscribed = true + WeakSubscribed[self] = true + resubscribeTail(self) + return self +end + +function EffectHandle:Subscribe() + if not canBound(self) then + error(if self.Subscribed then "already subscribed" else "already bound to an Instance", 2) + end + self.Subscribed = true + WeakSubscribed[self] = true + Subscribed[self] = true -- 강한 킵이 선 **뒤에** 꼬리 — `Rerun` 안의 + resubscribeTail(self) -- `fn`이 `self:Unsubscribe()`해도 가드 통과 + return self +end + +function EffectHandle:WeakUnsubscribe() -- 관대(`H-133`) — cleanup 안 건드림 + if Subscribed[self] ~= nil then + error("subscribed strongly; use :Unsubscribe()", 2) + end + WeakSubscribed[self] = nil + self.Subscribed = false + return self +end function EffectHandle:Unsubscribe() - Observer.Unsubscribe(self) -- ⭐ 게이트가 **먼저** — 강하게 구독된 적 없으면(leaf - -- 바인딩·약한 구독·미구독) 여기서 error, cleanup엔 - -- 손도 안 댄다(`E-11`). 통과하면 강한 킵 해제 + - -- `.Subscribed = false`(향후 재실행 차단). - self:_consumeCleanup() -- 통과했을 때만: 직전 cleanup 정확히 1회, `_installed = false` + if Subscribed[self] == nil then -- ⭐ 게이트가 **먼저** — 강하게 구독된 적 없으면 + error("not subscribed strongly; use :WeakUnsubscribe()", 2) -- (leaf 바인딩·약한 + end -- 구독·미구독) 여기서 error, cleanup엔 손도 + Subscribed[self] = nil -- 안 댄다(`E-11`). + WeakSubscribed[self] = nil + self.Subscribed = false -- 향후 재실행 차단 + self:_consumeCleanup() -- 통과했을 때만: 직전 cleanup 정확히 1회, `_installed = false` return self end ``` -- **`WeakUnsubscribe`는 cleanup을 소진하지 않는다** — 약한 구독은 "GC에 - 맡기는" 경로라 해제가 곧 종료 신호가 아니다. 종료 신호는 강한 구독의 - `Unsubscribe`와 leaf 사망(`unbindLifetime`의 훅) 둘뿐. -- 실측(9라운드 `core9.luau`, `t12` 매트릭스): 이 배정으로 leaf 바인딩된 +- **`WeakUnsubscribe` 자체는 cleanup을 소진하지 않는다** — 약한 구독은 "GC에 + 맡기는" 경로라 해제가 곧 종료 신호가 아니다. 종료 신호는 **[2026-08-27 `H-143` + 으로 셋]** — 강한 구독의 `Unsubscribe`, leaf 사망(`unbindLifetime`의 훅), 그리고 + **`fn`이 실행 중에 자기 핸들을 죽인 경우**(`fn` 안 `self:Unsubscribe()` / + `self:WeakUnsubscribe()` — `WeakUnsubscribe` 본문은 여전히 cleanup에 손대지 + 않지만, 그걸 감싼 `Rerun`의 죽음 판정 `wasAlive and not canExecute`가 그 + 실행이 돌려준 cleanup을 즉시 소진한다, 위 `Rerun` 의사코드). 여기 한때 + "둘뿐"이라 적혀 있었다(감사 7라운드 정정). +- **`fn` 안에서 허용되는 핸들 호출은 `self:Rerun()`과 자기 해제다 — 단 자기 + 해제는 *건 경로로*: 강구독 핸들은 `self:Unsubscribe()`, 약구독 핸들은 + `self:WeakUnsubscribe()`. leaf 바인딩된 핸들엔 자기 해제 경로가 없다**(종료는 + leaf 사망뿐 — `Unsubscribe()`는 가드에서 error하고 그 error는 `Rerun`을 뚫고 + 나가 `_running`이 참으로 남는 UB, `WeakUnsubscribe()`는 관대 통과하지만 + `isBoundAlive`가 그대로라 죽지 않는다; **[2026-08-28 `/code-review`]**). `fn` 안 + 재구독(`self:Subscribe()`/ + `self:WeakSubscribe()`)은 **[2026-08-27 기준] 서술하지 않는다**(트레이싱상 + `resubscribeTail`의 `Rerun`이 `_running` 재진입으로 `_pending`만 세워 크래시는 + 안 하지만 지원 목록이 아니다 — 필요가 관측되면 그때 정한다). +- 실측(9라운드 `core9.luau`, `t12` 매트릭스 — **당시 함수 배정 형태에서의 실측**이고 + (b) 재작성 뒤 재실행하지는 않았다[2026-08-27 기준]; 게이트 순서는 그대로라 + 같은 결과가 나올 것으로 추정할 뿐, 확정 근거는 M2 구현 테스트가 될 것): leaf 바인딩된 핸들의 `:Unsubscribe()`가 cleanup을 건드리지 않고 error, `:Subscribe()` 후 `:Unsubscribe()`는 cleanup 1회 — 전부 기대대로. @@ -542,10 +657,15 @@ end 등록하면 (a) `handle.Subscribed`가 안 세워져 `canExecute(handle)`이 영원히 거짓이고, (b) deps 없는 `Effect(fn):Subscribe()`는 등록할 게 아예 없어 핸들이 GC되고 cleanup이 유실된다. - - **`:Subscribe()`가 하는 일은 그것 하나뿐이다** — 내부 Observer와 `Ref` + - **`:Subscribe()`가 등록하는 것은 그것 하나뿐이다** — 내부 Observer와 `Ref` 콜백은 **생성자에서 이미 `Weak*`로 걸려 있다**(위 "확정 구조" 절). `Subscribed = true`가 서는 순간 `canExecute(handle)`이 참이 되어 그 - 경로들이 살아난다. + 경로들이 살아난다. **[2026-08-27 `H-144`]** 등록 뒤 꼬리로 `_epochs:Refresh()` + + 조건부 `Rerun`이 붙는다(위 의사코드) — **그 사이 dep이 안 변했으면** + no-op이다. 첫 구독이라도 생성과 `Subscribe()` 사이에 emit이 있었으면 + (`.Subscribed`가 거짓이라 `fire`가 첫 가드에서 버린다) `Refresh()`가 참을 + 돌려 캐치업으로 `fn`이 다시 돈다 — 바람직한 동작이고, "첫 구독은 재실행 + 안 한다"를 테스트에 인코딩하지 말 것(**[2026-08-28 `/code-review`]**). - **⚠️ 용도는 완전히 top-level(모듈/스크립트 레벨, 어떤 Instance 생명주기에도 안 묶인) 사이드 이펙트로 한정할 것 — 특정 `inst`에 묶인 경우엔 leaf 부착(`bindLifetime`)을 쓰지 `:Subscribe()`를 쓰지 @@ -606,8 +726,8 @@ end 이 요구는 성립하지 않는다 — **그대로 구현하면 `nil`을 순회한다.** - **[2026-08-21 기준]** 남은 건 **구현 시 회귀 확인**뿐이고, 설계상 열린 항목이 아니다. -- **`:Subscribe()`한 핸들에서는 `:Unsubscribe()`가 Observer의 것을 위임한 뒤 - cleanup 하나를 덧붙인다 — Effect 계층에서 의미가 확장됨.** Observer의 +- **`:Subscribe()`한 핸들에서는 `:Unsubscribe()`가 Observer와 같은 게이트·레지스트리 + 조작을 한 뒤 cleanup 하나를 덧붙인다 — Effect 계층에서 의미가 확장됨.** Observer의 `:Unsubscribe()`는 "미래 재실행만 끊는다"(Observer 자체엔 정리할 상태가 없음)로 충분하지만, Effect의 계약은 "생애주기가 끝나는 시점에 마지막 cleanup이 정확히 1회 호출된다" @@ -705,10 +825,12 @@ Effect의 의존성이 될 방법이 아예 없다.** 사용자 제기: *"Effect 인자마다 다른 규칙을 위치로 기억해야 했다. 아무것도 안 넘기므로 그 질문 자체가 없어지고, dep 값은 사용자가 클로저로 직접 읽는다. - `self`를 주는 덕에 `fn` 안에서 `self:Rerun()`/`self:Unsubscribe()` 같은 - 핸들 표면에 바로 닿는다. **⚠️ [2026-08-27 `/code-review high`, 판단 대기 - `H-143`]** `fn` 안 `Unsubscribe`는 그 실행이 돌려준 cleanup을 `Rerun`이 - 그대로 저장해 **아무도 소진 못 하게** 만든다 — 처방(`Rerun` 꼬리 분기 / - 금지 / 문서화)은 `question.md` 최우선 절. + 핸들 표면에 바로 닿는다. **[2026-08-27 확정, 9라운드 `H-143`]** `fn` 안 + `Unsubscribe`는 **지원 대상**이다("이 dep의 변경을 딱 한 번만 처리하고 + 끝나는 Effect") — 그 실행이 돌려준 cleanup을 `Rerun`이 그대로 저장하면 + 아무도 소진 못 하므로, `Rerun` 꼬리가 "이 실행 중에 죽었다"(`wasAlive and + not canExecute`)를 보면 **즉시 소진**하고 재요청도 버린다(위 `Rerun` + 의사코드). - **최소 1회는 실행된다 — React `useEffect`와 동일.** 아직 안 채워진 `Ref`가 섞여 있어도 그대로 돈다(사용자: *"최초 1회에서 어차피 if 로 확인해내게 될것이므로 괜찮음"*). "전부 채워질 때까지 대기"는 안 한다. diff --git a/.claude/base/lifecycle-pattern.md b/.claude/base/lifecycle-pattern.md index e2bc2a7..acdee20 100644 --- a/.claude/base/lifecycle-pattern.md +++ b/.claude/base/lifecycle-pattern.md @@ -354,8 +354,8 @@ function bindLifetime(inst, value) -- `base/effect-plan.md`의 "확정 구조" 절이 소스. if isEffect(value) then value:_bindDestroying(inst) -- Destroying 연결 + **조건부 캐치업 1회** - -- (`if not self._installed or - -- self._epochs:Refresh() then self:Rerun() end`) + -- (`local depsChanged = self._epochs:Refresh()` 먼저, + -- `if not self._installed or depsChanged then self:Rerun() end`) -- 의사코드는 `base/effect-plan.md`가 소스. -- 그 안에서 주입 op `onDestroying(inst, fn)`을 -- 부른다(base는 Instance를 모른다). @@ -444,10 +444,17 @@ end `:WeakSubscribe()`가 생기기 전(2026-08-25 이전) 서술이라 **약한 쪽이 어디서 `.Subscribed`를 세우는지가 통째로 빠져 있었다.** 아래가 **Observer의** 네 진입점 전량이고 소스다(사용자 원문 *"구현이 한 벌"*). **[2026-08-27 9라운드 -`H-127`]** `EffectHandle`도 같은 넷을 **그대로 재사용**한다(같은 레지스트리, -같은 게이트) — `Unsubscribe`만 이 게이트를 통과한 *뒤에* cleanup 소진을 -덧붙이며, 그 의사코드는 `base/effect-plan.md`의 "`EffectHandle:Subscribe()`" -절이 소스: +`H-127`, 같은 날 (b)로 정정]** `EffectHandle`은 **같은 레지스트리 둘과 같은 +`canBound` 게이트를 쓰되 네 진입점 본문은 자기 것**이다 — 한때 "같은 넷을 +그대로 재사용(함수 배정)"으로 적었는데, 아래 `Subscribe`/`Unsubscribe`가 +`self:WeakSubscribe()`/`self:WeakUnsubscribe()`로 **콜론 위임**하므로 그 함수를 +`EffectHandle`에 배정하면 위임이 `EffectHandle`의 오버라이드로 가서(재구독 꼬리 +두 번, 첫 번째는 강한 킵 전) 깨진다(감사 4라운드, `luau` 재현). **사용자 +확정**: *"observer 랑 effect 랑 헤테로지니어스한 타입인데 … '하나의 무언가가 두 +일을 동작하지 않는가에 유의하자'"* — Observer 본문은 Observer만 쓴다. `Effect` +쪽 넷(`Unsubscribe`는 cleanup 소진, `Subscribe`/`WeakSubscribe`는 +**[2026-08-27 `H-144`]** 등록 끝에 `_epochs:Refresh()` + 조건부 `Rerun`)은 +`base/effect-plan.md`의 "`EffectHandle:Subscribe()`" 절이 소스: ```lua local Subscribed = {} -- 강한 레지스트리(살려두는 게 목적) @@ -458,7 +465,8 @@ function Observer:WeakSubscribe() if not canBound(self) then -- bindLifetime과 정확히 같은 게이트(같은 isBoundAlive 공유) error(if self.Subscribed then "이미 구독된 값" -- 강/약 어느 쪽이든 이 분기 - else "이미 Instance에 바인딩된 값") + else "이미 Instance에 바인딩된 값", 2) -- [2026-08-27] `level 2` — 아래 둘과 같게 + -- (자리표시자 문구, 실제 메시지는 영어) end self.Subscribed = true -- ⭐ [H-111] 약한 쪽도 세운다 — 구독 경로 공용 플래그 WeakSubscribed[self] = true diff --git a/.claude/base/ref-plan.md b/.claude/base/ref-plan.md index e97717e..da6ab31 100644 --- a/.claude/base/ref-plan.md +++ b/.claude/base/ref-plan.md @@ -440,8 +440,9 @@ Instance를 직접 받으므로 — `base/dispatch-core-plan.md` "확정된 디 `Source`/`Ref`는 `:Sync`, `State`는 `:TrackFrom`, `base/state-epoch-plan.md` §4·§8. 균일해지는 건 **판정 쪽**이다). 그래서 포탈 재마운트의 캐치업이 dep 종류에 따라 갈리던 것(`H-64`)이 - **대칭**이 되고, 판정이 `if self._epochs:Refresh() then self:Rerun() end` - 한 줄이 된다. + **대칭**이 되고, 판정이 `local depsChanged = self._epochs:Refresh()` + + `if not self._installed or depsChanged then self:Rerun() end` 두 줄이 된다 + (`Refresh()`는 항상 먼저 — `base/effect-plan.md`의 `_bindDestroying` 캐비엇). - 같은 `Ref`를 deps에 두 번 넣어도 `EpochMap`이 키로 dedup하므로 **공짜로** 처리된다(`H-70`) — 옛 `_refCallbacks[ref] = cb` 덮어쓰기로 먼저 건 클로저가 `.Callbacks`에 남던 버그도 같이 사라진다. diff --git a/.claude/base/slot-plan.md b/.claude/base/slot-plan.md index 01a4df1..1fe2cd7 100644 --- a/.claude/base/slot-plan.md +++ b/.claude/base/slot-plan.md @@ -232,8 +232,9 @@ Slot에 들어간 요소는 **ownership이 귀속**되며 다른 곳에 마운 `isMounted`를 관리해서, **한 인스턴스에 대한 다중 마운팅이 라이브러리 차원에서 절대 일어나지 않도록 강제**하는 게 v1 대비 핵심 디자인 변화. v1의 `mount()`는 별다른 강제를 안 했지만(`reference/quad-v1-architecture.md`의 mount.lua 분석 참고 — -실제로는 부모/자식 부기까지 했지만 다중 마운트 방지는 없었음), v2는 mount -함수 자체가 이 강제를 담당. +실제로는 부모/자식 부기까지 했지만 다중 마운트 방지는 없었음), v2는 **Slot의 +마운트 경로 자체**(`attachSlot` 분해분 — 별도 `Mount` 함수가 아니다, **[2026-08-27 +`H-146`]** v2에 v1식 `Mount(parent, tree)` 표면은 없다)가 이 강제를 담당. Fusion의 `Children` SpecialKey는 이걸 "특정 SpecialKey 하나의 내부 부기"로만 구현했고(재사용 가능한 1급 프리미티브가 아님), Vide는 아예 이 개념이 없어서 @@ -2143,7 +2144,10 @@ UI에 직접 관측, (2) `Dispatch.setLength(inst, i, slot.Length)`가 형제 정확히 호출하는 유일한 정당 경로라, 이걸 우회해서(예: 외부 코드가 Slot이 마운트해둔 부모 Instance에 직접 `.Parent = parentInst`로 자식을 끼워 넣는 것) 자식을 추가/제거하면 `Length`/형제 순서 계산이 그 변화를 몰라 -조용히 어긋남 — 별도 방어 로직 없음, 문서 경고로만 남김. +조용히 어긋남 — 별도 방어 로직 없음, 문서 경고로만 남김. **[2026-08-27 확정, +9라운드 `H-146`] 이 금지의 범위는 *quad가 관리하는 자식 자리*다 — quad 트리의 +루트를 quad 밖 부모(`PlayerGui` 등)에 붙이는 `.Parent =`는 여기 해당하지 않고 +사용자가 밖에서 한다**(`base/bind-system-plan.md`의 `H-142` 항목). ## `Slot:Single(state, updateFn?, opts?)` — 확정 (2026-08-11 세션, `:List` 위의 순수 sugar) diff --git a/.claude/base/source-state-plan.md b/.claude/base/source-state-plan.md index 49f4c10..78a643d 100644 --- a/.claude/base/source-state-plan.md +++ b/.claude/base/source-state-plan.md @@ -1570,9 +1570,12 @@ Observer가 **안 묶인 것으로 오판**됨"은 실제로는 안 일어남 `unbindLifetime`이 leaf 해제의 실제 통로). - **Effect도 동일 규칙 적용(사용자 확인)** — Effect가 `state` 인자로 내부적으로 Observer를 조합하는 경우든, `state` 없는 경우든 같은 - `canBound` 게이트를 그대로 재사용(`base/effect-plan.md`) — Effect - 자신이 아니라 내부 Observer가 게이트를 갖고 있어서, Effect 구현이 - 이 정정을 몰라도 자동으로 커버됨. 이전에 그 문서에 적어뒀던 "leaf + `canBound` 게이트를 그대로 재사용(`base/effect-plan.md`) — **[2026-08-28 + 정정]** 여기 한때 "Effect 자신이 아니라 내부 Observer가 게이트를 갖고 있어서 + 자동으로 커버됨"이라 적혀 있었는데 틀렸다: 내부 Observer는 생성자에서 + `Weak*`로만 걸리고 발화는 `canExecute(handle)`이 막으므로(`H-58`/`H-59`) 그 + 게이트는 핸들의 이중 바인드를 막지 못한다. `EffectHandle`이 **자기 네 + 진입점에서 `canBound(self)`를 직접 돈다**(`H-144` (b)). 이전에 그 문서에 적어뒀던 "leaf 부착과 `:Subscribe()`를 동시에 쓰는 것도 안전"이라는 서술은 **이 규칙으로 대체(정정)** — 안전하게 지원하는 게 아니라 애초에 막아야 하는 조합이었음. diff --git a/.claude/conventions.md b/.claude/conventions.md index 08a6d6c..3257a05 100644 --- a/.claude/conventions.md +++ b/.claude/conventions.md @@ -29,6 +29,26 @@ haiku, 일반 작업은 sonnet. 특히 소스코드를 많이 읽어야 하는 확인됨. 사용자가 세션 중 구두로 말한 게 옮겨적히지 않았을 가능성이 크다는 사용자 본인 추정에 따라 여기 정식 관례로 승격(선택지 (a) 채택). 이 누락이 아래 "사용자 발언을 인용할 때" 관례가 생긴 계기이기도 함. +- **⭐ [2026-08-27 명문화] 하나의 무언가가 두 일을 하고 있지 않은지 유의한다 — + 이질적인 타입끼리 구현 본문을 공유하지 않는다.** 사용자 원문: *"항상 + 언급되는 말이지만, '하나의 무언가가 두 일을 동작하지 않는가에 유의하자'"* + (`session/2026-08-27-03-handtrace-round9-h143-h146.md`). **첫 사례는 하루 + 전의 `invalidAfter`다** — 사용자 확인(2026-08-27): *"그게 정확히 invalid after + 문제에서 났던 이야기였어"*. 한 필드가 "캐시가 여기까지 유효"와 "여기까지 + `:Set`을 마쳤다"라는 **다른 목적의 값을 같이 쥐고** 있어서 `getOffsetAt`의 + 꼬리가 splice의 되감기 신호를 지웠고(사용자 진단 *"Set을 해줬느냐와 캐시가 + 유효하지 않느냐라는 다른 목적의 값을 같은 값이 쥐고 있음"*), + `offsetCacheValidUpTo`/`offsetSetUpTo` 둘로 갈라졌다(`base/dispatch-core-plan.md`의 + "두 필드" 절, `session/2026-08-26-01-handtrace-round8-resolution.md`가 *"두 + 뜻이 실제로 같다는 확정은 의심할 것"*으로 남긴 교훈). 이번 계기: `Observer`의 + 네 진입점 함수 객체를 `EffectHandle` 메소드 테이블에 그대로 배정해 두 타입이 + 한 본문을 쓰게 했더니, 그 본문의 콜론 위임(`self:WeakSubscribe()`)이 + `EffectHandle`의 오버라이드로 가서 재구독 꼬리가 두 번 돌고 강한 킵 전에 + `Rerun`하는 결함이 났다(감사 4라운드, `base/effect-plan.md`의 + `EffectHandle` 블록). 공유해도 되는 건 **데이터**(레지스트리)와 **순수 + 술어**(`canBound`)이고, 타입별로 다른 후처리가 붙는 **본문**은 각자 가진다. + 위 첫 원칙과 짝 — "구조를 늘리지 않는다"가 "본문을 억지로 합친다"로 + 읽히면 안 된다. ## 문서 표기 규약 diff --git a/.claude/project-context.md b/.claude/project-context.md index 20f1a2e..fe57d1b 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`)이 `question.md` 최우선 절에 판단 대기**(M2 `Effect` 둘·M3·M5 하나씩). 게이트는 여전히 0. 저장소 루트에 +반영됐고**, 같은 날 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)로 확정·반영**돼 그 반영분의 `/code-review`가 낸 셋(`H-147`~`H-149`)은 **[2026-08-28] 10라운드 문항지**(`-round10.md`, 광범위 탐사 결과와 함께 배치 회신 대기)로. 게이트 0. 저장소 루트에 `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-brief.md b/.claude/qa-request/pre-implementation-handtrace-round10-brief.md new file mode 100644 index 0000000..943deee --- /dev/null +++ b/.claude/qa-request/pre-implementation-handtrace-round10-brief.md @@ -0,0 +1,169 @@ +# 구현 전 손 트레이싱 **10라운드** 감사 지시서 + +> **이 파일이 무엇인가**: 10라운드 탐사자에게 그대로 주는 지시서다. 산출물은 +> `pre-implementation-handtrace-round10.md`(§6). **[2026-08-28 신설]** 9라운드 +> 지시서(`-round9-brief.md`)와 같은 관례. +> +> **왜 10라운드인가**: 9라운드 후속(`H-143`~`H-146`, 2026-08-27)을 반영하고 +> 감사 8라운드 + `/code-review high`를 돌렸더니 **판단이 필요한 것 셋**이 또 +> 나왔다(`-round10.md` §4의 `H-147`~`H-149`). 사용자 판단(2026-08-28): *"인간을 +> 기다리는거 엄청 비효율이라서 … 늘 해오던것 처럼 batch 로 처리될 필요가 있는듯. +> 지금 결정해야할 분 10 으로 올리자. 커밋하고 정리한 다음 깔끔한 컨텍스트의 +> fable 탐사자 하나 띄워서 광범위한 핸드트레이싱, 테스팅, 문제 검사를 +> 하도록 해줘."* — 즉 이 라운드는 **한 번에 넓게 훑어 사용자가 한 자리에서 +> 전부 결정할 문항지**를 만드는 것이 목적이다. 발견을 하나씩 물으러 오지 +> 말 것. + +당신은 Roblox 엔진용 DOMless UI 렌더러 **quad**의 설계 코퍼스를 감사한다. +저장소 루트가 작업 디렉토리다. **구현(M2 = 반응형 코어) 착수 직전**이고, +여기서 놓친 설계 결함은 구현 한참 뒤에 터져 M2/M3를 다시 짜는 비용이 된다. +당신은 신선한 컨텍스트에서 시작한다 — 앞선 세션의 가정을 물려받지 않는 것이 +당신의 가치다. + +--- + +## §0 먼저 읽을 것 (이 순서로) + +1. `CLAUDE.md` → `.claude/conventions.md` / `.claude/project-context.md` / + `.claude/todos.md`. **특히 `conventions.md`의 두 규칙**: (1) "리뷰·감사가 + 내놓는 새 필드·인자·이름·메커니즘은 발견이지 결정이 아니다"(2026-08-27), + (2) "하나의 무언가가 두 일을 하고 있지 않은지 유의한다"(2026-08-27). +2. `.claude/qa-request/pre-implementation-handtrace-round9.md` — 9라운드 발견 + 원문(`H-124`~`H-146`). §5(이상 없음)·§6(남은 의심)은 다시 파지 않아도 되는 + 자리와 파야 할 자리다. +3. `.claude/qa-request/pre-implementation-handtrace-round9-followup.md` — + **이 라운드의 출발점.** 9라운드 결정 전량과, `H-143`~`H-146` 반영 뒤의 감사 + 8라운드 표·`/code-review high` 기록. 거기 적힌 **실패 모드**(반영분 자체가 + 결함을 만든다 / 판정식을 단순화하다 생성자 케이스를 죽인다 / 함수 본문 + 공유 + 콜론 위임)가 당신의 사냥 목록이다(§3). +4. `.claude/qa-request/pre-implementation-handtrace-round10.md` — **이미 `H-147` + ~`H-149`가 들어 있다.** 당신의 발견은 `H-150`부터 이어 매긴다. 그 셋을 + 재검토해도 좋다(전제가 틀렸으면 그렇게 적을 것). +5. `ROADMAP.md`(M2/M3 체크리스트), `.claude/question.md`, + `.claude/luau-test/STATUS.md`, `.claude/audit/handtrace-round7-reference-impl/README.md`. +6. `base/`는 **전량 완독**을 요구한다 — 이 라운드는 "델타"가 아니라 "광범위"가 + 전제다(사용자 요청). 먼저 `base/architecture.md`. + +--- + +## §1 이 라운드의 전제 + +- **스코프는 광범위다.** 9라운드까지의 델타 스코프와 달리 M2~M8 전 표면을 본다. + 다만 우선순위는 §2대로 — M2/M3가 먼저다. +- **한 번에 문항지를 만든다.** 사용자는 다음 세션에 이 파일 하나를 열어 §4의 + 표를 위에서 아래로 읽고 갈래만 골라 회신한다. 그러므로 모든 발견은 + **갈래 + 권고 + 권고 근거**를 갖춰야 하고, 판단이 필요 없는 것(오타·stale· + 개수)은 🟢로 분리해 §4에 넣지 않는다. +- **새 메커니즘은 승인이 아니다.** 당신이 처방으로 제안하는 필드·인자·이름· + 표면은 전부 "권고"다. `base/`에 이미 기각된 모양인지(`archive/`, 각 문서의 + "검토 후 안 만들기로 한 것"류 목록) 먼저 grep하고, 기각된 것을 다시 제안할 + 땐 그 사실을 적을 것. +- **실측이 가능한 주장은 실측한다.** 로컬에 `luau`/`luau-analyze`가 있다. + 테스트는 반드시 `./scripts/test.sh`로(`luau` CLI가 심볼릭 링크를 못 타서 + 그냥 돌리면 거짓 클린이 난다 — `project-context.md`). 참조 구현 + `audit/handtrace-round7-reference-impl/spikes/`는 **7라운드 시점 계약**이라 + 지금 계약(`H-107`~`H-149`)과 다르다 — 갱신해 돌리는 것이 레인 C다. + +--- + +## §2 레인 — 우선순위 A > C > B > D + +### 레인 A (최우선) — M2 반응형 코어 전체를 지금 계약으로 손 트레이싱 + +`base/source-state-plan.md` / `store-plan.md` / `state-epoch-plan.md` / +`effect-plan.md` / `gate-plan.md` / `blocker-plan.md` / `lifecycle-pattern.md` / +`ref-plan.md`(최소형) / `relate-plan.md` / `epoch-brand` 관련. 특히 **2026-08-27에 +바뀐 것끼리 겹쳐서**: +- `EffectHandle` 네 진입점 자기 것(`H-144` (b)) × `Rerun` 꼬리 `wasAlive and + not canExecute`(`H-143`) × `_bindDestroying` 캐치업 × 생성자 `_blocker` × + `fire` 가드 순서. 상태 기계 전체를 표로 그려 **모든 진입점 × 모든 상태**에서 + `_installed`/`_cleanup`/`.Subscribed`/레지스트리 전이를 확인. `H-147`의 + "이미 죽은 핸들에서 `Rerun`"도 그 표 안에서 다시 보라. +- `state:Gate` 유보 × Effect 재구독 × `EpochMap:Refresh` — `H-144` 재트레이싱이 + 한 케이스만 봤다. 다이아몬드·게이트 배치·`Peek`/`Update` 조합을 넓혀라. +- Store 명시적 초기화 × `Of<>` × `CheckReservedKeys>` — 타입을 + `luau-analyze`에 실제로 걸어라(레인 D와 겹침). +- `Source:Set` 동일값 emit(`H-68`) × `Blocker` × `_hold` 불변식 × 중간 State GC. + +### 레인 C — 참조 구현을 지금 계약으로 갱신해 실제로 돌리기 + +`audit/handtrace-round7-reference-impl/spikes/`를 복사해 +`audit/handtrace-round10-reference-impl/`를 만들고(원본은 건드리지 말 것 — +실측 시점 기록), `H-107`~`H-149`의 계약으로 갱신한 뒤 `luau`로 돌린다. +최소 목표: Effect 네 진입점 + `Rerun` 꼬리 + 재구독 + 게이트 유보 케이스, +`bk.indexOfElement` weak-key(Instance 키를 흉내낼 수 없으면 테이블 키로 — +그 한계를 적을 것), `reconcile` 배치 Blocker, `recompute` 되감기(`H-124`). +문서와 다르게 도는 자리가 발견이다. `README.md`에 무엇을 어떻게 갱신했는지 +남길 것. + +### 레인 B — M3 디스패치 / Slot 값 단위 트레이싱 + +`base/dispatch-core-plan.md` / `slot-plan.md` / `bind-system-plan.md`(`New`· +`drive` 파이프라인) / `handler-*`. 9라운드 레인 B의 연장 — `InstanceChildHandler` +부기, 숏핸드 우선순위, `H-142` `Parent` 금지와 `H-146` 루트 예외, `H-148`의 +에러 문구 자리, 최상위 Slot 교체 × weak-key, 포탈/`Detach`/`KeyGone` 조합. + +### 레인 D — 타입 계약과 실제 커밋된 M1 코드 + +`quad-base/src/`·`quad-types/src/`·`type-version-check/src/`를 `./scripts/test.sh`로 +돌리고, `base/typing-limits.md`·`quad-types-plan.md`·`project-setup-plan.md`가 +주장하는 것 중 실측 가능한 것을 `luau-analyze`로 확인. 확정 시그니처가 확정 +관용구를 통과시키는가(7라운드 5차 패스의 재현, 지금 계약으로). + +--- + +## §3 사냥 목록 — 이 코퍼스에서 **반복 관측된** 실패 모드 + +1. **반영분이 새 결함을 만든다** — 어제 하루에만 둘(`not canExecute` 판정식이 + 생성자 케이스를 죽임 / 함수 본문 공유 + 콜론 위임). 2026-08-27 diff + (`git log -p 7f58683..HEAD`)를 특히 의심하라. +2. **"두 뜻이 같다"는 확정** — `invalidAfter` 두 필드 분리의 교훈. 한 필드·한 + 함수·한 플래그가 두 목적을 쥐고 있는 자리. +3. **판정식 단순화가 경계 케이스(생성자·최초 설치·빈 배열·0번째 자리)를 + 죽인다.** +4. **의사코드 산문의 순서가 코드 순서와 다르다**(`H-127`형). +5. **인용문 옆에 인용이 승인하지 않은 메커니즘**(`H-141` `token`형). +6. **정정 배너는 달렸는데 본문 bullet·제목·색인이 옛 서술**(`H-130`형). +7. **개수·목록·상태를 두 곳에 값으로 적어 갈라진 것.** +8. **실측했다고 적혔는데 재작성 뒤 재실행 안 한 것.** + +--- + +## §4 각도 (이전 라운드와 같은 문자 — 상호참조용) + +A 겹침(결정 둘 이상이 만나는 자리) · B M5+ 값 단위 · C 참조 구현 실행 · +D ROADMAP 시뮬레이션(체크박스 순서대로 짜면 막히는 자리) · E fail-fast 3종 · +F 개수/목록 · G Luau 언어 사실 재확인 · H 타입. + +--- + +## §5 규칙 (엄수) + +- **`base/`·`research/`·`reference/`·`archive/`·인덱스 레이어를 고치지 말 것.** + 당신의 출력은 `-round10.md`와 `audit/handtrace-round10-reference-impl/`뿐이다. + (오타·stale은 발견으로 적되 고치지 않는다.) +- **`git stash`·`git commit`·`git checkout` 금지.** HEAD 대조는 `git show + HEAD:<경로>`/`git diff HEAD -- <경로>`. +- 발견 번호는 `H-150`부터 연속. 심각도 🔴(그대로 구현하면 크래시/누수/침묵) + 🟡(계약 위반·불일치, 판단 필요) 🟢(정정만). +- 절 인용은 `conventions.md`의 "절 인용 규약"대로 원문에서 잘라 쓸 것 — + `python3 .claude/tools/doc-check.py`를 마지막에 돌려 **당신 파일이 ERROR를 + 만들지 않는지** 확인(기존 WARN 59건은 무시). +- 사용자 원문을 인용할 땐 `followup`/`session/`에 있는 것만, 자르지 말 것. +- 사용자에게 질문하지 말 것 — 당신은 답을 받을 수 없다. 갈래를 적고 넘어간다. +- 토큰·시간 제한은 없다고 생각하되, **레인 A와 C는 반드시 끝내고** B·D는 남은 + 만큼. 못 본 것은 §6에 정직하게. + +--- + +## §6 산출물 + +`pre-implementation-handtrace-round10.md`에 이어 쓴다(이미 §0~§4 골격과 +`H-147`~`H-149`가 있다): +- **요약 표**(번호·심각도·한 줄·주 대상·성격·실측) — 기존 표에 행 추가. +- **상세** — 발견마다 트레이스(실제 값으로), 왜 새 메커니즘인지 여부, 갈래, + 권고, 권고 근거, 채택 시 고칠 자리. +- **§4 사용자 결정 표** — 판단 필요 항목만, 갈래를 한 셀에. 기존 `H-147`~`H-149` + 행 아래에 이어서. +- **§5 이상 없다고 확인한 것** / **§6 남은 의심·못 본 것** / **§7 레인 C 실행 + 기록**(어떤 스파이크를 어떻게 갱신했고 결과가 뭔지, 파일 경로). diff --git a/.claude/qa-request/pre-implementation-handtrace-round10.md b/.claude/qa-request/pre-implementation-handtrace-round10.md new file mode 100644 index 0000000..0f876c0 --- /dev/null +++ b/.claude/qa-request/pre-implementation-handtrace-round10.md @@ -0,0 +1,99 @@ +# 구현 전 손 트레이싱 **10라운드** — 발견 보고 + 배치 결정 문항지 + +> **이 파일이 무엇인가**: **[2026-08-28 신설]** 9라운드 후속(`H-143`~`H-146`) +> 반영 뒤 `/code-review high`가 낸 판단 대기 셋(`H-147`~`H-149`)을 씨앗으로, +> 신선한 탐사자가 **M2~M8 전 표면을 광범위하게** 손 트레이싱·실측한 결과를 +> 여기 이어 쓴다(`H-150`~). 지시서는 `-round10-brief.md`. **사용자는 §4 표를 +> 위에서 아래로 읽고 갈래만 회신한다**(배치). 결정이 나면 `-round10-followup.md`를 +> 새로 만들고 `base/`에 반영한다 — **이 파일은 발견 당시의 기록**이라 각 항목의 +> "갈래"는 선택 전 목록이니 반영 뒤엔 그대로 믿지 말 것. +> +> 상태: **[2026-08-28 기준] 탐사 진행 중 / 회신 대기.** `H-147`~`H-149`는 +> `H-143`~`H-146` 반영분에 `/code-review high`가 낸 10건 중 새 메커니즘·기존 +> 결정 변경이라 문항으로 올린 셋(나머지 일곱은 반영 — `-round9-followup.md`의 +> 마지막 code-review 절). + +## 요약 표 + +| 번호 | 심각도 | 한 줄 | 주 대상 | 성격 | 실측 | +|---|---|---|---|---|---| +| `H-147` | 🟡 | `Rerun` 꼬리 `wasAlive`가 루프 머리 `_consumeCleanup()` **뒤**에 잡히고 공개 `Rerun()`에 게이트가 없어 **진입 시점에 이미 죽은 핸들**(직전 cleanup이 `self:Unsubscribe()` / 해제 뒤 타이머의 `self:Rerun()`)은 else 분기 → 고아 cleanup + `_installed = true` | `effect-plan.md` `Rerun` | `/code-review` 발견, 처방은 규칙 또는 새 플래그 | — | +| `H-148` | 🟡 | `H-146`의 "`Parent` 거부는 **전용 문구**"가 새 메커니즘 — `isHandlable` 거부는 `Dispatch.process`의 **일반** 매치 실패 문구로 떨어지고 그 자리에 특수 분기는 두지 않기로 확정돼 있다. 사용자 회신에 문구 언급 없음(에이전트 선택이었음) | `bind-system-plan.md` `H-142` 항목, `ROADMAP.md` M5 | `/code-review` 발견, 처방은 새 핸들러 또는 철회 | — | +| `H-149` | 🟡 | `Observer:Subscribe`가 `self:WeakSubscribe()`로 위임하므로 거기 붙인 `error(…, 2)`가 `o:Subscribe()` 경로에선 사용자 호출부가 아니라 `Observer:Subscribe` 본문을 가리킴(`H-104` level 계약 위반). `EffectHandle`은 (b)로 인라인해 문제 없음 | `lifecycle-pattern.md` Observer 네 진입점 | `/code-review` 발견, 처방은 기존 결정("위임하므로 게이트 한 번") 변경 | — | + +## 상세 + +### `H-147` 🟡 — 이미 죽은 핸들에서 `Rerun()`이 불리면 (`H-143` 잔여) + +`effect-plan.md` `Rerun`: 루프 머리 `_consumeCleanup()` → `local wasAlive = +canExecute(self)` → `fn` → `if wasAlive and not canExecute(self)`. "실행 중에 +죽은" 건 잡지만 **"진입 시점에 이미 죽은"** 건 못 잡는다: +1. 직전 cleanup이 `self:Unsubscribe()`를 부름 → 루프 머리 소진 안에서 죽음 → + `wasAlive` 거짓 → `fn` 실행 → else → 저장 + `_installed = true`. 고아 cleanup, + 이후 재`Subscribe`의 `resubscribeTail`이 `_installed` 참을 보고 재설치 생략. +2. `fn`이 `task.delay(1, function() self:Rerun() end)`를 걸고 사용자가 그 전에 + `Unsubscribe()`. +생성자 최초 `Rerun`("한 번도 산 적 없음")과 구분할 상태가 없다. +**갈래**: (a) "죽은 핸들에서의 `Rerun()`은 사용자 책임(UB)"으로 문서화 — 새 +상태 없음; (1)은 "cleanup 안에서 자기 해제"를 지원 목록에서 빼는 것, (2)는 +`Unsubscribe`가 도는 cleanup이 그 예약을 취소해야 하는 것(그게 cleanup의 일). +기존 "error 시 UB / 수렴 책임은 `fn`" 계약과 같은 결 / (b) `_everAlive` 플래그 +하나(첫 `canExecute` 참 시점에 세움) → `Rerun` 진입에서 `everAlive and not +canExecute → return`. 필드 하나, 생성자 케이스도 가림 / (c) `wasAlive`를 +`_consumeCleanup()` **앞**에서 잡기 — (1)만 닫히고 (2)는 그대로. +**권고 (a)** — 두 시나리오 다 "죽은 뒤에도 자기를 부르는 코드"고, `Unsubscribe`가 +cleanup을 도는 이유가 바로 그런 예약을 거두라는 것. + +### `H-148` 🟡 — `H-146`의 "전용 에러 문구"는 새 메커니즘이었다 + +`PropertyHandler.isHandlable`이 `"Parent"`를 거부하면 나오는 건 `Dispatch.process`의 +**일반** 매치 실패 문구(*"no handler … check provider"*)이고, 그 자리에 특수 +분기는 두지 않기로 확정돼 있다(`dispatch-core-plan.md`의 매치 실패 절). 전용 +문구를 내려면 새 자리가 필요하다. 사용자 `H-146` 회신엔 문구 언급이 없었고 +에이전트가 "배선 세부"라며 붙였다 — `conventions.md` 2026-08-27 규칙 위반. +**갈래**: (a) 전용 문구 **철회** — 일반 매치 실패 error 그대로, 오해는 사용자 +문서("`Parent`는 props에 못 쓴다, 루트는 밖에서")로 흡수. 새 것 0 / (b) **가드 +핸들러** — `k == "Parent"`에 매치해 `process`에서 전용 문구로 error. Observer/ +Effect의 "동적 경로 가드"(`effect-plan.md`)와 같은 모양이라 새 *종류*는 아니나 +핸들러 하나 + 숏핸드보다 위 우선순위 필요 / (c) `Dispatch.process` 매치 실패 +문구에 키 이름을 싣기(일반 개선이지만 별도 결정). +**권고 (a)** — 원래 회신의 취지(새 API 없음, 최종 사용자 몫)와 같은 결. M5에서 +실제로 밟아 헷갈리면 (b)를 그때 얹는 게 싸다. + +### `H-149` 🟡 — `Observer:Subscribe`의 위임 때문에 `level 2`가 quad 내부 줄을 가리킨다 + +2026-08-27에 `Observer:WeakSubscribe`의 `error`에 `, 2`를 붙였는데(`H-104` +계약), `Subscribe`가 `self:WeakSubscribe()`로 위임하므로 `o:Subscribe()` 경로의 +level 2는 **`Observer:Subscribe` 본문**을 가리킨다. 가장 흔한 오용(leaf 바인딩된 +Observer에 `o:Subscribe()`)이 quad 내부 줄을 블레임한다. `EffectHandle` 쪽은 (b)로 +게이트를 인라인해 문제 없음. +**갈래**: (a) Observer도 `Subscribe`에 게이트·등록 세 줄을 **인라인** — Effect와 +같은 모양, level 2 정확. "게이트는 한 번만 돈다 — `Subscribe`가 `WeakSubscribe`에 +위임"이라는 기존 문장이 "각자 한 번"으로 바뀜. 중복 세 줄은 같은 타입 안이라 +dot 호출 로컬 헬퍼로 빼도 됨(가상 디스패치 없음) / (b) 내부 헬퍼 +`weakSubscribe(self, level)`로 위임하고 `Subscribe`는 `level 3` — 코퍼스에 +`level 3` 선례 없음 / (c) 그대로 두고 `Subscribe` 경로만 블레임이 한 단계 안쪽임을 +문서화. +**권고 (a)** — "본문을 섞지 않는다"(2026-08-27 원칙)와 결이 같다. + +## §4 ⭐ 사용자 결정이 필요한 것 (배치 회신용) + +| 문항 | 무엇 | 선택지 | 권고 | +|---|---|---|---| +| **`H-147`** | 죽은 핸들에서 `Rerun()` | (a) UB로 문서화(cleanup 안 자기 해제는 지원 목록 밖, 타이머 취소는 cleanup의 일) / (b) `_everAlive` 플래그 + 진입 게이트 / (c) `wasAlive`를 소진 앞에서 | **(a)** — 새 상태 없이 기존 UB 계약과 같은 결 | +| **`H-148`** | `Parent` 거부 문구 | (a) 전용 문구 철회, 일반 매치 실패 그대로 / (b) `Parent` 가드 핸들러(동적 경로 가드형) / (c) 일반 문구에 키 이름 | **(a)** — 회신 취지(새 API 없음) 그대로, 필요하면 M5에서 (b) | +| **`H-149`** | Observer `Subscribe` 위임과 `level 2` | (a) `Subscribe`에 게이트·등록 인라인(Effect와 동형) / (b) `level 3` 위임 / (c) 문서화만 | **(a)** — 본문 안 섞기 원칙과 동형 | + +(탐사자의 문항은 아래에 이어 붙는다.) + +## §5 이상 없다고 확인한 것 + +(탐사자가 채운다.) + +## §6 남은 의심 / 못 본 것 + +(탐사자가 채운다.) + +## §7 레인 C 실행 기록 + +(탐사자가 채운다 — `audit/handtrace-round10-reference-impl/`.) diff --git a/.claude/qa-request/pre-implementation-handtrace-round9-followup.md b/.claude/qa-request/pre-implementation-handtrace-round9-followup.md index 4ac9bcb..2915099 100644 --- a/.claude/qa-request/pre-implementation-handtrace-round9-followup.md +++ b/.claude/qa-request/pre-implementation-handtrace-round9-followup.md @@ -31,14 +31,14 @@ | — | `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` 발견 중 새 메커니즘 넷) | ⏳ **판단 대기** — `fn` 안 `Unsubscribe` 고아 cleanup / 재`Subscribe` 재설치 / `indexOfElement` 잔존 / 루트 마운트 경로 | +| — | `H-143`~`H-146` (`/code-review high` 발견 중 새 메커니즘 넷) | ✅ **확정·반영 (전부 권고 (a))** — `Rerun` 꼬리 실행 중 사망이면 즉시 소진(`wasAlive`) / 재구독 꼬리(`Refresh` 먼저; 감사 4라운드로 진입점 소유권 (b) — `EffectHandle` 자기 것 — 추가 확정) / `indexOfElement` weak-key / 루트는 금지 범위 밖 + 전용 문구 | **[2026-08-27] Q1~Q3는 `base/`·`ROADMAP.md`에 반영했다** — 반영 중 드러난 것과 열어둔 확인은 아래 "반영 기록" 절. **같은 날 이어서 Q4~Q8·Q10·`H-138`·`H-139`를 확정·반영했다**(아래 Q4 이하 절). **[같은 날 마지막] Q9 확인과 `H-142` 판단도 닫혀 9라운드 발견은 전량 처리됐다** — 그 뒤 감사 8라운드와 `/code-review high`를 -돌렸고, **리뷰가 낸 새 메커니즘 넷(`H-143`~`H-146`)이 판단 대기**(아래 -"`/code-review high`" 절). +돌렸고, 리뷰가 낸 새 메커니즘 넷(`H-143`~`H-146`)은 **[2026-08-27 다음 세션] +전량 확정·반영**(아래 "`H-143`~`H-146`" 절). **사용자 회신 원문(Q4~Q10 일괄, 2026-08-27)**: *"Q5 까지는 권고에 전부 동의함. WeakSubscribed 자체가 사라질 수 있는 요소라서, 그 사라지는걸 유저가 정하게 하는 @@ -377,7 +377,10 @@ if slot._physicalTarget == nil then return end `Subscribe`/`WeakSubscribe`/`WeakUnsubscribe`는 **Observer의 함수를 메소드 테이블에 그대로 배정**(같은 레지스트리·같은 `canBound` 게이트·같은 `.Subscribed`), `Unsubscribe`만 `Observer.Unsubscribe(self)` 통과 뒤 - `self:_consumeCleanup()`. `WeakUnsubscribe`는 cleanup을 안 건드린다(약한 + `self:_consumeCleanup()`. **⚠️ [2026-08-27 같은 날 정정] 함수 배정은 (b)로 + 뒤집혔다** — 네 진입점은 `EffectHandle` 자기 것, 공유는 레지스트리·게이트뿐 + (아래 `H-144` 절의 감사 4라운드 항목). 게이트가 먼저라는 이 문항의 결정 + 자체는 그대로다. `WeakUnsubscribe`는 cleanup을 안 건드린다(약한 구독은 GC에 맡기는 경로라 해제가 종료 신호가 아니다). - 산문의 번호 목록(플래그 → cleanup → fail-fast)은 **의미 목록이지 실행 순서가 아니라고** 그 자리에 적었다. `lifecycle-pattern.md`의 *"아래가 네 진입점 @@ -629,6 +632,121 @@ nil 로 지우는것만 안 한다면 맞아"*. 요청으로 신설한 블록. 캡에 밀린 하위 발견(`Slot:List` 잉여 블록 잔존 등)은 이미 "잉여" 주석으로 처리된 것과 같은 항목. +## `H-143`~`H-146` — 전부 권고 (a) 확정 (2026-08-27) + +문항·갈래 원문은 `-round9.md`의 `H-143`~`H-146` 절. 반영 자리는 각 항목에. + +**사용자 회신 원문**: *"H-143: 동의. 메니징된 effect 에서 해당 부분을 지원 안 +해야할 이유가 딱히 존재하지 않아. 특정 state 에 변경을 딱 한번만 처리하고 +cleanup 되는걸 만드는 요구가 존재하지 않을 이유가 딱히 없다고 봐. (a) 갈래를 +택하는데 큰 문제가 없어보여. H-144: 재설치 처리는 동의해. 그리고 원래 처음 +설치에서도 모든 옵저버와 ref 콜백이 하나하나 실행되지 않도록 blocker 를 통해서 +전부 블로킹 하고 명시적으로 실행한다는 점을 생각해야해 이건 그 동작과 유사해. 재 +sub 에서도 똑같게, 후행에서는 실행해야하는 부분이지. 그러고 나서는 사실 어떤 +것이라도 가만히 둬도 좋아. 그 이후 받는 모든 emit 의 경우는 들어올 때 기준으로 +항상 '다시 받는 emit' 에 속할것이거든. … 근데 blocker 로 블록된 다음 나중에 +들어오는 것에 대해서 어떻게 처리할지는 갈리는 부분이야. 그걸 생각하면 초기에 +epoch 를 전부 잘 설정해주는게 나을수도 있어. 왜냐하면 처음 연산 기준에 있어서도 +전부 값은 최신 값이거든. 따라서 다음 emit 을 받아야할 이유가 없을수도 있어. 이건 +중간 값이 emit 을 보류하는거랑 다른 이야기인듯. observer 에도 연관되는 +이야기일텐데 한번 확인 해볼래? H-145: (A) 의 단점은 slot 자체가 여기저기에 +마운트된다면 여러 bk 에서 남아있을 수 있다는건데, 그건 큰 문제가 안 돼. slot +자체가 소수의 생성이 되는 요소인데다가 포탈 경로가 많지 않거든. 아마 그래서 +충분히 weak 로 두는건 괜찮은 발상이야. 컴퓨팅 비용이 더 안들고, 복잡하지 않고 +깔끔한 경로거든. 다만 다른 inst 들이 삭제 되지 않는게 확실히 보장되어야해. +기본적으로 자신을 gchold 로 잡으니 괜찮아보여. H-146: 나도 (A) 갈래에 동의해. +왜냐하면, 실제로 React 에서도 최상위 경로는 보통 App.tsx 나 main 을 나누고 root +를 돔 api 로 가져오고 거기에 바운딩 하는 처리를 해야하거든. 유사하게 메인 +컴포넌트에 대해서 생성 결과를 Parent 를 설정해주는건 옳을 수 있어. 우선 해당 +방식은 엔진 마다 다를 수 있다는 점도 생각해야해. Quad 가 제공하는 마운트는 +완전히 다른 성격이라서(부기에 대한 처리가 들어가는데 PlayerGUI 등 Quad 가 +제공하지 않은 부기가 없는 객체에 대해서 Quad 의 객체를 주입하는 성격의 API 는 +아니거든) 해당 부분을 해결하기 위해서 다른 API 를 제공 할 이유가 없기도 해. 해당 +부분은 각 엔진을 사용하는 최종 사용자의 몫."* + +### `H-143` — `Rerun` 꼬리에서 실행 중 사망(`wasAlive and not canExecute`)이면 반환 cleanup 즉시 소진 (a) + +- 하위 결정: 판정은 **"이 실행 중에 죽었는가"(`wasAlive and not canExecute`)** + — `fn` 안 `WeakUnsubscribe`도 같은 경로로 소진된다. + - ⚠️ 처음엔 `not canExecute` 하나로 썼는데 **감사 2라운드가 회귀를 잡았다**: + 생성자의 최초 `Rerun()`은 어떤 바인드·구독보다 먼저 돌아 `canExecute`가 + 항상 거짓 → 첫 실행 cleanup 즉시 소진 + `_installed` 거짓 → 첫 바인드에서 + `fn`이 또 돈다(`H-58` 재현, "생성 즉시 1회 실행은 바인드로 미룰 수 없다"와 + 충돌). 진입 전 상태를 로컬로 잡아 "살아 있다가 죽었다"만 잡는 것으로 정정, + 같은 자리에서 죽은 핸들의 `_pending` 재요청도 버리기로(안 버리면 `fn` 안 + `Unsubscribe()` + `Rerun()` 조합이 죽은 핸들에서 `fn`을 한 번 더 돌린다 — + 감사 2라운드 의심 항목). **사용자 확정**: *"오 그렇네. 실행중 죽으면 + 클린업만 하기, 해당 방식 맞아보여"*. `H-133`의 "약한 해제는 cleanup을 안 건드린다"는 + *`WeakUnsubscribe` 자신*의 계약이고, 여기선 `Rerun`이 방금 받은 것을 죽은 + 핸들에 매다느냐의 문제라 행위자가 다르다. 강한 해제만 잡으려면 별도 판정이 + 필요해 더 든다(a2 기각). +- 반영: `base/effect-plan.md` `Rerun` 의사코드 + "`self`를 주는 덕에" bullet + (허용 문장이 확정으로), `ROADMAP.md` M2 `Effect` 체크박스. + +### `H-144` — `Subscribe`/`WeakSubscribe` 등록 끝에 `Refresh()` 먼저, `not _installed or depsChanged → Rerun` (a) + 진입점은 `EffectHandle` 자기 것 (b, 감사 4라운드) + +사용자가 요청한 **재트레이싱** 결과(회신의 두 질문): +1. *재구독 뒤 "다음 emit"이 오면 꼬여도 괜찮은가* — 괜찮다. `fire`의 가드 + 순서가 `canExecute → blocker:IsOn → _epochs:Update → Rerun`이라 해제 중 + 도착한 emit은 **첫 가드에서 버려져 `_epochs`를 안 건드린다.** 재구독 뒤 첫 + emit은 리비전이 새로우니 정상 `Rerun`, 읽는 값은 항상 최신(State는 lazy, + 수신 시점에 `valueEpochMap` 갱신). 다이아몬드 접힘도 그대로. +2. *재구독 시점에 epoch를 맞춰두는 게 나은가* — 낫고, **leaf 경로가 이미 그 + 모양**이다(`_bindDestroying`: `Refresh()` 먼저, 아니면 "다음 emit이 헛되이 + 한 번 더 돈다" 캐비엇). 구체 케이스: `state:Gate`가 유보 중일 때 재구독 → + `Rerun`이 읽는 값은 이미 새 리비전(게이트는 값은 수신 시점에 갱신하고 emit만 + 유보, `state-epoch-plan.md` §4 게이트 예외) → 유보가 풀려 `{A@r2}`가 오면 + `Refresh` 안 했으면 같은 값으로 `fn`+cleanup이 한 번 더, 했으면 접힌다. +- Blocker는 재구독에 안 쓴다 — dep 재등록이 없으므로(생성자에서 한 번, `_deps` + 강참조 유지) 억제할 발화가 없고 명시 `Rerun`만 하면 된다. +- Observer: 관련 없음 확인 — epoch가 없고(`H-107`) 구독 시 설치 발화도 없어 + 재`Subscribe`는 첫 구독과 동일. 고칠 자리 없음. +- 하위 결정: **`WeakSubscribe`도 같은 꼬리** — `WeakUnsubscribe`(cleanup 미소진, + `_installed` 유지) 뒤 재`WeakSubscribe`도 한 모양으로 맞는다(dep 안 변했으면 + no-op, 변했으면 옛 cleanup 소진 후 재실행). +- ⚠️ **감사 4라운드가 배선 결함을 잡았다 → (b) 네 진입점을 `EffectHandle` 자기 + 것으로.** 처음엔 Q4(`H-127`)의 "Observer 함수를 그대로 배정" 위에 래퍼를 + 얹었는데, `Observer:Subscribe`의 본문이 `self:WeakSubscribe()`로 **콜론 + 위임**하므로 `self`가 `EffectHandle`이면 그 조회가 새 `EffectHandle:WeakSubscribe` + 래퍼로 가서 꼬리가 **두 번** 돌고, 첫 번째는 `Subscribed[self] = true`(강한 킵) + **전에** `Rerun`해 `fn` 안 `self:Unsubscribe()`(`H-143`이 지원하는 패턴)가 + *"not subscribed strongly"*로 error(로컬 `luau`로 재현: 콜론 위임은 꼬리 + 2회·첫 번째 킵 전, dot 위임은 1회·킵 뒤). 갈래 (a) Observer 본문의 내부 위임을 + dot 호출로 / (b) 함수 공유를 접고 `EffectHandle`이 네 진입점을 따로(공유는 + `Observer.luau`의 레지스트리 둘과 `canBound`뿐). **사용자 확정 (b)**: *"b가 + 맞아. 내 머리에서 나왔던 처음 구조는 그것이였어. 항상 언급되는 말이지만, + '하나의 무언가가 두 일을 동작하지 않는가에 유의하자' - 이것도 마찬가지야. + 버그를 유발하기 좋은 포인트였고, 감사자의 좋은 지적이였어."* 앞서 *"애초에 + observer 랑 effect 랑 헤테로지니어스한 타입인데. effect 가 observer 를 + 만족시키진 않아. 생성 방법도 다르고"*라 결함 자체가 진짜인지 물었고, 재현으로 + 확인. 원칙은 `conventions.md`의 "설계 원칙" 절로 승격. Q4의 "그대로 배정"은 + 이로써 **좁혀졌다**(레지스트리·게이트 공유만 남음). + `Subscribe`는 `WeakSubscribe`를 부르지 않고 등록 세 줄을 펼쳐 쓴다 — 꼬리가 + 강한 킵 뒤에 정확히 한 번 돌아야 하므로. +- 반영: `base/effect-plan.md` `EffectHandle` 의사코드 블록(`resubscribeTail`) + + "`:Subscribe()`가 등록하는 것은" bullet, `base/lifecycle-pattern.md`의 (2) 절 + 머리 문단(`EffectHandle` 재사용 범위 정정), `ROADMAP.md` M2. + +### `H-145` — `bk.indexOfElement`를 weak-key로 (a) + +- 사용자가 붙인 조건: 요소 `inst`가 마운트 중 GC되지 않는 보장 — (0)의 + gchold(`lifecycle-pattern.md`)가 그것. 값이 정수라 Luau ephemeron 미지원도 + 무관. +- (b) 해제에 요소를 넘겨 `setLength`가 지우기(시그니처 의미 확장) / (c) inst + 층 명시 삭제 API 기각. +- 반영: `base/dispatch-core-plan.md` `getBookkeeping` 초기화 + `InstanceChildHandler` + 5번째 인자 근거 정리(weak가 되며 "옛 키 잔존" 근거는 사라지고 "지속 클로저 + 없음"만 남음), `ROADMAP.md` M3 `getBookkeeping` 체크박스. + +### `H-146` — 루트는 금지 범위 밖, `Mount` 표면 없음, 거부는 전용 문구 (a) + +- 인용문 *"외부에서 직접 Parent 설정해주지 말것"*의 범위를 **quad가 관리하는 + 자식 자리**로 명문화. 루트 부착은 엔진마다 다른 최종 사용자 코드. (b) + `Mount(root, parent)` / (c) 루트 전용 props 키 기각. +- 반영: `base/bind-system-plan.md` `H-142` 항목에 루트 bullet + 전용 문구, + `base/slot-plan.md`의 "동적 자식은 반드시" 절 각주 + "v2는 mount 함수 자체가" + 문구 정정(v1식 `Mount`는 v2에 없다), `ROADMAP.md` M5 `Property.luau`. + ## 감사 루프 (2026-08-27, Q4~Q10 반영분) 관례대로 `quad-doc-auditor` 한 턴에 하나, diff 범위, 라운드마다 각도 변경. @@ -644,3 +762,50 @@ nil 로 지우는것만 안 한다면 맞아"*. | 7 | 6라운드 수정분 + 세션 기록 사실성 | 확실 3 · 의심 0 | README `bind-system-plan` 행이 "배치 닫는 자리 … `Parent` 순서 미정"으로 1라운드 정정·`H-142` 확정 이전 상태 / `todos.md` 00번에서 "`/code-review high`·커밋 남음" 액션이 사라짐 → 복원 / 이 파일의 "48줄" 개수 주장(실제 47) → 개수 삭제. 세션 파일·summary·todos 본문 서술은 전부 사실과 일치 | | 8 | 7라운드 수정분만(좁게) | 확실 1 | `project-context.md`의 볼드 마커가 홀수(이번 갱신이 닫는 `**`를 지움) → 짝 맞춤. 지정 세 자리는 회귀 없음. **여기서 멈춤** — 1라운드 이후 설계 내용 발견은 0이고 [2026-08-27 8라운드 시점] 남은 건 기록 문서의 표기뿐이라, 비용 대비 `/code-review high`로 넘어가는 게 낫다고 판단(추이 5→3→4→5→3→1→3→1, 단조 수렴은 아님) | +## 감사 루프 (2026-08-27, `H-143`~`H-146` 반영분) + +`quad-doc-auditor` 한 턴에 하나, diff 범위, 라운드마다 각도 교체. 새 발견 +1→1→1→1→1→1→1→**0** (8라운드에서 수렴). + +| 라운드 | 각도 | 발견 | 처리 | +|---|---|---|---| +| 1 | 인덱스·문구 잔존 | `effect-plan.md`의 *`_installed`는 `Rerun`이 끝날 때 참* 문장이 무조건 서술 | 조건 반영 | +| 2 | `base/` 의미론 | **`H-143` 판정식 `not canExecute`가 생성자 최초 설치를 죽임**(회귀) + `fn` 안 `Unsubscribe`+`Rerun` 재진입 미서술 | 사용자 결정 → `wasAlive and not canExecute` + `_pending` 버림 | +| 3 | audit/archive/reference | `session-summary.md`가 정정 전 판정식을 요약 | 정정 | +| 4 | 수정분 재검토 | **Observer 함수 배정 + 콜론 위임 → 재구독 꼬리 2회, 강한 킵 전 `Rerun`**(설계 결함) | 사용자 결정 → (b) 진입점 자기 것 + 원칙 승격 | +| 5 | (b) 수정분 재검토 | `README.md` 색인 행 미갱신 / Observer `WeakSubscribe` `error` `level` 누락 / 실측 문구 과장 | 셋 다 정정 | +| 6 | 인덱스 레이어 | followup `H-143` 헤딩·진행 표가 정정 전 표현 | 정정 | +| 7 | `effect-plan.md` 통독 | "종료 신호 둘뿐"이 `Rerun` 꼬리와 모순 / 필드 목록에 `.Subscribed` 누락 / `fn` 안 재구독 미서술 | 셋 → 정정·명시 | +| 8 | 전체 diff 재통독 | **0건** | — | + +2·4라운드 발견은 둘 다 "권고 (a)의 의도는 맞는데 메인 세션이 고른 판정식·배선이 +틀린" 종류 — 1라운드(문구 잔존) 각도로는 안 나왔고, 의미론·수정분 재검토 +각도에서만 나왔다. + +## `/code-review high` (2026-08-28, `H-143`~`H-146` 반영분 + 감사 8라운드 뒤) + +10건(7 CONFIRMED 검증). **일곱은 기존 규칙 적용이라 반영**, **셋은 새 메커니즘· +기존 결정 변경이라 10라운드 문항으로**(`-round10.md` `H-147`~`H-149`). + +반영한 일곱: +1. `effect-plan.md` "`EffectHandle:Subscribe()`" 절 머리 볼드가 여전히 "Observer의 + 것을 재사용" — (b)로 정정. 717행 "Observer의 것을 위임한 뒤"도. +2. `Refresh()` 먼저의 계약을 세웠는데 `ROADMAP.md`(M2·M6)·`lifecycle-pattern.md`· + `ref-plan.md`가 단축평가형 `if not _installed or Refresh()` 한 줄을 그대로 — + 전부 두 줄 형태로. +3. "첫 구독에선 no-op"은 거짓(생성~첫 구독 사이 emit은 `fire`가 버려 `Refresh`가 + 참) — "그 사이 dep이 안 변했으면 no-op"으로. +4. `fn` 안 자기 해제의 범위 — 건 경로로만(강구독 `Unsubscribe`, 약구독 + `WeakUnsubscribe`), leaf 바인딩 핸들엔 자기 해제 경로 없음(종료는 leaf 사망). + `Rerun` 주석의 "leaf도 아니라"도 한정. +5. `source-state-plan.md`의 *내부 Observer가 게이트를 갖고 있어 Effect 구현이 + 몰라도 자동 커버* 서술 — 틀림(내부 Observer는 `Weak*`, 발화는 `canExecute(handle)`). + (b)대로 `EffectHandle`이 직접 `canBound`. +6. weak-key `indexOfElement`의 보장자 — 사용자가 든 gchold는 `inst → 값` 홀더라 + `inst` 자신을 안 잡음. 실제 보장자는 `slot._elements` + `gatedRecompute` 캡처로 + 정정, Instance weak key 미확인(`audit/gcconn-trick-verification.md`) 포인터. +7. `-round9.md` `H-144` 셀 "둘 다 래퍼"·`ROADMAP.md` 배너 "재구독 래퍼" → + (b) 표현으로. 캡 아래 넷(`question.md`의 *판단 대기 없음* 과장, "다음 세션" → + 파일 ID, 중복 세 줄, 한국어 자리표시자)도 앞 둘은 정정. + +문항으로 올린 셋은 `-round10.md`가 소스. diff --git a/.claude/qa-request/pre-implementation-handtrace-round9.md b/.claude/qa-request/pre-implementation-handtrace-round9.md index b33d971..96666c3 100644 --- a/.claude/qa-request/pre-implementation-handtrace-round9.md +++ b/.claude/qa-request/pre-implementation-handtrace-round9.md @@ -57,10 +57,10 @@ high` 7패스의 수정)를 처음부터 다시 트레이싱한 결과. 발견 | `H-140` | 🟢 | `ROADMAP.md:1124`가 아직 *"해제 시 `slot.Offset = nil`"* — `SL-75`/`D-60`이 전면 정정한 문장인데 **구현자가 실제로 보는 체크박스**에 살아남았다. 그대로 짜면 포탈 구독자가 영구히 끊긴다 | `ROADMAP.md` M6 | 사냥 #7 | — | | `H-139` | 🟢 | `New`가 둘(모듈 팩토리 `New(): Quad` / 인스턴스 생성자 `New "Frame" {…}`)이고, `D.Frame {…}`의 파이프라인(생성 → gcconn → flatten → drive)은 네 문서에 흩어져 있어 의사코드가 한 곳에 없다 | `module-lifecycle-plan.md` / `bind-system-plan.md` | 레인 B | — | | `H-142` | 🟡→닫힘 | **[2026-08-27 `H-139` 의사코드에서 발견]** `Dispatch.drive`의 해시 파트 순서가 계약이 아니라 `Frame { Parent = x, Size = … }`의 `Parent` 대입이 다른 프로퍼티보다 먼저 올 수 있다 — 사용자 확정은 순서가 아니라 **props에 `Parent` 금지** | `bind-system-plan.md` × `ROADMAP.md` M5 | 사냥 밖 — 의사코드 작성이 드러냄 | — | -| `H-143` | 🟡 | **[2026-08-27 `/code-review high`]** `fn` 안에서 `self:Unsubscribe()`를 부르면(문서가 허용하는 자리) `Rerun`이 `fn`의 반환 cleanup을 그대로 `_cleanup`에 저장하고 `_installed = true`로 되돌려 **아무도 소진 못 하는 cleanup**이 남는다 — "마지막 cleanup 정확히 1회" 계약 위반 | `effect-plan.md` `Rerun` | 사냥 밖 — 리뷰 발견, 처방은 새 메커니즘 | — | -| `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` | 리뷰 발견, 처방은 새 메커니즘 | — | -| `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 | 리뷰 발견, 처방은 새 메커니즘 | — | -| `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` | 리뷰 발견, 처방은 새 표면 | — | +| `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)** | — | +| `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) — `Refresh` 먼저, 두 구독 진입점 모두 꼬리; 진입점은 (b) `EffectHandle` 자기 것** | — | +| `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) 루트 예외 + 전용 문구** | — | --- @@ -617,7 +617,7 @@ Tween 절 스케치의 `hint == nil` 줄이 `v == nil` 규칙을 잘못 옮긴 --- -### `H-143`~`H-146` — 2026-08-27 `/code-review high`가 Q4~Q10 반영분에서 잡은 것 (사용자 판단) +### `H-143`~`H-146` — 2026-08-27 `/code-review high`가 Q4~Q10 반영분에서 잡은 것 (**[2026-08-27] 전량 확정 — 결정은 `-round9-followup.md`**) 반영 뒤 돌린 `/code-review high`(10건, 검증 12 CONFIRMED)가 낸 것 중 **처방이 새 메커니즘·표면인 넷**. 나머지 여섯(`InstanceChildHandler` retractor의 `Parent = nil` diff --git a/.claude/question.md b/.claude/question.md index f700016..214136a 100644 --- a/.claude/question.md +++ b/.claude/question.md @@ -12,24 +12,19 @@ --- -## ⭐ 최우선 — `/code-review`가 낸 새 메커니즘 넷 (2026-08-27 갱신) +## ⭐ 최우선 — 10라운드 문항지 (2026-08-28 갱신, 배치 회신 대기) -**[2026-08-27] 9라운드 손 트레이싱은 Q1~Q10·`H-138`·`H-139`·`H-142`까지 전량 -처리·반영됐고**, 그 반영분에 `/code-review high`를 돌린 결과 **처방이 새 -메커니즘·표면인 넷**이 나와 판단을 기다립니다 — 갈래·권고는 -`qa-request/pre-implementation-handtrace-round9.md`의 `H-143`~`H-146` 절이 -소스(여기서 반복하지 않음). 한 줄씩: -- **`H-143`** `fn` 안에서 `self:Unsubscribe()`하면 그 실행이 돌려준 cleanup이 - 영원히 안 불린다 — 권고 (a) `Rerun`이 `canExecute` 거짓이면 즉시 소진. **M2.** -- **`H-144`** `Unsubscribe`한 핸들을 다시 `Subscribe`해도 재설치가 없다 — 권고 - (a) `Subscribe` 래퍼가 `not _installed`면 `Rerun`(leaf 경로 `H-65`와 대칭). **M2.** -- **`H-145`** 최상위 Slot 교체 시 부모 `bk.indexOfElement`에 옛 Slot이 강참조로 - 남는다 — 권고 (a) 그 맵을 weak-key로. **M3.** -- **`H-146`** props `Parent` 금지 뒤 루트(`ScreenGui` → `PlayerGui`)를 붙이는 - 승인 경로가 없다 — 권고 (a) 루트는 예외로 명문화(금지는 quad가 관리하는 자식 - 자리에 한함). **M5.** -`H-143`/`H-144`는 M2 `Effect` 표면이라 **M2 착수 전 답이 있는 게 좋지만** 구현 -중 정해도 되는 크기입니다(게이트로 보지 않음). +**[2026-08-28] 10라운드 문항지가 `qa-request/pre-implementation-handtrace-round10.md`에 +있습니다** — §4 표를 위에서 아래로 읽고 갈래만 회신하시면 됩니다. 씨앗은 +`H-143`~`H-146` 반영분에 `/code-review high`가 낸 판단 대기 셋(`H-147` 죽은 +핸들에서 `Rerun` / `H-148` `Parent` 거부 전용 문구는 새 메커니즘 / `H-149` +Observer `Subscribe` 위임과 `level 2`), 그 아래에 신선한 탐사자의 **광범위 +탐사**(M2~M8, 레인 A/C/B/D — 지시서 `-round10-brief.md`) 결과가 이어집니다. +**M2 착수 게이트는 아닙니다**(셋 다 구현 중 정해도 되는 크기). 사용자 판단: +*"인간을 기다리는거 엄청 비효율이라서 … batch 로 처리될 필요가 있는듯"*. + +**[2026-08-27] 9라운드**(Q1~Q10·`H-138`·`H-139`·`H-142`·`H-143`~`H-146`)는 +전량 처리·반영됐습니다 — 소스는 `-round9-followup.md`. **[2026-08-26] 8라운드 손 트레이싱이 처리 완료됐습니다 — M2(반응형 코어) 착수를 막는 항목이 하나도 없습니다.** 발견 17건(`H-107`~`H-123`)의 결정 diff --git a/.claude/session-summary.md b/.claude/session-summary.md index 03c85dd..ab6d314 100644 --- a/.claude/session-summary.md +++ b/.claude/session-summary.md @@ -1961,3 +1961,21 @@ Q4(`EffectHandle` 네 진입점 의사코드 — Observer 것 재사용, `Unsubs `question.md` 최우선 절). 결정의 소스는 `qa-request/pre-implementation-handtrace-round9-followup.md`, 경위는 `session/2026-08-27-02-handtrace-round9-q4-q10.md`. + +- **`session/2026-08-27-03-handtrace-round9-h143-h146.md`** — 9라운드 후속: + `/code-review`가 낸 새 메커니즘 넷 **전부 권고 (a) 확정·반영**. `H-143` + `Rerun` 꼬리가 **실행 중에 죽었으면**(`wasAlive and not canExecute`) 반환 + cleanup 즉시 소진 + 재요청 버림(`fn` 안 `self:Unsubscribe()` 지원 — 처음 쓴 + `not canExecute` 하나짜리 판정은 생성자 최초 설치를 죽여 감사 2라운드가 정정) / `H-144` `Subscribe`·`WeakSubscribe` 등록 끝에 + `_epochs:Refresh()` + `not _installed or depsChanged → Rerun` 꼬리(사용자 + 요청으로 재구독 뒤 emit·epoch·Blocker 상호작용을 재트레이싱, leaf + `_bindDestroying`과 동형) — **그리고 감사 4라운드가 Q4의 "Observer 함수 + 배정"이 콜론 위임 때문에 이 꼬리를 두 번 태우는 걸 잡아 (b) `EffectHandle` + 네 진입점 자기 것으로 확정**(*"하나의 무언가가 두 일을 동작하지 않는가"* → + `conventions.md` 설계 원칙) / `H-145` `bk.indexOfElement` weak-key / `H-146` + 루트 부착은 금지 범위 밖 — `Mount` 표면 없이 사용자 몫(*"각 엔진을 사용하는 + 최종 사용자의 몫"*), `Parent` 거부는 전용 문구. `question.md` 최우선 절 비움. + 결정의 소스는 `qa-request/pre-implementation-handtrace-round9-followup.md`. + **[2026-08-28]** 감사 8라운드 수렴 → `/code-review high` 10건(일곱 반영, 셋은 + 판단 필요) → 사용자 판단으로 **10라운드 문항지**(`-round10.md`, `H-147`~`H-149` + 씨앗 + 광범위 탐사)로 이관, 배치 회신 대기. diff --git a/.claude/session/2026-08-27-03-handtrace-round9-h143-h146.md b/.claude/session/2026-08-27-03-handtrace-round9-h143-h146.md new file mode 100644 index 0000000..1f4761f --- /dev/null +++ b/.claude/session/2026-08-27-03-handtrace-round9-h143-h146.md @@ -0,0 +1,75 @@ +# 2026-08-27 — 9라운드 후속: `/code-review`가 낸 새 메커니즘 넷(`H-143`~`H-146`) 결정·반영 + +**무엇을 했나**: 앞 세션(`session/2026-08-27-02-handtrace-round9-q4-q10.md`)이 +`question.md` 최우선 절에 올려둔 넷을 사용자에게 갈래·권고와 함께 제시하고 한 턴에 +회신을 받아 `base/`·`ROADMAP.md`에 반영했다. 결정과 회신 원문은 +`qa-request/pre-implementation-handtrace-round9-followup.md`의 "`H-143`~`H-146`" +절이 소스. 여기는 흐름과 문서에 안 들어간 것. + +## 흐름 + +1. 컴팩트 직후 넷의 원문(`-round9.md` 620행 이하)과 관련 `base/` 서술(`Rerun`· + `_bindDestroying`·`Observer` 네 진입점·`EpochMap` API·`SlotHandler` retractor· + `H-142` 항목·`slot-plan.md`의 "동적 자식은 반드시" 절)을 읽고, 항목마다 + **트레이스 → 왜 새 메커니즘인가 → 갈래와 결과 → 권고 → 채택 시 고칠 자리**로 + 정리해 올렸다. 하위 결정 둘을 같이 올렸다 — `H-143`의 판정 predicate + (`canExecute` 하나 vs 강한 해제만)와 `H-144`의 `WeakSubscribe` 래퍼 여부. +2. 사용자가 넷 다 (a)에 동의하되 `H-144`에 **재트레이싱을 요청**했다 — + "재구독 뒤 emit이 꼬여도 괜찮은가"와 "재구독 시점에 epoch를 다 맞춰두는 게 + 낫지 않은가(Blocker로 막힌 뒤 늦게 오는 것 처리가 갈린다)", 그리고 Observer도 + 같은 문제가 있는지. +3. 재트레이싱 결과(followup에 기록): 첫 질문은 `fire`의 가드 순서 + (`canExecute`가 `_epochs:Update` **앞**)로 안전, 둘째는 **leaf 경로 + `_bindDestroying`이 이미 `Refresh()` 먼저**라 같은 모양으로 맞추면 된다 — + 게이트 유보 중 재구독 케이스가 그 차이를 실제로 드러낸다. Observer는 epoch가 + 없고 설치 발화도 없어 무관. 래퍼 모양을 제시하고 반영으로 넘어갔다. + +## 시행착오 / 다음 세션이 알아야 할 것 + +- **문서에 이미 있는 모양을 먼저 찾았다.** `H-144`의 래퍼는 새로 발명한 게 + 아니라 `_bindDestroying`(`H-65`)의 꼬리를 그대로 옮긴 것이다 — 앞 세션 교훈 + ("어디에도 없다"고 쓰기 전에 grep)의 반대 방향 적용: 새 메커니즘을 제안하기 + 전에 같은 문제를 이미 푼 자리가 있는지 grep했다. 그 덕에 사용자의 "epoch를 + 맞춰두자"가 곧 기존 캐비엇(287행)의 재발견이라는 걸 바로 연결할 수 있었다. +- `H-145` weak-key는 `InstanceChildHandler`가 5번째 인자를 안 넘기는 **근거 한 + 줄을 무효화**한다(옛 키 잔존) — 결정 하나가 다른 자리의 *근거*를 바꾸는 + 패턴이라 그 자리도 같이 고쳤다. 감사자가 잡을 종류의 stale을 반영 시점에 + 닫은 것. +- `slot-plan.md` 233행 *"v2는 mount 함수 자체가"*는 v1식 `Mount(parent, tree)` + 표면이 v2에 있는 것처럼 읽혔다(`H-146` 발견의 부수) — Slot의 마운트 경로를 + 뜻하는 문장이라 그렇게 고쳤다. + +## 감사 루프가 뒤집은 것 둘 (같은 날, 반영 뒤) + +- **2라운드**: `H-143` 판정식 `not canExecute`가 **생성자 최초 설치를 죽였다** + (바인드·구독 전이라 항상 거짓 → 첫 cleanup 즉시 소진, `_installed` 거짓, 첫 + 바인드에서 `fn` 재실행 = `H-58` 재현). `wasAlive and not canExecute`("실행 중에 + 죽었는가") + 죽은 핸들의 `_pending` 버림으로 정정. 사용자: *"오 그렇네. 실행중 + 죽으면 클린업만 하기, 해당 방식 맞아보여"*. +- **4라운드**: `H-144` 래퍼가 Q4의 "Observer 함수 배정" 위에 얹히면서 + `Observer:Subscribe`의 콜론 위임 `self:WeakSubscribe()`가 `EffectHandle` + 오버라이드로 가 꼬리 2회 + 강한 킵 전 `Rerun`. 사용자가 *"그게 진짜 날 수 + 있어?"*(Effect는 Observer가 아닌데)라 물어 로컬 `luau`로 재현해 보였고, + (a) dot 위임 / (b) 본문 공유 자체를 접기 중 **(b) 확정** — *"내 머리에서 + 나왔던 처음 구조는 그것이였어"*. 원칙 *"하나의 무언가가 두 일을 동작하지 + 않는가에 유의하자"*를 `conventions.md` "설계 원칙"으로 승격. + 사용자가 이어서 *"그게 정확히 invalid after 문제에서 났던 이야기였어"* — 같은 + 원칙의 첫 사례가 8라운드의 `invalidAfter` 두 필드 분리였다는 출처를 원칙 + 항목에 붙였다(필드 하나가 두 뜻 / 함수 본문 하나가 두 타입). +- 교훈: **두 건 다 "반영분 자체가 만든 결함"**이고, 둘 다 "권고 (a)의 의도는 + 맞는데 내가 고른 판정식·배선이 틀린" 종류다. 감사 1라운드(문구 잔존)로는 + 못 잡고 2·4라운드(의미론·수정분 재검토) 각도에서만 나왔다 — 각도를 바꾸는 + 라운드가 실제로 값을 한다는 재확인. + +## 마무리 (2026-08-28) — `/code-review high`와 10라운드로의 이관 + +감사 8라운드 수렴 뒤 `/code-review high` 10건 — 일곱 반영(`-round9-followup.md` +마지막 절), 셋은 판단 필요(`H-147` 죽은 핸들에서 `Rerun` / `H-148` `Parent` 전용 +문구가 새 메커니즘 / `H-149` Observer 위임과 `level 2`). 사용자: *"너무 길어지고 +있어서, round10 으로 옮기고 싶은데 … 인간을 기다리는거 엄청 비효율이라서 … +batch 로 처리될 필요가 있는듯. 지금 결정해야할 분 10 으로 올리자. 커밋하고 +정리한 다음 깔끔한 컨텍스트의 fable 탐사자 하나 띄워서 광범위한 핸드트레이싱, +테스팅, 문제 검사를 하도록 해줘."* → `-round10-brief.md`/`-round10.md` 신설, +커밋 후 탐사자 기동. 교훈: **대화형 처리는 발견 하나당 한 왕복이라 사람 쪽 +대기 비용이 크다** — 라운드 단위 배치가 이 프로젝트의 기본 리듬이고, 이번처럼 +반영분 리뷰에서 문항이 또 나오면 그 자리에서 다음 라운드로 넘긴다. diff --git a/.claude/todos.md b/.claude/todos.md index b9dd2fd..f3cb651 100644 --- a/.claude/todos.md +++ b/.claude/todos.md @@ -47,8 +47,13 @@ `H-142` `Parent` 순서 발견 → props에 `Parent` **금지**로 확정)까지 반영. Q9는 문항 전제가 틀린 것(Tween 절 스케치 한 줄 복사 오류)으로 닫힘. **[2026-08-27 기준] 감사 8라운드·`/code-review high`까지 돌렸다 — 리뷰 10건 중 - 여섯은 반영, 넷(`H-143`~`H-146`, 새 메커니즘)은 `question.md` 최우선 절에 - 판단 대기. 남은 액션: 커밋, 그리고 그 넷의 결정.** 아래는 돌리기 전(2026-08-26) 서술: + 여섯은 반영, 넷(`H-143`~`H-146`, 새 메커니즘)은 `session/2026-08-27-03-handtrace-round9-h143-h146.md`에서 사용자 결정으로 + 전부 권고 (a) 확정·반영(`Rerun` 꼬리 즉시 소진 / 재구독 꼬리 `Refresh` 먼저(진입점은 `EffectHandle` 자기 것) / + `bk.indexOfElement` weak-key / 루트 부착은 금지 범위 밖). **[2026-08-28]** 그 + 반영분에 `/code-review high`가 또 셋(`H-147`~`H-149`)을 냈고, 사용자 판단으로 + **10라운드 문항지**(`qa-request/pre-implementation-handtrace-round10.md`)로 + 올려 신선한 탐사자의 광범위 탐사(지시서 `-round10-brief.md`)와 함께 **배치 + 회신 대기**. 남은 액션: 그 회신 처리 → M2 착수.** 아래는 돌리기 전(2026-08-26) 서술: 지시서는 `qa-request/pre-implementation-handtrace-round9-brief.md`. 스코프는 **커밋 `9dd8213` 하나의 델타**다 — 8라운드 결정 반영과 그 뒤 `/code-review high` **7패스의 수정이 전부 그 커밋에 들어 있고 아무도 diff --git a/CLAUDE.md b/CLAUDE.md index 11a77cd..fdc676c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,9 +13,10 @@ M2(반응형 코어 — Source/State/Store)**. **⚠️ [2026-08-24] M2와 M3의 결정의 소스는 `.claude/qa-request/pre-implementation-handtrace-round8-followup.md` (7라운드 몫은 `-round7-followup.md`; **[2026-08-27] 9라운드 몫은 -`-round9-followup.md` — Q1~Q10·`H-138`·`H-139`·`H-142` 전량 반영 완료**)이고 `.claude/question.md` 최우선 -절엔 **그 반영분에 `/code-review`가 낸 새 메커니즘 넷(`H-143`~`H-146`)이 판단 -대기**로 올라 있다(M2 착수 게이트는 아님). 같은 상태를 `.claude/project-context.md`도 +`-round9-followup.md` — Q1~Q10·`H-138`·`H-139`·`H-142`, 그리고 `/code-review`가 +낸 `H-143`~`H-146`까지 전량 반영 완료**)이고, **[2026-08-28] `.claude/question.md` +최우선 절엔 10라운드 문항지(`-round10.md` §4 — `H-147`~`H-149` + 광범위 탐사 +결과)가 배치 회신 대기**로 올라 있다(M2 착수 게이트는 아님). 같은 상태를 `.claude/project-context.md`도 서술하니 마일스톤이 넘어갈 때 두 곳을 같이 고칠 것. 진행 상황의 소스는 항상 루트 `ROADMAP.md`. diff --git a/ROADMAP.md b/ROADMAP.md index c5dc743..c4babea 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -25,7 +25,12 @@ quad-v2 구현 단계 실행 계획. 설계 근거/아키텍처 자체는 여기 > 순서 / Slot 생성자의 `Offset`·`_baseObserver`·`_destroyed` / `bk.indexOfElement`로 > 토큰 폐기)에 이어 같은 날 Q4~Q10·`H-138`·`H-139`·`H-142`까지 **전량** > 반영됐습니다(`EffectHandle` 진입점 / M2에 `Ref` 최소형 / `InstanceChildHandler` -> 부기 / `reconcile` 배치 Blocker / `New`·`drive` 파이프라인 / props `Parent` 금지). +> 부기 / `reconcile` 배치 Blocker / `New`·`drive` 파이프라인 / props `Parent` 금지), +> 그 반영분에 `/code-review`가 낸 `H-143`~`H-146`(`Rerun` 꼬리 실행 중 사망이면 즉시 +> 소진 / 재구독 꼬리 + 진입점은 `EffectHandle` 자기 것 / `bk.indexOfElement` +> weak-key / 루트 부착은 사용자 몫)도 같은 날 확정·반영. **[2026-08-28]** 그 반영분의 +> `/code-review`가 낸 셋(`H-147`~`H-149`)은 **10라운드 문항지**(`-round10.md`)에서 +> 광범위 탐사 결과와 함께 배치 회신 대기(게이트 아님). > **어느 체크박스가 바뀌었는지는 여기서 세지 않습니다** — 해당 체크박스에 > 각각 `H-1xx` 표시가 붙어 있으니 그게 소스입니다. M2/M3 양쪽에 걸쳐 > 있습니다). M1까지의 산출물은 @@ -493,6 +498,16 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 `EffectHandle:Subscribe()`/`:Unsubscribe()`도 추가(leaf 없이 쓰는 모듈/스크립트 레벨 Effect) — `:Unsubscribe()`는 Observer와 달리 마지막 cleanup을 1회 트리거해야 함(2026-08-07 일곱 번째 세션). + **[2026-08-27 9라운드 `H-143`/`H-144`]** `Rerun` 꼬리는 `fn` 실행 중에 + 핸들이 죽었으면(`wasAlive and not canExecute`) 반환 cleanup을 **즉시 + 소진**하고 `_pending`을 버린다(`fn` 안 `self:Unsubscribe()` 지원 — + `not canExecute` 하나로 판정하면 생성자 최초 설치가 죽는다), 네 진입점은 + **`EffectHandle` 자기 것**(Observer 함수 본문을 배정하지 않는다 — 공유는 + `Observer.luau`의 레지스트리 둘과 `canBound`뿐; 콜론 위임이 오버라이드를 + 타 꼬리가 두 번 도는 걸 감사가 잡아 사용자가 (b)로 확정), `Subscribe`/ + `WeakSubscribe`는 등록 끝에 `_epochs:Refresh()` + `not _installed or + depsChanged → Rerun` 꼬리(재구독 재설치, leaf `_bindDestroying`과 동형) — + 의사코드는 `base/effect-plan.md`. **동적 경로 가드**도 Observer와 같은 패턴으로 등록(`base/effect-plan.md` "동적 경로 가드" 절, 2026-08-14 열한 번째 세션) **⚠️ [2026-08-24] 단 그 가드를 `Dispatch.addHandler`로 등록하는 것 @@ -529,7 +544,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 `:WeakCallback()`으로 걸고, 바인드/언바인드는 dep을 아예 안 건드린다 (`H-58`/`H-59`). 발화 게이트는 전부 **`canExecute(handle)`** 하나다 (`H-7`). 캐치업은 바인드 직후 **조건부 최대 1회** - (`if not self._installed or self._epochs:Refresh() then self:Rerun() end` + (`local depsChanged = self._epochs:Refresh()` **먼저**, 그다음 `if not self._installed or depsChanged then self:Rerun() end` — `or` 한 줄로 단축평가에 걸면 재설치 경로에서 `Refresh()`가 건너뛰어져 다음 emit이 헛돈다, `base/effect-plan.md` 캐비엇 — `H-64`/`H-65`). 의사코드는 `base/effect-plan.md`가 소스 - [ ] **[2026-08-24 `H-23`]** State 전파 루프는 구독자 집합을 **배열로 스냅샷한 뒤** 돈다 — 순회 중 새 구독자 추가가 정상 경로인데 Lua에서 @@ -757,7 +772,9 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 옆으로만 틀려 알아채기 특히 어렵다): (a) `getBookkeeping`이 `bk.offsetCache = {}`, **`bk.offsetCacheValidUpTo = 0`, `bk.offsetSetUpTo = 0`**, `bk.recomputeBlocker = Blocker()`, - `bk.indexOfElement = {}`(**[2026-08-27 Q3]**)으로 + `bk.indexOfElement = setmetatable({}, { __mode = "k" })`(**[2026-08-27 + Q3]**, weak-key는 **[2026-08-27 `H-145`]** — 최상위 Slot 교체 시 해제 + `setLength(…, 0)`이 옛 키를 못 지우므로)으로 초기화(`nil` 시작이면 첫 `setOffsetSource`가 `nil` 비교에서 죽는다), (b) **캐시를 앞으로 당기는 자리**(개수는 `base/dispatch-core-plan.md`의 무효화 표가 소스) — `setLength` 본문과 그 자리 length가 @@ -933,7 +950,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` 항목이 그렇게 갈라 적음), `Handlers/InstanceChild.luau` — +- [ ] `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-27 `H-146`]** 그 거부는 **전용 에러 문구** — "`Parent` is not a prop" 취지 — 를 내고, 루트를 quad 밖 부모에 붙이는 건 사용자가 밖에서 `.Parent =`로 한다(`Mount` 표면 없음, 같은 항목)), `Handlers/InstanceChild.luau` — **⭐ [2026-08-27 9라운드 `H-134`] `InstanceChildHandler`도 말단이라 부기를 등록한다**: `process`에서 `setOffsetSource(inst, k, None)` → `v.Parent = inst` → `setLength(inst, k, 1, inst)`(정적 단일 자식은 상수 @@ -1484,8 +1501,9 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 않는다** — 그 필드 자체가 폐기됐다(`_deps` 하나로 통합). 그리고 `_bindDestroying`은 **`Ref` dep 콜백을 (재)등록하지 않는다** — dep 등록은 생성자에서 끝나고, 여기서 하는 건 `Destroying` 연결과 - **조건부 캐치업 한 줄**(`if not self._installed or - self._epochs:Refresh() then self:Rerun() end`)뿐이다. **이게 `Effect`의 leaf 사망 cleanup을 실제로 + **조건부 캐치업 두 줄**(`local depsChanged = self._epochs:Refresh()` 뒤 + `if not self._installed or depsChanged then self:Rerun() end` — `Refresh()`를 + `or` 뒤에 두면 단축평가로 건너뛰어진다)뿐이다. **이게 `Effect`의 leaf 사망 cleanup을 실제로 발화시키는 유일한 배선**이고, M6의 `_detached` 정리가 여기 의존한다 (그 항목의 `H-50` 각주 참고). 의사코드는 `base/lifecycle-pattern.md`와 `base/effect-plan.md`가 소스. **[2026-08-14