From 8aa13ed435373e6db5668f28dac47f43fa0ab44c Mon Sep 17 00:00:00 2001 From: qwreey Date: Fri, 28 Aug 2026 20:56:00 +0900 Subject: [PATCH] =?UTF-8?q?fix(m2):=20=EB=8B=A8=EC=9C=84=202=20=EA=B0=90?= =?UTF-8?q?=EC=82=AC=203=EB=9D=BC=EC=9A=B4=EB=93=9C=20=EB=B0=98=EC=98=81?= =?UTF-8?q?=20=E2=80=94=20Source/Store=20Init=EC=9D=B4=20=EC=9E=90?= =?UTF-8?q?=EA=B8=B0=20=EC=9D=98=EC=A1=B4=EC=84=B1=EC=9D=84=20module:RunIn?= =?UTF-8?q?it=EB=A1=9C=20=EC=A7=81=EC=A0=91=20=EB=8B=B9=EA=B2=A8=EC=98=B4(?= =?UTF-8?q?H-177,=20New()=20=EC=88=9C=EC=84=9C=20=EB=AC=B4=EA=B4=80),=20in?= =?UTF-8?q?it.luau=20=EC=A3=BC=EC=84=9D=20=EB=AA=A8=EC=88=9C=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0,=20luau-test=20README=20=EC=8A=A4=ED=8C=8C=EC=9D=B4?= =?UTF-8?q?=ED=81=AC=2015=20=EC=83=81=ED=83=9C=20=EC=A3=BC=EC=84=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: qwreey --- .claude/luau-test/README.md | 2 +- .../pre-implementation-handtrace-round11.md | 17 +++++++++++++++-- quad-base/src/Source.luau | 7 ++++++- quad-base/src/Store.luau | 5 ++++- quad-base/src/init.luau | 4 ++-- 5 files changed, 28 insertions(+), 7 deletions(-) diff --git a/.claude/luau-test/README.md b/.claude/luau-test/README.md index 94fa0e1..20d67c6 100644 --- a/.claude/luau-test/README.md +++ b/.claude/luau-test/README.md @@ -87,7 +87,7 @@ ROADMAP 항목 근거인지, 어떻게 실행하는지, 실행 후 뭘 확인해 | `12-type-attribute-generic-key-narrowing.luau` (타입체크 전용) | `[AttributeKey<> "name"] = value`(구 `Attribute<>`)처럼 제네릭 특수 키를 쓸 때 `value`의 타입이 실제로 `T`로 좁혀지는지 — base 문서 자신이 "미검증"이라 명시한 항목 | `attribute-plan.md` "[실측 필요, M0/M10]" (2026-08-09 열한 번째 세션 신설) | | `13-type-ref-preref-subtype.luau` (타입체크 전용) | **[2026-08-19 재작성]** `PreRef`/`PostRef`가 `Ref`를 구조적으로 만족하는지 — 원래 이 파일에 있던 런타임(B) 부분은 A의 더미 스텁이 실행을 막아 도달 불가였던 문제라 `22`로 분리, PostRef까지 확장 | `brand-plan.md`의 `Brand` 절(2026-08-09 열한 번째 세션 재정정), `ref-plan.md`의 "`PostRef`" 절 | | `14-type-nilable-default-overload.luau` (타입체크 전용) | `Source(default)`/`Ref(default)`의 `default` 생략이 `T`가 nilable일 때만 안전하다는 캐비엇을, 함수 오버로드(교차 타입)로 실제로 타입 레벨에서 막을 수 있는지 | `source-state-plan.md` "State는 쓰기 대상이 아님" 절의 `default` 생략 캐비엇 | -| `15-type-compute-trailing-deps-typepack.luau` (타입체크 전용) | `:Compute(fn, ...)`의 trailing deps를 `fn`에 위치 인자(lazy State 핸들)로도 노출하는 확장, 최종 시그니처 `fn(self, previous?, ...deps)` — 이형(heterogeneous) 다중 deps를 제네릭 타입 팩(`U...`)으로 표현 가능한지, `previous?`가 팩 앞(정정된 순서)에서만 통과하고 팩 뒤(옛 순서)에서는 막히는지 | `source-state-plan.md` "trailing deps를 fn에 lazy positional 인자로도 노출" 절(2026-08-11 후속 세션, 순서는 같은 날 세 번째 세션에 정정) | +| `15-type-compute-trailing-deps-typepack.luau` (타입체크 전용) | **[2026-08-28 폐기 — `done/`로 이동, M2 단위 2가 실제 선언에서 타입팩 형태를 기각(`round11.md` `H-176`)]** `:Compute(fn, ...)`의 trailing deps를 `fn`에 위치 인자(lazy State 핸들)로도 노출하는 확장, 최종 시그니처 `fn(self, previous?, ...deps)` — 이형(heterogeneous) 다중 deps를 제네릭 타입 팩(`U...`)으로 표현 가능한지, `previous?`가 팩 앞(정정된 순서)에서만 통과하고 팩 뒤(옛 순서)에서는 막히는지 | `source-state-plan.md` "trailing deps를 fn에 lazy positional 인자로도 노출" 절(2026-08-11 후속 세션, 순서는 같은 날 세 번째 세션에 정정) | | `16-type-store-key-typefunction.luau` (타입체크 전용) | `Store`가 `T`의 각 필드를 `Source`로 감싼 타입을 Luau `type function`(`types.newtable`/`:setproperty`/`ty:properties()`)으로 실제 합성 가능한지, 결과가 구조적으로 `Source` 필드를 만족하는지. **[2026-08-15] 통과 → `done/`** — 원인은 설계가 아니라 `types.newfunction` API 버전 드리프트였음, `audit/type-recursive-issue-with-typeof/REPORT.md` 6-1절 | `typing-limits.md` "`store.key` 레코드 필드 타이핑" 절(2026-08-12 열일곱 번째 세션), `pre-implementation-audit.md` 1-10 **⛔ [2026-08-25 이동, `rewrite-required/`] 검증 대상이 폐기됐다** — `WrapStore`/`ProcessStoreType`로 결과 타입을 **합성**하는 접근 자체가 Store 재설계로 사라졌다(`base/store-plan.md`, 역전 원문은 `archive/store-value-field-redesign-withdrawn.md`). 재작성 지침은 `STATUS.md`가 소스 — **타입 함수 없는 평범한 레코드** 모양을 검증하고 음성 대조군에 **예약 키 충돌**(`CheckReservedKeys>` — **[2026-08-26 `/code-review high`]** 옛 이름·배선 `CheckReserved`는 `T`를 통째로 받아 실사용 `T`에서 아예 안 돈다, `H-112`)과 없는 키 접근, 그리고 **`Store<<{}>>()`(빈 `keyof`)** 를 넣을 것 — 상세 지침은 `STATUS.md`가 소스 | | `17-modifier-index-tableclone-chaining.luau` | Modifier의 제네릭 `__index`+`table.clone` 체이닝 — 임의 필드 이름에 대해 즉석 setter가 만들어지는지, `table.clone`이 메타테이블을 참조로 공유해 여러 단계 clone에서도 체이닝이 안 끊기는지, 원본이 mutate 안 되는지, 형제 분기끼리 오염 안 되는지 | `modifier-plan.md` "런타임은 클래스별 코드 없이 base에 딱 하나만 있으면 됨" 절 + "`table.clone`의 정확한 동작 — 확인됨" 절(2026-08-12 열일곱 번째 세션), `pre-implementation-audit.md` 1-11 | | `18-relate-mutual-cycle-gc.luau` | **[2026-08-13 신규]** 서로 다른 두 `Relate`가 서로의 키를 상대방의 강한 값으로 제공하는 상호 순환은 Luau에 ephemeron이 없어 GC가 못 푼다는 주장(지금까지 공식 문서 인용으로만 뒷받침됨) — 음성 대조군(순환 재현)과 양성 대조군(한쪽을 weak-value로 낮추면 풀리는지) 둘 다 실측 | `relate-plan.md` "위험한 패턴" 절(2026-08-12 열세/열네 번째 세션), `slot-plan.md`의 `kSlotMap`/`slotOwner`/`elementOwner` 실사례 | diff --git a/.claude/qa-request/pre-implementation-handtrace-round11.md b/.claude/qa-request/pre-implementation-handtrace-round11.md index fec6d9e..7f29fda 100644 --- a/.claude/qa-request/pre-implementation-handtrace-round11.md +++ b/.claude/qa-request/pre-implementation-handtrace-round11.md @@ -25,6 +25,7 @@ | `H-173` | ① | 1 | 🟢 | `ROADMAP.md` M7 체크박스·`tween-plan.md` 352가 `isTween`/`TweenBrand`를 `Tween.luau`에 둔다고 아직 서술(감사 3라운드가 두 곳만 고침) | ✅ 반영 | | `H-174` | **②** | 1→2 | 🔴 | 생명주기 4종은 **`New()` 인스턴스마다 다른 필드**(이 단위가 그렇게 만들었고 `spec.lifetime` 8이 고정)인데, 단위 2·3의 `base/` 의사코드(`Observer:_receive`의 `canExecute(self)`, `Subscribe` 넷의 `canBound(self)`, `EffectHandle.rawRerun`)는 **자유 함수**로 부른다 — `Observer.luau`/`Source.luau`가 자기 인스턴스의 필드에 어떻게 닿는지 어느 문서도 안 정했다. 결정 없이는 단위 2의 `_receive`를 쓸 수 없다 | ✅ (a) 사용자 확정 — 팩토리형, `module.canExecute(self)`를 발화 시점에 늦게 읽음(`lifecycle-pattern.md`·`module-lifecycle-plan.md`·`ROADMAP` 반응형 본체) | | `H-176` | ① | 2 | 🟡 | `:Compute`의 trailing deps를 타입팩 `D...`로 좁히는 선언은 strict에서 콜백 dep 추론이 깨져 정상 호출까지 막힌다(스파이크 15가 "미검증"으로 남긴 자리) | ✅ `...any` + 콜백 주석으로 확정, `source-state-plan.md` 실측 기록 | +| `H-177` | ① | 2 | 🟢 | `InitSource`/`InitStore`가 `State.implFor`만 불러 `New()`의 `RunInit` 순서에 의존했다 — `module-lifecycle-plan.md`는 "각 `InitXxx`가 `require`처럼 멱등하게 자기 의존성을 당겨온다"로 확정 | ✅ 각 Init이 `module:RunInit(dep)`를 직접 호출(감사 3라운드) | | `H-175` | ① | 1 | 🟢 | §5 "불변 업밸류만 잡는 클로저는 프로토에 캐시"는 범위가 넓다 — 실제 규칙은 **업밸류가 없거나 전부 톱레벨(함수 깊이 0) 불변 로컬**일 때만(컴파일러 `shouldShareClosure`). 함수 인자·지역을 잡는 클로저(단위 3 `Effect`의 콜백 모양)는 정상 GC됨을 실측 | ✅ §5·`spec.ref` 주석 좁힘 | ## 상세 @@ -154,6 +155,17 @@ - **처리**: deps 자리 `...any`, 콜백 안에서 `dep: StateData` 주석(콜백 파라미터 주석 관례 그대로). 런타임 계약 무변경. 문서에 실측 기록, 스파이크 `15` 닫힘. +### `H-177` 🟢 — 반응형 Init의 순서 의존 (①) + +- **어디서**: `quad-base/src/{Source,Store}.luau`의 `Init` / `init.luau`의 `RunInit` 순서 주석 vs + `base/module-lifecycle-plan.md` "New()의 내부 구성" 절의 *"순서 의존성은 각 `InitXxx`를 + `require`처럼 멱등하게 만들어서 해소한다"*. +- **무엇이**: 단위 2 첫 커밋은 `init.luau`가 State → Source → Store 순서를 지키게 하고 + `implFor`가 어기면 error하게 했다 — 동작은 하지만 문서 원칙과 다른 길. 감사 3라운드가 + "사용자 판단"으로 올렸으나 문서가 이미 답을 갖고 있어 ①. +- **처리**: `Source.Init`이 `module:RunInit(State.Init)`, `Store.Init`이 `module:RunInit(InitSource)`를 + 직접 호출. `init.luau` 순서 주석을 "무관"으로. + ### `H-175` 🟢 — 클로저 캐시 규칙의 범위 (①) - **어디서**: 이 파일 §5 "툴링 사실 둘"의 둘째 항, `spec.ref.luau` 7번 주석. @@ -218,8 +230,9 @@ lazy 하게 읽으면 되는거 아냐? Set 재진입 같은 경우는, 반복 - `Store`: `H-122`/`H-153`/`H-83` 전부 `spec.store`. `Names()`는 `pairs(self)` — 메소드가 `__index`라 안 센다는 `H-153` 전제가 실측으로 성립. - **조립 세부**(`H-174`의 구현, 새 표면 아님): `State.luau`는 `Init(module)`이 인스턴스별 임플을 - 만들고 `implFor(module)`로 `Source`/`Store` Init에 건넨다(`RunInit` 순서 의존 — 어기면 - `implFor`가 level 1 error). `Source`의 `__index` 체인이 그 임플을 가리켜 `With`/`Compute`/ + 만들고 `implFor(module)`로 `Source`/`Store` Init에 건넨다(각 Init이 `module:RunInit(dep)`로 + 의존성을 직접 당겨오므로 `New()` 순서 무관 — `H-177`; `implFor`의 level 1 error는 그래도 남겨둔 + 불변식 검사). `Source`의 `__index` 체인이 그 임플을 가리켜 `With`/`Compute`/ `Apply`가 위임된다. `Ref`/`Void`/`Relate`/`Brand`만 인스턴스 간 공유 잎. - **입력 검증 하나 추가**(`architecture.md` error 계약의 예 *"dep #3 is not a State/Source/Ref"* 그대로): `newNode`가 dep이 `isState`가 아니면 `error(…, 3)` — 없으면 `dep._subs` nil 인덱스로 diff --git a/quad-base/src/Source.luau b/quad-base/src/Source.luau index b683d99..41a55f3 100644 --- a/quad-base/src/Source.luau +++ b/quad-base/src/Source.luau @@ -19,7 +19,8 @@ payload ("출처" only — no value, no revision). Assembly (`H-174`): `InitSource(module)` — needs the per-instance State - impl for its `__index` chain, so `InitState` must have run first. + impl for its `__index` chain, and pulls it in itself via + `module:RunInit(State.Init)` (idempotent), so `New()` order is irrelevant. ]] local Brand = require("./Brand") @@ -35,6 +36,10 @@ local isModifier = Brand.isModifier local WEAK_KEY_MT = { __mode = "k" } local function Init(module: any) + -- Pull our dependency ourselves, `require`-style (`module-lifecycle-plan.md` + -- "순서 의존성은 각 `InitXxx`를 `require`처럼 멱등하게 만들어서 해소한다") — + -- `RunInit` is idempotent, so `New()` need not order us after `State.Init`. + module:RunInit(State.Init) local StateImpl = State.implFor(module) local emitDown = StateImpl._emitDown diff --git a/quad-base/src/Store.luau b/quad-base/src/Store.luau index 657783b..dd44217 100644 --- a/quad-base/src/Store.luau +++ b/quad-base/src/Store.luau @@ -25,10 +25,12 @@ one lazy spot left, and it is explicit about giving up type safety. Assembly (`H-174`): `InitStore(module)` — `Of` creates values through - `module.Source`, read at call time. + `module.Source`, read at call time; the init pulls `InitSource` in itself + (`RunInit` is idempotent), so `New()` order is irrelevant. ]] local Brand = require("./Brand") +local InitSource = require("./Source") local QuadTypes = require("../roblox_packages/quad_types") export type Store = QuadTypes.Store @@ -42,6 +44,7 @@ local isSource = Brand.isSource local RESERVED = { Of = true, Names = true, __reservedCheck = true } local function Init(module: any) + module:RunInit(InitSource) -- `Of` needs `module.Source`; pull it in ourselves (idempotent) local Impl = {} Impl.__index = Impl diff --git a/quad-base/src/init.luau b/quad-base/src/init.luau index eab3db7..4919e44 100644 --- a/quad-base/src/init.luau +++ b/quad-base/src/init.luau @@ -70,8 +70,8 @@ local function New(): Quad module:RunInit(InitDebug) module:RunInit(InitLifetimeHandle) -- 생명주기 4종 에러 스텁 — 백엔드가 덮어씀 - -- 반응형 코어(M2 단위 2) — `H-174`: 인스턴스별 팩토리. State 임플이 먼저 있어야 - -- Source가 `__index` 체인을 걸 수 있다(순서 의존, `State.implFor`가 검사). + -- 반응형 코어(M2 단위 2) — `H-174`: 인스턴스별 팩토리. 각 Init이 자기 의존성을 + -- `module:RunInit(...)`로 직접 당겨오므로(멱등) 여기 순서는 무관하다. module:RunInit(State.Init) module:RunInit(InitSource) module:RunInit(InitStore)