Commit graph

38 commits

Author SHA1 Message Date
eb321e1747
qa: 7라운드 발견 52건 처리·반영 — Store 재설계 시도와 철회, 타입 함수 원칙 신설
`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
2026-08-25 23:40:19 +09:00
8cc388cfab
qa: 7라운드 발견 52건 검증 패스 — 유효 49 / 범위 축소 2 (H-55~H-106)
발견을 더 찾는 패스가 아니라 이미 나온 것이 유효한지만 판정한다. 52건
전부를 한 세션이 한 컨텍스트에서 썼으므로 쓴 세션 자신은 자기가 뭘 잘못
봤는지 모른다(conventions.md가 반복 확인한 그 패턴). 판정 뒤 사용자가
batch로 결정하므로 무효 항목이 섞이면 그 비용을 그대로 문다.

**재실행이 아니라 대조로 판정했다.** audit/handtrace-round7-reference-impl/
README가 못박은 대로, 4·5·6차 패스의 발견은 스파이크를 다시 돌려서가 아니라
전사물(core/dispatch/chain.luau)과 base/ 확정 의사코드를 줄 단위로 맞춰
판정했다. Luau 언어 동작·저장소 상태를 주장하는 것(H-71/H-77/H-73/H-75/
H-76/H-78/H-83, 타입 스파이크 전량)만 직접 재실행해 교차 확인했다.

## 전사 대조 결과 — 전사 오류로 생긴 발견은 없다

State:_receive / _recompute / Get, GateNode _flush/_receive, Source:Set의
bit32.bnot(-rev), emitDown 스냅샷, getOffsetAt/recompute(에러 메시지까지)/
setLength/setOffsetSource, chain의 process/retractFrom — 전부 원문과 일치.

README가 신고한 의도적 차이 3개 외에 **미신고 차이 3개**를 찾았으나 셋 다
발견을 만들지 않는다: (1) Blocker:Policy가 2인자(죽은 인자, 아무도 안 넘김),
(2) Blocker.handles가 강한 배열(문서는 weak — H-63(2)를 오히려 가림),
(3) M.Observer가 즉시 1회 실행을 안 하고 self.owner 강참조를 둠(앞은
dispatch.luau가 명시 호출로 등가 보정, 뒤는 GC 문제를 가리는 방향).

## 판정

- 유효 49 / 범위 축소 2 / 무효 0 / 중복 0.
- **H-87 범위 축소** — "코퍼스가 error 경로를 어느 쪽도 안 다룬다"가 거짓.
  slot-plan.md가 같은 실패 모드를 명시하고 사용자가 2026-08-21에 "pcall로
  감싸지 않는다"로 이미 판단했다. 갈래 (a)가 든 두 자리 중 하나가 닫힌
  결정이라, 그대로 올리면 사용자가 자기 판단을 모른 채 재결정하게 된다.
  남는 진짜 질문은 그 판단을 Dispatch.drive(자가치유 없음)까지 확장할지.
- **H-105 범위 축소** — 개수를 직접 셌다. 영어 6은 맞지만 한국어는 17이
  아니라 약 23이고, 17+6이 애초에 "42곳"과 안 맞는다. 현상과 결정
  필요성은 유효.
- **H-88 정정** — 셋 문서에 error 0건은 참이나 "코퍼스에 한 줄도 없다"는
  과하다. 선례가 하나 있고 그건 갈래 (b) 쪽을 가리킨다.
- **H-104 정정** — "level 인자가 하나도 없다"는 전수 확인으로 참,
  "42곳"만 약 29곳으로 정정.

## batch용 요약

번호순 52건이 아니라 결정 단위 12묶음(A~L)으로 재편성했다. 같은 묶음은
결론이 서로를 규정하므로 따로 답하면 갈라진다. 몇 가지 눈에 띄는 것:

- H-55(버리기)와 H-86(읽기)은 emit(commit) -> boolean 하나로 같이 닫힌다.
- H-56/H-62/H-61은 "전파 루프 의사코드" 한 블록을 쓰면 같이 닫힌다.
- H-73을 탑레벨 함수로 닫으면 H-74가 통째로 소멸한다.
- H-58과 H-59는 Ref 콜백 등록을 공유 헬퍼로 빼는 같은 수정으로 닫히고,
  그 전에 H-60(Rerun 정의)이 있어야 나머지를 쓸 문장이 생긴다.
- H-71의 해법 (b)가 H-77엔 안 듣는다(실측) — Relate 슬롯 셋을 나눠 쓸 것.
- H-78이 제일 급하다: 지금 이 저장소에서 smoke 2개가 안 돌고
  luau-analyze가 거짓 클린을 준다(검증 세션에서 재현).
- H-66/H-67/H-82/H-91은 결정 불필요(근거·표기만 정정).

.claude/README.md의 qa-request 행에도 등재. doc-check.py ERROR 0.

Co-authored-by: qwreey <me@qwreey.moe>
2026-08-25 12:26:53 +09:00
6f939a8d2d
audit: 7라운드 4·5·6차 패스의 참조 구현·스파이크를 저장소로 (재현 가능하게)
지금까지 세션 스크래치패드에만 있던 것을
.claude/audit/handtrace-round7-reference-impl/로 옮긴다. 다음 세션이
발견 52건을 판정하려면 이게 있어야 한다.

구성:
- spikes/core.luau      — 반응형 코어(state-epoch §2~§5, 전파 모델,
                          gate 4·8번, Blocker, H-23 스냅샷)
- spikes/dispatch.luau  — Length/Offset 부기(getOffsetAt 접두합 캐시,
                          recompute, setLength/setOffsetSource, 배치 게이팅)
- spikes/chain.luau     — Dispatch 체인(하강 diff, retractFrom)
- spikes/*.luau         — 시나리오 29개(런타임 18 + 타입 11)
- RUN-runtime.txt / RUN-typecheck.txt — 2026-08-25 실행 결과 스냅샷
- README.md             — 원문 대조표 + 스파이크→발견 대조표

⚠️ README가 가장 중요하다: 이 참조 구현은 base/의 확정 의사코드를 손으로
옮긴 **전사물**이라 그 자체가 틀렸을 수 있다. 그래서 "재실행은 검증이
아니다"를 못박고, (a) 어느 파일이 어느 절을 옮긴 것인지 대조표와
(b) 문서와 **의도적으로** 다른 곳 3개(H-72 때문에 임시로 둔 PeekDiffers,
H-102의 A/B 대조를 위한 위치 박스, 생명주기 게이팅 부재=H-97)를 같이
싣는다. 그 셋 외의 모든 차이는 전사 오류 후보다.

부수로 round7 문서 머리에 이 폴더 포인터와 같은 경고를 달았고,
.claude/README.md의 audit/ 행에도 등재했다.

Co-authored-by: qwreey <me@qwreey.moe>
2026-08-25 12:00:08 +09:00
ec88253ad2
qa: 손 트레이싱 7라운드 6차 패스 — 부기 실측 + error 계약, 발견 6건 (H-101~H-106)
1~5차가 안 쓴 각도 둘:
(1) 4차의 참조 구현을 M2→M3 경계까지 이어 붙였다 — Length/Offset 부기
    (접두합 캐시 getOffsetAt / recompute / setLength / 배치 게이팅)와
    Dispatch 체인(하강 diff / retractFrom)을 의사코드 그대로 돌렸다.
    이 부기는 2026-08-21 접두합 캐시 도입과 6라운드 H-3/H-5/H-19 수정을
    거친 뒤 한 번도 실행된 적이 없다.
(2) quad 자신이 던지기로 확정한 error 42곳의 계약 감사 — 4차가 "사용자
    콜백이 던졌을 때"를 봤다면 이번엔 반대 방향이다.

🔴 H-101 recompute가 순회 도중 재진입되면 바깥 호출의 꼬리가
     ownerKey.Length를 낡은 합계로 덮어쓴다(합계 3인데 2가 남음).
     offset은 다 맞고 Length만 틀려서, 증상은 한 단계 위 형제부터 난다.
     H-85("긴 연산의 마지막 줄이 도중에 바뀐 값을 덮어쓴다")와 같은 계열
🔴 H-102 spliceArraysDown이 bk.observers를 옮기지만 setLength가 만든
     gatedRecompute 클로저에 박힌 position 인덱스는 안 고쳐진다 → 이후
     그 자리의 길이 변경이 캐시를 엉뚱한 범위로 무효화해 뒤 형제 offset이
     낡은 채로 남는다(A/B 대조: 1 vs 기대 5). 규칙 셋(2026-08-18 observers
     이동, 2026-08-21 접두합, 2026-08-24 H-3)이 각각 옳은데 겹치는 자리가
     아무 목록에도 없었다
🟡 H-103 process가 던지면 체인 슬롯에 NOOP retractor가 영구히 남아 그
     자리가 잡은 자원의 정리가 사라진다. 미주입 op 스텁(확정된 정상 동작)
     때문에 흔하다 — attribute 이름이 영구 잠기고, 다음 사용자에겐 진짜
     원인이 아니라 "이미 다른 소유자가 잡고 있다"는 엉뚱한 에러가 보인다
🟡 H-104 error 42곳 전부에 level 인자가 없다 → 전부 quad 내부 줄을
     가리킨다. 사용자 입력 검증과 내부 불변식 위반을 갈라야 한다
🟡 H-105 error 메시지 언어가 이미 갈려 있다(한국어 17 / 영어 6).
     conventions.md의 "사용자=본인"과 "라이브러리 사용자"가 다르므로
     에이전트가 임의로 정할 수 없는 항목
🟢 H-106 C-6이 승격한 "부기가 깨졌음" 진단이 getOffsetAt 경로에선 우회돼
     익명 산술 에러로 먼저 터진다

부록에 "문제가 없던 것" 5건 — 접두합 캐시 자체의 정확성, 배치 게이팅이
실제로 O(N)으로 접히는 것, 등록 시점 길이가 최종이 아니어도 자가교정되는
것, 빈 Slot 방어, Dispatch 체인 정상 경로.

아무것도 base/에 반영하지 않았다 — 회신 대기.

Co-authored-by: qwreey <me@qwreey.moe>
2026-08-25 11:53:58 +09:00
3247ea5907
qa: 손 트레이싱 7라운드 5차 패스 — 타입 실측, 발견 7건 (H-94~H-100)
1~4차가 안 쓴 각도: **확정된 M2 표면을 문서에 적힌 시그니처 그대로 Luau
타입으로 선언하고 `luau-analyze`에 걸었다.** 2차 패스도 타입을 봤지만
대상이 Store의 타입 합성 하나였다(H-73~H-76). 이번엔 호출부 관점 —
"확정된 타입이 확정된 관용구를 통과시키는가"다. 루트 .luaurc가
languageMode: strict라 아래 진단은 프로젝트에 그대로 적용된다.

🔴 H-94 `__call` 테이블은 `(State<T>) -> U` 자리에 안 들어간다 →
     `state:Apply(Debounce{...})` 확정 관용구가 타입에러. gate-plan이
     "확인할 필요도 없어졌다"며 접은 불확실성이 Debounce/Throttle 쪽에
     그대로 남아 있었다(팩토리에 Flush/Cancel을 붙이는 이상 콜러블
     테이블일 수밖에 없다). 런타임은 멀쩡하고 타입만 막힌다
🔴 H-95 콜백이 "선언보다 적게 반환"하면 strict에서 에러 — Effect의
     `fn -> (() -> ())?`는 cleanup 없는 흔한 모양이, :List의
     `updateFn -> (T?, UD?)`는 userdata를 안 쓰는 모든 모양이 안 통과한다.
     통과하는 두 형태(가변 반환 팩 / 함수 타입 유니온)를 대조군으로 확인
🟡 H-96 trailing deps가 붙는 순간 콜백 파라미터 무주석 추론이 깨진다 —
     typing-limits ②쪼개기가 푸는 범위 밖인데 경계가 어디에도 없다.
     2차 패스 부록의 "무주석 콜백도 살아 있다"도 이 구분을 안 했다
🟡 H-97 M2의 `mock 대상 테스트`가 전파 루프를 한 번도 못 돈다 — 루프가
     매 발화마다 부르는 canExecute가 M8에서만 구현되고, 미주입 슬롯의
     확정 기본값은 에러 스텁이다. 커밋된 mock엔 재료(Signal/Connection)가
     이미 있는데 쓰는 쪽이 없다
🟡 H-98 `:Subscribe()`의 공개 계약("참조를 안 담아도 계속 돎")이 중간
     State GC 미해결에 종속 — 잘못 닫히면 "GC도 안 되고 발화도 안 하는"
     조합이 된다(실제 collectgarbage로 재현)
🟢 H-99 Observer가 파일 자리를 못 받았고 :Subscribe() 전역 강참조
     레지스트리의 주인이 어디에도 없다 (Effect.luau는 있는데)
🟢 H-100 `{[Source<T>]: true}`가 `{[Epoch]: true}` 자리에 안 들어간다
     (인덱서 키 불변). 단일 Epoch 전달과 §2의 구조적 만족은 성립 확인

부록에 "걸어봤는데 문제가 없던 것" 6건 — state:Gate + blocker:Policy가
타입으로 성립하고 gate-plan의 2026-08-24 정정(오답 b.Policy)은 타입이
잡아준다, source:Apply에 State용 팩토리 통과, Effect의 이형 deps는 타입으로
표현 가능, 게이트 2겹 unfold가 어느 순서로 풀려도 안 샌다 등.

아무것도 base/에 반영하지 않았다 — 회신 대기.

Co-authored-by: qwreey <me@qwreey.moe>
2026-08-25 11:35:00 +09:00
03219af33f
qa: 손 트레이싱 7라운드 4차 패스 — 참조 구현 실측 + 예외 경로, 발견 9건 (H-85~H-93)
1~3차가 안 쓴 각도 둘로 같은 라운드를 이어서 돌렸다.

(1) M2 코어(state-epoch §2~§5 / 전파 모델 / gate 4·8번 / Blocker)를
    방어 코드 없이 문서 그대로 옮긴 참조 구현을 Luau로 짜서 시나리오를
    돌렸다. 손 트레이싱이 못 보는 "여러 규칙이 동시에 걸리는 자리"가
    여기서 나온다. 참조 구현의 대조군은 §1이 예고한 다이아몬드 glitch가
    실제로 사라지는지 확인한 것.
(2) 예외 경로 — 반응형 문서 셋에 "error"가 0건이다. 사용자 콜백이
    던졌을 때 반쯤 갱신된 부기가 무엇을 남기는지 정한 문장이 없다.

🔴 H-85 재계산 끝의 `rawInvalid = false`가 재계산 도중 도착한 무효화를
     지운다 → 캐시가 다음 Set까지 영구 stale (재현 + 한 줄 수정 대조까지)
🔴 H-86 정책이 "보류분이 있는가"를 읽을 수 없어 Throttle의 창이 idle로
     못 돌아온다 → leading 영구 소실 + 타이머 체인이 안 끝나 8절의
     "유계 GC" 주장도 깨짐 (H-55와 별개 — 그쪽은 버리기, 이쪽은 읽기)
🔴 H-87 배치 게이팅 도중 error → 그 owner의 Blocker가 영구 On,
     recompute가 조용히 영영 안 돎 (문서는 yield만 UB로 못박음)
🟡 H-88 전파 중 콜백이 던지면 나머지 구독자는 영구 침묵(값만 자가치유)
🟡 H-89 flush 중 error → 떼어낸 배치 소멸, :Sync와 전파의 순서 미정
🟡 H-90 Effect의 dedup이 루트 에포크 기준이라 dep 하나만 게이팅하면
     같은 루트를 공유하는 다른 dep 때문에 게이팅이 무력화
🟢 H-91 §8의 "항상 Get이 최신"이 과한 서술 (Animate가 반례를 설계로 씀)
🟢 H-92 구독자 스냅샷이 emit마다·노드마다 배열 하나 — §2의 GC 근거와 충돌
🟢 H-93 EpochMap 키가 weak라 중간 State GC 미해결이 "최신이라고 오판"
     으로도 나타남 (실측 스파이크 요구가 하나 늘어남)

부록에 "돌려봤는데 문제가 없던 것" 6건(다이아몬드 glitch 해소, 유한
재진입 안전, Blocker 정책 배선, 빈 배치 무통지, Store lazy __index의
identity 안정성, GateNode 순환 require 없음)도 같이 남겼다.

부수로 .claude/README.md의 qa-request 행에 7라운드가 아예 빠져 있던 걸
채웠다(1~3차 패스 때부터 비어 있었음).

아무것도 base/에 반영하지 않았다 — 회신 대기.

Co-authored-by: qwreey <me@qwreey.moe>
2026-08-25 11:18:05 +09:00
1adafea53d
qa: 손 트레이싱 7라운드 3차 패스 — 실측 8건 추가 (H-77~H-84, 회신 대기)
2차 패스와 같은 방식으로 7라운드 문서에 **3차 패스**로 이어붙였다(발견
번호도 계속 이어서 매김). 여전히 아무것도 반영하지 않았고 README 색인도
안 넣음(결정 후 followup과 함께). doc-check ERROR 0.

## 이번 패스의 각도

2차는 문서가 주장하는 Luau 동작을 **새로 짠 최소 재현**으로 걸었다.
3차는 각도를 둘로 바꿨다 — (1) **저장소에 이미 있는 것을 그대로 돌린다**
(`quad-base/test/smoke.*`, `luau-test/done/` 전량, `doc-check.py`, 커밋된
`quad-base/src/*.luau`의 `luau-analyze`). 즉 "설계가 맞나"가 아니라
"지금 이 저장소가 문서가 말하는 상태인가"를 본다. (2) 1차가 범위에서
명시적으로 뺐던 **M2 *소비자* 문서**(`modifier`/`attribute`/`tween`/
`onchange`/`quad-types`/`architecture`/`project-setup`/`module-lifecycle`)
— M2를 만드는 문서가 아니라 M2가 만든 걸 쓰는 문서라, "소비자가
요구하는데 M2 설계엔 없는 것"이 여기서만 보인다.

검증 중 `pesde` 심볼릭 링크를 실제 디렉토리로 치환한 구간이 있으나 전부
원상복구했다(링크 20개, 전부 `.gitignore` 대상이라 저장소 파일 무변경).

## 발견

- `H-77` 🔴 **`Relate`의 *내부 키*가 `inst`를 되참조하면 `SetStrong`/
  `SetWeak` 둘 다 샌다.** `H-71`은 *값*의 되참조였고 결론이 "`SetWeak`은
  안전"이었는데, `WeakMap`이 `__mode = "v"`라 **키는 언제나 강함**이다 —
  즉 `H-71`의 해법 (b)가 여기선 전혀 안 듣는다. `relate-plan.md`의 규칙은
  위험을 전부 "값" 기준으로만 서술해 이 슬롯이 아예 없다. 커밋된
  `Relate.luau`로 50/50, 그리고 **커밋된 `init.luau`의 `RunInit`이 실제로
  물린다** — `initFn`이 module을 캡처하면 30/30이 안 죽고, 그 파일 주석
  ("module이 GC되면 이 기록도 같이 사라짐")이 지금 거짓이다.
- `H-78` 🔴 **커밋된 M1 스모크 2개와 `done/`의 타입 스파이크 `23`이 지금
  저장소 상태에서 안 돈다.** 원인은 pesde 워크스페이스 심볼릭 링크(이
  `luau` CLI가 require 경로의 심볼릭 링크를 디렉토리든 파일이든 해소
  안 함 — 격리 확인). 더 나쁜 쪽은 타입으로, `luau-analyze`가 `Quad`/
  `CheckVersion`을 아예 못 봐 스파이크 `23`의 음성 대조군이 한 건도 안
  뜬다 = "진단 0건"으로 오독된다. 워크어라운드를 적용하면 전부 정상이라
  설계가 아니라 **절차가 없는 것**이고, 그 치환은 `project-setup-plan.md`가
  스스로 "정식화 안 함"이라 밝힌 상태다. `ROADMAP.md` M1의 "전부 PASS"는
  날짜도 전제조건도 없다.
- `H-79` 🟡 **`Store`에 열거 표면이 없는데** 그룹 `Attribute(...)`/
  `:NameMap()`이 그걸 요구한다 — lazy `__index` 때문에 키 집합이 접근
  이력에 좌우되고(실측 0개/1개/2개), `defaults` 없이 만든 Store에선 빈
  맵이 된다. `:NameMap()`이 스냅샷이냐 라이브냐도 미정.
- `H-80` 🟡 M2가 `quad-types`의 `Quad`에 추가할 목록이 `Source`/`State`/
  `Store`뿐 — **`State`는 런타임 값이 아예 없고**(타입 재수출만 가능),
  M2가 실제로 얹는 나머지 탑레벨 값(`Effect`·`is*` 전량·`bindLifetime`
  4종·`Relate`·`Blocker`)이 전부 빠져 `H-25`가 만든 그 벽에 똑같이
  부딪힌다. 부수로 옛 표기 `state(state)`가 두 문서에 남아 있다.
- `H-81` 🟡 `isModifier` 런타임 가드의 적용 지점이 **전부 M2 코드**
  (`Source:Set`/Store 생성/`:Compute` 캐싱)인데 체크박스는 M7에만 있고
  M2 체크리스트엔 한 줄도 없다(그 사이 M4가 가드 없이 돈다). 게다가
  적용 지점 목록이 두 문서에서 갈린다 — `source-state-plan.md`는 독립
  `Source(someModifier)` 생성자도 포함하는데 `modifier-plan.md` 7번엔
  그 자리가 없다.
- `H-82` 🟢 `:With`를 실노드로 확정한 **근거 2번이 pass-through 노드엔
  성립하지 않는다** — 계산 함수가 없으니 공유될 계산도 없다. 2026-08-14
  재작성이 "근거가 더 강해졌다"면서 실제론 정확한 서술(순회 비용)에서
  부정확한 서술(중복 계산)로 내려갔다. 결론은 안 바뀜(근거 1·3 유효).
- `H-83` 🟢 확정된 Store 구현 스케치를 그대로 쓰면 무인자 `Store()`가
  `table.clone(nil)`로 크래시한다 — 같은 절이 `defaults`는 선택이라고
  확정해뒀다.
- `H-84` 🟢 `:With(...)`/`state:Block(b)`/`Source:Emit()`이 M2 체크리스트에
  개별 항목으로 없다 — `H-55`/`H-68`/"중간 State GC"의 결론이 각각 어디로
  가야 하는지가 로드맵에 자리가 없다는 뜻.

## 부록 — 돌려봤는데 문제가 없던 것

다음 라운드가 같은 곳을 다시 파지 않도록 남겼다 — `bit32.bnot(-rev)`
실측 표 정확, `doc-check.py` ERROR 0, `done/` 런타임 스파이크 8개 전원
재통과, `STATUS.md` 개수 표가 실제 폴더와 일치, 워크어라운드 적용 시
커밋된 `quad-base/src/*.luau` 진단 0건, `Relate.luau` 구현이
`relate-plan.md`의 "실제 구조" 절과 일치(문제는 코드가 아니라 규칙 공백).

Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_012oLwATeQdq9TCFdENPutFG
2026-08-25 10:50:56 +09:00
a39ad034b4
qa: 손 트레이싱 7라운드 2차 패스 — 실측 6건 추가 (H-71~H-76, 회신 대기)
별도 라운드로 쪼개지 않고 7라운드 문서에 **2차 패스**로 이어붙였다
(6라운드가 패스를 이어 붙인 것과 같은 방식, 발견 번호도 패스를 가로질러
이어서 매김). 여전히 아무것도 반영하지 않았고 README 색인도 안 넣음
(결정 후 followup과 함께). doc-check ERROR 0.

## 이번 패스의 각도

1차는 문서 대 문서 손 트레이싱이었고, 2차는 **문서가 "확인했다"고 적어둔
런타임/타입 주장을 실제로 `luau`/`luau-analyze`에 걸어봤다** — 6건 중
5건이 추측이 아니라 실행 결과이고 재현 코드를 항목마다 넣었다. 부수로
드러난 패턴: **"검증 완료"로 인용되는 스파이크들이 정작 위험한 모양을
안 테스트했다**(`07`은 되참조 없는 payload만, `16`은 대입을 아예 안 함).

## 발견

- `H-71` 🔴 **단일 `Relate` 안의 자기참조(값→키)는 GC 안전하지 않다.**
  `relate-plan.md`의 "위험한 패턴" 절이 안전하다고 단언한 바로 그 모양이
  커밋된 `Relate.luau`로 50/50 누수(SetWeak은 0). 그 절은 "Luau엔
  ephemeron이 없다"를 근거로 두-`Relate` 순환을 금지하면서 단일 `Relate`
  자기참조엔 ephemeron 의미론을 가정한다. `RefLeafHandler`가 정확히 그
  모양이고(`SetStrong(inst,k,ref)` + `ref.Value = inst`), 정상 `Destroy`
  경로엔 그 기록을 지우는 코드가 없다.
- `H-72` 🟡 `GateNode`가 `state-epoch-plan.md` §4 규칙 1~3을 돌려면
  `emitEpochMap`을 갱신 없이 비교해야 하는데 `EpochMap` 표면
  (`Update`/`Refresh`/`Sync`/`TrackFrom`)에 그 연산이 없다.
- `H-73` 🟡 `store:GetDynamic<<T>>(name): Source<T>`는 콜론이든 탑레벨이든
  `T`를 바인딩할 수 없다 — Luau엔 호출부 명시 타입 인자가 없고 기대 타입
  추론도 안 돼 `T`가 `unknown`으로 떨어진다. 콜론 쪽은 합성 타입에 키
  자체가 없어 `Key 'GetDynamic' not found`. `question.md` 최우선 항목의
  실측 근거.
- `H-74` 🟡 eager `defaults` 경로가 `__index`를 통째로 우회해 예약 키
  방어가 성립 안 함 — `Store({GetDynamic = 3}):GetDynamic("x")`가
  `attempt to call a table value`. Modifier와 "정확히 같은 구조"가 아니다.
- `H-75` 🔴 `WrapStore`가 스파이크 `16`의 평평한 모양이면
  `store.key:Compute(무주석 콜백)`이 깨진다 — `typing-limits.md` ②쪼개기를
  `type function` 안에서도 해야 한다(그 모양이면 통과함까지 확인).
- `H-76` 🔴 `type function`은 바깥 타입 별칭을 참조 못 하므로 `Source<T>`
  전 표면을 손으로 중복 유지해야 하고, 메소드 self 파라미터가 불변이라
  `Revision` 하나만 빠뜨려도 `store.key`가 `State<T>` 파라미터 자리에 안
  들어간다. 스파이크 `16`은 그 대입을 한 번도 안 해봤다. 충실한 재현 +
  정합성 단언 한 줄이면 닫힌다(실측 확인).

## 부록 — 열려 있던 실측 항목 하나는 성립

스파이크 `15`의 (B)(이형 다중 deps를 제네릭 팩으로 좁혀 받기)를 같이
재봤고 **된다** — 확정된 `previous?`-앞-팩 순서 그대로, 음성 대조군도
정확히 잡힌다.

Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_012oLwATeQdq9TCFdENPutFG
2026-08-25 10:01:36 +09:00
34e176398e
qa: 손 트레이싱 7라운드 — M2 범위·M2→M3 경계, 발견 16건 (H-55~H-70, 회신 대기)
6라운드가 안 쓴 각도(시간축 겹치기 / 정책 분리 후 권한 경계 / 두 진입
경로 중 한쪽만 배선 / 두 문서가 다른 말로 부르는 자리)로 반응형 코어
문서를 다시 돌렸다. 아무것도 반영하지 않았고 README 색인도 아직 안 넣음
(결정 후 followup과 함께). doc-check ERROR 0.

Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01Sv6h6YbuzYwPaEoRDQBQsc
2026-08-25 00:53:10 +09:00
19cd046275
design: 손 트레이싱 6라운드 전량 처리·반영 — H-1~H-54 확정, M2/M3 설계 게이트 해소
사용자 요청("같이 하나하나 처리해나가보자. 질문 모드로 계속 물어보며")으로
발견 보고 `H-1`~`H-54`를 문항지로 만들지 않고 **갈래 선택이 필요한 것만 급한
순서로** 물어 전량 결정하고 `base/` 24개 문서에 반영했다. 결정과 근거는
`qa-request/pre-implementation-handtrace-round6-followup.md`가 소스이고,
진행 경위와 사용자 발언 원문은
`session/2026-08-24-01-handtrace-round6-resolution.md`.

**M2/M3를 막던 것이 전부 닫혔다** — 말단 핸들러 4종의 `setLength`/
`setOffsetSource` 미등록(`H-39`), `New(): Quad`가 닫힌 타입이라 `quad.Dispatch`가
타입에러인 것(`H-25`), `Effect`의 leaf 사망 cleanup 배선 부재(`H-11`),
`:List`의 좌표계 결함 둘(`H-1`/`H-2`).

## 구조가 바뀐 것 넷

- **`slot._elemIndex`**(물리 요소→인덱스 역방향 맵) 신설로 `indexOfRaw`가 O(1)
  기본 경로가 되고, `:List`의 `keyIndex`는 **단순 키 집합(`prevKeys`)**으로
  강등. 사용자 역제안 — *"raw* 가 층위를 알아야할 이유를 모르겠는 상태 …
  realElem->index 해시맵을 만들어주고, index 밀고 당기는 동작에서 이걸 같이
  업데이트해주는 편이"*. 맵을 `_elements`와 같은 층에 두니 층 분리가 오히려
  깨끗해졌다
- **`_mounted`가 "물리 인스턴스 유무"만 뜻하게 좁혀지고 `slot._physicalTarget`
  신설** — 상태가 셋이 됐다(미실체화/실체화/마운트). `raw*`는 부기를 실체화
  시점부터 항상 하고 `native*`만 가른다. 그래야 최초 population 중
  `getOffsetAt`이 성립해 `updateFn`의 `index`를 거기서 뽑을 수 있다
- **`Ref.Callbacks`가 해시맵 셋 + `:Uncallback`**(사용자 발견) — 해제가 O(1)이
  되고 `ref-plan.md`의 `#t` border 실측 항목이 폐기됐다
- **`blocker:Policy(emit)` 노출** — `Debounce`/`Throttle`이 emit을 안 쥐고 자기
  Blocker를 On/Off만 하는 정책이 된다. `Gate`엔 `Flush`/`Cancel`을 안 둔다

요소 타입 검증은 블랙리스트에서 **주입 술어 `isInst` 기반 화이트리스트**로
뒤집혔고(`H-40`), 주입 op이 둘 늘었다(`isInst`/`onDestroying` — 조합 폴백이
불가능해 미주입이면 에러).

## 사용자가 에이전트 갈래를 뒤집은 자리가 여럿

`H-1`(세 갈래가 전부 차선), `H-2`(*"부기 확정에서 length 를 확정해도 되는거
아님?"* — 부기와 물리 마운트를 분리하라는 되물음), `H-40`(브랜드 판정이
2026-08-21 인스턴스 브랜드 재작성 이후 성립 불가임을 지적), `H-33`(제 중첩
합성안이 unblock 시 디바운스 창을 새로 시작시켜 창이 안 끝난다는 지적).
`Ref.Callbacks` 해시맵화와 `Ref` 콜백의 `canExecute` 확인은 사용자가 먼저 발견.

## 반영 후 재검토 — `/code-review high` 7건 + 감사 9라운드 34건

**`/code-review high` 7건 중 셋이 이번 반영이 만든 회귀**였다 — 상태가 셋이
됐다고 산문에 쓰고 코드엔 경계 하나만 남겨 `Slot { frameA }` 생성자가
크래시하던 것, `native*`를 `_mounted`로 가리면서 그게 곧 파괴였다는 걸 놓쳐
영구 누수를 만든 것, "`:List`와 CRUD는 상호배타"라며 승인받은 분기가
**재마운트 경로를 안 봐서** 포탈을 깬 것. 셋 다 코퍼스 정합성 각도로는
구조적으로 안 보이는 종류라 *"`/code-review`는 감사자를 대체하지 않는다"*가
실측으로 재확인됐다. 같은 리뷰가 `H-11`의 두 결정이 서로 모순임을 잡아
재결정했다 — *"`EffectHandle`이 자기 `bindLifetime` 직후에 건다"*는 **그 호출부가
실재하지 않았고**, 사용자 판단으로 `bindLifetime`/`unbindLifetime`이 `isEffect`를
보고 직접 처리하는 것으로 바뀌었다.

**`quad-doc-auditor` 감사 루프는 9라운드에서 새 발견 0건으로 수렴**(라운드별
5→7→2→2→3→9→2→4→0, 각도와 목록은 followup의 E절). 한 턴에 하나씩 돌리고
라운드마다 각도를 바꿨다. 가장 많이 잡은 6라운드(9건)는 *"이 체크박스로 코드를
짜면 무엇이 나오는가"*를 물은 라운드였고, 그때 `ROADMAP.md`의 미완료 항목이
대거 stale인 게 드러났다(폐기된 `pos` 공식이 "확정"으로, 접두합 캐시 무효화
계약이 통째로 부재, `bindLifetime`이 "둘만 한다"고 적혀 `H-11`과 직접 모순).

**반복된 실패 패턴은 하나 — "고쳐야 할 자리가 N개인데 일부만 고쳤다"**:
배너를 달고 그 배너가 부정하는 문장을 안 고침(1라운드), `base/` 19개를 바꾸고
`.claude/README.md`를 한 줄도 안 고침(2라운드), 그 README를 고칠 때 11행 중
6행만(7·8라운드). 셋 다 핸드오버 체크리스트가 명시적으로 경고하는 항목이라,
규율이 없어서가 아니라 지켰는지 스스로 확인하지 않아서 생긴 실패다.

## 백로그 하나

`Fallback`/`Traceback` 중 생성된 부분 트리의 회수(`H-26`) — 그 둘이 슈가라
구현 시점에 같이 다룬다. 같이 확인된 것: `dispatch-core-plan.md`가 잔여 부기를
인스턴스 GC가 정리한다고 적은 문장은 **gcconn 불멸성과 양립하지 않는 틀린
안전망 주장**이라 삭제했다.

**M2 착수를 막는 설계 항목은 이제 없다** — 남은 건 `question.md` 2번(M2↔M3
양방향 의존, 마일스톤 순서)뿐이다. `doc-check.py` ERROR 0.

Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01Jjrec9xAS7TZstMx5gi3cm
2026-08-24 18:54:14 +09:00
068971fd56
qa: 손 트레이싱 6라운드 3·4차 패스 — 미탐색 문서 전수 + 축 전환, 발견 34건 (실측 검증 포함)
사용자 요청으로 6라운드에 두 패스를 더 이어붙였다. 3차는 *"지금 나온거
이외에 더 문제될만한게 있는지 … 그러고 나서 모든 요소들이 진짜 실존하는
문제인지 사실확인을 해줘"*, 4차는 *"문제를 계속 쌓고, 한번에 몰아 처리하는게
효율적이라, 한번 더 순회하고"*.

**`base/`는 여전히 한 줄도 안 고쳤다** — 전부 발견 보고이고 회신 대기.

## 3차 패스 (`H-21`~`H-38`) — 1·2차가 한 번도 안 연 문서 전체

`store-plan.md`/`source-state-plan.md` 코어, `modifier-plan.md`/
`component-composition-plan.md`/`bind-system-plan.md`, `event-plan.md`/
`onchange-plan.md`/`relate-plan.md`/`module-lifecycle-plan.md`/
`fallback-plan.md`/`lifecycle-hooks-plan.md`, `tween-plan.md`/
`debounce-throttle-plan.md`, `typing-limits.md`/`quad-types-plan.md`/
`project-setup-plan.md`와 **실제 커밋된 M1 코드**, `slot-plan.md`의 공개
CRUD·`:Single`·래핑·`dispose` 계층 + 통합 시나리오 넷.

**이번부터 추론으로 끝내지 않고 로컬 `luau` 0.734 / `luau-analyze`로 직접
재현했다.** 그 결과 **기존 `H-2`의 크래시 주장이 틀렸음이 드러났다** —
`table.insert(t, 0, x)`는 Luau에서 안 터지고 조용히 `t[0]`에 쓴다. 결론과
고칠 방향은 유효하지만 실제 결과는 크래시가 아니라 **영구 고아**다(claim은
됐는데 모든 walk가 `ipairs`라 아무도 안 닿음).

🔴 다섯: `unwrapElement`가 Instance에서 크래시(Roblox는 없는 멤버 인덱싱이
에러) / 기본 identity `updateFn`이 `KeyGone`을 그대로 반환해 항상 error /
전파 도중 새 구독자가 붙으면 **구독자 하나가 누락되고 다른 하나가 두 번
발화**(실측, 실행마다 결과가 다름) / `Tween<T>:Mapped` 재귀 제네릭 타입
누수(`luau-analyze` 0 진단) / `New(): Quad`가 닫힌 타입이라 M2가 붙일
`quad.Dispatch`가 타입에러(`luau-analyze`로 재현).

## 4차 패스 (`H-39`~`H-54`) — 문서 단위가 아니라 축 전환

핸들러 레지스트리 전수 / `ref-plan.md`·`attribute-plan.md` 심층 /
**`luau-test` 스파이크 실제 재실행** / 프리미티브 조합 매트릭스 /
`reference`·`archive`·로드맵 M3~M9 / **엔진·언어 사실 주장 전수 검증**.

**`H-39`(🔴)는 세 축에서 독립적으로 같은 결론에 도달했다** — 배열 자리를
차지하는 말단 핸들러 4종(`TagHandler`/`AttributeGroupHandler`/
`RefLeafHandler`/`ObserverEffectLeafHandler`)이 `setLength`/
`setOffsetSource`를 **아예 등록하지 않아** `Frame { Tag("x"), Child{} }`
같은 흔한 배치가 첫 마운트에 `recompute`의 명시적 error로 죽는다. 문서를
하나씩 읽는 방식으로는 구조적으로 안 보이는 종류(등록 의무는 디스패치 코어
문서에만 있고 각 핸들러 문서는 자기 로직만 서술).

그 외 🔴 둘: 공개 `Slot:Add` 의사코드가 CRUD 절이 확정한 가드 넷을 하나도
안 함(`H-37`은 이 문제의 반쪽이었다) / `groupClaimKeys` 확정이
`AttributeGroupHandler` 의사코드에 배선 안 됨.

**엔진 사실 검증의 성과 둘** — `H-21`의 전제가 공식 문서로 **확인**됐고,
반대로 **`PreRef`의 존재 근거가 `Workspace.SignalBehavior`에 조건부**임이
드러났다(`H-42`, 신규 템플릿 place는 이미 `Deferred`가 기본이고 코퍼스에
이 단어가 0건).

**스파이크는 깨끗하다** — `done/` 16개 전원이 `STATUS.md` 주장과 실행 결과
일치, GC 스파이크는 3회 반복에도 수치 동일, 설계 드리프트 0건.

색인 두 곳(`README.md`의 `qa-request/` 행, `todos.md` 00번)도 같이 갱신.
`doc-check.py` ERROR 0.

Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01TPzatFCEs1CfWNPxF1pidr
2026-08-24 10:06:07 +09:00
f650a1cfd6
qa: 손 트레이싱 6라운드 2차 패스 — 1차 미탐색 영역 확장, 발견 9건 (+ 1차 전수 재검증)
사용자 요청("지금 나온거 이외에 더 문제될만한게 있는지 조사해봐줘 …
시스템 전체 계획을 봐도 좋고")으로 같은 파일에 2차 패스를 이어붙였다.
1차는 사용자가 지목한 5개 영역이 범위였고, 2차는 `dispatch-core-plan.md`
전문 / `source-state-plan.md` / `lifecycle-pattern.md` / `ref-plan.md` /
`tag-plan.md` / `attribute-plan.md` / `ui-shorthand-plan.md` /
`brand-plan.md` / `gate-plan.md` / `blocker-plan.md` /
`state-epoch-plan.md`와 `slot-plan.md`의 `raw*` 계층을 의사코드 단위로
훑었다.

**`base/`는 여전히 한 줄도 안 고쳤다** — 전부 발견 보고이고 회신 대기.

## 🔴 셋

- `H-11` **`Effect`의 leaf 사망 cleanup을 발화시키는 배선이 어느
  의사코드에도 없다.** `lifecycle-pattern.md`는 "`Effect`가 `Destroying`
  훅을 쓰는 유일한 소비자"라 못박고 `effect-plan.md`는 "leaf당 실제
  Destroying 바인딩 하나"라고 비용까지 적어뒀는데, leaf가 붙는 유일한
  경로인 `bindLifetime`의 실 구현 스케치는 gchold 등록 + gcconn 복사가
  전부다. `Effect`가 통째로 안 돌고, 그 위의 `OnDestroyed`와
  `slot._detachCleanup`(detach 요소를 파괴하는 *유일한* 경로 —
  같은 문서가 "GC 폴백이 아예 없으므로 필수"라 명시)이 같이 죽는다
- `H-12` `rawRemove`/`rawUnmount`/`rawDetach`에만 "아직 마운트 전"
  얼리리턴이 없다(`rawAdd`/`rawReplace`엔 있음). `Slot()` → `:Add(x)` →
  `:Remove(1)`이 `nativeRemove(nil, …)`로 터진다
- `H-13` `Effect(fn, ...deps)` 역전(5라운드 `C-6`)이
  `source-state-plan.md`에 반영 안 됐고, 거긴 아직 "기각"이 **일반
  원칙**으로 서 있다. 반응형 코어 정본만 읽고 구현하면 단수 dep으로
  되돌아가 `Ref` 의존성 갭이 재발한다

## 🟡🟢 여섯

`Effect`의 `fn` 시그니처 미확정(`self`/`previous` 자리, `Ref` dep은 lazy
핸들이 아님) / `newKeyIndex[key] = pos`가 마운트 안 된 키에도 배열
인덱스를 기록(`0` 포함) / `Dispatch.drive`의 Blocker 범위가 `F-4-1`
단일 루프와 안 맞고 `PostRef`가 게이트 안에서 발화하는 귀결이 미문서화 /
attribute 이름을 그룹 A→B로 옮기면 emit 순서에 따라 error /
`rawAdd`·`rawReplace`의 plain 분기가 `recompute`를 두 번 돌림 /
`Processed*RefHandler`의 `process` 시그니처가 핸들러 계약과 다름

## 커밋 전 `H-1`~`H-20` 전수 재검증

사용자 요청으로 커밋 전에 20건 전부를 `base/` 원문과 다시 대조했다.

- **`H-15` 철회(오탐)** — `ui-shorthand-plan.md`에 이미 사용자 판정
  예외(4라운드 `UI-11`, "`retractFrom(child, prop, 1)`을 정석으로 요구하지
  않는다 — 실익이 없다")가 있었는데 그걸 못 보고 `dispatch-core-plan.md`의
  일반 규칙만 인용했다. 번호는 비우지 않고 철회 기록 + 재발 방지 교훈으로
  남긴다(남은 건 인자 이름 `hint`가 폐기된 옛 용어라는 🟢 하나)
- **`H-2` 트레이스 정정** — 결론은 맞지만 수치가 틀렸다. `updateFn`이
  반환하는 중첩 Slot은 정의상 마운트 전이고 `Length`를 쓰는 주체는
  `recompute` 하나뿐이라(5라운드 `C-2`), 그 시점 `.Length`는 **항상 0**이다.
  원래 적은 `+3`이 나올 수 없다. 실제 동작은 더 나쁘다 —
  `isSlot` 분기가 언제나 `+0`이라 (a) 첫 아이템이면 `rawAdd(self, S, 0)`로
  바로 out of bounds, (b) `pos` 전진이 원래 노리던 목적("다음 형제의
  index가 물리 개수를 반영")도 첫 사이클엔 성립하지 않아 같은 데이터의
  `index`가 사이클마다 달라진다
- **나머지 18건은 유효 확인.** `H-3`은 `bk.invalidAfter` 대입이 코퍼스
  전체에서 `getOffsetAt` 안의 두 줄뿐임을, `H-11`은 `Destroying`이 산문에만
  등장하고 어떤 의사코드에도 연결되지 않음을 각각 grep으로 재확인
- 1차의 "이상 없다고 확인한 것" 중 `Ref` 콜백 발화 계약 표현도 정정
  ("이미 채워져 있으면" → 미설정이어도 무조건 1회)

색인 두 곳(`README.md`의 `qa-request/` 행, `todos.md` 00번)도 같이 갱신.
`doc-check.py` ERROR 0.

Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01TPzatFCEs1CfWNPxF1pidr
2026-08-23 21:09:50 +09:00
471f64f9ee
qa: 손 트레이싱 6라운드 — 최근 확정분 전수 추적, 발견 10건 (회신 대기)
사용자 요청으로 최근 확정된 5개 영역을 실제 값으로 손 트레이싱했다 —
`Effect(fn, ...deps)` / `Gate`·`Blocker` / State 전파(`rawInvalid`·emit 지연) /
Slot의 `native*`·offset·length·mount / `Brand`·`Epoch`·`EpochMap`.
4·5라운드가 "예/아니오 문항지"였던 것과 달리 2·3라운드와 같은 성격이라
파일명도 `handtrace`로 갈랐고, 발견 번호는 `H-n`.

**`base/`는 한 줄도 안 고쳤다** — 전부 발견 보고이고 사용자 회신 대기.

## 🔴 셋 (크래시하거나 조용히 어긋남)

- `H-1` `:List`의 `keyIndex`가 사이클 **도중**엔 stale인데 `rawMove`/
  `rawReplace`/`releaseElement`/`rawDetach`의 live `_elements` 인덱스로
  쓰인다. 앞에 하나 끼우면 순서가 조용히 뒤집히고, 리스트 비우기는 소멸
  루프의 해시 순회 순서에 따라 `_elements[i] == nil`로 터진다. 5라운드가
  "raw\*를 전부 index 기준으로 통일"한 근거였던 *"그 값은 이미 `keyIndex`가
  들고 있다"*가 여기서 깨지므로 **그 결정의 전제를 다시 봐야 한다** — 세
  갈래(라이브 인덱스 맵 / `indexOfRaw` 기본 경로화 / reconcile 2패스)를
  적어뒀고 이게 유일한 사용자 판단 항목
- `H-2` `pos`가 리프 개수와 `_elements` 배열 인덱스를 겸한다. `updateFn`이
  중첩 Slot(멀티루트 결과)을 반환하면 첫 사이클에 바로
  `table.insert` position out of bounds
- `H-3` `getOffsetAt`의 접두합 캐시를 **아무도 무효화하지 않는다**.
  `invalidAfter = min(...)` 규칙이 표로만 존재하고 `setLength`/
  `gatedRecompute`/`_baseObserver`/`spliceArrays*` 어느 의사코드에도 없다.
  형제 길이가 변해도 뒤 형제 offset이 고정되고(`Length`만 맞고 offset은
  틀리는 형태), 포탈 재마운트는 옛 베이스가 든 캐시를 그대로 물려받는다

## 🟡🟢 일곱

`bk.invalidAfter`/`offsetCache`가 부기 스펙에 없고 초기값 `nil`이면 비교에서
터짐 / `spliceArraysUp`이 `bk.N`을 먼저 올려 스스로 금지한 창을 열고 그 안에
`nativeInsert`가 들어 있음 / `unmountSlotTree`가 미정의 `physicalTarget` 참조
(+ 순차 extract의 offset 스큐) / `Effect`의 `Ref` 의존성은 해제 API도
`canExecute` 게이팅도 없어 죽은 leaf에서 계속 발화 + 누수 /
`EffectHandle._observer`가 아직 단수라 N-deps에서 2번째 이후 재실행이 죽음 /
게이트 `withheld`가 flush 스왑에서 weak를 잃음 / `recompute`의 `sum` 시작값
주석이 stale.

## 이상 없다고 확인한 것

Epoch/EpochMap 판정 규칙(§1 다이아몬드 실제 추적), 게이트 flush 스냅샷과
`:Sync(batch)` 예외, `Effect`의 EpochMap dedup + `_installing` 순서,
`materializeSlotTree`/`mountSlotTree` 분해(depth 2 부기·물리 양쪽),
단건과 배치의 순서가 반대인 것, `Brand` 다중 태깅 — 그 문서 마지막 절이
소스이고, 다시 트레이싱할 필요 없다.

인덱스 레이어는 `.claude/README.md`(qa-request 행)와 `.claude/todos.md`
00번 갱신. `doc-check.py` ERROR 0.

Claude-Session: https://claude.ai/code/session_01TiW21rnti9SbLgF6twtn6D

Co-authored-by: qwreey <me@qwreey.moe>
2026-08-22 04:08:11 +09:00
0498816c10
design: Epoch/EpochMap/Brand 전면 승격 + 해소 기록 flatten
앞 세션이 컨텍스트 피로로 미뤄둔 승격(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
2026-08-22 01:27:18 +09:00
a55e726808
design: 빈 배치 emit은 통지하지 않음 — 에이전트 권고 기각, Blocker 계약의 일반화
에이전트 권고("빈 배치 = 무조건 통지")를 사용자가 기각. 빈 배치는 "이미 다
던져서 더 던질 게 없다"는 뜻이고, 같은 카운트가 두 번 흘러든 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
2026-08-21 22:35:58 +09:00
46287ee393
fix: "Gate 재진입 계약"은 잘못 옮긴 서술 — 열린 항목에서 제거, emit=flush로 정정
사용자 반문으로 에이전트 서술 오류 둘을 정정. 결론은 안 바뀌었고 근거와
열린 항목 목록만 정리됐다.

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
2026-08-21 22:27:07 +09:00
3b46d5ef68
fix: 두 번째 /code-review high 7건 — 유실 경로 둘 포함 5건 수정, 2건 승격
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
2026-08-21 22:14:16 +09:00
e2b85bca55
design: 게이트가 게이트 emit을 받는 경우 확정 — 출처를 넘기지 말고 풀어서 합친다
사용자 발견. 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
2026-08-21 22:02:01 +09:00
930e45bfad
design: 게이트 통과/유보 미구분으로 단순화 + 새 노드 두 맵 비대칭 초기화 확정
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
2026-08-21 21:57:45 +09:00
356a308ce0
design: 게이트 emit 출처를 emit(self)+흡수 집합으로 확정, 재계산 시 count 전부 갱신
/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
2026-08-21 21:40:05 +09:00
cb838d3172
fix: /code-review high 12건 — 9건 수정, 3건은 열린 설계 항목으로 승격
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
2026-08-21 21:21:44 +09:00
c58c97a877
design: Gate 표면 확정(state:Gate + GateNode) + State 에포크 채택, 두 문서 base/ 승격
사용자 확정 둘로 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
2026-08-21 21:04:27 +09:00
76cf74e80d
design: 에포크 순회 처분을 sourceCountMap/sourceEmitMap 두 테이블로 확정
사용자가 열려 있던 마지막 자리를 제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
2026-08-21 20:49:28 +09:00
4c395cd383
design: State 에포크 안 3차 정정 — rawInvalid 기제 반전, seen/computedAt 분리 철회
사용자가 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
2026-08-21 20:15:28 +09:00
a4b517aee2
design: native* 물리 조작 계층 확정 + C-7("부기가 물리보다 먼저") 역전
주입 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>
2026-08-21 20:07:21 +09:00
c6fdf1b348
qa: 구현 전 QA 5라운드 — 문항지·회신·전량 반영 + 감사 2라운드
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>
2026-08-21 18:19:58 +09:00
d5e5e1a1b9
fix: /code-review high 발견 2건 — plain-table :List 재마운트 크래시 등
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>
2026-08-21 13:30:33 +09:00
4622fbeec8
qa: 반영 후 감사 6라운드 — 실제 크래시 3건 포함 18건 수정
커밋 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>
2026-08-21 13:20:42 +09:00
9b7f847014
design: Detach 보존 주체/KeyGone/Owned 확정 + attachSlot 분해
QA 4라운드 followup의 마지막 열린 항목(F-3)이 사용자 회신으로 전량
닫히면서 :List 요소 소유권 모델과 attachSlot 책임 분해를 base/에 반영.

- Detach 보존 주체를 userdata → slot._detached 필드로 전면 정정.
  근거는 gcconn 트릭 — detach된 quad-제작 Instance는 GC 폴백이 없어
  명시적 정리 경로가 필수인데 userdata는 :List에게 opaque라 처분 불가.
  재-Detach는 nop, prev 반환은 재마운트. raw 3형제(rawRemove/rawUnmount/
  rawDetach)로 "파괴하는가"와 "소유권을 놓는가"를 분리.
- KeyGone 센티널 신설 — 키가 사라진 자리도 조용히 처분하지 않고
  updateFn(KeyGone, 0, offset, prev, ud)로 한 번 더 묻는다. owner 사망
  시 최종 정리는 mountSlotTree가 거는 Effect가 담당.
- Owned 설치 플래그 신설 — Detach(사이클 단위)와 직교하는 축.
  state<Frame> 의미론 충돌(C-2)이 이걸로 닫힘.
- attachSlot을 materializeSlotTree(부기) + mountSlotTree(물리)로 분해.
  "부모에게 미는 길이는 최종값"(C6)과 "부기가 물리보다 먼저"(C7)가 한
  함수 안에선 동시 만족 불가라는 진단이 근거. 공개 표면은 두 줄짜리
  래퍼로 유지해 호출부 무변경. research/slot-attach-decomposition.md 확정.
- ROADMAP M6의 옛 Detach 서술 2건과 미결 마커 정정, question.md/todos.md
  해소 반영, session/2026-08-21-01 원문 + session-summary 색인 공백 4건 보강.

doc-check.py ERROR 0.

Co-authored-by: qwreey <me@qwreey.moe>
2026-08-21 11:55:11 +09:00
1e6cb0111d
design: QA 4라운드 3차 회신 반영 + attachSlot 분해 논의 자료 준비
확인받은 것 반영:
- 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>
2026-08-21 11:10:42 +09:00
f36bdcbe5d
design: QA 4라운드 2차 회신 반영 — B절 확인분 + C절 결정 7건
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>
2026-08-21 10:47:45 +09:00
9642cf8f18
qa: QA 4라운드 followup — 회신 처리 결과 + 재질문 정리
사용자 회신을 (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>
2026-08-20 00:57:19 +09:00
e80aa11c4f
qa: 구현 전 QA 4라운드 문항지 — base/ 전 문서 확정 주장 전수 문항화
사용자 요청("모든 확정 부분에 있어서 예가 되어야하는 질문들을 계속")으로
base/ 26개 문서를 서브에이전트 없이 한 맥락에서 의존성 순서로 읽으며,
확정으로 적힌 주장을 전부 "예가 나와야 정상인 단정문"으로 뽑았다.
결론만이 아니라 근거까지 문항에 넣었는데, 코퍼스에 "결론은 그대로인데
근거가 뒤집힌" 사례가 여럿 있어서 결론만 물으면 그런 걸 못 잡기 때문.

⚠️로 열려 있다고 적힌 항목은 "확정이 맞나"가 아니라 "아직 열려 있다는
인식이 맞나"를 묻는 문항으로 따로 표시했다.

정정은 하나도 안 했다 — 사용자 지시대로 기록만 하고 회신 대기 상태.

Co-authored-by: qwreey <me@qwreey.moe>
2026-08-20 00:33:49 +09:00
96c8c2eaa1
qa: 구현 전 QA 3라운드 — attachSlot/bk.N 트레이싱으로 RC-3/RC-4 발견·해결
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>
2026-08-19 00:07:30 +09:00
b419d8c5e5
qa: New()/Quad() 다중 인스턴스화 이름 정정 — 이전 커밋의 오역 정정
직전 커밋에서 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>
2026-08-18 22:23:32 +09:00
a1f948ae34
qa: 구현 전 QA 2라운드 — recompute 손 트레이싱으로 RC-1 발견·해결
`: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>
2026-08-18 22:11:59 +09:00
8b57cfbb3c
qa: 구현 전 QA 1라운드 결과를 base/에 전량 반영
`.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>
2026-08-18 19:39:03 +09:00
32601487ae
chore: add empty folders 2026-08-10 01:22:53 +09:00