quad/.claude/session/2026-08-14-10-dispose-scope-resolved.md
qwreey 573dd452af
docs(base): dispose(value) 시그니처/범위 확정 — question.md 0-B 해소
범위를 Slot+엔진 객체로 좁히고 Observer/Effect는 명시적으로 제외
(GC-native bindLifetime만으로 충분, 트리 부기 없음). isSlot이 아니면
disposeInst 주입 op으로 위임(addTag/removeTag/setAttribute와 같은
패턴). 부수적으로 OnDestroyed 이름 재검토 조건도 종결.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 06:18:28 +09:00

7.9 KiB

2026-08-14 열 번째 세션 — dispose(value) 시그니처/범위 확정 (question.md 0-B 해소)

배경

이전 대화(다른 세션이 동시에 작업 중이라 이 세션은 처음엔 읽기 전용으로 시작)에서 사용자가 question.md 0-B(dispose(any) — 시그니처/범위, 2026-08-13 여섯 번째 세션 신설)에 대해 물었고, 남은 미확정 항목(시그니처, Slot 외 대상 범위, unbindLifetime과의 역할 분담)을 확인하는 것으로 시작했다.

1차 논의 — Observer/Effect 범위 관련 오류와 정정

사용자가 먼저 "Observer/Effect는 dispose 범위에서 빼야 한다"는 결론을 제시하며 근거로 "State<Observer>/State<Effect>가 이미 지원 사양"이라고 주장했다. 어시스턴트는 처음에 이걸 검증하며 modifier-plan.md의 "Modifier 필드에 핸들러 계층 값(Ref/PreRef/PostRef/Observer/Effect/Slot/Modifier)이 들어오면 즉시 error" 규칙과 slot-plan.md의 Slot 원소 금지 규칙을 근거로 "State가 지원된다는 명시적 문서는 없다"고 (잘못) 답했다.

사용자가 "modifier가 왜 나온 말인지 전혀 모르겠다"며 반박 — Modifier 필드 금지 규칙은 이 논의와 무관한 별개 컨텍스트였다. 재조사 결과 사용자 말이 맞았음이 확인됨:

  • base/architecture.md(Leaf.luau 파일 설명)와 base/effect-plan.md가 이미 children 배열의 leaf 위치(k=number)에 Ref/PreRef/PostRef 뿐 아니라 Observer/Effect도 명시적으로 지원 대상으로 확정해뒀음 (Dispatch/Leaf.luau(i:number, v=Observer/Effect)를 매치).
  • base/source-state-plan.md "이중 바인딩 금지" 절이 "children 배열에 Observer를 직접 놓으면 Dispatch/Leaf.luau가 매치해 내부적으로 bindLifetime(inst, observer)를 호출한다"고 이미 명시 — leaf 부착이 곧 bindLifetime 호출 그 자체.
  • base/dispatch-core-plan.mdDispatch/StoreBind.luau는 "범용, 엔진 무관"이고 k는 무엇이든 받음 — 즉 children 배열의 k=number 슬롯에도 똑같이 적용되는 일반 재귀 재디스패치 계층이라, State<Observer>/ State<Effect>가 그 자리에 놓여도 StoreBind가 실체를 뽑아 재귀 Dispatch.process로 넘기고 Leaf.luau가 풀린 값을 매치 — 새 메커니즘 없이 이미 있는 일반 원칙("모든 (inst,k)는 T든 State<T>든 균일하게 처리")의 자연스러운 귀결.
  • Modifier 필드 금지 규칙(modifier-plan.md:240)과 Slot 원소 금지 규칙 (slot-plan.md:81)은 완전히 다른 컨텍스트(Modifier 자신의 필드, Slot:Add/:List의 CRUD 원소)라 children 배열 leaf 위치와 무관함 — 어시스턴트가 이 두 컨텍스트를 혼동한 게 오류의 원인이었음.

어시스턴트가 이 오류를 인정하고 정정, State<Observer>/State<Effect>가 실제로 확정 사양임을 사용자에게 재확인했다.

2차 논의 — 모델을 Opus로 올릴지 질문, 반영 시작

사용자가 "모델을 opus로 올리고 반영 시작할까? 너는 어렵게 느껴? 아니면 가능한 정도야?"라고 질문. 어시스턴트는 이번 반영 작업(정해진 패턴을 따라 문서 여러 곳에 확정된 결정을 박아넣는 기계적 작업)이 Sonnet으로 충분하다고 판단해 답변, CLAUDE.md의 "토큰 맥싱" 방침과도 일치한다고 설명. 사용자가 "가능하다면 문서 반영하고, 세션 기록 남겨. 다른쪽 전부 꺼서, 작업해도 좋음. 핸드오버 준비하고 커밋해줘"로 승인.

최종 확정 — dispose(value)

