# 스파이크 상태판 — **폴더가 곧 상태** > 마지막 갱신: 2026-08-19 — 신규 `quad-types` 패키지(`CheckedQuad` > 버전 패턴 체크 + `AddPlugin` 체이닝) 검증용 `23` 신규 추가 → > `done/` 직행, 같은 날 후속으로 `type-version-check` 분리에 맞춰 재작성. > 그 과정에서 `type function`을 거친 값은 패스스루라도 이후 > 제네릭 self 체이닝이 깨진다는 새 Luau 함정을 발견(`typing-limits.md` > §6로 승격). 직전 갱신은 같은 날 — `13`을 타입 전용/런타임 두 파일로 분리 > (A의 더미 스텁이 B 실행을 막던 문제 해결) + PostRef까지 확장, 런타임 > 절반은 신규 `22`로 분가 → 둘 다 `done/`. 직전 갱신은 같은 날 — > M0/M1 스캐폴딩을 처음 실제로 짜보는 과정에서 `05`를 현행 모델("emit은 > 항상 전파, 재계산만 캐시로 dedup")로 재작성해 통과 → `rewrite-required/` > → `done/` 이동, `todos.md` 00번이 요구하던 "Store 미선언 키 타입 에러" > 확인도 신규 스파이크 `21`로 완료 → `done/` 직행. 직전 갱신은 > 2026-08-15 — `16`이 `types.newfunction` API 버전 드리프트 > (배열이 아니라 `{head=..., tail=...}` 레코드를 받음) 수정으로 통과, > `rewrite-required/` → `done/` 이동. 근거: > `audit/type-recursive-issue-with-typeof/REPORT.md` 6-1절. > 직전 갱신은 2026-08-14(**"emit은 항상 전파"** 정정으로 `05`가 옛 모델을 > 검증 중이라 `rewrite-required/`로 이동 — > `archive/invalidate-dedup-propagation-reversed.md`). 직전 갱신은 같은 날 **다섯 번째 세션**(`bindLifetime`/`canExecute`/ > `unbindLifetime` 재정정으로 `10`이 옛 모델을 검증하고 있어 > `rewrite-required/`로 이동 — 이제 `not-run/`에는 스파이크가 없고 헬퍼만 > 남음). 직전 갱신은 2026-08-13 열네 번째 세션(하강 diff 재디스패치 확정으로 > `04`/`19` 이동). > 첫 실측은 여섯 번째 세션 — 상세 결과는 `.claude/audit/luau-test-first-run-2026-08-13.md`. > 실행법: `luau <파일>` (런타임) / `luau-analyze <파일>` (타입 전용). **[2026-08-13 열세 번째 세션] `review-required/`가 비었습니다** — 마지막 한 건이던 `08`이 해소돼 `done/`으로 갔습니다. **[2026-08-14 다섯 번째 세션] `not-run/`의 유일한 스파이크였던 `10`도 `rewrite-required/`로 갔습니다** — 지금 남은 건 전부 에이전트가 먼저 재작성해야 할 일(`rewrite-required/`)이고, `not-run/`엔 스파이크가 아닌 헬퍼 하나만 있습니다. | 폴더 | 뜻 | 개수 | 누가 처리 | |---|---|---|---| | `review-required/` | **설계가 걸림 — 사람 결정 필요** | **0** | ⭐ 사용자 | | `rewrite-required/` | 스파이크가 낡음(코드가 깨졌거나, 설계가 바뀌어 옛 모델을 검증 중) | 4 | 에이전트 | | `not-run/` | 이 환경에서 못 돌림(Studio 전용) | 0(+헬퍼 1) | 사용자 or MCP 연결 후 에이전트 | | `done/` | 통과 or 판정 끝, 더 할 일 없음 | 19 | — | **폴더를 옮기는 게 곧 상태 갱신** — 스파이크를 고치거나 돌렸으면 파일을 해당 폴더로 `git mv`하고 아래 표의 줄도 같이 옮길 것. 파일별 "무엇을 왜 검증하는가"는 `README.md`가 담당(이 파일은 상태만). --- ## ⭐ `review-required/` — 사람 결정 필요 (0건, 비어 있음) **[2026-08-13 열세 번째 세션] 마지막 한 건이 해소됐습니다.** `08-type-source-satisfies-state.luau`가 남겨뒀던 잔여 케이스(`State`가 자기 자신을 다른 타입 인자로 재귀 참조하면 막힘)는 **Luau의 현 한계로 확정**되어 quad가 설계로 풀 대상이 아님이 정해졌고(구 `question.md` 0-Y 해소), 스파이크는 `done/`으로 이동했습니다. 당시 검토됐던 "구울 때 인라이닝" 방향은 **채택 안 함**. - 지금 유효한 규약: **`base/typing-limits.md`** - 실측 근거 전문(스파이크 다수 포함 — 개수는 `spikes/` 폴더가 소스): `audit/type-recursion-issue/` `15`도 같은 계약을 다루지만 **스파이크 자체가 파싱 실패**라 `rewrite-required/`에 그대로 둠 — 재작성 대상이지 사람 결정 대상이 아님(계약 자체는 위에서 이미 확정됨). ## 🟠 `rewrite-required/` — 스파이크가 낡음 (4건) **[2026-08-13 열네 번째 세션] 앞의 두 건은 "코드가 깨진" 게 아니라 "설계가 바뀐" 경우** — `question.md` 0-A/0-Z 확정으로 재디스패치가 **하강 diff**가 되면서, 이 둘이 검증하던 전제(선행 `retractFrom` + 4-인자 힌트 + 인덱스 점유 체크)가 더 이상 설계가 아님. 통과 상태로 `done/`에 두면 **옛 모델을 "검증됨"으로 오독하게 되므로** 옮김. 새 정본은 `base/dispatch-core-plan.md`/`base/attribute-plan.md`. **[2026-08-14 다섯 번째 세션] `10`도 같은 이유로 합류** — `bindLifetime`/ `canExecute`/`unbindLifetime` 재정정으로 A 섹션이 폐기된 모델(`canBound`, `bindLifetime`의 `.Subscribed` 세팅, 2-인자 `canExecute`)을 검증 중. `10`은 **Studio 전용이라 재작성해도 이 환경에서는 못 돌린다** — 재작성 후 다시 `not-run/`으로 내려가 사용자/MCP를 기다리는 자리다. | 파일 | 상태 | 무엇을 고쳐야 하나 | |---|---|---| | `04-dispatch-chain-retractFrom.luau` | 옛 모델 기준으로는 ✅ 통과였음 | (1) `chains` 슬롯이 `{handler, retractor}`가 되고 `Dispatch.process`가 핸들러를 먼저 비교하는 **하강 diff**로 재작성, (2) `retractFrom`은 **3-인자**(힌트 인자 없음), (3) "힌트가 target 인덱스에만 간다"를 검증하던 부분은 **정반대**로 뒤집힘 — 이제 각 레벨이 자기 값을 받는지를 검증해야 함. **살릴 것**: `chains:SetStrong` 순서 음성 대조군(그 버그는 새 모델에서도 그대로 유효) | | `19-ownership-refcount-relate-patterns.luau` | A/C ✅ 유효, **B 섹션이 낡음** | B가 검증하던 "공개 `AttributeKey(name)` + 인덱스 1 점유 체크"가 폐기됨 — **그룹 전용 키 + `AttributeKeyHandler`의 이름 claim**으로 재작성하고, 음성 대조군도 "두 그룹이 같은 이름 → 즉시 error", "그룹↔직접 쓰기 → 즉시 error"로 바꿀 것(0-Z 확정 내용). A/C는 손댈 것 없음 | | `15-type-compute-trailing-deps-typepack.luau` | **파싱 실패**(SyntaxError) | 음성 대조군의 타입 표기가 `TypeError`가 아니라 `SyntaxError`로 걸려 **파일 전체가 아무것도 검증 못 함** — 대조군을 별도 파일/블록으로 격리 | | `10-roblox-studio-checks.server.luau` (Studio 전용) | 미실행 + **A 섹션이 옛 모델** | A가 옛 2-인자 `canExecute(inst,value)`와 `bindLifetime`의 `.Subscribed` 세팅을 검증 중 — **`bindLifetime`이 gcconn을 `value` 쪽 릴레이션에 복사하는 모델**로 재작성할 것(`base/lifecycle-pattern.md`). **[2026-08-14 열한 번째 세션 재정정, 2026-08-18 방향 정정]** 이중 바인딩 게이트는 `canBound(value)`(`if not canBound(v) then error(...) end` — `canBound` 참 = "지금 묶어도 됨") — `canExecute`는 State emit 전파 게이팅 전용으로 분리됨, 둘 다 비공개 헬퍼 `isBoundAlive`를 공유하는 1-인자 진입점이지만 **서로의 부정**(`base/lifecycle-pattern.md`의 "`canBound` vs `canExecute`" 절). **살릴 것**: "ClassName 신호 미발화 / Destroy 시 `Connected` 즉시 전환" 검증(새 모델에서 더 중요해짐), gcconn/gchold를 **Instance 생성 시점**에 만드는 것으로 바꿀 것(옛 lazy 생성 폐기). B/C 섹션은 손댈 것 없음 | ## ⚪ `not-run/` — 이 환경에서 못 돌림 **[2026-08-14 다섯 번째 세션] 스파이크는 0건** — 유일했던 `10`이 `rewrite-required/`로 갔음(위 표). 남은 건 헬퍼 하나뿐. | 파일 | 이유 | |---|---| | `gc-trigger-helper.server.luau` | 스파이크가 아니라 **헬퍼** — Studio에 `collectgarbage()`가 없어서 GC를 강제 트리거하는 기법. `10`을 돌릴 때 같이 씀 | ## ✅ `done/` — 통과 or 판정 끝 (19건) **런타임 14개 전원 통과**(crash 0 / FAIL 0) — **[열네 번째 세션] `04`/`19`는 검증 대상 설계가 바뀌어 `rewrite-required/`로 이동했고, [2026-08-19] `05`는 현행 모델로 재작성해 다시 여기로 돌아왔고, 신규 `22`(구 `13` 런타임 절반, PostRef까지 확장)가 합류**: | 파일 | 확인된 것 | |---|---| | `01-two-pass-array-hash-order` | 배열 파트 전체 → 해시 파트 순. `Dispatch.drive` 두 패스 계약과 `PreRef` 호이스팅의 전제 | | `02-none-sentinel-vs-nil-holes` | `nil` 소진 시 `#t` 50→49로 무너짐 / `None`은 항상 50. 반대로 Ref 콜백 배열은 `None` 쓰면 죽은 슬롯 1000개 잔존 — **두 배열의 규칙이 서로 반대여야 함**이 정량 확인 | | `03-recursive-store-bind-dispatch` | StoreBind 재귀 재-dispatch, `None`→`nil` 흐름, 무한재귀 없이 종료 | | `05-store-state-diamond-propagation` | **[2026-08-19 재작성]** emit은 자기 invalid 상태와 무관하게 항상 전파(다이아몬드 두 경로 모두 끝까지 도달), 재계산은 `:Get()` 시점 캐시로 1회만, `:Get()`을 안 부르는 Observer는 source 변경마다 경로 수만큼(2) 계속 발화 — 옛(역전된) 모델이면 2번째 변경부터 침묵해야 하는데 안 그럼을 확인 | | `06-component-boundary-nil-hole-props` | `or None` 없으면 앞쪽 nil-hole로 슬롯 소실, 관용구 쓰면 항상 보존 | | `07-relate-weak-table-gc` | **연쇄 GC 확정**(아래 별도 절) — GC-native 아키텍처의 핵심 전제 | | `11-modifier-illegal-value-error` | Modifier 필드/Source에 핸들러 계층 값 넣으면 즉시 error — 16개 케이스 전원 | | `17-modifier-index-tableclone-chaining` | 제네릭 `__index` + `table.clone` 체이닝, 메타테이블 참조 공유, 형제 분기 무오염 | | `18-relate-mutual-cycle-gc` | **두 `Relate` 상호 순환은 실제로 GC 안 됨**(아래 별도 절) | | `20-slot-splice-index-arithmetic` | `Splice` 산술 11개 경계 케이스 전부 참조 구현과 일치 | | `22-runtime-ref-preref-postref-brand` | **[2026-08-19 신규]** `isPreRef`/`isPostRef`가 서로 배타적 형제(둘 다 `isRef`엔 `true`, 서로에겐 `false`)임을 확인, Leaf 핸들러 흉내(`isRef(v) and not isPreRef(v) and not isPostRef(v)`)가 Ref/PreRef/PostRef 셋을 정확히 갈라냄 | **타입 스파이크 중 판정이 끝나 더 할 일 없는 것**: | 파일 | 판정 | |---|---| | `08-type-source-satisfies-state` | ✅ 핵심 질문(Source⊇State 구조적 서브타이핑) 통과. 잔여 케이스(자기 이름을 다른 인자로 재귀 참조)는 **[2026-08-13 13차 세션] Luau 현 한계로 확정** — quad가 풀 대상 아님, `base/typing-limits.md` 1번 | | `09-type-modifier-overridden-subtype` | ✅ 통과 — 문서가 우려한 `FrameModifier`↔`GuiObjectModifier` 서브타입 깨짐이 그대로 재현, fallback(`any`)은 정상 | | `12-type-attribute-generic-key-narrowing` | ❌지만 **설계 영향 없음** — 제네릭 키 narrowing이 안 되는 건 `attribute-plan.md`가 이미 fallback으로 예비해둔 결과(타입 패밀리가 유일하게 믿을 경로) | | `13-type-ref-preref-subtype` | **[2026-08-19 재작성]** ✅ 통과 — `PreRef`/`PostRef` 둘 다 `Ref`를 구조적으로 만족(음성 대조군도 정확히 에러). 런타임 B섹션은 `22`로 분리(A의 더미 스텁이 B 실행을 막던 문제 해결) | | `14-type-nilable-default-overload` | ⚠️ 부분 — 의도한 오용은 막지만 정상 nilable 사용례까지 막아 현 스케치로는 채택 불가. **설계 결정은 아직 필요 없음**(대안이 이미 UB 경고로 존재)이라 `review-required`가 아님 | | `16-type-store-key-typefunction` | **[2026-08-15]** ✅ 통과 — 원인은 설계 문제가 아니라 `types.newfunction` API 버전 드리프트(배열이 아니라 `{head=...}` 레코드). `ProcessStoreType`이 정확히 `{ty: Source, count: Source}` 구조를 만족, 음성 대조군 4건(틀린 Get/Set 타입 2건, 존재하지 않는 메소드) 전부 정확히 에러. 근거: `audit/type-recursive-issue-with-typeof/REPORT.md` 6-1절 | | `21-type-store-undeclared-key-rejected` | **[2026-08-19 신규]** ✅ 통과 — `16`의 `ProcessStoreType`을 재사용해 미선언 키 접근 2건(읽기, 메소드 체이닝)이 정확히 `TypeError`로 거부됨을 확인, 양성 경로(선언된 키 3개) 클린. `store-plan.md`가 "아마 그럴 것"으로만 적어뒀던 걸 M0에서 실측 확정 | | `23-type-quadtypes-checkversion-addplugin` | **[2026-08-19 신규, 같은 날 후속으로 재작성]** ✅ 통과 — 실제 `quad-types`/`quad-base`/`type-version-check`로 `CheckedQuad`+`AddPlugin` 통합 검증. 재작성 과정에서 `type function`을 거친 값은 패스스루라도 이후 제네릭 self 체이닝이 조용히 깨진다는 새 Luau 함정 발견(`typing-limits.md` §6으로 승격), `export type function`/이중 꺾쇠 제네릭 인스턴스화 요구도 같이 실측 — 최종 설계(별도 가상 필드로 격리)는 양성/음성 경로 모두 클린 | ### 특별히 중요한 통과 3건 **`04` — 직전 감사가 찾은 버그가 음성 대조군으로 재현됨**(파일은 지금 `rewrite-required/`에 있음 — 아래 관측 자체는 새 모델에서도 유효) | 관측 지점 | 정상(수정본) | 대조군(버그) | |---|---|---| | 최초 마운트 후 체인 깊이 | **3** | **1** | | 재발행 후 옛 store 구독 | 0 (끊김) | 1 (안 끊김) | | 죽은 store를 건드리면 | 값 유지 | **`STALE`로 덮어써짐** | `chains:SetStrong`을 `handler.process` 뒤에 두면 하위 retractor가 통째로 유실되고, 결국 **버려진 store가 나중에 UI를 덮어쓰는** 데까지 감. **`07` — GC-native 아키텍처의 핵심 전제** ``` inst 5개만 살린 상태 → 살아남은 payload 5 / 엔트리 5 (기대치 일치) 모든 참조를 놓은 뒤 → 살아남은 payload 0 / 엔트리 0 (기대치 일치) ``` `bindLifetime`으로 매달아둔 자원이 `inst`와 함께 연쇄 소멸함이 확인됨. (이 스파이크는 원래 sanity check만 하고 있어서 이번에 보강한 것 — 파일이 스스로 적어둔 "weak table 엔트리를 셀 방법 없음"이라는 전제가 틀렸음.) **`18` — `relate-plan.md`의 상호 순환 경고** ``` 상호 강참조 순환: inst=true, value=true (GC 못 풂) 한쪽을 weak-value로 낮춤: inst=false, value=false (풀림) ``` 추측이 아니라 **실제로 GC가 안 됨** — `Slot`의 두-`Relate` 수정이 필수 조치였음이 입증.