에이전트 권고("빈 배치 = 무조건 통지")를 사용자가 기각. 빈 배치는 "이미 다
던져서 더 던질 게 없다"는 뜻이고, 같은 카운트가 두 번 흘러든 emit을 전파하지
않는 것과 같은 자리다. 그걸 흘리는 건 표면적으로 State 중간에 Source:Emit을
추가하는 격이라 Gate의 성격과 안 맞는다.
확정: next(withheld) == nil이면 통지 자체를 안 한다. 새 규칙이 아니라 기존
계약의 일반화임을 확인 — blocker-plan.md가 이미 "HasBlockedEmit이 false면
emit 값과 무관하게 아무 것도 안 함(idempotent)"으로 확정해뒀고
HasBlockedEmit은 next(withheld) ~= nil의 특수형이다. Debounce/Throttle도
if pending일 때만 passThrough()를 부른다.
따름정리 — Effect(fn, ...deps)의 설치 구간 억제가 Gate 소비자에서 빠졌다.
설치 구간엔 어떤 Set도 안 일어나 게이트에 쌓이는 소스가 없으므로 게이트가
내보낼 것 자체가 없고, Effect 내부 플래그면 충분하다. effect-plan.md에서
"⚠️ 억제 장치의 모양은 Gate 설계에 딸려 있다"와 우선순위 문단의 "Gate보다
뒤다"라는 순서 제약이 같이 빠졌다.
이로써 Gate에 사용자 판단이 필요한 항목은 없다 — 남은 건 생명주기 계약과
M2 범위뿐이고 둘 다 구현 시 결정. 처리 전량은 V절. doc-check.py ERROR 0.
Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01TiW21rnti9SbLgF6twtn6D
사용자 반문으로 에이전트 서술 오류 둘을 정정. 결론은 안 바뀌었고 근거와
열린 항목 목록만 정리됐다.
1) gate-plan.md 6번의 "onUpstreamEmit 안에서 같은 게이트의 emit()을 재귀적으로
부르는 경우"는 blocker-plan.md의 재진입 절을 잘못 옮긴 것이다. 그 절은
같은 Blocker 인스턴스를 중첩해 On()/Off() 하는 것을 말하고 정책의 emit()
호출과 무관하며, 정책이 flush를 부르는 건 재귀가 아니라 평범한 통과
경로다. 계약 셋으로 정리: 끝나지 않는 되먹임은 UB(dispatch-core-plan.md의
2026-08-04 확정 원칙), 유한한 재진입은 지원(debounce-throttle-plan.md의
onWindowEnd 주석이 이미 대비), 같은 인스턴스 중첩 금지는 Blocker 규칙 그대로.
그래서 question.md의 사용자 판단 항목에서 재진입을 뺐다.
2) 정책이 받는 emit은 "이 값을 내보내라"가 아니라 "쌓인 걸 지금 흘려보내라"
(flush)이고, debounce-throttle-plan.md가 이미 gate:passThrough()로 부르던
것이다. 배치를 떼어내는 것도 그 핸들 안에서 일어나므로, 직전 커밋이
"재진입 위험"이라 부른 것은 에이전트가 적은 "전파 후 table.clear" 의사코드의
결함이지 모델의 구멍이 아니었다. 수정(flush 진입 시 스왑)은 그대로 유효하고
서술만 그렇게 고쳤다.
Gate에 남은 사용자 판단은 빈 배치 emit 하나뿐. 처리 전량은 U절.
doc-check.py ERROR 0.
Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01TiW21rnti9SbLgF6twtn6D
High 둘은 실제 유실 경로였다.
- withheld를 페이로드로 그대로 넘기면 재진입에 깨진다. 전파 중 Observer가
Set을 불러 게이트에 재진입하면 중첩 전파가 끝나며 table.clear가 돌아
바깥 전파의 남은 갈래가 빈 집합을 받는다. 전파 직전에 새 테이블로 스왑해
배치를 떼어내고 그 배치를 페이로드로 넘기는 것으로 수정.
- OffWithoutEmit()이 집합을 안 비웠다. Dispatch.drive의 배치 게이팅이 매
프레임 On() -> OffWithoutEmit()을 돌므로 집합이 단조 증가하고(weak 설계와도
충돌) 나중에 아무 소스나 통과할 때 폐기분이 같이 실려 나간다. 그 경로도
비우도록 확정하고 withheld를 weak key로 명시.
나머지 수정 셋:
- "무조건 withheld에 넣는다"가 수신 규칙 1~3을 건너뛴다는 뜻으로 읽히던 것을
"정책의 통과/유보와 무관하게"로 명시(그대로 두면 다이아몬드에서 Throttle
정책이 두 번 돌아 유령 trailing emit이 나간다)
- luau-test/STATUS.md의 "런타임 12개"가 이미 나간 04/10/19를 포함한 옛
총계에서 이어져 온 수라 실제(9건)와 안 맞던 것
- followup D절 색인 표가 삭제된 research/ 두 문서를 현재형으로 서술하던 것
열린 항목으로 승격 둘(question.md의 "남은 것은 사용자 판단이 아니다"도 정정):
- Gate 재진입 계약 — 스냅샷으로 유실은 막았으나 정책 안 재귀 호출 계약은 미정
- 소스 없는 emit(빈 배치) — 정책이 상류 신호와 무관하게 emit()을 부르면 빈
배치가 나가 하류가 조용히 삼킨다. Effect(fn, ...deps) 설치 구간 억제
용례가 정확히 이 모양이라 그대로는 성립하지 않음. 권고는 "빈 배치 = 무조건
통지"
처리 전량은 round5-followup.md의 T절. doc-check.py ERROR 0.
Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01TiW21rnti9SbLgF6twtn6D
사용자 발견. R절까지의 규칙("출처가 GateNode면 받은 출처를 그대로 아래로
넘긴다")은 받는 쪽이 또 게이트인 경우가 정의돼 있지 않았고, 그대로 넘기면
깨진다 — 상류 게이트는 자기 전파가 끝나자마자 table.clear 하므로 하류
게이트가 출처만 들고 유보했다가 나중에 풀면 빈 집합을 내보내 변경이 통째로
증발한다.
확정: 수신 시점에 unfold 해서 자기 _withheld에 합친다(Source면 하나,
GateNode면 그 집합 전부). 게이트가 몇 겹으로 겹쳐도 각 층이 자기 집합을
들고 있으므로 어느 층이 먼저 풀리든 정보가 안 샌다.
같이 못박은 것: 게이트의 sourceEmitMap은 수신 때가 아니라 실제로 전파할 때
집합 전체에 대해 갱신한다 — 그래야 "내가 하류로 던진 에포크"라는 맵의 뜻이
게이트에서도 참이 된다.
반영은 base/gate-plan.md 4번, base/state-epoch-plan.md §2, followup S절.
doc-check.py ERROR 0.
Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01TiW21rnti9SbLgF6twtn6D
1) 게이트는 통과와 유보를 구분하지 않는다. 상류 emit이 오면 정책 실행 전에
무조건 withheld[source] = true 로 넣고, 정책이 emit()을 부르면 게이트가
자기를 출처로 전파한 뒤(동기) table.clear 한다. 그냥 통과시킬 때도 상류
출처를 넘기지 않고 언제나 자기를 낸다 — 하류가 보는 차이는 집합 원소가
하나냐 여럿이냐뿐이고 판정 규칙은 같다. 그래서 직전 라운드에 넣었던
"정책이 그 자리에서 emit()을 불렀는지 노드가 되짚는다"는 감지 로직이
통째로 불필요해졌다.
2) 새 노드의 두 맵은 비대칭으로 초기화한다.
- sourceEmitMap: 비운다. nil ~= source.count 라 어떤 emit도 "처음 보는
것"으로 걸리고, 새 노드는 실제로 emit을 받아본 적이 없으므로 그게 맞다.
- sourceCountMap: 상류에서 전부 끌어와 실제 count로 채우고 rawInvalid를
true로 둔다. 순회가 훑을 목록이 곧 이 맵이라 비워두면 "훑을 게 없으니
유효하다"로 오판한다 — 여기는 lazy할 수 없고 "내가 뭘 추적하는가"가
필요하다.
그래서 :With 병합 규칙은 필요 없어졌다(생성 시점 라이브 count로 통일되므로
두 상류가 같은 소스에 다른 count를 들 일이 없다). /code-review Med-3이
열어둔 (b)/(c)가 이걸로 전부 닫혔다.
처리 전량은 round5-followup.md의 R절. doc-check.py ERROR 0.
Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01TiW21rnti9SbLgF6twtn6D
/code-review high가 잡은 3건에 대한 사용자 회신 반영.
1) 게이트가 유보했다 내보내는 emit — (c) 채택, 다만 에이전트 안보다 단순한
형태로. 게이트에 자체 count를 주는 대신 흡수한 소스 집합
withheld : {[source]=true} 만 들고 있다가, 풀 때 자기를 출처로 하류에
emit 하고 전파가 동기이므로 반환 뒤 table.clear 한다. 하류는 출처가
GateNode면 그 집합의 소스들에 평소 규칙(1~3)을 적용하고 받은 출처를
그대로 더 아래로 넘긴다. OffWithoutEmit도 안전(다음 진짜 emit이 스스로
낫게 함). 그래서 emit의 출처는 Source | GateNode 둘 다가 된다.
⭐ setup 시그니처는 안 바뀐다 — 흡수 집합을 채우는 건 정책이 아니라
노드이기 때문(노드가 onUpstreamEmit 전후로 정책이 그 자리에서 emit()을
불렀는지만 보면 됨). P절이 "M2 표면에 영향"이라 적은 건 기우였다.
2) 재계산 후 sourceCountMap은 자기가 읽은 상류 전부를 갱신한다(확정).
다만 이걸 제기한 에이전트 근거("A:Set(); Z:Set()이면 같은 값을 두 번
계산")는 사용자가 반증 — 전파가 동기라 A 파동이 완전히 끝난 뒤 Z:Set()이
시작되므로 통지가 두 번 나는 건 중복이 아니라 맞는 동작이다. 전부 갱신이
실제로 값을 하는 자리는 게이트 유보 중 하류가 Get()으로 앞당겨 읽는
경우뿐이고, 그때 해제 통지가 규칙 2(통지만)로 떨어져 재계산을 안 한다.
3) 같이 명문화한 대원칙: 무효화를 결정하는 건 언제나 count 비교지 emit의
도착이 아니다. emit은 "이 원천을 확인해봐"라는 요청일 뿐이라, 통과해도
count가 최신이면 캐시는 유효한 채로 남는다.
남은 열린 항목은 새 노드의 두 맵 초기값(복사 vs 첫 재계산 때 구성)과 그에
종속된 :With 병합 규칙뿐이고, 동기 전파 덕에 차이가 나는 경우가 게이트 유보
중 파생 노드가 생길 때 하나뿐이라 어느 쪽이든 무해 — M3 구현 시 결정.
처리 전량은 round5-followup.md의 Q절. doc-check.py ERROR 0.
Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01TiW21rnti9SbLgF6twtn6D
O절 커밋(c58c97a) 직후 돌린 리뷰에서 12건이 나왔고 전부 유효했다.
열린 항목으로 승격(임의로 정하지 않음):
- [M2 착수 전] 게이트가 유보했다 내보내는 emit이 어느 source를 싣는가.
확정된 setup은 (emit: () -> ()) -> (() -> ())라 양쪽 다 source를 안 받는데
에포크 수신 규칙은 전부 [source] 키로 판정한다 — 그대로면 blocker:Off()가
묶어둔 배치 emit이 하류에서 규칙 3으로 삼켜져 통지가 통째로 사라진다.
5라운드 M절이 이미 짚었는데 표면 확정 때 같이 안 닫힌 것. 후보 (a) emit(nil)
전체 확인 / (b) 유보 소스마다 emit(Blocker의 "정확히 1회"가 깨짐) /
(c) GateNode 자신을 source처럼 취급 — 권고는 (c). gate-plan.md 4번.
- [M3 착수 전] 두 맵의 초기값·:With 병합·재계산 시 갱신 범위.
규칙 1이 발행 소스 항목만 건드려 다중 소스 배치에서 같은 값을 두 번
계산하고, "상류에서 복사"는 순회가 앞당긴 지연분 상속 여부가 미정이라
새 노드가 통지를 삼킬 수 있다. state-epoch-plan.md §5 7번.
이에 따라 todos.md 00번의 "M2를 막는 설계 항목 없음"도 정정.
그 자리에서 수정:
- state-epoch-plan.md: §4/§5-2의 sourceList 잔재(코퍼스에 bk.sourceList라는
무관한 동명 식별자가 있어 오독 위험), §3의 "rawInvalid가 켜져도" 정정
- gate-plan.md: 배너가 부정하는 본문 두 문장을 같이 수정
- question.md 1번: Gate 이름 항목이 열린 채였던 것 해소로 갱신
- luau-test/STATUS.md: 05 이동이 반영 안 된 개수 3곳 + 같은 파일 안의
모순 문장("05가 다시 돌아왔다")
- comparison-fusion-vide.md: 배너 바로 위 본문이 배너와 어긋나던 것
- N절/README가 가리키던 research/ 옛 경로
처리 전량은 round5-followup.md의 P절. doc-check.py ERROR 0.
Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01TiW21rnti9SbLgF6twtn6D
사용자 확정 둘로 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
사용자가 열려 있던 마지막 자리를 제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
(1) research/state-epoch-validation.md §5-3 재작성. 사용자 지적대로 순회가
count를 올리면 뒤늦게 온 진짜 emit이 삼켜져 하류 통지가 영구히 죽는다
(2026-08-14에 폐기된 옛 dedup의 "영구 침묵"과 같은 계열). 두 해법을
대조로 남김 — (b) 순회도 emit(사용자 제안, 게이트 누출이 남고 그 누출을
막을 기제가 둘 다 대가가 큼: 에포크 경계는 blocker의 ":Get()엔 영향 없음"
확정 계약을 뒤집고, source = nil 규약은 사후 정합성만 맞춤) vs
(c) 순회는 rawInvalid만 세우고 count는 안 올림(에이전트 권고 — 원인만
제거하므로 누출도 nil 규약도 계약 반전도 안 생김, 대가는 emit 도착 전까지
Get마다 재계산과 OffWithoutEmit 캐비엇). 미결로 남김.
(2) research/gate-primitive.md 2번 해소. 사용자 확정으로 Gate는
state:Gate(setup) 메소드다. 경계는 "Apply가 노드를 못 만든다"가 아니라
"프리미티브는 메소드 / 유저랜드 조합 팩토리는 :Apply". 그래서
Debounce/Throttle의 Apply 관용구는 유지되고, Blocker 배선은 이미 확정된
state:Block(blocker)가 내부에서 :Gate를 부르는 것으로 자동 해소되며,
__call은 쓰지 않는다.
question.md/ROADMAP 인덱스 동기화. doc-check.py ERROR 0.
Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01TiW21rnti9SbLgF6twtn6D
사용자가 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
주입 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>
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>