Commit graph

5 commits

Author SHA1 Message Date
c58c97a877
design: Gate 표면 확정(state:Gate + GateNode) + State 에포크 채택, 두 문서 base/ 승격
사용자 확정 둘로 M2 착수를 막던 설계 항목이 전부 사라졌다.

1) Gate — 탑레벨 프리미티브를 만들지 않고 state:Gate(setup) 메소드로 확정.
   ComputeNode와 같은 층위의 GateNode를 만든다. Blocker는 그 위의 별개
   프리미티브로, 이미 확정돼 있던 state:Block(blocker)가 내부에서
   self:Gate(policy)를 부른다. Debounce/Throttle의 state:Apply(...) 관용구는
   그대로 — 팩토리가 내부에서 :Gate를 부르면 되기 때문. 이름 문제(Gater?)도
   메소드 자리로 가면서 소멸. Get()엔 영향 없음(통지만 막음)까지 확정.
   research/gate-primitive.md -> base/gate-plan.md.

2) State 에포크 — 채택 확정. 구현은 M3.
   research/state-epoch-validation.md -> base/state-epoch-plan.md.

에포크 채택으로 source-state-plan.md의 두 확정 서술("emit은 항상 전파" /
"quad가 접지 않는 것은 중복 통지뿐")이 역전됐다. 원문은
archive/always-propagate-no-dedup-superseded.md. 지금 계약은 "invalid로는
절대 안 접고, 같은 소스의 같은 에포크가 두 번째로 도착했을 때만 접는다" —
2026-08-14의 invalid 기반 dedup 금지를 되돌린 게 아니라는 점을 역전 문서와
source-state-plan.md, README 세 곳에 못박음(흐려지면 "영구 침묵" 버그로
되돌아감).

같이 갱신: architecture.md 전파 모델 요약, blocker-plan.md(:Gate 배선 +
Get 계약이 에포크 안의 전제라는 것), debounce-throttle-plan.md(공용 게이트
권고가 실현됨 / 파동 단위 최적화 서술 정정), reference/comparison-fusion-vide.md,
source-state-plan.md의 Observer 계약 각주(이제 "새 에포크는 항상 통과"에
의존), ROADMAP M0 각주·M2 각주·M3 체크박스, README/question/todos 인덱스.

스파이크 05-store-state-diamond-propagation은 done/ -> rewrite-required/ 로
되돌렸다 — 다이아몬드 Observer가 이제 변경당 1회만 울어야 해서 핵심 assert가
정반대가 됐다(살릴 것/새로 넣을 것은 STATUS.md에 기재).

처리 전량의 소스는 qa-request/pre-implementation-qa-round5-followup.md의 O절.
doc-check.py ERROR 0.

Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01TiW21rnti9SbLgF6twtn6D
2026-08-21 21:04:27 +09:00
76cf74e80d
design: 에포크 순회 처분을 sourceCountMap/sourceEmitMap 두 테이블로 확정
사용자가 열려 있던 마지막 자리를 제3안으로 닫음. 판정 기준을 둘로 나눈다 —
sourceCountMap(값 유효성)은 순회가 앞당겨 올리고, sourceEmitMap(전파 dedup)은
상류의 진짜 emit을 기다린다. emit 수신 규칙은 셋: count가 다르면 둘 다 갱신 +
rawInvalid + 전파 / count는 같은데 emit 기록이 다르면 전파만(순회가 앞질러
흡수한 경우) / 둘 다 같으면 삼킨다. 순회는 emit을 하지 않는다.

효과 — 통지가 죽는 "영구 침묵"이 사라지고, 순회가 emit을 안 하므로 게이트
누출 경로 자체가 없어져 source = nil 규약도 "게이트를 에포크 경계로" 같은
계약 반전도 불필요해진다. 직전 라운드에서 에이전트가 냈던 (c)안의 약점
(emit 도착 전까지 Get마다 재계산)도 sourceCountMap을 실제로 올리므로 없다.

같이 검토된 rawEmit+nil 안은 구조 위생(상류 emit과 내부 발생 emit의 진입점
통일)만 살리고 해법으로는 안 씀 — 막는 게이트는 보통 순회하는 노드 자신이
아니라 상류에 있어 자기 rawEmit을 태워도 누출이 남고, nil emit은 하류마다
전체 순회를 강제해 같은 문제를 연쇄시킨다.

M절에서 철회했던 seen/computedAt 분리가 다른 근거(순회가 값과 통지를
비대칭으로 앞당김)로 되살아난 것이라는 점도 명시. 이제 기제는 다 정해졌고
남은 건 채택 여부 자체 — README/question.md/ROADMAP 동기화.
doc-check.py ERROR 0.

Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01TiW21rnti9SbLgF6twtn6D
2026-08-21 20:49:28 +09:00
4c395cd383
design: State 에포크 안 3차 정정 — rawInvalid 기제 반전, seen/computedAt 분리 철회
사용자가 research/state-epoch-validation.md를 직접 읽고 기제 서술 세 건을
정정. 채택 여부 자체는 여전히 미정(M3 전 결론 필요).

