- 신선한 탐사자(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
98 lines
8.3 KiB
Markdown
98 lines
8.3 KiB
Markdown
# 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`의 체인만 쓴다.
|