quad/.claude/qa-request/pre-implementation-handtrace-round10.md
qwreey 0ec22fbe73
qa: 9라운드 후속 H-143~H-146 확정·반영 + 감사 8라운드·code-review — 잔여 셋은 10라운드 문항지로
- 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 <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01546hjsYNLSMZdHdPyTZaGb
2026-08-28 00:28:52 +09:00

99 lines
7.7 KiB
Markdown

# 구현 전 손 트레이싱 **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/`.)