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>
This commit is contained in:
qwreey 2026-08-25 12:26:53 +09:00
parent 6f939a8d2d
commit 8cc388cfab
Signed by: qwreey
GPG key ID: D28DB79297A214BD
2 changed files with 406 additions and 1 deletions

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,405 @@
# 7라운드 손 트레이싱 발견 **검증 패스**`H-55`~`H-106` 판정 (2026-08-25)
**무엇인가**: `.claude/qa-request/pre-implementation-handtrace-round7.md`
발견 52건이 **정말 유효한지만** 판정한 결과. 새 발견을 찾는 패스가 아니다 —
원문에 없는 새 항목은 이 문서에 없다.
**왜 있나**: 52건 전부를 한 세션이 한 컨텍스트에서 썼다. 쓴 세션은 자기가
뭘 잘못 봤는지 모른다는 게 이 코퍼스가 반복 확인한 사실이고
(`.claude/conventions.md`의 "작업 방식"), 판정이 끝나면 사용자가 batch로
결정을 내리므로 무효 항목이 섞여 있으면 그 비용을 그대로 문다.
**⚠️ 이 패스가 지킨 규칙 — 재실행은 검증이 아니다.**
`audit/handtrace-round7-reference-impl/README.md`가 못박은 그대로,
4·5·6차 패스의 발견은 **스파이크를 다시 돌려서** 판정하지 않고
**전사물(`spikes/core.luau`·`dispatch.luau`·`chain.luau`)과 `base/`의 확정
의사코드를 줄 단위로 대조**해서 판정했다. Luau **언어 동작**을 주장하는
부분(타입 스파이크, `Relate` GC)만 재실행으로 교차 확인했다 — 그쪽은
전사물이 아니라 언어 자체가 판정 대상이기 때문.
---
## 결론 요약
| 판정 | 건수 | 번호 |
|---|---|---|
| **유효** | 49 | 아래 표에서 "유효"인 전부 |
| **범위 축소** | 2 | `H-87`, `H-105` |
| **무효** | 0 | — |
| **중복** | 0 | — |
**무효가 0건인 것 자체를 의심해봤다.** 우선 의심 대상으로 지정된 네 축
(참조 구현 전사 오류 / 부정 주장 / 이미 닫힌 항목 / 트리거 과장)을 전부
개별로 훑었고, 실제로 걸린 것은 **범위 축소 2건 + 부정·개수 주장 정정 2건**
이다. 나머지가 살아남은 이유는 원문이 대체로 **자기 주장의 근거를
`base/` 원문 인용으로 달아뒀고**, "이건 `H-xx`와 별개다"를 항목마다 직접
적어 중복을 미리 걷어냈기 때문이다.
**다만 아래 넷은 배치 결정 전에 반드시 같이 읽을 것** — 판정은 유효지만
그대로 사용자에게 올리면 **이미 내려진 판단을 다시 묻거나, 틀린 수치를
근거로 결정하게 된다**:
1. **`H-87` — 범위 축소.** "코퍼스가 error 경로를 어느 쪽도 다루지 않는다"는
**거짓**이다. `base/slot-plan.md`가 같은 실패 모드를 명시하고
**사용자가 2026-08-21에 "pcall로 감싸지 않는다"로 이미 판단**했다.
2. **`H-105` — 범위 축소.** 개수를 직접 세보니 **한국어 17이 아니라 약 23**
이고, `17 + 6 = 23``H-104`의 "42곳"과 애초에 안 맞는다. 현상과 결정
필요성은 그대로 유효.
3. **`H-88` — 유효, 부정 주장 1건 정정.** "예외 안전성 계약이 코퍼스에 한
줄도 없다"는 과하다 — `slot-plan.md`에 **선례가 하나 있고 그건 갈래 (b)
쪽을 가리킨다**.
4. **`H-104` — 유효, 개수 정정.** "`level` 인자가 하나도 없다"는 **전수
확인으로 참**이지만, "42곳"은 산문 언급까지 센 수치로 보인다(실제 quad
자신의 error 코드 자리는 **약 29**).
---
## 참조 구현 대조 결과 (전사물 ↔ 원문)
`README.md`**의도적 차이 3개**(`PeekDiffers` / `box.pos` / 생명주기
게이팅 부재)를 미리 신고해뒀고, 그 셋은 차이로 세지 않았다. **나머지
차이를 전부 후보로 놓고 훑은 결과, 미신고 차이 3개를 찾았다 — 셋 다
발견을 만들어내지 않는다**(전부 보수적 방향이거나 등가).
| 대조 항목 | 결과 |
|---|---|
| `EpochMap:Update`/`Refresh`/`Sync`/`TrackFrom` | `state-epoch-plan.md` §3과 일치 |
| `State:_receive`(규칙 1~3) | §4 의사코드와 **축자 일치** |
| `State:_recompute``rawInvalid = false` 위치 | §4 *"재계산이 끝나면"* 그대로 — **`H-85`는 전사 오류가 아니다** |
| `State:Get`(재계산 판정) | §4 *"재계산 판정"* 절과 일치 |
| `Source:Set``bit32.bnot(-self.Revision)` | §2와 일치(실측 표까지) |
| `emitDown`의 배열 스냅샷 | `source-state-plan.md``H-23` 확정과 일치 |
| `GateNode:_flush`(빈 배치 얼리리턴 → 스왑 → `Sync` → 전파) | `gate-plan.md` 4·8번과 일치 |
| `GateNode:_receive`(규칙 먼저 → unfold → 정책) | `gate-plan.md` 4번과 일치 |
| `Dispatch.getOffsetAt`(접두합 캐시) | `dispatch-core-plan.md`**축자 일치** |
| `recompute` | **에러 메시지 문자열까지 축자 일치** |
| `setLength`/`setOffsetSource` | 일치(아래 미신고 차이 3 참고) |
| `chain.luau``process`/`retractFrom` | **축자 일치**(`NOOP` 마커 포함) |
**미신고 차이 3개**:
1. **`Blocker:Policy(emit, discard)`가 2인자다.** 확정 시그니처는
`blocker:Policy(emit) -> onUpstreamEmit`(`blocker-plan.md`). 다만
**어느 스파이크도 `discard`를 넘기지 않는다**(죽은 인자) — 결과에 영향
없음. 아이러니하게도 이 인자가 존재한다는 사실 자체가 `H-55`가 필요하다고
말하는 그 통로다.
2. **`Blocker.handles`가 평범한 배열이다.** 문서는 *"onunblock 핸들을
blocker의 weak 배열에 등록"*. 전사물이 **강하게** 들었으므로 `H-63`(2)
(핸들이 GC되면 `Off()`가 조용히 no-op)를 **가려주는** 방향이다 —
발견을 만들어내는 게 아니라 오히려 덜 나오게 한다.
3. **`M.Observer`가 "등록 즉시 1회 실행"을 안 하고, `self.owner = state`
(상류 강참조)를 둔다.** 앞의 것은 `dispatch.luau`가 그 자리에 명시
`gatedRecompute()`를 넣어 **등가로 보정**했다. 뒤의 것은 문서에 없는
필드인데 **상류를 살려두는 방향**이라 중간 State GC 문제를 가린다 —
그래서 `H-98`은 일부러 `q.Observer`를 안 쓰고 별도 `Obs`를 썼다(원문이
그 이유를 코드 주석으로 밝혀뒀다, 정직한 처리).
**즉 전사 오류로 생긴 발견은 없다.**
---
## 항목별 판정
| 번호 | 판정 | 한 줄 근거 |
|---|---|---|
| `H-55` | 유효 | `gate-plan.md` 4번 *"emit 없이 푸는 경로는 집합을 **버려야** 한다"* + `setup: (emit: () -> ()) -> (onUpstreamEmit: () -> ())` — 정책이 `withheld`에 닿는 통로가 실제로 없다 |
| `H-56` | 유효 | `lifecycle-pattern.md` (4) *"발화 시 각 구독자에 대해 `canExecute(observer)`를 확인하고, 거짓이면 그 구독자만 조용히 건너뜀"* + `isBoundAlive`가 State 노드엔 `false` |
| `H-57` | 유효 | `effect-plan.md` 2번 *"`unbindLifetime`은 cleanup을 부르지 않는다"* + `source-state-plan.md`의 retract 클로저가 `unbindLifetime(v)`만 부름 |
| `H-58` | 유효 | `bindLifetime``BindData:SetWeak(value,"gcconn",…)` **뒤에** `_bindDestroying`을 부르고, `ref-plan.md`가 *"등록 즉시 그 값으로 1회 호출됨"*을 확정 |
| `H-59` | 유효 | `Ref` 콜백을 거는 코드가 `_bindDestroying(inst)`뿐이고 `:Subscribe()``inst`가 없음. `ref-plan.md`는 *"`:Unsubscribe()`에서 `:Uncallback`한다"*고 적음 |
| `H-60` | 유효 | 코퍼스 전체 grep — `Rerun`은 호출 5곳뿐, 정의 0곳. `ROADMAP.md` M2의 "`Effect` 구현 시 같이 만들 것" 목록에도 없음 |
| `H-61` | 유효 | `source-state-plan.md` *"그냥 이 State가 계속 재계산되게만 강제하고 싶을 때 씀"* vs 같은 문서 *"값을 안 실어줌 — 반드시 `Get()`을 다시 해야 함"* |
| `H-62` | 유효 | *"중간에 만들어놓고 아무도 안 보는 State는 구독 등록 자체가 안 일어남"*(lazy) vs *"이 노드는 Observer와 같은 패턴(외부 weak table)으로 상위 노드의 구독자 목록에 등록됨"*(eager) + §4 시딩 |
| `H-63` | 유효 | `blocker-plan.md` *"onunblock 핸들을 blocker의 weak 배열에 등록"* — 구멍/강참조 주체/순회 중 등록 셋 다 미정. (2)가 GC 타이밍 의존이라 제일 아프다 |
| `H-64` | 유효(미확정) | `effect-plan.md` *"bind/unbind가 대칭이라 포탈이 자연히 성립한다"* — State dep 캐치업 경로가 실제로 없음. `H-58`과 같이 정해야 하는 건 원문 서술 그대로 |
| `H-65` | 유효(미확정) | `lifecycle-pattern.md` *"바인딩이 죽은 뒤의 재사용은 허용"* + `Destroying` 클로저의 `self._cleanup = nil` 소진 |
| `H-66` | 유효 | `typing-limits.md:245``EffectHandle 반환`, `source-state-plan.md:169``state:Observer(fn) → Observer` |
| `H-67` | 유효 | `blocker-plan.md`의 두 번째 용례 *"이 용례는 `state:Block()`을 전혀 호출하지 않으므로 gated state도 … 생기지 않는다"* — 결론은 안 바뀐다는 원문 스코핑도 정확 |
| `H-68` | 유효 | `source-state-plan.md`/`state-epoch-plan.md`/`store-plan.md` 전수 grep — 동일값 `Set`의 리비전 처리 서술 0건 |
| `H-69` | 유효 | `gate-plan.md` 4번의 무조건 삽입 + flush 진입 시 `newWeakK()` 스왑. 정확성 아님(원문도 그렇게 적음) |
| `H-70` | 유효 | `effect-plan.md`의 deps 절에 `nil` 구멍·타입 검증·같은 `Ref` 중복 서술 0건. `_refCallbacks[ref] = cb` 덮어쓰기도 실재 |
| `H-71` | 유효 | 커밋된 `quad-base/src/Relate.luau`(`buckets` `__mode="k"` + 평범한 `StrongMap`)를 그대로 돌려 **대조군 0 / 케이스1 50 / SetWeak 0** 재현. `relate-plan.md`가 *"단일 `Relate` 안에서 일어나는 한 안전함"*이라 단언 |
| `H-72` | 유효 | `state-epoch-plan.md` §3의 4개 연산이 전부 쓰기를 함(`Update` = 읽고 덮음). §4 게이트 예외는 *"판정(규칙 1~3)은 똑같이 먼저 돈다"* + *"`emitEpochMap:Update`를 수신 시점에 부르지 않으므로"* — 둘을 동시에 만족할 연산이 없다 |
| `H-73` | 유효 | `getDynamic<T>(store,name): Source<T>` 재현 — `Source<unknown>`로 떨어짐. `store-plan.md`가 확정한 표기는 `store:GetDynamic<<T>>(name): Source<T>` |
| `H-74` | 유효 | `store-plan.md`의 확정 스케치가 `table.clone(defaults)` 결과에 `Source`**직접 써넣음** → raw 키라 `__index` 미실행. *"고정 메소드 테이블을 먼저 확인"* 방어가 eager 경로엔 없음 |
| `H-75` | 유효 | 재현 — 평평한 선언은 실패(`Expected type table, got 'T'`), ②쪼개기는 **진단 0건**. `store-plan.md``WrapStore` 스케치는 평평하고 `typing-limits.md` §5는 그걸 *"✅ 검증 완료"*라 적음 |
| `H-76` | 유효 | 재현 — `type function`이 바깥 별칭을 반환하면 `returned a non-type value`. `store-plan.md`의 *"문제없이 `Source<string>` 자리에 대입 가능"*은 검증된 적 없음 |
| `H-77` | 유효 | 커밋된 `Relate.luau``WEAK_VALUE_MT = { __mode = "v" }`**키는 항상 강함**. 재현: 대조군 0 / SetStrong 50 / **SetWeak도 50**. 커밋된 `init.luau:23`*"weak-키잉되므로 별도 정리 불필요"* 주석이 거짓이 됨 |
| `H-78` | 유효 | 이 검증 세션에서 그대로 재현 — `smoke.init`/`smoke.plugin`이 `could not resolve child component "src"`로 죽고, 스파이크 `23`은 링크 실패 진단 2건만 내고 **의도한 음성 대조군이 안 뜬다**. `ROADMAP.md:198`의 *"전부 PASS"*엔 날짜도 전제조건도 없음 |
| `H-79` | 유효 | `attribute-plan.md``attr:NameMap(): {[string]: Source<any>}` vs `store-plan.md`에 열거 표면 0건. eager/lazy 이중 모델이라 키 집합이 접근 이력에 좌우됨 |
| `H-80` | 유효 | `ROADMAP.md:447` 체크박스가 `Source`/`State`/`Store` 셋뿐. 같은 M2가 `Effect`/`is*` 전량/`bindLifetime` 4종/`Blocker`/`Relate`를 탑레벨로 얹음. `State`는 런타임 생성자가 코퍼스에 없음 |
| `H-81` | 유효 | `isModifier` 게이트 체크박스는 `ROADMAP.md:1076`(M7)에만 있고 M2엔 0건. 적용 지점이 전부 M2 파일. `source-state-plan.md`가 독립 `Source(someModifier)`를 추가로 요구하는데 `modifier-plan.md` 7번 목록엔 없음 |
| `H-82` | 유효 | 근거 2번은 *"`w`의 캐시를 c1/c2가 공유"*인데 같은 절이 `:With`를 *"계산 함수는 없고 값은 `self`를 그대로 통과(pass-through)"*로 확정 — 공유될 계산이 없다. 결론 불변이라는 스코핑도 정확 |
| `H-83` | 유효 | `luau`로 확인 — `table.clone(nil)``table expected, got nil`. 같은 절이 *"`defaults`는 선택"*을 확정 |
| `H-84` | 유효 | `ROADMAP.md` M2 절 grep — `:With`/`state:Block`/`Source:Emit` 개별 체크박스 0건 |
| `H-85` | 유효 | **전사 대조 통과** — §4 *"재계산이 끝나면 … `rawInvalid = false`"*가 원문이고 전사물이 그 순서다. 코퍼스에 `rawInvalid``fn` 앞에 내리라는 서술 0건 |
| `H-86` | 유효 | `gate-plan.md` 5번 *"`pending` 같은 정책 상태는 `HasBlockedEmit`으로 흡수한다"* + `blocker-plan.md``HasBlockedEmit`을 **gated state의 필드**로 확정 + *"구현 시 두 개를 따로 들지 말 것"* → 정책이 읽을 통로가 없다. `debounce-throttle-plan.md`가 그 값을 **두 자리**에서 요구 |
| `H-87` | **범위 축소** | 아래 상세 |
| `H-88` | 유효(부정 주장 1건 정정) | `state-epoch-plan`/`gate-plan`/`blocker-plan` 셋에 `error` **0건** 확인. 다만 *"코퍼스에 한 줄도 없다"*는 과함 — 아래 상세 |
| `H-89` | 유효 | `gate-plan.md` 4번이 `:Sync(batch)`를 *"실제로 전파할 때"*라고만 하고 앞/뒤를 안 정함. `blocker-plan.md``Off()`를 *"`IsBlocked = false`로 먼저 설정, 그 다음 등록된 onunblock 핸들 전부 실행"*으로 확정 → 중간 실패 시 "안 막혔는데 안 풀린" 상태 |
| `H-90` | 유효 | `effect-plan.md`의 확정 의사코드가 `from`(루트 `Epoch`)으로만 접음 + `gate-plan.md` 3번 *"게이트를 통과하지 않은 값도 `:Get()`으로는 보인다"*. 전사물이 그 의사코드 그대로 |
| `H-91` | 유효 | §8 인용 *"이제 정말로 항상 state 는 get 이 최신을 던지는게 맞다"* vs `tween-plan.md``Animate`*"`info.Style`이 State여도 이 내부 `:Compute`의 trailing deps로 안 넘어가므로 구독 목록에 안 걸림"* — 미선언 읽기를 **설계로** 씀 |
| `H-92` | 유효 | §2가 테이블 리비전을 기각한 근거가 *"`Set` 한 번마다 테이블 하나를 할당"*인데 `H-23` 스냅샷은 **파동이 지나는 노드 수만큼** 할당. §7 비용 절이 이걸 안 셈 |
| `H-93` | 유효(미해결의 파생) | §3 *"키는 weak다. `epoch`가 죽으면 항목이 사라진다"* + §4 순회가 *"자기가 이미 들고 있는 키 전부"*만 봄 → §4가 시딩에 대해 경고한 *"비어 있으면 '훑을 게 없으니 유효하다'로 오판한다"*가 수거로 재발 |
| `H-94` | 유효 | `luau-analyze` 재현 — `GateFactory`가 함수 타입 자리에서 거부(제네릭·비제네릭 양쪽). `gate-plan.md` 2번이 *"확인할 필요도 없어졌다"*고 접었는데 같은 항목이 *"`Debounce`/`Throttle`은 `:Apply` 그대로 둔다"*를 확정 |
| `H-95` | 유효 | 재현 — `Not all codepaths in this function return '(() -> ())?'` / `Expected 'El?, number?', but got 'El?'`. 유니온 해법은 통과하고 음성 대조군만 잡힘(`ty7_fix` 진단 1건) |
| `H-96` | 유효 | 재현 — deps 0개는 통과, deps가 붙으면 `Consider annotating the return` + dep 파라미터 미해소. 2차 패스 부록의 자기 정정도 정확(그 코드엔 dep 주석이 달려 있었음) |
| `H-97` | 유효 | `ROADMAP.md:274` *"인터페이스만 … 실 구현 없음 — quad-roblox 실 구현은 M8"* + `module-lifecycle-plan.md:343` *"명시적으로 에러내는 스텁"* + `architecture.md``bindLifetime/canBound/canExecute`도 주입 대상으로 명시. 커밋된 mock에 signal/Connection이 이미 있다는 관찰도 확인 |
| `H-98` | 유효(미해결의 파생) | `source-state-plan.md:1178` *"참조를 아무 데도 안 담아도 정상"* / `:1185` *"GC되지 않고 영원히 계속 실행됨"*. 전사물이 상류 되참조를 일부러 뺀 이유를 코드에 밝혀둠 |
| `H-99` | 유효 | `architecture.md` 소스 트리 전수 — `Observer.luau` 0건, 레지스트리(`Subscribed`) 소유 모듈 미지정. `EpochMap.luau`는 *"`State.luau`에 묻지 않고 별도 모듈"*로 명시돼 대비가 성립 |
| `H-100` | 유효 | 재현 — `{ [Source<number>]: true }``Epoch | EpochSet` 자리에서 거부. `Source``Epoch`를 구조적으로 만족한다는 §2 확정은 그대로 성립 |
| `H-101` | 유효 | **전사 대조 통과**`recompute``sum` 누적/꼬리 `Length:Set(sum)`/`for i = 1, bk.N`이 전부 원문 축자. `gate-plan.md` 6번이 *"유한한 재진입은 지원한다"*고 확정한 경로 |
| `H-102` | 유효 | `dispatch-core-plan.md``gatedRecompute``i`를 캡처(`bk.invalidAfter = math.min(bk.invalidAfter, i)`). `slot-plan.md`의 splice 요구 목록(`observers` 당김 / `bk.N` 감소 / `invalidAfter` 당김) **셋 어디에도 옮겨진 클로저의 인덱스가 없다** |
| `H-103` | 유효 | **전사 대조 통과**`list[index] = { handler = h, retractor = NOOP }` 마커가 원문 그대로. `attribute-plan.md``AttributeKeyHandler.process``nameClaims:SetStrong` **뒤에** `setAttribute`를 부르는 것도 원문 확인 |
| `H-104` | 유효(개수 정정) | 전수 grep — `error(x, N)` 형태는 `fallback-plan.md:85`**산문 설명 1건뿐**, quad 자신의 error 코드 자리엔 0건. "42곳"만 정정 필요(아래 상세) |
| `H-105` | **범위 축소** | 아래 상세 |
| `H-106` | 유효 | `getOffsetAt``cur += contribution(bk, i)``nil` 가드 없음(원문 확인). `recompute``C-6` 메시지를 갖고, `setOffsetSource`가 등록 시점마다 `getOffsetAt`을 부름 |
---
## 상세 — 범위 축소 2건
### 🔻 `H-87` — 범위 축소: **절반은 이미 사용자가 판단한 항목이다**
**현상은 맞다.** `dispatch-core-plan.md`의 배치 게이팅이
`On()` … 사용자 코드 … `OffWithoutEmit()` 모양이고, 중간에서 던지면 그
owner의 Blocker가 영구 On으로 남는다 — 원문 인용도 정확하고 재현도
타당하다.
**틀린 것은 부정 주장이다.** 원문은 *"문서는 yield만 UB로 못박고 error는
안 다룬다"*, *"error 경로는 그 실패 모드에 정확히 해당하는데 **어느 쪽도
다루지 않는다**"*라고 적는다. `base/slot-plan.md``materializeSlotTree`
끝(`blocker:OffWithoutEmit()` 직전)에 **정확히 그 내용이 이미 적혀 있고,
사용자 판단까지 붙어 있다**:
> `[2026-08-21 감사] 위 재귀가 **예외를 던지면 이 줄에 도달하지 못해
> Blocker가 켜진 채 남는다** … `pcall`로 감싸지 않는 것이
> **사용자 판단(2026-08-21)**: 마운트 도중 예외는 quad가 복구를 보장하지
> 않는 상태이고 … 아직 실제로 밟은 적 없는 경로다 — `conventions.md`
> "드문 오용이나 가상의 미래 요구까지" 절이 세운 원칙 그대로. 실제로
> 물리면 그때 넣는다.`
**그래서 무엇이 바뀌나**:
- 원문의 갈래 (a)는 *"감싸는 자리는 정확히 둘 — `Dispatch.drive`
`materializeSlotTree`"*라고 적는데, **뒤쪽은 닫힌 결정이다.** 그대로 올리면
사용자가 2026-08-21의 자기 판단을 모른 채 다시 결정하게 된다.
- **남는 진짜 질문은 하나**: 그 판단을 `Dispatch.drive`까지 확장할 것인가.
확장을 고민할 값이 있는 이유는 원문이 짚은 비대칭이다 —
`materializeSlotTree`는 같은 Slot에 재마운트가 일어나면 자가치유되지만,
`Dispatch.drive`의 owner는 `inst`이고 배열 파트 배치는 생성 시 한 번뿐이라
**사실상 영구**다.
- **심각도**: 🔴 → 🟡로 읽는 게 맞다. 새 결함 보고가 아니라 **기존 판단의
적용 범위** 문제이고, 같은 원칙("실제로 물리면 그때")을 적용하면 지금은
아무것도 안 하는 게 답일 수 있다.
### 🔻 `H-105` — 범위 축소: **개수가 안 맞는다**
**현상과 결정 필요성은 유효하다.** error 메시지 언어가 갈려 있고, 갈리는
경계도 원문 서술대로다 — **동적 경로 가드 4형제 + 모듈 초기화 + attribute
이름 충돌만 영어**, 나머지는 한국어. `conventions.md`의 *"사용자가 보게 될
것은 한국어"*가 **quad 라이브러리 사용자**에게도 적용되는지는 실제로 정해진
적이 없고, 이건 에이전트가 임의로 정할 수 없는 사용자 결정이라는 원문
판단도 정확하다.
**틀린 것은 수치다.** 직접 세었다(`.claude/base/*.md` 전수, 한글 포함
여부로 분류):
| | 원문 주장 | 실제 |
|---|---|---|
| `error(` 등장 총계 | 42 | **46**(산문 언급 포함) |
| 그중 실제 error 코드 자리 | (=42로 읽힘) | **약 29** |
| 영어 메시지 | 6 | **6** ✅ (binding 가드 4 + `Quad module already initialized` + `attribute … already bound`) |
| 한국어 메시지 | 17 | **약 23** |
`17 + 6 = 23`은 원문 자신이 든 "42곳"과 애초에 안 맞는다. 결정에 영향은
없지만(어느 쪽이든 "지금 정해두면 한 번에 맞출 수 있다"는 논거는 그대로),
**`H-104`가 같은 42를 쓰므로 두 항목의 수치를 같이 고쳐야 한다.**
---
## 상세 — 정정이 필요한 유효 항목 2건
### `H-88` — 부정 주장 1건 정정
*"`state-epoch-plan.md`/`gate-plan.md`/`blocker-plan.md` 셋에 'error'라는
단어가 0건"* → **확인, 참**(각 0건).
*"코퍼스에 **예외 안전성 계약이 한 줄도 없다**"* → **과하다.**`H-87`
인용한 `slot-plan.md`의 문단이 유일한 선례이고, 그 선례가 고른 방향은
원문의 갈래 **(b)**("던지면 UB로 명문화, 실제로 물리면 그때 넣는다")다.
전파 루프에 대한 결정이 **선례 없이 백지에서 시작하는 게 아니라 그 선례를
따를지 갈릴지의 문제**라는 걸 사용자에게 같이 보여야 한다.
### `H-104` — 개수 정정
*"`level` 인자가 하나도 없다"* → **전수 확인으로 참.**
`grep -rn "error([^)]*,\s*[0-9]"`가 잡는 건 `fallback-plan.md:85`
**산문 설명**(사용자 코드가 던지는 쪽) 1건뿐이고, quad 자신의 error 코드
자리엔 level 인자가 0건이다.
*"42곳"* → **약 29곳**으로 정정(위 `H-105` 표). "사용자 입력 검증"과
"내부 불변식 위반"을 나누자는 제안 자체는 그대로 유효하다.
---
## batch용 요약 — **한 결정으로 닫히는 묶음**
번호순 52건이 아니라 **결정 단위 12묶음**으로 재편성했다. 같은 묶음 안의
항목은 결론이 서로를 규정하므로 **따로 답하면 갈라진다.**
### 🅐 게이트 정책의 상태 접근 통로 — `H-55` · `H-86` · `H-72` (+ 종속 `H-63`)
정책이 손에 쥔 게 `emit` 하나뿐이라 **버릴 수도 없고(`H-55`) 읽을 수도
없다(`H-86`)**. 게다가 게이트 노드 자신도 `emitEpochMap`을 **갱신 없이
비교**할 연산이 없다(`H-72`). 셋 다 *"`Epoch` 일반화 때 게이트 쪽 요구가
표면에 덜 반영됐다"* 하나에서 나온다.
- **한 번에 닫히는 형태가 있다**: `emit`에 인자와 반환값을 준다 —
`emit(commit: boolean?) -> boolean`(`H-55`의 갈래 (b) + `H-86`의 갈래 (a)가
그대로 합성). `H-49`의 *"`setup` 시그니처는 안 바뀐다"*를 **인자 목록은
유지한 채** 최소로만 되짚는다.
- `H-72``EpochMap:Peek(...)` 한 줄 추가로 독립적으로 닫힌다.
- `H-63`(2)(정책 클로저가 onunblock 핸들을 강하게 잡아야 한다)는 이 묶음의
결론이 정해져야 문장을 쓸 수 있다.
- **같이 움직일 문서**: `gate-plan.md` 4·5번, `blocker-plan.md`,
`debounce-throttle-plan.md` 7절, `state-epoch-plan.md` §3.
### 🅑 전파 루프를 **코드로** 확정 — `H-56` · `H-62` · `H-61` (+ `H-92` · `H-69`)
지금 전파 루프는 **산문만 있고 의사코드가 없다**. 그 공백에서 셋이 동시에
나온다 — 구독자가 State 노드면 `canExecute`에 걸려 죽고(`H-56`), 엣지
등록 시점이 lazy/eager로 갈려 있고(`H-62`), 인자 없는 `Observer()`
`Get()`을 안 불러 자기 용도를 못 한다(`H-61`).
- **한 블록을 쓰면 셋이 같이 닫힌다**: 구독자 종류별 분기 + 등록 시점 +
`H-23` 스냅샷 + `from` 전달 + 재진입.
- 그 블록을 쓸 때 스냅샷 할당 비용(`H-92`)과 게이트 통과 모드 할당(`H-69`)도
같은 자리에서 정하면 된다(정확성 아님 — §2/§7 서술만 실제와 맞추면 됨).
- **`H-84`가 이 묶음의 로드맵 각주다**(`:With`/`state:Block`/`Source:Emit`
체크박스 부재).
### 🅒 예외 안전성 — 하나의 질문, 네 자리 — `H-88` · `H-89` · `H-87` · `H-103`
질문은 하나다: **"예외가 나면 부기를 어디까지 되돌리는가."**
- 자리는 넷 — 전파 루프(`H-88`), 게이트 flush + `Blocker:Off()` 순회(`H-89`),
배치 게이팅(`H-87`), Dispatch 체인 슬롯(`H-103`).
- **선례가 이미 하나 있다**(`slot-plan.md`, 2026-08-21 사용자 판단: 감싸지
않는다). 그래서 실질 선택은 **"그 선례를 전 자리에 확장한다"** 대
**"전파/체인만 예외로 감싼다"**다.
- 비용 축이 자리마다 다르다는 게 결정 재료다 — 배치당 1회(`H-87`) /
구독자당(`H-88`, hot path) / 자리당(`H-103`, hot path) / flush당(`H-89`).
### 🅓 "긴 연산의 꼬리가 도중 변경을 덮어쓴다" — `H-85` · `H-101`
같은 모양이 두 자리에서 나왔다 — 재계산 꼬리의 `rawInvalid = false`(`H-85`)와
`recompute` 꼬리의 `Length:Set(sum)`(`H-101`). **원칙 하나로 같이 정하는
게 자연스럽다**: *"긴 연산의 마지막 줄은 그 연산 도중 갱신된 값을 무조건
덮어쓰지 않는다."*
- `H-85`는 한 줄 이동(`rawInvalid = false`를 `fn` 앞으로), `H-101`은 재진입
가드 또는 세대 번호. 둘 다 원문이 대조군까지 확인해뒀다.
### 🅔 `Effect` 배선 — `H-57` · `H-58` · `H-59` · `H-60` (+ `H-64` · `H-65` · `H-70`)
전부 `effect-plan.md` 한 문서이고 **서로 얽혀 있다**:
- `Ref` 콜백 등록 자리를 `_bindDestroying`에서 떼어 공유 헬퍼로 만들면
`H-58`(바인드마다 `Rerun`)과 `H-59`(`Subscribe`가 콜백을 안 검)이 **같은
수정으로 닫힌다** — 원문도 *"`H-58`의 갈래 (a)와 같은 자리"*라고 적는다.
- 그 결정이 `H-64`(포탈 캐치업)를 규정한다 — 즉시 호출을 없애면 캐치업
`Rerun`이 필요해진다.
- `H-57`(값 교체 cleanup)과 `H-60`(`Rerun` 정의)은 그 다음에 쓰면 된다 —
`H-60`이 정의되지 않으면 나머지 셋을 쓸 문장이 없다. **`H-60`부터.**
- `H-65`(죽은 바인딩 재사용 뒤 inert)와 `H-70`(deps 세부)은 독립이지만 같은
문서라 한 번에 처리하는 게 싸다.
### 🅕 `Relate` 되참조 규칙 재작성 — `H-71` · `H-77`
`relate-plan.md`의 "위험한 패턴" 절 **하나를 다시 쓰면서 둘 다** 닫힌다.
지금 그 절은 위험을 "값" 기준으로만 서술하는데, 실제 슬롯은 셋이다:
| 슬롯 | `SetStrong` | `SetWeak` |
|---|---|---|
| 바깥 키(`inst`) | weak | weak |
| **내부 키(`key`)** | **강함** | **강함**`H-77`, 규칙 자체가 없었다 |
| 값(`value`) | 강함 ← `H-71` | weak |
- **`H-71`의 해법 (b)(`SetWeak`으로 낮춤)가 `H-77`엔 안 듣는다**는 게 실측으로
확인됐으므로 두 슬롯을 따로 정해야 한다.
- **부수로 반드시 같이 고칠 것**: 커밋된 `quad-base/src/init.luau:23`의 주석
(*"weak-키잉되므로 별도 정리 불필요"*)이 지금 거짓이다. `luau-test/done/07`
되참조 음성 대조군 추가도 원문 제안대로.
### 🅖 Store 표면 — `H-73` · `H-74` · `H-79` · `H-83` (+ `H-75` · `H-76`)
**`H-73`을 먼저 정하면 `H-74`가 통째로 사라진다**(원문 갈래 (c)) — `GetDynamic`
탑레벨 함수로 옮기면 예약 키 자체가 안 생긴다. 그러니 **순서가 있다**:
1. `H-73``question.md` 최우선 항목. 실측 결론은 *"어느 표면이든 `T`
못 묶으므로 비제네릭 + 호출부 캐스트"*이고, 그러면 탑레벨 쪽이 명확히
유리하다.
2. `H-74`는 1의 결론이 (c)면 소멸, 아니면 (a)(생성 시 예약 키 대조 error).
3. `H-79`(열거 표면)는 독립 — 그룹 `Attribute`**작동하려면** 필요하다.
4. `H-83`은 한 글자(`table.clone(defaults or {})`).
**`H-75`/`H-76`은 같은 Store지만 축이 다르다** — 런타임이 아니라 **정본
`Source<T>` 선언 모양**이다. `WrapStore`가 ②쪼개기를 안 하면
`store.key:Compute(무주석)`이 깨지고(`H-75`), `Revision` 하나만 어긋나도
`store.key``Source<number>` 자리에 못 들어간다(`H-76`). **M2에서
`Source`/`State` 타입을 쓰기 전에 정해야 하는 것**이라 🅗보다 먼저다.
### 🅗 확정 시그니처 vs 확정 관용구 — `H-94` · `H-95` · `H-96` · `H-100`
전부 같은 모양이다 — **문서가 확정한 타입이 문서가 확정한 관용구를 통과시키지
않는다.** 넷 다 실측으로 해법까지 대조돼 있어 결정은 "시그니처를 고칠까
관용구를 고칠까" 하나다.
- `H-94`: `:Apply``__call` 팩토리를 못 받음 → 유니온으로 열기 vs
`Debounce`가 함수만 반환.
- `H-95`: `Effect``fn``:List``updateFn` → 가변 반환 팩/유니온 vs
"항상 명시적으로 반환하라" 계약.
- `H-96`: **결정이 아니라 문서에 경계 한 줄**(§7에 *"단 trailing deps가
붙으면 dep 파라미터엔 주석이 필요하다"*).
- `H-100`: `EpochSet` 리터럴에 캐스트 필요 — 구현 시 정하면 됨.
### 🅘 부기(Length/Offset) 결함 — `H-102` · `H-106`
- `H-102`는 **실제 오작동**이다(`Slot:Remove` 뒤 다른 항목 길이 변경 →
뒤 형제 offset이 낡음). 규칙 셋이 각각 다른 날 추가되며 "옮겨진 클로저 안의
인덱스"가 어느 목록에도 안 들어간 자리다.
- `H-106`은 한 줄 가드(`contribution`에 `nil` 검사) — `C-6`이 승격시킨
진단이 새 경로에서 반쯤 무효화된 것.
### 🅙 error 계약 — `H-104` · `H-105`
**42곳(실제 ~29곳)을 쓰기 전에** 정하면 한 번에 맞고, 나중에 바꾸면 전수를
다시 훑어야 한다.
- `H-104`: 사용자 입력 검증(level 2) / 내부 불변식(level 1) 이분.
- `H-105`: 메시지 언어. **사용자만 정할 수 있다**`conventions.md`
한국어 규칙이 **quad 라이브러리 사용자**에게도 적용되는지가 정해진 적 없다.
- ⚠️ 두 항목의 수치를 위 정정대로 같이 고칠 것.
### 🅚 "중간 State GC" 미해결에 딸린 것 — `H-93` · `H-98` (+ `H-62`의 eager 갈래)
셋 다 `question.md` 최우선 항목의 파생이라 **그 항목을 닫을 때 같이 봐야
한다**는 게 결론이다. 지금 그 항목은 실패 모드를 **하나**로만 서술한다:
- (기존) 중간 State가 수거되어 **전파가 끊긴다**.
- (`H-93`) 루트 `Epoch`가 수거되면 `:Refresh()``false`를 줘 **낡은 값을
최신이라고 확신하고 반환한다** — 관측이 훨씬 늦다. **예고된 스파이크에
이 확인을 하나 더 넣어야 한다.**
- (`H-98`) `:Subscribe()`의 **공개 계약 문장 두 개가 이 결과에 종속돼 있다**
— 나쁜 쪽으로 닫히면 *"GC도 안 되고 발화도 안 한다"*가 된다.
- (`H-62`) eager로 확정하면 이 미해결이 더 절실해진다(원문 지적 그대로).
### 🅛 저장소·로드맵 정합 — `H-78` · `H-80` · `H-81` · `H-97` · `H-99`
설계 결정이 아니라 **"M2를 짜기 시작하면 바로 막히는 것"**들이다.
- **`H-78`이 제일 급하다** — 지금 이 저장소에서 `smoke.init`/`smoke.plugin`이
안 돌고 `luau-analyze`가 **거짓 클린**을 준다(검증 세션에서 재현). M2가
`Quad`에 필드를 추가하는 첫 마일스톤인데, 그게 먹었는지 확인하는 유일한
경로가 정확히 그 망가진 경로다.
- `H-97`(mock에 생명주기 배선이 없어 전파 테스트가 아예 못 돎)이 그 바로 옆.
- `H-80`/`H-81`/`H-99`는 체크박스·소스 트리 갱신.
### 🅜 문서 정합 — 결정 불필요 — `H-66` · `H-67` · `H-82` · `H-91`
전부 **결론은 안 바뀌고 근거·표기만 고치면 되는 것**이다. 사용자 결정을
기다릴 이유가 없어 반영 시점에 그냥 고치면 된다.
- `H-66`: 표의 `EffectHandle 반환``Observer 반환`.
- `H-67`: `OffWithoutEmit` 근거가 드는 용례를 gated state 쓰는 것으로 교체.
- `H-82`: `:With` 근거 2번을 "중복 계산"에서 **"엣지 수와 에포크 부기"**로.
- `H-91`: §8 인용을 *"선언한 의존성에 대해서는 항상 최신"*으로 좁히고
`Animate`를 의도적 예외로 가리키기.
---
## 이 검증 패스가 **안 한 것**
- **원문에 없는 새 발견을 찾지 않았다.** 대조 중에 눈에 걸린 것도 이 문서에
안 적었다(이 패스의 정의가 판정이므로).
- **`base/`를 하나도 안 고쳤다.** 반영은 사용자 결정 뒤 `-followup.md`에서.
- **1~3차 패스의 인라인 재현 코드를 전부 재실행하진 않았다**`H-71`/`H-77`/
`H-73`/`H-75`/`H-76`/`H-78`/`H-83`(즉 Luau 언어 동작·저장소 상태를
주장하는 것)만 직접 돌렸고, 나머지는 `base/` 원문 대조로 판정했다.