fix(docs): 리뷰 라운드 — 의사코드 결함 3건 + doc-check 사각지대 수정
다른 에이전트 감사와 사용자 트레이싱으로 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
This commit is contained in:
parent
69466abc47
commit
32e9db0773
21 changed files with 191 additions and 80 deletions
|
|
@ -195,27 +195,24 @@ identity 하나로** 판정할 수 있고, 그러면 claim 로직이 그룹을
|
|||
|
||||
```lua
|
||||
-- AttributeKeyHandler(quad-base) — 이름 claim만 자기 상태로 가짐
|
||||
-- Relate는 항상 (inst, key) 2단 — `base/relate-plan.md` API 참고.
|
||||
-- 여기선 key = attribute 이름, value = 그 이름을 지금 잡고 있는 키 객체.
|
||||
local nameClaims = Relate() -- {[inst(weak)] = {[name] = key(strong)}}
|
||||
|
||||
function AttributeKeyHandler.process(inst, k, v, index)
|
||||
local claims = nameClaims:GetStrong(inst)
|
||||
if not claims then
|
||||
claims = {}
|
||||
nameClaims:SetStrong(inst, claims)
|
||||
end
|
||||
local cur = claims[k.Name]
|
||||
local cur = nameClaims:GetStrong(inst, k.Name)
|
||||
if cur ~= nil and cur ~= k then
|
||||
error(`attribute "{k.Name}" is already bound by another owner`)
|
||||
end
|
||||
claims[k.Name] = k
|
||||
nameClaims:SetStrong(inst, k.Name, k)
|
||||
|
||||
setAttribute(inst, k.Name, v) -- v가 nil이든 아니든 무조건(위 "메커니즘" 절)
|
||||
|
||||
return function()
|
||||
-- 엔진 부작용 없음 — claim 반납만. "내가 실제로 물러날 때만" 지움
|
||||
-- (`dispatch-core-plan.md` "Handler 작성 체크리스트" 4번).
|
||||
if claims[k.Name] == k then
|
||||
claims[k.Name] = nil
|
||||
if nameClaims:GetStrong(inst, k.Name) == k then
|
||||
nameClaims:SetStrong(inst, k.Name, nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -330,6 +327,20 @@ function AttributeGroupHandler.process(inst, k, v, index)
|
|||
end
|
||||
```
|
||||
|
||||
- **[부분 실패 경로, 2026-08-14 리뷰에서 명시화] 순회 도중 소유권 충돌
|
||||
error가 나면, 그 전에 이미 등록된 이름들은 이 사이클의 클로저가 만들어지지
|
||||
못해 즉시 회수되지 않는다.** `error`가 `process` 밖으로 전파되므로 `return
|
||||
function() ... end`에 도달하지 못하고, `Dispatch`는 성공한 반환값만
|
||||
저장하기 때문. 다만 **피해 범위는 그 인스턴스의 수명으로 한정됨**:
|
||||
(1) `nameClaims`/`chains`는 `Relate`라 `inst`에 대해 weak — 그 인스턴스가
|
||||
GC되면 잔여 claim도 같이 사라짐, (2) 같은 자리가 다시 프로세스되면
|
||||
`Dispatch`가 남겨둔 마커 슬롯 덕분에 (A) 분기를 타고, 이미 claim된 이름은
|
||||
`cur == k`라 **에러 없이 그대로 통과**해 같은 자리에서 같은 error로 다시
|
||||
멈춤 — 조용한 오작동이 아니라 **반복 재현되는 시끄러운 실패**. 그래서
|
||||
별도 롤백 장치를 넣지 않음(코퍼스의 "에러=패닉 상태, 그 이후 정합성은
|
||||
관리 대상 아님" 원칙과 같은 결). 원자적 롤백이 필요하다고 판단되면
|
||||
그때 그룹 `process`에만 국소적으로 넣을 수 있음 — `question.md` 3번에
|
||||
열어둠.
|
||||
- **`groupKey(v, name)`는 그룹 값 객체별·이름별 메모이즈** — 같은 그룹
|
||||
값이 재프로세스될 때 같은 키가 나와야 claim이 자기 자신과 안 부딪힘.
|
||||
구현은 `Relate<그룹 값 → {[name] = key}>` 하나면 충분하고, **공개
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ unbindLifetime(inst: any, value: any): ()
|
|||
canExecute(inst: any, value: any): boolean
|
||||
```
|
||||
|
||||
**`unbindLifetime` 추가 이유(2026-08-09 세션, `bind-system-plan.md`의
|
||||
**`unbindLifetime` 추가 이유(2026-08-09 세션, `dispatch-core-plan.md`의
|
||||
"Length/Offset" 논의에서 파생)**: `Dispatch.setLength`(같은 위치에 새
|
||||
`State<number>`가 들어오면 이전 것에 걸어둔 Observer를 먼저 정리해야 함,
|
||||
`State<Slot>` 교체가 대표 사례)처럼 **`inst` 전체 생명주기보다 먼저,
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ Lua 테이블 리터럴은 배열 파트/해시 파트 사이에 소스 텍스
|
|||
- **실제 "지우기" 동작은 디스패치 쪽 `NoneHandler`가 담당** — merge가 끝난
|
||||
뒤 최종 flatten 결과에 `None`이 남아있으면, base 드라이버가 그 키를 어떻게
|
||||
처리하는지는 새 개념이 아니라 이미 확정된 디스패치 모델 그대로다.
|
||||
상세는 `base/bind-system-plan.md`의 "`None` 센티널 — StoreBind와
|
||||
상세는 `base/dispatch-core-plan.md`의 "`None` 센티널 — StoreBind와
|
||||
같은 재귀 재디스패치 패턴 재사용" 절 참고 — 핵심만 요약하면 `NoneHandler`도
|
||||
`StoreBind` 핸들러와 완전히 같은 모양(`isHandlable`이 `v == None`을
|
||||
잡고, `process`가 `v`를 진짜 `nil`로 바꿔 `process(inst, k, nil)`을 재귀
|
||||
|
|
@ -312,7 +312,7 @@ Modifier에는 없음).
|
|||
됨 — `mod:UICorner(8)`/`mod:FontSize(...)`처럼 DI 쪽 "제네릭 생성자 함수
|
||||
하나 + 자주 쓰는 것만 정적 필드로 미리 바인딩" 패턴 재사용.
|
||||
(주의: 이벤트는 이 관습의 유일한 예외라 인용 대상에서 제외 — 이벤트 바인딩은
|
||||
PA님 방식인 문자열 키 + 런타임 리플렉션으로 감, `base/bind-system-plan.md`
|
||||
PA님 방식인 문자열 키 + 런타임 리플렉션으로 감, `base/event-plan.md`
|
||||
"이벤트 바인딩 정정" 절 참고. Modifier는 이벤트가 아니라 Store/인스턴스
|
||||
생성과 같은 카테고리라 dot-access 관습이 그대로 적용됨.)
|
||||
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ init하려 하면 오류, 없는데 뭔가 생성해서 bind하려 해도 오류
|
|||
뭐라고 부를지("provider" vs "processor" vs 그냥 "plug") 아직 안 정함~~
|
||||
**[해소됨]** — **`Handler`로 확정**, 위 항목이 가리키는 계약의 정식 이름.
|
||||
`Dispatch`(그 계약을 스캔/실행하는 엔진, 프리미티브 아닌 탑레벨 싱글톤)와
|
||||
구분해서 쓸 것 — `base/bind-system-plan.md` "Dispatch는 프리미티브가
|
||||
구분해서 쓸 것 — `base/dispatch-core-plan.md` "Dispatch는 프리미티브가
|
||||
아니다" 절. **왜 다른 후보들을 기각했는지(2026-08-08 세션, 재확인)**:
|
||||
`Processor`는 계약 메소드 자체가 `process`라 이름 안에 같은 단어가
|
||||
겹쳐 눈에 거슬림, `Provider`는 `canProvide`처럼 "뭔가를 공급한다"는
|
||||
|
|
@ -125,7 +125,7 @@ init하려 하면 오류, 없는데 뭔가 생성해서 bind하려 해도 오류
|
|||
번도 실행 안 된 상태에서 dispatch가 호출되면 어떻게 되는지
|
||||
(`pre-implementation-audit.md` 1-4) — 별도 케이스로 처리하지 않음.
|
||||
provider 미주입 상태는 결국 그 클래스의 핸들러가 레지스트리에 하나도
|
||||
없는 상태이므로, `base/bind-system-plan.md` "우선순위 동률/매치 실패
|
||||
없는 상태이므로, `base/dispatch-core-plan.md` "우선순위 동률/매치 실패
|
||||
처리" 절의 일반 "매치 실패 시 즉시 error" 규칙 하나로 자연히 커버됨.
|
||||
- base 유틸(per-instance 상태 저장소, 생명 바인드 유틸)이 인터페이스만 두고
|
||||
실제 구현은 백엔드 팩토리(`RobloxFactory(BaseModule)`류)가 뮤테이션으로
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
함.** 콜백 파라미터 타입은 호출부가 인라인으로 직접 명시
|
||||
(`function(v: UDim2) ... end`) — Luau가 그 타입이 실제 프로퍼티 타입과
|
||||
일치하는지 검증해주지 않음. 이미 확정된 "이벤트 바인딩은 콜백 시그니처를
|
||||
Luau가 검증 못 하는 대가를 받아들인다"는 결정(`bind-system-plan.md` "이벤트
|
||||
Luau가 검증 못 하는 대가를 받아들인다"는 결정(`event-plan.md` "이벤트
|
||||
바인딩 — `On.EventName` 도트액세스 안 씀" 절, "타입 안전성을 어느 정도
|
||||
포기하는 대가")과 같은 급의 트레이드오프라 새로 정당화할 것 없음 — 오히려
|
||||
`AttributeKey<<T>>`처럼 제네릭으로 정확히 맞추려는 시도는 이벤트 키보다 더
|
||||
|
|
@ -55,7 +55,7 @@
|
|||
별도 `retract(inst,k,v)` 필드가 Disconnect를 담당한다고 적혀 있었으나,
|
||||
Handler 계약이 `process` 1-메소드로 합쳐지며 그 로직이 반환 클로저로
|
||||
이동 — `connection`은 `process`의 로컬 변수를 클로저가 upvalue로 그대로
|
||||
캡처하므로 별도 `Relate` 저장/재조회가 필요 없음(`bind-system-plan.md`
|
||||
캡처하므로 별도 `Relate` 저장/재조회가 필요 없음(`dispatch-core-plan.md`
|
||||
"핸들러 내부 상태 저장" 절).
|
||||
- **`State<function>` 지원 — 새 메커니즘 없음.** 이미 확정된 "이벤트도
|
||||
store-bind 가능 — `false`로 disconnect" 메커니즘(`bind-system-plan.md`)이
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Relate — inst와 임의의 값을 weak하게 엮는 범용 릴레이션 프리미티브
|
||||
|
||||
**상태**: base — 2026-08-08 세션에서 신설, 확정. `base/bind-system-plan.md`의
|
||||
**상태**: base — 2026-08-08 세션에서 신설, 확정. `base/dispatch-core-plan.md`의
|
||||
"핸들러 내부 상태 저장"과 `base/lifecycle-pattern.md`의 `bindLifetime`/
|
||||
`canExecute` 양쪽이 필요로 했던 "`inst`를 weak 키로 하는 저장소"가 지금까지
|
||||
`base.perInstanceState(inst)`라는 이름만 있고 인터페이스가 미정인 placeholder로
|
||||
|
|
|
|||
|
|
@ -202,7 +202,7 @@ Slot이 여럿 형제로 존재할 때, 최종 자식 순서가 저작 순서(
|
|||
있었던 질문 — **메커니즘 확정으로 해소됨**: `Dispatch.setLength`/
|
||||
`Dispatch.setOffsetSource` + 형제별 개수 누적합(`offset`)을 리액티브
|
||||
프로퍼티(Roblox `LayoutOrder`)에 바인딩하는 방식 — 상세는 `base/
|
||||
bind-system-plan.md`의 "Length/Offset — 여러 Slot이 형제로 섞일 때 순서
|
||||
dispatch-core-plan.md`의 "Length/Offset — 여러 Slot이 형제로 섞일 때 순서
|
||||
보장" 절 참고. **DOM류 물리 순서 백엔드에도 같은 base 메커니즘이 그대로
|
||||
재사용됨**(offset이 바뀌어도 이미 마운트된 원소를 물리적으로 옮길 필요
|
||||
없음 — `insertBefore`가 뒤 형제를 자연히 밀어주므로, backend Handler의
|
||||
|
|
@ -1314,7 +1314,7 @@ UI에 직접 관측, (2) `Dispatch.setLength(inst, i, slot.Length)`가 형제
|
|||
게 맞고, 그건 사용자가 별도 State로 계산해야 하는 몫.
|
||||
|
||||
**동적 자식은 반드시 `Slot` 또는 `state<Frame>`류 store-bind를 통해서만
|
||||
추가/제거 — 그 외 경로는 UB(2026-08-10 세션, `base/bind-system-plan.md`의
|
||||
추가/제거 — 그 외 경로는 UB(2026-08-10 세션, `base/dispatch-core-plan.md`의
|
||||
"Length/Offset" 절 반영).** 둘 다 `Dispatch.setLength`/`setOffsetSource`를
|
||||
정확히 호출하는 유일한 정당 경로라, 이걸 우회해서(예: 외부 코드가 Slot이
|
||||
마운트해둔 부모 Instance에 직접 `.Parent = parentInst`로 자식을 끼워
|
||||
|
|
@ -1589,7 +1589,7 @@ Roblox뿐 아니라 web에도 그대로 필요.
|
|||
|
||||
`offset`/`sum`은 0-based 개수(카디널 수)고, `_elements`/`updateFn`의
|
||||
`index`는 1-based Lua 배열 관례 — `index + offset` 공식이 이 둘을
|
||||
의도적으로 섞는 것. 상세는 `base/bind-system-plan.md`의 "0-based
|
||||
의도적으로 섞는 것. 상세는 `base/dispatch-core-plan.md`의 "0-based
|
||||
개수" 절 참고.
|
||||
|
||||
## 반응형 raw 요소 — `State<T>`/`Source<T>`도 Slot 요소로 허용 (2026-08-11 일곱 번째 세션)
|
||||
|
|
@ -1834,7 +1834,7 @@ Slot이 마운트될 때 **자기 하위 요소들까지 `bindLifetime`으로
|
|||
**[정정, 사용자 지적] "해제 짝"이라는 새 API는 필요 없음** — 옛 owner에
|
||||
대해 그냥 **`Dispatch.setOffsetSource(ownerKey, position, None)` +
|
||||
`Dispatch.setLength(ownerKey, position, 0)`을 다시 부르면 끝**.
|
||||
이건 이미 확정된 관용구 그대로임(`base/bind-system-plan.md`의 "실제
|
||||
이건 이미 확정된 관용구 그대로임(`base/dispatch-core-plan.md`의 "실제
|
||||
마운트를 하지 않는 위치는 `None`을 등록 — `setLength`도 짝을 맞춰 `0`").
|
||||
즉 **해제 = 0/`None`으로 재등록**이고 별도 unregister 함수가 없어도 됨 —
|
||||
앞서 "이게 실제 작업량"이라고 적었던 판단은 과했음.
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ Handler는 quad 사용자가 아니라 **백엔드/핸들러 구현자가 채우
|
|||
지점**이라는 완전히 다른 축의 개념이라, 여기 분류를 "왜 Handler가
|
||||
빠졌는지" 궁금해할 필요 없음 — 프리미티브 분류가 불완전한 게 아니라
|
||||
Handler가 애초에 다른 층위. 관련해서 Handler를 담는 엔진(`Dispatch`) 자체가
|
||||
왜 프리미티브가 아니라 탑레벨 싱글톤인지는 `base/bind-system-plan.md`의
|
||||
왜 프리미티브가 아니라 탑레벨 싱글톤인지는 `base/dispatch-core-plan.md`의
|
||||
"Dispatch는 프리미티브가 아니다" 절 참고.
|
||||
|
||||
과거 "미해결로 남은 것"으로 적었던 두 항목도 모두 해소됨: `:Compute`의
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ nil`/`or None`(and/or 삼항)으로 적었으나, `Tag(...)`가 항상-truthy라
|
|||
|
||||
구 모델과 달리 **핸들러 타입이 사이클마다 바뀔 수 있음**(`Tag(...)` ↔
|
||||
`nil`, 값이 `Tag`가 아니게 되면 `TagHandler.isHandlable`이 더 이상 안
|
||||
맞음) — 그래서 `retract`가 실제로 필요해짐(`bind-system-plan.md` "확정된
|
||||
맞음) — 그래서 `retract`가 실제로 필요해짐(`dispatch-core-plan.md` "확정된
|
||||
디스패치 모델" 절의 일반 원칙 그대로).
|
||||
|
||||
**[전면 정정, 2026-08-12 열한 번째 세션] 아래는 이전 버전(단일 `relate`,
|
||||
|
|
@ -156,6 +156,7 @@ TagHandler.priority = <일반>
|
|||
TagHandler.isHandlable(inst, k, v) = isTag(v) -- Brand 기반, array-part 전용
|
||||
|
||||
function TagHandler.process(inst, k, v, index)
|
||||
local added = {}
|
||||
for name in v:Names() do
|
||||
local holders = tagNameMap:GetStrong(inst, name)
|
||||
if not holders then
|
||||
|
|
@ -163,10 +164,13 @@ function TagHandler.process(inst, k, v, index)
|
|||
tagNameMap:SetStrong(inst, name, holders)
|
||||
end
|
||||
if next(holders) == nil then
|
||||
addTag(inst, { name }) -- 주입된 엔진 op(아래 "패키지 배치" 절)
|
||||
table.insert(added, name) -- 이 이름을 처음 거는 위치일 때만 실제 호출 대상
|
||||
end
|
||||
holders[k] = true -- 이 "위치"가 이 이름을 걺(Tag 객체가 다른 위치와 같아도 무관)
|
||||
end
|
||||
if #added > 0 then
|
||||
addTag(inst, added) -- 한 번에 — 웹 className 일괄 갱신을 위한 배치 계약
|
||||
end
|
||||
return function(nextValue)
|
||||
-- nextValue는 nil(단순 철거)이거나 같은 핸들러가 곧 처리할 새 Tag —
|
||||
-- 타입이 계약으로 보장되므로 isTag 가드가 필요 없음(2026-08-13 열네 번째 세션).
|
||||
|
|
@ -174,26 +178,33 @@ function TagHandler.process(inst, k, v, index)
|
|||
-- 이름 집합도 절대 안 바뀜 — holders 순회 자체가 불필요한 순수 최적화
|
||||
local removed = {}
|
||||
for name in v:Names() do
|
||||
-- [정정, 2026-08-14 리뷰] 생존 이름은 **홀더 등록 자체를 유지**한다.
|
||||
-- 예전엔 홀더를 일단 빼고 removeTag 호출만 skip했는데, 그러면 곧
|
||||
-- 이어지는 process가 빈 holders를 보고 `addTag`를 **다시** 부름
|
||||
-- (엔진이 멱등이라 안 보였을 뿐, "진짜 바뀐 이름에만 호출"이라는
|
||||
-- 이 절의 설계 목표와 어긋났고 아래 서술과도 모순이었음).
|
||||
if nextValue ~= nil and nextValue:Contains(name) then continue end
|
||||
local holders = tagNameMap:GetStrong(inst, name) -- 이미 등록됐으므로 항상 있음
|
||||
holders[k] = nil -- 이 "위치"가 이 이름을 놓음(같은 Tag 객체를 다른 위치도 쓰고 있어도 무관)
|
||||
if next(holders) == nil and not (nextValue ~= nil and nextValue:Contains(name)) then
|
||||
table.insert(removed, name) -- 곧 새 process가 재확정할 이름이면 skip(깜빡임 방지)
|
||||
if next(holders) == nil then
|
||||
table.insert(removed, name)
|
||||
end
|
||||
end
|
||||
if #removed > 0 then
|
||||
removeTag(inst, removed) -- 한 번에 — 웹 className 일괄 갱신을 위한 배치 계약
|
||||
removeTag(inst, removed) -- 한 번에
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
- **`addTag`는 온전히 `process`, `removeTag`는 온전히 반환하는 클로저**
|
||||
— 서로 겹치는 diff 계산이 없음. 클로저가 이전 `Tag`(`v`, 자기 자신이
|
||||
캡처)가 걸었던 이름 전부를 소유 목록에서 빼되(항상 실행), 그 결과
|
||||
목록이 비었을 때 **실제 `removeTag` 호출만** "새로 들어올 값이
|
||||
그 이름을 여전히 Contains하는가"로 판단해 skip — 소유 목록 자체는
|
||||
항상 최신 객체로 갱신되므로(정확히 `v`를 빼고 새 `process`가 새 값을
|
||||
넣는 두 단계), 이름이 살아남는 경우에도 stale 레퍼런스가 안 남음.
|
||||
— 서로 겹치는 diff 계산이 없음. 클로저는 이전 `Tag`(`v`, 자기 자신이
|
||||
캡처)가 걸었던 이름 중 **새 값이 더 이상 안 거는 것만** 소유 목록에서
|
||||
빼고, 그 결과 목록이 빈 이름들을 모아 `removeTag`를 **한 번** 호출함.
|
||||
생존 이름은 소유 목록을 그대로 두므로(**[정정, 2026-08-14 리뷰]** 예전엔
|
||||
일단 뺐다가 호출만 skip했는데 그러면 곧이은 `process`가 빈 목록을 보고
|
||||
`addTag`를 다시 불렀음) 뒤이은 `process`에서 `addTag` 자체가 안 불림 —
|
||||
"실제 엔진 호출은 진짜 바뀐 이름에만"이 코드 수준에서도 성립.
|
||||
`process`는 `v`가 새로 거는 이름 전부를 무조건 등록(소유 목록이
|
||||
비어있던 경우에만 실제 `addTag`) — 자기 나름의 old-vs-new diff가 전혀
|
||||
필요 없음(그 일을 클로저가 매번 정확히 해줌).
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ v1에서도 `Corner`/`PaddingAll`/`Scale`은 store 값으로 바인드 가능했
|
|||
다시 세팅하는 정도로 충분. 구현 비용도 낮음: 각 Handler가 `process`에서
|
||||
"이전에 자기가 찾거나 만든 자식 Instance"를 얻어야 하는데, 이건 이미
|
||||
base가 범용 유틸로 제공하기로 확정한 per-instance weak-keyed 저장소
|
||||
(`Relate:SetStrong(inst,k,...)`, `base/relate-plan.md`/`base/bind-system-plan.md`
|
||||
(`Relate:SetStrong(inst,k,...)`, `base/relate-plan.md`/`base/dispatch-core-plan.md`
|
||||
"핸들러 내부 상태 저장" 절)를 그대로 재사용하면 됨 — PropertyHandler가 실행 중인
|
||||
Tween 상태를 기억해두는 것과 정확히 같은 패턴. 새 메커니즘 발명 불필요, 이미
|
||||
있는 "store 바인드는 pluggable 바인드를 재실행하는 래핑" 원칙
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@
|
|||
| 폴더 | 뜻 | 누가 처리 |
|
||||
|---|---|---|
|
||||
| `review-required/` | **설계가 걸림 — 사람 결정 필요**(**[2026-08-13 13차 세션] 현재 비어 있음** — 마지막 한 건이던 `08`이 해소돼 `done/`으로 감) | ⭐ 사용자 |
|
||||
| `rewrite-required/` | 스파이크 코드가 깨짐(설계 문제 아님, `13`/`15`/`16`) | 에이전트 |
|
||||
| `rewrite-required/` | 스파이크가 낡음 — 코드가 깨졌거나(`13`/`15`/`16`), **설계가 바뀌어 옛 모델을 검증 중**(`04`/`19`, 2026-08-13 14차 세션 하강 diff) | 에이전트 |
|
||||
| `not-run/` | 이 환경에서 못 돌림(Studio 전용 `10` + GC 헬퍼) | 사용자 or MCP 연결 후 |
|
||||
| `done/` | 통과 or 판정 끝, 더 할 일 없음(15개) | — |
|
||||
| `done/` | 통과 or 판정 끝, 더 할 일 없음(14개) | — |
|
||||
|
||||
**스파이크를 고치거나 돌렸으면 파일을 해당 폴더로 `git mv`하고 STATUS.md의
|
||||
줄도 같이 옮길 것** — 그게 곧 상태 갱신이다. 아래 파일 목록의 경로는
|
||||
|
|
@ -68,7 +68,7 @@ ROADMAP 항목 근거인지, 어떻게 실행하는지, 실행 후 뭘 확인해
|
|||
| 파일 | 검증 대상 | 근거 문서 |
|
||||
|---|---|---|
|
||||
| `01-two-pass-array-hash-order.luau` | 배열 파트(children/Ref) 먼저, 해시 파트(프로퍼티/이벤트) 나중이라는 두 패스 순회 계약 | `dispatch-core-plan.md` "props 순회 순서", ROADMAP M0-4 |
|
||||
| `02-none-sentinel-vs-nil-holes.luau` | **[2026-08-09 커밋 f198fd9 반영해 전면 재작성]** 순서가 중요한 배열(PreRef pre-pass, sourceList)은 `None` 소진이 맞고, 순서가 안 중요하고 재사용이 필요한 배열(Ref 콜백/대기자)은 `nil`+슬롯 재사용이 맞다는 최종 구분 + `None`을 잘못 쓰면 배열이 무한정 자라는 버그의 정량적 재현 | `bind-system-plan.md` "왜 None이 아니라 nil인가"(2026-08-09 열한 번째 세션 최종 정정), ROADMAP M0-4 |
|
||||
| `02-none-sentinel-vs-nil-holes.luau` | **[2026-08-09 커밋 f198fd9 반영해 전면 재작성]** 순서가 중요한 배열(PreRef pre-pass, sourceList)은 `None` 소진이 맞고, 순서가 안 중요하고 재사용이 필요한 배열(Ref 콜백/대기자)은 `nil`+슬롯 재사용이 맞다는 최종 구분 + `None`을 잘못 쓰면 배열이 무한정 자라는 버그의 정량적 재현 | `ref-plan.md` "왜 None이 아니라 nil인가"(2026-08-09 열한 번째 세션 최종 정정), ROADMAP M0-4 |
|
||||
| `03-recursive-store-bind-dispatch.luau` | `process`/`retract` 재귀 재-dispatch 기본 모델, 우선순위 스캔 | `dispatch-core-plan.md` "확정된 디스패치 모델", ROADMAP M0-3 |
|
||||
| `04-dispatch-chain-retractFrom.luau` | **[⚠️ 2026-08-13 열네 번째 세션: 하강 diff 확정으로 낡음 → `rewrite-required/`]** 아래는 옛 모델 기준 설명 — **[2026-08-13 감사에서 전면 재작성 + 파일명 변경]** 인덱스 기반 `chains`/`Dispatch.retractFrom`이 다단 재귀 위임에서 정확한지 — 3단 체인이 인덱스 1/2/3으로 안 겹치고 쌓이는지(= `State<State<T>>` **정상 동작**, UB 아님), 안/바깥 store 재발행 시 깊은 인덱스부터 정리되는지, hint가 target 인덱스에만 가는지 + **음성 대조군**: `chains:SetStrong`을 `handler.process` 뒤에 두면 최초 마운트에서 하위 retractor가 유실되는 버그 재현. 옛 버전은 핸들러 identity 기반 추적과 "중복 push 즉시 error" 가드를 검증했는데 그 가드는 다섯 번째 세션 재설계로 **없어져서** 설계와 정반대를 테스트하고 있었음 | `dispatch-core-plan.md` "Dispatch 체인"(2026-08-13 다섯 번째 세션 재설계) + 2026-08-13 감사 |
|
||||
| `05-store-state-diamond-propagation.luau` | push-invalidate/pull-recompute가 다이아몬드 의존성에서 중복 재계산 없이 동작하는지 | ROADMAP M0-1 |
|
||||
|
|
@ -82,7 +82,7 @@ ROADMAP 항목 근거인지, 어떻게 실행하는지, 실행 후 뭘 확인해
|
|||
| `13-type-ref-preref-subtype.luau` | (A, 타입) `PreRef<T>`가 `Ref<T>`를 구조적으로 만족하는지, (B, 런타임) `isRef`/`isPreRef` 합성이 재정정대로 동작하는지(`isRef(preRefInstance)`가 이제 `true`) + Leaf 핸들러가 `isRef(v) and not isPreRef(v)`로 명시적으로 좁혀야 하는 이유 | `brand-plan.md`의 `Brand` 절(2026-08-09 열한 번째 세션 재정정) |
|
||||
| `14-type-nilable-default-overload.luau` (타입체크 전용) | `Source(default)`/`Ref(default)`의 `default` 생략이 `T`가 nilable일 때만 안전하다는 캐비엇을, 함수 오버로드(교차 타입)로 실제로 타입 레벨에서 막을 수 있는지 | `bind-system-plan.md` "[보강, 2026-08-09 열한 번째 세션]" 절 |
|
||||
| `15-type-compute-trailing-deps-typepack.luau` (타입체크 전용) | `:Compute(fn, ...)`의 trailing deps를 `fn`에 위치 인자(lazy State 핸들)로도 노출하는 확장, 최종 시그니처 `fn(self, previous?, ...deps)` — 이형(heterogeneous) 다중 deps를 제네릭 타입 팩(`U...`)으로 표현 가능한지, `previous?`가 팩 앞(정정된 순서)에서만 통과하고 팩 뒤(옛 순서)에서는 막히는지 | `bind-system-plan.md` "trailing deps를 fn에 lazy positional 인자로도 노출" 절(2026-08-11 후속 세션, 순서는 같은 날 세 번째 세션에 정정) |
|
||||
| `16-type-store-key-typefunction.luau` (타입체크 전용) | `Store<T>`가 `T`의 각 필드를 `Source`로 감싼 타입을 Luau `type function`(`types.newtable`/`:setproperty`/`ty:properties()`)으로 실제 합성 가능한지, 결과가 구조적으로 `Source<T>` 필드를 만족하는지 | `bind-system-plan.md` "`store.key` 레코드 필드 타이핑" 절(2026-08-12 열일곱 번째 세션), `pre-implementation-audit.md` 1-10 |
|
||||
| `16-type-store-key-typefunction.luau` (타입체크 전용) | `Store<T>`가 `T`의 각 필드를 `Source`로 감싼 타입을 Luau `type function`(`types.newtable`/`:setproperty`/`ty:properties()`)으로 실제 합성 가능한지, 결과가 구조적으로 `Source<T>` 필드를 만족하는지 | `typing-limits.md` "`store.key` 레코드 필드 타이핑" 절(2026-08-12 열일곱 번째 세션), `pre-implementation-audit.md` 1-10 |
|
||||
| `17-modifier-index-tableclone-chaining.luau` | Modifier의 제네릭 `__index`+`table.clone` 체이닝 — 임의 필드 이름에 대해 즉석 setter가 만들어지는지, `table.clone`이 메타테이블을 참조로 공유해 여러 단계 clone에서도 체이닝이 안 끊기는지, 원본이 mutate 안 되는지, 형제 분기끼리 오염 안 되는지 | `modifier-plan.md` "런타임은 클래스별 코드 없이 base에 딱 하나만 있으면 됨" 절 + "`table.clone`의 정확한 동작 — 확인됨" 절(2026-08-12 열일곱 번째 세션), `pre-implementation-audit.md` 1-11 |
|
||||
| `18-relate-mutual-cycle-gc.luau` | **[2026-08-13 신규]** 서로 다른 두 `Relate`가 서로의 키를 상대방의 강한 값으로 제공하는 상호 순환은 Luau에 ephemeron이 없어 GC가 못 푼다는 주장(지금까지 공식 문서 인용으로만 뒷받침됨) — 음성 대조군(순환 재현)과 양성 대조군(한쪽을 weak-value로 낮추면 풀리는지) 둘 다 실측 | `relate-plan.md` "위험한 패턴" 절(2026-08-12 열세/열네 번째 세션), `slot-plan.md`의 `kSlotMap`/`slotOwner`/`elementOwner` 실사례 |
|
||||
| `19-ownership-refcount-relate-patterns.luau` | **[2026-08-13 신규, 같은 날 B/C 전면 재작성 — 지금은 현행 설계 기준]** 세 소유권/참조카운트 알고리즘 검증. **A**: Tag `tagNameMap` 참조 카운트(여러 위치가 같은 이름을 겹쳐 가져도 마지막 홀더가 빠질 때만 실제 `RemoveTag`. 옛 `kTagMap`은 클로저 캡처로 대체돼 삭제됨). **B**: Attribute 이름 소유권 — 공개 `AttributeKey(name)` 캐시 + `Dispatch.process`의 인덱스 1 **점유 체크**가 충돌을 잡는지(옛 `rawNew`+`owners` 수동 레지스트리는 폐기). **C**: Slot 소유권 — nested 엄격 `claimOwner`(같은 owner 재클레임도 error) vs top-level `claimOwnerAt(inst,k)`(정확히 같은 자리 재발행만 no-op). **셋 다 음성 대조군 포함** — 옛 로직이 `Slot{a,a}`/`Frame{slot,slot}`을 조용히 통과시키는 걸 재현. **[2026-08-13 열네 번째 세션] 0-Z가 확정되며 B 섹션이 낡음 → `rewrite-required/`** — 이제 "그룹 전용 키 + `AttributeKeyHandler`의 이름 claim"을 검증해야 함(A/C는 그대로 유효) | `tag-plan.md` "메커니즘", `attribute-plan.md` "이름 소유권", `slot-plan.md` "요소 소유권" |
|
||||
|
|
|
|||
|
|
@ -192,6 +192,13 @@
|
|||
남은 근거는 편의성과 Slot offset이 밀리고 당겨지는 케이스뿐이라
|
||||
우선순위가 더 내려감 — `state:Flatten()`류 콤비네이터 아이디어는
|
||||
그대로 백로그. 상세는 `research/operator-sugar-plan.md` 마지막 절.
|
||||
- **[신설, 2026-08-14 리뷰] `AttributeGroupHandler.process`의 부분 실패
|
||||
롤백** — 이름 순회 도중 소유권 충돌 error가 나면 그 전에 등록된 이름들이
|
||||
이 사이클엔 회수되지 않음(클로저가 안 만들어짐). 피해는 그 인스턴스
|
||||
수명으로 한정되고 재현도 시끄럽게 반복돼서 **지금은 별도 장치 없이
|
||||
문서화만** 했는데(`base/attribute-plan.md` "메커니즘" 절), 원자적
|
||||
롤백(그룹 `process`에만 국소적인 unwind)을 넣을지는 열어둠. 지금 결정
|
||||
불필요 — M10 구현 시점에 판단.
|
||||
- **[신설, 2026-08-13 열네 번째 세션] `Attribute.Merged`의 이름 중복** —
|
||||
두 Store가 같은 이름을 가지면 지금은 `:NameMap()` 평탄화 단계에서
|
||||
조용히 하나가 이김(dispatch 이전이라 이름 claim이 못 잡는 자리).
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ additional-primitives-plan.md`의 "문서화 백로그" 절이 원자료)**:
|
|||
인가`)을 더 깊게 확장, `Blocker` 같은 파생 프리미티브가 이 목표 위에서
|
||||
왜 자연스럽게 나왔는지까지 포함하는 설계 철학 에세이
|
||||
6. **왜 배열/해시 두 패스 순서를 안 뒤집는가, `PreRef`는 왜 그 예외로
|
||||
따로 필요한가** (2026-08-07 세 번째 세션 원자료, `bind-system-plan.md`
|
||||
따로 필요한가** (2026-08-07 세 번째 세션 원자료, `ref-plan.md`
|
||||
"`phase` 옵션 폐기" 절 마지막 항목이 이 자리를 지목해뒀던 것 — 지금까지
|
||||
여기 안 옮겨져 있었음) — "프로퍼티/이벤트가 항상 children/Ref보다
|
||||
나중"이라는 순서를 고치는 대신 `PreRef`라는 별도 타입으로 예외를
|
||||
|
|
@ -213,7 +213,7 @@ additional-primitives-plan.md`의 "문서화 백로그" 절이 원자료)**:
|
|||
**[해소됨, 2026-08-12 여섯 번째 세션]** 취소 가능 여부 — PreRef는
|
||||
구조적으로 `retract` 체인에 아예 안 올라가므로 취소 개념 자체가 없고,
|
||||
대신 이미 fire된 PreRef를 재사용하면(두 번째 construction에 다시
|
||||
놓으면) 즉시 `error` — "1회용, 재할당 불가"로 확정. `bind-system-plan.md`
|
||||
놓으면) 즉시 `error` — "1회용, 재할당 불가"로 확정. `ref-plan.md`
|
||||
"동적 경로로 도착한 PreRef는 런타임에도 명시적으로 에러" 절 바로 아래
|
||||
"PreRef는 '취소'라는 개념이 없다" 항목 참고.
|
||||
|
||||
|
|
|
|||
|
|
@ -302,7 +302,7 @@ Attribute는 이미 "겹치면 error"로 소유 코드가 명확히 갈리는
|
|||
|
||||
**배경**: 2026-08-13 다섯 번째 세션의 인덱스 기반 `Dispatch` 재설계로
|
||||
`State<State<T>>`가 UB에서 **정상 지원 대상**이 됐음(각 재귀 단계가
|
||||
다른 인덱스를 써서 슬롯 충돌이 없어짐, `base/bind-system-plan.md`
|
||||
다른 인덱스를 써서 슬롯 충돌이 없어짐, `base/dispatch-core-plan.md`
|
||||
"Dispatch 체인" 절). 하지만 **동작한다고 해서 권장 방향인 건 아님**
|
||||
(사용자 판단: "UB는 아니지만 우리가 원치 않는 방향인건 맞습니다").
|
||||
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ store.color }`처럼 애니메이션 없이 그냥 반응형으로 값만 바뀌
|
|||
여러 단계(A→B→C)로 겹칠 때 자기 자신의 상태와 위임한 핸들러의 상태가
|
||||
슬롯 하나를 두고 충돌하는 문제가 있어 기각되고, 대신 Dispatch 자신이
|
||||
전체 체인을 배열로 들고 있는 쪽으로 정리됨. 상세는 `base/
|
||||
bind-system-plan.md` "Dispatch 체인" 절, `ROADMAP.md` M2/M4. 아래는
|
||||
dispatch-core-plan.md` "Dispatch 체인" 절, `ROADMAP.md` M2/M4. 아래는
|
||||
원래 발견 당시 기록.
|
||||
|
||||
**위치**: `base/dispatch-core-plan.md` "확정된 디스패치 모델" 절 90-91행 —
|
||||
|
|
@ -137,7 +137,7 @@ bind-system-plan.md` "Dispatch 체인" 절, `ROADMAP.md` M2/M4. 아래는
|
|||
**해소**: 별도 케이스로 처리하지 않음 — provider 미주입 상태는 결국 그
|
||||
클래스의 핸들러가 레지스트리에 하나도 없는 상태이므로, 위 1-3에서 확정된
|
||||
일반 "매치 실패 시 즉시 error" 규칙 하나로 자연히 커버됨. 상세는
|
||||
`base/module-lifecycle-plan.md` 해당 항목, `base/bind-system-plan.md`
|
||||
`base/module-lifecycle-plan.md` 해당 항목, `base/dispatch-core-plan.md`
|
||||
"우선순위 동률/매치 실패 처리" 절. 아래는 원래 발견 당시 기록.
|
||||
|
||||
**위치**: `base/module-lifecycle-plan.md` "Bind는 누가, 어떻게 구현하는가"
|
||||
|
|
@ -299,7 +299,7 @@ base 인터페이스가 그보다 늦은 M8에서 만들어지는 순서 역전.
|
|||
이미 쓰이는 패턴). 결과가 구조적으로 `Source<T>`를 만족하기만 하면 Luau가
|
||||
이름이 아니라 구조로 일치를 검사하므로 문제없음. 기술적으로 막힐 위험이
|
||||
없어졌으니 M0/M3 어느 시점에 검증해도 무방 — `ROADMAP.md` 배치를 억지로
|
||||
안 옮겨도 됨. 상세는 `base/bind-system-plan.md` "`store.key` 레코드 필드
|
||||
안 옮겨도 됨. 상세는 `base/typing-limits.md` "`store.key` 레코드 필드
|
||||
타이핑" 절, 실제 문법 실측은
|
||||
`luau-test`의 `16-type-store-key-typefunction.luau`(신규). 아래는
|
||||
원래 발견 당시 기록.
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ v1(`.claude/initreq/quad/src`) 조사 결과, API는 성격이 다른 두 계층
|
|||
|
||||
### 3-1. (a)는 얇게 재현 가능 — opt-in 서브패키지로 격리하면 근거 문제도 해소됨
|
||||
|
||||
- **이벤트 self 관습**: 클로저 한 겹으로 재현 가능. `base/bind-system-plan.md`
|
||||
- **이벤트 self 관습**: 클로저 한 겹으로 재현 가능. `base/event-plan.md`
|
||||
"이벤트 핸들러는 self를 받지 않는다" 절이 든 반대 근거 4가지(Ref 중복
|
||||
채널, Modifier 정적 flatten과 경쟁, quad-debug 추적 밖 mutate 경로, 클로저
|
||||
비용)는 **코어에 넣을 때** 문제가 되는 것들 — 별도 opt-in 패키지
|
||||
|
|
|
|||
|
|
@ -119,7 +119,9 @@ clear 하게 준비해두고, 커밋하세요."*
|
|||
`확정된 디스패치 모델`/`None 센티널`/`Dispatch는 프리미티브가 아니다`/
|
||||
`Dispatch 체인`/`Handler 작성 체크리스트`/`Length·Offset`/`store 바인드는
|
||||
래핑` 블록(1074줄)을 **`base/dispatch-core-plan.md`**로 옮기고 새 모델로
|
||||
재작성. `bind-system-plan.md`는 2263 → 1219줄(반응형 코어 + 인체공학).
|
||||
재작성. `bind-system-plan.md`는 **2291 → 1213줄**(반응형 코어 + 인체공학).
|
||||
(커밋 `69466ab` 메시지엔 이 숫자가 "2263→1219"로 잘못 적혀 있음 —
|
||||
분할 직전/직후가 아니라 어림한 값이었음. 히스토리 재작성 대신 여기 정정.)
|
||||
|
||||
인바운드 참조는 `doc-check.py` 출력을 그대로 입력으로 삼아 기계적으로
|
||||
고침(파일별 라인 지정 치환) — 15개 파일 30여 곳.
|
||||
|
|
@ -196,3 +198,61 @@ end
|
|||
"국소 레지스트리 + 즉시 error" 모양이 자연스러운 선택지라는 점을
|
||||
질문 문서에 덧붙임.
|
||||
- `doc-check.py` **ERROR 0** 유지 확인.
|
||||
|
||||
---
|
||||
|
||||
## 6. 후속 리뷰 라운드 (2026-08-14, 다른 에이전트 감사 + 사용자 트레이싱)
|
||||
|
||||
커밋 `69466ab`를 다른 에이전트 셋이 감사하고 사용자가 직접 트레이싱한
|
||||
결과를 받음. **핵심 알고리즘(하강 diff, 인덱스 체인)에는 버그 없음**으로
|
||||
확인됐고, 의사코드/문서 정합성에서 **실제 결함 3건 + stale 참조 다수**가
|
||||
나와 전부 수정:
|
||||
|
||||
**1. `nameClaims`가 `Relate`의 3-인자 계약을 위반** (실제 버그)
|
||||
`GetStrong(inst)`/`SetStrong(inst, claims)`로 1·2-인자를 쓰고 있었음 —
|
||||
`relate-plan.md`가 확정한 API는 항상 `(inst, key[, value])`. 같은 커밋의
|
||||
`tagNameMap`은 정확히 3-인자를 쓰고 있어 대조로 바로 드러남.
|
||||
→ `GetStrong(inst, k.Name)`/`SetStrong(inst, k.Name, k)`로 정정(중간
|
||||
`claims` 테이블 자체가 없어져 코드도 짧아짐).
|
||||
|
||||
**2. `TagHandler`의 서술과 의사코드 불일치** (실제 버그)
|
||||
`Tag(A)→Tag(B)`에서 생존 이름("selected")을 손 트레이싱하면: 클로저가
|
||||
`holders[k]=nil`로 홀더를 **비운 뒤** `removeTag` 호출만 skip → 곧이은
|
||||
`process`가 빈 홀더를 보고 **`addTag`를 다시 호출**함. 문서는 정반대로
|
||||
"이미 걸려있던 이름은 소유 목록이 안 비어 있어 `addTag` 자체가 안 불림"
|
||||
이라고 적고 있었음. 엔진이 멱등이라 눈에 안 보였을 뿐, "실제 호출은 진짜
|
||||
바뀐 이름에만"이라는 이 절의 설계 목표와 어긋남.
|
||||
→ **생존 이름은 홀더 등록 자체를 유지**하도록 정정(`nextValue`가 그
|
||||
이름을 `Contains`하면 `continue`). 덤으로 `addTag`도 `removeTag`처럼
|
||||
**배치 호출**로 바꿈 — `{string}` 시그니처를 도입한 근거(웹 `className`
|
||||
일괄 갱신)와 코드가 안 맞던 것도 같이 해소.
|
||||
|
||||
**3. 그룹 `process`의 부분 실패 경로가 문서에 없었음** (문서 갭)
|
||||
이름 순회 도중 소유권 충돌 `error`가 나면 클로저가 안 만들어져 앞서
|
||||
등록된 이름들이 그 사이클에 회수되지 않음. **다만 감사가 말한 "영구
|
||||
누수"보다는 좁음** — `nameClaims`/`chains`가 `inst`에 weak라 인스턴스
|
||||
GC와 함께 사라지고, 재프로세스 시 이미 claim된 이름은 `cur == k`라
|
||||
에러 없이 통과해 **같은 자리에서 같은 error로 반복 재현**됨(조용한
|
||||
오작동이 아님). 그래서 롤백 장치는 안 넣고 그 경로를 명시적으로
|
||||
문서화, 원자적 롤백 여부는 `question.md` 3번에 열어둠.
|
||||
|
||||
**4. 문서 분할 후 stale 절 참조 20여 곳** — `doc-check.py`의 사각지대였음
|
||||
`REF` 정규식이 **줄 단위**로 돌아서 파일명과 "절 제목"이 줄바꿈에 걸친
|
||||
자연스러운 인용(`` `base/\nbind-system-plan.md`의 "Length/Offset" ``)을
|
||||
통째로 놓치고 있었음. 이번 분할뿐 아니라 **9차 세션의 1단계 분할 stale도
|
||||
같은 이유로 계속 숨어 있었음**(`event-plan`/`ref-plan`/`typing-limits`로
|
||||
간 절들).
|
||||
→ 검사기를 **파일 전체 스캔 + 개행 허용**으로 고치고(줄 번호는 매치
|
||||
오프셋에서 역산), 새로 드러난 WARN을 기준으로 30여 곳을 기계적으로
|
||||
정정. WARN 56 → 120건으로 늘었다가 정정 후 101건(남은 건 의역 인용 등
|
||||
판단이 필요한 것들).
|
||||
|
||||
**5. 사소한 것**: 커밋 메시지의 줄수(2263→1219)가 실제(2291→1213)와 다름
|
||||
(히스토리 재작성 대신 위 4-1에 정정 기록), `CLAUDE.md`가 `04`만 언급하고
|
||||
`19` 이동을 빠뜨린 것, `luau-test/README.md` 상단 요약표가 STATUS.md와
|
||||
어긋난 것 — 전부 수정.
|
||||
|
||||
**교훈**: 문서 분할 시 stale 참조는 "검사기가 ERROR 0이면 됐다"로 끝나지
|
||||
않음 — **검사기 자신의 사각지대**를 먼저 의심할 것. 이번엔 감사가 그
|
||||
사각지대를 짚어줬고, 고치자마자 1단계 분할의 오래된 stale까지 같이
|
||||
드러났다.
|
||||
|
|
|
|||
BIN
.claude/tools/__pycache__/doc-check.cpython-314.pyc
Normal file
BIN
.claude/tools/__pycache__/doc-check.cpython-314.pyc
Normal file
Binary file not shown.
|
|
@ -61,7 +61,11 @@ def rel(p):
|
|||
|
||||
# ---------- 1 & 2: 참조 무결성 ----------
|
||||
# `(경로/)파일.md` 또는 `파일.luau` (+ 바로 뒤에 "절 제목"이 오면 절 검사도)
|
||||
REF = re.compile(r'`\.?/?((?:[\w.-]+/)*[\w.@-]+\.(?:md|luau))`(\s*(?:의\s*)?"([^"\n]{2,60})")?')
|
||||
# [2026-08-14] 파일명과 "절 제목"이 **줄바꿈에 걸쳐 있어도** 잡도록 개행 허용.
|
||||
# 예전엔 줄 단위로 돌려서 `base/\nbind-system-plan.md`의 "Length/Offset" 같은
|
||||
# 자연스러운 줄바꿈 인용을 통째로 놓쳤고, 실제로 문서 분할 후 stale 참조
|
||||
# 20여 곳이 이 사각지대로 빠져나갔음(14차 세션 리뷰에서 발견).
|
||||
REF = re.compile(r'`\.?/?((?:[\w.-]+/\s*)*[\w.@-]+\.(?:md|luau))`(\s*(?:의\s*)?"([^"]{2,60})")?')
|
||||
|
||||
def resolve(target, src):
|
||||
"""참조 문자열을 실제 경로로 해석 — 상대/부분 경로를 관대하게 매칭."""
|
||||
|
|
@ -122,28 +126,35 @@ def check_refs(docs):
|
|||
for d in docs:
|
||||
# archive는 히스토리라 절 참조까지 강제하지 않음(파일 존재만)
|
||||
is_archive = '/archive/' in rel(d)
|
||||
for ln, line in enumerate(open(d, encoding='utf-8'), 1):
|
||||
for m in REF.finditer(line):
|
||||
target, _, section = m.group(1), m.group(2), m.group(3)
|
||||
if not interesting(target):
|
||||
continue
|
||||
p = resolve(target, d)
|
||||
if p is None:
|
||||
name = os.path.basename(target)
|
||||
msg = f"{rel(d)}:{ln} 깨진 파일 참조 → `{target}`"
|
||||
(errors if OURS.search(name) else warns).append(
|
||||
msg if OURS.search(name) else msg + " (외부 문서명일 수 있음)")
|
||||
continue
|
||||
if section and not is_archive and p.endswith('.md'):
|
||||
if p not in hcache:
|
||||
hcache[p] = headings(p) or []
|
||||
core = re.sub(r'[`*]', '', section).strip()
|
||||
if not any(core in h for h in hcache[p]):
|
||||
# 절 제목은 의역해서 인용하는 관례가 있어 WARN — 다만
|
||||
# 문서를 쪼개거나 헤딩을 고칠 때 여기가 제일 먼저 걸린다.
|
||||
warns.append(
|
||||
f"{rel(d)}:{ln} 절 참조 불일치 → {os.path.basename(p)}에 "
|
||||
f'"{section}" 절 없음')
|
||||
# 줄 단위가 아니라 **파일 전체**를 한 번에 스캔 — 파일명/절 제목이
|
||||
# 줄바꿈에 걸친 인용도 잡기 위함(위 REF 주석 참고). 줄 번호는 매치
|
||||
# 시작 오프셋으로 역산.
|
||||
text = open(d, encoding='utf-8').read()
|
||||
for m in REF.finditer(text):
|
||||
ln = text.count('\n', 0, m.start()) + 1
|
||||
target, _, section = m.group(1), m.group(2), m.group(3)
|
||||
target = re.sub(r'\s+', '', target)
|
||||
if section is not None:
|
||||
section = re.sub(r'\s+', ' ', section).strip()
|
||||
if not interesting(target):
|
||||
continue
|
||||
p = resolve(target, d)
|
||||
if p is None:
|
||||
name = os.path.basename(target)
|
||||
msg = f"{rel(d)}:{ln} 깨진 파일 참조 → `{target}`"
|
||||
(errors if OURS.search(name) else warns).append(
|
||||
msg if OURS.search(name) else msg + " (외부 문서명일 수 있음)")
|
||||
continue
|
||||
if section and not is_archive and p.endswith('.md'):
|
||||
if p not in hcache:
|
||||
hcache[p] = headings(p) or []
|
||||
core = re.sub(r'[`*]', '', section).strip()
|
||||
if not any(core in h for h in hcache[p]):
|
||||
# 절 제목은 의역해서 인용하는 관례가 있어 WARN — 다만
|
||||
# 문서를 쪼개거나 헤딩을 고칠 때 여기가 제일 먼저 걸린다.
|
||||
warns.append(
|
||||
f"{rel(d)}:{ln} 절 참조 불일치 → {os.path.basename(p)}에 "
|
||||
f'"{section}" 절 없음')
|
||||
|
||||
|
||||
# ---------- 3: 색인 누락 ----------
|
||||
|
|
|
|||
19
CLAUDE.md
19
CLAUDE.md
|
|
@ -213,7 +213,8 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션
|
|||
실제 코드 작성에 재사용.
|
||||
- **[주의] 위 "남은 것은 셋뿐"은 여섯 번째 세션 기준** — 13차 세션에
|
||||
`08`이 `done/`으로 가며 `review-required/`가 비었고, **14차 세션에
|
||||
`04`가 하강 diff 재설계로 전제가 바뀌어 `rewrite-required/`로 갔음**.
|
||||
`04`와 `19`가 하강 diff 재설계로 전제가 바뀌어 `rewrite-required/`로
|
||||
갔음**(`19`는 B 섹션만 낡음, A/C는 유효).
|
||||
**개수의 소스는 항상 `luau-test/STATUS.md`.** 지금 M0 착수를 막는
|
||||
설계 결정은 없고, 0-Y/0-A가 남긴 규약(`base/typing-limits.md`/
|
||||
`base/dispatch-core-plan.md`)은 착수 전 필독.
|
||||
|
|
@ -1097,6 +1098,16 @@ claim**으로 확정. 이걸로 마지막 게이트가 열려 **0-A(하강 diff
|
|||
알고리즘이 백엔드마다 복제됨), 그 실패 모드를 위해 **`HANDLER_PRIORITY_FALLBACK`**
|
||||
신설(사용자 제안 — base 핸들러는 최하위 밴드, 백엔드가 덮어쓰면 언제나
|
||||
이김). 옛 모델은 `archive/dispatch-hintvalue-model-reversed.md`로 이전,
|
||||
스파이크 `04`/`19`는 옛 모델을 검증 중이라 `rewrite-required/`로 이동.
|
||||
**M0 착수를 막는 결정이 이제 없음** — 새로 연 것은 사소한 둘뿐
|
||||
(`Attribute.Merged` 이름 중복, `hintValue` 이름 재검토).
|
||||
스파이크 `04`(체인/`retractFrom`)와 `19`(B 섹션 = Attribute 소유권)는 옛
|
||||
모델을 검증 중이라 `rewrite-required/`로 이동 — `19`의 A/C 섹션은 그대로
|
||||
유효.
|
||||
**M0 착수를 막는 결정이 이제 없음** — 새로 연 것은 사소한 셋뿐
|
||||
(`Attribute.Merged` 이름 중복, `hintValue` 이름 재검토, 그룹 `process`
|
||||
부분 실패 롤백). **[2026-08-14 후속 리뷰 라운드]** 다른 에이전트 감사 +
|
||||
사용자 트레이싱으로 의사코드 결함 3건 수정 — `nameClaims`가 `Relate`의
|
||||
3-인자 계약을 위반, `TagHandler`가 생존 이름의 홀더를 비웠다 되돌려
|
||||
`addTag`를 헛되이 재호출(+`addTag` 배치 누락), 그룹 `process` 부분 실패
|
||||
경로 미문서화. 더 큰 수확은 **`doc-check.py` 자신의 사각지대 발견** —
|
||||
`REF` 정규식이 줄 단위라 파일명과 절 제목이 줄바꿈에 걸친 인용을 통째로
|
||||
놓치고 있었고, 고치자 이번 분할뿐 아니라 **9차 세션 1단계 분할의 stale
|
||||
참조까지** 무더기로 드러나 30여 곳 정정.
|
||||
|
|
|
|||
14
ROADMAP.md
14
ROADMAP.md
|
|
@ -42,7 +42,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
- [ ] props 순회의 "배열 파트 먼저, 해시 파트 나중" 두 패스 계약이 실제
|
||||
Luau 테이블에서 관찰한 대로 동작하는지 확인, `PreRef` pre-pass +
|
||||
일반 `Ref`의 위치 기반 순서까지 최소 스파이크로 검증
|
||||
(2026-08-07 세 번째 세션, `base/bind-system-plan.md` "`phase` 옵션
|
||||
(2026-08-07 세 번째 세션, `base/ref-plan.md` "`phase` 옵션
|
||||
폐기 → 위치로 표현, `PreRef` 신설" 절) — **PreRef pre-pass의 소진은
|
||||
`nil`이 아니라 `None`으로(2026-08-07 열 번째 세션 정정, 사용자가
|
||||
Luau REPL로 반례 제시 — 키가 듬성듬성해지면 순회가 index 순서를
|
||||
|
|
@ -180,7 +180,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
- [ ] `Dispatch/Leaf.luau` — `(i:number, v=Ref/Observer/PreRef)` children-array
|
||||
leaf 매칭 Handler, `StoreBind.luau`와 같은 층위(범용/엔진무관) —
|
||||
quad-base 소속으로 확정(2026-08-08 두 번째 세션, `base/
|
||||
bind-system-plan.md` "Dispatch는 프리미티브가 아니다" 절)
|
||||
dispatch-core-plan.md` "Dispatch는 프리미티브가 아니다" 절)
|
||||
- [ ] `chains`(Relate 기반, `{[inst(weak)]={[k]={[index]={handler, retractor}}}}`
|
||||
— **재귀 깊이 인덱스 → (담당 핸들러, 그가 반환한 retractor 클로저)**) +
|
||||
**3-인자** `Dispatch.retractFrom(inst,k,index)` — 재귀 재-dispatch
|
||||
|
|
@ -213,7 +213,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
- [ ] `store.key` dot-access 타입 추론 확인 — Luau `type function`
|
||||
(`WrapStore`/`ProcessStoreType`)으로 `Store<T>`가 `T`의 각 필드를
|
||||
`Source`로 감싼 레코드 타입을 합성 가능함을 확인(2026-08-12 열일곱
|
||||
번째 세션, `base/bind-system-plan.md` "`store.key` 레코드 필드
|
||||
번째 세션, `base/typing-limits.md` "`store.key` 레코드 필드
|
||||
타이핑" 절) — 실제 문법이 통과하는지는
|
||||
`luau-test`의 `16-type-store-key-typefunction.luau`로 실측 필요
|
||||
- [ ] `:Compute(fn, ...)` — trailing args로 추가 의존성 직접 받는 sugar
|
||||
|
|
@ -328,7 +328,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
|
||||
- [x] **"여러 Slot이 형제로 섞일 때 순서 보장" 해소**(2026-08-09 여섯 번째
|
||||
세션) — `Dispatch.setLength`/`setOffsetSource` 메커니즘, `base/
|
||||
bind-system-plan.md` "Length/Offset" 절. `Slot.Length: State<number>`도
|
||||
dispatch-core-plan.md` "Length/Offset" 절. `Slot.Length: State<number>`도
|
||||
이때 확정(CRUD/`:List` 여부 무관 항상 노출, 순서 계산과 "n개 검색됨"
|
||||
UI 둘 다 겸함) — 구현 시 이 두 API를 `:List`/CRUD의 `raw*`가 호출.
|
||||
- [x] **Slot의 `Add`/`Remove`/`Extract`/`ExtractAll`/`Clear`/`Move`/`Swap`/
|
||||
|
|
@ -377,7 +377,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
위치"(`candidateIndex`, filter로 압축됨), `key`와도 무관(순서/레이아웃
|
||||
전용, 식별 목적 아님) — 문서화 시 셋(원본 raw index/`key`/`updateFn`의
|
||||
`index`)을 혼동하지 않게 주의. **`offset`은 `Slot.Offset`을 그대로
|
||||
전달**(형제 Slot/정적 자식 누적합, `base/bind-system-plan.md`의
|
||||
전달**(형제 Slot/정적 자식 누적합, `base/dispatch-core-plan.md`의
|
||||
"Length/Offset" 절) — `index`/`offset` 둘 다 **raw 값으로만 전달,
|
||||
`Slot`/Handler가 `LayoutOrder` 등을 자동으로 세팅해주지 않음**
|
||||
(2026-08-11 세션 확정 — 자동 바인딩은 컴포넌트가 이미 지정한 값을
|
||||
|
|
@ -443,7 +443,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
요소를 모른 채 충돌하는 gap이 있었음(2026-08-11 세션, `base/
|
||||
slot-plan.md` "CRUD API 확정" 절).
|
||||
- [x] **`recompute` off-by-one 버그 수정**(2026-08-11 세션, `base/
|
||||
bind-system-plan.md` "Length/Offset" 절) — `sum` 누적과
|
||||
dispatch-core-plan.md` "Length/Offset" 절) — `sum` 누적과
|
||||
`offset:Set` 순서가 뒤바뀌어 `Offset`이 자기 자신을 포함해버리던
|
||||
버그(예: 유일한 자식인데도 `Offset`이 0이 아니게 됨) 수정. 재진입
|
||||
방지 가드는 검토 후 기각 — 각 Slot이 `Relate(자기 자신)`으로
|
||||
|
|
@ -526,7 +526,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
- [ ] `Ref.luau`(`.Value` 읽기 전용 필드 + `:Set(value)`/`:Callback(fn)`/
|
||||
`:Wait(thread?)`, 전부 self 반환) + `PreRef.luau`(별도 파일, Ref
|
||||
런타임 재사용 + children 배열 전용, Modifier/Store 타입 차단,
|
||||
위치 무관 호이스팅 pre-pass — `base/bind-system-plan.md` "`phase`
|
||||
위치 무관 호이스팅 pre-pass — `base/ref-plan.md` "`phase`
|
||||
옵션 폐기 → 위치로 표현, `PreRef` 신설" 절 + "API 모양" 절)
|
||||
- [ ] `(v=Ref)` 매치 핸들러 — children 배열의 숫자 슬롯에 놓인
|
||||
`Ref(default)` 인스턴스를 인식해 바인드(별도 `CreatedRef` 래퍼
|
||||
|
|
|
|||
Loading…
Reference in a new issue