quad/.claude/archive/bookkeeping-before-physical-reversed.md
qwreey a4b517aee2
design: native* 물리 조작 계층 확정 + C-7("부기가 물리보다 먼저") 역전
주입 op 셋(mountInst/unmountInst/disposeInst)으로는 Move/Swap을 아예 표현할
수 없다는 지적에서 시작해 물리 조작 계층 전체를 재설계했다.

층위 정의(사용자): raw*는 그 Slot 스코프 안의 연산(평탄화 전, _elements
인덱스), native*는 확정된 offset/length로 표현되는 물리 트리 연산(평탄화 후,
절대 좌표).

표면 여섯:
  nativeInsert (target, offset, elements)
  nativeExtract(target, offset, elements, newElements?)   -- 빼되 살림
  nativeRemove (target, offset, elements, newElements?)   -- 빼면서 파괴
  nativeMove   (target, fromOffset, elements, toOffset)
  nativeSwap   (target, offsetA, elementsA, offsetB, elementsB)
  nativeDispose(element)                                   -- 트리 밖 값 파괴

- Replace는 별도 op이 아니라 newElements가 있는 Remove/Extract(Splice도 동일)
  — 제거+삽입을 한 호출로 합쳐 리플로우 2회와 그 사이 인덱스가 어긋난 창을
  없앤다
- 파괴/비파괴를 불리언이 아니라 이름으로 가름 — 공개 CRUD의 Remove/Extract
  어휘를 물려받고, Roblox의 "Parent=nil 없이 그 자리에서 Destroy" 융합을 연다
- 빠지는 요소는 반드시 배열로 넘김 — (target, offset, count)로 대상을 찾을 수
  있는 건 DOM뿐이고 Roblox는 자식이 순서 없는 집합이라 offset 역조회가 안 됨
- nativeSwap은 별도 — Move는 사이를 전부 밀지만 Swap은 가운데 고정
- 미주입이면 에러가 아니라 조합 폴백(addTag 계열과 갈리는 지점)
- 전제: 한 Slot의 물리 자식은 부모 안에서 연속 구간을 차지한다

그 여파로 4라운드 C-7 일반 계약이 역전됨 — "Length를 먼저 올려 밀어내고 그
공간에 넣는다"는 그림은 base에 물리적으로 자리를 비워둘 수단이 없어 성립하지
않는다. 규칙이 "빼기는 물리 먼저/넣기는 부기 먼저" 두 얼굴에서
"자기 자리를 정하는 것(setOffsetSource) 먼저 / 뒤를 미는 것(setLength→
recompute) 나중" 하나로 줄었다. 배치 경로(materializeSlotTree→mountSlotTree)의
부기-전량-먼저는 C6가 요구하는 별개 사안이라 그대로.
원문은 archive/bookkeeping-before-physical-reversed.md.

같이: getOffsetAt을 사용자 의사코드대로 단일 함수 + invalidAfter로 정정
(무효화는 min(invalidAfter, i) 하나, recompute도 그 캐시 위에 얹혀 O(N)).

doc-check.py ERROR 0. 상세는
qa-request/pre-implementation-qa-round5-followup.md의 L절.

Co-authored-by: qwreey <me@qwreey.moe>
2026-08-21 20:07:21 +09:00

64 lines
4.5 KiB
Markdown