- sourceList 순회 조건이 반대였다: rawInvalid == false일 때만 돈다.
  true면 재계산이 이미 확정이라 훑을 이유가 없고, 순회의 목적은 오직
  "못 받은 emit(게이트에 막혔던 것)을 여기서 먼저 받는 것".
- emit은 (source, count)가 아니라 발행 source만 싣는다 — 받는 쪽이
  source의 count 필드를 그냥 읽으면 된다.
- 에이전트가 요구했던 seen/computedAt 두 카운트 분리는 철회. count 갱신과
  rawInvalid = true가 같은 스텝이라 캐시 오인 경로가 없다.
- emit 수신 규약 확정형: 같으면 삼킴 / 다르면 count 먼저 갱신 →
  rawInvalid = true → 그 다음 뒤로 emit. 다른 소스 항목은 안 건드림.
- 부수로 열린 것: 순회가 발견한 변경을 뒤로 emit 할 것인가. 다이아몬드
  쪽은 사용자가 스스로 안전으로 정정(D도 count를 갱신해 중복을 삼킴),
  게이트 쪽만 "해제 emit이 source = nil을 싣고 받는 쪽이 전체 확인"
  규약으로 남음 — 게이트는 보통 최종단이라 채택을 막지 않는다는 판단.
- 비용 서술도 뒤집었고(훑는 쪽이 흔한 경로), question.md에 남아 있던
  이미 뒤집힌 옛 결론("중복 통지는 안 고쳐짐 / UB 명문화 필요")도 정정.

처리 기록은 round5-followup.md M절. doc-check.py ERROR 0.

Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01TiW21rnti9SbLgF6twtn6D
2026-08-21 20:15:28 +09:00
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
c6fdf1b348
qa: 구현 전 QA 5라운드 — 문항지·회신·전량 반영 + 감사 2라운드
4라운드 종결 때 "안 만든다"고 했던 5라운드를 사용자 요청으로 신설(205문항).
범위를 셋으로 좁힘 — (1) 4라운드에 문항이 아예 없던 영역(project-setup /
quad-types, 그리고 문서가 아니라 실제 커밋된 M1 코드), (2) 그 이후 확정된 것
(Detach/KeyGone/Owned/attachSlot 분해), (3) 큰 문서의 심화. 회신을 4차에 걸쳐
받아 전량 반영했고, 커밋 전 감사를 각도를 바꿔 2라운드 돌렸다.

주요 확정/역전:
- slot._detached lazy화, KeyGone엔 새 값 반환도 error,
  Owned=false에서 Detach는 _detached에 안 들어감(rawUnmount)
- Slot:Replace 신설 + rawReplace/rawAdd 의사코드 신설(문서에 정의가 없었음)
- raw* 인자를 index로 전부 통일 — 오래 열려 있던 캐비엇 종결.
  래핑은 raw* 바깥에서만(공개 표면 + settle), raw*는 물리 요소만 다룸
- 물리 조작을 주입 op로(mountInst/unmountInst/disposeInst, 이름 가칭) —
  base는 Parent를 모른다는 지적. mountInst는 0-based 절대 offset을 받음
- Dispatch.setLength에 anchor(생략 시 ownerKey) — 부기 키와 생명주기 앵커
  분리, 4라운드 D-56 역전(archive로)
- Dispatch.getOffsetAt 신설(pull) + 접두합 캐시(offsetDirtyFrom),
  setOffsetSource(None)은 얼리 리턴, None의 뜻을 "발행 채널 없음"으로 정정
- recompute가 owner 베이스에서 시작(중첩 offset이 부모 베이스를 못 받던
  결함), _baseObserver로 깊은 전파, Offset Source identity 재사용(포탈),
  bk.N or 0(빈 Slot 크래시)
- Effect(fn, ...deps) 확정 — Ref도 의존성(옛 "trailing args sugar 안 만듦"
  역전), Tween:Mapped, groupClaimKeys 키 = (inst, groupValue) → k
- 게이팅 먼저(M2로 앞당김) — 다만 대상이 Blocker가 아니라 공용 Gate 노드로
  바뀌었고, 설계는 사용자 지시로 다음 세션(M2 착수를 막는 유일한 항목)

새 research 둘: gate-primitive.md(다음 세션이 이어받을 재료),
state-epoch-validation.md(전파 중 Get이 섞인 값을 캐시하는 glitch — 정확성
결정이라 M3 전 결론 필요).

감사가 잡은 것 중 큰 것: 확정한 Owned가 Slot:List 시그니처에 배선이 안 돼
코드에 도달 못 하던 것, effect-plan.md의 역전 배너 없는 자기모순,
그리고 손대지 않은 문서(ROADMAP 백로그·debounce-throttle-plan)가 "Gate는
M3에서"로 남아 있던 사각지대.

doc-check.py ERROR 0. 상세는 qa-request/pre-implementation-qa-round5-followup.md
(A~K절, 마지막이 최신).

Co-authored-by: qwreey <me@qwreey.moe>
2026-08-21 18:19:58 +09:00