diff --git a/.claude/base/slot-plan.md b/.claude/base/slot-plan.md index 45811fa..6dc3ecf 100644 --- a/.claude/base/slot-plan.md +++ b/.claude/base/slot-plan.md @@ -1165,7 +1165,14 @@ function activateList(self, physicalTarget) -- 자원이 옛 physicalTarget에 매달린 채로 남아, 그게 죽는 순간 -- 살아있는 이 Slot의 `:List`가 조용히 반응을 멈추고(`_listObserver`) -- 살아있는 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) return end @@ -1602,14 +1609,17 @@ Slot:Single(state, updateFn?, opts?) 실제 `data:Observer(fn)` 구독 + 최초 `reconcile`은 Slot 컨테이너 자신이 마운트되는 순간(`Dispatch/Slot.luau`의 `process(inst,k,self,index)` — 위 "`isMounted` 이중 추적 분리" 절이 이미 `self._mounted`를 세팅하는 바로 그 -지점)에 `activateList(self, inst)`가 수행. `Dispatch.setLength(inst,i, -self.Length)`를 부르는 것과 같은 자리에서 같이 트리거되면 됨. +지점)에 `activateList(self, physicalTarget)`가 수행(**[리네이밍, +2026-08-21]** 2번째 인자 이름은 `inst`였으나 owner 키가 Slot일 수도 있는 +문맥과 헷갈리지 않도록 `physicalTarget`으로 통일 — 아래 실제 정의가 소스). +`Dispatch.setLength(inst,i, self.Length)`를 부르는 것과 같은 자리에서 같이 +트리거되면 됨. **`:List()`가 마운트 이후에 불리는 경우 — `self._mounted`면 즉시 활성화 (확정)**: 마운트는 1회성 이벤트라, `:List()`가 마운트보다 늦게 호출되면 그 이벤트를 기다리는 방식으론 영영 활성화가 안 됨 — `:List()`가 `self._mounted`를 확인해서 이미 참이면 그 자리에서 바로 -`activateList(self, self._mountedInst)`를 호출(마운트 시점에 `inst`를 +`activateList(self, self._mountedInst)`를 호출(마운트 시점에 물리 target을 `self._mountedInst`로 같이 저장해둠). CRUD와의 상호배타 가드(`self._listed`)와 같은 자리에서 자연스럽게 처리됨 — 호출 순서에 대한 새 제약을 추가하지 않음. diff --git a/.claude/qa-request/pre-implementation-qa-round4-followup.md b/.claude/qa-request/pre-implementation-qa-round4-followup.md index ec1c61a..7adc630 100644 --- a/.claude/qa-request/pre-implementation-qa-round4-followup.md +++ b/.claude/qa-request/pre-implementation-qa-round4-followup.md @@ -1,8 +1,9 @@ # 구현 전 QA **4라운드 followup** — 회신 처리 결과 + 재질문 -**상태**: **[2026-08-21] 4차 처리로 종결 + 반영 후 감사까지 완료 — 아래 -I절이 최신.** 감사 4라운드(의사코드 트레이싱)가 **실제 크래시 3건**을 잡아 -같은 날 전부 닫았다(`I-1`~`I-3`). H절이 반영 내용, I절이 그 감사 결과다. +**상태**: **[2026-08-21] 4차 처리 + 감사 6라운드 + `/code-review high` +까지 전부 완료 — 아래 I절이 최신이자 마지막.** H절이 반영 내용, I절이 +그 뒤의 검증 전량(감사 트레이싱이 잡은 크래시 3건 `I-1`~`I-3`, 사용자가 +가져온 이관 `I-7`, `/code-review`가 잡은 2건 `I-8`)이다. `F-3`이 전량 확인됐고 `attachSlot` 분해도 확정돼 `base/`에 전부 반영됐다. **이 followup에 열린 질문은 남아있지 않다.** 5라운드 문항지는 만들지 않는다(사용자 지시). 아래 A~G절은 거기까지 온 처리 과정의 기록. @@ -1345,3 +1346,25 @@ stale하게 남기지 않고(그 필드는 top-level `claimOwnerAt` 전용), Slot일 수는 없다 — 그 Slot은 이미 1번째 인자 `self`다. 옆 함수들 (`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일 수도 있는 문맥과 헷갈리지 않기 위해서"인데, 정작 그 + 절의 산문 설명만 안 옮겨져 있으면 구현자가 이 절만 읽고 옛 이름을 + 그대로 들고 갈 위험이 있었다. 두 곳 정정.