사용자 회신을 (a) 바로 반영 (b) 설명 부족으로 재질문 (c) 사용자 판단 필요 (d) 조사로 닫힘으로 갈라 followup 문서로 정리했다. A절(반영 완료)은 앞 커밋에서 이미 base/에 들어갔고, 이 커밋은 그 기록과 남은 항목이다. 가장 큰 두 미결: - C-1 KeyGone 센티널 — 키 소멸 시 updateFn을 한 번 더 불러 처분을 묻는 사용자 제안. SL-45의 "파괴도 반환도 안 되고 참조만 끊기는" 상태를 정확히 메우지만, 반환값 의미/userdata 수명/소멸 루프 순회 대상/index 인자 넷이 안 정해지면 구현이 못 나감. - C-2 "밀려난 prev는 dispose" vs state<Frame> 의미론 충돌 — 회신의 SL-43과 SL-51이 서로 반대를 가리켰는데, 따져보니 둘 다 맞고 갈리는 축이 "누가 그 요소를 만들었는가"였다. :List의 updateFn이 만든 건 reconcile이 지워야 하고 (만든 쪽이 자기 손으로 못 지움), Slot:Add(state) sugar로 온 건 사용자 소유라 죽이면 안 됨. 지금 설계엔 후자를 표현할 방법이 없어 선택지 셋을 올리고 per-installation 소유권 옵션을 추천. 부수로 사용자가 문서에 아예 없던 갭 둘을 찾아냄 — Attribute 생성자 자신의 이름 겹침 정책(앞 커밋에서 "뒤가 이김"으로 명시)과, flatten이 소진한 Modifier 자리 처리(C-3, ProcessedModifier 센티널 vs flatten 압축). Co-authored-by: qwreey <me@qwreey.moe>
104 lines
7.7 KiB
Markdown
104 lines
7.7 KiB
Markdown
# 2026-08-20-01 — QA 4라운드 회신 1차 처리 + 업스트림 스캐폴딩 병합
|
||
|
||
**한 줄**: 사용자가 남긴 `pre-implementation-qa-round4-response.md`를 받아
|
||
판단이 명확한 것 20여 건을 `base/`에 반영하고, 나머지를
|
||
`pre-implementation-qa-round4-followup.md`로 정리했다. 같은 세션에 업스트림
|
||
12커밋(M0 검증 + M1 스캐폴딩)을 pull해 문항지 커밋을 그 위로 rebase했다.
|
||
|
||
## 순서
|
||
|
||
1. **업스트림 병합 먼저** — `git fetch` 결과 `origin/main`이 12커밋 앞서
|
||
있었음(pesde 전환, mise/selene, RunInit 재설계, quad-types/type-version-check
|
||
신설, M0 스파이크 21~23 추가, ROADMAP/CLAUDE.md stale 정정). 전 세션
|
||
산출물(QA 4라운드 문항지)을 먼저 커밋한 뒤 `git rebase origin/main` —
|
||
**충돌 없이 통과**(문항지가 신규 파일이고, 인덱스 2줄만 수정했기 때문).
|
||
2. **회신 읽고 분류** — 회신은 문항 번호별로 자유서술이라, (a) 바로 반영,
|
||
(b) 설명 보강 후 재질문, (c) 사용자 판단 필요, (d) 조사해서 답이 나옴으로
|
||
갈랐다.
|
||
3. **(a)만 문서에 반영** — (b)/(c)는 손대지 않고 followup에만 적었다.
|
||
|
||
## 반영한 것 중 설계가 실제로 바뀐 것
|
||
|
||
목록 전체는 followup A절이 소스(여기서 반복 안 함). 그 중 "표현 정정"이 아니라
|
||
**동작이 달라지는** 것만:
|
||
|
||
- **`SL-75`/`D-60` — 언마운트 시 `slot.Offset = nil` 금지.** 옛 서술은
|
||
"마운트 전 상태로 복원"이었는데, 사용자가 **포탈이 깨진다**고 지적. 이유가
|
||
깔끔했다 — `updateFn`이 `layoutOrder:With(offset):Compute(...)`처럼 그
|
||
Source를 파생 그래프에 엮어둔 상태에서 필드를 갈아치우면, 그 구독은 옛
|
||
Source를 계속 보는데 Slot은 새 Source를 등록하게 되어 **영영 갈라진다.**
|
||
재마운트가 "값이 다시 채워지는 일"이 아니라 "연결이 끊긴 채 조용히 멈추는
|
||
일"이 되는 것. 부수로 `Slot.Offset`의 마운트 전 기본값도 `nil` → `0`.
|
||
- **`E-11` — leaf 바인딩된 Effect엔 `:Unsubscribe()`가 아예 안 먹음.**
|
||
옛 서술의 "(3) 이후 leaf가 실제로 죽어도 중복 호출되지 않는다"가 사용자
|
||
눈에 이상해 보였는데, 따져보니 **성립할 수 없는 문장**이었다 — 이중 바인딩
|
||
게이트가 `:Subscribe()`(전역)와 `bindLifetime`(leaf)을 상호 배타로 막아두므로
|
||
`:Subscribe()`된 핸들은 애초에 leaf에 붙어 있을 수가 없고, "그 leaf가 죽는"
|
||
사건 자체가 없다. Observer와 같은 규칙으로 통일하는 것으로 정리.
|
||
- **`AT-20` — 생존 이름 최적화는 "안 하는" 게 아니라 "못 하는" 것.**
|
||
옛 서술은 "이론상 가능하지만 부품이 늘어나서 기본은 균일 철거"였는데,
|
||
사용자 지적대로 **이름이 같아도 값이 바뀌었을 수 있어** 이름 목록 비교만으로는
|
||
아무것도 못 건너뛴다. 값을 비교하려면 `:Get()`이 필요하고 그건 State 계약
|
||
위반 — 즉 설계 원칙상 애초에 막힌 길이다. `Tag`가 되는 이유(자원이 이름
|
||
집합이라 겹쳐도 합집합)와의 대조까지 같이 적었다.
|
||
- **`D-56` — `bindLifetime`의 첫 인자가 Instance가 아닐 수 있다.**
|
||
`Dispatch.setLength(ownerKey, ...)`의 `ownerKey`가 Slot-in-Slot에선 Slot
|
||
자신인데, 구현 스케치는 `InstData`에서 gcconn을 찾는 걸 전제하고 있었다.
|
||
gcconn 트릭은 엔진 객체에만 걸 수 있으므로 **백엔드가 별도 경로를 제공해야
|
||
한다**는 요구사항으로 신설. `isBoundAlive`에 세 번째 분기가 필요하다는 것
|
||
(지금은 gcconn 없으면 `.Subscribed` 폴백으로 떨어져 **살아있는데 `canBound`가
|
||
참으로 잘못 나옴**)까지 같이 짚었다.
|
||
|
||
## 사용자가 새로 찾아낸 갭 — 문서에 아예 없던 것 둘
|
||
|
||
1. **`Attribute(a, b, ...)` 생성자의 이름 겹침 정책이 어디에도 없었다.**
|
||
`Merged`(error)/`Overridden`(뒤가 이김)만 적혀 있었고, 생성자가 인자들을
|
||
하나의 이름 맵으로 평탄화하는데 겹치면 어떻게 되는지가 공백. 배열 순서
|
||
규칙과 결을 맞춰 **뒤가 이김**으로 명시했다.
|
||
2. **flatten이 소진한 Modifier 자리를 어떻게 하는지가 없었다**(`M-2`).
|
||
"`isModifier(v)`인 항목만 필드를 뽑아 merge"만 적혀 있고 그 자리를 지우는지
|
||
센티널로 채우는지가 공백 — 그냥 지우면 `PreRef` pre-pass가 `ProcessedPreRef`로
|
||
소진하는 바로 그 이유(sparse 테이블)에 걸린다. 사용자는 `ProcessedModifier`
|
||
센티널을 제안했고, 나는 **flatten이 어차피 새 테이블을 만드는 순수 변환이라
|
||
압축(compact)이 더 싸다**고 봐서 두 안을 followup C-3에 나란히 올렸다.
|
||
|
||
## followup으로 넘긴 것 중 가장 큰 둘
|
||
|
||
- **`C-1` `KeyGone` 센티널** — 사용자 제안(키 소멸 시 `updateFn`을 한 번 더
|
||
불러 처분을 묻기)이 `SL-45`의 어정쩡한 상태를 정확히 메운다. 다만 반환값
|
||
의미/`userdata` 수명/소멸 루프 순회 대상/`index` 인자 넷이 안 정해지면 구현이
|
||
못 나가서 그대로 물었다.
|
||
- **`C-2` "밀려난 `prev`는 dispose" vs `state<Frame>` 의미론** — 회신의 두
|
||
답변(`SL-43`, `SL-51`)이 서로 반대를 가리켰다. 따져보니 **둘 다 맞고 갈리는
|
||
축이 "누가 그 요소를 만들었는가"**였다 — `:List`의 `updateFn`이 만든 것은
|
||
reconcile이 지워줘야 하고(만든 쪽이 자기 손으로 못 지움), `Slot:Add(state)`
|
||
sugar로 들어온 것은 사용자 소유라 죽이면 안 된다. 지금 설계엔 후자를 표현할
|
||
방법이 없어서(`Detach`는 교체와 같이 못 씀) 선택지 셋을 제시했고,
|
||
**per-installation 소유권 옵션**을 추천했다 — 갈리는 기준이 per-cycle이
|
||
아니라 설치 단위 속성이기 때문.
|
||
|
||
## 조사만으로 닫은 것
|
||
|
||
- **`E-10`(dedup 대칭)** — 의사코드를 다시 따라가니 `bindLifetime`(내부
|
||
Observer cascade 포함)이 `old ~= v` 분기 안에, `unbindLifetime`이
|
||
`nextValue ~= v` 분기 안에 있어 이미 대칭이었다. 사용자가 말한
|
||
"relate로 old를 들고 있다가 process에서 조회해 같으면 dedup"이 정확히 지금
|
||
코드다. 설계 갭 없음, 구현 시 회귀 확인만.
|
||
- **`D-10`** — 사용자 지적("Luau 일반화 for가 그냥 지원함")이 맞고, 문항이
|
||
"M0에서 순회 동작을 검증"이라 적어 언어 동작을 의심하는 것처럼 읽힌 게
|
||
문제였다. 스파이크 `01`이 검증한 건 언어가 아니라 **우리가 짠 두 패스
|
||
드라이버**이고, 두 패스를 명시하는 진짜 이유는 이식성이다. 업스트림에서
|
||
이미 통과했으므로 base의 "M0에서 검증할 것"도 닫았다.
|
||
- **`SL-76`** — 사용자 의문("해제에서 nil이 관측될 일이 없다")대로 지금
|
||
설계에서 도달 경로를 못 찾았다. `bk.N` 정의 확정 + Blocker 게이팅 +
|
||
`spliceArraysDown`의 압축이 각각 그 경로를 막는다. 그래서 "관대한 skip"을
|
||
유지/삭제/error 승격 중 고르는 문제로 바꿔 올렸고 **error 승격을 추천**했다.
|
||
|
||
## 안 한 것
|
||
|
||
- **감사 루프 안 돌림** — 이번 변경이 크긴 하지만 아직 **회신 처리가 절반**
|
||
(B/C절이 남음)이라, 감사는 그 라운드까지 끝난 뒤 한 번에 도는 게 맞다고 봄.
|
||
`doc-check.py`는 매 편집 후 돌려 ERROR 0 유지.
|
||
- **5라운드 문항지** — 업스트림이 들여온 신규 문서 둘(`project-setup-plan.md`,
|
||
`quad-types-plan.md`)과 이번에 내용이 늘어난 `architecture.md`/`typing-limits.md`는
|
||
문항이 아예 없다. followup D절에 적어두고 **사용자 지시 대기**.
|