docs(base): Fallback/Traceback 승격 — research/에서 base/fallback-plan.md로

pcall 기반 Fallback과 xpcall+debug.traceback 기반 Traceback으로 분리,
정확한 제네릭 시그니처(OkComp/ErrComp/Args... 독립 제네릭)와 err: any를
확정. 패키지(quad-base)·이름(Fallback/Traceback) 확정으로 남은 열린
질문이 없어져 base/로 승격. 스파이크를 audit/fallback-xpcall-spike.luau로
옮기고 base/fallback-plan.md와 이름을 맞춰 내부 함수를 Traceback으로
정정, audit/fallback-xpcall-verification.md에 실측 결과 기록. README/
question.md/archive/question-resolved.md/lifecycle-hooks-plan.md의 상호
참조 동기화.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
qwreey 2026-08-14 04:32:29 +09:00
parent f6117723eb
commit 2d9cc7b5f7
Signed by: qwreey
GPG key ID: D28DB79297A214BD
10 changed files with 274 additions and 186 deletions

View file

@ -16,7 +16,7 @@
| `archive/` | 완료 + 사용자가 실사용/실기기로 직접 검증까지 마침 (구현 대상). **[2026-08-06 확장]** 완전히 뒤집힌 설계 결정을 원문+역전 이유+diff와 함께 보존하는 용도로도 사용(제목 `[역전됨]` — 한 번 확정했다가 뒤집힌 것) — 더 이상 능동적으로 참고 안 해도 되지만(토큰 낭비 방지 위해 `base/`/`research/`에서 뺌) `quadnomicon` 소재로는 나중에 쓸 수 있음. **[2026-08-07 확장]** 후보였다가 채택 안 된 것(확정한 적 없이 검토 후 기각)도 같은 방식으로 보존, 제목은 구분을 위해 `[기각됨]``[역전됨]`과 의미가 다르므로 혼동하지 말 것. **[2026-08-07 세 번째 확장]** 설계 반전/기각과 별개로, 에이전트가 문서 작성 중 스스로 낸 개념 혼동을 정정한 이력은 `[에이전트 실수]` 태그로 `agent-mistake.md` 하나에 모음(CLAUDE.md 세션 로그 중복 방지) | | `archive/` | 완료 + 사용자가 실사용/실기기로 직접 검증까지 마침 (구현 대상). **[2026-08-06 확장]** 완전히 뒤집힌 설계 결정을 원문+역전 이유+diff와 함께 보존하는 용도로도 사용(제목 `[역전됨]` — 한 번 확정했다가 뒤집힌 것) — 더 이상 능동적으로 참고 안 해도 되지만(토큰 낭비 방지 위해 `base/`/`research/`에서 뺌) `quadnomicon` 소재로는 나중에 쓸 수 있음. **[2026-08-07 확장]** 후보였다가 채택 안 된 것(확정한 적 없이 검토 후 기각)도 같은 방식으로 보존, 제목은 구분을 위해 `[기각됨]``[역전됨]`과 의미가 다르므로 혼동하지 말 것. **[2026-08-07 세 번째 확장]** 설계 반전/기각과 별개로, 에이전트가 문서 작성 중 스스로 낸 개념 혼동을 정정한 이력은 `[에이전트 실수]` 태그로 `agent-mistake.md` 하나에 모음(CLAUDE.md 세션 로그 중복 방지) |
| `feedback/` | 실사용 피드백을 정리한 긴 로그 — 지금은 비어있음(구현 시작 전) | | `feedback/` | 실사용 피드백을 정리한 긴 로그 — 지금은 비어있음(구현 시작 전) |
| `luau-test/` | **[2026-08-09 신설]** `base/` 확정 사항 중 "추론만으로 확정하고 실제 Luau로 부딪혀본 적 없는 것"(M0 스파이크 대상)을 `luau`/`luau-analyze`/`luau-lsp`/Roblox Studio로 사용자가 직접 돌려볼 독립 실행 스크립트 모음. **[2026-08-13 여섯 번째 세션, 첫 실측]** `luau`/`luau-analyze` 바이너리가 생겨 처음으로 실제 실행 — **런타임 12개 전원 통과**, 타입 쪽에서 `:Compute(fn)` lazy 핸들 계약이 Luau 추론과 충돌하는 게 드러남(당시 `question.md` 0-Y). **[2026-08-13 열세 번째 세션]** 그 0-Y가 해소되며 `review-required/`**비었음** — 계약은 유지 확정, 남은 건 Luau 자체 한계라 `base/typing-limits.md`가 담당. **`STATUS.md`가 상태의 소스**(pass / 사람 결정 필요 / 스파이크 깨짐 / 미실행 분류 — 사람이 먼저 볼 것만 위에), `luau-test/README.md`는 각 파일의 검증 의도·배경, 실행 결과 상세는 `audit/luau-test-first-run-2026-08-13.md` | | `luau-test/` | **[2026-08-09 신설]** `base/` 확정 사항 중 "추론만으로 확정하고 실제 Luau로 부딪혀본 적 없는 것"(M0 스파이크 대상)을 `luau`/`luau-analyze`/`luau-lsp`/Roblox Studio로 사용자가 직접 돌려볼 독립 실행 스크립트 모음. **[2026-08-13 여섯 번째 세션, 첫 실측]** `luau`/`luau-analyze` 바이너리가 생겨 처음으로 실제 실행 — **런타임 12개 전원 통과**, 타입 쪽에서 `:Compute(fn)` lazy 핸들 계약이 Luau 추론과 충돌하는 게 드러남(당시 `question.md` 0-Y). **[2026-08-13 열세 번째 세션]** 그 0-Y가 해소되며 `review-required/`**비었음** — 계약은 유지 확정, 남은 건 Luau 자체 한계라 `base/typing-limits.md`가 담당. **`STATUS.md`가 상태의 소스**(pass / 사람 결정 필요 / 스파이크 깨짐 / 미실행 분류 — 사람이 먼저 볼 것만 위에), `luau-test/README.md`는 각 파일의 검증 의도·배경, 실행 결과 상세는 `audit/luau-test-first-run-2026-08-13.md` |
| `audit/` | **[2026-08-13 신설]** `luau-test/` 등 스파이크를 실제로 돌려본 뒤 "무엇이 확인됐고 무엇이 아직 안 됐는지"를 기록하는 곳 — 스크립트/계획 자체가 아니라 **실측 결과**만 다룸. base/luau-test와 달리 부분 확인(일부만 통과)도 있는 그대로 기록, 완전히 해소되면 관련 `base/`/`luau-test/README.md` 캐비엇을 지우고 이 문서는 근거로 남김. **현재 3개**: `luau-test-first-run-2026-08-13.md`(첫 실측 라운드 전체 — 런타임 12개 통과, 구 `question.md` 0-Y의 1차 근거. **단 이 문서의 "콜백이 raw 값을 받으면 완전 클린" 판정은 아래 `type-recursion-issue/`가 뒤집었음**), `gcconn-trick-verification.md`(사용자가 Studio에서 직접 돌린 gcconn 트릭 부분 확인 — `10`의 A 섹션 앞부분만. **[2026-08-14 다섯 번째 세션]** 실측된 사실 자체는 그대로 유효하고 `canExecute(value)` 1-인자 재정정으로 오히려 더 중요해졌으나, 인용하던 `canBound`가 폐기돼 미확인 항목 목록을 새 모델 기준으로 갱신함 — 이중 바인딩 게이트/재바인딩 허용/`value` 쪽 복사 gcconn 판정/Instance userdata 동일성/B/C가 미확인), **`type-recursion-issue/`**(**[2026-08-13 열세 번째 세션 신설]** 0-Y 재실측 전체 — `REPORT.md` + `spikes/` 44개. 다른 audit 기록과 달리 **스크립트를 같이 둠**: 이 건의 근거가 "여러 formulation을 서로 대조한 것"이라 개별 파일을 직접 돌려야 판정이 재현되기 때문. 결론은 `base/typing-limits.md`로 승격됨) | | `audit/` | **[2026-08-13 신설]** `luau-test/` 등 스파이크를 실제로 돌려본 뒤 "무엇이 확인됐고 무엇이 아직 안 됐는지"를 기록하는 곳 — 스크립트/계획 자체가 아니라 **실측 결과**만 다룸. base/luau-test와 달리 부분 확인(일부만 통과)도 있는 그대로 기록, 완전히 해소되면 관련 `base/`/`luau-test/README.md` 캐비엇을 지우고 이 문서는 근거로 남김. **현재 4개**: `luau-test-first-run-2026-08-13.md`(첫 실측 라운드 전체 — 런타임 12개 통과, 구 `question.md` 0-Y의 1차 근거. **단 이 문서의 "콜백이 raw 값을 받으면 완전 클린" 판정은 아래 `type-recursion-issue/`가 뒤집었음**), `gcconn-trick-verification.md`(사용자가 Studio에서 직접 돌린 gcconn 트릭 부분 확인 — `10`의 A 섹션 앞부분만. **[2026-08-14 다섯 번째 세션]** 실측된 사실 자체는 그대로 유효하고 `canExecute(value)` 1-인자 재정정으로 오히려 더 중요해졌으나, 인용하던 `canBound`가 폐기돼 미확인 항목 목록을 새 모델 기준으로 갱신함 — 이중 바인딩 게이트/재바인딩 허용/`value` 쪽 복사 gcconn 판정/Instance userdata 동일성/B/C가 미확인), **`type-recursion-issue/`**(**[2026-08-13 열세 번째 세션 신설]** 0-Y 재실측 전체 — `REPORT.md` + `spikes/` 44개. 다른 audit 기록과 달리 **스크립트를 같이 둠**: 이 건의 근거가 "여러 formulation을 서로 대조한 것"이라 개별 파일을 직접 돌려야 판정이 재현되기 때문. 결론은 `base/typing-limits.md`로 승격됨), `fallback-xpcall-verification.md`(**[2026-08-14 신설]** `base/fallback-plan.md``Traceback` 메커니즘 전부 확인 — 클로저 업밸류 배선/중첩 스택 캡처/`err: any`/`error(msg)` 위치 접두 10개 검증 전부 통과. 스크립트 1개뿐이라 재현용으로 같이 둠: `fallback-xpcall-spike.luau`) |
| `tools/` | **[2026-08-13 아홉 번째 세션 신설]** 코퍼스 기계 점검 — `doc-check.py`가 깨진 파일/절 참조, README 색인 누락, 날짜 없는 시한부 주장("아직 안 돌려봄" 등), 미반영 ⚠️ 배너를 한 번에 훑음. **중대 변경 후 커밋 전에 돌릴 것**(`python3 .claude/tools/doc-check.py`) — 수동 감사에서 나온 발견의 대부분이 이 종류였고, 실제로 문서를 쪼개다 잘못 옮긴 참조를 이게 잡아냄. ERROR는 고치고 WARN은 판단 대상 | | `tools/` | **[2026-08-13 아홉 번째 세션 신설]** 코퍼스 기계 점검 — `doc-check.py`가 깨진 파일/절 참조, README 색인 누락, 날짜 없는 시한부 주장("아직 안 돌려봄" 등), 미반영 ⚠️ 배너를 한 번에 훑음. **중대 변경 후 커밋 전에 돌릴 것**(`python3 .claude/tools/doc-check.py`) — 수동 감사에서 나온 발견의 대부분이 이 종류였고, 실제로 문서를 쪼개다 잘못 옮긴 참조를 이게 잡아냄. ERROR는 고치고 WARN은 판단 대상 |
| `session/` | **[2026-08-11 신설]** 세션별 상세 로그 원문(시행착오·정정 전 서술 포함, `quadnomicon` 개발로그 소재용) — 루트 `CLAUDE.md`가 3196줄까지 불어나 성능 저하를 유발해서 분리함. 파일명 `YYYY-MM-DD-NN-slug.md`, CLAUDE.md의 "세션 히스토리" 절에서 각 항목이 여기로 링크. 항상 읽을 필요 없음 — 결정의 논의 과정이 궁금할 때만 | | `session/` | **[2026-08-11 신설]** 세션별 상세 로그 원문(시행착오·정정 전 서술 포함, `quadnomicon` 개발로그 소재용) — 루트 `CLAUDE.md`가 3196줄까지 불어나 성능 저하를 유발해서 분리함. 파일명 `YYYY-MM-DD-NN-slug.md`, CLAUDE.md의 "세션 히스토리" 절에서 각 항목이 여기로 링크. 항상 읽을 필요 없음 — 결정의 논의 과정이 궁금할 때만 |
| `initreq/` | 프로젝트 착수 시 클론해둔 참고 레포(quad v1, fusion, vide, rbvm, tbox, code-docker) + PA님 실 코드(`artworks/`, 4차 라운드 교차검증 근거) + 원본 요청(`req.md`, `raw-userinput.md`) + `quad2-try`(이전에 시도했다 폐기한 v2 재작성 시도 — 리서치 완료, 결론은 `base/bind-system-plan.md`) — 읽기 전용 리서치 소스, 여기 내용을 옮기지 말고 항상 원본 그대로 유지 | | `initreq/` | 프로젝트 착수 시 클론해둔 참고 레포(quad v1, fusion, vide, rbvm, tbox, code-docker) + PA님 실 코드(`artworks/`, 4차 라운드 교차검증 근거) + 원본 요청(`req.md`, `raw-userinput.md`) + `quad2-try`(이전에 시도했다 폐기한 v2 재작성 시도 — 리서치 완료, 결론은 `base/bind-system-plan.md`) — 읽기 전용 리서치 소스, 여기 내용을 옮기지 말고 항상 원본 그대로 유지 |
@ -51,6 +51,7 @@
| `event-plan.md` | **[2026-08-13 아홉 번째 세션, `bind-system-plan.md`에서 분리 — 사용자가 직접 지목]** 이벤트 바인딩 — 핸들러가 self(Instance)를 **안** 받는다는 확정(Ref가 이미 커버, 이중 쓰기 경로 방지), 이벤트도 store-bind 가능하며 `false`를 넣으면 disconnect. 이벤트 *네이밍* 관례는 인스턴스 생성과 한 절에 섞여 있어 `bind-system-plan.md`에 남음, `GetPropertyChangedSignal``onchange-plan.md`. **분리는 순수 이동** | | `event-plan.md` | **[2026-08-13 아홉 번째 세션, `bind-system-plan.md`에서 분리 — 사용자가 직접 지목]** 이벤트 바인딩 — 핸들러가 self(Instance)를 **안** 받는다는 확정(Ref가 이미 커버, 이중 쓰기 경로 방지), 이벤트도 store-bind 가능하며 `false`를 넣으면 disconnect. 이벤트 *네이밍* 관례는 인스턴스 생성과 한 절에 섞여 있어 `bind-system-plan.md`에 남음, `GetPropertyChangedSignal``onchange-plan.md`. **분리는 순수 이동** |
| `brand-plan.md` | **[2026-08-13 아홉 번째 세션, `bind-system-plan.md`에서 분리]** `Brand` — 런타임 nominal 타입 판별 통합 메커니즘(`Brand.set`/`Brand.get`), `isState`를 10종 branded 타입 전부로 일반화. 동작/구현은 확정, **이름 `Brand` 자체만 용어 정리 대기**(`question.md` 1번). **분리는 순수 이동** | | `brand-plan.md` | **[2026-08-13 아홉 번째 세션, `bind-system-plan.md`에서 분리]** `Brand` — 런타임 nominal 타입 판별 통합 메커니즘(`Brand.set`/`Brand.get`), `isState`를 10종 branded 타입 전부로 일반화. 동작/구현은 확정, **이름 `Brand` 자체만 용어 정리 대기**(`question.md` 1번). **분리는 순수 이동** |
| `tween-plan.md` | **[2026-08-12 세션, `research/`에서 승격]** 값-레벨 `Tween<T>` 래퍼(PropertyHandler가 소비, 구 특수 bind key 모델은 `archive/tween-special-bind-key-reversed.md`). 3-상태 릴레이션 슬롯(`{Tween,Value}\|true\|nil`), `T'=T\|Tween<T>` 타입 치환. 옵션 값 모양은 `Info: TweenInfo?` 우선+편의 필드 폴백, override는 `Tween.Cancel`(기본)/`Tween.Finish` 2값. `Animate(info)``Tween` opts를 `T\|State<T>`로 받아 `:Apply`로 꽂는 sugar. 자연완료 시 per-instance 북키핑은 정리 안 해도 됨으로 확정(목표값 도달 상태라 부작용 없음, Completed 이벤트 구독 장치는 오버엔지니어링으로 판단). `initValue`는 사용자가 직접 처리(에이전트 범위 제외) | | `tween-plan.md` | **[2026-08-12 세션, `research/`에서 승격]** 값-레벨 `Tween<T>` 래퍼(PropertyHandler가 소비, 구 특수 bind key 모델은 `archive/tween-special-bind-key-reversed.md`). 3-상태 릴레이션 슬롯(`{Tween,Value}\|true\|nil`), `T'=T\|Tween<T>` 타입 치환. 옵션 값 모양은 `Info: TweenInfo?` 우선+편의 필드 폴백, override는 `Tween.Cancel`(기본)/`Tween.Finish` 2값. `Animate(info)``Tween` opts를 `T\|State<T>`로 받아 `:Apply`로 꽂는 sugar. 자연완료 시 per-instance 북키핑은 정리 안 해도 됨으로 확정(목표값 도달 상태라 부작용 없음, Completed 이벤트 구독 장치는 오버엔지니어링으로 판단). `initValue`는 사용자가 직접 처리(에이전트 범위 제외) |
| `fallback-plan.md` | **[2026-08-14 세션, `research/`에서 승격]** `Fallback`/`Traceback` — 컴포넌트 함수를 감싸 에러 시 플레이스홀더를 그려주는 순수 슈가(`additional-primitives-plan.md`의 "Error Boundary는 빈 자리 아님" 결론 위에 얹힘). `Fallback``pcall` 기반(trace 없음), `Traceback``xpcall`+`debug.traceback` 기반(trace 항상 있음) — 플래그 대신 별도 함수로 분리(`Ref`/`PreRef`와 같은 패턴). `err: any`(Lua `error()`가 임의 값을 던질 수 있음, `error(msg)` 기본 호출의 위치 접두 캐비엇 포함) 확정. 패키지는 `quad-base`, 이름 확정. 메커니즘 실측은 `audit/fallback-xpcall-verification.md`. 구현 우선순위는 형제 백로그(`quad-mock`/`quad-debug`/`Operator`)와 동급, 맨 뒤 |
## `reference/` — 온디맨드 참고 자료 (2026-08-07 신설) ## `reference/` — 온디맨드 참고 자료 (2026-08-07 신설)
@ -72,7 +73,6 @@
| `additional-primitives-plan.md` | **[2026-08-09 세 번째 세션, 전부 해소]** 마지막으로 남아있던 키 기반 동적 컬렉션 재조정도 `Slot:List(...)`로 확정되어 `base/slot-plan.md`로 승격 — 이 문서엔 새로 열린 설계 질문 없음, "빈 자리 아닌 것"/"문서화 백로그"/조사 소스 목록만 배경 자료로 유지 | 하 — 배경 리서치 기록용, 열린 결정 없음 | | `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/` 스파이크 실측만 남음 | | `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<State<T>|T>``State<T>` 평탄화 항목 신설(백로그) — `State<State<T>>`가 정상 동작하게 됐지만 `retractFrom`의 힌트가 직속 1단계에만 가서 깊은 중첩에선 깜빡임 방지가 꺼진다는 게 구체적 동기, 사용자 판단으로 "UB는 아니지만 원치 않는 방향". `Operator.*`가 아니라 `state:Flatten()` 메소드로 제공하는 게 맞아 보이며, **반환 노드가 동적 의존성을 갖는다는 난점**(quad가 의도적으로 비지원하기로 한 바로 그것)이 확정 전 최대 쟁점 | | `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<State<T>|T>``State<T>` 평탄화 항목 신설(백로그) — `State<State<T>>`가 정상 동작하게 됐지만 `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` 위에 얹힌 것과 같은 관계, 그 문서 재오픈 아님). **[같은 날 두 번째 세션]** `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 개발 상당 부분 끝난 뒤" | | `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 코어 구현 시점까지 미결 | | `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 코어 구현 시점까지 미결 |

View file

@ -582,6 +582,7 @@ context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만**
| v1 내부 동작 스냅샷 | `reference/quad-v1-architecture.md` | | v1 내부 동작 스냅샷 | `reference/quad-v1-architecture.md` |
| 트윈 — 값-레벨 `Tween<T>` 래퍼(2026-08-10)+옵션 값 모양·override 정책·`Animate` 콤비네이터·자연완료 북키핑(2026-08-12) 전부 확정, `base/`로 승격 | `base/tween-plan.md` | | 트윈 — 값-레벨 `Tween<T>` 래퍼(2026-08-10)+옵션 값 모양·override 정책·`Animate` 콤비네이터·자연완료 북키핑(2026-08-12) 전부 확정, `base/`로 승격 | `base/tween-plan.md` |
| quad2-try(폐기된 이전 시도) 리서치 — OOP 상속/커스텀 파서/Slot 스텁/`Pipe` COW 전부 죽은 접근으로 확인, 반복 조사 금지 | `base/bind-system-plan.md` | | quad2-try(폐기된 이전 시도) 리서치 — OOP 상속/커스텀 파서/Slot 스텁/`Pipe` COW 전부 죽은 접근으로 확인, 반복 조사 금지 | `base/bind-system-plan.md` |
| `Fallback`/`Traceback`(2026-08-14) — 컴포넌트 에러 격리 유틸, `pcall`(가벼움)과 `xpcall`+`debug.traceback`(trace 항상 있음)으로 분리, `err: any` 확정(테이블 에러 등), 패키지·이름 전부 확정, `research/`에서 승격 | `base/fallback-plan.md` |
--- ---
전체 순서/우선순위는 루트 `CLAUDE.md`가 최종 소스 — 위 표는 힌트일 뿐 그쪽이 전체 순서/우선순위는 루트 `CLAUDE.md`가 최종 소스 — 위 표는 힌트일 뿐 그쪽이

View file

@ -1,5 +1,5 @@
-- 스파이크: research/component-fallback-plan.md의 Fallback 메커니즘 의사코드가 -- 스파이크: base/fallback-plan.md의 Traceback 메커니즘 의사코드가
-- 실제 Luau에서 그대로 동작하는지 실측. 열린 질문: -- 실제 Luau에서 그대로 동작하는지 실측. 결과는 audit/fallback-xpcall-verification.md.
-- "xpcall 에러 핸들러 배선의 실측 — 에러 핸들러 안에서 클로저 업밸류에 쓴 -- "xpcall 에러 핸들러 배선의 실측 — 에러 핸들러 안에서 클로저 업밸류에 쓴
-- 값이 바깥에서 제대로 보이는지, debug.traceback이 에러 시점 스택을 정확히 -- 값이 바깥에서 제대로 보이는지, debug.traceback이 에러 시점 스택을 정확히
-- 찍는지" -- 찍는지"
@ -15,8 +15,8 @@ local function check(name, cond)
end end
end end
-- 문서의 의사코드 그대로 옮김 -- 문서의 Traceback 의사코드 그대로 옮김
local function Fallback(original, onError) local function Traceback(original, onError)
return function(...) return function(...)
local trace: string? = nil local trace: string? = nil
local ok, resultOrErr = xpcall(original, function(err) local ok, resultOrErr = xpcall(original, function(err)
@ -36,7 +36,7 @@ do
return "OK:" .. props.name return "OK:" .. props.name
end end
local calledOnError = false local calledOnError = false
local SafeWidget = Fallback(Widget, function(msg, trace) local SafeWidget = Traceback(Widget, function(msg, trace)
calledOnError = true calledOnError = true
return "ERR" return "ERR"
end) end)
@ -51,7 +51,7 @@ do
error("boom: " .. props.name) error("boom: " .. props.name)
end end
local capturedMsg, capturedTrace = nil, nil local capturedMsg, capturedTrace = nil, nil
local SafeWidget = Fallback(Widget, function(msg, trace) local SafeWidget = Traceback(Widget, function(msg, trace)
capturedMsg = msg capturedMsg = msg
capturedTrace = trace capturedTrace = trace
return "PLACEHOLDER" return "PLACEHOLDER"
@ -77,7 +77,7 @@ do
return level2(props) return level2(props)
end end
local capturedTrace = nil local capturedTrace = nil
local SafeWidget = Fallback(level1, function(msg, trace) local SafeWidget = Traceback(level1, function(msg, trace)
capturedTrace = trace capturedTrace = trace
return "PLACEHOLDER" return "PLACEHOLDER"
end) end)
@ -95,7 +95,7 @@ do
error({ code = 42, reason = "custom" }) error({ code = 42, reason = "custom" })
end end
local capturedMsg = nil local capturedMsg = nil
local SafeWidget = Fallback(Widget, function(msg, trace) local SafeWidget = Traceback(Widget, function(msg, trace)
capturedMsg = msg capturedMsg = msg
return "PLACEHOLDER" return "PLACEHOLDER"
end) end)
@ -113,7 +113,7 @@ do
return "PLACEHOLDER(" .. context .. "):" .. message return "PLACEHOLDER(" .. context .. "):" .. message
end end
end end
local SafeWidget = Fallback(Widget, makeErrorHandler("someContext")) local SafeWidget = Traceback(Widget, makeErrorHandler("someContext"))
local result = SafeWidget({}) local result = SafeWidget({})
-- error("ctx-fail")의 기본 level(=1)이 "파일:줄: " 접두를 붙이므로 -- error("ctx-fail")의 기본 level(=1)이 "파일:줄: " 접두를 붙이므로
-- message가 정확히 "ctx-fail"이 아님 — 아래 6번에서 별도로 실측/확정. -- message가 정확히 "ctx-fail"이 아님 — 아래 6번에서 별도로 실측/확정.
@ -130,7 +130,7 @@ do
local Widget = function() local Widget = function()
error("no-level-specified") error("no-level-specified")
end end
local SafeWidget = Fallback(Widget, function(msg) return msg end) local SafeWidget = Traceback(Widget, function(msg) return msg end)
defaultLevelMsg = SafeWidget() defaultLevelMsg = SafeWidget()
end end
check("error(msg) 기본 호출 — onError가 받는 message에 위치 접두(\"파일:줄: \")가 자동으로 붙음", check("error(msg) 기본 호출 — onError가 받는 message에 위치 접두(\"파일:줄: \")가 자동으로 붙음",
@ -144,7 +144,7 @@ do
local Widget = function() local Widget = function()
error("no-level-specified", 0) error("no-level-specified", 0)
end end
local SafeWidget = Fallback(Widget, function(msg) return msg end) local SafeWidget = Traceback(Widget, function(msg) return msg end)
zeroLevelMsg = SafeWidget() zeroLevelMsg = SafeWidget()
end end
check("error(msg, 0) — 위치 접두 없이 순수 메시지만 onError에 전달됨", check("error(msg, 0) — 위치 접두 없이 순수 메시지만 onError에 전달됨",
@ -152,7 +152,7 @@ do
print(" zeroLevelMsg = " .. tostring(zeroLevelMsg)) print(" zeroLevelMsg = " .. tostring(zeroLevelMsg))
end end
-- 6) original에 여러 인자를 넘기는 vararg 경로(문서 시그니처 (T...) -> Comp) -- 7) original에 여러 인자를 넘기는 vararg 경로(문서 시그니처 (Args...) -> Comp)
do do
local Widget = function(a, b, c) local Widget = function(a, b, c)
if c == nil then if c == nil then
@ -160,7 +160,7 @@ do
end end
return a + b + c return a + b + c
end end
local SafeWidget = Fallback(Widget, function(msg, trace) local SafeWidget = Traceback(Widget, function(msg, trace)
return -1 return -1
end) end)
check("vararg 성공 경로", SafeWidget(1, 2, 3) == 6) check("vararg 성공 경로", SafeWidget(1, 2, 3) == 6)

View file

@ -0,0 +1,54 @@
# `Fallback`/`Traceback` — `xpcall`+`debug.traceback` 배선 실측 결과
**상태**: 전부 확인(2026-08-14). `base/fallback-plan.md``Traceback`
메커니즘 스케치가 실제 Luau에서 그대로 동작하는지 `luau` 스파이크
(`fallback-xpcall-spike.luau`, 같은 폴더)로 검증 — 10개 검증 전부 통과,
`luau-analyze`도 클린(타입 에러 0).
## 배경
`base/fallback-plan.md`(당시엔 research/ 초안) 단계에서 열어뒀던 질문:
`xpcall`의 에러 핸들러 안에서 클로저
업밸류에 쓴 값(`trace`)이 `xpcall` 리턴 이후에도 바깥에서 정상적으로
보이는지, `debug.traceback`이 에러 시점 스택을 정확히 담는지 — 의사코드
수준이라 Luau로 직접 부딪혀본 적 없었음.
## 확인된 것
1. **성공 경로**`onError`가 아예 안 불리고 `base`의 원래 반환값이
그대로 통과함.
2. **실패 경로**`onError`의 반환값이 최종 결과, 에러 메시지가
`onError`에 정상 전달됨.
3. **클로저 업밸류 배선(가장 핵심)**`xpcall`의 에러 핸들러 안에서
업밸류 `trace`에 쓴 값이 `xpcall` 리턴 후 `onError` 호출 시점에
정상적으로 채워져 있음.
4. **중첩 호출에서의 `debug.traceback`** — 3단 중첩(`level1→level2→level3`)
호출에서도 `debug.traceback(nil, 2)`가 실패 지점(`level3`)까지 정확히
담음. `level=2`가 익명 에러 핸들러 프레임 자체를 올바르게 스킵.
5. **`err: any`** — 비-문자열 에러 값(`error({code=42})`류 table)도
손실 없이 `onError`에 그대로 전달됨. 사용자가 별도로 Luau REPL에서
`error({aa=true})``pcall`로 잡은 뒤 `b.aa == true`를 직접 재확인,
스파이크 결과와 일치.
6. **커링 관용구**`onError` 자체를 클로저로 만들어 추가 컨텍스트를
캡처하는 관용구가 `Fallback`/`Traceback` 쪽 손댈 것 없이 그대로 동작.
7. **`error(msg)`의 기본 위치 접두(신규 발견)** — 레벨 지정 없이
(Luau 기본 level=1) `error("메시지")`를 호출하면 `onError`가 받는
`err``"파일:줄: 메시지"`처럼 위치 접두가 자동으로 붙음.
`error(msg, 0)`으로 호출해야 접두 없는 순수 메시지가 옴 — quad가
붙이는 게 아니라 Luau `error()` 자체의 기본 동작.
8. **vararg 컴포넌트 시그니처**`(Args...) -> Comp` 모양(여러 인자를
받는 컴포넌트 함수)도 성공/실패 경로 둘 다 정상 동작.
## 실측 방법
`fallback-xpcall-spike.luau`(같은 폴더) — `base/fallback-plan.md`
`Traceback` 의사코드를 그대로 옮겨 10개 assert로 검증.
`luau fallback-xpcall-spike.luau`로 실행,
`luau-analyze fallback-xpcall-spike.luau`로 타입 체크(무출력 = 클린).
## 참고
`Fallback`(순수 `pcall`, trace 없음) 쪽은 `Traceback`보다 메커니즘이
단순(에러 핸들러/업밸류 배선이 아예 없음)해서 별도 스파이크 없이도
`pcall` 자체의 기본 동작(성공/실패 경로, `err: any` 통과)으로 충분히
갈음됨 — 위 5번 확인이 그대로 적용됨.

View file

@ -0,0 +1,146 @@
# `Fallback`/`Traceback` — 컴포넌트 에러 격리 유틸
**상태**: base — 확정(2026-08-14 세션). research/에서 신설(사용자 제안) →
`luau` 스파이크로 `xpcall`/`debug.traceback` 배선
실측(같은 날 두 번째 세션) → `Fallback`/`Traceback` 분리·정확한 제네릭
시그니처·`err: any` 확정(같은 날 세 번째 세션, 사용자 확정)까지 한 흐름 —
`base/`로 승격. **구현 우선순위는 여전히 맨 뒤**(아래 "우선순위" 절), 승격은
설계가 다 정해졌다는 뜻이지 지금 만든다는 뜻이 아님.
## 동기
컴포넌트마다 개별적으로 `pcall`을 직접 감싸는 건 실용적이지 않음 — 매
호출 자리마다
`local ok, result = pcall(MyComp, props); if not ok then ... end`
손으로 반복해 쓰는 건 번거롭고 빠뜨리기도 쉬움. 대신 컴포넌트
함수 하나를 받아서 "에러 나면 자동으로 플레이스홀더를 그려주는 버전"으로
바꿔주는 아주 단순한 유틸이면 충분함 — 클린업 동작(언마운트/리소스 해제)이
목적이 아니라, **실제 에러가 났을 때 디버깅이나 프로덕션 유저 리포트를
편하게 만드는 게 유일한 목적**.
## 왜 새 프리미티브가 아닌가
`research/additional-primitives-plan.md`가 이미 "Error Boundary는 빈
자리 아님 — `pcall(MyComp, props)`만으로 React Error Boundary와 같은
격리 효과를 프레임워크 지원 없이 얻는다"고 확정해둔 결론을 뒤집는 게
아니라, **그 결론 위에 얹는 순수 슈가**(그 문서를 다시 열 필요 없음) —
`Operator` 콤비네이터(`research/operator-sugar-plan.md`)가 `:Compute`/
`:Apply` 위에 얹힌 것과 같은 관계. `Fallback`/`Traceback` 둘 다 `original`
호출하고 결과를 그대로 돌려주는 순수 함수일 뿐, 디스패치/Store/Handler
계층에 아무것도 새로 안 만듦.
## API — 왜 둘로 나뉘는가
```
Fallback<OkComp, ErrComp, Args...>(
base: (Args...) -> OkComp,
onError: (err: any) -> ErrComp
) -> (Args...) -> (OkComp | ErrComp)
Traceback<OkComp, ErrComp, Args...>(
base: (Args...) -> OkComp,
onError: (err: any, trace: string) -> ErrComp
) -> (Args...) -> (OkComp | ErrComp)
```
- **`Fallback`** — `pcall` 기반, 가벼움, `onError``err`만 넘어감(trace
없음).
- **`Traceback`** — `xpcall`+`debug.traceback` 기반, `onError``err`
함께 `trace: string`**항상**(옵셔널 아님) 넘어감.
- **왜 플래그 하나로 안 합쳤는가**: quad는 이미 이런 갈림을 별도 타입/함수로
가르는 쪽을 택해왔음(`Ref`/`PreRef`가 같은 예) — 항상 `xpcall`+
`debug.traceback` 비용을 물지 않아도 되는 가벼운 경로를 자연스럽게 분리해
둘 수 있고, `onError`의 시그니처 자체가 달라서(trace 유무) 타입으로도
둘을 구분하는 게 더 정확함.
- `OkComp`/`ErrComp`를 하나로 합친 `Comp`가 아니라 **독립 제네릭**으로 둔
이유: 원래 컴포넌트와 에러 플레이스홀더가 다른 컴포넌트 타입일 수 있고,
래핑된 함수의 실제 반환 타입은 정확히 `OkComp | ErrComp` 유니온이기
때문(사용자 확정).
- `onError` 자신이 추가 컨텍스트를 캡처하려고 커링된 클로저인 건 완전히
자유 — `Fallback`/`Traceback`은 여기 관여하지 않음(아래 예시).
```lua
local SafeWidget = Fallback(Widget, function(err)
return ErrorPlaceholder { Message = err }
end)
-- 추가 컨텍스트가 필요하면 onError 쪽에서 그냥 커링
local function makeErrorHandler(context)
return function(err, trace)
return ErrorPlaceholder { Message = err, Context = context, Trace = trace }
end
end
local SafeWidget2 = Traceback(Widget, makeErrorHandler(someContext))
-- 호출부는 원래 컴포넌트 대신 그대로 씀
Frame { SafeWidget{ ... }, SafeWidget2{ ... } }
```
### `err: any`임을 반드시 문서화 — 흔한 함정
Lua/Luau의 `error()`는 문자열이 아닌 **임의의 값**(테이블 등)을 던질 수
있음 — `Fallback`/`Traceback` 둘 다 `err``any`로 그대로 전달하고
어떤 가공도 안 함. `error(msg)`를 레벨 지정 없이(Luau 기본 level=1)
호출하면 `err`가 문자열이더라도 quad가 아무것도 안 붙였는데 Luau가
자동으로 `"파일:줄: "` 위치 접두를 붙여서 옴 — `error(msg, 0)`으로
호출해야 접두 없는 순수 메시지가 옴. **다들 `err``string`으로 가정하고
코드를 짜는 게 제일 흔한 실수라 문서화에서 최우선으로 경고할 것**(가공은
`Fallback`/`Traceback`이 대신해주지 않음 — 가공까지 대신해주면 그게 또
다른 매직이라는 원칙, `onError` 구현 몫으로 완전히 열어둠).
## 메커니즘 스케치
```lua
function Fallback(base, onError)
return function(...)
local ok, resultOrErr = pcall(base, ...)
if ok then
return resultOrErr
end
return onError(resultOrErr)
end
end
function Traceback(base, onError)
return function(...)
local trace: string? = nil
local ok, resultOrErr = xpcall(base, function(err)
trace = debug.traceback(nil, 2)
return err
end, ...)
if ok then
return resultOrErr
end
return onError(resultOrErr, trace :: string)
end
end
```
`Traceback``debug.traceback(nil, 2)` 배선(클로저 업밸류가 `xpcall`
리턴 이후에도 정상적으로 보이는지, 중첩 호출에서도 실패 지점까지
스택을 정확히 담는지)과 `err: any`(테이블 에러도 손실 없이 통과하는지)는
`luau` 스파이크로 실측 확인됨 — `audit/fallback-xpcall-verification.md`
참고(스크립트: `audit/fallback-xpcall-spike.luau`).
`research/debug-tooling-plan.md`가 이미 확인해둔 선례(Vide/Fusion 둘 다
`xpcall`+`debug.traceback`으로 **에러 나는 순간에만** 스택을 찍는
패턴)를 그대로 재사용 — 새 트레이싱 메커니즘을 발명하지 않음.
## 패키지 배치
`quad-base``base`를 그냥 호출하고 결과를 그대로 돌려주는 순수 함수라
Store/Dispatch 어디에도 안 걸림, 엔진 지식이 전혀 필요 없음. `Operator`
콤비네이터와 같은 결(사용자 확정).
## 이름
`Fallback`/`Traceback` 확정 — 낱개 함수 둘뿐이라 `Tag`/`Attribute`류
네임스페이스가 필요했던 것과 달리 충돌 표면이 작다고 판단, 용어 정리
대기열에 안 올리고 바로 점유(사용자 확정).
## 우선순위
**형제 백로그 항목들(`quad-mock`/`quad-debug`/문서 사이트/`Operator`)과
동급, 맨 뒤 — "quad 개발 상당 부분 끝난 뒤"로 사용자가 못박은 후순위**
(`CLAUDE.md` "지금 할 일" 4번). 이 문서가 `base/`로 승격된 건 설계가 다
확정됐다는 뜻이지, 구현 착수 순서가 앞당겨졌다는 뜻이 아님 — `Operator`처럼
순수 슈가라 없어도 quad 기능상 완전함(`pcall`을 직접 쓰면 되므로).

View file

@ -213,12 +213,6 @@
사용자 확인 필요 — `base/attribute-plan.md` "열린 질문" 절. 사용자 확인 필요 — `base/attribute-plan.md` "열린 질문" 절.
- `research/existing-instance-bind-plan.md` — 스코프 논의만 필요, 구현 - `research/existing-instance-bind-plan.md` — 스코프 논의만 필요, 구현
착수를 막지 않음. 착수를 막지 않음.
- **컴포넌트 에러 격리 유틸 `Fallback`(2026-08-14 신설, 사용자 제안)**
컴포넌트 함수를 감싸 에러 시 자동으로 플레이스홀더를 그려주는
`pcall`/`xpcall` 래퍼. 기존 "Error Boundary는 빈 자리 아님"
(`additional-primitives-plan.md`) 결론 위의 순수 슈가라 그 결론 자체는
안 흔들림 — `pcall` vs `xpcall`+`debug.traceback`, 패키지 배치
(`quad-base` 추정), 이름 전부 미정. 상세는 `research/component-fallback-plan.md`.
- **`quad-debug` 세부 API 이름** — `research/debug-tooling-plan.md` 참고. - **`quad-debug` 세부 API 이름** — `research/debug-tooling-plan.md` 참고.
채널 실현 가능성(BindableEvent/Function이 플러그인↔Play 중 게임 경계를 채널 실현 가능성(BindableEvent/Function이 플러그인↔Play 중 게임 경계를
넘는지)까지 사용자가 Studio에서 직접 실측 검증 완료 — 기술적 불확실성은 넘는지)까지 사용자가 Studio에서 직접 실측 검증 완료 — 기술적 불확실성은

View file

@ -1,152 +0,0 @@
# 컴포넌트 에러 격리 유틸 — `Fallback`
**상태**: research — 사용자 제안(2026-08-14 세션)으로 신설, 착수 전
백로그. `research/additional-primitives-plan.md`가 이미 "Error Boundary는
빈 자리 아님 — `pcall(MyComp, props)`만으로 React Error Boundary와 같은
격리 효과를 얻는다"고 확정해둔 결론을 뒤집는 게 아니라, **그 결론 위에
얹는 순수 슈가**(그 문서를 다시 열 필요 없음) — `Operator` 콤비네이터
(`research/operator-sugar-plan.md`)가 `:Compute`/`:Apply` 위에 얹힌 것과
같은 관계. 우선순위는 그 형제 백로그 항목들(`quad-mock`/`quad-debug`/
문서 사이트/`Operator`)과 동급 — "quad 개발 상당 부분 끝난 뒤"로 사용자가
명시(`CLAUDE.md` "지금 할 일" 4번). **[2026-08-14 두 번째 세션]** 메커니즘
자체(`xpcall`+`debug.traceback` 배선)는 `research/component-fallback-xpcall-spike.luau`
실측 확인 완료 — 아래 "메커니즘 스케치"/"열린 질문" 절 참고, 백로그
우선순위 자체는 안 바뀜(여전히 착수 안 함).
## 동기 (사용자 원 메모)
컴포넌트마다 개별적으로 `pcall`을 직접 감싸는 건 실용적이지 않음 — 매
컴포넌트 호출 자리마다
`local ok, result = pcall(MyComp, props); if not ok then ... end`
손으로 반복해 쓰는 건 번거롭고 빠뜨리기도 쉬움. 대신
컴포넌트 함수 하나를 받아서 "에러 나면 자동으로 플레이스홀더를 그려주는
버전"으로 바꿔주는 아주 단순한 유틸이 있으면 충분함 — 클린업 동작
(언마운트/리소스 해제)이 목적이 아니라, **실제 에러가 났을 때 디버깅이나
프로덕션 유저 리포트를 편하게 만드는 게 유일한 목적**.
## 제안 API — `Fallback(original, onError) -> wrapped`
```
Fallback(
original: (T...) -> Comp,
onError: (errorMessage: string, trace: string?) -> Comp
) -> (T...) -> Comp
```
`original`은 평범한 컴포넌트 함수(`function(props) return Frame{...} end`
모양) 그대로. `Fallback`은 그걸 감싼 **같은 시그니처의 새 컴포넌트 함수**를
돌려주므로, 호출부 입장에선 원래 컴포넌트를 쓰던 자리에 그대로 대체해
끼워 넣을 수 있음(`MyComp{...}` → `Fallback(MyComp, OnMyCompError){...}`).
```lua
local SafeWidget = Fallback(Widget, function(message, trace)
return ErrorPlaceholder { Message = message }
end)
-- 호출부는 Widget 대신 SafeWidget을 그대로 씀
Frame { SafeWidget{ ... } }
```
### 메커니즘 스케치 — 새 프리미티브 아님, `pcall`/`xpcall` 위의 순수 함수
```lua
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
```
**[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 없이 그냥 커링으로 풀림:
```lua
local function makeErrorHandler(context)
return function(message, trace)
return ErrorPlaceholder { Message = message, Context = context }
end
end
local SafeWidget = Fallback(Widget, makeErrorHandler(someContext))
```
`Fallback` 자신은 이런 경우를 특별히 신경 쓸 필요 없음 — `onError`
이미 평범한 함수이기 때문.
## 왜 기존 "Error Boundary는 빈 자리 아님" 결론과 안 부딪히는가
`research/additional-primitives-plan.md`의 결론은 "새 프리미티브가 필요
없다"는 것이었지 "지금 이대로 편하다"는 게 아니었음 — `Fallback`은 그
문서가 이미 지목한 정확히 같은 메커니즘(`pcall(MyComp, props)`)을 감싸는
얇은 편의 함수일 뿐, 디스패치/Store/Handler 계층에 아무것도 새로 안 만듦.
`Operator` 콤비네이터가 `:Compute`/`:Apply` 위에서 그랬던 것과 동일한
관계 — 그 문서를 다시 열 필요 없음.
## 열린 질문
- **`pcall` vs `xpcall`+`debug.traceback`**: 스택 트레이스까지 항상
캡처할지, 아니면 가벼운 `pcall`(에러 메시지만)을 기본으로 하고 트레이스는
옵션(`onError`가 2번째 인자를 안 받으면 그냥 안 계산)으로 둘지. Roblox
`debug` 라이브러리가 제한적이라는 건 이미 확인돼 있어서(`debug-tooling-plan.md`)
부담은 크지 않음 — 여전히 미정인 건 "항상 캡처 vs 옵션"이라는 정책
판단뿐, 메커니즘 자체는 아래처럼 실측 완료.
- **[2026-08-14 두 번째 세션, 해소]** ~~`xpcall` 에러 핸들러 배선의
실측~~: `luau` 스파이크(`research/component-fallback-xpcall-spike.luau`,
10개 검증 전부 통과)로 확인 — 에러 핸들러 안에서 클로저 업밸류에 쓴
값이 바깥에서 정상적으로 보이고, 3단 중첩 호출까지 `debug.traceback`
실패 지점을 정확히 담음. 부수적으로 `error(msg)` 기본 호출이 위치
접두(`"파일:줄: "`)를 자동으로 붙인다는 캐비엇을 새로 확인(아래
"프로덕션에서의 동작" 항목에 반영).
- **패키지 배치**: `original`을 그냥 호출하고 결과를 그대로 돌려주는
순수 함수라 Store/Dispatch 어디에도 안 걸림 — `quad-base`(엔진 무종속)가
자연스러워 보임, `Operator`와 같은 결. 최종 확인 필요.
- **이름**: `Fallback`이 흔한 단어라 top-level 노출 시 충돌 위험 — 다른
가칭들과 같은 용어 정리 대기열로 볼지, 아니면 이 유틸 하나뿐이라
네임스페이스 없이 top-level 함수로 둬도 괜찮을지(`Tag`/`Attribute`류
네임스페이스가 필요했던 건 그 안에 여러 이름이 몰려서였고, 이건 낱개
함수 하나뿐이라 충돌 표면이 작음).
- **프로덕션에서의 동작**: 에러 메시지/트레이스를 유저에게 보이는 화면에
그대로 노출할지, 아니면 로그로만 보내고 화면엔 일반화된 메시지만
보여줄지는 `onError` 구현(사용자 코드) 몫으로 완전히 열어두는 게 맞아
보임 — `Fallback` 자체는 raw 에러 정보를 그대로 넘기기만 하고 가공은
안 함(가공까지 대신해주면 그게 또 다른 매직). **[2026-08-14 두 번째
세션 추가]** 이 raw 정보엔 `error(msg)`(레벨 지정 없는 기본 호출)의
자동 위치 접두("파일:줄: ")도 포함됨이 실측으로 확인됨 — 화면에 그대로
노출하고 싶지 않은 저자는 `error(msg, 0)`으로 직접 접두를 꺼야 함,
`Fallback`이 대신 벗겨주지는 않음(문서화로 안내할 사항, 위 "가공 안
함" 원칙과 일치).
- 그 외 확정된 결정 없음 — 착수 시점에 위 항목들을 순서대로 확인.
## 우선순위
**형제 백로그 항목들과 동급, 맨 뒤.** `Operator`처럼 순수 슈가라 없어도
기능 격차 없음(`pcall`을 직접 쓰면 되므로, `additional-primitives-plan.md`
이미 확인한 그대로) — 편의성 문제일 뿐.

View file

@ -3,7 +3,7 @@
**상태**: research — 사용자 제안(2026-08-14 세션)으로 신설, 착수 전 **상태**: research — 사용자 제안(2026-08-14 세션)으로 신설, 착수 전
백로그. 이미 확정된 `PreRef`/`Ref`(`base/ref-plan.md`)와 백로그. 이미 확정된 `PreRef`/`Ref`(`base/ref-plan.md`)와
`Effect`(`base/effect-plan.md`) 프리미티브 위에 얹는 순수 슈가 후보 — `Effect`(`base/effect-plan.md`) 프리미티브 위에 얹는 순수 슈가 후보 —
`research/component-fallback-plan.md`의 `Fallback` `base/fallback-plan.md`의 `Fallback`/`Traceback`
`additional-primitives-plan.md`의 기존 결론 위에 얹혔던 것과 같은 관계, `additional-primitives-plan.md`의 기존 결론 위에 얹혔던 것과 같은 관계,
이 문서도 그 프리미티브들의 확정 사항을 하나도 안 뒤집음. 우선순위는 그 이 문서도 그 프리미티브들의 확정 사항을 하나도 안 뒤집음. 우선순위는 그
형제 백로그들(`quad-mock`/`quad-debug`/`Operator`/`Fallback`)과 동급 — 형제 백로그들(`quad-mock`/`quad-debug`/`Operator`/`Fallback`)과 동급 —

View file

@ -0,0 +1,30 @@
# 2026-08-14 세 번째 세션 — `Fallback`/`Traceback` 승격
`research/component-fallback-plan.md``base/fallback-plan.md`로 승격.
해소된 내용:
- **`Fallback`/`Traceback`으로 분리** — `pcall` 기반 `Fallback`(trace
없음)과 `xpcall`+`debug.traceback` 기반 `Traceback`(trace 항상 있음)을
플래그 하나 대신 별도 함수 둘로 확정(`Ref`/`PreRef`와 같은 패턴).
- **정확한 시그니처 확정**:
`Fallback<OkComp, ErrComp, Args...>(base: (Args...) -> OkComp, onError: (err: any) -> ErrComp) -> (Args...) -> (OkComp | ErrComp)`,
`Traceback``onError``(err: any, trace: string)`을 받는 것만 다름.
- **`err: any` 확정** — Lua `error()`가 임의 값(테이블 등)을 던질 수 있음을
사용자 REPL 확인과 스파이크 결과로 재확인, `error(msg)` 기본 호출의
위치 접두("파일:줄: ") 캐비엇도 같이 문서화.
- **패키지는 `quad-base`**(사용자 확정), **이름은 `Fallback`/`Traceback`으로
점유**(사용자 확정, 용어 정리 대기열 안 올림).
- 열려있던 질문 전부 해소 — 남은 건 구현 자체뿐(우선순위는 그대로 맨 뒤).
## 반영
- `base/fallback-plan.md` 신설(승격), `research/component-fallback-plan.md`
삭제.
- 스파이크 스크립트를 `research/`에서 `audit/fallback-xpcall-spike.luau`
이동, 내부 함수명도 `Fallback`→`Traceback`으로 정정(실제 검증 대상과
일치시킴), 재실행으로 통과 재확인.
- `audit/fallback-xpcall-verification.md` 신설(실측 결과 기록).
- `README.md`(base/research/audit 표), `question.md`(해소 항목 제거),
`archive/question-resolved.md`(요약 테이블에 추가), `CLAUDE.md`(4번
백로그 목록), `research/lifecycle-hooks-plan.md`(경로 참조 정정)
전부 동기화.

View file

@ -239,19 +239,20 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션
구조(초심자/api/심화/`quadnomicon` 4축 + 콘텐츠 맵), `Operator` 콤비네이터 구조(초심자/api/심화/`quadnomicon` 4축 + 콘텐츠 맵), `Operator` 콤비네이터
슈가(`Sum`/`Product`/`Not`/비트연산 등 `:Compute`/`:Apply`용 — 메커니즘은 슈가(`Sum`/`Product`/`Not`/비트연산 등 `:Compute`/`:Apply`용 — 메커니즘은
확정, 네임스페이스 이름만 미정, 구현은 순수 슈가라 맨 마지막), 컴포넌트 확정, 네임스페이스 이름만 미정, 구현은 순수 슈가라 맨 마지막), 컴포넌트
에러 격리 유틸 `Fallback`(**[2026-08-14 신설]** 컴포넌트 함수를 감싸 에러 에러 격리 유틸 `Fallback`/`Traceback`(**[2026-08-14 세션, 설계 확정 —
시 자동으로 플레이스홀더를 그려주는 `pcall`/`xpcall` 래퍼 — 기존 `research/`에서 `base/fallback-plan.md`로 승격]** `pcall` 기반
"Error Boundary는 빈 자리 아님" 결론 위의 순수 슈가, `Fallback``xpcall`+`debug.traceback` 기반 `Traceback`으로 분리,
`research/component-fallback-plan.md`), 생명주기 훅 `err: any` 확정, 패키지·이름 전부 확정 — **설계만 끝났을 뿐 구현
우선순위는 그대로 맨 뒤**), 생명주기 훅
`OnCreated`/`OnDestroyed`(**[2026-08-14 신설]** `PreRef`/`Effect`를 `OnCreated`/`OnDestroyed`(**[2026-08-14 신설]** `PreRef`/`Effect`를
반환하는 순수 팩토리 함수 슈가, `research/lifecycle-hooks-plan.md` 반환하는 순수 팩토리 함수 슈가, `research/lifecycle-hooks-plan.md`
`OnRendered`는 base에 없는 post-pass가 필요해 공짜가 아니라 지금은 `OnRendered`는 base에 없는 post-pass가 필요해 공짜가 아니라 지금은
의도적으로 구현 안 함, 거울상 `PostRef` 스케치만 백로그 후보) — 전부 의도적으로 구현 안 함, 거울상 `PostRef` 스케치만 백로그 후보) — 전부
"quad 개발 상당 부분 끝난 뒤"로 사용자가 못박은 후순위. 상세는 "quad 개발 상당 부분 끝난 뒤"로 사용자가 못박은 후순위. 상세는
`.claude/README.md``research/` 표(`debug-tooling-plan.md`/ `.claude/README.md``base/` 표(`fallback-plan.md`)와 `research/`
`documentation-plan.md`/`documentation-content-map.md`/ (`debug-tooling-plan.md`/`documentation-plan.md`/
`framework-comparison-findings.md`/`operator-sugar-plan.md`/ `documentation-content-map.md`/`framework-comparison-findings.md`/
`component-fallback-plan.md`/`lifecycle-hooks-plan.md`). `operator-sugar-plan.md`/`lifecycle-hooks-plan.md`).
5. 자율 작업 루프/스케줄 설정 여부는 사용자 결정 대기 중 5. 자율 작업 루프/스케줄 설정 여부는 사용자 결정 대기 중
(`HUMAN_TODO.md` 2번 항목). (`HUMAN_TODO.md` 2번 항목).
@ -1130,7 +1131,8 @@ claim**으로 확정. 이걸로 마지막 게이트가 열려 **0-A(하강 diff
`research/additional-primitives-plan.md`가 이미 확정한 "Error Boundary는 `research/additional-primitives-plan.md`가 이미 확정한 "Error Boundary는
빈 자리 아님, `pcall(MyComp,props)`로 충분"이라는 결론을 뒤집는 게 아니라 빈 자리 아님, `pcall(MyComp,props)`로 충분"이라는 결론을 뒤집는 게 아니라
그 위에 얹는 순수 슈가(`Operator`가 `:Compute`/`:Apply` 위에 얹힌 것과 그 위에 얹는 순수 슈가(`Operator`가 `:Compute`/`:Apply` 위에 얹힌 것과
같은 관계)로 판단해 새 `research/component-fallback-plan.md` 신설 — 같은 관계)로 판단해 새 research 문서 신설(세 번째 세션에
`base/fallback-plan.md`로 승격, 이하 경로는 신설 당시 기준) —
`xpcall`+`debug.traceback` 메커니즘 스케치, 커링 관용구, 열린 질문(pcall `xpcall`+`debug.traceback` 메커니즘 스케치, 커링 관용구, 열린 질문(pcall
vs xpcall, 패키지 배치, 이름, 프로덕션 동작) 정리, 설계 확정은 아직 없음. vs xpcall, 패키지 배치, 이름, 프로덕션 동작) 정리, 설계 확정은 아직 없음.
부수적으로 워크트리가 계획 문서 없이 빈 채로 시작되는 걸 발견 — 부수적으로 워크트리가 계획 문서 없이 빈 채로 시작되는 걸 발견 —
@ -1145,12 +1147,25 @@ vs xpcall, 패키지 배치, 이름, 프로덕션 동작) 정리, 설계 확정
**2026-08-14 두 번째 세션 — `Fallback` 메커니즘 `xpcall` 실측 확인** **2026-08-14 두 번째 세션 — `Fallback` 메커니즘 `xpcall` 실측 확인**
(`session/2026-08-14-02-fallback-xpcall-spike-verified.md`) (`session/2026-08-14-02-fallback-xpcall-spike-verified.md`)
직전 세션이 열어둔 "`xpcall` 에러 핸들러 배선의 실측 필요"를 새 워크트리에서 직전 세션이 열어둔 "`xpcall` 에러 핸들러 배선의 실측 필요"를 새 워크트리에서
`luau` 스파이크(`research/component-fallback-xpcall-spike.luau`)로 확인 — `luau` 스파이크(현재 `audit/fallback-xpcall-spike.luau`로 이동)로 확인 —
클로저 업밸류 배선, 3단 중첩 `debug.traceback` 캡처 등 10개 검증 전부 클로저 업밸류 배선, 3단 중첩 `debug.traceback` 캡처 등 10개 검증 전부
통과. 부수 발견으로 `error(msg)` 기본 호출(level=1)이 위치 접두 통과. 부수 발견으로 `error(msg)` 기본 호출(level=1)이 위치 접두
("파일:줄: ")를 자동으로 붙인다는 캐비엇을 새로 확인해 문서에 반영 — ("파일:줄: ")를 자동으로 붙인다는 캐비엇을 새로 확인해 문서에 반영 —
`research/component-fallback-plan.md`의 해당 열린 질문을 해소로 표시, 당시 research 문서(현재 `base/fallback-plan.md`)의 해당 열린 질문을
백로그 우선순위 자체는 그대로. 해소로 표시, 백로그 우선순위 자체는 그대로.
**2026-08-14 세 번째 세션 — `Fallback`/`Traceback` 승격**
(`session/2026-08-14-03-fallback-traceback-promoted.md`)
사용자가 `Fallback`/`Traceback`으로 분리(`pcall` 기반 vs `xpcall`+trace
기반), 정확한 제네릭 시그니처(`Traceback`은 `onError``trace: string`
받는 것만 `Fallback`과 다름 — 전체 시그니처는 `base/fallback-plan.md`
참고), `err: any`(사용자 REPL로 테이블 에러 통과 재확인), 패키지
(`quad-base`), 이름(`Fallback`/`Traceback` 그대로 점유)까지 한 번에
확정 — 남은 열린 질문이 없어져 research/ 초안을 `base/fallback-plan.md`
승격(파일 이동), 스파이크는
`audit/fallback-xpcall-spike.luau`로 옮기며 내부 함수명도 `Traceback`으로
정정. `README.md`/`question.md`/`archive/question-resolved.md`/
`research/lifecycle-hooks-plan.md`의 상호 참조 전부 동기화.
**2026-08-14 네 번째 세션 — `ProcessedPreRef` 신설로 Length/Offset 등록 **2026-08-14 네 번째 세션 — `ProcessedPreRef` 신설로 Length/Offset 등록
갭 해소, `PostRef` 완전 대칭화** 갭 해소, `PostRef` 완전 대칭화**