From 16f97105b96b0e755f1e4a287b0241c430d1f741 Mon Sep 17 00:00:00 2001 From: qwreey-agent-selene Date: Mon, 31 Aug 2026 16:23:40 +0900 Subject: [PATCH] =?UTF-8?q?fix+docs:=20/code-review=20high=2010=EA=B1=B4?= =?UTF-8?q?=20=EC=B2=98=EB=A6=AC=20=E2=80=94=20=E2=91=A0=20=EC=97=AC?= =?UTF-8?q?=EC=84=AF=20=EB=B0=98=EC=98=81,=20=E2=91=A1=20=EC=85=8B=20?= =?UTF-8?q?=C2=A74=20=ED=95=A9=EB=A5=98,=20=EA=B8=B0=EA=B0=81=20=ED=95=98?= =?UTF-8?q?=EB=82=98=20(H-218~H-227)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ① 반영: - H-220: BRAND_PROBES specific-first(isPreRef/isPostRef가 isRef 앞) + 손 복사 목록 확장 주의 주석 — 상위 술어가 앞이라 둘이 도달 불능이었음 - H-223: retractor 생략 error에 위반 핸들러 특정 정보(name/priority/k/index) — h.process 프레임은 반환돼 도달 불가라 메시지가 유일한 단서. 동률 경고도 같은 describeHandler. base 의사코드 동기 - H-227: local Dispatch = {} :: any → 로컬 함수 + 타입 주석 조립로 생산자 표면을 quad-types 선언과 analyze 대조 가능하게 - H-221: ROADMAP 우선순위 체크박스의 stale H-214 주석("§4 대기") 종결로 갱신 - H-224: M4 잔여 몫 문구에 효과 수준 단언(옛 구독 0, stale Set 불전파) 복원 - H-225: 세션 파일에 감사 6라운드 수렴·리뷰 결과 기록 ② §4 합류(증상 확정, 처방 대기 — 거짓 사실 주장만 선반영): - H-218: chains retractor 클로저의 inst 캡처 → H-71 "버킷 값이 weak 키 되참조" 누수 패턴. dispatch-core-plan·ui-shorthand-plan의 "버리면 결국 GC" 주장 정정(retractFrom 의무화 여부는 §4) - H-219: drive 경로 매치 실패 error의 도착지가 내부 프레임 - H-222: "제공자 계약 위반" level 분류가 계약 표에 없음 — H-212 문단을 잠정 표시로 완화 기각: H-226(process 꼬리 병합 리팩터 — 확정 의사코드 1:1 유지 우선, 실측 병목 아님. 메시지 drift 우려는 H-223 공용 함수로 소멸) 감사 6라운드는 da67a8c diff 전수 점검으로 확실 0건 수렴(의심 1건 표현 정밀도만 정정). 테스트·analyze·doc-check ERROR 0 클린. Co-authored-by: qwreey Claude-Session: https://claude.ai/code/session_01LF78pXeFGD1ZSVD3ifteYG --- .claude/base/dispatch-core-plan.md | 26 ++- .claude/base/ui-shorthand-plan.md | 11 +- .../qa-request/m3-implementation-round12.md | 22 +- .claude/session-summary.md | 6 +- ...08-31-02-m3-kickoff-unit1-dispatch-core.md | 19 +- ROADMAP.md | 12 +- quad-base/src/Dispatch/init.luau | 213 +++++++++++------- 7 files changed, 203 insertions(+), 106 deletions(-) diff --git a/.claude/base/dispatch-core-plan.md b/.claude/base/dispatch-core-plan.md index 8cb8bed..84bac1b 100644 --- a/.claude/base/dispatch-core-plan.md +++ b/.claude/base/dispatch-core-plan.md @@ -996,7 +996,9 @@ function Dispatch.process(inst, k, v, index) -- (h.process가 재귀하는 동안 잠깐 열려 있는 구간) local retractor = h.process(inst, k, v, index) if retractor == nil then - error("quad.Dispatch: handler returned no retractor — return Void when there is nothing to undo", 2) + -- 메시지는 핸들러 특정 정보(name/priority)와 k·index를 싣는다(`H-223` — + -- h.process 프레임은 이미 반환돼 어떤 level로도 도달 불가하므로 메시지가 유일한 단서) + error(noRetractorMessage(h, k, index), 2) end slot.retractor = retractor else @@ -1007,7 +1009,7 @@ function Dispatch.process(inst, k, v, index) list[index] = { handler = h, retractor = NOOP } local retractor = h.process(inst, k, v, index) if retractor == nil then - error("quad.Dispatch: handler returned no retractor — return Void when there is nothing to undo", 2) + error(noRetractorMessage(h, k, index), 2) end list[index] = { handler = h, retractor = retractor } end @@ -1045,8 +1047,11 @@ end 남아 있었다 — 이 절이 쓰인 뒤(2026-08-13) 확정된 `base/architecture.md`의 "error 계약 — `level` 이분과 메시지 언어" 절(*"`base/`의 예시 메시지도 영어로 쓴다"*)이 반영 안 된 것. M3 단위 1 구현과 같은 커밋에서 영어 + `level`로 -정정했다 — retractor 생략은 핸들러(제공자) 쪽 계약 위반이라 호출부를 가리키는 -`2`, 배열 구멍은 내부 부기 파손이라 그 자리를 가리키는 `1`. +정정했다 — 배열 구멍은 내부 부기 파손이라 그 자리를 가리키는 `1`. +retractor 생략의 `2`는 **잠정 구현 선택**이다(`H-222`, round12 §4 대기) — +`architecture.md`의 계약 표는 "사용자 입력 검증(2) / 내부 불변식 위반(1)" +두 행뿐이라 **"제공자 계약 위반"이 어느 행인지 정한 적이 없다**(이 자리에 +한때 그걸 확정 서술처럼 적었었다 — 리뷰 지적으로 잠정 표시). - **래핑 핸들러는 재-dispatch 전에 아무것도 철거하지 않는다 — 그냥 아래로 내려보낸다.** `StoreBind`/`NoneHandler`가 하는 일은 이제 한 줄: @@ -1182,9 +1187,16 @@ end `base/tween-plan.md`). 단 **그 자식의 수명은 위임한 핸들러가 책임진다** — Dispatch는 `(child,prop)` 체인이 누구 소유인지 모르므로, 자식을 없앨 때 `retractFrom(child, prop, 1)`까지 부르는 건 위임한 쪽 몫 - (자식 Instance 자체를 버리면 `chains`가 `inst`로 weak-keyed라 결국 - GC되지만, 실행 중인 Tween/구독처럼 즉시 끊어야 하는 게 있으면 명시적 - 정리가 필요). + (**[2026-08-31 정정, `H-218`]** 여기 한때 *"자식 Instance 자체를 버리면 + `chains`가 `inst`로 weak-keyed라 결국 GC되지만"*이라 적혀 있었으나 + **틀렸다** — 체인 리스트의 retractor 클로저가 `inst`를 캡처하므로 버킷 + 값이 weak 키를 되참조해 GC가 안 되고(`relate-plan.md`의 `H-71` 실측 + 패턴 그대로), quad 제작 인스턴스는 gcconn 때문에 애초에 `Destroy`로만 + 회수된다(위 `H-26` 정정과 같은 사실). 즉 명시적 `retractFrom(child, + prop, 1)`은 "즉시 끊어야 하는 게 있으면"이 아니라 **사실상 항상** + 필요하다 — 이걸 위임 핸들러의 계약으로 의무화할지(그리고 + `ui-shorthand-plan.md`의 자식 폐기 경로를 어떻게 고칠지)는 round12 §4의 + `H-218` 대기). - **`handler.process(inst,k,v,index)`를 `Dispatch.process`를 거치지 않고 직접 호출하는 것은 UB — 반드시 `Dispatch.process`를 통해서만 진입할 것.** 이유: 핸들러 비교·`chains` 저장 bookkeeping이 `Dispatch.process` diff --git a/.claude/base/ui-shorthand-plan.md b/.claude/base/ui-shorthand-plan.md index 69a24ac..5af442e 100644 --- a/.claude/base/ui-shorthand-plan.md +++ b/.claude/base/ui-shorthand-plan.md @@ -277,8 +277,15 @@ PropertyHandler의 "첫 세팅은 애니메이션 없이 즉시"(`prev == nil`) 대한 `retractFrom`을 부르는 것 자체는 여전히 허용된 경로다 (`base/dispatch-core-plan.md`의 retract 계약 — 금지된 건 같은 `(inst,k)`에 대한 재진입). 다만 이 자리에서 **필요하지 않다**는 것. -- `chains`는 `child`에 대해 weak-keyed라 자식을 버리면 결국 GC된다 — 명시적 - 정리가 필요한 자원이 이 자리엔 없다. +- ~~`chains`는 `child`에 대해 weak-keyed라 자식을 버리면 결국 GC된다~~ — + **[2026-08-31 정정, `H-218`]** 이 근거는 **틀렸다**: 체인의 retractor + 클로저가 `child`를 캡처해 버킷 값이 weak 키를 되참조하므로 GC가 안 되고 + (`relate-plan.md` `H-71` 실측 패턴), quad 제작 인스턴스는 gcconn 때문에 + `Destroy`로만 회수된다. 위 (a)/(b) 근거(엔진의 Tween 자동 정리 / + `PropertyHandler` retractor가 no-op)는 **정적 값일 때** 그대로 유효하지만, + 숏핸드 값이 **반응형**(`UICorner = state`)이면 `StoreBind` 구독이 chains에 + 남아 자식 파괴/재생성 사이클마다 누적된다 — `UI-11` 결론을 반응형 경로까지 + 유지할지는 round12 §4의 `H-218`이 정한다. ## store-bind — 이 숏핸드도 지원 diff --git a/.claude/qa-request/m3-implementation-round12.md b/.claude/qa-request/m3-implementation-round12.md index a6a83fe..af1f45d 100644 --- a/.claude/qa-request/m3-implementation-round12.md +++ b/.claude/qa-request/m3-implementation-round12.md @@ -21,7 +21,17 @@ | `H-214` | **②** | 1 | 🟡 | `listHandlers`가 "이름/priority를 반환"이고 동률 경고·`quad-debug` 체인 덤프도 핸들러 이름을 원하는데, Handler 계약(3종)엔 `name` 필드가 없다 — 새 필드라 자율 반영 불가 | ✅ **(a) 사용자 확정**(2026-08-31 §4 회신) — 계약에 선택 필드 `name: string?`(진단 전용). `quad-types`·`Dispatch/init.luau`(동률 경고에 이름, `TODO` 마커 제거)·`dispatch-core-plan.md` "핸들러 계약" 절·`spec.dispatch` 11번 반영 | | `H-215` | **②** | 1 | 🟢 | 스파이크 `04`(Dispatch 체인 retractFrom) 처분 — `spec.dispatch.luau`가 검증 대상 대부분을 실측했는데 재귀 재발행 경로는 로컬 wrapping 핸들러 근사라 실제 `StoreBind`(M4) 몫이 남는다. 폐기는 `01`처럼 사용자 승인 사안(감사 2라운드 발견) | ✅ **(a) 사용자 확정**(2026-08-31 §4 회신) — 폐기·`done/` 이동, 잔여 몫(실제 `StoreBind` 경유 재발행)은 ROADMAP M4 mock 테스트 항목에 명시. `STATUS.md`·`README.md`·ROADMAP 재검증 대기 절 `[x]` | | `H-216` | ① | 1 | 🟢 | `slot-plan.md`의 옛 error 예시 11곳(감사 5라운드가 계수 정정 — 처음 12로 잘못 셌다)이 한국어·`level` 없음(대표: `Slot:Add` 범위 검증, 요소 타입 3종, `dispose` 둘) — `H-212`와 같은 부류로, 그 문서 자신이 별도 절에서 "level 2, 영어" 규칙을 알고 있으면서 예시가 안 따라온 자리. M3 단위 2~4·M6이 옮겨 적기 전에 정리(감사 4라운드 발견) | ✅ 반영(`slot-plan.md` — 사용자 입력 검증은 level 2, `releaseOwner` 소유권 추적 파손만 내부 불변식 level 1) | -| `H-217` | ① | 1 | 🟢 | 같은 부류 마지막 4곳 — `attribute-plan.md`(그룹 이중 배치)/`debounce-throttle-plan.md`(Leading·Trailing 둘 다 false)/`ref-plan.md`(`PreRef` 재사용 — 파이프라인 의사코드의 `"PreRef instance reused"`와 다른 문구로 갈라져 있던 것도 통일)/`source-state-plan.md`(`bindLifetime` 이중 바인드 — mock 실구현 문구에 맞춤)가 한국어·`level` 없음(감사 5라운드 전수 스윕) | ✅ 반영 — 전부 영어+level 2. **base/ 코드 리터럴의 한국어 error는 이제 0**(잔여는 산문·옛 모델 인용·주석뿐) | +| `H-217` | ① | 1 | 🟢 | 같은 부류 마지막 4곳 — `attribute-plan.md`(그룹 이중 배치)/`debounce-throttle-plan.md`(Leading·Trailing 둘 다 false)/`ref-plan.md`(`PreRef` 재사용 — 파이프라인 의사코드의 `"PreRef instance reused"`와 다른 문구로 갈라져 있던 것도 통일)/`source-state-plan.md`(`bindLifetime` 이중 바인드 — mock 실구현이 던지는 두 분기 메시지의 공통 접두를 딴 **근사**, 실분기는 그 절의 게이트 스케치 몫)가 한국어·`level` 없음(감사 5라운드 전수 스윕) | ✅ 반영 — 전부 영어+level 2. **base/ 코드 리터럴의 한국어 error는 이제 0**(잔여는 산문·옛 모델 인용·주석뿐) | +| `H-218` | **②** | 1 | 🟡 | (`/code-review high`) `chains` 리스트의 retractor 클로저가 `inst`를 캡처해 **weak 키를 버킷 값이 되참조** — `H-71`이 실측한 "100% 새는" 패턴이라, `dispatch-core-plan.md`의 *"자식을 버리면 결국 GC되지만"* 주장과 그 위에 선 `ui-shorthand-plan.md` `UI-11`의 GC 근거가 **거짓**. 반응형 숏핸드 자식을 파괴/재생성하는 사이클마다 구독·gchold가 누적. 구현은 확정 의사코드에 충실 — 처방(위임 자식 철거 시 `retractFrom` 의무화 여부)이 계약 변경 | 거짓 사실 주장 둘은 정정 완료(`dispatch-core-plan.md`·`ui-shorthand-plan.md`, `H-218` 대기 표시) — **의무화 여부는 §4 대기** | +| `H-219` | **②** | 1 | 🟡 | (`/code-review high`) 매치 실패 `error(…, 2)`의 도착지가 `drive` 경로(가장 흔한 사용자 경로)에선 사용자 코드가 아니라 quad 내부 프레임 — *"프레임 수가 아니라 도착지가 계약"* 위반. 단 `process`는 핸들러 재귀도 받는 공개 진입점이라 level 하나로 두 경로를 다 못 맞춤 — 증상 확정, 처방(재상승 등)은 새 메커니즘 | ⏳ §4 대기 | +| `H-220` | ① | 1 | 🟡 | (`/code-review high`) `BRAND_PROBES`가 상위 술어 `isRef`를 `isPreRef`/`isPostRef`보다 앞에 둬 둘이 도달 불능 — `brand-plan.md`의 술어 합성(`isRef` = PreRef∪PostRef∪RefBrand)이 소스라 specific-first가 답 | ✅ 반영 — 순서 재배열 + "손 복사 목록이라 새 브랜드 마일스톤마다 확장" 주석 | +| `H-221` | ① | 1 | 🟢 | (`/code-review high`) ROADMAP 우선순위 체크박스 주석이 `H-214`를 여전히 "§4 대기·임시 반환"으로 서술 — 같은 커밋에서 종결됐는데 배너가 부정하는 문장을 안 고친 자리 | ✅ 반영 — "(a) 확정으로 닫힘"으로 갱신 | +| `H-222` | **②** | 1 | 🟢 | (`/code-review high`) `H-212` 문단이 *"제공자 계약 위반 → 2"*라는 **계약 표에 없는 제3 분류**를 확정 서술처럼 `base/`에 넣었다 — 표는 "사용자 입력 검증(2)/내부 불변식(1)" 두 행뿐 | 문단은 "잠정 구현 선택" 표시로 완화 완료 — **표 확장 여부는 §4 대기** | +| `H-223` | ① | 1 | 🟢 | (`/code-review high`) retractor 생략 error가 위반 핸들러를 특정할 정보(방금 확정된 `name`·priority·k·index)를 하나도 안 실음 — `h.process` 프레임은 이미 반환돼 어떤 level로도 도달 불가라 메시지가 유일한 단서 | ✅ 반영 — `noRetractorMessage(h, k, index)`(코드+의사코드 동기), 동률 경고도 같은 `describeHandler` 사용 | +| `H-224` | ① | 1 | 🟢 | (`/code-review high`) `H-215`가 M4로 넘긴 잔여 몫 문구가 순서 단언만 요구해, 스파이크 `04`의 존재 이유였던 **효과 수준 검증**(retractor가 실제로 구독을 끊는다)을 떨어뜨림 | ✅ 반영 — ROADMAP M4 mock 항목에 효과 단언(옛 구독 0, stale Set 불전파) 보강 | +| `H-225` | ① | 1 | 🟢 | (`/code-review high`) 세션 파일이 감사 루프를 5라운드에서 멈춘 것처럼 서술(6라운드 수렴·리뷰 반영 미기재) | ✅ 반영 — 세션 파일·summary에 6라운드 수렴과 리뷰 결과까지 기록 | +| `H-226` | 기각 | 1 | 🟢 | (`/code-review high`) `process` (A)/(B) 꼬리 병합·(B) 이중 할당 제거·`retractFrom` 재조회 제거 리팩터 제안 | ❌ 반영 안 함 — 확정 의사코드와의 1:1 유지가 우선이고 실측 병목 아님("실제로 관측된 문제에만 구조"). 메시지 drift 우려는 `H-223`의 공용 `noRetractorMessage`로 소멸 | +| `H-227` | ① | 1 | 🟡 | (`/code-review high`) `local Dispatch = {} :: any`가 생산자 표면을 `quad-types` 선언과 대조 불능으로 만듦 — `H-25`가 막으려던 드리프트가 생산자 쪽에서 무검사 | ✅ 반영 — 로컬 함수 정의 후 `local Dispatch: Dispatch = { … }` 타입 주석 조립(analyze가 표면 검사) | ### `H-212` — base 의사코드 error가 error 계약 이전 표기로 남아 있었다 (①) @@ -68,13 +78,17 @@ quad-debug에 유리" 항목에 있다(처음엔 `research/debug-tooling-plan.md ## §4 배치 문항지 (사용자가 읽을 유일한 자리) -**⭐ [2026-08-31 회신 — 단위 1 몫 전량 종결]** 사용자: *"배치 문항은 중간확인 -완료했어. 전부 권고안에 동의해. 나중에 천천히 반영해줘"* — `H-214`·`H-215` -둘 다 **권고 (a) 채택**, 같은 날 반영 완료(각 행 상태 참고). 열린 문항 0. +**⭐ [2026-08-31 회신 1]** 사용자: *"배치 문항은 중간확인 완료했어. 전부 +권고안에 동의해. 나중에 천천히 반영해줘"* — `H-214`·`H-215` 둘 다 **권고 (a) +채택**, 같은 날 반영 완료(각 행 상태 참고). **그 뒤 `/code-review high`가 +문항 셋(`H-218`/`H-219`/`H-222`)을 새로 올렸다 — 아래 표가 회신 대기.** | 번호 | 무엇 | 선택지 | 권고 | 권고 근거 | |---|---|---|---|---| | `H-214` | `listHandlers`·동률 경고·(나중의) `quad-debug` 덤프가 쓸 핸들러 **이름** — Handler 계약(3종)엔 `name`이 없다 | (a) 계약에 **선택 필드 `name: string?`** 추가 — 있으면 경고·덤프·`listHandlers`가 쓰고 없으면 priority만 / (b) 이름 없이 감 — `listHandlers`는 핸들러 객체 배열만 반환(지금 임시 구현), "이름/priority" 서술을 문서에서 걷어냄 / (c) 다른 방식(별도 등록 인자 `addHandler(h, name)` 등) | **(a)** | `dispatch-core-plan.md` **두 자리**("우선순위 동률/매치 실패 처리"의 `listHandlers` 이름/priority + "부수 효과 — quad-debug에 유리"의 체인 슬롯 이름 덤프)가 이미 "이름"을 전제하고(**[감사 3라운드 정정]** 후자를 처음엔 `research/debug-tooling-plan.md`로 잘못 인용 — 거긴 대신 선택적 `describe` 훅(가칭)이 전례), 선택 필드면 기존 3종 계약을 안 깬다. **(b)를 고르면 두 자리 다 걷어야 한다.** (c)는 이름이 핸들러 자신이 아니라 레지스트리에 살게 돼 체인 슬롯 덤프(슬롯엔 handler 객체만 저장)가 역조회를 또 요구함 | +| `H-218` | **위임 자식 철거 시 `retractFrom(child, prop, 1)`을 계약으로 의무화할지** — chains의 retractor 클로저가 `inst`를 캡처해 자식을 버리는 것만으론 회수 안 됨(`H-71` 패턴, 거짓 GC 주장 둘은 이미 정정). 반응형 숏핸드(`UICorner = state`) 자식 파괴/재생성 사이클마다 구독·gchold 누적 | (a) **항상 의무화** — 위임 핸들러는 자식을 버릴 때 무조건 `retractFrom(child, prop, 1)`(정적 값이면 no-op retractor라 비용 ~0; **`UI-11`의 "필요하지 않다" 결론 일부 역전** — 옛 결정 역전 표시) / (b) 반응형 값이 걸린 자식만 의무(정적은 `UI-11` 유지 — 단 폐기 주체가 값의 반응형 여부를 추적해야) / (c) 다른 방식(chains 구조 변경 등 — 단 리스트 `SetWeak` 전환은 살아있는 체인을 잃는 오답) | **(a)** | 계약이 조건 없이 한 줄이라 어기기 어렵고, 정적 경로 비용이 사실상 0이라 `UI-11`의 실익 논거("불러봐야 하는 일이 없다")와 실충돌이 없다 — 그 논거는 "호출 금지"가 아니라 "요구 안 함"이었고, 새로 드러난 누수가 요구할 이유를 만들었다. (b)는 폐기 주체마다 반응형 추적 부기가 하나 더 생긴다 | +| `H-219` | 매치 실패 `error`의 **도착지** — `drive` 경로(리터럴 props의 미지원 값, 가장 흔한 사용자 실수)에서 level 2가 사용자 코드가 아니라 quad 내부 프레임을 가리킨다. `process`는 핸들러 재귀도 받는 공개 진입점이라 level 하나로 두 경로를 다 못 맞춤 | (a) **지금 유지 + 한계 명시** — 메시지가 key·typeof·브랜드·provider 안내로 자기설명적이라 위치 없이도 진단 가능. M5에서 `New` 파이프라인이 완성돼 drive 경유 프레임 수가 고정되면 재평가 / (b) `drive`가 매치 실패를 잡아 사용자 호출부 level로 재상승 — 단 `pcall` 금지 계약(예외 안전성)과 긴장, 새 메커니즘 / (c) 다른 방식 | **(a)** | (b)는 `pcall`을 안 쓰기로 한 전 자리 계약과 정면 충돌하고, 지금 잃는 건 위치 접두뿐 메시지 자체는 원인을 다 싣는다. 재평가 시점(M5)이 자연스럽게 온다 | +| `H-222` | 제공자(핸들러 작성자) 계약 위반 — retractor 생략·매치 실패류 — 의 **`level` 분류**가 `architecture.md` 계약 표(사용자 입력 2 / 내부 불변식 1)에 없다. 지금 코드는 잠정 2 | (a) **표에 세 번째 행 신설** — "제공자 계약 위반 = 2(그 계약을 어긴 호출 구조에 가장 가까운 프레임)" / (b) 표는 안 늘리고 잠정 2 유지(주석만) / (c) 내부 불변식으로 보고 1 | **(a)** | M3 단위 2~4·M5·M10의 provider-facing error 전부가 같은 분류를 반복해서 물을 자리라, 표에 한 줄 넣는 게 자리마다 잠정 표시를 다는 것보다 싸다. (c)는 "quad 자신의 버그"가 아니라 제공자의 버그라 표의 1행 정의와 안 맞다 | | `H-215` | 스파이크 `04`(Dispatch 체인 retractFrom, `rewrite-required/`) 처분 — `spec.dispatch.luau`가 체인 깊이·레벨별 힌트·3-인자 `retractFrom`·`SetStrong` 음성 대조군을 이미 실측했으나, 재귀 재발행은 spec-로컬 wrapping 핸들러 **근사**다(실제 `StoreBind`는 M4) | (a) 지금 폐기(`01`처럼 `done/` 이동) — 잔여 몫(실제 `StoreBind` 경유 재발행 경로)은 **M4 StoreBind spec이 진다**고 그 단위 계획에 명시 / (b) M4까지 `rewrite-required/`에 유지(현 ROADMAP 문구 "M3 착수 시 같이 처리"를 "M4에서"로 정정) / (c) 지금 재작성 | **(a)** | 체인 메커니즘 자체는 `spec.dispatch.luau` 12절이 실제 구현에 대고 고정했고(스파이크는 격리 재현이라 오히려 약함), `05`/`15`/`01` 폐기와 같은 근거 구조다. 잔여 몫을 M4 spec 항목으로 옮겨 적으면 잊히지 않는다 | ## §5 이상 없음 확인 (탐사자·구현이 확인만 하고 문제 없었던 자리) diff --git a/.claude/session-summary.md b/.claude/session-summary.md index 95b3833..7be7215 100644 --- a/.claude/session-summary.md +++ b/.claude/session-summary.md @@ -2074,4 +2074,8 @@ Q4(`EffectHandle` 네 진입점 의사코드 — Observer 것 재사용, `Unsubs 3: error 영어화 확장+`H-214` 근거 정정 / 4: `H-216` slot-plan 11곳 / 5: 계수 정정+`H-217` 4곳 — base 코드 리터럴 한국어 error 0), §4 배치 회신 도착(*"전부 권고안에 동의해"*) — `H-214` (a) Handler 선택 필드 `name: string?` / `H-215` (a) 스파이크 `04` 폐기(잔여 - `StoreBind` 경유 경로는 ROADMAP M4 mock 항목). 단위 1 §4 열린 문항 0·코드 마커 0. + `StoreBind` 경유 경로는 ROADMAP M4 mock 항목). 감사 6라운드 수렴(확실 0) 뒤 + `/code-review high` 10건(`H-218`~`H-227`): ① 여섯 반영(`BRAND_PROBES` 순서 / + retractor 생략 error에 핸들러 특정 / 타입 주석 조립 등), ② 셋 §4 합류(`H-218` + chains 캡처 누수 — 거짓 GC 주장 정정 선반영 / `H-219` error 도착지 / `H-222` + 제공자 level 분류), 기각 하나(`H-226`). 코드 마커 0, §4 열린 문항 셋. diff --git a/.claude/session/2026-08-31-02-m3-kickoff-unit1-dispatch-core.md b/.claude/session/2026-08-31-02-m3-kickoff-unit1-dispatch-core.md index 708a882..df0d735 100644 --- a/.claude/session/2026-08-31-02-m3-kickoff-unit1-dispatch-core.md +++ b/.claude/session/2026-08-31-02-m3-kickoff-unit1-dispatch-core.md @@ -65,7 +65,22 @@ debounce-throttle·ref(`"PreRef instance reused"` 문구 통일)·source-state `04` 폐기·`done/` 이동(잔여 `StoreBind` 경유 재발행 경로는 ROADMAP M4 mock 항목에 명시). 테스트·analyze·doc-check 전부 클린. +**감사 6라운드(수렴 확인)** — 회신 반영 커밋 `da67a8c`의 diff 전수 점검, +**새 확실 발견 0건**(의심 1건: `H-217` 요약의 "mock 문구에 맞춤" 표현 정밀도 +— "공통 접두를 딴 근사"로 정정). 감사 루프는 여기서 수렴. + +## `/code-review high` — 10건 (`H-218`~`H-227`) + +수렴 직후 리뷰가 10건을 냈다(감사자와 보는 축이 다르다는 관례 그대로). +분류·처리는 round12 요약 표가 소스 — 요지: ① 여섯 반영(`H-220` `BRAND_PROBES` +specific-first / `H-221` ROADMAP stale / `H-223` retractor 생략 error에 핸들러 +특정 정보 / `H-224` M4 잔여 몫에 효과 수준 단언 복원 / `H-225` 이 파일의 루프 +서술 stale / `H-227` `{} :: any` → 타입 주석 조립), ② 셋 §4 합류(`H-218` +chains 캡처 누수 — 거짓 GC 주장 둘은 사실 정정만 선반영 / `H-219` `drive` +경로의 error 도착지 / `H-222` 제공자 계약 위반의 level 분류), 기각 하나 +(`H-226` 꼬리 병합 리팩터 — 확정 의사코드 1:1 유지 우선). + ## 이 다음 -`/code-review high` → 커밋 → fable 탐사자(round12.md에 발견 이어붙임) → -사용자에게 "§4를 보라" 한 줄. +리뷰 반영 커밋 → fable 탐사자(round12.md에 발견 이어붙임) → 사용자에게 +"§4를 보라" 한 줄(열린 문항: `H-218`/`H-219`/`H-222`). diff --git a/ROADMAP.md b/ROADMAP.md index 590d54c..63dfb57 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -897,9 +897,9 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 호출, `base/dispatch-core-plan.md` "핸들러 계약" 절, 2026-08-08 세션 / **2026-08-13 다섯 번째 세션에 별도 `retract` 필드가 `process` 반환값으로 합쳐지며 대상만 바뀜**) -- [x] **[2026-08-31 M3 단위 1 — 단, 목록의 핸들러 "이름"은 계약에 `name` - 필드가 없어 `H-214`로 §4 대기, `listHandlers`는 임시로 핸들러 객체 - 배열 반환]** 우선순위 동률/매치 실패 처리(2026-08-12 열일곱 번째 세션 확정, +- [x] **[2026-08-31 M3 단위 1 — 같은 날 `H-214` (a) 사용자 확정으로 계약에 + 선택 필드 `name: string?`(진단 전용)이 추가돼 목록의 "이름"도 닫힘, + `listHandlers`는 핸들러 객체 배열(사본) 반환]** 우선순위 동률/매치 실패 처리(2026-08-12 열일곱 번째 세션 확정, `base/dispatch-core-plan.md` "우선순위 동률/매치 실패 처리" 절) — `HANDLER_PRIORITY_HIGH`/`_NORMAL`/`_LOW`/**`_FALLBACK`**(base 제공 핸들러의 기본 밴드 — 백엔드가 평범한 우선순위로 자기 핸들러를 @@ -976,7 +976,11 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 **[2026-08-31 `H-215` (a)]** 이 항목이 폐기된 스파이크 `04`의 잔여 몫도 진다 — M3 단위 1 `spec.dispatch.luau`는 재귀 재발행을 로컬 wrapping 핸들러로 근사했으므로, **실제 `StoreBind` 경유** 재발행에서 - 깊은 인덱스부터 정리되는 것까지 여기서 실측할 것 + 깊은 인덱스부터 정리되는 것까지 여기서 실측할 것. + **[2026-08-31 보강, 리뷰 지적]** 순서 단언만으로 닫지 말 것 — `04`가 + 존재 이유로 명시했던 **효과 수준 검증**(retractor가 로그가 아니라 + 실제로 옛 store 구독을 끊는다: 재발행 후 옛 store 구독 0, 죽은 + store를 건드려도 값이 안 덮인다)까지 단언해야 잔여 몫이 닫힌다 ## M5 — quad-roblox 최소 프로바이더 diff --git a/quad-base/src/Dispatch/init.luau b/quad-base/src/Dispatch/init.luau index 76c4f69..7f001fe 100644 --- a/quad-base/src/Dispatch/init.luau +++ b/quad-base/src/Dispatch/init.luau @@ -15,8 +15,9 @@ - same doc "핸들러 계약" / "우선순위 동률/매치 실패 처리" — priority scan, first match wins; match failure errors immediately (message built only on failure, with brand if known + typeof + provider guidance); named - priority bands; tie warning printed only when `module.debug`; - `listHandlers` is a pure query (returns, never prints — `D-8`). + priority bands; optional diagnostics-only `name` field (`H-214` (a)); + tie warning printed only when `module.debug`; `listHandlers` is a pure + query (returns, never prints — `D-8`). - `bind-system-plan.md` "`New(name)(props)` 파이프라인 의사코드" — `drive`. M3 unit-1 scope (`qa-request/m3-implementation-round12-brief.md` §6): only stage (b), the single generalized `for`, lives here now. The ⓪/⓪' @@ -24,9 +25,13 @@ are born); the (a) PreRef/PostRef pre-pass and (c) postRefList come with M8 (where those values are born). - Error levels follow `architecture.md` "error 계약": provider/user-side - contract violations point at the caller (level 2), broken internal - bookkeeping points at itself (level 1). Messages in English. + Error levels follow `architecture.md` "error 계약": user-side validation + points at the caller (level 2), broken internal bookkeeping points at + itself (level 1). Messages in English. The `level 2` on the two + handler-contract violations (no match / no retractor) is a provisional + implementation choice — the contract table has no "provider contract + violation" row and the blame destination through `drive` is an open + question (`H-219`/`H-222`, round12 §4). ]] local Relate = require("./Relate") @@ -51,46 +56,46 @@ local HANDLER_PRIORITY_FALLBACK = -1000000 -- message — there is deliberately no reverse lookup on Brand -- (`archive/brand-shared-registry-reversed.md`), so we ask the module's own -- public predicates. Base predicates only; backend-added brands just fall --- back to typeof. `isEpoch` last (most generic). +-- back to typeof. +-- Order is most-specific-first (`H-220`): `isRef` is a superset of +-- `isPreRef`/`isPostRef` (`brand-plan.md` predicate composition) and +-- `isState` of `isSource`, so subtypes must come before their union +-- predicates or they are unreachable; `isEpoch` last (most generic). +-- ⚠️ This is a hand-copied predicate list (alongside Brand.luau/init.luau/ +-- quad-types) — when a milestone adds `isTag`/`isTween`/`isSlot` etc. to the +-- module surface, extend this list too or the new brand silently falls back +-- to typeof (the `type(predicate) == "function"` guard hides omissions). local BRAND_PROBES = { + "isPreRef", + "isPostRef", + "isRef", "isSource", - "isState", "isStore", + "isState", "isObserver", "isEffect", "isBlocker", - "isRef", - "isPreRef", - "isPostRef", "isModifier", "isEpoch", } +-- Diagnostics label for a handler in error/warning messages — uses the +-- optional `name` contract field (`H-214` (a)), falls back to priority. +local function describeHandler(h: Handler): string + return `"{h.name or "?"}" (priority {h.priority})` +end + local function Init(module: any) -- {[inst(weak)] = {[k] = {[index] = {handler, retractor}}(strong)}} + -- ⚠️ The list values hold retractor closures that capture `inst`, so a + -- chain entry does NOT go away by dropping outside references to `inst` + -- (no ephemerons in Luau — `relate-plan.md` `H-71`); whoever abandons a + -- delegated (inst, k) must retract it explicitly. The base-doc contract + -- wording around this is pending `H-218` (round12 §4). local chains = Relate() local handlers: { Handler } = {} -- kept sorted, higher priority first local NOOP = Void -- `H-162`: the single exported no-op, never a fresh closure - local Dispatch = {} :: any - Dispatch.HANDLER_PRIORITY_HIGH = HANDLER_PRIORITY_HIGH - Dispatch.HANDLER_PRIORITY_NORMAL = HANDLER_PRIORITY_NORMAL - Dispatch.HANDLER_PRIORITY_LOW = HANDLER_PRIORITY_LOW - Dispatch.HANDLER_PRIORITY_FALLBACK = HANDLER_PRIORITY_FALLBACK - - -- Pure scan: first handler (in priority order) whose isHandlable accepts. - -- Returns nil on no match — the immediate error is `process`'s job, so the - -- failure message is only ever built there ("에러 메시지는 매치 실패 - -- 시에만" — 핸들러 계약 절의 지연 생성 규칙). - function Dispatch.getHandler(inst: any, k: any, v: any): Handler? - for _, h in handlers do - if h.isHandlable(inst, k, v) then - return h - end - end - return nil - end - local function noMatchMessage(k: any, v: any): string local brand: string? = nil for _, probeName in BRAND_PROBES do @@ -104,57 +109,25 @@ local function Init(module: any) .. " — check that the provider for this value (e.g. quad-roblox) is initialized" end - function Dispatch.process(inst: any, k: any, v: any, index: number) - -- [순서 주의] list 확보 + chains 등록은 반드시 h.process 호출 *전에* — - -- h.process가 재귀 Dispatch.process(inst,k,…,index+1)를 부르는 게 정상 - -- 경로이고(StoreBind/NoneHandler), 그때 chains에 이 list가 아직 없으면 - -- 재귀 호출이 자기만의 새 테이블을 만들어 저장한 뒤 바깥이 덮어써서 - -- 하위 위임 retractor가 통째로 유실됨(최초 마운트에서 항상 발생). - local list: { Slot } = chains:GetStrong(inst, k) :: any - if list == nil then - list = {} - chains:SetStrong(inst, k, list) - end - - local slot = list[index] - local h = Dispatch.getHandler(inst, k, v) - if h == nil then - error(noMatchMessage(k, v), 2) - end - - if slot ~= nil and slot.handler == h then - -- (A) same handler — leave everything below untouched; hand the new - -- value to the sitting retractor so it does its own transition, then - -- replace this one slot with the fresh retractor. - -- v is guaranteed to satisfy h.isHandlable — getHandler picked h on v. - slot.retractor(v) - slot.retractor = NOOP -- consumed; no double call while h.process recurses - local retractor = h.process(inst, k, v, index) - if retractor == nil then - error("quad.Dispatch: handler returned no retractor — return Void when there is nothing to undo", 2) - end - slot.retractor = retractor - else - -- (B) different handler (or empty slot) — retract this slot and below, - -- then install fresh. - Dispatch.retractFrom(inst, k, index) - -- Occupancy marker first: while h.process recurses, `list` must stay a - -- hole-free sequence for `#list` to be defined (`#` on holey tables is - -- unspecified). - list[index] = { handler = h, retractor = NOOP } - local retractor = h.process(inst, k, v, index) - if retractor == nil then - error("quad.Dispatch: handler returned no retractor — return Void when there is nothing to undo", 2) - end - list[index] = { handler = h, retractor = retractor } - end + local function noRetractorMessage(h: Handler, k: any, index: number): string + return `quad.Dispatch: handler {describeHandler(h)} returned no retractor at key {tostring(k)}, index {index}` + .. " — return Void when there is nothing to undo" end - -- ⚠️ `H-103`: if h.process throws, the NOOP marker above stays permanently — - -- that slot's cleanup is gone and even an explicit retract recovers nothing. - -- Deliberately NOT wrapped in pcall (`architecture.md` "예외 안전성 계약": - -- hot path, and quad does not guarantee bookkeeping integrity after a throw). - function Dispatch.retractFrom(inst: any, k: any, index: number) + -- Pure scan: first handler (in priority order) whose isHandlable accepts. + -- Returns nil on no match — the immediate error is `process`'s job, so the + -- failure message is only ever built there ("에러 메시지는 매치 실패 + -- 시에만" — 핸들러 계약 절의 지연 생성 규칙). + local function getHandler(inst: any, k: any, v: any): Handler? + for _, h in handlers do + if h.isHandlable(inst, k, v) then + return h + end + end + return nil + end + + local function retractFrom(inst: any, k: any, index: number) -- From index (inclusive) to the tail, retracted tail-first (deepest -- index first — LIFO: the shallower slot created the deeper one). -- The argument is always nil here: plain retract with no follow-up. @@ -172,18 +145,70 @@ local function Init(module: any) end end - function Dispatch.addHandler(handler: Handler) + local function process(inst: any, k: any, v: any, index: number) + -- [순서 주의] list 확보 + chains 등록은 반드시 h.process 호출 *전에* — + -- h.process가 재귀 Dispatch.process(inst,k,…,index+1)를 부르는 게 정상 + -- 경로이고(StoreBind/NoneHandler), 그때 chains에 이 list가 아직 없으면 + -- 재귀 호출이 자기만의 새 테이블을 만들어 저장한 뒤 바깥이 덮어써서 + -- 하위 위임 retractor가 통째로 유실됨(최초 마운트에서 항상 발생). + local list: { Slot } = chains:GetStrong(inst, k) :: any + if list == nil then + list = {} + chains:SetStrong(inst, k, list) + end + + local slot = list[index] + local h = getHandler(inst, k, v) + if h == nil then + error(noMatchMessage(k, v), 2) + end + + if slot ~= nil and slot.handler == h then + -- (A) same handler — leave everything below untouched; hand the new + -- value to the sitting retractor so it does its own transition, then + -- replace this one slot with the fresh retractor. + -- v is guaranteed to satisfy h.isHandlable — getHandler picked h on v. + slot.retractor(v) + slot.retractor = NOOP -- consumed; no double call while h.process recurses + local retractor = h.process(inst, k, v, index) + if retractor == nil then + error(noRetractorMessage(h, k, index), 2) + end + slot.retractor = retractor + else + -- (B) different handler (or empty slot) — retract this slot and below, + -- then install fresh. + retractFrom(inst, k, index) + -- Occupancy marker first: while h.process recurses, `list` must stay a + -- hole-free sequence for `#list` to be defined (`#` on holey tables is + -- unspecified). + list[index] = { handler = h, retractor = NOOP } + local retractor = h.process(inst, k, v, index) + if retractor == nil then + error(noRetractorMessage(h, k, index), 2) + end + list[index] = { handler = h, retractor = retractor } + end + end + -- ⚠️ `H-103`: if h.process throws, the NOOP marker above stays permanently — + -- that slot's cleanup is gone and even an explicit retract recovers nothing. + -- Deliberately NOT wrapped in pcall (`architecture.md` "예외 안전성 계약": + -- hot path, and quad does not guarantee bookkeeping integrity after a throw). + -- The (A)/(B) tails are intentionally left duplicated, 1:1 with the + -- confirmed pseudocode ("Dispatch 체인" 절) — merging them was reviewed and + -- declined (`H-226`: no measured bottleneck, and structural divergence from + -- the confirmed shape costs more than it saves). + + local function addHandler(handler: Handler) -- Tie detection is free at registration time (the registry is sorted -- here, statically). No tiebreak rule is enforced — ties are usually a -- handler-design mistake, surfaced via debug visibility only. if module.debug then for _, existing in handlers do if existing.priority == handler.priority then - -- `name` is the optional diagnostics-only contract field - -- (`H-214` (a)) — fall back to priority alone when absent. print( - `quad.Dispatch: handler priority tie at {handler.priority}` - .. ` between "{existing.name or "?"}" and "{handler.name or "?"}"` + `quad.Dispatch: handler priority tie between {describeHandler(existing)}` + .. ` and {describeHandler(handler)}` .. " — ties have no defined order; offset from a HANDLER_PRIORITY_* band" ) break @@ -196,7 +221,7 @@ local function Init(module: any) end) end - function Dispatch.listHandlers(): { Handler } + local function listHandlers(): { Handler } -- Pure query — returns the registered handlers in scan order, prints -- nothing (`D-8`); whether to display them is the caller's business. -- A copy, so callers can't disturb the scan order. Each handler carries @@ -205,17 +230,33 @@ local function Init(module: any) return table.clone(handlers) end - function Dispatch.drive(inst: any, flattened: { [any]: any }) + local function drive(inst: any, flattened: { [any]: any }) -- Pipeline stage (b) only in M3 unit 1 (see header). Single generalized -- `for`: Luau's traversal gives the whole array part before the hash -- part (`F-4-1`) — that ordering stays a *contract* base promises, and -- quad-base keeps it by leaning on exactly this language behavior -- (spec.drive.luau measures it). Chain entry is always index 1. for k, v in flattened do - Dispatch.process(inst, k, v, 1) + process(inst, k, v, 1) end end + -- Assembled with the declared type so luau-analyze checks this producer + -- surface against `quad-types` (`H-227` — a bare `:: any` table left the + -- exact drift `H-25` exists to prevent unchecked on the producing side). + local Dispatch: Dispatch = { + getHandler = getHandler, + process = process, + retractFrom = retractFrom, + addHandler = addHandler, + listHandlers = listHandlers, + drive = drive, + HANDLER_PRIORITY_HIGH = HANDLER_PRIORITY_HIGH, + HANDLER_PRIORITY_NORMAL = HANDLER_PRIORITY_NORMAL, + HANDLER_PRIORITY_LOW = HANDLER_PRIORITY_LOW, + HANDLER_PRIORITY_FALLBACK = HANDLER_PRIORITY_FALLBACK, + } + module.Dispatch = Dispatch end