발견 17건(H-107~H-123)의 사용자 결정을 base/ 전체에 반영. 7라운드 확정 중
뒤집힌 건 없고, 고친 건 전부 7라운드가 base/에 내려앉을 때 생긴 누락·충돌
(하루 차로 확정된 결정들이 서로를 못 본 자리)이다. 결정의 소스는
qa-request/pre-implementation-handtrace-round8-followup.md.
계약 변경 넷:
- Ref 콜백이 fn(value, ref) — 2번째가 곧 출처 Epoch. Effect가 Update(from)에
넘길 유일한 통로였다(k(value)뿐이면 Update(nil) 크래시, 실측 재현).
- Observer fn이 세 자리 fn(targetState, self, emitFrom) + observer._state
강참조. 옛 2-인자는 "self는 리시버" 계약과 정면 충돌해 무인자
state:Observer()의 내부 콜백이 즉사했다.
- WeakSubscribe도 .Subscribed를 세운다. 안 그러면 Effect의 State dep 전량이
조용히 침묵. 해제는 "건 경로로 푼다"(양방향 fail-fast).
- 예약 키 진단이 CheckReservedKeys<keyof<T>> — T를 통째로 넘기는 배선은
실사용 T에서 아예 안 돈다(Source<T>가 *error-type*을 품어 유효한 Store
전부에 스퓨리어스 에러).
사용자가 문항의 전제를 두 번 정정: Ref 콜백과 Observer 콜백은 이질적이라
애초에 통합 대상이 아니었고(Observer엔 자기 epoch가 없다), H-118은 소유권
문제가 아니라 gate-plan 5번의 문장이 틀린 것이었다(🟡→🟢).
커밋 전 검증 — 감사 11라운드(44건, 0건으로 수렴) + /code-review high
7라운드(42건) = 86건. 감사가 0으로 수렴한 직후 code-review가 42건을 냈고,
그중 하나가 H-101의 "새 필드를 안 만든다"를 역전시켰다: getOffsetAt의
부수효과가 splice의 되감기 신호를 지우는 경로가 실재해, 부기 필드를
offsetCacheValidUpTo(캐시)와 offsetSetUpTo(:Set 완료) 둘로 분리했다.
"Set을 해줬느냐"와 "캐시가 유효하냐"를 한 값이 쥔 게 원인이었다.
그 외: splice 무효화 i-1, 명시 recompute 호출부 전부 재진입 게이트,
recompute 되감기 클램프, Store defaults isSource 검증, isModifier 가드를
Source 생성자로, 훅 슈가 nil 가드, pesde.lock 커밋 확정, :Single 3-인자.
2026-08-25 session/ 원문 공백은 2026-08-19 선례대로 재구성 없이 기록만.
doc-check ERROR 0 / WARN 기준선 유지.
Claude-Session: https://claude.ai/code/session_01F9zgJ4c4kDitAoQMm9qxKn
Co-authored-by: qwreey <me@qwreey.moe>
`qa-request/pre-implementation-handtrace-round7.md`의 `H-55`~`H-106`을
사용자와 대화형으로 처리하고 `base/`에 전량 반영했다. 결정의 소스는
`-followup.md`(개수·개별 항목은 여기서 세지 않는다).
## 처분
확정 39 / 무효·소멸 4(`H-73`~`H-76`) / 기각 1(`H-77`) / 범위 축소 2 /
다른 항목으로 흡수 6.
**부수로 `question.md` 최우선 절이 비었다** — 중간 State GC는 `_hold`
불변식(하류 → 상류 강함)으로, 동적 키 표면 위치는 `store:Of<<T>>(name)`
하나로 닫혔다. **M2 착수를 막는 항목이 없다.**
## 구조가 바뀐 것
- `Ref`가 `Epoch`를 만족(`.Revision` + `EpochBrand`) — 포탈 캐치업 비대칭과
같은 `Ref` 중복 dep이 같이 닫힘
- `Weak*` 등록 표면 신설(`Ref:WeakCallback` / `Observer:WeakSubscribe`) —
Weak 쪽이 프리미티브고 강한 쪽이 "GC 킵"을 얹은 것
- `Effect`: dep 등록이 생성자 한 곳으로, 강한 주인은 `_deps` 하나,
억제는 사적 `Blocker`, `bindLifetime`은 핸들 하나에만 적용.
`:Rerun()` 정의 신설(재진입은 지연 재실행), `_installed` 신설
- 전파 루프 의사코드 확정 — 구독자 집합의 원소는 Observer **값**이고
**자식 State 노드는 `canExecute`를 안 탄다**(그대로 짜면 파생 State
아래가 전부 침묵했다)
- `rawInvalid` → `cacheTargetCount`/`cacheCurrCount` 카운터 쌍
- `recompute` 재진입 차단 + `invalidAfter` 되감기, `gatedRecompute`가
인덱스를 캡처하지 않고 토큰으로 조회
- `emit(commit) -> boolean`, `EpochMap:Peek` — 정책이 흡수 집합을
버리지도 읽지도 못하던 것을 닫음
- error 계약(`level` 이분, 메시지는 영어)과 예외 계약(`pcall`로 안 감쌈)을
`architecture.md`에 신설
## Store — 같은 날 재설계하고 철회했다
`H-75`/`H-76`으로 `WrapStore`/`ProcessStoreType`이 폐기되자 그 자리를
"`store.key`는 값, `store:Of(k)`가 프리미티브"로 채웠다가 **같은 날
철회**했다(`archive/store-value-field-redesign-withdrawn.md`). 살아남은 건
**명시적 초기화** 하나다. 최종형은 타입 인자에 `Source<T>`를 직접 쓰고
`store.key`는 평범한 레코드 필드이며 **타입 함수를 안 쓴다**.
철회 이유 중 하나가 원칙으로 승격됐다 — **"타입 함수는 타입이 못 잡는
문제를 에러로 띄우는 정도 이상으로 가지 않는다"**(`typing-limits.md` §0).
`index<>`/`keyof<>`도 Luau가 predefine한 타입 함수라 같은 함정을 갖는다.
## 툴체인
**`luau` CLI가 심볼릭 링크를 못 탄다**(디렉토리·파일 둘 다)는 것이 최소
재현으로 밝혀졌다 — pesde 워크스페이스 링크가 전부 심볼릭이라 스모크 2개가
안 돌았고 `luau-analyze`는 **조용히 통과**했다("거짓 클린"). `scripts/relink.sh`
+ `scripts/test.sh` 신설, 이제 스모크 셋 전부 PASS.
## 검증
`/code-review high` 2회(12건 + 14건)와 감사 8패스(12/6/6/4/13/3/6/0건)를
각도를 바꿔가며 돌렸고 전부 반영했다 — 마지막 패스가 무발견으로 수렴.
각 패스의 각도와 대표 발견은 `-followup.md`의 검증 절이 소스.
`doc-check.py` ERROR 0.
Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_012oLwATeQdq9TCFdENPutFG
앞 세션이 컨텍스트 피로로 미뤄둔 승격(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
사용자 확정 둘로 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
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>
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>
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>
`.claude/pre-implementation-qa.md`(사용자가 base/ 확정 문서를 문항으로
재심사한 결과)를 실제 문서에 반영하고, 그 문서를 qa-request/로 옮기며
1라운드임을 파일명·제목에 명시(2라운드는 새 파일).
그대로 구현하면 반대로 돌던 것 2건:
- canBound의 판정 방향이 이름과 반대였음 → canBound(v) == not
isBoundAlive(v), 게이트는 전부 `if not canBound(v) then error(...)`.
canExecute와는 값이 같은 게 아니라 서로의 부정이고, 그게 오히려 이름
분리의 명분이 됨(옛 근거 "값이 항상 같다"는 폐기).
- gcconn/gchold 보관이 SetStrong으로 적혀 있었음 → SetWeak. 근거 문장까지
틀렸던 것이라 같이 교체(그대로 짰으면 두-Relate 상호 강참조 누수).
설계가 바뀐 것:
- Dispatch.drive의 None 스킵 분기 폐기 → NoneHandler는 재귀 전담,
NilHandler 신설(k=number and v==nil 말단이 setLength/setOffsetSource
등록). 깨진 전제는 "배열 파트의 None은 process를 안 탄다".
- Length/Offset 등록 책임이 "처음 매치한 Handler" → 말단 Handler.
- 이벤트 disconnect 센티널 false → None/nil.
- Ref 내부 구조를 .Callbacks 분리 + 평범한 .Value 필드로 단순화,
RefLeafHandler에 빠져 있던 type(k)=="number" 추가(leaf는 배열 전용).
- :List reconcile의 nil 리턴은 다시 파괴가 기본, 값 교체와 PopOnly(가칭)만
비파괴.
- base 소유 Fallback Handler 등록 주체를 백엔드 팩토리 → quad-base 자신으로
재역전(백엔드 미로드 시 안내 에러 경로가 안 돌았음).
- "이벤트 콜백 시그니처는 Luau가 검증 못 한다"가 거짓임이 사용자 반례로
확인 → onchange-plan.md의 파생 근거까지 교체(결론은 유지).
이름/표면: DI → D(Declarative) 확정 및 전수 반영, New 커링 + D는 전량
코드 생성, Attribute.Merged/Overridden 둘 다 제공, Quad.debug 신설,
store "key" 문자열 커링 기각(→ store:GetDynamic).
판단이 갈리던 4건(PopOnly 채택 / D-7 재역전 / NoneHandler·NilHandler 역할
분담 / 동적 키 경로)은 사용자에게 물어 확정.
커밋 전 검증: quad-doc-auditor 1패스가 1건, 사용자가 돌린
`/code-review high`가 10건을 더 잡아 전부 반영(ROADMAP이 SL-3 역전을 안
따라오던 것, 설계 갭 2건은 새 열린 질문으로 등록). doc-check.py ERROR 0.
Co-authored-by: qwreey <me@qwreey.moe>
사용자 제기 — doc-check.py가 정규식으로 결정론적 판정을 하는데 표기가 흔들리면
문제가 커지니, 정규식을 늘리기보다 "예상 가능 범위"를 컨벤션으로 좁히는 게 싸지
않냐. 실측해보니 날짜 표기는 이미 100% 균일해서 고칠 게 없었고(강제 장치 없이),
드리프트는 절 인용 쪽이었다 — WARN 86건 중 78건(91%)이 절 참조 불일치.
핵심은 그 78건이 코퍼스가 지저분한 게 아니라 **검사기가 못 읽는 것**이었다는
점이다. 이 코퍼스는 `**볼드**` 줄을 하위 절로 쓰는데 headings()가 `#`만 봤다.
## 동작 변화 (문서 정정으로만 보이지만 게이트가 바뀐다)
- 절 참조 불일치가 **WARN → ERROR**. 이제 절 인용 오류가 커밋을 막는다.
- 절 인식이 `#` 헤딩 + `**볼드**` 절로 확장. 단 볼드는 **빈 줄 다음이나
리스트 항목 머리**만 인정 — 문단이 줄바꿈되며 우연히 줄머리에 걸린 강조를
절로 오인하던 걸 커밋 전 감사가 잡아 조였다.
- 인용 길이 상한 60→160자. 60자를 넘으면 매칭 자체가 안 걸려 검사에서
**조용히** 빠져나갔음(위양성보다 나쁜 구멍).
- 비교를 공백 무시로(줄바꿈 인용 대응), 선두 장식·상태/날짜 태그 정규화,
`initreq/` 대상 인용은 절 검사 면제(읽기 전용 외부 원본).
## 규약
`conventions.md`에 "문서 표기 규약" 절 신설 — 절 인용 규약(의역 금지, 헤딩은
부분문자열/볼드는 앞부분일치, 태그로 닫히는 볼드 캐비엇, blockquote 함정),
세션은 산문 서수 말고 파일 ID로 지칭. 날짜 마커 라벨 어휘 닫기는 사용자 판단
으로 기각(기계 검사 대상이 아니라 읽는 쪽 판단 재료).
## 결과
절 참조 불일치 78 → 0. 36건은 검사기 수정으로 사라졌고(애초에 위양성), 42건은
인용을 실제 절 제목으로 손으로 고쳤다. 마지막 15건은 서브에이전트 3개에 병렬
위임해 추적 — **설계 서술이 유실된 건은 0건**, 대부분 코드 주석·본문 산문·
주제명처럼 애초에 절이 아닌 걸 절로 인용해온 것이었다.
부수로 드러나 같이 고친 것: onchange-plan이 9차 분할 때 일부러 안 옮긴 절을
잘못된 파일로 가리키던 것, brand-plan이 이미 이행된 정정을 "정정 대상"이라
부르던 것, ROADMAP의 blockquote가 인용 줄바꿈 때문에 깨져 있던 것,
pre-implementation-audit의 해소된 항목이 "아직 안 고침" 절에 남아 있던 것
(사용자 결정으로 "이미 고침"으로 이동).
커밋 전 감사 4라운드(에이전트 8개)를 돌렸고, 발견 추이는 2→2→1→0이다.
매 라운드 발견이 "직전 라운드 수정이 만든 새 결함"이었던 게 특징 — 규약을
세우는 커밋이 그 규약의 첫 위반자가 된다는 걸 실측으로 확인했다. 상세는
.claude/session/2026-08-16-03-doc-check-section-convention.md.
부수: __pycache__를 .gitignore에 추가하고 추적 해제(32e9db0에 실수로 딸려
들어가 있었음). quad-doc-auditor에 작업 트리를 바꾸는 git 명령 금지 규약 추가
— 감사자가 git stash를 걸어 메인 세션 스테이지가 반복적으로 풀렸다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011zk7XHkSfiBfdPLZUQHdZf
## CLAUDE.md 분할
1537줄이라 (a) 사람이 검토 불가, (b) 공식 권장치(파일당 200줄) 7.7배
초과로 지침 준수도 자체가 저하, (c) 긴 파일 편집 시 에이전트 실수 증가.
CLAUDE.md (39줄, 진입점)
├─ @.claude/conventions.md 언어/모델 관례 + 작업 방식
├─ @.claude/project-context.md 프로젝트 설명 + 문서 구조
└─ @.claude/todos.md 지금 할 일
.claude/session-summary.md ← import 안 함(의도적, 온디맨드)
세션 히스토리 1231줄(전체의 80%)은 그 문서 스스로 "항상 읽을 필요 없음,
base/가 소스"라고 명시해온 색인이라 @import에서 뺐음. 내용 유실 없음
(1537→1647줄, 추가 헤더만큼 증가).
주의: @import는 컨텍스트를 줄이지 않음(전부 로드됨). 분할이 사는 건
사람 검토성 + 편집 정확도 + 파일 단위 자동생성 가능성.
CLAUDE.md 계열의 블록 HTML 주석은 주입 전 제거되므로 지시는 본문에 쓸 것.
## 워크플로 가짜 초록불 수정
첫 실측에서 감사 에이전트 6개 전원 실패했는데 converged:true가 나왔음
(전멸하면 fresh가 비어 "깨끗한 라운드"와 구분 불가). 감사 도구 최악의
실패 모드라 (1) 전멸이면 throw, (2) 반영 에이전트 실패 시 그 발견을
seen에서 빼 다음 라운드가 재시도하도록 수정.
## 부수
- 분할로 깨진 상호참조 20여 곳 정정(병렬 에이전트 3개).
ref-plan.md:541의 사전 존재 오류(→ pre-implementation-audit 1-5)도 정정.
- doc-check.py: 새 파일 4개를 OURS에 등록(안 하면 깨진 참조가 WARN으로만
잡힘), is_history()로 session-summary.md를 archive/와 같이 면제.
- doc-include-plan.md: 목적지가 통째로 생성되는 파일이 되면서 양방향
마커 설계의 절반(목적지 마커)이 불필요해져 단방향 생성으로 단순화.
doc-check.py ERROR 0 유지.
미해결: session-summary.md 자동생성 미착수(91개 세션 파일 마커 삽입 필요),
orphan 인용 3건(modifier-plan.md:536, v1-compat-plan.md:50,
pre-implementation-audit.md:434 — 분할 이전부터 존재).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HvEKn9f67kkx2nGLG8PsP
type-recursive-issue-try-callback/ 신설(spikes 35개) — type function/
메타테이블/제네릭/이중 꺾쇠 명시 인스턴스화까지 전방위로 재시도했으나
quad의 state:Compute(fn) 단일 호출 모양을 유지한 채로는 여전히 안 됨,
근본 원인은 재귀 특유가 아니라 "제네릭 콜백 인자엔 컨텍스트 타입
전파가 안 됨"이라는 더 일반적 Luau 한계임을 확인. typing-limits.md
§1/§7에 반영, 원칙 자체는 안 바뀜.
이어진 /code-review 2회전이 이 작업과 luau-test/16 복구 세션
전반에서 정합성 문제 다수 발견 — 스파이크 결과 표 stale(16번 실패로
잔존), 폴더 개수 off-by-one 여러 건, type-recursion-issue/REPORT.md의
실제 오류(self가 제네릭이면 자유 함수로 빼도 여전히 실패한다는 사실이
"통과"로 잘못 서술됨), 옛 솔버 캐비엇 누락, 체크리스트 항목 간 모순,
이 세션 자체의 히스토리 누락 등 — 전부 수정. doc-check.py ERROR 0 유지.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HvEKn9f67kkx2nGLG8PsP
typing-limits.md §1에 검증된 선언 스타일 규약(③) 추가 — 재귀 메소드를
인라인 대신 이름 붙은 함수 + typeof로 선언하면 LHS 명시 없이도
다운스트림이 안전해짐(체이닝 50단·타입 변경·중첩 self 호출 확인).
setmetatable 확장은 quad 계약에서 솔버 버그를 만나 채택 안 함.
luau-test/16(type function으로 Store<T> 필드 합성)을 복구해 done/으로
이동 — 원인은 설계 문제가 아니라 API 버전 드리프트, §5를 검증 완료로
승격.
전체 실측: audit/type-recursive-issue-with-typeof/(REPORT.md + spikes
15개). code-review 지적 4건(불건전 판정 오류, 음성 대조군 개수, 체이닝
깊이 주장의 재현 스파이크 누락, store-plan.md stale 참조) 전부 반영.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
전체 .claude/ 코퍼스를 doc-check.py + 6개 병렬 서브에이전트로 감사해
stale 세션 번호, 잘못된 인용, 자기모순 배너 등 15개 파일의 실제 사실
오류를 발견·수정. 이어서 question.md 0-W(같은 Ref 객체가 두 자리에
동시에 놓이는 문제)를 선택지 (a)로 확정 — RefLeafHandler가 새 Relate
없이 bindLifetime/unbindLifetime을 재사용해 이중 배치를 방지.
이 과정에서 bindLifetime의 이중 바인딩 가드(bound 문맥)와 State emit
전파 게이팅(execute 문맥)이 서로 다른 질문인데 canExecute 하나로
뭉쳐 있던 걸 발견 — canBound를 별도 진입점으로 재도입(판정 로직은
비공개 헬퍼 하나를 공유, 코드 중복 없음). Tag/Attribute의 미지원
백엔드 처리 모델도 여러 라운드 논의 끝에 확정: TagHandler 등은
quad-base가 스스로 등록하고, addTag/removeTag/setAttribute만 백엔드
팩토리가 채우는 타입 계약 — 안 채운 슬롯은 명시적으로 에러내는 스텁.
/code-review high가 추가로 3건(gcconn-trick-verification.md 배너,
README.md 인덱스 4곳, luau-test/STATUS.md의 재작성 지침) 발견해 정정.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXqrGrh83Sh61C3tUCMdmY
- CLAUDE.md 아홉 번째 세션 항목 제목이 "미보장으로 역전"으로 남아 본문
(철회·보장 유지)과 정면 모순 → 정정
- CLAUDE.md 여섯 번째 세션 항목이 "OnRendered는 지금 의도적으로 구현 안 함"을
현재형 확정으로 서술 중 → 아홉 번째 세션 역전 포인터 추가
- research/documentation-content-map.md:
(1) "PreRef는 retract 체인에 아예 안 올라감" — 2026-08-14 두 번째 세션의
ProcessedPreRef 정정이 미반영된 옛 근거였음(이번 변경과 무관한 기존
stale) → 정정, PostRef도 동일하다고 명시
(2) hook/pre-hook 프레이밍에 PostRef(post-hook) 3항 확장 + 순서 보장
왕복을 에세이 소재로 메모
(3) 서베이가 2026-08-06 파일 구성 기준이라 bind-system-plan.md 포인터가
전부 분할 전 것 → 파일 상단에 읽는 법 배너
- luau-test/README.md: 13번 재작성 시 PostRef까지 커버할 것
- base/lifecycle-hooks-plan.md: 시그니처의 `Instance`는 가독성용 표기이고
실제로는 백엔드 타입을 모르는 자리라는 주석(quad-base 소속과의 정합)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
워크트리(worktree-debounce-throttle-plan)에서 네 라운드로 다듬은 결과를
메인의 3단계 분할 구조에 맞춰 필요한 변경만 이식.
## 신설: research/debounce-throttle-plan.md
- Blocker가 이미 쓰는 게이티드 노드의 릴리스 트리거만 타이머로 바꾼 것.
공개 Blocker API엔 "상류 신호 도착" 통지가 없어 그 위엔 못 얹음 →
M3에서 게이트를 공용 Gate로 뺄 것.
- 두 도구의 차이는 "신호가 창 타이머를 리셋하는가" 한 비트뿐.
공개 생성자 2개 + 내부 구현 1개(초안이 옮겨온 lodash식 maxWait 공식엔
trailing 통과 직후 이중 발화 버그가 있었음).
- quad-base + 주입 op 2개: setTimeout(func, delay) -> Timeout /
clearTimeout. Roblox는 task.delay/task.cancel로 배선(인자 순서 반대).
os.clock()은 Luau 표준 라이브러리라 주입 대상 아님(diff 전용).
Timeout = { __type_timeout: true, _native: any }.
## 역전: emit은 자기 invalid 상태와 무관하게 항상 전파된다
source-state-plan.md의 "이미 invalid였다면 그 아래로 더 전파하지 않는다"가
확정된 Observer 계약(fn이 :Get()을 안 불러도 됨)과 정면 충돌 — 액면대로면
:Get() 안 하는 Observer는 한 번 울고 영구 침묵. architecture.md가 같은
다이아몬드 문제를 pull-recompute로 설명하는 것과도 어긋나 있었음.
정정 모델: invalid는 캐시 낡음 표시일 뿐, 중복 재계산은 pull-recompute+
캐시가 막고 중복 통지는 안 접음(접으려면 Blocker 같은 명시적 게이트).
- source-state-plan.md: 전파 규칙 재작성, "다이아몬드 의존성은 무엇이
푸는가" 절 신설, Observer 절 상호 참조. 플래튼 기각/:With 빌더 기각
근거를 캐시 공유로 재작성(두 결론 유지, 근거 강도는 상승)
- architecture.md, blocker-plan.md(전파를 지연시키는 유일한 요소로 위치
명문화), comparison-fusion-vide.md, framework-comparison-findings.md
- ROADMAP M0 체크리스트: 확인할 것이 정반대가 됨
- luau-test 05 → rewrite-required/(옛 모델을 통과 상태로 검증 중이었음),
STATUS.md 개수 동기화(rewrite 6→7, done 14→13)
- audit: 05 행 정정 + "12개 전원 통과"를 액면대로 읽지 말라는 경고
- archive/invalidate-dedup-propagation-reversed.md 신설
doc-check: ERROR 0 / WARN 84(작업 전 85).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
세 건을 한 커밋에 처리:
1. ui-shorthand-plan.md — Tween 지원 확정. 숏핸드가 자식 프로퍼티를 직접
대입하지 않고 Dispatch.process(child, prop, ..., 1)로 위임하면 Tween이
공짜로 따라옴(해석 코드는 PropertyHandler 하나에만 남음). "process 중
inst를 바꾸는 건 키를 바꾸는 것과 같은 층위라 UB 아님"을
dispatch-core-plan.md에 일반 규칙으로 명문화. wrap을 Tween<T>.Value에만
적용되도록 들어올리는 헬퍼가 새로 필요한 유일한 부품. ROADMAP M10에
통째로 빠져 있던 UI 숏핸드 항목도 보강.
2. existing-instance-bind — 기각, research/ → archive/. 사유: Length/Offset
등 quad가 만든 트리를 전제한 부기를 바깥에서 밀고 당기는 버그 표면이
치명적으로 넓어짐. "열려 있음"을 전제로 쓰인 본문 7곳도 같이 정정
(architecture.md의 "아직 미정" 절은 유일 항목이었어서 절 자체를 갱신).
3. bind-system-plan.md 3단계 분할 + store-semantics.md 흡수(순수 이동):
- base/store-plan.md 신설 — Store = 이름 붙은 Source 모음
- base/source-state-plan.md 신설 — 반응형 코어(Source⊇State, 전파 모델,
:With/:Compute/:Apply/previous, Observer, 구독·생명주기 게이트)
- bind-system-plan.md 1238→203줄(인스턴스 생성·이벤트 네이밍 + 색인)
- store-semantics.md 삭제
참조 40여 곳 스윕. doc-check.py ERROR 0, WARN 101→84.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
canExecute(inst,value) 2-인자 시그니처를 폐기하고 canExecute(value)로 정정.
2-인자는 증상이었고 원인은 2026-08-08 세션이 .Subscribed에 "leaf 바인딩
생존"이라는 두 번째 의미를 겹쳐 얹은 것 — .Subscribed는 전역 :Subscribe()
전용 필드이고 bindLifetime과 무관함. bindLifetime이 바인딩 시점에 inst의
gcconn 참조를 value 쪽 Relate로 복사해두면 생존을 value 하나로 물을 수 있음.
- canBound(handle) 폐기 → canExecute(value)로 통합(이중 바인딩 게이트 겸함)
- unbindLifetime도 1-인자로 축소 — 호출부가 _mountedInst를 되짚을 필요 없어져
"홀더가 갈아치워지면 해제가 빗나가는" 잠재 버그 클래스 소멸(slot-plan 5곳)
- gcconn/gchold를 lazy 생성에서 Instance 생성 시점으로 전환, 클로저가 inst까지
캡처 — Instance userdata 포인터 동일성은 inst-키 Relate 전체의 전제였음
(relate-plan에 "전제" 절 + "안전히 유지되면 항상 SetWeak" 일반 규칙 신설)
- canExecute의 실제 호출부를 State 전파 루프로 명시(구독자 weak + 발화마다
게이팅) — 이게 코드로 한 번도 안 적힌 게 오류가 여섯 세션 살아남은 이유
역전 원문/오염 경로/교훈은 archive/canexecute-inst-arg-reversed.md.
luau-test/10은 폐기된 모델을 검증 중이라 rewrite-required/로 이동.
부수: 3~4차 세션이 남겨둔 CLAUDE.md 세션 히스토리 항목과 4차 세션 로그
파일도 미커밋 상태여서 같이 실림. doc-check ERROR 1건(CLAUDE.md:1156 →
session/2026-08-14-03-lifecycle-hooks-plan.md)은 그 파일이 디스크 어디에도
없어서 남음 — 3차 세션 쪽에서 채워야 함.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
69466ab/32e9db0 이후 5개 영역 병렬 에이전트 감사 + 직접 트레이싱으로
프로젝트 전체를 다시 훑음. 핵심 설계(하강 diff 재디스패치, Tag
참조카운트, nameClaims, Slot-in-Slot 해체)는 재트레이싱해도 버그
없음 — 전부 같은 클래스의 문서 참조 잔여 오류였음: `bind-system-plan.md`
2단계 분할(14차 세션) 이후에도 그 파일을 계속 가리키는 느슨한 산문
인용(따옴표 절 제목이 아니라 "~가 말하는"/"~ 참고" 식이라 doc-check.py
정규식이 못 잡는 형태).
- `ref-plan.md`/`ui-shorthand-plan.md`(2곳)/`tween-plan.md`/
`slot-plan.md`(3곳) — `dispatch-core-plan.md`로 정정.
- `module-lifecycle-plan.md:21` — 32e9db0가 같은 파일 114/128행은
고쳤지만 21행만 놓쳤던 것.
- `ROADMAP.md`(2곳)/`luau-test/README.md` — `None`/`recompute` 절
참조 정정.
- `luau-test/done/02`/`03` 스파이크 주석 — 02는 `ref-plan.md`(9차
세션에 이미 옮겨간 절이었음, bind-system-plan.md였던 적 없음),
03은 `dispatch-core-plan.md`로 정정.
- `audit/luau-test-first-run-2026-08-13.md` — "no-op 점유 마커"를
여전히 유효한 수정 근거처럼 서술하던 부분에 정정 각주 추가(점유
체크 자체는 하강 diff 재설계로 폐지됨, `chains:SetStrong` 순서
버그만 지금도 유효).
doc-check.py ERROR 0 유지, WARN 101 그대로(전부 판단 필요한 기존
느슨한 인용, 이번 라운드가 새로 만든 건 없음). 5개 에이전트 전원
"설계 결함 0건" 보고로 수렴 판단.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
다른 에이전트 감사와 사용자 트레이싱으로 69466ab에서 나온 지적 반영.
핵심 알고리즘(하강 diff/인덱스 체인)엔 버그 없음이 확인됐고, 의사코드와
문서 정합성에서 나온 것들:
1. `nameClaims`가 Relate의 3-인자 계약 위반(`GetStrong(inst)` /
`SetStrong(inst, claims)`) → `(inst, name[, key])`로 정정. 같은 커밋의
`tagNameMap`은 이미 3-인자였어서 대조로 드러남.
2. TagHandler가 생존 이름의 홀더를 비웠다가 곧이은 process에서 `addTag`를
헛되이 재호출 — 문서 서술("addTag 자체가 안 불림")과 정반대였음.
생존 이름은 홀더를 유지하도록 정정하고, `addTag`도 `removeTag`처럼
배치 호출로 통일(`{string}` 시그니처 도입 근거와 맞춤).
3. 그룹 process의 부분 실패 경로(순회 중 충돌 error) 문서화 — 피해가
인스턴스 수명으로 한정되고 재현이 시끄럽게 반복됨을 트레이싱으로
확인, 롤백 장치는 안 넣고 question.md 3번에 열어둠.
doc-check.py: REF 정규식이 줄 단위라 파일명과 절 제목이 줄바꿈에 걸친
인용을 통째로 놓치고 있었음(이번 2단계 분할뿐 아니라 9차 세션 1단계
분할 stale까지 숨어 있던 원인). 파일 전체 스캔 + 개행 허용으로 고치고,
새로 드러난 stale 참조 30여 곳을 dispatch-core/event-plan/ref-plan/
typing-limits로 정정. 오탐 없음(참조–인용 스팬 3줄 미만 전수 확인).
부수: 세션 로그에 리뷰 라운드 절 추가(커밋 메시지 줄수 오기 2291→1213
정정 포함), CLAUDE.md의 스파이크 이동 서술에 `19` 누락 보강,
luau-test/README 상단 요약표를 STATUS.md와 동기화.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KckSawrsJSJmDBcSojJPxZ
`question.md` 최우선 두 건을 한 패스로 닫고, 배너를 달고 있던 7개 문서 +
인덱스 레이어 전체를 갱신했다.
0-Z (Attribute 이름 소유권):
- 그룹은 비공개 `GetKey`로 이름마다 자기 전용 키를 써서 위임 → 교차 오염
구조적 제거. 이름 소유권은 `AttributeKeyHandler`의 이름 claim이 판정
(`nameClaims` Relate, 다른 키가 같은 이름을 노리면 즉시 error).
- 권고안 (a)(그룹 안 claimant Relate)는 그룹↔직접 쓰기를 못 잡아 기각 —
두 경로가 만나는 말단 핸들러에서 공개 키는 같은 객체라 소유자 구분 불가.
0-A (재디스패치 = 하강 diff):
- 래핑 핸들러의 선행 `retractFrom` 폐기, `Dispatch.process`가 슬롯의
`handler`를 먼저 비교(같으면 클로저에 새 값 전달 후 재process, 다르면
그 자리부터 전량 철거).
- 귀결: `retractFrom`이 3-인자로 축소(힌트를 외부에서 만들어 넣을 자리
소멸), `isX(hintValue)` 가드 규칙 폐지, 깊은 체인 힌트 유실 캐비엇 삭제,
Dispatch의 점유 체크 폐지.
- 9차 세션이 미뤄둔 2단계 분할을 같이 수행 — 디스패치 코어를
`base/dispatch-core-plan.md`로 분리하며 재작성(bind-system-plan은
2263→1219줄). 옛 모델은 archive/dispatch-hintvalue-model-reversed.md.
패키지 재배치 (사용자 제기):
- Tag/Attribute의 부기 알고리즘 전체를 quad-base로, 백엔드는
`addTag`/`removeTag(inst,{string})`/`setAttribute(inst,name,v)` 3개 op만
주입(웹 className/data-* 대응). 엔진 고유 타입 패밀리만 백엔드.
- `HANDLER_PRIORITY_FALLBACK` 신설 — base 제공 핸들러의 밴드, 백엔드가
평범한 우선순위로 덮어쓰면 언제나 이김.
부수: 스파이크 04/19가 옛 모델을 검증 중이라 rewrite-required로 이동,
question.md 최우선 칸 비움, 새 소소 항목 2건 등록(Merged 이름 중복,
`hintValue` 이름 재검토). doc-check ERROR 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KckSawrsJSJmDBcSojJPxZ
44개 스파이크로 question.md 0-Y를 재실측한 결과, 여섯 번째 세션의
"콜백이 raw 값을 받으면 완전 클린" 판정이 틀렸음이 드러남 — 그건 진단
0건만 확인한 것이었고, luau-analyze --annotate로 열어보니 반환 타입이
Unifiable<Error>로 조용히 새고 있었음(틀린 대입도 안 잡힘).
진짜 원인은 콜백 계약이 아니라 Compute가 State<U>(자기 이름을 다른 타입
인자로 감싼 타입)를 반환한다는 것 자체 — RFC relax-recursive-type-restriction이
Promise<T>.andThen으로 예시 든 바로 그 패턴. 사용자 확정: quad가 타입을
비틀 일이 아니라 상위 Luau의 현 한계이고, RFC/이슈 수혜를 받을 때 해결될
일이라 당장 할 수 있는 바 없음.
- base/typing-limits.md 신설 — 흩어져 있던 타입 한계 5건 통합, 대전제
"Luau 한계를 우회하려 타입/API를 비틀지 않는다", 새 API 설계 체크리스트
- audit/type-recursion-issue/ 신설 — REPORT.md + spikes 44개(audit 폴더에
스크립트를 같이 둔 첫 예외, 판정 재현에 개별 실행이 필요해서)
- 0-Y 해소 전파: question.md(최우선 2건→1건) / archive / base 5개 /
research 2개 / 인덱스 4개 / luau-test(08을 done/으로, review-required 비움)
- audit/luau-test-first-run-2026-08-13.md: 판정이 뒤집힌 당사자라 배너뿐
아니라 본문 표·문단·결론까지 전수 수정
- HUMAN_TODO 6번 신설: luau-lsp 기본이 옛 솔버라 CLI와 진단이 다름
교훈: luau-analyze 진단 0건은 타입 해소를 뜻하지 않음 — 타입 스파이크는
--annotate로 실제 추론 타입을 확인하고 음성 대조군을 같이 둘 것.
doc-check.py ERROR 0 유지(WARN 59건, 변경 전과 동일).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KckSawrsJSJmDBcSojJPxZ
9차 세션의 구조 변경(luau-test 재편/bind-system-plan 분할/question.md
트림)이 남긴 반영 누락을 6개 병렬 에이전트로 찾아 즉시 수정:
- luau-test 재편 후 깨진 flat 경로 참조 9곳을 파일명+실제 폴더로 정정
- bind-system-plan.md 분할 후 자기참조/외부참조 깨짐 8곳 정정
- ref-plan.md에 0-Z 배너가 안 옮겨와 옛 재디스패치 모델을 무배너로
서술 중이던 것 발견 — 배너 추가, 반영 대상 6개→7개로 갱신
- "8차 세션"으로 잘못 표기된 9차 세션 작업 17곳(git 커밋 타임스탬프로
교차검증) 정정
- question.md 트림 중 빠진 열린 질문(State<State<T>> 평탄화) 복원,
트림 후 깨진 참조 2곳 정정
- ROADMAP.md M0 섹션에 0-Y/0-Z 게이트 표시 누락 보강
doc-check.py ERROR 0 유지. 새로 연 설계 질문 없음 — 전부 기존 서술
정합성 문제.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6hzeUi5QdLPEk69B6cXFa
사용자 질문: "이런 깊은 감사를 재발 방지하는 방법이 있나. 중대 변경에 대해
그 사항을 아는 세션이 stale 없게 핸드오버해야 하는데 그런 게 없어서 쌓임."
진단: 7·8차 감사에서 찾은 55건이 거의 전부 **변경한 세션이 그 자리에서
안 한 일**이었고, 그중 상당수는 grep으로 잡히는 기계적 종류였음. 그래서
규율 문서만 쓰지 않고 **실제 검사기**를 만듦.
.claude/tools/doc-check.py — 라이브 문서 61개 대상:
[ERROR] 깨진 파일 참조(우리 문서 명명 관례에 맞는 것만; 아직 없는 소스
트리 .luau와 외부 문서명은 오탐이라 WARN으로 분리)
[WARN] 절 참조 불일치 — `foo.md` "절 제목"이 그 파일에 없음
[ERROR] README 색인 누락
[WARN] 날짜 없는 시한부 주장("아직 안 돌려봄"/"열린 질문 없음" 등)
[WARN] 미반영 ⚠️ 배너 단 파일 vs 반영 목록 일치
현재 ERROR 0 / WARN 58(대부분 절 제목 의역 인용, 판단 대상).
**검사기가 즉시 실효를 증명함** — 같은 세션에 bind-system-plan.md를 쪼개며
"이중 바인딩 금지" 절 참조 4곳을 ref-plan.md로 잘못 옮긴 걸(실제로는
Observer/Effect 절이라 이동 대상이 아니었음) 이 스크립트가 잡아내 되돌림.
CLAUDE.md "작업 방식"에 중대 변경 핸드오버 체크리스트 6단계 명문화:
doc-check 실행 → 부정당한 *본문 문장* 전수 grep(배너만 달고 본문 방치가
최다 실패) → 뒤집힌 원문은 archive로 → 개수/상태는 소스 하나만 → 시한부
주장엔 날짜 → 인덱스 레이어 3개 동시 갱신.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6hzeUi5QdLPEk69B6cXFa
사용자 지적: 2989줄이라 검토가 불가능하고 한 곳의 실수가 미치는 범위가
너무 큼. 특히 "이벤트 연결은 다른 base 문서가 되어야 할 듯"이라고 직접 지목.
**1단계 (완료, 2989 → 2263줄)** — 인바운드 참조가 적고 자기 완결적인 것부터:
- base/ref-plan.md (509줄) — Ref/PreRef 전체
- base/event-plan.md (103줄) — 이벤트 self 미전달, false로 disconnect
- base/brand-plan.md (137줄) — Brand 런타임 nominal 판별
내용은 **순수 이동, 결정은 하나도 안 바뀜**. 원위치엔 포인터 스텁을 남기고,
이 절들을 지목하던 인바운드 참조 20곳을 새 파일로 갱신(잔존 0 확인).
README 색인에 3개 행 추가.
**2단계는 의도적으로 미룸** — 남은 디스패치 코어(~1000줄)/반응형 코어
(~950줄)는 0-Z 확정 시 어차피 전면 재작성 대상이라, 재작성하는 그 패스에서
파일을 가르는 게 총 변경량과 실수 위험이 모두 작음(인바운드 ~37곳을 한 번만
고침). dispatch-redispatch-diff-plan.md 6절에 그 지시를 명시했고,
bind-system-plan.md 헤더에도 분할 현황표를 남김.
부수: 이벤트 *네이밍* 관례는 인스턴스 생성과 한 절에 섞여 있어 쪼개면
양쪽이 반토막 나므로 원위치 유지(event-plan.md 헤더에 명시).
luau-test/README의 "에이전트가 직접 실행 못 함" stale 서술도 정정.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6hzeUi5QdLPEk69B6cXFa
사용자 지적: 파일 21개가 평평하게 쌓여 있어 사람이 "지금 내가 볼 게 뭔지"
못 고름. 이전 세션에 README/STATUS 문서만 바뀌고 폴더 구조는 그대로였음.
- review-required/ (1) — 설계가 걸려 **사람 결정 필요**: 08
- rewrite-required/ (3) — 스파이크 코드가 깨짐(설계 문제 아님): 13/15/16
- not-run/ (1+헬퍼) — Studio 전용이라 이 환경에서 못 돌림: 10 + gc-trigger-helper
- done/ (15) — 통과 or 판정 끝
**폴더를 옮기는 게 곧 상태 갱신**이라는 규칙을 README/STATUS 양쪽에 명시.
STATUS.md는 폴더 구조를 그대로 반영해 재작성(맨 위 요약표 → 사람이 볼
review-required부터).
폴더가 상태라 파일이 옮겨 다니므로, 다른 문서의 참조를 경로 대신
파일명 기준으로 정규화(ROADMAP/pre-implementation-audit/bind-system-plan
6곳) — 안 그러면 파일을 옮길 때마다 링크가 깨져 stale이 또 쌓임.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6hzeUi5QdLPEk69B6cXFa
## luau-test/STATUS.md 신설 (사용자 요청)
"사람이 보기엔 뭐를 바로 봐야하는지 눈에 안 띄어서" — 파일별 상태를
🔴 사람 결정 필요 / 🟠 스파이크 깨짐 / ⚪ 미실행 / ✅ 통과로 분류하고
봐야 할 것을 위에 배치. 역할 분담도 명시: STATUS.md가 상태의 소스,
README.md는 각 파일의 검증 의도·배경, audit/는 실행 결과 상세.
## 코퍼스 전반 감사 (에이전트 둘, 보고받은 것 전부 직접 사실 확인 후 수정)
가장 위험: ROADMAP.md가 최우선 게이트(question.md 0-Z, 하강 diff 재설계)를
전혀 안 짚고 있었음. base 4개 문서엔 ⚠️ 배너를 달아뒀는데 정작 구현 순서의
소스인 ROADMAP엔 없어 M2/M4/M10 담당자가 옛 모델로 구현할 위험이 실재.
→ 세 마일스톤 전부에 배너/포인터 추가.
두 번째: slot-plan.md 앞부분이 뒤집힌 결정을 여전히 "확정"으로 자칭.
"폐기, 옮기지 않음, portal은 오버엔지니어링"과 "열린 질문" 절에 역전 표시가
없어 앞에서부터 읽는 구현자가 구 모델로 짤 위험 → 🔄 역전 배너.
더 심각하게 reconcile 의사코드가 여전히 rawRemove(파괴)를 부르고 있었음
(같은 문서가 "[반영 완료]"라 태그해둔 것과 정면 모순) → rawUnmount 신설.
filter/toggle 근거 절도 "제거=파괴" 전제라 캐비엇 추가(결론은 유효).
그 외:
- ROADMAP M11 Tween이 이미 확정된 넷을 미결로 둠(override 정책/옵션 값
모양/Animate 시그니처/initValue) + research/tween-plan.md 죽은 링크 4곳
- bind-system-plan.md:328, architecture.md:213의 "4종 계약" 잔여
(isHandlable(k,v) 구식 시그니처 포함)
- store-semantics.md의 "State는 가칭" stale — 2026-08-12에 최종 확정됨
- luau-test/README.md의 04/19 판정 기준이 재작성된 파일을 못 따라감.
특히 19 C섹션 기준이 정상 동작(nested error)을 실패로 오판하게 돼 있었음
- documentation-content-map.md의 4종 계약 / Attribute<T> 옛 이름
- modifier-plan.md의 "정정이 원문에 소급 안 됨" 패턴
- attribute-plan.md "이름 소유권"이 0-Z 미결인데 "최종"이라 적힌 것 캐비엇
- relate-plan.md의 kSlotMap 역할 서술 부정확, 상호참조 방향 오류 2곳
판단해서 안 고친 것: effect-plan.md의 "cleanup" 14곳 — Handler retract
(내부 배관)와 달리 Effect(fn)에서 사용자가 작성한 fn이 반환하는 React식
콜백이라 다른 층위. 매 감사마다 재지적되므로 lifecycle-pattern.md에
"확인 완료, 의도된 별개 개념"으로 못박음.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
luau/luau-analyze 바이너리가 사용 가능해져, 2026-08-09에 스파이크를 만들기
시작한 이래 처음으로 실제로 돌림(CLAUDE.md가 "M0 착수 전 남은 유일한
게이트"로 꼽아온 항목). 전문: .claude/audit/luau-test-first-run-2026-08-13.md
## 설계 검증 결과 — 런타임은 전부 성립
- 04가 직전 커밋의 감사에서 찾은 버그를 음성 대조군으로 재현: chains:SetStrong을
process 뒤에 두면 체인 깊이가 3 대신 1로 무너지고, 죽은 store가 나중에 UI를
덮어씀(STALE). 감사→수정 사이클이 실측으로 닫힘.
- 07은 보강해야 실제 검증이 됐음. 3번 섹션이 sanity check만 하고 있었고 헤더가
내세운 연쇄 GC 주장은 미검증이었음. 파일이 스스로 적어둔 "weak table 엔트리를
셀 표준 API가 없다"는 전제도 틀렸음(outer가 __mode="k"라 GC 후 pairs에서
사라짐) — _countEntries + weak-value canary로 4번 섹션 신설, GC-native
아키텍처의 핵심 전제(연쇄 GC)가 실측 확정됨.
- 18이 relate-plan.md의 상호 순환 경고를 실증 — 추측이 아니라 실제로 GC 안 됨.
- 01/02/03/05/06/20도 전부 통과.
## 타입 — 진짜 설계 이슈 1건 (question.md 0-Y 신설)
:Compute(fn)의 lazy 핸들 계약이 Luau 양방향 추론과 충돌.
`state:Compute(function(s) return s:Get() * 2 end)`가 타입 에러를 냄.
최소 재현으로 원인 확정: read/self 표기 조정으로는 안 풀리고, 콜백이 raw 값을
받으면 완전 클린. Effect/Observer/Animate/Operator 등 같은 계약을 공유하는
API 전부에 걸림 — M0 착수 전 결정 필요.
## 문서 결함 발견 — modifier-plan.md
"데이터를 테이블에 직접 두고"가 "self 최상위 리터럴 키"로 읽힐 여지가 있었는데,
그렇게 하면 __index가 rawget 성공 시 안 불려 같은 필드 재호출이 죽음. 그 재호출
패턴이 문서 3·4절의 대표 용례라 실사용에서 즉시 터지는 경로였음 — 경고 문단 추가.
## 스파이크 수정
- 17: self 최상위 키 저장 → 내부 저장소 구조로 재작성(크래시 해소)
- 11: 브랜드 판별 크래시로 "다른 Modifier" 케이스가 엉뚱하게 통과하던 것 수정
- 19: B/C를 폐기 설계(rawNew+owners, 3분기 claimOwner)에서 현행 설계로 재작성,
음성 대조군으로 옛 로직이 Slot{a,a}/Frame{slot,slot}을 통과시킴을 재현
- 07: 위 참고
남은 것은 13/15/16의 스파이크 격리·API 재확인(설계 문제 아님).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
c33ae04(인덱스 기반 Dispatch 재설계) 전체를 직접 정독. 방향은 옳으나 새로 쓴
의사코드가 손 트레이싱을 안 거친 채 커밋됐음이 드러나 실제 버그 4건 발견·수정:
1. Dispatch.process가 chains:SetStrong을 h.process 뒤에 둬서 최초 마운트에서
하위 위임 retractor가 통째로 유실(재귀가 자기 테이블을 만들었다 바깥이
덮어씀 — Slot이면 서브트리 중복 마운트로 직결). SetStrong hoist + 재귀 중
hole 방지용 no-op 점유 마커 추가.
2. Attribute 그룹이 process에서 retractFrom을 선행 호출해 인덱스 1이 무조건
비워지는 바람에 점유 체크(=소유권 충돌 감지)가 전혀 작동 안 함. 철거를
반환 클로저로 이동.
3. SlotHandler가 claim 실패에도 파괴적 클로저를 반환 — nested는 엄격
claimOwner, top-level은 claimOwnerAt(inst,k)로 분리. rawRemove의
releaseOwner 누락, destroySlotTree의 GC 타이밍 의존 오류도 수정.
4. Ref retractor가 spurious 재발행에서도 relate를 지워 dedup 무력화.
사용자 설계 결정:
- State<Slot> 교체 = 파괴가 아니라 언마운트(state<Frame>와 동일). 포탈이
별도 기능이 아니라 이 결정의 귀결이 됨. 해제는 setOffsetSource(None) →
setLength(0) 순서 고정(반대면 죽는 중인 서브트리 Source에 헛된 Set이
날아감). dispose(value) 신설 — 트리가 아직 요구하면 파괴 거부하고 error.
- 재디스패치를 "하강 diff"로 재설계 — 래핑 핸들러의 retractFrom 선행 호출을
폐기하고 Dispatch.process가 핸들러를 먼저 비교. 힌트가 None/래퍼로 오염돼
깜빡임 방지가 조용히 꺼지는 결함이 계기. 아직 research/에만 있고 base
미반영(Attribute 이름 소유권 하나가 남음) — base 4개 문서에 경고 배너.
전파 누락 정리: ROADMAP.md가 2026-08-08 이전 모델로 남아있던 것, base 내
"3종 vs 4종 계약" 모순 6개 문서, luau-test/04가 없어진 가드를 검증하던 것
(04는 인덱스 모델 + 버그 1 재현 음성 대조군으로 전면 재작성).
재발 방지: bind-system-plan.md "Handler 작성 체크리스트"(7항목),
relate-plan.md "언제 Relate를 쓰고 언제 쓰면 안 되는가" 신설.
다음 세션 최우선: question.md 0-Z(Attribute 이름 소유권) — 사용자가 직접
스케치하며 심층 분석 예정.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Haskell Monad/Applicative 비교 리서치 중 사용자가 retractUnder의 꼬리부터
cutoff 로직을 재검토하다 제기한 의심을 pseudocode 손 트레이싱으로 확인 —
store가 emit하는 값 자체가 또 State/Source면 같은 (inst,k)에 같은 핸들러가
중복 push되어 안쪽 구독이 등록 직후 스스로 끊기는 실제 체인 파손 버그였음.
Dispatch.process에 중복 핸들러 즉시 error 가드를 추가하고, 같은 시나리오를
이미 다루고 있었지만 no-op retract 스텁 때문에 증상을 못 잡던 luau-test/04의
사각지대도 재작성. operator-sugar-plan.md에 Alternative(nil 대체값) 후보 신설.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
User confirmed the two Roblox-engine-dependent assumptions behind the
gcconn trick (ClassName signal never fires, Connection.Connected flips
synchronously on Destroy) via a Studio script; record what's verified vs
still open in a new audit/ folder and document the GC-trigger technique
used. While re-auditing the corpus, found relate-plan.md's ephemeron/
mutual-cycle claim was never actually run in Luau, and CLAUDE.md's
open-questions list still listed State as unresolved after it was
finalized in session 20 — add spike 18 and fix the stale entry. A
background sweep synced .claude/README.md's summary rows against
sessions 8-21 and added spikes 19/20 for newer ownership/refcount
mechanisms lacking coverage.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGWwZ8khc3Zq7DAnd4Uw6a
사용자가 우선순위 동률/매치실패(1-3), provider 미주입(1-4), store.key
type function 타이핑(1-10), Modifier __index+table.clone 트릭(1-11)에
대해 구체적 결정을 제시 - 전부 base/ 문서에 반영해 우선순위1 11개 전원
해소. 핸드오버 점검 중 1-10/1-11이 설계 레벨로만 확인됐고 실제 Luau
스파이크 파일이 없었던 갭을 발견해 16/17 신규 추가, ROADMAP.md M2/M3/M7
체크리스트 누락분도 보강.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVG74qV2nQVykhvMRQW2UC
Adds Attribute(store1, store2, ...) as a Tag-shaped array-part value
object that projects one or more Stores' named Source fields onto
native Roblox attributes in one binding, with .Merged for combining
heterogeneous Stores. Rejected: a bare [Attribute] = Store{...} hash
slot (only one slot per instance, can't merge heterogeneous stores)
and making Attribute a Store subtype (would let Store<T>'s T be a
Store again, colliding with the existing "handler-layer values can't
live in Source" rule). Verified the value-slot aggregation in .Merged
doesn't reopen the earlier "layered Store" rejection (archive/
context-rejected.md) since it's an explicit one-time author-time
composition, not an implicit read-time parent-chain fallback.
Renamed the existing single-key constructor Attribute<<T>>(name) to
AttributeKey<<T>>(name) to remove the name collision with the new
group primitive (provisional per existing OnChange/OnChangeKey
precedent; final bikeshed still queued in question.md).
Follow-up: AttributeKey(name) now memoizes through a name-keyed weak
table, guaranteeing AttributeKey(a) == AttributeKey(a) while any
strong reference (e.g. a live Dispatch chain entry) keeps it alive.
This let the group Attribute handler drop its originally-planned
self-contained SetAttribute/subscription logic and instead recurse
into the existing single-key AttributeKey dispatch path per field,
reusing its None/retract/store-bind handling instead of duplicating
it — the group handler's own state shrinks to just a prior-name-set
diff for deciding which keys to retractUnder. The same memoization
was applied to OnChangeKey for the same reason (pure name-to-key
mapping, no other varying state), confirmed safe even once
State<function> callbacks are involved.
Reflected across base/attribute-plan.md, base/onchange-plan.md,
base/architecture.md (source tree + Brand isX list), ROADMAP.md M2/M10,
question.md, README.md, and the luau-test type-narrowing spike.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Expose :Compute(fn, ...)'s trailing deps to fn as lazy State-handle
positional args (removes the closure-capture duplication/drift risk in
curried fn factories). Confirms self's lazy-handle sugar generalizes
here because trailing args are local to one call, unlike :With chains.
previous must sit right after self, before the deps pack
(fn(self, previous?, ...deps)) — not after it as first proposed:
Luau's "..." must trail the parameter list, so a fixed arg after a
generic type pack is very likely unrepresentable. Adds luau-test 15
with a negative control (old broken ordering) and positive control
(corrected ordering) to verify, plus the remaining open question
(whether heterogeneous dep types survive one generic pack).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BBBW9GakG8J3CFumTSJ8bw
base/ 확정 사항 중 아직 실제 Luau로 부딪혀본 적 없는 것(M0 스파이크
대상)을 사용자가 luau/luau-analyze/luau-lsp/Roblox Studio로 직접
돌려볼 수 있는 독립 실행 스크립트 14개 + README 색인으로 정리. 커밋
f198fd9의 정정사항(Ref 콜백/대기자 배열 소진을 None에서 nil로 되돌린
것 등)을 반영해 02번을 재작성했고, 타입 관련 실측이 필요한 항목
(Attribute 제네릭 DI 키 narrowing, Ref/PreRef 구조적 서브타입, Source/
Ref nilable-default 오버로드)을 새로 찾아 12~14번으로 추가함. 처음엔
git 자동 제외 폴더(luau-ignoreme/)에 만들었으나 커밋해서 레포에
남기기로 해 .claude/luau-test/로 이동, .claude/README.md에 색인 추가.
CLAUDE.md에 세션 요약 반영 — 아직 실행 결과는 미확인.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>