fix: /code-review high 발견 2건 — plain-table :List 재마운트 크래시 등

quad-doc-auditor 6라운드 수렴 후 /code-review high로 diff 자체를 재검토한
결과. 감사자는 코퍼스 전체 정합성만 보고 diff 결함은 못 잡는다는 게
conventions.md가 이미 명시한 한계인데, 실제로 이번 diff 안의 결함이
나왔다.

- activateList의 재마운트 분기가 bindLifetime(physicalTarget, nil)로
  크래시할 수 있었음. _listObserver는 data가 reactive(State/Source)일 때만
  세팅되고 plain table data(문서가 지원하는 형태)면 영원히 nil인데,
  가드 없이 불렀음. 짝인 unmountSlotTree 쪽은 이미 방어돼 있었던 것과
  비대칭 — 가드 추가.
- "구독 시점" 절에 activateList의 옛 파라미터 이름 inst가 리네이밍 후에도
  남아 있어, 그 절만 읽는 구현자가 physicalTarget 리네이밍의 취지(owner
  키가 Slot일 수도 있는 문맥과의 혼동 방지)를 놓칠 위험 — 정정.

qa-request/pre-implementation-qa-round4-followup.md에 I-8로 기록.
doc-check.py ERROR 0.

Co-authored-by: qwreey <me@qwreey.moe>
This commit is contained in:
qwreey 2026-08-21 13:30:33 +09:00
parent 4622fbeec8
commit d5e5e1a1b9
Signed by: qwreey
GPG key ID: D28DB79297A214BD
2 changed files with 40 additions and 7 deletions

View file

@ -1165,7 +1165,14 @@ function activateList(self, physicalTarget)
-- 자원이 옛 physicalTarget에 매달린 채로 남아, 그게 죽는 순간 -- 자원이 옛 physicalTarget에 매달린 채로 남아, 그게 죽는 순간
-- 살아있는 이 Slot의 `:List`가 조용히 반응을 멈추고(`_listObserver`) -- 살아있는 이 Slot의 `:List`가 조용히 반응을 멈추고(`_listObserver`)
-- 살아있는 detached 요소가 파괴된다(`_detachCleanup`). -- 살아있는 detached 요소가 파괴된다(`_detachCleanup`).
bindLifetime(physicalTarget, self._listObserver) -- [2026-08-21 /code-review high] `_listObserver``data`가 reactive
-- (State/Source)일 때만 세팅된다(아래 `isState(data)` 분기) — plain
-- table `data`(문서가 지원하는 형태)면 영원히 `nil`이라, 가드 없이
-- 부르면 `bindLifetime(physicalTarget, nil)``gchold[nil] = true`
-- 죽는다(`base/lifecycle-pattern.md`의 `bindLifetime` 계약).
-- 언마운트 쪽(`unmountSlotTree`)은 이미 `if slot._listObserver then`
-- 방어돼 있었는데 이 재마운트 분기만 빠져 있었다.
if self._listObserver then bindLifetime(physicalTarget, self._listObserver) end
bindLifetime(physicalTarget, self._detachCleanup) bindLifetime(physicalTarget, self._detachCleanup)
return return
end end
@ -1602,14 +1609,17 @@ Slot:Single(state, updateFn?, opts?)
실제 `data:Observer(fn)` 구독 + 최초 `reconcile`은 Slot 컨테이너 자신이 실제 `data:Observer(fn)` 구독 + 최초 `reconcile`은 Slot 컨테이너 자신이
마운트되는 순간(`Dispatch/Slot.luau`의 `process(inst,k,self,index)` — 위 마운트되는 순간(`Dispatch/Slot.luau`의 `process(inst,k,self,index)` — 위
"`isMounted` 이중 추적 분리" 절이 이미 `self._mounted`를 세팅하는 바로 그 "`isMounted` 이중 추적 분리" 절이 이미 `self._mounted`를 세팅하는 바로 그
지점)에 `activateList(self, inst)`가 수행. `Dispatch.setLength(inst,i, 지점)에 `activateList(self, physicalTarget)`가 수행(**[리네이밍,
self.Length)`를 부르는 것과 같은 자리에서 같이 트리거되면 됨. 2026-08-21]** 2번째 인자 이름은 `inst`였으나 owner 키가 Slot일 수도 있는
문맥과 헷갈리지 않도록 `physicalTarget`으로 통일 — 아래 실제 정의가 소스).
`Dispatch.setLength(inst,i, self.Length)`를 부르는 것과 같은 자리에서 같이
트리거되면 됨.
**`:List()`가 마운트 이후에 불리는 경우 — `self._mounted`면 즉시 활성화 **`:List()`가 마운트 이후에 불리는 경우 — `self._mounted`면 즉시 활성화
(확정)**: 마운트는 1회성 이벤트라, `:List()`가 마운트보다 늦게 호출되면 (확정)**: 마운트는 1회성 이벤트라, `:List()`가 마운트보다 늦게 호출되면
그 이벤트를 기다리는 방식으론 영영 활성화가 안 됨 — `:List()` 그 이벤트를 기다리는 방식으론 영영 활성화가 안 됨 — `:List()`
`self._mounted`를 확인해서 이미 참이면 그 자리에서 바로 `self._mounted`를 확인해서 이미 참이면 그 자리에서 바로
`activateList(self, self._mountedInst)`를 호출(마운트 시점에 `inst` `activateList(self, self._mountedInst)`를 호출(마운트 시점에 물리 target을
`self._mountedInst`로 같이 저장해둠). CRUD와의 상호배타 가드(`self._listed`)와 `self._mountedInst`로 같이 저장해둠). CRUD와의 상호배타 가드(`self._listed`)와
같은 자리에서 자연스럽게 처리됨 — 호출 순서에 대한 새 제약을 추가하지 않음. 같은 자리에서 자연스럽게 처리됨 — 호출 순서에 대한 새 제약을 추가하지 않음.