# [역전됨, 2026-08-21] "부기가 물리 트리 조작보다 항상 먼저 끝난다"
**상태**: 역전됨. 2026-08-20 구현 전 QA 4라운드 `C-7`에서 **모든 `raw*`가 따르는
일반 계약**으로 승격됐다가, **2026-08-21 5라운드에 `native*` 주입 op 계층이
확정되면서 폐기**됐다.
**왜 뒤집혔나**: 이 계약은 *"`Length`를 먼저 올려 뒤 형제를 밀어내고 비워진
공간에 넣는다"*는 그림 위에 서 있었는데, **base에는 물리적으로 자리를 비워둘
수단이 없다.** 미는 주체는 언제나 백엔드의 삽입 연산 자신이다 — 사용자 지적:
*"밀어내고 null을 넣어둘 수도 없는데, 그리고 nativeInsert 라는것 자체가
밀어내기 동작을 강제하는데, 그렇다면 length 를 나중에 설정한 다음 offset들이
무시되어야함."*
**무엇이 남았나**: "빼기는 물리 먼저 / 넣기는 부기 먼저"라는 두 얼굴 대신
**"자기 자리를 정하는 것 먼저(`setOffsetSource`) / 뒤를 미는 것 나중
(`setLength` → `recompute`)"** 하나로 줄었다. 배치 경로(`materializeSlotTree` →
`mountSlotTree`)가 부기 전량을 먼저 끝내는 것은 **C6가 요구하는 별개 사안**이라
그대로다. 지금 유효한 서술은 `base/dispatch-core-plan.md`의 "일반 계약 — 물리와
부기의 순서" 절.
---
## 역전 전 원문
### ⭐ 일반 계약 — 부기가 물리 트리 조작보다 항상 먼저 끝난다 (2026-08-20 구현 전 QA 4라운드 `C-7` 승격)
**지금까지 이 규칙은 `rawAdd` 한 곳에만 적혀 있었다**(바로 아래 "동기 순서"
문단). `rawRemove`/`rawUnmount`는 의사코드가 우연히 같은 모양이었을 뿐
계약화돼 있지 않았고, `Splice`는 물리 detach/attach와 부기의 선후가 **아예
안 적혀 있었다.** 사용자 판정으로 **모든 `raw*`가 따르는 일반 계약으로
승격**한다(*"각각의 동작에 따라 다른 동작 보다, 일관성 있는 동작을 제공하는게
나아보이고, 이것을 단순 일반 계약 승격으로 도달 될 수 있기 때문"*).
> **계약**: 어떤 CRUD/재조정 연산이든 **`Length`/`offset` 부기 갱신이 물리
> 트리 조작(`Parent` 대입/해제)보다 먼저 완료**돼야 한다. 즉 "**먼저 밀어내고
> 그 공간에 넣는다**" / "**먼저 비운 걸 반영하고 그 다음 당긴다**".
- **왜 일반 계약이어야 하나**: 백엔드가 "밀어내기"를 물리적으로 구현해야
하는 경우(DOM `insertBefore` 밖의 백엔드 등), **부기가 이미 정확하다**는 걸
전제할 수 있어야 자기 일을 할 수 있다. 연산마다 선후가 다르면 백엔드
작성자가 매번 다시 확인해야 한다.
- **각 연산에 적용하면**:
- `rawAdd` — 부기(`setOffsetSource`/`setLength` 등록 → `recompute`) 완료 →
물리 마운트(`mountInst`). **[정정, 2026-08-21 5라운드 `C-2`]** 예전엔 여기
`Length:Set(newCount)`라고 적혀 있었으나 **틀렸다** — 아래 문단 참고.
- `rawRemove`/`rawUnmount`/**`rawDetach`**(**[2026-08-21]** `Detach`
경로용으로 신설된 세 번째 형제 — 소유권을 **유지**한 채 언마운트만
한다는 점만 다르고 순서는 같음, `base/slot-plan.md`) — 파괴/언마운트 →
`spliceArraysDown`
`recompute`가 지금 의사코드인데, **이건 물리 조작이 먼저**라 계약과
어긋나 보인다. 다만 여기선 "빼는" 방향이라 부기를 먼저 줄이면 아직
트리에 있는 요소가 순서 계산에서 빠지는 역전이 생긴다 — **"빼기는
물리 먼저, 넣기는 부기 먼저"**가 실제로는 같은 원칙(항상 **좁은 쪽이
먼저**)의 두 얼굴이다. 문서화 시 이 대칭으로 적을 것.
- `Splice` — 제거 구간과 삽입 구간이 겹치므로 위 두 규칙을 그대로 이어
붙이면 된다(제거는 물리 먼저, 삽입은 부기 먼저). **shift/recompute를
1회로 묶는다는 최적화는 그 사이에서만** 일어난다.
- `rawMove`/`rawSwap` — `Parent`를 안 건드리므로 이 계약의 대상이 아님.
- **⚠️ 프레임 경계는 어차피 안 낀다** — `process`/`attachSlot` 체인 도중
코루틴 yield가 금지돼 있으므로(위 "Handler 작성 체크리스트" 9번) 이
순서가 어긋나도 사용자에게 보일 프레임이 그 사이에 없다. 그래서 이
계약은 "안 지키면 깜빡인다"가 아니라 **"백엔드가 전제할 수 있게 하나로
고정한다"**가 진짜 이유다.