앞 세션이 컨텍스트 피로로 미뤄둔 승격(todos 000번)을 수행하고, 코퍼스에
쌓여 있던 [해소]/[정정] 층을 걷어냈다. 감사 3라운드 + /code-review high로
15건을 잡아 전부 반영했다.
승격 — base/ 넷 + 파급 넷
- state-epoch-plan.md 재작성: Epoch 인터페이스({Revision:number}, 그 자체로
키가 되는 unique 테이블, Source가 구조적으로 만족)와 EpochMap(Update/
Refresh/Sync/TrackFrom) 신설. State는 EpochMap을 둘 컴포지션 —
sourceCountMap/sourceEmitMap -> valueEpochMap/emitEpochMap. §1~§8로 재편.
- brand-plan.md 전면 재작성: 공유 레지스트리 + Brand.get(객체당 태그 하나)
-> 인스턴스 브랜드 Brand() + :register/:is, 다중 태깅 허용. 발단은 Source가
SourceBrand이면서 동시에 EpochBrand여야 하는데 옛 모양으로 표현 불가.
역조회는 제거(전수 조사에서 쓰는 자리 0). weak-key/테이블 아이덴티티/
duck-typing 기각 근거/predicate 합성은 전부 유지.
- source-state-plan.md: Source가 Epoch도 구조적으로 만족(Revision은 공개여야
타입 레벨에서 성립), Observer 클로저가 fn(self, from: (Epoch|EpochSet)?).
- ⭐ effect-plan.md: 다중 의존성 중복 발화 미해결 항목이 닫힘 —
EffectHandle이 자기 EpochMap을 들어 첫 번째만 통과시킨다.
- gate-plan.md/architecture.md/bind-system-plan.md/ROADMAP.md 어휘 통일,
EpochMap.luau가 M2에도 필요하다는 것 반영(GateNode가 씀).
리비전 갱신은 bit32.bnot(-rev) — 사용자 확정
- a>0이면 a-1, 0이면 4294967295인 랩어라운드 감소. 갱신과 랩이 FASTCALL
하나로 끝난다(luau 실측). 근거: 2^53 포화는 도달 불가능한데 그걸 피하려고
값을 double 영역까지 키울 이유가 없다, 매번 도는 hot path다.
- 에이전트가 이걸 band(rev+1, mask)로 잘못 옮기고 "그러니 bit32가 더 싼 건
아니다"라는 틀린 단서까지 달았다가 사용자 정정("제가 말한건 bit32.bnot(-a)
입니다"). 세 문서에 정정 경위를 남겼다.
- 따름정리: 리비전은 증가가 아니라 감소한다. ==/~= 만 쓰는 지금 규칙에서만
무해하다는 경고를 §2에 명시.
archive / flatten
- archive/brand-shared-registry-reversed.md 신설(옛 Brand 표면 원문).
- question.md 421->208줄: 해소 항목 18건을 archive/question-resolved.md로
이관. 그 문서가 스스로 정한 규칙("해소하면 여기서 지우고 archive로")을
다시 어기고 있었다. 이관분의 옛 필드명은 소급 수정하지 않고 머리에 경고만.
- todos.md 000번 삭제, "M3 착수 전 필요" 목록에서 해소 항목 일곱 제거
(실제로 열린 건 중간 State GC와 store:GetDynamic 둘뿐).
- research/ -> reference/ 이동 둘(epoch-brand-composition,
slot-attach-decomposition). 확정된 결정의 근거 기록은 research(상의 필요)도
archive(뒤집힘)도 아니므로, reference의 폴더 기준에 그 용도를 명문화했다.
감사 3라운드(각도: base 정합성 / 인덱스+luau-test / archive+qa-request)
- §8에 "기각된 대안 — 게이트를 에포크 경계로" 논거 신설(재작성 때 떨어뜨렸고,
두 문서가 서로 다른 없는 §번호를 대고 있었다).
- luau-test 스파이크 22를 done/ -> rewrite-required/(옛 Brand.set/get을 직접
구현). STATUS.md 개수와 절 제목의 하드코딩 개수 정리.
- state-epoch/source-state가 "Revision을 증가시킨다"고 적어놓고 20줄 뒤에
"감소한다"로 반박하던 자기모순 정정.
커밋 전 /code-review high — 9건, 전부 유효(감사자가 못 보는 축)
- ⭐ {Epoch}는 Luau에서 배열인데 실제 게이트 배치는 {[Epoch]:true} 집합.
그대로 ipairs로 구현하면 유보됐다 풀린 emit이 전부 삼켜진다(gate-plan 4번이
애초에 고치려던 그 버그) -> EpochSet으로 확정.
- ⭐ 새 노드 시딩이 확정된 EpochMap 표면으로 표현 불가능했다(:With의 상류는
State이지 Epoch가 아니고, 키 열거/병합 연산이 없었음) -> :TrackFrom 신설.
- GateNode 예외(emitEpochMap을 전파 시점에 갱신)가 §4에 미기록.
- 설치 발화엔 from이 없다 -> 옵셔널로, Effect의 억제 플래그가 Update보다
먼저여야 함을 명시.
- 2^32 랩을 "똑같이 도달 불가능"이라 한 근거가 틀렸다(같은 척도로 285년 vs
72분). 실제 안전 근거는 충돌 조건이 한 점이라는 것으로 정정.
- 그 외 :Sync 용도 충돌, isEpoch 누락, TweenTag 3곳, Effect(fn,state?) 4곳,
§ 참조 3곳.
에이전트가 이름 붙인 연산 둘은 사용자 검토로 확정
- :Refresh 유지 — "Update는 받은 것을 처리, Refresh는 내가 받았던 걸 처리라
표면적 의미 자체가 다르다"(오버로드로 합치지 않음).
- :Absorb -> :TrackFrom 개명 — absorb는 상위에서 제거할 것처럼 읽히고,
gate-plan이 이미 "흡수 집합"을 다른 뜻으로 쓴다. TrackFrom은 이 맵의 존재
이유("내가 뭘 추적하고 있나")를 그대로 쓰고 From이 비파괴를 못박는다.
Epoch/EpochMap/Brand에 열린 설계 항목 없음. doc-check.py ERROR 0.
Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01TiW21rnti9SbLgF6twtn6D
사용자 지시로 승격은 다음 세션에("이 세션은 너무 길어요"), 감사는 승격
이후에 돌린다. 이번 세션에서 알게 된 것 중 base/에 안 남아 있던 것을
전부 채웠다.
- base/ 네 문서 상단에 research/epoch-brand-composition.md를 가리키는 ⚠️
배너 신설: state-epoch-plan(두 맵 -> EpochMap 둘), source-state-plan
(Source가 Epoch를 구조적으로 만족 + Observer 클로저 fn(self, from)),
brand-plan(인스턴스 브랜드 전면 재작성), gate-plan(emit 페이로드 타입).
넷 다 "기제는 그대로 유효, 표현만 바뀐다 / 승격 전엔 이 문서가 정본"을
명시.
- ⭐ base/effect-plan.md에 다중 의존성 Effect 갭을 미해결로 기록. 이번
세션에 발견했는데 research 문서에만 있었다 — 제안이 기각되더라도 갭은
남으므로 base에 있어야 한다. 해법 후보(Effect가 자기 EpochMap을 듦)와
접은 대안(deps를 한 노드로 수렴), 그리고 "useEffect처럼 N번 허용"
선택지까지 같이 적었다.
- base/blocker-plan.md에 HasBlockedEmit == (next(withheld) ~= nil)이라는
것과 "빈 배치면 무통지"가 그 일반화라는 것 기록(구현 시 둘을 따로 들지
말 것).
- todos.md에 000번 신설 — 다음 세션 첫 작업, 고칠 문서 넷과 남은 미정
하나(bit32 랩 vs +1)까지 명시.
- question.md 1번에 리비전 증가 방식 항목 추가.
- session-summary.md의 이 세션 항목이 "Gate만 다음 세션 — M2를 막는 유일한
항목"에서 멈춰 있던 것을 전량 갱신(같은 날 Gate/에포크가 확정됐고,
code-review 두 번에 19건이 나왔으며, Epoch/Brand 제안이 대기 중).
doc-check.py ERROR 0.
Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01TiW21rnti9SbLgF6twtn6D
- 리비전은 숫자(Revision)로 확정. 증가 방식만 미정 — 사용자는 bit32 랩을
염두에 뒀고(uint32 랩어라운드라 double 포화가 안 생김) 그건 맞지만, 충돌
거리는 오히려 짧아진다는 점을 같이 기록했다(평이한 +1은 2^53 포화,
bit32 랩은 2^32마다 한 바퀴). 둘 다 도달 불가능이라 실질 위험은 없다.
- "재계산 후 전부 최신으로 맞추려면 Update 외의 연산(:Sync)이 필요하다"는
에이전트 주장 철회 — Update가 Epoch|{Epoch}를 받으므로 전체 deps를 넘기면
그게 곧 sync다. Update를 "하나만 받는 것"으로 좁게 본 착오. 노드 초기화도
같은 연산 하나로 끝나고(반환값 무시 + rawInvalid = true), 이미 확정된
생성 규칙과 정확히 같은 동작이다. :Sync는 읽기를 건너뛰는 순수 최적화
변형으로만 남는다. 목록 순회 중 한 번 다름을 찾으면 반환값이 확정되므로
나머지는 읽지 않고 쓰기만 하면 된다는 내부 최적화도 기록.
- Observer 클로저는 :Compute와 같은 모양 fn(self, from: Epoch|{Epoch})로
방향 확정. 값이 아니라 핸들과 메타데이터만 넘기므로 "값을 안 실어주는
구독" 계약이 안 깨진다.
base/ 승격은 사용자 확인 대기 — 승격 시 state-epoch-plan/source-state-plan/
effect-plan/brand-plan 넷을 같이 고쳐야 한다. doc-check.py ERROR 0.
Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01TiW21rnti9SbLgF6twtn6D
- Brand.get 역조회는 불필요로 확정(코퍼스가 쓰는 건 전부 isX 멤버십 질문).
- "포함 관계가 생성자로 흩어진다"는 에이전트 착오라 철회 — 자기 등록이 여러
브랜드 등록을 강제하지 않으므로 isRef = PreRefBrand:is(x) or ... 처럼
predicate 합성으로 지금과 똑같이 한 곳에 쓸 수 있다.
- Epoch는 Source가 구조적으로 만족하는 것으로 확정(State를 만족하는 기존
패턴과 같은 모양).
- 리비전 필드는 공개로 확정. 근거가 정확하다 — 비공개면 구조적 만족이 타입
레벨에서 성립하지 않는다. Store와 달리 Source는 키가 사용자 것이 아니라
예약 이름이 늘어도 무방. 이름은 코퍼스 PascalCase 관례를 따라 Revision
(숫자안) 또는 Token(테이블안) 권고.
- 계약은 "직전 값과 다르다"만 요구(순서 비교는 규칙 어디에도 안 쓰임).
⚠️ 다만 "오버플로가 나든 이전값과 다르다"는 근거는 정정 — Luau 숫자는
double이라 랩어라운드가 아니라 포화하고, 2^53에서 n+1 == n이 되어 보장이
정확히 그 지점에서 깨진다. 도달 불가능해서 문제가 아닌 것이므로 근거를
"도달 불가능"으로 적어둠.
- Brand 전환 범위/마일스톤 해소 — 커밋된 M1 코드는 Brand를 아직 안 쓴다
(quad-base/src는 init/Relate/Debug뿐) 전환 비용은 문서뿐.
남은 열린 항목 셋: 숫자 리비전 vs 테이블 토큰(권고는 숫자 — 테이블은 Set
한 번마다 할당이라 매 프레임 Set하는 소스에서 GC 압력), State가 EpochMap을
둘 들고 :Sync가 필요한지, Observer 클로저 인자.
doc-check.py ERROR 0.
Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01TiW21rnti9SbLgF6twtn6D
사용자 제안을 research/epoch-brand-composition.md로 신설. 아직 아무것도
확정 안 함 — base/state-epoch-plan.md 등이 여전히 정본.
요지: (1) 에포크 부기를 State에서 떼어내 EpochMap:Update(Epoch|{Epoch}) ->
boolean(뒤로 전파가 필요한가)으로 컴포지션, (2) Observer가 emit 출처를
클로저 인자로 흘려주고 Effect가 자기 EpochMap을 들어 다중 dep 중복 발화를
접음, (3) emit 페이로드에서 게이트를 빼고 Source|{Source}로, (4) 나아가
Epoch = {Count: number} 인터페이스로 일반화, (5) 그걸 위해 Brand를
인스턴스화 가능하게(Brand() + :register/:is, 다중 태깅).
에이전트 평가: 방향 타당. 특히 다중 의존성 Effect 갭(A→b, A→c일 때 접어줄
공통 하류가 없어 fn이 두 번 도는 것)이 정확히 닫히고, 맵이 Source에서
요구하는 게 identity + 단조 카운터뿐이라는 점에서 Epoch 일반화가 계약을
정확하게 만든다. 다만 대가 둘을 명시 — Brand.get 역조회 상실, 그리고
2026-08-09에 의도적으로 설계한 "포함 관계가 코드 모양에 드러남"이 자기
등록 방식에서는 생성자로 흩어짐.
결정 7건을 열린 채로 기록(Epoch를 구조적 만족으로 둘지, Count 공개 여부,
인터페이스 계약, State가 EpochMap을 둘 들고 :Sync가 필요한지, Observer
클로저 인자, Brand 전환 범위, 마일스톤).
doc-check.py ERROR 0.
Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01TiW21rnti9SbLgF6twtn6D
에이전트 권고("빈 배치 = 무조건 통지")를 사용자가 기각. 빈 배치는 "이미 다
던져서 더 던질 게 없다"는 뜻이고, 같은 카운트가 두 번 흘러든 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>
quad-doc-auditor 6라운드 수렴 후 /code-review high로 diff 자체를 재검토한
결과. 감사자는 코퍼스 전체 정합성만 보고 diff 결함은 못 잡는다는 게
conventions.md가 이미 명시한 한계인데, 실제로 이번 diff 안의 결함이
나왔다.
- activateList의 재마운트 분기가 bindLifetime(physicalTarget, nil)로
크래시할 수 있었음. _listObserver는 data가 reactive(State/Source)일 때만
세팅되고 plain table data(문서가 지원하는 형태)면 영원히 nil인데,
가드 없이 불렀음. 짝인 unmountSlotTree 쪽은 이미 방어돼 있었던 것과
비대칭 — 가드 추가.
- "구독 시점" 절에 activateList의 옛 파라미터 이름 inst가 리네이밍 후에도
남아 있어, 그 절만 읽는 구현자가 physicalTarget 리네이밍의 취지(owner
키가 Slot일 수도 있는 문맥과의 혼동 방지)를 놓칠 위험 — 정정.
qa-request/pre-implementation-qa-round4-followup.md에 I-8로 기록.
doc-check.py ERROR 0.
Co-authored-by: qwreey <me@qwreey.moe>
커밋 9b7f847(Detach/KeyGone/Owned/attachSlot 분해) 반영 후 각도를 바꿔가며
quad-doc-auditor를 6라운드 돌린 결과. 라운드별 확실 발견 4/6/2/3/3/0으로
6라운드에서 새 발견 0건 — 수렴 확인 후 종료. 경위 전량은
qa-request/pre-implementation-qa-round4-followup.md의 I절이 소스.
트레이싱 라운드(4~5)가 잡은 실제 크래시 — 셋 다 Detach가 신설한 경로가
기존 불변식과 부딪히는데 그쪽이 안 고쳐진 것:
- I-1 (치명): rawDetach가 소유권을 유지하는데 재마운트는 rawAdd →
claimOwner를 거치고, claimOwner는 같은 owner의 재클레임도 무조건 error다
(2026-08-13 감사가 Slot{a,a}를 막으려고 넣은 것). 문서가 권장하는
"prev를 그대로 반환하면 재마운트" 패턴이 그대로 죽었음. fromDetached
플래그로 그 경로만 좁게 예외 처리.
- I-2: 재마운트된 자식 Slot이 activateList를 두 번 실행해 구독이 이중으로
생기고 mounted/keyIndex 클로저 상태가 통째로 새로 만들어짐 → 멱등 가드.
가드만으로는 :List 구독이 옛 physicalTarget에 앵커된 채 남아 포탈
재마운트 후 조용히 멈추므로, _listObserver 핸들 보관 + 재앵커까지 처리.
- I-3: _detachCleanup이 releaseOwner를 안 불러 Owned=false 요소가 죽은
Slot을 owner로 달고 남음 → 두 분기 공통으로 호출.
- I-6: 위 수정의 회귀 트레이싱 — destroySlotTree에 _listObserver 해제 누락,
claimOwner의 옛 논증 두 문단이 fromDetached와 정면 모순, 소유권 예시
코드가 C-4와 모순.
- I-7: 사용자가 별도 상의해 가져온 두 건 — _detachCleanup 설치를
mountSlotTree → activateList로 이관(:List 없는 Slot마다 no-op Effect를
트리 크기만큼 심고 있었음), activateList의 inst → physicalTarget 리네이밍.
이관 근거가 멱등 가드 이전 동작을 전제하고 있어 가드 분기의 재앵커까지
같이 반영. 이로써 _listObserver/_detachCleanup이 같은 범주로 통일됨.
I-4(materializeSlotTree 중 예외 시 Blocker 잔류)는 사용자 판단으로 pcall
없이 문서화만 — 아직 밟은 적 없는 경로이고 옛 단일 attachSlot에도 있었을
구조적 갭.
문서 정합성 라운드(1~3)에서 나온 것: slot-plan의 "값 교체는 비파괴" 잔존,
분해 완료 후에도 남아 있던 "논의 대기 중" 배너, attachSlot의 flush 루프를
가리키던 문장 5곳, README 색인 행이 2026-08-19에서 멈춰 있던 것,
qa-round4 문항지/followup의 "회신 대기" 상태줄, todos의 용어 목록 이중 소스,
dispatch-core의 raw* 일반 계약에 rawDetach 누락.
luau-test: 스파이크 01이 "재작성 필요" 마커를 단 채 done/에 남아 있어
STATUS.md 자신의 "폴더가 곧 상태" 규칙을 어기고 있었음 → rewrite-required/로
이동하고 개수 정정. "만들어야 할 스파이크" 절 신설(아직 파일조차 없는 실측
항목이 어느 폴더로도 표현되지 않아 구조적으로 잊히던 자리).
doc-check.py ERROR 0.
Co-authored-by: qwreey <me@qwreey.moe>
사용자가 짚은 "물리 마운트가 관측 이후 일괄로 밀린다"는 차이를 따져본 결과
감수하는 비용이 아니라 개선이라는 결론.
- 마운트 순서 자체는 안 바뀐다(둘 다 깊이 우선 같은 순서) — 바뀌는 건 각
Parent 대입 시점에 부기가 얼마나 완성돼 있는가뿐.
- 그 차이를 실제로 관측하는 주체가 있다: Parent 대입이 ChildAdded/
DescendantAdded를 동기 발화시키므로 사용자 핸들러가 마운트 도중에 끼어든다.
현행은 A의 ChildAdded 시점에 inner.Length가 0이고 뒤 형제 offset도 stale인
미완성 스냅샷을 보여주는데, 분해하면 첫 발화 때 서브트리 전체가 최종값이다.
slot.Length 구독자도 0→최종 두 번이 아니라 최종값으로 한 번 발화.
- "합치는 거대 함수"는 만들어도 목적을 못 이룬다 — 완전 병합은 자식 길이를
그 자식 재귀가 끝나야 알므로 C6를 못 지키고(그게 지금 코드), 부분 병합은
인터리브는 유지하지만 부모 레벨 C7 위반이 그대로 남는다. 실질 선택지는
(A) 인터리브+자기교정 / (B) 분해+일괄 둘뿐.
- 부수 이득 둘: Blocker가 materialize만 감싸게 되어 "배치 등록 게이팅"이라는
정의와 코드 범위가 일치하고, mountSlotTree가 순수 walk라 일괄 삽입이
유리한 백엔드(웹 DocumentFragment 등)가 그 함수 하나만 갈아끼울 수 있는
seam이 생긴다.
판단을 "약한 추천"에서 "추천"으로 갱신 — RC-1/RC-3/RC-4가 전부 한 함수 안의
줄 순서를 잘못 잡아 난 버그였는데, 분해하면 C2/C3/C7이 함수 경계로 강제되어
그 실수 클래스가 구조적으로 사라진다는 사용자 지적이 결정적. 다만 M6 착수 전
실제로 짜보며 확정해도 늦지 않다는 판단은 유지.
Co-authored-by: qwreey <me@qwreey.moe>
사용자 질문("혹시 쪼갠다면 어떻게 쪼갤것 같아?")에 대한 답을 6절로 추가.
핵심은 쪼개야 하는 게 호출부에 보이는 함수가 아니라 재귀 자체라는 것 —
(B)가 요구하는 "부기는 bottom-up, 물리는 top-down"을 하려면 재귀가 두 번
돌아야 하고, 그게 "쪼갠다"의 실체다. attachSlot이라는 이름이 정당하다는
판단에 동의하므로 공개 진입점은 이름/시그니처/호출부 전부 그대로 두고
몸통만 비공개 재귀 헬퍼 둘(materializeSlotTree/mountSlotTree — 코퍼스가
이미 쓰는 unmountSlotTree/destroySlotTree의 ...SlotTree 접미사를 따름)로
나누는 안을 제시. attachSlot의 몸통은 두 줄이 된다.
setLength가 materializeSlotTree의 끝으로 가는 게 관건 — 자기 서브트리
부기가 다 끝난 뒤라 처음부터 최종값이고, 그러면서도 모든 Parent 대입보다
먼저다. 그래서 C6(최종값 등록)와 C7(부기가 물리보다 먼저)이 동시에 만족되고
배치 밖 재마운트의 부모 recompute가 2회→1회로 준다. C1~C7 전부 유지되는지
표로 재점검했고, 비용(_elements 순회 2회)과 안 고쳐지는 것(부모 등록과 자식
배치가 여전히 한 함수), 비대칭 하나(materialize의 거울상은 함수가 아니라
호출부 관용구)도 같이 적었다.
판단은 약한 추천 — 값이 틀려지는 문제가 아니라 "일반 계약을 세워놓고 자기가
예외"인 상태와 _mounted의 애매한 중간 시점(RC-3/RC-4의 출처)이 정리되는
것이 이득. M6 착수 시점에 실제로 짜보며 정해도 늦지 않다.
Co-authored-by: qwreey <me@qwreey.moe>
확인받은 것 반영:
- Owned 설치 플래그 확정 — "누가 요소를 만들었는가"는 사이클마다 달라지는 게
아니라 설치 시점 속성이라는 사용자 판단. Detach(사이클 판단)와 직교하는
축이므로 반환값 계열에 unowned 센티널을 더 만들지 않는다. destroySlotTree/
dispose도 이 플래그를 봐야 해서 클로저가 아닌 Slot 필드여야 함.
- props 순회를 일반화 for 한 번으로 정정 — flattened는 항상 평범한 Luau
테이블(__pairs/__ipairs를 갈아끼운 ud가 들어올 경로가 없음)이라 옛 근거
"다른 백엔드가 Lua 테이블이 아닌 자료구조로"는 inst엔 해당해도 flattened엔
해당하지 않았다. 계약(배열 먼저)은 그대로, 구현만 1회 순회로. 스파이크 01은
두 루프 버전이라 재작성 필요로 STATUS에 표시.
attachSlot 분해는 research/slot-attach-decomposition.md로 준비:
setLength를 flush 앞/뒤 어디에 둘지가 안 풀린 이유가 자리 선택이 아니라
"한 함수가 책임 일곱을 지고 있어서"라는 사용자 진단에 따라, 책임 목록과
순서 제약(전부 RC-1/RC-3/RC-4 등 실제로 밟은 버그가 출처)을 모으고 C6(길이
최종값은 flush 뒤에야 정해짐)와 C7(부기가 물리보다 먼저)이 단일 함수로는
동시 만족 불가능함을 보인 뒤 분해 후보 넷을 대조. 확정은 아무것도 안 함.
Co-authored-by: qwreey <me@qwreey.moe>
B절(설명 보강 재질문) 전부 확인됨. 확인 과정에서 나온 보강:
- B-1: (A) 분기는 "교체"라 stack-down이 아니고 retractFrom만 스택을 역순으로
푼다는 구분을 명시. "자기 아래는 이미 정리된 뒤" 보장도 retractFrom 한정이고,
(A)에서 아래가 살아 있는 게 깜빡임 없는 갈아끼우기의 근거.
- B-3: 고아 체인이 실제로 어떻게 생기는지 가상 위반 예시(MaybeWrapHandler) 추가.
- B-4: "Brand는 데이터 타입에 부작용 없이 런타임 명시 타이핑을 하기 위한 것"을
존재 이유로 명시하고, duck-typing 기각 근거를 정확성/안전성 둘로 분리.
C절 결정 반영:
- C-3: flatten의 정확한 형태 확정 — in-place 뮤테이션(클론 안 함),
ProcessedModifier로 소진, 인라인 우선이 `~= nil` 하나로 성립. 단 주신 코드의
반복 방향은 역순이어야 "나중 modifier가 우선"이 성립해서 그것만 정정(F-4-2).
- C-4: destroySlotTree의 명시적 releaseOwner 제거. 파괴된 걸 재사용하는 코드는
그 자체로 버그이므로 "비결정적으로 실패"보다 "항상 실패"가 낫다.
- C-6: recompute의 sourceList[i] == nil을 skip에서 즉시 error로 승격. 재추적
결과 도달 경로가 없으므로 관측되면 부기가 깨진 것.
- C-7: "부기가 물리 트리 조작보다 항상 먼저"를 일반 계약으로 승격. 빼기는 물리
먼저/넣기는 부기 먼저가 같은 원칙(좁은 쪽이 먼저)의 두 얼굴이라는 것과,
yield 금지 덕에 프레임 경계가 안 끼므로 진짜 근거는 "백엔드가 전제할 수
있게 하나로 고정"이라는 것까지.
followup F절에 남은 것: KeyGone 홀드 + Owned 설치 플래그 설계 제안(F-3),
단일 일반화 for 전환 여부/flatten 반복 방향/setLength 위치(F-4).
Co-authored-by: qwreey <me@qwreey.moe>
사용자 회신을 (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>
사용자 회신(pre-implementation-qa-round4-response.md) 중 판단이 명확한
항목을 base/에 반영. 굵직한 것만:
- LP-1: quad의 `Connected`는 "계산된 속성"이 아니라 그냥 RBXScriptConnection의
네이티브 필드다 — rbvm 프록시 사정을 잘못 옮겨온 서술이었음. 실제 판정은
"gcconn이 없음" / "있는데 Connected==false" 두 상태뿐.
- D-56: `bindLifetime`의 첫 인자가 Instance가 아닐 수 있다(Slot-in-Slot의
ownerKey) — 백엔드가 반드시 핸들링해야 하는 요구사항으로 신설. gcconn
트릭이 안 통하므로 세 번째 판정 분기가 필요하다는 것까지 명시.
- SL-75/D-60: 언마운트 시 `slot.Offset = nil`은 포탈을 깨뜨림(이미 구독
중인 다운스트림이 영구히 끊김) — stale하게 두는 게 맞고, 마운트 전 기본값도
nil이 아니라 0.
- SL-74: `SetAndDispose`는 `source:SetAndDispose(value)` 콜론 메서드로 확정.
`Apply` 오버라이딩은 Source→State 단방향 때문에 타입이 안 성립.
- E-11: leaf 바인딩된 Effect엔 `:Unsubscribe()`가 아예 안 먹는 것으로
Observer와 통일. 옛 "(3) 이후 leaf가 죽어도 중복 호출 안 됨"은 이중 바인딩
게이트상 성립할 수 없는 문장이라 삭제.
- AT-20: 생존 이름 최적화는 "부품이 늘어나서 안 하는" 게 아니라 값 비교가
필요해 **원리적으로 불가능**하다(State 계약상 `:Get()` 비교 금지).
- UI-8: `mapTweenValue` 로컬 헬퍼를 `Tween<T>:Map(fn)` 공개 메소드로 승격.
부수로 D-3(retract가 깊은 인덱스부터인 이유)/D-10(두 패스를 명시하는 진짜
이유는 순서를 못 믿어서가 아니라 이식성)/LH-8(자기 아래 vs 자기 위)을 코드
예시로 풀어 썼고, TW-16으로 HUMAN_TODO에 initValue 항목을 신설했다.
Co-authored-by: qwreey <me@qwreey.moe>
사용자 요청("모든 확정 부분에 있어서 예가 되어야하는 질문들을 계속")으로
base/ 26개 문서를 서브에이전트 없이 한 맥락에서 의존성 순서로 읽으며,
확정으로 적힌 주장을 전부 "예가 나와야 정상인 단정문"으로 뽑았다.
결론만이 아니라 근거까지 문항에 넣었는데, 코퍼스에 "결론은 그대로인데
근거가 뒤집힌" 사례가 여럿 있어서 결론만 물으면 그런 걸 못 잡기 때문.
⚠️로 열려 있다고 적힌 항목은 "확정이 맞나"가 아니라 "아직 열려 있다는
인식이 맞나"를 묻는 문항으로 따로 표시했다.
정정은 하나도 안 했다 — 사용자 지시대로 기록만 하고 회신 대기 상태.
Co-authored-by: qwreey <me@qwreey.moe>
session-summary.md에 오늘 세션 04~08 색인 항목이 통째로 빠져 있던 걸 발견해
신설. 더 크게는 CLAUDE.md/project-context.md/ROADMAP.md가 "구현 아직 시작
전"이라는 낡은 전제를 깔고 있었는데, 실측해보니 M0 스파이크 4개와 M1
스캐폴딩 대부분이 이미 완료돼 있어 전부 정정(ROADMAP 체크박스 갱신 포함,
wally.toml→pesde.toml 표기도 같이 정정). quad-roblox-types 백로그는
todos.md/ROADMAP.md M5에 짧은 포인터 보강.
Co-authored-by: qwreey <me@qwreey.moe>
quad-spring-roblox류 독립 게시 플러그인엔 정확 버전 일치가 과하다는 지적에
따라, 버전 패턴 매칭(글롭 "*"/캐럿 "N^")을 quad에 종속되지 않은 범용
워크스페이스 멤버 type-version-check로 분리하고 quad-types의 CheckedQuad를
CheckedQuad<T, Pattern>으로 확장. 새 Luau 함정 2건(type function의 outer
local 참조 불가, cross-package엔 export type function + 이중 꺾쇠 제네릭
인스턴스화 필요) 발견·문서화. 독립 저장소 분리는 HUMAN_TODO로 위임.
Co-authored-by: qwreey <me@qwreey.moe>
quad-roblox가 quad-base를 런타임 주입(QuadRoblox(Quad): QuadRoblox)으로만
받으면 pesde 의존 선언이 필요 없어 보이지만, 타입 참조용 require도
런타임에 실제 실행됨을 실측 확인 — dev-dependency로 두면 게시 후
소비자 환경에서 크래시함. 해법으로 구현 없는 타입 계약 전용 워크스페이스
패키지 quad-types 신설, quad-base/quad-roblox 모두 이것만 의존하도록
전환.
AddPlugin<Self,P>(self:Self,fn:(Self)->P):Self&P — 제네릭 self로 둬야
체이닝이 누적됨을 실측 확인(고정하면 이전 확장을 잃음), quad-base에
실제 mutate 기반 구현 반영.
CheckedQuad<T> 버전 체크는 배선하며 세 번 깨짐 — error() 대신
print+types.never, 함수 본문 로컬 별칭 대신 리턴 타입 표현식에 직접,
그리고 가장 중요하게 type function을 한 번이라도 거친 값(패스스루
포함)은 이후 AddPlugin 같은 제네릭 self 체이닝이 조용히 깨진다는 새
Luau 함정 발견 — typing-limits.md §6으로 승격. 최종 설계(검증 결과를
원본과 격리된 가상 필드로)만 AddPlugin과 완전히 호환.
부수로 quad-base 자신도 quad-types workspace 의존 때문에 CLI symlink
함정(지난 세션 발견)에 걸림 — 로컬 테스트용 symlink 실체화로 임시 우회.
Co-authored-by: qwreey <me@qwreey.moe>
사용자 결정: RunInit(함수 identity 추적)은 backend 설치 진입점
(QuadRoblox 등)에 재사용하지 않는다. 대신 bind-system-plan.md 3차
라운드가 이미 정해둔 _initializedBy 문자열 마커(같은 팩토리 재호출=
no-op, 다른 팩토리=에러)를 그대로 별도 메커니즘으로 유지 — "멱등 실행"과
"유일 슬롯 점유"는 의미가 달라 억지로 합치면 RunInit의 단순함만 깨짐.
실제 InitRoblox 구현 예시 의사코드 추가(M5 실착수 시 RobloxFactory.luau
참고용).
Co-authored-by: qwreey <me@qwreey.moe>
New()의 멱등 Init 가드를 파일마다 Relate+센티널을 두던 방식에서
module:RunInit(initFn) 하나로 통합 — 함수 자체를 릴레이션 키로 써서
"이 함수, 이 모듈에 실행했는가"를 (module, initFn) -> boolean? 하나로
표현(사용자 제안). Debug/init.luau는 가드 없이 순수 뮤테이션만 하도록
단순화. smoke.init.luau로 재호출 무시/인스턴스별 독립/함수별 독립 3개
시나리오 검증(luau/luau-analyze/selene 클린).
darklua process를 실제로 돌려 @self/@game은 안 건드리고 커스텀 .luaurc
alias(@pkg)만 script.Parent류로 치환한다는 걸 확인 — project-setup-plan.md의
darklua 기각 근거를 "지금은 커스텀 alias를 안 쓰니 불필요, 나중에 도입하면
그때 필요해짐"으로 정밀화.
⚠️ 미결: RunInit을 backend 설치 진입점(QuadRoblox(Quad))에도 재사용할지
— 함수 identity 추적으로는 "다른 팩토리 재호출은 에러" 계약을 못 만족.
module-lifecycle-plan.md에 반영, M2/M5 착수 전 확인 필요.
Co-authored-by: qwreey <me@qwreey.moe>
Word30210/roblox-project-example(initreq/에 클론)를 참고해 두 가지 채택:
(1) rokit.toml → mise.toml — mise install이 pesde/rojo/luau-lsp/selene을
GitHub attestation+SLSA provenance 검증까지 거쳐 설치하는 걸 이 샌드박스
에서 직접 확인(rokit은 끝내 검증 불가), 이 환경 자체가 이미 mise로
luau를 관리 중이라 더 자연스러움. (2) selene 린터 — 참고 레포의
selene.toml을 패키지별로 채택, 단 CWD 상대 config 탐색 함정을 발견
(루트 단일 설정으로 두면 다른 디렉토리에서 실행 시 조용히 Lua 5.1
std로 폴백해 Luau 타입 문법 전체가 파싱 에러로 잘못 보임) — 참고 레포
그대로 패키지별 selene.toml + 패키지 안에서 실행하는 걸로 확정. 도입
즉시 smoke.mock.luau의 assert 메시지 누락 3건을 잡아 수정.
darklua의 convert_require 변환은 검토 후 기각 — 사용자 판단: Roblox
엔진 자체도 이미 같은 require-by-string 의미론(@self/@game)을 지원해
변환 계층이 불필요.
Co-authored-by: qwreey <me@qwreey.moe>
luau-lsp 1.69.0을 pesde/rojo와 같은 방식으로 직접 설치해
--flag:LuauSolverV2=true/false로 spike 08을 대조(옛 솔버 에러 3건 vs
새 솔버 1건) — HUMAN_TODO 6번이 사람에게 넘겨뒀던 "실제 에디터에서
확인"을 CLI 분석 모드로 대신 검증. quad/.vscode/settings.json에
enableNewSolver:true 반영, rokit.toml에 luau-lsp 핀 추가. 부수로
typing-limits.md 1번의 핵심 주장(0 진단으로 조용히 새는 것)이 Luau
0.734에서도 그대로 재현됨을 별도로 재확인(정정 불필요).
Co-authored-by: qwreey <me@qwreey.moe>
13은 타입(A)/런타임(B) 두 섹션이 한 파일에 있었는데 A의 더미 스텁이
런타임 실행 시 크래시를 내 B가 전혀 검증되지 못하고 있었음(STATUS.md
지적 사항). 13은 타입 전용으로 남기고(PostRef<T>도 Ref<T>를 만족하는지
추가), 런타임 절반은 신규 22로 분리 — isPreRef/isPostRef가 서로 배타적
형제이고 Leaf 핸들러 흉내가 Ref/PreRef/PostRef 셋을 정확히 갈라내는지
확인. 둘 다 done/으로.
Co-authored-by: qwreey <me@qwreey.moe>
pesde처럼 rojo도 /code/.local/bin에 직접 설치(7.7.0, rokit.toml 핀과
일치). rojo sourcemap/build가 quad-roblox/roblox_packages의 심볼릭
링크(quad-base로의 workspace 의존성)를 실제 파일까지 투명하게 따라감을
확인 — 이전 세션이 찾은 "luau CLI가 symlink를 안 따라간다"는 문제는
standalone CLI 전용이고 Rojo/Studio 배포 경로엔 영향 없음이 확정됨.
덤으로 luau-lsp가 rojo를 감지해 자동으로 sourcemap을 watch하는 것도
확인 — wally가 안고 있던 에디터 타입 링킹 단절 문제가 이 구성에서
재현 안 됨.
Co-authored-by: qwreey <me@qwreey.moe>
05는 "emit은 항상 전파, 재계산만 :Get() 캐시로 dedup" 현행 모델로
재작성해 rewrite-required/ -> done/ 이동. 21은 todos.md 00번이 요구하던
"Store 미선언 키 타입 에러" 확인 신규 스파이크 — ProcessStoreType 결과
타입이 미선언 키 접근을 정확히 TypeError로 거부함을 확인, store-plan.md의
"아마" 표시를 해소. STATUS.md/README.md/todos.md 텍스트도 같이 갱신.
Co-authored-by: qwreey <me@qwreey.moe>
의미론은 (A) emit-gate로 확정(value-hold 안은 laziness와 상충해 철회),
제어 핸들은 개별 Ref 아웃파라미터 + 전체 팩토리 브로드캐스트(weak
레지스트리)로 수렴, Time/MaxTime은 number|State<number> 허용(스케줄
시점에만 폴링), 이름은 Debounce/Throttle 유지로 확정. 전부 닫히면서
quad-base에 새 코어 메커니즘을 안 더하는 순수 슈가로 재평가됨(Blocker의
gated state + Ref + 주입 op 2개 위에 전부 얹힘) — 우선순위 서술도 갱신.
Co-authored-by: qwreey <me@qwreey.moe>
`:List` reconcile의 비파괴 반환 sentinel 이름을 사용자와 후보 검토(Bench/
Stash/Hold/Detach 등) 끝에 Detach로 확정 — 이미 있는 Extract(호출자 직접
호출, 명령형 소유권 이관)와 동사가 겹쳐도 "화면에서만 떼고 관리 주체는
reconcile"이라는 의미가 자연스럽게 구분됨. 공개 표면 위치도 같이 확정 —
Slot이 함수라 Slot.Detach로 못 붙이므로, None sentinel의 선례(공개 표면은
패키지 최상위 export, 정의는 관련 로직 옆)를 그대로 따름.
base/slot-plan.md 전량 반영(가칭 표기 제거, 이름/배치 두 결정 불릿 신설),
question.md/todos.md/ROADMAP.md/archive/question-resolved.md/README.md
인덱스 갱신, session/2026-08-19-02-*.md로 논의 원문 남김. 키 소멸 시 홀드
중이던 요소 처분 문제는 이름과 무관한 별개 항목으로 여전히 미결.
quad-doc-auditor 1라운드(무발견) 거쳐 doc-check.py ERROR 0 확인 후 커밋.
Co-authored-by: qwreey <me@qwreey.moe>
2026-08-19 세션이 점검해보니 2026-08-18(커밋 10개, QA 1~2라운드 포함)에
session/ 파일이 1개뿐이었고 2026-08-19도 이 점검 전까지 0개였음 — 설계
결정이 오간 세션이 원문을 안 남기는 일이 실제로 반복됐다는 뜻. 사용자
판단: 이미 지난 공백은 재구성하지 말고(대화 원문 없이 지어내면 그 자체가
허위 기록) 인정하고 넘어가되, 앞으로 반복 안 되도록 규율을 명문화.
Co-authored-by: qwreey <me@qwreey.moe>
InitRoblox(Module) backend 주입 패턴을 quad-base 자기 내부(Dispatch 등)에도
대칭 적용 — 각 서브시스템이 InitXxx(module)로 module을 뮤테이션. 서브시스템
간 호출 순서 문제는 각 InitXxx 파일 톱레벨에 Relate() 하나를 두고 module을
weak key 삼아 인스턴스별 완료 여부를 기록해 require처럼 멱등하게 만들어
해소(relate-plan.md 체크리스트에도 용례 추가). module-lifecycle-plan.md에
"New()의 내부 구성" 절 신설, architecture.md/dispatch-core-plan.md/
ROADMAP.md(M1 체크리스트)에서 상호 참조.
핸드오버 감사 루프 4라운드(무발견 1회로 수렴) — 라운드 1~2는 절 인용
사각지대·상호참조 누락·SetWeak/SetStrong 일관성을 잡았고, 라운드 3~4는 그
수정 자체가 남긴 커밋 개수/날짜 오기, 원문 인용 파라프레이즈 등을 추가로
잡음. 부수로 session/ 기록 공백(2026-08-18/19 다수 커밋에 원문 누락)을
발견해 이번 세션분만 session/2026-08-19-01-*.md로 남김 — 과거 공백 처리는
사용자 확인 대기.
Co-authored-by: qwreey <me@qwreey.moe>
RC-1 Blocker 게이팅이 실제로 attachSlot에 반영된 걸 손으로 트레이싱하다
:List 최초 population이 이중 처리되는 결함(RC-3/RC-4)과 recompute가
의존하는 bk.N의 수명주기가 문서에 없던 갭을 발견. 필자의 최초 분석
오류(bk.N을 그때그때 실제 개수로 두면 크래시가 되돌아온다는 판단)를
사용자가 직접 정정 — Blocker 게이팅은 bk.N이 아니라 blocker:IsOn()만
보므로 무관함이 밝혀졌고, RC-3/RC-4도 slot._mounted를 activateList
호출 뒤로 미루는 사용자 설계로 해결됨. ROADMAP M2가 M3의 Blocker.luau에
의존하게 된 마일스톤 순서 불일치도 발견해 각주로 반영.
quad-doc-auditor 감사 루프 4라운드(1~3라운드 총 9건 발견·수정, 4라운드
무발견으로 종료) 거쳐 doc-check.py ERROR 0 확인 후 커밋.
Co-authored-by: qwreey <me@qwreey.moe>
직전 커밋에서 New()를 전부 Quad()로 치환했던 게 사용자 의도를 잘못 읽은
것이었음 — 사용자가 직접 바로잡음. 실제 설계: Quad(require의 반환값)는
이미 만들어진 기본 싱글톤 인스턴스이고, 그 안의 New 필드를 명시적으로
호출해야만 별도의 새 Quad 네임스페이스가 생긴다. "그냥 Quad()를 부르면
매번 새 인스턴스"였다면, 컴포넌트를 여러 모듈로 쪼갠 앱에서 각자
인스턴스를 "얻는" 것 자체가 "새로 만들기"가 되어 서로 다른 인스턴스가
생기는 사고로 이어졌을 것.
architecture.md/dispatch-core-plan.md/bind-system-plan.md/
module-lifecycle-plan.md의 New() 서술을 되돌리고, qa-request round1의
A-3 절에 후속 정정 문단을 추가했다. quad-doc-auditor로 두 라운드 감사해
새 발견 0건까지 수렴시켰다.
Co-authored-by: qwreey <me@qwreey.moe>
`:List` reconcile과 dispatch-core-plan.md의 recompute를 실제로 손으로
실행해보다 RC-1(배열 위치가 순차 등록되는 동안 아직 안 채워진 자리를
읽어 산술 에러가 나는 크래시, 정적 자식 2개짜리 Frame도 재현)을 찾았다.
같은 세션 후속 대화에서 사용자가 제시한 Blocker 재사용 게이팅 설계로
해결 — owner별 전용 Blocker가 배치 등록 동안 recompute를 막고,
setOffsetSource는 등록 즉시 앞선 형제 합을 직접 계산하며, attachSlot의
호출 순서(setOffsetSource→실체화→setLength→물리 마운트)도 바로잡고
코루틴 yield 금지 불변식을 명문화했다. Blocker에는 IsOn()/
OffWithoutEmit()이 새로 생겼다.
/code-review high가 이 diff에서 10건을 더 찾아 반영 — D-7 재역전과의
정합성, filter/nil 재역전 반영 누락, PreRef 가드의 typeof(k) 누락,
New()→Quad() 리네임 전파 누락, M6/M8 마일스톤 오기 등. quad-doc-auditor
감사 루프도 여러 라운드 돌려 새 발견 0건까지 수렴시켰다.
Co-authored-by: qwreey <me@qwreey.moe>
사용자 지침 — "시간이 걸려도 상관 없으니, 차라리 병렬 에이전트를 덜 써줘.
그냥 한 턴에 하나씩만 사용하고, 0 이 나올때 까지 턴 수를 늘리는게
나아보임. 토큰을 너무 많이 소비해서 다른 작업을 못 하고, 세션 한도에
닿더라고."
- quad-doc-auditor는 한 턴에 하나만 호출(병렬 금지) — 2026-08-16의
"최소 2개, 변경이 많으면 3~4개" 지침을 대체. 커버리지는 병렬 폭이
아니라 턴 수로 얻는다. 근거 실측: 전 코퍼스 한 패스가 서브에이전트
토큰 21만/툴 호출 82회, 4개 병렬이면 한 번에 80만대.
- 프롬프트로 범위를 diff로 좁힐 것을 신설(바뀐 파일 + 그걸 인용하는 곳).
전 코퍼스 스윕은 오래 안 돌렸을 때만. 라운드마다 각도를 바꿔 병렬로
얻던 폭을 턴으로 대체.
- 종료 조건을 "무발견 2연속"에서 "무발견 1회"로 완화(2연속은 병렬 다패스
전제였음). 비용 때문에 중간에 멈출 때도 몇 라운드에서 왜 멈췄는지
반드시 보고.
- /code-review는 감사자를 대체하지 않는다를 실측과 함께 명문화 — 이번
핸드오버에서 감사자 1패스(1건) 뒤 /code-review high가 10건을 더 잡았고
전부 유효했다(보는 축이 다름). 사용자만 호출 가능하므로 큰 변경 커밋
전엔 돌릴지 물어볼 것.
Co-authored-by: qwreey <me@qwreey.moe>