범위: Slot + 엔진 객체(Instance)만. Observer/Effect는 명시적으로 제외.

  • 이유: Observer/Effect는 children 배열 leaf에서 bindLifetime/ canExecute/unbindLifetime(GC-native, gcconn 기반)로만 관리되고, Slot처럼 "죽는 순간 elementOwner/lengthList/sourceList가 어긋나는" 트리 부기 자체가 없음. dispose가 막아야 하는 문제(quad 내부 자료구조 붕괴)가 Observer/Effect에는 원천적으로 발생하지 않아 dispose가 다룰 이유가 없음. 아무도 안 들고 있으면 그냥 GC, 조기에 끊고 싶으면 unbindLifetime으로 충분.

시그니처: dispose(value: Slot | Instance): ()

function dispose(value)
    if isSlot(value) then
        -- 기존 elementOwner 기반 소유권 판정 재사용: 요구 중이면 error, 아니면 재귀 파괴
        ...
    else
        disposeInst(value)  -- 백엔드 주입 op
    end
end

base/backend 분리: isSlot이 아닌 값은 disposeInst(inst: any): ()로 위임 — base/dispatch-core-plan.md의 "base가 소유하는 핸들러와 주입되는 엔진 op" 패턴(addTag/removeTag/setAttribute가 선례) 그대로 재사용. quad-roblox는 inst:Destroy()로 구현.

네이밍 경위(사용자가 직접 검토): free()는 GC-native 언어 맥락과 안 맞아 기각. Destroy는 엔진 자체 :Destroy() 메소드와 동명이라 사용자가 "그냥 :Destroy() 부르는 거 아님?"으로 착각할 위험이 있어 기각. dispose 유지.

unbindLifetime과의 역할 분담: dispose는 트리 소유권 부기가 있는 대상(Slot/Instance)이 아직 요구되는데 강제로 죽이려는 시도를 막는 것, unbindLifetime은 Observer/Effect류 GC 앵커의 조기 해제 — 축이 달라 대체 불가.

부수 해소 — OnDestroyed 이름 재검토 조건 종결

base/lifecycle-hooks-plan.md가 "0-B가 'quad가 만드는 모든 것의 유일한 파괴 경로'로 풀리면 OnDisposed와 이름을 맞추는 재검토가 자연스러워질 수 있다"는 조건부 열린 항목을 갖고 있었음. 이번 해소로 dispose()의 범위가 오히려 좁아지고(Slot+Instance만) Observer/Effect가 제외됐으므로, 그 조건은 발동하지 않는 쪽으로 영구 종결OnDestroyed가 최종 이름, 용어 정리 대기열에서도 제외.

반영한 문서

  • .claude/question.md — 0-B 섹션 제거(해소), 0-W만 남은 "결정 대기" 항목으로 정리, 1. 용어 정리OnDestroyed 조건부 항목 제거.
  • .claude/archive/question-resolved.md — 0-B 결론+근거 추가(원 문서 형식과 동일하게 "해소됨" 표시, 원문 요약 포함).
  • .claude/base/slot-plan.mddispose(value) 절 전면 갱신(범위/시그니처/ disposeInst 위임/네이밍 근거 확정 반영).
  • .claude/base/dispatch-core-plan.md — "base가 소유하는 핸들러와 주입되는 엔진 op" 절에 dispose/disposeInst를 재사용 사례로 추가.
  • .claude/base/architecture.mdEngineOps.luau 파일 설명에 disposeInst 추가.
  • .claude/base/lifecycle-hooks-plan.mdOnDestroyed 이름 컨벤션 절과 "열린 질문" 절 모두 조건 종결로 갱신.
  • ROADMAP.md — M6 dispose(value) 체크리스트 항목에 확정된 시그니처/ 분기 반영, "0-B 미확정" 문구 제거.
  • HUMAN_TODO.md — 3번 항목에서 0-B를 해소로 갱신.
  • CLAUDE.md — "지금 할 일" 0번 섹션에서 0-B를 해소로 갱신(0-W만 잔여).
  • .claude/README.mdslot-plan.md/lifecycle-hooks-plan.md/ dispatch-core-plan.md 세 행에 이번 세션 반영 내용 추가.

doc-check.py ERROR 0 유지 확인(WARN 84, 편집 전과 동일 — 새로 생긴 불일치 없음).

참고 — 세션 중 발견한 별개 사실

다른 세션(quad-4a)이 동시에 커밋한 2c575d9(PostRef 반영 후 코퍼스 정합성 감사)가 이 세션 시작 시점엔 이미 반영돼 있었음 — 대화 시작 시점의 CLAUDE.md 스냅샷(시스템 리마인더)이 살짝 stale했으므로, 실제 편집 전 git log/파일 재확인으로 최신 상태를 다시 잡고 작업함(세션 번호를 "아홉 번째" 다음인 "열 번째"로 올바르게 잡을 수 있었던 것도 이 재확인 덕분).