quad/.claude/audit/handtrace-round10-reference-impl/README.md
qwreey d2d67c7aeb
qa: 10라운드 광범위 탐사 완료 — 발견 H-150~H-157 (🔴 0 / 🟡 5 / 🟢 3), §4 배치 문항 7건 회신 대기
- 신선한 탐사자(fable) 단일 컨텍스트, 지시서 -round10-brief.md §2 레인 A·C 완료, B 부분, D ALL PASS
- audit/handtrace-round10-reference-impl/: round7 참조 구현을 현재 계약으로 갱신·재실행
  (부수: round7/ref9 _recompute 첫 인자 오류 발견·정정)
- base/·인덱스 레이어는 미변경 — 결정은 사용자 배치 회신 뒤 -round10-followup.md로

Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01546hjsYNLSMZdHdPyTZaGb
2026-08-28 01:03:05 +09:00

98 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 10라운드 참고 구현 — `H-107`~`H-149` 계약으로 갱신한 스파이크 모음
**[2026-08-28 신설]** `audit/handtrace-round7-reference-impl/`(7라운드)와 9라운드
스크래치(`core9.luau`/`dispatch9.luau`, 지금은 여기 같이 둠)를 **커밋 `0ec22fb`
시점의 `base/`**(8·9라운드 반영 + `H-143`~`H-146`)로 옮긴 것.
발견 보고는 `qa-request/pre-implementation-handtrace-round10.md`(§5/§7이 이 폴더를
가리킨다). 이 README는 **무엇을 갱신했고 어디서 결과가 나왔는지**만 적는다 —
결정의 소스가 아니다.
실행: `cd spikes && luau <파일>.luau` (타입 스파이크 `ty*``luau-analyze`).
`luau``~/.local/share/mise/installs/luau/latest`(mise). 저장소 코드는 전혀
`require`하지 않는다 — 참고 구현은 `base/`의 의사코드를 손으로 옮긴 것이다.
## 폴더 구성
| 파일 | 무엇 |
|---|---|
| `spikes/core10.luau` | **반응형 코어 참고 구현(이번 라운드)** — 아래 "계약 갱신" 표 |
| `spikes/dispatch10.luau` | **디스패치 부기 + Slot 참고 구현(이번 라운드)** |
| `spikes/core.luau` / `dispatch.luau` / `chain.luau` | 7라운드 원본(그대로 복사, `cp -n`). `chain.luau`(하강 diff 체인)는 `d23`이 그대로 재사용 |
| `spikes/core9.luau` / `dispatch9.luau` | 9라운드 스크래치 원본(그대로) |
| `spikes/t14`~`t24`, `d20`~`d23`, `ty11` | **이번 라운드 신규** — 아래 결과 표 |
| `spikes/d10`/`d11`/`d13`/`d14` `_r10` | 9라운드 `H-124` 스파이크를 `sed``dispatch10`/`core10`에 물린 사본 |
| 그 외 `t1`~`t13`, `d1`~`d18`, `g*`, `e1`, `ty1`~`ty10` | 7·9라운드 원본 그대로(옛 `core`/`dispatch`/`core9`를 `require`함 — 이번에 다시 돌리지 않았고 결과도 그쪽 README/파일이 소스) |
## 계약 갱신 — round7 `core.luau`/ref9 `core9.luau` 대비 `core10.luau`가 바꾼 것
| 항목 | 옛 참고 구현 | `core10.luau` (근거) |
|---|---|---|
| `EpochMap` | `Update`/`Refresh`/`Sync`/`TrackFrom` | + **`Peek`**(`GateNode:_receive`의 emit 판정, `H-124` 이전 7라운드 확정) |
| Observer `fn` | 2-인자 | **3-인자 `fn(targetState, self, emitFrom)`** + `observer._state` 강참조(`H-109`/`H-110`) |
| Observer 진입점 | `Subscribe`/`Unsubscribe` | 넷 — `WeakSubscribe`(프리미티브, `.Subscribed = true`, `H-111`)/`Subscribe`(위임 — `H-149` 그대로 재현하려고 **일부러** 안 고침)/`WeakUnsubscribe`(관대, `H-133`)/`Unsubscribe`(엄격) |
| `Ref` | `Callbacks` 배열 | 해시맵 셋 + **`WeakCallbacks`**(weak-키) + `Uncallback`, `:Set` 순서 값 → 리비전 → 콜백(스냅샷, 교차 dedup), 콜백 `k(value, self)`(`H-107`/`H-108`) |
| `Effect` 생성자 | dep마다 바인드 시 등록 | **생성자에서 한 번**, `_blocker:On()``OffWithoutEmit()`, dep 종류별 클로저 둘(`onRefFire(_, ref)` / `onStateFire(_, _, from)`), 공통 `fire`: `canExecute``_blocker:IsOn``_epochs:Update``Rerun`. `M.effectTrace` 훅으로 가드 판정을 기록할 수 있다(`t18`) |
| `_bindDestroying` | 없음/부분 | `_unbindDestroying()` 선행 → `onDestroying` 연결 → **`Refresh()` 먼저** → `not _installed or depsChanged → Rerun` |
| `Rerun` | 단순 | 루프 머리 `_consumeCleanup``fn` → 꼬리 `wasAlive and not canExecute` 즉시 소진 + `_pending` 드롭(`H-143`) |
| `Effect` 진입점 | 없음 | 네 개 + `resubscribeTail`(`H-144` (b)) |
| `GateNode` | `_receive` `Update`만 | `Update` + `Peek`, 배치 **unfold**해서 `_withheld`에, `_flush(commit) -> boolean`, 빈 배치 무시 |
| `Blocker` | 강한 핸들 목록 | weak 핸들 집합(`H-63`), 강한 주인은 `onUpstreamEmit` 클로저(`Policy(emit)`가 돌려준 핸들을 업밸류로) |
| `state:Block` | 별도 노드 | `Gate(self, function(emit) return blocker:Policy(emit) end)` |
| 생명주기 mock | `bindLifetime`만 | `isEffect(value)``_bindDestroying(inst)`/`_unbindDestroying()`, `onDestroying(inst, fn) -> {Connected, Disconnect}`, `destroyInst`**Immediate** 모드(콜백 동기 실행 후 `conn.Connected = false`) |
`dispatch10.luau``dispatch9.luau` 대비 바꾼 것: `bk.indexOfElement`를 **weak-키**로
(`H-145`), `recompute`의 되감기 판정을 `lengthList[i]` 읽기 **앞**으로(`H-124`,
`continue`), Slot 생성자에서 `Length`/`Offset`/`_baseObserver` 생성 + `_destroyed`
(9라운드 Q2), `materializeSlotTree` 순서(blocker On → `_baseObserver` bind →
`setOffsetSource`), `reconcile``ownsGate` 배치(`H-136`), `mountTop`의 retractor가
`setLength(inst, k, 0)`**요소 없이** 부름(`H-145`의 해제 경로), `rawMove`
`H-29` 규약(배열 permute, `N` 불변, `minPos - 1` 무효화, 이동 범위 `setLength`
재등록)을 탐사자가 옮긴 것(base에 의사코드 없음 — 검증 대상이 아니라 도구).
## 옮기며 고친 것 (참고 구현 자체의 오류 — 발견 번호 없음)
1. **`_recompute`의 첫 인자.** round7 `core.luau`/ref9 `core9.luau`
`fn(resultNode, cache, deps…)`처럼 **결과 노드**를 첫 인자로 넘겼다. base
(`source-state-plan.md`)는 `fn(self, previous?, ...deps)``self`가 **리시버의
lazy 핸들**이다. 옛 스파이크들은 첫 인자를 안 써서 무해했으나, `t16`/`t17`처럼
`fn` 안에서 `s:Get()`을 부르면 결과 노드 자신을 다시 계산해 **스택 오버플로**.
`core10.luau``self.fn(self._hold[1], self.cache, table.unpack(self._hold, 2))`.
2. **`elementOwner`의 값 모드.** 처음 옮긴 `dispatch10`은 값을 강하게 잡아
weak-키 → 값 → 키 순환(Luau엔 ephemeron이 없다, `relate-plan.md`)으로 옛
Slot이 GC되지 않았다(`d20` 첫 실행). `slot-plan.md`가 요소 소유권 절에서 전부 `SetWeak`이라 못박은 대로
`__mode = "kv"`로 고쳤다.
3. **`isState``GateNode` 포함.** `core10.luau``isState``Source`/`State`/
`GateNode` 메타테이블을 직접 나열한다 — base엔 그 등록이 없다. 그게
`H-152`다(`t24`가 빼면 어떻게 되는지 재현).
4. 브랜드는 `Brand` 대신 **메타테이블 identity**로 판별한다(스파이크 단순화) —
`t24`는 이 단순화를 이용해 "브랜드 누락"을 흉내낸다.
## 결과 (2026-08-28, 전부 종료 코드 0)
| 스파이크 | 확인 대상 | 결과 → 발견 |
|---|---|---|
| `t14_effect_state_matrix` | 네 진입점 × 상태 전이표 | 문서 표와 일치 |
| `t15_rerun_dead_handle` | `H-147` 케이스 1·2·3 | 재현. 케이스 3만 `BindData` 죽은 gcconn으로 `wasAlive` 참 |
| `t16_gate_resub_refresh` | 게이트 × 재구독/재바인드 × `Refresh`/`Peek` 7건 | 1·2(캐치업 우회)·4(형제 dep) → `H-151`; 나머지 확정대로 |
| `t17_hold_gc` | `_hold` 불변식 | 양성/음성 확정대로 |
| `t18_blocker_vestigial_and_set_same` | `Effect._blocker` / 같은 값 `Set` | `drop:canExecute` 3, `drop:blocker` **0**`H-150` |
| `t19_observer_level2` | `H-149` | `Subscribe`만 내부 줄 blame — 재현 |
| `t22_fn_destroys_inst` | `H-143` 동기 자기 파괴 | cleanup 즉시 1회 |
| `t23_store_reserved_runtime` | `Of("Of")`/`defaults.Of` × 구현 (I)/(II) | 둘 다 깨짐 → `H-153` |
| `t24_gate_brand_missing` | 브랜드 누락 | 발화 2 → 0, `Get` 정상 → `H-152` |
| `ty11_store_final`(analyze) | 최종형 Store 타입 + 빈 Store | 진단: 56행 예약 키(H), 43/45/46/52행 음성; 나머지 0 → `H-157` |
| `d10`/`d11`/`d13`/`d14` `_r10` | `H-124` | 통과 |
| `d20_indexOfElement_weak` | `H-145` | 키 수거 확인(테이블 키 한정 — `Instance`는 CLI 밖) |
| `d21_reconcile_batch` | `H-136` + 중첩 Slot | 사이클당 `recompute(L)` 1, 물리 op 최소 |
| `d22_detach_keygone` | `Detach`/`KeyGone` | 확정대로 |
| `d23_instancechild_spurious` | `InstanceChildHandler` 같은 값 | `Parent` nil→inst 2회, `recompute` 2 → `H-154` |
## 한계
- 물리 층은 전부 mock(`D.native` 로그) — Roblox `Instance`의 weak 키 거동,
`SignalBehavior = Deferred`, `ChildAdded`류 이벤트 횟수는 여기서 볼 수 없다
(발견 보고 §6).
- `Debounce`/`Throttle`은 옮기지 않았다(7절이 무효화 배너 아래라 확정 의사코드가
없음). 게이트 정책은 `Blocker:Policy`까지만.
- `Dispatch.drive`/`New` 파이프라인(`H-139`)은 옮기지 않았다 — `d23`은 round7
`chain.luau`의 체인만 쓴다.