From b228efc2a6f65034b4f3ae2357e957e035687fc1 Mon Sep 17 00:00:00 2001 From: qwreey Date: Thu, 13 Aug 2026 17:14:58 +0900 Subject: [PATCH] =?UTF-8?q?docs(audit):=20=EC=BD=94=ED=8D=BC=EC=8A=A4=204?= =?UTF-8?q?=EC=B0=A8=20=EA=B0=90=EC=82=AC=20=E2=80=94=200-Y/0-Z=20?= =?UTF-8?q?=ED=8F=AC=EC=9D=B8=ED=84=B0=20=EB=88=84=EB=9D=BD=20+=20?= =?UTF-8?q?=ED=8C=8C=EA=B4=B4/=EC=96=B8=EB=A7=88=EC=9A=B4=ED=8A=B8=20?= =?UTF-8?q?=EC=9A=A9=EC=96=B4=20=EC=9E=94=EC=A1=B4=20=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 3차 감사(에이전트 5개, "수렴 확인" 초점)에서 발견한 것 정리: - bind-system-plan.md: trailing-deps 절이 self-lazy-핸들 계약(0-Y) 위에 얹혀 있는데, 실제 0-Y 배너는 650줄 뒤에 있어 위에서부터 읽으면 확정으로 오인하기 쉬웠던 것 — 절 앞에 포인터 추가 - modifier-plan.md: State 필드 transform 콜백이 같은 0-Y 계약에 의존하는데 파일 전체에 0-Y 언급이 전혀 없던 것 — 포인터 추가 - question.md: "retract 필드 생략 불가"라는 요약 표 항목이 2026-08-13 다섯 번째 세션에 폐기된 2-메소드(process+retract 필드) 계약을 그대로 서술하던 것 정정(현재는 process가 반환하는 클로저) - tag-plan.md: 최상단 배너("0-Z 해소 전엔 옛 모델")와 정면 모순되는 "상태" 헤더의 "열린 질문 없음"이 지난 라운드에 하단 절만 고쳐지고 같은 문제가 상단에도 남아있던 것 정정 - attribute-plan.md: "열린 질문" 목록에 0-Z가 아예 안 실려있던 것(본문 다른 곳엔 있었음) — 추가 - slot-plan.md: reconcile의 "버림"/"다시 그림" 갈래 설명 2곳이 여전히 "실제로 파괴됨"을 현재형으로 서술, filter/toggle 절의 캐비엇 바로 다음 문장("해법")이 그 캐비엇과 반대로 "진짜 파괴"를 재주장하던 자기모순 정정 - documentation-content-map.md: 같은 파일에서 stale Tag 해시키 모델, stale Slot destroy/portal 서술, Tween 중복 stale 언급 추가로 정정 (지난 두 라운드에서 놓친 곳들) Co-Authored-By: Claude Sonnet 5 --- .claude/base/attribute-plan.md | 5 +++++ .claude/base/bind-system-plan.md | 7 +++++++ .claude/base/modifier-plan.md | 6 ++++++ .claude/base/slot-plan.md | 16 +++++++++++----- .claude/base/tag-plan.md | 16 +++++++++------- .claude/question.md | 2 +- .claude/research/documentation-content-map.md | 8 ++++---- 7 files changed, 43 insertions(+), 17 deletions(-) diff --git a/.claude/base/attribute-plan.md b/.claude/base/attribute-plan.md index 633dbcf..ae11dba 100644 --- a/.claude/base/attribute-plan.md +++ b/.claude/base/attribute-plan.md @@ -414,6 +414,11 @@ UICorner 숏핸드/Tween/Tag와 같은 판단 재사용 — `quad-roblox` 코어 ## 열린 질문 (`.claude/question.md`에도 취합) +- **[2026-08-13 3차 감사에서 보강] `hintValue`/`retractFrom` 재-dispatch + 메커니즘은 `question.md` **0-Z**(이 문서 자신의 이름 소유권 결정) + 해소 대기 중** — 이 문서 최상단 배너와 "이름 소유권" 절에 이미 + 명시돼 있지만, 이 목록에는 빠져 있어서 여기만 훑는 독자가 놓치기 + 쉬움. `research/dispatch-redispatch-diff-plan.md` 먼저 읽을 것. - **이름은 잠정 확정, 최종 확정은 대기열**: 겹침 방지를 위해 그룹 값은 `Attribute`, 단일 키는 `AttributeKey<>`로 코드/문서 전체 통일해서 당장의 해석 모호성은 없앴음 — 그래도 최종 이름은 다른 가칭들(`State`/ diff --git a/.claude/base/bind-system-plan.md b/.claude/base/bind-system-plan.md index 878439a..162fcd6 100644 --- a/.claude/base/bind-system-plan.md +++ b/.claude/base/bind-system-plan.md @@ -1808,6 +1808,13 @@ Tag/Modifier의 클론은 호출 즉시 결과가 확정되는 값이라 "-ed"( ### trailing deps를 `fn`에 lazy positional 인자로도 노출 — 방향+순서(`fn(self, previous?, ...deps)`) 확정, 이형 다중 deps 표현 가능 여부만 실측 필요 (2026-08-11 후속) +> **⚠️ [2026-08-13 3차 감사에서 발견] 이 절도 아래 "self도 lazy 핸들로 +> 통일"과 같은 계약(`question.md` **0-Y**) 위에 직접 얹혀 있음** — self가 +> lazy `State` 핸들이라는 전제가 흔들리면 trailing deps를 같은 방식으로 +> 노출한다는 이 절의 결론도 같이 흔들림. 0-Y 배너는 이 문서 아래쪽(이 +> 절보다 한참 뒤)에 있어 위에서부터 읽으면 이 절을 확정으로 오인하기 +> 쉬움 — 실제 배너는 "self 인자도 lazy 핸들로 통일" 절 참고. + **문제 제기(사용자)**: `:Compute(fn, a, b, c)`가 이미 `a,b,c`를 trailing args로 받아 구독을 건다면, 그 값을 `fn(self, a, b, c)`처럼 위치 인자로도 그대로 넘겨줘도 되지 않는가 — `:With`가 값을 포지셔널로 안 주는 이유는 diff --git a/.claude/base/modifier-plan.md b/.claude/base/modifier-plan.md index ef533b2..7fbd15d 100644 --- a/.claude/base/modifier-plan.md +++ b/.claude/base/modifier-plan.md @@ -170,6 +170,12 @@ raw 값, State면 State 핸들 그 자체(아래 4-1 표의 "State + 함수" 행 없이 그냥 지금 들고 있는 걸 그대로 준다는 원칙 하나로 이 절과 4-1절 표가 전부 설명됨. +> **⚠️ [2026-08-13 3차 감사에서 발견]** 위 "State 핸들 그 자체"/`field:Compute(fn)` +> 위임은 `:Compute`/`:With`의 self-lazy-핸들 계약을 그대로 물려받음 — +> 그 계약 자체가 Luau 추론과 충돌한다는 게 `question.md` **0-Y**로 열려 +> 있음(`base/bind-system-plan.md`의 동일 배너). 0-Y 결론에 따라 이 절의 +> "State + 함수" 위임 방식도 같이 바뀔 수 있음. + **별도 `func(state) -> state` 인자 모양은 불필요(검토 후 기각).** "여러 Compute를 합치고 싶다"는 동기였는데, 이미 두 가지로 다 커버됨: (1) 여러 계산을 합치고 싶으면 변환 함수 본문 안에서 다른 함수를 그냥 호출하면 diff --git a/.claude/base/slot-plan.md b/.claude/base/slot-plan.md index c4081f3..4a96b45 100644 --- a/.claude/base/slot-plan.md +++ b/.claude/base/slot-plan.md @@ -833,13 +833,16 @@ fail-fast 톤으로 그 자리에서 막음 — `keyFn` 작성자(주로 위 `it 되는 식): - **버림** — 첫 번째 값으로 `nil`(또는 `None`, 동일 취급)을 반환. "지금 이 key는 렌더 안 함"(filter 탈락 등) — `prev`가 있었다면 - 실제로 파괴됨(단순 `Visible = false` 아님 — 아래 참고). 편의상 + **[정정, 2026-08-13 3차 감사] 파괴가 아니라 언마운트됨**(단순 + `Visible = false`도 아님 — 위 "구현" 절 `rawUnmount` 참고, 이 + 문단 작성 당시엔 아직 destroy 모델이었음). 편의상 `nil` 권장(반환값이 raw Slot 요소로 직접 들어가는 게 아니라 `:List`의 reconcile이 해석만 하므로 "요소 타입 제약"의 raw `nil`/`None` 금지와 안 부딪힘). - **다시 그림** — `prev`와 다른(보통 새로) 만든 값을 반환. 첫 렌더 (이 key 최초 등장) 또는 의도적 전체 교체 — `prev`가 있었다면 그건 - 파괴되고 새 값이 그 자리를 대신함. 이 갈래에서 반응형 값(예: + **[정정, 2026-08-13 3차 감사] 파괴가 아니라 언마운트되고** 새 값이 + 그 자리를 대신함. 이 갈래에서 반응형 값(예: `LayoutOrder`용 `Source`)이 필요하면 **항상 새로 만들어서** 처음부터 올바른 값으로 시작해야 함, 이전 `userdata`에 뭐가 남아있었든 재사용/`Set`하면 안 됨(아무도 안 구독하는 상태라 무의미한 연산). @@ -957,9 +960,12 @@ end **해법**: `updateFn`을 매 사이클 호출하되, `prev`를 줘서 "바꿀 게 없으면 그대로 돌려주기만 하면 되는" 저렴한 경로를 만들고, filter 탈락은 `nil` -반환으로 **진짜 파괴**되게 함 — Visible 토글이 아니라 실제 Remove. -200개 중 20개만 통과하는 필터면 20개만 실제로 살아있고 나머지 180개는 -정말로 존재하지 않음(애니메이션도 안 돎). +반환으로 **[정정, 2026-08-13 3차 감사] 언마운트**되게 함(위 캐비엇대로 +파괴 아님) — Visible 토글이 아니라 실제 물리 트리 이탈. 200개 중 20개만 +통과하는 필터면 20개만 실제로 마운트돼 있고 나머지 180개는 물리적으로 +존재하지 않음(애니메이션도 안 돎, 다만 아무도 안 들고 있지 않은 한 +GC되기 전까지 `nil` 아닌 언마운트된 채로 재사용 가능하게 남아있을 수 +있음 — 위 캐비엇 참고). **"이전 상태를 다음 호출에 어떻게 넘기냐" 문제는 `userdata`가 그 채널** — item이 plain table이라 매번 `Source`를 새로 안 만들고 재사용하려면 그 diff --git a/.claude/base/tag-plan.md b/.claude/base/tag-plan.md index 02dcb5d..b297c50 100644 --- a/.claude/base/tag-plan.md +++ b/.claude/base/tag-plan.md @@ -10,13 +10,15 @@ > 구현하면 옛 모델로 짜게 됨** — 반드시 > `research/dispatch-redispatch-diff-plan.md`를 먼저 읽을 것. -**상태**: base — 2026-08-08 세 번째 세션에서 값 모양을 전면 재설계(구 -모델은 `archive/tag-hash-key-model-reversed.md`에 원문·역전 이유 보존). -2026-08-12 열한 번째 세션에 `TagHandler`의 `process`/`retract` 메커니즘을 -참조 카운트 기반으로 전면 정정(옛 버전은 `archive/ -retract-always-fires-reversed.md`). 2026-08-12 열다섯 번째 세션에 -`Added`/`Removed`를 단일 이름에서 `string | {string}`으로 정정(아래 값 -모양 절). 새 결정만 반영, 열린 질문 없음. +**상태**: base — 값 모양/이름은 확정. 2026-08-08 세 번째 세션에서 값 +모양을 전면 재설계(구 모델은 `archive/tag-hash-key-model-reversed.md`에 +원문·역전 이유 보존). 2026-08-12 열한 번째 세션에 `TagHandler`의 +`process`/`retract` 메커니즘을 참조 카운트 기반으로 전면 정정(옛 버전은 +`archive/retract-always-fires-reversed.md`). 2026-08-12 열다섯 번째 +세션에 `Added`/`Removed`를 단일 이름에서 `string | {string}`으로 +정정(아래 값 모양 절). **단, 위 배너대로 `hintValue`/`retractFrom` 재-dispatch +메커니즘은 `question.md` 0-Z 해소 대기 중 — "열린 질문 없음"은 값 +모양/이름에 한정.** ## 왜 재설계됐나 diff --git a/.claude/question.md b/.claude/question.md index ddee280..7949bb9 100644 --- a/.claude/question.md +++ b/.claude/question.md @@ -504,7 +504,7 @@ context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만** | Blocker(값 기반 emit 지연/합치기) | `base/blocker-plan.md` | | Effect(설치+확정 정리, `state` 있으면 Observer 조합해 재실행도 지원 — 확정) | `base/effect-plan.md` | | `Relate`(inst-weak 릴레이션 프리미티브, `SetWeak`/`GetWeak`/`SetStrong`/`GetStrong`), `bindLifetime`/`canExecute`(inst,value) 탑레벨 함수 | `base/relate-plan.md`, `base/lifecycle-pattern.md` | -| `retract` 필드 생략 불가(no-op 허용, 누락 시 핸들러 교체 순간 크래시), store-bind 재실행은 `state:Observer(fn):Subscribe()` 재사용 | `base/bind-system-plan.md` | +| **[2026-08-13 3차 감사에서 정정]** retract 생략 불가(no-op 허용, 누락 시 핸들러 교체 순간 크래시) — 2026-08-13 다섯 번째 세션부터 별도 `retract` 필드가 아니라 `process`가 반환하는 클로저, 자리만 옮겨왔을 뿐 원칙은 동일. store-bind 재실행은 `state:Observer(fn):Subscribe()` 재사용 | `base/bind-system-plan.md` | | UICorner/UIPadding/UIScale 인라인 편의 키 — 이름·메커니즘·store-bind 가능성까지 확정 | `base/ui-shorthand-plan.md` | | Batch(lexical) 기각, Context(+레이어드 Store) 기각 | `archive/batch-rejected.md`, `archive/context-rejected.md` | | Fusion/Vide 비교 리서치(주의: 일부 서술은 이후 라운드에서 뒤집힘, 문서 내 정정 표시 참고) | `reference/comparison-fusion-vide.md` | diff --git a/.claude/research/documentation-content-map.md b/.claude/research/documentation-content-map.md index f691ced..c515376 100644 --- a/.claude/research/documentation-content-map.md +++ b/.claude/research/documentation-content-map.md @@ -26,7 +26,7 @@ 1. **초기화** — `RobloxFactory(QuadBase)`로 base+backend 조립 (`module-lifecycle-plan.md`, `bind-system-plan.md`) 2. **Instance 만들기** — DOMless 즉시 생성 모델, 제네릭 `new` + 자주 쓰는 ~25개 클래스 정적 필드(`Frame`, `TextButton` 등) (`architecture.md`, `bind-system-plan.md`) -3. **속성 채우기** — `[Attribute "Name"]`, `[Tag ""] = true` 특수 바인드 키 (`architecture.md`) +3. **속성 채우기** — `[Attribute "Name"]`, ~~`[Tag ""] = true`~~ **[2026-08-13 정정] 구모델(폐기, `archive/tag-hash-key-model-reversed.md`) — 실제로는 `Tag(...)` array-part 값 객체** 특수 바인드 키 (`architecture.md`) 4. **반응형 기초** — `Source`/`Store` 생성, `store.key`(dot-access)로 Source 읽기(Source는 State를 만족), `store.key:Set(value)`로 쓰기, State는 항상 읽기 전용 (`bind-system-plan.md`, `store-semantics.md`; 2026-08-06 후속 세션에서 dot-access가 Source를 직접 반환하고 쓰기가 `:Set()`으로 바뀜) 5. **스타일링** — Modifier 기본 체이닝(`:FontSize(14)`), 배열/인라인 merge 우선순위 규칙 (`modifier-plan.md`) 6. **자식 전달** — Slot 기본 개념(children 배열, add/remove/clear), 마운트된 slot 재마운트 시 throw (`slot-plan.md`) @@ -92,8 +92,8 @@ v1 폐기 API/버그/구조 결함 전부 v2 설계를 정당화하는 내부 같은 프레이밍의 특수 케이스로 소개 — "1개 아니면 0개"의 동적 렌더일 뿐, 별도 개념 아님. - 초심자: Modifier 기본 체이닝+merge 우선순위 규칙 실제 예시 / Slot 기본 개념(children 배열)+클래스가 슬롯 받는 방법(Named Slot 없음) / 마운트된 slot 재마운트 시 즉시 throw -- api: Setter가 리터럴/변환 함수 둘 다 받음(→심화: getter 없는 이유) / 필드가 State일 수 있는 4가지 조합 표(→심화: 반응성 유지/끊김 이유) / `mod:UICorner(8)` dot-access 생성자 관습 / Slot은 인스턴스당 여럿 가능 / 중첩 인스턴스 자식 처리 / retract 시 slot 내용 폐기(→심화: portal 없는 이유) / `:Apply(factory)` 기본 체이닝 관용구(→심화: 언제 `Apply` vs `Overridden`인지 성능 기준) / `:Peek<>(key)` + `isState`(→심화: `Get`과 이름을 다르게 한 이유) -- 심화: 정적 merge vs 런타임 pluggable 기각 이유(CSS cascade) / immutable+clone 체이닝 이유(형제 오염 방지) / getter 미채택 이유 / `__index` 런타임 구현 통찰 / Modifier가 핸들러 계층을 모르는 이유 / base/roblox 패키지 경계(Dispatch/Slot vs Handlers/Slot) / Slot 단일 마운트 소유권이 v1/Fusion/Vide 대비 개선인 이유 / retract=폐기 확정 히스토리(portal 검토 후 기각) / **왜 `Apply`가 기본이고 `Overridden`는 최적화 특수 케이스인가**(계산 의존성 있는 조합 vs 독립적 재사용 가능 조각의 병합 — 2026-08-07 다섯 번째 세션, `modifier-plan.md` 9번) / 왜 `Apply`가 clone 대신 mutate하지 않는가(형제 오염 방지가 개별 clone 비용 절감보다 우선) +- api: Setter가 리터럴/변환 함수 둘 다 받음(→심화: getter 없는 이유) / 필드가 State일 수 있는 4가지 조합 표(→심화: 반응성 유지/끊김 이유) / `mod:UICorner(8)` dot-access 생성자 관습 / Slot은 인스턴스당 여럿 가능 / 중첩 인스턴스 자식 처리 / ~~retract 시 slot 내용 폐기(→심화: portal 없는 이유)~~ **[2026-08-13 정정] 2026-08-13 여섯 번째 세션에 역전 — `State` 교체는 이제 파괴가 아니라 언마운트, portal은 그 자연스러운 귀결(`base/slot-plan.md`)** / `:Apply(factory)` 기본 체이닝 관용구(→심화: 언제 `Apply` vs `Overridden`인지 성능 기준) / `:Peek<>(key)` + `isState`(→심화: `Get`과 이름을 다르게 한 이유) +- 심화: 정적 merge vs 런타임 pluggable 기각 이유(CSS cascade) / immutable+clone 체이닝 이유(형제 오염 방지) / getter 미채택 이유 / `__index` 런타임 구현 통찰 / Modifier가 핸들러 계층을 모르는 이유 / base/roblox 패키지 경계(Dispatch/Slot vs Handlers/Slot) / Slot 단일 마운트 소유권이 v1/Fusion/Vide 대비 개선인 이유 / ~~retract=폐기 확정 히스토리(portal 검토 후 기각)~~ **[2026-08-13 정정] 위와 같은 이유로 역전 — 이 항목은 "왜 한때 destroy+no-portal로 결정했었는가"라는 히스토리 소재로만 유효, 현재 결론 아님** / **왜 `Apply`가 기본이고 `Overridden`는 최적화 특수 케이스인가**(계산 의존성 있는 조합 vs 독립적 재사용 가능 조각의 병합 — 2026-08-07 다섯 번째 세션, `modifier-plan.md` 9번) / 왜 `Apply`가 clone 대신 mutate하지 않는가(형제 오염 방지가 개별 clone 비용 절감보다 우선) - 열린 질문(문서화 보류): ~~여러 Slot이 형제로 섞일 때 순서 보장~~ **[해소됨, 2026-08-09 여섯 번째 세션]** Length/Offset 누적합으로 확정, 심화 목록에 추가 필요(`base/bind-system-plan.md` "Length/Offset" 절). @@ -132,7 +132,7 @@ v1 폐기 API/버그/구조 결함 전부 v2 설계를 정당화하는 내부 7. 왜 컴포넌트 경계는 named parameter인가(Compose/Fusion/Vide/v1 수렴) — `component-composition-plan.md` 8. 왜 "다중 루트 반환" 개념을 없앴는가 — `component-composition-plan.md` 9. 왜 Slot은 단일 마운트 소유권을 강제하는가(v1/Fusion/Vide 대비) — `slot-plan.md`, `comparison-fusion-vide.md` -10. 왜 Tween은 반응 그래프 밖에 있는가 — `base/tween-plan.md` +10. ~~왜 Tween은 반응 그래프 밖에 있는가~~ **[2026-08-13 정정] 위 §2 심화 항목과 같은 stale 표현 — 실제로는 Property 값 타입 치환(`T|Tween`)으로 그래프 안에 자연스럽게 있음** — `base/tween-plan.md` 11. 왜 `:Emit()`은 Source 전용이고 파생 State엔 없는가(호출부는 `source:Emit()`, 2026-08-06 후속 세션에서 `Store:Emit(key)`→이 형태로 정리) — `store-semantics.md` 12. 독립 프리미티브 vs 파생 데이터 — 생성자 모양을 결정하는 원칙 — `store-semantics.md` 14. 왜 컴포넌트는 전역 store를 직접 참조하면 안 되는가(이식성) — `purity-and-effects-plan.md`