docs(dispatch): 0-Z/0-A 확정 — Attribute 전용 키+이름 claim, 하강 diff 전면 반영
`question.md` 최우선 두 건을 한 패스로 닫고, 배너를 달고 있던 7개 문서 +
인덱스 레이어 전체를 갱신했다.
0-Z (Attribute 이름 소유권):
- 그룹은 비공개 `GetKey`로 이름마다 자기 전용 키를 써서 위임 → 교차 오염
구조적 제거. 이름 소유권은 `AttributeKeyHandler`의 이름 claim이 판정
(`nameClaims` Relate, 다른 키가 같은 이름을 노리면 즉시 error).
- 권고안 (a)(그룹 안 claimant Relate)는 그룹↔직접 쓰기를 못 잡아 기각 —
두 경로가 만나는 말단 핸들러에서 공개 키는 같은 객체라 소유자 구분 불가.
0-A (재디스패치 = 하강 diff):
- 래핑 핸들러의 선행 `retractFrom` 폐기, `Dispatch.process`가 슬롯의
`handler`를 먼저 비교(같으면 클로저에 새 값 전달 후 재process, 다르면
그 자리부터 전량 철거).
- 귀결: `retractFrom`이 3-인자로 축소(힌트를 외부에서 만들어 넣을 자리
소멸), `isX(hintValue)` 가드 규칙 폐지, 깊은 체인 힌트 유실 캐비엇 삭제,
Dispatch의 점유 체크 폐지.
- 9차 세션이 미뤄둔 2단계 분할을 같이 수행 — 디스패치 코어를
`base/dispatch-core-plan.md`로 분리하며 재작성(bind-system-plan은
2263→1219줄). 옛 모델은 archive/dispatch-hintvalue-model-reversed.md.
패키지 재배치 (사용자 제기):
- Tag/Attribute의 부기 알고리즘 전체를 quad-base로, 백엔드는
`addTag`/`removeTag(inst,{string})`/`setAttribute(inst,name,v)` 3개 op만
주입(웹 className/data-* 대응). 엔진 고유 타입 패밀리만 백엔드.
- `HANDLER_PRIORITY_FALLBACK` 신설 — base 제공 핸들러의 밴드, 백엔드가
평범한 우선순위로 덮어쓰면 언제나 이김.
부수: 스파이크 04/19가 옛 모델을 검증 중이라 rewrite-required로 이동,
question.md 최우선 칸 비움, 새 소소 항목 2건 등록(Merged 이름 중복,
`hintValue` 이름 재검토). doc-check ERROR 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KckSawrsJSJmDBcSojJPxZ
This commit is contained in:
parent
1f4f21c75a
commit
69466abc47
28 changed files with 2252 additions and 1784 deletions
File diff suppressed because one or more lines are too long
|
|
@ -1,16 +1,42 @@
|
|||
# 재디스패치 = 하강 diff — `retractFrom` 선행 폐기, 핸들러 비교를 클로저 호출 앞에 (설계안)
|
||||
# [역전됨] 재디스패치 = "철거 후 재구축" + `hintValue` 힌트 모델
|
||||
|
||||
**상태**: research — 2026-08-13 여섯 번째 세션에 사용자가 제기하고 방향을
|
||||
제시, 같은 세션 후속 라운드에서 모델이 거의 확정됨. **`base/` 반영 전
|
||||
남은 열린 항목은 아래 5절의 하나뿐**(Attribute 이름 소유권). 그 하나만
|
||||
정해지면 `bind-system-plan.md`/`tag-plan.md`/`slot-plan.md`/
|
||||
`attribute-plan.md`를 한 번에 옮기면 됨.
|
||||
**상태**: 역전됨 — 2026-08-13 여섯 번째 세션에 사용자가 결함을 제기하고
|
||||
방향을 제시, 같은 날 열네 번째 세션에 마지막 열린 항목(Attribute 이름
|
||||
소유권)까지 확정되며 **`base/dispatch-core-plan.md`로 전면 반영 완료**.
|
||||
이 문서는 그때까지 research/ 아래 dispatch-redispatch-diff-plan이었던 설계안
|
||||
원문이고, **지금 유효한 모델은 `base/dispatch-core-plan.md`의 "Dispatch
|
||||
체인" 절** — 여기 남기는 이유는 "왜 힌트 모델을 버렸는가"의 재현 사례와
|
||||
근거가 통째로 보존될 가치가 있어서(같은 함정을 다시 설계하지 않도록).
|
||||
|
||||
> **문서 이력**: 최초엔 dispatch-hint-to-oldvalue-plan(현재 없는 옛 파일명)으로
|
||||
> "힌트 대신 `oldValue`를 넘기자"는 보완안을 담고 있었으나, 사용자가
|
||||
> **"이전 값인 oldValue는 처음부터 클로저라 이미 본인이 알지 않아요?"**
|
||||
> 라고 지적해 그 보완안이 통째로 불필요함이 드러남(아래 3-2) — 파일명도
|
||||
> 바꿈.
|
||||
## 뒤집힌 옛 모델의 골자 (원문 보존)
|
||||
|
||||
```lua
|
||||
-- 옛 계약: 래핑 핸들러가 재-dispatch 전에 자기 아래를 먼저 철거하고,
|
||||
-- 그때 "곧 디스패치될 raw 값"을 힌트로 실어 보냄.
|
||||
function StoreBind.process(inst, k, state, index)
|
||||
local observer = state:Observer(function()
|
||||
local realv = state:Get()
|
||||
Dispatch.retractFrom(inst, k, index + 1, realv) -- 선행 철거 + 힌트
|
||||
Dispatch.process(inst, k, realv, index + 1)
|
||||
end)
|
||||
...
|
||||
end
|
||||
|
||||
function Dispatch.retractFrom(inst, k, index, v) -- 4-인자(마지막이 힌트)
|
||||
...
|
||||
retractor(if i == index then v else nil) -- 힌트는 직속 1단계에만
|
||||
...
|
||||
end
|
||||
```
|
||||
|
||||
옛 모델이 같이 요구하던 규칙들(전부 지금은 폐기):
|
||||
- **`isX(hintValue)` 가드 필수** — 힌트의 타입이 계약으로 보장되지 않아서.
|
||||
- **`hintValue`는 직속 위임 1단계에만 보장, 깊은 인덱스는 항상 `nil`** —
|
||||
그래서 `State<State<Tag>>`류에서 깜빡임 방지가 조용히 꺼짐.
|
||||
- **`Dispatch.process`의 점유 체크(이미 점유된 인덱스면 즉시 error)** —
|
||||
Attribute 이름 소유권이 이 부수 효과에 얹혀 있었음.
|
||||
|
||||
## 아래는 역전을 이끈 분석 원문 (당시 `research/` 문서 그대로)
|
||||
|
||||
## 1. 현행 `hintValue`의 실제 결함 (사용자 지적, 확인됨)
|
||||
|
||||
|
|
@ -267,3 +293,21 @@ Dispatch.process(inst, k, realv, index + 1) -- retractFrom 선행 호출 없
|
|||
**요약**: 배너를 달고 있는 파일 = 반영 대상. 위 7개(`bind-system-plan`/
|
||||
`tag-plan`/`slot-plan`/`attribute-plan`/`architecture`/`ROADMAP`/
|
||||
`ref-plan`)가 전부이고, 반영이 끝나면 각 파일의 ⚠️ 배너도 같이 제거할 것.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 최종 결말 (2026-08-13 열네 번째 세션)
|
||||
|
||||
- **5절의 열린 항목은 (a)가 아니라 그 변형으로 확정됨** — 사용자가 제시한
|
||||
**그룹 전용 키(`GetKey`, 비공개) + `AttributeKeyHandler`의 이름 claim**.
|
||||
(a)(그룹 안에 이름별 claimant `Relate`)를 그대로 쓰면 **그룹↔직접 쓰기
|
||||
충돌을 못 잡는다**는 게 트레이싱으로 드러났기 때문 — 두 경로가 만나는
|
||||
유일한 지점인 말단 핸들러에서 `k`가 같은 객체라 소유자를 구분할 수 없음.
|
||||
근거와 최종 의사코드는 `base/attribute-plan.md` "이름 소유권" 절.
|
||||
- **`Dispatch.retractFrom`은 4-인자에서 3-인자가 됐음** — 값을 넘기는
|
||||
경로가 `Dispatch.process`의 "같은 핸들러" 분기 하나로 통일되면서, 외부가
|
||||
힌트를 만들어 넣을 자리 자체가 없어짐(옛 결함이 구조적으로 재발 불가).
|
||||
- 6절의 반영 목록(7개 문서 + `bind-system-plan.md` 2단계 분할)은 같은
|
||||
세션에 전부 수행됨 — 디스패치 코어는 `base/dispatch-core-plan.md`로
|
||||
분리되며 재작성됐다.
|
||||
|
|
@ -104,7 +104,17 @@ API 전부에 걸리며, 2026-08-07 일곱 번째 세션(커링 스타일 확정
|
|||
정상 nilable 사용례까지 막음), `16`(`type function` API 불일치). 전부
|
||||
`audit/luau-test-first-run-2026-08-13.md`.
|
||||
|
||||
### 0-Z. ⭐ **최우선 — Attribute 이름 소유권을 무엇으로 판정할 것인가** (2026-08-13 여섯 번째 세션, 사용자가 다음 세션 심층 분석으로 이관)
|
||||
### 0-Z. ~~⭐ 최우선~~ **[해소됨, 2026-08-13 열네 번째 세션]** — Attribute 이름 소유권을 무엇으로 판정할 것인가 (2026-08-13 여섯 번째 세션 신설)
|
||||
|
||||
> **결론**: 후보 (a)가 아니라 그 변형 — **그룹 전용 키(비공개 `GetKey`)
|
||||
> + `AttributeKeyHandler`의 이름 claim**. 사용자가 `Attribute:GetKey(name)`
|
||||
> 아이디어를 제시했고, 트레이싱 결과 (a)만으로는 **그룹↔직접 쓰기 충돌을
|
||||
> 못 잡는다**는 게 드러나(두 경로가 만나는 말단 핸들러에서 공개 키는
|
||||
> 같은 객체라 소유자 구분 불가) 전용 키가 필요함이 확인됨. 전용 키가
|
||||
> 교차 오염을 구조적으로 없애고, claim이 남은 충돌을 즉시 error로
|
||||
> 드러냄. `GetKey`는 공개 API로 내지 않음(같은 키가 두 자리에 놓이는
|
||||
> 0-W류 갭을 원천 차단). 정본은 `base/attribute-plan.md` "이름 소유권"
|
||||
> 절. **아래는 해소 전 원문.**
|
||||
|
||||
**이게 지금 유일하게 `base/` 반영을 막고 있는 결정.** 아래 0-A의
|
||||
재디스패치 모델은 나머지가 전부 확정됐고, 이 항목 하나만 정해지면
|
||||
|
|
@ -138,21 +148,29 @@ Attribute가 직접 해야 함.**
|
|||
claimant 단방향이면 충분할 것이라는 방향.
|
||||
- 원문 맥락과 기각된 두 중간안(`rawNew` 전용 키, `AttributeGroupKeyHandler`
|
||||
체크포인트)은 `archive/checkpoint-handler-pattern-reversed.md`,
|
||||
분석은 `research/dispatch-redispatch-diff-plan.md` 5절.
|
||||
분석은 `archive/dispatch-hintvalue-model-reversed.md` 5절.
|
||||
|
||||
**대안 후보(정리해둠)**: (a) 이름별 claimant `Relate`를 Attribute에
|
||||
국소적으로 — 권고, (b) UB로 두고 문서로만 금지 — 증상이 "조용한 오작동 +
|
||||
교차 오염"이라 다른 UB들(즉시 스택오버플로/즉시 error)보다 나빠서 비권장,
|
||||
(c) `Dispatch`에 claimant 개념 일반화 — 이번에 걷어낸 방향이라 반대.
|
||||
|
||||
### 0-A. `hintValue` 폐기 → process 하강 중 핸들러 비교 (2026-08-13 여섯 번째 세션, **Attribute 건 외 확정**)
|
||||
### 0-A. ~~`hintValue` 폐기 → process 하강 중 핸들러 비교~~ **[해소됨, 2026-08-13 열네 번째 세션 — base 반영 완료]** (2026-08-13 여섯 번째 세션)
|
||||
|
||||
> **결론**: 모델 그대로 채택되어 `base/dispatch-core-plan.md`(같은 세션에
|
||||
> `bind-system-plan.md`에서 분리 신설)로 전면 반영. 부수적으로
|
||||
> `Dispatch.retractFrom`이 4-인자에서 **3-인자**가 됐음(값을 넘기는 경로가
|
||||
> `Dispatch.process`의 "같은 핸들러" 분기 하나로 통일되어, 외부가 힌트를
|
||||
> 만들어 넣을 자리 자체가 사라짐). 배너를 달고 있던 7개 문서 전부 갱신
|
||||
> 완료. 뒤집힌 옛 모델 원문은 `archive/dispatch-hintvalue-model-reversed.md`.
|
||||
> **아래는 해소 전 원문.**
|
||||
|
||||
**검토 결과 사용자 지적이 맞음 — 현행 `hintValue`엔 실제 결함이 있음.**
|
||||
힌트가 "그 자리에 곧 디스패치될 raw 값"이라 `None` 센티널이나 `State`/
|
||||
`Tween` 같은 래퍼가 그대로 넘어갈 수 있고, 그러면 말단 핸들러의
|
||||
`isTag(hint)` 가드가 거짓이 되어 **깜빡임/재생성 방지가 조용히 꺼짐**
|
||||
(정확성은 유지돼서 지금까지 안 드러났음). 상세 재현·분석·제안은
|
||||
`research/dispatch-redispatch-diff-plan.md`.
|
||||
`archive/dispatch-hintvalue-model-reversed.md`.
|
||||
|
||||
**후속 라운드에서 모델은 거의 확정됨** — 래핑 핸들러가 `retractFrom`을
|
||||
선행 호출하는 걸 폐기하고, `Dispatch.process` 안에서 **핸들러를 먼저
|
||||
|
|
@ -172,7 +190,7 @@ Attribute가 직접 해야 함.**
|
|||
**실행 규모**: `base/`의 `bind-system-plan.md`/`tag-plan.md`/`slot-plan.md`/
|
||||
`attribute-plan.md` 의사코드 재작성 + `architecture.md` 소스트리 서술과
|
||||
`ROADMAP.md` M2/M4/M6/M10 체크리스트 — 0-Z 하나만 정해지면 한 번에 옮기면
|
||||
됨(어디를 어떻게 고칠지는 `research/dispatch-redispatch-diff-plan.md` 6절에
|
||||
됨(어디를 어떻게 고칠지는 `archive/dispatch-hintvalue-model-reversed.md` 6절에
|
||||
파일별로 적어둠 — 뒤 둘은 2026-08-13 7차 감사에서 그 목록에 빠져 있던 걸
|
||||
발견해 추가). **그때까지 `base/`의 현행 `hintValue` 서술이 유효** —
|
||||
아직 안 옮겼다는 걸 잊고 base만 읽으면 옛 모델로 구현하게 되니 주의.
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
# quad-v2 전체 아키텍처 (현재 상태 요약)
|
||||
|
||||
> **⚠️ [2026-08-13 4차 감사에서 발견] 이 문서는 세션 시작 시 가장 먼저
|
||||
> 읽는 진입점인데, 아래 소스 트리의 `chains`/`retractFrom`(Dispatch/init.luau
|
||||
> 항목)과 `retractFrom`에 재귀 위임(Attribute.luau 항목) 서술은 현행
|
||||
> (교체 예정) 재-dispatch 모델을 전제로 쓰여 있음 — `question.md` **0-Z**
|
||||
> 해소 전엔 그대로 구현하면 옛 모델로 짜게 됨. 상세는
|
||||
> `base/bind-system-plan.md` 최상단 배너, `research/dispatch-redispatch-diff-plan.md`.
|
||||
> **✅ [2026-08-13 열네 번째 세션] 재디스패치 모델(0-A)/Attribute 이름
|
||||
> 소유권(0-Z) 확정·반영 완료 — 아래 소스 트리도 갱신됨.** 이전 ⚠️ 배너가
|
||||
> 경고하던 "옛 모델로 짜게 됨" 위험은 해소. 디스패치 코어는
|
||||
> `base/bind-system-plan.md`에서 **`base/dispatch-core-plan.md`로
|
||||
> 분리**됐고, `Tag`/`Attribute`는 알고리즘까지 quad-base로 재배치됐음
|
||||
> (엔진 op만 주입). 뒤집힌 옛 모델은
|
||||
> `archive/dispatch-hintvalue-model-reversed.md`.
|
||||
|
||||
**상태**: base — 횡단 결정의 최종 상태 요약. 특정 기능 plan이 아니라 프로젝트
|
||||
전체에 걸친 결정이라 완료 개념 없음. 근거가 된 원본 브레인스토밍은
|
||||
|
|
@ -78,7 +79,7 @@ quad는 이제 "스크립트"가 아니라 **라이브러리**다. DOMless Roblo
|
|||
방지, 비용은 무시 가능한 수준으로 확인됨).
|
||||
8. **특수 이벤트는 특수 플러깅으로.** `PropertyChangedSignal`, `PropertyChangedEvent ""`
|
||||
같은 것들은 일반 이벤트 바인드가 아니라 pluggable 바인드 핸들러 중 하나로
|
||||
구현(`base/bind-system-plan.md`).
|
||||
구현(`base/dispatch-core-plan.md`).
|
||||
9. **Tracker 미구현.** v1의 소스 변경 감지 자동 재렌더 기능(hot-reload watcher,
|
||||
실제로는 `.claude/initreq/quad/src/tracker.lua` — v1에서도 이미 `exports.lua`에
|
||||
연결 안 된 죽은 코드였음, `reference/quad-v1-architecture.md` 참고)은 렌더
|
||||
|
|
@ -106,7 +107,7 @@ quad는 이제 "스크립트"가 아니라 **라이브러리**다. DOMless Roblo
|
|||
`BaseModule` 테이블을 만들어 팩토리로 채우는 것뿐. Dispatch의 handler
|
||||
레지스트리를 포함해 지금 module-level state로 사는 모든 것(`_initializedBy`
|
||||
마커, Dispatch 레지스트리 등)이 자동으로 테이블별 스코핑됨 — 상세 근거는
|
||||
`base/bind-system-plan.md`의 "Dispatch는 프리미티브가 아니다" 절.
|
||||
`base/dispatch-core-plan.md`의 "Dispatch는 프리미티브가 아니다" 절.
|
||||
14. **pluggable 초기화는 팩토리 함수로.** rbvm처럼 네임스페이스 하나하나 수동
|
||||
init 하는 방식(`base/lifecycle-pattern.md` 5번 항목 참고)은 피하고,
|
||||
`InitRoblox(Module)` 같은 팩토리 함수가 생성된 모듈을 뮤테이션하는 도구를
|
||||
|
|
@ -135,6 +136,15 @@ RbxUtil`이 정확히 이 패턴(루트 하나로 통합 개발/테스트, 서
|
|||
**pluggable 디스패치 엔진 자체도 "인터페이스"로 base가 소유**한다(엔진마다
|
||||
큰 구현을 중복하지 않기 위함 — rbvm이 relation을 하나로 통합하려 했던 것과
|
||||
같은 동기). `quad-roblox`는 그 인터페이스의 **실제 구현체**만 제공.
|
||||
**[2026-08-13 열네 번째 세션 확장] 이 원칙은 "값 타입"이 아니라 "부기가
|
||||
엔진 지식을 요구하는가"로 가른다** — `Tag`의 참조 카운트와 `Attribute`의
|
||||
이름 claim/그룹 위임은 엔진과 무관한 순수 부기라 **핸들러째로 quad-base**,
|
||||
백엔드는 `addTag`/`removeTag`/`setAttribute` 세 op만 주입한다(웹에도
|
||||
`className`/`data-*`라는 대응물이 있어서, 그러지 않으면 같은 알고리즘이
|
||||
백엔드마다 복제됨). 반대로 Property/Event/OnChange는 Reflection·시그널
|
||||
자체가 로직이라 그대로 백엔드 소속 — 상세 기준과 op 시그니처는
|
||||
`base/dispatch-core-plan.md`의 "base가 소유하는 핸들러와 주입되는 엔진
|
||||
op" 절.
|
||||
|
||||
```
|
||||
quad/
|
||||
|
|
@ -148,15 +158,19 @@ quad/
|
|||
│ ├── Store.luau # source 집합체, dot-access로 Source 그대로 반환
|
||||
│ ├── Blocker.luau # 값 기반 emit 지연/합치기(`base/blocker-plan.md`), State/Source와 밀접 연관돼 같은 위치
|
||||
│ ├── Modifier.luau # flatten-before-dispatch, immutable 체이닝, 제네릭 `__index` 필드 setter 합성 + `:Apply`/`:Peek`/`Overridden`(`base/modifier-plan.md`)
|
||||
│ ├── Tag.luau # 값 타입+immutable clone 체이닝(`Tag(...)`/`:Added`/`:Removed`/`:Contains`/`:Apply`/`Merged`), CollectionService 글루는 quad-roblox Handlers/Tag.luau(`base/tag-plan.md`, 2026-08-08 세 번째 세션)
|
||||
│ ├── Attribute.luau # 그룹 값 타입+API(`Attribute(store1, store2, ...)`/`Merged`, `Tag`와 동형) — `SetAttribute` 글루는 quad-roblox Handlers/Attribute.luau(`base/attribute-plan.md`, 2026-08-11 아홉 번째 세션). 단일 키(`AttributeKey<<T>>`)는 값 타입 레이어 없이 quad-roblox 단독 소속(아래)
|
||||
│ ├── Tag.luau # 값 타입+immutable clone 체이닝(`Tag(...)`/`:Added`/`:Removed`/`:Contains`/`:Apply`/`Merged`/`:Names`) — 참조 카운트 Handler는 Dispatch/Tag.luau(아래), 엔진 호출은 주입된 addTag/removeTag(`base/tag-plan.md`)
|
||||
│ ├── Attribute.luau # 그룹 값 타입+API(`Attribute(store1, store2, ...)`/`Merged`/`:NameMap`, `Tag`와 동형) — Handler는 Dispatch/Attribute.luau(아래) (`base/attribute-plan.md`)
|
||||
│ ├── AttributeKey.luau # 단일 키 `AttributeKey<<T>>(name)` + 이름별 weak 캐시(동등성 보장) + 스칼라 편의 패밀리(String/Number/BooleanAttribute) — 엔진 고유 타입 패밀리(Color3Attribute류)만 백엔드 소속(`base/attribute-plan.md` "패키지 배치" 절, 2026-08-13 열네 번째 세션 재배치)
|
||||
│ ├── Tween.luau # 값 타입만(`Tween(opts)` 팩토리, `isTween`/`TweenTag`) — 엔진 무관, 독립 Dispatch 핸들러 아님. 실제 애니메이션 처리는 quad-roblox Handlers/Property.luau 내부 분기(`base/tween-plan.md`, 2026-08-10 세션 재설계)
|
||||
│ ├── Effect.luau # `Effect(fn, state?)` — state 없으면 설치1회+leaf사망시 정리, 있으면 State.Observer를 조합해 재실행(`base/effect-plan.md`)
|
||||
│ ├── Dispatch/
|
||||
│ │ ├── init.luau # process 엔진(반환값=retract 클로저), isHandlable 우선순위 스캔, `chains`(inst,k별 인덱스 배열)+`retractFrom`(`bind-system-plan.md` "Dispatch 체인" 절, 2026-08-08 세 번째 세션 신설, 2026-08-13 다섯 번째 세션 인덱스 기반 전면 재설계)
|
||||
│ │ ├── init.luau # process 엔진 — `chains`(inst,k별 인덱스 배열, 슬롯마다 {handler, retractor}) + 하강 diff(핸들러가 같으면 그 자리 클로저에 새 값을 넘기고 재process, 다르면 그 자리부터 retractFrom) + 3-인자 `retractFrom(inst,k,index)` (`dispatch-core-plan.md` "Dispatch 체인" 절, 2026-08-08 신설 → 2026-08-13 다섯 번째 세션 인덱스화 → 같은 날 열네 번째 세션 하강 diff)
|
||||
│ │ ├── Handler.luau # 핸들러 계약 타입(isHandlable/priority/process — process가 자기 retract 클로저를 반환)
|
||||
│ │ ├── StoreBind.luau # store 값 재귀 재실행 로직(범용, 엔진 무관)
|
||||
│ │ ├── Leaf.luau # (i:number, v=Ref/Observer/PreRef) children-array leaf 매칭 Handler, StoreBind와 같은 층위(범용/엔진무관, 2026-08-08 두 번째 세션 확정)
|
||||
│ │ ├── Tag.luau # TagHandler — 이름별 참조 카운트(`tagNameMap`), 실제 호출은 주입된 addTag/removeTag(inst, {string}). HANDLER_PRIORITY_FALLBACK으로 등록(`base/tag-plan.md`, 2026-08-13 열네 번째 세션 base로 이동)
|
||||
│ │ ├── AttributeKey.luau # AttributeKeyHandler — 이름 claim(`nameClaims`, 소유권 충돌 즉시 error) + 주입된 setAttribute(inst,name,v) 호출, `None`→nil은 재디스패치로 자동(`base/attribute-plan.md` "이름 소유권" 절)
|
||||
│ │ ├── Attribute.luau # AttributeGroupHandler — 그룹 전용 키(비공개 GetKey)로 이름마다 AttributeKey 경로에 인덱스 1 위임, 클로저가 자기 키 전부 retractFrom(`base/attribute-plan.md` "메커니즘" 절)
|
||||
│ │ └── Slot.luau # add/remove/clear 재조정 로직(추상 자식 참조 기준)
|
||||
│ ├── Relate.luau # inst를 weak 키로 하는 범용 릴레이션(`SetWeak`/`GetWeak`/`SetStrong`/`GetStrong`), 비싱글톤 생성자(`base/relate-plan.md`) — 구 PerInstanceState/perInstanceState 대체
|
||||
│ ├── LifetimeHandle.luau # `bindLifetime(inst,value)`/`canExecute(inst,value)` 탑레벨 함수 "인터페이스"(타입/계약만), 내부는 Relate 사용(`base/lifecycle-pattern.md`)
|
||||
|
|
@ -166,15 +180,13 @@ quad/
|
|||
└── quad-roblox/
|
||||
├── wally.toml
|
||||
└── src/
|
||||
├── RobloxFactory.luau # BaseModule 뮤테이션, 재호출 가드(같은 팩토리=무시/다른=에러)
|
||||
├── RobloxFactory.luau # BaseModule 뮤테이션, 재호출 가드(같은 팩토리=무시/다른=에러) — 주입 대상엔 bindLifetime/canExecute 외에 addTag/removeTag/setAttribute도 포함(2026-08-13 열네 번째 세션)
|
||||
├── EngineOps.luau # 주입되는 엔진 op 구현: addTag(inst,{string})/removeTag(inst,{string})=CollectionService, setAttribute(inst,name,v)=inst:SetAttribute(v==nil이면 삭제) (`base/dispatch-core-plan.md` "base가 소유하는 핸들러와 주입되는 엔진 op" 절)
|
||||
├── LifetimeHandle.luau # bindLifetime/canExecute 실제 구현 — GetPropertyChangedSignal("ClassName") 연결 트릭으로 gcconn 확보, Relate:SetStrong으로 gcconn/gchold 저장(`base/lifecycle-pattern.md`). Relate 자체는 순수 Lua라 quad-roblox 쪽 재구현 없음(quad-base 그대로 재사용)
|
||||
├── Handlers/
|
||||
│ ├── Property.luau # 일반 프로퍼티 세팅 + `isTween(realv)` 분기(3-상태 릴레이션 슬롯 `RobloxTween|true|nil`, hasBeenSet 억제, override 정책) — 구 `Handlers/Tween.luau`(높은 우선순위 store-bind 핸들러)는 폐기(`archive/tween-special-bind-key-reversed.md`)
|
||||
│ ├── Event.luau # ReflectionService 기반 자동 판별
|
||||
│ ├── OnChange.luau # `OnChange(name)` DI 키 팩토리+Handler, `GetPropertyChangedSignal` 바인딩 + 이름별 weak 캐시(`AttributeKey`와 동일 기법, `base/onchange-plan.md`, 2026-08-10 세션)
|
||||
│ ├── AttributeKey.luau # 단일 키(`AttributeKey<<T>>(name)`/`BooleanAttribute`류) DI 키 팩토리+Handler, `SetAttribute`/`None` 지우기 + 이름별 weak 캐시(동등성 보장, `base/attribute-plan.md` "동등성" 절)
|
||||
│ ├── Attribute.luau # 그룹(`Attribute(store1, store2, ...)`) process — 이름 집합 diff만 자체 로직(반환 클로저에 캡처, 별도 Relate 불필요), 실제 `SetAttribute`/구독은 공개 `AttributeKey(name)`으로 항상 인덱스 1부터 `Dispatch.process`/`retractFrom`에 재귀 위임(단일 키 경로 재사용, 중복 구현 없음) — 값 타입/API는 quad-base Attribute.luau(`base/attribute-plan.md`)
|
||||
│ ├── Tag.luau # CollectionService 글루만(process, 반환 클로저가 정리 담당) — 값 타입/API는 quad-base Tag.luau(`base/tag-plan.md`)
|
||||
│ ├── Slot.luau # base Slot 재조정 로직의 실제 적용/해제(Instance Parent 조작)
|
||||
│ └── InstanceChild.luau # k:number, v:Instance — 중첩 인스턴스 자식(예: Frame { Frame {} })
|
||||
├── Animate.luau # `Animate(info)` 편의 콤비네이터 — `factory(self)->State`, `:Apply`로 붙임(내부는 `:Compute`/`Tween{...}` 조합), base 프리미티브 아님(`base/tween-plan.md`)
|
||||
|
|
@ -225,7 +237,7 @@ existing-instance-bind는 여전히 `research/`에 남아있고 이 구조 확
|
|||
전용 소유물인가?"로 물으면 됨 — 그렇다면 대문자(생성자/메서드/그
|
||||
타입의 정적 결합 함수), 아니면(범용 유틸이거나 프리미티브가 아닌 엔진
|
||||
소속) 소문자. `Dispatch`/`Brand`가 프리미티브가 아닌 이유는
|
||||
`base/bind-system-plan.md`의 "Dispatch는 프리미티브가 아니다" 절/
|
||||
`base/dispatch-core-plan.md`의 "Dispatch는 프리미티브가 아니다" 절/
|
||||
`base/store-semantics.md`의 "세 번째 카테고리 — Handler" 절 참고.
|
||||
|
||||
## 코드 스타일 — Luau 문법 관례: `if-then-else`/`const` (2026-08-12 세션 신설)
|
||||
|
|
@ -234,7 +246,7 @@ existing-instance-bind는 여전히 `research/`에 남아있고 이 구조 확
|
|||
간주해 `and`/`or`로 "고치지" 말 것.** 2021년 10월 Luau에 정식 도입된
|
||||
표현식 문법(공식 릴리스 노트: <https://luau.org/news/2021-10-31-luau-recap-october-2021/#if-then-else-expression>)
|
||||
— `cond and truthyOnly or fallback` 삼항 관용구와 달리 가운데 값이
|
||||
falsy(`nil`/`false`)여도 정확하게 동작함(`bind-system-plan.md`의
|
||||
falsy(`nil`/`false`)여도 정확하게 동작함(`dispatch-core-plan.md`의
|
||||
`Dispatch.retractUnder`(현 `retractFrom`) 정정 사례가 실제 버그 예시). **[강화, 2026-08-12
|
||||
세션 후속] `cond and x or y` 삼항 관용구는 전면 금지 — `if-then-else`만
|
||||
쓸 것, 가운데 값이 항상-truthy임이 보장돼도 예외 없음.** 처음엔 그
|
||||
|
|
@ -265,8 +277,8 @@ property bag + property별 변경 시그널 정도만 흉내내고, `IsA()`/클
|
|||
Studio/엔진 없이 테스트(Vide가 실제로 이렇게 CI에 물려놓음) — Fusion처럼
|
||||
Studio 안에서만 도는 방식은 채택 안 함. 근거: quad-base 코어(Store/State/
|
||||
Source/Modifier/Slot, 디스패치 엔진)는 이미 `inst`를 `any`로 취급하고
|
||||
Instance 특정 동작을 전혀 참조하지 않도록 설계돼 있어(`bind-system-plan.md`
|
||||
"inst가 항상 Roblox Instance일 필요는 없음" 절), mock이 실제 Roblox 충실도를
|
||||
Instance 특정 동작을 전혀 참조하지 않도록 설계돼 있어(`dispatch-core-plan.md`
|
||||
"확정된 디스패치 모델" 절의 "`inst`가 항상 살아있는 엔진 객체일 필요는 없음"), mock이 실제 Roblox 충실도를
|
||||
가질 이유가 없음.
|
||||
|
||||
**스코프는 "정적 디버깅"으로 한정** — **사용자 확정**: mock으로 확인하려는
|
||||
|
|
@ -316,5 +328,5 @@ pull-recompute(`Get()` 시점) — Fusion식 eager 노드 없이도 다이아몬
|
|||
참고, 전체 색인은 `.claude/README.md`. 바인드 디스패치/Slot/모듈
|
||||
라이프사이클/Modifier/컴포넌트화(컴포넌트 경계 modifier/Ref 전달 포함)는
|
||||
위 "구현 착수" 섹션대로 확정되어 `.claude/base/`로 승격됨
|
||||
(`bind-system-plan.md`/`module-lifecycle-plan.md`/`slot-plan.md`/
|
||||
`modifier-plan.md`/`component-composition-plan.md`).
|
||||
(`bind-system-plan.md`/`dispatch-core-plan.md`/`module-lifecycle-plan.md`/
|
||||
`slot-plan.md`/`modifier-plan.md`/`component-composition-plan.md`).
|
||||
|
|
|
|||
|
|
@ -1,27 +1,31 @@
|
|||
# Attribute — 단일 키(`AttributeKey`)와 그룹(`Attribute(...)`) 두 프리미티브
|
||||
|
||||
> **⚠️ [2026-08-13 여섯 번째 세션] 이 문서의 `hintValue`/`retractFrom` 선행
|
||||
> 호출 서술은 곧 교체될 예정 — 아직 반영 안 됨.** 힌트가 `None` 센티널이나
|
||||
> `State`/`Tween` 래퍼로 오염돼 말단 핸들러의 `isX(hint)` 가드를 거짓으로
|
||||
> 만들고 깜빡임/재생성 방지를 조용히 끄는 결함이 확인됐고, 대체 모델
|
||||
> (**래핑 핸들러의 `retractFrom` 선행 호출 폐기 + `Dispatch.process`가
|
||||
> 핸들러를 먼저 비교**)까지 거의 확정됐음 — 다만 `question.md` **0-Z**
|
||||
> (Attribute 이름 소유권) 하나가 남아 아직 옮기지 않음. **여기 적힌 대로
|
||||
> 구현하면 옛 모델로 짜게 됨** — 반드시
|
||||
> `research/dispatch-redispatch-diff-plan.md`를 먼저 읽을 것.
|
||||
> **✅ [2026-08-13 열네 번째 세션] `question.md` 0-Z(이름 소유권) 확정 +
|
||||
> 하강 diff 재디스패치 반영 완료 — 이 문서는 이제 최신 모델을 서술합니다.**
|
||||
> 이전 ⚠️ 배너가 예고하던 교체가 전부 끝났음: (1) 그룹은 **자기 전용 키**로
|
||||
> 위임하고(비공개 `GetKey`), 이름 소유권은 **`AttributeKeyHandler`의 이름
|
||||
> claim**이 판정(아래 "이름 소유권" 절), (2) `retractFrom` 선행 호출은
|
||||
> 폐기되고 `Dispatch.process`가 핸들러를 먼저 비교
|
||||
> (`base/dispatch-core-plan.md`), (3) **값 타입·알고리즘 전부 quad-base
|
||||
> 소속으로 재배치**되고 `setAttribute` op만 백엔드가 주입(아래 "패키지
|
||||
> 배치" 절). 뒤집힌 옛 모델 원문은
|
||||
> `archive/dispatch-hintvalue-model-reversed.md`.
|
||||
|
||||
**상태**: base — 단일 키 메커니즘/`None`/`retract` 동작과 타입 파라미터화는
|
||||
전부 확정(2026-08-09 열한 번째 세션, **2026-08-12 세션 후속에서
|
||||
`retract` 완전 no-op화 + 그룹 청소 정책 전면 재정정 — 아래 "메커니즘"/
|
||||
"그룹 `Attribute(...)`" 절이 최신**). **[2026-08-13 세션, 하루 안에서
|
||||
두 차례 재설계]** 그룹/직접 쓰기 이름 충돌 방지 방식이 `rawNew`+`owners`
|
||||
수동 레지스트리 → `AttributeGroupKeyHandler` 체크포인트(`Dispatch.
|
||||
processAs`/`retractSelfAndUnder`) → **최종적으로 `Dispatch` 자체의
|
||||
인덱스 기반 재설계(`base/bind-system-plan.md` "Dispatch 체인" 절)에
|
||||
올라타 체크포인트도 필요 없어짐**(공개 `AttributeKey(name)`으로 항상
|
||||
인덱스 1에 직접 위임, 점유 체크가 소유권 충돌 감지를 대신함) — 아래
|
||||
"이름 소유권"/"메커니즘" 절이 최신, 중간 버전은 `archive/
|
||||
checkpoint-handler-pattern-reversed.md`에 보존. **[2026-08-11 아홉 번째 세션 추가]**
|
||||
네 차례 재설계 — 지금은 마지막 것이 확정]** 그룹/직접 쓰기 이름 충돌
|
||||
방지 방식이 `rawNew`+`owners` 수동 레지스트리 → `AttributeGroupKeyHandler`
|
||||
체크포인트(`Dispatch.processAs`/`retractSelfAndUnder`) → 공개
|
||||
`AttributeKey(name)`+Dispatch 점유 체크 → **최종적으로 그룹 전용 키
|
||||
(비공개 `GetKey`) + `AttributeKeyHandler`의 이름 claim**(2026-08-13
|
||||
열네 번째 세션, `question.md` 0-Z 확정). 세 번째 안이 다시 뒤집힌
|
||||
이유는 하강 diff 재디스패치에선 **두 그룹이 똑같이 `StoreBind`로 보여
|
||||
"같은 핸들러"로 판정**되므로 점유 체크가 성립하지 않기 때문 — 아래
|
||||
"이름 소유권"/"메커니즘" 절이 최신, 중간 버전들은 `archive/
|
||||
checkpoint-handler-pattern-reversed.md`와
|
||||
`archive/dispatch-hintvalue-model-reversed.md`에 보존. **[2026-08-11 아홉 번째 세션 추가]**
|
||||
Store 여러 개를 한 번에 attribute로 묶어 바인드하는 그룹 `Attribute(...)`
|
||||
프리미티브 신설, 이름 충돌 방지를 위해 기존 단일 키 생성자를
|
||||
`Attribute<<T>>` → `AttributeKey<<T>>`로 리네임(잠정 확정 — 최종 이름은
|
||||
|
|
@ -119,100 +123,129 @@ end
|
|||
"a"`가 외부에 관찰되는 것도 의도적으로 허용 가능한 동작(사용자 확인).
|
||||
`base/onchange-plan.md` "확정" 절 참고.
|
||||
|
||||
### 메커니즘, `None`, `retract` — 전부 확정 (2026-08-07 여덟 번째 세션)
|
||||
### 메커니즘, `None`, 반환 클로저 — 전부 확정 (2026-08-07 여덟 번째 세션, 2026-08-13 열네 번째 세션에 이름 claim 추가)
|
||||
|
||||
타입 파라미터화 이름과 무관하게 런타임 동작은 확정:
|
||||
|
||||
- `process(inst, k, v, index)` — `inst:SetAttribute(name, v)`가 사실상
|
||||
- `process(inst, k, v, index)` — `setAttribute(inst, name, v)`가 사실상
|
||||
전부, **`v`가 뭐든(실제 값이든 `nil`이든) 무조건 그대로 호출** — 일반
|
||||
프로퍼티 핸들러와 완전히 동일한 무조건 set. **Attribute는 `None`의
|
||||
가장 깔끔한 사례** — Roblox API 자체가 `SetAttribute(name, nil)`을
|
||||
"그 Attribute 엔트리를 지운다"는 뜻으로 네이티브 지원하므로, `None →
|
||||
nil` 재디스패치(`base/bind-system-plan.md`의 `None` 센티널 절)가
|
||||
도착했을 때 handler가 **아무 특별 처리도 없이** `inst:SetAttribute(name,
|
||||
nil)`을 그대로 호출하면 끝 — UICorner 숏핸드처럼 "만들어둔 자식을
|
||||
수동으로 찾아 지우는" 로직조차 필요 없음.
|
||||
- **반환하는 클로저는 완전 no-op — [재정정, 2026-08-12 세션 후속] "매번
|
||||
불리지만 대부분 no-op"이라던 직전 서술도 틀렸음, "대부분"이 아니라
|
||||
"항상"** — 일반 프로퍼티 핸들러(반환 클로저가 완전 무조건 no-op,
|
||||
`bind-system-plan.md` "일반 프로퍼티는 애초에 'unset' 개념이 없음")와
|
||||
완전히 같은 성격으로 재정정. **`AttributeKeyHandler`가 반환하는
|
||||
클로저는 `SetAttribute`를 절대 호출하지 않음** — attribute를 지우는
|
||||
유일한 경로는 `process(inst,k,nil,index)`(`None`이든, State가 스스로
|
||||
`nil`로 바뀌든) 뿐. 이전 버전("이름이 사라질 때(`v==nil`)만 retract가
|
||||
`SetAttribute(name,nil)`을 호출")은 두 가지 문제가 있었음 — (1)
|
||||
이 클로저 안에 관측 가능한 부작용이 생겨 `bind-system-plan.md`의
|
||||
"이 클로저는 구조적 팝만, process 트리거 금지" 일반 규칙과 어긋나는
|
||||
성격의 코드가 됨, (2) 그룹이 survivor 이름에 재위임할 때 그 시점에
|
||||
`SetAttribute`가 잘못 끼어들 수 있는 경로가 생겨 `a→nil→b` 깜빡임
|
||||
위험(사용자 지적) — 클로저가 완전 no-op이면 이 경로 자체가 물리적으로
|
||||
없어짐.
|
||||
가장 깔끔한 사례** — 주입 op의 계약 자체가 `setAttribute(inst,name,nil)`
|
||||
= "그 Attribute 엔트리를 지운다"이므로(Roblox `SetAttribute`의 네이티브
|
||||
동작 그대로, `base/dispatch-core-plan.md` "base가 소유하는 핸들러와
|
||||
주입되는 엔진 op" 절), `None → nil` 재디스패치(같은 문서의 `None`
|
||||
센티널 절)가 도착했을 때 handler가 **아무 특별 처리도 없이** 그대로
|
||||
호출하면 끝 — UICorner 숏핸드처럼 "만들어둔 자식을 수동으로 찾아
|
||||
지우는" 로직조차 필요 없음.
|
||||
- **반환하는 클로저는 `setAttribute`를 절대 호출하지 않음** — attribute를
|
||||
지우는 유일한 경로는 `process(inst,k,nil,index)`(`None`이든, State가
|
||||
스스로 `nil`로 바뀌든) 뿐(2026-08-12 세션 후속 확정, 그대로 유지).
|
||||
**[2026-08-13 열네 번째 세션] 다만 "완전 no-op"은 더 이상 아님** — 아래
|
||||
"이름 소유권" 절의 이름 claim을 반납하는 한 줄이 들어감. 엔진에 대한
|
||||
부작용은 여전히 0이고(관측 가능한 attribute 값은 안 건드림), 반납하는
|
||||
건 순수 부기라 아래 "`a→nil→b` 깜빡임" 위험도 그대로 없음. 이전
|
||||
버전("이름이 사라질 때(`v==nil`)만 클로저가 `SetAttribute(name,nil)`을
|
||||
호출")이 기각된 이유는 그대로 유효: (1) 클로저 안에 관측 가능한
|
||||
부작용이 생겨 "이 클로저는 구조적 팝만" 일반 규칙과 어긋남,
|
||||
(2) 그룹이 survivor 이름에 재위임할 때 `SetAttribute`가 잘못 끼어들어
|
||||
`a→nil→b` 깜빡임이 생길 수 있었음.
|
||||
- store-bind 가능(일반 프로퍼티와 동일하게 취급, `Store<T>`/`State<T>`
|
||||
값도 받음).
|
||||
|
||||
### 이름 소유권 — 그룹/직접 쓰기 충돌 방지 (2026-08-12 열 번째 세션, 2026-08-13 다섯 번째 세션 전면 재정정)
|
||||
### 이름 소유권 — 그룹 전용 키 + 이름 claim (2026-08-12 열 번째 세션 신설, 2026-08-13 열네 번째 세션 확정 = `question.md` 0-Z)
|
||||
|
||||
**문제**: `AttributeKey(name)`이 이름별 weak 캐시로 항상 같은 객체를
|
||||
리턴하고, 그룹 `Attribute(...)`가 그 경로를 그대로 재사용(아래 "메커니즘"
|
||||
절)하다 보니, **서로 다른 원래 위치(해시파트 직접 쓰기 `[AttributeKey
|
||||
"name"]=value` vs 배열파트 `Attribute(store)`, 또는 서로 다른 두
|
||||
`Attribute(...)` 그룹)가 같은 이름을 동시에 관리하려 하면 정확히 같은
|
||||
`(inst, k=AttributeKey(name))` 자리로 수렴해 조용히 마지막 쓰기가
|
||||
이기는 충돌이 생김.** Modifier 필드는 정적이라 override로 이미 해소되지만
|
||||
(같은 해시 키는 한 Modifier 안에 하나뿐), 그룹의 이름 집합은 런타임에
|
||||
동적이라 이 해소망 밖에 있음.
|
||||
**문제**: 같은 attribute 이름을 **서로 다른 두 자리가 동시에 관리**할 수
|
||||
있음 — 해시파트 직접 쓰기 `[AttributeKey "name"] = value` vs 배열파트
|
||||
`Attribute(store)`, 또는 서로 다른 두 `Attribute(...)` 그룹. Modifier
|
||||
필드는 정적이라 override로 이미 해소되지만(같은 해시 키는 한 Modifier
|
||||
안에 하나뿐), 그룹의 이름 집합은 런타임에 동적이라 그 해소망 밖에 있음.
|
||||
|
||||
**[역사, 2026-08-13 세션 안에서 두 번 뒤집힘]** 첫 버전(`rawNew`로 그룹
|
||||
전용 키를 만들고 `owners` Relate로 이름별 소유권을 수동 추적)은 "그룹이
|
||||
이름을 놓았다 나중에 같은 그룹이 그 이름을 다시 포함하면 자기 자신과
|
||||
충돌"하는 실제 버그가 있었음(소유권 반납이 `process`의 `v==nil` 분기에만
|
||||
있어서, 그룹이 이름을 통째로 놓는 경로는 그 분기를 안 타서 옛 소유권
|
||||
기록이 안 지워짐). 두 번째 버전(`AttributeGroupKeyHandler`라는 스캔
|
||||
불가 체크포인트 핸들러를 `Dispatch.processAs`로 명시 push, 소유권 충돌을
|
||||
Dispatch의 재진입 가드에 얹어 감지)은 이 버그를 고치긴 했으나, 같은 날
|
||||
다섯 번째 세션에 `Dispatch` 자체가 `chains`를 핸들러 identity가 아니라
|
||||
**인덱스**로 추적하도록 재설계되며(`base/bind-system-plan.md` "Dispatch
|
||||
체인" 절) 체크포인트가 하던 일 자체가 통째로 불필요해짐 — 원문·역전
|
||||
이유는 `archive/checkpoint-handler-pattern-reversed.md`.
|
||||
**왜 Dispatch가 대신 잡아줄 수 없는가**: 옛 모델에선 그룹이 공개
|
||||
`AttributeKey(name)`(이름별 weak 캐시라 항상 같은 객체)으로 위임했고,
|
||||
같은 `(inst,k)` 인덱스 1을 두 소유자가 노리면 `Dispatch.process`의 점유
|
||||
체크가 error를 냈음. **하강 diff 재디스패치에선 이게 성립하지 않음** —
|
||||
그룹 A가 등록해둔 인덱스 1의 핸들러는 `StoreBind`이고 그룹 B가 넘기는
|
||||
`sourceB`도 `StoreBind`에 매치되므로 **"같은 핸들러"로 판정되어 조용히
|
||||
갈아탐**. 게다가 나중에 A의 클로저가 자기 이름들을 `retractFrom`할 때
|
||||
**B의 바인딩을 대신 철거**함(교차 오염) — 예전의 "조용한 last-write-wins"가
|
||||
그대로 돌아옴.
|
||||
|
||||
**최종(세 번째 버전) — 체크포인트도 `owners`도 없이, 항상 인덱스 1부터
|
||||
직접 위임.** **[캐비엇, 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` 자신의 점유 체크가 소유권 충돌
|
||||
감지를 그대로 대신함**: 다른 그룹이나 직접 쓰기가 이미 그 이름을
|
||||
점유했다면, 이 호출은(체크포인트를 거칠 필요도 없이) 곧바로 "이미
|
||||
점유됨" error를 냄. `AttributeKeyHandler`도 다시 완전 무상태로 되돌아감:
|
||||
**확정된 해법(2026-08-13 열네 번째 세션) — 두 조각**:
|
||||
|
||||
1. **그룹은 이름마다 자기 전용 키를 쓴다(비공개 `GetKey`)**. 그룹 값
|
||||
객체별·이름별로 메모이즈된 `AttributeKey`를 만들어 그걸로 위임 →
|
||||
그룹 A와 그룹 B와 직접 쓰기가 **서로 다른 체인**을 갖게 되어, 한
|
||||
소유자의 철거가 다른 소유자의 바인딩을 건드리는 **교차 오염이
|
||||
구조적으로 불가능**해짐.
|
||||
2. **이름 자체의 소유권은 `AttributeKeyHandler`가 이름 claim으로 판정**.
|
||||
`(inst, name) → 지금 그 이름을 잡고 있는 키 객체`를 `Relate` 하나로
|
||||
들고, 다른 키 객체가 같은 이름에 들어오면 **즉시 error**.
|
||||
|
||||
**왜 이 조합인가 — 대안 (a)(그룹 안에 이름별 claimant `Relate`)로는 부족**:
|
||||
(a)는 그룹↔그룹은 잡지만 **그룹↔직접 쓰기를 못 잡음**. 직접 쓰기는 그룹
|
||||
코드를 아예 안 지나가서 그룹 쪽 레지스트리에 등록될 자리가 없고, 두
|
||||
경로가 실제로 만나는 유일한 지점인 `AttributeKeyHandler`에서는 공개 키를
|
||||
쓰는 한 `k`가 **같은 객체**라 소유자를 구분할 방법이 원천적으로 없기
|
||||
때문. 전용 키가 있어야 비로소 "이 이름을 지금 누가 잡고 있나"를 **키
|
||||
identity 하나로** 판정할 수 있고, 그러면 claim 로직이 그룹을 알 필요도
|
||||
없어짐(그룹인지 직접 쓰기인지 구분 안 함 — 소유자 종류가 늘어도 그대로
|
||||
동작). 후보 (b)(UB로 두고 문서화만)는 증상이 "조용한 오작동+교차 오염"이라
|
||||
비권장으로 기각, (c)(`Dispatch`에 claimant 일반화)는 "Dispatch는 diff만
|
||||
한다"는 방향과 어긋나 기각.
|
||||
|
||||
```lua
|
||||
-- AttributeKeyHandler(quad-roblox) — 완전 무상태, 소유권 추적 없음
|
||||
-- AttributeKeyHandler(quad-base) — 이름 claim만 자기 상태로 가짐
|
||||
local nameClaims = Relate() -- {[inst(weak)] = {[name] = key(strong)}}
|
||||
|
||||
function AttributeKeyHandler.process(inst, k, v, index)
|
||||
inst:SetAttribute(k.Name, v) -- v가 nil이든 아니든 무조건 — 일반 프로퍼티와 완전히 동일
|
||||
return function() end -- 지울 게 없음 — SetAttribute는 오직 process(inst,k,nil)로만
|
||||
local claims = nameClaims:GetStrong(inst)
|
||||
if not claims then
|
||||
claims = {}
|
||||
nameClaims:SetStrong(inst, claims)
|
||||
end
|
||||
local cur = claims[k.Name]
|
||||
if cur ~= nil and cur ~= k then
|
||||
error(`attribute "{k.Name}" is already bound by another owner`)
|
||||
end
|
||||
claims[k.Name] = k
|
||||
|
||||
setAttribute(inst, k.Name, v) -- v가 nil이든 아니든 무조건(위 "메커니즘" 절)
|
||||
|
||||
return function()
|
||||
-- 엔진 부작용 없음 — claim 반납만. "내가 실제로 물러날 때만" 지움
|
||||
-- (`dispatch-core-plan.md` "Handler 작성 체크리스트" 4번).
|
||||
if claims[k.Name] == k then
|
||||
claims[k.Name] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
- **해제 → 재클레임 순서는 `Dispatch`가 보장함.** 같은 핸들러 재프로세스는
|
||||
`slot.retractor(v)` → `h.process(...)` 순서이고, 핸들러가 바뀌는 경우는
|
||||
`retractFrom` → `process` 순서(`base/dispatch-core-plan.md` "Dispatch
|
||||
체인" 절) — 어느 경로든 **옛 claim 반납이 새 claim보다 먼저**라 자기
|
||||
자신과 충돌하는 일이 없음. `5 → None → 5`처럼 체인 깊이가 오가는
|
||||
경우도 인덱스가 바뀌는 자리에서 `retractFrom`이 먼저 돌므로 동일.
|
||||
- **`GetKey`는 공개 API가 아님(사용자 확정)** — 그룹 값 객체 바깥으로
|
||||
키를 반출하면, 사용자가 그 키를 다른 자리에 다시 놓아 **같은 키가 두
|
||||
자리에서 수렴**할 수 있고 그건 claim(키 identity 기준)으로도 잡히지
|
||||
않음(0-W "같은 `Ref` 객체 이중 배치"와 같은 형태의 갭). 비공개로
|
||||
두면 이 경로 자체가 없음 — 구현상으로도 그룹 핸들러 안의
|
||||
`Relate<그룹 값 → {[name] = key}>` 또는 클로저 캡처면 충분하고, base의
|
||||
`Attribute` 값 객체 공개 표면에는 아무것도 안 늘어남.
|
||||
- **에러 메시지는 도메인 언어로** — 옛 모델의 일반 점유 error("이 인덱스는
|
||||
이미 점유돼 있음")와 달리 여기선 이름을 그대로 찍을 수 있음.
|
||||
`base/dispatch-core-plan.md`가 "상세 에러가 필요하면 호출부가 도메인
|
||||
언어로 다시 던지는 건 자유"라고 남겨둔 자리를 이게 채움.
|
||||
- **직접 리터럴 쓰기**(`[AttributeKey<<T>> "name"] = value`)는 공개
|
||||
`AttributeKey(name)`을 그대로 씀, 정상 스캔으로 바로
|
||||
`AttributeKeyHandler`에 도달(인덱스 1) — 한 Modifier 안에 같은 해시
|
||||
키가 중복될 수 없어 이 경로 자체의 claimant는 항상 유일. 그룹이
|
||||
이미 그 이름의 인덱스 1을 점유 중이면, 이 직접 쓰기의
|
||||
`Dispatch.process(inst,key,value,1)`가 그 자리에서 곧바로 점유 error —
|
||||
그룹↔직접 쓰기 충돌도 같은 점유 체크 하나로 잡힘.
|
||||
- **그룹**은 아래 "메커니즘" 절에서 이름마다 `Dispatch.process`만으로
|
||||
위임하고(철거는 반환 클로저가 전담) — 전용 키 객체도, 소유권 레지스트리도
|
||||
필요 없음(항상 공개 `AttributeKey(name)`, 항상 인덱스 1). **`process`가
|
||||
위임 직전에 `retractFrom`을 부르면 안 된다는 게 이 절이 성립하는
|
||||
전제** — 그러면 점유 여부와 무관하게 인덱스 1이 비워져 아래 점유
|
||||
체크가 무력화됨(2026-08-13 감사에서 실제 그렇게 적혀 있던 걸 정정,
|
||||
"메커니즘" 절 참고).
|
||||
- **패키지 경계**: `AttributeKey`는 이미 quad-roblox 소속(Tag와 달리
|
||||
base/roblox로 안 쪼갬, 아래 "패키지 배치" 절) — base쪽
|
||||
`Attribute(...)` 값 객체 자신은 이 메커니즘을 전혀 모름.
|
||||
`AttributeKey(name)`을 그대로 씀 — 한 Modifier 안에 같은 해시 키가
|
||||
중복될 수 없어 이 경로 자체의 소유자는 항상 유일하고, 그룹이 이미 그
|
||||
이름을 잡고 있으면 claim이 즉시 error.
|
||||
- **`Tag`와의 대조** — `Tag`는 같은 이름을 여러 위치가 공유하는 게
|
||||
**의도된 동작**(웹 `className` 합집합)이라 참조 카운트로 가고, Attribute는
|
||||
값이 하나뿐이라 겹침이 곧 충돌이라 claim으로 감. 두 정책의 차이는
|
||||
자원의 성질에서 나옴(`base/tag-plan.md`).
|
||||
|
||||
## 그룹 `Attribute(...)` — 여러 Store를 한 번에 attribute로 (2026-08-11 아홉 번째 세션 신설)
|
||||
|
||||
|
|
@ -254,20 +287,18 @@ Frame { Attribute(styleStore), Attribute(stateStore) } -- 여러 개 나란히
|
|||
슬롯을 그대로 가져와 자기 자신의 key→Source 맵에 넣는 것 — 아래 "레이어드
|
||||
Store 기각과 안 부딪히나" 참고.
|
||||
|
||||
### 메커니즘 — 항상 인덱스 1부터 기존 단일 키 경로에 직접 위임
|
||||
### 메커니즘 — 그룹 전용 키로 단일 키 경로에 위임 (2026-08-13 열네 번째 세션 확정)
|
||||
|
||||
**[2026-08-11 아홉 번째 세션 후속, 개정]** 최초안은 "자기 완결형 Handler,
|
||||
Dispatch 재진입 없이 직접 `SetAttribute`+수동 per-field StoreBind 구독"
|
||||
이었으나, 위 "동등성" 절의 이름별 weak 캐시가 확정되며 그 회피 이유
|
||||
자체가 없어짐 — 그래서 그룹 Handler는 **자기만의 SetAttribute/구독 로직을
|
||||
새로 만들지 않고, 각 필드를 기존 단일 키 `AttributeKey` 경로에 그대로
|
||||
재귀 위임** — `None`/store-bind 전부 이미 확정된 단일 키 메커니즘을
|
||||
100% 재사용, 중복 구현 없음. **[전면 재정정, 2026-08-13 다섯 번째 세션]
|
||||
`rawNew(name)` 그룹 전용 키 → `AttributeGroupKeyHandler` 체크포인트로
|
||||
두 번 거쳐온 위임 메커니즘이, `Dispatch`의 인덱스 기반 재설계로 다시
|
||||
한번 단순화됨 — 이제 그냥 공개 `AttributeKey(name)`으로 인덱스 1에
|
||||
직접 위임**(경위는 위 "이름 소유권" 절, 원문은 `archive/
|
||||
checkpoint-handler-pattern-reversed.md`):
|
||||
이었으나, 위 "동등성" 절의 이름별 캐시가 확정되며 그 회피 이유 자체가
|
||||
없어짐 — 그래서 그룹 Handler는 **자기만의 set/구독 로직을 새로 만들지
|
||||
않고, 각 필드를 기존 단일 키 `AttributeKey` 경로에 그대로 재귀 위임**한다
|
||||
(`None`/store-bind 전부 이미 확정된 단일 키 메커니즘을 100% 재사용, 중복
|
||||
구현 없음). **위임에 쓰는 키만 세 번 바뀌었고 지금은 그룹 전용 키**
|
||||
(`rawNew(name)` 전용 키 → `AttributeGroupKeyHandler` 체크포인트 → 공개
|
||||
`AttributeKey(name)`+점유 체크 → **비공개 `GetKey`(그룹 값 객체별·이름별
|
||||
메모이즈) + 이름 claim**) — 경위와 근거는 위 "이름 소유권" 절.
|
||||
|
||||
**그룹의 `process`** — 다른 모든 핸들러와 똑같은 4-인자 계약
|
||||
`process(inst, k, v, index)`를 따름(`k`는 이 그룹 값이 놓인 array-part
|
||||
|
|
@ -275,68 +306,60 @@ checkpoint-handler-pattern-reversed.md`):
|
|||
다른 것이라 헷갈리지 말 것**. 2026-08-13 감사 전까지 이 자리 시그니처가
|
||||
`process(inst, index, v)` 3-인자로 적혀 있었고 배열 위치를 하필 `index`로
|
||||
부르고 있어서, 코퍼스에서 유일하게 계약과 안 맞는 핸들러였음). 반환하는
|
||||
클로저가 "이 호출이 등록한 이름 집합"을 직접 캡처 — **별도 `Relate`
|
||||
불필요**(2026-08-13 다섯 번째 세션, 클로저가 매 호출마다 자기 자신의
|
||||
이름 집합을 새로 만들어 캡처하므로 사이클을 가로질러 저장해둘 이유가
|
||||
없어짐):
|
||||
|
||||
**[정정, 2026-08-13 감사] `process`는 `retractFrom`을 부르지 않는다 —
|
||||
철거는 전적으로 반환 클로저 몫.** 최초 작성본은 `process` 안에서 이름마다
|
||||
`Dispatch.retractFrom(inst, key, 1, source)`를 먼저 부른 뒤 `process`를
|
||||
불렀는데, 이러면 **인덱스 1을 누가 점유했든 무조건 비워버리므로 뒤따르는
|
||||
`Dispatch.process`가 점유 error를 낼 수가 없음** — 위 "이름 소유권" 절이
|
||||
이 재설계의 핵심 근거로 내세운 "점유 체크가 소유권 충돌 감지를 그대로
|
||||
대신함"이 그룹↔그룹 사이에서 전혀 작동하지 않았음(그룹 B가 그룹 A의
|
||||
바인딩을 조용히 파괴하고 이김 — 이 절이 없앴다고 선언한 바로 그
|
||||
last-write-wins). 클로저가 자기가 등록한 이름 전부를 책임지고 걷어내면
|
||||
`process`는 순수하게 `Dispatch.process`만 부르면 되고, 점유 체크가 다시
|
||||
살아남:
|
||||
클로저가 "이 호출이 등록한 키 목록"을 직접 캡처:
|
||||
|
||||
```lua
|
||||
function AttributeGroupHandler.process(inst, k, v, index)
|
||||
local names = {}
|
||||
local keys = {}
|
||||
for name, source in pairs(v:NameMap()) do -- isHandlable이 이미 isAttribute(v)를 보장
|
||||
-- 공개 캐시 키 그대로(그룹 전용 키 불필요), 항상 인덱스 1부터 위임 —
|
||||
-- 이미 다른 그룹/직접 쓰기가 그 이름을 점유 중이면 여기서 즉시 점유 error
|
||||
Dispatch.process(inst, AttributeKey(name), source, 1)
|
||||
names[name] = true
|
||||
-- 그룹 전용 키(비공개) — 공개 AttributeKey(name)이 아님.
|
||||
-- 같은 그룹 값 객체 + 같은 이름이면 항상 같은 키 객체가 나와야 함.
|
||||
local key = groupKey(v, name)
|
||||
Dispatch.process(inst, key, source, 1) -- 다른 키로 위임이므로 항상 인덱스 1
|
||||
keys[key] = true
|
||||
end
|
||||
return function()
|
||||
-- 이 그룹이 등록했던 이름 전부를 철거 — 생존/소멸 구분 없이 균일.
|
||||
-- hintValue를 안 봄: 생존 이름도 일단 철거하고 다음 process가 다시
|
||||
-- 등록하는 순서라(StoreBind가 retractFrom → process 순서를 보장),
|
||||
-- 이 그룹이 등록했던 것 전부를 철거 — 생존/소멸 구분 없이 균일.
|
||||
-- 인자(새 값)를 안 봄: 생존 이름도 일단 철거하고 다음 process가 다시
|
||||
-- 등록하는 순서라(Dispatch가 retractor → process 순서를 보장),
|
||||
-- "다음 값에 이 이름이 있나"를 미리 알 필요가 없음.
|
||||
for name in pairs(names) do
|
||||
Dispatch.retractFrom(inst, AttributeKey(name), 1, nil) -- SetAttribute는 안 일어남(아래 원칙)
|
||||
for key in pairs(keys) do
|
||||
Dispatch.retractFrom(inst, key, 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
- **`groupKey(v, name)`는 그룹 값 객체별·이름별 메모이즈** — 같은 그룹
|
||||
값이 재프로세스될 때 같은 키가 나와야 claim이 자기 자신과 안 부딪힘.
|
||||
구현은 `Relate<그룹 값 → {[name] = key}>` 하나면 충분하고, **공개
|
||||
API로 노출하지 않음**(위 "이름 소유권" 절). 그룹 값 객체 자체가 바뀌면
|
||||
(`State<Attribute>`가 새 객체를 emit) 새 키가 나오는데, 그때는 옛
|
||||
클로저가 먼저 전부 철거하므로 claim 충돌이 없음.
|
||||
- **생존 이름도 매 사이클 철거→재등록됨(의도된 트레이드오프)** — 비용은
|
||||
그 이름의 `StoreBind` 구독 해제+재구독, 그리고 재구독의 "등록 즉시 1회
|
||||
실행"이 같은 값으로 `SetAttribute`를 한 번 더 쏘는 것뿐. 이 문서가 이미
|
||||
실행"이 같은 값으로 `setAttribute`를 한 번 더 쏘는 것뿐. 이 문서가 이미
|
||||
"값 비교(`:Get()`으로 old/new 비교)는 안 함"을 확정해뒀으므로(아래 항목)
|
||||
결이 같고, `SetAttribute`는 같은 값 재기록이 관측상 무해함. **`Tag`식
|
||||
`hintValue == v` 조기 반환을 여기에 넣으면 안 됨** — 클로저가 아무것도
|
||||
안 걷어낸 상태로 다음 `process`가 같은 이름에 다시 인덱스 1을 잡으려
|
||||
들어 자기 자신에게 점유 error를 냄.
|
||||
결이 같고, `setAttribute`는 같은 값 재기록이 관측상 무해함. **체인이
|
||||
그룹 전용이 된 지금은 이론상 "생존 이름은 그냥 다시 `Dispatch.process`만
|
||||
불러 하강 diff에 맡기는" 최적화도 가능하지만**(다른 소유자를 건드릴
|
||||
위험이 없어졌으므로), 그러려면 옛 이름 집합을 `(inst,위치)`별로 또
|
||||
들고 있어야 해서 부품이 늘어남 — **기본은 균일 철거 유지**, 최적화는
|
||||
실제로 비용이 문제될 때 재검토.
|
||||
- **그룹이 이름을 아예 놓는 경우도 같은 코드로 자연히 처리됨** — 클로저가
|
||||
`names` 전부를 걷어내고, 새 `process`가 새 이름 집합만 등록하므로 사라진
|
||||
이름은 그냥 재등록이 안 될 뿐. 별도 diff 분기가 없어짐(이전 버전이
|
||||
`newNames`를 계산하던 로직 자체가 불필요).
|
||||
자기 키 전부를 걷어내고, 새 `process`가 새 이름 집합만 등록하므로 사라진
|
||||
이름은 그냥 재등록이 안 될 뿐. 별도 diff 분기가 없음.
|
||||
- **값 비교(`:Get()`으로 old/new 비교)는 안 함** — State
|
||||
계약("값은 항상 선언된 Compute 재실행 결과, 캐시 비교 금지",
|
||||
`store-semantics.md` "하드 경계" 절)과 어긋나고, `source`가
|
||||
`State`/`Source`면 `Dispatch/StoreBind`가 알아서 언랩+구독까지 다
|
||||
해줌(그룹 Handler가 따로 구독 관리 안 함)이라 굳이 비교할 이유가 없음.
|
||||
- **[확정, 2026-08-12 세션 후속, 사용자 결정] `retract`는 `SetAttribute`를
|
||||
- **[확정, 2026-08-12 세션 후속, 사용자 결정] 클로저는 `setAttribute`를
|
||||
절대 안 부름 — Attribute는 오직 명시적 `None`/`nil`로만 지워진다.**
|
||||
그룹에서 이름이 조용히 빠지든(diff로 사라짐), 그룹 바인딩 자체가
|
||||
통째로 사라지든(컴포넌트 언마운트 등, `v`가 더 이상 Attribute가 아님)
|
||||
프레임워크가 자동으로 `SetAttribute(name,nil)`을 대신 불러주지
|
||||
않음 — 값이 이전 것 그대로 남는 게 정상 동작. `Ref`가 Destroy와
|
||||
무관하게 동작하는 것과 같은 철학("지울 거면 명시적으로 지우라",
|
||||
그룹에서 이름이 조용히 빠지든, 그룹 바인딩 자체가 통째로 사라지든
|
||||
(컴포넌트 언마운트 등) 프레임워크가 자동으로 `setAttribute(inst,name,nil)`을
|
||||
대신 불러주지 않음 — 값이 이전 것 그대로 남는 게 정상 동작. `Ref`가
|
||||
Destroy와 무관하게 동작하는 것과 같은 철학("지울 거면 명시적으로 지우라",
|
||||
`ref-plan.md`의 "`Ref`의 retract" 절)으로 통일. **이전 초안은
|
||||
"Tag와 동일하게 확실히 청소"였으나 뒤집힘** — 이유: (1) diff로
|
||||
조용히 빠지는 이름은 안 지워주면서 통째 소멸일 땐 지워주면, 두 경우가
|
||||
|
|
@ -349,40 +372,31 @@ end
|
|||
그룹과 비교해 사라진 이름을 `None`으로 명시적으로 채워 넣는 유틸)을
|
||||
나중에 opt-in으로 추가하면 됨 — 그건 사용자가 고른 명시적 선택이라
|
||||
모호하지 않음, 지금은 범위 밖(백로그).
|
||||
- **다만 *구독*은 반드시 끊음 — 값은 안 지워도 자원은 새면
|
||||
안 됨.** 위 "값은 안 지운다" 원칙과 별개로, 그룹이 더 이상 관리하지
|
||||
않는 이름의 `(inst,key)` 체인을 그대로 두면 그 키에 걸려있던
|
||||
`StoreBind` 구독이 인스턴스가 살아있는 동안 영원히 남아 원본
|
||||
`Source`가 바뀔 때마다 계속 `SetAttribute`를 쏘는 실제 리소스 누수가
|
||||
됨(이건 "마지막 값이 남는다"는 것과 다른 문제 — 안 죽는 구독 자체가
|
||||
문제). 그래서 반환하는 클로저는 **자기가 등록했던 이름 전부**에 대해
|
||||
`Dispatch.retractFrom(inst, AttributeKey(name), 1, nil)`을 부름
|
||||
(**[정정, 2026-08-13 감사]** 원래는 "사라진 이름에 한해"였으나, 그
|
||||
선별을 하려면 `process` 쪽이 생존 이름을 `retractFrom`으로 강제
|
||||
회수해야 해서 점유 체크가 무력화됐음 — 위 "메커니즘" 절 참고. 전부
|
||||
걷어내고 새 `process`가 다시 등록하는 쪽이 점유 체크를 살리면서
|
||||
코드도 더 단순함) —
|
||||
**`Dispatch.process`는 절대 안 부르므로**(이 클로저 안에서 새 등록을
|
||||
트리거하는 건 체인 추적을 꼬는 UB, `bind-system-plan.md` 일반 규칙)
|
||||
그 이름 아래가 전부 자기 자신의 클로저만 타고 끝나 `SetAttribute`는
|
||||
여기서도 절대 안 일어남 — 위 "명시적 None으로만 지운다" 원칙과 안
|
||||
부딪힘.
|
||||
- **다만 *구독*은 반드시 끊음 — 값은 안 지워도 자원은 새면 안 됨.**
|
||||
그룹이 더 이상 관리하지 않는 이름의 체인을 그대로 두면 그 키에 걸려있던
|
||||
`StoreBind` 구독이 인스턴스가 살아있는 동안 영원히 남아 원본 `Source`가
|
||||
바뀔 때마다 계속 `setAttribute`를 쏘는 실제 리소스 누수가 됨(이건
|
||||
"마지막 값이 남는다"는 것과 다른 문제 — 안 죽는 구독 자체가 문제).
|
||||
그래서 반환하는 클로저는 자기가 등록했던 키 전부에 대해
|
||||
`Dispatch.retractFrom(inst, key, 1)`을 부름 — **`Dispatch.process`는
|
||||
절대 안 부르므로**(이 클로저 안에서 새 등록을 트리거하는 건 체인 추적을
|
||||
꼬는 UB, `dispatch-core-plan.md` 일반 규칙) 그 키 아래가 전부 자기
|
||||
자신의 클로저만 타고 끝나 `setAttribute`는 여기서도 절대 안 일어남 —
|
||||
위 "명시적 None으로만 지운다" 원칙과 안 부딪힘.
|
||||
- **필드 하나만 바뀌는 흔한 경우**(`storeA.foo:Set(v)`, 그룹 자체는
|
||||
안 바뀜)는 위 그룹 재처리를 아예 거치지 않음 — 마운트 시 이미 걸린
|
||||
단일 키 `AttributeKeyHandler`의 store-bind 구독이 바로
|
||||
`SetAttribute("foo", v)`를 호출(그룹 재진입 없이 그 경로 스스로).
|
||||
`setAttribute(inst,"foo",v)`를 호출(그룹 재진입 없이 그 경로 스스로).
|
||||
**`AttributeChanged`/`GetAttributeChangedSignal` 남발 걱정 없음** —
|
||||
키 집합이 안 바뀌는 한 diff 로직 자체가 안 돎.
|
||||
키 집합이 안 바뀌는 한 그룹 로직 자체가 안 돎.
|
||||
|
||||
**그룹 Handler에 남는 자기 로직은 사실상 "이름 집합을 순회하며 위임하고,
|
||||
클로저가 같은 집합을 걷어내는 것"뿐**(**[정정, 2026-08-13 감사]** 예전엔
|
||||
"이름 집합 diff"라고 적었으나, 위 재정정으로 diff 자체가 없어짐 —
|
||||
`process`는 새 집합을 전부 등록, 클로저는 옛 집합을 전부 철거) — 실제
|
||||
`SetAttribute` 호출/`None` 처리/store-bind 구독은 전부 기존 단일 키
|
||||
경로가 그대로 담당. `TagHandler`가 `CollectionService` 호출을 직접 하는
|
||||
것과 달리, 여기서는 그 실행 자체를 위임한다는 점이 다름(Attribute만
|
||||
"이미 완성된 재사용 가능한 단일 키 경로"가 있어서 가능한 차이 — Tag는
|
||||
애초에 이름 하나짜리 단일 키 대응물이 없음).
|
||||
**그룹 Handler에 남는 자기 로직은 사실상 "이름 집합을 순회하며 자기 전용
|
||||
키로 위임하고, 클로저가 같은 키들을 걷어내는 것"뿐** — 실제
|
||||
`setAttribute` 호출/`None` 처리/store-bind 구독/이름 claim은 전부 단일 키
|
||||
경로가 담당. `TagHandler`가 `addTag`/`removeTag`를 직접 호출하는 것과
|
||||
달리 여기서는 그 실행 자체를 위임한다는 점이 다름(Attribute만 "이미
|
||||
완성된 재사용 가능한 단일 키 경로"가 있어서 가능한 차이 — Tag는 애초에
|
||||
이름 하나짜리 단일 키 대응물이 없음).
|
||||
|
||||
### `Attribute.Merged`가 레이어드 Store 기각과 안 부딪히나 — 점검, 안 부딪힘
|
||||
|
||||
|
|
@ -402,30 +416,60 @@ end
|
|||
기각 사유(범용 컨테이너의 암묵적 런타임 폴백)가 이 케이스엔 적용 안 됨 —
|
||||
새 primitive 추가에 문제없음.
|
||||
|
||||
### 패키지 배치 — Tag와 동일 원칙
|
||||
### 패키지 배치 — 값 타입도 알고리즘도 quad-base, 주입되는 건 `setAttribute` 하나 (2026-08-13 열네 번째 세션 전면 재배치)
|
||||
|
||||
값 타입+API(`Attribute(...)`/`Merged`)는 quad-base(엔진 무관, 순수 데이터+
|
||||
연산). `SetAttribute` 실제 호출 글루만 quad-roblox.
|
||||
**[전면 재배치, 2026-08-13 열네 번째 세션, 사용자 판단]** 예전엔 값
|
||||
타입/API만 quad-base이고 **단일 키 `AttributeKey`와 두 Handler는 통째로
|
||||
quad-roblox** 소속이었음 — 그런데 실제로 엔진에 종속된 건 마지막 한 줄
|
||||
(`inst:SetAttribute`)뿐이고, 이름 claim·그룹 위임·`None` 처리·이름별 weak
|
||||
캐시는 전부 순수 부기임. 웹에도 대응물이 있으므로(`data-*`) 그 배치대로면
|
||||
**같은 소유권 알고리즘을 백엔드마다 재구현**하게 됨 — `architecture.md`의
|
||||
"엔진마다 큰 구현을 중복하지 않기 위해 디스패치 엔진을 base가 인터페이스로
|
||||
소유한다"는 원칙에 정면으로 어긋남.
|
||||
|
||||
## 패키지 배치 (단일 키 `AttributeKey`)
|
||||
**확정된 배치**:
|
||||
|
||||
UICorner 숏핸드/Tween/Tag와 같은 판단 재사용 — `quad-roblox` 코어에 직접
|
||||
포함, 별도 opt-out 패키지로 안 쪼갬.
|
||||
| 무엇 | 어디 |
|
||||
|---|---|
|
||||
| 그룹 값 타입+API(`Attribute(...)`/`Merged`/`:NameMap`) | quad-base |
|
||||
| 단일 키 `AttributeKey<<T>>(name)` + 이름별 weak 캐시 | quad-base |
|
||||
| 스칼라 편의 패밀리(`StringAttribute`/`NumberAttribute`/`BooleanAttribute`) | quad-base |
|
||||
| `AttributeKeyHandler`(이름 claim 포함) / `AttributeGroupHandler`(전용 키 위임) | quad-base, `HANDLER_PRIORITY_FALLBACK`으로 등록 |
|
||||
| 엔진 고유 타입 패밀리(`Color3Attribute`/`UDim2Attribute`/`InstanceAttribute`류) | 백엔드(quad-roblox의 `D`/`DI` 층) |
|
||||
| **`setAttribute(inst, name, v)`** — `v == nil`이면 그 이름을 지움 | 백엔드가 주입 |
|
||||
|
||||
- **왜 타입 패밀리만 갈리는가**: Roblox attribute가 받는 타입 집합
|
||||
(`Color3`/`UDim2`/`CFrame`/`Instance` 등)은 엔진 고유 어휘라 base가 알
|
||||
수 없음. 반대로 string/number/boolean은 어느 백엔드에나 있으므로 base에
|
||||
둔다. "이 값이 이 백엔드에서 표현 가능한가"라는 **검증도 base가 아니라
|
||||
주입된 `setAttribute`의 몫** — base는 값을 그대로 흘려보냄.
|
||||
- **백엔드가 통째로 다르게 하고 싶으면** 평범한 우선순위로 자기 핸들러를
|
||||
등록하면 됨(base 것은 최하위 밴드라 자동으로 짐) — 상세는
|
||||
`base/dispatch-core-plan.md`의 "base가 소유하는 핸들러와 주입되는 엔진
|
||||
op" 절. `Tag`도 정확히 같은 구조(`base/tag-plan.md`).
|
||||
- 단일 키를 별도 opt-out 패키지로 쪼개지 않는다는 기존 판단은 그대로
|
||||
(UICorner 숏핸드/Tween/Tag와 같은 결) — 다만 "어느 패키지의 코어인가"가
|
||||
quad-roblox에서 quad-base로 바뀐 것.
|
||||
|
||||
## 열린 질문 (`.claude/question.md`에도 취합)
|
||||
|
||||
- **[2026-08-13 3차 감사에서 보강] `hintValue`/`retractFrom` 재-dispatch
|
||||
메커니즘은 `question.md` **0-Z**(이 문서 자신의 이름 소유권 결정)
|
||||
해소 대기 중** — 이 문서 최상단 배너와 "이름 소유권" 절에 이미
|
||||
명시돼 있지만, 이 목록에는 빠져 있어서 여기만 훑는 독자가 놓치기
|
||||
쉬움. `research/dispatch-redispatch-diff-plan.md` 먼저 읽을 것.
|
||||
- **[해소, 2026-08-13 열네 번째 세션] 이름 소유권(`question.md` 0-Z)과
|
||||
하강 diff 재디스패치(0-A)는 확정·반영 완료** — 위 "이름 소유권"/
|
||||
"메커니즘" 절이 정본, 뒤집힌 옛 모델은
|
||||
`archive/dispatch-hintvalue-model-reversed.md`.
|
||||
- **[열림, 사소함, 2026-08-13 열네 번째 세션 신설] `Attribute.Merged`에서
|
||||
두 Store가 같은 이름을 가지면 지금은 조용히 하나가 이김** —
|
||||
`:NameMap()` 평탄화가 dispatch 이전 단계라 위 이름 claim이 못 잡는
|
||||
자리. 이름 겹침을 error로 잡는 게 이 문서의 다른 결정들과 결이 같고
|
||||
구현도 싸지만(합성 시점 1회 체크), "Merged는 뒤가 이긴다"를 의도된
|
||||
override로 볼 여지도 있어서 사용자 확인 대기 — `question.md` 3번.
|
||||
- **이름은 잠정 확정, 최종 확정은 대기열**: 겹침 방지를 위해 그룹 값은
|
||||
`Attribute`, 단일 키는 `AttributeKey<<T>>`로 코드/문서 전체 통일해서
|
||||
당장의 해석 모호성은 없앴음 — 그래도 최종 이름은 다른 가칭들(`State`/
|
||||
`DI`→`D`/`Slot`/`canExecute`/`Brand`)과 함께 `.claude/question.md` 용어정리
|
||||
당장의 해석 모호성은 없앴음 — 그래도 최종 이름은 다른 가칭들(`DI`→`D`/
|
||||
`Slot`/`canExecute`/`Brand`)과 함께 `.claude/question.md` 용어정리
|
||||
대기열에 있음, 나중에 한꺼번에 재검토.
|
||||
- **[백로그, 2026-08-12 세션 후속]** 그룹이 이름을 조용히 놓아도
|
||||
`SetAttribute(name,nil)`을 자동으로 안 해준다는 위 "그룹 `Attribute(...)`"
|
||||
`setAttribute(inst,name,nil)`을 자동으로 안 해준다는 위 "그룹 `Attribute(...)`"
|
||||
절의 결정 — 그래도 명시적 자동 unset이 갖고 싶으면 `Animate`와 같은
|
||||
모양의 `:Apply` opt-in 유틸(이전 이름 집합과 비교해 사라진 이름을
|
||||
`None`으로 채워주는 콤비네이터)을 나중에 추가할 수 있음, 착수 안 함 —
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
1214
.claude/base/dispatch-core-plan.md
Normal file
1214
.claude/base/dispatch-core-plan.md
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -242,7 +242,7 @@ end
|
|||
직접 읽기로 확정했으나(위 구현), 실제 Luau 필드 접근 비용/weak table 조회
|
||||
비용 비교는 여전히 quad-roblox 구현 단계에서 실측 확인 대상.
|
||||
|
||||
이건 `base/bind-system-plan.md`의 "핸들러 내부 상태 저장" 유틸(`Relate`
|
||||
이건 `base/dispatch-core-plan.md`의 "핸들러 내부 상태 저장" 유틸(`Relate`
|
||||
직접 사용)과 짝을 이루는 별도 유틸 — 하나는 "상태를 어디에 저장할지"
|
||||
(`Relate:SetStrong`/`:SetWeak`), 다른 하나는 "언제까지 실행되어도 되는지"
|
||||
(`bindLifetime` + `canExecute`)를 다룸. 후자는 내부적으로 전자가 제공하는
|
||||
|
|
@ -325,6 +325,6 @@ Roblox 엔진 자체가 Destroy 시 Tag/Attribute/실행 중인 Tween을 전부
|
|||
자체는 그대로 유효하고(이 절의 결론은 안 바뀜), 다만 코드에서
|
||||
`handler.retract(...)`를 찾으면 안 됨 — `local retractor =
|
||||
handler.process(inst,k,v,index)` 형태로 받아서 `Dispatch`가 `chains`에
|
||||
보관했다가 부름(`base/bind-system-plan.md` "핸들러 계약"/"Dispatch 체인"
|
||||
보관했다가 부름(`base/dispatch-core-plan.md` "핸들러 계약"/"Dispatch 체인"
|
||||
절). 이 문서가 계속 쓰는 "retract 시점"/"retract가 불린다"는 표현은
|
||||
전부 그 클로저가 호출되는 시점을 가리킴.
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ init하려 하면 오류, 없는데 뭔가 생성해서 bind하려 해도 오류
|
|||
수행하는 "처리된 값을 다시 `Dispatch.process(inst,k,realv)`로 넘기는" 재실행
|
||||
로직 자체도 base가 한 번만 구현**해야 함(모든 백엔드/핸들러가 각자
|
||||
재구현하면 안 됨). 근거: "모든 곳에서 다시 구현하는 건 나쁘니까." →
|
||||
`base/bind-system-plan.md`의 "확정된 디스패치 모델"/`Dispatch` 네이밍 절이
|
||||
`base/dispatch-core-plan.md`의 "확정된 디스패치 모델"/`Dispatch` 네이밍 절이
|
||||
바로 이 base 제공 로직.
|
||||
|
||||
부수적으로 확인된 것:
|
||||
|
|
@ -104,9 +104,9 @@ init하려 하면 오류, 없는데 뭔가 생성해서 bind하려 해도 오류
|
|||
인터페이스"가 곧 Handler 계약: `isHandlable(inst,key,value)`/
|
||||
`priority`/`process(inst,key,value,index)` **3종**(**[정정, 2026-08-13
|
||||
다섯 번째 세션]** 원래 별도 `retract(inst,key,value)` 필드가 있던 4종
|
||||
계약이었으나, `process`가 자기 retract 클로저 `(hintValue) -> ()`를
|
||||
계약이었으나, `process`가 자기 retract 클로저 `(nextValue: any?) -> ()`를
|
||||
반환하는 1-메소드로 합쳐짐), 정리할 게 없어도 `function() end`
|
||||
반환 생략 불가까지 확정. `base/bind-system-plan.md` "핸들러 계약" 절.
|
||||
반환 생략 불가까지 확정. `base/dispatch-core-plan.md` "핸들러 계약" 절.
|
||||
- ~~**네이밍 미정(2026-08-04 보강)**: "프로바이더"라고 불러온 개념을 정확히
|
||||
뭐라고 부를지("provider" vs "processor" vs 그냥 "plug") 아직 안 정함~~
|
||||
**[해소됨]** — **`Handler`로 확정**, 위 항목이 가리키는 계약의 정식 이름.
|
||||
|
|
@ -129,7 +129,14 @@ init하려 하면 오류, 없는데 뭔가 생성해서 bind하려 해도 오류
|
|||
처리" 절의 일반 "매치 실패 시 즉시 error" 규칙 하나로 자연히 커버됨.
|
||||
- base 유틸(per-instance 상태 저장소, 생명 바인드 유틸)이 인터페이스만 두고
|
||||
실제 구현은 백엔드 팩토리(`RobloxFactory(BaseModule)`류)가 뮤테이션으로
|
||||
주입한다는 패턴이 확정됨 — 상세는 `base/bind-system-plan.md`의 "base
|
||||
주입한다는 패턴이 확정됨. **[2026-08-13 열네 번째 세션] 주입 대상 목록에
|
||||
엔진 op 3개가 추가됨** — `addTag(inst,{string})`/`removeTag(inst,{string})`/
|
||||
`setAttribute(inst,name,v)`(`v==nil`이면 삭제). `Tag`/`Attribute`의 부기
|
||||
알고리즘이 통째로 quad-base로 옮겨오면서, 엔진에 실제로 손대는 마지막
|
||||
한 줄만 이 경로로 주입받게 됨(`base/dispatch-core-plan.md` "base가
|
||||
소유하는 핸들러와 주입되는 엔진 op" 절). 미주입 백엔드에서는 base
|
||||
스텁이 명확한 에러를 내고, 백엔드가 통째로 다르게 처리하고 싶으면
|
||||
`HANDLER_PRIORITY_FALLBACK`보다 높은 우선순위로 자기 핸들러를 등록하면 됨 — 상세는 `base/bind-system-plan.md`의 "base
|
||||
유틸은 인터페이스, 실제 구현은 백엔드 팩토리가 주입" 절 참고. **중복 호출
|
||||
가드/`New()`와의 관계는 2026-08-04 3차 라운드에서 확정**: 같은 팩토리로
|
||||
재호출하면 무시(no-op), 다른 팩토리로 재호출하면 에러(유일 슬롯 충돌 —
|
||||
|
|
@ -137,4 +144,4 @@ init하려 하면 오류, 없는데 뭔가 생성해서 bind하려 해도 오류
|
|||
생기면 인스턴스별 테이블이 분리되므로 이 가드도 자연히 인스턴스별로
|
||||
스코핑됨, 별도 재설계 불필요. **이 결론이 Dispatch의 handler 레지스트리에도
|
||||
그대로 적용된다는 게 2026-08-08 두 번째 세션에서 재확인/일반화됨** —
|
||||
`base/bind-system-plan.md` "Dispatch는 프리미티브가 아니다" 절.
|
||||
`base/dispatch-core-plan.md` "Dispatch는 프리미티브가 아니다" 절.
|
||||
|
|
|
|||
|
|
@ -37,9 +37,14 @@
|
|||
"제네릭 + 자주 쓰는 것만 정적 지름길" 절충과 겉보기엔 비슷해 보이지만
|
||||
규모가 다른 문제라 기각.
|
||||
- **패키지 경계: 전부 quad-roblox** — `Handlers/OnChange.luau`에 `OnChange(name)`
|
||||
키 팩토리와 Handler를 같이 둠(단일 키 `AttributeKey.luau`와 같은 배치,
|
||||
base 쪽 값 타입 파일 없음 — 그룹 `Attribute.luau`는 값 타입 자체가
|
||||
quad-base 소속이라 다름, `base/attribute-plan.md` 참고).
|
||||
키 팩토리와 Handler를 같이 둠. **[정정, 2026-08-13 열네 번째 세션]**
|
||||
예전엔 "단일 키 `AttributeKey.luau`와 같은 배치"라고 적었으나 그
|
||||
`AttributeKey`는 같은 세션에 **quad-base로 옮겨갔음**(부기가 엔진 지식을
|
||||
요구하지 않아서, `base/attribute-plan.md` "패키지 배치" 절) — `OnChange`가
|
||||
quad-roblox에 남는 이유는 그것과 달리 **`GetPropertyChangedSignal`
|
||||
자체가 로직**이라 "한 줄 op 주입"으로 줄어들지 않기 때문
|
||||
(`base/dispatch-core-plan.md` "base가 소유하는 핸들러와 주입되는 엔진
|
||||
op" 절의 분할 기준).
|
||||
`GetPropertyChangedSignal` 자체가 Roblox 엔진 API라 base에
|
||||
둘 이유가 없음 — Tag처럼 백엔드 무관한 값/API 레이어가 따로 있는 경우와
|
||||
다름.
|
||||
|
|
@ -73,7 +78,7 @@
|
|||
| | 소스 | 값 타입 | 패키지 경계 |
|
||||
|---|---|---|---|
|
||||
| 이벤트(`MouseButton1Click = fn`) | `inst[key]`가 이미 Signal | 콜백, 타입 미검증 | quad-roblox(`Handlers/Event.luau`) |
|
||||
| `AttributeKey(name)` | `SetAttribute`/`GetAttribute` | 값(제네릭 또는 정적 타입 패밀리로 타입 파라미터화) | quad-roblox(`Handlers/AttributeKey.luau`) |
|
||||
| `AttributeKey(name)` | 주입된 `setAttribute` op | 값(제네릭 또는 정적 타입 패밀리로 타입 파라미터화) | **quad-base**(키+Handler, 2026-08-13 열네 번째 세션 재배치) / 엔진 op만 백엔드 |
|
||||
| `OnChange(name)` | `GetPropertyChangedSignal(name)` | 콜백, 타입 미검증(제네릭 없음) | quad-roblox(`Handlers/OnChange.luau`) |
|
||||
|
||||
`OnChange`가 Attribute처럼 제네릭화되지 않은 이유는 "콜백을 받는다"는
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@
|
|||
|
||||
**중요한 정정**: Ref는 Tween이 대상을 얻기 위해 필요한 게 아님(트윈을
|
||||
실제로 처리하는 PropertyHandler도 `process(inst,k,v)`처럼 항상 대상
|
||||
Instance를 직접 받으므로 — 위 "확정된 디스패치 모델" 참고, `research/
|
||||
tween-plan.md`도 이에 맞춰 갱신됨). Ref의 진짜 용도는 다름:
|
||||
Instance를 직접 받으므로 — `base/dispatch-core-plan.md` "확정된 디스패치
|
||||
모델" 참고, `base/tween-plan.md`도 이에 맞춰 갱신됨). Ref의 진짜 용도는 다름:
|
||||
|
||||
- v1의 `Frame "id" {}` + `Store.GetObject(id)` 식 id 매핑은 폐기 확정
|
||||
(`base/architecture.md` 5번 항목) — "비현실적"이라는 게 이유.
|
||||
|
|
@ -210,16 +210,13 @@ tween-plan.md`도 이에 맞춰 갱신됨). Ref의 진짜 용도는 다름:
|
|||
|
||||
### `Ref`의 retract — `State<Ref>` 재바인드 시 이전 Ref에 `nil` (2026-08-12 여덟 번째 세션, `TagHandler`와 같은 메커니즘 재사용)
|
||||
|
||||
> **⚠️ [2026-08-13 감사 보강] 이 절의 "이전 `process`가 반환한 클로저가
|
||||
> `hintValue`로 먼저 불려 언바인딩하고, 그 다음 `process`가 바인딩"이라는
|
||||
> 서술은 곧 교체될 예정 — 아직 반영 안 됨. `research/dispatch-redispatch-diff-plan.md`가
|
||||
> 폐기 대상으로 지목한 바로 그 **선행 `retractFrom` 호출** 패턴이다 — 이 문서가
|
||||
> `bind-system-plan.md`에서 분리(2026-08-13 아홉 번째 세션, 순수 이동)될
|
||||
> 때 원문이 그대로 옮겨오면서 `bind-system-plan.md`/`tag-plan.md`/
|
||||
> `slot-plan.md`/`attribute-plan.md`가 이미 달고 있는 것과 같은 0-Z
|
||||
> 배너가 같이 안 옮겨졌음. `question.md` **0-Z**(Attribute 이름 소유권)
|
||||
> 해소 시 "하강 diff" 모델대로 이 절도 같이 재작성할 것 — **여기 적힌
|
||||
> 대로 구현하면 옛 모델로 짜게 됨.**
|
||||
> **✅ [2026-08-13 열네 번째 세션] 하강 diff 재디스패치 반영 완료.**
|
||||
> "이전 클로저가 먼저 불려 언바인딩, 그 다음 `process`가 바인딩"이라는
|
||||
> **두 단계 자체는 그대로**이고, 그걸 일으키는 주체만 바뀌었음(래핑
|
||||
> 핸들러의 선행 `retractFrom` → `Dispatch.process`의 핸들러 선비교).
|
||||
> 클로저가 받는 값의 타입도 이제 계약으로 보장됨(항상 `Ref`이거나
|
||||
> `nil`). 상세는 `base/dispatch-core-plan.md` "Dispatch 체인" 절, 옛
|
||||
> 모델 원문은 `archive/dispatch-hintvalue-model-reversed.md`.
|
||||
|
||||
**배경**: `Ref`는 이미 "일반 프로퍼티/Modifier 필드/Store 값 어디든 자유롭게
|
||||
들어감"(아래 "동적 경로 가드" 절)이 확정돼 있어 — `State<Ref>`가 실제로
|
||||
|
|
@ -229,19 +226,20 @@ tween-plan.md`도 이에 맞춰 갱신됨). Ref의 진짜 용도는 다름:
|
|||
조용한 버그가 남음 — `PreRef` 재사용 버그(위 절)와 같은 클래스의 문제.
|
||||
|
||||
**메커니즘 — retractor가 매번 불린다는 전제 위에서 언바인딩 전담
|
||||
(2026-08-12 열한 번째 세션 정정, 2026-08-13 다섯 번째 세션에 클로저
|
||||
반환 계약으로 서술 갱신).** `Dispatch.retractFrom`은 store 값이 바뀔
|
||||
때마다(핸들러 타입이 그대로여도) 무조건 불림 — 위 "확정된 디스패치
|
||||
모델"/일반 retract 계약 절 참고. 그래서 `refA→refB` 전환도 이전
|
||||
`process`가 반환한 클로저가 `hintValue=refB`로 먼저 불려 `refA`를
|
||||
언바인딩하고, 그 다음 `process(inst,k,refB,index)`가 `refB`를 바인딩하는
|
||||
두 단계로 자연히 갈림 — `process`가 old-vs-new diff를 따로 계산할
|
||||
(2026-08-12 열한 번째 세션 정정, 2026-08-13 다섯 번째/열네 번째 세션에
|
||||
서술 갱신).** 이전 `process`가 반환한 클로저는 store 값이 바뀔
|
||||
때마다(핸들러 타입이 그대로여도) 무조건 불림 — 같은 핸들러면
|
||||
`Dispatch.process`가 그 자리 클로저에 새 값을 넘기고 곧바로 `process`를
|
||||
다시 부르기 때문(`base/dispatch-core-plan.md` "Dispatch 체인" 절 (A)
|
||||
분기). 그래서 `refA→refB` 전환은 이전 클로저가 `nextValue=refB`로 먼저
|
||||
불려 `refA`를 언바인딩하고, 그 다음 `process(inst,k,refB,index)`가
|
||||
`refB`를 바인딩하는 두 단계로 자연히 갈림 — `process`가 old-vs-new diff를 따로 계산할
|
||||
필요가 없어짐(그 일을 클로저가 매번 정확히 대신 해줌). **`process` 쪽엔
|
||||
여전히 `Relate`가 필요** — "spurious하게 같은 Ref가 재발행되면 재통지
|
||||
skip"이라는 dedup은 `process`가 "이전에 뭐가 있었는지"를 알아야 하는데,
|
||||
그건 인자로 안 들어오고(클로저의 `hintValue`는 다음 값이지 이전 값이
|
||||
아님) 오직 여러 호출을 가로지르는 저장소로만 알 수 있음(위 "핸들러
|
||||
내부 상태 저장" 절이 이런 경우엔 `Relate`가 여전히 맞다고 한 그 사례):
|
||||
그건 인자로 안 들어오고(클로저가 받는 건 다음 값이지 이전 값이
|
||||
아님) 오직 여러 호출을 가로지르는 저장소로만 알 수 있음
|
||||
(`base/dispatch-core-plan.md` "핸들러 내부 상태 저장" 절이 이런 경우엔 `Relate`가 여전히 맞다고 한 그 사례):
|
||||
|
||||
```lua
|
||||
local relate = Relate() -- Ref-leaf handler 전용, (inst,k)별 마지막으로 바인딩한 Ref 기억 —
|
||||
|
|
@ -255,14 +253,14 @@ function RefLeafHandler.process(inst, k, v, index)
|
|||
v:Set(inst)
|
||||
end
|
||||
relate:SetStrong(inst, k, v)
|
||||
return function(hintValue)
|
||||
-- hintValue는 nil일 수도, 대체하는 새 Ref 자체일 수도 있음 — v는
|
||||
return function(nextValue)
|
||||
-- nextValue는 nil이거나 같은 핸들러가 곧 처리할 새 Ref(타입 보장됨) — v는
|
||||
-- 이 process 호출이 만든 클로저가 직접 캡처(Relate 재조회 불필요)
|
||||
if hintValue ~= v then
|
||||
if nextValue ~= v then
|
||||
v:Set(nil) -- 매 :Set()마다 콜백 재통지되는 기존 Ref 규칙(위 "해소됨 —
|
||||
-- 반복 재설정 가능" 항목)을 그대로 재사용, 새 알림 경로 아님
|
||||
-- [정정, 2026-08-13 감사] relate 정리는 반드시 이 분기 *안*에 있어야
|
||||
-- 함 — 밖에 두면 spurious 재발행(hintValue == v)에서도 기록이
|
||||
-- 함 — 밖에 두면 spurious 재발행(nextValue == v)에서도 기록이
|
||||
-- 지워져, 곧바로 이어지는 process가 `old ~= v`를 항상 참으로 보고
|
||||
-- `v:Set(inst)`를 재실행함(콜백 헛 재통지). 즉 아래 dedup 항목이
|
||||
-- 약속한 "spurious면 둘 다 스킵"이 성립을 안 했음.
|
||||
|
|
@ -273,7 +271,7 @@ end
|
|||
```
|
||||
|
||||
- **retractor가 언바인딩 전담, `process`는 바인딩 전담** — 겹치는 diff
|
||||
로직이 없음. `hintValue == v`(같은 Ref 객체가 스스로 재발행된
|
||||
로직이 없음. `nextValue == v`(같은 Ref 객체가 스스로 재발행된
|
||||
spurious한 경우)만 둘 다 스킵해 콜백이 `nil`→`inst`로 헛되이 두 번 안
|
||||
불리게 함.
|
||||
- **children 배열 리터럴 `Ref`도 같은 코드 경로를 그대로 씀** — 그 경우
|
||||
|
|
@ -309,7 +307,8 @@ end
|
|||
아홉 번째 세션에서 폐기됨, 위 "바인드 방법" 절 참고)
|
||||
|
||||
**children 배열에 놓는 Ref에 `{phase="created"|"mounted"}` 옵션으로 두
|
||||
타이밍을 고르게 하던 것 자체를 없앤다.** 위 "확정된 디스패치 모델" 절에
|
||||
타이밍을 고르게 하던 것 자체를 없앤다.** `base/dispatch-core-plan.md`의
|
||||
"확정된 디스패치 모델" 절에
|
||||
새로 추가된 두 패스 보장(배열 파트는 index 순서대로, 그 다음 해시 파트)
|
||||
덕분에, 같은 인스턴스 안에서 **일반 `Ref`를** 다른 children보다 앞/뒤
|
||||
어디에 놓느냐가
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ relate:GetWeak(inst: any, key: any): any?
|
|||
## 언제 `Relate`를 쓰고 언제 쓰면 안 되는가 — 체크리스트 (2026-08-13 여섯 번째 세션 신설)
|
||||
|
||||
Handler 계약이 "`process`가 자기 retract 클로저를 반환"으로 바뀌면서
|
||||
(`base/bind-system-plan.md` "핸들러 계약" 절) **`Relate`가 필요한 범위가
|
||||
(`base/dispatch-core-plan.md` "핸들러 계약" 절) **`Relate`가 필요한 범위가
|
||||
크게 줄었음** — 이 전환기에 양방향으로 실수가 나왔어서 기준을 못박아 둠.
|
||||
|
||||
**쓰지 말 것 — 클로저 캡처로 충분한 경우**: 이 `process` 호출이 만든
|
||||
|
|
@ -111,7 +111,7 @@ Handler 계약이 "`process`가 자기 retract 클로저를 반환"으로 바뀌
|
|||
- **여러 위치가 하나의 자원을 공유**할 때의 참조 카운트 — `Tag`의
|
||||
`tagNameMap`(이름 → 그 이름을 걸고 있는 위치 집합).
|
||||
- **여러 `process` 호출을 가로지르는 dedup 기록** — `Ref`의 "이 자리에
|
||||
마지막으로 바인딩한 Ref"(클로저의 `hintValue`는 *다음* 값이지 *이전*
|
||||
마지막으로 바인딩한 Ref"(클로저가 받는 인자는 *다음* 값이지 *이전*
|
||||
값이 아니라서 캡처로 대체 불가).
|
||||
- **소유권/멤버십 전역 판정** — `Slot`의 `elementOwner`.
|
||||
- **"언제까지 실행돼도 되는가"** — `bindLifetime`/`canExecute`
|
||||
|
|
@ -162,7 +162,7 @@ Handler 계약이 "`process`가 자기 retract 클로저를 반환"으로 바뀌
|
|||
|
||||
## 대체하는 것
|
||||
|
||||
- `base/bind-system-plan.md` "핸들러 내부 상태 저장" 절의 `base.perInstanceState(inst)`
|
||||
- `base/dispatch-core-plan.md` "핸들러 내부 상태 저장" 절의 `base.perInstanceState(inst)`
|
||||
placeholder — Tween 등 핸들러가 `retract` 대상을 저장하는 용도, `SetStrong`으로.
|
||||
- `base/lifecycle-pattern.md`의 `bindLifetime`/`canExecute` — gcconn/gchold를
|
||||
`Relate`의 `SetStrong`으로 저장(둘 다 존재 이유가 "안 죽는 것"이므로
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
# Slot — 뮤터블 자식 배열, 엄격한 단일 마운트 소유권 (base로 승격됨)
|
||||
|
||||
> **⚠️ [2026-08-13 여섯 번째 세션] 이 문서의 `hintValue`/`retractFrom` 선행
|
||||
> 호출 서술은 곧 교체될 예정 — 아직 반영 안 됨.** 힌트가 `None` 센티널이나
|
||||
> `State`/`Tween` 래퍼로 오염돼 말단 핸들러의 `isX(hint)` 가드를 거짓으로
|
||||
> 만들고 깜빡임/재생성 방지를 조용히 끄는 결함이 확인됐고, 대체 모델
|
||||
> (**래핑 핸들러의 `retractFrom` 선행 호출 폐기 + `Dispatch.process`가
|
||||
> 핸들러를 먼저 비교**)까지 거의 확정됐음 — 다만 `question.md` **0-Z**
|
||||
> (Attribute 이름 소유권) 하나가 남아 아직 옮기지 않음. **여기 적힌 대로
|
||||
> 구현하면 옛 모델로 짜게 됨** — 반드시
|
||||
> `research/dispatch-redispatch-diff-plan.md`를 먼저 읽을 것.
|
||||
> **✅ [2026-08-13 열네 번째 세션] 하강 diff 재디스패치 반영 완료.**
|
||||
> 이전 ⚠️ 배너가 예고하던 교체가 끝났음 — 래핑 핸들러의 `retractFrom`
|
||||
> 선행 호출이 폐기되고 `Dispatch.process`가 핸들러를 먼저 비교하며,
|
||||
> `SlotHandler`가 반환하는 클로저가 받는 값의 **타입이 계약으로 보장**됨
|
||||
> (같은 핸들러로 재프로세스될 때만 값이 넘어오므로 항상 `Slot`이거나
|
||||
> `nil`). 상세는 `base/dispatch-core-plan.md` "Dispatch 체인" 절, 옛
|
||||
> 모델 원문은 `archive/dispatch-hintvalue-model-reversed.md`.
|
||||
|
||||
**상태**: base — 설계 방향(소유권 귀속, 재마운트 시 throw, **[2026-08-13
|
||||
여섯 번째 세션 역전] retract=언마운트** — 옛 "retract=폐기"는 뒤집혔음,
|
||||
|
|
@ -225,16 +223,17 @@ Slot이 store 바인드로 들어오는 경우, pluggable 처리기에 `retract`
|
|||
> 동작이 '부모 위임' 잠정안에서 '폐기(옮기지 않음)'로 확정") 참고. 이 문단은
|
||||
> 검토 과정의 히스토리로만 남겨둠, 현재 유효한 동작 아님.
|
||||
|
||||
**[전면 정정, 2026-08-12 열한 번째 세션, 2026-08-13 다섯 번째 세션에
|
||||
클로저 반환 계약으로 서술 갱신] 위 "핸들러 타입이 안 바뀌면 retract 없이
|
||||
process가 diff 담당"이라는 전제 자체가 틀렸음** — 실제로는
|
||||
`Dispatch.retractFrom`이 store 재발행마다(핸들러가 그대로여도) **항상**
|
||||
이전 `process`가 반환한 클로저를 먼저 부름(`base/bind-system-plan.md`
|
||||
"확정된 디스패치 모델"/일반 retract 계약 절 참고). 그래서 `State<Slot>`이
|
||||
`slotA→slotB`로 바뀔 때도 **`slotA`를 처리했던 클로저가 `hintValue=slotB`로
|
||||
먼저 불려 `slotA`를 폐기하고, 그 다음 `process(inst,k,slotB,index)`가
|
||||
`slotB`를 마운트**하는 두 단계로 자연히 갈림 — 클로저가 "이전 것 정리",
|
||||
`process`가 "새 것 마운트" 전담:
|
||||
**[전면 정정, 2026-08-12 열한 번째 세션, 2026-08-13 다섯 번째/열네 번째
|
||||
세션에 서술 갱신] 위 "핸들러 타입이 안 바뀌면 retract 없이 process가 diff
|
||||
담당"이라는 전제 자체가 틀렸음** — store 재발행마다(핸들러가 그대로여도)
|
||||
**항상** 이전 `process`가 반환한 클로저가 먼저 불림. **[갱신, 열네 번째
|
||||
세션] 부르는 주체는 `Dispatch.process` 자신**(같은 핸들러면 그 자리
|
||||
클로저에 새 값을 넘기고 곧바로 `process`를 다시 부름 —
|
||||
`base/dispatch-core-plan.md` "Dispatch 체인" 절 (A) 분기). 그래서
|
||||
`State<Slot>`이 `slotA→slotB`로 바뀔 때 **`slotA`를 처리했던 클로저가
|
||||
`nextValue=slotB`로 먼저 불려 `slotA`를 언마운트하고, 그 다음
|
||||
`process(inst,k,slotB,index)`가 `slotB`를 마운트**하는 두 단계로 자연히
|
||||
갈림 — 클로저가 "이전 것 정리", `process`가 "새 것 마운트" 전담:
|
||||
|
||||
**[정정, 2026-08-12 열두 번째 세션] "같은 값인가"를 위치별 relate로
|
||||
간접 비교하는 대신, Slot 자신이 지금 어느 `inst`에 바인딩됐는지를 직접
|
||||
|
|
@ -339,11 +338,13 @@ function SlotHandler.process(inst, k, slotValue, index)
|
|||
-- 방금 새로 클레임했든, 직전 사이클의 클레임이 spurious 재발행을 거쳐 그대로
|
||||
-- 살아있든 둘 중 하나(다른 소유자였다면 claimOwnerAt이 이미 error). 어느 쪽이든
|
||||
-- "이 자리가 결국 다른 값으로 바뀔 때 할 일"은 slotValue/inst가 같아서 동일하므로
|
||||
-- 클로저도 하나로 통일 — 여기서 no-op 클로저를 반환하면 안 됨: retractFrom은
|
||||
-- 클로저를 early-return시키든 말든 체인에서 항상 *소비*하므로, spurious 사이클에서
|
||||
-- no-op을 심어두면 그 다음 진짜 교체 때 이전 서브트리를 정리할 주체가 사라짐.
|
||||
return function(hintValue)
|
||||
if slotValue == hintValue then return end -- 같은 Slot 재발행 → no-op
|
||||
-- 클로저도 하나로 통일 — 여기서 no-op 클로저를 반환하면 안 됨: 체인은
|
||||
-- 클로저를 early-return시키든 말든 항상 *소비*하므로(retractFrom도, 재프로세스도),
|
||||
-- spurious 사이클에서 no-op을 심어두면 그 다음 진짜 교체 때 이전 서브트리를
|
||||
-- 정리할 주체가 사라짐.
|
||||
return function(nextValue)
|
||||
-- nextValue는 nil이거나 같은 핸들러가 곧 처리할 Slot — 타입 보장됨
|
||||
if slotValue == nextValue then return end -- 같은 Slot 재발행 → no-op
|
||||
-- [정정, 2026-08-13 감사 후속] 파괴가 아니라 **언마운트** —
|
||||
-- 아래 "`State<Slot>` 교체는 파괴가 아니라 언마운트" 절이 확정한
|
||||
-- 결정이 적용돼야 하는 자리가 바로 여기(최상위 dispatch 경로).
|
||||
|
|
@ -379,8 +380,8 @@ end
|
|||
`kSlotMap`에 안 적힌 자리는 `retract`가 자연히 no-op이라 우연히 막혀
|
||||
있었는데, `kSlotMap` 제거가 이 방어를 같이 걷어낸 회귀였음. 이제
|
||||
`claimOwnerAt`이 `k=2`에서 곧바로 error를 내므로 그 상태 자체가 안
|
||||
만들어짐 — **클로저를 두 갈래로 쪼개는 방식으로는 못 고침**: `retractFrom`은
|
||||
클로저가 early-return하든 말든 체인에서 항상 소비하므로, spurious
|
||||
만들어짐 — **클로저를 두 갈래로 쪼개는 방식으로는 못 고침**: 체인은
|
||||
클로저가 early-return하든 말든 항상 소비하므로, spurious
|
||||
사이클에서 no-op 클로저를 심으면 다음 진짜 교체 때 이전 서브트리를
|
||||
정리할 주체가 사라져 오히려 더 큰 누수가 됨(감사 중 실제로 그 방향을
|
||||
먼저 써봤다가 되돌림).
|
||||
|
|
@ -405,7 +406,7 @@ error가 맞음**. 반대로 top-level은 store 재발행마다 같은 Slot으
|
|||
**위치를 키에 넣어도 안전한 이유(사용자 확인)** — 여기서 쓰는 `k`는
|
||||
"바깥 컨테이너 안에서의 인덱스"고, 이건 nested Slot의 `Length`가 변해도
|
||||
바뀌지 않음. 실제 물리 배치의 변동은 전부 `offset`이 흡수하도록 설계돼
|
||||
있음(`base/bind-system-plan.md` "Length/Offset" 절의 `recompute`가 그
|
||||
있음(`base/dispatch-core-plan.md` "Length/Offset" 절의 `recompute`가 그
|
||||
증거 — `lengthList`/`sourceList`는 위치별 배열이고 순서 계산만
|
||||
누적합으로 함). top-level의 `k`는 특히 props 배열 리터럴의 위치라
|
||||
저작 시점에 고정. **nested는 `Move`/`Swap`/`Splice`/`Remove`가
|
||||
|
|
@ -656,7 +657,7 @@ Remove/Extract/Move하려 해도 참조를 안 들고 있는 경우가 잦음. `
|
|||
Dispatch 호출일 뿐이라 "일반적 무한루프는 방어 안 함, provider 버그로
|
||||
간주"라는 기존 원칙이 그대로 적용됨. `recompute` 자체의 재진입(같은
|
||||
Slot의 length를 자기 계산 도중 다시 건드리는 것)도 같은 톤으로 UB —
|
||||
`base/bind-system-plan.md`의 "Length/Offset" 절, `Source⊇State`
|
||||
`base/dispatch-core-plan.md`의 "Length/Offset" 절, `Source⊇State`
|
||||
단방향 원칙과 같은 카테고리로 명명됨(2026-08-11 세션).
|
||||
- **`Slot(initial?: {T})` 생성자 — [정정, 2026-08-11 세션] "인자 없는
|
||||
빈 생성자로 확정"을 뒤집고 초기 배열을 받는 옵션 생성자를 다시 엶.**
|
||||
|
|
@ -810,7 +811,7 @@ fail-fast 톤으로 그 자리에서 막음 — `keyFn` 작성자(주로 위 `it
|
|||
Slot이 대신 안 해주는가" 절의 예시 참고.
|
||||
- **`offset: Source<number>`** — 이 `Slot` 자신의 `Slot.Offset`을 그대로
|
||||
전달(모든 key가 같은 값을 공유) — 형제로 섞인 다른 Slot/정적 자식이
|
||||
기여한 개수의 누적합(`base/bind-system-plan.md`의 "Length/Offset" 절
|
||||
기여한 개수의 누적합(`base/dispatch-core-plan.md`의 "Length/Offset" 절
|
||||
참고). `index`와 마찬가지로 실제 프로퍼티에 어떻게 반영할지는
|
||||
전적으로 `updateFn` 몫 — `Slot`/Handler가 자동으로 해주지 않음
|
||||
(2026-08-11 세션 확정, 아래 참고).
|
||||
|
|
@ -1294,7 +1295,7 @@ GC에 위임" 원칙 그대로.
|
|||
위 "CRUD API 확정"/"`isMounted` 이중 추적 분리"/"`Slot:List`" 절 참고.
|
||||
- **[해소됨, 2026-08-09 여섯 번째 세션]** 여러 Slot이 형제로 섞일 때
|
||||
순서 보장 — 위 "여러 Slot이 섞일 때 순서 보장" 절 참고, 메커니즘은
|
||||
`base/bind-system-plan.md`의 "Length/Offset" 절이 최신 소스.
|
||||
`base/dispatch-core-plan.md`의 "Length/Offset" 절이 최신 소스.
|
||||
|
||||
## Slot.Length — `:List`뿐 아니라 항상 노출됨 (2026-08-09 여섯 번째 세션)
|
||||
|
||||
|
|
@ -1382,7 +1383,7 @@ nil/None 금지)는 그대로.
|
|||
|
||||
### 재귀 메커니즘 — 새 프리미티브 없이 `Dispatch.setLength`/`setOffsetSource`를 Slot 자신 키로 재사용
|
||||
|
||||
`base/bind-system-plan.md`의 "Length/Offset" 절이 이미 확정해둔 두 함수는
|
||||
`base/dispatch-core-plan.md`의 "Length/Offset" 절이 이미 확정해둔 두 함수는
|
||||
owner 키(`inst`)가 물리 Instance일 필요가 없음(`Relate`가 아무 테이블이나
|
||||
weak 키로 받음) — **Slot 자신을 owner 키로 재사용하면 최상위 마운트와
|
||||
중첩 마운트가 완전히 같은 함수 호출**이 됩니다.
|
||||
|
|
@ -1844,7 +1845,7 @@ Slot이 마운트될 때 **자기 하위 요소들까지 `bindLifetime`으로
|
|||
|
||||
- `Dispatch.setLength`는 끝에서 `recompute`를 돌리고, `recompute`는
|
||||
`sourceList`를 순회하며 각 자리의 `offset:Set(sum)`을 호출함
|
||||
(`base/bind-system-plan.md` "Length/Offset" 절).
|
||||
(`base/dispatch-core-plan.md` "Length/Offset" 절).
|
||||
- 그래서 **`setLength(0)`을 먼저 부르면**, 그 안의 `recompute`가 도는
|
||||
시점에 해제 중인 자리의 `sourceList[i]`엔 **아직 옛 Slot의 offset
|
||||
`Source`가 그대로 남아 있음** → 지금 막 떼어내는 서브트리의 Source에
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ pull-recompute)·`:Compute` 인자 규칙·State 쓰기 금지·`Source` 독립
|
|||
**세 번째 카테고리 — Handler는 둘 중 어디에도 안 낌(2026-08-08 두 번째
|
||||
세션, 명시화).** `Handler`(`isHandlable`/`priority`/`process` 3종 계약 —
|
||||
`process`가 자기 retract 클로저를 반환, 2026-08-13 다섯 번째 세션 정정,
|
||||
`base/bind-system-plan.md` "핸들러 계약" 절)는 위 분류가 다루는
|
||||
`base/dispatch-core-plan.md` "핸들러 계약" 절)는 위 분류가 다루는
|
||||
"quad 사용자가 직접 다루는 리액티브 값"이 아니라 **그 자체로는 구현체가
|
||||
없는 순수 타입 계약**이라 애초에 이 분류표의 대상이 아님 — Source/Ref처럼
|
||||
`Type(args)` 자유 함수로 인스턴스를 만들 수도 없고(계약을 만족하는 값은
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
# Tag — array-part 값 객체, `CollectionService` 얇은 래퍼
|
||||
# Tag — array-part 값 객체, 참조 카운트는 base / 엔진 호출은 주입 op
|
||||
|
||||
> **⚠️ [2026-08-13 여섯 번째 세션] 이 문서의 `hintValue`/`retractFrom` 선행
|
||||
> 호출 서술은 곧 교체될 예정 — 아직 반영 안 됨.** 힌트가 `None` 센티널이나
|
||||
> `State`/`Tween` 래퍼로 오염돼 말단 핸들러의 `isX(hint)` 가드를 거짓으로
|
||||
> 만들고 깜빡임/재생성 방지를 조용히 끄는 결함이 확인됐고, 대체 모델
|
||||
> (**래핑 핸들러의 `retractFrom` 선행 호출 폐기 + `Dispatch.process`가
|
||||
> 핸들러를 먼저 비교**)까지 거의 확정됐음 — 다만 `question.md` **0-Z**
|
||||
> (Attribute 이름 소유권) 하나가 남아 아직 옮기지 않음. **여기 적힌 대로
|
||||
> 구현하면 옛 모델로 짜게 됨** — 반드시
|
||||
> `research/dispatch-redispatch-diff-plan.md`를 먼저 읽을 것.
|
||||
> **✅ [2026-08-13 열네 번째 세션] 하강 diff 재디스패치 반영 + 패키지
|
||||
> 재배치 완료.** 이전 ⚠️ 배너가 예고하던 교체가 끝났음: (1) 클로저가 받는
|
||||
> 값의 **타입이 계약으로 보장**되어 `isTag(hintValue)` 방어 가드가
|
||||
> 불필요해졌고 깜빡임 방지가 **깊은 체인에서도 유지**됨
|
||||
> (`base/dispatch-core-plan.md` "Dispatch 체인" 절), (2) **참조 카운트
|
||||
> 알고리즘 전체가 quad-base 소속**이 되고 `addTag`/`removeTag` op만
|
||||
> 백엔드가 주입(아래 "패키지 배치" 절). 옛 모델 원문은
|
||||
> `archive/dispatch-hintvalue-model-reversed.md`.
|
||||
|
||||
**상태**: base — 값 모양/이름은 확정. 2026-08-08 세 번째 세션에서 값
|
||||
모양을 전면 재설계(구 모델은 `archive/tag-hash-key-model-reversed.md`에
|
||||
|
|
@ -16,9 +15,9 @@
|
|||
`process`/`retract` 메커니즘을 참조 카운트 기반으로 전면 정정(옛 버전은
|
||||
`archive/retract-always-fires-reversed.md`). 2026-08-12 열다섯 번째
|
||||
세션에 `Added`/`Removed`를 단일 이름에서 `string | {string}`으로
|
||||
정정(아래 값 모양 절). **단, 위 배너대로 `hintValue`/`retractFrom` 재-dispatch
|
||||
메커니즘은 `question.md` 0-Z 해소 대기 중 — "열린 질문 없음"은 값
|
||||
모양/이름에 한정.**
|
||||
정정(아래 값 모양 절). **[2026-08-13 열네 번째 세션] 재디스패치 모델
|
||||
(0-A)과 패키지 배치까지 반영 완료 — 이 문서에 남은 열린 질문은 이름
|
||||
자체(용어 정리 대기열)뿐.**
|
||||
|
||||
## 왜 재설계됐나
|
||||
|
||||
|
|
@ -83,7 +82,7 @@ plain string이라(핸들러 계층 값처럼 identity/생명주기가 얽힌
|
|||
`store.activeTag:Compute(function(name) return (if name:Get() == "btn1"
|
||||
then Tag("selected") else nil) end)`처럼 그냥 `nil`을 리턴하면 됨. `None`
|
||||
센티널은 "정적 테이블 리터럴에서 `키 = nil`이 키 없음과 구별 안 되는"
|
||||
문제의 해법이지(`bind-system-plan.md` "`None` 센티널" 절), 이건 함수
|
||||
문제의 해법이지(`dispatch-core-plan.md` "`None` 센티널" 절), 이건 함수
|
||||
리턴값이 동적으로 흘러가는 경우라 그 문제 자체가 없음 — `nil`을 인자로
|
||||
넘기는 건 아무 문제 없음. (단, `Frame { (if cond then Tag("a") else
|
||||
nil), sibling }`처럼 **정적 리터럴**에서 조건부로 Tag를 넣거나 빼고
|
||||
|
|
@ -106,13 +105,13 @@ nil`/`or None`(and/or 삼항)으로 적었으나, `Tag(...)`가 항상-truthy라
|
|||
`assert(v==nil)`, "Tag(A)→Tag(B)는 retract 안 불림")을 대체함 — 그 버전은
|
||||
두 가지를 놓쳤음:**
|
||||
|
||||
1. **`retract`는 실제로 store 재발행마다(핸들러 타입이 안 바뀌어도) 항상
|
||||
불림** — `bind-system-plan.md`의 "확정된 디스패치 모델" 절이 처음부터
|
||||
말해온 대로 `StoreBind`가 재-dispatch 전에 무조건
|
||||
`Dispatch.retractFrom`(2026-08-13 다섯 번째 세션 전까지의 이름은
|
||||
`retractUnder`)을 부르기 때문. "Tag(A)→Tag(B)는 retract 안 불림"이라는 옛 서술은 틀렸음
|
||||
(상세 근거는 `bind-system-plan.md` 일반 retract 계약 절, `archive/
|
||||
retract-always-fires-reversed.md`).
|
||||
1. **반환하는 클로저는 store 재발행마다(핸들러 타입이 안 바뀌어도) 항상
|
||||
불림** — "Tag(A)→Tag(B)는 retract 안 불림"이라는 옛 서술은 틀렸음
|
||||
(`archive/retract-always-fires-reversed.md`). **[갱신, 2026-08-13
|
||||
열네 번째 세션]** 부르는 주체는 이제 `StoreBind`의 선행 `retractFrom`이
|
||||
아니라 `Dispatch.process` 자신임 — 같은 핸들러면 그 자리 클로저에 새
|
||||
값을 넘기고 곧바로 `process`를 다시 부름(`dispatch-core-plan.md`
|
||||
"Dispatch 체인" 절 (A) 분기). 호출 빈도는 그대로.
|
||||
2. **서로 다른 배열 위치의 두 `Tag(...)`가 같은 이름을 겹쳐 가질 수
|
||||
있음**(`Frame { Tag("a"), Tag("a","b") }`류, 웹 `className="a a a"`와
|
||||
같은 합집합 시맨틱) — 한 위치의 diff만 보고 `RemoveTag`를 부르면 다른
|
||||
|
|
@ -145,7 +144,7 @@ identity로 홀더를 추적하면 같은 객체를 두 위치(`k1`, `k2`)에
|
|||
불필요해짐** — `retract`가 필요로 했던 "이 위치에 걸려 있던 Tag가
|
||||
뭐였는가"는 이제 그 `process` 호출이 반환하는 클로저가 `v`를 upvalue로
|
||||
직접 캡처하므로, 별도 저장소에서 다시 조회할 이유가 없음(위
|
||||
`base/bind-system-plan.md` "핸들러 내부 상태 저장" 절 — 단발성 handoff는
|
||||
`base/dispatch-core-plan.md` "핸들러 내부 상태 저장" 절 — 단발성 handoff는
|
||||
클로저로 충분). **`tagNameMap`(이름별 현재 걸고 있는 위치 집합)은 여전히
|
||||
필요** — 이건 서로 다른 여러 위치를 가로지르는, `process`/클로저 하나의
|
||||
호출 수명을 넘어서는 누적 상태라 `Relate`가 맞는 경우:
|
||||
|
|
@ -164,86 +163,109 @@ function TagHandler.process(inst, k, v, index)
|
|||
tagNameMap:SetStrong(inst, name, holders)
|
||||
end
|
||||
if next(holders) == nil then
|
||||
inst:AddTag(name)
|
||||
addTag(inst, { name }) -- 주입된 엔진 op(아래 "패키지 배치" 절)
|
||||
end
|
||||
holders[k] = true -- 이 "위치"가 이 이름을 걺(Tag 객체가 다른 위치와 같아도 무관)
|
||||
end
|
||||
return function(hintValue)
|
||||
if v == hintValue then return end -- Tag는 immutable이라 객체가 안 바뀌면
|
||||
-- 이름 집합도 절대 안 바뀜 — holders 순회 자체가 불필요한 순수 최적화
|
||||
local hintIsTag = isTag(hintValue) -- hintValue는 nil일 수도, 대체하는 새 Tag 자체일 수도 있음
|
||||
return function(nextValue)
|
||||
-- nextValue는 nil(단순 철거)이거나 같은 핸들러가 곧 처리할 새 Tag —
|
||||
-- 타입이 계약으로 보장되므로 isTag 가드가 필요 없음(2026-08-13 열네 번째 세션).
|
||||
if v == nextValue then return end -- Tag는 immutable이라 객체가 안 바뀌면
|
||||
-- 이름 집합도 절대 안 바뀜 — holders 순회 자체가 불필요한 순수 최적화
|
||||
local removed = {}
|
||||
for name in v:Names() do
|
||||
local holders = tagNameMap:GetStrong(inst, name) -- 이미 등록됐으므로 항상 있음
|
||||
holders[k] = nil -- 이 "위치"가 이 이름을 놓음(같은 Tag 객체를 다른 위치도 쓰고 있어도 무관)
|
||||
if next(holders) == nil and not (hintIsTag and hintValue:Contains(name)) then
|
||||
inst:RemoveTag(name) -- 곧 새 process가 재확정할 이름이면 실제 호출은 skip(깜빡임 방지)
|
||||
if next(holders) == nil and not (nextValue ~= nil and nextValue:Contains(name)) then
|
||||
table.insert(removed, name) -- 곧 새 process가 재확정할 이름이면 skip(깜빡임 방지)
|
||||
end
|
||||
end
|
||||
if #removed > 0 then
|
||||
removeTag(inst, removed) -- 한 번에 — 웹 className 일괄 갱신을 위한 배치 계약
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
- **`AddTag`는 온전히 `process`, `RemoveTag`는 온전히 반환하는 클로저**
|
||||
- **`addTag`는 온전히 `process`, `removeTag`는 온전히 반환하는 클로저**
|
||||
— 서로 겹치는 diff 계산이 없음. 클로저가 이전 `Tag`(`v`, 자기 자신이
|
||||
캡처)가 걸었던 이름 전부를 소유 목록에서 빼되(항상 실행), 그 결과
|
||||
목록이 비었을 때 **실제 `RemoveTag` 호출만** "새로 들어올 `hintValue`가
|
||||
그 이름을 여전히 Contains하는가"로 힌트를 줘서 skip — 소유 목록 자체는
|
||||
목록이 비었을 때 **실제 `removeTag` 호출만** "새로 들어올 값이
|
||||
그 이름을 여전히 Contains하는가"로 판단해 skip — 소유 목록 자체는
|
||||
항상 최신 객체로 갱신되므로(정확히 `v`를 빼고 새 `process`가 새 값을
|
||||
넣는 두 단계), 이름이 살아남는 경우에도 stale 레퍼런스가 안 남음.
|
||||
`process`는 `v`가 새로 거는 이름 전부를 무조건 등록(소유 목록이
|
||||
비어있던 경우에만 실제 `AddTag`) — 자기 나름의 old-vs-new diff가 전혀
|
||||
비어있던 경우에만 실제 `addTag`) — 자기 나름의 old-vs-new diff가 전혀
|
||||
필요 없음(그 일을 클로저가 매번 정확히 해줌).
|
||||
- **`Tag(A)→Tag(B)`(같은 위치, 내용만 바뀜)**: `A`를 처리했던 `process`가
|
||||
반환한 클로저가 `hintValue=B`로 먼저 불려 `A`가 걸었던 이름 중 `B`에
|
||||
없는 것만 실제로 `RemoveTag`, 남은 건 힌트로 skip — 그 다음
|
||||
반환한 클로저가 `nextValue=B`로 먼저 불려 `A`가 걸었던 이름 중 `B`에
|
||||
없는 것만 실제로 `removeTag`, 남은 건 skip — 그 다음
|
||||
`process(inst,k,B,index)`가 `B`의 이름 전부를 등록(이미 걸려있던
|
||||
이름은 `AddTag`가 no-op으로 재확인만 됨, 소유 목록엔 `B`가 새로 등록).
|
||||
결과적으로 실제 `RemoveTag`/`AddTag` 호출은 진짜 변경된 이름에만
|
||||
이름은 소유 목록이 안 비어 있어 `addTag` 자체가 안 불림, 소유 목록엔
|
||||
`B`의 위치가 그대로 유지).
|
||||
결과적으로 실제 `removeTag`/`addTag` 호출은 진짜 변경된 이름에만
|
||||
일어남 — 스타일 깜빡임 방지라는 원래 목적은 그대로 달성.
|
||||
**[범위 한정, 2026-08-13 감사] 이 깜빡임 방지는 "이 Tag를 바로 위에서
|
||||
직속으로 위임한 한 단계"에서만 성립함** — `Dispatch.retractFrom(inst,
|
||||
k, index, v)`는 힌트 `v`를 정확히 `index` 자리에만 넘기고 그보다 깊은
|
||||
인덱스에는 `nil`을 넘기기 때문(`bind-system-plan.md` "Dispatch 체인"
|
||||
절의 `retractFrom` 의사코드). 그래서 `State<State<Tag>>`에서 **바깥**
|
||||
store가 재발행하면 TagHandler(더 깊은 인덱스)는 `hintValue=nil`을
|
||||
받아 이름 전부를 실제로 `RemoveTag`했다가 새 체인이 다시 `AddTag`함.
|
||||
구조상 불가피함(바깥 단계는 안쪽이 결국 어떤 Tag를 내놓을지 모름)
|
||||
이고, 흔한 경로(`State<Tag>` 한 겹)는 영향 없음 — 실사용에서 문제가
|
||||
되면 그때 힌트를 깊은 인덱스까지 전파하는 안을 재검토.
|
||||
- **`Tag(A)→nil`**: `A`의 클로저가 `hintValue=nil`로만 불림(값이 `Tag`가
|
||||
아니게 돼 `process`는 매치 자체가 안 됨) — `hintIsTag=false`라 힌트가
|
||||
항상 거짓이 되어 `A`가 걸었던 이름 전부가 무조건 실제로 `RemoveTag`됨
|
||||
**[범위 확대, 2026-08-13 열네 번째 세션] 이 깜빡임 방지는 이제 깊은
|
||||
체인에서도 성립함** — 옛 모델에선 힌트가 `retractFrom`이 지목한 한
|
||||
자리에만 전달돼서 `State<State<Tag>>`의 바깥이 재발행하면 TagHandler가
|
||||
`nil`을 받아 전량 `RemoveTag` 후 재`AddTag`했으나, 하강 diff에선 **각
|
||||
레벨이 자기 재프로세스에서 자기 값을 받으므로** 인덱스가 얼마나 깊든
|
||||
TagHandler는 진짜 `Tag` 객체를 받음(`dispatch-core-plan.md` "Dispatch
|
||||
체인" 절의 "깊은 체인에서도 힌트가 안 사라짐").
|
||||
- **`Tag(A)→nil`**: `A`의 클로저가 `nextValue=nil`로만 불림(값이 `Tag`가
|
||||
아니게 돼 핸들러가 바뀌므로 `Dispatch.retractFrom` 경로) — `Contains`
|
||||
검사가 항상 거짓이 되어 `A`가 걸었던 이름 전부가 무조건 실제로 `removeTag`됨
|
||||
(다른 위치가 그 이름을 계속 쓰고 있지 않다면).
|
||||
- **여러 위치가 같은 이름을 겹쳐 가지는 경우**(`Frame { Tag("a"), Tag("a","b") }`):
|
||||
두 위치가 서로 다른 `k`로 각자 독립적으로 `process`/자기 클로저를
|
||||
타지만, `tagNameMap["a"]`는 **양쪽 위치(`k`)를 모두 담는 하나의 공유
|
||||
집합** — 한쪽이 "a"를 잃어도 다른 쪽 위치가 집합에 남아있으면 실제
|
||||
`RemoveTag`가 안 불림. 웹 `className`처럼 손실 없는 합집합이 정확히
|
||||
`removeTag`가 안 불림. 웹 `className`처럼 손실 없는 합집합이 정확히
|
||||
나옴. **위치 기준이므로 두 위치가 물리적으로 같은 `Tag` 객체를
|
||||
재사용해도(흔한 관례) 정확히 같은 방식으로 안전 — 이게 바로 위 "정정"
|
||||
절에서 객체 identity 기준을 버린 이유.**
|
||||
- **클로저가 자기 위임 대상까지 수동으로 안 쫓아가도 됨** —
|
||||
`Dispatch.retractFrom`이 체인 전체를 알아서 훑어주므로 TagHandler는
|
||||
자기 자원(위 `tagNameMap` 하나)만 정리하면 됨. 상세 메커니즘은
|
||||
`bind-system-plan.md` "Dispatch 체인" 절.
|
||||
`dispatch-core-plan.md` "Dispatch 체인" 절.
|
||||
|
||||
## 패키지 배치 — base는 값+API, roblox는 process 글루
|
||||
## 패키지 배치 — 값도 알고리즘도 quad-base, 주입되는 건 `addTag`/`removeTag` (2026-08-13 열네 번째 세션 재배치)
|
||||
|
||||
**Tag의 "값 타입과 clone 체이닝 API"(`Tag(...)`/`:Added`/`:Removed`/
|
||||
`:Contains`/`:Apply`/`Merged`)는 quad-base 소속** — `Modifier`와 정확히
|
||||
같은 층위(엔진 무관, 순수 데이터+연산). `CollectionService` 실제 호출
|
||||
(`TagHandler.process` 및 그 반환 클로저)만 quad-roblox 소속 — 이미
|
||||
확정된 "base는 인터페이스/값, backend는 process 글루" 패턴(`LifetimeHandle`,
|
||||
`Dispatch.addHandler` 자체가 이 패턴)을 값 타입 수준까지 그대로 확장한
|
||||
것뿐, 새 아키텍처 개념 아님.
|
||||
**Tag의 값 타입/clone 체이닝 API(`Tag(...)`/`:Added`/`:Removed`/
|
||||
`:Contains`/`:Apply`/`Merged`/`:Names`)가 quad-base인 건 처음부터 그대로**
|
||||
(`Modifier`와 같은 층위 — 엔진 무관, 순수 데이터+연산). **[재배치,
|
||||
2026-08-13 열네 번째 세션] 여기에 더해 `TagHandler`(위 참조 카운트
|
||||
알고리즘 전체)도 quad-base로 옮김** — 예전엔 `CollectionService` 실제
|
||||
호출이 있다는 이유로 핸들러 통째로 quad-roblox였으나, 엔진에 종속된 건
|
||||
`AddTag`/`RemoveTag` 두 줄뿐이고 `tagNameMap` 참조 카운트는 순수 부기임.
|
||||
웹에도 대응물이 있으므로(`className` 합집합) 그 배치대로면 **같은 참조
|
||||
카운트 알고리즘을 백엔드마다 재구현**하게 됨.
|
||||
|
||||
```lua
|
||||
addTag(inst: any, names: {string}): () -- 백엔드가 주입
|
||||
removeTag(inst: any, names: {string}): ()
|
||||
```
|
||||
|
||||
- **`{string}`을 받는 이유**: 호출자는 항상 quad 자신이고 넘기는 것도
|
||||
"이번 사이클에 실제로 추가/제거된 이름 집합"이라 테이블이 자연 단위임.
|
||||
vararg면 `table.unpack(t)`가 **인자 목록 tail 위치일 때만** 완전히
|
||||
펼쳐진다는 Lua 문법 제약에 걸리는데, 이건 이미 `Tag:Added`가
|
||||
vararg → `string | {string}`으로 되돌아갔던 것과 **같은 이유**(위 "값
|
||||
모양" 절). 배치 호출 자체는 테이블로도 되므로 웹 `className` 일괄
|
||||
갱신 요구도 그대로 충족됨.
|
||||
- **등록 우선순위는 `HANDLER_PRIORITY_FALLBACK`** — 특정 백엔드가 태그
|
||||
처리를 통째로 다르게 하고 싶으면 평범한 우선순위로 자기 핸들러를
|
||||
등록하면 자동으로 이김. 주입 op이 없는 백엔드에서는 base 스텁이
|
||||
명확한 에러를 냄. 일반 원칙과 근거는 `base/dispatch-core-plan.md`의
|
||||
"base가 소유하는 핸들러와 주입되는 엔진 op" 절 — `Attribute`도 정확히
|
||||
같은 구조(`base/attribute-plan.md`).
|
||||
- 이건 새 아키텍처 개념이 아니라 이미 확정된 "base는 인터페이스/값,
|
||||
backend는 구현"(`LifetimeHandle`의 `bindLifetime`/`canExecute`,
|
||||
`Dispatch.addHandler` 자체가 그 패턴)을 핸들러 층까지 밀어붙인 것.
|
||||
|
||||
## 열린 질문
|
||||
|
||||
값 모양/이름(`Tag`/`Added`/`Removed`/`Merged`, 용어 정리 대상,
|
||||
`.claude/question.md`) 자체는 없음. **단, 이 문서 최상단 배너가 이미
|
||||
경고하듯 `hintValue`/`retractFrom` 선행 호출 메커니즘은 `question.md`
|
||||
**0-Z**(Attribute 이름 소유권) 해소 대기 중인 "하강 diff" 재디스패치
|
||||
모델로 교체 예정** — `research/dispatch-redispatch-diff-plan.md` 6절이
|
||||
이 파일을 반영 대상으로 명시 지목(`isTag(hintValue)` 가드 제거 등).
|
||||
이 절이 예전엔 "없음"으로만 적혀 있었던 건 그 배너가 붙기 전 작성된
|
||||
서술이 안 갱신된 stale — 재발 방지용으로 여기 명시.
|
||||
값 모양/메커니즘/패키지 배치엔 **[2026-08-13 열네 번째 세션 기준] 없음** —
|
||||
재디스패치 모델(`question.md` 0-A)과 패키지 재배치까지 전부 반영 완료.
|
||||
남은 건 이름 자체(`Tag`/`Added`/`Removed`/`Merged`)가 용어 정리
|
||||
대기열(`.claude/question.md` 1번)에 있다는 것뿐.
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ base가 범용 유틸로 제공하기로 확정한 per-instance weak-keyed 저
|
|||
"핸들러 내부 상태 저장" 절)를 그대로 재사용하면 됨 — PropertyHandler가 실행 중인
|
||||
Tween 상태를 기억해두는 것과 정확히 같은 패턴. 새 메커니즘 발명 불필요, 이미
|
||||
있는 "store 바인드는 pluggable 바인드를 재실행하는 래핑" 원칙
|
||||
(`base/bind-system-plan.md` "확정된 디스패치 모델" 절)이 그대로 적용됨.
|
||||
(`base/dispatch-core-plan.md` "확정된 디스패치 모델" 절)이 그대로 적용됨.
|
||||
|
||||
## 패키지 배치 — `quad-roblox` 코어에 직접 포함, 확정
|
||||
|
||||
|
|
|
|||
|
|
@ -67,10 +67,10 @@ ROADMAP 항목 근거인지, 어떻게 실행하는지, 실행 후 뭘 확인해
|
|||
|
||||
| 파일 | 검증 대상 | 근거 문서 |
|
||||
|---|---|---|
|
||||
| `01-two-pass-array-hash-order.luau` | 배열 파트(children/Ref) 먼저, 해시 파트(프로퍼티/이벤트) 나중이라는 두 패스 순회 계약 | `bind-system-plan.md` "props 순회 순서", ROADMAP M0-4 |
|
||||
| `01-two-pass-array-hash-order.luau` | 배열 파트(children/Ref) 먼저, 해시 파트(프로퍼티/이벤트) 나중이라는 두 패스 순회 계약 | `dispatch-core-plan.md` "props 순회 순서", ROADMAP M0-4 |
|
||||
| `02-none-sentinel-vs-nil-holes.luau` | **[2026-08-09 커밋 f198fd9 반영해 전면 재작성]** 순서가 중요한 배열(PreRef pre-pass, sourceList)은 `None` 소진이 맞고, 순서가 안 중요하고 재사용이 필요한 배열(Ref 콜백/대기자)은 `nil`+슬롯 재사용이 맞다는 최종 구분 + `None`을 잘못 쓰면 배열이 무한정 자라는 버그의 정량적 재현 | `bind-system-plan.md` "왜 None이 아니라 nil인가"(2026-08-09 열한 번째 세션 최종 정정), ROADMAP M0-4 |
|
||||
| `03-recursive-store-bind-dispatch.luau` | `process`/`retract` 재귀 재-dispatch 기본 모델, 우선순위 스캔 | `bind-system-plan.md` "확정된 디스패치 모델", ROADMAP M0-3 |
|
||||
| `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 감사 |
|
||||
| `03-recursive-store-bind-dispatch.luau` | `process`/`retract` 재귀 재-dispatch 기본 모델, 우선순위 스캔 | `dispatch-core-plan.md` "확정된 디스패치 모델", ROADMAP M0-3 |
|
||||
| `04-dispatch-chain-retractFrom.luau` | **[⚠️ 2026-08-13 열네 번째 세션: 하강 diff 확정으로 낡음 → `rewrite-required/`]** 아래는 옛 모델 기준 설명 — **[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" 가드를 검증했는데 그 가드는 다섯 번째 세션 재설계로 **없어져서** 설계와 정반대를 테스트하고 있었음 | `dispatch-core-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 착수 시 실측 확인" **[2026-08-13 보강]** 4번 섹션 신설 — `_countEntries()`(테스트 전용) + weak-value canary로 **"inst가 죽으면 중첩 StrongMap 안의 payload까지 연쇄 GC되는가"를 직접 검증**(원래는 sanity check만 하고 헤더의 핵심 주장은 미검증이었음). 파일이 스스로 적어둔 "weak table 엔트리를 셀 표준 API가 없다"는 전제도 틀렸음 — outer가 `__mode="k"`라 GC 후 `pairs`에서 사라짐 |
|
||||
|
|
@ -85,7 +85,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 신규, 같은 날 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` "요소 소유권" |
|
||||
| `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}`을 조용히 통과시키는 걸 재현. **[2026-08-13 열네 번째 세션] 0-Z가 확정되며 B 섹션이 낡음 → `rewrite-required/`** — 이제 "그룹 전용 키 + `AttributeKeyHandler`의 이름 claim"을 검증해야 함(A/C는 그대로 유효) | `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 여섯 번째 세션) |
|
||||
|
||||
## 공통 유틸리티
|
||||
|
|
@ -179,7 +179,7 @@ Luau로 재현해본 적이 없었던 갭 — `Slot`의 `kSlotMap`/`slotOwner` G
|
|||
`retractUnder`의 꼬리부터-cutoff 로직을 직접 되짚다가 "같은 키에서 핸들러가
|
||||
재사용되면 문제 아닌가"를 제기 — 손으로 트레이싱해 `State<State<T>>`(store가
|
||||
emit하는 값 자체가 또 State/Source)가 실제로 체인을 파손시킴을 확인
|
||||
(`bind-system-plan.md` "확정된 디스패치 모델" 절 신규 항목). 기존 `04`가
|
||||
(`dispatch-core-plan.md` "확정된 디스패치 모델" 절 신규 항목). 기존 `04`가
|
||||
정확히 이 시나리오(store-in-store)를 이미 스트레스 테스트로 다루고 있었지만,
|
||||
`retract`가 print만 하는 no-op 스텁이라 자기-retract 버그의 실제 증상(구독이
|
||||
조용히 끊기는 것)을 절대 드러낼 수 없었다는 사각지대도 같이 발견 —
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
# 스파이크 상태판 — **폴더가 곧 상태**
|
||||
|
||||
> 마지막 갱신: 2026-08-13 열세 번째 세션(`08` 해소 → `done/`, `review-required/` 비워짐).
|
||||
> 마지막 갱신: 2026-08-13 **열네 번째 세션**(하강 diff 재디스패치 확정으로
|
||||
> `04`/`19`가 옛 모델을 검증하고 있어 `rewrite-required/`로 이동).
|
||||
> 첫 실측은 여섯 번째 세션 — 상세 결과는 `.claude/audit/luau-test-first-run-2026-08-13.md`.
|
||||
> 실행법: `luau <파일>` (런타임) / `luau-analyze <파일>` (타입 전용).
|
||||
|
||||
|
|
@ -11,9 +12,9 @@
|
|||
| 폴더 | 뜻 | 개수 | 누가 처리 |
|
||||
|---|---|---|---|
|
||||
| `review-required/` | **설계가 걸림 — 사람 결정 필요** | **0** | ⭐ 사용자 |
|
||||
| `rewrite-required/` | 스파이크 코드가 깨짐(설계 문제 **아님**) | 3 | 에이전트 |
|
||||
| `rewrite-required/` | 스파이크가 낡음(코드가 깨졌거나, 설계가 바뀌어 옛 모델을 검증 중) | 5 | 에이전트 |
|
||||
| `not-run/` | 이 환경에서 못 돌림(Studio 전용) | 1(+헬퍼 1) | 사용자 or MCP 연결 후 에이전트 |
|
||||
| `done/` | 통과 or 판정 끝, 더 할 일 없음 | 16 | — |
|
||||
| `done/` | 통과 or 판정 끝, 더 할 일 없음 | 14 | — |
|
||||
|
||||
**폴더를 옮기는 게 곧 상태 갱신** — 스파이크를 고치거나 돌렸으면 파일을
|
||||
해당 폴더로 `git mv`하고 아래 표의 줄도 같이 옮길 것. 파일별 "무엇을 왜
|
||||
|
|
@ -37,10 +38,19 @@
|
|||
`rewrite-required/`에 그대로 둠 — 재작성 대상이지 사람 결정 대상이
|
||||
아님(계약 자체는 위에서 이미 확정됨).
|
||||
|
||||
## 🟠 `rewrite-required/` — 스파이크가 깨짐, 설계 문제 아님 (3건)
|
||||
## 🟠 `rewrite-required/` — 스파이크가 낡음 (5건)
|
||||
|
||||
**[2026-08-13 열네 번째 세션] 앞의 두 건은 "코드가 깨진" 게 아니라 "설계가
|
||||
바뀐" 경우** — `question.md` 0-A/0-Z 확정으로 재디스패치가 **하강 diff**가
|
||||
되면서, 이 둘이 검증하던 전제(선행 `retractFrom` + 4-인자 힌트 + 인덱스
|
||||
점유 체크)가 더 이상 설계가 아님. 통과 상태로 `done/`에 두면 **옛 모델을
|
||||
"검증됨"으로 오독하게 되므로** 옮김. 새 정본은
|
||||
`base/dispatch-core-plan.md`/`base/attribute-plan.md`.
|
||||
|
||||
| 파일 | 상태 | 무엇을 고쳐야 하나 |
|
||||
|---|---|---|
|
||||
| `04-dispatch-chain-retractFrom.luau` | 옛 모델 기준으로는 ✅ 통과였음 | (1) `chains` 슬롯이 `{handler, retractor}`가 되고 `Dispatch.process`가 핸들러를 먼저 비교하는 **하강 diff**로 재작성, (2) `retractFrom`은 **3-인자**(힌트 인자 없음), (3) "힌트가 target 인덱스에만 간다"를 검증하던 부분은 **정반대**로 뒤집힘 — 이제 각 레벨이 자기 값을 받는지를 검증해야 함. **살릴 것**: `chains:SetStrong` 순서 음성 대조군(그 버그는 새 모델에서도 그대로 유효) |
|
||||
| `19-ownership-refcount-relate-patterns.luau` | A/C ✅ 유효, **B 섹션이 낡음** | B가 검증하던 "공개 `AttributeKey(name)` + 인덱스 1 점유 체크"가 폐기됨 — **그룹 전용 키 + `AttributeKeyHandler`의 이름 claim**으로 재작성하고, 음성 대조군도 "두 그룹이 같은 이름 → 즉시 error", "그룹↔직접 쓰기 → 즉시 error"로 바꿀 것(0-Z 확정 내용). A/C는 손댈 것 없음 |
|
||||
| `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 재확인 후 재시도 |
|
||||
|
|
@ -52,23 +62,23 @@
|
|||
| `10-roblox-studio-checks.server.luau` | **Studio 전용**(`luau` CLI로 못 돌림). A 섹션 앞부분만 사용자 자작 스크립트로 실측 — `audit/gcconn-trick-verification.md`. **A-1/A-2(`canBound` 게이트)/B/C는 여전히 미확인** |
|
||||
| `gc-trigger-helper.server.luau` | 스파이크가 아니라 **헬퍼** — Studio에 `collectgarbage()`가 없어서 GC를 강제 트리거하는 기법. `10`을 돌릴 때 같이 씀 |
|
||||
|
||||
## ✅ `done/` — 통과 or 판정 끝 (16건)
|
||||
## ✅ `done/` — 통과 or 판정 끝 (14건)
|
||||
|
||||
**런타임 12개 전원 통과**(crash 0 / FAIL 0):
|
||||
**런타임 12개 전원 통과**(crash 0 / FAIL 0) — **[열네 번째 세션] 그중
|
||||
`04`/`19`는 검증 대상 설계가 바뀌어 위 `rewrite-required/`로 이동했고,
|
||||
아래 표엔 남은 10개만 있음**(실측 당시 통과였다는 사실 자체는 유효):
|
||||
|
||||
| 파일 | 확인된 것 |
|
||||
|---|---|
|
||||
| `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 아키텍처의 핵심 전제 |
|
||||
| `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` — **음성 대조군 포함** 전원 통과. ⚠️ **B 섹션은 `question.md` 0-Z가 정해지면 다시 손봐야 함**(하강 diff 모델에선 그룹↔그룹을 점유 체크만으론 못 잡음) |
|
||||
| `20-slot-splice-index-arithmetic` | `Splice` 산술 11개 경계 케이스 전부 참조 구현과 일치 |
|
||||
|
||||
**타입 스파이크 중 판정이 끝나 더 할 일 없는 것**:
|
||||
|
|
@ -82,7 +92,8 @@
|
|||
|
||||
### 특별히 중요한 통과 3건
|
||||
|
||||
**`04` — 직전 감사가 찾은 버그가 음성 대조군으로 재현됨**
|
||||
**`04` — 직전 감사가 찾은 버그가 음성 대조군으로 재현됨**(파일은 지금
|
||||
`rewrite-required/`에 있음 — 아래 관측 자체는 새 모델에서도 유효)
|
||||
|
||||
| 관측 지점 | 정상(수정본) | 대조군(버그) |
|
||||
|---|---|---|
|
||||
|
|
|
|||
|
|
@ -12,68 +12,25 @@
|
|||
|
||||
---
|
||||
|
||||
## ⭐ 최우선 — M0 착수를 막고 있음 (1건)
|
||||
## ⭐ 최우선 — **없음** (2026-08-13 열네 번째 세션 기준)
|
||||
|
||||
이 항목은 사용자가 **직접 스케치하며 판단하겠다고 명시 이관**한 것이라
|
||||
에이전트가 기본값으로 밀고 갈 수 없음. 루트 `HUMAN_TODO.md` 4번에도 있음.
|
||||
|
||||
> **[2026-08-13 열세 번째 세션] 여기 같이 있던 `0-Y`(`:Compute(fn)`의
|
||||
> lazy 핸들 계약)는 해소됨** — 실측 결론은 "계약은 그대로 두고, 파생
|
||||
> State를 만드는 자리마다 결과 타입을 명시 주석으로 바인딩한다. 그 외는
|
||||
> Luau의 현 한계라 지금 우리가 할 수 있는 바 없다". 지금 유효한 규약은
|
||||
> **`base/typing-limits.md`**, 실측 근거 전문은
|
||||
> `audit/type-recursion-issue/`, 해소 전 원문은
|
||||
> `archive/question-resolved.md`의 0-Y 절.
|
||||
|
||||
### 0-Z. ⭐ **최우선 — Attribute 이름 소유권을 무엇으로 판정할 것인가** (2026-08-13 여섯 번째 세션, 사용자가 다음 세션 심층 분석으로 이관)
|
||||
|
||||
**이게 지금 유일하게 `base/` 반영을 막고 있는 결정.** 아래 0-A의
|
||||
재디스패치 모델은 나머지가 전부 확정됐고, 이 항목 하나만 정해지면
|
||||
⚠️ 배너를 단 **7개 문서**(`bind-system-plan.md`/`tag-plan.md`/
|
||||
`slot-plan.md`/`attribute-plan.md`/`ref-plan.md`/`architecture.md`/
|
||||
`ROADMAP.md` — `ref-plan.md`는 9차 세션 분할 때 배너가 같이 안 옮겨간
|
||||
걸 10차 세션 감사에서 발견해 추가)를 한 번에 옮기면 됨.
|
||||
|
||||
**문제**: 새 모델(핸들러 선비교)에서는 그룹 A가 잡아둔
|
||||
`AttributeKey("foo")` 인덱스 1에 그룹 B가 들어와도 **양쪽 다 `StoreBind`에
|
||||
매치되므로 "같은 핸들러"로 판정돼 조용히 갈아탐.** 그리고 나중에 A의
|
||||
클로저가 자기 이름들을 `retractFrom`할 때 B의 바인딩을 대신 철거함
|
||||
(교차 오염). 예전 "조용한 last-write-wins"가 그대로 돌아옴 — 이번 감사에서
|
||||
고쳤던 바로 그 증상. 즉 **Dispatch의 점유 체크가 대신 잡아주던 걸 이제
|
||||
Attribute가 직접 해야 함.**
|
||||
|
||||
**사용자 방향(2026-08-13, 심층 분석은 다음 세션)**:
|
||||
> "Attribute 소유권은 아마 이전 결정을 다시 가져오는게 맞아보이긴 하네요.
|
||||
> 막 깊게 Key -> Group 필요한것 같지는 않고, 본인 retract 처리를 수행할 때
|
||||
> 무언가 하면 될듯 한데. 이 부분은 나중에 제가 물리적으로 스케치 해보며
|
||||
> 심층 분석해보겠습니다."
|
||||
|
||||
- **"이전 결정을 다시 가져온다"** = 2026-08-13 네 번째 세션의 이름별
|
||||
claimant `Relate`(당시 이름 `owners`). **당시 기각 사유는 새 모델에서
|
||||
구조적으로 소멸함** — 그때 버그는 "소유권 반납이 `process`의 `v==nil`
|
||||
분기에만 있어서, 그룹이 이름을 통째로 놓는 경로가 그 분기를 안 타
|
||||
옛 소유권이 안 지워짐"이었는데, 지금은 **클로저가 항상 불리므로 거기서
|
||||
반납**하면 그 구멍이 안 생김. 사용자의 "본인 retract 처리를 수행할 때
|
||||
무언가 하면 될듯"이 정확히 이 지점.
|
||||
- **"막 깊게 Key → Group 필요한 것 같지는 않다"** — 키에서 그룹으로
|
||||
거슬러 올라가는 양방향 레지스트리까지는 필요 없고, 이름 → 현재
|
||||
claimant 단방향이면 충분할 것이라는 방향.
|
||||
- 원문 맥락과 기각된 두 중간안(`rawNew` 전용 키, `AttributeGroupKeyHandler`
|
||||
체크포인트)은 `archive/checkpoint-handler-pattern-reversed.md`,
|
||||
분석은 `research/dispatch-redispatch-diff-plan.md` 5절.
|
||||
|
||||
**대안 후보(정리해둠)**: (a) 이름별 claimant `Relate`를 Attribute에
|
||||
국소적으로 — 권고, (b) UB로 두고 문서로만 금지 — 증상이 "조용한 오작동 +
|
||||
교차 오염"이라 다른 UB들(즉시 스택오버플로/즉시 error)보다 나빠서 비권장,
|
||||
(c) `Dispatch`에 claimant 개념 일반화 — 이번에 걷어낸 방향이라 반대.
|
||||
> **M0 착수를 막던 항목이 전부 해소됐습니다.** `0-Y`(`:Compute(fn)`의
|
||||
> lazy 핸들 계약)는 열세 번째 세션에, `0-Z`(Attribute 이름 소유권)와
|
||||
> `0-A`(재디스패치 하강 diff)는 **열네 번째 세션에 확정·`base/` 반영
|
||||
> 완료**. 해소 전 원문과 결론은 `archive/question-resolved.md`,
|
||||
> 뒤집힌 옛 재디스패치 모델은 `archive/dispatch-hintvalue-model-reversed.md`.
|
||||
>
|
||||
> **M0 착수 전 읽을 것**: `base/typing-limits.md`(0-Y가 남긴 구현 규약),
|
||||
> `base/dispatch-core-plan.md`(0-A/0-Z가 반영된 디스패치 코어 — 열네 번째
|
||||
> 세션에 `bind-system-plan.md`에서 분리 신설).
|
||||
|
||||
## 결정 대기 — M0는 안 막음
|
||||
|
||||
### 0-W. 같은 `Ref` 객체가 두 자리에 놓이는 걸 막을 것인가 (2026-08-13 열세 번째 세션 신설, 0-Z 확인 중 발견)
|
||||
|
||||
**0-Z(Attribute)를 보다가 "Ref에도 같은 문제가 있냐"는 사용자 질문에서
|
||||
나온 것 — 있고, 막는 장치가 전혀 없음.** 단 메커니즘은 0-Z와 **반대
|
||||
방향**이라 별개 항목으로 분리: Attribute는 *두 소유자 → 한 자리*(이름별
|
||||
**0-Z(Attribute, 열네 번째 세션에 해소됨)를 보다가 "Ref에도 같은 문제가
|
||||
있냐"는 사용자 질문에서 나온 것 — 있고, 막는 장치가 전혀 없음.** 단
|
||||
메커니즘은 0-Z와 **반대 방향**이라 별개 항목으로 분리: Attribute는 *두 소유자 → 한 자리*(이름별
|
||||
메모이즈된 키라 수렴), Ref는 *한 객체 → 두 자리*(발산).
|
||||
|
||||
**손 트레이싱**(`base/ref-plan.md`의 `RefLeafHandler` 의사코드에 대입,
|
||||
|
|
@ -82,7 +39,7 @@ Attribute가 직접 해야 함.**
|
|||
1. `process(inst1,"Ref",r)` → `relate[inst1]["Ref"]`가 nil → `r:Set(inst1)`
|
||||
2. `process(inst2,"Ref",r)` → `relate[inst2]["Ref"]`도 nil(**다른 키**) →
|
||||
`r:Set(inst2)` — inst1 바인딩이 **조용히 유실, 에러 없음**
|
||||
3. inst1 자리가 retract → `hintValue(nil) ~= v(r)` → **`r:Set(nil)`** —
|
||||
3. inst1 자리가 retract → 클로저 인자 `nil ~= v(r)` → **`r:Set(nil)`** —
|
||||
inst2가 정당하게 들고 있던 값을 지움(교차 오염)
|
||||
|
||||
`relate`가 `(inst,k)`별로만 있어 "이 Ref가 이미 다른 자리에 있다"를
|
||||
|
|
@ -95,7 +52,7 @@ Attribute가 직접 해야 함.**
|
|||
| `Slot` | element | `claimOwner`/`claimOwnerAt` → 즉시 error(`Slot{a,a}`/`Frame{slot,slot}`) | 막힘 |
|
||||
| `PreRef` | 자기 자신 | `_fired` → 재사용 시 error | 막힘 |
|
||||
| `Tag` | 태그 이름 | 위치별 참조 카운트 — 겹침이 **의도된 동작**(합집합) | 설계상 정상 |
|
||||
| `Attribute` | 이름 | 없음 | **0-Z** |
|
||||
| `Attribute` | 이름 | 그룹 전용 키 + 이름 claim → 즉시 error | 막힘(0-Z 해소, 14차 세션) |
|
||||
| **`Ref`** | 자기 자신 | **없음** | **이 항목** |
|
||||
|
||||
특히 걸리는 두 가지:
|
||||
|
|
@ -107,48 +64,18 @@ Attribute가 직접 해야 함.**
|
|||
검증하는데 **`Ref`만 커버가 없음**.
|
||||
|
||||
**0-Z와의 관계 — 독립**: 이건 하강 diff 모델이 만든 회귀가 **아니라
|
||||
원래부터 있던 갭**(점유 체크는 같은 `(inst,k,index)`만 봤지, 서로 다른
|
||||
자리를 가로지르는 건 원래 안 봤음). 0-Z를 어떻게 정하든 별도 결정이고,
|
||||
0-Z와 달리 **M0를 막지 않음** — 다만 사용자가 소유권 설계를 스케치할 때
|
||||
같이 보는 게 자연스러움.
|
||||
원래부터 있던 갭**(옛 점유 체크도 같은 `(inst,k,index)`만 봤지, 서로 다른
|
||||
자리를 가로지르는 건 원래 안 봤음). **[2026-08-13 열네 번째 세션] 0-Z가
|
||||
해소되면서 이 표에서 `Ref`만 유일하게 비어 있게 됐음** — Attribute는
|
||||
"이름 claim"이라는 국소 레지스트리로 갔으니, `Ref`도 같은 모양(`Ref →
|
||||
현재 자리` 단방향 `Relate` + 즉시 error)이 자연스러운 선택지. 여전히
|
||||
**M0를 막지는 않음**.
|
||||
|
||||
**선택지**: (a) `Slot`/`PreRef`와 같이 즉시 error(일관성 높음, `Relate`
|
||||
하나로 Ref→현재 자리 추적), (b) UB로 두고 문서화만(현상 유지 —
|
||||
단 증상이 "조용한 값 소실"이라 다른 UB보다 나쁨), (c) 마지막 쓰기 승리를
|
||||
정식 동작으로 인정(비권장, `Ref`의 "확정된 값 박스" 의미와 충돌).
|
||||
|
||||
### 0-A. `hintValue` 폐기 → process 하강 중 핸들러 비교 (2026-08-13 여섯 번째 세션, **Attribute 건 외 확정**)
|
||||
|
||||
**검토 결과 사용자 지적이 맞음 — 현행 `hintValue`엔 실제 결함이 있음.**
|
||||
힌트가 "그 자리에 곧 디스패치될 raw 값"이라 `None` 센티널이나 `State`/
|
||||
`Tween` 같은 래퍼가 그대로 넘어갈 수 있고, 그러면 말단 핸들러의
|
||||
`isTag(hint)` 가드가 거짓이 되어 **깜빡임/재생성 방지가 조용히 꺼짐**
|
||||
(정확성은 유지돼서 지금까지 안 드러났음). 상세 재현·분석·제안은
|
||||
`research/dispatch-redispatch-diff-plan.md`.
|
||||
|
||||
**후속 라운드에서 모델은 거의 확정됨** — 래핑 핸들러가 `retractFrom`을
|
||||
선행 호출하는 걸 폐기하고, `Dispatch.process` 안에서 **핸들러를 먼저
|
||||
비교**해 (같으면 그 자리 클로저에 새 값을 넘기고 자기 `process` 재호출,
|
||||
다르면 그 자리부터 아래를 전량 철거). 이걸로 (a) 힌트의 타입이
|
||||
구조적으로 보장되고(같은 핸들러일 때만 값이 넘어가므로), (b) 깊은 체인의
|
||||
힌트 유실도 사라지며(각 레벨이 자기 재프로세스에서 자기 힌트를 받음),
|
||||
(c) `oldValue`를 따로 넘기자던 보완안은 불필요해짐(사용자 지적:
|
||||
"클로저라 이미 본인이 알지 않아요?" — 맞음, `chains`에 추가로 저장할 건
|
||||
비교용 `handler` 하나뿐), (d) `HandlerChanged` 마커도 불필요(핸들러가
|
||||
바뀌었다는 건 retractor가 `nil` 힌트로 불린다는 사실로 이미 표현됨).
|
||||
|
||||
**남은 열린 항목은 Attribute 이름 소유권 하나뿐 — 위 0-Z로 분리해
|
||||
최우선 배치**(사용자가 다음 세션에 직접 스케치하며 심층 분석하기로).
|
||||
그 하나 외에는 이 항목에 결정할 게 없음.
|
||||
|
||||
**실행 규모**: `base/`의 `bind-system-plan.md`/`tag-plan.md`/`slot-plan.md`/
|
||||
`attribute-plan.md`/`ref-plan.md` 의사코드 재작성 + `architecture.md`
|
||||
소스트리 서술과 `ROADMAP.md` M2/M4/M6/M10 체크리스트 — 0-Z 하나만 정해지면
|
||||
한 번에 옮기면 됨(어디를 어떻게 고칠지는
|
||||
`research/dispatch-redispatch-diff-plan.md` 6절에 파일별로 적어둠 — 뒤
|
||||
셋은 2026-08-13 7차/10차 감사에서 그 목록에 빠져 있던 걸 발견해 추가).
|
||||
**그때까지 `base/`의 현행 `hintValue` 서술이 유효** — 아직 안 옮겼다는 걸
|
||||
잊고 base만 읽으면 옛 모델로 구현하게 되니 주의.
|
||||
### 0-B. `dispose(any)` — 시그니처/범위 (2026-08-13 여섯 번째 세션 신설, 사용자 제안)
|
||||
|
||||
`State<Slot>` 교체를 파괴가 아니라 **언마운트**로 확정하면서(`state<Frame>`와
|
||||
|
|
@ -201,6 +128,13 @@ Attribute가 직접 해야 함.**
|
|||
질문이라 `is`보다 `can` 계열 접두를 유지하는 쪽이 낫다는 방향으로 사용자가
|
||||
기욺 — 여전히 미확정, 다음에 `can`으로 시작하는 구체 대안(예: `canRun`)을
|
||||
같이 검토할 것.
|
||||
- **클로저 인자 이름 `hintValue`(3순위, 사소함, 2026-08-13 열네 번째
|
||||
세션 신설)**: 하강 diff 재디스패치에서 이 인자는 더 이상 "힌트"가
|
||||
아니라 **`nil`이거나 같은 핸들러가 곧 처리할 새 값**임이 계약으로
|
||||
보장됨(`base/dispatch-core-plan.md`) — 이름이 옛 모델의 잔재라
|
||||
`nextValue`류가 더 정확함. 코퍼스에 이미 널리 쓰인 이름이라 이번엔
|
||||
안 바꾸고 대기열에만 올림(의사코드는 새로 쓰는 자리부터 `nextValue`를
|
||||
쓰기 시작했음).
|
||||
- **`Brand`(3순위, 사소함, 2026-08-07 여덟 번째 세션 추가)**: 런타임
|
||||
nominal 타입 판별 통합 메커니즘(`Brand.set`/`Brand.get`, `isState`를
|
||||
10종 branded 타입 전부로 일반화) — `brand-plan.md`의 `Brand`
|
||||
|
|
@ -251,12 +185,19 @@ Attribute가 직접 해야 함.**
|
|||
규칙에 그대로 맞아 포함 근거는 있음. 상세는 `research/operator-sugar-plan.md`.
|
||||
구현 자체는 맨 마지막 우선순위(순수 슈가, 없어도 무방) — 여전함.
|
||||
- **중첩 State 평탄화 `State<State<T>>` → `State<T>`(2026-08-13 여섯 번째
|
||||
세션 신설, 백로그)** — 인덱스 기반 `Dispatch` 재설계로 `State<State<T>>`는
|
||||
UB에서 정상 지원 대상이 됐지만, 깊이가 늘수록 `retractFrom`의 힌트가
|
||||
더 깊은 인덱스엔 `nil`로 전달돼 `Tag`/`Ref`/`Slot` 등의 힌트 기반
|
||||
최적화(깜빡임 방지)가 무력화되는 실제 기능 손실이 있음 — 값 층에서
|
||||
평탄화하는 `state:Flatten()`류 콤비네이터 아이디어는 나왔으나 착수 안
|
||||
함. 상세는 `research/operator-sugar-plan.md` 마지막 절.
|
||||
세션 신설, 백로그)** — **[근거 축소, 열네 번째 세션]** 원래 이 항목의
|
||||
주 근거는 "깊은 체인에선 힌트가 `nil`로 전달돼 깜빡임 방지가 꺼진다"는
|
||||
실제 기능 손실이었는데, **하강 diff 재디스패치로 각 레벨이 자기 값을
|
||||
받게 되면서 그 손실 자체가 없어졌음**(`base/dispatch-core-plan.md`).
|
||||
남은 근거는 편의성과 Slot offset이 밀리고 당겨지는 케이스뿐이라
|
||||
우선순위가 더 내려감 — `state:Flatten()`류 콤비네이터 아이디어는
|
||||
그대로 백로그. 상세는 `research/operator-sugar-plan.md` 마지막 절.
|
||||
- **[신설, 2026-08-13 열네 번째 세션] `Attribute.Merged`의 이름 중복** —
|
||||
두 Store가 같은 이름을 가지면 지금은 `:NameMap()` 평탄화 단계에서
|
||||
조용히 하나가 이김(dispatch 이전이라 이름 claim이 못 잡는 자리).
|
||||
합성 시점 1회 체크로 error를 내는 게 이 문서 다른 결정들과 결이
|
||||
같지만, "Merged는 뒤가 이긴다"를 의도된 override로 볼 여지도 있어
|
||||
사용자 확인 필요 — `base/attribute-plan.md` "열린 질문" 절.
|
||||
- `research/existing-instance-bind-plan.md` — 스코프 논의만 필요, 구현
|
||||
착수를 막지 않음.
|
||||
- **`quad-debug` 세부 API 이름** — `research/debug-tooling-plan.md` 참고.
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ v1 폐기 API/버그/구조 결함 전부 v2 설계를 정당화하는 내부
|
|||
- 심화: 정적 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" 절).
|
||||
추가 필요(`base/dispatch-core-plan.md` "Length/Offset" 절).
|
||||
**[2026-08-09 추가]** `Slot:List`의 `prev`/`userdata` 재사용 최적화를
|
||||
getting-started에서 "항상 파괴 후 재생성" 단순 버전만 가르치고 나중에
|
||||
최적화 단계에서 별도로 알려줄지, 아니면 Slot이 학습 순서상 core loop
|
||||
|
|
@ -250,7 +250,7 @@ additional-primitives-plan.md`의 "문서화 백로그" 절이 원자료)**:
|
|||
|
||||
- **[해소됨]** Slot 형제 순서 보장 — `Dispatch.setLength`/
|
||||
`setOffsetSource`(Length/Offset)로 2026-08-09 여섯 번째 세션에 확정,
|
||||
`bind-system-plan.md` "Length/Offset" 절 참고.
|
||||
`dispatch-core-plan.md` "Length/Offset" 절 참고.
|
||||
- **[해소됨, 2026-08-13 정정]** Tween 오버라이드/옵션 값 모양 —
|
||||
2026-08-12 첫 번째 세션에 `Info: TweenInfo?`+편의 필드 폴백,
|
||||
override 정책은 `Tween.Cancel`(기본)/`Tween.Finish` 2값으로 확정,
|
||||
|
|
|
|||
|
|
@ -306,15 +306,17 @@ Attribute는 이미 "겹치면 error"로 소유 코드가 명확히 갈리는
|
|||
"Dispatch 체인" 절). 하지만 **동작한다고 해서 권장 방향인 건 아님**
|
||||
(사용자 판단: "UB는 아니지만 우리가 원치 않는 방향인건 맞습니다").
|
||||
|
||||
**왜 원치 않는가 — 단순 취향이 아니라 실제 기능 손실이 있음**:
|
||||
`Dispatch.retractFrom(inst,k,index,v)`는 힌트 `v`를 정확히 `index`
|
||||
자리에만 넘기고 더 깊은 인덱스엔 `nil`을 넘김. 그래서 `State<Tag>`
|
||||
(한 겹, StoreBind@1 → TagHandler@2)에서는 재발행 시 TagHandler가
|
||||
`hintValue=newTag`를 **확실히** 받아 깜빡임 방지가 동작하지만,
|
||||
`State<State<Tag>>`(StoreBind@1 → StoreBind@2 → TagHandler@3)에서
|
||||
**바깥** store가 재발행하면 TagHandler는 `nil`을 받아 `RemoveTag`→
|
||||
`AddTag` 왕복이 실제로 일어남. 깊이가 늘수록 힌트 기반 최적화
|
||||
(`Tag`의 `Contains`, `Ref`/`Slot`의 identity 비교)가 전부 무력화됨.
|
||||
**[근거 축소, 2026-08-13 열네 번째 세션] 원래 이 항목의 주 근거였던
|
||||
"깊은 체인에선 힌트가 사라져 깜빡임 방지가 꺼진다"는 손실은 없어졌음.**
|
||||
당시 서술: 옛 `Dispatch.retractFrom(inst,k,index,v)`가 힌트 `v`를 정확히
|
||||
`index` 자리에만 넘기고 더 깊은 인덱스엔 `nil`을 넘겼기 때문에
|
||||
`State<State<Tag>>`에서 바깥이 재발행하면 TagHandler가 `nil`을 받아
|
||||
`RemoveTag`→`AddTag` 왕복이 일어났음. **하강 diff 재디스패치**에선 각
|
||||
레벨이 자기 재프로세스에서 자기 값을 받으므로 깊이와 무관하게 진짜
|
||||
`Tag` 객체가 전달됨(`base/dispatch-core-plan.md` "Dispatch 체인" 절).
|
||||
남은 근거는 (a) 편의성/의도 표현, (b) `state<state<Frame>>`류에서 Slot
|
||||
offset이 밀리고 당겨지는 케이스(이건 이미 "그냥 확인된 것"으로 수용)
|
||||
정도라 **우선순위가 더 내려감**.
|
||||
|
||||
**아이디어(착수 안 함)**: 중첩을 Dispatch 층에서 감내하는 대신, 값
|
||||
층에서 **평탄화하는 콤비네이터**를 제공 — `State<State<T>>`를 받아
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ PropertyHandler가 직접 판별. 상세는 `base/tween-plan.md`(전면
|
|||
재작성), 구 모델은 `archive/tween-special-bind-key-reversed.md`. 아래는
|
||||
원래 발견 당시 기록.
|
||||
|
||||
**위치**: `base/bind-system-plan.md` "확정된 디스패치 모델" 절 67-79행 —
|
||||
**위치**: `base/dispatch-core-plan.md` "확정된 디스패치 모델" 절 67-79행 —
|
||||
"Tween의 store-bind 핸들러는 **`k`는 무엇이든 받고 `v`가 Store인 경우를
|
||||
잡아내는, 우선순위가 매우 높은 핸들러**"; `architecture.md` 소스트리엔 이
|
||||
역할을 하는 quad-roblox 파일이 `Handlers/Tween.luau` 하나뿐(별도 범용
|
||||
|
|
@ -79,10 +79,11 @@ store.color }`처럼 애니메이션 없이 그냥 반응형으로 값만 바뀌
|
|||
### 1-2. retract 시 "이전에 실제로 매치됐던 핸들러"를 누가 추적하는지 불명 — [해소됨, 2026-08-08 세 번째 세션]
|
||||
|
||||
**해소**: `Dispatch`가 `(inst,k)`별 핸들러 체인(순서 있는 배열, `chains`)을
|
||||
직접 소유하고, `Dispatch.retractFrom(inst,k,index,v)`가 꼬리부터 `index`
|
||||
직접 소유하고, `Dispatch.retractFrom(inst,k,index)`가 꼬리부터 `index`
|
||||
까지 정리해주는 걸로 확정(2026-08-08 확정 당시 이름은 `retractUnder`이고
|
||||
체인이 핸들러 배열이었음 — 2026-08-13 다섯 번째 세션에 인덱스 기반으로
|
||||
재설계되며 개명, 결론 자체는 유지) — 아래 원래 제안(`Dispatch/StoreBind.luau`가
|
||||
재설계되며 개명, 같은 날 열네 번째 세션에 힌트 인자가 빠져 3-인자가 됨,
|
||||
결론 자체는 유지) — 아래 원래 제안(`Dispatch/StoreBind.luau`가
|
||||
"마지막 선택된 핸들러"를 직접 들고 있는 방식)은 재귀/래핑 핸들러가
|
||||
여러 단계(A→B→C)로 겹칠 때 자기 자신의 상태와 위임한 핸들러의 상태가
|
||||
슬롯 하나를 두고 충돌하는 문제가 있어 기각되고, 대신 Dispatch 자신이
|
||||
|
|
@ -90,7 +91,7 @@ store.color }`처럼 애니메이션 없이 그냥 반응형으로 값만 바뀌
|
|||
bind-system-plan.md` "Dispatch 체인" 절, `ROADMAP.md` M2/M4. 아래는
|
||||
원래 발견 당시 기록.
|
||||
|
||||
**위치**: `base/bind-system-plan.md` "확정된 디스패치 모델" 절 90-91행 —
|
||||
**위치**: `base/dispatch-core-plan.md` "확정된 디스패치 모델" 절 90-91행 —
|
||||
"store bind가 새 값으로 넘어갈 때 이전 핸들러의 `retract(inst, k, v)`를
|
||||
한 번 호출해주면 됨."
|
||||
|
||||
|
|
@ -115,10 +116,10 @@ bind-system-plan.md` "Dispatch 체인" 절, `ROADMAP.md` M2/M4. 아래는
|
|||
매치 실패는 조용한 무시 없이 즉시 `error`(브랜드+`typeof` 출력, provider
|
||||
초기화 확인 안내)로 확정. 핸들러 등록 시점 동률 감지 print 경고와
|
||||
`Dispatch.listHandlers()`류 전체 목록 조회 함수도 M2 기본 기능으로
|
||||
확정 — 상세는 `base/bind-system-plan.md` "우선순위 동률/매치 실패 처리"
|
||||
확정 — 상세는 `base/dispatch-core-plan.md` "우선순위 동률/매치 실패 처리"
|
||||
절. 아래는 원래 발견 당시 기록.
|
||||
|
||||
**위치**: `base/bind-system-plan.md` "핸들러 계약" 절 — "디스패치는 등록된
|
||||
**위치**: `base/dispatch-core-plan.md` "핸들러 계약" 절 — "디스패치는 등록된
|
||||
핸들러를 우선순위 순으로 스캔하며 `isHandlable`을 호출, 첫 매치가 처리."
|
||||
|
||||
**문제**: (a) 두 핸들러가 같은 `priority` 값을 가질 때 어느 쪽이 우선인지
|
||||
|
|
|
|||
|
|
@ -0,0 +1,198 @@
|
|||
# 2026-08-13 열네 번째 세션 — 0-Z(`Attribute:GetKey`) 확정, 하강 diff 재디스패치 전면 반영, Tag/Attribute를 quad-base로 재배치
|
||||
|
||||
**한 줄 요약**: 사용자가 `Attribute:GetKey(name)` 아이디어를 제시하며 0-Z를
|
||||
다시 열었고, 트레이싱으로 검증한 뒤 **그룹 전용 키 + 이름 claim**으로
|
||||
확정 → 그 김에 **0-A(하강 diff 재디스패치)까지 base 전면 반영 + 디스패치
|
||||
코어 분리(`dispatch-core-plan.md` 신설) + Tag/Attribute 패키지 재배치**를
|
||||
한 패스로 처리하고 커밋. **M0 착수를 막는 결정이 이제 하나도 없음.**
|
||||
|
||||
---
|
||||
|
||||
## 1. 시작 — 사용자 문제 제기
|
||||
|
||||
> "Attribute 에 대해서 말이지, 우린 이전의 결정을 다시 돌아봐야할 필요가
|
||||
> 있는듯. `Attribute:GetKey(name)` 구현으로써 '조용한 문제 없음' 을
|
||||
> 해결할 수 있을것으로 보이는데. 한번 확인해볼래?"
|
||||
|
||||
`question.md` 0-Z(Attribute 이름 소유권)는 여섯 번째 세션에 사용자가
|
||||
"다음 세션에 직접 물리적으로 스케치하며 심층 분석"으로 명시 이관해둔
|
||||
최우선 항목이었고, 그때의 잠정 방향은 **(a) 이름별 claimant `Relate`를
|
||||
Attribute 안에 두기**였음.
|
||||
|
||||
## 2. 검증 — 문제를 두 갈래로 분해
|
||||
|
||||
| | 증상 | 하강 diff 모델에서 |
|
||||
|---|---|---|
|
||||
| **C1 그룹 A ↔ 그룹 B** | 같은 이름 | 둘 다 `StoreBind` → "같은 핸들러"로 조용히 인계 + A 클로저가 B 걸 철거(교차 오염) |
|
||||
| **C2 그룹 ↔ 직접 쓰기** | 같은 이름 | 핸들러가 다름 → `retractFrom` 후 조용히 교체 |
|
||||
| **C3 같은 그룹 객체 두 자리** | 같은 이름 | 키가 같아 수렴 → C1과 동일 |
|
||||
|
||||
**핵심 발견 — 권고안 (a)는 C2를 못 잡음.** 직접 쓰기는 그룹 코드를 아예
|
||||
안 지나가서 그룹 쪽 레지스트리에 등록될 자리가 없고, 두 경로가 실제로
|
||||
만나는 유일한 지점인 `AttributeKeyHandler`에서는 공개 캐시 키를 쓰는 한
|
||||
`k`가 **같은 객체**라 소유자를 구분할 방법이 원천적으로 없음. 옛 모델에선
|
||||
Dispatch의 점유 체크가 이걸 대신 잡아줬는데, 하강 diff에선 점유가 정상
|
||||
상태라 그 체크 자체가 성립하지 않음.
|
||||
|
||||
**그래서 `GetKey`가 필요한 이유가 분명해짐**: 전용 키를 쓰면
|
||||
- 체인이 소유자별로 갈려 **교차 오염이 구조적으로 불가능**해지고,
|
||||
- 소유권이 **키 identity**로 표현되므로 말단 핸들러가 "이 이름을 지금
|
||||
누가 잡고 있나"를 단방향 맵 하나로 판정할 수 있음(그룹인지 직접
|
||||
쓰기인지 알 필요조차 없음).
|
||||
|
||||
즉 정확히는 **`GetKey`(교차 오염 제거) + 이름 claim(감지)** 두 조각이
|
||||
한 세트. `GetKey`만으로는 두 소유자가 각자 `setAttribute`를 쏘는
|
||||
flip-flop이 조용히 남음.
|
||||
|
||||
**순서 검증**(하강 diff에서 "해제 → 재클레임"이 항상 보장되는가):
|
||||
같은 핸들러 재프로세스는 `slot.retractor(v)` → `h.process(...)`,
|
||||
핸들러 교체는 `retractFrom`(꼬리부터) → `process` — 어느 경로든 옛 claim
|
||||
반납이 먼저. `5 → None → 5`처럼 체인 깊이가 오가는 경우도 인덱스가
|
||||
바뀌는 자리에서 `retractFrom`이 먼저 돌아 동일. `State<Modifier>`가 이미
|
||||
차단돼 있고 배열 위치는 정적이라, 위치를 가로지르는 "새 소유자 먼저
|
||||
claim" 경로도 없음.
|
||||
|
||||
**남은 구멍으로 보고한 것**: (1) `GetKey`를 공개 API로 내면 사용자가 그
|
||||
키를 다른 자리에 다시 놓아 수렴시킬 수 있고 그건 claim(키 identity 기준)
|
||||
으로도 못 잡음 → **사용자가 "비공개로 안 내도 될듯"으로 확정**, (2)
|
||||
base/roblox 패키지 경계, (3) `Attribute.Merged`의 이름 중복이 dispatch
|
||||
이전 단계라 claim이 못 잡음(→ 새 열린 질문으로 등록).
|
||||
|
||||
## 3. 사용자의 두 번째 제기 — 패키지 배치가 이상하다
|
||||
|
||||
> "Attribute 는 왜 quad-base 인지 모르겠습니다. 사실, Attribute 랑 Tag
|
||||
> 모두 다른 곳에서도 사용할 수 있거나 없거나 해서, 그냥 quad-base 에 있고,
|
||||
> set 메커니즘(최종 inst:Set... 와 Tag 제거 추가) 부분만 quad-roblox 에
|
||||
> 작성하는게 좋을수도 있어보여요. 왜냐하면 웹에도 className, data-
|
||||
> attribute 가 있습니다. (...) 안 그럼 다시 재구현 될 부분이 너무 많은것
|
||||
> 같은 느낌."
|
||||
|
||||
동의. 당시 배치는 **알고리즘 전체가 quad-roblox, 값 타입만 base**였는데
|
||||
실제로 엔진에 종속된 건 마지막 한 줄뿐:
|
||||
|
||||
| 층 | 내용 | 엔진 종속? |
|
||||
|---|---|---|
|
||||
| 값 타입/API | `Tag(...)`, `Attribute(...)`, `AttributeKey(name)`+weak 캐시 | ✗ |
|
||||
| 알고리즘 | Tag 참조 카운트, 그룹 위임, **이름 claim**, `None` 처리 | ✗ |
|
||||
| 실행 | `AddTag`/`RemoveTag` / `SetAttribute` | **✓ 3줄** |
|
||||
|
||||
`architecture.md`가 이미 *"pluggable 디스패치 엔진 자체도 인터페이스로
|
||||
base가 소유 — 엔진마다 큰 구현을 중복하지 않기 위함"*이라고 못박아뒀는데
|
||||
Tag/Attribute만 그 원칙 밖에 있었음. 선례도 있음 — `LifetimeHandle`이
|
||||
base엔 인터페이스, quad-roblox엔 gcconn 구현으로 갈려 있고 주입은
|
||||
`RobloxFactory(BaseModule)` 뮤테이션. **`addTag`/`removeTag`/`setAttribute`는
|
||||
그 목록에 3개를 더하는 것뿐, 새 메커니즘이 아님.**
|
||||
|
||||
세부 합의:
|
||||
- **`addTag(inst, names: {string})`** — vararg가 아니라 테이블. 근거는
|
||||
열다섯 번째 세션에 `Tag:Added`가 vararg → `string | {string}`으로
|
||||
되돌아갔던 것과 같음(`table.unpack`이 tail 위치에서만 완전히 펼쳐짐 +
|
||||
대량 이름에서 unpack 한계). 웹 `className` 배치 갱신 요구도 테이블로
|
||||
충족됨. **사용자 동의**.
|
||||
- **타입 패밀리는 갈림** — 제네릭 `AttributeKey<<T>>`와 스칼라 3종은
|
||||
base, `Color3Attribute`류는 백엔드. 사용자: "그건 D/DI 쪽에서 각자
|
||||
구현임. 타입 쪽은 그쪽에서 처리하면 되는것 같고".
|
||||
- **미주입 백엔드 실패 모드** → 사용자가 더 나은 안을 제시:
|
||||
|
||||
> "그렇다면 failback... 이름의 아주 낮은 우선순위의 요소,
|
||||
> PRIORITY_FAILBACK 정도를 잡아두는게 좋겠네요. 그건 base 에서
|
||||
> 주입해버려도 되고, 위에서 처리되면 상관 없게 잘 처리되니까요."
|
||||
|
||||
→ **`HANDLER_PRIORITY_FALLBACK`** 신설(기존 `HANDLER_PRIORITY_*` 패밀리에
|
||||
추가, 철자는 영어 표준형 FALLBACK으로 정규화). base 제공 핸들러는 이
|
||||
밴드에 등록하므로 백엔드가 평범한 우선순위로 자기 핸들러를 등록하면
|
||||
언제나 이김 — 비활성화나 등록 순서 조정이 필요 없음. op 자체가 없는
|
||||
백엔드에서는 base 스텁이 명확한 에러를 냄.
|
||||
|
||||
## 4. 반영 — 한 패스로
|
||||
|
||||
사용자 지시: *"모두 제 생각과 같으니까, 그렇게 처리해도 좋아요. 다만
|
||||
이전처럼 많은 루프를 돌며 문서를 안 고치게, 유의해가며 정리해줘요.
|
||||
(그걸 위해서 상위 모델로 올리기도 했고.) 처리 후 핸드오버 할거예요.
|
||||
clear 하게 준비해두고, 커밋하세요."*
|
||||
|
||||
### 4-1. `bind-system-plan.md` 2단계 분할 + 재작성
|
||||
|
||||
9차 세션이 *"디스패치 코어는 0-Z 확정 시 어차피 전면 재작성 대상이라,
|
||||
재작성하는 그 패스에서 파일을 가르는 게 총 변경량·실수 위험이 모두
|
||||
작음"*이라며 의도적으로 미뤄둔 계획을 그대로 실행 — `문제`/`핸들러 계약`/
|
||||
`확정된 디스패치 모델`/`None 센티널`/`Dispatch는 프리미티브가 아니다`/
|
||||
`Dispatch 체인`/`Handler 작성 체크리스트`/`Length·Offset`/`store 바인드는
|
||||
래핑` 블록(1074줄)을 **`base/dispatch-core-plan.md`**로 옮기고 새 모델로
|
||||
재작성. `bind-system-plan.md`는 2263 → 1219줄(반응형 코어 + 인체공학).
|
||||
|
||||
인바운드 참조는 `doc-check.py` 출력을 그대로 입력으로 삼아 기계적으로
|
||||
고침(파일별 라인 지정 치환) — 15개 파일 30여 곳.
|
||||
|
||||
### 4-2. 하강 diff 모델의 실제 내용
|
||||
|
||||
```lua
|
||||
-- chains[inst][k][index] = { handler = h, retractor = fn }
|
||||
function Dispatch.process(inst, k, v, index)
|
||||
local list = <확보 + chains 등록> -- 순서 규칙 그대로(h.process 前)
|
||||
local slot, h = list[index], Dispatch.getHandler(inst, k, v)
|
||||
if slot ~= nil and slot.handler == h then -- (A) 같은 핸들러
|
||||
slot.retractor(v) -- v는 isHandlable(v) 보장됨
|
||||
slot.retractor = h.process(inst, k, v, index)
|
||||
else -- (B) 다른 핸들러/빈 자리
|
||||
Dispatch.retractFrom(inst, k, index)
|
||||
list[index] = { handler = h, retractor = NOOP }
|
||||
list[index] = { handler = h, retractor = h.process(inst, k, v, index) }
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**이 세션에서 새로 도출한 귀결 — `retractFrom`이 3-인자가 됨.** 값을
|
||||
넘기는 경로가 (A) 분기 하나로 통일되면서 외부가 힌트를 만들어 넣을 자리
|
||||
자체가 사라짐 → 옛 결함(래퍼/센티널이 힌트로 새는 것)이 **구조적으로
|
||||
재발 불가**. 설계안 원문(6절)엔 4-인자 `retractFrom(inst,k,index,nil)`이
|
||||
그대로 남아 있었는데, 모델의 필연적 귀결이라 판단해 3-인자로 정리하고
|
||||
핸드오버에 명시.
|
||||
|
||||
같이 폐기/갱신된 것:
|
||||
- `isX(hintValue)` 방어 가드 **일반 규칙 폐지**(타입이 계약으로 보장됨)
|
||||
- "`hintValue`는 직속 1단계에만, 깊은 인덱스는 `nil`" 캐비엇 **삭제** →
|
||||
각 레벨이 자기 값을 받으므로 `State<State<Tag>>`에서도 깜빡임 방지 유효
|
||||
- Dispatch의 **점유 체크(소유권 감지) 폐지** → 필요한 도메인(Attribute)이
|
||||
직접
|
||||
- Handler 작성 체크리스트 2·3번 전면 교체, 8번(중간 노드는 `inst`에
|
||||
손대지 않는다 + 항상 재위임) 신설
|
||||
|
||||
### 4-3. 반영 대상 문서 (배너 7개 + 인덱스 레이어)
|
||||
|
||||
`base/`: `dispatch-core-plan.md`(신설) / `bind-system-plan.md` /
|
||||
`attribute-plan.md` / `tag-plan.md` / `slot-plan.md` / `ref-plan.md` /
|
||||
`architecture.md`(소스 트리 포함) / `module-lifecycle-plan.md`(주입 op
|
||||
목록) / `onchange-plan.md`(AttributeKey 위치 정정) / `relate-plan.md`
|
||||
문구.
|
||||
루트: `ROADMAP.md`(M0/M2/M4/M6/M10 배너·체크리스트) / `CLAUDE.md` /
|
||||
`HUMAN_TODO.md`.
|
||||
인덱스/질문: `.claude/README.md`(행 5개 갱신 + 신설 2행) /
|
||||
`question.md`(최우선 칸 비움) / `archive/question-resolved.md`(0-Z/0-A
|
||||
해소 마킹).
|
||||
아카이브: `research/dispatch-redispatch-diff-plan.md` →
|
||||
`archive/dispatch-hintvalue-model-reversed.md`(옛 모델 골자 + 폐기된
|
||||
규칙 목록을 머리에 추가).
|
||||
|
||||
### 4-4. 스파이크 상태 갱신
|
||||
|
||||
`04`(체인/`retractFrom`)와 `19`(소유권/참조카운트)가 **옛 모델을 검증
|
||||
중**이라 `done/` → `rewrite-required/`로 이동. "코드가 깨진" 게 아니라
|
||||
"설계가 바뀐" 경우라 STATUS.md에 그 구분을 명시하고, 각각 무엇을
|
||||
살리고 무엇을 바꿔야 하는지 적음(`04`의 `chains:SetStrong` 순서 음성
|
||||
대조군은 새 모델에서도 유효 → 살릴 것).
|
||||
|
||||
## 5. 새로 연 것 / 남은 것
|
||||
|
||||
- **새 열린 질문 1개(사소)**: `Attribute.Merged`의 이름 중복 —
|
||||
`:NameMap()` 평탄화가 dispatch 이전이라 이름 claim이 못 잡음.
|
||||
error로 갈지 "뒤가 이긴다"를 의도된 override로 볼지 사용자 확인
|
||||
대기(`question.md` 3번).
|
||||
- **용어 대기열 1개**: 클로저 인자 이름 `hintValue` — 이제 "힌트"가
|
||||
아니므로 `nextValue`류가 정확함. 코퍼스 전반에 퍼진 이름이라 이번엔
|
||||
안 바꾸고 대기열에만 올림(새로 쓴 의사코드는 `nextValue` 사용).
|
||||
- **0-W**(같은 `Ref` 객체 이중 배치)는 그대로 열림 — 0-Z가 닫히면서
|
||||
형제 프리미티브 표에서 `Ref`만 유일하게 비게 됐고, Attribute가 간
|
||||
"국소 레지스트리 + 즉시 error" 모양이 자연스러운 선택지라는 점을
|
||||
질문 문서에 덧붙임.
|
||||
- `doc-check.py` **ERROR 0** 유지 확인.
|
||||
113
CLAUDE.md
113
CLAUDE.md
|
|
@ -158,58 +158,39 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션
|
|||
|
||||
## 지금 할 일 (우선순위순)
|
||||
|
||||
0. **⭐ 최우선 — `.claude/question.md` 0-Z 결정.** (`question.md`엔
|
||||
0-B `dispose(any)` 시그니처/범위도 열려 있지만, M6 구현 세부만 막을 뿐
|
||||
M0 착수 자체는 안 막아서 이 최우선 항목과 급 다르게 취급 — 별도 항목
|
||||
아님.)
|
||||
0. **⭐ M0 착수를 막는 결정은 이제 없음 (2026-08-13 열네 번째 세션 기준).**
|
||||
`question.md`의 최우선 항목이 **전부 비었음** — `0-Y`(`:Compute` lazy
|
||||
핸들 계약)는 13차 세션에, **`0-Z`(Attribute 이름 소유권)와 `0-A`(재디스패치
|
||||
하강 diff)는 14차 세션에 확정·`base/` 반영 완료**. 남은 `0-W`(같은 `Ref`
|
||||
이중 배치)/`0-B`(`dispose` 시그니처)는 M0가 아니라 각각 M4/M6 구현 세부를
|
||||
막을 뿐임.
|
||||
|
||||
> **[2026-08-13 열세 번째 세션] 여기 같이 있던 `0-Y`는 해소됨.**
|
||||
> `:Compute(fn)`의 lazy 핸들 계약은 **그대로 유지**로 확정. 44개
|
||||
> 스파이크 재실측 결과 진짜 원인이 콜백 계약이 아니라 **Luau 자체의
|
||||
> 한계**(재귀 제네릭이 다른 타입 인자로 자기를 반환하면 타입 안전성이
|
||||
> 에러 없이 조용히 사라짐)였고, 당시 "raw 값이면 완전 클린"이라던
|
||||
> 1차 판정도 **뒤집혔음**(raw 값 계약도 똑같이 불안전). 사용자 정리:
|
||||
> "quad가 타입을 비틀 일이 아니라 상위 Luau의 현 한계이고, RFC/이슈
|
||||
> 수혜를 받을 때 해결될 일이라 당장 우리가 할 수 있는 바 없다."
|
||||
> **구현 시 지켜야 할 규약이 생겼으니 M0 착수 전 반드시 읽을 것:
|
||||
> `base/typing-limits.md`**(핵심은 "파생 State를 만드는 자리마다
|
||||
> 결과 타입을 명시 주석으로 바인딩" + 7번 설계 체크리스트). 실측
|
||||
> 근거는 `audit/type-recursion-issue/`, 해소 전 원문은
|
||||
> `archive/question-resolved.md`의 0-Y 절.
|
||||
**M0 착수 전 반드시 읽을 것 — 이 두 개는 "결정"이 아니라 "구현 규약"이라
|
||||
여전히 유효**:
|
||||
- **`base/typing-limits.md`**(0-Y의 산물) — 핵심은 "파생 State를 만드는
|
||||
자리마다 결과 타입을 명시 주석으로 바인딩" + 7번 설계 체크리스트.
|
||||
재귀 제네릭이 자기를 다른 타입 인자로 반환하면 Luau가 타입 안전성을
|
||||
**에러 없이 조용히** 잃는 상위 한계라 quad 쪽에서 우회하지 않기로
|
||||
확정(RFC `relax-recursive-type-restriction` 수혜 대기, 추적
|
||||
`luau-lang/luau#2380`). 실측 근거는 `audit/type-recursion-issue/`.
|
||||
- **`base/dispatch-core-plan.md`**(0-A/0-Z의 산물, 14차 세션에
|
||||
`bind-system-plan.md`에서 분리 신설) — 재디스패치가 "철거 후 재구축"이
|
||||
아니라 **하강 diff**임, `retractFrom`은 3-인자, 클로저 인자는
|
||||
`nil`이거나 같은 핸들러가 처리할 값(타입 보장), `HANDLER_PRIORITY_FALLBACK`,
|
||||
"base가 소유하는 핸들러와 주입되는 엔진 op"(`addTag`/`removeTag`/
|
||||
`setAttribute`). **Handler 작성 체크리스트 8개**를 새 핸들러 짜기 전에
|
||||
훑을 것 — 지난 세션들에서 실제로 반복된 실수 목록임.
|
||||
|
||||
**0-Z: Attribute 이름 소유권 결정.**
|
||||
2026-08-13 여섯 번째 세션에 `Dispatch` 재디스패치 모델이 "하강 diff"로
|
||||
다시 정리되면서(`research/dispatch-redispatch-diff-plan.md`), **그
|
||||
모델에서 유일하게 안 풀린 것이 Attribute 그룹의 이름 소유권 충돌
|
||||
감지**임. 사용자가 "이전 결정(이름별 claimant `Relate`)을 다시 가져오는
|
||||
게 맞아 보이나, 다음 세션에 직접 물리적으로 스케치하며 심층 분석"으로
|
||||
명시 이관 — 그전까지 아래 항목들보다 우선.
|
||||
**핸드오버 시 반드시 알아야 할 것**:
|
||||
- **`base/`의 현행 `hintValue` 서술은 아직 옛 모델(철거 선행)이다.**
|
||||
새 모델은 `research/dispatch-redispatch-diff-plan.md`에만 있음 —
|
||||
base만 읽고 구현하면 옛 모델로 짜게 됨. 0-Z가 정해지면 그 문서 6절의
|
||||
파일별 반영 목록대로 **한 번에** 옮길 것 — 대상은 ⚠️ 배너를 달고
|
||||
있는 **7개**(`bind-system-plan.md`/`tag-plan.md`/`slot-plan.md`/
|
||||
`attribute-plan.md` + **`architecture.md`/`ROADMAP.md`** — 뒤 둘은
|
||||
2026-08-13 7차 감사에서 6절 목록에 빠져 있던 걸 발견해 추가 +
|
||||
**`ref-plan.md`** — 9차 세션 분할 때 "`Ref`의 retract" 절이 배너
|
||||
없이 옮겨간 걸 이번 감사에서 발견해 추가),
|
||||
반영 후 각 배너도 같이 제거.
|
||||
- 반대로 **`base/slot-plan.md`의 "언마운트/`dispose`/해제 순서"는 이미
|
||||
확정 반영됨**(재디스패치 모델과 독립적인 결정이라 먼저 들어감).
|
||||
- 이 세션에서 같은 날 두 차례 급하게 쓴 의사코드가 각각 버그를 냈다는
|
||||
사실 자체가 교훈 — 0-Z 반영도 서두르지 말고 손 트레이싱을 거칠 것
|
||||
(`bind-system-plan.md` "Handler 작성 체크리스트" 절이 그 산물).
|
||||
해소 전 원문은 `archive/question-resolved.md`(0-Y/0-Z/0-A 절), 뒤집힌 옛
|
||||
재디스패치 모델 전문은 `archive/dispatch-hintvalue-model-reversed.md`.
|
||||
|
||||
1. **구현 시작 — 루트 `ROADMAP.md`의 M0부터.** 설계 단계는 2026-08-04 로드맵
|
||||
인수인계 라운드로 종료. `research/pre-implementation-audit.md` 우선순위1은
|
||||
2026-08-12 열일곱 번째 세션에 마지막 넷(1-3/1-4/1-10/1-11)까지 전부
|
||||
해소되어 **11개 전원 완료** — 이 항목(우선순위1) 기준 남은 유일한
|
||||
게이트는 아래였음(**[2026-08-13 4차 감사 정정] 위 0번 항목의 0-Y/0-Z가
|
||||
이후 같은 날 발견돼 실제로는 게이트가 하나 더 있었음 — "유일한"은 그
|
||||
발견 전 서술. **[13차 세션 재정정] 그중 0-Y는 해소됐고 지금 남은
|
||||
게이트는 0-Z 하나**, 다만 0-Y가 남긴 구현 규약
|
||||
`base/typing-limits.md`는 M0 착수 전에 읽어야 함**):
|
||||
해소되어 **11개 전원 완료**. **[14차 세션 기준] 0-Y/0-Z/0-A까지 전부
|
||||
해소돼 설계 게이트는 남아있지 않음** — 착수 전 읽을 것은 위 0번의 두
|
||||
문서(`typing-limits.md`/`dispatch-core-plan.md`)뿐이고, 스파이크 상태는
|
||||
아래 그대로:
|
||||
- **`.claude/luau-test/`(2026-08-09 신설, 2026-08-13 기준 20개) 스파이크
|
||||
결과 — [2026-08-13 여섯 번째 세션에 첫 실측 완료, 대부분 닫힘].**
|
||||
**상태의 소스는 `.claude/luau-test/STATUS.md`**(pass / 사람 결정 필요 /
|
||||
|
|
@ -231,9 +212,11 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션
|
|||
`SyntaxError` / `types.*` 실제 API 불일치). (3) 그 외엔 그대로 M0
|
||||
실제 코드 작성에 재사용.
|
||||
- **[주의] 위 "남은 것은 셋뿐"은 여섯 번째 세션 기준** — 13차 세션에
|
||||
`08`이 `done/`으로 가며 `review-required/`가 비었음. **개수의 소스는
|
||||
항상 `luau-test/STATUS.md`.** 지금 M0 착수를 막는 건 0-Z 하나이고,
|
||||
0-Y가 남긴 규약(`base/typing-limits.md`)은 착수 전 필독.
|
||||
`08`이 `done/`으로 가며 `review-required/`가 비었고, **14차 세션에
|
||||
`04`가 하강 diff 재설계로 전제가 바뀌어 `rewrite-required/`로 갔음**.
|
||||
**개수의 소스는 항상 `luau-test/STATUS.md`.** 지금 M0 착수를 막는
|
||||
설계 결정은 없고, 0-Y/0-A가 남긴 규약(`base/typing-limits.md`/
|
||||
`base/dispatch-core-plan.md`)은 착수 전 필독.
|
||||
- 참고로 `04`(인덱스 기반 재설계 반영)와 `19`의 B/C 섹션(폐기된
|
||||
`rawNew`+`owners`/3분기 `claimOwner` 검증하던 것)은 **둘 다 여섯
|
||||
번째 세션에 재작성 완료**되어 통과 상태 — 더 이상 대기 항목 아님.
|
||||
|
|
@ -965,8 +948,9 @@ handoff용 저장이 불필요해짐 — `Relate`는 여러 위치/사이클을
|
|||
무력화, `SlotHandler`의 claim 실패 시 이중 파괴, `Ref` dedup 무력화).
|
||||
이어 사용자 결정으로 **`State<Slot>` 교체를 파괴→언마운트로 전환**(포탈이
|
||||
그 귀결이 됨), **`dispose(value)`**(트리가 요구하면 파괴 거부·error) 신설,
|
||||
**재디스패치를 "하강 diff"로 재설계**(`research/dispatch-redispatch-diff-plan.md`,
|
||||
**base 미반영 — `question.md` 0-Z 하나 남음**). `ROADMAP.md`/base 계약 개수
|
||||
**재디스패치를 "하강 diff"로 재설계**(당시 `research/`의 설계안 —
|
||||
**base 미반영, `question.md` 0-Z 하나 남음**이었고 14차 세션에 확정·반영 후
|
||||
`archive/dispatch-hintvalue-model-reversed.md`로 이전). `ROADMAP.md`/base 계약 개수
|
||||
모순/`luau-test` stale도 정리. 마지막으로 `luau` 바이너리가 생겨 **첫 실측**
|
||||
— 런타임 12개 전원 통과, `04`가 위 버그를 음성 대조군으로 재현, `07` 보강으로
|
||||
GC-native 전제 확정, `18`이 `Relate` 순환 경고 실증. 타입에선 `:Compute(fn)`
|
||||
|
|
@ -1025,7 +1009,7 @@ Slot 언마운트 전환 미반영 6곳. 뒤집힌 "폐기, 옮기지 않음 + p
|
|||
**1단계 분할**(2989→2263줄, `ref-plan.md`/`event-plan.md`/`brand-plan.md`로
|
||||
순수 이동) — 남은 디스패치·반응형 코어는 0-Z 반영 때 **어차피 전면 재작성**
|
||||
대상이라 그 패스에서 같이 가르는 게 총 변경량·위험이 작다고 판단해 의도적
|
||||
연기(`dispatch-redispatch-diff-plan.md` 6절에 지시). (3) `question.md`를
|
||||
연기(당시 재디스패치 설계안 6절에 지시 — 14차 세션에 실제로 그렇게 처리됨). (3) `question.md`를
|
||||
**사용자가 답할 것만**으로 축소(525→279줄, 해소분은
|
||||
`archive/question-resolved.md`). (4) 재발 방지는 규율 문서가 아니라
|
||||
**검사기**로 — `.claude/tools/doc-check.py` 신설(깨진 파일/절 참조, 색인
|
||||
|
|
@ -1054,7 +1038,7 @@ CLAUDE.md "작업 방식"에 중대 변경 핸드오버 체크리스트 6단계
|
|||
정독. 열 번째 세션이 이미 고친 것과 같은 종류의 stale이 두 곳 더 남아있던
|
||||
게 핵심 발견 — `question.md` 0-Z/0-A와 `HUMAN_TODO.md` 4번이 여전히
|
||||
"6개 문서"(ref-plan.md 누락)로 서술 중이던 것을 "7개"로 정정(같은 정정이
|
||||
`dispatch-redispatch-diff-plan.md`/`CLAUDE.md`엔 이미 반영돼 있었으나 이
|
||||
재디스패치 설계안/`CLAUDE.md`엔 이미 반영돼 있었으나 이
|
||||
두 파일엔 안 퍼져 있었음). 별도로 `ROADMAP.md` 백로그의
|
||||
`objectListClass.__newIndex` 재현 테스트 항목이 세 번째 세션에 이미
|
||||
불필요로 해소됐는데 그 반영이 이 파일에만 안 퍼져 있던 것도 정정. base/
|
||||
|
|
@ -1093,3 +1077,26 @@ research/reference/luau-test/archive 전체는 정합성 문제 없음 확인
|
|||
배너뿐 아니라 본문 표·문단·결론까지 전수 수정(체크리스트 2번 준수).
|
||||
교훈 — **`luau-analyze` 진단 0건은 타입 해소를 뜻하지 않음**, 타입
|
||||
스파이크는 `--annotate` + 음성 대조군 필수.
|
||||
|
||||
**2026-08-13 열네 번째 세션 — 0-Z(`Attribute:GetKey`) 확정, 하강 diff 재디스패치
|
||||
전면 반영, Tag/Attribute를 quad-base로 재배치**
|
||||
(`session/2026-08-13-14-attribute-getkey-dispatch-diff-reflected.md`)
|
||||
사용자가 `Attribute:GetKey(name)`으로 0-Z를 다시 열었고, 트레이싱 결과
|
||||
**권고안 (a)(그룹 안 claimant `Relate`)가 그룹↔직접 쓰기 충돌을 못 잡는다**는
|
||||
게 드러나(두 경로가 만나는 말단 핸들러에서 공개 키는 같은 객체라 소유자
|
||||
구분 불가) **그룹 전용 키(비공개 `GetKey`) + `AttributeKeyHandler`의 이름
|
||||
claim**으로 확정. 이걸로 마지막 게이트가 열려 **0-A(하강 diff 재디스패치)까지
|
||||
한 패스로 base 전면 반영** — 9차 세션이 미뤄뒀던 `bind-system-plan.md`
|
||||
2단계 분할을 같이 수행해 디스패치 코어를 **`base/dispatch-core-plan.md`로
|
||||
분리·재작성**(선행 `retractFrom` 폐기, `chains` 슬롯에 `handler` 동거,
|
||||
`retractFrom`이 **3-인자**로 축소, `isX(hintValue)` 가드 규칙 폐지, 깊은
|
||||
체인 힌트 유실 캐비엇 삭제, 점유 체크 폐지). 사용자 제기로 **Tag/Attribute의
|
||||
부기 알고리즘을 통째로 quad-base로 재배치**하고 백엔드는
|
||||
`addTag`/`removeTag(inst,{string})`/`setAttribute(inst,name,v)` 3개 op만
|
||||
주입(웹 `className`/`data-*` 대응 — 안 그러면 같은 참조카운트/소유권
|
||||
알고리즘이 백엔드마다 복제됨), 그 실패 모드를 위해 **`HANDLER_PRIORITY_FALLBACK`**
|
||||
신설(사용자 제안 — base 핸들러는 최하위 밴드, 백엔드가 덮어쓰면 언제나
|
||||
이김). 옛 모델은 `archive/dispatch-hintvalue-model-reversed.md`로 이전,
|
||||
스파이크 `04`/`19`는 옛 모델을 검증 중이라 `rewrite-required/`로 이동.
|
||||
**M0 착수를 막는 결정이 이제 없음** — 새로 연 것은 사소한 둘뿐
|
||||
(`Attribute.Merged` 이름 중복, `hintValue` 이름 재검토).
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
에이전트가 못 하거나(로컬 GUI 조작, 외부 계정/기기 필요) 사용자의 결정이 필요해서
|
||||
멈춰둔 것만 여기 모음. 설계 질문은 대체로 `.claude/question.md`에 따로 있고 디폴트를
|
||||
잡아둔 채 진행 중이라 급하지 않음 — **단 2026-08-13부터는 예외가 생겨 아래 4번에
|
||||
올렸음**(0-Z, M0 착수를 실제로 막고 있고 사용자가 직접 판단하겠다고 한 항목.
|
||||
같이 올렸던 0-Y는 같은 날 열세 번째 세션에 해소됨).
|
||||
올렸었음**(0-Z) — **[2026-08-13 열네 번째 세션] 그 0-Z도 해소되어 지금은
|
||||
사람이 결정해야 M0가 열리는 항목이 없음**(0-Y는 열세 번째 세션에 해소).
|
||||
|
||||
## 1. Roblox Studio에 MCP로 연결 (테스트 자동화용)
|
||||
|
||||
|
|
@ -56,34 +56,27 @@ git.qwreey.moe에 제한된 계정 생성). 로컬 git 저장소는 이미 초
|
|||
내용은 항상 `.claude/`에 자기 문서화(완료 표시, 다음 TODO 갱신)해서 다음 세션이나
|
||||
사람이 바로 이어받을 수 있게 할 것.
|
||||
|
||||
## 4. ⭐ **[2026-08-13 신설, 막고 있음] `question.md` 0-Z 결정**
|
||||
## 4. ~~`question.md` 0-Z 결정~~ **[해소됨, 2026-08-13 열네 번째 세션]**
|
||||
|
||||
**이건 위 3번과 성격이 다름 — 실제로 M0 구현 착수를 막고 있고, 사용자가
|
||||
"직접 스케치하며 판단하겠다"고 명시 이관한 항목**이라 에이전트가 기본값으로
|
||||
밀고 갈 수 없음.
|
||||
**더 이상 사람이 막고 있는 결정이 아님.** 사용자가 같은 세션에 직접
|
||||
`Attribute:GetKey(name)` 방향을 제시했고, 트레이싱으로 검증한 뒤
|
||||
**그룹 전용 키(비공개 `GetKey`) + `AttributeKeyHandler`의 이름 claim**으로
|
||||
확정 → `base/attribute-plan.md` "이름 소유권" 절에 반영. 같이 묶여 있던
|
||||
재디스패치 모델(0-A)도 같은 패스에서 `base/dispatch-core-plan.md`(신설)로
|
||||
전면 반영됐고, ⚠️ 배너를 달고 있던 7개 문서 전부 갱신 완료.
|
||||
|
||||
- **0-Z — Attribute 이름 소유권을 무엇으로 판정할 것인가.** 재디스패치
|
||||
모델을 "하강 diff"로 재설계하면서 유일하게 안 풀린 항목. 사용자 코멘트:
|
||||
"이전 결정(이름별 claimant `Relate`)을 다시 가져오는 게 맞아 보이나, 나중에
|
||||
제가 물리적으로 스케치해보며 심층 분석해보겠습니다."
|
||||
**같은 세션에 사용자가 추가로 결정한 것** — `Tag`/`Attribute`의 알고리즘을
|
||||
통째로 quad-base로 옮기고 백엔드는 `addTag`/`removeTag`/`setAttribute` 세
|
||||
op만 주입(웹의 `className`/`data-*` 대응 때문). 상세는
|
||||
`base/dispatch-core-plan.md` "base가 소유하는 핸들러와 주입되는 엔진 op" 절.
|
||||
|
||||
> **[2026-08-13 열세 번째 세션] 여기 같이 있던 `0-Y`는 해소됨 — 사람이
|
||||
> 결정할 게 더 없음.** 44개 스파이크 재실측으로 원인이 콜백 계약이 아니라
|
||||
> **Luau 자체의 한계**(재귀 제네릭이 다른 타입 인자로 자기를 반환하면 타입
|
||||
> 안전성이 조용히 사라짐)임이 확정됐고, 사용자가 "quad가 타입을 비틀 일이
|
||||
> 아니라 상위 Luau 한계이니 당장 할 수 있는 바 없다"로 정리. 계약은 그대로
|
||||
> 유지, 대응은 "파생 State를 만드는 자리마다 결과 타입 명시 주석 바인딩"
|
||||
> 관례 하나. 규약은 `.claude/base/typing-limits.md`, 근거는
|
||||
> `.claude/audit/type-recursion-issue/`.
|
||||
>
|
||||
> 다만 **거기서 파생된 작은 확인거리 하나가 아래 6번으로 넘어감**(에디터의
|
||||
> Luau 솔버 설정) — M0 착수 때 확인하면 되고 지금 막고 있진 않음.
|
||||
|
||||
**막고 있는 범위**: 0-Z가 정해져야 `bind-system-plan.md`/`tag-plan.md`/
|
||||
`slot-plan.md`/`attribute-plan.md`/`ref-plan.md`/`architecture.md`/
|
||||
`ROADMAP.md` 7개를 새 모델로 한 번에 옮길 수 있음 — 그 전에 M2/M4/M6/M10을
|
||||
구현하면 곧 갈아엎어야 하는 코드를 짜게 됨. 상세/선택지는
|
||||
`.claude/question.md` 최상단.
|
||||
> **[2026-08-13 열세 번째 세션] 여기 같이 있던 `0-Y`도 해소됨** — 44개
|
||||
> 스파이크 재실측으로 원인이 콜백 계약이 아니라 **Luau 자체의 한계**임이
|
||||
> 확정됐고, 대응은 "파생 State를 만드는 자리마다 결과 타입 명시 주석
|
||||
> 바인딩" 관례 하나. 규약은 `.claude/base/typing-limits.md`, 근거는
|
||||
> `.claude/audit/type-recursion-issue/`. 거기서 파생된 작은 확인거리
|
||||
> 하나(에디터의 Luau 솔버 설정)만 아래 6번에 남아 있음 — M0 착수 때
|
||||
> 확인하면 되고 지금 막고 있진 않음.
|
||||
|
||||
## 5. Studio 전용 스파이크 `10` 마저 돌리기 (사람만 가능)
|
||||
|
||||
|
|
@ -120,8 +113,9 @@ M0 착수 시점에 확인하면 되고 지금 막고 있진 않음. 배경은
|
|||
|
||||
디자인 결정 중 Lua/Roblox 엔진에 대한 깊은 경험이 필요한 것들은 합리적 기본값으로
|
||||
진행하면서 `.claude/question.md`에 모아두는 중. 깨어있을 때 훑어보고 기본값이
|
||||
마음에 안 드는 것만 답해주면 됨 — **위 4번(0-Z)을 제외하면** 막고 있는
|
||||
항목은 없음(0-B `dispose` 시그니처는 M6 구현 세부만 막고 M0 착수는 안 막음).
|
||||
마음에 안 드는 것만 답해주면 됨 — **[2026-08-13 열네 번째 세션 기준]
|
||||
M0 착수를 막는 항목은 하나도 없음**(0-W `Ref` 이중 배치는 M4, 0-B
|
||||
`dispose` 시그니처는 M6 구현 세부만 막음).
|
||||
|
||||
---
|
||||
Sources (MCP 리서치): [Roblox/studio-rust-mcp-server](https://github.com/Roblox/studio-rust-mcp-server), [How to Connect Claude Code to Roblox Studio — Clauder Navi](https://www.clauder-navi.com/en/claude-roblox-studio)
|
||||
|
|
|
|||
187
ROADMAP.md
187
ROADMAP.md
|
|
@ -8,16 +8,16 @@ quad-v2 구현 단계 실행 계획. 설계 근거/아키텍처 자체는 여기
|
|||
확정될 때마다 각 마일스톤 체크박스가 계속 갱신돼왔음 — 그래도 아직 M0
|
||||
자체는 시작 안 함.** 다음 세션은 바로 M0부터.
|
||||
|
||||
> **⚠️ M0 착수 자체가 한 결정으로 막혀 있음 — `.claude/question.md`
|
||||
> **0-Z**(Attribute 이름 소유권). "M0 착수 전에 정할 것"으로 명시돼
|
||||
> 있음 — `HUMAN_TODO.md` 4번, `CLAUDE.md` "지금 할 일" 0번 항목 참고.
|
||||
> 아래 M0 체크리스트 자체는 이 결정과 무관하게 유효하지만, 순서상 이게
|
||||
> 먼저.**
|
||||
> **✅ [2026-08-13 열네 번째 세션] M0 착수를 막던 결정이 전부 해소됐음.**
|
||||
> `0-Y`(13차 세션), `0-Z`(Attribute 이름 소유권)/`0-A`(재디스패치 하강
|
||||
> diff, 둘 다 14차 세션) 확정·반영 완료 — `.claude/question.md`의 최우선
|
||||
> 칸이 비었습니다.
|
||||
>
|
||||
> **[2026-08-13 열세 번째 세션] 같이 막고 있던 `0-Y`(`:Compute(fn)` lazy
|
||||
> 핸들 계약)는 해소됨** — 계약은 그대로 유지로 확정, 남은 건 Luau의 현
|
||||
> 한계라 quad가 지금 할 수 있는 게 없음. **다만 구현 시 지켜야 할 규약이
|
||||
> 하나 생겼으니 M0 착수 전에 반드시 읽을 것: `.claude/base/typing-limits.md`**
|
||||
> **다만 M0 착수 전에 반드시 읽을 구현 규약 두 개**:
|
||||
> `.claude/base/typing-limits.md`(0-Y의 산물 — 파생 State마다 결과 타입을
|
||||
> 명시 주석으로 바인딩)와 `.claude/base/dispatch-core-plan.md`(0-A/0-Z의
|
||||
> 산물 — 하강 diff 재디스패치, 3-인자 `retractFrom`, Handler 작성
|
||||
> 체크리스트 8개, `HANDLER_PRIORITY_FALLBACK`, 주입되는 엔진 op).
|
||||
> (특히 "파생 State를 만드는 자리마다 결과 타입을 명시 주석으로 바인딩"과
|
||||
> 7번 체크리스트).
|
||||
|
||||
|
|
@ -76,23 +76,22 @@ 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 감사에서 지적됨.)
|
||||
> **✅ [2026-08-13 열네 번째 세션] 재디스패치 모델 교체 완료 — 아래
|
||||
> 체크리스트는 새 모델("하강 diff") 기준으로 갱신됐습니다.** 래핑 핸들러의
|
||||
> 선행 `retractFrom`은 폐기됐고, `Dispatch.process`가 슬롯의 `handler`를
|
||||
> 먼저 비교해 (같으면 그 자리 클로저에 새 값을 넘기고 자기 `process`
|
||||
> 재호출, 다르면 그 자리부터 전량 철거) 처리합니다. 정본은
|
||||
> `.claude/base/dispatch-core-plan.md`(같은 세션에 `bind-system-plan.md`에서
|
||||
> 분리 신설), 뒤집힌 옛 모델은
|
||||
> `.claude/archive/dispatch-hintvalue-model-reversed.md`.
|
||||
|
||||
|
||||
- [ ] `Dispatch/init.luau` — `Dispatch.getHandler(inst,k,v): Handler?`(순수
|
||||
스캔, `isHandlable`+`priority`) / `Dispatch.process(inst,k,v,index)`
|
||||
(오케스트레이터: 그 인덱스 점유 여부 체크 → getHandler → 매치된
|
||||
핸들러의 `.process`를 불러 **그 반환값(retractor 클로저)을
|
||||
`chains`의 그 인덱스에 저장**) / `Dispatch.retractFrom(inst,k,index,v)`
|
||||
(오케스트레이터: getHandler → **그 인덱스의 기존 핸들러와 비교** →
|
||||
같으면 그 자리 클로저에 새 값을 넘기고 같은 핸들러의 `.process`로
|
||||
자리 교체, 다르면 `retractFrom` 후 새로 설치. 반환값이 `nil`이면
|
||||
즉시 error) / **3-인자** `Dispatch.retractFrom(inst,k,index)`
|
||||
(아래 항목) / `Dispatch.addHandler(handler)`(레지스트리 등록,
|
||||
quad-roblox가 팩토리 뮤테이션 시점에 호출) / `Dispatch.drive(inst,
|
||||
flattened)`(배열→해시 두 패스 순회하며 각 `(k,v)`에
|
||||
|
|
@ -160,17 +159,19 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
바인딩), array part 모든 number 인덱스에 대해 둘 다 호출 필수(생략
|
||||
UB, Handler 구현체 작성자만의 계약) — `recompute`는 leaf-lifetime
|
||||
경로(`bindLifetime`/`unbindLifetime`)로 등록, `:Subscribe()` 아님
|
||||
(2026-08-09 여섯 번째 세션, `base/bind-system-plan.md` "Length/Offset"
|
||||
(2026-08-09 여섯 번째 세션, `base/dispatch-core-plan.md` "Length/Offset"
|
||||
절 — `base/slot-plan.md` "여러 Slot이 섞일 때 순서 보장" 해소)
|
||||
- [ ] 핸들러 계약 검증: `process`가 retractor 클로저를 **반환하지 않는**
|
||||
핸들러를 등록하면 리뷰/린트에서 걸러내기(정리할 게 없어도 항상
|
||||
`function() end`를 반환 — `Dispatch.retractFrom`이 nil 체크 없이
|
||||
호출, `base/bind-system-plan.md` "핸들러 계약" 절, 2026-08-08 세션
|
||||
호출, `base/dispatch-core-plan.md` "핸들러 계약" 절, 2026-08-08 세션
|
||||
/ **2026-08-13 다섯 번째 세션에 별도 `retract` 필드가 `process`
|
||||
반환값으로 합쳐지며 대상만 바뀜**)
|
||||
- [ ] 우선순위 동률/매치 실패 처리(2026-08-12 열일곱 번째 세션 확정,
|
||||
`base/bind-system-plan.md` "우선순위 동률/매치 실패 처리" 절) —
|
||||
`HANDLER_PRIORITY_HIGH`/`_NORMAL`/`_LOW` 등 목적별 우선순위 상수,
|
||||
`base/dispatch-core-plan.md` "우선순위 동률/매치 실패 처리" 절) —
|
||||
`HANDLER_PRIORITY_HIGH`/`_NORMAL`/`_LOW`/**`_FALLBACK`**(base 제공
|
||||
핸들러의 기본 밴드 — 백엔드가 평범한 우선순위로 자기 핸들러를
|
||||
등록하면 언제나 이김, 2026-08-13 열네 번째 세션 신설) 등 목적별 상수,
|
||||
매치 실패(`isHandlable`을 만족하는 핸들러 없음)는 `Brand`+`typeof(v)`
|
||||
출력 후 즉시 error(provider 초기화 확인 안내 포함 — provider
|
||||
미주입 상태도 이 경로로 자동 커버, `pre-implementation-audit.md`
|
||||
|
|
@ -180,25 +181,30 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
leaf 매칭 Handler, `StoreBind.luau`와 같은 층위(범용/엔진무관) —
|
||||
quad-base 소속으로 확정(2026-08-08 두 번째 세션, `base/
|
||||
bind-system-plan.md` "Dispatch는 프리미티브가 아니다" 절)
|
||||
- [ ] `chains`(Relate 기반, `{[inst(weak)]={[k]={[index]=retractor}}}` —
|
||||
**핸들러 배열이 아니라 재귀 깊이 인덱스→retractor 클로저 맵**) +
|
||||
`Dispatch.retractFrom(inst,k,index,v)` — 재귀 재-dispatch(StoreBind/
|
||||
NoneHandler)의 정리를 다단 체인까지 정확히 전파(2026-08-08 세 번째
|
||||
세션 신설, **2026-08-13 다섯 번째 세션 인덱스 기반 전면 재설계** —
|
||||
`base/bind-system-plan.md` "Dispatch 체인" 절,
|
||||
`pre-implementation-audit.md` 1-2번 "이전 핸들러 추적" 항목 해소).
|
||||
`Dispatch.process(inst,k,v,index)`가 반환 클로저를 그 인덱스에
|
||||
저장하는 것도 이 항목에 포함. **구현 시 반드시 지킬 것**:
|
||||
- [ ] `chains`(Relate 기반, `{[inst(weak)]={[k]={[index]={handler, retractor}}}}`
|
||||
— **재귀 깊이 인덱스 → (담당 핸들러, 그가 반환한 retractor 클로저)**) +
|
||||
**3-인자** `Dispatch.retractFrom(inst,k,index)` — 재귀 재-dispatch
|
||||
(StoreBind/NoneHandler)의 정리를 다단 체인까지 정확히 전파(2026-08-08
|
||||
신설 → 2026-08-13 다섯 번째 세션 인덱스화 → **같은 날 열네 번째 세션
|
||||
하강 diff로 전면 교체**, `base/dispatch-core-plan.md` "Dispatch 체인"
|
||||
절, `pre-implementation-audit.md` 1-2번 "이전 핸들러 추적" 항목 해소).
|
||||
**구현 시 반드시 지킬 것**:
|
||||
- **재디스패치는 하강 diff** — 래핑 핸들러는 선행 `retractFrom`을
|
||||
부르지 않고 그냥 `Dispatch.process(inst,k,realv,index+1)`. 비교는
|
||||
`Dispatch.process` 안에서: 슬롯의 `handler`가 같으면 그 자리
|
||||
클로저에 새 값을 넘긴 뒤 같은 핸들러의 `process`로 자리 교체,
|
||||
다르면 `retractFrom(inst,k,index)` 후 새로 설치
|
||||
- `chains:SetStrong(inst,k,list)`는 `handler.process` 호출 **전에** —
|
||||
뒤에 두면 재귀 위임이 자기 테이블을 만들었다가 바깥이 덮어써
|
||||
하위 retractor가 통째로 유실됨(2026-08-13 감사에서 잡힌 버그)
|
||||
- `handler.process` 호출 전에 그 인덱스에 no-op 점유 마커를 박아
|
||||
`list`를 구멍 없는 시퀀스로 유지(hole 있는 테이블의 `#`는 Lua가
|
||||
보장 안 함) + 같은 index 재진입도 가드에 걸리게
|
||||
- 점유 체크는 `getHandler`/`handler.process`보다 **먼저**(핸들러
|
||||
부작용 낭비 없음)
|
||||
- 새 자리를 여는 (B) 분기에선 `handler.process` 호출 전에 no-op
|
||||
점유 마커를 박아 `list`를 구멍 없는 시퀀스로 유지(hole 있는
|
||||
테이블의 `#`는 Lua가 보장 안 함)
|
||||
- `process`가 `nil`을 반환하면 (A)/(B) 양쪽에서 즉시 error
|
||||
- 다른 키로 위임할 땐 항상 `index=1`, 같은 키 재귀는 `index+1`;
|
||||
`Dispatch.drive`의 진입도 항상 `1`
|
||||
- **소유권 충돌 감지는 Dispatch의 일이 아님**(옛 점유 error 폐지) —
|
||||
필요한 도메인이 직접(Attribute 이름 claim, M10)
|
||||
- [ ] mock 대상 테스트
|
||||
|
||||
## M3 — Store/State/Source
|
||||
|
|
@ -255,14 +261,14 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
|
||||
## M4 — 첫 end-to-end 반응형 업데이트
|
||||
|
||||
> **⚠️ M2의 ⚠️ 배너와 같은 주의** — 재디스패치 모델이 교체 예정,
|
||||
> `question.md` 0-Z 먼저 해소할 것.
|
||||
> **✅ [2026-08-13 열네 번째 세션] 재디스패치 모델 교체 완료** — 아래
|
||||
> 항목은 `base/dispatch-core-plan.md`의 하강 diff 기준으로 읽을 것.
|
||||
|
||||
|
||||
- [ ] `Dispatch/StoreBind.luau`(재귀 재실행 로직, 엔진 무관 — 재-dispatch
|
||||
전 `Dispatch.retractFrom(inst,k,index+1,realv)` 호출 필수, 그 다음
|
||||
`Dispatch.process(inst,k,realv,index+1)`. `base/bind-system-plan.md`
|
||||
"Dispatch 체인" 절)
|
||||
- [ ] `Dispatch/StoreBind.luau`(재귀 재실행 로직, 엔진 무관 — **선행
|
||||
`retractFrom` 없이** `Dispatch.process(inst,k,realv,index+1)` 한 줄
|
||||
(2026-08-13 열네 번째 세션 하강 diff), 반환 클로저는 자기 Observer
|
||||
구독만 해제. `base/dispatch-core-plan.md` "Dispatch 체인" 절)
|
||||
- [ ] mock 대상으로 "store 값 바꾸면 `process`가 다시 호출된다" +
|
||||
"이전 값이 다른 타입이면 이전 `process`가 반환했던 retractor 클로저가
|
||||
정확히 불린다" 확인 + **`State<State<T>>`(값이 또 State/Source)가
|
||||
|
|
@ -281,10 +287,12 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
|
||||
## M6 — Slot
|
||||
|
||||
> **⚠️ M2의 ⚠️ 배너와 같은 주의** — 아래 "`SlotHandler.process`는 claim
|
||||
> 실패 시에도 파괴적 클로저를 반환해야 함(`retractFrom`은... 항상 소비)"
|
||||
> 항목은 현행(교체 예정) 재-dispatch 모델을 전제로 쓰여 있음.
|
||||
> `question.md` 0-Z 먼저 해소할 것.
|
||||
> **✅ [2026-08-13 열네 번째 세션] 재디스패치 모델 교체 완료** — 아래
|
||||
> "`SlotHandler.process`는 claim 실패 시에도 파괴적 클로저를 반환해야 함"
|
||||
> 항목은 새 모델에서도 그대로 유효함(체인은 클로저를 early-return
|
||||
> 여부와 무관하게 항상 소비 — `base/dispatch-core-plan.md` "Handler 작성
|
||||
> 체크리스트" 1번). 클로저가 받는 값이 항상 `Slot`이거나 `nil`임이
|
||||
> 계약으로 보장된다는 점만 새로 추가됨.
|
||||
|
||||
- [ ] **[2026-08-13 여섯 번째 세션 — 이 세션의 Slot 결정 전부, 구현 전 필독]**
|
||||
- **`State<Slot>` 교체 = 파괴가 아니라 언마운트**(`state<Frame>`와 동일).
|
||||
|
|
@ -402,7 +410,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
**`Slot.Offset: Source<number>`도 `Slot.Length`처럼 공개 필드로
|
||||
노출 — Slot 마운트 시점에 `Dispatch.setOffsetSource`가 등록하는
|
||||
바로 그 Source를 `self.Offset`으로도 저장**(2026-08-11 세션,
|
||||
`base/bind-system-plan.md`의 "Slot.Length와 Slot.Offset은 별개" 절)
|
||||
`base/dispatch-core-plan.md`의 "Slot.Length와 Slot.Offset은 별개" 절)
|
||||
- [ ] base `Dispatch/Slot.luau`(추상 재조정, mount/unmount/reposition 3훅) +
|
||||
quad-roblox `Handlers/Slot.luau`(실제 Parent 조작 + reposition —
|
||||
`SetSiblingIndex` 또는 `LayoutOrder` 기반이면 no-op, 구현 선택)
|
||||
|
|
@ -504,9 +512,10 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
(`bind-system-plan.md`의 `None` 센티널 절, M2 dispatch 엔진의
|
||||
"이전 매치 핸들러 추적" 항목과 함께 구현 — `StoreBind` 핸들러와
|
||||
동일한 재귀 재디스패치 패턴이라 새 메커니즘 아님) — `None` 센티널
|
||||
자체는 확정 완료지만, **⚠️ M2 배너와 같은 주의**: `NoneHandler`가
|
||||
쓰는 재-dispatch 배관(선행 `retractFrom` 호출)은 재디스패치 모델
|
||||
교체 대상이라 `question.md` 0-Z 먼저 해소할 것
|
||||
자체는 확정 완료. **[2026-08-13 열네 번째 세션 갱신]** `NoneHandler`가
|
||||
쓰는 재-dispatch 배관에서 **선행 `retractFrom` 호출은 폐기됨** —
|
||||
그냥 `Dispatch.process(inst,k,nil,index+1)` 한 줄
|
||||
(`base/dispatch-core-plan.md`)
|
||||
- [ ] 프로퍼티류 필드 타입에 `T' = T | Tween<T>` 치환 반영(타입 생성
|
||||
스크립트가 `Position: UDim2` 자리를 `UDim2 | Tween<UDim2>`로 만들면
|
||||
끝, Modifier 런타임/`__index` 자체엔 변경 없음 — `modifier-plan.md`
|
||||
|
|
@ -556,9 +565,11 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
|
||||
## M10 — Event / OnChange / Attribute / Tag
|
||||
|
||||
> **⚠️ M2의 ⚠️ 배너와 같은 주의** — 재디스패치 모델이 교체 예정이고,
|
||||
> 특히 **Attribute 이름 소유권(`question.md` 0-Z)이 이 마일스톤의 직접
|
||||
> 대상**임. 0-Z 먼저 해소할 것.
|
||||
> **✅ [2026-08-13 열네 번째 세션] 0-Z(Attribute 이름 소유권)/0-A(하강
|
||||
> diff) 확정 완료** — 이 마일스톤의 Tag/Attribute 항목은 **quad-base로
|
||||
> 재배치**됐고(엔진 op `addTag`/`removeTag`/`setAttribute`만 주입),
|
||||
> 이름 소유권은 그룹 전용 키 + `AttributeKeyHandler`의 이름 claim이
|
||||
> 판정함. 정본은 `base/attribute-plan.md`/`base/tag-plan.md`.
|
||||
|
||||
|
||||
- [ ] `Handlers/Event.luau`(`ReflectionService` 기반 자동 판별)
|
||||
|
|
@ -567,33 +578,47 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
명시, 이름별 weak 캐시로 `OnChange(a) == OnChange(a)` 동등성 보장
|
||||
(`AttributeKey`와 동일 기법), `base/onchange-plan.md`, 2026-08-10
|
||||
세션 확정·2026-08-11 아홉 번째 세션 후속(캐시))
|
||||
- [ ] `Handlers/AttributeKey.luau`(단일 키 `AttributeKey<<T>>(name)`/
|
||||
`BooleanAttribute`류 DI 키 팩토리+Handler — 메커니즘/`None`/`retract`
|
||||
불필요 확정, 이름별 weak 캐시로 동등성 보장, 타입 파라미터화 이름만
|
||||
착수 전 확인, `base/attribute-plan.md`)
|
||||
- [ ] **[2026-08-13 열네 번째 세션 재배치] `quad-roblox/EngineOps.luau` —
|
||||
주입되는 엔진 op 3개**: `addTag(inst,{string})`/`removeTag(inst,{string})`
|
||||
(`CollectionService`), `setAttribute(inst,name,v)`(`v==nil`이면 삭제).
|
||||
`RobloxFactory`가 `BaseModule`에 주입(`bindLifetime`/`canExecute`와
|
||||
같은 패턴) — 아래 base 핸들러들이 이걸 호출함
|
||||
(`base/dispatch-core-plan.md` "base가 소유하는 핸들러와 주입되는
|
||||
엔진 op" 절)
|
||||
- [ ] `quad-base/AttributeKey.luau`(단일 키 `AttributeKey<<T>>(name)` +
|
||||
이름별 weak 캐시로 동등성 보장 + 스칼라 편의 패밀리
|
||||
`String`/`Number`/`BooleanAttribute` — 엔진 고유 타입 패밀리
|
||||
(`Color3Attribute`류)만 quad-roblox의 `D`/`DI` 층에서 각자 추가.
|
||||
타입 파라미터화 이름만 착수 전 확인, `base/attribute-plan.md`)
|
||||
- [ ] `quad-base/Dispatch/AttributeKey.luau`(`AttributeKeyHandler` —
|
||||
`setAttribute(inst,name,v)`를 `v`가 뭐든 무조건 호출 + **이름
|
||||
claim**(`nameClaims` Relate, 다른 키 객체가 같은 이름에 들어오면
|
||||
즉시 error, 반환 클로저는 자기 claim만 반납하고 엔진 부작용 없음).
|
||||
`HANDLER_PRIORITY_FALLBACK`으로 등록. `question.md` 0-Z 결정 —
|
||||
`base/attribute-plan.md` "이름 소유권" 절)
|
||||
- [ ] `Attribute.luau`(quad-base — 그룹 값 타입+API: `Attribute(store1,
|
||||
store2, ...)`/`Merged`, `Tag`와 동형 array-part 값 객체,
|
||||
store2, ...)`/`Merged`/`:NameMap`, `Tag`와 동형 array-part 값 객체,
|
||||
`base/attribute-plan.md`)
|
||||
- [ ] `Handlers/Attribute.luau`(quad-roblox — 그룹 `process`가 이름마다
|
||||
공개 `AttributeKey(name)`로 `Dispatch.process(inst,key,source,1)`만
|
||||
부르고, 반환 클로저가 **자기가 등록한 이름 전부**에
|
||||
`Dispatch.retractFrom(inst,key,1,nil)`. 실제 `SetAttribute`/store-bind
|
||||
구독은 전부 단일 키 경로 재사용(중복 구현 없음).
|
||||
**`process` 안에서 `retractFrom`을 먼저 부르면 안 됨** — 인덱스 1이
|
||||
무조건 비워져 소유권 충돌 점유 체크가 무력화됨(2026-08-13 감사),
|
||||
`base/attribute-plan.md` "메커니즘" 절)
|
||||
- [ ] `quad-base/Dispatch/Attribute.luau`(`AttributeGroupHandler` — 이름마다
|
||||
**그룹 전용 키**(비공개 `GetKey`, 그룹 값 객체별·이름별 메모이즈)로
|
||||
`Dispatch.process(inst,key,source,1)`만 부르고, 반환 클로저가 자기가
|
||||
등록한 키 전부에 `Dispatch.retractFrom(inst,key,1)`.
|
||||
**`process` 안에서 `retractFrom`을 먼저 부르면 안 됨**(철거는 전적으로
|
||||
클로저 몫). 실제 `setAttribute`/store-bind 구독/이름 claim은 전부 단일
|
||||
키 경로 재사용 — `base/attribute-plan.md` "메커니즘" 절)
|
||||
- [ ] `Tag.luau`(quad-base — 값 타입+immutable clone 체이닝: `Tag(...)`/
|
||||
`:Added`/`:Removed`/`:Contains`/`:Apply`/`Merged`, `base/tag-plan.md`
|
||||
— 2026-08-08 세 번째 세션 array-part 값 객체로 재설계, 구 해시 파트
|
||||
모델은 `archive/tag-hash-key-model-reversed.md`)
|
||||
- [ ] `Handlers/Tag.luau`(quad-roblox — `CollectionService` 글루만,
|
||||
`isHandlable`은 `isTag(v)`. **`AddTag`는 온전히 `process`, `RemoveTag`는
|
||||
온전히 반환 클로저** — 이름별 홀더 집합(`tagNameMap`, 위치 `k` 기준
|
||||
참조 카운트)이 비었을 때만 실제 `RemoveTag`, 그마저도 `hintValue`가
|
||||
그 이름을 `Contains`하면 skip해 깜빡임 방지(전체 삭제 후 재생성
|
||||
금지). `process` 쪽 별도 diff 없음, `kTagMap`도 불필요(클로저가 `v`를
|
||||
직접 캡처) — 2026-08-12 열한 번째 / 2026-08-13 네·다섯 번째 세션,
|
||||
`base/tag-plan.md` "메커니즘" 절)
|
||||
`:Added`/`:Removed`/`:Contains`/`:Apply`/`Merged`/`:Names`,
|
||||
`base/tag-plan.md` — 2026-08-08 세 번째 세션 array-part 값 객체로
|
||||
재설계, 구 해시 파트 모델은 `archive/tag-hash-key-model-reversed.md`)
|
||||
- [ ] `quad-base/Dispatch/Tag.luau`(`TagHandler` — `isHandlable`은 `isTag(v)`.
|
||||
**`addTag`는 온전히 `process`, `removeTag`는 온전히 반환 클로저** —
|
||||
이름별 홀더 집합(`tagNameMap`, 위치 `k` 기준 참조 카운트)이 비었을
|
||||
때만 실제 `removeTag`, 그마저도 클로저가 받은 새 값이 그 이름을
|
||||
`Contains`하면 skip해 깜빡임 방지. 제거할 이름은 모아서 **한 번에**
|
||||
`removeTag(inst, names)`. `process` 쪽 별도 diff 없음, `kTagMap`도
|
||||
불필요(클로저가 `v`를 직접 캡처). `HANDLER_PRIORITY_FALLBACK`으로
|
||||
등록 — 2026-08-12 열한 번째 / 2026-08-13 네·다섯·열네 번째 세션,
|
||||
`base/tag-plan.md`)
|
||||
|
||||
## M11 — Tween
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue