quad/.claude/session/2026-08-20-01-qa-round4-response-processing.md
qwreey 9642cf8f18
qa: QA 4라운드 followup — 회신 처리 결과 + 재질문 정리
사용자 회신을 (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>
2026-08-20 00:57:19 +09:00

104 lines
7.7 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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절에 적어두고 **사용자 지시 대기**.