View file

@ -1,8 +1,9 @@
# 구현 전 QA **4라운드 followup** — 회신 처리 결과 + 재질문 # 구현 전 QA **4라운드 followup** — 회신 처리 결과 + 재질문
**상태**: **[2026-08-21] 4차 처리로 종결 + 반영 후 감사까지 완료 — 아래 **상태**: **[2026-08-21] 4차 처리 + 감사 6라운드 + `/code-review high`
I절이 최신.** 감사 4라운드(의사코드 트레이싱)가 **실제 크래시 3건**을 잡아 까지 전부 완료 — 아래 I절이 최신이자 마지막.** H절이 반영 내용, I절이
같은 날 전부 닫았다(`I-1`~`I-3`). H절이 반영 내용, I절이 그 감사 결과다. 그 뒤의 검증 전량(감사 트레이싱이 잡은 크래시 3건 `I-1`~`I-3`, 사용자가
가져온 이관 `I-7`, `/code-review`가 잡은 2건 `I-8`)이다.
`F-3`이 전량 확인됐고 `attachSlot` 분해도 확정돼 `base/`에 전부 반영됐다. `F-3`이 전량 확인됐고 `attachSlot` 분해도 확정돼 `base/`에 전부 반영됐다.
**이 followup에 열린 질문은 남아있지 않다.** 5라운드 문항지는 만들지 **이 followup에 열린 질문은 남아있지 않다.** 5라운드 문항지는 만들지
않는다(사용자 지시). 아래 A~G절은 거기까지 온 처리 과정의 기록. 않는다(사용자 지시). 아래 A~G절은 거기까지 온 처리 과정의 기록.
@ -1345,3 +1346,25 @@ stale하게 남기지 않고(그 필드는 top-level `claimOwnerAt` 전용),
Slot일 수는 없다 — 그 Slot은 이미 1번째 인자 `self`다. 옆 함수들 Slot일 수는 없다 — 그 Slot은 이미 1번째 인자 `self`다. 옆 함수들
(`materializeSlotTree`/`mountSlotTree`/`attachSlot`)과 이름을 맞춘 (`materializeSlotTree`/`mountSlotTree`/`attachSlot`)과 이름을 맞춘
**순수 리네이밍**. **순수 리네이밍**.
## I-8. `/code-review high` — 2건, 전부 반영
`quad-doc-auditor` 6라운드가 수렴한 뒤 사용자가 `/code-review`(diff 기반)를
직접 돌렸다. `conventions.md`의 "`/code-review`는 감사자를 대체하지 않는다"
항목이 예고한 대로, 감사자 각도(코퍼스 전체 의미론적 정합성)에선 안 보이던
**diff 자체의 결함**이 나왔다 — 둘 다 이번 세션에 새로 생긴 코드다.
1. **`activateList`의 재마운트 분기가 `bindLifetime(physicalTarget, nil)`
크래시할 수 있었다.** `_listObserver``data`가 reactive(State/Source)일
때만 세팅되는데(`isState(data)` 분기), `data`가 plain table이면(문서가
지원하는 형태) 영원히 `nil`이다. 그런데 `I-7`이 넣은 재마운트 분기는
가드 없이 `bindLifetime(physicalTarget, self._listObserver)`를 불렀다 —
`bindLifetime``gchold[value] = true`를 하므로 `value``nil`이면
그 자리에서 죽는다. **짝인 언마운트 쪽은 이미 `if slot._listObserver
then`으로 방어돼 있었는데 이 재마운트 분기만 빠져 있었다** — `I-7`
반영하며 대칭을 놓친 것. 가드 추가로 반영.
2. **"구독 시점" 절에 `activateList`의 옛 파라미터 이름 `inst`가 리네이밍
후에도 남아 있었다.** 이 세션이 `physicalTarget`으로 통일한 이유가
"owner 키가 Slot일 수도 있는 문맥과 헷갈리지 않기 위해서"인데, 정작 그
절의 산문 설명만 안 옮겨져 있으면 구현자가 이 절만 읽고 옛 이름을
그대로 들고 갈 위험이 있었다. 두 곳 정정.