From d5e5e1a1b9197736a56d4b70acf17f8d16a723a3 Mon Sep 17 00:00:00 2001 From: qwreey Date: Fri, 21 Aug 2026 13:30:33 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20/code-review=20high=20=EB=B0=9C=EA=B2=AC?= =?UTF-8?q?=202=EA=B1=B4=20=E2=80=94=20plain-table=20:List=20=EC=9E=AC?= =?UTF-8?q?=EB=A7=88=EC=9A=B4=ED=8A=B8=20=ED=81=AC=EB=9E=98=EC=8B=9C=20?= =?UTF-8?q?=EB=93=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .claude/base/slot-plan.md | 18 +++++++++--- .../pre-implementation-qa-round4-followup.md | 29 +++++++++++++++++-- 2 files changed, 40 insertions(+), 7 deletions(-) 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일 수도 있는 문맥과 헷갈리지 않기 위해서"인데, 정작 그 + 절의 산문 설명만 안 옮겨져 있으면 구현자가 이 절만 읽고 옛 이름을 + 그대로 들고 갈 위험이 있었다. 두 곳 정정.