docs(audit): 스파이크 상태판 신설 + 코퍼스 전반 모순·stale 감사 정정
## luau-test/STATUS.md 신설 (사용자 요청) "사람이 보기엔 뭐를 바로 봐야하는지 눈에 안 띄어서" — 파일별 상태를 🔴 사람 결정 필요 / 🟠 스파이크 깨짐 / ⚪ 미실행 / ✅ 통과로 분류하고 봐야 할 것을 위에 배치. 역할 분담도 명시: STATUS.md가 상태의 소스, README.md는 각 파일의 검증 의도·배경, audit/는 실행 결과 상세. ## 코퍼스 전반 감사 (에이전트 둘, 보고받은 것 전부 직접 사실 확인 후 수정) 가장 위험: ROADMAP.md가 최우선 게이트(question.md 0-Z, 하강 diff 재설계)를 전혀 안 짚고 있었음. base 4개 문서엔 ⚠️ 배너를 달아뒀는데 정작 구현 순서의 소스인 ROADMAP엔 없어 M2/M4/M10 담당자가 옛 모델로 구현할 위험이 실재. → 세 마일스톤 전부에 배너/포인터 추가. 두 번째: slot-plan.md 앞부분이 뒤집힌 결정을 여전히 "확정"으로 자칭. "폐기, 옮기지 않음, portal은 오버엔지니어링"과 "열린 질문" 절에 역전 표시가 없어 앞에서부터 읽는 구현자가 구 모델로 짤 위험 → 🔄 역전 배너. 더 심각하게 reconcile 의사코드가 여전히 rawRemove(파괴)를 부르고 있었음 (같은 문서가 "[반영 완료]"라 태그해둔 것과 정면 모순) → rawUnmount 신설. filter/toggle 근거 절도 "제거=파괴" 전제라 캐비엇 추가(결론은 유효). 그 외: - ROADMAP M11 Tween이 이미 확정된 넷을 미결로 둠(override 정책/옵션 값 모양/Animate 시그니처/initValue) + research/tween-plan.md 죽은 링크 4곳 - bind-system-plan.md:328, architecture.md:213의 "4종 계약" 잔여 (isHandlable(k,v) 구식 시그니처 포함) - store-semantics.md의 "State는 가칭" stale — 2026-08-12에 최종 확정됨 - luau-test/README.md의 04/19 판정 기준이 재작성된 파일을 못 따라감. 특히 19 C섹션 기준이 정상 동작(nested error)을 실패로 오판하게 돼 있었음 - documentation-content-map.md의 4종 계약 / Attribute<T> 옛 이름 - modifier-plan.md의 "정정이 원문에 소급 안 됨" 패턴 - attribute-plan.md "이름 소유권"이 0-Z 미결인데 "최종"이라 적힌 것 캐비엇 - relate-plan.md의 kSlotMap 역할 서술 부정확, 상호참조 방향 오류 2곳 판단해서 안 고친 것: effect-plan.md의 "cleanup" 14곳 — Handler retract (내부 배관)와 달리 Effect(fn)에서 사용자가 작성한 fn이 반환하는 React식 콜백이라 다른 층위. 매 감사마다 재지적되므로 lifecycle-pattern.md에 "확인 완료, 의도된 별개 개념"으로 못박음. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
b90b3a6b94
commit
56ba2b3f37
15 changed files with 302 additions and 45 deletions
|
|
@ -15,7 +15,7 @@
|
|||
| `qa-request/` | 구현 완료(코드/에이전트 검증까지 끝남) + 사용자 본인의 실기기(Roblox Studio) QA만 남음 — 지금은 구현 자체가 시작 전이라 비어있음 |
|
||||
| `archive/` | 완료 + 사용자가 실사용/실기기로 직접 검증까지 마침 (구현 대상). **[2026-08-06 확장]** 완전히 뒤집힌 설계 결정을 원문+역전 이유+diff와 함께 보존하는 용도로도 사용(제목 `[역전됨]` — 한 번 확정했다가 뒤집힌 것) — 더 이상 능동적으로 참고 안 해도 되지만(토큰 낭비 방지 위해 `base/`/`research/`에서 뺌) `quadnomicon` 소재로는 나중에 쓸 수 있음. **[2026-08-07 확장]** 후보였다가 채택 안 된 것(확정한 적 없이 검토 후 기각)도 같은 방식으로 보존, 제목은 구분을 위해 `[기각됨]` — `[역전됨]`과 의미가 다르므로 혼동하지 말 것. **[2026-08-07 세 번째 확장]** 설계 반전/기각과 별개로, 에이전트가 문서 작성 중 스스로 낸 개념 혼동을 정정한 이력은 `[에이전트 실수]` 태그로 `agent-mistake.md` 하나에 모음(CLAUDE.md 세션 로그 중복 방지) |
|
||||
| `feedback/` | 실사용 피드백을 정리한 긴 로그 — 지금은 비어있음(구현 시작 전) |
|
||||
| `luau-test/` | **[2026-08-09 신설]** `base/` 확정 사항 중 "추론만으로 확정하고 실제 Luau로 부딪혀본 적 없는 것"(M0 스파이크 대상)을 `luau`/`luau-analyze`/`luau-lsp`/Roblox Studio로 사용자가 직접 돌려볼 독립 실행 스크립트 모음. **[2026-08-13]** `10`의 일부가 처음으로 실측 확인됨(부분) — 확인된 결과 자체는 `audit/`에 기록, 이 폴더는 스크립트/색인만 유지. 나머지 16개+`10`의 남은 부분은 여전히 결과 미확인 — `luau-test/README.md`가 색인 |
|
||||
| `luau-test/` | **[2026-08-09 신설]** `base/` 확정 사항 중 "추론만으로 확정하고 실제 Luau로 부딪혀본 적 없는 것"(M0 스파이크 대상)을 `luau`/`luau-analyze`/`luau-lsp`/Roblox Studio로 사용자가 직접 돌려볼 독립 실행 스크립트 모음. **[2026-08-13 여섯 번째 세션, 첫 실측]** `luau`/`luau-analyze` 바이너리가 생겨 처음으로 실제 실행 — **런타임 12개 전원 통과**, 타입 쪽에서 `:Compute(fn)` lazy 핸들 계약이 Luau 추론과 충돌하는 게 드러남(`question.md` 0-Y). **`STATUS.md`가 상태의 소스**(pass / 사람 결정 필요 / 스파이크 깨짐 / 미실행 분류 — 사람이 먼저 볼 것만 위에), `luau-test/README.md`는 각 파일의 검증 의도·배경, 실행 결과 상세는 `audit/luau-test-first-run-2026-08-13.md` |
|
||||
| `audit/` | **[2026-08-13 신설]** `luau-test/` 등 스파이크를 실제로 돌려본 뒤 "무엇이 확인됐고 무엇이 아직 안 됐는지"를 기록하는 곳 — 스크립트/계획 자체가 아니라 **실측 결과**만 다룸. base/luau-test와 달리 부분 확인(일부만 통과)도 있는 그대로 기록, 완전히 해소되면 관련 `base/`/`luau-test/README.md` 캐비엇을 지우고 이 문서는 근거로 남김 |
|
||||
| `session/` | **[2026-08-11 신설]** 세션별 상세 로그 원문(시행착오·정정 전 서술 포함, `quadnomicon` 개발로그 소재용) — 루트 `CLAUDE.md`가 3196줄까지 불어나 성능 저하를 유발해서 분리함. 파일명 `YYYY-MM-DD-NN-slug.md`, CLAUDE.md의 "세션 히스토리" 절에서 각 항목이 여기로 링크. 항상 읽을 필요 없음 — 결정의 논의 과정이 궁금할 때만 |
|
||||
| `initreq/` | 프로젝트 착수 시 클론해둔 참고 레포(quad v1, fusion, vide, rbvm, tbox, code-docker) + PA님 실 코드(`artworks/`, 4차 라운드 교차검증 근거) + 원본 요청(`req.md`, `raw-userinput.md`) + `quad2-try`(이전에 시도했다 폐기한 v2 재작성 시도 — 리서치 완료, 결론은 `base/bind-system-plan.md`) — 읽기 전용 리서치 소스, 여기 내용을 옮기지 말고 항상 원본 그대로 유지 |
|
||||
|
|
|
|||
|
|
@ -211,7 +211,8 @@ existing-instance-bind는 여전히 `research/`에 남아있고 이 구조 확
|
|||
고유의 어휘"가 아니라 여러 타입에 걸쳐 쓰이거나(`isX`류) 프리미티브
|
||||
자체가 아닌 것(Dispatch/Brand는 `Type(args)` 생성자가 없는 내부 엔진)의
|
||||
구성원이라 PascalCase 대상이 아님. Handler 계약 필드(`isHandlable`/
|
||||
`priority`/`process`/`retract`)도 여기 속함 — 이건 애초에 "함수"라기보다
|
||||
`priority`/`process` — 2026-08-13 다섯 번째 세션에 `retract`가 `process`의
|
||||
반환값으로 합쳐지기 전엔 4종이었음)도 여기 속함 — 이건 애초에 "함수"라기보다
|
||||
구현체가 채워 넣는 구조체 필드.
|
||||
- **경계 판단 기준**: 새 이름을 지을 때 "이게 특정 프리미티브 타입 하나의
|
||||
전용 소유물인가?"로 물으면 됨 — 그렇다면 대문자(생성자/메서드/그
|
||||
|
|
|
|||
|
|
@ -176,7 +176,12 @@ Dispatch의 재진입 가드에 얹어 감지)은 이 버그를 고치긴 했으
|
|||
이유는 `archive/checkpoint-handler-pattern-reversed.md`.
|
||||
|
||||
**최종(세 번째 버전) — 체크포인트도 `owners`도 없이, 항상 인덱스 1부터
|
||||
직접 위임.** 그룹이 이름마다 공개 `AttributeKey(name)`으로 그냥
|
||||
직접 위임.** **[캐비엇, 2026-08-13 여섯 번째 세션] 여기서 "최종"은 *현행
|
||||
`hintValue` 모델 기준*이고, 이 주제 자체가 `question.md` **0-Z**로 다시
|
||||
열려 있음** — 문서 상단 ⚠️ 배너가 예고하는 "하강 diff" 재디스패치 모델에선
|
||||
그룹 A/B가 둘 다 `StoreBind`로 보여 "같은 핸들러"로 판정되므로 **아래 점유
|
||||
체크만으로는 그룹↔그룹 충돌을 못 잡음**(`research/dispatch-redispatch-diff-plan.md`
|
||||
5절). 0-Z가 정해지면 이 절이 네 번째 버전으로 다시 갱신됨. 그룹이 이름마다 공개 `AttributeKey(name)`으로 그냥
|
||||
`Dispatch.process(inst, key, source, 1)`를 부르면 끝 — **"인덱스 1이 이미
|
||||
점유돼 있는가"라는 `Dispatch.process` 자신의 점유 체크가 소유권 충돌
|
||||
감지를 그대로 대신함**: 다른 그룹이나 직접 쓰기가 이미 그 이름을
|
||||
|
|
@ -242,8 +247,8 @@ Frame { Attribute(styleStore), Attribute(stateStore) } -- 여러 개 나란히
|
|||
|
||||
**[2026-08-13 감사에서 추가] `:NameMap()`은 원래 "메커니즘" 절 의사코드에만
|
||||
등장하고 이 API 목록엔 빠져 있었음** — Handler가 이름 집합을 순회하려면
|
||||
반드시 필요한 공개 접근자라 여기 명시(`Tag:Names()`가 `tag-plan.md`에서
|
||||
같은 이유로 빠져 있던 것과 같은 누락).
|
||||
반드시 필요한 공개 접근자라 여기 명시(`Tag:Names()`도 `tag-plan.md`에서
|
||||
같은 누락이었고, 같은 감사에서 함께 추가됨 — 지금은 양쪽 다 문서화돼 있음).
|
||||
|
||||
`Attribute.Merged`가 내부적으로 하는 일은 각 Store에서 이름 붙은 `Source`
|
||||
슬롯을 그대로 가져와 자기 자신의 key→Source 맵에 넣는 것 — 아래 "레이어드
|
||||
|
|
|
|||
|
|
@ -326,8 +326,10 @@ end
|
|||
```
|
||||
|
||||
- **매치 predicate는 `isHandlable`** — `canExecute`가 아님. 둘은 완전히
|
||||
다른 개념이라 혼동하지 말 것: `isHandlable(k,v)`는 KV 매치 predicate(핸들러
|
||||
계약 4종 중 하나, 이 절에서 다루는 것), `canExecute`는 인자로 받은 특정
|
||||
다른 개념이라 혼동하지 말 것: `isHandlable(inst,k,v)`는 KV 매치 predicate
|
||||
(핸들러 계약 3종 중 하나, 이 절에서 다루는 것 — 예전엔 `(k,v)` 2-인자에
|
||||
4종 계약이었으나 각각 2026-08-07 여덟 번째/2026-08-13 다섯 번째 세션에
|
||||
바뀜, 이 문단만 갱신에서 누락돼 있던 걸 같은 날 감사에서 발견), `canExecute`는 인자로 받은 특정
|
||||
바인딩/등록 하나가 "지금 살아있어서 실행돼도 되는가"만 보는 별개의
|
||||
라이프타임 게이트(`base/lifecycle-pattern.md` "생명 바인드 유틸" 절) —
|
||||
KV 매치와 무관.
|
||||
|
|
@ -1309,7 +1311,7 @@ tween-plan.md`도 이에 맞춰 갱신됨). Ref의 진짜 용도는 다름:
|
|||
### `Ref`의 retract — `State<Ref>` 재바인드 시 이전 Ref에 `nil` (2026-08-12 여덟 번째 세션, `TagHandler`와 같은 메커니즘 재사용)
|
||||
|
||||
**배경**: `Ref`는 이미 "일반 프로퍼티/Modifier 필드/Store 값 어디든 자유롭게
|
||||
들어감"(위 "동적 경로 가드" 절)이 확정돼 있어 — `State<Ref>`가 실제로
|
||||
들어감"(아래 "동적 경로 가드" 절)이 확정돼 있어 — `State<Ref>`가 실제로
|
||||
가능하고, 그러면 Store 값이 `refA`에서 `refB`로 바뀌는 경우가 생김. 이때
|
||||
`refA`가 계속 "확정된 값(대개 이전 `inst`)"을 들고 있으면, 그 자리가 이제
|
||||
`refB`로 넘어갔다는 걸 모르는 코드가 `refA.Value`를 계속 유효하다고 믿는
|
||||
|
|
|
|||
|
|
@ -307,8 +307,18 @@ Roblox 엔진 자체가 Destroy 시 Tag/Attribute/실행 중인 Tween을 전부
|
|||
쉬움) — 실제 의미는 "이전에 적용한 처리를 무른다/멈춘다"이므로 **`retract`**
|
||||
로 통일. (`revert`, `rescind`도 검토했으나 `retract`가 "이전에 취한 조치를
|
||||
철회한다"는 의미로 가장 정확 — `process`/`retract` 쌍으로 자연스럽게 대구를
|
||||
이룸.) 대부분의 문서에서 이 이름으로 갱신됨 — 잔여 "cleanup" 표기가 남은
|
||||
문서가 있을 수 있으며, 그 확인/정리는 진행 중.
|
||||
이룸.) 대부분의 문서에서 이 이름으로 갱신됨.
|
||||
|
||||
**[확인 완료, 2026-08-13 여섯 번째 세션] `base/effect-plan.md`의 "cleanup"은
|
||||
잔여 stale이 아니라 의도된 별개 개념** — 매 감사마다 재지적되므로 여기
|
||||
못박아 둠. 두 층위가 다름:
|
||||
- **`retract`**: Handler 계약의 것. `process`가 반환하는 클로저로, quad
|
||||
**내부 배관**이 "이전 처리를 무른다".
|
||||
- **`cleanup`**: `Effect(fn)`에서 **사용자가 작성한 `fn`이 반환하는 콜백**.
|
||||
React `useEffect`의 그것과 동형이고, 사용자 API 표면의 어휘라 `retract`로
|
||||
통일할 대상이 아님(오히려 통일하면 React 배경 사용자에게 더 낯설어짐).
|
||||
|
||||
즉 "cleanup 잔여 확인"은 `effect-plan.md`에 대해서는 **끝난 것**으로 봐도 됨.
|
||||
|
||||
**[2026-08-13 다섯 번째 세션] `retract`는 더 이상 Handler의 *필드*가
|
||||
아니라 `process`가 반환하는 클로저의 *역할 이름*이다.** 개념/이름
|
||||
|
|
|
|||
|
|
@ -197,10 +197,12 @@ Compute를 합치고 싶다"는 동기였는데, 이미 두 가지로 다 커버
|
|||
|
||||
**런타임은 클래스별 코드 없이 base에 딱 하나만 있으면 됨(2026-08-06 후속
|
||||
세션, 핵심 통찰).** `mod:FontSize(14)`는 `mod.FontSize(mod, 14)`로 풀리는
|
||||
문법 설탕이고, `mod.FontSize`는 `FontSize`가 리터럴 키로 안 박혀있으니
|
||||
문법 설탕이고, `mod.FontSize`는 `FontSize`가 **self의 리터럴 키로 절대 안
|
||||
박히므로**(아래 ⚠️ — 이게 설계상 필수 조건이지 우연이 아님) 항상
|
||||
`__index(self, key)`가 잡음 — 그러니 `__index`가 **어떤 key가 오든** 그
|
||||
key를 클로저에 캡쳐한 `function(self, arg) local clone = table.clone(self)
|
||||
... end`류 함수를 즉석에서 만들어 리턴하기만 하면 끝.
|
||||
... end`류 함수를 즉석에서 만들어 리턴하기만 하면 끝(단 그 clone은 필드를
|
||||
self 최상위가 아니라 **내부 저장소**에 써야 함 — 역시 아래 ⚠️).
|
||||
|
||||
> **⚠️ [2026-08-13 여섯 번째 세션, 실측 중 발견] 필드 값은 `self`의 리터럴
|
||||
> 키에 저장하면 안 되고, 반드시 `__index`를 거치는 내부 저장소에 둬야 함.**
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ weak여야 함 — 그런데 그 안에 담기는 값은 경우에 따라 **강
|
|||
있는지 먼저 확인할 것 — 갖고 있다면 **두 `Relate` 중 최소 한쪽은
|
||||
`SetWeak`로 낮추고, 실제 GC 앵커는 `bindLifetime`/`unbindLifetime`
|
||||
하나로 통일**할 것(구체 사례는 `base/slot-plan.md` "Slot과 Store
|
||||
바인드의 관계" 절 참고 — `kSlotMap`(inst→slot)/`slotOwner`(slot→inst)가
|
||||
바인드의 관계" 절 참고 — `kSlotMap`(`(inst,k)`별 마지막 Slot)/`slotOwner`(slot→inst)가
|
||||
정확히 이 패턴이었음. **[2026-08-13 다섯 번째 세션 이후]** 그 두 `Relate`는
|
||||
지금은 존재하지 않음 — `kSlotMap`은 Handler 계약이 클로저 반환으로 바뀌며
|
||||
불필요해져 삭제됐고 `slotOwner`는 `elementOwner`(전부 `SetWeak`)로
|
||||
|
|
|
|||
|
|
@ -437,7 +437,7 @@ claimOwner(element, self) -- self = 담는 Slot. 이미 누가(같은 self 포
|
|||
-- rawRemove(self, index)/rawExtract 안, 요소를 내보내는 자리
|
||||
releaseOwner(element, self)
|
||||
|
||||
-- destroySlotTree(slot) 안, 자식들을 파괴하기 직전(위 "파괴" 절)
|
||||
-- destroySlotTree(slot) 안, 자식들을 파괴하기 직전(아래 "파괴" 절)
|
||||
releaseOwner(element, slot)
|
||||
```
|
||||
|
||||
|
|
@ -484,6 +484,13 @@ nested `Slot`을 파괴 후 재사용하는 경로가 정확히 이 케이스).
|
|||
Destroy 시점엔 `retract`가 호출되지 않는다는 원칙(`base/lifecycle-pattern.md`)도
|
||||
동일하게 적용.
|
||||
|
||||
> **🔄 [역전됨, 2026-08-13 여섯 번째 세션] 아래 "폐기, 옮기지 않음" 결정은
|
||||
> 뒤집혔습니다 — 지금은 `State<Slot>` 교체가 **파괴가 아니라 언마운트**이고,
|
||||
> **portal은 별도 기능이 아니라 그 결정의 자연스러운 귀결**입니다.
|
||||
> 최신 설계는 이 문서 아래쪽 "`State<Slot>` 교체는 파괴가 아니라 언마운트"
|
||||
> 절(+ `unmountSlotTree`)이 정본. 아래 문단은 그 결론에 이르기까지의
|
||||
> 히스토리로만 남겨둡니다 — **여기 적힌 "확정"을 그대로 믿고 구현하지 말 것.**
|
||||
|
||||
**확정(2026-08-04 검증 라운드, 메커니즘은 위처럼 2026-08-12 아홉 번째
|
||||
세션에 정정): retract/재바인드되는 slot은 옮겨지지 않고 그냥 폐기된다.**
|
||||
Slot은 바인딩되는 순간 그 안의 요소를 전부 own해버리는 데이터형 — 새 slot
|
||||
|
|
@ -940,6 +947,14 @@ end
|
|||
계속 돎. 리스트가 200개+가 되면 "보이는 건 20개인데 200개가 전부 계속
|
||||
돌아가는" 문제가 실제 비용으로 드러남.
|
||||
|
||||
**[캐비엇, 2026-08-13 여섯 번째 세션] 아래 근거는 "제거 = 진짜 파괴"를
|
||||
전제로 쓰였는데 그 전제가 언마운트 재설계로 바뀌었음** — 다만 **결론은
|
||||
그대로 유효**함: 언마운트도 물리 트리에서 떼어내므로 `Visible=false` 토글과
|
||||
달리 렌더/레이아웃 비용이 사라지고, 아무도 안 들고 있으면 GC되어 이벤트
|
||||
연결·재계산도 결국 정리됨. 즉 "lazy하지 않음" 문제는 파괴가 아니라
|
||||
**언마운트만으로도 해소**되고, 오히려 사용자가 그 요소를 들고 있다가 다시
|
||||
쓸 수 있게 되는 이득이 붙음.
|
||||
|
||||
**해법**: `updateFn`을 매 사이클 호출하되, `prev`를 줘서 "바꿀 게 없으면
|
||||
그대로 돌려주기만 하면 되는" 저렴한 경로를 만들고, filter 탈락은 `nil`
|
||||
반환으로 **진짜 파괴**되게 함 — Visible 토글이 아니라 실제 Remove.
|
||||
|
|
@ -1056,7 +1071,11 @@ function activateList(self, inst)
|
|||
end
|
||||
|
||||
if result ~= prev then
|
||||
if prev ~= nil then rawRemove(self, prev) end -- 파괴
|
||||
-- [정정, 2026-08-13 여섯 번째 세션] 파괴가 아니라 **언마운트** —
|
||||
-- rawUnmount는 rawRemove와 같되 element를 Destroy하지 않고
|
||||
-- 소유권만 반납(unmountSlotTree 사용, 아래 "파괴" 절).
|
||||
-- 명시적 CRUD Remove/Clear/dispose만 여전히 파괴.
|
||||
if prev ~= nil then rawUnmount(self, prev) end
|
||||
if result ~= nil then rawAdd(self, result, pos) end -- 새로 배치, 압축 위치 기준
|
||||
mounted[key] = result
|
||||
elseif prev ~= nil and keyIndex[key] ~= pos then
|
||||
|
|
@ -1069,7 +1088,7 @@ function activateList(self, inst)
|
|||
for key in pairs(keyIndex) do -- 직전 사이클에 존재했던 전체 key
|
||||
if not seen[key] then
|
||||
local prev = mounted[key]
|
||||
if prev ~= nil then rawRemove(self, prev) end
|
||||
if prev ~= nil then rawUnmount(self, prev) end -- 위와 같은 이유로 비파괴
|
||||
mounted[key], userdata[key] = nil, nil
|
||||
end
|
||||
end
|
||||
|
|
@ -1140,7 +1159,10 @@ raw `i`를 그대로 위치 인자로 썼는데, 앞쪽 item이 filter로 마운
|
|||
실행)의 로컬 변수(클로저 업밸류) — 별도 전역 weak table(`Relate` 등)
|
||||
불필요, `inst`/`self`가 살아있는 동안만 존재하면 되고 죽으면 클로저도
|
||||
같이 GC됨(아래 "구독 시점" 절).
|
||||
- **`reconcile`이 직접 호출하는 건 `rawAdd`/`rawRemove`/`rawMove`뿐** —
|
||||
- **`reconcile`이 직접 호출하는 건 `rawAdd`/`rawUnmount`/`rawMove`뿐**
|
||||
(**[정정, 2026-08-13 여섯 번째 세션]** 예전엔 `rawRemove`(파괴)였으나
|
||||
언마운트 전환으로 바뀜 — 데이터에서 빠진 아이템도 파괴되지 않고
|
||||
언마운트만 되며, 아무도 안 들고 있으면 GC) —
|
||||
`rawExtract`/`rawSwap`/`rawClear`도 (위 "모든 공개 CRUD는 가드+위임"
|
||||
구조상) 당연히 존재하지만, `:List`의 reconcile 알고리즘 자체가 그
|
||||
셋을 쓸 일이 없을 뿐(제거는 항상 파괴 확정이라 `Extract` 아닌 `Remove`
|
||||
|
|
@ -1236,9 +1258,12 @@ GC에 위임" 원칙 그대로.
|
|||
|
||||
## 열린 질문 (`.claude/question.md`에도 취합)
|
||||
|
||||
- 재마운트 에러 처리(throw), retract 시 폐기(옮기지 않음) 둘 다 확정. 남은 건
|
||||
실제 구현 단계에서 이 "폐기" 동작이 실사용에서 불편하지 않은지 재검증하는
|
||||
정도 — 설계 방향 자체는 더 이상 열려있지 않음.
|
||||
- 재마운트 에러 처리(throw)는 확정. **[역전됨, 2026-08-13 여섯 번째 세션]
|
||||
"retract 시 폐기(옮기지 않음)"는 뒤집혔음** — 지금은 `State<Slot>` 교체가
|
||||
**언마운트**이고 이전 Slot은 파괴되지 않음(파괴는 명시적 `Remove`/`Clear`/
|
||||
`dispose`만). "실사용에서 불편하지 않은지 재검증"이라던 남은 항목도 그
|
||||
재설계로 해소됨 — 최신 설계는 아래 "`State<Slot>` 교체는 파괴가 아니라
|
||||
언마운트" 절이 정본.
|
||||
- "클래스가 슬롯을 받는 방법"(Named Slot 없음)도 확정됨(위 "클래스가 슬롯을
|
||||
받는 방법" 절 참고).
|
||||
- **[해소됨, 2026-08-09 세 번째 세션]** `add`/`remove`/`clear` CRUD 의미론,
|
||||
|
|
@ -1461,6 +1486,21 @@ end
|
|||
-- 이미 못박았듯 reconcile(위 "여러 Slot이 섞일 때" 절 근처)은 element 기준으로
|
||||
-- rawRemove(self, prev)를 부름. 둘 중 하나로 통일할지 얇은 변환 계층을 둘지는
|
||||
-- 아직 M6 구현 세부로 열려 있음 — 이 블록은 그 결정 전 illustrative 예시.
|
||||
-- [신설, 2026-08-13 여섯 번째 세션] rawRemove의 비파괴 짝 — `:List`의
|
||||
-- reconcile과 `Extract` 계열이 씀. rawRemove와 **딱 하나만 다름: 안 죽인다.**
|
||||
function rawUnmount(self, index)
|
||||
local element = self._elements[index]
|
||||
local bk = getBookkeeping(self)
|
||||
if bk.observers[index] then
|
||||
unbindLifetime(self._mountedInst, bk.observers[index])
|
||||
end
|
||||
releaseOwner(element, self) -- 소유권은 반납(이제 다른 곳에 넣을 수 있음)
|
||||
if isSlot(element) then unmountSlotTree(element) else element.Parent = nil end
|
||||
|
||||
spliceArraysDown(self, index)
|
||||
recompute(self, bk)
|
||||
end
|
||||
|
||||
function rawRemove(self, index)
|
||||
local element = self._elements[index]
|
||||
local bk = getBookkeeping(self)
|
||||
|
|
|
|||
|
|
@ -202,9 +202,11 @@ State를 만족하도록 만들고, RefSource라는 별도 타입은 폐기**하
|
|||
케이스(Source가 State를 만족하는 제네릭 메소드 체이닝)를 포함해서
|
||||
검증할 것.
|
||||
|
||||
**이름 주의**: `Source`/`State`라는 이름 자체가 `CLAUDE.md` "지금 할 일"
|
||||
2번의 용어 정리 대상(특히 `State`)과 겹침 — 구조(서브타입 관계, RefSource
|
||||
폐기)는 지금 확정해도 정확한 이름은 용어 정리 라운드까지 가칭으로 남김.
|
||||
**이름 주의 — [해소됨, 2026-08-12 스무 번째 세션]**: `Source`/`State`라는
|
||||
이름이 한때 용어 정리 대상(특히 `State`)이었으나, **`State`는 현재 이름
|
||||
그대로 유지로 최종 확정됨**(`Computed`/`Derived`/`Pipe` 전부 기각 — 근거는
|
||||
`bind-system-plan.md` "네이밍 — `Compute`가 `-ed`가 아닌 이유" 절과
|
||||
`question.md` 1번). 더 이상 가칭이 아님.
|
||||
|
||||
## Store 값 설정 문법 — `myStore.key = value` 폐기, `source:Set(value)`로 전환 (2026-08-06 후속 세션, 정정)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
# .claude/luau-test — M0 착수 전 실 Luau 기술검증 스파이크 모음
|
||||
|
||||
> **⚡ 지금 뭘 봐야 하는지부터 보려면 [`STATUS.md`](STATUS.md)** — 파일별
|
||||
> pass / 사람 결정 필요 / 스파이크 깨짐 / 미실행 분류. 이 README는 각 파일이
|
||||
> **무엇을 왜 검증하는지**(의도·배경)를 담고, 상태는 STATUS.md가 소스.
|
||||
|
||||
|
||||
**[2026-08-09 이동]** 처음엔 레포 루트 `luau-ignoreme/`(git 자동 제외
|
||||
폴더)에 만들었으나, 사용자가 직접 확인해볼 만한 검증 코드라 커밋해서
|
||||
레포에 남기기로 함 — `.claude/luau-test/`로 옮기고 일반 추적 대상으로
|
||||
|
|
@ -52,7 +57,7 @@ ROADMAP 항목 근거인지, 어떻게 실행하는지, 실행 후 뭘 확인해
|
|||
| `04-dispatch-chain-retractFrom.luau` | **[2026-08-13 감사에서 전면 재작성 + 파일명 변경]** 인덱스 기반 `chains`/`Dispatch.retractFrom`이 다단 재귀 위임에서 정확한지 — 3단 체인이 인덱스 1/2/3으로 안 겹치고 쌓이는지(= `State<State<T>>` **정상 동작**, UB 아님), 안/바깥 store 재발행 시 깊은 인덱스부터 정리되는지, hint가 target 인덱스에만 가는지 + **음성 대조군**: `chains:SetStrong`을 `handler.process` 뒤에 두면 최초 마운트에서 하위 retractor가 유실되는 버그 재현. 옛 버전은 핸들러 identity 기반 추적과 "중복 push 즉시 error" 가드를 검증했는데 그 가드는 다섯 번째 세션 재설계로 **없어져서** 설계와 정반대를 테스트하고 있었음 | `bind-system-plan.md` "Dispatch 체인"(2026-08-13 다섯 번째 세션 재설계) + 2026-08-13 감사 |
|
||||
| `05-store-state-diamond-propagation.luau` | push-invalidate/pull-recompute가 다이아몬드 의존성에서 중복 재계산 없이 동작하는지 | ROADMAP M0-1 |
|
||||
| `06-component-boundary-nil-hole-props.luau` | `props.Modifier or None` 관용구가 컴포넌트 경계 nil-hole을 막는지 + `Params` 타입 체크 | `component-composition-plan.md` "필수 관용구", ROADMAP M0-5 |
|
||||
| `07-relate-weak-table-gc.luau` | `Relate`의 lazy 서브테이블 생성 + weak-key GC가 실제로 동작하는지 | `relate-plan.md` "M2 착수 시 실측 확인" |
|
||||
| `07-relate-weak-table-gc.luau` | `Relate`의 lazy 서브테이블 생성 + weak-key GC가 실제로 동작하는지 | `relate-plan.md` "M2 착수 시 실측 확인" **[2026-08-13 보강]** 4번 섹션 신설 — `_countEntries()`(테스트 전용) + weak-value canary로 **"inst가 죽으면 중첩 StrongMap 안의 payload까지 연쇄 GC되는가"를 직접 검증**(원래는 sanity check만 하고 헤더의 핵심 주장은 미검증이었음). 파일이 스스로 적어둔 "weak table 엔트리를 셀 표준 API가 없다"는 전제도 틀렸음 — outer가 `__mode="k"`라 GC 후 `pairs`에서 사라짐 |
|
||||
| `08-type-source-satisfies-state.luau` (타입체크 전용) | `Source<T>`가 `State<T>`를 구조적으로 만족하는 제네릭 타입이 솔버에서 안전한지 | `store-semantics.md` "검증 필요", ROADMAP M0-2 |
|
||||
| `09-type-modifier-overridden-subtype.luau` (타입체크 전용) | `FrameModifier <: GuiObjectModifier`처럼 서브타입 관계인 Modifier를 `Overridden`으로 섞을 때 타입이 통과하는지 | `modifier-plan.md` 9-2번, ROADMAP M7 |
|
||||
| `10-roblox-studio-checks.server.luau` (Studio 전용) | (A) `bindLifetime`/`unbindLifetime`/`canExecute`/`canBound`의 gcconn 트릭 + 이중 바인딩 게이트(Destroy 시 Connected 전환 포함), (B) Attribute의 Instance 참조 타입 지원, (C) CollectionService 태그/GetTagged 왕복 — **[2026-08-13]** A 섹션 앞부분(신호 미발화, Destroy 시 Connected 즉시 전환)은 사용자 자작 스크립트로 부분 확인됨, `audit/gcconn-trick-verification.md` 참고. A-1/A-2(`canBound` 게이트)/B/C는 이 공식 파일로 아직 확인 안 됨 | `lifecycle-pattern.md`, `bind-system-plan.md` "이중 바인딩 금지", CLAUDE.md 2026-08-06 세션, `debug-tooling-plan.md` |
|
||||
|
|
@ -64,7 +69,7 @@ ROADMAP 항목 근거인지, 어떻게 실행하는지, 실행 후 뭘 확인해
|
|||
| `16-type-store-key-typefunction.luau` (타입체크 전용) | `Store<T>`가 `T`의 각 필드를 `Source`로 감싼 타입을 Luau `type function`(`types.newtable`/`:setproperty`/`ty:properties()`)으로 실제 합성 가능한지, 결과가 구조적으로 `Source<T>` 필드를 만족하는지 | `bind-system-plan.md` "`store.key` 레코드 필드 타이핑" 절(2026-08-12 열일곱 번째 세션), `pre-implementation-audit.md` 1-10 |
|
||||
| `17-modifier-index-tableclone-chaining.luau` | Modifier의 제네릭 `__index`+`table.clone` 체이닝 — 임의 필드 이름에 대해 즉석 setter가 만들어지는지, `table.clone`이 메타테이블을 참조로 공유해 여러 단계 clone에서도 체이닝이 안 끊기는지, 원본이 mutate 안 되는지, 형제 분기끼리 오염 안 되는지 | `modifier-plan.md` "런타임은 클래스별 코드 없이 base에 딱 하나만 있으면 됨" 절 + "`table.clone`의 정확한 동작 — 확인됨" 절(2026-08-12 열일곱 번째 세션), `pre-implementation-audit.md` 1-11 |
|
||||
| `18-relate-mutual-cycle-gc.luau` | **[2026-08-13 신규]** 서로 다른 두 `Relate`가 서로의 키를 상대방의 강한 값으로 제공하는 상호 순환은 Luau에 ephemeron이 없어 GC가 못 푼다는 주장(지금까지 공식 문서 인용으로만 뒷받침됨) — 음성 대조군(순환 재현)과 양성 대조군(한쪽을 weak-value로 낮추면 풀리는지) 둘 다 실측 | `relate-plan.md` "위험한 패턴" 절(2026-08-12 열세/열네 번째 세션), `slot-plan.md`의 `kSlotMap`/`slotOwner`/`elementOwner` 실사례 |
|
||||
| `19-ownership-refcount-relate-patterns.luau` | **[2026-08-13 신규]** "retract는 항상 불림" 정정(2026-08-12 열한 번째 세션) 이후 새로 생긴 세 소유권/참조카운트 알고리즘(A: Tag `kTagMap`/`tagNameMap` 참조 카운트 — 여러 위치가 같은 이름을 겹쳐 가져도 마지막 홀더가 빠질 때만 실제 `RemoveTag`, B: ~~Attribute `rawNew`+`owners` 소유권~~ — **[2026-08-13 감사] 이 섹션은 폐기된 설계를 검증 중이라 재작성 필요**: `rawNew`/`owners` 수동 레지스트리는 두 번 뒤집혀 사라졌고, 지금은 그룹이 공개 `AttributeKey(name)`으로 항상 인덱스 1에 위임하고 `Dispatch.process`의 **점유 체크**가 소유권 충돌을 대신 잡음 — 검증 대상도 그쪽으로 바뀌어야 함, C: Slot `elementOwner`의 `claimOwner`/`releaseOwner` — **[2026-08-13 감사] 이 섹션도 갱신 필요**: nested는 엄격 `claimOwner`(같은 owner 재클레임도 error), top-level만 `claimOwnerAt`으로 `(inst,k)`까지 봐서 spurious 재발행을 구분)가 실제로 정확히 갈리는지 | `tag-plan.md` "메커니즘" 절, `attribute-plan.md` "이름 소유권" 절, `slot-plan.md` "요소 소유권 — `elementOwner`" 절(전부 2026-08-12 세션들, 03/04/11번과는 다른 새 알고리즘 모양이라 별도 실측 필요하다고 판단) |
|
||||
| `19-ownership-refcount-relate-patterns.luau` | **[2026-08-13 신규, 같은 날 B/C 전면 재작성 — 지금은 현행 설계 기준]** 세 소유권/참조카운트 알고리즘 검증. **A**: Tag `tagNameMap` 참조 카운트(여러 위치가 같은 이름을 겹쳐 가져도 마지막 홀더가 빠질 때만 실제 `RemoveTag`. 옛 `kTagMap`은 클로저 캡처로 대체돼 삭제됨). **B**: Attribute 이름 소유권 — 공개 `AttributeKey(name)` 캐시 + `Dispatch.process`의 인덱스 1 **점유 체크**가 충돌을 잡는지(옛 `rawNew`+`owners` 수동 레지스트리는 폐기). **C**: Slot 소유권 — nested 엄격 `claimOwner`(같은 owner 재클레임도 error) vs top-level `claimOwnerAt(inst,k)`(정확히 같은 자리 재발행만 no-op). **셋 다 음성 대조군 포함** — 옛 로직이 `Slot{a,a}`/`Frame{slot,slot}`을 조용히 통과시키는 걸 재현. **캐비엇**: B는 `question.md` 0-Z(하강 diff 모델에선 그룹↔그룹을 점유 체크만으론 못 잡음)가 정해지면 다시 손봐야 함 | `tag-plan.md` "메커니즘", `attribute-plan.md` "이름 소유권", `slot-plan.md` "요소 소유권" |
|
||||
| `20-slot-splice-index-arithmetic.luau` | **[2026-08-13 신규]** `Slot:Splice(index, removeCount, ...newElements)`의 shift+recompute 1회 계산이, `Extract`/`Add` 반복으로 재현한 참조 구현과 항상 같은 결과를 내는지 — 제거/삽입 길이가 다를 때(delta 양수/음수) 뒤 요소가 밀리는 방향과 양을 헷갈리는 off-by-one 위험(이 프로젝트가 `Dispatch.recompute`에서 실제로 냈던 것과 같은 클래스의 버그)을 경계값 케이스로 검증 | `slot-plan.md` "확정" CRUD 표 + "`Splice` 신설" 절(2026-08-12 열다섯 번째 세션), `bind-system-plan.md`의 `recompute` off-by-one 수정 사례(2026-08-11 여섯 번째 세션) |
|
||||
|
||||
## 공통 유틸리티
|
||||
|
|
@ -207,10 +212,13 @@ error)를 추가하고, `04`의 3~4단계를 "가드가 실제로 걸리는지 +
|
|||
gcconn-trick-verification.md`), 재확인 불필요. A-2(재-bindLifetime 허용
|
||||
여부)가 실패하면 `canBound`/`unbindLifetime` 설계 자체를 재검토해야
|
||||
함 — **이건 아직 미확인, 공식 `10` 파일로 꼭 돌려볼 것.**
|
||||
- `04`는 3단계에서 `error로 막혔는가: true`, 4단계에서 체인 길이가 2로
|
||||
복구되는 게 기대값 — 만약 3단계가 error 없이 통과해버리면(`ok == true`)
|
||||
가드 pseudocode 자체가 어딘가 안 맞는 것이니 바로 알려줄 것(체인 파손이
|
||||
다시 조용히 넘어가는 회귀).
|
||||
- `04`는 **[2026-08-13 여섯 번째 세션에 전면 재작성 — 옛 판정 기준 폐기]**
|
||||
두 시나리오를 연달아 돌림. **정상 설계**는 [1] 체인 깊이 3, [3] 옛 inner
|
||||
구독 0, [4] `STALE` 미반영이 기대값. **음성 대조군**은 [1] 깊이가 **1**로
|
||||
무너지고 [4]에서 `STALE`이 반영되는 게 기대값(= 버그가 재현돼야 정상).
|
||||
대조군이 정상 시나리오와 똑같이 나오면 스파이크 모델링이 실제 구현과
|
||||
어긋난 것이니 스파이크 쪽을 먼저 의심할 것. (옛 기준이던 "3단계에서
|
||||
error로 막혔는가"는 다섯 번째 세션이 그 가드 자체를 없애서 폐기됨.)
|
||||
- `11`은 전부 PASS가 기대값 — FAIL이 하나라도 있으면 어느 케이스인지
|
||||
그대로 알려줄 것(특히 "변환 함수가 반환한 값" 케이스는 놓치기 쉬운
|
||||
경로라 실제 구현에서도 잘 짜였는지 중요한 신호).
|
||||
|
|
@ -239,9 +247,13 @@ error)를 추가하고, `04`의 3~4단계를 "가드가 실제로 걸리는지 +
|
|||
절과 `slot-plan.md`의 `kSlotMap`/`slotOwner`/`elementOwner` GC 설계
|
||||
전체를 최우선으로 재검토해야 함(Slot GC 안전성의 유일한 근거였음).
|
||||
- `19`는 전부 PASS가 기대값 — A 섹션이 FAIL이면 `tag-plan.md`의 참조
|
||||
카운트 알고리즘 자체를, C 섹션이 FAIL이면(특히 "같은 owner 재클레임은
|
||||
no-op"이 안 되면) `slot-plan.md`의 `elementOwner` 설계를 최우선으로
|
||||
재검토할 것 — C가 깨지면 재귀 재emit마다 마운트된 서브트리 전체가
|
||||
카운트 알고리즘 자체를, C 섹션이 FAIL이면 `slot-plan.md`의 `elementOwner`
|
||||
설계를 최우선으로 재검토할 것. **[2026-08-13 판정 기준 정정]** C의 기대
|
||||
동작이 바뀌었음 — 예전의 "같은 owner 재클레임은 no-op"은 버그로 판정돼
|
||||
둘로 쪼개짐: **nested `claimOwner`는 같은 owner 재클레임도 error**(엄격),
|
||||
**top-level `claimOwnerAt(element,inst,k)`만** 정확히 같은 `(inst,k)`의
|
||||
재발행에서 `false`. 옛 문구를 그대로 적용하면 정상 동작(nested error)을
|
||||
실패로 오판함 — C가 깨지면 재귀 재emit마다 마운트된 서브트리 전체가
|
||||
파괴됐다 재생성되는 파괴적 버그로 직결됨.
|
||||
- `20`도 전부 PASS가 기대값 — FAIL이 있으면 어느 케이스(특히 delta 부호가
|
||||
바뀌는 경계값)인지와 최종 배열/제거분이 어떻게 달랐는지 그대로 알려줄
|
||||
|
|
|
|||
87
.claude/luau-test/STATUS.md
Normal file
87
.claude/luau-test/STATUS.md
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
# 스파이크 상태판 — **사람이 먼저 볼 것만 위에**
|
||||
|
||||
> 마지막 갱신: 2026-08-13 여섯 번째 세션(첫 실측 라운드).
|
||||
> 상세 결과는 `.claude/audit/luau-test-first-run-2026-08-13.md`.
|
||||
> 실행법: `luau <파일>` (런타임) / `luau-analyze <파일>` (타입 전용).
|
||||
|
||||
## 🔴 사람 결정 필요 — 설계가 걸린 것 (2건)
|
||||
|
||||
| 파일 | 무엇이 걸렸나 | 어디로 |
|
||||
|---|---|---|
|
||||
| `15-type-compute-trailing-deps-typepack.luau` | **`:Compute(fn)`의 lazy 핸들 계약이 Luau 추론과 충돌.** `state:Compute(function(s) return s:Get()*2 end)`가 타입 에러. 콜백이 raw 값을 받으면 완전 클린 — 즉 표기 조정이 아니라 계약 자체가 원인. `Effect`/`Observer`/`Animate`/`Operator` 등 같은 계약 공유 API 전부에 걸림 | `question.md` **0-Y** |
|
||||
| `08-type-source-satisfies-state.luau` | 핵심 질문(Source⊇State)은 통과. 다만 `State<T>`가 **자기 자신**을 다른 타입 인자로 재귀 참조하면 `Recursive type being used with different parameters` — 사용자 방향은 "구울 때 인라이닝" | `question.md` **0-Y** 하단 |
|
||||
|
||||
## 🟠 스파이크 자체가 깨져 있음 — 재작성 필요 (3건, 설계 문제 아님)
|
||||
|
||||
| 파일 | 상태 | 무엇을 고쳐야 하나 |
|
||||
|---|---|---|
|
||||
| `13-type-ref-preref-subtype.luau` | 타입 A섹션은 ✅ 통과 / **런타임 B섹션 실행 불가** | B가 A의 더미 스텁(`fakePreRef = nil`)에 막혀 도달 못 함 — 두 섹션 분리 |
|
||||
| `15-type-compute-trailing-deps-typepack.luau` | **파싱 실패**(SyntaxError) | 음성 대조군의 타입 표기가 `TypeError`가 아니라 `SyntaxError`로 걸려 **파일 전체가 아무것도 검증 못 함** — 대조군을 별도 파일/블록으로 격리 |
|
||||
| `16-type-store-key-typefunction.luau` | ❌ 실패 | `types.newfunction` 시그니처가 설치된 버전의 실제 API와 안 맞음 — 실제 API 재확인 후 재시도 |
|
||||
|
||||
## ⚪ 아직 안 돌림
|
||||
|
||||
| 파일 | 이유 |
|
||||
|---|---|
|
||||
| `10-roblox-studio-checks.server.luau` | **Studio 전용**(`luau` CLI로 못 돌림). A 섹션 일부만 사용자가 자작 스크립트로 실측 — `audit/gcconn-trick-verification.md`. A-1/A-2/B/C는 여전히 미확인 |
|
||||
|
||||
---
|
||||
|
||||
## ✅ 통과 — 설계 성립 확인됨 (런타임 12개)
|
||||
|
||||
| 파일 | 확인된 것 |
|
||||
|---|---|
|
||||
| `01-two-pass-array-hash-order` | 배열 파트 전체 → 해시 파트 순. `Dispatch.drive` 두 패스 계약과 `PreRef` 호이스팅의 전제 |
|
||||
| `02-none-sentinel-vs-nil-holes` | `nil` 소진 시 `#t` 50→49로 무너짐 / `None`은 항상 50. 반대로 Ref 콜백 배열은 `None` 쓰면 죽은 슬롯 1000개 잔존 — **두 배열의 규칙이 서로 반대여야 함**이 정량 확인 |
|
||||
| `03-recursive-store-bind-dispatch` | StoreBind 재귀 재-dispatch, `None`→`nil` 흐름, 무한재귀 없이 종료 |
|
||||
| `04-dispatch-chain-retractFrom` | 인덱스 기반 체인 + **음성 대조군이 감사 버그를 재현**(아래 별도 절) |
|
||||
| `05-store-state-diamond-propagation` | 다이아몬드에서 재계산 정확히 1회, invalidate 2번째는 즉시 중단 |
|
||||
| `06-component-boundary-nil-hole-props` | `or None` 없으면 앞쪽 nil-hole로 슬롯 소실, 관용구 쓰면 항상 보존 |
|
||||
| `07-relate-weak-table-gc` | **연쇄 GC 확정**(아래 별도 절) — GC-native 아키텍처의 핵심 전제 |
|
||||
| `09-type-modifier-overridden-subtype` | 문서가 우려한 `FrameModifier`↔`GuiObjectModifier` 서브타입 깨짐이 그대로 재현, fallback(`any`)은 정상 |
|
||||
| `11-modifier-illegal-value-error` | Modifier 필드/Source에 핸들러 계층 값 넣으면 즉시 error — 16개 케이스 전원 |
|
||||
| `17-modifier-index-tableclone-chaining` | 제네릭 `__index` + `table.clone` 체이닝, 메타테이블 참조 공유, 형제 분기 무오염 |
|
||||
| `18-relate-mutual-cycle-gc` | **두 `Relate` 상호 순환은 실제로 GC 안 됨**(아래 별도 절) |
|
||||
| `19-ownership-refcount-relate-patterns` | Tag 참조 카운트 / Attribute 점유 체크 / Slot `claimOwner` vs `claimOwnerAt` — **음성 대조군 포함** 전원 통과 |
|
||||
| `20-slot-splice-index-arithmetic` | `Splice` 산술 11개 경계 케이스 전부 참조 구현과 일치 |
|
||||
| `12-type-attribute-generic-key-narrowing` | ❌지만 **설계 영향 없음** — 제네릭 키 narrowing이 안 되는 건 `attribute-plan.md`가 이미 fallback으로 예비해둔 결과(타입 패밀리가 유일하게 믿을 경로) |
|
||||
| `14-type-nilable-default-overload` | ⚠️ 부분 — 의도한 오용은 막지만 정상 nilable 사용례까지 막아 현 스케치로는 채택 불가. 설계 결정은 아직 필요 없음(대안이 이미 UB 경고로 존재) |
|
||||
|
||||
### 특별히 중요한 통과 3건
|
||||
|
||||
**`04` — 직전 감사가 찾은 버그가 음성 대조군으로 재현됨**
|
||||
|
||||
| 관측 지점 | 정상(수정본) | 대조군(버그) |
|
||||
|---|---|---|
|
||||
| 최초 마운트 후 체인 깊이 | **3** | **1** |
|
||||
| 재발행 후 옛 store 구독 | 0 (끊김) | 1 (안 끊김) |
|
||||
| 죽은 store를 건드리면 | 값 유지 | **`STALE`로 덮어써짐** |
|
||||
|
||||
`chains:SetStrong`을 `handler.process` 뒤에 두면 하위 retractor가 통째로
|
||||
유실되고, 결국 **버려진 store가 나중에 UI를 덮어쓰는** 데까지 감.
|
||||
|
||||
**`07` — GC-native 아키텍처의 핵심 전제**
|
||||
```
|
||||
inst 5개만 살린 상태 → 살아남은 payload 5 / 엔트리 5 (기대치 일치)
|
||||
모든 참조를 놓은 뒤 → 살아남은 payload 0 / 엔트리 0 (기대치 일치)
|
||||
```
|
||||
`bindLifetime`으로 매달아둔 자원이 `inst`와 함께 연쇄 소멸함이 확인됨.
|
||||
(이 스파이크는 원래 sanity check만 하고 있어서 이번에 보강한 것 — 파일이
|
||||
스스로 적어둔 "weak table 엔트리를 셀 방법 없음"이라는 전제가 틀렸음.)
|
||||
|
||||
**`18` — `relate-plan.md`의 상호 순환 경고**
|
||||
```
|
||||
상호 강참조 순환: inst=true, value=true (GC 못 풂)
|
||||
한쪽을 weak-value로 낮춤: inst=false, value=false (풀림)
|
||||
```
|
||||
추측이 아니라 **실제로 GC가 안 됨** — `Slot`의 두-`Relate` 수정이 필수
|
||||
조치였음이 입증.
|
||||
|
||||
---
|
||||
|
||||
## 이 표를 갱신하는 방법
|
||||
|
||||
스파이크를 돌리거나 고칠 때마다 **여기 분류부터 옮기고**, 상세 서술은
|
||||
`audit/`의 실행 결과 문서에 쓸 것. 이 파일은 "지금 뭘 봐야 하는가"만
|
||||
빠르게 답하는 용도라 길어지면 안 됨(파일별 검증 의도/배경은
|
||||
`luau-test/README.md`가 이미 담당).
|
||||
|
|
@ -68,7 +68,7 @@ v1 폐기 API/버그/구조 결함 전부 v2 설계를 정당화하는 내부
|
|||
|
||||
### bind-system-plan.md (943줄, 최대 문서)
|
||||
- 초심자: Source/Store/State 기본 정의+생성자, State 읽기 전용 규칙 / dot-access가 값 읽기 1급 경로 / `:With`+`:Compute` 최소 사용법 / Ref 기본 개념(children 배열에 직접 놓기, 별도 `CreatedRef` 없음) / 이벤트 self 미채택 기본 규칙+문자열 키 / 인스턴스 생성(제네릭+정적 필드) / 라이브러리 초기화 3줄(`RobloxFactory(QuadBase)`)
|
||||
- api: `state:Observer(fn)` 사용법(→심화: weak-table 내부 인덱싱) / `:Subscribe()`/`:Unsubscribe()` 시그니처(→심화: 강참조 레지스트리 구조) / Ref 일반화 표면 API(→심화: "왜 값이 아니라 콜백인가") / 이벤트 store-bind 존재+권장 안 함 가이드(→심화: 엔지니어링 비용 근거) / 핸들러 4종 계약(`isHandlable`/`priority`/`process`/`retract`) / `Attribute<T>` 특수 키 후보(미확정 명시 필요)
|
||||
- api: `state:Observer(fn)` 사용법(→심화: weak-table 내부 인덱싱) / `:Subscribe()`/`:Unsubscribe()` 시그니처(→심화: 강참조 레지스트리 구조) / Ref 일반화 표면 API(→심화: "왜 값이 아니라 콜백인가") / 이벤트 store-bind 존재+권장 안 함 가이드(→심화: 엔지니어링 비용 근거) / 핸들러 3종 계약(`isHandlable`/`priority`/`process` — `process`가 자기 retract 클로저를 반환, 2026-08-13 다섯 번째 세션에 4종에서 축소) / `AttributeKey<T>` 특수 키(2026-08-11 아홉 번째 세션에 `Attribute<T>`에서 개명, 그룹 값 `Attribute(...)`와 구분 — 확정됨)
|
||||
- 심화: push-invalidate/pull-recompute 전파 모델+"관측해야 실체화된다" 원칙+`previous` 캐비엇 / **왜 State를 Modifier처럼 플래튼하지 않는가**(이미 문서화 완료, 아래 3번 참고) / Store가 Store를 못 담는 이유 / 이벤트 self 미채택 4가지 근거 / store-bind 재귀 래핑 내부 메커니즘, retract가 Destroy 시 호출 안 되는 이유 / 같은 팩토리 재호출 no-op·다른 팩토리 충돌 에러 내부 안전장치
|
||||
- skip: quad2-try 리서치 결과 섹션 전체(OOP 상속/커스텀 파서/Slot 스텁/`Pipe` 폐기 이력) / PA님 코드 교차검증 절(역사적 검증 기록) / "남은 열린 질문"/"확정된 것" 메타 요약
|
||||
|
||||
|
|
|
|||
|
|
@ -488,3 +488,63 @@ dispatch-redispatch-diff-plan.md`에만 있음** — base만 읽고 구현하면
|
|||
이미 base에 확정 반영됨**(재디스패치 모델과 독립적인 결정이라 먼저 들어감)
|
||||
— 이 비대칭을 헷갈리지 말 것.
|
||||
|
||||
## 여섯 번째 라운드 (같은 세션, 마무리) — 첫 실측 + 코퍼스 전반 감사
|
||||
|
||||
### 스파이크 첫 실측
|
||||
|
||||
`luau`/`luau-analyze`가 사용 가능해져 2026-08-09 이래 처음으로 돌림.
|
||||
런타임 12개 전원 통과. 상세는 `audit/luau-test-first-run-2026-08-13.md`,
|
||||
상태 분류는 신설한 `luau-test/STATUS.md`(사용자 요청: "사람이 보기엔 뭐를
|
||||
바로 봐야하는지 눈에 안 띄어서").
|
||||
|
||||
핵심 셋: `04`가 이번 세션 감사의 버그를 음성 대조군으로 재현(체인 깊이
|
||||
3→1, 죽은 store가 UI를 덮어씀), `07`은 보강해야 실제 검증이 됐고(연쇄 GC
|
||||
미검증 상태였음 + "weak 엔트리를 셀 방법 없다"는 자기 전제가 틀렸음),
|
||||
`18`이 `Relate` 상호 순환 경고를 실증.
|
||||
|
||||
타입에서 진짜 이슈 하나 — `:Compute(fn)`의 lazy 핸들 계약이 Luau 양방향
|
||||
추론과 충돌. 최소 재현으로 직접 좁힘(에이전트 보고를 액면 그대로 안 받음):
|
||||
`read`/`self` 표기 조정으로는 안 풀리고 콜백이 raw 값을 받으면 완전 클린 —
|
||||
즉 계약 자체가 원인. `question.md` **0-Y** 신설. 사용자 방향은 "순환 타입을
|
||||
만들기보다 `State` 타입을 구울 때 인라이닝"(`Modifier` flat 타입 생성과
|
||||
같은 결)이고, 사람이 직접 확인할 부분이라 0-Z와 함께 사용자 판단 목록.
|
||||
|
||||
부수 수확: `17` 크래시의 원인이 실은 `modifier-plan.md`의 문서 결함이었음
|
||||
— "데이터를 테이블에 직접 두고"가 "self 최상위 리터럴 키"로 읽히는데,
|
||||
그러면 `__index`가 `rawget` 성공 시 안 불려 **같은 필드 재호출이 죽음**.
|
||||
그 재호출 패턴이 문서 3·4절의 대표 용례라 실사용에서 즉시 터지는 경로.
|
||||
|
||||
### 코퍼스 전반 모순·stale 감사 (사용자 요청)
|
||||
|
||||
에이전트 둘로 base 내부 / base↔바깥 정합성을 나눠 감사. **오탐 방지를 위해
|
||||
"의도적으로 미반영인 것"(⚠️ 배너가 예고하는 `hintValue` 건, `research/`의
|
||||
미래 설계안, `archive/`·`session/`의 원문 보존)은 보고 대상에서 제외**하도록
|
||||
지시. 보고받은 것은 전부 직접 사실 확인 후 수정:
|
||||
|
||||
**가장 위험했던 것 — `ROADMAP.md`가 최우선 게이트를 전혀 안 짚고 있었음.**
|
||||
base 4개 문서엔 ⚠️ 배너를 달아뒀는데 정작 **구현 순서의 소스**인 ROADMAP엔
|
||||
없어서, M2/M4/M10 담당자가 그대로 옛 모델로 구현할 위험이 실재했음 — 세
|
||||
마일스톤 전부에 배너/포인터 추가.
|
||||
|
||||
**두 번째 — `slot-plan.md` 앞부분이 뒤집힌 결정을 "확정"으로 자칭.**
|
||||
487-495행("폐기, 옮기지 않음, portal은 오버엔지니어링")과 "열린 질문" 절이
|
||||
역전 표시 없이 남아 있어, 문서를 앞에서부터 읽는 구현자가 구 모델로 짤
|
||||
위험. 🔄 역전 배너 추가. 더 심각하게 **`reconcile` 의사코드가 여전히
|
||||
`rawRemove`(파괴)를 부르고 있었음** — 같은 문서가 "[반영 완료]"라 태그해둔
|
||||
것과 정면 모순이라 `rawUnmount`를 신설해 교체. filter/toggle 근거 절도
|
||||
"제거=파괴" 전제 위에 서 있어 캐비엇 추가(결론 자체는 언마운트로도 유효).
|
||||
|
||||
**그 외**: `ROADMAP.md` M11 Tween이 이미 확정된 결정 넷을 미결로 두고
|
||||
있던 것(override 정책/옵션 값 모양/`Animate` 시그니처/`initValue`)과 죽은
|
||||
링크 4곳, `bind-system-plan.md`/`architecture.md`의 "4종 계약" 잔여,
|
||||
`store-semantics.md`의 "`State`는 가칭" stale(2026-08-12에 확정됨),
|
||||
`luau-test/README.md`의 `04`/`19` 판정 기준이 재작성된 파일을 못 따라간 것
|
||||
(**`19` C섹션 기준이 정상 동작을 실패로 오판하게 돼 있었음**),
|
||||
`documentation-content-map.md`의 4종 계약/`Attribute<T>` 옛 이름,
|
||||
`modifier-plan.md`의 "정정이 원문에 소급 안 됨" 패턴.
|
||||
|
||||
**판단해서 안 고친 것**: `effect-plan.md`의 "cleanup" 14곳 —
|
||||
Handler `retract`(내부 배관)와 달리 **`Effect(fn)`에서 사용자가 작성한
|
||||
`fn`이 반환하는 React식 콜백**이라 다른 층위. 매 감사마다 재지적되므로
|
||||
`lifecycle-pattern.md`에 "확인 완료, 의도된 별개 개념"으로 못박아 둠.
|
||||
|
||||
|
|
|
|||
|
|
@ -903,3 +903,9 @@ handoff용 저장이 불필요해짐 — `Relate`는 여러 위치/사이클을
|
|||
— 런타임 12개 전원 통과, `04`가 위 버그를 음성 대조군으로 재현, `07` 보강으로
|
||||
GC-native 전제 확정, `18`이 `Relate` 순환 경고 실증. 타입에선 `:Compute(fn)`
|
||||
lazy 핸들 계약이 Luau 추론과 충돌하는 게 드러나 `question.md` **0-Y** 신설.
|
||||
스파이크 상태는 이제 **`luau-test/STATUS.md`가 소스**(pass/사람 결정 필요/
|
||||
스파이크 깨짐/미실행 분류). 마지막으로 코퍼스 전반 모순·stale 감사 —
|
||||
`ROADMAP.md`가 최우선 게이트(0-Z)를 전혀 안 짚고 M11 Tween을 이미 끝난
|
||||
결정인데 미결로 두던 것, `slot-plan.md` 앞부분이 뒤집힌 "폐기, portal 안 함"을
|
||||
여전히 "확정"으로 자칭하던 것(구현자가 앞에서부터 읽으면 구 모델로 짤 위험),
|
||||
`reconcile`이 여전히 파괴 경로였던 것(→ `rawUnmount` 신설) 등을 정정.
|
||||
|
|
|
|||
54
ROADMAP.md
54
ROADMAP.md
|
|
@ -63,6 +63,18 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
|
||||
## M2 — 디스패치 엔진
|
||||
|
||||
> **⚠️ 착수 전 필독 — 아래 체크리스트는 *현행* 모델(래핑 핸들러가 재-dispatch
|
||||
> 전에 `Dispatch.retractFrom`을 선행 호출)을 기준으로 쓰여 있고, 그 모델은
|
||||
> 교체가 예정돼 있습니다.** 새 모델("하강 diff": 선행 `retractFrom`을 폐기하고
|
||||
> `Dispatch.process`가 **핸들러를 먼저 비교** — 같으면 그 자리 클로저에 새
|
||||
> 값을 넘기고 자기 `process` 재호출, 다르면 그 자리부터 전량 철거)은
|
||||
> `.claude/research/dispatch-redispatch-diff-plan.md`에 있고, `.claude/question.md`
|
||||
> **0-Z**(Attribute 이름 소유권) 하나만 정해지면 base와 이 문서에 한 번에
|
||||
> 반영됩니다. **0-Z가 미해결인 채로 M2/M4/M10을 구현하면 곧 갈아엎어야 하는
|
||||
> 코드를 짜게 됩니다** — 먼저 0-Z를 해소할 것. (base 4개 문서에도 같은 취지의
|
||||
> ⚠️ 배너가 달려 있는데, ROADMAP에만 없어서 2026-08-13 감사에서 지적됨.)
|
||||
|
||||
|
||||
- [ ] `Dispatch/init.luau` — `Dispatch.getHandler(inst,k,v): Handler?`(순수
|
||||
스캔, `isHandlable`+`priority`) / `Dispatch.process(inst,k,v,index)`
|
||||
(오케스트레이터: 그 인덱스 점유 여부 체크 → getHandler → 매치된
|
||||
|
|
@ -230,6 +242,10 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
|
||||
## M4 — 첫 end-to-end 반응형 업데이트
|
||||
|
||||
> **⚠️ M2의 ⚠️ 배너와 같은 주의** — 재디스패치 모델이 교체 예정,
|
||||
> `question.md` 0-Z 먼저 해소할 것.
|
||||
|
||||
|
||||
- [ ] `Dispatch/StoreBind.luau`(재귀 재실행 로직, 엔진 무관 — 재-dispatch
|
||||
전 `Dispatch.retractFrom(inst,k,index+1,realv)` 호출 필수, 그 다음
|
||||
`Dispatch.process(inst,k,realv,index+1)`. `base/bind-system-plan.md`
|
||||
|
|
@ -467,7 +483,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
- [ ] 프로퍼티류 필드 타입에 `T' = T | Tween<T>` 치환 반영(타입 생성
|
||||
스크립트가 `Position: UDim2` 자리를 `UDim2 | Tween<UDim2>`로 만들면
|
||||
끝, Modifier 런타임/`__index` 자체엔 변경 없음 — `modifier-plan.md`
|
||||
10번, 2026-08-10 세션, `research/tween-plan.md`)
|
||||
10번, 2026-08-10 세션, `base/tween-plan.md`)
|
||||
|
||||
## M8 — Ref
|
||||
|
||||
|
|
@ -513,6 +529,11 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
|
||||
## M10 — Event / OnChange / Attribute / Tag
|
||||
|
||||
> **⚠️ M2의 ⚠️ 배너와 같은 주의** — 재디스패치 모델이 교체 예정이고,
|
||||
> 특히 **Attribute 이름 소유권(`question.md` 0-Z)이 이 마일스톤의 직접
|
||||
> 대상**임. 0-Z 먼저 해소할 것.
|
||||
|
||||
|
||||
- [ ] `Handlers/Event.luau`(`ReflectionService` 기반 자동 판별)
|
||||
- [ ] `Handlers/OnChange.luau`(`OnChange(name)` DI 키 팩토리+Handler,
|
||||
`GetPropertyChangedSignal` 바인딩 — 제네릭 없이 콜백 타입은 인라인
|
||||
|
|
@ -550,7 +571,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
## M11 — Tween
|
||||
|
||||
**[2026-08-10 세션, 구조 재설계]** 독립 Dispatch 핸들러 모델에서 값-레벨
|
||||
`Tween<T>` 래퍼 모델로 전환 — 상세는 `research/tween-plan.md`(전면
|
||||
`Tween<T>` 래퍼 모델로 전환 — 상세는 `base/tween-plan.md`(전면
|
||||
재작성), 구 모델은 `archive/tween-special-bind-key-reversed.md`.
|
||||
|
||||
- [ ] `quad-base/Tween.luau`(값 타입만 — `Tween(opts)` 팩토리, `isTween`/
|
||||
|
|
@ -561,16 +582,25 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
없음, 엔진 객체=활성 트윈) + 첫 세팅은 무조건 애니메이션 없이
|
||||
스냅(hasBeenSet 억제) + 활성 트윈 정리는 override 정책 완료 후에만
|
||||
새 값 세팅(순서 뒤바뀌면 트윈 다음 프레임이 방금 세팅한 값을 덮어씀)
|
||||
- [ ] override 정책 4가지(기본 Cancel/Override/Delete-restart/
|
||||
Move-to-end-restart) 중 기본값 외 옵션 키 이름/시그니처 확정,
|
||||
Tween→plain 전환에 5번째 옵션이 필요한지 확인
|
||||
- [ ] `research/tween-plan.md` "트윈 옵션 값 모양" 확정(TweenInfo 그대로
|
||||
vs 편의 필드+기본값 — 소견은 후자)
|
||||
- [ ] `quad-roblox/Animate.luau`(편의 콤비네이터 — `:Apply`로 체이닝,
|
||||
`useTween` 우회는 이걸로 자연히 커버되어 별도 옵션 필드 불필요,
|
||||
정확한 시그니처는 M11에서 확정)
|
||||
- [ ] `initValue`(진입 애니메이션) 필요성 재검토 — 필요해지면 hasBeenSet
|
||||
억제 동작과의 상충부터 풀 것(`research/tween-plan.md` 참고)
|
||||
- [x] **override 정책 확정 완료**(2026-08-12 세션, `base/tween-plan.md`
|
||||
"확정: `Tween{...}` 최종 모양" 절) — 검토했던 4가지가 **`Tween.Cancel`
|
||||
(기본)/`Tween.Finish` 2값으로 압축**됨(로블록스 `TweenBase` API 현실상
|
||||
나머지가 관찰상 Cancel과 동일). Tween→plain 전환도 두 옵션 모두
|
||||
"정리 후 즉시 덮어쓰기"로 수렴해 5번째 옵션 불필요로 확정.
|
||||
**구현 시 순서 주의**: 이전 트윈 정리 → 그 다음 새 값 세팅
|
||||
- [x] **트윈 옵션 값 모양 확정 완료**(2026-08-12 세션, `base/tween-plan.md`)
|
||||
— `Info: TweenInfo?` 우선 + 편의 필드(`Time`/`Style`/...) 폴백,
|
||||
기본값은 로블록스 `TweenInfo.new()` 자체 기본값과 일치. 옵션 필드는
|
||||
전부 plain만(State 불가)
|
||||
- [ ] `quad-roblox/Animate.luau` — **시그니처도 이미 확정 완료**(2026-08-12
|
||||
두 번째/세 번째 세션, `base/tween-plan.md`): `Tween` opts(`Value` 제외)를
|
||||
`T|State<T>`로 받아 각 필드를 resolve한 뒤 `Tween{...}`을 반환하는
|
||||
`function(self)...end` — `:Apply(Animate{...})`로 체이닝(`:Compute`가
|
||||
아님, `research/operator-sugar-plan.md` "왜 `:Apply`인가"). `CanAnimate`
|
||||
필드 포함(`false`면 `Tween`으로 안 감싸고 plain 값 그대로). M11은
|
||||
**구현만** 하면 됨
|
||||
- [x] **`initValue`(진입 애니메이션) — 에이전트 범위 제외로 확정**
|
||||
(2026-08-12 세션, 사용자가 직접 처리하기로) — 재검토 항목 아님
|
||||
|
||||
## 특정 마일스톤에 안 묶이고 병행 가능
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue