design: M2/M3 마일스톤 순서 교체 — 반응형 코어를 먼저, 디스패치를 그 위에
`question.md`에 열려 있던 마지막 항목(마일스톤 경계)을 사용자가 **(a) 순서 교체**로 닫았다 — 이제 **M2 = 반응형 코어(Source/State/Store), M3 = 디스패치 엔진**이다. 결정과 기각된 선택지는 `archive/question-resolved.md`의 "마일스톤 경계" 절, 경위와 시행착오는 `session/2026-08-24-02-milestone-order-swap.md`. ## 왜 (a)인가 의존이 양방향처럼 보였지만 실제로는 한 방향이었다 — 디스패치→반응형은 *본체* 의존(`setLength`가 `State<number>`를, `setOffsetSource`가 `Source<number>`를 받고 `recompute`가 `offset:Set()`을 부름)이라 우회 불가고, 반대는 *등록 표면* 의존(핸들러 셋)이라 미루면 그만이다. 옛 순서로는 디스패치 마일스톤의 `mock 대상 테스트` 체크박스조차 State 없이 불가능했고, 반응형 코어는 순수 Lua라 `luau`만으로 단독 테스트가 된다. (b) 분할은 기존 `M2` 참조 전부를 "M2a인가 M2b인가"로 만들어 오히려 비싸고, (c) 유지는 "순서의 소스는 `ROADMAP.md`"라는 원칙을 스스로 무효화한다. ## 그냥 맞바꾸기로는 안 끝났다 - **`Brand`/`Relate`/`LifetimeHandle`(인터페이스)이 M2 앞머리 `### 공통 기반` 절로** — 반응형이 이 셋을 먼저 요구한다(`Source`가 `SourceBrand`+`EpochBrand`에 등록되고, State 전파 루프가 매 발화마다 `canExecute`를 부르며, 그 판정이 `Relate` 위에 얹힌다). 셋 다 State-free 이자 dispatch-free라 어느 쪽에도 안 걸린다. - **2026-08-22에 디스패치로 앞당겼던 `EpochMap`/`GateNode`/`Blocker`가 M2로 복귀** — 앞당길 이유 자체가 순서 교체로 사라졌다. "게이팅 먼저"는 그대로 지켜진다(게이팅이 디스패치보다 먼저 지어진다). - **Observer/Effect 동적 경로 가드 등록과 `ObserverEffectLeafHandler`는 M3로** — 둘 다 "핸들러를 등록한다"뿐이라 본체와 잘라내기 쉽다. 이것이 M2가 M3에 개념상 지던 유일한 의존이고, 미뤘으므로 빌드 순서엔 역방향 간선이 없다. - **`EpochMap`이 State 본체보다 앞으로**(감사 5라운드 발견) — State가 `valueEpochMap`/`emitEpochMap` 둘을 컴포지션하므로 옛 배치로는 `State.luau`를 못 짠다. 문서 자신이 *"아래 `Source.luau`/`State.luau`가 이걸 전제로"*라 쓰면서 그 둘이 위에 있는 형태로 증거를 남기고 있었다. ## 대가 하나 — 게이트 둘이 "바로 다음"으로 올라왔다 `question.md` 낮은 우선순위에 있던 **중간 State GC 미검증**과 **`store:GetDynamic` 위치**가 원래 반응형(옛 M3)의 게이트였는데, 반응형이 M2가 되면서 착수 직전 항목이 됐다 — 최우선 절로 승격했다. 순수 *설계* 결정 대기는 여전히 0건이다(하나는 실측 미완, 하나는 표면 위치 선택). ## 번호 재부여의 경계 라이브 문서의 `M2`/`M3` 참조 248건은 전량 새 번호로 맞췄다(코퍼스의 참조가 거의 전부 "그 내용이 사는 마일스톤"을 가리켜 기계적 맞교환으로 의미가 보존됨). **`session/`·`archive/`·`qa-request/`는 히스토리라 소급 수정하지 않았다** — 그 문서들의 `M2`/`M3`는 옛 의미(M2=디스패치, M3=반응형)이고, 이 경고를 인덱스 다섯 곳에 박아뒀다. 일괄 치환에 `\bM([23])\b`를 썼다가 Python `\w`가 유니코드라 한글이 붙은 93건(`M2는`/`M2로`)이 안 바뀌는 실수를 냈고, `git show HEAD:<경로>`로 되돌린 뒤 ASCII 경계 lookaround로 재실행했다. 그 부수로 `Relate`/ `LifetimeHandle` 참조들은 구 M2 → 신 M2로 두 번 옮겨져 **번호가 우연히 보존**됐다는 것도 드러났다(치환하면 안 되는 자리 — 감사가 잡아 되돌렸다). ## 검증 `quad-doc-auditor` 루프가 라운드마다 각도를 바꿔 **7라운드에서 새 발견 0건으로 수렴**(10→8→1→1→2→2→0). 가장 값이 큰 건 5라운드(구현 순서 시뮬레이션)로, 위 `EpochMap` 배치 오류를 잡았고 **순서 교체의 전제 자체도 검증**했다(새 M2 전체를 디스패치 심볼로 훑어 "가드 등록 둘 말고는 없음"). **수렴 뒤 사용자가 돌린 `/code-review high`가 5건을 더 잡았고 전부 유효** — 넷이 *"라벨은 치환됐는데 그 라벨을 설명하던 산문이 안 고쳐진"* 종류였고, 그중 하나는 `research/` 안의 **히스토리 블록**이 치환을 맞아 원래 논거가 문장 그대로 거짓이 된 것이었다(소급 수정 제외 대상을 세 폴더로만 잡은 게 샜다). `conventions.md`의 *"`/code-review`는 감사자를 대체하지 않는다"*가 또 재확인됐다. `doc-check.py` ERROR 0. Co-authored-by: qwreey <me@qwreey.moe> Claude-Session: https://claude.ai/code/session_01Jjrec9xAS7TZstMx5gi3cm
This commit is contained in:
parent
19cd046275
commit
56f8269236
26 changed files with 869 additions and 495 deletions
File diff suppressed because one or more lines are too long
|
|
@ -1217,3 +1217,78 @@ hot path다.
|
|||
`base/state-epoch-plan.md` §2, 근거 기록은
|
||||
`reference/epoch-brand-composition.md` §4의 4번.
|
||||
(필드 이름 `Revision`과 타입 `Epoch`는 확정, 승격 시 `base/`에 반영됨.)
|
||||
|
||||
## [해소됨, 2026-08-24] M2/M3 마일스톤 경계 — 순서 교체로 확정
|
||||
|
||||
2026-08-22 `ROADMAP.md` 전반 점검에서 드러난 항목. **설계 결정이 아니라
|
||||
마일스톤 *순서* 문제**라 당시 `question.md`의 최우선 절이 아니라 2번
|
||||
항목으로 따로 뒀었다. 2026-08-24에 사용자가 **(a) 순서 교체**를 선택해
|
||||
같은 날 전량 반영됐다.
|
||||
|
||||
**발단** — 옛 M2(디스패치 엔진)와 옛 M3(Store/State/Source)의 의존이
|
||||
양방향이라 그 순서로는 M2를 끝까지 짤 수 없었다.
|
||||
|
||||
- **디스패치 → 반응형 (본체 의존)**: `Dispatch.setLength`가
|
||||
`len: number | State<number>`를, `Dispatch.setOffsetSource`가
|
||||
`Source<number>`를 받고, `recompute`가 `offset:Set()`을 부른다
|
||||
(`base/dispatch-core-plan.md`의 "Length/Offset" 절). 2026-08-22에
|
||||
디스패치 쪽으로 앞당겼던 `GateNode`/`Blocker`도 State 위에 얹힌다.
|
||||
- **반응형 → 디스패치 (얕은 의존)**: `state:Observer`/`Effect`의 동적 경로
|
||||
가드가 `Dispatch.addHandler` + `Handler.luau` 계약을 쓴다 — 레지스트리
|
||||
등록 표면만 있으면 되므로 디스패치 **전체**를 요구하지 않는다.
|
||||
|
||||
**선택지와 판단** — (a) 순서 교체 / (b) 디스패치를 둘로 분할 / (c) 지금
|
||||
구조를 두고 구현 시 알아서 오간다. **사용자 선택은 (a)**("(a) M2와 M3의
|
||||
순서를 바꾸는게 맞겠던데"). 그 자리에서 같이 확인된 근거:
|
||||
|
||||
1. **의존의 비대칭**이 명확하다 — 무거운 쪽(본체 의존)을 아래에 깔고
|
||||
가벼운 쪽(핸들러 등록)을 뒤로 미루는 게 맞다.
|
||||
2. 옛 순서로는 **디스패치 마일스톤의 `mock 대상 테스트` 체크박스가
|
||||
원리적으로 불가능**했다(`setLength`/`recompute`를 State 없이 테스트할
|
||||
수 없음). 반대로 반응형 코어는 순수 Lua라 `luau`만으로 단독 테스트가
|
||||
된다.
|
||||
3. 2026-08-22의 `EpochMap`/`GateNode`/`Blocker` 앞당김이 **불필요해진다**
|
||||
— 그 이동은 "디스패치가 먼저인데 디스패치가 쟤들을 호출한다"는 이유로
|
||||
한 것이었다. 순서를 바꾸면 마일스톤 내용이 의존 계층과 그대로 일치하고
|
||||
끌어온 항목이 0개가 된다. "게이팅 먼저"는 그대로 지켜진다.
|
||||
|
||||
**(b)를 안 고른 이유**: 참조 비용이 오히려 더 크다 — 기존 `M2` 참조 전부가
|
||||
"M2a인가 M2b인가"로 애매해지는데 이건 순서 교체처럼 기계적으로 못 푼다.
|
||||
게다가 `Brand`를 앞으로 빼는 일은 (b)에서도 똑같이 해야 하고, 얻는 건
|
||||
"디스패치 코어를 조금 일찍 짠다"뿐인데 M4 전까지 그걸 소비하는 게 없다.
|
||||
**(c)를 안 고른 이유**: `project-context.md`가 못박은 *"어떤 순서로
|
||||
만드는가"*의 소스가 `ROADMAP.md`라는 원칙을 스스로 무효화한다.
|
||||
|
||||
**같이 확정된 것 — 순서만 바꿔서는 안 끝났다.** 옛 M2 안에 반응형보다
|
||||
먼저 와야 하는 항목이 셋 섞여 있었고, 이들은 새 M2 앞머리의
|
||||
`### 공통 기반` 절로 옮겨졌다(State-free이자 dispatch-free):
|
||||
|
||||
- **`Brand.luau`** — `Source`가 `SourceBrand`+`EpochBrand`에 등록돼야 함
|
||||
(`base/source-state-plan.md`), `isState`/`isObserver`/`isEffect`가 전부
|
||||
여기 얹힘.
|
||||
- **`Relate.luau`** — `bindLifetime`/`unbindLifetime`이 그 위에 구현됨
|
||||
(`base/lifecycle-pattern.md`의 `InstData`/`BindData`).
|
||||
- **`LifetimeHandle.luau` 인터페이스** — State 전파 루프가 발화마다
|
||||
`canExecute`를, 이중 바인딩 게이트가 `canBound`를 부름. 이미 M8→디스패치로
|
||||
한 번 앞당긴 전례가 있던 항목이라 한 칸 더 앞당긴 것뿐.
|
||||
|
||||
반대로 새 M3로 넘어간 것은 둘 — Observer/Effect **동적 경로 가드 등록**과
|
||||
**`ObserverEffectLeafHandler`**(`H-39`). 둘 다 "핸들러를 등록한다"뿐이라
|
||||
본체와 잘라내기 쉽고, **이것이 M2가 M3에 개념상 지던 유일한 의존**이었다 —
|
||||
그 둘을 M3로 미뤘으므로 **빌드 순서상 역방향 간선은 남지 않는다**:
|
||||
|
||||
```
|
||||
L0 공통 기반 Brand · Relate · LifetimeHandle(인터페이스) ┐
|
||||
L1 반응형 Source/State/Store · EpochMap · Gate/GateNode · ├ M2
|
||||
Blocker · :Compute/:Apply · Observer · Effect ┘
|
||||
L2 디스패치 Handler · Dispatch 코어 · chains · None · ┐
|
||||
setLength/setOffsetSource/getOffsetAt · ├ M3
|
||||
Leaf · 가드 등록 · ObserverEffectLeafHandler ┘
|
||||
```
|
||||
|
||||
**⚠️ 번호 재부여의 부작용** — 라이브 문서의 `M2`/`M3` 참조는 전부 새 번호로
|
||||
맞췄지만(코퍼스의 참조가 거의 전부 "그 내용이 사는 마일스톤"을 가리켜서
|
||||
기계적 맞교환으로 의미가 보존됨), **`session/`·`archive/`·`qa-request/`는
|
||||
히스토리 문서라 소급 수정하지 않았다.** 2026-08-24 이전에 쓰인 그 문서들의
|
||||
`M2`/`M3`는 **옛 의미**(M2=디스패치, M3=반응형)로 읽을 것. 이 경고는
|
||||
`ROADMAP.md`의 M2 배너에도 있다.
|
||||
|
|
|
|||
|
|
@ -483,7 +483,7 @@ end
|
|||
멈춤 — 조용한 오작동이 아니라 **반복 재현되는 시끄러운 실패**. 그래서
|
||||
별도 롤백 장치를 넣지 않음(코퍼스의 "에러=패닉 상태, 그 이후 정합성은
|
||||
관리 대상 아님" 원칙과 같은 결). 원자적 롤백이 필요하다고 판단되면
|
||||
그때 그룹 `process`에만 국소적으로 넣을 수 있음 — `question.md` 3번에
|
||||
그때 그룹 `process`에만 국소적으로 넣을 수 있음 — `question.md` 2번에
|
||||
열어둠.
|
||||
- **`groupKey(v, name)`는 그룹 값 객체별·이름별 메모이즈** — 같은 그룹
|
||||
값이 재프로세스될 때 같은 키가 나와야 claim이 자기 자신과 안 부딪힘.
|
||||
|
|
|
|||
|
|
@ -16,13 +16,16 @@
|
|||
문제를 콜스택/코루틴이 아니라 사용자가 들고 있는 "값"으로 표현**해서 이
|
||||
위험을 구조적으로 우회한다.
|
||||
|
||||
**store 개발(M3)과 밀접하게 연관됨** — `state:Block(blocker)`가 State
|
||||
**store 개발(M2)과 밀접하게 연관됨** — `state:Block(blocker)`가 State
|
||||
위에 얹히는 메소드이므로 `base/source-state-plan.md`의 Source/State
|
||||
온톨로지, 특히 push-invalidate/pull-recompute 전파 모델(`base/source-state-plan.md` "전파 모델 확정" 절)을 전제로 함.
|
||||
**⚠️ [2026-08-22 정정] 구현 마일스톤은 M3가 아니라 M2다** — 여기 "State와
|
||||
같은 마일스톤(`ROADMAP.md` M3)에서 함께 구현할 것"이라고 적혀 있었으나,
|
||||
`Dispatch.drive`의 배치 등록이 `Blocker`를 호출하므로 "게이팅 먼저" 결정에
|
||||
따라 `Blocker.luau` 체크박스가 M2로 이동했다. 별도 파일로 두는 것은 그대로.
|
||||
**[2026-08-24 재확정] 구현 마일스톤은 다시 M2다 — "State와 같은
|
||||
마일스톤에서 함께 구현"이라는 원래 서술이 맞다.** 2026-08-22엔 "게이팅
|
||||
먼저"(`Dispatch.drive`의 배치 등록이 `Blocker`를 호출한다) 결정에 따라
|
||||
`Blocker.luau` 체크박스를 디스패치 쪽으로 앞당겼었는데, 2026-08-24에
|
||||
마일스톤 순서 자체가 교체되어(반응형이 M2, 디스패치가 M3) 앞당길 이유가
|
||||
사라졌다 — 게이팅은 여전히 디스패치보다 먼저 지어진다. 별도 파일로 두는
|
||||
것은 그대로.
|
||||
**그리고 이제 `Blocker`는 바닥부터 짜는 게 아니라 공용 `GateNode`
|
||||
(`base/gate-plan.md`) 위에 얹는 정책이다** — 노드를 다시 만들지 말 것.
|
||||
|
||||
|
|
|
|||
|
|
@ -201,7 +201,9 @@ leading/trailing/통과 후 창 재개방은 **완전히 동일**. 그래서 공
|
|||
생겼으니 이름 붙여 꺼내는 것뿐.
|
||||
|
||||
**[2026-08-21 실현 — 이 권고가 확정됐다]** 그 노드는 `state:Gate(setup)`가
|
||||
만드는 **`GateNode`**이고 **M2**에서 구현된다(`base/gate-plan.md`). 다만
|
||||
만드는 **`GateNode`**이고 **M2**에서 구현된다(`base/gate-plan.md` —
|
||||
**[2026-08-24]** 2026-08-22엔 디스패치 쪽이었다가 마일스톤 순서 교체로
|
||||
반응형 코어로 돌아왔다). 다만
|
||||
"내부 공용"은 아니게 됐다 — **공개 표면**이다. `Debounce`/`Throttle` 쪽
|
||||
관용구는 안 바뀐다: `Debounce{...}`가 돌려주는 팩토리가 내부에서
|
||||
`s:Gate(policy)`를 부르므로 `state:Apply(Debounce{...})`가 그대로 성립한다.
|
||||
|
|
@ -1186,15 +1188,15 @@ additional-primitives-plan.md`가 원래 "안 만들어도 된다"고 판단했
|
|||
맨 뒤로 미뤄도 됨(다른 기능이 이걸 의존하지 않고, 없어도 다른 기능이 안
|
||||
막힘).
|
||||
|
||||
**의존성**: State 코어(`ROADMAP.md` M3) + 백엔드 주입 표면(`setTimeout`/
|
||||
**의존성**: State 코어(`ROADMAP.md` M2) + 백엔드 주입 표면(`setTimeout`/
|
||||
`clearTimeout`) + `Blocker`(gated state) + `Ref`. **[2026-08-22 정정]**
|
||||
각각 **M2**(게이트/`Blocker`) / **M3**(State 코어) / **M8**(`Ref`)에서
|
||||
확정되는 것들이라 그 이후 언제든 얹을 수 있다(옛 표기는 "전부 M3/M8" —
|
||||
`Blocker.luau`가 M2로 옮겨지기 전 기준). **[정정, 2026-08-21 구현 전 QA 5라운드] 그 "게이트 노드를
|
||||
공용으로 빼는" 작업은 M3가 아니라 M2로 앞당겨졌다** — 사용자 결정
|
||||
"게이팅 먼저"(`Dispatch.drive`의 배치 등록이 이미 그 게이팅에 의존하므로).
|
||||
1절에서 봤듯 같은 노드를 공유하므로 따로 하면 같은 걸 두 번 설계하게 되는
|
||||
것은 그대로이고, 바뀐 건 **언제**뿐이다. 표면/이름은 아직 미정 —
|
||||
`base/gate-plan.md`가 소스(이 문서의 1절이 그 일반화를 처음
|
||||
**[2026-08-24 재정리]** 각각 **M2**(State 코어 · 게이트 · `Blocker`) /
|
||||
**M8**(`Ref`)에서 확정되는 것들이라 그 이후 언제든 얹을 수 있다. 2026-08-22엔
|
||||
`Blocker.luau`가 디스패치 쪽으로 앞당겨져 있어서 "게이트/`Blocker`는 다른
|
||||
마일스톤"이라고 적었으나, 2026-08-24 마일스톤 순서 교체로 되돌아왔다. 그
|
||||
"게이트 노드를 공용으로 빼는" 작업도 같은 M2에서 State 코어와 함께 한다 —
|
||||
사용자 결정 "게이팅 먼저"(`Dispatch.drive`의 배치 등록이 이미 그 게이팅에
|
||||
의존하므로)는 그대로 지켜진다. 1절에서 봤듯 같은 노드를 공유하므로 따로
|
||||
하면 같은 걸 두 번 설계하게 되는 것도 그대로다. 표면/이름은 `base/gate-plan.md`가 소스(이 문서의 1절이 그 일반화를 처음
|
||||
권고한 자리로 거기 인용돼 있다). 프리미티브 자체(`Debounce`/`Throttle` 함수)는 그 위에 아무
|
||||
때나 나중에 얹으면 된다.
|
||||
|
|
|
|||
|
|
@ -205,7 +205,7 @@ src/schema/union.luau:48-68`) — 에러 메시지는 즉시 문자열로 만들
|
|||
그래서 `Dispatch.listHandlers()`는 현재 등록된 전체 핸들러(이름/priority)를
|
||||
**반환**하는 함수로 둔다.
|
||||
구현 비용이 거의 없고 실제 개발 중 디버깅에 바로 도움되는 항목이라
|
||||
M2(Dispatch 엔진) 착수 시 기본 기능으로 같이 넣음 — 런타임 플러그인인
|
||||
M3(Dispatch 엔진) 착수 시 기본 기능으로 같이 넣음 — 런타임 플러그인인
|
||||
`quad-debug`(후순위, `research/debug-tooling-plan.md`)와는 다른 층위의,
|
||||
라이브러리 자체에 내장된 개발자 편의 기능.
|
||||
|
||||
|
|
|
|||
|
|
@ -215,7 +215,14 @@ end
|
|||
### 동적 경로 가드 — `k` 무관 매치, `HANDLER_PRIORITY_FALLBACK`
|
||||
|
||||
(2026-08-14 열한 번째 세션, `PreRef`/`Observer`와 같은 패턴, `base/
|
||||
source-state-plan.md`의 "동적 경로 가드" 절 참고.) `EffectHandle`도
|
||||
source-state-plan.md`의 "동적 경로 가드" 절 참고.)
|
||||
**⚠️ [2026-08-24] 이 가드를 실제로 `Dispatch.addHandler`로 등록하는 것은
|
||||
M3(디스패치)다.** `HANDLER_PRIORITY_FALLBACK` 상수도 `Dispatch.addHandler`도
|
||||
M3에서 처음 생기므로, M2(반응형 코어)에서 본체를 짤 때는 **핸들러 정의만
|
||||
준비해두고 등록 호출은 미룬다** — `ROADMAP.md` M3의 "Observer/Effect 동적
|
||||
경로 가드 등록" 체크박스가 그 자리다(2026-08-24 마일스톤 순서 교체의 산물,
|
||||
M2가 M3에 개념상 지던 유일한 의존이라 이쪽으로 미뤄졌다).
|
||||
`EffectHandle`도
|
||||
children 배열 리터럴 전용이라, 해시 파트 named 자리 등으로 동적으로
|
||||
흘러들어오면 명확히 에러내야 함 — `{ priority = HANDLER_PRIORITY_FALLBACK,
|
||||
isHandlable = function(inst,k,v) return isEffect(v) end, process =
|
||||
|
|
@ -472,7 +479,7 @@ Effect의 의존성이 될 방법이 아예 없다.** 사용자 제기: *"Effect
|
|||
같이 처리해야 한다(위 `E-10`/`EF-5`와 같은 함정). 사용자 확인: *"어차피
|
||||
모든 옵져버들이 내부에 들어가 있을것이므로 가능하다."*
|
||||
|
||||
**우선순위**: 새 코어 메커니즘이 아니라 `Effect` 표면 확장이므로 M3의
|
||||
**우선순위**: 새 코어 메커니즘이 아니라 `Effect` 표면 확장이므로 M2의
|
||||
`Effect` 구현과 같이 간다. **[2026-08-21]** 여기 있던 "억제 장치 때문에
|
||||
`Gate`보다 뒤"라는 순서 제약은 **없어졌다** — 억제가 `Effect` 내부 플래그로
|
||||
확정돼 `Gate`에 안 걸린다.
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@
|
|||
메커니즘을 `state:Gate(setup)` **메소드**로 확정했다 — *"Gate 는 따로 프리미티브
|
||||
없이 `state:Gate( (emit) -> ()->() )` 처럼 선언되고 마치 Compute 처럼
|
||||
GateNode(ComputeNode 처럼) 생성된다 그리고 Blocker 는 해당 내부 배선을 따른다
|
||||
← 동의합니다 해당 방법대로 확정하면 됩니다."* 구현은 **M2**("게이팅 먼저"
|
||||
결정, `ROADMAP.md`). **남은 것은 아래 "아직 안 정한 것"의 생명주기와 M2 범위뿐이고, 둘 다
|
||||
← 동의합니다 해당 방법대로 확정하면 됩니다."* 구현은 **M2**(반응형 코어 —
|
||||
**[2026-08-24 정정]** 2026-08-22에 디스패치 쪽으로 앞당겼다가 마일스톤 순서
|
||||
교체로 되돌아옴, `ROADMAP.md`). **남은 것은 아래 "아직 안 정한 것"의 생명주기와 마일스톤 범위뿐이고, 둘 다
|
||||
구현 시 정하면 되는 것들이다 — 사용자 판단이 필요한 항목은 없다** — `/code-review high`가 잡았던 4번(유보된 emit이 싣는 출처)은 같은
|
||||
날 흡수 집합으로 닫혔고, **`setup` 시그니처는 안 바뀌었다.**
|
||||
(**[2026-08-22 표기 정정]** 여기와 4번 제목에 `emit(self)`라 적혀 있었으나
|
||||
|
|
@ -35,18 +36,19 @@ emit을 가로채 내려보낼지 말지를 정책이 정하는** 노드를 **`s
|
|||
## 왜 지금인가 — 두 갈래가 같은 자리를 가리켰다
|
||||
|
||||
1. **`CR-3`(마일스톤 순서)** — `Dispatch.drive`의 배치 등록이 Blocker 게이팅을
|
||||
전제하므로 M2가 M3의 `Blocker.luau`에 구조적으로 의존한다. 사용자 결정:
|
||||
전제하므로 M3가 M2의 `Blocker.luau`에 구조적으로 의존한다. 사용자 결정:
|
||||
**게이팅을 먼저 만든다.** (이건 그 결정에 이르게 된 *당시* 상태 서술이다 —
|
||||
**[2026-08-22] 지금은 `Blocker.luau` 자체가 M2에 있다**, 아래 9번.
|
||||
**⚠️ 다만 그걸로 순환이 닫힌 건 아니다** — `state:Gate`는 State 메소드이고
|
||||
`GateNode`/`Blocker`는 State 위에 얹히므로, M2로 옮겨도 M2→M3 참조는
|
||||
그대로 남는다. 그 사실은 `.claude/question.md` 2번이 별도 미결로 다룬다.)
|
||||
**[2026-08-24] 지금은 `Blocker.luau`가 M2(반응형 코어)에 있고 그 M2가
|
||||
먼저 지어진다**, 아래 9번. 2026-08-22에 잠깐 디스패치 쪽으로 옮겼던 것은
|
||||
마일스톤 순서 교체로 되돌려졌다 — `state:Gate`가 State 메소드이고
|
||||
`GateNode`/`Blocker`가 State 위에 얹히는 이상 게이팅을 State보다 먼저 둘
|
||||
수는 없었고, 그래서 **반응형 전체를 앞으로 옮기는** 쪽으로 풀렸다.)
|
||||
2. **`DT-4`(Debounce/Throttle)** — 공개 `Blocker` API 위에는 시간 기반 게이트를
|
||||
못 얹는다. `base/debounce-throttle-plan.md`가 이미 "게이티드 노드를 내부 공용
|
||||
`Gate`로 일반화하고 그 위에 정책을 얹으라"고 권고해뒀고, Blocker를
|
||||
만들 때 같이 해두지 않으면 같은 설계를 두 번 하게 된다(항목 1과 마찬가지로
|
||||
**당시** 서술은 "M3에서 Blocker를 만들 때"였다 — **[2026-08-22]** 지금은
|
||||
`Blocker.luau`도 M2다, 아래 9번).
|
||||
**당시** 서술은 "반응형 마일스톤에서 Blocker를 만들 때"였다 —
|
||||
**[2026-08-24]** 지금은 `Blocker.luau`도 M2다, 아래 9번).
|
||||
|
||||
**사용자 논거(`DT-4`)** — Blocker + Observer 조합으로는 왜 안 되는가:
|
||||
*"스로틀/디바운싱에 Observer 를 걸어야하는데, 이 옵저버의 emit 이 먼저이냐
|
||||
|
|
@ -330,17 +332,17 @@ end)
|
|||
- **따름정리: `Effect(fn, ...deps)`의 설치 구간 억제는 `Gate` 소비자가
|
||||
아니다** — 아래 7번 참고.
|
||||
|
||||
9. **M2 범위 — [2026-08-22 해소] `Gate`와 `Blocker` 둘 다 M2다.**
|
||||
`Dispatch.drive`의 배치 등록이 실제로 쓰는 건
|
||||
9. **마일스톤 범위 — [2026-08-24 재확정] `Gate`와 `Blocker` 둘 다 M2(반응형
|
||||
코어)다.** `Dispatch.drive`의 배치 등록이 실제로 쓰는 건
|
||||
`blocker:On()`/`OffWithoutEmit()`/`IsOn()`이므로(배치 게이팅 절) 최소한
|
||||
그 세 메서드가 도는 형태까지는 M2에 필요한데, 그렇다고 `Blocker`만
|
||||
M3에 남겨두면 M2가 다시 뒤 마일스톤을 참조하게 된다. 같은 날 `ROADMAP.md`
|
||||
전반 점검에서 `EpochMap.luau`/`GateNode`/`Blocker.luau` **체크박스가 전부
|
||||
M2로 이동**했다(사용자 판단). `Blocker`는 `GateNode`를 다시 만들지 말고
|
||||
그 위의 정책으로 얹을 것.
|
||||
**⚠️ 이 이동이 M2↔M3 의존을 없애지는 않는다** — 셋 다 State 위에
|
||||
얹히므로 M2는 여전히 `Source.luau`/`State.luau`를 필요로 한다. 마일스톤
|
||||
경계를 어떻게 그을지는 `.claude/question.md` 2번(사용자 회신 대기).
|
||||
그 세 메서드가 도는 형태까지는 M3(디스패치)가 요구한다. 2026-08-22엔
|
||||
그래서 `EpochMap.luau`/`GateNode`/`Blocker.luau` 체크박스를 디스패치
|
||||
쪽으로 **앞당겼는데**, 셋 다 State 위에 얹히는 이상 그걸로는 순환이 안
|
||||
닫혔다 — 디스패치가 여전히 `Source.luau`/`State.luau`를 필요로 했다.
|
||||
**2026-08-24에 마일스톤 순서 자체를 교체**(반응형이 M2, 디스패치가 M3)해
|
||||
그 앞당김은 되돌려졌고, 셋은 반응형 쪽으로 복귀했다. 결과적으로
|
||||
"게이팅 먼저"는 그대로 지켜진다 — 게이팅이 디스패치보다 먼저 지어진다.
|
||||
`Blocker`는 `GateNode`를 다시 만들지 말고 그 위의 정책으로 얹을 것.
|
||||
|
||||
## 관련 문서
|
||||
|
||||
|
|
@ -348,5 +350,5 @@ end)
|
|||
- `base/debounce-throttle-plan.md` — "공개 `Blocker` API 위엔 못 얹음" 절이
|
||||
`Gate` 일반화를 처음 권고한 자리, 그리고 정책 쪽 설계 전량.
|
||||
- `base/dispatch-core-plan.md` — "배치 등록을 안전하게 만드는 Blocker 게이팅"
|
||||
절이 M2가 실제로 요구하는 표면.
|
||||
절이 M3가 실제로 요구하는 표면.
|
||||
- `ROADMAP.md` M2/M3.
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
설치·`pesde install` 실행으로 검증]**
|
||||
|
||||
**전제**: 이 문서가 서술하는 건 **M0/M1 스캐폴딩 단계에서 확인된 사실**이지
|
||||
M3 이후 실제 구현이 아님 — `quad-base/src`는 아직 `Relate.luau`/골격
|
||||
M2 이후 실제 구현이 아님 — `quad-base/src`는 아직 `Relate.luau`/골격
|
||||
`New()`/`Debug` 서브시스템뿐이고 `quad-roblox/src`는 비어 있음
|
||||
(`.claude/todos.md`가 여전히 진행 상황의 소스). 여기 적힌 require/pesde
|
||||
규칙은 실제 소스가 늘어나도 안 바뀔 구조적 사실이라 base로 승격했지만,
|
||||
|
|
|
|||
|
|
@ -96,16 +96,20 @@ local quad = New()
|
|||
quad.Dispatch.addHandler(function() end)
|
||||
```
|
||||
`luau-analyze` → `TypeError: Key 'Dispatch' not found in table 'Quad'`.
|
||||
즉 M2가 `Dispatch.luau`를 만들고 `module:RunInit(InitDispatch)` 패턴(이 문서가
|
||||
즉 M3가 `Dispatch.luau`를 만들고 `module:RunInit(InitDispatch)` 패턴(이 문서가
|
||||
이미 예시로 보여준 그 패턴)으로 붙이면 **런타임엔 붙지만 타입엔 영원히 안
|
||||
보인다.** `ROADMAP.md` M5의 quad-roblox 주입 경로도 같은 벽에 부딪힌다 —
|
||||
quad-roblox는 `quad-types`의 좁은 `Quad`만 본다.
|
||||
|
||||
**확정(사용자, 2026-08-24): `quad-types`의 `Quad`를 마일스톤마다 갱신한다.**
|
||||
- **M2**가 `Dispatch: Dispatch` 필드와 그 타입 재수출을 여기 추가한다 —
|
||||
`ROADMAP.md` M2 체크리스트에 **항목으로 명시**한다(지금까지 아무도 이
|
||||
필요성을 항목화해두지 않았다).
|
||||
- 이후 서브시스템도 같은 규칙을 따른다.
|
||||
- 규칙이 쓰인 계기는 `Dispatch`이고, **M3**가 `Dispatch: Dispatch` 필드와
|
||||
그 타입 재수출을 여기 추가한다 — `ROADMAP.md` M3 체크리스트에 **항목으로
|
||||
명시**한다(지금까지 아무도 이 필요성을 항목화해두지 않았다).
|
||||
- **[2026-08-24 정정] 다만 규칙이 *처음 적용되는* 마일스톤은 M2다** —
|
||||
마일스톤 순서 교체로 반응형 코어가 앞에 오면서, `Source`/`State`/`Store`
|
||||
필드 추가가 `Dispatch`보다 먼저 온다(`ROADMAP.md` M2의 `H-25` 파생 항목).
|
||||
- 이후 서브시스템도 같은 규칙을 따른다 — 서브시스템을 붙이는 **모든**
|
||||
마일스톤(M2 · M3 · M6 · M7 · M8 · M10)이 같은 항목을 진다.
|
||||
- **"가벼운 타입 계약"이라는 이 패키지의 존재 이유와 상충하지 않는다** —
|
||||
타입만 재수출하므로 런타임 무게는 안 는다.
|
||||
- 검토했다 기각된 둘: **quad-base 내부만 넓은 로컬 교차 타입**
|
||||
|
|
|
|||
|
|
@ -3383,7 +3383,7 @@ state 가 안전히 성립 못해서, Apply 라는 이름을 그대로 쓰지는
|
|||
- **확정 형태**: `source:SetAndDispose(value)` — `:Set(value)`와 **한 세트로
|
||||
묶인 `Source` 전용 콜론 메서드**. `Set`(언마운트) → 옛 값 `dispose` 순서를
|
||||
안에서 수행하므로 호출부가 `Get()`으로 옛 값을 미리 잡아둘 필요가 없다.
|
||||
- **`state:Apply`는 손대지 않는다** — 시그니처 영향이 없으므로 M3 착수 전
|
||||
- **`state:Apply`는 손대지 않는다** — 시그니처 영향이 없으므로 M2 착수 전
|
||||
결론이 필요하던 항목에서 빠진다(`question.md`/`.claude/todos.md`에서 제거).
|
||||
|
||||
#### 구현상 바뀌어야 하는 것
|
||||
|
|
|
|||
|
|
@ -314,7 +314,7 @@ Observer와 동일한 패턴(외부 weak table, `{[child] = true}` 류)으로
|
|||
유일한 중복 방지 수단이 되면서 "State는 캐싱하는 존재"라는 근거가 더
|
||||
강해짐.
|
||||
|
||||
### ⚠️ 미해결 — 중간 State가 살아남는가(구독 엣지의 방향성) (2026-08-18 구현 전 QA에서 제기, **M3 착수 전 결론 필요**)
|
||||
### ⚠️ 미해결 — 중간 State가 살아남는가(구독 엣지의 방향성) (2026-08-18 구현 전 QA에서 제기, **M2 착수 전 결론 필요**)
|
||||
|
||||
**사용자가 지목한 미검증 항목**: *"확인해봐야 하는게 State -> State ->
|
||||
State -> Observer Leaf Bind 에서 중간 State 는 참조되지 않아도 사라지지
|
||||
|
|
@ -343,7 +343,7 @@ With 등이 있는 경우 parent 와 연결된 상대를 자기 자신에 가지
|
|||
**해야 할 일**: (a) 이 방향성(상류 strong / 하류 weak)을 이 문서의
|
||||
불변식으로 명문화할지 결정, (b) `luau-test`에 실측 스파이크 추가
|
||||
(`07-relate-weak-table-gc.luau`가 연쇄 GC를 이미 다루므로 그 옆에).
|
||||
**미검증 상태로 M3에 착수하면 안 되는 항목** — 아래 "결론"의 "관리 부담은
|
||||
**미검증 상태로 M2에 착수하면 안 되는 항목** — 아래 "결론"의 "관리 부담은
|
||||
작음"은 이 항목이 닫히기 전까지는 잠정이다.
|
||||
|
||||
**결론**: 노드별 캐시 유지(현재 모델) 유지, 플래튼 기각. Modifier가
|
||||
|
|
@ -389,11 +389,12 @@ lazy State 핸들로 통일, 아래 "`:With`/`:Compute` — self 인자도 lazy
|
|||
`Blocker` 참고.** 위 `:With`+`:Compute`만으로는 "state1, state2를 연달아
|
||||
Set하면 결합된 파생값이 두 번 재계산/재대입된다"는 문제(즉시 pull하는
|
||||
store-bind 소비자 기준)는 안 풀림 — 이건 별도 확정 프리미티브
|
||||
`base/blocker-plan.md`가 다룸(**[2026-08-22 정정]** 여기 "State 개발과 같은
|
||||
마일스톤, `ROADMAP.md` M3에서 함께 구현"이라 적혀 있었으나 `Blocker.luau`는
|
||||
**M2**로 이동했고, 바닥부터 짜는 게 아니라 공용 `GateNode`
|
||||
(`base/gate-plan.md`) 위의 정책이다 — 마일스톤 소속의 소스는
|
||||
`blocker-plan.md`의 정정 배너와 `ROADMAP.md` M2). lexical `Batch(fn)`으로 풀려던
|
||||
`base/blocker-plan.md`가 다룸(**[2026-08-24 재확정]** "State 개발과 같은
|
||||
마일스톤, `ROADMAP.md` M2에서 함께 구현"이 맞다 — 2026-08-22엔 `Blocker.luau`가
|
||||
디스패치 쪽으로 앞당겨져 갈라져 있었으나 마일스톤 순서 교체로 되돌아왔다.
|
||||
다만 바닥부터 짜는 게 아니라 공용 `GateNode`(`base/gate-plan.md`) 위의
|
||||
정책이라는 점은 그대로 — 마일스톤 소속의 소스는 `blocker-plan.md`의 정정
|
||||
배너와 `ROADMAP.md` M2). lexical `Batch(fn)`으로 풀려던
|
||||
초기 시도는 코루틴 yield 위에서 구조적으로 위험해 기각됨 —
|
||||
`archive/batch-rejected.md` 참고.
|
||||
|
||||
|
|
@ -665,7 +666,7 @@ deps만 받고 싶어도 `previous`가 2번째 자리를 차지하므로, 그
|
|||
제약상 다른 선택지가 없음(대안은 애초에 이 확장 자체를 안 하는 것뿐).
|
||||
|
||||
**실측 필요 — `luau-test`의 `15-type-compute-trailing-deps-typepack.luau`
|
||||
신규(ROADMAP.md M3 반영).** 순서 문제 자체는 위 정정으로 구조적으로
|
||||
신규(ROADMAP.md M2 반영).** 순서 문제 자체는 위 정정으로 구조적으로
|
||||
풀렸으므로, 스파이크가 실제로 확인할 진짜 불확실성은 (B) 하나로 좁혀짐 —
|
||||
나머지는 그 결론을 뒷받침하는 대조군: (A) 균일 타입 dep 1개를 고정
|
||||
인자로 좁히는 대조군(실패하면 B/C/D를 볼 것도 없이 기반 자체가 문제),
|
||||
|
|
@ -1014,6 +1015,13 @@ retract/Destroy되면 자동으로 정리됨.
|
|||
### 동적 경로 가드 — `k` 무관 매치, `HANDLER_PRIORITY_FALLBACK`
|
||||
|
||||
(2026-08-14 열한 번째 세션, `PreRef`의 동적 경로 가드와 같은 패턴.)
|
||||
**⚠️ [2026-08-24] 이 가드를 실제로 `Dispatch.addHandler`로 등록하는 것은
|
||||
M3(디스패치)다.** `HANDLER_PRIORITY_FALLBACK` 상수도 `Dispatch.addHandler`도
|
||||
M3에서 처음 생기므로, M2(반응형 코어)에서 본체를 짤 때는 **핸들러 정의만
|
||||
준비해두고 등록 호출은 미룬다** — `ROADMAP.md` M3의 "Observer/Effect 동적
|
||||
경로 가드 등록" 체크박스가 그 자리다(2026-08-24 마일스톤 순서 교체의 산물,
|
||||
M2가 M3에 개념상 지던 유일한 의존이라 이쪽으로 미뤄졌다).
|
||||
|
||||
`Observer`도 children 배열 리터럴 전용이라, 해시 파트 named 자리
|
||||
등으로 동적으로 흘러들어오면(타입 우회 버그) 명확히 에러내야 함 —
|
||||
전용 `Handler` 등록: `{ priority = HANDLER_PRIORITY_FALLBACK,
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@
|
|||
**상태**: **확정.** 사용자 제안으로 시작해 같은 날 여러 라운드에 걸쳐 다듬은 뒤
|
||||
**채택 확정**됨 — *"gate 와 epoch 가 제가 만족할만한 정도로 올라왔습니다.
|
||||
채택하면 될것 같아요."*
|
||||
**구현 마일스톤은 둘로 갈린다** — **[2026-08-22 정정]** 여기 "구현은 M3"라고만
|
||||
적혀 있었다. 부기 객체 `EpochMap.luau`와 `Epoch` 인터페이스·리비전 갱신
|
||||
규칙은 **M2**(`GateNode`가 `emitEpochMap`을 쓰므로 — `ROADMAP.md` M2),
|
||||
그걸 `valueEpochMap`/`emitEpochMap` 둘로 컴포지션하는 **State 본체 통합은
|
||||
M3**다.
|
||||
**구현 마일스톤은 전부 M2**(반응형 코어)다 — 부기 객체 `EpochMap.luau`,
|
||||
`Epoch` 인터페이스·리비전 갱신 규칙, 그걸 `valueEpochMap`/`emitEpochMap`
|
||||
둘로 컴포지션하는 State 본체 통합이 전부 한 마일스톤 안이다.
|
||||
**[2026-08-24 재확정]** 2026-08-22엔 `GateNode`가 디스패치 쪽에 있어서
|
||||
`EpochMap`/`Epoch`만 그리 앞당겨져 **둘로 갈려 있었는데**, 마일스톤 순서
|
||||
교체(`ROADMAP.md`의 M2 배너)로 `GateNode`가 반응형으로 돌아오면서 그 분리
|
||||
자체가 없어졌다.
|
||||
|
||||
**⚠️ 이 문서는 `base/source-state-plan.md`의 "전파 모델 확정" 절을 대체하는
|
||||
게 아니라 그 절이 정한 push-invalidate/pull-recompute 위에 **판정 규칙**을
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ Store는 "이름 붙은 Source 모음, 그 이상 아님"으로 더 단순해짐
|
|||
탑레벨"이라는 기존 네이밍 규칙(`base/architecture.md`의 "코드 스타일 —
|
||||
네이밍 케이싱")에도 오히려 더 맞는다. 사용자가 지정한 표기는
|
||||
`GetDynamic<T>(name)`이므로 **일단 콜론 메소드 + 예약 키로 적어두되,
|
||||
M3/M4 구현 전에 어느 쪽인지 확인할 것**(`question.md` 3번).
|
||||
M2/M4 구현 전에 어느 쪽인지 확인할 것**(`question.md` 최우선 절).
|
||||
- 이 패턴은 Store에만 국한되지 않고 **인스턴스 생성까지 관통하는 프로젝트
|
||||
전역 관습으로 확정**됨 — 단 이벤트는 이후 4차 라운드에서 이 관습의
|
||||
**유일한 예외**로 빠졌음(PA님 방식인 문자열 키+런타임 리플렉션으로 전환).
|
||||
|
|
@ -216,8 +216,8 @@ end
|
|||
(flatten) 익명 타입이지만, **Luau는 이름이 아니라 "만족하는가"로 구조적
|
||||
일치를 검사**하므로 문제없이 `Source<string>` 자리에 대입 가능 — 오히려 이
|
||||
방식과 정확히 맞는 조합. 이걸로 `store.key`가 실제로 타입 명시 가능함이
|
||||
확인돼 M0/M3 어느 시점에 검증해도 기술적으로 막힐 위험은 없음 —
|
||||
`ROADMAP.md`의 M0/M3 배치를 강제로 바꿀 필요는 없어짐, 설계 레벨의 검증
|
||||
확인돼 M0/M2 어느 시점에 검증해도 기술적으로 막힐 위험은 없음 —
|
||||
`ROADMAP.md`의 M0/M2 배치를 강제로 바꿀 필요는 없어짐, 설계 레벨의 검증
|
||||
난이도 문제였던 것만 해소. **[2026-08-15] 이 `type function` 접근 자체의
|
||||
실측도 완료** — 스파이크(`luau-test/done/16-type-store-key-
|
||||
typefunction.luau`)는 원래 `types.newfunction` 시그니처 불일치로 깨져
|
||||
|
|
|
|||
|
|
@ -207,5 +207,5 @@ inst 5개만 살린 상태 → 살아남은 payload 5 / 엔트리 5 (기대치
|
|||
| 검증할 것 | 왜 | 출처 |
|
||||
|---|---|---|
|
||||
| ~~`table.insert`가 배열 중간의 구멍을 재사용하는가~~ **[2026-08-24 폐기]** | **전제 자체가 없어졌다** — 6라운드 `H-7`로 `Ref.Callbacks`가 배열에서 `{[callback\|thread] = true}` **해시맵 셋**으로 바뀌었고, 해시맵엔 border 개념도 구멍도 없다(`base/ref-plan.md`). 이 스파이크는 만들지 말 것 | QA 4라운드 `R-11`(폐기), 6라운드 `H-7` |
|
||||
| 중간 State가 상류 strong / 하류 weak 불변식으로 실제로 살아남는가 | `State → State → State → Observer` 체인에서 중간 노드를 강하게 붙잡는 주체가 문서 어디에도 없어 전파가 조용히 끊길 수 있음. **M3 착수 전 필요** | `base/source-state-plan.md`의 "미해결 — 중간 State가 살아남는가" 절, `question.md` 3번 |
|
||||
| 중간 State가 상류 strong / 하류 weak 불변식으로 실제로 살아남는가 | `State → State → State → Observer` 체인에서 중간 노드를 강하게 붙잡는 주체가 문서 어디에도 없어 전파가 조용히 끊길 수 있음. **M2 착수 전 필요** | `base/source-state-plan.md`의 "미해결 — 중간 State가 살아남는가" 절, `question.md` 최우선 절 |
|
||||
| `Visible = false`인 GuiObject의 `AbsoluteSize`/`AbsolutePosition`이 갱신되는가 | `quad-roblox-fastscroll` 설계의 선행 실측. **Studio 필요** — 만들면 `not-run/`행 | `research/fastscroll-plan.md` |
|
||||
|
|
|
|||
|
|
@ -10,12 +10,17 @@ Roblox 엔진에서 동작하는 DOMless UI 렌더러 **quad**를 처음부터
|
|||
지속 가능성 — 빠른 이터레이션보다 정확성/설계 정합성이 우선. 작업 기간은
|
||||
길게 잡음.
|
||||
|
||||
**[2026-08-22 기준] M0(스파이크 검증)/M1(스캐폴딩) 완료, 다음은 M2(디스패치
|
||||
엔진)**(마일스톤이 넘어갈 때 루트 `CLAUDE.md` 머리말도
|
||||
같이 고칠 것 — 같은 상태를 두 곳이 서술하고 있음). **⚠️ 단 M2 착수를 막는
|
||||
순서 문제가 하나 열려 있음** — M2↔M3 양방향 의존, 소스는
|
||||
`.claude/question.md` 2번(설계 결정이 아니라 마일스톤 경계 문제라
|
||||
"설계 게이트는 없다"는 아래·`todos.md` 서술과 모순되지 않음). 저장소 루트에
|
||||
**[2026-08-24 기준] M0(스파이크 검증)/M1(스캐폴딩) 완료, 다음은 M2(반응형
|
||||
코어 — Source/State/Store)**(마일스톤이 넘어갈 때 루트 `CLAUDE.md` 머리말도
|
||||
같이 고칠 것 — 같은 상태를 두 곳이 서술하고 있음). **⚠️ [2026-08-24] M2와
|
||||
M3의 번호·순서가 맞바뀌었다** — 열려 있던 마일스톤 순서 문제가 (a) 순서
|
||||
교체로 닫힌 결과다(경위는 `archive/question-resolved.md`의 "마일스톤 경계"
|
||||
절, 새 구성은 `ROADMAP.md`의 M2 배너). **2026-08-24 이전에 쓰인
|
||||
`session/`·`archive/`·`qa-request/`의 `M2`/`M3`는 옛 의미**(M2=디스패치,
|
||||
M3=반응형)다. 그 교체의 부작용으로 **M2 착수 전에 답이 필요한 항목이
|
||||
`question.md` 최우선 절로 올라왔다**(무엇이 몇 개인지는 그 절이 소스 —
|
||||
여기서 세지 않는다. 설계 게이트가 아니라 실측·표면 선택이라, 설계 게이트가
|
||||
없다는 아래와 `todos.md`의 서술과 모순되지 않음). 저장소 루트에
|
||||
`quad-base/src/`(`New()`/`RunInit`/`AddPlugin`/`Relate`/`Debug`)/
|
||||
`quad-types/src/`/`type-version-check/src/`가 실제로 존재(`quad-roblox/src`는
|
||||
아직 빈 폴더 — M5에서 채워짐), 자세한 진행 상황은 루트 `ROADMAP.md`가
|
||||
|
|
|
|||
|
|
@ -12,18 +12,42 @@
|
|||
|
||||
---
|
||||
|
||||
## ⭐ 최우선 — 설계 결정은 **없음**, 다만 순서 문제 하나 (2026-08-22 갱신)
|
||||
## ⭐ 최우선 — **M2(반응형 코어) 착수 전에 답이 필요한 것 둘** (2026-08-24 갱신)
|
||||
|
||||
> **[2026-08-22] 아래 2번(M2/M3 마일스톤 경계)이 M2 착수를 막습니다.**
|
||||
> 그건 "무엇을 확정할까"가 아니라 "어떤 순서로 짤까"라 성격이 달라서
|
||||
> 이 절이 아니라 2번에 뒀습니다 — **설계 결정 대기는 여전히 0건**입니다.
|
||||
> **[2026-08-24] M2/M3 마일스톤 경계 문제는 닫혔습니다.** 그건 설계 결정이
|
||||
> 아니라 순서 문제라 이 절이 아니라 별도 항목으로 뒀었는데, 사용자가
|
||||
> **(a) 순서 교체**(반응형이 M2, 디스패치가 M3)를 선택해 같은 날 전량
|
||||
> 반영됐습니다 — 결정과 근거는 `archive/question-resolved.md`의
|
||||
> "마일스톤 경계" 절, 새 마일스톤 구성은 `ROADMAP.md`의 M2 배너.
|
||||
>
|
||||
> **⚠️ 다만 그 교체의 부작용으로 아래 둘이 "한 마일스톤 뒤"에서
|
||||
> "바로 다음"으로 올라왔습니다.** 둘 다 반응형(옛 M3, 지금 M2)의 게이트라
|
||||
> 예전엔 급하지 않았는데, 반응형이 먼저 지어지게 되면서 **지금 답이
|
||||
> 필요합니다.** 순수한 *설계* 결정 대기는 여전히 0건입니다 — 하나는 실측
|
||||
> 미완이고 하나는 표면 위치 선택입니다.
|
||||
|
||||
- **[신설, 2026-08-18 구현 전 QA] 중간 State GC 미검증** — `State → State →
|
||||
State → Observer` 체인에서 중간 노드를 강하게 붙잡는 주체가 문서 어디에도
|
||||
없어 전파가 조용히 끊길 수 있음. 방향(상류 strong / 하류 weak)은 사용자가
|
||||
지목했고, **명문화 여부 결정 + `luau-test` 실측이 M2 착수 전에 필요** —
|
||||
`base/source-state-plan.md`의 "미해결 — 중간 State가 살아남는가" 절.
|
||||
- **[신설, 2026-08-18 커밋 전 `/code-review high`] `store:GetDynamic`을
|
||||
콜론 메소드로 둘지, 탑레벨 함수로 둘지** — 콜론 메소드로 두면 Store의
|
||||
lazy `__index`(없는 키를 인덱싱하면 그 자리에서 `Source`를 만들어 저장)와
|
||||
부딪혀서, `__index`가 고정 메소드 테이블을 먼저 확인해야 하고 그 결과
|
||||
**`GetDynamic`이 모든 Store의 예약 키 이름**이 된다(그 이름의 Source는
|
||||
dot-access로 못 만듦). Store 키는 사용자 도메인 데이터 이름이라 충돌
|
||||
확률이 `Modifier`의 예약 이름들보다 높다. 대안은 탑레벨
|
||||
`getDynamic(store, name)` — "특정 프리미티브에 안 묶인 범용 유틸은 소문자
|
||||
탑레벨"이라는 기존 네이밍 규칙에는 오히려 더 맞는다. **M2/M4 착수 전
|
||||
필요**, `base/store-plan.md`의 "타입 추론 문제" 절.
|
||||
|
||||
> **M0 착수를 막던 항목이 전부 해소됐습니다.** `0-Y`(`:Compute(fn)`의
|
||||
> lazy 핸들 계약)는 열세 번째 세션에, `0-Z`(Attribute 이름 소유권)와
|
||||
> `0-A`(재디스패치 하강 diff)는 열네 번째 세션에, **`0-W`(`Ref` 이중
|
||||
> 배치 방지)는 2026-08-14 열한 번째 세션에 확정·`base/` 반영 완료** —
|
||||
> 그래서 이 문서엔 이제 "결정 대기" 절 자체가 없음(비어서 헤딩째로
|
||||
> 삭제). 해소 전 원문과 결론은
|
||||
> 그래서 이 문서의 "결정 대기" 절은 한동안 비어 있었음(위 두 항목이
|
||||
> 2026-08-24 순서 교체로 올라오기 전까지). 해소 전 원문과 결론은
|
||||
> `archive/question-resolved.md`, 뒤집힌 옛 재디스패치 모델은
|
||||
> `archive/dispatch-hintvalue-model-reversed.md`.
|
||||
>
|
||||
|
|
@ -115,42 +139,7 @@
|
|||
- `Store`/`Source`/`Modifier`/`process`/`retract`/`isHandlable`은 업계
|
||||
선례와 잘 맞거나 이미 신중하게 결정된 이름들이라 특별한 문제 없음.
|
||||
|
||||
## 2. M2/M3 마일스톤 경계 — **M2 착수 전에 답이 필요** (2026-08-22 신설)
|
||||
|
||||
**M2(디스패치 엔진)와 M3(Store/State/Source)의 의존이 양방향이라,
|
||||
`ROADMAP.md` 순서대로면 M2를 끝까지 짤 수 없습니다.**
|
||||
|
||||
- **M2 → M3 (본체 의존)**: `Dispatch.setLength`가
|
||||
`len: number | State<number>`를, `Dispatch.setOffsetSource`가
|
||||
`Source<number>`를 받고, `recompute`가 `offset:Set()`을 부릅니다
|
||||
(`base/dispatch-core-plan.md`의 "Length/Offset" 절). 2026-08-22에 M2로
|
||||
옮긴 `GateNode`/`Blocker`도 State 위에 얹힙니다. 즉 **M2는
|
||||
`Source.luau`/`State.luau` 없이는 구현이 안 됩니다.**
|
||||
- **M3 → M2 (얕은 의존)**: `state:Observer`/`Effect`의 동적 경로 가드가
|
||||
`Dispatch.addHandler` + `Handler.luau` 계약을 씁니다 — 이건 레지스트리
|
||||
등록 표면만 있으면 되므로 M2 **전체**를 요구하지 않습니다.
|
||||
**⚠️ 다만 "M2 앞머리 두 항목(`Dispatch/init.luau` + `Handler.luau`)이면
|
||||
된다"고는 말할 수 없습니다** — `Dispatch/init.luau`에는 `Dispatch.drive`가
|
||||
들어 있고, `dispatch-core-plan.md`가 *"적용 지점 — `Dispatch.drive`와
|
||||
`attachSlot`, 각각 자기 owner 키로 별도 Blocker"*라고 확정해 `drive`도
|
||||
게이팅을 씁니다. 즉 그 첫 항목 자체가 State-free가 아닙니다. 선택지 (b)로
|
||||
쪼갠다면 `drive`를 어느 쪽에 두느냐가 경계선이 됩니다.
|
||||
|
||||
**선택지**:
|
||||
- **(a) M2와 M3의 순서를 바꾼다** — 반응형(Source/State/EpochMap/Gate/
|
||||
Blocker)을 먼저 짜고 그 위에 디스패치를 올림. 얕은 쪽(가드 Handler)만
|
||||
뒤로 미루면 됨. 지금까지의 결정 흐름("게이팅 먼저")과 방향이 같음.
|
||||
- **(b) M2를 둘로 쪼갠다** — `Dispatch.getHandler`/`process`/`retractFrom`/
|
||||
`Handler`/`Brand`/`Relate`/`chains`까지가 M2a, State가 필요한
|
||||
Length/Offset·게이팅은 M3 뒤의 M2b로.
|
||||
- **(c) 지금 구조를 두고 구현 시 알아서 오간다** — 로드맵은 "순서"가
|
||||
아니라 "묶음"으로만 읽음.
|
||||
|
||||
**[2026-08-22 기준] 이 항목이 M2 착수를 막습니다** — `.claude/todos.md`
|
||||
0번이 "M2 착수를 막는 설계 항목은 없다"고 하는 것은 **설계** 얘기이고,
|
||||
이건 설계가 아니라 **순서** 문제라 별개입니다.
|
||||
|
||||
## 3. 낮은 우선순위 — 열려 있지만 급하지 않음
|
||||
## 2. 낮은 우선순위 — 열려 있지만 급하지 않음
|
||||
|
||||
- **`Operator` 콤비네이터 슈가 네임스페이스 이름+포함 범위(2026-08-12 신설,
|
||||
같은 날 후속으로 외부 리서치 완료)** — `Sum`/`Product`/`Not`/비트연산 등
|
||||
|
|
@ -176,21 +165,6 @@
|
|||
남은 근거는 편의성과 Slot offset이 밀리고 당겨지는 케이스뿐이라
|
||||
우선순위가 더 내려감 — `state:Flatten()`류 콤비네이터 아이디어는
|
||||
그대로 백로그. 상세는 `research/operator-sugar-plan.md` 마지막 절.
|
||||
- **[신설, 2026-08-18 커밋 전 `/code-review high`] `store:GetDynamic`을
|
||||
콜론 메소드로 둘지, 탑레벨 함수로 둘지** — 콜론 메소드로 두면 Store의
|
||||
lazy `__index`(없는 키를 인덱싱하면 그 자리에서 `Source`를 만들어 저장)와
|
||||
부딪혀서, `__index`가 고정 메소드 테이블을 먼저 확인해야 하고 그 결과
|
||||
**`GetDynamic`이 모든 Store의 예약 키 이름**이 된다(그 이름의 Source는
|
||||
dot-access로 못 만듦). Store 키는 사용자 도메인 데이터 이름이라 충돌
|
||||
확률이 `Modifier`의 예약 이름들보다 높다. 대안은 탑레벨
|
||||
`getDynamic(store, name)` — "특정 프리미티브에 안 묶인 범용 유틸은 소문자
|
||||
탑레벨"이라는 기존 네이밍 규칙에는 오히려 더 맞는다. **M3/M4 착수 전
|
||||
필요**, `base/store-plan.md`의 "타입 추론 문제" 절.
|
||||
- **[신설, 2026-08-18 구현 전 QA] 중간 State GC 미검증** — `State → State →
|
||||
State → Observer` 체인에서 중간 노드를 강하게 붙잡는 주체가 문서 어디에도
|
||||
없어 전파가 조용히 끊길 수 있음. 방향(상류 strong / 하류 weak)은 사용자가
|
||||
지목했고, **명문화 여부 결정 + `luau-test` 실측이 M3 착수 전에 필요** —
|
||||
`base/source-state-plan.md`의 "미해결 — 중간 State가 살아남는가" 절.
|
||||
- **[신설, 2026-08-14 리뷰] `AttributeGroupHandler.process`의 부분 실패
|
||||
롤백** — 이름 순회 도중 소유권 충돌 error가 나면 그 전에 등록된 이름들이
|
||||
이 사이클엔 회수되지 않음(클로저가 안 만들어짐). 피해는 그 인스턴스
|
||||
|
|
@ -205,8 +179,8 @@
|
|||
읽는 게 quad 관습"이라는 언급은 2026-08-06 후속 세션에서 해소 —
|
||||
채택 안 함으로 확정, `base/event-plan.md` "이벤트 핸들러는
|
||||
self(Instance)를 받지 않는다" 절 참고). 사용자가 "quad 개발 완료 전엔
|
||||
착수 못 함"으로 직접 후순위 지정한 건 여전함 — base 설계(M2 Dispatch/
|
||||
M3 Source/M5 `D` 생성자) 시점에 훅 확장 지점만 고려해두면 됨.
|
||||
착수 못 함"으로 직접 후순위 지정한 건 여전함 — base 설계(M3 Dispatch/
|
||||
M2 Source/M5 `D` 생성자) 시점에 훅 확장 지점만 고려해두면 됨.
|
||||
- **문서화 전략(UI 네이밍 컨벤션, Store 부작용을 게임 시스템에서 쓰는
|
||||
패턴)** — `research/documentation-plan.md`(뼈대만). 정식 백로그 항목으로
|
||||
올릴지, 착수 시점을 언제로 볼지 사용자 판단 필요.
|
||||
|
|
@ -228,9 +202,13 @@
|
|||
Instance를 동적 배열 원소로 받을 수 있는지, retract 시 어떻게 다루는지.
|
||||
**Slot 코어 구현(M6) 시점에 확인** — `research/v1-compat-plan.md` 7-3.
|
||||
|
||||
> **없어진 번호에 대해**: 예전 "0번(추가 프리미티브)"과 "2번(구현 착수
|
||||
> 직전 감사 결과)"은 전원 해소되어 통째로 `archive/question-resolved.md`로
|
||||
> 갔음. 우선순위1 11개의 개별 상태가 궁금하면
|
||||
> **⚠️ 번호는 재사용된다 — 옛 문서가 가리키는 번호를 그대로 믿지 말 것.**
|
||||
> 예전 "0번(추가 프리미티브)"과 "2번(구현 착수 직전 감사 결과)"은 전원
|
||||
> 해소되어 통째로 `archive/question-resolved.md`로 갔고, 그 뒤 **"2번"은 두
|
||||
> 번 더 다른 내용으로 재사용됐다** — 2026-08-22엔 "M2/M3 마일스톤 경계"
|
||||
> (2026-08-24 해소·아카이브 이관), 지금은 바로 위 "낮은 우선순위" 절이다.
|
||||
> 옛 세션/아카이브 문서가 `question.md` N번을 가리키면 **그 문서가 쓰인
|
||||
> 시점의 번호**로 읽을 것. 우선순위1 11개의 개별 상태가 궁금하면
|
||||
> `research/pre-implementation-audit.md`가 원본이자 최신.
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -471,10 +471,10 @@ Tween mock 등 동적 동작 포함")와 목적이 다름:
|
|||
설계 시 "고려는 해두되 지금 확정/구현하지는 않는" 참고용 메모로 남김
|
||||
(`ROADMAP.md`의 M2/M3/M5 근처에 훅 확장 지점 존재 가능성만 인지해두는 정도):
|
||||
|
||||
- M2(디스패치 엔진) 구현 시 `process`/`retract` 스캔 루프에 나중에 훅
|
||||
- M3(디스패치 엔진) 구현 시 `process`/`retract` 스캔 루프에 나중에 훅
|
||||
하나를 끼워 넣기 쉬운 모양으로 짜여 있는지 정도만 유의(지금 훅 자체를
|
||||
만들 필요는 없음).
|
||||
- M3(Source) 구현 시 마찬가지로 나중에 weak-registry 등록 훅을 끼우기
|
||||
- M2(Source) 구현 시 마찬가지로 나중에 weak-registry 등록 훅을 끼우기
|
||||
쉬운 생성자 모양인지만 유의.
|
||||
- M5(quad-roblox `D` 제네릭 생성자) 구현 시 caller 정보를 나중에 끼워넣기
|
||||
쉬운 단일 진입점(생성자 함수 하나)인지만 유의 — 이건 이미
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ introspection 로직을 추가하는 거라 라이브러리 복잡도가 늘어
|
|||
`modifier-plan.md` 8번 절), 네임스페이스 이름으로 쓰면 "이 특정
|
||||
모듈"과 "패턴을 가리키는 일반 용어"가 헷갈릴 수 있어 후보에서 제외.
|
||||
|
||||
`.claude/question.md` 3번(낮은 우선순위)에도 반영. 용어 정리 라운드
|
||||
`.claude/question.md` 2번(낮은 우선순위)에도 반영. 용어 정리 라운드
|
||||
(`question.md` 1번, `Brand`/`Tag`류)와 같은 카테고리로 나중에 같이
|
||||
검토해도 됨 — 급하지 않음.
|
||||
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ store.color }`처럼 애니메이션 없이 그냥 반응형으로 값만 바뀌
|
|||
여러 단계(A→B→C)로 겹칠 때 자기 자신의 상태와 위임한 핸들러의 상태가
|
||||
슬롯 하나를 두고 충돌하는 문제가 있어 기각되고, 대신 Dispatch 자신이
|
||||
전체 체인을 배열로 들고 있는 쪽으로 정리됨. 상세는 `base/
|
||||
dispatch-core-plan.md` "Dispatch 체인" 절, `ROADMAP.md` M2/M4. 아래는
|
||||
dispatch-core-plan.md` "Dispatch 체인" 절, `ROADMAP.md` M3/M4. 아래는
|
||||
원래 발견 당시 기록.
|
||||
|
||||
**위치**: `base/dispatch-core-plan.md` "확정된 디스패치 모델" 절 90-91행 —
|
||||
|
|
@ -107,7 +107,7 @@ dispatch-core-plan.md` "Dispatch 체인" 절, `ROADMAP.md` M2/M4. 아래는
|
|||
|
||||
**제안**: `Dispatch/StoreBind.luau`가 "마지막으로 선택된 핸들러" 자체를
|
||||
`(inst, k)`별 상태로 들고 있다가, 새 `realv` 처리 전에 그 핸들러의
|
||||
`retract`를 호출하는 식으로 지금 결정해두는 게 좋아 보임 — M2/M4에서 바로
|
||||
`retract`를 호출하는 식으로 지금 결정해두는 게 좋아 보임 — M3/M4에서 바로
|
||||
부딪힐 지점.
|
||||
|
||||
### 1-3. 우선순위 스캔의 동률 처리, 매치 실패 시 동작이 정의 안 됨 — [해소됨, 2026-08-12 열일곱 번째 세션]
|
||||
|
|
@ -116,7 +116,7 @@ dispatch-core-plan.md` "Dispatch 체인" 절, `ROADMAP.md` M2/M4. 아래는
|
|||
(`HANDLER_PRIORITY_HIGH` 등, "밴드+오프셋" 패턴)로 애초에 안 나게 유도,
|
||||
매치 실패는 조용한 무시 없이 즉시 `error`(브랜드+`typeof` 출력, provider
|
||||
초기화 확인 안내)로 확정. 핸들러 등록 시점 동률 감지 print 경고와
|
||||
`Dispatch.listHandlers()`류 전체 목록 조회 함수도 M2 기본 기능으로
|
||||
`Dispatch.listHandlers()`류 전체 목록 조회 함수도 M3 기본 기능으로
|
||||
확정 — 상세는 `base/dispatch-core-plan.md` "우선순위 동률/매치 실패 처리"
|
||||
절. 아래는 원래 발견 당시 기록.
|
||||
|
||||
|
|
@ -131,7 +131,7 @@ dispatch-core-plan.md` "Dispatch 체인" 절, `ROADMAP.md` M2/M4. 아래는
|
|||
|
||||
**제안**: 최소한 "매치 실패는 에러(silent 무시 금지)"만이라도 지금
|
||||
결정해두면 구현 중 인터럽트를 막을 수 있음. 동률은 "등록 순서가 tiebreak"
|
||||
정도로 명시만 해둬도 충분. M2(Dispatch 엔진) 착수 직전 확인.
|
||||
정도로 명시만 해둬도 충분. M3(Dispatch 엔진) 착수 직전 확인.
|
||||
|
||||
### 1-4. provider(팩토리) 미주입 상태에서 dispatch가 호출되면 어떻게 되는지 세 번째 케이스가 빠짐 — [해소됨, 2026-08-12 열일곱 번째 세션]
|
||||
|
||||
|
|
@ -153,7 +153,7 @@ nil-index 크래시 vs 조용한 no-op)가 안 정해져 있음.
|
|||
|
||||
**제안**: base dispatch 엔진이 "아직 provider 미주입" 상태를 감지해 명확한
|
||||
에러를 던지도록 지금 결정해두면, 구현 중 흔한 초기화 순서 실수를 훨씬 덜
|
||||
헷갈리게 만들 수 있음. 1-2번과 같은 타이밍(M2)에 같이 확정.
|
||||
헷갈리게 만들 수 있음. 1-2번과 같은 타이밍(M3)에 같이 확정.
|
||||
|
||||
### 1-5. `props.Modifier`/`props.Ref` forwarding 관례가 Lua 배열 리터럴의 nil-hole 함정에 그대로 노출됨
|
||||
|
||||
|
|
@ -278,7 +278,8 @@ element별 weak-set) — throw 조건을 "Slot 핸들러의 `process`가 같은
|
|||
|
||||
**[2026-08-07 세 번째 세션 갱신 — 반영 완료.]** 아래 제안대로
|
||||
`LifetimeHandle.luau`/`PerInstanceState.luau` 인터페이스가 `ROADMAP.md`
|
||||
M2로 이동됐고, M8은 quad-roblox 실제 구현만 담당하도록 분리됨 — 더 이상
|
||||
M2로 이동됐고(**[2026-08-24]** 마일스톤 순서 교체로 반응형 코어 앞머리의
|
||||
"공통 기반" 절), M8은 quad-roblox 실제 구현만 담당하도록 분리됨 — 더 이상
|
||||
열린 항목 아님, 아래는 원래 발견 당시 기록.
|
||||
|
||||
**위치(당시)**: `ROADMAP.md` M8 "Ref" — `"LifetimeHandle 인터페이스 + quad-roblox
|
||||
|
|
@ -287,40 +288,44 @@ M2로 이동됐고, M8은 quad-roblox 실제 구현만 담당하도록 분리됨
|
|||
**문제**: `base/lifecycle-pattern.md`("생명 바인드 유틸"을 State-invalidate
|
||||
리스너 클로저 등록에도 재사용)와 `base/slot-plan.md`(Slot의 `retract`가
|
||||
같은 canExecute 패턴을 그대로 씀)는 둘 다 이 유틸을 State/Store 구독
|
||||
(M3/M4 영역)과 Slot(M6)에서 이미 쓴다고 명시하는데, `LifetimeHandle`
|
||||
(M2/M4 영역)과 Slot(M6)에서 이미 쓴다고 명시하는데, `LifetimeHandle`
|
||||
인터페이스 자체는 M8에서야 정의된다. 즉 M4/M6이 개념적으로 필요로 하는
|
||||
base 인터페이스가 그보다 늦은 M8에서 만들어지는 순서 역전.
|
||||
|
||||
**제안**: `LifetimeHandle.luau`(quad-base, 인터페이스만)를 M2(Dispatch
|
||||
엔진) 또는 M3(Store/State)로 옮기고, M8은 "quad-roblox 실제 구현(Instance
|
||||
**제안**: `LifetimeHandle.luau`(quad-base, 인터페이스만)를 M3(Dispatch
|
||||
엔진) 또는 M2(Store/State)로 옮기고, M8은 "quad-roblox 실제 구현(Instance
|
||||
`Connected` 기반)"만 담당하도록 분리. M1 mock에도 이 인터페이스의 트리비얼
|
||||
스텁(항상 true)을 붙여두면 M4/M6 테스트가 자연스러워짐.
|
||||
|
||||
### 1-10. `store.key`의 레코드 필드 타이핑 검증이 M0가 아니라 M3로 밀려 있음 — [해소됨, 2026-08-12 열일곱 번째 세션]
|
||||
### 1-10. `store.key`의 레코드 필드 타이핑 검증이 M0가 아니라 M2로 밀려 있음 — [해소됨, 2026-08-12 열일곱 번째 세션]
|
||||
|
||||
**해소**: 걱정했던 "검증 난이도" 자체가 사라짐 — Luau `type function`
|
||||
(`https://luau.org/types/type-functions/`)으로 `Store<T>`가 `T`의 각
|
||||
필드를 `Source`로 감싼 레코드 타입을 실제로 합성 가능함을 확인(tbox에서도
|
||||
이미 쓰이는 패턴). 결과가 구조적으로 `Source<T>`를 만족하기만 하면 Luau가
|
||||
이름이 아니라 구조로 일치를 검사하므로 문제없음. 기술적으로 막힐 위험이
|
||||
없어졌으니 M0/M3 어느 시점에 검증해도 무방 — `ROADMAP.md` 배치를 억지로
|
||||
없어졌으니 M0/M2 어느 시점에 검증해도 무방 — `ROADMAP.md` 배치를 억지로
|
||||
안 옮겨도 됨. 상세는 `base/typing-limits.md` "`store.key` 레코드 필드
|
||||
타이핑" 절, 실제 문법 실측은 `luau-test/done/16-type-store-key-
|
||||
typefunction.luau`(**[2026-08-15] 통과** — 원래 스파이크가 API 버전
|
||||
드리프트로 깨져있던 걸 고침, `audit/type-recursive-issue-with-typeof/
|
||||
REPORT.md` 6-1절). 아래는 원래 발견 당시 기록.
|
||||
|
||||
**위치**: `ROADMAP.md` M0 vs M3 `"store.key dot-access 타입 추론 확인"`.
|
||||
**위치**: `ROADMAP.md` M0 vs M2 `"store.key dot-access 타입 추론 확인"`.
|
||||
|
||||
**문제**: M0의 정의 자체가 "추론만으로 확정하고 실제 Luau로 부딪혀본 적
|
||||
없는 것"을 검증하는 단계다. `base/source-state-plan.md`가 요청한 M0 항목(
|
||||
"Source가 State를 만족하는 제네릭 메소드 체이닝"의 솔버 안정성)은 이미
|
||||
반영됐지만, 이건 `:Compute` 같은 제네릭 메소드 체이닝만 다루고 `{key:
|
||||
Source<number>}` 같은 **레코드 필드로서의 dot-access 타이핑**(읽기/쓰기
|
||||
대칭성 논거의 핵심 전제)은 별개로 M3에 남아있다. 같은 리스크 카테고리인데
|
||||
M1(스캐폴딩)·M2(디스패치 엔진) 투자가 먼저 이뤄진 뒤에야 검증되는 셈이라,
|
||||
여기서 걸리면 이미 만든 스캐폴딩/디스패치 타입 시그니처를 다시 손봐야 할
|
||||
수 있음.
|
||||
대칭성 논거의 핵심 전제)은 별개로 Store/State 마일스톤에 남아있다. 같은
|
||||
리스크 카테고리인데 M1(스캐폴딩)·디스패치 엔진 투자가 먼저 이뤄진 뒤에야
|
||||
검증되는 셈이라, 여기서 걸리면 이미 만든 스캐폴딩/디스패치 타입 시그니처를
|
||||
다시 손봐야 할 수 있음. (**[2026-08-24]** 이 문단은 *발견 당시*의 서술이고,
|
||||
그 우려는 **[2026-08-15 실측 완료]**로 이미 해소됐다. 덧붙여 2026-08-24
|
||||
마일스톤 순서 교체로 Store/State가 디스패치보다 **먼저** 오게 되어 "디스패치
|
||||
투자를 다 한 뒤에야 검증된다"는 전제 자체도 더는 성립하지 않는다 — 히스토리
|
||||
블록이라 원문 논거는 그대로 두고 마일스톤 번호만 뺐다.)
|
||||
|
||||
**제안**: M0 항목에 "`store.key`가 실제로 `Source<T>` 레코드 필드로
|
||||
안전하게 추론되는지"도 같이 넣을 것 — 어차피 같은 스파이크 파일에서 몇 줄
|
||||
|
|
@ -714,7 +719,7 @@ Handler"라고만 서술해, 사실상 3개의 거의 동일한 형태(리터럴
|
|||
개념을 참조하는 채로 남아있진 않은지 확인 — 이미 "Source가 State를
|
||||
만족함" 최신 모델로 정정돼 있어 문제없음.
|
||||
- `ROADMAP.md`에 `store.key = value`(구 `__newindex`) 모델을 암시하는
|
||||
잔여 표현은 없음 — M3/M4 서술 모두 문법을 명시하지 않아 최신 `:Set()`
|
||||
잔여 표현은 없음 — M2/M4 서술 모두 문법을 명시하지 않아 최신 `:Set()`
|
||||
모델과 직접 충돌하는 곳은 없음.
|
||||
- M9(컴포넌트 합성)이 M7(Modifier)·M8(Ref) 뒤에 오는 순서 — M9는 "M0
|
||||
스파이크(named-parameter 전달)를 정식 Modifier/Ref로 검증"하는 단계라고
|
||||
|
|
@ -732,7 +737,7 @@ Handler"라고만 서술해, 사실상 3개의 거의 동일한 형태(리터럴
|
|||
착수 전에 열려있는 우선순위1 항목이 없음. 아래는 남은 실측/검증 항목만
|
||||
정리(전부 "설계는 확정, 실제 Luau/구현으로 부딪혀볼 것만 남음" 상태):
|
||||
|
||||
- **M2(Dispatch) 착수 시**: 1-2/1-3/1-4는 설계 확정 완료 — M2 스파이크에서
|
||||
- **M3(Dispatch) 착수 시**: 1-2/1-3/1-4는 설계 확정 완료 — M3 스파이크에서
|
||||
다단 체인 케이스, 동률 감지 디버그 print, 매치 실패 에러 경로가 실제로
|
||||
맞게 동작하는지 실측.
|
||||
- **M2/M3 착수 전**: 1-6(canExecute 실제 구현) 실측(1-9는 반영 완료, 위
|
||||
|
|
|
|||
|
|
@ -1839,3 +1839,38 @@ CRUD는 상호배타"라며 승인받은 분기가 **재마운트 경로를 안
|
|||
자리 넷을 열거해 세기")으로 잡은 게 그래서고 거기서 4건이 더 나왔다.
|
||||
감사에서 파생된 새 결정 셋(주입 op `onDestroying` 신설, `EffectHandle`의 필드
|
||||
다섯, `quad-types`의 `Quad` 갱신을 M3/M6/M7/M8/M10에도 항목화)도 같이 반영했다.
|
||||
|
||||
## 2026-08-24-02 — M2/M3 마일스톤 순서 교체 (반응형 먼저)
|
||||
|
||||
2026-08-22가 남긴 마지막 열린 항목(마일스톤 순서)을 사용자가 **(a) 순서 교체**로
|
||||
닫았다 — 이제 **M2=반응형 코어, M3=디스패치 엔진**이다. 의존이 양방향처럼
|
||||
보였지만 디스패치→반응형만 본체 의존이었고, 옛 순서로는 디스패치 마일스톤의
|
||||
`mock 대상 테스트`조차 State 없이 불가능했다. 부수로 **`Brand`/`Relate`/
|
||||
`LifetimeHandle` 인터페이스가 M2 앞머리 "공통 기반" 절로** 왔고(반응형이 이 셋을
|
||||
먼저 요구), 2026-08-22에 디스패치로 앞당겼던 `EpochMap`/`GateNode`/`Blocker`는
|
||||
**되돌아왔다**(앞당길 이유가 사라짐, "게이팅 먼저"는 그대로). 역방향으로 남은
|
||||
건 "핸들러를 등록한다"뿐인 둘(동적 경로 가드, `ObserverEffectLeafHandler`)이라
|
||||
M3로 넘겼다 — 그래서 빌드 순서상 역방향 간선이 없다.
|
||||
**대가 하나** — 반응형이 앞으로 오면서 그 게이트 둘(중간 State GC 실측,
|
||||
`store:GetDynamic` 위치)이 낮은 우선순위에서 **`question.md` 최우선 절로
|
||||
승격**됐다. **실행 중 실수 하나** — 일괄 치환에 `\bM([23])\b`를 써서 한글이
|
||||
바로 뒤에 오는 `M2는`/`M2로` 93건이 안 바뀌었다(Python `\w`는 유니코드라 한글도
|
||||
단어 문자). `git show HEAD:<경로>`로 되돌린 뒤 ASCII 경계 lookaround로 재실행해
|
||||
248건 전량 교체. **라이브 문서만 새 번호로 맞췄고 `session/`·`archive/`·
|
||||
`qa-request/`는 히스토리라 소급 수정하지 않았다** — 그 경고를 인덱스 레이어
|
||||
다섯 곳에 박아뒀다. 원문은 `session/2026-08-24-02-milestone-order-swap.md`.
|
||||
|
||||
**검증**: `quad-doc-auditor` 루프가 **7라운드에서 새 발견 0건으로 수렴**
|
||||
(10→8→1→1→2→2→0, 총 24건 수정, 라운드마다 각도 교체). 가장 값이 큰 건
|
||||
5라운드(구현 순서 시뮬레이션) — 새 M2에서 `Source`/`State`/`Store`가
|
||||
`EpochMap.luau`보다 앞에 놓여 **그 순서로는 `State.luau`를 못 짜는** 걸
|
||||
잡았고(State가 `valueEpochMap`/`emitEpochMap`을 컴포지션), 같은 라운드가
|
||||
순서 교체의 전제("M2가 디스패치 없이 완주 가능한가")도 검증했다.
|
||||
**수렴 뒤 `/code-review high`가 5건을 더 잡았고 전부 유효** — 다섯 중 넷이
|
||||
*"라벨은 치환됐는데 그 라벨을 설명하던 산문이 안 고쳐진"* 종류(그중 둘은
|
||||
`quad-types`의 "마일스톤마다 갱신" 규칙이 M3에 앵커된 채 첫 적용만 M2로
|
||||
옮겨간 같은 뿌리)였고, 그중
|
||||
하나는 `research/` 안의 **히스토리 블록**이 치환을 맞아 원래 논거가 문장
|
||||
그대로 거짓이 된 것이었다(소급 수정 제외 대상을 `session/`·`archive/`·
|
||||
`qa-request/`로만 잡은 게 샜다). `conventions.md`의 *"`/code-review`는
|
||||
감사자를 대체하지 않는다"*가 또 재확인됐다.
|
||||
|
|
|
|||
170
.claude/session/2026-08-24-02-milestone-order-swap.md
Normal file
170
.claude/session/2026-08-24-02-milestone-order-swap.md
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
# 2026-08-24-02 — M2/M3 마일스톤 순서 교체 (반응형 먼저)
|
||||
|
||||
**요청**: *"질문절에 마일스톤 순서 이슈는 (a) M2와 M3의 순서를 바꾼다 를
|
||||
택하는게 맞겠던데 어떻게 봐?"* → 검토 후 동의, *"진행해줘"*로 전량 반영.
|
||||
|
||||
## 1. 발단
|
||||
|
||||
2026-08-22 `ROADMAP.md` 전반 점검이 남긴 마지막 열린 항목. 옛 M2(디스패치
|
||||
엔진)와 옛 M3(Store/State/Source)의 의존이 양방향이라 그 순서로는 M2를
|
||||
끝까지 짤 수 없었다. 설계 결정이 아니라 **순서** 문제라 `question.md`의
|
||||
최우선 절이 아니라 2번 항목으로 따로 뒀었다.
|
||||
|
||||
## 2. 검토 — (a)에 동의한 근거
|
||||
|
||||
사용자가 이미 (a)로 기울어 있었고, 확인 결과 방향은 맞았다. 근거 셋:
|
||||
|
||||
1. **의존의 비대칭이 명확하다.** 디스패치 → 반응형은 *본체* 의존이라
|
||||
우회 불가(`setLength`가 `State<number>`를, `setOffsetSource`가
|
||||
`Source<number>`를 받고 `recompute`가 `offset:Set()`을 부름). 반대는
|
||||
*등록 표면* 의존(핸들러 3개 등록)이라 뒤로 미루면 그만이다.
|
||||
2. **옛 순서면 디스패치 마일스톤의 `mock 대상 테스트` 체크박스가 원리적으로
|
||||
불가능했다** — `setLength`/`recompute`를 State 없이 테스트할 수 없다.
|
||||
반대로 반응형 코어는 순수 Lua라 `luau`만으로 단독 테스트가 된다.
|
||||
3. **2026-08-22의 `EpochMap`/`GateNode`/`Blocker` 앞당김이 불필요해진다.**
|
||||
그 이동은 "디스패치가 먼저인데 디스패치가 쟤들을 호출한다"는 이유였다.
|
||||
순서를 바꾸면 마일스톤 내용이 의존 계층과 그대로 일치하고 끌어온 항목이
|
||||
0개가 된다. "게이팅 먼저"도 그대로 지켜진다(게이팅이 디스패치보다 먼저).
|
||||
|
||||
**(b) 분할을 안 고른 이유**: 참조 비용이 오히려 크다 — 기존 `M2` 참조
|
||||
전부가 "M2a인가 M2b인가"로 애매해지는데 순서 교체처럼 기계적으로 못 푼다.
|
||||
`Brand`를 앞으로 빼는 일은 (b)에서도 똑같이 해야 하고, 얻는 건 "디스패치
|
||||
코어를 조금 일찍 짠다"뿐인데 M4 전까지 그걸 소비하는 게 없다.
|
||||
**(c) 유지를 안 고른 이유**: `project-context.md`가 못박은 "순서의 소스는
|
||||
`ROADMAP.md`"를 스스로 무효화한다.
|
||||
|
||||
## 3. 그냥 맞바꾸기로는 안 끝났다 — 공통 기반 셋
|
||||
|
||||
검토 중 드러난 것. 옛 M2 안에 **반응형보다 먼저 와야 하는** 항목이 셋
|
||||
섞여 있었고, State-free이자 dispatch-free라 어느 쪽에도 안 걸린다:
|
||||
|
||||
- `Brand.luau` — `Source`가 `SourceBrand`+`EpochBrand`에 등록돼야 함.
|
||||
- `Relate.luau` — `bindLifetime`/`unbindLifetime`이 그 위에 구현됨.
|
||||
- `LifetimeHandle.luau` 인터페이스 — State 전파 루프가 매 발화마다
|
||||
`canExecute`를, 이중 바인딩 게이트가 `canBound`를 부름.
|
||||
|
||||
새 M2 앞머리에 `### 공통 기반` 절을 만들어 셋을 옮겼다. 반대로 새 M3로
|
||||
넘어간 것은 둘 — Observer/Effect **동적 경로 가드 등록**과
|
||||
**`ObserverEffectLeafHandler`**(`H-39`). 결과적으로 역방향 간선이 없다.
|
||||
|
||||
## 4. 실행 중 낸 실수 하나 — 한글 인접 `M2`가 안 바뀌었다
|
||||
|
||||
첫 일괄 치환에 `re.sub(r'\bM([23])\b', ...)`를 썼는데, **Python `re`의
|
||||
`\w`는 유니코드라 한글도 단어 문자**다. 그래서 `M2는`/`M2로`/`M2가`처럼
|
||||
한글이 바로 뒤에 오는 경우 `\b`가 성립하지 않아 **155건만 바뀌고 93건이
|
||||
그대로 남았다** — 코퍼스가 옛 번호와 새 번호가 섞인 상태가 됐다.
|
||||
|
||||
`git checkout -- .`은 하네스가 막아서(destructive) `git show HEAD:<경로>`로
|
||||
26개 파일을 되돌린 뒤, ASCII 경계만 보는
|
||||
`(?<![0-9A-Za-z_])M([23])(?![0-9A-Za-z_])`로 다시 돌려 **248건 전량**을
|
||||
바꿨다. **교훈: 한국어 문서에서 `\b`로 영숫자 토큰 경계를 잡지 말 것.**
|
||||
|
||||
## 5. 부작용 — 게이트 둘이 "바로 다음"으로 올라왔다
|
||||
|
||||
순서 교체의 실질적 대가. `question.md`의 낮은 우선순위 절에 있던 둘이
|
||||
반응형(옛 M3)의 게이트였는데, 반응형이 M2가 되면서 **착수 직전 항목**이
|
||||
됐다 — 그래서 최우선 절로 승격했다:
|
||||
|
||||
- **중간 State GC 미검증** — 상류 strong / 하류 weak 불변식 명문화 여부 +
|
||||
`luau-test` 실측(`base/source-state-plan.md`).
|
||||
- **`store:GetDynamic` 위치** — 콜론 메소드 vs 탑레벨 함수
|
||||
(`base/store-plan.md`).
|
||||
|
||||
순수한 *설계* 결정 대기는 여전히 0건이다(하나는 실측 미완, 하나는 표면
|
||||
위치 선택).
|
||||
|
||||
## 6. 번호 재부여의 사각지대
|
||||
|
||||
라이브 문서의 `M2`/`M3` 참조는 전부 새 번호로 맞췄다 — 코퍼스의 참조가
|
||||
거의 전부 "그 내용이 사는 마일스톤"을 가리켜서 기계적 맞교환으로 의미가
|
||||
보존된다. 반면 **`session/`·`archive/`·`qa-request/`는 히스토리 문서라
|
||||
소급 수정하지 않았다.** 2026-08-24 이전에 쓰인 그 문서들의 `M2`/`M3`는
|
||||
옛 의미(M2=디스패치, M3=반응형)다 — 이 경고를 `ROADMAP.md` M2 배너,
|
||||
루트 `CLAUDE.md`, `project-context.md`, `todos.md`, `archive/
|
||||
question-resolved.md`에 같이 박아뒀다.
|
||||
|
||||
## 7. 반영 범위
|
||||
|
||||
실제로 diff에 남은 파일은 **25개 + 이 세션 로그**다(정확한 목록은
|
||||
`git show --stat`이 소스 — 여기서 세지 않는다). 갈래만 적으면:
|
||||
|
||||
- `ROADMAP.md` — 두 절 재편 + 최상단 배너 + 흩어진 마일스톤 참조. diff의
|
||||
대부분이 여기다.
|
||||
- `question.md` — 마일스톤 경계 항목 삭제·아카이브 이관, 3번→2번 재번호,
|
||||
최우선 절 재작성(두 항목 승격), 번호 재사용 경고.
|
||||
- `archive/question-resolved.md` — 해소 기록 신설.
|
||||
- `base/gate-plan.md`/`blocker-plan.md`/`debounce-throttle-plan.md`/
|
||||
`state-epoch-plan.md`/`source-state-plan.md` — 2026-08-22 앞당김 서술을
|
||||
되돌림 서술로. 뒤의 둘엔 "동적 경로 가드 등록은 M3" 캐비엇도 신설.
|
||||
- `base/effect-plan.md` — 같은 캐비엇.
|
||||
- `base/quad-types-plan.md` — "마일스톤마다 갱신" 규칙의 첫 적용 지점이
|
||||
M2가 된 것.
|
||||
- `base/store-plan.md`/`attribute-plan.md`/`slot-plan.md`/
|
||||
`dispatch-core-plan.md`/`project-setup-plan.md`,
|
||||
`research/operator-sugar-plan.md`/`debug-tooling-plan.md`,
|
||||
`luau-test/STATUS.md` — 번호 참조 갱신.
|
||||
- `research/pre-implementation-audit.md` — 번호 갱신 + 히스토리 블록 정정
|
||||
(8절 참고).
|
||||
- 인덱스 레이어: `.claude/README.md`/`todos.md`/`project-context.md`/
|
||||
루트 `CLAUDE.md`/`HUMAN_TODO.md`.
|
||||
- `session-summary.md` + 이 파일.
|
||||
|
||||
**⚠️ diff에 *안* 남은 파일이 있다는 게 오히려 중요하다.**
|
||||
`base/relate-plan.md`·`base/lifecycle-pattern.md`·`luau-test/README.md`·
|
||||
`luau-test/`의 스파이크 둘·`reference/slot-attach-decomposition.md`는
|
||||
일괄 치환으로 한 번 바뀌었다가 감사 1라운드 수정으로 **HEAD와 글자 단위로
|
||||
같아졌다**. 그 참조들이 가리키는 것(`Relate`/`LifetimeHandle`)이 구 M2에서
|
||||
신 M2로 **두 번 옮겨져 번호가 우연히 보존**됐기 때문이다 — 즉 "치환하면
|
||||
안 되는 자리"였고, 감사가 그걸 잡아 되돌린 결과 diff가 비었다.
|
||||
|
||||
## 8. 검증 — 감사 7라운드 수렴 후 `/code-review high`가 5건 더
|
||||
|
||||
`quad-doc-auditor` 루프는 라운드마다 각도를 바꿔 **7라운드에서 새 발견
|
||||
0건으로 수렴**했다(라운드별 10→8→1→1→2→2→0, 총 24건 수정). 각도는
|
||||
순서대로 — (1) 마일스톤 번호 정합성, (2) 인덱스 레이어와 이번에 새로 쓴
|
||||
서술의 자기모순, (3) `base/` 본문의 선행관계 서술, (4) 완결성(열거),
|
||||
(5) 구현 순서 시뮬레이션, (6) 관례 준수와 2차 stale, (7) 조사가 가장 적게
|
||||
닿은 파일.
|
||||
|
||||
**가장 값이 컸던 건 5라운드(구현 순서 시뮬레이션)**다. 문서 정합성이
|
||||
아니라 *"이 로드맵대로 실제로 코드를 짤 수 있는가"*를 물었더니, 새 M2에서
|
||||
`Source`/`State`/`Store`가 `EpochMap.luau`보다 **앞에** 놓여 있는 게
|
||||
드러났다 — State가 `valueEpochMap`/`emitEpochMap` 둘을 컴포지션하므로 그
|
||||
순서로는 `State.luau`를 못 짠다. 같은 라운드가 **순서 교체의 전제 자체도
|
||||
검증**했다(새 M2 전체를 디스패치 심볼로 훑어 "가드 등록 둘 말고는 없음"
|
||||
확인).
|
||||
|
||||
**그리고 수렴 뒤 사용자가 돌린 `/code-review high`가 5건을 더 잡았고 전부
|
||||
유효했다** — `conventions.md`의 *"`/code-review`는 감사자를 대체하지
|
||||
않는다"*가 또 한 번 실측으로 재확인된 셈이다. 다섯 중 **넷이 "라벨은
|
||||
치환됐는데 그 라벨을 설명하던 산문이 안 고쳐진" 종류**였고, 그중 둘
|
||||
(아래 두 번째·세 번째)은 같은 뿌리 — `quad-types`의 "마일스톤마다 갱신"
|
||||
규칙이 M3에 앵커된 채 첫 적용 지점만 M2로 옮겨간 것이다:
|
||||
|
||||
- **(HIGH)** M3의 `setLength` 항목이 여전히 *"필요한 `GateNode`/`Blocker`/
|
||||
`EpochMap`은 이제 **이 마일스톤에** 체크박스로 있다(위 세 항목)"*라고
|
||||
말하고 있었다 — 셋은 M2로 되돌아갔고, "위 세 항목"이 가리킬 대상이 M3에
|
||||
없으며, 15줄 위의 M3 배너가 정반대를 말한다. 핸드오버 체크리스트 2번
|
||||
("배너를 달았으면 그 배너가 부정하는 문장을 같은 커밋에서 고쳤는지")의
|
||||
전형적 실패.
|
||||
- **(MEDIUM)** `quad-types`의 "마일스톤마다 갱신" 규칙이 M3에 앵커돼 있는데
|
||||
**첫 적용 지점은 M2**가 됐다 — M2 구현자가 아직 안 나온 마일스톤을 앞으로
|
||||
참조해야 규칙을 알게 되는 구조. 규칙 요지를 M2 항목에도 적었다
|
||||
(`base/quad-types-plan.md`와 `ROADMAP.md` M2/M3 항목).
|
||||
- **(MEDIUM)** 같은 뿌리의 다른 자리 — `.claude/README.md`의 `quad-types-plan.md`
|
||||
색인 행이 *"**M3(`Dispatch`)를 시작으로** M2/M6/M7/M8/M10에 체크박스가
|
||||
뿌려졌다"*로 치환돼 **나열 순서가 뒤집혔다**. M2가 먼저 지어지므로
|
||||
"M3를 시작으로 … M2"는 성립하지 않는다 — 서브시스템을 붙이는 모든
|
||||
마일스톤을 순서대로 적고 "계기는 `Dispatch`, 첫 적용은 M2"로 갈라 썼다.
|
||||
- **(MEDIUM)** `research/pre-implementation-audit.md`의 **히스토리 블록**이
|
||||
기계 치환을 맞았다 — *"M1·M3(디스패치) 투자가 먼저 이뤄진 뒤에야
|
||||
검증되는 셈"*이라는 원래 우려가 새 번호에선 **문장 그대로 거짓**이 됐다
|
||||
(Store/State가 이제 디스패치보다 먼저다). `session/`·`archive/`·
|
||||
`qa-request/`는 소급 수정 대상에서 뺐는데 **`research/` 안의 명시적
|
||||
히스토리 블록은 그 방침에서 새어 있었다.**
|
||||
- **(LOW)** M2로 옮겨온 `Brand.luau` 항목이 `isNone`을 자기 책임처럼 적고
|
||||
있었는데, `brand-plan.md`는 2026-08-18에 *"`Brand → None` 의존을 만들지
|
||||
않는다"*로 확정해뒀다 — 마일스톤이 갈리면서 처음 눈에 띈 잔재.
|
||||
|
||||
**교훈**: 기계 치환은 *토큰*을 바꾸지 *주장*을 바꾸지 않는다. 치환 대상이
|
||||
많을수록 "그 토큰을 설명하던 산문"과 "히스토리 블록"을 따로 훑어야 한다.
|
||||
|
|
@ -10,8 +10,9 @@
|
|||
**M2 착수를 막는 설계 항목은 더 이상 없다.**** `Gate`는
|
||||
`state:Gate(setup)` + `GateNode`로(`base/gate-plan.md`), State의
|
||||
재계산/전파 판정은 **`Epoch` 리비전 비교** 채택으로(`base/state-epoch-plan.md`
|
||||
— **[2026-08-22 정정]** 구현 마일스톤은 갈린다: `EpochMap`/`Epoch`
|
||||
인터페이스는 **M2**, State 본체 통합은 **M3**) 닫혔다. 두 문서 모두 `research/`가 아니라 **`base/`**에 있다.
|
||||
— **[2026-08-24 재정정]** 둘 다 **M2**다: 2026-08-22엔 `EpochMap`/`Epoch`
|
||||
인터페이스만 디스패치 쪽으로 앞당겨 갈라져 있었으나, 마일스톤 순서
|
||||
교체로 되돌아와 State 본체와 같은 마일스톤이 됐다) 닫혔다. 두 문서 모두 `research/`가 아니라 **`base/`**에 있다.
|
||||
**[2026-08-21 후속] `Epoch`/`EpochMap`/`Brand` 승격도 같은 날 완료** —
|
||||
부기가 재사용 가능한 `EpochMap`으로 떨어져 나오고, 판정 인터페이스가
|
||||
`Source`에서 `Epoch`로 일반화되고, `Brand`가 인스턴스 브랜드로 재작성됐다
|
||||
|
|
@ -21,7 +22,7 @@
|
|||
리비전 증가 방식도 `bit32` 랩으로 확정** — `Epoch`/`EpochMap`/`Brand`에
|
||||
열린 설계 항목은 **하나도 없다**(`base/state-epoch-plan.md` §2).
|
||||
**[2026-08-21 경위]** 같은 날 `/code-review high`가 "게이트가 유보했다
|
||||
내보내는 emit이 어느 출처를 싣는지"가 안 정해진 걸 잡아 한때 M2 항목으로
|
||||
내보내는 emit이 어느 출처를 싣는지"가 안 정해진 걸 잡아 한때 M3 항목으로
|
||||
되돌아갔으나, **사용자가 그 자리에서 흡수 집합 스냅샷으로
|
||||
확정**했다(`setup` 시그니처는 안 바뀜 — `base/gate-plan.md` 4번.
|
||||
**[2026-08-22 표기 정정]** 여기 `emit(self)` + 흡수 집합이라 적혀
|
||||
|
|
@ -38,24 +39,26 @@
|
|||
`Effect`의 설치 구간 억제가 `Gate` 소비자에서 빠지며 `effect-plan.md`의
|
||||
순서 제약도 사라짐). **다시, M2 착수를 막는 설계 항목은 없다.**
|
||||
|
||||
**⚠️ [2026-08-22 신설] 다만 설계가 아니라 *순서*가 막고 있다.**
|
||||
`ROADMAP.md` 전반 점검에서 **M2와 M3의 의존이 양방향**인 게 드러났다 —
|
||||
`Dispatch.setLength`/`setOffsetSource`/`recompute`가 `State<number>`/
|
||||
`Source<number>`를 쓰므로 M2는 `Source.luau`/`State.luau` 없이 구현이
|
||||
안 되고, 반대로 M3의 `Observer`/`Effect` 동적 경로 가드는 M2의
|
||||
`Dispatch.addHandler`를 쓴다. 선택지 셋(M2/M3 순서 교체 / M2를 둘로
|
||||
분할 / 지금 구조 유지)은 **`question.md` 2번이 소스** — 여기서 반복하지
|
||||
않는다. 같은 점검에서 `EpochMap`/`GateNode`/`Blocker`/`None`이 M3·M7에서
|
||||
**M2로 실제 이동**했고(각주로만 예고돼 있던 것), `Dispatch.drive`가
|
||||
두 패스가 아니라 단일 일반화 `for`라는 `F-4-1` 정정과 물리 조작 주입 op
|
||||
이름 `native*` 확정이 `ROADMAP.md`/`dispatch-core-plan.md`에 반영됐다.
|
||||
M0 체크박스가 검증했던 스파이크 중 재작성 대기인 것들도
|
||||
`ROADMAP.md`의 "재검증 대기" 절로 모았다(현황의 소스는 여전히
|
||||
`luau-test/STATUS.md`).
|
||||
**✅ [2026-08-24 해소] 설계가 아니라 *순서*가 막던 것도 닫혔다.**
|
||||
2026-08-22 `ROADMAP.md` 전반 점검에서 옛 M2(디스패치)와 옛 M3(반응형)의
|
||||
의존이 양방향인 게 드러났었는데, **사용자가 (a) 순서 교체를 선택**해
|
||||
같은 날 전량 반영됐다 — 이제 **M2가 반응형 코어, M3가 디스패치 엔진**
|
||||
이고 역방향 의존이 없다. 결정·근거·기각된 선택지는
|
||||
`archive/question-resolved.md`의 "마일스톤 경계" 절이 소스, 새 마일스톤
|
||||
구성은 `ROADMAP.md`의 M2 배너 — 여기서 반복하지 않는다. 부수로
|
||||
`Brand`/`Relate`/`LifetimeHandle` 인터페이스가 M2 앞머리 "공통 기반"
|
||||
절로 왔고, 2026-08-22에 디스패치로 앞당겼던
|
||||
`EpochMap`/`GateNode`/`Blocker`는 M2로 되돌아왔으며, Observer/Effect
|
||||
동적 경로 가드 등록과 `ObserverEffectLeafHandler`는 M3로 갔다.
|
||||
**⚠️ 2026-08-24 이전에 쓰인 `session/`·`archive/`·`qa-request/`의
|
||||
`M2`/`M3`는 옛 의미**(M2=디스패치, M3=반응형)로 읽을 것.
|
||||
같은 2026-08-22 점검에서 `Dispatch.drive`가 두 패스가 아니라 단일 일반화
|
||||
`for`라는 `F-4-1` 정정과 물리 조작 주입 op 이름 `native*` 확정도
|
||||
`ROADMAP.md`/`dispatch-core-plan.md`에 반영됐고, M0 체크박스가 검증했던
|
||||
스파이크 중 재작성 대기인 것들도 `ROADMAP.md`의 "재검증 대기" 절로
|
||||
모았다(현황의 소스는 여전히 `luau-test/STATUS.md`).
|
||||
그 외 남은 것은 판단이 아니라 구현 시 정할 것 하나 — 스파이크 `05`
|
||||
재작성(`luau-test/STATUS.md`).
|
||||
**[2026-08-22 해소]** 여기 있던 "M2에 `Blocker`까지 넣을지"는 같은 날
|
||||
`ROADMAP.md` 전반 점검에서 **둘 다 M2**로 확정되며 닫혔다.
|
||||
**[2026-08-24 해소, 6라운드 `H-49`]** 여기 `Gate`의 "생명주기·재진입 계약"을
|
||||
같이 나열했었는데 **두 항목 다 부정확했다** — 재진입은 이미
|
||||
`[2026-08-21 정리 — 열린 항목 아님]`으로 닫혀 있었고, 생명주기는 "판단이
|
||||
|
|
@ -73,7 +76,7 @@
|
|||
|
||||
**M2/M3에 직접 걸리던 것들이 전부 닫혔다** — 말단 핸들러 4종의
|
||||
`setLength`/`setOffsetSource` 미등록(`H-39`), `New(): Quad`가 닫힌 타입이라
|
||||
`quad.Dispatch`가 타입에러인 것(`H-25`, M2 체크리스트에 항목 신설),
|
||||
`quad.Dispatch`가 타입에러인 것(`H-25`, M3 체크리스트에 항목 신설),
|
||||
`Effect`의 leaf 사망 cleanup 배선 부재(`H-11`), `:List`의 인덱스/좌표계
|
||||
결함 둘(`H-1`/`H-2`).
|
||||
|
||||
|
|
@ -112,7 +115,7 @@
|
|||
**`Owned = false`에서 `Detach`는 `_detached`에 안 들어감**, 조상 파괴 시
|
||||
unowned 요소도 같이 죽는다는 계약 신설, `groupClaimKeys` 키 확정
|
||||
(`(inst, groupValue) → k`), `Tween<T>:Mapped` 이름 확정, 4라운드가 반영을
|
||||
빠뜨렸던 `E-10` dedup 대칭 결론 실반영, 그리고 **"게이팅 먼저"(M2로 앞당김)**.
|
||||
빠뜨렸던 `E-10` dedup 대칭 결론 실반영, 그리고 **"게이팅 먼저"**(당시엔 디스패치 마일스톤으로 앞당기는 형태였으나 **[2026-08-24]** 순서 교체로 반응형이 먼저가 되며 그 앞당김은 불필요해짐).
|
||||
**아직 회신 대기인 것은 그 followup의 C절**(`rawAdd` 의사코드 승인,
|
||||
`rawAdd`의 `Length:Set` 제거, `updateFn`이 State를 반환할 때의 래핑/`prev`,
|
||||
`setLength` 앵커를 물리 target으로 되돌리기, `Gate` 이름·표면, `Effect`
|
||||
|
|
@ -158,26 +161,29 @@
|
|||
단순한 해법을 직접 제시 — flush 루프를 분기하는 대신 `attachSlot`의
|
||||
`slot._mounted = true`를 `activateList` 호출 뒤로 옮기는 것 하나로
|
||||
둘 다 닫힘. 부수로 `spliceArraysDown`이 밀어야 할 배열에
|
||||
`bk.observers`가 빠져 있던 것도 발견·반영, `ROADMAP.md` M2가 M3의
|
||||
`Blocker.luau`에 구조적으로 의존하게 된 것도 각주로 반영(마일스톤
|
||||
재편 여부는 열림 — `pre-implementation-qa-round3.md`의 "ROADMAP.md
|
||||
마일스톤 정합성" 절 참고).
|
||||
`bk.observers`가 빠져 있던 것도 발견·반영, `ROADMAP.md` M3(디스패치)가
|
||||
M2의 `Blocker.luau`에 구조적으로 의존하게 된 것도 각주로 반영
|
||||
(**[2026-08-24]** 그때 열어뒀던 마일스톤 재편 여부가 순서 교체로
|
||||
닫혔다 — 위 00번 머리말).
|
||||
|
||||
**아래는 M3 착수 전에 결론이 필요한 항목 목록** — **설계** 게이트
|
||||
얘기다. M0은 아예 막혀 있지 않고, M2도 설계로는 안 막혀 있으나
|
||||
**[2026-08-22] 마일스톤 순서 문제 하나가 M2를 막는다**(위 00번의
|
||||
⚠️ 문단 + `question.md` 2번). 둘 다 `question.md` 3번에도 있고 각 `base/` 문서에도
|
||||
⚠️로 표시돼 있다. **[2026-08-21 정리]** 여기 쌓여 있던 `[해소]` 항목들
|
||||
**⚠️⚠️ [2026-08-24 승격] 아래 둘은 이제 *바로 다음* 마일스톤의
|
||||
게이트다.** 순서 교체 전엔 반응형이 M3라 "한 마일스톤 뒤"의 일이었는데,
|
||||
반응형이 M2가 되면서 **지금 착수 직전에 결론이 필요한 항목**이 됐다
|
||||
(위 00번이 "M2 착수를 막는 **설계** 항목은 없다"고 하는 것과 모순되지
|
||||
않는다 — 하나는 실측 미완, 하나는 표면 위치 선택이라 성격이 다르지만,
|
||||
**어느 쪽이든 M2를 짜기 전에 답이 있어야 한다**). 둘 다
|
||||
`question.md`의 **최우선 절**로 올라가 있고(2026-08-24에 낮은 우선순위
|
||||
절에서 승격), 각 `base/` 문서에도 ⚠️로 표시돼 있다. **[2026-08-21 정리]** 여기 쌓여 있던 `[해소]` 항목들
|
||||
(`Blocker.luau` 마일스톤 순서, 그룹 `Attribute` 위치 claim 키,
|
||||
`SetAndDispose`, `PopOnly`→`Detach`, `KeyGone` 처분, `Store` 미선언 키
|
||||
타입 에러, dedup 경로 대칭)은 **전부 `archive/question-resolved.md`와
|
||||
각 `base/` 문서로 옮겼다** — 목록이 절반 넘게 해소 항목으로 차 있어
|
||||
"지금 할 일"로 읽히지 않던 것을 걷어낸 것.
|
||||
- **중간 State GC 미검증**(`base/source-state-plan.md`) — 상류 strong /
|
||||
하류 weak 불변식을 명문화할지 + `luau-test` 실측. **M3 착수 전 필요.**
|
||||
하류 weak 불변식을 명문화할지 + `luau-test` 실측. **M2 착수 전 필요.**
|
||||
- **`store:GetDynamic`을 콜론 메소드로 둘지 탑레벨 함수로 둘지**
|
||||
(`base/store-plan.md`) — 콜론이면 `GetDynamic`이 모든 Store의 예약 키가
|
||||
됨(lazy `__index`와 충돌). M3/M4 착수 전 필요.
|
||||
됨(lazy `__index`와 충돌). M2/M4 착수 전 필요.
|
||||
|
||||
0. **⭐ M0 착수를 막는 결정은 이제 없음 (2026-08-14 열한 번째 세션 기준).**
|
||||
`question.md`의 최우선 항목이 **전부 비었음** — `0-Y`(`:Compute` lazy
|
||||
|
|
@ -287,13 +293,13 @@
|
|||
백로그이지만 위 항목들과는 발단이 다름 — **사용자가 직접 요청한 실제
|
||||
기능 갭**에서 시작됨(그 문서 13절). 다만 제어 핸들 설계까지 닫히고 나니
|
||||
실제로 quad-base에 새 코어 메커니즘을 추가하지 않는 **순수 슈가**로
|
||||
확인돼(같은 절), 위 항목들과 우선순위는 다시 같아짐 — M0/M3를 막지
|
||||
확인돼(같은 절), 위 항목들과 우선순위는 다시 같아짐 — M0/M2를 막지
|
||||
않고, **그 게이티드 노드는 [2026-08-21] `state:Gate`로 확정돼 M2에서
|
||||
만들어진다**(`base/gate-plan.md`) — `Debounce`/`Throttle`은 그 위의
|
||||
정책으로 얹으면 되고, 같은 설계를 두 번 할 일은 없어졌다.
|
||||
주입 op 2개(`setTimeout`/`clearTimeout`)가 백엔드 팩토리 표면에
|
||||
추가될 예정이라는 것도 M1 설계 시 인지. 남은 열린 질문 없음(구
|
||||
`question.md` 3번, 전량 해소로 항목 자체가 빠짐).
|
||||
`question.md` 낮은 우선순위 절, 전량 해소로 항목 자체가 빠짐).
|
||||
**[2026-08-18 추가]** 사용자 아이디어 메모 두 건도 같은 성격의 백로그로
|
||||
신설 — 스크롤 최적화 외부 유틸 `quad-roblox-fastscroll`
|
||||
(`research/fastscroll-plan.md`, 선행으로 `Visible=false`일 때
|
||||
|
|
|
|||
16
CLAUDE.md
16
CLAUDE.md
|
|
@ -1,12 +1,16 @@
|
|||
# CLAUDE.md
|
||||
|
||||
Roblox 엔진용 DOMless UI 렌더러 **quad**를 처음부터 다시 짜는 프로젝트.
|
||||
**[2026-08-22 기준] M0(스파이크 검증)/M1(스캐폴딩)까지 완료, 다음은
|
||||
M2(디스패치 엔진)** — 다만 **⚠️ M2 착수를 막는 항목이 하나 있음**:
|
||||
M2와 M3의 의존이 양방향이라(M2의 Length/Offset 배관이 `State`/`Source`를
|
||||
씀) 지금 순서로는 M2를 끝까지 짤 수 없다. 설계 결정이 아니라 **마일스톤
|
||||
순서** 문제이고, 선택지와 근거의 소스는 `.claude/question.md` 2번(사용자
|
||||
회신 대기). 같은 상태를 `.claude/project-context.md`도
|
||||
**[2026-08-24 기준] M0(스파이크 검증)/M1(스캐폴딩)까지 완료, 다음은
|
||||
M2(반응형 코어 — Source/State/Store)**. **⚠️ [2026-08-24] M2와 M3의
|
||||
번호·순서가 맞바뀌었다** — 예전엔 M2=디스패치, M3=반응형이었는데 의존이
|
||||
한 방향(디스패치 → 반응형)이라 반응형을 먼저 짓기로 확정했다. 그래서
|
||||
**2026-08-24 이전에 쓰인 `session/`·`archive/`·`qa-request/` 문서의
|
||||
`M2`/`M3`는 옛 의미로 읽을 것**(라이브 문서는 전부 새 번호로 맞춰뒀음).
|
||||
경위는 `.claude/archive/question-resolved.md`의 "마일스톤 경계" 절,
|
||||
새 구성은 `ROADMAP.md`의 M2 배너. **⚠️ M2 착수 전에 답이 필요한 항목이
|
||||
남아 있다** — 무엇이 몇 개인지는 여기서 세지 말고 `.claude/question.md`의
|
||||
최우선 절을 볼 것. 같은 상태를 `.claude/project-context.md`도
|
||||
서술하니 마일스톤이 넘어갈 때 두 곳을 같이 고칠 것. 진행 상황의 소스는
|
||||
항상 루트 `ROADMAP.md`.
|
||||
|
||||
|
|
|
|||
|
|
@ -6,17 +6,17 @@
|
|||
올렸었음**(0-Z) — **[2026-08-13 열네 번째 세션] 그 0-Z도 해소되어 지금은
|
||||
사람이 결정해야 M0가 열리는 항목이 없음**(0-Y는 열세 번째 세션에 해소).
|
||||
|
||||
## ⭐ 11. **[2026-08-22 신설] M2 착수를 막는 마일스톤 순서 결정** — 사용자 답변 필요
|
||||
## ✅ 11. **[2026-08-22 신설 → 2026-08-24 해소] 마일스톤 순서 결정**
|
||||
|
||||
**설계 결정이 아니라 순서 문제**라서 에이전트가 임의로 정하면 안 되는 자리다.
|
||||
`ROADMAP.md` 전반 점검에서 **M2(디스패치 엔진)와 M3(Store/State/Source)의
|
||||
의존이 양방향**인 게 드러났다 — M2의 Length/Offset 배관이 `State<number>`/
|
||||
`Source<number>`를 쓰므로 **M2는 State/Source 없이 구현이 안 되고**, 반대로
|
||||
M3의 동적 경로 가드는 M2의 `Dispatch.addHandler`를 쓴다(그쪽은 얕음).
|
||||
사용자가 **(a) 순서 교체**를 선택해 닫혔다 — 반응형이 M2, 디스패치가 M3다.
|
||||
결정과 근거는 `.claude/archive/question-resolved.md`의 "마일스톤 경계" 절,
|
||||
새 마일스톤 구성은 `ROADMAP.md`의 M2 배너가 소스.
|
||||
|
||||
**선택지 (a) M2/M3 순서 교체 / (b) M2를 둘로 분할 / (c) 지금 구조 유지**의
|
||||
근거와 상세는 **`.claude/question.md` 2번이 소스** — 여기서 반복하지 않는다.
|
||||
답이 나오면 `ROADMAP.md`의 마일스톤 경계와 `question.md` 2번을 같이 닫으면 된다.
|
||||
**⚠️ 다만 그 교체로 `question.md` 최우선 절에 항목 둘이 올라왔다** — 중간
|
||||
State GC 실측과 `store:GetDynamic` 위치. 둘 다 원래 반응형(옛 M3)의
|
||||
게이트라 급하지 않았는데 반응형이 먼저 지어지게 되면서 **지금 답이
|
||||
필요**해졌다. 둘 다 설계 질문이라 이 문서가 아니라 `question.md`가
|
||||
소스다(아래 3번 항목이 가리키는 그 절) — 여기서 따로 번호를 만들지 않는다.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -205,7 +205,7 @@ Debounce/Throttle 작업에 쓴 워크트리는 **사용자 확인 후 정리
|
|||
Tween 자체가 다른 base 요소와 깊게 안 얽혀 있어(거의 전부
|
||||
`Handlers/Property.luau` 한 파일 + 릴레이션 슬롯) 사용자가 직접 처리하는 데
|
||||
범위상 문제가 없다.
|
||||
- **지금 상태**: 미확정("필요성 낮은 쪽으로 기움", 완전 폐기는 아님). **M0/M2를
|
||||
- **지금 상태**: 미확정("필요성 낮은 쪽으로 기움", 완전 폐기는 아님). **M0/M2(지금 착수 대상)를
|
||||
막지 않으므로 급하지 않고**, 실제로 진입 애니메이션이 필요해지는 시점에
|
||||
사용자가 착수하면 된다. 설계 맥락은 `base/tween-plan.md`의 "초기 진입
|
||||
애니메이션(`initValue`)" 절.
|
||||
|
|
@ -214,9 +214,11 @@ Debounce/Throttle 작업에 쓴 워크트리는 **사용자 확인 후 정리
|
|||
|
||||
디자인 결정 중 Lua/Roblox 엔진에 대한 깊은 경험이 필요한 것들은 합리적 기본값으로
|
||||
진행하면서 `.claude/question.md`에 모아두는 중. 깨어있을 때 훑어보고 기본값이
|
||||
마음에 안 드는 것만 답해주면 됨 — **[2026-08-22 갱신] 설계 결정 대기는
|
||||
여전히 0건**이지만, **마일스톤 순서 결정 하나가 새로 열렸다**(`question.md`
|
||||
2번, 위 11번 항목이 소스). 아래는 그 전 서술: **[2026-08-14 열한 번째 세션 기준]
|
||||
마음에 안 드는 것만 답해주면 됨 — **[2026-08-24 갱신] 순수 설계 결정
|
||||
대기는 여전히 0건**이고 2026-08-22에 열렸던 마일스톤 순서 결정도
|
||||
**해소됐다**(위 11번 항목). 다만 그 해소의 부작용으로 **`question.md`
|
||||
최우선 절에 M2 착수 전 답이 필요한 항목 둘**이 올라와 있다(중간 State GC
|
||||
실측, `store:GetDynamic` 위치). 아래는 그 전 서술: **[2026-08-14 열한 번째 세션 기준]
|
||||
`question.md`엔 이제 "결정 대기" 절 자체가 없음**(비어서 헤딩째로 삭제 —
|
||||
마지막 남았던 0-W
|
||||
`Ref` 이중 배치도 이 세션에 해소 — `base/ref-plan.md` "이중 배치 방지"
|
||||
|
|
|
|||
634
ROADMAP.md
634
ROADMAP.md
|
|
@ -7,23 +7,31 @@ quad-v2 구현 단계 실행 계획. 설계 근거/아키텍처 자체는 여기
|
|||
**2026-08-04 세션에 준비만 해둔 상태로 신설, 이후 여러 세션에 걸쳐 설계가
|
||||
확정될 때마다 각 마일스톤 체크박스가 계속 갱신돼왔음.**
|
||||
|
||||
> **✅ [2026-08-22 기준] M0/M1의 *원래* 체크박스는 전부 닫혔고 다음은
|
||||
> M2(디스패치 엔진)** — 단 M0에는 **[2026-08-22] "재검증 대기" 미체크
|
||||
> **✅ [2026-08-24 기준] M0/M1의 *원래* 체크박스는 전부 닫혔고 다음은
|
||||
> M2(반응형 코어)** — 단 M0에는 **[2026-08-22] "재검증 대기" 미체크
|
||||
> 항목들이 새로 붙었습니다**(설계가 바뀌어 무효화된 스파이크들, 아래 그
|
||||
> 절이 소스). 그건 아직 열린 작업이므로 "M0 완료"로 읽고 넘어가면 안 됩니다. — **⚠️ 다만 M2를 바로 착수하면 안 됩니다: M2와 M3의 의존이
|
||||
> 양방향이라 이 순서로는 M2를 끝까지 짤 수 없습니다**(설계가 아니라
|
||||
> 마일스톤 **순서** 문제 — 아래 M2 배너와 `.claude/question.md` 2번,
|
||||
> 사용자 회신 대기). M1까지의 산출물은
|
||||
> 절이 소스). 그건 아직 열린 작업이므로 "M0 완료"로 읽고 넘어가면 안 됩니다.
|
||||
> **[2026-08-24] 착수를 막던 마일스톤 순서 문제는 해소됐습니다** — M2와
|
||||
> M3의 번호를 맞바꿔 반응형을 먼저 짓기로 확정했습니다(아래 M2 배너가
|
||||
> 소스). **⚠️ 다만 그 교체로 M2 착수 전에 답이 필요한 항목 둘이
|
||||
> `.claude/question.md` 최우선 절로 올라왔습니다** — 중간 State GC 실측과
|
||||
> `store:GetDynamic` 위치(원래 반응형의 게이트였는데 반응형이 앞으로
|
||||
> 오면서 같이 당겨짐). M1까지의 산출물은
|
||||
> quad-base/quad-roblox 폴더+pesde.toml, 루트
|
||||
> default.project.json/.luaurc, mock 테스트 하네스, `New()`/`RunInit`/
|
||||
> `AddPlugin` 골격. **다만 M0의 검증 스파이크 여러 개가 설계 변경으로
|
||||
> 재작성 대기 상태입니다** — 아래 "재검증 대기" 절 참고(개수·현황의
|
||||
> 소스는 `.claude/luau-test/STATUS.md`, 여기서도 그 절에서도 세지 않음).
|
||||
>
|
||||
> **[2026-08-22] 이 문서에 이번에 반영된 것**: `Dispatch.drive`가 두
|
||||
> 패스가 아니라 단일 일반화 `for`(`F-4-1`) / 물리 조작 주입 op 이름이
|
||||
> `native*`로 확정 / `EpochMap`·`GateNode`·`Blocker`·`None`이 M2로 이동 /
|
||||
> M2↔M3 의존이 양방향이라는 것(M2 헤더 배너) / `[x]` 표기 의미 분리
|
||||
> **[2026-08-24] 이 문서에 이번에 반영된 것**: M2(반응형)와 M3(디스패치)의
|
||||
> **번호·순서 교체**, 그에 따라 `Brand`/`Relate`/`LifetimeHandle`
|
||||
> 인터페이스가 M2 앞머리로 이동하고 `EpochMap`/`GateNode`/`Blocker`가 M2로
|
||||
> 복귀, Observer/Effect 동적 경로 가드 등록과 `ObserverEffectLeafHandler`가
|
||||
> M3로 이동.
|
||||
>
|
||||
> **[2026-08-22] 그 전 회차에 반영된 것**: `Dispatch.drive`가 두 패스가
|
||||
> 아니라 단일 일반화 `for`(`F-4-1`) / 물리 조작 주입 op 이름이 `native*`로
|
||||
> 확정 / `None`이 M7에서 디스패치로 이동 / `[x]` 표기 의미 분리
|
||||
> (바로 아래).
|
||||
>
|
||||
> **⚠️ `[x]`의 의미** — 체크박스는 **"짜야 할 코드"만** 담습니다.
|
||||
|
|
@ -31,13 +39,13 @@ quad-v2 구현 단계 실행 계획. 설계 근거/아키텍처 자체는 여기
|
|||
> 마일스톤의 **`### 확정된 것`** 절(항목이 여럿일 때) 또는
|
||||
> **`- **[확정된 것 — 코드 아님]**`** 불릿(하나뿐일 때)에 둡니다. 예전엔
|
||||
> 둘이 같은 목록에 섞여 있어 `[x]`만 보고는 코드가 있는지 알 수
|
||||
> 없었습니다 — M0/M1의 `[x]`는 실제 구현이고, M3/M6/M11에 섞여 있던
|
||||
> 없었습니다 — M0/M1의 `[x]`는 실제 구현이고, M2/M6/M11에 섞여 있던
|
||||
> `[x]`는 설계 확정·실측이었습니다. **새 항목을 추가할 때도 이 구분을
|
||||
> 지킬 것.**
|
||||
|
||||
> M1 착수 도중
|
||||
> wally→pesde 전환이 확정돼(`base/project-setup-plan.md`) M1 체크박스의
|
||||
> `wally.toml` 표기도 `pesde.toml`로 정정. 부수로 M3가 의존하는
|
||||
> `wally.toml` 표기도 `pesde.toml`로 정정. 부수로 M2가 의존하는
|
||||
> `quad-types`/`type-version-check` 두 워크스페이스 멤버도 이 과정에서
|
||||
> 먼저 신설됨(`base/quad-types-plan.md`).
|
||||
|
||||
|
|
@ -141,7 +149,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
Observer가 이제 변경당 **1회**만 울어야 함(옛 "emit은 항상 전파" 모델을
|
||||
검증 중). `base/state-epoch-plan.md` 기준으로 재작성
|
||||
- [ ] **`04`(Dispatch 체인 retractFrom)** — 하강 diff 확정으로 무효화.
|
||||
**M2 착수 시 같이 처리**하는 게 자연스러움
|
||||
**M3 착수 시 같이 처리**하는 게 자연스러움
|
||||
- [ ] **`19`(소유권/참조카운트 Relate 패턴)** — **B 섹션만** 낡음
|
||||
(공개 `AttributeKey(name)` + 인덱스 1 점유 체크가 폐기되고 그룹 전용
|
||||
키 + `AttributeKeyHandler`의 이름 claim으로 바뀜, `0-Z` 확정).
|
||||
|
|
@ -154,13 +162,18 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
**M8 착수 시 같이 처리**
|
||||
- [ ] **`15`(`:Compute` trailing deps 타입팩)** — 이형 다중 deps를 제네릭
|
||||
타입 팩으로 표현 가능한지 미실측(안 되면 동종 dep 1개로 한정).
|
||||
**M3 착수 시 같이 처리**
|
||||
**M2 착수 시 같이 처리**
|
||||
- [ ] **`10`(Roblox Studio 확인)** — `bindLifetime`/`canExecute`/
|
||||
`unbindLifetime` 재정정으로 무효화. **Studio 작업이라
|
||||
`HUMAN_TODO.md` 1번(계정 분리)이 선행**
|
||||
- [ ] **아직 파일이 없는 실측 항목** — `R-11`의 `table.insert` 구멍 재사용,
|
||||
**중간 State GC**(`base/source-state-plan.md`, 상류 strong / 하류 weak
|
||||
불변식 — `.claude/todos.md`가 "M3 착수 전 필요"로 지정)
|
||||
- [ ] **아직 파일이 없는 실측 항목** — **중간 State GC**
|
||||
(`base/source-state-plan.md`, 상류 strong / 하류 weak 불변식 —
|
||||
`.claude/todos.md`가 "M2 착수 전 필요"로 지정. **[2026-08-24 승격]**
|
||||
순서 교체로 반응형이 바로 다음 마일스톤이 되면서 `question.md`
|
||||
최우선 절로 올라갔다). **[2026-08-24 정리]** 여기 같이 적혀 있던
|
||||
`R-11`의 `table.insert` 구멍 재사용은 6라운드 `H-7`로
|
||||
**전제 자체가 없어져 폐기**됐다(`Ref.Callbacks`가 해시맵 셋이 되어
|
||||
구멍 개념이 없음) — `luau-test/STATUS.md`가 소스
|
||||
|
||||
## M1 — 실제 스캐폴딩
|
||||
|
||||
|
|
@ -194,82 +207,34 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
계속 같은 방식으로 쓰이는 중이라 "실사용 시작"이라는 조건은 사실상
|
||||
항상 충족돼 있었음)
|
||||
|
||||
## M2 — 디스패치 엔진
|
||||
## M2 — 반응형 코어 (Source/State/Store)
|
||||
|
||||
> **✅ [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`.
|
||||
|
||||
> **⚠️ [2026-08-22] M2는 이름과 달리 "디스패치만"이 아닙니다 — 반응형
|
||||
> 하부구조 일부가 여기 있습니다.** `EpochMap`/`GateNode`/`Blocker`/`None`
|
||||
> 체크박스가 M3·M7에서 옮겨왔습니다(각 항목의 이동 표시 참고). 그동안
|
||||
> 각주로만 예고돼 있어 M2 체크리스트를 훑는 구현자에게 **항목으로 보이지
|
||||
> 않던** 것을 고친 것이고, `LifetimeHandle` 인터페이스를 M8→M2로 옮겼던
|
||||
> 것과 같은 처리입니다.
|
||||
> **⭐ [2026-08-24 순서 교체] 옛 M3(반응형)와 옛 M2(디스패치)의 번호를
|
||||
> 맞바꿨습니다.** 의존이 양방향처럼 보였지만 실제로는 한 방향이었고
|
||||
> (디스패치 → 반응형이 본체 의존, 반대는 핸들러 등록 표면뿐), 옛 순서로는
|
||||
> 디스패치의 Length/Offset 배관도 그 마일스톤의 `mock 대상 테스트`도 State
|
||||
> 없이 짤 수 없었습니다. 근거와 기각된 선택지는
|
||||
> `.claude/archive/question-resolved.md`의 "마일스톤 경계" 절.
|
||||
>
|
||||
> **⚠️ 다만 이동으로 드러난 더 큰 문제가 하나 남아 있습니다 — M2와 M3의
|
||||
> 의존이 양방향이라, 이 순서대로면 M2를 끝까지 짤 수 없습니다.**
|
||||
> 요지만: **M2는 `Source.luau`/`State.luau` 없이는 구현할 수 없고**
|
||||
> (Length/Offset 배관이 `State<number>`/`Source<number>`를 쓰고,
|
||||
> `Dispatch.drive` 자신도 배치 등록을 Blocker로 게이팅합니다),
|
||||
> 반대 방향은 핸들러 등록 표면만 요구하는 얕은 의존입니다.
|
||||
> **어느 쪽으로 가를지(순서 교체 / M2 분할 / 유지)는 사용자 결정
|
||||
> 대기이고, 근거와 선택지의 소스는 `.claude/question.md` 2번입니다** —
|
||||
> 여기서 반복하지 않습니다.
|
||||
> **⚠️ 이 날짜 이전에 쓰인 `session/`·`archive/`·`qa-request/` 문서의
|
||||
> `M2`/`M3`는 옛 의미**(M2=디스패치, M3=반응형)입니다 — 히스토리 문서라
|
||||
> 소급 수정하지 않았습니다. 라이브 문서(`base/`/`research/`/`reference/`/
|
||||
> `luau-test/`/인덱스 레이어)는 전부 새 번호로 맞췄습니다.
|
||||
>
|
||||
> 부수로 **`Brand`/`Relate`/`LifetimeHandle` 인터페이스가 디스패치에서 이
|
||||
> 마일스톤 앞머리로 왔습니다** — 반응형이 이 셋을 먼저 요구하기 때문:
|
||||
> `Source`가 `SourceBrand`+`EpochBrand`에 등록되고(`Brand`), State 전파
|
||||
> 루프가 매 발화마다 `canExecute`를 부르며(`LifetimeHandle`), 그 판정이
|
||||
> `Relate`에 복사해둔 gcconn 위에 얹힙니다. 반대로 2026-08-22에 여기서
|
||||
> 디스패치로 옮겼던 **`EpochMap`/`GateNode`/`Blocker`는 되돌아왔습니다** —
|
||||
> 앞당길 이유 자체가 순서 교체로 사라졌고, "게이팅 먼저"는 그대로
|
||||
> 지켜집니다.
|
||||
|
||||
### 공통 기반 — 반응형보다 먼저 (구 M2, 지금의 M3에서 이동)
|
||||
|
||||
> 셋 다 State-free이자 dispatch-free라 어느 쪽에도 안 걸립니다. 이 절이
|
||||
> 끝나야 아래 반응형 본체를 짤 수 있습니다.
|
||||
|
||||
- [ ] `Dispatch/init.luau` — `Dispatch.getHandler(inst,k,v): Handler?`(순수
|
||||
스캔, `isHandlable`+`priority`) / `Dispatch.process(inst,k,v,index)`
|
||||
(오케스트레이터: getHandler → **그 인덱스의 기존 핸들러와 비교** →
|
||||
같으면 그 자리 클로저에 새 값을 넘기고 같은 핸들러의 `.process`로
|
||||
자리 교체, 다르면 `retractFrom` 후 새로 설치. 반환값이 `nil`이면
|
||||
즉시 error) / **3-인자** `Dispatch.retractFrom(inst,k,index)`
|
||||
(아래 항목) / `Dispatch.addHandler(handler)`(레지스트리 등록,
|
||||
quad-roblox가 팩토리 뮤테이션 시점에 호출) / `Dispatch.drive(inst,
|
||||
flattened)`(**일반화 `for` 한 번**으로 순회하며 각 `(k,v)`에
|
||||
`Dispatch.process(inst,k,v,1)` 호출 — `dispatch-core-plan.md`의 `None`
|
||||
센티널 절, 2026-08-07 여덟 번째 세션에 네이밍 확정).
|
||||
**[2026-08-21 구현 전 QA 4라운드 `F-4-1`] 순회는 두 패스가 아니라
|
||||
단일 일반화 `for`다** — "배열 파트 전체가 해시 파트보다 먼저"는 여전히
|
||||
base가 보장하는 **계약**이지만, `flattened`가 항상 평범한 Luau
|
||||
테이블이라 일반화 `for` 한 번이 그 순서를 그대로 주고 두 층위는
|
||||
`type(k) == "number"` 분기로 가른다(`base/dispatch-core-plan.md`의
|
||||
`F-4-1` 정정 문단). 옛 "명시적 두 패스" 서술은 구현까지 2회 순회로
|
||||
못박은 것처럼 읽혀 정정됨 — 그 때문에 스파이크 `01`도 재작성 대기
|
||||
(`luau-test/STATUS.md`)
|
||||
- [ ] **⭐ [2026-08-24 신설, 6라운드 손 트레이싱 `H-39`] 말단 핸들러는 예외
|
||||
없이 자기 배열 자리의 `setOffsetSource(inst,k,None)` → `setLength(inst,k,0)`을
|
||||
등록한다** — 이 계약을 핸들러 작성 체크리스트에 넣고, 실제로 넷이
|
||||
빠져 있었으니 각 마일스톤에서 확인할 것: `TagHandler`(M10) /
|
||||
`AttributeGroupHandler`(M10) / `RefLeafHandler`(M8) /
|
||||
`ObserverEffectLeafHandler`(M3). 안 하면 `bk.N`이 다른 자리 등록으로
|
||||
커질 때 그 구멍이 범위 안에 끼어 `recompute`가 **명시적 error로 죽는다**
|
||||
(`Frame { Tag("card"), TextLabel{} }`처럼 말단이 앞에 오는 흔한 배치).
|
||||
**배열 맨 끝이면 안 터지므로 "가끔 되고 가끔 터지는" 형태로 드러난다.**
|
||||
소스는 `base/dispatch-core-plan.md`의 등록 책임 절
|
||||
- [ ] **⭐ [2026-08-24 신설, 6라운드 손 트레이싱 `H-25`] `quad-types`의 `Quad`
|
||||
타입에 `Dispatch` 필드와 그 타입 재수출을 추가** — `Quad`가 5필드 닫힌
|
||||
레코드이고 `RunInit`은 반환값이 없어 타입을 못 넓히므로, 이걸 안 하면
|
||||
`module:RunInit(InitDispatch)` 뒤의 `quad.Dispatch` 접근이 **런타임엔
|
||||
되는데 `luau-analyze`에선 타입에러**다(실측 재현). `base/architecture.md`의
|
||||
확정된 결정 13번이 그 접근을 표준 사용법으로 못박아뒀다.
|
||||
상세는 `base/quad-types-plan.md`의 "`Quad` 타입 — 확정된 표면" 절.
|
||||
**⚠️ 이건 M2 한 번으로 끝나는 일이 아니다** — 그 문서가 *"마일스톤마다
|
||||
갱신한다 … 이후 서브시스템도 같은 규칙을 따른다"*로 확정했으므로,
|
||||
**서브시스템을 붙이는 모든 마일스톤이 같은 항목을 진다**:
|
||||
M3(`Source`/`State`/`Store`) · M6(`Slot`) · M7(`Modifier`) ·
|
||||
M8(`Ref`) · M10(`Tag`/`Attribute`). 빠뜨리면 그 마일스톤 완료 후
|
||||
`quad.Store`/`quad.Slot` 접근이 런타임엔 되는데 `luau-analyze`에선
|
||||
타입에러인, `H-25`가 실측한 그 문제가 **마일스톤마다 반복된다.**
|
||||
- [ ] `Handler.luau`(핸들러 계약 타입: `isHandlable(inst,k,v)`/`priority`/
|
||||
`process(inst,k,v,index) -> (hintValue)->()` **3종** — `isHandlable`도
|
||||
`inst`를 받도록 확정(2026-08-07 여덟 번째 세션), 별도 `retract` 필드는
|
||||
`process` 반환값으로 합쳐짐(2026-08-13 다섯 번째 세션))
|
||||
- [ ] `Brand.luau`(**[2026-08-21 재작성]** 인스턴스 브랜드 — `Brand()`가
|
||||
브랜드마다 weak-key 집합 하나를 들고 `:register(x)`/`:is(x)`,
|
||||
**다중 태깅 허용**(`Source`가 `SourceBrand`이면서 동시에 `EpochBrand`).
|
||||
|
|
@ -294,49 +259,12 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
array-part `v`를 판별, `isTag`와 같은 결)로 분리됨 — 그룹
|
||||
`Attribute(...)` 프리미티브 신설로 같은 이름이 서로 다른 두
|
||||
대상(키 vs 값)을 가리키게 돼서 갈라짐, `base/attribute-plan.md`
|
||||
참고) 전부의 기반. `isNone`만 예외로 레지스트리 없이 `x == None`
|
||||
항등 비교 — `brand-plan.md`의 `Brand` 절, 2026-08-07 여덟
|
||||
번째 세션 신설)
|
||||
- [ ] **[2026-08-22 M3에서 이동] `EpochMap.luau`** — 재사용 가능한 에포크
|
||||
부기 객체(`:Update(Epoch|EpochSet) -> boolean`이 "뒤로 전파가
|
||||
필요한가"를 답함, `:Refresh`/`:Sync`/`:TrackFrom`. `EpochSet =
|
||||
{[Epoch]: true}`로 **배열이 아니라 집합**). `State.luau`에 묻지 말고
|
||||
별도 모듈로 낼 것 — `GateNode`(아래)와 `State`(M3)/`Effect`(M3)가
|
||||
전부 같은 것을 쓴다. `Epoch` 인터페이스 자체(`{ Revision: number }`)와
|
||||
리비전 갱신(`bit32.bnot(-rev)`)도 여기서 확정 — `base/state-epoch-plan.md`
|
||||
- [ ] **[2026-08-22 M3에서 이동] `state:Gate(setup)` + `GateNode`** —
|
||||
emit을 가로채 유보했다가 한 번에 내보내는 공용 게이트 노드
|
||||
(`ComputeNode`와 같은 층위, 탑레벨 `Gate(...)` 프리미티브는 안 만듦).
|
||||
유보 배치는 `withheld : { [epoch] : true }`(집합), flush 때 테이블을
|
||||
통째로 갈고, **내보내는 emit이 싣는 건 그렇게 떼어낸 `EpochSet`
|
||||
스냅샷뿐이다 — 게이트 노드 자신은 안 싣는다**(하류가 게이트 identity를
|
||||
한 번도 안 쓴다, `base/state-epoch-plan.md` §5). **빈 배치는
|
||||
아무것도 안 함**. `emitEpochMap`을 쓰므로 위 `EpochMap.luau`가 선행 —
|
||||
`base/gate-plan.md`. **"게이팅 먼저" 결정으로 M3에서 앞당겨진 항목**
|
||||
(사용자: *"게이팅 먼저. 게이팅을 base 에 만들 준비를 해야한다"*)
|
||||
- [ ] **[2026-08-22 M3에서 이동] `Blocker.luau`** — 위 `GateNode` 위에
|
||||
얹히는 **정책**(다시 노드를 만들지 말 것). 여러 Source를 한꺼번에
|
||||
바꿔도 파생값 재계산/재대입이 한 번만 되게 하는 primitive
|
||||
(`base/blocker-plan.md`). 아래 `Dispatch.setLength`/`setOffsetSource`의
|
||||
배치 등록이 `:On()`/`:IsOn()`/`:OffWithoutEmit()` **세 메서드**를
|
||||
호출하므로(`base/gate-plan.md` 9번이 소스 — Blocker 인스턴스를 lazy
|
||||
조회하는 `getBlocker(ownerKey)`는 Blocker 메서드가 아니라 Dispatch
|
||||
쪽 헬퍼다) **최소한 그 셋이 도는 형태까지는 M2에 필요**
|
||||
- [ ] **[2026-08-22 M7에서 이동] `None.luau`(센티널) + `Dispatch/None.luau`
|
||||
(`NoneHandler` + `NilHandler`)** — `None`은 modifier 전용 값이 아니라
|
||||
디스패치 배관이라 여기 소속(`architecture.md`의 소스 트리에 있는 건
|
||||
핸들러 쪽 `Dispatch/None.luau`뿐 — **[2026-08-22] 탑레벨 센티널
|
||||
`None.luau`와 `Brand.luau`는 그 트리에 아직 줄이 없다**, M2 구현 시
|
||||
트리도 같이 채울 것).
|
||||
`NoneHandler`는 배열/해시 구분 없이 `Dispatch.process(inst,k,nil,index+1)`
|
||||
**재귀만** 하고(선행 `retractFrom` 없음 — 하강 diff), 실제 정리는
|
||||
`NilHandler`(`isHandlable`이 `type(k) == "number" and v == nil`일 때만
|
||||
매치하는 말단)가 `Dispatch.setLength(inst,k,0)` +
|
||||
`Dispatch.setOffsetSource(inst,k,None)` 등록으로 맡는다.
|
||||
**`Dispatch.drive`에 `None` 스킵 분기는 없다**(반응형 값이 내놓는
|
||||
`None`은 어차피 `process`에 도착하므로 — 2026-08-18 재설계) —
|
||||
`base/dispatch-core-plan.md`의 "`None` 센티널"/"`NilHandler`" 절.
|
||||
Modifier 쪽 표면(인라인 키로 필드 지우기, `Peek` 반환 타입)은 M7
|
||||
참고) 전부의 기반. `isNone`은 레지스트리를 안 쓰고 `x == None` 항등
|
||||
비교이지만 **`Brand.luau`가 `None`을 참조하지는 않는다**
|
||||
(**[2026-08-18 정정]** `brand-plan.md`가 *"`Brand → None` 의존을 만들지
|
||||
않는다"*로 확정 — `isNone`은 `None.luau`(M3) 쪽에 산다. 이 마일스톤
|
||||
분리로 처음 눈에 띈 잔재를 **[2026-08-24]** 정정한 것) —
|
||||
`brand-plan.md`의 `Brand` 절, 2026-08-07 여덟 번째 세션 신설)
|
||||
- [ ] `Relate.luau`(전체가 quad-base, 순수 Lua — `base/relate-plan.md`) —
|
||||
`Relate()` 비싱글톤 생성자, `:SetWeak`/`:GetWeak`/`:SetStrong`/`:GetStrong`.
|
||||
`inst`(첫 인자)는 항상 weak, `StrongMap`/`WeakMap` 서브테이블은 lazy
|
||||
|
|
@ -373,9 +301,259 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
`canExecute`/`unbindLifetime` — 확정" 절 참고. **이중 바인딩 금지
|
||||
게이트는 `canBound`**(`canExecute`는 emit 전파 게이팅 전용 —
|
||||
**[2026-08-14 열한 번째 세션] `canBound`가 별도 진입점으로 재도입되어
|
||||
다시 갈라짐, 판정 로직은 공유하는 비공개 헬퍼 하나 — M3 체크박스
|
||||
다시 갈라짐, 판정 로직은 공유하는 비공개 헬퍼 하나 — M2 체크박스
|
||||
참고**), children 배열 leaf 부착이 실제로는 `bindLifetime` 호출이라
|
||||
이 게이트를 그대로 탐
|
||||
|
||||
### 반응형 본체
|
||||
|
||||
- [ ] **`EpochMap.luau`** (**[2026-08-24]** 2026-08-22에 디스패치로 옮겼다가 순서 교체로 되돌아옴) — 재사용 가능한 에포크
|
||||
부기 객체(`:Update(Epoch|EpochSet) -> boolean`이 "뒤로 전파가
|
||||
필요한가"를 답함, `:Refresh`/`:Sync`/`:TrackFrom`. `EpochSet =
|
||||
{[Epoch]: true}`로 **배열이 아니라 집합**). `State.luau`에 묻지 말고
|
||||
별도 모듈로 낼 것 — `GateNode`(아래)와 `State`/`Effect`가
|
||||
전부 같은 것을 쓴다. `Epoch` 인터페이스 자체(`{ Revision: number }`)와
|
||||
리비전 갱신(`bit32.bnot(-rev)`)도 여기서 확정 — `base/state-epoch-plan.md`
|
||||
- [ ] **[2026-08-21 5라운드 — 채택 확정, 같은 날 `Epoch`로 일반화]** State의
|
||||
재계산/전파 판정은 **`Epoch` 리비전 비교**다(`base/state-epoch-plan.md`)
|
||||
— `invalid` 플래그가 아니다. **아래 `Source.luau`/`State.luau`가 이걸
|
||||
전제로 짜여야 하므로 `EpochMap.luau`(위 항목)가 State 본체보다 먼저
|
||||
온다**(`State`가 `valueEpochMap`/`emitEpochMap` 둘을 컴포지션한다 —
|
||||
**[2026-08-24] 감사에서 순서가 반대로 놓여 있던 걸 잡아 앞으로 옮김**): `Source`가 `type Epoch = { Revision: number }`를
|
||||
구조적으로 만족하고(`EpochBrand`에도 등록), 부기는 재사용 가능한
|
||||
**`EpochMap`**(`:Update(Epoch|EpochSet) -> boolean`, `:Refresh`, `:Sync`,
|
||||
`:TrackFrom` — `EpochSet = {[Epoch]: true}`, **배열 아님**)
|
||||
으로 떼어내며, State가 그걸 **둘** 컴포지션한다 — `valueEpochMap`(값
|
||||
유효성)/`emitEpochMap`(전파 dedup). emit은 값도 리비전도 안 싣고
|
||||
**출처(`Epoch`나 그 집합)만** 싣고, 순회는 `rawInvalid == false`일 때만
|
||||
돌며 **값만 앞당기고 통지는 상류 emit을 기다린다**. 다이아몬드 중복
|
||||
통지가 접히므로 스파이크 `05`도 그에 맞춰 재작성해야 한다
|
||||
(`luau-test/STATUS.md`).
|
||||
- [ ] `Source.luau`/`State.luau`/`Store.luau`
|
||||
- [ ] **[2026-08-18 신설]** `store:GetDynamic<<T>>(name): Source<T>` — 런타임에
|
||||
이름이 정해지는 동적 키의 정식 창구(옛 `store "key"` 문자열 커링은
|
||||
기각). **⚠️ 콜론 메소드로 두면 `__index`가 고정 메소드 테이블을 먼저
|
||||
확인해야 하고 `GetDynamic`이 예약 키가 됨** — 탑레벨 함수로 둘지
|
||||
아직 미결(`base/store-plan.md`의 "타입 추론 문제" 절, `question.md` 최우선 절)
|
||||
- [ ] **State 전파 루프 — 구독자는 weak, 발화마다 `canExecute` 게이팅**
|
||||
(2026-08-14 다섯 번째 세션 확정, `base/lifecycle-pattern.md`의 "실제
|
||||
호출부 — State 전파(`emit`)가 `canExecute`로 게이팅한다" 절) —
|
||||
State는 구독자(Observer의 emit 클로저)를 **weak로만** 담고, 살려두는
|
||||
책임은 `gchold`(leaf) 또는 전역 `Subscribed` 테이블(전역)에 있음
|
||||
(어디에도 안 묶인 Observer는 GC되어 목록에서 자연히 빠짐). 발화 시
|
||||
각 구독자에 대해 `canExecute(observer)`가 거짓이면 **그 구독자만
|
||||
조용히 건너뜀**(no-op) — 이게 `canExecute`의 유일한 실제 호출부이고,
|
||||
`inst`를 인자로 받을 수 없는 이유(State는 자기가 어느 Instance에
|
||||
걸렸는지 모름). `state:Observer(fn)`의 "등록 즉시 1회 실행"은
|
||||
`bindLifetime` 이전에 동기적으로 일어나므로 이 게이팅과 무관
|
||||
- **[확정된 것 — 코드 아님]** `store.key` dot-access 타입 추론 확인 — Luau `type function`
|
||||
(`WrapStore`/`ProcessStoreType`)으로 `Store<T>`가 `T`의 각 필드를
|
||||
`Source`로 감싼 레코드 타입을 합성 가능함을 확인(2026-08-12 열일곱
|
||||
번째 세션, `base/typing-limits.md` §5) — **[2026-08-15 실측 완료]**
|
||||
`luau-test/done/16-type-store-key-typefunction.luau` 통과(원인은
|
||||
설계 문제가 아니라 `types.newfunction` API 버전 드리프트였음)
|
||||
- [ ] `:Compute(fn, ...)` — trailing args로 추가 의존성 직접 받는 sugar
|
||||
(2026-08-11 세션, `base/source-state-plan.md` "`:Compute(fn, ...)`"
|
||||
절) — `:With(...):Compute(fn)` 체인과 달리 노드 1개(Compute 노드
|
||||
자신에 구독만 추가)로 끝나야 함, 새 노드 생성 없이 구현되는지 M0/M2
|
||||
스파이크에서 확인. **[2026-08-24 정정, 6라운드 `H-13`]** 여기 원래
|
||||
*"`Effect`/`Observer`는 대칭 sugar 없이 `:With` 명시 유지"*라고 적혀
|
||||
있었는데 **`Effect`는 `C-6`에서 이미 역전됐다** — 각 dep에 구독을 따로
|
||||
걸면 합치는 노드 자체가 안 생겨 감출 비용이 없다. **기각으로 남은 건
|
||||
`Observer` 하나**이고 근거도 새로 쓰였다("Observer는 리시버 State
|
||||
하나에 붙는 구독, 여럿을 엮는 건 Effect가 대신한다")
|
||||
- [ ] `state:Apply(factory)`(`base/source-state-plan.md` "`state:Apply(factory)`"
|
||||
절, 2026-08-07 일곱 번째 세션) — `factory(self)`를 체이닝 문법으로
|
||||
부르는 순수 설탕, `factory: (State<T>) -> U): U`로 열린 타입. Source도
|
||||
기존 `:With`/`:Compute` 델리게이션에 얹혀 자동 포함
|
||||
- [ ] `state:Observer(fn)` — children 배열 leaf 참가자, **등록 즉시 1회
|
||||
실행 확정**(`base/source-state-plan.md`의 Observer 절), `isObserver`
|
||||
판별자, canExecute 게이팅, `:Subscribe()`/`:Unsubscribe()`. **동적
|
||||
경로 가드**(`{priority = HANDLER_PRIORITY_FALLBACK, isHandlable = v
|
||||
is Observer, process = error(...)}`, `k` 타입 안 가림, 2026-08-14
|
||||
열한 번째 세션 — `PreRef`와 같은 패턴)도 같이 등록
|
||||
**⚠️ [2026-08-24] 단 그 가드를 `Dispatch.addHandler`로 등록하는 것
|
||||
자체는 M3다** — 레지스트리가 거기서 생긴다(M3의 그 항목).
|
||||
- [ ] `Effect(fn, ...deps)`(`base/effect-plan.md`, **[2026-08-21 5라운드
|
||||
`C-6`]** 옛 시그니처는 `Effect(fn, state?)`) — deps 생략 시 설치
|
||||
1회+leaf 사망 시 확정 정리, deps 지정 시 **각각에 맞는 구독**
|
||||
(State/Source는 `Observer`, `Ref`는 `:Callback`)을 걸어
|
||||
재실행+cleanup 체이닝(React `useEffect` 동형). **`EffectHandle`이
|
||||
`EpochMap`을 하나 들어** 공통 상류로 인한 중복 발화를 접고, 설치 구간
|
||||
억제 플래그가 그 `Update`보다 먼저 와야 함
|
||||
(`base/state-epoch-plan.md`). Observer 구현 이후에 착수(의존 관계).
|
||||
`EffectHandle:Subscribe()`/`:Unsubscribe()`도 추가(leaf 없이 쓰는
|
||||
모듈/스크립트 레벨 Effect) — `:Unsubscribe()`는 Observer와 달리
|
||||
마지막 cleanup을 1회 트리거해야 함(2026-08-07 일곱 번째 세션).
|
||||
**동적 경로 가드**도 Observer와 같은 패턴으로 등록(`base/effect-plan.md`
|
||||
"동적 경로 가드" 절, 2026-08-14 열한 번째 세션)
|
||||
**⚠️ [2026-08-24] 단 그 가드를 `Dispatch.addHandler`로 등록하는 것
|
||||
자체는 M3다** — 레지스트리가 거기서 생긴다(M3의 그 항목).
|
||||
- [ ] **⭐ [2026-08-24 신설, 6라운드] `Effect` 구현 시 같이 만들 것** —
|
||||
**`handle._observers`**(배열, 단수 `_observer`에서 바뀜 `H-8`) ·
|
||||
**`handle._cleanup`**(직전 cleanup 보관, `Rerun`과 `Destroying` 클로저가
|
||||
같은 자리를 읽는다) · **`handle._refDeps`/`_refCallbacks`**(`Ref` dep과
|
||||
거기 건 클로저 — 해제 시 값으로 떼야 해서 보관 필요) ·
|
||||
**`handle._destroyConn`** · **`:_bindDestroying(inst)`/`:_unbindDestroying()`**
|
||||
(`bindLifetime`/`unbindLifetime`이 `isEffect`일 때 부르는 훅, `H-11`).
|
||||
`fn` 시그니처는 **`fn(self: EffectHandle) -> (() -> ())?`**이고
|
||||
**`...deps`는 `fn`에 안 넘어간다**(`H-14`). `Ref` 콜백은 본문 맨 앞에서
|
||||
**`canExecute(handle)`를 확인**한다(`H-7`). 의사코드는
|
||||
`base/effect-plan.md`가 소스
|
||||
- [ ] **[2026-08-24 `H-23`]** State 전파 루프는 구독자 집합을 **배열로
|
||||
스냅샷한 뒤** 돈다 — 순회 중 새 구독자 추가가 정상 경로인데 Lua에서
|
||||
미정의라, 실측에서 실행마다 결과가 달라지고 한 Observer가 통째로
|
||||
누락됐다. "이번 파동 중에 붙은 구독자는 다음 파동부터"가 계약
|
||||
- [ ] Observer/Effect 이중 바인딩 금지 — `canBound(value)` 게이트로
|
||||
`:Subscribe()`(전역)와 `bindLifetime`(inst-scoped, leaf 부착도
|
||||
내부적으로 이걸 호출)이 동시에 걸리면 즉시 `error`(`base/source-state-plan.md` "이중 바인딩 금지" 절, 2026-08-07 일곱 번째
|
||||
세션 신설, 2026-08-09 여섯 번째 세션에서 "leaf 부착=bindLifetime
|
||||
호출"로 정정 — 진짜 독립 경로는 둘뿐).
|
||||
**[2026-08-14 다섯 번째 세션에 별도 predicate `canBound(handle)`을
|
||||
폐기하고 `canExecute` 하나로 합쳤다가, 같은 날 열한 번째 세션에
|
||||
다시 갈라짐]** — "지금 묶어도 되는가"(bound 문맥)와 "지금
|
||||
발화해도 되는가"(execute 문맥)는 호출부의 질문이 다르고
|
||||
**[2026-08-18 구현 전 QA 정정] 판정값도 같은 게 아니라 서로의
|
||||
부정**이라(`canBound(v) == not canExecute(v)`, 게이트는 항상
|
||||
`if not canBound(v) then error(...)`), `Ref` 이중 배치
|
||||
방지(`question.md` 0-W)를 계기로 `canBound`가
|
||||
별도 진입점으로 재도입됨 — 판정 로직(비공개 `isBoundAlive` 헬퍼)은
|
||||
공유해 코드 중복은 없음. **이 절이 쓰는 게이트는 이제 `canBound`**
|
||||
(emit 전파 게이팅 전용 `canExecute`가 아님). `.Subscribed` 필드가
|
||||
leaf 경로와 무관하다는 것, leaf 생존 판정을 `bindLifetime`이 `value`
|
||||
쪽 `Relate`에 복사해둔 gcconn으로 하는 것은 안 바뀜 — `base/
|
||||
lifecycle-pattern.md`의 "`canBound` vs `canExecute`" 절, 역전 경위는
|
||||
`archive/canexecute-inst-arg-reversed.md`. 부수 효과로 **바인딩이
|
||||
죽은 뒤(`Destroy`/`unbindLifetime`)의 재사용은 게이트를 통과**
|
||||
(살아있는 바인딩만 막는 게 의도, 안 바뀜)
|
||||
- [ ] **`state:Gate(setup)` + `GateNode`** (**[2026-08-24]** 위 `EpochMap`과 같이 되돌아옴) —
|
||||
emit을 가로채 유보했다가 한 번에 내보내는 공용 게이트 노드
|
||||
(`ComputeNode`와 같은 층위, 탑레벨 `Gate(...)` 프리미티브는 안 만듦).
|
||||
유보 배치는 `withheld : { [epoch] : true }`(집합), flush 때 테이블을
|
||||
통째로 갈고, **내보내는 emit이 싣는 건 그렇게 떼어낸 `EpochSet`
|
||||
스냅샷뿐이다 — 게이트 노드 자신은 안 싣는다**(하류가 게이트 identity를
|
||||
한 번도 안 쓴다, `base/state-epoch-plan.md` §5). **빈 배치는
|
||||
아무것도 안 함**. `emitEpochMap`을 쓰므로 위 `EpochMap.luau`가 선행 —
|
||||
`base/gate-plan.md`. **"게이팅 먼저" 결정의 산물**
|
||||
(사용자: *"게이팅 먼저. 게이팅을 base 에 만들 준비를 해야한다"*)
|
||||
- [ ] **`Blocker.luau`** (**[2026-08-24]** 위 둘과 같이 되돌아옴) — 위 `GateNode` 위에
|
||||
얹히는 **정책**(다시 노드를 만들지 말 것). 여러 Source를 한꺼번에
|
||||
바꿔도 파생값 재계산/재대입이 한 번만 되게 하는 primitive
|
||||
(`base/blocker-plan.md`). **M3의** `Dispatch.setLength`/`setOffsetSource`의
|
||||
배치 등록이 `:On()`/`:IsOn()`/`:OffWithoutEmit()` **세 메서드**를
|
||||
호출하므로(`base/gate-plan.md` 9번이 소스 — Blocker 인스턴스를 lazy
|
||||
조회하는 `getBlocker(ownerKey)`는 Blocker 메서드가 아니라 Dispatch
|
||||
쪽 헬퍼다) **최소한 그 셋이 도는 형태까지는 M3(디스패치)가 요구**
|
||||
- [ ] **[2026-08-24 `H-25` 파생]** `quad-types`의 `Quad`에 `Source`/`State`/
|
||||
`Store` 필드 추가 — **규칙 요지**: `New(): Quad`가 닫힌 레코드이고
|
||||
`RunInit`은 반환값이 없어 타입을 못 넓히므로, 서브시스템을 붙이는
|
||||
마일스톤마다 `quad-types`의 `Quad`에 그 필드와 타입 재수출을 같이
|
||||
추가한다. 안 하면 그 마일스톤 완료 후 `quad.Store` 접근이 런타임엔
|
||||
되는데 `luau-analyze`에선 타입에러다(`H-25` 실측). **순서 교체로
|
||||
이 마일스톤이 그 규칙의 첫 적용 지점**이고, 규칙의 정본은
|
||||
`base/quad-types-plan.md`의 "`Quad` 타입 — 확정된 표면" 절
|
||||
(M3의 `H-25` 항목이 같은 규칙을 `Dispatch` 기준으로 서술한다)
|
||||
- [ ] trailing deps를 `fn`에 lazy positional 인자로도 노출(**⚠️ [2026-08-24
|
||||
`H-14`] 이 항목은 `:Compute` 한정이다** — `Effect`의 `fn`엔 deps가 안
|
||||
넘어간다) — (`fn(self,
|
||||
previous?, dep1, ..., depN)` — 순서는 Luau 값 레벨 `...`가 파라미터
|
||||
리스트 맨 끝이어야 하는 것과 같은 이유로 `previous?`가 deps 팩
|
||||
**앞**에 와야 함, 2026-08-11 후속 세션 제안 → 같은 날 세 번째
|
||||
세션에 순서 정정, `base/source-state-plan.md` "trailing deps를 fn에
|
||||
lazy positional 인자로도 노출" 절) — 방향/순서는 확정,
|
||||
`luau-test`의 `15-type-compute-trailing-deps-typepack.luau`로
|
||||
이형 다중 deps를 제네릭 타입 팩으로 표현 가능한지만 실측 필요(안
|
||||
되면 동종 타입 dep 1개로 한정)
|
||||
- [ ] mock 대상 테스트
|
||||
|
||||
## M3 — 디스패치 엔진
|
||||
|
||||
> **✅ [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`.
|
||||
|
||||
> **⭐ [2026-08-24 순서 교체] 이 마일스톤은 옛 M2입니다** — 번호를 반응형
|
||||
> 코어와 맞바꿔 그 뒤로 옮겼습니다(경위는 M2 배너가 소스, 여기서 반복하지
|
||||
> 않음). 2026-08-22에 여기로 옮겼던 `EpochMap`/`GateNode`/`Blocker`와,
|
||||
> 반응형이 먼저 요구하는 `Brand`/`Relate`/`LifetimeHandle` 인터페이스는
|
||||
> M2로 갔습니다. 여기 남은 것은 전부 디스패치 배관이고, **이제 이
|
||||
> 마일스톤은 M2 위에 단방향으로 얹힙니다.** 개념상 역방향이던 둘
|
||||
> (Observer/Effect 가드 등록, `ObserverEffectLeafHandler` — "핸들러를
|
||||
> 등록한다"뿐인 것)은 **맨 아래 두 항목으로 여기 옮겨져** 있으므로,
|
||||
> **빌드 순서상 역방향 간선은 없습니다.**
|
||||
|
||||
|
||||
- [ ] `Dispatch/init.luau` — `Dispatch.getHandler(inst,k,v): Handler?`(순수
|
||||
스캔, `isHandlable`+`priority`) / `Dispatch.process(inst,k,v,index)`
|
||||
(오케스트레이터: getHandler → **그 인덱스의 기존 핸들러와 비교** →
|
||||
같으면 그 자리 클로저에 새 값을 넘기고 같은 핸들러의 `.process`로
|
||||
자리 교체, 다르면 `retractFrom` 후 새로 설치. 반환값이 `nil`이면
|
||||
즉시 error) / **3-인자** `Dispatch.retractFrom(inst,k,index)`
|
||||
(아래 항목) / `Dispatch.addHandler(handler)`(레지스트리 등록,
|
||||
quad-roblox가 팩토리 뮤테이션 시점에 호출) / `Dispatch.drive(inst,
|
||||
flattened)`(**일반화 `for` 한 번**으로 순회하며 각 `(k,v)`에
|
||||
`Dispatch.process(inst,k,v,1)` 호출 — `dispatch-core-plan.md`의 `None`
|
||||
센티널 절, 2026-08-07 여덟 번째 세션에 네이밍 확정).
|
||||
**[2026-08-21 구현 전 QA 4라운드 `F-4-1`] 순회는 두 패스가 아니라
|
||||
단일 일반화 `for`다** — "배열 파트 전체가 해시 파트보다 먼저"는 여전히
|
||||
base가 보장하는 **계약**이지만, `flattened`가 항상 평범한 Luau
|
||||
테이블이라 일반화 `for` 한 번이 그 순서를 그대로 주고 두 층위는
|
||||
`type(k) == "number"` 분기로 가른다(`base/dispatch-core-plan.md`의
|
||||
`F-4-1` 정정 문단). 옛 "명시적 두 패스" 서술은 구현까지 2회 순회로
|
||||
못박은 것처럼 읽혀 정정됨 — 그 때문에 스파이크 `01`도 재작성 대기
|
||||
(`luau-test/STATUS.md`)
|
||||
- [ ] **⭐ [2026-08-24 신설, 6라운드 손 트레이싱 `H-39`] 말단 핸들러는 예외
|
||||
없이 자기 배열 자리의 `setOffsetSource(inst,k,None)` → `setLength(inst,k,0)`을
|
||||
등록한다** — 이 계약을 핸들러 작성 체크리스트에 넣고, 실제로 넷이
|
||||
빠져 있었으니 각 마일스톤에서 확인할 것: `TagHandler`(M10) /
|
||||
`AttributeGroupHandler`(M10) / `RefLeafHandler`(M8) /
|
||||
`ObserverEffectLeafHandler`(이 마일스톤, 맨 아래). 안 하면 `bk.N`이 다른 자리 등록으로
|
||||
커질 때 그 구멍이 범위 안에 끼어 `recompute`가 **명시적 error로 죽는다**
|
||||
(`Frame { Tag("card"), TextLabel{} }`처럼 말단이 앞에 오는 흔한 배치).
|
||||
**배열 맨 끝이면 안 터지므로 "가끔 되고 가끔 터지는" 형태로 드러난다.**
|
||||
소스는 `base/dispatch-core-plan.md`의 등록 책임 절
|
||||
- [ ] **⭐ [2026-08-24 신설, 6라운드 손 트레이싱 `H-25`] `quad-types`의 `Quad`
|
||||
타입에 `Dispatch` 필드와 그 타입 재수출을 추가** — `Quad`가 5필드 닫힌
|
||||
레코드이고 `RunInit`은 반환값이 없어 타입을 못 넓히므로, 이걸 안 하면
|
||||
`module:RunInit(InitDispatch)` 뒤의 `quad.Dispatch` 접근이 **런타임엔
|
||||
되는데 `luau-analyze`에선 타입에러**다(실측 재현). `base/architecture.md`의
|
||||
확정된 결정 13번이 그 접근을 표준 사용법으로 못박아뒀다.
|
||||
상세는 `base/quad-types-plan.md`의 "`Quad` 타입 — 확정된 표면" 절.
|
||||
**⚠️ 이건 M3 한 번으로 끝나는 일이 아니고, 첫 적용은 오히려 M2다** —
|
||||
그 문서가 *"마일스톤마다 갱신한다 … 이후 서브시스템도 같은 규칙을
|
||||
따른다"*로 확정했으므로, **서브시스템을 붙이는 모든 마일스톤이 같은
|
||||
항목을 진다**: **M2(`Source`/`State`/`Store`, 규칙이 처음 적용되는
|
||||
자리)** · M6(`Slot`) · M7(`Modifier`) · M8(`Ref`) ·
|
||||
M10(`Tag`/`Attribute`). (**[2026-08-24]** 규칙 자체는 `Dispatch`를
|
||||
계기로 쓰였지만 마일스톤 순서 교체로 M2가 앞에 오게 됐다 — M2를 짜는
|
||||
사람이 이 항목을 앞으로 참조하지 않아도 되게, 규칙 요지는 M2의
|
||||
`H-25` 파생 항목에도 적어뒀다.) 빠뜨리면 그 마일스톤 완료 후
|
||||
`quad.Store`/`quad.Slot` 접근이 런타임엔 되는데 `luau-analyze`에선
|
||||
타입에러인, `H-25`가 실측한 그 문제가 **마일스톤마다 반복된다.**
|
||||
- [ ] `Handler.luau`(핸들러 계약 타입: `isHandlable(inst,k,v)`/`priority`/
|
||||
`process(inst,k,v,index) -> (hintValue)->()` **3종** — `isHandlable`도
|
||||
`inst`를 받도록 확정(2026-08-07 여덟 번째 세션), 별도 `retract` 필드는
|
||||
`process` 반환값으로 합쳐짐(2026-08-13 다섯 번째 세션))
|
||||
- [ ] **[2026-08-22 M7에서 이동] `None.luau`(센티널) + `Dispatch/None.luau`
|
||||
(`NoneHandler` + `NilHandler`)** — `None`은 modifier 전용 값이 아니라
|
||||
디스패치 배관이라 여기 소속(`architecture.md`의 소스 트리에 있는 건
|
||||
핸들러 쪽 `Dispatch/None.luau`뿐 — **[2026-08-22] 탑레벨 센티널
|
||||
`None.luau`와 `Brand.luau`는 그 트리에 아직 줄이 없다**, M3 구현 시
|
||||
트리도 같이 채울 것).
|
||||
`NoneHandler`는 배열/해시 구분 없이 `Dispatch.process(inst,k,nil,index+1)`
|
||||
**재귀만** 하고(선행 `retractFrom` 없음 — 하강 diff), 실제 정리는
|
||||
`NilHandler`(`isHandlable`이 `type(k) == "number" and v == nil`일 때만
|
||||
매치하는 말단)가 `Dispatch.setLength(inst,k,0)` +
|
||||
`Dispatch.setOffsetSource(inst,k,None)` 등록으로 맡는다.
|
||||
**`Dispatch.drive`에 `None` 스킵 분기는 없다**(반응형 값이 내놓는
|
||||
`None`은 어차피 `process`에 도착하므로 — 2026-08-18 재설계) —
|
||||
`base/dispatch-core-plan.md`의 "`None` 센티널"/"`NilHandler`" 절.
|
||||
Modifier 쪽 표면(인라인 키로 필드 지우기, `Peek` 반환 타입)은 M7
|
||||
- [ ] `Dispatch.setLength(ownerKey,i,len:number|State<number>,anchor?)`/
|
||||
`Dispatch.setOffsetSource(ownerKey,i,offset:Source<number>|None)`/
|
||||
**`Dispatch.getOffsetAt(ownerKey,i)`** —
|
||||
|
|
@ -418,9 +596,12 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
(`:On()`/`:IsOn()`/`:OffWithoutEmit()` 세 메서드 + 그 인스턴스를
|
||||
lazy 조회하는 Dispatch 쪽 헬퍼 `getBlocker(ownerKey)`)를 호출하는 이유는
|
||||
크래시 방지가 아니라 배치 등록 비용(O(N²)→O(N)) 절감.
|
||||
**[2026-08-22] 그래서 필요한 `GateNode`/`Blocker`/`EpochMap`은 이제
|
||||
이 마일스톤에 체크박스로 있다**(위 세 항목) — 예전엔 M3에 있는 걸
|
||||
각주로 가리키기만 했다. 결정 경위("게이팅 먼저", 그리고 앞당기는
|
||||
**[2026-08-24 정정] 그래서 필요한 `GateNode`/`Blocker`/`EpochMap`은
|
||||
M2(반응형 코어)에 체크박스로 있다** — 2026-08-22엔 각주로만 예고돼
|
||||
있던 걸 이 마일스톤으로 앞당겼었는데, 같은 달 24일 마일스톤 순서
|
||||
교체로 M2가 먼저 지어지게 되면서 셋 다 그리로 되돌아갔다(앞당길 이유가
|
||||
사라짐). 여기서는 **M2가 이미 만들어둔 것을 호출하기만 한다.**
|
||||
결정 경위("게이팅 먼저", 그리고 앞당기는
|
||||
대상이 `Blocker` 자체가 아니라 그 아래 공용 `Gate` 노드로 바뀐 것)는
|
||||
`qa-request/pre-implementation-qa-round5-followup.md`와
|
||||
`base/gate-plan.md`가 소스 — 여기서 반복하지 않는다.
|
||||
|
|
@ -471,137 +652,20 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
`Dispatch.drive`의 진입도 항상 `1`
|
||||
- **소유권 충돌 감지는 Dispatch의 일이 아님**(옛 점유 error 폐지) —
|
||||
필요한 도메인이 직접(Attribute 이름 claim, M10)
|
||||
- [ ] mock 대상 테스트
|
||||
|
||||
## M3 — Store/State/Source
|
||||
|
||||
- [ ] `Source.luau`/`State.luau`/`Store.luau`
|
||||
- [ ] **[2026-08-18 신설]** `store:GetDynamic<<T>>(name): Source<T>` — 런타임에
|
||||
이름이 정해지는 동적 키의 정식 창구(옛 `store "key"` 문자열 커링은
|
||||
기각). **⚠️ 콜론 메소드로 두면 `__index`가 고정 메소드 테이블을 먼저
|
||||
확인해야 하고 `GetDynamic`이 예약 키가 됨** — 탑레벨 함수로 둘지
|
||||
아직 미결(`base/store-plan.md`의 "타입 추론 문제" 절, `question.md` 3번)
|
||||
- [ ] **State 전파 루프 — 구독자는 weak, 발화마다 `canExecute` 게이팅**
|
||||
(2026-08-14 다섯 번째 세션 확정, `base/lifecycle-pattern.md`의 "실제
|
||||
호출부 — State 전파(`emit`)가 `canExecute`로 게이팅한다" 절) —
|
||||
State는 구독자(Observer의 emit 클로저)를 **weak로만** 담고, 살려두는
|
||||
책임은 `gchold`(leaf) 또는 전역 `Subscribed` 테이블(전역)에 있음
|
||||
(어디에도 안 묶인 Observer는 GC되어 목록에서 자연히 빠짐). 발화 시
|
||||
각 구독자에 대해 `canExecute(observer)`가 거짓이면 **그 구독자만
|
||||
조용히 건너뜀**(no-op) — 이게 `canExecute`의 유일한 실제 호출부이고,
|
||||
`inst`를 인자로 받을 수 없는 이유(State는 자기가 어느 Instance에
|
||||
걸렸는지 모름). `state:Observer(fn)`의 "등록 즉시 1회 실행"은
|
||||
`bindLifetime` 이전에 동기적으로 일어나므로 이 게이팅과 무관
|
||||
- **[확정된 것 — 코드 아님]** `store.key` dot-access 타입 추론 확인 — Luau `type function`
|
||||
(`WrapStore`/`ProcessStoreType`)으로 `Store<T>`가 `T`의 각 필드를
|
||||
`Source`로 감싼 레코드 타입을 합성 가능함을 확인(2026-08-12 열일곱
|
||||
번째 세션, `base/typing-limits.md` §5) — **[2026-08-15 실측 완료]**
|
||||
`luau-test/done/16-type-store-key-typefunction.luau` 통과(원인은
|
||||
설계 문제가 아니라 `types.newfunction` API 버전 드리프트였음)
|
||||
- [ ] `:Compute(fn, ...)` — trailing args로 추가 의존성 직접 받는 sugar
|
||||
(2026-08-11 세션, `base/source-state-plan.md` "`:Compute(fn, ...)`"
|
||||
절) — `:With(...):Compute(fn)` 체인과 달리 노드 1개(Compute 노드
|
||||
자신에 구독만 추가)로 끝나야 함, 새 노드 생성 없이 구현되는지 M0/M3
|
||||
스파이크에서 확인. **[2026-08-24 정정, 6라운드 `H-13`]** 여기 원래
|
||||
*"`Effect`/`Observer`는 대칭 sugar 없이 `:With` 명시 유지"*라고 적혀
|
||||
있었는데 **`Effect`는 `C-6`에서 이미 역전됐다** — 각 dep에 구독을 따로
|
||||
걸면 합치는 노드 자체가 안 생겨 감출 비용이 없다. **기각으로 남은 건
|
||||
`Observer` 하나**이고 근거도 새로 쓰였다("Observer는 리시버 State
|
||||
하나에 붙는 구독, 여럿을 엮는 건 Effect가 대신한다")
|
||||
- [ ] **⭐ [2026-08-24 신설, 6라운드] `Effect` 구현 시 같이 만들 것** —
|
||||
**`handle._observers`**(배열, 단수 `_observer`에서 바뀜 `H-8`) ·
|
||||
**`handle._cleanup`**(직전 cleanup 보관, `Rerun`과 `Destroying` 클로저가
|
||||
같은 자리를 읽는다) · **`handle._refDeps`/`_refCallbacks`**(`Ref` dep과
|
||||
거기 건 클로저 — 해제 시 값으로 떼야 해서 보관 필요) ·
|
||||
**`handle._destroyConn`** · **`:_bindDestroying(inst)`/`:_unbindDestroying()`**
|
||||
(`bindLifetime`/`unbindLifetime`이 `isEffect`일 때 부르는 훅, `H-11`).
|
||||
`fn` 시그니처는 **`fn(self: EffectHandle) -> (() -> ())?`**이고
|
||||
**`...deps`는 `fn`에 안 넘어간다**(`H-14`). `Ref` 콜백은 본문 맨 앞에서
|
||||
**`canExecute(handle)`를 확인**한다(`H-7`). 의사코드는
|
||||
`base/effect-plan.md`가 소스
|
||||
- [ ] **[2026-08-24 `H-39`]** `ObserverEffectLeafHandler.process`가 자기 배열
|
||||
자리의 `setOffsetSource(inst,k,None)`/`setLength(inst,k,0)`을 등록 —
|
||||
빠져 있었다(위 M2의 그 항목)
|
||||
- [ ] **[2026-08-24 `H-23`]** State 전파 루프는 구독자 집합을 **배열로
|
||||
스냅샷한 뒤** 돈다 — 순회 중 새 구독자 추가가 정상 경로인데 Lua에서
|
||||
미정의라, 실측에서 실행마다 결과가 달라지고 한 Observer가 통째로
|
||||
누락됐다. "이번 파동 중에 붙은 구독자는 다음 파동부터"가 계약
|
||||
- [ ] **[2026-08-24 `H-25` 파생]** `quad-types`의 `Quad`에 `Source`/`State`/
|
||||
`Store` 필드 추가(위 M2 항목의 "마일스톤마다" 규칙)
|
||||
- [ ] trailing deps를 `fn`에 lazy positional 인자로도 노출(**⚠️ [2026-08-24
|
||||
`H-14`] 이 항목은 `:Compute` 한정이다** — `Effect`의 `fn`엔 deps가 안
|
||||
넘어간다) — (`fn(self,
|
||||
previous?, dep1, ..., depN)` — 순서는 Luau 값 레벨 `...`가 파라미터
|
||||
리스트 맨 끝이어야 하는 것과 같은 이유로 `previous?`가 deps 팩
|
||||
**앞**에 와야 함, 2026-08-11 후속 세션 제안 → 같은 날 세 번째
|
||||
세션에 순서 정정, `base/source-state-plan.md` "trailing deps를 fn에
|
||||
lazy positional 인자로도 노출" 절) — 방향/순서는 확정,
|
||||
`luau-test`의 `15-type-compute-trailing-deps-typepack.luau`로
|
||||
이형 다중 deps를 제네릭 타입 팩으로 표현 가능한지만 실측 필요(안
|
||||
되면 동종 타입 dep 1개로 한정)
|
||||
> **[2026-08-22 이동] `EpochMap.luau` / `state:Gate` + `GateNode` /
|
||||
> `Blocker.luau`는 M2로 옮겼습니다** — 셋 다 M2가 실제로 호출하는데
|
||||
> 각주로만 예고돼 있어서, `LifetimeHandle` 인터페이스를 M8→M2로 옮겼던
|
||||
> 전례대로 체크박스 자체를 옮겼습니다. M3는 그 위에 State/Source 본체를
|
||||
> 얹기만 하면 됩니다.
|
||||
- [ ] **[2026-08-21 5라운드 — 채택 확정, 같은 날 `Epoch`로 일반화]** State의
|
||||
재계산/전파 판정은 **`Epoch` 리비전 비교**다(`base/state-epoch-plan.md`)
|
||||
— `invalid` 플래그가 아니다. 아래 `Source.luau`/`State.luau`가 이걸
|
||||
전제로 짜여야 한다: `Source`가 `type Epoch = { Revision: number }`를
|
||||
구조적으로 만족하고(`EpochBrand`에도 등록), 부기는 재사용 가능한
|
||||
**`EpochMap`**(`:Update(Epoch|EpochSet) -> boolean`, `:Refresh`, `:Sync`,
|
||||
`:TrackFrom` — `EpochSet = {[Epoch]: true}`, **배열 아님**)
|
||||
으로 떼어내며, State가 그걸 **둘** 컴포지션한다 — `valueEpochMap`(값
|
||||
유효성)/`emitEpochMap`(전파 dedup). emit은 값도 리비전도 안 싣고
|
||||
**출처(`Epoch`나 그 집합)만** 싣고, 순회는 `rawInvalid == false`일 때만
|
||||
돌며 **값만 앞당기고 통지는 상류 emit을 기다린다**. 다이아몬드 중복
|
||||
통지가 접히므로 스파이크 `05`도 그에 맞춰 재작성해야 한다
|
||||
(`luau-test/STATUS.md`).
|
||||
- [ ] `state:Apply(factory)`(`base/source-state-plan.md` "`state:Apply(factory)`"
|
||||
절, 2026-08-07 일곱 번째 세션) — `factory(self)`를 체이닝 문법으로
|
||||
부르는 순수 설탕, `factory: (State<T>) -> U): U`로 열린 타입. Source도
|
||||
기존 `:With`/`:Compute` 델리게이션에 얹혀 자동 포함
|
||||
- [ ] `state:Observer(fn)` — children 배열 leaf 참가자, **등록 즉시 1회
|
||||
실행 확정**(`base/source-state-plan.md`의 Observer 절), `isObserver`
|
||||
판별자, canExecute 게이팅, `:Subscribe()`/`:Unsubscribe()`. **동적
|
||||
경로 가드**(`{priority = HANDLER_PRIORITY_FALLBACK, isHandlable = v
|
||||
is Observer, process = error(...)}`, `k` 타입 안 가림, 2026-08-14
|
||||
열한 번째 세션 — `PreRef`와 같은 패턴)도 같이 등록
|
||||
- [ ] `Effect(fn, ...deps)`(`base/effect-plan.md`, **[2026-08-21 5라운드
|
||||
`C-6`]** 옛 시그니처는 `Effect(fn, state?)`) — deps 생략 시 설치
|
||||
1회+leaf 사망 시 확정 정리, deps 지정 시 **각각에 맞는 구독**
|
||||
(State/Source는 `Observer`, `Ref`는 `:Callback`)을 걸어
|
||||
재실행+cleanup 체이닝(React `useEffect` 동형). **`EffectHandle`이
|
||||
`EpochMap`을 하나 들어** 공통 상류로 인한 중복 발화를 접고, 설치 구간
|
||||
억제 플래그가 그 `Update`보다 먼저 와야 함
|
||||
(`base/state-epoch-plan.md`). Observer 구현 이후에 착수(의존 관계).
|
||||
`EffectHandle:Subscribe()`/`:Unsubscribe()`도 추가(leaf 없이 쓰는
|
||||
모듈/스크립트 레벨 Effect) — `:Unsubscribe()`는 Observer와 달리
|
||||
마지막 cleanup을 1회 트리거해야 함(2026-08-07 일곱 번째 세션).
|
||||
**동적 경로 가드**도 Observer와 같은 패턴으로 등록(`base/effect-plan.md`
|
||||
"동적 경로 가드" 절, 2026-08-14 열한 번째 세션)
|
||||
- [ ] Observer/Effect 이중 바인딩 금지 — `canBound(value)` 게이트로
|
||||
`:Subscribe()`(전역)와 `bindLifetime`(inst-scoped, leaf 부착도
|
||||
내부적으로 이걸 호출)이 동시에 걸리면 즉시 `error`(`base/source-state-plan.md` "이중 바인딩 금지" 절, 2026-08-07 일곱 번째
|
||||
세션 신설, 2026-08-09 여섯 번째 세션에서 "leaf 부착=bindLifetime
|
||||
호출"로 정정 — 진짜 독립 경로는 둘뿐).
|
||||
**[2026-08-14 다섯 번째 세션에 별도 predicate `canBound(handle)`을
|
||||
폐기하고 `canExecute` 하나로 합쳤다가, 같은 날 열한 번째 세션에
|
||||
다시 갈라짐]** — "지금 묶어도 되는가"(bound 문맥)와 "지금
|
||||
발화해도 되는가"(execute 문맥)는 호출부의 질문이 다르고
|
||||
**[2026-08-18 구현 전 QA 정정] 판정값도 같은 게 아니라 서로의
|
||||
부정**이라(`canBound(v) == not canExecute(v)`, 게이트는 항상
|
||||
`if not canBound(v) then error(...)`), `Ref` 이중 배치
|
||||
방지(`question.md` 0-W)를 계기로 `canBound`가
|
||||
별도 진입점으로 재도입됨 — 판정 로직(비공개 `isBoundAlive` 헬퍼)은
|
||||
공유해 코드 중복은 없음. **이 절이 쓰는 게이트는 이제 `canBound`**
|
||||
(emit 전파 게이팅 전용 `canExecute`가 아님). `.Subscribed` 필드가
|
||||
leaf 경로와 무관하다는 것, leaf 생존 판정을 `bindLifetime`이 `value`
|
||||
쪽 `Relate`에 복사해둔 gcconn으로 하는 것은 안 바뀜 — `base/
|
||||
lifecycle-pattern.md`의 "`canBound` vs `canExecute`" 절, 역전 경위는
|
||||
`archive/canexecute-inst-arg-reversed.md`. 부수 효과로 **바인딩이
|
||||
죽은 뒤(`Destroy`/`unbindLifetime`)의 재사용은 게이트를 통과**
|
||||
(살아있는 바인딩만 막는 게 의도, 안 바뀜)
|
||||
- [ ] **[2026-08-24 M2에서 이동] Observer/Effect 동적 경로 가드 등록** —
|
||||
`{priority = HANDLER_PRIORITY_FALLBACK, isHandlable = v가
|
||||
Observer/Effect, process = error(...)}` 둘을 `Dispatch.addHandler`로
|
||||
등록(`k` 타입 안 가림, `PreRef`와 같은 패턴 —
|
||||
`base/source-state-plan.md`의 Observer 절, `base/effect-plan.md`의
|
||||
"동적 경로 가드" 절). **M2에서 Observer/Effect 본체를 짤 때는 이 등록만
|
||||
빼고 간다** — 레지스트리(`Dispatch.addHandler` + `Handler.luau` 계약)가
|
||||
이 마일스톤에서 생기기 때문. **M2가 M3에 개념상 지던 의존은 이 항목과
|
||||
바로 아래 항목뿐이었고, 둘 다 "핸들러를 등록한다"뿐이라 이쪽으로 미룰
|
||||
수 있었다** — 그래서 빌드 순서엔 역방향 간선이 남지 않는다.
|
||||
순서 교체(2026-08-24)의 근거
|
||||
- [ ] **[2026-08-24 M2에서 이동, `H-39`]** `ObserverEffectLeafHandler.process`가
|
||||
자기 배열 자리의 `setOffsetSource(inst,k,None)`/`setLength(inst,k,0)`을
|
||||
등록 — 빠져 있었다(위 말단 핸들러 항목)
|
||||
- [ ] mock 대상 테스트
|
||||
|
||||
## M4 — 첫 end-to-end 반응형 업데이트
|
||||
|
|
@ -619,7 +683,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
정확히 불린다" 확인 + **`State<State<T>>`(값이 또 State/Source)가
|
||||
인덱스 N/N+1로 안 겹치고 정상 동작하는지**(2026-08-13 다섯 번째
|
||||
세션에 UB→정상 지원으로 재정정) + **최초 마운트 직후 첫 재발행에서
|
||||
인덱스 2의 retractor가 실제로 불리는지**(위 M2의 `SetStrong` 순서
|
||||
인덱스 2의 retractor가 실제로 불리는지**(위 M3의 `SetStrong` 순서
|
||||
버그가 정확히 여기서 증상으로 나타남)
|
||||
|
||||
## M5 — quad-roblox 최소 프로바이더
|
||||
|
|
@ -696,7 +760,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
이때 확정(CRUD/`:List` 여부 무관 항상 노출, 순서 계산과 "n개 검색됨"
|
||||
UI 둘 다 겸함) — 구현 시 이 두 API를 `:List`/CRUD의 `raw*`가 호출.
|
||||
**`recompute` 트리거 모델의 크래시(`RC-1`)는 Blocker 게이팅으로
|
||||
해결됨**, 위 M2 항목 참고.
|
||||
해결됨**, 위 M3의 `Dispatch.setLength`/`setOffsetSource` 항목 참고.
|
||||
- **Slot의 `Add`/`Remove`/`Extract`/`ExtractAll`/`Clear`/`Move`/`Swap`/
|
||||
`Get`/`IndexOf`/`Splice` CRUD 의미론 확정** (2026-08-09 세 번째 세션,
|
||||
2026-08-09 열한 번째 세션에 식별 기준 재정정, `Splice`는 2026-08-12
|
||||
|
|
@ -766,7 +830,8 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
`disposeInst`가 남아 있었음), 중첩 offset이 부모 베이스를 못 받던 결함과
|
||||
재마운트가 `Offset` Source를 새로 만들던 결함도 같이 수정됐다.
|
||||
**`recompute` 트리거 모델의 크래시(`RC-1`)는 Blocker 게이팅으로
|
||||
해결됨 — 위 M2 항목 참고(`base/slot-plan.md` "재귀 메커니즘" 절).**
|
||||
해결됨 — 위 M3의 `Dispatch.setLength`/`setOffsetSource` 항목
|
||||
참고(`base/slot-plan.md` "재귀 메커니즘" 절).**
|
||||
**[재설계, 2026-08-21] `attachSlot`은 비공개 재귀 둘로 분해됨** —
|
||||
`materializeSlotTree`(부기만, Blocker가 감싸는 건 이제 여기뿐) +
|
||||
`mountSlotTree`(물리 `Parent` 대입만, Blocker 불필요), 그리고 그 둘을
|
||||
|
|
@ -855,7 +920,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
`:List`의 **`prevKeys`**(옛 `keyIndex`의 강등판, 단순 키 집합).
|
||||
전부 `base/slot-plan.md`가 소스
|
||||
- [ ] **[2026-08-24 `H-25` 파생]** `quad-types`의 `Quad`에 `Slot` 필드 추가
|
||||
(위 M2 항목의 "마일스톤마다" 규칙)
|
||||
(위 M3 항목의 "마일스톤마다" 규칙)
|
||||
- [ ] **[2026-08-13 여섯 번째 세션 — 이 세션의 Slot 결정 전부, 구현 전 필독]**
|
||||
- **`State<Slot>` 교체 = 파괴가 아니라 언마운트**(`state<Frame>`와 동일).
|
||||
비파괴 경로 `unmountSlotTree`를 `destroySlotTree`와 별도로 구현 —
|
||||
|
|
@ -934,7 +999,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
**`bindLifetime`/`unbindLifetime`이 `isEffect(value)`를 보고 `Destroying`을
|
||||
걸고/끊는 것**으로 닫혔고(`base/effect-plan.md` +
|
||||
`base/lifecycle-pattern.md`의 그 의사코드), 이 문장은 그 배선이 구현된
|
||||
뒤에야 참이 된다 — M3 `Effect` 구현이 M6의 이 항목을 **선행**한다는 뜻이다.
|
||||
뒤에야 참이 된다 — M2 `Effect` 구현이 M6의 이 항목을 **선행**한다는 뜻이다.
|
||||
(**[같은 날 재결정]** 처음엔 *"`EffectHandle`이 자기 `bindLifetime` 직후에
|
||||
건다"*였으나 `/code-review high`가 **그 호출부가 실재하지 않는다**는 걸
|
||||
지적했다 — 핸들은 남이 자기를 bind하는 걸 관측할 수 없고, `Effect`가
|
||||
|
|
@ -1022,9 +1087,9 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
`brand-plan.md`의 "`isX` wrapper" 절, M2의 `Brand.luau`에 이미
|
||||
구현돼 있어야 함)
|
||||
> **[2026-08-22 이동] `None` 센티널 + `Dispatch/None.luau`(`NoneHandler`/
|
||||
> `NilHandler`)는 M2로 옮겼습니다** — `None`은 modifier 전용 값이 아니라
|
||||
> `NilHandler`)는 M3로 옮겼습니다** — `None`은 modifier 전용 값이 아니라
|
||||
> quad-base 디스패치 배관이고(`architecture.md`의 소스 트리에서도
|
||||
> `Dispatch/None.luau`), M0의 `props.Modifier or None` 관용구 · M2의
|
||||
> `Dispatch/None.luau`), M0의 `props.Modifier or None` 관용구 · M3의
|
||||
> `Dispatch.drive` · M6의 `setOffsetSource(..., None)`이 전부 이미 전제합니다.
|
||||
> M7에서는 **Modifier 쪽 표면만** 다룹니다 — 인라인 키/setter로 필드를
|
||||
> 지우는 용법과 `Peek` 반환 타입에 `None`을 추가하는 것(`modifier-plan.md`
|
||||
|
|
@ -1043,12 +1108,12 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
빠져 있어 구현자가 존재 자체를 놓칠 수 있던 자리다. 의사코드는
|
||||
`base/modifier-plan.md`의 flatten 절이 소스
|
||||
- [ ] **[2026-08-24 `H-25` 파생]** `quad-types`의 `Quad`에 `Modifier` 관련
|
||||
표면이 노출돼야 하면 같이 갱신(위 M2 항목의 "마일스톤마다" 규칙)
|
||||
표면이 노출돼야 하면 같이 갱신(위 M3 항목의 "마일스톤마다" 규칙)
|
||||
|
||||
## M8 — Ref
|
||||
|
||||
- [ ] **[2026-08-24 `H-25` 파생]** `quad-types`의 `Quad`에 `Ref` 필드 추가
|
||||
(위 M2 항목의 "마일스톤마다" 규칙)
|
||||
(위 M3 항목의 "마일스톤마다" 규칙)
|
||||
- [ ] `Ref.luau`(`.Value` 읽기 전용 필드 + `:Set(value)`/`:Callback(fn)`/
|
||||
`:Wait(thread?)`, 전부 self 반환) + `PreRef.luau`/`PostRef.luau`(별도 파일, Ref
|
||||
런타임 재사용 + children 배열 전용, Modifier/Store 타입 차단,
|
||||
|
|
@ -1186,7 +1251,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
|
||||
- [ ] **[2026-08-24 `H-39`]** `TagHandler`/`AttributeGroupHandler`가 자기 배열
|
||||
자리의 `setOffsetSource(inst,k,None)`/`setLength(inst,k,0)`을 등록 —
|
||||
**둘 다 0건이었다**(위 M2의 그 항목). 같이 **`type(k) == "number"` 가드**도
|
||||
**둘 다 0건이었다**(위 M3의 그 항목). 같이 **`type(k) == "number"` 가드**도
|
||||
추가(`H-52` — `RefLeafHandler`가 2026-08-18에 받은 수정을 이 둘은 못 받았다)
|
||||
- [ ] **[2026-08-24 `H-41`]** `AttributeGroupHandler.process`에 `groupClaimKeys`
|
||||
위치 claim 배선 — 5라운드 `AT-1`에서 `(inst, groupValue) → k`로 확정해놓고
|
||||
|
|
@ -1196,7 +1261,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
없으면 `None`으로 콜백을 끄는 게 실제로는 **나중에 터질 Connection을 새로
|
||||
심는** 동작이 된다
|
||||
- [ ] **[2026-08-24 `H-25` 파생]** `quad-types`의 `Quad`에 `Tag`/`Attribute`
|
||||
필드 추가(위 M2 항목의 "마일스톤마다" 규칙)
|
||||
필드 추가(위 M3 항목의 "마일스톤마다" 규칙)
|
||||
- [ ] `Handlers/Event.luau`(`ReflectionService` 기반 자동 판별)
|
||||
- [ ] `Handlers/OnChange.luau`(`OnChange(name)` 특수 키 팩토리+Handler,
|
||||
`GetPropertyChangedSignal` 바인딩 — 제네릭 없이 콜백 타입은 인라인
|
||||
|
|
@ -1352,13 +1417,14 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
시간 기반 전파 게이트 `Debounce`/`Throttle`(`base/debounce-throttle-plan.md`)
|
||||
— 제어 핸들 설계까지 닫히면서 quad-base에 새 코어 메커니즘을
|
||||
추가하지 않는 **순수 슈가**로 확인됨(`Blocker`의 gated state + `Ref` +
|
||||
아래 주입 op 2개 위에 전부 얹힘, 그 문서 13절). **[정정, 2026-08-21 5라운드]
|
||||
그 공용 `Gate` 추출은 M3가 아니라 M2로 앞당겨졌다** — "게이팅 먼저"
|
||||
결정(위 M2 각주)으로 `Dispatch.drive`의 배치 등록이 쓰는 게이팅부터
|
||||
만들기로 했고, 그때 `Blocker`/`Debounce`/`Throttle`이 공유할 노드를
|
||||
같이 빼둔다(따로 하면 같은 설계를 두 번 함). **[2026-08-21] 표면 확정 —
|
||||
아래 주입 op 2개 위에 전부 얹힘, 그 문서 13절). **[정정, 2026-08-24]
|
||||
그 공용 `Gate` 추출은 M2(반응형 코어)에서 이뤄진다** — 2026-08-21에
|
||||
"게이팅 먼저" 결정으로 디스패치 쪽에 앞당겨뒀다가, 2026-08-24 마일스톤
|
||||
순서 교체(위 M2 배너)로 반응형이 먼저가 되면서 앞당길 필요 자체가
|
||||
사라졌다. `Blocker`/`Debounce`/`Throttle`이 공유할 노드를 거기서 같이
|
||||
빼둔다(따로 하면 같은 설계를 두 번 함). **[2026-08-21] 표면 확정 —
|
||||
`state:Gate(setup)` 메소드**, `base/gate-plan.md`.
|
||||
프리미티브 자체는 그 위에 나중에 얹으면 되고 M0/M3를 막지 않음.
|
||||
프리미티브 자체는 그 위에 나중에 얹으면 되고 M0/M2를 막지 않음.
|
||||
주입 op 2개(`setTimeout(func, delay) -> Timeout` / `clearTimeout`,
|
||||
Roblox는 `task.delay`/`task.cancel`로 배선 — **인자 순서가 반대라
|
||||
주의**)가 `bindLifetime`/`canExecute`와 같은 base 범용 유틸 그룹에
|
||||
|
|
|
|||
Loading…
Reference in a new issue