diff --git a/.claude/README.md b/.claude/README.md index 3f79a13..21f5e4c 100644 --- a/.claude/README.md +++ b/.claude/README.md @@ -72,7 +72,7 @@ | `additional-primitives-plan.md` | **[2026-08-09 세 번째 세션, 전부 해소]** 마지막으로 남아있던 키 기반 동적 컬렉션 재조정도 `Slot:List(...)`로 확정되어 `base/slot-plan.md`로 승격 — 이 문서엔 새로 열린 설계 질문 없음, "빈 자리 아닌 것"/"문서화 백로그"/조사 소스 목록만 배경 자료로 유지 | 하 — 배경 리서치 기록용, 열린 결정 없음 | | `pre-implementation-audit.md` | M0 착수 직전 크리티컬 감사(2026-08-06 신설) — `base/` 전체를 모호성/지연결정리스크/단순화후보 세 렌즈로 재검토, 11개 우선순위1 + 11개 우선순위2 + 2개 단순화후보. **[2026-08-12 열일곱 번째 세션]** 우선순위1 11개 전원 해소 — 남은 건 `.claude/luau-test/` 스파이크 실측 확인뿐 | 상 — 설계는 전부 해소, `.claude/luau-test/` 스파이크 실측만 남음 | | `operator-sugar-plan.md` | **[2026-08-12 신설]** `Sum`/`Product`/`Not`/비트연산 등 `:Compute`/`:Apply`용 연산자 콤비네이터 슈가 — 메커니즘은 이미 확정된 계약(`Animate`와 동형 패턴) 재사용이라 확정, 네임스페이스 이름만 미정. **[2026-08-12 열아홉 번째 세션]** 서브 에이전트 외부 리서치로 다른 리액티브 라이브러리 선례와 대조 — `Operator`가 가장 강한 선례(Python `operator` 모듈), `Clamp`/`Min`/`Max`가 추가 후보로 부상, 비트연산·비교연산자·`Sub`/`Div`는 선례 전무로 드랍 후보, Debounce/Throttle은 `Blocker`와 다른 시간 기반 메커니즘이라 별도 질문으로 분리, `Filtered`의 Slot 안/밖 구분 판단이 ReactiveUI/SolidJS 선례로 뒷받침됨 — 최종 이름 결정은 여전히 사용자 몫. **[2026-08-13 세션, 두 번째]** Haskell 비교 리서치 중 `Alternative`(nil 대체값, coalesce류) 후보 신설 — 카탈로그 확정 규칙에 그대로 맞음, 이전엔 없던 게 확인됨 | 하 — 구현은 맨 마지막(순수 슈가, 없어도 무방, 함수 간 의존 없음), 사용자가 직접 후순위 지정 **[2026-08-13 여섯 번째 세션]** `State|T>` → `State` 평탄화 항목 신설(백로그) — `State>`가 정상 동작하게 됐지만 `retractFrom`의 힌트가 직속 1단계에만 가서 깊은 중첩에선 깜빡임 방지가 꺼진다는 게 구체적 동기, 사용자 판단으로 "UB는 아니지만 원치 않는 방향". `Operator.*`가 아니라 `state:Flatten()` 메소드로 제공하는 게 맞아 보이며, **반환 노드가 동적 의존성을 갖는다는 난점**(quad가 의도적으로 비지원하기로 한 바로 그것)이 확정 전 최대 쟁점 | -| `component-fallback-plan.md` | **[2026-08-14 신설]** 컴포넌트 함수를 감싸 에러 시 자동으로 플레이스홀더(fallback 컴포넌트)를 그려주는 유틸 `Fallback(original, onError)` — `additional-primitives-plan.md`가 이미 확정한 "Error Boundary는 빈 자리 아님, `pcall(MyComp,props)`로 충분"이라는 결론 위에 얹는 순수 슈가(`Operator`가 `:Compute`/`:Apply` 위에 얹힌 것과 같은 관계, 그 문서 재오픈 아님). `pcall`/`xpcall` 선택, 패키지 배치, 이름 전부 미정 | 하 — 형제 백로그(`quad-mock`/`quad-debug`/`Operator`)와 동급, "quad 개발 상당 부분 끝난 뒤" | +| `component-fallback-plan.md` | **[2026-08-14 신설]** 컴포넌트 함수를 감싸 에러 시 자동으로 플레이스홀더(fallback 컴포넌트)를 그려주는 유틸 `Fallback(original, onError)` — `additional-primitives-plan.md`가 이미 확정한 "Error Boundary는 빈 자리 아님, `pcall(MyComp,props)`로 충분"이라는 결론 위에 얹는 순수 슈가(`Operator`가 `:Compute`/`:Apply` 위에 얹힌 것과 같은 관계, 그 문서 재오픈 아님). **[같은 날 두 번째 세션]** `xpcall`+`debug.traceback` 메커니즘은 `component-fallback-xpcall-spike.luau`로 실측 확인 완료(클로저 업밸류 배선/중첩 스택 캡처 정상, `error(msg)` 기본 위치 접두 캐비엇 신규 확인) — `pcall` vs `xpcall` 정책, 패키지 배치, 이름은 여전히 미정 | 하 — 형제 백로그(`quad-mock`/`quad-debug`/`Operator`)와 동급, "quad 개발 상당 부분 끝난 뒤" | | `lifecycle-hooks-plan.md` | **[2026-08-14 신설]** `OnCreated`/`OnDestroyed` 생명주기 훅 — `OnCreated(fn)`은 `PreRef():Callback(fn)`, `OnDestroyed(fn)`은 `Effect(function() return fn end)`를 반환하는 순수 팩토리 함수라 새 타입/Dispatch 메커니즘이 전혀 필요 없음(호출 즉시 평가돼 기존 `PreRef`/`EffectHandle` 인스턴스로 사라짐), 여러 개 나란히 등록도 자연 지원. `OnRendered`는 현재 base에 없는 post-pass가 실제로 필요해 공짜가 아니라 **지금은 의도적으로 구현 안 함** — 거울상 `PostRef` 스케치(`PreRef`의 pre-pass와 대칭인 post-pass)만 백로그 후보로 남김 | 하 — 형제 백로그(`quad-mock`/`quad-debug`/`Operator`/`Fallback`)와 동급, "quad 개발 상당 부분 끝난 뒤" | | `v1-compat-plan.md` | v1 하위호환(compat) 레이어 — `quad-roblox-v1-compat` 패키지, v2→v1 단방향 브리지(`state:Observer()`+v1 프로퍼티 재대입), v2-in-v1/v1-in-v2 두 임베딩 방향의 기술 규칙까지 확정. quad2-try의 `quad-compat`은 빈 폴더로 실제 시도된 적 없었음을 확인 | 하 — Slot이 foreign Instance를 어떻게 다루는지만 Slot 코어 구현 시점까지 미결 | diff --git a/.claude/research/component-fallback-plan.md b/.claude/research/component-fallback-plan.md index cef4dda..a045ace 100644 --- a/.claude/research/component-fallback-plan.md +++ b/.claude/research/component-fallback-plan.md @@ -8,7 +8,10 @@ (`research/operator-sugar-plan.md`)가 `:Compute`/`:Apply` 위에 얹힌 것과 같은 관계. 우선순위는 그 형제 백로그 항목들(`quad-mock`/`quad-debug`/ 문서 사이트/`Operator`)과 동급 — "quad 개발 상당 부분 끝난 뒤"로 사용자가 -명시(`CLAUDE.md` "지금 할 일" 4번). +명시(`CLAUDE.md` "지금 할 일" 4번). **[2026-08-14 두 번째 세션]** 메커니즘 +자체(`xpcall`+`debug.traceback` 배선)는 `research/component-fallback-xpcall-spike.luau`로 +실측 확인 완료 — 아래 "메커니즘 스케치"/"열린 질문" 절 참고, 백로그 +우선순위 자체는 안 바뀜(여전히 착수 안 함). ## 동기 (사용자 원 메모) @@ -62,13 +65,25 @@ function Fallback(original, onError) end ``` -(의사코드 수준 — `xpcall` 에러 핸들러 안에서 잡은 `trace`를 업밸류로 -빼내는 게 실제로 원하는 순서/타이밍에 실행되는지는 Luau로 직접 실측 -필요, 아래 "열린 질문" 참고.) `research/debug-tooling-plan.md`가 이미 -확인해둔 선례(Vide/Fusion 둘 다 `xpcall`+`debug.traceback`으로 **에러 +**[2026-08-14 두 번째 세션, 실측 완료]** 위 의사코드 그대로 `luau` +스파이크(`research/component-fallback-xpcall-spike.luau`)로 돌려 확인 — +`xpcall` 에러 핸들러 안에서 업밸류 `trace`에 쓴 값이 `xpcall` 리턴 이후 +`onError` 호출 시점에 정상적으로 채워져 있고, `debug.traceback(nil, 2)`가 +익명 에러 핸들러 프레임을 건너뛰고 실패 지점까지의 실제 호출 스택(3단 +중첩까지 확인)을 정확히 담는 것도 확인됨. `research/debug-tooling-plan.md`가 +이미 확인해둔 선례(Vide/Fusion 둘 다 `xpcall`+`debug.traceback`으로 **에러 나는 순간에만** 스택을 찍는 패턴)를 그대로 재사용 — 새 트레이싱 메커니즘을 발명하지 않음. +**같은 실측에서 새로 확인된 캐비엇 — `error(msg)`의 기본 위치 접두**: +컴포넌트 저자가 레벨 지정 없이 `error("메시지")`만 호출하면(Luau 기본 +level=1), `onError`가 받는 `errorMessage`엔 quad가 아무것도 안 붙였는데도 +`"MyComp.luau:42: 메시지"`처럼 **파일:줄 접두가 이미 붙어서** 옴 — +`error(msg, 0)`으로 호출해야 접두 없는 순수 메시지가 옴. `Fallback` 쪽 +코드가 만드는 게 아니라 Luau `error()` 자체의 기본 동작이라 `Fallback`이 +따로 손댈 지점은 아니지만, 아래 "프로덕션에서의 동작" 열린 질문(화면에 +그대로 노출할지)에 실제로 영향을 주므로 그 항목에도 반영. + ### `ErrorComp`가 추가 상태가 필요하면 — 커링 (사용자 명시) `onError` 자체가 클로저이므로, 별도 API 없이 그냥 커링으로 풀림: @@ -101,10 +116,15 @@ local SafeWidget = Fallback(Widget, makeErrorHandler(someContext)) 캡처할지, 아니면 가벼운 `pcall`(에러 메시지만)을 기본으로 하고 트레이스는 옵션(`onError`가 2번째 인자를 안 받으면 그냥 안 계산)으로 둘지. Roblox `debug` 라이브러리가 제한적이라는 건 이미 확인돼 있어서(`debug-tooling-plan.md`) - 부담은 크지 않아 보이나 실측 필요. -- **`xpcall` 에러 핸들러 배선의 실측**: 위 의사코드가 실제 Luau에서 - 그대로 동작하는지(에러 핸들러 안에서 클로저 업밸류에 쓴 값이 바깥에서 - 제대로 보이는지 등) 착수 시점에 `luau`로 직접 확인 필요. + 부담은 크지 않음 — 여전히 미정인 건 "항상 캡처 vs 옵션"이라는 정책 + 판단뿐, 메커니즘 자체는 아래처럼 실측 완료. +- **[2026-08-14 두 번째 세션, 해소]** ~~`xpcall` 에러 핸들러 배선의 + 실측~~: `luau` 스파이크(`research/component-fallback-xpcall-spike.luau`, + 10개 검증 전부 통과)로 확인 — 에러 핸들러 안에서 클로저 업밸류에 쓴 + 값이 바깥에서 정상적으로 보이고, 3단 중첩 호출까지 `debug.traceback`이 + 실패 지점을 정확히 담음. 부수적으로 `error(msg)` 기본 호출이 위치 + 접두(`"파일:줄: "`)를 자동으로 붙인다는 캐비엇을 새로 확인(아래 + "프로덕션에서의 동작" 항목에 반영). - **패키지 배치**: `original`을 그냥 호출하고 결과를 그대로 돌려주는 순수 함수라 Store/Dispatch 어디에도 안 걸림 — `quad-base`(엔진 무종속)가 자연스러워 보임, `Operator`와 같은 결. 최종 확인 필요. @@ -117,7 +137,12 @@ local SafeWidget = Fallback(Widget, makeErrorHandler(someContext)) 그대로 노출할지, 아니면 로그로만 보내고 화면엔 일반화된 메시지만 보여줄지는 `onError` 구현(사용자 코드) 몫으로 완전히 열어두는 게 맞아 보임 — `Fallback` 자체는 raw 에러 정보를 그대로 넘기기만 하고 가공은 - 안 함(가공까지 대신해주면 그게 또 다른 매직). + 안 함(가공까지 대신해주면 그게 또 다른 매직). **[2026-08-14 두 번째 + 세션 추가]** 이 raw 정보엔 `error(msg)`(레벨 지정 없는 기본 호출)의 + 자동 위치 접두("파일:줄: ")도 포함됨이 실측으로 확인됨 — 화면에 그대로 + 노출하고 싶지 않은 저자는 `error(msg, 0)`으로 직접 접두를 꺼야 함, + `Fallback`이 대신 벗겨주지는 않음(문서화로 안내할 사항, 위 "가공 안 + 함" 원칙과 일치). - 그 외 확정된 결정 없음 — 착수 시점에 위 항목들을 순서대로 확인. ## 우선순위 diff --git a/.claude/research/component-fallback-xpcall-spike.luau b/.claude/research/component-fallback-xpcall-spike.luau new file mode 100644 index 0000000..6fe29d6 --- /dev/null +++ b/.claude/research/component-fallback-xpcall-spike.luau @@ -0,0 +1,175 @@ +-- 스파이크: research/component-fallback-plan.md의 Fallback 메커니즘 의사코드가 +-- 실제 Luau에서 그대로 동작하는지 실측. 열린 질문: +-- "xpcall 에러 핸들러 배선의 실측 — 에러 핸들러 안에서 클로저 업밸류에 쓴 +-- 값이 바깥에서 제대로 보이는지, debug.traceback이 에러 시점 스택을 정확히 +-- 찍는지" +-- luau fallback-xpcall-spike.luau 로 실행 + +local failures = 0 +local function check(name, cond) + if cond then + print("[PASS] " .. name) + else + failures += 1 + print("[FAIL] " .. name) + end +end + +-- 문서의 의사코드 그대로 옮김 +local function Fallback(original, onError) + return function(...) + local trace: string? = nil + local ok, resultOrErr = xpcall(original, function(err) + trace = debug.traceback(nil, 2) + return err + end, ...) + if ok then + return resultOrErr + end + return onError(resultOrErr, trace) + end +end + +-- 1) 성공 경로 — onError 호출 안 되고 원래 값 그대로 통과하는지 +do + local Widget = function(props) + return "OK:" .. props.name + end + local calledOnError = false + local SafeWidget = Fallback(Widget, function(msg, trace) + calledOnError = true + return "ERR" + end) + local result = SafeWidget({ name = "widget1" }) + check("성공 경로 — 원래 반환값 그대로 통과", result == "OK:widget1") + check("성공 경로 — onError 호출 안 됨", calledOnError == false) +end + +-- 2) 실패 경로 — 업밸류 trace가 onError 호출 시점에 이미 채워져 있는지 +do + local Widget = function(props) + error("boom: " .. props.name) + end + local capturedMsg, capturedTrace = nil, nil + local SafeWidget = Fallback(Widget, function(msg, trace) + capturedMsg = msg + capturedTrace = trace + return "PLACEHOLDER" + end) + local result = SafeWidget({ name = "widget2" }) + check("실패 경로 — onError의 반환값이 최종 결과", result == "PLACEHOLDER") + check("실패 경로 — 에러 메시지가 onError에 전달됨", capturedMsg ~= nil and string.find(capturedMsg, "boom: widget2") ~= nil) + check("실패 경로 — trace 업밸류가 onError 호출 시점에 nil이 아님(클로저 배선 정상)", capturedTrace ~= nil) + if capturedTrace then + print(" captured trace:\n" .. capturedTrace) + end +end + +-- 3) 깊은 중첩 호출에서도 traceback이 실패 지점까지 스택을 담는지 +do + local function level3(props) + error("deep failure") + end + local function level2(props) + return level3(props) + end + local function level1(props) + return level2(props) + end + local capturedTrace = nil + local SafeWidget = Fallback(level1, function(msg, trace) + capturedTrace = trace + return "PLACEHOLDER" + end) + SafeWidget({}) + local mentionsLevel3 = capturedTrace ~= nil and string.find(capturedTrace, "level3") ~= nil + check("깊은 중첩 — traceback에 실패 지점 함수(level3)가 보임", mentionsLevel3) + if capturedTrace then + print(" nested trace:\n" .. capturedTrace) + end +end + +-- 4) 문자열이 아닌 에러 값(table)도 onError로 그대로 전달되는지 +do + local Widget = function(props) + error({ code = 42, reason = "custom" }) + end + local capturedMsg = nil + local SafeWidget = Fallback(Widget, function(msg, trace) + capturedMsg = msg + return "PLACEHOLDER" + end) + SafeWidget({}) + check("비-문자열 에러 값(table)도 onError에 그대로 전달됨", type(capturedMsg) == "table" and capturedMsg.code == 42) +end + +-- 5) onError 자체가 커링된 클로저인 경우(문서의 "추가 상태 필요하면 커링" 관용구) +do + local Widget = function(props) + error("ctx-fail") + end + local function makeErrorHandler(context) + return function(message, trace) + return "PLACEHOLDER(" .. context .. "):" .. message + end + end + local SafeWidget = Fallback(Widget, makeErrorHandler("someContext")) + local result = SafeWidget({}) + -- error("ctx-fail")의 기본 level(=1)이 "파일:줄: " 접두를 붙이므로 + -- message가 정확히 "ctx-fail"이 아님 — 아래 6번에서 별도로 실측/확정. + check("커링된 onError 관용구가 그대로 동작(접두 포함 메시지가 이어붙여짐)", + string.find(result, "^PLACEHOLDER%(someContext%):.*ctx%-fail$") ~= nil) +end + +-- 6) error(msg)의 기본 동작 — onError가 받는 message에 "파일:줄: " 접두가 +-- 자동으로 붙는지(레벨 지정 없이 호출한 경우), error(msg, 0)으로 접두를 +-- 끌 수 있는지 +do + local defaultLevelMsg = nil + do + local Widget = function() + error("no-level-specified") + end + local SafeWidget = Fallback(Widget, function(msg) return msg end) + defaultLevelMsg = SafeWidget() + end + check("error(msg) 기본 호출 — onError가 받는 message에 위치 접두(\"파일:줄: \")가 자동으로 붙음", + defaultLevelMsg ~= defaultLevelMsg:match("^no%-level%-specified$") + and string.find(defaultLevelMsg, "no%-level%-specified$") ~= nil + and defaultLevelMsg ~= "no-level-specified") + print(" defaultLevelMsg = " .. tostring(defaultLevelMsg)) + + local zeroLevelMsg = nil + do + local Widget = function() + error("no-level-specified", 0) + end + local SafeWidget = Fallback(Widget, function(msg) return msg end) + zeroLevelMsg = SafeWidget() + end + check("error(msg, 0) — 위치 접두 없이 순수 메시지만 onError에 전달됨", + zeroLevelMsg == "no-level-specified") + print(" zeroLevelMsg = " .. tostring(zeroLevelMsg)) +end + +-- 6) original에 여러 인자를 넘기는 vararg 경로(문서 시그니처 (T...) -> Comp) +do + local Widget = function(a, b, c) + if c == nil then + error("missing c") + end + return a + b + c + end + local SafeWidget = Fallback(Widget, function(msg, trace) + return -1 + end) + check("vararg 성공 경로", SafeWidget(1, 2, 3) == 6) + check("vararg 실패 경로", SafeWidget(1, 2) == -1) +end + +print("") +if failures == 0 then + print("=== ALL PASS ===") +else + print("=== " .. failures .. " FAILURE(S) ===") +end diff --git a/.claude/session/2026-08-14-02-fallback-xpcall-spike-verified.md b/.claude/session/2026-08-14-02-fallback-xpcall-spike-verified.md new file mode 100644 index 0000000..e309392 --- /dev/null +++ b/.claude/session/2026-08-14-02-fallback-xpcall-spike-verified.md @@ -0,0 +1,56 @@ +# 2026-08-14 두 번째 세션 — `Fallback` 메커니즘 `xpcall` 실측 확인 + +## 배경 + +직전 세션(`2026-08-14-01-component-fallback-plan.md`)에서 +`research/component-fallback-plan.md`를 신설하며 "`xpcall` 에러 핸들러 +배선의 실측 필요"를 열린 질문으로 남겨뒀음. 사용자가 새 워크트리로 이동해 +직접 `luau`로 확인해보라고 요청, 메인 체크아웃은 다른 에이전트가 쓰는 +동안 워크트리를 벗어나지 말라고 지시. + +## 한 일 + +`.claude/research/component-fallback-xpcall-spike.luau` 스파이크 작성 — +문서의 `Fallback` 의사코드를 그대로 옮겨 `luau`로 10개 검증: + +1. 성공 경로 — `onError` 안 불리고 원래 반환값 그대로 통과 +2. 실패 경로 — `onError`의 반환값이 최종 결과, 에러 메시지 전달됨 +3. 에러 핸들러 안에서 클로저 업밸류(`trace`)에 쓴 값이 `xpcall` 리턴 후 + `onError` 호출 시점에 정상적으로 보임(가장 핵심적인 확인 대상) +4. 3단 중첩 호출(`level1→level2→level3`)에서도 `debug.traceback(nil, 2)`가 + 실패 지점(`level3`)까지 정확히 담음, level=2가 익명 에러 핸들러 + 프레임을 올바르게 스킵 +5. 비-문자열 에러 값(`error({...})`)도 손실 없이 `onError`에 전달됨 +6. 문서의 "추가 상태 필요하면 커링" 관용구가 그대로 동작 +7. vararg 컴포넌트 시그니처(`(T...) -> Comp`)도 정상 동작 + +**전부 통과**, `luau-analyze`도 클린(타입 에러 0). + +**부수 발견 — 문서에 없던 캐비엇**: `error(msg)`를 레벨 지정 없이(Luau +기본 level=1) 호출하면 `onError`가 받는 메시지에 Luau가 자동으로 +`"파일:줄: "` 위치 접두를 붙임 — `Fallback`이 붙이는 게 아니라 `error()` +자체의 기본 동작. `error(msg, 0)`으로 호출하면 접두 없이 순수 메시지만 +전달됨. 최초 스파이크 작성 시 이걸 몰라 커링 테스트가 실패했었고 +(exact-match assert가 접두 포함 문자열과 안 맞아서), 원인 확인 후 전용 +테스트(6번)를 추가하고 기존 assert를 contains 방식으로 고쳐서 재통과시킴 +— 테스트 버그가 아니라 실제 Luau 동작이었음을 별도 `pcall` 디버그로 +먼저 확인한 뒤 정식 반영. + +## 반영 + +`research/component-fallback-plan.md`: +- 메커니즘 스케치 절에 실측 완료 표시 + 위치 접두 캐비엇 추가 +- "열린 질문 — `xpcall` 에러 핸들러 배선의 실측" 항목을 **해소**로 표시 +- "프로덕션에서의 동작" 항목에 위치 접두가 raw 정보에 포함된다는 점 추가 +- 상단 상태 요약에 스파이크 파일 포인터 추가 + +`README.md` research 표 갱신. 새로 연 설계 질문 없음 — 백로그 +우선순위(맨 뒤, 착수 안 함)는 그대로. + +## 워크트리 메모 + +이번엔 처음부터 필요한 파일(`README.md`/`CLAUDE.md`/`component-fallback-plan.md`/ +`doc-check.py`)만 메인 체크아웃(로컬 `main`)에서 복사해 워크트리 안에서 +편집 — 직전 세션에서 정정한 원인(`EnterWorktree` 기본값이 +`origin/master`에서 갈라치는데 계획 문서는 `SAFETY.md` 때문에 로컬 +`main`에만 있음)을 그대로 재확인, 새로 놀랄 것 없었음. diff --git a/CLAUDE.md b/CLAUDE.md index bd9c168..d5c32fa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1136,3 +1136,13 @@ vs xpcall, 패키지 배치, 이름, 프로덕션 동작) 정리, 설계 확정 원인 — 필요한 파일만 메인 체크아웃(로컬 `main`)에서 복사해 편집 후 다시 복사하는 방식으로 처리, 상세 정정은 `session/2026-08-14-01-component-fallback-plan.md` 참고. + +**2026-08-14 두 번째 세션 — `Fallback` 메커니즘 `xpcall` 실측 확인** +(`session/2026-08-14-02-fallback-xpcall-spike-verified.md`) +직전 세션이 열어둔 "`xpcall` 에러 핸들러 배선의 실측 필요"를 새 워크트리에서 +`luau` 스파이크(`research/component-fallback-xpcall-spike.luau`)로 확인 — +클로저 업밸류 배선, 3단 중첩 `debug.traceback` 캡처 등 10개 검증 전부 +통과. 부수 발견으로 `error(msg)` 기본 호출(level=1)이 위치 접두 +("파일:줄: ")를 자동으로 붙인다는 캐비엇을 새로 확인해 문서에 반영 — +`research/component-fallback-plan.md`의 해당 열린 질문을 해소로 표시, +백로그 우선순위 자체는 그대로.