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

7.7 KiB
Raw Blame History

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 금지. 옛 서술은 "마운트 전 상태로 복원"이었는데, 사용자가 포탈이 깨진다고 지적. 이유가 깔끔했다 — updateFnlayoutOrder:With(offset):Compute(...)처럼 그 Source를 파생 그래프에 엮어둔 상태에서 필드를 갈아치우면, 그 구독은 옛 Source를 계속 보는데 Slot은 새 Source를 등록하게 되어 영영 갈라진다. 재마운트가 "값이 다시 채워지는 일"이 아니라 "연결이 끊긴 채 조용히 멈추는 일"이 되는 것. 부수로 Slot.Offset의 마운트 전 기본값도 nil0.
  • E-11 — leaf 바인딩된 Effect엔 :Unsubscribe()가 아예 안 먹음. 옛 서술의 "(3) 이후 leaf가 실제로 죽어도 중복 호출되지 않는다"가 사용자 눈에 이상해 보였는데, 따져보니 성립할 수 없는 문장이었다 — 이중 바인딩 게이트가 :Subscribe()(전역)와 bindLifetime(leaf)을 상호 배타로 막아두므로 :Subscribe()된 핸들은 애초에 leaf에 붙어 있을 수가 없고, "그 leaf가 죽는" 사건 자체가 없다. Observer와 같은 규칙으로 통일하는 것으로 정리.
  • AT-20 — 생존 이름 최적화는 "안 하는" 게 아니라 "못 하는" 것. 옛 서술은 "이론상 가능하지만 부품이 늘어나서 기본은 균일 철거"였는데, 사용자 지적대로 이름이 같아도 값이 바뀌었을 수 있어 이름 목록 비교만으로는 아무것도 못 건너뛴다. 값을 비교하려면 :Get()이 필요하고 그건 State 계약 위반 — 즉 설계 원칙상 애초에 막힌 길이다. Tag가 되는 이유(자원이 이름 집합이라 겹쳐도 합집합)와의 대조까지 같이 적었다.
  • D-56bindLifetime의 첫 인자가 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)이 서로 반대를 가리켰다. 따져보니 **둘 다 맞고 갈리는 축이 "누가 그 요소를 만들었는가"**였다 — :ListupdateFn이 만든 것은 reconcile이 지워줘야 하고(만든 쪽이 자기 손으로 못 지움), Slot:Add(state) sugar로 들어온 것은 사용자 소유라 죽이면 안 된다. 지금 설계엔 후자를 표현할 방법이 없어서(Detach는 교체와 같이 못 씀) 선택지 셋을 제시했고, per-installation 소유권 옵션을 추천했다 — 갈리는 기준이 per-cycle이 아니라 설치 단위 속성이기 때문.

조사만으로 닫은 것

  • E-10(dedup 대칭) — 의사코드를 다시 따라가니 bindLifetime(내부 Observer cascade 포함)이 old ~= v 분기 안에, unbindLifetimenextValue ~= 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절에 적어두고 사용자 지시 대기.