fix+docs: /code-review high 10건 처리 — ① 여섯 반영, ② 셋 §4 합류, 기각 하나 (H-218~H-227)

① 반영:
- 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 <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01LF78pXeFGD1ZSVD3ifteYG
This commit is contained in:
qwreey-agent-selene 2026-08-31 16:23:40 +09:00
parent da67a8c009
commit 16f97105b9
No known key found for this signature in database
7 changed files with 203 additions and 106 deletions

View file

@ -996,7 +996,9 @@ function Dispatch.process(inst, k, v, index)
-- (h.process가 재귀하는 동안 잠깐 열려 있는 구간) -- (h.process가 재귀하는 동안 잠깐 열려 있는 구간)
local retractor = h.process(inst, k, v, index) local retractor = h.process(inst, k, v, index)
if retractor == nil then 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 end
slot.retractor = retractor slot.retractor = retractor
else else
@ -1007,7 +1009,7 @@ function Dispatch.process(inst, k, v, index)
list[index] = { handler = h, retractor = NOOP } list[index] = { handler = h, retractor = NOOP }
local retractor = h.process(inst, k, v, index) local retractor = h.process(inst, k, v, index)
if retractor == nil then 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 end
list[index] = { handler = h, retractor = retractor } list[index] = { handler = h, retractor = retractor }
end end
@ -1045,8 +1047,11 @@ end
남아 있었다 — 이 절이 쓰인 뒤(2026-08-13) 확정된 `base/architecture.md` 남아 있었다 — 이 절이 쓰인 뒤(2026-08-13) 확정된 `base/architecture.md`
"error 계약 — `level` 이분과 메시지 언어" 절(*"`base/`의 예시 메시지도 영어로 "error 계약 — `level` 이분과 메시지 언어" 절(*"`base/`의 예시 메시지도 영어로
쓴다"*)이 반영 안 된 것. M3 단위 1 구현과 같은 커밋에서 영어 + `level` 쓴다"*)이 반영 안 된 것. M3 단위 1 구현과 같은 커밋에서 영어 + `level`
정정했다 — retractor 생략은 핸들러(제공자) 쪽 계약 위반이라 호출부를 가리키는 정정했다 — 배열 구멍은 내부 부기 파손이라 그 자리를 가리키는 `1`.
`2`, 배열 구멍은 내부 부기 파손이라 그 자리를 가리키는 `1`. retractor 생략의 `2`는 **잠정 구현 선택**이다(`H-222`, round12 §4 대기) —
`architecture.md`의 계약 표는 "사용자 입력 검증(2) / 내부 불변식 위반(1)"
두 행뿐이라 **"제공자 계약 위반"이 어느 행인지 정한 적이 없다**(이 자리에
한때 그걸 확정 서술처럼 적었었다 — 리뷰 지적으로 잠정 표시).
- **래핑 핸들러는 재-dispatch 전에 아무것도 철거하지 않는다 — 그냥 아래로 - **래핑 핸들러는 재-dispatch 전에 아무것도 철거하지 않는다 — 그냥 아래로
내려보낸다.** `StoreBind`/`NoneHandler`가 하는 일은 이제 한 줄: 내려보낸다.** `StoreBind`/`NoneHandler`가 하는 일은 이제 한 줄:
@ -1182,9 +1187,16 @@ end
`base/tween-plan.md`). 단 **그 자식의 수명은 위임한 핸들러가 책임진다** `base/tween-plan.md`). 단 **그 자식의 수명은 위임한 핸들러가 책임진다**
— Dispatch는 `(child,prop)` 체인이 누구 소유인지 모르므로, 자식을 — Dispatch는 `(child,prop)` 체인이 누구 소유인지 모르므로, 자식을
없앨 때 `retractFrom(child, prop, 1)`까지 부르는 건 위임한 쪽 몫 없앨 때 `retractFrom(child, prop, 1)`까지 부르는 건 위임한 쪽 몫
(자식 Instance 자체를 버리면 `chains``inst`로 weak-keyed라 결국 (**[2026-08-31 정정, `H-218`]** 여기 한때 *"자식 Instance 자체를 버리면
GC되지만, 실행 중인 Tween/구독처럼 즉시 끊어야 하는 게 있으면 명시적 `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`를 거치지 않고 - **`handler.process(inst,k,v,index)``Dispatch.process`를 거치지 않고
직접 호출하는 것은 UB — 반드시 `Dispatch.process`를 통해서만 진입할 직접 호출하는 것은 UB — 반드시 `Dispatch.process`를 통해서만 진입할
것.** 이유: 핸들러 비교·`chains` 저장 bookkeeping이 `Dispatch.process` 것.** 이유: 핸들러 비교·`chains` 저장 bookkeeping이 `Dispatch.process`

View file

@ -277,8 +277,15 @@ PropertyHandler의 "첫 세팅은 애니메이션 없이 즉시"(`prev == nil`)
대한 `retractFrom`을 부르는 것 자체는 여전히 허용된 경로다 대한 `retractFrom`을 부르는 것 자체는 여전히 허용된 경로다
(`base/dispatch-core-plan.md`의 retract 계약 — 금지된 건 같은 `(inst,k)` (`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 — 이 숏핸드도 지원 ## store-bind — 이 숏핸드도 지원

View file

@ -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-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-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-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` = PreRefPostRefRefBrand)이 소스라 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 계약 이전 표기로 남아 있었다 (①) ### `H-212` — base 의사코드 error가 error 계약 이전 표기로 남아 있었다 (①)
@ -68,13 +78,17 @@ quad-debug에 유리" 항목에 있다(처음엔 `research/debug-tooling-plan.md
## §4 배치 문항지 (사용자가 읽을 유일한 자리) ## §4 배치 문항지 (사용자가 읽을 유일한 자리)
**⭐ [2026-08-31 회신 — 단위 1 몫 전량 종결]** 사용자: *"배치 문항은 중간확인 **⭐ [2026-08-31 회신 1]** 사용자: *"배치 문항은 중간확인 완료했어. 전부
완료했어. 전부 권고안에 동의해. 나중에 천천히 반영해줘"* — `H-214`·`H-215` 권고안에 동의해. 나중에 천천히 반영해줘"* — `H-214`·`H-215` 둘 다 **권고 (a)
둘 다 **권고 (a) 채택**, 같은 날 반영 완료(각 행 상태 참고). 열린 문항 0. 채택**, 같은 날 반영 완료(각 행 상태 참고). **그 뒤 `/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-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 항목으로 옮겨 적으면 잊히지 않는다 | | `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 이상 없음 확인 (탐사자·구현이 확인만 하고 문제 없었던 자리) ## §5 이상 없음 확인 (탐사자·구현이 확인만 하고 문제 없었던 자리)

View file

@ -2074,4 +2074,8 @@ Q4(`EffectHandle` 네 진입점 의사코드 — Observer 것 재사용, `Unsubs
3: error 영어화 확장+`H-214` 근거 정정 / 4: `H-216` slot-plan 11곳 / 5: 계수 정정+`H-217` 3: error 영어화 확장+`H-214` 근거 정정 / 4: `H-216` slot-plan 11곳 / 5: 계수 정정+`H-217`
4곳 — base 코드 리터럴 한국어 error 0), §4 배치 회신 도착(*"전부 권고안에 동의해"*) — 4곳 — base 코드 리터럴 한국어 error 0), §4 배치 회신 도착(*"전부 권고안에 동의해"*) —
`H-214` (a) Handler 선택 필드 `name: string?` / `H-215` (a) 스파이크 `04` 폐기(잔여 `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 열린 문항 셋.

View file

@ -65,7 +65,22 @@ debounce-throttle·ref(`"PreRef instance reused"` 문구 통일)·source-state
`04` 폐기·`done/` 이동(잔여 `StoreBind` 경유 재발행 경로는 ROADMAP M4 mock `04` 폐기·`done/` 이동(잔여 `StoreBind` 경유 재발행 경로는 ROADMAP M4 mock
항목에 명시). 테스트·analyze·doc-check 전부 클린. 항목에 명시). 테스트·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에 발견 이어붙임) → 리뷰 반영 커밋 → fable 탐사자(round12.md에 발견 이어붙임) → 사용자에게
사용자에게 "§4를 보라" 한 줄. "§4를 보라" 한 줄(열린 문항: `H-218`/`H-219`/`H-222`).

View file

@ -897,9 +897,9 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
호출, `base/dispatch-core-plan.md` "핸들러 계약" 절, 2026-08-08 세션 호출, `base/dispatch-core-plan.md` "핸들러 계약" 절, 2026-08-08 세션
/ **2026-08-13 다섯 번째 세션에 별도 `retract` 필드가 `process` / **2026-08-13 다섯 번째 세션에 별도 `retract` 필드가 `process`
반환값으로 합쳐지며 대상만 바뀜**) 반환값으로 합쳐지며 대상만 바뀜**)
- [x] **[2026-08-31 M3 단위 1 — 단, 목록의 핸들러 "이름"은 계약에 `name` - [x] **[2026-08-31 M3 단위 1 — 같은 날 `H-214` (a) 사용자 확정으로 계약에
필드가 없어 `H-214`로 §4 대기, `listHandlers`는 임시로 핸들러 객체 선택 필드 `name: string?`(진단 전용)이 추가돼 목록의 "이름"도 닫힘,
배열 반환]** 우선순위 동률/매치 실패 처리(2026-08-12 열일곱 번째 세션 확정, `listHandlers`는 핸들러 객체 배열(사본) 반환]** 우선순위 동률/매치 실패 처리(2026-08-12 열일곱 번째 세션 확정,
`base/dispatch-core-plan.md` "우선순위 동률/매치 실패 처리" 절) — `base/dispatch-core-plan.md` "우선순위 동률/매치 실패 처리" 절) —
`HANDLER_PRIORITY_HIGH`/`_NORMAL`/`_LOW`/**`_FALLBACK`**(base 제공 `HANDLER_PRIORITY_HIGH`/`_NORMAL`/`_LOW`/**`_FALLBACK`**(base 제공
핸들러의 기본 밴드 — 백엔드가 평범한 우선순위로 자기 핸들러를 핸들러의 기본 밴드 — 백엔드가 평범한 우선순위로 자기 핸들러를
@ -976,7 +976,11 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
**[2026-08-31 `H-215` (a)]** 이 항목이 폐기된 스파이크 `04`의 잔여 **[2026-08-31 `H-215` (a)]** 이 항목이 폐기된 스파이크 `04`의 잔여
몫도 진다 — M3 단위 1 `spec.dispatch.luau`는 재귀 재발행을 로컬 몫도 진다 — M3 단위 1 `spec.dispatch.luau`는 재귀 재발행을 로컬
wrapping 핸들러로 근사했으므로, **실제 `StoreBind` 경유** 재발행에서 wrapping 핸들러로 근사했으므로, **실제 `StoreBind` 경유** 재발행에서
깊은 인덱스부터 정리되는 것까지 여기서 실측할 것 깊은 인덱스부터 정리되는 것까지 여기서 실측할 것.
**[2026-08-31 보강, 리뷰 지적]** 순서 단언만으로 닫지 말 것 — `04`
존재 이유로 명시했던 **효과 수준 검증**(retractor가 로그가 아니라
실제로 옛 store 구독을 끊는다: 재발행 후 옛 store 구독 0, 죽은
store를 건드려도 값이 안 덮인다)까지 단언해야 잔여 몫이 닫힌다
## M5 — quad-roblox 최소 프로바이더 ## M5 — quad-roblox 최소 프로바이더

View file

@ -15,8 +15,9 @@
- same doc "핸들러 계약" / "우선순위 동률/매치 실패 처리" — priority scan, - same doc "핸들러 계약" / "우선순위 동률/매치 실패 처리" — priority scan,
first match wins; match failure errors immediately (message built only first match wins; match failure errors immediately (message built only
on failure, with brand if known + typeof + provider guidance); named on failure, with brand if known + typeof + provider guidance); named
priority bands; tie warning printed only when `module.debug`; priority bands; optional diagnostics-only `name` field (`H-214` (a));
`listHandlers` is a pure query (returns, never prints — `D-8`). 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`. - `bind-system-plan.md` "`New(name)(props)` 파이프라인 의사코드" — `drive`.
M3 unit-1 scope (`qa-request/m3-implementation-round12-brief.md` §6): M3 unit-1 scope (`qa-request/m3-implementation-round12-brief.md` §6):
only stage (b), the single generalized `for`, lives here now. The ⓪/⓪' 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 are born); the (a) PreRef/PostRef pre-pass and (c) postRefList come with
M8 (where those values are born). M8 (where those values are born).
Error levels follow `architecture.md` "error 계약": provider/user-side Error levels follow `architecture.md` "error 계약": user-side validation
contract violations point at the caller (level 2), broken internal points at the caller (level 2), broken internal bookkeeping points at
bookkeeping points at itself (level 1). Messages in English. 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") local Relate = require("./Relate")
@ -51,46 +56,46 @@ local HANDLER_PRIORITY_FALLBACK = -1000000
-- message — there is deliberately no reverse lookup on Brand -- message — there is deliberately no reverse lookup on Brand
-- (`archive/brand-shared-registry-reversed.md`), so we ask the module's own -- (`archive/brand-shared-registry-reversed.md`), so we ask the module's own
-- public predicates. Base predicates only; backend-added brands just fall -- 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 = { local BRAND_PROBES = {
"isPreRef",
"isPostRef",
"isRef",
"isSource", "isSource",
"isState",
"isStore", "isStore",
"isState",
"isObserver", "isObserver",
"isEffect", "isEffect",
"isBlocker", "isBlocker",
"isRef",
"isPreRef",
"isPostRef",
"isModifier", "isModifier",
"isEpoch", "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) local function Init(module: any)
-- {[inst(weak)] = {[k] = {[index] = {handler, retractor}}(strong)}} -- {[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 chains = Relate()
local handlers: { Handler } = {} -- kept sorted, higher priority first local handlers: { Handler } = {} -- kept sorted, higher priority first
local NOOP = Void -- `H-162`: the single exported no-op, never a fresh closure 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 function noMatchMessage(k: any, v: any): string
local brand: string? = nil local brand: string? = nil
for _, probeName in BRAND_PROBES do 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" .. " — check that the provider for this value (e.g. quad-roblox) is initialized"
end end
function Dispatch.process(inst: any, k: any, v: any, index: number) local function noRetractorMessage(h: Handler, k: any, index: number): string
-- [순서 주의] list 확보 + chains 등록은 반드시 h.process 호출 *전에* — return `quad.Dispatch: handler {describeHandler(h)} returned no retractor at key {tostring(k)}, index {index}`
-- h.process가 재귀 Dispatch.process(inst,k,…,index+1)를 부르는 게 정상 .. " — return Void when there is nothing to undo"
-- 경로이고(StoreBind/NoneHandler), 그때 chains에 이 list가 아직 없으면
-- 재귀 호출이 자기만의 새 테이블을 만들어 저장한 뒤 바깥이 덮어써서
-- 하위 위임 retractor가 통째로 유실됨(최초 마운트에서 항상 발생).
local list: { Slot } = chains:GetStrong(inst, k) :: any
if list == nil then
list = {}
chains:SetStrong(inst, k, list)
end end
local slot = list[index] -- Pure scan: first handler (in priority order) whose isHandlable accepts.
local h = Dispatch.getHandler(inst, k, v) -- Returns nil on no match — the immediate error is `process`'s job, so the
if h == nil then -- failure message is only ever built there ("에러 메시지는 매치 실패
error(noMatchMessage(k, v), 2) -- 시에만" — 핸들러 계약 절의 지연 생성 규칙).
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 end
if slot ~= nil and slot.handler == h then local function retractFrom(inst: any, k: any, index: number)
-- (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
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)
-- From index (inclusive) to the tail, retracted tail-first (deepest -- From index (inclusive) to the tail, retracted tail-first (deepest
-- index first — LIFO: the shallower slot created the deeper one). -- index first — LIFO: the shallower slot created the deeper one).
-- The argument is always nil here: plain retract with no follow-up. -- The argument is always nil here: plain retract with no follow-up.
@ -172,18 +145,70 @@ local function Init(module: any)
end end
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 -- Tie detection is free at registration time (the registry is sorted
-- here, statically). No tiebreak rule is enforced — ties are usually a -- here, statically). No tiebreak rule is enforced — ties are usually a
-- handler-design mistake, surfaced via debug visibility only. -- handler-design mistake, surfaced via debug visibility only.
if module.debug then if module.debug then
for _, existing in handlers do for _, existing in handlers do
if existing.priority == handler.priority then 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( print(
`quad.Dispatch: handler priority tie at {handler.priority}` `quad.Dispatch: handler priority tie between {describeHandler(existing)}`
.. ` between "{existing.name or "?"}" and "{handler.name or "?"}"` .. ` and {describeHandler(handler)}`
.. " — ties have no defined order; offset from a HANDLER_PRIORITY_* band" .. " — ties have no defined order; offset from a HANDLER_PRIORITY_* band"
) )
break break
@ -196,7 +221,7 @@ local function Init(module: any)
end) end)
end end
function Dispatch.listHandlers(): { Handler } local function listHandlers(): { Handler }
-- Pure query — returns the registered handlers in scan order, prints -- Pure query — returns the registered handlers in scan order, prints
-- nothing (`D-8`); whether to display them is the caller's business. -- 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 -- 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) return table.clone(handlers)
end 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 -- Pipeline stage (b) only in M3 unit 1 (see header). Single generalized
-- `for`: Luau's traversal gives the whole array part before the hash -- `for`: Luau's traversal gives the whole array part before the hash
-- part (`F-4-1`) — that ordering stays a *contract* base promises, and -- part (`F-4-1`) — that ordering stays a *contract* base promises, and
-- quad-base keeps it by leaning on exactly this language behavior -- quad-base keeps it by leaning on exactly this language behavior
-- (spec.drive.luau measures it). Chain entry is always index 1. -- (spec.drive.luau measures it). Chain entry is always index 1.
for k, v in flattened do for k, v in flattened do
Dispatch.process(inst, k, v, 1) process(inst, k, v, 1)
end end
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 module.Dispatch = Dispatch
end end