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>
112 lines
6.9 KiB
Markdown
112 lines
6.9 KiB
Markdown
# 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`로
|
|
교체, `ProcessedPreRefHandler` pseudocode 신설. 파생 서술 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.md` M0/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`가 raw `PostRef`를 정상 스캔에서 잡아
|
|
등록) 자체가 안 생김.
|
|
|
|
`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`는
|
|
여전히 백로그 상태 그대로(우선순위·"착수 여부 미정" 변경 없음) — 착수
|
|
시점에 이번에 정리된 대칭 설계를 그대로 가져다 쓰면 됨.
|