From 1adafea53d0c4c13d226e7e9d4c06341016c84dc Mon Sep 17 00:00:00 2001 From: qwreey Date: Tue, 25 Aug 2026 10:50:56 +0900 Subject: [PATCH] =?UTF-8?q?qa:=20=EC=86=90=20=ED=8A=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=8B=B1=207=EB=9D=BC=EC=9A=B4=EB=93=9C=203=EC=B0=A8=20?= =?UTF-8?q?=ED=8C=A8=EC=8A=A4=20=E2=80=94=20=EC=8B=A4=EC=B8=A1=208?= =?UTF-8?q?=EA=B1=B4=20=EC=B6=94=EA=B0=80=20(H-77~H-84,=20=ED=9A=8C?= =?UTF-8?q?=EC=8B=A0=20=EB=8C=80=EA=B8=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_012oLwATeQdq9TCFdENPutFG --- .../pre-implementation-handtrace-round7.md | 523 +++++++++++++++++- 1 file changed, 519 insertions(+), 4 deletions(-) diff --git a/.claude/qa-request/pre-implementation-handtrace-round7.md b/.claude/qa-request/pre-implementation-handtrace-round7.md index 08b4682..d3bb263 100644 --- a/.claude/qa-request/pre-implementation-handtrace-round7.md +++ b/.claude/qa-request/pre-implementation-handtrace-round7.md @@ -1,9 +1,12 @@ # 구현 전 손 트레이싱 **7라운드** — M2(반응형 코어) 범위 + M2→M3 경계 -**구성**: **패스 2개, 발견 22건(`H-55`~`H-76`).** 1차 패스는 문서 대 문서 +**구성**: **패스 3개, 발견 30건(`H-55`~`H-84`).** 1차 패스는 문서 대 문서 손 트레이싱(`H-55`~`H-70`), **2차 패스는 문서가 "확인했다"고 적은 주장을 -실제로 `luau`/`luau-analyze`에 걸어본 실측**(`H-71`~`H-76`, 2026-08-25 추가). -발견 번호는 패스를 가로질러 이어서 매긴다(6라운드와 같은 방식). +실제로 `luau`/`luau-analyze`에 걸어본 실측**(`H-71`~`H-76`, 2026-08-25 추가), +**3차 패스는 (a) 실제로 커밋된 M1 코드·툴체인을 이 저장소에서 그대로 돌려본 +것과 (b) 1·2차가 범위에서 뺐던 "M2를 *소비하는* 문서"**(`H-77`~`H-84`, +2026-08-25 추가). 발견 번호는 패스를 가로질러 이어서 매긴다(6라운드와 같은 +방식). **상태**: **[2026-08-25] 발견 보고 — 아무것도 반영하지 않았다.** 판정은 사용자가 이 목록을 보고 한다. 6라운드까지의 결정은 뒤집지 않는 것을 @@ -43,7 +46,8 @@ GC 실측, `store:GetDynamic` 위치)에서 파생되는 것. 아래에서 그 항목과 닿는 지점은 "이건 그 미해결과 별개다"라고만 적었다. -**읽는 순서**: 🔴 다섯이 그대로 구현하면 동작이 어긋나는 것, 🟡은 정의가 +**읽는 순서**: 🔴은 그대로 구현하면(또는 지금 상태 그대로 두면) 동작이 +어긋나는 것, 🟡은 정의가 비어 있거나 두 서술이 갈려 M2 구현자가 임의로 정하게 되는 것, 🟢은 문서 정합·구현 시 정하면 되는 것. "미확정" 표시는 트레이싱으로 확신까지 못 간 의심이다. **2차 패스 표의 마지막 열은 그 항목을 실제로 돌려본 결과**라, @@ -80,6 +84,20 @@ GC 실측, `store:GetDynamic` 위치)에서 파생되는 것. 아래에서 그 | `H-75` | 🔴 | `WrapStore`가 스파이크 `16`의 **평평한** 모양이면 `store.key:Compute(무주석 콜백)`이 깨진다 — `typing-limits.md` ②쪼개기를 `type function` 안에서도 해야 한다 | `store-plan.md`, `typing-limits.md` §5, `luau-test/done/16` | ✅ 평평=실패 / 쪼개기=통과 | | `H-76` | 🔴 | `type function`은 **바깥 타입 별칭을 참조 못 한다** → `Source` 전 표면을 구조적으로 중복 작성해야 하고, 메소드 self 파라미터가 **불변**이라 필드 하나만 어긋나도 `store.key`가 `State` 파라미터 자리에 **안 들어간다**. 스파이크 `16`은 그 대입을 안 해봤다 | `store-plan.md`, `typing-limits.md` §5·§6 | ✅ 별칭 참조 실패 / `Revision` 누락만으로 대입 실패 | +**⭐ [2026-08-25] 3차 패스 — 커밋된 코드·툴체인 실측 + M2 소비자 문서 (`H-77`~`H-84`).** +상세는 아래 "3차 패스" 절. + +| 번호 | 심각도 | 한 줄 | 주 대상 | 실측 | +|---|---|---|---|---| +| `H-77` | 🔴 | **`Relate`의 *내부 키*가 `inst`를 되참조하면 `SetStrong`/`SetWeak` 둘 다 샌다** — 규칙에 이 슬롯 자체가 없고, `H-71`의 해법 (b)(“`SetWeak`으로 낮춘다”)가 여기선 **전혀 안 듣는다**. 커밋된 `RunInit`이 실제로 물린다 | `relate-plan.md`, `module-lifecycle-plan.md`, 커밋된 `quad-base/src/init.luau` | ✅ 커밋된 코드로 30/30 누수 | +| `H-78` | 🔴 | **커밋된 M1 스모크 2개와 `done/`의 타입 스파이크 `23`이 지금 저장소 상태에서 안 돈다** — 게다가 실패 모드가 `luau-analyze` "진단 0건"이라 **통과로 오독된다**. `ROADMAP.md` M1은 날짜도 전제조건도 없이 "전부 PASS" | `ROADMAP.md` M1, `project-setup-plan.md`, `luau-test/STATUS.md` | ✅ 워크어라운드 전/후 대조 | +| `H-79` | 🟡 | **`Store`에 열거 표면이 없는데** 그룹 `Attribute(...)`/`:NameMap()`이 그걸 요구한다 — lazy `__index` 때문에 키 집합이 **접근 이력에 좌우**되고, `defaults` 없이 만든 Store에선 빈 맵이 된다 | `store-plan.md`, `attribute-plan.md` | ✅ 0개 / 1개 / 2개로 갈림 | +| `H-80` | 🟡 | M2가 `quad-types`의 `Quad`에 추가할 목록이 `Source`/`State`/`Store`뿐 — **`State`는 런타임 값이 아예 없고**, M2가 실제로 얹는 나머지 탑레벨 값(`Effect`·`is*` 전량·`bindLifetime` 4종…)이 전부 빠져 있다. 그 전부가 `H-25`가 만든 바로 그 벽에 부딪힌다 | `ROADMAP.md` M2, `quad-types-plan.md`, `source-state-plan.md` | 표면 대조 | +| `H-81` | 🟡 | `isModifier` 런타임 가드는 **전부 M2 코드**(`Source:Set`/Store 생성/`:Compute` 캐싱)에 들어가는데 체크박스는 **M7에만** 있고 M2 체크리스트엔 한 줄도 없다. 게다가 적용 지점 목록이 두 문서에서 다르다 | `ROADMAP.md` M7, `modifier-plan.md` 7번, `source-state-plan.md` | 표면 대조 | +| `H-82` | 🟢 | `:With`를 실노드로 확정한 **근거 2번이 pass-through 노드엔 성립하지 않는다** — 2026-08-14 재작성이 "근거가 더 강해졌다"면서 실제론 정확도를 낮췄다. 결론은 안 바뀜(근거 1·3이 유효) | `source-state-plan.md` | 문서 정합 | +| `H-83` | 🟢 | 확정된 Store 구현 스케치를 그대로 쓰면 **무인자 `Store()`가 `table.clone(nil)`로 크래시**한다 — 같은 문서가 `defaults`는 선택이라고 확정해뒀다 | `store-plan.md` | ✅ `table expected, got nil` | +| `H-84` | 🟢 | `:With(...)`/`state:Block(b)`/`Source:Emit()`이 M2 체크리스트에 개별 항목으로 없다 — `:Compute`/`:Apply`/`:Observer`는 각각 있는데 | `ROADMAP.md` M2 | 표면 대조 | + --- # 1차 패스 — 문서 대 문서 손 트레이싱 @@ -1007,6 +1025,494 @@ local rn: number = r -- ✅ U == number 로 정확히 추론 팩(`...deps`)이라 지금은 안 물리지만, "dep 1개짜리 특수 오버로드"를 나중에 만들고 싶어지면 여기부터 볼 것. +--- + +# 3차 패스 — 커밋된 코드·툴체인을 실제로 돌려봤다 + M2 소비자 문서 (2026-08-25) + +**왜 이 패스가 있는가**: 사용자 요청 — *"저기에 포함되지 않은 문제점을 +더 찾아봐. 찾은 다음에 진짜 있는 문제인지 재검증까지 다 해줘. 찾은걸 +이어붙이면 돼"*. + +**1·2차와 다른 각도 둘**: + +1. **2차는 문서가 주장하는 Luau 동작을 *새로 짠 최소 재현*으로 걸었다. + 3차는 저장소에 이미 있는 것을 그대로 돌린다** — `quad-base/test/smoke.*.luau`, + `luau-test/done/`의 스파이크 전량, `python3 .claude/tools/doc-check.py`, + 그리고 커밋된 `quad-base/src/*.luau`에 대한 `luau-analyze`. 즉 "설계가 + 맞나"가 아니라 **"지금 이 저장소가 문서가 말하는 상태인가"**를 본다. +2. **1차 패스가 범위에서 명시적으로 뺐던 문서들** — `tween-plan.md` / + `modifier-plan.md` / `attribute-plan.md` / `onchange-plan.md` / + `bind-system-plan.md` / `quad-types-plan.md` / `architecture.md` / + `project-setup-plan.md` / `module-lifecycle-plan.md`. 이들은 M2를 만드는 + 문서가 아니라 **M2가 만든 것을 쓰는** 문서라, "소비자가 요구하는데 M2 + 설계엔 없는 것"이 여기서만 보인다. + +**실측 환경**: `luau` / `luau-analyze` +(`~/.local/share/mise/installs/luau/latest`), `python3`, 2026-08-25 실행, +작업 트리는 이 패스 시작 시점 상태 그대로(커밋되지 않은 변경 없음). +**아래에서 `pesde` 링크를 실제 디렉토리로 치환한 구간이 있으나 검증 후 +전부 원상복구했고**(심볼릭 링크 20개, `git status` 클린), 그 폴더들은 +전부 `.gitignore` 대상이라 저장소 파일은 손대지 않았다. + +**이 패스의 범위**: 커밋된 `quad-base/src/`(`init.luau`/`Relate.luau`/ +`Debug/init.luau`) · `quad-types/src/` · `type-version-check/src/` · +`quad-base/test/` 3개 · 루트 `pesde.toml`/`.luaurc`/`default.project.json`/ +`mise.toml` · `.claude/luau-test/`(`STATUS.md` + `done/` 전량) · +`base/store-plan.md` 런타임 절 · `base/attribute-plan.md` 그룹 절 · +`base/modifier-plan.md` 7번 · `base/source-state-plan.md`의 온톨로지·전파 +모델·`:With` 절 · `base/state-epoch-plan.md` §2 · `base/quad-types-plan.md` · +`base/project-setup-plan.md` · `base/relate-plan.md` · `ROADMAP.md` M1/M2/M7. +**1·2차 패스(`H-55`~`H-76`)와 겹치는 항목은 없다** — 겹칠 뻔한 자리는 +항목 안에 "이건 `H-xx`와 별개다"로 적었다. + +--- + +## 🔴 `H-77` — `Relate`의 *내부 키*가 `inst`를 되참조하면 `SetStrong`/`SetWeak` **둘 다** 샌다 (실측) + +**어디**: `base/relate-plan.md`의 "위험한 패턴 — 서로 다른 두" 절, +`base/module-lifecycle-plan.md`의 "New()의 내부 구성 — InitXxx 팩토리 체이닝" 절, +그리고 **실제로 커밋된 `quad-base/src/init.luau`**. + +**`H-71`과 무엇이 다른가**: `H-71`은 `SetStrong(inst, key, **value**)`의 +**값**이 `inst`를 되참조하는 경우였고, 결론은 *"`SetWeak`은 안전하다"* +(케이스3이 0/50)였다. 이 항목은 **가운데 인자(`key`)** 다. `Relate`의 +실제 구조상 — + +```lua +-- quad-base/src/Relate.luau, 커밋된 그대로 +buckets[inst] = bucket -- buckets는 __mode = "k" +bucket.StrongMap[key] = value -- 평범한 테이블 → 키·값 모두 강함 +bucket.WeakMap = setmetatable({}, { __mode = "v" }) -- **값만** weak +``` + +`WeakMap`이 `__mode = "v"`이므로 **`SetWeak`도 키는 강하게 잡는다.** +그래서 키가 `inst`를 잡으면 `buckets`의 weak 키가 자기 자신의 버킷을 +통해 살아남는다 — `SetStrong`이든 `SetWeak`이든 똑같이. + +**실측 1 — 커밋된 `Relate.luau`를 그대로 require**: + +```lua +local Relate = require("./Relate") +-- 대조군: 내부 키가 inst를 캡처하지 않음 +local keyFn = function() return i end -- → 50개 중 0개 생존 +-- 케이스: 내부 키가 inst를 캡처 +local initFn = function() return inst end -- → 50개 중 50개 생존 +-- 같은 모양을 SetWeak으로 -- → 50개 중 50개 생존 +``` + +``` +[대조군 — 키가 inst 미참조] 0 +[케이스 — 키가 inst 캡처] 50 +[SetWeak — 키가 inst 캡처] 50 ← H-71의 해법 (b)가 여기선 안 듣는다 +``` + +**실측 2 — 커밋된 `quad-base/src/init.luau`의 `RunInit`이 실제로 물린다.** +그 파일은 `runInitRelate:SetStrong(self, initFn, true)`로 **함수 자신을 +내부 키**로 쓰고, 바로 위에 이렇게 적어뒀다: + +> `New()가 몇 번 불려도 module마다 weak-키잉되므로 별도 정리 불필요(module이 GC되면 이 기록도 같이 사라짐).` + +`initFn`이 module을 캡처하는 순간 이 주석이 거짓이 된다: + +```lua +local Quad = require("../src") +-- A: initFn이 module을 캡처하지 않는 정상형 +local function initFn(m) m.tagA = true end +for i = 1, 30 do local q = Quad.New(); q:RunInit(initFn); canaryA[i] = q end +-- B: initFn이 바깥 module 변수를 캡처 +for i = 1, 30 do local q = Quad.New(); q:RunInit(function() q.tagB = true end); canaryB[i] = q end +``` + +``` +[A] initFn이 module 미캡처 — 살아남은 module 수: 0 +[B] initFn이 module 캡처 — 살아남은 module 수: 30 +``` + +`runInitRelate`는 **모듈 레벨 상수**라 프로세스 수명 내내 산다. 즉 B +모양으로 `RunInit`을 쓰면 그 `New()` 인스턴스는 **영원히 안 죽는다.** +`q:RunInit(function() ... q ... end)`는 특별히 이상한 코드가 아니다 — +`initFn(self)`가 `self`를 주긴 하지만, 바깥 변수를 그냥 쓰는 게 더 짧아서 +자연히 나온다. + +**어느 서술이 틀렸나**: `relate-plan.md`의 규칙 문단은 위험을 전부 +**"값"** 기준으로만 서술한다 — *"어떤 값(`inst` 아닌 임의 객체 …)을 다른 +`Relate`의 바깥 키로 쓰고 싶어지면 … 그 값 자체가 `inst`로 되돌아가는 강한 +back-reference를 갖고 있는지 먼저 확인할 것"*. **`Relate` 자신의 두 번째 +인자(내부 키)는 이 문서 어디에도 등장하지 않는다.** `H-71`이 이미 그 +절 전체를 다시 쓰기로 만들어놨으니, 다시 쓸 때 **슬롯을 셋으로 나눠** +적어야 한다: + +| 슬롯 | `SetStrong` | `SetWeak` | +|---|---|---| +| 바깥 키(`inst`) | weak(설계) | weak(설계) | +| 내부 키(`key`) | **강함** | **강함** ← 여기 규칙이 없었다 | +| 값(`value`) | 강함(`H-71`이 다룸) | weak | + +**갈래(결정 전 목록)**: +(a) **규칙만 넓힌다** — "`Relate`의 내부 키로 쓰는 객체는 `inst`를 되참조하면 +안 된다(문자열/숫자/`inst`와 무관한 값 객체만)"를 못박고, 지금 내부 키가 +객체인 자리를 전수 확인한다. 실제로 객체를 내부 키로 쓰는 건 +`runInitRelate`(함수)와 `groupClaimKeys`(그룹 `Attribute` 값 객체) 둘뿐이고, +뒤는 값 객체가 `inst`를 안 잡으므로 안전하다 — **확정적으로 물리는 건 +`RunInit` 하나다.** +(b) **`RunInit`의 키를 바꾼다** — 함수 자신 대신 호출부가 주는 이름/토큰을 +키로 쓴다. 다만 `module-lifecycle-plan.md`가 "함수 자체를 릴레이션 키로 쓴다"를 +**센티널을 없애는 근거**로 확정해뒀으므로(위 절 참고) 이걸 되짚는 셈이 된다. +(c) **`Relate`에 내부 키까지 weak인 저장 모드를 추가한다** — `__mode = "kv"` +서브맵. 표면이 하나 늘고, ephemeron이 없으므로 이번엔 **키가 값을 잡는** +반대 방향 문제가 생긴다(값이 `true`인 `runInitRelate`엔 무해). +(a)가 제일 싸고 `H-71`의 규칙 재작성과 한 번에 끝난다. 어느 쪽이든 +**`quad-base/src/init.luau`의 그 주석은 지금 거짓이므로 같이 고쳐야 한다.** + +**이건 `H-71`과 별개다** — 같은 파일의 같은 절을 고치게 되지만, `H-71`의 +해법 (b)(`SetStrong` → `SetWeak`)가 **이 경우엔 아무 효과가 없다**는 게 +실측으로 확인됐으므로 따로 결정해야 한다. + +## 🔴 `H-78` — 커밋된 M1 스모크 2개와 타입 스파이크 `23`이 지금 저장소 상태에서 안 돈다 (실측) + +**어디**: `ROADMAP.md` M1의 *"`quad-base/test/mock.luau` + `smoke.*.luau`, 전부 PASS"* / +*"`RunInit`/`AddPlugin`으로 구현·smoke 테스트 검증 완료"*, +`base/project-setup-plan.md`의 "워크스페이스 의존성은 심볼릭 링크로 연결된다" 절, +`.claude/luau-test/STATUS.md`(스파이크 `23`이 `done/`). + +**실측 — 이 패스 시작 시점의 저장소 상태 그대로**: + +``` +$ luau quad-base/test/smoke.init.luau +error while running module: ./quad-base/src/init.luau:17: error while running +module: error requiring module "./.pesde/qwreey+quad_types/0.0.0/quad_types/src": +could not resolve child component "src" + +$ luau quad-base/test/smoke.plugin.luau → 같은 에러 +$ luau quad-base/test/smoke.mock.luau → ALL PASS (src를 require 안 함) +``` + +**원인은 심볼릭 링크다 — 격리해서 확인했다.** 이 `luau` CLI는 require +경로에 심볼릭 링크가 끼면 디렉토리든 파일이든 해소하지 않는다: + +```lua +require("./real/src") -- 실제 디렉토리 → ok +require("./linked") -- 같은 곳을 가리키는 심볼릭 링크 → could not resolve child component +``` + +`pesde`의 워크스페이스 링크가 전부 심볼릭 링크이므로(`find -type l` → 20개), +`quad-base/src/init.luau`가 `quad_types`를 require하는 순간 걸린다. + +**⭐ 더 나쁜 쪽은 타입 검사다 — 실패가 "진단 0건"처럼 보인다.** + +``` +$ luau-analyze quad-base/src/init.luau quad-base/src/Relate.luau ... +quad-base/roblox_packages/quad_types.luau(1,16): TypeError: Unknown require: unsupported path +quad-base/roblox_packages/quad_types.luau(2,21): TypeError: Unknown type 'module.Quad' +./quad-base/src/init.luau(37,3): TypeError: Cannot call a value of type *error-type* ... +``` + +즉 **`Quad` 타입 계약이 quad-base 쪽에서 아예 안 보인다.** 그리고 +`done/`에 들어 있는 `23-type-quadtypes-checkversion-addplugin.luau`는 +이 상태에서 **자기 음성 대조군이 한 건도 안 뜬다**: + +``` +$ luau-analyze .claude/luau-test/done/23-type-quadtypes-checkversion-addplugin.luau +quad-types/luau_packages/type_version_check.luau(1,16): TypeError: Unknown require: unsupported path +quad-types/luau_packages/type_version_check.luau(2,45): TypeError: Unknown type 'module.CheckVersion' +``` + +그 파일이 검증하려는 것(버전 불일치 시 진단)은 **한 줄도 안 나온다.** +`base/project-setup-plan.md`가 이미 경고해둔 실패 모드 그대로다 — +*"`luau-analyze`가 진단 0건이어도 타입이 제대로 해소됐다는 뜻이"* 아니다. + +**대조 — 문서가 적어둔 워크어라운드를 적용하면 전부 정상이다.** +심볼릭 링크 20개를 실제 디렉토리 복사본으로 치환하고 다시 돌리면: + +``` +smoke.init.luau → === ALL PASS === +smoke.plugin.luau → === ALL PASS === +luau-analyze quad-base/src/... quad-types/src/... type-version-check/src/... → 진단 0건 +스파이크 23 → TypeError: type-version-check: version "9.9.9" does not match pattern "0.0.0" + (정확히 의도한 음성 대조군 1건, 그 외 0건) +``` + +**즉 설계도 코드도 멀쩡하다 — 빠진 건 "이 저장소를 돌릴 수 있게 만드는 +단계"가 어디에도 절차로 없다는 것이다.** `project-setup-plan.md`는 그 +치환을 **과거에 한 번 손으로 했다는 기록**으로만 적어두고 스스로 +*"아직 반복 가능한 스크립트/mise task로 정식화하진 않음"*이라고 밝힌다. +그 사이에 — + +- `ROADMAP.md` M1의 "전부 PASS"에는 **날짜도 전제조건도 없다** — + `conventions.md`의 시한부 주장 규약(날짜를 붙일 것)에 걸리는 자리인데 + `doc-check.py`는 이 문장 모양을 안 잡는다(실제로 지금 ERROR 0이다). +- `luau-test/STATUS.md`는 `23`을 `done/`에 두고 실행법을 + `luau-analyze <파일>`이라고만 적는다 — 그대로 따라 하면 위 상태가 된다. +- **작업 트리가 이 패스 시작 시점에 이미 실패 상태였다.** 즉 그 사이의 + 어떤 세션이 `luau-analyze`를 돌렸다면 **거짓 클린**을 받았다. + +**왜 지금 이게 M2 문제인가**: M2는 **`quad-types`의 `Quad`에 필드를 추가하는 +첫 마일스톤**이고(`ROADMAP.md` M2의 `H-25` 파생 항목, 아래 `H-80`), 그 +추가가 실제로 먹었는지 확인하는 유일한 수단이 정확히 지금 조용히 망가져 +있는 그 경로다. + +**갈래**: (a) 치환을 `mise` task(또는 `.claude/tools/`의 스크립트)로 +정식화하고 `project-setup-plan.md`/`STATUS.md`/`ROADMAP.md`가 그걸 +가리키게 한다 — 문서가 이미 *"이 시점에 정식 스크립트화를 고려할 것"*이라 +예고해둔 선택지다. (b) 그 스크립트가 없으면 진단이 무의미하다는 걸 +`luau-test/README.md`/`STATUS.md`의 실행법 줄에 명시한다(최소 조치). +(c) `ROADMAP.md` M1의 "전부 PASS"에 **날짜와 전제조건**을 붙인다. +셋 다 서로 배타적이지 않다. + +**이건 `H-25`(닫힌 `Quad` 레코드)와 별개다** — 그쪽은 타입이 **좁아서** +에러가 나는 것이고, 이쪽은 타입이 **아예 안 보여서** 에러가 안 나는 것이다. + +## 🟡 `H-79` — `Store`에 열거 표면이 없는데 그룹 `Attribute`가 그걸 요구한다 (실측) + +**어디**: `base/attribute-plan.md`의 "그룹 `Attribute(...)` — 여러 Store를 한 번에 attribute로" 절 +(`attr:NameMap(): {[string]: Source}`, 그리고 *"각 Store에서 이름 붙은 +`Source` 슬롯을 그대로 가져와 자기 자신의 key→Source 맵에 넣는 것"*), +같은 문서의 "메커니즘 — 그룹 전용 키로 단일 키 경로에 위임" 절 +(`for name, source in pairs(v:NameMap()) do`), +`base/store-plan.md`의 "Store = Source들의 이름 붙은 모음" 절. + +**무엇이 어긋나나**: `Attribute(store)`는 Store를 **이름 집합으로 평탄화**해야 +하는데, `store-plan.md`는 열거 표면을 하나도 정의하지 않는다. 남는 건 +`pairs(store)`뿐이고, 그건 **raw 키만** 준다 — 그런데 같은 문서가 확정한 +Store 모델은 **eager + lazy**다: + +> `Store<>()`처럼 `defaults` 없이 만든 뒤 `.Key:Set(v)`를 부르는 경우 + +이 형태의 Store는 **생성 직후 raw 키가 하나도 없다.** + +**실측** — 확정 스케치를 그대로 옮긴 최소 모델(eager `table.clone` + +lazy `__index` + `rawset`): + +```lua +local s1 = Store({hp = 10, mp = 5}) +local n = 0; for k in s1 do n += 1 end --> 2 + +local s2 = Store() -- Store<{hp:number, mp:number}>() 상당 +local n2 = 0; for k in s2 do n2 += 1 end --> 0 ← Attribute가 볼 이름이 없다 +local _ = s2.hp -- 어딘가에서 한 번 읽히면 +local n3 = 0; for k in s2 do n3 += 1 end --> 1 ← 이제 1개 +``` + +``` +eager store 열거 개수: 2 +lazy-only store 열거 개수(Attribute가 볼 이름 수): 0 +hp 한 번 접근 후: 1 +``` + +즉 `Attribute(store)`가 잡는 이름 집합이 **"그 시점까지 누가 어떤 키를 +읽었는가"에 좌우된다.** 렌더 순서가 조금만 바뀌어도 attribute가 붙었다 +안 붙었다 하는, 재현이 어려운 종류의 버그다. + +**여기서 갈라지는 부수 질문 하나** — `:NameMap()`이 **생성 시점 스냅샷**인지 +**호출 시점 라이브 조회**인지도 안 정해져 있다. `attribute-plan.md`의 +`Attribute.Merged` 서술은 *"자기 자신의 key→Source 맵에 넣는 것"*이라 +스냅샷처럼 읽히는데, 의사코드는 `process`마다 `v:NameMap()`을 다시 부른다. +스냅샷이면 위 타이밍 의존이 그대로 굳고, 라이브면 **같은 그룹 값이 +디스패치마다 다른 키 집합을 내놓을 수 있어** 그 절이 확정한 "자기가 +등록했던 키 전부를 걷어내는" 클로저 계약과 부딪힌다(클로저는 `keys`를 +캡처하므로 실제론 안전하지만, 그러면 "라이브"인 의미가 없다). + +**필요한 것**: 셋 중 하나를 M2에서 정해야 한다 — +(a) **Store가 선언된 키 집합을 런타임에도 안다** — `Store(defaults)`가 +`defaults` 없이도 이름 목록을 받을 수 있게 하거나, 타입 쪽 선언에서 +런타임 목록을 만들 방법을 둔다(지금은 없다 — `store-plan.md`가 확정한 +방어선이 *"런타임이 아니라 타입"*이라 런타임엔 선언 정보가 0이다). +(b) **`Attribute(store)`는 그 시점 materialize된 키만 본다고 계약으로 +못박는다** — 그러면 `defaults`를 주는 게 사실상 필수가 되고, 그 사실을 +`store-plan.md`/`attribute-plan.md` 양쪽에 적어야 한다. +(c) **Store에 명시적 열거 표면을 하나 둔다**(`store:Names()` 류) — +`Tag:Names()`/`attr:NameMap()`과 같은 계열이고, `H-73`/`H-74`가 이미 +제기한 "예약 키냐 탑레벨 함수냐" 문제를 같이 받는다. + +**이건 `H-74`(eager 경로가 `__index`를 우회한다)와 별개다** — 그쪽은 +예약 키가 가려지는 문제이고, 이쪽은 **키가 아직 존재하지 않는** 문제다. +다만 둘 다 "eager와 lazy가 서로 다른 것을 보고 있다"는 같은 뿌리라 +같이 보는 게 낫다. + +## 🟡 `H-80` — M2가 `Quad`에 추가할 목록이 `Source`/`State`/`Store`뿐이다 + +**어디**: `ROADMAP.md` M2의 `H-25` 파생 체크박스(*"`quad-types`의 `Quad`에 +`Source`/`State`/`Store` 필드 추가"*), `base/quad-types-plan.md`의 +"`Quad` 타입 — 확정된 표면" 절과 "`AddPlugin` — 실측 검증된 플러그인 체이닝" 절, +`base/source-state-plan.md`의 "핵심 온톨로지" 절. + +**두 가지가 어긋난다.** + +**(1) `State`는 런타임 값이 아예 없다.** 코퍼스 어디에도 `State(...)` +생성자가 없다 — State는 `:With`/`:Compute`/`:Gate`로만 생기고, 같은 +문서가 *"State는 쓰기 대상이 아님"*으로 확정해뒀다. 그래서 `Quad`에 +넣을 수 있는 건 **타입 재수출**(`export type State`)뿐인데, 체크박스는 +`Source`/`Store`와 나란히 **"필드"**라고 적는다. 구현자가 그대로 읽으면 +`Quad`에 `State: ???`를 만들려다 막힌다. + +> **부수 — `state(state)`라는 옛 표기가 아직 살아 있다.** +> `source-state-plan.md`의 "핵심 온톨로지" 절이 State의 합성 모델을 +> *"`state(state)`로 기존 state의 결과를 받아 새 state를 만들어 분기 가능"* +> 이라고 적고, `base/architecture.md`의 소스 트리 주석(`State.luau`)도 +> *"state(state) 분기"*를 그대로 복사해뒀다. 이건 2026-08-04 시점 표기이고 +> 실제 확정 표면은 `:With`/`:Compute`다 — **호출 가능한 `State(x)`가 있는 +> 것처럼 읽히므로** (1)과 같이 정리하는 게 좋다. + +**(2) M2가 얹는 나머지 탑레벨 값이 전부 빠져 있다.** 같은 마일스톤의 +다른 체크박스들이 이미 요구하는 것만 모아도 — + +| M2가 만드는 탑레벨 값 | 어느 체크박스가 요구하나 | +|---|---| +| `Effect(fn, ...deps)` | M2 "`Effect(fn, ...deps)`" 항목 | +| `is*` 전량(`isState`/`isSource`/`isObserver`/`isEffect`/`isEpoch`/`isStore`/…) | M2 `Brand.luau` 항목 | +| `bindLifetime`/`unbindLifetime`/`canBound`/`canExecute` | M2 `LifetimeHandle.luau` 항목(*"네임스페이스 없이 탑레벨 함수로 export"*) | +| `Relate()` | M2 `Relate.luau` 항목 | +| `Blocker()` | M2 `Blocker.luau` 항목 | + +`H-25`가 실측으로 확인한 벽은 *"`New(): Quad`가 닫힌 레코드이고 `RunInit`은 +반환값이 없어 타입을 못 넓힌다"*였다. 그 벽은 `Dispatch` 하나에만 있는 게 +아니라 **위 전부에 똑같이 있다** — `quad.isState(v)`도 `quad.Effect(fn)`도 +지금 `Quad`엔 없으므로 `luau-analyze`에서 그대로 `Key not found`다. + +**갈래**: (a) M2 체크박스의 목록을 위 표까지 확장하고, `State`는 +"타입 재수출만"으로 명시한다(가장 싸다). (b) 규칙 자체를 *"그 +마일스톤이 `quad-base`의 `init.luau`에 심는 모든 표면"*으로 다시 쓰고 +마일스톤마다 목록을 안 세게 한다 — `quad-types-plan.md`의 그 절이 규칙의 +정본이므로 거기서 한 번만 정하면 된다. 어느 쪽이든 `ROADMAP.md` M3의 +같은 항목(`Dispatch` 기준)도 같이 봐야 한다. + +## 🟡 `H-81` — `isModifier` 런타임 가드는 전부 M2 코드인데 체크박스는 M7에만 있다 + +**어디**: `base/modifier-plan.md`의 "7. State/Source가 Modifier를 값으로 담는 것" 절, +`base/source-state-plan.md`의 "따름정리 — `Store`/`Source`의 `T`는 Modifier가 될 수 없음" 절, +`ROADMAP.md` M7의 *"`State` 조합에 `isModifier` 기반 명시적 error 적용"* 체크박스. + +**(1) 마일스톤이 어긋난다.** `modifier-plan.md` 7번이 확정한 적용 지점은 +셋인데 **전부 M2가 쓰는 파일**이다: + +- `Source:Set(value)` → `Source.luau`(M2) +- `Store({defaults})` 생성 시 각 키를 `Source(v)`로 만드는 시점 → `Store.luau`(M2) +- State의 `:Compute(fn)` 결과를 캐시로 저장하기 직전 → `State.luau`(M2) + +판별자 `isModifier` 자체는 M2 앞머리 `Brand.luau` 항목에 이미 들어 있으므로 +**M2 시점에 쓸 수 있다.** 그런데 체크박스는 M7에만 있고 **M2 체크리스트엔 +한 줄도 없다.** 그대로 가면 (a) M2 구현자가 이 훅 자리를 모른 채 세 파일을 +짜고, (b) M7이 M2 코드를 다시 열어야 하며, (c) 그 사이 **M4(첫 end-to-end +반응형 업데이트)가 가드 없이 돈다.** + +**(2) 적용 지점 목록이 두 문서에서 다르다.** `source-state-plan.md`의 +"따름정리" 절은 *"`Source`(Store를 거치지 않는 독립 +`Source(someModifier)`)에도 동일하게 적용됨"*이라고 **독립 생성자**를 +명시하는데, `modifier-plan.md` 7번의 적용 지점 열거엔 그 자리가 없다 +(`Source:Set` / Store defaults / `:Compute` 캐싱 셋뿐). 구현자가 +`modifier-plan.md`만 보면 `Source(someModifier)`가 그냥 통과한다. + +**필요한 것**: (a) `ROADMAP.md` M2에 "위 세 자리(+ `Source(v)` 생성자)에 +`isModifier` 게이트를 같이 심는다"를 항목으로 넣고 M7 항목은 그걸 +가리키게 한다, (b) `modifier-plan.md` 7번의 적용 지점 목록에 독립 +생성자를 추가해 두 문서를 맞춘다. 둘 다 순수 문서 작업이고 설계는 안 +바뀐다. + +## 🟢 `H-82` — `:With`를 실노드로 확정한 근거 2번이 pass-through 노드엔 성립하지 않는다 + +**어디**: `base/source-state-plan.md`의 "`:With`도 새 State 노드로 확정, 가변인자로 체인 남발 방지" 절의 +근거 2번(*"공유 캐시를 못 타고 중복 계산이 생김. [2026-08-14 근거 재작성]"*). + +**무엇이 어긋나나**: 그 근거는 `w = key1:With(key2)`에서 갈라지는 +`c1 = w:Compute(g1)` / `c2 = w:Compute(g2)`를 들며 *"빌더면 `w`라는 노드가 +아예 없어서 c1/c2가 key1/key2에 각자 직접 구독을 걸고 각자 계산하므로, +공유 지점이 사라짐"*이라고 한다. 그런데 **같은 절이 바로 아래에서 +`:With` 노드를 "계산 함수는 없고 값은 `self`를 그대로 통과(pass-through)"** +로 확정한다. + +계산이 없으므로 **공유될 계산이 없다.** `c1`은 `g1`을, `c2`는 `g2`를 +돌리고, 그 둘이 읽는 `key1`/`key2`의 캐시는 **빌더든 노드든 어느 쪽이든 +key1/key2 자신이 들고 있다.** 실노드 `w`가 실제로 아끼는 건 계산이 +아니라 **엣지 수와 에포크 부기**다(`key1 → w` 하나 대 `key1 → c1`, +`key1 → c2` 둘 — `state-epoch-plan.md` §7의 맵 크기가 그만큼 덜 는다). + +**왜 이게 눈에 띌 만한가**: 그 근거는 2026-08-14에 *"원래 이 항목은 +`invalid` 플래그로 다이아몬드 중복 워크 방지 장치를 근거로 들었으나 그 +장치는 폐기됨"*이라며 **일부러 다시 쓴 것**이고, 그러면서 +*"근거의 강도는 이 재작성으로 오히려 올라감: 예전 근거는 순회 비용 +최적화였지만, 지금 근거는 실제 중복 **계산**임"*이라고 스스로 평가한다. +실제로는 **정확한 서술(순회 비용)에서 부정확한 서술(중복 계산)으로 +내려간 것**이다. 바로 위 "왜 State 체인을 Modifier처럼 플래튼하지 않는가" 절의 +같은 논증은 `b = a:Compute(f)`(계산이 **있는** 노드)를 예로 들어서 맞는데, +그 논증을 계산이 없는 노드에 그대로 복사하면서 어긋났다. + +**결론은 안 바뀐다** — 근거 1(디버그 그래프 1:1 대응)과 근거 3(clone이 +Compute 노드의 캐시 슬롯까지 복사해 실제로 깨짐)이 그대로 유효하다. +근거 2를 "엣지/부기 공유"로 고쳐 적기만 하면 된다. + +## 🟢 `H-83` — 확정된 Store 구현 스케치를 그대로 쓰면 무인자 `Store()`가 크래시한다 (실측) + +**어디**: `base/store-plan.md`의 "Store = Source들의 이름 붙은 모음" 절 — +*"`defaults`는 선택(안 줘도 됨, 순수 편의용 초기값 템플릿)"*과, 같은 절의 +확정된 구현 스케치 *"`local sources = table.clone(defaults); for k, v in +sources do sources[k] = Source(v) end`"*. + +``` +$ luau -e 'print(pcall(function() return table.clone(nil) end))' +false invalid argument #1 to 'clone' (table expected, got nil) +``` + +`defaults`가 선택인데 스케치엔 `or {}`가 없다. **`table.clone(defaults or {})`** +한 곳만 고치면 된다 — 구현 시 자연히 걸릴 수도 있지만, 그 스케치는 +*"성능 근거"*까지 붙여 확정된 문장이라 그대로 옮겨 적힐 가능성이 높다. +같은 자리에서 `Source()`(무인자 = `Source(nil)`)는 이미 확정돼 있으므로 +추가 결정은 없다. + +## 🟢 `H-84` — `:With`/`state:Block`/`Source:Emit`이 M2 체크리스트에 개별 항목으로 없다 + +**어디**: `ROADMAP.md` M2의 "반응형 본체" 절. + +`:Compute(fn, ...)`·`state:Apply(factory)`·`state:Observer(fn)`· +`state:Gate(setup)`는 각각 체크박스를 갖는데, 다음 셋은 `Source.luau`/ +`State.luau`/`Store.luau`라는 한 줄짜리 포괄 항목 안에만 있다: + +- **`:With(...)`** — 새 노드를 만드는 프리미티브이고, `source-state-plan.md`가 + *"`:With`가 만드는 pass-through 노드는 계산 함수가 없어서"* 우연한 캡처가 + 없다며 **미해결 항목(중간 State GC)의 핵심 사례로 지목**한 바로 그 자리다. + 그 미해결이 `question.md` 최우선 절에 있으므로, 체크박스가 없으면 + "결론이 어디에 반영돼야 하는가"가 로드맵에서 안 보인다. +- **`state:Block(b)`** — `Blocker.luau` 항목은 `:On()`/`:IsOn()`/ + `:OffWithoutEmit()`만 나열한다. **State 쪽 진입점**은 안 적혀 있는데, + `blocker-plan.md`가 확정한 공개 API이고 `H-55`의 결정이 정확히 이 + 진입점의 계약을 바꾼다. +- **`Source:Emit()`** — `state-epoch-plan.md` §8이 *"`Revision`만 갱신하면 + 그대로 동작한다"*로 확정해둔 자리이고, `H-68`(같은 값 `Set`의 동작)의 + 결정이 이것의 존재 이유를 바꾼다. + +순수 로드맵 정합이라 🟢이지만, **`H-55`/`H-68`과 "중간 State GC"의 결론이 +각각 어디로 가야 하는지가 지금 로드맵에 자리가 없다**는 뜻이라 그 셋을 +결정할 때 같이 처리하는 게 낫다. + +--- + +## 부록 — 3차 패스에서 돌려봤는데 **문제가 없던 것** + +같은 각도로 훑었지만 실측 결과 문서와 일치했던 것들. 다음 라운드가 +같은 곳을 다시 파지 않도록 남긴다. + +- **`bit32.bnot(-rev)` 리비전 갱신** — `base/state-epoch-plan.md` §2의 + 실측 표(`0 → 4294967295`, `1 → 0`, `2 → 1`, `4294967295 → 4294967294`)가 + **정확하다**. `0`에서 10회 돌려도 `4294967295`부터 1씩 내려가며 중복이 + 없다. `-0` 입력(`rev == 0`)도 문서 서술대로 동작한다. +- **`python3 .claude/tools/doc-check.py`** — **ERROR 0건**, WARN 37건 + (전부 판단이 필요한 종류: `-followup.md`류 상대 표기, 날짜 없는 완결 + 주장). 회귀 없음. +- **`luau-test/done/`의 런타임 스파이크 8개**(`02`/`03`/`06`/`07`/`11`/ + `17`/`18`/`20`) — **전원 재통과**, FAIL 문자열 0건. +- **`luau-test/STATUS.md`의 개수 표** — 실제 폴더와 일치 + (`done/` 16, `rewrite-required/` 7, `not-run/` 0+헬퍼 1). +- **타입 스파이크의 진단 개수** — 워크어라운드 적용 후 `08`/`09`/`12`/ + `13`/`14`/`16`/`21`/`23` 전부 각자 의도한 음성 대조군 수만큼만 뜬다 + (`08`의 `Recursive type being used with different parameters`는 + `base/typing-limits.md`가 확정한 **의도된** 결과다). +- **커밋된 `quad-base/src/*.luau`의 타입** — 워크어라운드 적용 시 + `luau-analyze` **진단 0건**. `H-78`의 에러는 전부 링크 해소 실패에서 + 파생된 것이고 소스 자체의 타입 문제가 아니다. +- **`Relate.luau`의 `WeakMap` 공유 메타테이블/버킷 lazy 생성** — + `base/relate-plan.md`의 "실제 구조" 절 서술과 커밋된 코드가 일치한다 + (문제는 코드가 아니라 `H-77`의 **규칙 쪽 공백**이다). + + --- ## 회신 방법 @@ -1028,3 +1534,12 @@ local rn: number = r -- ✅ U == number 로 정확히 추론 `SetStrong`을 쓰는 모든 자리가 같은 실수를 반복한다. `H-72`는 `H-55`와 같은 뿌리(`Epoch` 일반화 때 게이트 쪽 요구가 표면에 덜 반영됨)라 그것과 같이 결정하는 게 낫다. + +**3차 패스**: `H-77`은 **`H-71`과 반드시 같이** 볼 것 — 같은 절을 고치게 +되지만 `H-71`의 해법 (b)가 여기선 안 듣는다는 게 실측으로 확인됐다. +`H-78`은 설계 결정이 아니라 **작업 환경 결정**이라 다른 것들과 독립이고, +지금 상태로 두면 M2 내내 타입 검사가 조용히 무의미해지므로 **착수 전에 +닫는 게 낫다**. `H-79`/`H-83`은 Store 런타임 하나에서 갈라져 나온 것이라 +`H-73`/`H-74`와 같이 보면 되고, `H-80`/`H-81`/`H-84`는 전부 **로드맵 +체크리스트가 실제 작업을 다 안 담고 있다**는 한 가지 문제의 세 얼굴이라 +한 번에 고치면 된다. `H-82`는 순수 문서 정정이고 결론을 안 바꾼다.