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>
6.9 KiB
2026-08-14 네 번째 세션 — ProcessedPreRef 신설로 Length/Offset 등록 갭 해소, PostRef 완전 대칭화
배경 — 사용자 질문에서 시작된 읽기 전용 조사
사용자가 "PreRef 처리로 인해 공백이 생기면 Dispatch의 setLength/
setOffsetSource가 안 터지는가"를 물으며 읽기 전용을 명시(다른
에이전트가 같은 레포 파일을 편집 중이었음). Explore 에이전트로
base/ref-plan.md(PreRef pre-pass)와 base/dispatch-core-plan.md
(Length/Offset)를 조사한 결과:
- 설계상으로는 "충돌 없음"으로 다뤄져 있었음 — PreRef pre-pass가 소진시킨
슬롯은
Dispatch.setLength(inst,i,0)/setOffsetSource(inst,i,None)으로 등록돼야 한다고dispatch-core-plan.md가 명시. - 그런데 누가 그 등록을 실제로 호출하는지가 어느 문서에도 없는 진짜
갭이었음. 소진 값이
None이었기 때문에, 정상 두 패스 스캔이 그 자리를Dispatch.process/핸들러 매칭 자체를 안 거치고 드라이버 루프 자신이if v == None then continue end로 직접 건너뜀 — 그런데 Length/Offset 등록 책임은 "이 위치를 처음 매치한 Handler"에게 있다고 못박혀 있었으니(dispatch-core-plan.md"Length/Offset" 절), 애초에 매치되는 Handler 자체가 없는 그 자리는 등록 주체가 없었음.
사용자 제안 1 — ProcessedPreRef 전용 센티널 + ProcessedPreRefHandler
사용자가 해법 제시: PreRef pre-pass 소진 값을 None이 아니라 전용
센티널 ProcessedPreRef(단일 {}, None과 같은 급의 유니크 키)로 바꾸고,
그 값을 매치하는 ProcessedPreRefHandler를 정상 우선순위 레지스트리에
등록 — 이 Handler가 setLength(0)/setOffsetSource(None)을 등록하고
no-op retract를 반환. 기존 "이미 있는 걸 재활용", "매치되는 Handler가
곧 등록자"라는 원칙을 그대로 타서 특수 취급이 없어짐. PreRefHandler
(동적 경로 가드, 정상 스캔에 원본 isPreRef(v)가 걸리면 error)는 그대로
유지.
반영
base/ref-plan.md"PreRef" 절 —flattened[i] = None→= ProcessedPreRef로 교체,ProcessedPreRefHandlerpseudocode 신설. 파생 서술 3곳도 같이 정정:- "동적 경로 가드" Handler 설명의 "정상 두 패스 스캔에 다시 노출 안 됨" → "이 가드 Handler에는 다시 노출 안 됨(스캔 자체엔 노출됨)"으로 정확화.
- "PreRef는 취소 개념이 없다"의 근거를 "체인에 안 올라감"에서
"체인엔 올라가지만 retract가 하드코딩된 no-op(fire의 부작용을
되돌릴 방법이 없어서)"로 정정 —
ProcessedPreRefHandler신설로 "체인에 안 올라간다"는 옛 전제 자체가 깨졌기 때문. - sparse-table 회피 근거의 "None으로 소진" 표현을 "실재하는 센티널로 소진"으로 일반화.
base/dispatch-core-plan.md—None센티널 절과 Length/Offset 절 두 곳에서 "PreRef pre-pass 소진 슬롯도 None 목록에 포함"이라던 서술을 제거하고,ProcessedPreRefHandler가 그 등록을 전담한다고 명시.sourceList가None을 쓰는 이유 문단의 PreRef 인용도 갱신.ROADMAP.mdM0/M8 체크리스트 —None서술을 정정하고ProcessedPreRefHandler구현 항목 신설.luau-test/done/02-none-sentinel-vs-nil-holes.luau— 주석에 센티널 개명 사실만 추가(스크립트가 검증하는 성질 자체는 "실재하는 non-nil 값이면 순서/#t가 안 깨진다"는 일반 성질이라 어느 센티널을 쓰든 결과는 유효, 재작성/재실행 불필요).
사용자 제안 2 — PostRef도 같이, 그런데 더 단순하게
사용자가 research/lifecycle-hooks-plan.md의 백로그 PostRef 스케치도
같은 방식으로 갱신하자고 제안. 1차로 필자가 "PreRef는 소진 후
값이 매치 대상, PostRef는 fire가 뒤로 미뤄지니 소진 전 원본 값이
매치 대상이어야 한다"는 비대칭 설계를 초안으로 썼는데, 사용자가 더 나은
배선을 제시: PreRef pre-pass가 이미 배열 전체를 index 순서로 한 번
훑고 있으니, 같은 스윕에서 isPostRef(v)도 같이 잡아 즉시
ProcessedPostRef로 소진하고 그 인스턴스를 postRefList(그
Dispatch.drive 호출 하나에만 로컬인 평범한 배열)에 순서대로 적재해두면
된다. 그러면:
PreRef/PostRef가 소진 메커니즘 완전 대칭(둘 다 pre-pass에서 즉시Processed*로 소진, 둘 다 전담Processed*Handler가 Length/Offset 등록) — 유일한 차이는 "실제 콜백을 언제 부르는가"뿐.- 별도 후행 전체 재순회(두 번째
for i=1,N)가 필요 없어짐 — 두 패스가 끝난 뒤postRefList만 순서대로 소비하면 끝. - 1차 초안이 필요하다고 짚었던 "PostRef 전용 비대칭 규칙"(원본 값이
매치 대상,
PostRefHandler가 rawPostRef를 정상 스캔에서 잡아 등록) 자체가 안 생김.
research/lifecycle-hooks-plan.md의 ② 절(OnRendered/PostRef, 여전히
"의도적으로 지금 구현 안 함" 백로그 상태 그대로)을 이 설계로 갱신 — 스코프
논의((a)/(b)/(c) 선택지)도 "후행 스캔" 표현을 "postRefList 소비"로
정정.
검증
python3 .claude/tools/doc-check.py — 편집 전/후 모두 ERROR 0건,
WARN 101건(개수 동일, 기존 false-positive 그대로 — git stash로 대조
확인). 새로 도입한 문장이 만든 신규 WARN 없음.
커밋
사용자가 "지금 커밋해도 될듯"(워크트리 바깥 메인을 아무도 안 만짐)이라고
해서 커밋 e0ef7ce(docs(dispatch): PreRef 소진 센티널을 ProcessedPreRef로 교체, Length/Offset 등록 갭 해소)로 반영 — 로컬
main에만, push 안 함(SAFETY.md).
code-review 두 차례 시도, 결과 미도착
커밋 전에 사용자가 /code-review high를 두 번 돌림(첫 번째 실행 도중
"커밋은 바로 하지 마, code-review 돌릴게"라고 지시). 두 번 다 8개(→5개)
관점 파인더 중 마지막 하나가 안 끝난 상태로 세션 안에서 최종 결과가
도착하지 않았음 — 사용자가 두 번째 실행도 "안 온다"며 포기하고 커밋을
바로 지시. code-review 결과 자체는 이 세션에 반영되지 않음 — 나중에
결과가 도착하면 별도로 검토해 필요하면 후속 커밋으로 반영할 것.
반영 상태
새로 연 설계 질문 없음 — ProcessedPreRef/ProcessedPreRefHandler는
이미 확정된 PreRef 메커니즘의 정제(같은 결론을 특수 취급 없이 만족시키는
재배선)라 question.md에 올릴 항목 없음. PostRef/OnRendered는
여전히 백로그 상태 그대로(우선순위·"착수 여부 미정" 변경 없음) — 착수
시점에 이번에 정리된 대칭 설계를 그대로 가져다 쓰면 됨.