docs(audit): 코퍼스 4차 감사 — 0-Y/0-Z 포인터 누락 + 파괴/언마운트 용어 잔존 정정

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 <noreply@anthropic.com>
This commit is contained in:
qwreey 2026-08-13 17:14:58 +09:00
parent 1aa01c60fb
commit b228efc2a6
Signed by: qwreey
GPG key ID: D28DB79297A214BD
7 changed files with 43 additions and 17 deletions

View file

@ -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<<T>>`로 코드/문서 전체 통일해서
당장의 해석 모호성은 없앴음 — 그래도 최종 이름은 다른 가칭들(`State`/

View file

@ -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`가 값을 포지셔널로 안 주는 이유는

View file

@ -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) 여러
계산을 합치고 싶으면 변환 함수 본문 안에서 다른 함수를 그냥 호출하면

View file

@ -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`를 새로 안 만들고 재사용하려면 그

View file

@ -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 해소 대기 중 — "열린 질문 없음"은 값
모양/이름에 한정.**
## 왜 재설계됐나

View file

@ -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` |

View file

@ -26,7 +26,7 @@
1. **초기화**`RobloxFactory(QuadBase)`로 base+backend 조립 (`module-lifecycle-plan.md`, `bind-system-plan.md`)
2. **Instance 만들기** — DOMless 즉시 생성 모델, 제네릭 `new<Class>` + 자주 쓰는 ~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<<T>>(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<Slot>` 교체는 이제 파괴가 아니라 언마운트, portal은 그 자연스러운 귀결(`base/slot-plan.md`)** / `:Apply(factory)` 기본 체이닝 관용구(→심화: 언제 `Apply` vs `Overridden`인지 성능 기준) / `:Peek<<T>>(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<T>`)으로 그래프 안에 자연스럽게 있음**`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`